From cb2cdc9d9aa9b5278b8264fe15cf9ee30ea38483 Mon Sep 17 00:00:00 2001 From: Keisuke Kurosawa Date: Sat, 20 Sep 2025 12:20:59 +0900 Subject: [PATCH 1/8] fix: Merge into one file Define methods directly without defining separate classes or modules. * 05.wc/lib/wc_methods.rb: Remove file. * 05.wc/lib/word_count.rb: Remove file. * 05.wc/wc.rb: Merge into this file. --- 05.wc/lib/wc_methods.rb | 76 ------------------ 05.wc/lib/word_count.rb | 138 -------------------------------- 05.wc/wc.rb | 170 +++++++++++++++++++++++++++++++++++++++- 3 files changed, 167 insertions(+), 217 deletions(-) delete mode 100644 05.wc/lib/wc_methods.rb delete mode 100644 05.wc/lib/word_count.rb diff --git a/05.wc/lib/wc_methods.rb b/05.wc/lib/wc_methods.rb deleted file mode 100644 index b506ddbf6d..0000000000 --- a/05.wc/lib/wc_methods.rb +++ /dev/null @@ -1,76 +0,0 @@ -# frozen_string_literal: true - -require 'optparse' -require_relative './word_count' - -OPTION_STRING = 'lwc' - -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? ? [WordCount::Pathname.new] : args.map { WordCount::Pathname.new(_1) } - - word_count_types = WordCount.extract_types(displayed_items) - - output_format = displayed_output_format(displayed_items, wc_paths) - - results = - wc_paths.map { _1.word_count_result(word_count_types) } - .each { print_word_count_result(output_format, _1) } - - if wc_paths.size >= 2 - count_total = - word_count_types.to_h { [_1, 0] } - .merge!(*results.filter_map { _1[:count] }) { |_, total, count| total + count } - - print_word_count_result(output_format, { path: 'total', count: count_total, message: nil }) - end - - results.any? { _1[:message] } ? 1 : 0 -end - -def parse_args(args) - parsed_options = OptionParser.new.getopts(args, OPTION_STRING).transform_keys(OPTION_NAME_TO_WORD_COUNT_TYPE) - - # `wc [file ...]` == `wc -lwc [file ...]` - parsed_options.transform_values! { |_| true } unless parsed_options.value?(true) - - parsed_options[:path] = !args.empty? - - parsed_options.select { |_, val| val }.keys -end - -def displayed_output_format(displayed_items, wc_paths) - 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) - - displayed_items.map { output_format_string(_1, digit) }.join(' ') -end - -def adjust_digit(wc_paths) - default_digit = wc_paths.any?(&:exist_non_regular_file?) ? 7 : 1 - - total_bytes_digit = wc_paths.sum(&:regular_file_size).to_s.size - - [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] - - puts format(output_format, **result[:count], path: result[:path]) unless result[:count].nil? -end diff --git a/05.wc/lib/word_count.rb b/05.wc/lib/word_count.rb deleted file mode 100644 index 0d4dc99534..0000000000 --- a/05.wc/lib/word_count.rb +++ /dev/null @@ -1,138 +0,0 @@ -# frozen_string_literal: true - -module WordCount - TYPES = %i[newline word bytesize].freeze - - def extract_types(types) - TYPES & types - end - - 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 = nil) - @path = path.to_s - end - - def to_path - return '-' if @path.empty? - - @path - end - - def stdin? - to_path == '-' - end - - def inspect - "#<#{self.class}:#{to_path}>" - end - - def open(mode = 'r', perm = 0o0666, &block) - return block&.call($stdin) || $stdin if stdin? - - File.open(to_path, mode, perm, &block) - end - - def exist? - 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, to_path) } - end - - def regular_file_size - file? ? size : 0 - end - - def exist_non_regular_file? - exist? && !file? - end - - def word_count_result(word_count_types = WordCount::TYPES) - path = @path.empty? ? 'standard input' : @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 } - rescue Errno::EPERM => e - { path:, count: nil, message: e.message.partition(' @ ').first } - 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?(:bytesize) - - return { bytesize: size } if word_count_types == %i[bytesize] - - counts = open { _1.set_encoding('ASCII-8BIT').word_count(word_count_types - %i[bytesize]) } - - { **counts, bytesize: size } - end -end - -module WordCount::IO - BUFFER_SIZE = 16 * 1024 - - private_constant :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 - - def each_buffer(limit = BUFFER_SIZE) - return to_enum(__callee__, limit) unless block_given? - - loop do - yield readpartial(limit) - rescue EOFError - break - end - - self - end -end - -module WordCount::String - def word_count(word_count_types = WordCount::TYPES) - 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 newline - count("\n") - end - - def word - num = 0 - - split { num += 1 if _1.match?(/[[:graph:]]/) } - - num - end -end - -class IO - include WordCount::IO -end - -class String - include WordCount::String -end diff --git a/05.wc/wc.rb b/05.wc/wc.rb index e272b0508a..3051f5bd93 100755 --- a/05.wc/wc.rb +++ b/05.wc/wc.rb @@ -1,8 +1,172 @@ #!/usr/bin/env ruby # frozen_string_literal: true -require_relative './lib/wc_methods' +require 'optparse' -errno = main(ARGV) +OPTION_STRING = 'lwc' +WORD_COUNT_TYPES = %i[newline word bytesize].freeze -exit(errno) +OPTION_NAME_TO_WORD_COUNT_TYPE = OPTION_STRING.chars.zip(WORD_COUNT_TYPES).to_h.freeze + +def main(args) + enabled_options = parse_args(args) + + paths = args.empty? ? [''] : args + + word_count_types = extract_word_count_types(enabled_options) + + output_format = displayed_output_format(enabled_options, paths) + + results = + paths.map { word_count_result(_1, word_count_types) } + .each { print_word_count_result(output_format, _1) } + + if paths.size >= 2 + count_total = word_count_sum(results.filter_map { _1[:count] }, word_count_types) + + print_word_count_result(output_format, { path: 'total', count: count_total, message: nil }) + end + + results.any? { _1[:message] } ? 1 : 0 +end + +def parse_args(args) + parsed_options = OptionParser.new.getopts(args, OPTION_STRING).transform_keys(OPTION_NAME_TO_WORD_COUNT_TYPE) + + # `wc [file ...]` == `wc -lwc [file ...]` + parsed_options.transform_values! { |_| true } unless parsed_options.value?(true) + + parsed_options[:path] = !args.empty? + + parsed_options.select { |_, val| val }.keys +end + +def extract_word_count_types(types) + WORD_COUNT_TYPES & types +end + +def displayed_output_format(enabled_options, paths) + one_type_one_operand = extract_word_count_types(enabled_options).size == 1 && paths.size <= 1 + + digit = one_type_one_operand ? 1 : adjust_digit(paths) + + enabled_options.map { output_format_string(_1, digit) }.join(' ') +end + +def adjust_digit(paths) + default_digit = paths.any? { exist_non_regular_file?(_1) } ? 7 : 1 + + total_bytes_digit = paths.sum { regular_file_size(_1) }.to_s.size + + [default_digit, total_bytes_digit].max +end + +def stdin?(path) + path == '-' || path.empty? +end + +def exist_non_regular_file?(path) + exist?(path) && !file?(path) +end + +def exist?(path) + stdin?(path) || FileTest.exist?(path) +end + +def regular_file_size(path) + file?(path) ? size(path) : 0 +end + +def file?(path) + stdin?(path) ? $stdin.stat.file? : FileTest.file?(path) +end + +def size(path) + stdin?(path) ? $stdin.stat.size : FileTest.size(path) +end + +def output_format_string(enabled_option, digit) + case enabled_option + when *WORD_COUNT_TYPES + "%<#{enabled_option}>#{digit}d" + when :path + "%<#{enabled_option}>s" + else + raise ArgumentError, "enabled_option: allow only #{[*WORD_COUNT_TYPES, :path].map(&:inspect).join(', ')}" + end +end + +def word_count_result(path, word_count_types = WORD_COUNT_TYPES) + display_path = path.empty? ? 'standard input' : path + + return { path: display_path, count: nil, message: exist?(path) ? 'Permission denied' : 'No such file or directory' } unless readable?(path) + + return { path: display_path, count: word_count_types.to_h { [_1, 0] }, message: 'Is a directory' } if directory?(path) + + { path: display_path, count: word_count(path, word_count_types), message: nil } +rescue Errno::EPERM => e + { path: display_path, count: nil, message: e.message.partition(' @ ').first } +end + +def readable?(path) + stdin?(path) ? $stdin.stat.readable? : FileTest.readable?(path) +end + +def directory?(path) + stdin?(path) ? $stdin.stat.directory? : FileTest.directory?(path) +end + +def word_count(path, word_count_types = WORD_COUNT_TYPES) + return { bytesize: size(path) } if file?(path) && word_count_types == %i[bytesize] + + file_size_is_available = file?(path) && word_count_types.include?(:bytesize) + + types = file_size_is_available ? word_count_types - %i[bytesize] : word_count_types + + counts_each_line = File.open(stdin?(path) ? 0 : path) { |io| io.set_encoding('ASCII-8BIT').map { word_count_for_string(_1, types) } } + + count = word_count_sum(counts_each_line, types) + + file_size_is_available ? { **count, bytesize: size(path) } : count +end + +def word_count_for_string(str, word_count_types = WORD_COUNT_TYPES) + word_count_types.to_h do |word_count_type| + case word_count_type + when *WORD_COUNT_TYPES + [word_count_type, word_count_for_string_per_type(str, word_count_type)] + else + raise ArgumentError, "word_count_type: allow only #{WORD_COUNT_TYPES.map(&:inspect).join(', ')}" + end + end +end + +def word_count_for_string_per_type(str, word_count_type) + case word_count_type + when :newline + str.count("\n") + when :word + num = 0 + str.split { num += 1 if _1.match?(/[[:graph:]]/) } + num + when :bytesize + str.bytesize + end +end + +def word_count_sum(counts, word_count_types = WORD_COUNT_TYPES) + word_count_types.to_h { [_1, 0] } + .merge!(*counts) { |_, total, count| total + count } +end + +def print_word_count_result(output_format, result) + warn "wc: #{result[:path]}: #{result[:message]}" if result[:message] + + puts format(output_format, **result[:count], path: result[:path]) unless result[:count].nil? +end + +if __FILE__ == $PROGRAM_NAME + errno = main(ARGV) + + exit(errno) +end From 85c5b631778a3e8e8aa43fbfc78416fdd0d18c39 Mon Sep 17 00:00:00 2001 From: Keisuke Kurosawa Date: Thu, 9 Oct 2025 15:10:35 +0900 Subject: [PATCH 2/8] refactor: word_count MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 05.wc/wc.rb (#word_count): 上から順番に読めるようになった。 (#word_count_for_string, #word_count_for_string_per_type): Remove method. (#count_newline, #count_word, #count_bytesize): New method. --- 05.wc/wc.rb | 41 ++++++++++++++++------------------------- 1 file changed, 16 insertions(+), 25 deletions(-) diff --git a/05.wc/wc.rb b/05.wc/wc.rb index 3051f5bd93..7089e5217b 100755 --- a/05.wc/wc.rb +++ b/05.wc/wc.rb @@ -117,43 +117,34 @@ def directory?(path) end def word_count(path, word_count_types = WORD_COUNT_TYPES) - return { bytesize: size(path) } if file?(path) && word_count_types == %i[bytesize] + arg_path = stdin?(path) ? 0 : path - file_size_is_available = file?(path) && word_count_types.include?(:bytesize) + lines = File.open(arg_path, encoding: 'ASCII-8BIT', &:readlines) + word_count = {} - types = file_size_is_available ? word_count_types - %i[bytesize] : word_count_types + word_count[:newline] = count_newline(lines) if word_count_types.include?(:newline) + word_count[:word] = count_word(lines) if word_count_types.include?(:word) + word_count[:bytesize] = (file?(path) ? size(path) : count_bytesize(lines)) if word_count_types.include?(:bytesize) - counts_each_line = File.open(stdin?(path) ? 0 : path) { |io| io.set_encoding('ASCII-8BIT').map { word_count_for_string(_1, types) } } - - count = word_count_sum(counts_each_line, types) - - file_size_is_available ? { **count, bytesize: size(path) } : count + word_count end -def word_count_for_string(str, word_count_types = WORD_COUNT_TYPES) - word_count_types.to_h do |word_count_type| - case word_count_type - when *WORD_COUNT_TYPES - [word_count_type, word_count_for_string_per_type(str, word_count_type)] - else - raise ArgumentError, "word_count_type: allow only #{WORD_COUNT_TYPES.map(&:inspect).join(', ')}" - end - end +def count_newline(lines) + lines.sum { |line| line.count("\n") } end -def word_count_for_string_per_type(str, word_count_type) - case word_count_type - when :newline - str.count("\n") - when :word +def count_word(lines) + lines.sum do |line| num = 0 - str.split { num += 1 if _1.match?(/[[:graph:]]/) } + line.split { num += 1 if _1.match?(/[[:graph:]]/) } num - when :bytesize - str.bytesize end end +def count_bytesize(lines) + lines.sum(&:bytesize) +end + def word_count_sum(counts, word_count_types = WORD_COUNT_TYPES) word_count_types.to_h { [_1, 0] } .merge!(*counts) { |_, total, count| total + count } From 3fb9b6a32820767cb434c32fa4ae702ca8c11cd4 Mon Sep 17 00:00:00 2001 From: Keisuke Kurosawa Date: Sat, 18 Oct 2025 14:59:10 +0900 Subject: [PATCH 3/8] =?UTF-8?q?fix:=20=E5=BC=95=E6=95=B0=E8=A7=A3=E6=9E=90?= =?UTF-8?q?=E3=81=AE=E7=B5=90=E6=9E=9C=E3=82=92=E3=81=AA=E3=82=8B=E3=81=B9?= =?UTF-8?q?=E3=81=8F=E3=81=84=E3=81=98=E3=82=89=E3=81=AA=E3=81=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 05.wc/wc.rb | 111 +++++++++++++++++++--------------------------------- 1 file changed, 41 insertions(+), 70 deletions(-) diff --git a/05.wc/wc.rb b/05.wc/wc.rb index 7089e5217b..fc02b09fea 100755 --- a/05.wc/wc.rb +++ b/05.wc/wc.rb @@ -4,65 +4,61 @@ require 'optparse' OPTION_STRING = 'lwc' -WORD_COUNT_TYPES = %i[newline word bytesize].freeze - -OPTION_NAME_TO_WORD_COUNT_TYPE = OPTION_STRING.chars.zip(WORD_COUNT_TYPES).to_h.freeze +DEFAULT_OPTION_CHARS = OPTION_STRING.chars.freeze def main(args) - enabled_options = parse_args(args) - - paths = args.empty? ? [''] : args + options = OptionParser.getopts(args, OPTION_STRING) - word_count_types = extract_word_count_types(enabled_options) + option_chars = extract_option_chars(options) - output_format = displayed_output_format(enabled_options, paths) + format_string = generate_format_string(args, option_chars) + results = word_count_results(args, option_chars) - results = - paths.map { word_count_result(_1, word_count_types) } - .each { print_word_count_result(output_format, _1) } + results.each { print_word_count_result(format_string, _1) } - if paths.size >= 2 - count_total = word_count_sum(results.filter_map { _1[:count] }, word_count_types) + if args.size >= 2 + count_total = + option_chars.to_h { [_1, 0] } + .merge!(*results.filter_map { _1[:count] }) { |_, total, count| total + count } - print_word_count_result(output_format, { path: 'total', count: count_total, message: nil }) + print_word_count_result(format_string, { path: 'total', count: count_total }) end - results.any? { _1[:message] } ? 1 : 0 + 0 end -def parse_args(args) - parsed_options = OptionParser.new.getopts(args, OPTION_STRING).transform_keys(OPTION_NAME_TO_WORD_COUNT_TYPE) +def extract_option_chars(options) + option_chars = options.filter_map { |opt, bool| opt if bool } # `wc [file ...]` == `wc -lwc [file ...]` - parsed_options.transform_values! { |_| true } unless parsed_options.value?(true) + option_chars.empty? ? DEFAULT_OPTION_CHARS : option_chars +end - parsed_options[:path] = !args.empty? +def generate_format_string(parsed_args, option_chars = DEFAULT_OPTION_CHARS) + enabled_option_count = option_chars.size - parsed_options.select { |_, val| val }.keys -end + need_padding = enabled_option_count >= 2 || parsed_args.size >= 2 -def extract_word_count_types(types) - WORD_COUNT_TYPES & types -end + digit = need_padding ? calc_digit(parsed_args) : 1 -def displayed_output_format(enabled_options, paths) - one_type_one_operand = extract_word_count_types(enabled_options).size == 1 && paths.size <= 1 + format_string = Array.new(enabled_option_count, "%#{digit}d") - digit = one_type_one_operand ? 1 : adjust_digit(paths) + format_string << '%s' unless parsed_args.empty? - enabled_options.map { output_format_string(_1, digit) }.join(' ') + format_string.join(' ') end -def adjust_digit(paths) - default_digit = paths.any? { exist_non_regular_file?(_1) } ? 7 : 1 +def calc_digit(parsed_args) + paths = parsed_args.empty? ? ['-'] : parsed_args + default_digit = paths.any? { exist_non_regular_file?(_1) } ? 7 : 1 total_bytes_digit = paths.sum { regular_file_size(_1) }.to_s.size [default_digit, total_bytes_digit].max end def stdin?(path) - path == '-' || path.empty? + path == '-' end def exist_non_regular_file?(path) @@ -85,46 +81,28 @@ def size(path) stdin?(path) ? $stdin.stat.size : FileTest.size(path) end -def output_format_string(enabled_option, digit) - case enabled_option - when *WORD_COUNT_TYPES - "%<#{enabled_option}>#{digit}d" - when :path - "%<#{enabled_option}>s" - else - raise ArgumentError, "enabled_option: allow only #{[*WORD_COUNT_TYPES, :path].map(&:inspect).join(', ')}" - end -end - -def word_count_result(path, word_count_types = WORD_COUNT_TYPES) - display_path = path.empty? ? 'standard input' : path - - return { path: display_path, count: nil, message: exist?(path) ? 'Permission denied' : 'No such file or directory' } unless readable?(path) +def word_count_results(parsed_args, option_chars = DEFAULT_OPTION_CHARS) + paths = parsed_args.empty? ? ['-'] : parsed_args - return { path: display_path, count: word_count_types.to_h { [_1, 0] }, message: 'Is a directory' } if directory?(path) + paths.map do |path| + results = {} - { path: display_path, count: word_count(path, word_count_types), message: nil } -rescue Errno::EPERM => e - { path: display_path, count: nil, message: e.message.partition(' @ ').first } -end - -def readable?(path) - stdin?(path) ? $stdin.stat.readable? : FileTest.readable?(path) -end + results[:path] = path + results[:count] = word_count(path, option_chars) -def directory?(path) - stdin?(path) ? $stdin.stat.directory? : FileTest.directory?(path) + results + end end -def word_count(path, word_count_types = WORD_COUNT_TYPES) +def word_count(path, option_chars = DEFAULT_OPTION_CHARS) arg_path = stdin?(path) ? 0 : path lines = File.open(arg_path, encoding: 'ASCII-8BIT', &:readlines) word_count = {} - word_count[:newline] = count_newline(lines) if word_count_types.include?(:newline) - word_count[:word] = count_word(lines) if word_count_types.include?(:word) - word_count[:bytesize] = (file?(path) ? size(path) : count_bytesize(lines)) if word_count_types.include?(:bytesize) + word_count['l'] = count_newline(lines) if option_chars.include?('l') + word_count['w'] = count_word(lines) if option_chars.include?('w') + word_count['c'] = (file?(path) ? size(path) : count_bytesize(lines)) if option_chars.include?('c') word_count end @@ -145,15 +123,8 @@ def count_bytesize(lines) lines.sum(&:bytesize) end -def word_count_sum(counts, word_count_types = WORD_COUNT_TYPES) - word_count_types.to_h { [_1, 0] } - .merge!(*counts) { |_, total, count| total + count } -end - -def print_word_count_result(output_format, result) - warn "wc: #{result[:path]}: #{result[:message]}" if result[:message] - - puts format(output_format, **result[:count], path: result[:path]) unless result[:count].nil? +def print_word_count_result(format_string, result) + puts format(format_string, *result[:count].values, result[:path]) unless result[:count].nil? end if __FILE__ == $PROGRAM_NAME From 957298fc9c103728b285abccfece990be2037e99 Mon Sep 17 00:00:00 2001 From: Keisuke Kurosawa Date: Tue, 28 Oct 2025 15:09:34 +0900 Subject: [PATCH 4/8] refactor: Make some code easier to understand * 05.wc/wc.rb (#main) : Separate the initial value and the total processing. (#word_count_results): Create `Hash` simply. --- 05.wc/wc.rb | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/05.wc/wc.rb b/05.wc/wc.rb index fc02b09fea..1afc7231c9 100755 --- a/05.wc/wc.rb +++ b/05.wc/wc.rb @@ -17,9 +17,13 @@ def main(args) results.each { print_word_count_result(format_string, _1) } if args.size >= 2 - count_total = - option_chars.to_h { [_1, 0] } - .merge!(*results.filter_map { _1[:count] }) { |_, total, count| total + count } + init_value_for_total = option_chars.to_h { [_1, 0] } + + count_total = results.each_with_object(init_value_for_total) do |result, total| + option_chars.each do |option| + total[option] += result[:count][option] + end + end print_word_count_result(format_string, { path: 'total', count: count_total }) end @@ -85,12 +89,10 @@ def word_count_results(parsed_args, option_chars = DEFAULT_OPTION_CHARS) paths = parsed_args.empty? ? ['-'] : parsed_args paths.map do |path| - results = {} - - results[:path] = path - results[:count] = word_count(path, option_chars) - - results + { + path:, + count: word_count(path, option_chars) + } end end From b024333d5feee6faea8d6bf4f07047b9f0ede4f8 Mon Sep 17 00:00:00 2001 From: Keisuke Kurosawa Date: Sat, 1 Nov 2025 13:52:03 +0900 Subject: [PATCH 5/8] refactor: Use ARGV * 05.wc/wc.rb (#main): No arguments are required as `ARGV` is used within the method. (#parse_commandline_options): New method. (#generate_format_string, #calc_digit, #word_count_results): Rename argument. --- 05.wc/wc.rb | 34 ++++++++++++++++++++-------------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/05.wc/wc.rb b/05.wc/wc.rb index 1afc7231c9..72ead35f0b 100755 --- a/05.wc/wc.rb +++ b/05.wc/wc.rb @@ -6,17 +6,17 @@ OPTION_STRING = 'lwc' DEFAULT_OPTION_CHARS = OPTION_STRING.chars.freeze -def main(args) - options = OptionParser.getopts(args, OPTION_STRING) +def main + options, paths = parse_commandline_options option_chars = extract_option_chars(options) - format_string = generate_format_string(args, option_chars) - results = word_count_results(args, option_chars) + format_string = generate_format_string(paths, option_chars) + results = word_count_results(paths, option_chars) results.each { print_word_count_result(format_string, _1) } - if args.size >= 2 + if paths.size >= 2 init_value_for_total = option_chars.to_h { [_1, 0] } count_total = results.each_with_object(init_value_for_total) do |result, total| @@ -31,6 +31,12 @@ def main(args) 0 end +def parse_commandline_options + options = OptionParser.getopts(ARGV, OPTION_STRING) + + [options, ARGV] +end + def extract_option_chars(options) option_chars = options.filter_map { |opt, bool| opt if bool } @@ -38,22 +44,22 @@ def extract_option_chars(options) option_chars.empty? ? DEFAULT_OPTION_CHARS : option_chars end -def generate_format_string(parsed_args, option_chars = DEFAULT_OPTION_CHARS) +def generate_format_string(paths, option_chars = DEFAULT_OPTION_CHARS) enabled_option_count = option_chars.size - need_padding = enabled_option_count >= 2 || parsed_args.size >= 2 + need_padding = enabled_option_count >= 2 || paths.size >= 2 - digit = need_padding ? calc_digit(parsed_args) : 1 + digit = need_padding ? calc_digit(paths) : 1 format_string = Array.new(enabled_option_count, "%#{digit}d") - format_string << '%s' unless parsed_args.empty? + format_string << '%s' unless paths.empty? format_string.join(' ') end -def calc_digit(parsed_args) - paths = parsed_args.empty? ? ['-'] : parsed_args +def calc_digit(paths) + paths = paths.empty? ? ['-'] : paths default_digit = paths.any? { exist_non_regular_file?(_1) } ? 7 : 1 total_bytes_digit = paths.sum { regular_file_size(_1) }.to_s.size @@ -85,8 +91,8 @@ def size(path) stdin?(path) ? $stdin.stat.size : FileTest.size(path) end -def word_count_results(parsed_args, option_chars = DEFAULT_OPTION_CHARS) - paths = parsed_args.empty? ? ['-'] : parsed_args +def word_count_results(paths, option_chars = DEFAULT_OPTION_CHARS) + paths = paths.empty? ? ['-'] : paths paths.map do |path| { @@ -130,7 +136,7 @@ def print_word_count_result(format_string, result) end if __FILE__ == $PROGRAM_NAME - errno = main(ARGV) + errno = main exit(errno) end From e9ade88491dfaff86c7e0b8f65e8ffa01f2cbfd3 Mon Sep 17 00:00:00 2001 From: Keisuke Kurosawa Date: Sat, 1 Nov 2025 16:32:25 +0900 Subject: [PATCH 6/8] refactor: Use `String` for counting * 05.wc/wc.rb (#word_count) : Use `String` instead of `[String]`. (#count_newline, #count_word, #count_bytesize): Stop using `Array#sum`. --- 05.wc/wc.rb | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/05.wc/wc.rb b/05.wc/wc.rb index 72ead35f0b..9aae6c498d 100755 --- a/05.wc/wc.rb +++ b/05.wc/wc.rb @@ -105,30 +105,28 @@ def word_count_results(paths, option_chars = DEFAULT_OPTION_CHARS) def word_count(path, option_chars = DEFAULT_OPTION_CHARS) arg_path = stdin?(path) ? 0 : path - lines = File.open(arg_path, encoding: 'ASCII-8BIT', &:readlines) + buf = File.open(arg_path, encoding: 'ASCII-8BIT', &:read) word_count = {} - word_count['l'] = count_newline(lines) if option_chars.include?('l') - word_count['w'] = count_word(lines) if option_chars.include?('w') - word_count['c'] = (file?(path) ? size(path) : count_bytesize(lines)) if option_chars.include?('c') + word_count['l'] = count_newline(buf) if option_chars.include?('l') + word_count['w'] = count_word(buf) if option_chars.include?('w') + word_count['c'] = (file?(path) ? size(path) : count_bytesize(buf)) if option_chars.include?('c') word_count end -def count_newline(lines) - lines.sum { |line| line.count("\n") } +def count_newline(buf) + buf.count("\n") end -def count_word(lines) - lines.sum do |line| - num = 0 - line.split { num += 1 if _1.match?(/[[:graph:]]/) } - num - end +def count_word(buf) + num = 0 + buf.split { num += 1 if _1.match?(/[[:graph:]]/) } + num end -def count_bytesize(lines) - lines.sum(&:bytesize) +def count_bytesize(buf) + buf.bytesize end def print_word_count_result(format_string, result) From e1ef5fe8a877f564bc37b8b18d15bd8126c057dc Mon Sep 17 00:00:00 2001 From: Keisuke Kurosawa Date: Sat, 1 Nov 2025 15:34:55 +0900 Subject: [PATCH 7/8] fix: Treat '-' as filename * 05.wc/wc.rb (#calc_digit, #word_count_results): Stop treating '-' as standard input. : Do not rewrite this argument. (#stdin?, #exist_non_regular_file?, #exist?, #regular_file_size) (#file?, #size): Remove unused method. (#word_count): Change the return value to nested `Hash`. --- 05.wc/wc.rb | 48 +++++++----------------------------------------- 1 file changed, 7 insertions(+), 41 deletions(-) diff --git a/05.wc/wc.rb b/05.wc/wc.rb index 9aae6c498d..3516b4a8ca 100755 --- a/05.wc/wc.rb +++ b/05.wc/wc.rb @@ -59,60 +59,26 @@ def generate_format_string(paths, option_chars = DEFAULT_OPTION_CHARS) end def calc_digit(paths) - paths = paths.empty? ? ['-'] : paths + return 7 if paths.empty? - default_digit = paths.any? { exist_non_regular_file?(_1) } ? 7 : 1 - total_bytes_digit = paths.sum { regular_file_size(_1) }.to_s.size - - [default_digit, total_bytes_digit].max -end - -def stdin?(path) - path == '-' -end - -def exist_non_regular_file?(path) - exist?(path) && !file?(path) -end - -def exist?(path) - stdin?(path) || FileTest.exist?(path) -end - -def regular_file_size(path) - file?(path) ? size(path) : 0 -end - -def file?(path) - stdin?(path) ? $stdin.stat.file? : FileTest.file?(path) -end - -def size(path) - stdin?(path) ? $stdin.stat.size : FileTest.size(path) + paths.sum { FileTest.size(_1) }.to_s.size end def word_count_results(paths, option_chars = DEFAULT_OPTION_CHARS) - paths = paths.empty? ? ['-'] : paths + return [word_count('', option_chars)] if paths.empty? - paths.map do |path| - { - path:, - count: word_count(path, option_chars) - } - end + paths.map { word_count(_1, option_chars) } end def word_count(path, option_chars = DEFAULT_OPTION_CHARS) - arg_path = stdin?(path) ? 0 : path - - buf = File.open(arg_path, encoding: 'ASCII-8BIT', &:read) + buf = path.empty? ? $stdin.set_encoding('ASCII-8BIT').read : File.open(path, encoding: 'ASCII-8BIT', &:read) word_count = {} word_count['l'] = count_newline(buf) if option_chars.include?('l') word_count['w'] = count_word(buf) if option_chars.include?('w') - word_count['c'] = (file?(path) ? size(path) : count_bytesize(buf)) if option_chars.include?('c') + word_count['c'] = count_bytesize(buf) if option_chars.include?('c') - word_count + { path:, count: word_count } end def count_newline(buf) From 1dee3a86d675fb5dd1d027c638d3f106b7bce559 Mon Sep 17 00:00:00 2001 From: Keisuke Kurosawa Date: Sun, 2 Nov 2025 19:40:38 +0900 Subject: [PATCH 8/8] fix: Do not use `Kernel.#format` to format output Use `Hash#values_at` or `String#rjust` instead of. * 05.wc/wc.rb (#generate_format_string, #word_count_results) (#print_word_count_result): Remove method. (#main): Extract the process to output the count and its total. : New variable. (#count_newline_word_byte): Similar to `#word_count`, but counts all items (newline, word, and byte). (#print_counts, #stdin?, #total_counts): New method. (#calc_digit): Change arguments and processing for use with `#print_counts`. --- 05.wc/wc.rb | 94 +++++++++++++++++++++++++---------------------------- 1 file changed, 45 insertions(+), 49 deletions(-) diff --git a/05.wc/wc.rb b/05.wc/wc.rb index 3516b4a8ca..fb0878b942 100755 --- a/05.wc/wc.rb +++ b/05.wc/wc.rb @@ -10,23 +10,9 @@ def main options, paths = parse_commandline_options option_chars = extract_option_chars(options) + counts = paths.empty? ? [count_newline_word_bytesize] : paths.map { count_newline_word_bytesize(_1) } - format_string = generate_format_string(paths, option_chars) - results = word_count_results(paths, option_chars) - - results.each { print_word_count_result(format_string, _1) } - - if paths.size >= 2 - init_value_for_total = option_chars.to_h { [_1, 0] } - - count_total = results.each_with_object(init_value_for_total) do |result, total| - option_chars.each do |option| - total[option] += result[:count][option] - end - end - - print_word_count_result(format_string, { path: 'total', count: count_total }) - end + print_counts(counts, option_chars) 0 end @@ -44,59 +30,69 @@ def extract_option_chars(options) option_chars.empty? ? DEFAULT_OPTION_CHARS : option_chars end -def generate_format_string(paths, option_chars = DEFAULT_OPTION_CHARS) - enabled_option_count = option_chars.size +def count_newline_word_bytesize(path = '') + buf = path.empty? ? $stdin.set_encoding('ASCII-8BIT').read : File.open(path, encoding: 'ASCII-8BIT', &:read) + count = {} - need_padding = enabled_option_count >= 2 || paths.size >= 2 + count['l'] = count_newline(buf) + count['w'] = count_word(buf) + count['c'] = count_bytesize(buf) - digit = need_padding ? calc_digit(paths) : 1 + { path:, count: } +end - format_string = Array.new(enabled_option_count, "%#{digit}d") +def count_newline(buf) + buf.count("\n") +end - format_string << '%s' unless paths.empty? +def count_word(buf) + num = 0 + buf.split { num += 1 if _1.match?(/[[:graph:]]/) } + num +end - format_string.join(' ') +def count_bytesize(buf) + buf.bytesize end -def calc_digit(paths) - return 7 if paths.empty? +def print_counts(counts, option_chars) + digit = calc_digit(counts, option_chars) + need_with_path = !stdin?(counts) - paths.sum { FileTest.size(_1) }.to_s.size -end + counts << total_counts(counts) if counts.size >= 2 + + counts.each do |result| + counts = result[:count].values_at(*option_chars).map { _1.to_s.rjust(digit) } -def word_count_results(paths, option_chars = DEFAULT_OPTION_CHARS) - return [word_count('', option_chars)] if paths.empty? + counts << result[:path] if need_with_path - paths.map { word_count(_1, option_chars) } + puts counts.join(' ') + end end -def word_count(path, option_chars = DEFAULT_OPTION_CHARS) - buf = path.empty? ? $stdin.set_encoding('ASCII-8BIT').read : File.open(path, encoding: 'ASCII-8BIT', &:read) - word_count = {} +def calc_digit(counts, option_chars) + need_padding = option_chars.size >= 2 || counts.size >= 2 - word_count['l'] = count_newline(buf) if option_chars.include?('l') - word_count['w'] = count_word(buf) if option_chars.include?('w') - word_count['c'] = count_bytesize(buf) if option_chars.include?('c') + return 1 unless need_padding + return 7 if stdin?(counts) - { path:, count: word_count } + counts.sum { _1[:count]['c'] }.to_s.size end -def count_newline(buf) - buf.count("\n") +def stdin?(counts) + counts.size == 1 && counts.first[:path].empty? end -def count_word(buf) - num = 0 - buf.split { num += 1 if _1.match?(/[[:graph:]]/) } - num -end +def total_counts(counts) + init_value_for_total = DEFAULT_OPTION_CHARS.to_h { [_1, 0] } -def count_bytesize(buf) - buf.bytesize -end + count_total = counts.each_with_object(init_value_for_total) do |result, total| + DEFAULT_OPTION_CHARS.each do |option| + total[option] += result[:count][option] + end + end -def print_word_count_result(format_string, result) - puts format(format_string, *result[:count].values, result[:path]) unless result[:count].nil? + { path: 'total', count: count_total } end if __FILE__ == $PROGRAM_NAME