From f329d433cfd3a2cafa683156cb8753e19310b72c Mon Sep 17 00:00:00 2001 From: Keisuke Kurosawa Date: Sun, 30 Mar 2025 15:03:36 +0900 Subject: [PATCH 01/20] Implement 'wc' in Ruby * 05.wc/lib/wc_methods.rb: New file. * 05.wc/wc.rb: New file. --- 05.wc/lib/wc_methods.rb | 167 ++++++++++++++++++++++++++++++++++++++++ 05.wc/wc.rb | 8 ++ 2 files changed, 175 insertions(+) create mode 100644 05.wc/lib/wc_methods.rb create mode 100755 05.wc/wc.rb diff --git a/05.wc/lib/wc_methods.rb b/05.wc/lib/wc_methods.rb new file mode 100644 index 0000000000..187e5dbde0 --- /dev/null +++ b/05.wc/lib/wc_methods.rb @@ -0,0 +1,167 @@ +# frozen_string_literal: true + +require 'optparse' + +WcData = Data.define(:paths) do + attr_reader(*%i[errno results total]) + + def initialize(paths:) + @errno = readable_files?(paths) ? 0 : 1 + @results = wc_results(paths).freeze + + @total = wc_results_total(@results) + + super + end +end + +WcResult = Data.define(*%i[path count message]) do + def initialize(path:, count: nil, message: nil) + count, message = wc_count_with_message(path) unless count || message + + super + end +end + +WcCount = Data.define(*%i[newline word byte]) do + def initialize(newline: 0, word: 0, byte: 0) + super + end +end + +def main(args) + print_opts, paths = parse_args(args) + + wc_data = WcData.new(paths) + + print_wc_data(print_opts, wc_data) +end + +def parse_args(args) + copy_args = args.dup + optsym_by_opt = { 'l' => :newline, 'w' => :word, 'c' => :byte } + + optarg_by_opt = OptionParser.new.getopts(copy_args, 'lwc') + + optarg_by_opt.transform_keys!(optsym_by_opt) + optarg_by_opt.value?(true) || optarg_by_opt.transform_values! { |_| true } + + optarg_by_opt[:path] = !copy_args.empty? + + print_opts = optarg_by_opt.select { |_, val| val }.keys + + [print_opts, copy_args] +end + +def readable_files?(paths) + paths.all? do |path| + path == '-' || FileTest.readable?(path) && !FileTest.directory?(path) + end +end + +def wc_results(paths) + return [WcResult.new(path: '-')] if paths.empty? + + paths.map { |path| WcResult.new(path) } +end + +def wc_count_with_message(path) + path == '-' || IO.read(path) +rescue SystemCallError => e + count = e.is_a?(Errno::EISDIR) ? WcCount.new : nil + message = e.message.gsub(/ @ .*$/, '') + + [count, message] +else + [wc_count_for_valid_path(path), nil] +end + +def wc_count_for_valid_path(valid_path) + fd = valid_path == '-' ? $stdin.fileno : IO.sysopen(valid_path.to_s) + + IO.open(fd) { |io| wc_count_for_io(io.set_encoding('ASCII-8BIT')) } +end + +def wc_count_for_io(io) + count_by_type = { newline: 0, word: 0, byte: 0 } + + io.each do |str| + count_by_type[:newline] += str.count("\n") + count_by_type[:word] += str.scan(/[[:graph:]]+/).size + count_by_type[:byte] += str.bytesize + end + + WcCount.new(**count_by_type) +end + +def wc_results_total(wc_results) + total_count_by_type = { newline: 0, word: 0, byte: 0 } + counts_by_type = wc_results.filter_map { |result| result.count&.to_h } + + total_count_by_type.merge!(*counts_by_type) do |_key, total, count| + total + count + end + + WcResult.new(path: 'total', count: WcCount.new(**total_count_by_type)) +end + +def print_wc_data(print_opts, wc_data) + padding_width = padding_width(print_opts, wc_data) + wc_results = wc_data.results.dup + + wc_results << wc_data.total if wc_data.paths.size >= 2 + + wc_results.each do |wc_result| + print_warn(wc_result) + + next if wc_result.count.nil? + + puts format_wc_result(wc_result, print_opts, padding_width) + end + + wc_data.errno +end + +def padding_width(print_opts, wc_data) + return 1 if simple_output?(print_opts, wc_data.paths) + + base_digit = include_non_regular_files?(wc_data.paths) ? 7 : 1 + total_bytes_digit = wc_data.total.count.byte.to_s.size + + [base_digit, total_bytes_digit].max +end + +def simple_output?(print_opts, paths) + (print_opts & WcCount.members).size <= 1 && paths.size <= 1 +end + +def include_non_regular_files?(paths) + paths.empty? || paths.any? do |path| + path == '-' || FileTest.exist?(path) && !FileTest.file?(path) + end +end + +def print_warn(wc_result) + return if wc_result.message.nil? + + path, message = wc_result.deconstruct_keys(%i[path message]).values + + warn "wc: #{path}: #{message}" +end + +def format_wc_result(wc_result, print_opts, padding_width) + raise(ArgumentError, 'empty array: print_opts') if print_opts.empty? + + print_counts = + wc_result.count.deconstruct_keys(print_opts & WcCount.members).values + + padding_width >= 2 && + print_counts.map! do |count_value| + count_value.to_s.rjust(padding_width) + end + + print_opts.include?(:path) && + print_counts << wc_result.path + + print_counts.join(' ') +end diff --git a/05.wc/wc.rb b/05.wc/wc.rb new file mode 100755 index 0000000000..e272b0508a --- /dev/null +++ b/05.wc/wc.rb @@ -0,0 +1,8 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require_relative './lib/wc_methods' + +errno = main(ARGV) + +exit(errno) From f61c5ec536164b8b921e68b8d17eea9e42c50c81 Mon Sep 17 00:00:00 2001 From: Keisuke Kurosawa Date: Wed, 2 Apr 2025 15:55:39 +0900 Subject: [PATCH 02/20] refactor: Delete formfeed character --- 05.wc/lib/wc_methods.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/05.wc/lib/wc_methods.rb b/05.wc/lib/wc_methods.rb index 187e5dbde0..f00b2a1b77 100644 --- a/05.wc/lib/wc_methods.rb +++ b/05.wc/lib/wc_methods.rb @@ -28,7 +28,7 @@ def initialize(newline: 0, word: 0, byte: 0) super end end - + def main(args) print_opts, paths = parse_args(args) @@ -104,7 +104,7 @@ def wc_results_total(wc_results) WcResult.new(path: 'total', count: WcCount.new(**total_count_by_type)) end - + def print_wc_data(print_opts, wc_data) padding_width = padding_width(print_opts, wc_data) wc_results = wc_data.results.dup From 3f3ff3958193e47a0fc44e2e53535e40a17710d3 Mon Sep 17 00:00:00 2001 From: Keisuke Kurosawa Date: Fri, 4 Apr 2025 00:13:15 +0900 Subject: [PATCH 03/20] refactor: Separate responsibility for returning exit status * 05.wc/lib/wc_methods.rb (#main): Return exit status. (#print_wc_data): No longer return exit status. --- 05.wc/lib/wc_methods.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/05.wc/lib/wc_methods.rb b/05.wc/lib/wc_methods.rb index f00b2a1b77..d724649d19 100644 --- a/05.wc/lib/wc_methods.rb +++ b/05.wc/lib/wc_methods.rb @@ -35,6 +35,8 @@ def main(args) wc_data = WcData.new(paths) print_wc_data(print_opts, wc_data) + + wc_data.errno end def parse_args(args) @@ -118,8 +120,6 @@ def print_wc_data(print_opts, wc_data) puts format_wc_result(wc_result, print_opts, padding_width) end - - wc_data.errno end def padding_width(print_opts, wc_data) From eda1b6860eccaef3c557e204959c46342658d1c4 Mon Sep 17 00:00:00 2001 From: Keisuke Kurosawa Date: Fri, 4 Apr 2025 01:35:57 +0900 Subject: [PATCH 04/20] refactor: Use singleton method of `File` class * 05.wc/lib/wc_methods.rb (#wc_count_with_message) (#wc_count_for_valid_path) : Change from singleton method of `IO` class. (#wc_count_for_valid_path) : No longer use. --- 05.wc/lib/wc_methods.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/05.wc/lib/wc_methods.rb b/05.wc/lib/wc_methods.rb index d724649d19..0be7dcf97d 100644 --- a/05.wc/lib/wc_methods.rb +++ b/05.wc/lib/wc_methods.rb @@ -68,7 +68,7 @@ def wc_results(paths) end def wc_count_with_message(path) - path == '-' || IO.read(path) + path == '-' || File.read(path) rescue SystemCallError => e count = e.is_a?(Errno::EISDIR) ? WcCount.new : nil message = e.message.gsub(/ @ .*$/, '') @@ -79,9 +79,9 @@ def wc_count_with_message(path) end def wc_count_for_valid_path(valid_path) - fd = valid_path == '-' ? $stdin.fileno : IO.sysopen(valid_path.to_s) + path = valid_path == '-' ? $stdin.fileno : valid_path - IO.open(fd) { |io| wc_count_for_io(io.set_encoding('ASCII-8BIT')) } + File.open(path) { |f| wc_count_for_io(f.set_encoding('ASCII-8BIT')) } end def wc_count_for_io(io) From 83141a5a69a9e3de734a512ff61ef4f9794b2409 Mon Sep 17 00:00:00 2001 From: Keisuke Kurosawa Date: Fri, 4 Apr 2025 01:49:19 +0900 Subject: [PATCH 05/20] refactor: Extract option constants and simplify argument parsing * 05.wc/lib/wc_methods.rb (OPTION_STRING, WORD_COUNT_TYPES) (OPTION_NAME_TO_WORD_COUNT_TYPE): New constant. (WcCount) : Use `WORD_COUNT_TYPES`. (#main): Return enabled option keys only. (#parse_args): Extract constant. Simplify option parsing. --- 05.wc/lib/wc_methods.rb | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/05.wc/lib/wc_methods.rb b/05.wc/lib/wc_methods.rb index 0be7dcf97d..59d774f7c2 100644 --- a/05.wc/lib/wc_methods.rb +++ b/05.wc/lib/wc_methods.rb @@ -2,6 +2,11 @@ require 'optparse' +OPTION_STRING = 'lwc' +WORD_COUNT_TYPES = %i[newline word byte].freeze + +OPTION_NAME_TO_WORD_COUNT_TYPE = OPTION_STRING.chars.zip(WORD_COUNT_TYPES).to_h.freeze + WcData = Data.define(:paths) do attr_reader(*%i[errno results total]) @@ -23,16 +28,16 @@ def initialize(path:, count: nil, message: nil) end end -WcCount = Data.define(*%i[newline word byte]) do +WcCount = Data.define(*WORD_COUNT_TYPES) do def initialize(newline: 0, word: 0, byte: 0) super end end def main(args) - print_opts, paths = parse_args(args) + print_opts = parse_args(args) - wc_data = WcData.new(paths) + wc_data = WcData.new(args) print_wc_data(print_opts, wc_data) @@ -40,19 +45,14 @@ def main(args) end def parse_args(args) - copy_args = args.dup - optsym_by_opt = { 'l' => :newline, 'w' => :word, 'c' => :byte } - - optarg_by_opt = OptionParser.new.getopts(copy_args, 'lwc') - - optarg_by_opt.transform_keys!(optsym_by_opt) - optarg_by_opt.value?(true) || optarg_by_opt.transform_values! { |_| true } + parsed_options = OptionParser.new.getopts(args, OPTION_STRING).transform_keys(OPTION_NAME_TO_WORD_COUNT_TYPE) - optarg_by_opt[:path] = !copy_args.empty? + # `wc [file ...]` == `wc -lwc [file ...]` + parsed_options.transform_values! { |_| true } unless parsed_options.value?(true) - print_opts = optarg_by_opt.select { |_, val| val }.keys + parsed_options[:path] = !args.empty? - [print_opts, copy_args] + parsed_options.select { |_, val| val }.keys end def readable_files?(paths) From b010690d9c7dde1742b6064bb12e1fda06456a7c Mon Sep 17 00:00:00 2001 From: Keisuke Kurosawa Date: Fri, 2 May 2025 04:06:40 +0900 Subject: [PATCH 06/20] feat: Define `WcPathname` class * 05.wc/lib/wc_pathname.rb: New file. (WORD_COUNT_TYPES, IO_BUFFER_SIZE): New constant. (WcPathname): New class. (#word_count_for_io, #word_count_for_string) (#word_count_for_string_per_type): New method for `WcPathname#word_count`. --- 05.wc/lib/wc_pathname.rb | 83 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 05.wc/lib/wc_pathname.rb diff --git a/05.wc/lib/wc_pathname.rb b/05.wc/lib/wc_pathname.rb new file mode 100644 index 0000000000..b05f4c90b5 --- /dev/null +++ b/05.wc/lib/wc_pathname.rb @@ -0,0 +1,83 @@ +# frozen_string_literal: true + +require 'pathname' + +WORD_COUNT_TYPES = %i[newline word byte].freeze + +IO_BUFFER_SIZE = 16 * 1024 + +class WcPathname < Pathname + def initialize(path) + super(path) + + @pathname_or_stat = stdin? ? $stdin.stat : self + end + + def stdin? + @path == '-' + end + + def regular_file? + @pathname_or_stat.file? + end + + def regular_file_size? + @pathname_or_stat.size if regular_file? + end + + def regular_file_size + regular_file_size?.to_i + end + + def exist_non_regular_file? + return !regular_file? if stdin? + + exist? && !file? + end + + def readable_non_directory? + @pathname_or_stat.readable? && !@pathname_or_stat.directory? + end + + def word_count(word_count_types = WORD_COUNT_TYPES) + return { byte: regular_file_size } if word_count_types == %i[byte] && !regular_file_size?.nil? + + return word_count_for_io($stdin.set_encoding('ASCII-8BIT'), word_count_types, regular_file_size?) if stdin? + + open { word_count_for_io(_1.set_encoding('ASCII-8BIT'), word_count_types, regular_file_size?) } + end +end + +def word_count_for_io(io, word_count_types = WORD_COUNT_TYPES, file_size = nil) + counts = [] + + loop do + str = io.readpartial(IO_BUFFER_SIZE) + + count = word_count_for_string(str, word_count_types, file_size) + + counts << count + rescue EOFError + break + end + + word_count_types.to_h { [_1, _1 == :byte ? file_size.to_i : 0] } + .merge!(*counts) { |_, total, count| total + count } +end + +def word_count_for_string(str, word_count_types = WORD_COUNT_TYPES, file_size = nil) + word_count_types.to_h { [_1, _1 == :byte && !file_size.nil? ? 0 : word_count_for_string_per_type(str, _1)] } +end + +def word_count_for_string_per_type(str, word_count_type) + case word_count_type + when :newline + str.count("\n") + when :word + str.split.size + when :byte + str.bytesize + else + raise(ArgumentError, "invalid or not symbol: word_count_type ==> #{word_count_type}") + end +end From de45d451f6eacdd71ac15367c9298b5b3178a533 Mon Sep 17 00:00:00 2001 From: Keisuke Kurosawa Date: Fri, 2 May 2025 04:13:00 +0900 Subject: [PATCH 07/20] fix: Use `WcPathname` class * 05.wc/lib/wc_methods.rb: Load `05.wc/lib/wc_pathname.rb`. (WORD_COUNT_TYPES): Remove duplicate constant. (WcData, WcResult, WcCount): Remove `Data` subclass. (#readable_files?, #wc_results, #wc_count_with_message) (#wc_count_for_valid_path, #wc_count_for_io, #wc_results_total) (#print_wc_data, #padding_width, #simple_output?) (#include_non_regular_files, #print_warn, #format_wc_result): Remove method. (#main): Use new method. Return error if `$stdin` is directory. (#extract_word_count_types, #displayed_output_format, #adjust_digit) (#word_count_results, #with_system_call_error_handler) (#word_count_error_handler, #print_word_count_result): New method. --- 05.wc/lib/wc_methods.rb | 161 +++++++++++----------------------------- 1 file changed, 44 insertions(+), 117 deletions(-) diff --git a/05.wc/lib/wc_methods.rb b/05.wc/lib/wc_methods.rb index 59d774f7c2..ab6be0092a 100644 --- a/05.wc/lib/wc_methods.rb +++ b/05.wc/lib/wc_methods.rb @@ -1,47 +1,34 @@ # frozen_string_literal: true require 'optparse' +require_relative './wc_pathname' OPTION_STRING = 'lwc' -WORD_COUNT_TYPES = %i[newline word byte].freeze OPTION_NAME_TO_WORD_COUNT_TYPE = OPTION_STRING.chars.zip(WORD_COUNT_TYPES).to_h.freeze -WcData = Data.define(:paths) do - attr_reader(*%i[errno results total]) +def main(args) + displayed_items = parse_args(args) - def initialize(paths:) - @errno = readable_files?(paths) ? 0 : 1 - @results = wc_results(paths).freeze + wc_paths = args.empty? ? [WcPathname.new('-')] : args.map { WcPathname.new(_1) } - @total = wc_results_total(@results) + word_count_types = extract_word_count_types(displayed_items) - super - end -end + output_format = displayed_output_format(displayed_items, wc_paths) -WcResult = Data.define(*%i[path count message]) do - def initialize(path:, count: nil, message: nil) - count, message = wc_count_with_message(path) unless count || message + counts = + word_count_results(word_count_types, wc_paths).each { print_word_count_result(output_format, _1) } + .filter_map { _1[:count] } - super - end -end + if wc_paths.size >= 2 + count_total = + word_count_types.to_h { [_1, 0] } + .merge!(*counts) { |_, total, count| total + count } -WcCount = Data.define(*WORD_COUNT_TYPES) do - def initialize(newline: 0, word: 0, byte: 0) - super + print_word_count_result(output_format, { path: 'total', count: count_total, message: nil }) end -end - -def main(args) - print_opts = parse_args(args) - - wc_data = WcData.new(args) - - print_wc_data(print_opts, wc_data) - wc_data.errno + wc_paths.all?(&:readable_non_directory?) ? 0 : 1 end def parse_args(args) @@ -55,113 +42,53 @@ def parse_args(args) parsed_options.select { |_, val| val }.keys end -def readable_files?(paths) - paths.all? do |path| - path == '-' || FileTest.readable?(path) && !FileTest.directory?(path) - end -end - -def wc_results(paths) - return [WcResult.new(path: '-')] if paths.empty? - - paths.map { |path| WcResult.new(path) } -end - -def wc_count_with_message(path) - path == '-' || File.read(path) -rescue SystemCallError => e - count = e.is_a?(Errno::EISDIR) ? WcCount.new : nil - message = e.message.gsub(/ @ .*$/, '') - - [count, message] -else - [wc_count_for_valid_path(path), nil] -end - -def wc_count_for_valid_path(valid_path) - path = valid_path == '-' ? $stdin.fileno : valid_path - - File.open(path) { |f| wc_count_for_io(f.set_encoding('ASCII-8BIT')) } +def extract_word_count_types(displayed_items) + WORD_COUNT_TYPES & displayed_items end -def wc_count_for_io(io) - count_by_type = { newline: 0, word: 0, byte: 0 } +def displayed_output_format(displayed_items, wc_paths) + one_type_one_operand = extract_word_count_types(displayed_items).size == 1 && wc_paths.size == 1 - io.each do |str| - count_by_type[:newline] += str.count("\n") - count_by_type[:word] += str.scan(/[[:graph:]]+/).size - count_by_type[:byte] += str.bytesize - end + digit = one_type_one_operand ? 1 : adjust_digit(wc_paths) - WcCount.new(**count_by_type) + { newline: "%#{digit}d", + word: "%#{digit}d", + byte: "%#{digit}d", + path: '%s' }.values_at(*displayed_items).join(' ') end -def wc_results_total(wc_results) - total_count_by_type = { newline: 0, word: 0, byte: 0 } - counts_by_type = wc_results.filter_map { |result| result.count&.to_h } +def adjust_digit(wc_paths) + default_digit = wc_paths.any?(&:exist_non_regular_file?) ? 7 : 1 - total_count_by_type.merge!(*counts_by_type) do |_key, total, count| - total + count - end + total_bytes_digit = wc_paths.sum(&:regular_file_size).to_s.size - WcResult.new(path: 'total', count: WcCount.new(**total_count_by_type)) + [default_digit, total_bytes_digit].max end -def print_wc_data(print_opts, wc_data) - padding_width = padding_width(print_opts, wc_data) - wc_results = wc_data.results.dup - - wc_results << wc_data.total if wc_data.paths.size >= 2 +def word_count_results(word_count_types, wc_paths) + wc_paths.map do |wc_path| + count, message = + with_system_call_error_handler(:word_count_error_handler, word_count_types) { wc_path.word_count(_1) } - wc_results.each do |wc_result| - print_warn(wc_result) - - next if wc_result.count.nil? - - puts format_wc_result(wc_result, print_opts, padding_width) + { path: wc_path.to_s, count:, message: }.freeze end end -def padding_width(print_opts, wc_data) - return 1 if simple_output?(print_opts, wc_data.paths) - - base_digit = include_non_regular_files?(wc_data.paths) ? 7 : 1 - total_bytes_digit = wc_data.total.count.byte.to_s.size - - [base_digit, total_bytes_digit].max -end - -def simple_output?(print_opts, paths) - (print_opts & WcCount.members).size <= 1 && paths.size <= 1 -end - -def include_non_regular_files?(paths) - paths.empty? || paths.any? do |path| - path == '-' || FileTest.exist?(path) && !FileTest.file?(path) - end +def with_system_call_error_handler(error_handler, *args) + yield(*args) +rescue SystemCallError => e + method(error_handler).call(e, *args) end -def print_warn(wc_result) - return if wc_result.message.nil? +def word_count_error_handler(error, word_count_types) + count = error.is_a?(Errno::EISDIR) ? word_count_types.to_h { [_1, 0] } : nil + message = error.message.split(' @ ').first - path, message = wc_result.deconstruct_keys(%i[path message]).values - - warn "wc: #{path}: #{message}" + [count, message] end -def format_wc_result(wc_result, print_opts, padding_width) - raise(ArgumentError, 'empty array: print_opts') if print_opts.empty? - - print_counts = - wc_result.count.deconstruct_keys(print_opts & WcCount.members).values - - padding_width >= 2 && - print_counts.map! do |count_value| - count_value.to_s.rjust(padding_width) - end - - print_opts.include?(:path) && - print_counts << wc_result.path +def print_word_count_result(output_format, result) + warn "wc: #{result[:path]}: #{result[:message]}" if result[:message] - print_counts.join(' ') + puts format(output_format, **result[:count], path: result[:path]) unless result[:count].nil? end From d09bfa9f5b797f93c0e0b056a4bdb7302e3c9a46 Mon Sep 17 00:00:00 2001 From: Keisuke Kurosawa Date: Thu, 15 May 2025 15:44:22 +0900 Subject: [PATCH 08/20] feat: Define `WordCount` module * 05.wc/lib/word_count.rb: New file. (WordCount, WordCount::IO, WordCount::String): New module. (IO): Mix-in `WordCount::IO`. (String): Mix-in `WordCount::String`. --- 05.wc/lib/word_count.rb | 63 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 05.wc/lib/word_count.rb diff --git a/05.wc/lib/word_count.rb b/05.wc/lib/word_count.rb new file mode 100644 index 0000000000..dc8a615dc3 --- /dev/null +++ b/05.wc/lib/word_count.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +module WordCount + TYPES = %i[newline word byte].freeze + + def extract_types(types) + TYPES & types + end + + module_function :extract_types +end + +module WordCount::IO + BUFFER_SIZE = 16 * 1024 + + private_constant :BUFFER_SIZE + + def word_count(word_count_types = WordCount::TYPES, file_size = nil) + counts = [] + + loop do + str = readpartial(BUFFER_SIZE) + + count = str.word_count(word_count_types, file_size) + + counts << count + rescue EOFError + break + end + + word_count_types.to_h { [_1, _1 == :byte ? file_size.to_i : 0] } + .merge!(*counts) { |_, total, count| total + count } + end +end + +module WordCount::String + def word_count(word_count_types = WordCount::TYPES, file_size = nil) + word_count_types.to_h { [_1, _1 == :byte && !file_size.nil? ? 0 : word_count_per_type(_1)] } + end + + private + + def word_count_per_type(word_count_type) + case word_count_type + when :newline + count("\n") + when :word + split.size + when :byte + bytesize + else + raise(ArgumentError, "word_count_type: allow only #{WordCount::TYPES.map(&:inspect).join(', ')}") + end + end +end + +class IO + include WordCount::IO +end + +class String + include WordCount::String +end From 313f081d88a06d25e11e21ed1dee576596e6ac81 Mon Sep 17 00:00:00 2001 From: Keisuke Kurosawa Date: Sat, 5 Jul 2025 00:00:51 +0900 Subject: [PATCH 09/20] fix: Use `WordCount` module * 05.wc/lib/wc_methods.rb (OPTION_NAME_TO_WORD_COUNT_TYPE) : Rename constant. (#main) (#displayed_output_format) : Use `WordCount#.extract_types`. (#extract_word_count_types): Remove method. * 05.wc/lib/wc_pathname.rb: Load `05.wc/lib/word_count.rb`. (WORD_COUNT_TYPES, IO_BUFFER_SIZE): Remove duplicate constant. (WcPathname#word_count): Use `WordCount::IO#word_count`. : Rename constant. (#word_count_for_io, #word_count_for_string) (#word_count_for_string_per_type): Remove method. --- 05.wc/lib/wc_methods.rb | 10 +++------ 05.wc/lib/wc_pathname.rb | 45 ++++------------------------------------ 2 files changed, 7 insertions(+), 48 deletions(-) diff --git a/05.wc/lib/wc_methods.rb b/05.wc/lib/wc_methods.rb index ab6be0092a..2fd43728c3 100644 --- a/05.wc/lib/wc_methods.rb +++ b/05.wc/lib/wc_methods.rb @@ -5,14 +5,14 @@ OPTION_STRING = 'lwc' -OPTION_NAME_TO_WORD_COUNT_TYPE = OPTION_STRING.chars.zip(WORD_COUNT_TYPES).to_h.freeze +OPTION_NAME_TO_WORD_COUNT_TYPE = OPTION_STRING.chars.zip(WordCount::TYPES).to_h.freeze def main(args) displayed_items = parse_args(args) wc_paths = args.empty? ? [WcPathname.new('-')] : args.map { WcPathname.new(_1) } - word_count_types = extract_word_count_types(displayed_items) + word_count_types = WordCount.extract_types(displayed_items) output_format = displayed_output_format(displayed_items, wc_paths) @@ -42,12 +42,8 @@ def parse_args(args) parsed_options.select { |_, val| val }.keys end -def extract_word_count_types(displayed_items) - WORD_COUNT_TYPES & displayed_items -end - def displayed_output_format(displayed_items, wc_paths) - one_type_one_operand = extract_word_count_types(displayed_items).size == 1 && wc_paths.size == 1 + one_type_one_operand = WordCount.extract_types(displayed_items).size == 1 && wc_paths.size == 1 digit = one_type_one_operand ? 1 : adjust_digit(wc_paths) diff --git a/05.wc/lib/wc_pathname.rb b/05.wc/lib/wc_pathname.rb index b05f4c90b5..b27ecaff2e 100644 --- a/05.wc/lib/wc_pathname.rb +++ b/05.wc/lib/wc_pathname.rb @@ -1,10 +1,7 @@ # frozen_string_literal: true require 'pathname' - -WORD_COUNT_TYPES = %i[newline word byte].freeze - -IO_BUFFER_SIZE = 16 * 1024 +require_relative './word_count' class WcPathname < Pathname def initialize(path) @@ -39,45 +36,11 @@ def readable_non_directory? @pathname_or_stat.readable? && !@pathname_or_stat.directory? end - def word_count(word_count_types = WORD_COUNT_TYPES) + def word_count(word_count_types = WordCount::TYPES) return { byte: regular_file_size } if word_count_types == %i[byte] && !regular_file_size?.nil? - return word_count_for_io($stdin.set_encoding('ASCII-8BIT'), word_count_types, regular_file_size?) if stdin? - - open { word_count_for_io(_1.set_encoding('ASCII-8BIT'), word_count_types, regular_file_size?) } - end -end - -def word_count_for_io(io, word_count_types = WORD_COUNT_TYPES, file_size = nil) - counts = [] - - loop do - str = io.readpartial(IO_BUFFER_SIZE) - - count = word_count_for_string(str, word_count_types, file_size) - - counts << count - rescue EOFError - break - end - - word_count_types.to_h { [_1, _1 == :byte ? file_size.to_i : 0] } - .merge!(*counts) { |_, total, count| total + count } -end - -def word_count_for_string(str, word_count_types = WORD_COUNT_TYPES, file_size = nil) - word_count_types.to_h { [_1, _1 == :byte && !file_size.nil? ? 0 : word_count_for_string_per_type(str, _1)] } -end + return $stdin.set_encoding('ASCII-8BIT').word_count(word_count_types, regular_file_size?) if stdin? -def word_count_for_string_per_type(str, word_count_type) - case word_count_type - when :newline - str.count("\n") - when :word - str.split.size - when :byte - str.bytesize - else - raise(ArgumentError, "invalid or not symbol: word_count_type ==> #{word_count_type}") + open { _1.set_encoding('ASCII-8BIT').word_count(word_count_types, regular_file_size?) } end end From 129503db0d6dc710bdd7cd1d5e73c31040f14d8c Mon Sep 17 00:00:00 2001 From: Keisuke Kurosawa Date: Sat, 5 Jul 2025 16:40:00 +0900 Subject: [PATCH 10/20] fix: No longer require `pathname` * 05.wc/lib/wc_pathname.rb (WcPathname): Does not inherit from `Pathname`. (WcPathname::USE_FILETEST_MODULE_FUNCTIONS): New private constant. (WcPathname#initialize): Set `@path` only. (WcPathname#inspect, WcPathname#to_s, WcPathname#open) (WcPathname#exist?, WcPathname#directory?, WcPathname#file?) (WcPathname#readable?, WcPathname#size): New method. (WcPathname#regular_file?): Remove method. Migrate to `WcPathname#file?`. (WcPathame#regular_file_size?, WcPathame#exist_non_regular_file?) (WcPathame#readable_non_directory?, WcPathame#word_count): Use new method. --- 05.wc/lib/wc_pathname.rb | 41 +++++++++++++++++++++++++++------------- 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/05.wc/lib/wc_pathname.rb b/05.wc/lib/wc_pathname.rb index b27ecaff2e..1a2d26a328 100644 --- a/05.wc/lib/wc_pathname.rb +++ b/05.wc/lib/wc_pathname.rb @@ -1,25 +1,44 @@ # frozen_string_literal: true -require 'pathname' require_relative './word_count' -class WcPathname < Pathname - def initialize(path) - super(path) +class WcPathname + USE_FILETEST_MODULE_FUNCTIONS = %i[directory? file? readable? size].freeze + + private_constant :USE_FILETEST_MODULE_FUNCTIONS - @pathname_or_stat = stdin? ? $stdin.stat : self + def initialize(path) + @path = path end def stdin? @path == '-' end - def regular_file? - @pathname_or_stat.file? + def inspect + "#<#{self.class}:#{@path}>" + end + + def to_s + @path + end + + def open(mode = 'r', perm = 0o0666, &block) + return block&.call($stdin) || $stdin if stdin? + + File.open(@path, mode, perm, &block) + end + + def exist? + stdin? || FileTest.exist?(@path) + end + + USE_FILETEST_MODULE_FUNCTIONS.each do |method| + define_method(method) { stdin? ? $stdin.stat.public_send(method) : FileTest.public_send(method, @path) } end def regular_file_size? - @pathname_or_stat.size if regular_file? + size if file? end def regular_file_size @@ -27,20 +46,16 @@ def regular_file_size end def exist_non_regular_file? - return !regular_file? if stdin? - exist? && !file? end def readable_non_directory? - @pathname_or_stat.readable? && !@pathname_or_stat.directory? + readable? && !directory? end def word_count(word_count_types = WordCount::TYPES) return { byte: regular_file_size } if word_count_types == %i[byte] && !regular_file_size?.nil? - return $stdin.set_encoding('ASCII-8BIT').word_count(word_count_types, regular_file_size?) if stdin? - open { _1.set_encoding('ASCII-8BIT').word_count(word_count_types, regular_file_size?) } end end From fe98d848d31bfdb89c010d5cd246cd2d93abf8f7 Mon Sep 17 00:00:00 2001 From: Keisuke Kurosawa Date: Sat, 5 Jul 2025 00:11:05 +0900 Subject: [PATCH 11/20] fix: Do not use exception handling for control flow * 05.wc/lib/wc_methods.rb (#word_count_results) : Use `WcPathname#word_count`, `WcPathname#word_count_message`. (#with_system_call_error_handler, #word_count_error_handler): Remove method. * 05.wc/lib/wc_pathname.rb (WcPathname#word_count): Return the corresponding value instead of the exception (`Errno::ENOENT`, `Errno::EACCES`, `Errno::EISDIR`) that is thrown when reading a file fails. (WcPathname#word_count_message): New method. --- 05.wc/lib/wc_methods.rb | 17 ++--------------- 05.wc/lib/wc_pathname.rb | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/05.wc/lib/wc_methods.rb b/05.wc/lib/wc_methods.rb index 2fd43728c3..98caa15f3e 100644 --- a/05.wc/lib/wc_methods.rb +++ b/05.wc/lib/wc_methods.rb @@ -63,26 +63,13 @@ def adjust_digit(wc_paths) def word_count_results(word_count_types, wc_paths) wc_paths.map do |wc_path| - count, message = - with_system_call_error_handler(:word_count_error_handler, word_count_types) { wc_path.word_count(_1) } + count = wc_path.word_count(word_count_types) + message = wc_path.word_count_message { path: wc_path.to_s, count:, message: }.freeze end end -def with_system_call_error_handler(error_handler, *args) - yield(*args) -rescue SystemCallError => e - method(error_handler).call(e, *args) -end - -def word_count_error_handler(error, word_count_types) - count = error.is_a?(Errno::EISDIR) ? word_count_types.to_h { [_1, 0] } : nil - message = error.message.split(' @ ').first - - [count, message] -end - def print_word_count_result(output_format, result) warn "wc: #{result[:path]}: #{result[:message]}" if result[:message] diff --git a/05.wc/lib/wc_pathname.rb b/05.wc/lib/wc_pathname.rb index 1a2d26a328..be7cc8cca7 100644 --- a/05.wc/lib/wc_pathname.rb +++ b/05.wc/lib/wc_pathname.rb @@ -54,8 +54,22 @@ def readable_non_directory? end def word_count(word_count_types = WordCount::TYPES) + return nil unless exist? && readable? + + return word_count_types.to_h { [_1, 0] } if directory? + return { byte: regular_file_size } if word_count_types == %i[byte] && !regular_file_size?.nil? open { _1.set_encoding('ASCII-8BIT').word_count(word_count_types, regular_file_size?) } end + + def word_count_message + return 'No such file or directory' unless exist? + + return 'Permission denied' unless readable? + + return 'Is a directory' if directory? + + nil + end end From 5a1e75fe22b19c4812b9bc465df2c4b5f52e3802 Mon Sep 17 00:00:00 2001 From: Keisuke Kurosawa Date: Wed, 16 Jul 2025 13:53:33 +0900 Subject: [PATCH 12/20] refactor: Integrate word count method * 05.wc/lib/wc_methods.rb (#main) : No longer use `#word_count_results`. (#word_count_results): Remove unused method. * 05.wc/lib/wc_pathname.rb (WcPathname#to_s) (WcPathname#regular_file_size?): Remove unused method. (WcPathname#word_count_result): New method. (WcPathname#word_count): Return the word count only. Change to private method. (WcPathname#word_count_message): Remove method. Integrated to `WcPathname#word_count_result`. --- 05.wc/lib/wc_methods.rb | 14 +++----------- 05.wc/lib/wc_pathname.rb | 32 +++++++++++++------------------- 2 files changed, 16 insertions(+), 30 deletions(-) diff --git a/05.wc/lib/wc_methods.rb b/05.wc/lib/wc_methods.rb index 98caa15f3e..f9a2aee2bc 100644 --- a/05.wc/lib/wc_methods.rb +++ b/05.wc/lib/wc_methods.rb @@ -17,8 +17,9 @@ def main(args) output_format = displayed_output_format(displayed_items, wc_paths) counts = - word_count_results(word_count_types, wc_paths).each { print_word_count_result(output_format, _1) } - .filter_map { _1[:count] } + wc_paths.map { _1.word_count_result(word_count_types) } + .each { print_word_count_result(output_format, _1) } + .filter_map { _1[:count] } if wc_paths.size >= 2 count_total = @@ -61,15 +62,6 @@ def adjust_digit(wc_paths) [default_digit, total_bytes_digit].max end -def word_count_results(word_count_types, wc_paths) - wc_paths.map do |wc_path| - count = wc_path.word_count(word_count_types) - message = wc_path.word_count_message - - { path: wc_path.to_s, count:, message: }.freeze - end -end - def print_word_count_result(output_format, result) warn "wc: #{result[:path]}: #{result[:message]}" if result[:message] diff --git a/05.wc/lib/wc_pathname.rb b/05.wc/lib/wc_pathname.rb index be7cc8cca7..6d3b766e51 100644 --- a/05.wc/lib/wc_pathname.rb +++ b/05.wc/lib/wc_pathname.rb @@ -19,10 +19,6 @@ def inspect "#<#{self.class}:#{@path}>" end - def to_s - @path - end - def open(mode = 'r', perm = 0o0666, &block) return block&.call($stdin) || $stdin if stdin? @@ -37,12 +33,8 @@ def exist? define_method(method) { stdin? ? $stdin.stat.public_send(method) : FileTest.public_send(method, @path) } end - def regular_file_size? - size if file? - end - def regular_file_size - regular_file_size?.to_i + file? ? size : 0 end def exist_non_regular_file? @@ -53,23 +45,25 @@ def readable_non_directory? readable? && !directory? end - def word_count(word_count_types = WordCount::TYPES) - return nil unless exist? && readable? + def word_count_result(word_count_types = WordCount::TYPES) + path = @path - return word_count_types.to_h { [_1, 0] } if directory? + return { path:, count: nil, message: exist? ? 'Permission denied' : 'No such file or directory' } unless readable? - return { byte: regular_file_size } if word_count_types == %i[byte] && !regular_file_size?.nil? + return { path:, count: word_count_types.to_h { [_1, 0] }, message: 'Is a directory' } if directory? - open { _1.set_encoding('ASCII-8BIT').word_count(word_count_types, regular_file_size?) } + { path:, count: word_count(word_count_types), message: nil } end - def word_count_message - return 'No such file or directory' unless exist? + private + + def word_count(word_count_types) + return open { _1.set_encoding('ASCII-8BIT').word_count(word_count_types, nil) } unless file? && word_count_types.include?(:byte) - return 'Permission denied' unless readable? + return { byte: size } if word_count_types == %i[byte] - return 'Is a directory' if directory? + counts = open { _1.set_encoding('ASCII-8BIT').word_count(word_count_types - %i[byte], size) } - nil + { **counts, byte: size } end end From 14cccf1219e0b06dd5a0028744e1bbc756487c33 Mon Sep 17 00:00:00 2001 From: Keisuke Kurosawa Date: Thu, 17 Jul 2025 05:24:42 +0900 Subject: [PATCH 13/20] refactor: Reduce unnecessary method argument * 05.wc/lib/wc_pathname.rb (WcPathname#word_count): Remove the second argument. * 05.wc/lib/word_count.rb (WordCount::IO#word_count) (WordCount::String#word_count) : The second argument is no longer used. --- 05.wc/lib/wc_pathname.rb | 4 ++-- 05.wc/lib/word_count.rb | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/05.wc/lib/wc_pathname.rb b/05.wc/lib/wc_pathname.rb index 6d3b766e51..c6355abfe1 100644 --- a/05.wc/lib/wc_pathname.rb +++ b/05.wc/lib/wc_pathname.rb @@ -58,11 +58,11 @@ def word_count_result(word_count_types = WordCount::TYPES) private def word_count(word_count_types) - return open { _1.set_encoding('ASCII-8BIT').word_count(word_count_types, nil) } unless file? && word_count_types.include?(:byte) + return open { _1.set_encoding('ASCII-8BIT').word_count(word_count_types) } unless file? && word_count_types.include?(:byte) return { byte: size } if word_count_types == %i[byte] - counts = open { _1.set_encoding('ASCII-8BIT').word_count(word_count_types - %i[byte], size) } + counts = open { _1.set_encoding('ASCII-8BIT').word_count(word_count_types - %i[byte]) } { **counts, byte: size } end diff --git a/05.wc/lib/word_count.rb b/05.wc/lib/word_count.rb index dc8a615dc3..35979c82cb 100644 --- a/05.wc/lib/word_count.rb +++ b/05.wc/lib/word_count.rb @@ -15,27 +15,27 @@ module WordCount::IO private_constant :BUFFER_SIZE - def word_count(word_count_types = WordCount::TYPES, file_size = nil) + def word_count(word_count_types = WordCount::TYPES) counts = [] loop do str = readpartial(BUFFER_SIZE) - count = str.word_count(word_count_types, file_size) + count = str.word_count(word_count_types) counts << count rescue EOFError break end - word_count_types.to_h { [_1, _1 == :byte ? file_size.to_i : 0] } + word_count_types.to_h { [_1, 0] } .merge!(*counts) { |_, total, count| total + count } end end module WordCount::String - def word_count(word_count_types = WordCount::TYPES, file_size = nil) - word_count_types.to_h { [_1, _1 == :byte && !file_size.nil? ? 0 : word_count_per_type(_1)] } + def word_count(word_count_types = WordCount::TYPES) + word_count_types.to_h { [_1, word_count_per_type(_1)] } end private From 7b9d052f502b57451893a53513678c859c5f22ff Mon Sep 17 00:00:00 2001 From: Keisuke Kurosawa Date: Sat, 12 Jul 2025 12:21:41 +0900 Subject: [PATCH 14/20] refactor: Migrate `WcPathname` to `WordCount::Pathname` * 05.wc/lib/wc_methods.rb: Load `05.wc/lib/word_count.rb`. (#main) : Use `WordCount::Pathname.new`. * 05.wc/lib/wc_pathname.rb: Delete file. : Migrate to `WordCount::Pathname`. * 05.wc/lib/word_count.rb: (WordCount::Pathname): New class. --- 05.wc/lib/wc_methods.rb | 4 +-- 05.wc/lib/wc_pathname.rb | 69 ---------------------------------------- 05.wc/lib/word_count.rb | 66 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 68 insertions(+), 71 deletions(-) delete mode 100644 05.wc/lib/wc_pathname.rb diff --git a/05.wc/lib/wc_methods.rb b/05.wc/lib/wc_methods.rb index f9a2aee2bc..067b376b41 100644 --- a/05.wc/lib/wc_methods.rb +++ b/05.wc/lib/wc_methods.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true require 'optparse' -require_relative './wc_pathname' +require_relative './word_count' OPTION_STRING = 'lwc' @@ -10,7 +10,7 @@ def main(args) displayed_items = parse_args(args) - wc_paths = args.empty? ? [WcPathname.new('-')] : args.map { WcPathname.new(_1) } + wc_paths = args.empty? ? [WordCount::Pathname.new('-')] : args.map { WordCount::Pathname.new(_1) } word_count_types = WordCount.extract_types(displayed_items) diff --git a/05.wc/lib/wc_pathname.rb b/05.wc/lib/wc_pathname.rb deleted file mode 100644 index c6355abfe1..0000000000 --- a/05.wc/lib/wc_pathname.rb +++ /dev/null @@ -1,69 +0,0 @@ -# frozen_string_literal: true - -require_relative './word_count' - -class WcPathname - USE_FILETEST_MODULE_FUNCTIONS = %i[directory? file? readable? size].freeze - - private_constant :USE_FILETEST_MODULE_FUNCTIONS - - def initialize(path) - @path = path - end - - def stdin? - @path == '-' - end - - def inspect - "#<#{self.class}:#{@path}>" - end - - def open(mode = 'r', perm = 0o0666, &block) - return block&.call($stdin) || $stdin if stdin? - - File.open(@path, mode, perm, &block) - end - - def exist? - stdin? || FileTest.exist?(@path) - end - - USE_FILETEST_MODULE_FUNCTIONS.each do |method| - define_method(method) { stdin? ? $stdin.stat.public_send(method) : FileTest.public_send(method, @path) } - end - - def regular_file_size - file? ? size : 0 - end - - def exist_non_regular_file? - exist? && !file? - end - - def readable_non_directory? - readable? && !directory? - end - - def word_count_result(word_count_types = WordCount::TYPES) - path = @path - - return { path:, count: nil, message: exist? ? 'Permission denied' : 'No such file or directory' } unless readable? - - return { path:, count: word_count_types.to_h { [_1, 0] }, message: 'Is a directory' } if directory? - - { path:, count: word_count(word_count_types), message: nil } - end - - private - - def word_count(word_count_types) - return open { _1.set_encoding('ASCII-8BIT').word_count(word_count_types) } unless file? && word_count_types.include?(:byte) - - return { byte: size } if word_count_types == %i[byte] - - counts = open { _1.set_encoding('ASCII-8BIT').word_count(word_count_types - %i[byte]) } - - { **counts, byte: size } - end -end diff --git a/05.wc/lib/word_count.rb b/05.wc/lib/word_count.rb index 35979c82cb..f2e7a2e412 100644 --- a/05.wc/lib/word_count.rb +++ b/05.wc/lib/word_count.rb @@ -10,6 +10,72 @@ def extract_types(types) module_function :extract_types end +class WordCount::Pathname + USE_FILETEST_MODULE_FUNCTIONS = %i[directory? file? readable? size].freeze + + private_constant :USE_FILETEST_MODULE_FUNCTIONS + + def initialize(path) + @path = path + end + + def stdin? + @path == '-' + end + + def inspect + "#<#{self.class}:#{@path}>" + end + + def open(mode = 'r', perm = 0o0666, &block) + return block&.call($stdin) || $stdin if stdin? + + File.open(@path, mode, perm, &block) + end + + def exist? + stdin? || FileTest.exist?(@path) + end + + USE_FILETEST_MODULE_FUNCTIONS.each do |method| + define_method(method) { stdin? ? $stdin.stat.public_send(method) : FileTest.public_send(method, @path) } + end + + def regular_file_size + file? ? size : 0 + end + + def exist_non_regular_file? + exist? && !file? + end + + def readable_non_directory? + readable? && !directory? + end + + def word_count_result(word_count_types = WordCount::TYPES) + path = @path + + return { path:, count: nil, message: exist? ? 'Permission denied' : 'No such file or directory' } unless readable? + + return { path:, count: word_count_types.to_h { [_1, 0] }, message: 'Is a directory' } if directory? + + { path:, count: word_count(word_count_types), message: nil } + end + + private + + def word_count(word_count_types) + return open { _1.set_encoding('ASCII-8BIT').word_count(word_count_types) } unless file? && word_count_types.include?(:byte) + + return { byte: size } if word_count_types == %i[byte] + + counts = open { _1.set_encoding('ASCII-8BIT').word_count(word_count_types - %i[byte]) } + + { **counts, byte: size } + end +end + module WordCount::IO BUFFER_SIZE = 16 * 1024 From 7a74cace5b88e219280ae7ccb1b57f2668b56956 Mon Sep 17 00:00:00 2001 From: Keisuke Kurosawa Date: Tue, 12 Aug 2025 22:55:48 +0900 Subject: [PATCH 15/20] fix: Count words correctly * 05.wc/lib/word_count.rb (WordCount::IO#word_count): Extract method. Change word counting. (WordCount::IO#each_buffer): New extracted method. (WordCount::String#word_count_per_type) <:word>: Count words that contain only ASCII printable characters. --- 05.wc/lib/word_count.rb | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/05.wc/lib/word_count.rb b/05.wc/lib/word_count.rb index f2e7a2e412..96c90c4b59 100644 --- a/05.wc/lib/word_count.rb +++ b/05.wc/lib/word_count.rb @@ -81,21 +81,20 @@ module WordCount::IO private_constant :BUFFER_SIZE - def word_count(word_count_types = WordCount::TYPES) - counts = [] - - loop do - str = readpartial(BUFFER_SIZE) + def word_count(word_count_types = WordCount::TYPES, bufsize: BUFFER_SIZE) + each_buffer(bufsize).inject(:<<).to_s.word_count(word_count_types) + end - count = str.word_count(word_count_types) + def each_buffer(limit = BUFFER_SIZE) + return to_enum(__callee__, limit) unless block_given? - counts << count + loop do + yield readpartial(limit) rescue EOFError break end - word_count_types.to_h { [_1, 0] } - .merge!(*counts) { |_, total, count| total + count } + self end end @@ -111,7 +110,11 @@ def word_count_per_type(word_count_type) when :newline count("\n") when :word - split.size + num = 0 + + split { num += 1 if _1.match?(/[[:graph:]]/) } + + num when :byte bytesize else From b8139891a32f2e75544bc418184654742f358506 Mon Sep 17 00:00:00 2001 From: Keisuke Kurosawa Date: Sun, 17 Aug 2025 16:47:24 +0900 Subject: [PATCH 16/20] fix: Output when there is no file operand * 05.wc/lib/wc_methods.rb (#main) : If there is no file operand, an instance is created with no arguments. * 05.wc/lib/word_count.rb:(WordCount::Pathname#initialize) <@path>: This instance variable must be `String`. (WordCount::Pathname#to_path): New method. (WordCount::Pathname#stdin?, WordCount::Pathname#inspect) (WordCount::Pathname#open, WordCount::Pathname#exist?) (WordCount::Pathname#directory?, WordCount::Pathname#file?) (WordCount::Pathname#readable?, WordCount::Pathname#size): Use `WordCount::Pathname#to_path`. (WordCount::Pathname#word_count_result) : Substitute 'standard input' if `@path` is empty string. --- 05.wc/lib/wc_methods.rb | 2 +- 05.wc/lib/word_count.rb | 22 ++++++++++++++-------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/05.wc/lib/wc_methods.rb b/05.wc/lib/wc_methods.rb index 067b376b41..2f28f59db7 100644 --- a/05.wc/lib/wc_methods.rb +++ b/05.wc/lib/wc_methods.rb @@ -10,7 +10,7 @@ def main(args) displayed_items = parse_args(args) - wc_paths = args.empty? ? [WordCount::Pathname.new('-')] : args.map { WordCount::Pathname.new(_1) } + wc_paths = args.empty? ? [WordCount::Pathname.new] : args.map { WordCount::Pathname.new(_1) } word_count_types = WordCount.extract_types(displayed_items) diff --git a/05.wc/lib/word_count.rb b/05.wc/lib/word_count.rb index 96c90c4b59..1ecb298df8 100644 --- a/05.wc/lib/word_count.rb +++ b/05.wc/lib/word_count.rb @@ -15,30 +15,36 @@ class WordCount::Pathname private_constant :USE_FILETEST_MODULE_FUNCTIONS - def initialize(path) - @path = path + def initialize(path = nil) + @path = path.to_s + end + + def to_path + return '-' if @path.empty? + + @path end def stdin? - @path == '-' + to_path == '-' end def inspect - "#<#{self.class}:#{@path}>" + "#<#{self.class}:#{to_path}>" end def open(mode = 'r', perm = 0o0666, &block) return block&.call($stdin) || $stdin if stdin? - File.open(@path, mode, perm, &block) + File.open(to_path, mode, perm, &block) end def exist? - stdin? || FileTest.exist?(@path) + stdin? || FileTest.exist?(to_path) end USE_FILETEST_MODULE_FUNCTIONS.each do |method| - define_method(method) { stdin? ? $stdin.stat.public_send(method) : FileTest.public_send(method, @path) } + define_method(method) { stdin? ? $stdin.stat.public_send(method) : FileTest.public_send(method, to_path) } end def regular_file_size @@ -54,7 +60,7 @@ def readable_non_directory? end def word_count_result(word_count_types = WordCount::TYPES) - path = @path + path = @path.empty? ? 'standard input' : @path return { path:, count: nil, message: exist? ? 'Permission denied' : 'No such file or directory' } unless readable? From a0c5c48fa6ea76d36f5a862f081688ab8570e59d Mon Sep 17 00:00:00 2001 From: Keisuke Kurosawa Date: Tue, 26 Aug 2025 00:21:04 +0900 Subject: [PATCH 17/20] fix: Handling `Errno::EPERM` (Operation not permitted) As far as I know, there are no methods in Ruby's built-in classes or standard libraries that can detect in advance whether a file will result in `Errno::EPERM`. The only way to deal with this is to use exception handling. * 05.wc/lib/wc_methods.rb (#main): Error status is determined by whether each element in `results` has an error message. : Remove variable. : New variable. : Use `results`. * 05.wc/lib/word_count.rb (WordCount::Pathname#readable_non_directory?): Remove unused method. (WordCount::Pathname#word_count_result): Add `Errno::EPERM` exception handling. --- 05.wc/lib/wc_methods.rb | 7 +++---- 05.wc/lib/word_count.rb | 6 ++---- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/05.wc/lib/wc_methods.rb b/05.wc/lib/wc_methods.rb index 2f28f59db7..3ee5412f45 100644 --- a/05.wc/lib/wc_methods.rb +++ b/05.wc/lib/wc_methods.rb @@ -16,20 +16,19 @@ def main(args) output_format = displayed_output_format(displayed_items, wc_paths) - counts = + results = wc_paths.map { _1.word_count_result(word_count_types) } .each { print_word_count_result(output_format, _1) } - .filter_map { _1[:count] } if wc_paths.size >= 2 count_total = word_count_types.to_h { [_1, 0] } - .merge!(*counts) { |_, total, count| total + count } + .merge!(*results.filter_map { _1[:count] }) { |_, total, count| total + count } print_word_count_result(output_format, { path: 'total', count: count_total, message: nil }) end - wc_paths.all?(&:readable_non_directory?) ? 0 : 1 + results.any? { _1[:message] } ? 1 : 0 end def parse_args(args) diff --git a/05.wc/lib/word_count.rb b/05.wc/lib/word_count.rb index 1ecb298df8..83626877c7 100644 --- a/05.wc/lib/word_count.rb +++ b/05.wc/lib/word_count.rb @@ -55,10 +55,6 @@ def exist_non_regular_file? exist? && !file? end - def readable_non_directory? - readable? && !directory? - end - def word_count_result(word_count_types = WordCount::TYPES) path = @path.empty? ? 'standard input' : @path @@ -67,6 +63,8 @@ def word_count_result(word_count_types = WordCount::TYPES) return { path:, count: word_count_types.to_h { [_1, 0] }, message: 'Is a directory' } if directory? { path:, count: word_count(word_count_types), message: nil } + rescue Errno::EPERM => e + { path:, count: nil, message: e.message.partition(' @ ').first } end private From 3f637355a234c6d5a56f2d03e690ac36ee38eec0 Mon Sep 17 00:00:00 2001 From: Keisuke Kurosawa Date: Thu, 11 Sep 2025 14:39:45 +0900 Subject: [PATCH 18/20] refactor: Rename `:byte` to `:bytesize` * 05.wc/lib/wc_methods.rb (#displayed_output_format) <:bytesize>: Rename symbol. * 05.wc/lib/word_count.rb (WordCount::Pathname#word_count) (WordCount::String#word_count_per_type) <:bytesize>: Rename symbol. --- 05.wc/lib/wc_methods.rb | 2 +- 05.wc/lib/word_count.rb | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/05.wc/lib/wc_methods.rb b/05.wc/lib/wc_methods.rb index 3ee5412f45..d4ef4fa225 100644 --- a/05.wc/lib/wc_methods.rb +++ b/05.wc/lib/wc_methods.rb @@ -49,7 +49,7 @@ def displayed_output_format(displayed_items, wc_paths) { newline: "%#{digit}d", word: "%#{digit}d", - byte: "%#{digit}d", + bytesize: "%#{digit}d", path: '%s' }.values_at(*displayed_items).join(' ') end diff --git a/05.wc/lib/word_count.rb b/05.wc/lib/word_count.rb index 83626877c7..b19bcfd9f2 100644 --- a/05.wc/lib/word_count.rb +++ b/05.wc/lib/word_count.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module WordCount - TYPES = %i[newline word byte].freeze + TYPES = %i[newline word bytesize].freeze def extract_types(types) TYPES & types @@ -70,13 +70,13 @@ def word_count_result(word_count_types = WordCount::TYPES) private def word_count(word_count_types) - return open { _1.set_encoding('ASCII-8BIT').word_count(word_count_types) } unless file? && word_count_types.include?(:byte) + return open { _1.set_encoding('ASCII-8BIT').word_count(word_count_types) } unless file? && word_count_types.include?(:bytesize) - return { byte: size } if word_count_types == %i[byte] + return { bytesize: size } if word_count_types == %i[bytesize] - counts = open { _1.set_encoding('ASCII-8BIT').word_count(word_count_types - %i[byte]) } + counts = open { _1.set_encoding('ASCII-8BIT').word_count(word_count_types - %i[bytesize]) } - { **counts, byte: size } + { **counts, bytesize: size } end end @@ -119,7 +119,7 @@ def word_count_per_type(word_count_type) split { num += 1 if _1.match?(/[[:graph:]]/) } num - when :byte + when :bytesize bytesize else raise(ArgumentError, "word_count_type: allow only #{WordCount::TYPES.map(&:inspect).join(', ')}") From d416cdab457a433a4d3594bbc5bdf236b5b550a2 Mon Sep 17 00:00:00 2001 From: Keisuke Kurosawa Date: Thu, 11 Sep 2025 14:50:51 +0900 Subject: [PATCH 19/20] refactor: Replace case statement with dedicated each count methods * 05.wc/lib/word_count.rb (WordCount::String#word_count): Integrate `WordCount::String#word_count_per_type`. (WordCount::String#word_count_per_type): Remove method. (WordCount::String#newline, WordCount::String#word): New method. --- 05.wc/lib/word_count.rb | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/05.wc/lib/word_count.rb b/05.wc/lib/word_count.rb index b19bcfd9f2..0d4dc99534 100644 --- a/05.wc/lib/word_count.rb +++ b/05.wc/lib/word_count.rb @@ -104,26 +104,28 @@ def each_buffer(limit = BUFFER_SIZE) module WordCount::String def word_count(word_count_types = WordCount::TYPES) - word_count_types.to_h { [_1, word_count_per_type(_1)] } + word_count_types.to_h do |type| + case type + when *WordCount::TYPES + [type, __send__(type)] + else + raise ArgumentError, "word_count_type: allow only #{WordCount::TYPES.map(&:inspect).join(', ')}" + end + end end private - def word_count_per_type(word_count_type) - case word_count_type - when :newline - count("\n") - when :word - num = 0 + def newline + count("\n") + end + + def word + num = 0 - split { num += 1 if _1.match?(/[[:graph:]]/) } + split { num += 1 if _1.match?(/[[:graph:]]/) } - num - when :bytesize - bytesize - else - raise(ArgumentError, "word_count_type: allow only #{WordCount::TYPES.map(&:inspect).join(', ')}") - end + num end end From 5e48a33cf1a8a345866b871ccb33aa8d75187864 Mon Sep 17 00:00:00 2001 From: Keisuke Kurosawa Date: Thu, 11 Sep 2025 15:06:52 +0900 Subject: [PATCH 20/20] refactor: Extract format string generation into dedicated method * 05.wc/lib/wc_methods.rb (#displayed_output_format): Use `#output_format_string`. (#output_format_string): New method. --- 05.wc/lib/wc_methods.rb | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/05.wc/lib/wc_methods.rb b/05.wc/lib/wc_methods.rb index d4ef4fa225..b506ddbf6d 100644 --- a/05.wc/lib/wc_methods.rb +++ b/05.wc/lib/wc_methods.rb @@ -47,10 +47,7 @@ def displayed_output_format(displayed_items, wc_paths) digit = one_type_one_operand ? 1 : adjust_digit(wc_paths) - { newline: "%#{digit}d", - word: "%#{digit}d", - bytesize: "%#{digit}d", - path: '%s' }.values_at(*displayed_items).join(' ') + displayed_items.map { output_format_string(_1, digit) }.join(' ') end def adjust_digit(wc_paths) @@ -61,6 +58,17 @@ def adjust_digit(wc_paths) [default_digit, total_bytes_digit].max end +def output_format_string(displayed_item, digit) + case displayed_item + when *WordCount::TYPES + "%<#{displayed_item}>#{digit}d" + when :path + '%s' + else + raise ArgumentError, "displayed_item: allow only #{[*WordCount::TYPES, :path].map(&:inspect).join(', ')}" + end +end + def print_word_count_result(output_format, result) warn "wc: #{result[:path]}: #{result[:message]}" if result[:message]