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..fb0878b942 100755 --- a/05.wc/wc.rb +++ b/05.wc/wc.rb @@ -1,8 +1,102 @@ #!/usr/bin/env ruby # frozen_string_literal: true -require_relative './lib/wc_methods' +require 'optparse' -errno = main(ARGV) +OPTION_STRING = 'lwc' +DEFAULT_OPTION_CHARS = OPTION_STRING.chars.freeze -exit(errno) +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) } + + print_counts(counts, option_chars) + + 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 } + + # `wc [file ...]` == `wc -lwc [file ...]` + option_chars.empty? ? DEFAULT_OPTION_CHARS : option_chars +end + +def count_newline_word_bytesize(path = '') + buf = path.empty? ? $stdin.set_encoding('ASCII-8BIT').read : File.open(path, encoding: 'ASCII-8BIT', &:read) + count = {} + + count['l'] = count_newline(buf) + count['w'] = count_word(buf) + count['c'] = count_bytesize(buf) + + { path:, count: } +end + +def count_newline(buf) + buf.count("\n") +end + +def count_word(buf) + num = 0 + buf.split { num += 1 if _1.match?(/[[:graph:]]/) } + num +end + +def count_bytesize(buf) + buf.bytesize +end + +def print_counts(counts, option_chars) + digit = calc_digit(counts, option_chars) + need_with_path = !stdin?(counts) + + 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) } + + counts << result[:path] if need_with_path + + puts counts.join(' ') + end +end + +def calc_digit(counts, option_chars) + need_padding = option_chars.size >= 2 || counts.size >= 2 + + return 1 unless need_padding + return 7 if stdin?(counts) + + counts.sum { _1[:count]['c'] }.to_s.size +end + +def stdin?(counts) + counts.size == 1 && counts.first[:path].empty? +end + +def total_counts(counts) + init_value_for_total = DEFAULT_OPTION_CHARS.to_h { [_1, 0] } + + 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 + + { path: 'total', count: count_total } +end + +if __FILE__ == $PROGRAM_NAME + errno = main + + exit(errno) +end