commit 6f5e27fad4ea92eb8a793103147f847c85a36809
Author: Rikuoh <mail@riq0h.jp>
Date:   Sun Feb 16 21:39:06 2025 +0900

    1.0

diff --git a/README.md b/README.md
new file mode 100644
index 0000000..c2d95fb
--- /dev/null
+++ b/README.md
@@ -0,0 +1 @@
+GNU Coreutils commands reimplemented in Ruby.
diff --git a/cal.rb b/cal.rb
new file mode 100755
index 0000000..3fc01ff
--- /dev/null
+++ b/cal.rb
@@ -0,0 +1,28 @@
+#!/usr/bin/ruby
+# frozen_string_literal: true
+
+require 'date'
+require 'optparse'
+
+year = Date.today.year
+month = Date.today.month
+week = %w[日 月 火 水 木 金 土]
+
+opt = OptionParser.new
+opt.on('-m month') { |v| month = v.to_i }
+opt.on('-y year') { |v| year = v.to_i }
+opt.parse(ARGV)
+
+first_day = Date.new(year, month, 1)
+last_day = Date.new(year, month, -1)
+puts "#{month.to_s.rjust(7)}月\s#{year}"
+puts week.join(' ')
+print "\s\s\s" * first_day.wday
+
+(first_day..last_day).each do |date|
+  day_string = date.day.to_s.rjust(2, "\s")
+  day_string = "\e[7m#{day_string}\e[0m" if date == Date.today
+  print "#{day_string}\s"
+  puts if date.saturday?
+end
+puts
diff --git a/ls.rb b/ls.rb
new file mode 100644
index 0000000..0c7d257
--- /dev/null
+++ b/ls.rb
@@ -0,0 +1,106 @@
+# frozen_string_literal: true
+
+require 'optparse'
+require 'etc'
+
+COLUMNS = 3
+MARGIN = 3
+
+TYPES = {
+  'fifo' => 'p',
+  'characterSpecial' => 'c',
+  'directory' => 'd',
+  'blockSpecial' => 'b',
+  'file' => '-',
+  'link' => 'l',
+  'socket' => 's'
+}.freeze
+
+PERMISSIONS = {
+  '0' => '---',
+  '1' => '--x',
+  '2' => '-w-',
+  '3' => '-wx',
+  '4' => 'r--',
+  '5' => 'r-x',
+  '6' => 'rw-',
+  '7' => 'rwx'
+}.freeze
+
+def run
+  params = ARGV.getopts('a', 'r', 'l')
+  filenames = params['a'] ? Dir.glob('*', File::FNM_DOTMATCH) : Dir.glob('*')
+  filenames = filenames.reverse if params['r']
+  params['l'] ? output_file_datails(filenames) : output(filenames)
+end
+
+def output(filenames)
+  rows = filenames.size.ceildiv(COLUMNS)
+  pads = pad_created(filenames, rows)
+  (0...rows).each do |row|
+    COLUMNS.times do |column|
+      index = row + column * rows
+      print filenames[index].ljust(pads[column] + COLUMNS) if filenames[index]
+    end
+    puts
+  end
+end
+
+def pad_created(filenames, rows)
+  filenames.each_slice(rows).map { _1.map(&:length).max }
+end
+
+def output_file_datails(filenames)
+  puts total_file_blocks(filenames)
+  filenames.each do |file|
+    file_stat = File::Stat.new(file)
+    print TYPES[file_stat.ftype]
+    print file_mode(file_stat)
+    print owner_info(file_stat, filenames)
+    print time_stamp(file_stat)
+    print symbolic(file)
+    puts
+  end
+end
+
+def owner_info(file_stat, filenames)
+  [
+    file_stat.nlink.to_s.prepend(' '),
+    Etc.getpwuid(file_stat.uid).name,
+    Etc.getgrgid(file_stat.gid).name,
+    file_stat.size.to_s.rjust(max_filename_length(filenames))
+  ].join(' ')
+end
+
+def symbolic(filenames)
+  if File.lstat(filenames).symlink?
+    " #{filenames} -> #{File.readlink(filenames)}"
+  else
+    " #{filenames}"
+  end
+end
+
+def file_mode(file_stat)
+  file_count = file_stat.mode.to_s(8).slice(-3, 3)
+  file_permission = file_count.split('').map do |file|
+    PERMISSIONS[file]
+  end
+  file_permission.join('')
+end
+
+def max_filename_length(filenames)
+  filenames.map { |file| File.size(file) }.max.to_s.length + MARGIN
+end
+
+def time_stamp(file_stat)
+  file_stat.mtime.strftime('%_m月 %_d %H:%M')
+end
+
+def total_file_blocks(filenames)
+  blocks = filenames.map do |file|
+    File::Stat.new(file).blocks
+  end
+  "total #{blocks.sum}"
+end
+
+run
diff --git a/wc.rb b/wc.rb
new file mode 100644
index 0000000..509a821
--- /dev/null
+++ b/wc.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+require 'optparse'
+
+def main
+  options, filenames = parse_options
+  file_stats = collect_file_stats(filenames)
+  total_stat = calculate_total_stat(file_stats)
+  max_widths = calculate_max_widths(total_stat)
+
+  file_stats << total_stat if filenames.size > 1
+  file_stats.each { |file_stat| puts format_row(file_stat, max_widths, options) }
+end
+
+def parse_options
+  options = {}
+  OptionParser.new do |opts|
+    opts.on('-l') { options[:lines] = true }
+    opts.on('-w') { options[:words] = true }
+    opts.on('-c') { options[:bytes] = true }
+  end.parse!
+  options = { bytes: true, lines: true, words: true } if options.empty?
+  filenames = ARGV.empty? ? [''] : ARGV
+  [options, filenames]
+end
+
+def collect_file_stats(filenames)
+  filenames.map do |filename|
+    text = filename.empty? ? ARGF.read : File.read(filename)
+    {
+      filename: filename,
+      lines: text.lines.count,
+      words: text.split.size,
+      bytes: text.bytesize
+    }
+  end
+end
+
+def calculate_total_stat(file_stats)
+  total_stat = { filename: '合計', lines: 0, words: 0, bytes: 0 }
+  file_stats.each do |stats|
+    total_stat[:lines] += stats[:lines]
+    total_stat[:words] += stats[:words]
+    total_stat[:bytes] += stats[:bytes]
+  end
+  total_stat
+end
+
+def calculate_max_widths(total_stat)
+  %i[lines words bytes].to_h { |key| [key, total_stat[key].to_s.length] }
+end
+
+def format_row(stats, max_widths, options)
+  columns = %i[lines words bytes].filter_map do |key|
+    stats[key].to_s.rjust(max_widths[key]) if options[key]
+  end
+  [*columns, stats[:filename]].join(' ')
+end
+
+main