ポンコツ・キャンプ -文字列の中に含まれる複数の引数を操作するためのクラス
DOS画面からコマンドを入力した際、様々な引数を夫々のメソッド内で検査していく内にコードが煩雑になってしまったので、基本的なチェックを行なうためのクラスを作成してみました。
引数のチェックはいつも煩雑です。
一例を挙げると、linked_fileと言うコマンドには次のバリエーションを持たせています。
linked_file uid
linked_file uid フルファイル名/フルファイル名/...
linked_file uid :delete
次の引数の検査をする必要が出てきました。
引数には、1つの場合と2つの場合がある。
1つ目の引数は整数
UIDは、メールボックスの個々のメール指し示す都合上、サーバー側に存在するのか確認する必要がある。
また、2つ目の引数は、複数のファイル名が記されている。
デリミター'/'で区切られているものの、画面入力のため' /', '/ ', ' / 'などの使用が想定される。
2つ目の引数の位置に:deleteが指定される場合がある。
#partitionを使えば、コマンドと引数の塊りとに簡単に分離可能ですが、引数に含まれる個々の検査の多いこと。
主に次の事が出来るようにしました。
文字列に含まれる引数の個数の提供
引数の数と指定範囲との照合
個々の引数は、Stringから各々引数の特徴に応じて、Integer, Range, Time, Symbolのインスタンスに変換
個々の引数のクラス名を配列で提供
クラス名の配列を想定されているクラス名の配列と照合
このお蔭で、検査項目の1, 2, 6は、解消されました。
4もオプションを設けることで回避することが出来ました。
コード量は、それほど減りませんが精神的な安心感とコードの見通しの良さが得られたと思います。
以下は、commander.rbの各メソッドの冒頭で、arg_hash.rbがこのように処理をしていますと言うサンプルです。
main.rb, commander.rb, arg_hash.rb, gadget.rbを同じディレクトリに入れて、main.rbを起動すればサンプルが動きます。
main.rb
# frozen_string_literal: true
require_relative 'commander'
require_relative 'arg_hash'
####################
### Main routine ###
####################
commander = Commander.new
command_list = [:linked_file, :end]
loop do
print "\nコマンドを入力して下さい。 => "
message = gets.chomp
command, _, parameters = message.partition(' ')
command = command.to_sym
params = ArgHash[parameters, split: 2]
command_list.include?(command) ? commander.method(command).call(params) : (print "\n認識できないコマンドです。")
break if command == :end && params.size?(0)
end
commander.rb (サンプルのために用意したコマンド; linked_fileとendのみ用意)
# frozen_string_literal: true
class Commander
def linked_file(options)
return print "\nパラメータの数が違います。" unless options.size?(1..2)
combination = [Integer, [Integer, String], [Integer, Symbol]]
return print "\n認識出来ないパラメータです。" unless options.classes_match?(*combination)
para_list = [:delete]
case [*options.convert.classes, para_list.include?(options[1])]
when [Integer, false]
print "\nPass: linked UID"
print "\n UID: #{options[0]}"
when [Integer, String, false]
print "\nPass: linked UID filenames"
print "\n UID: #{options[0]}"
print "\n filenames: #{options[1]}"
when [Integer, Symbol, true]
print "\nPass: linked UID :delete"
print "\n UID: #{options[0]}"
print "\n Symbol: #{options[1]}"
else
print "\nUnknow parameters!!"
print "\n UID: #{options[0]}"
print "\n option: #{options[1]}"
end
end
def end(options)
return print "\nパラメータの数が違います。" unless options.size?(0)
print "\n終了します。"
sleep 1
end
end
arg_hash.rb (これが該当のクラス)
# frozen_string_literal: true
require_relative 'gadget'
# for checking the method arguments
class ArgHash < Hash
using Gadget
class << self
# [], combination are Singleton class
def [](string_to_hash, transform_keys: [], delimiter: ' ', split: -1)
# generate ArgHash instance from a string with some mixed argument keywords
# eg. ArgHash["apple, 320, 2024-02-13", delimiter: ','] ;delimiter => ','
# => {0 => "apple", 1 => "320", 2 => "2024-02-13"}
# ArgHash["apple 320 2024-02-13"] ;no option
# => {0 => "apple", 1 => "320", 2 => "2024-02-13"}
# ArgHash["apple 320 2024-02-13", transform_keys: [:fruit, :weight, :harvest_date]] ;transform_keys => specifying
# => {:fruit => "apple", :weight => "320", :harvest_date => "2024-02-13"}
# ArgHash["apple 320 2024-02-13 2024-02-15", transform_keys: [:fruit, :weight, :harvest_date]] ;values => 4, keys => 3
# => {:fruit => "apple", :weight => "320", :harvest_date => "2024-02-13", 3 => "2024-02-15"}
# ArgHash["apple 320 2024-02-13", transform_keys: [:fruit, :weight, :harvest_date, :delivery_date]] ;values => 3, keys => 4
# => {:fruit => "apple", :weight => "320", :harvest_date => "2024-02-13"}
# ArgHash["apple 320 2024-02-13", transform_keys: [:fruit, :weight, :harvest_date], split: 3] ;values => 2, keys => 3, split => 3
# => {:fruit => "apple", :weight => "320"}
# ArgHash["apple, 320,", transform_keys: [:fruit, :weight, :harvest_date], delimiter: ',', split: 3] ;values => 2, keys => 3, delimiter => ','
# => {:fruit => "apple", :weight => "320", :harvest_date => ""}
# ArgHash["apple 15 10 20", transform_keys: [:fruit, :purchase_order], split: 2] ;values => 3, keys => 2, delimiter => ' ', split => 2
# => {:fruit => "apple", :purchase_order => "15 10 20"} ; note: It can behave like String#partition
# ArgHash["apple, 320,", transform_keys: [:fruit, :weight, :harvest_date], delimiter: ',', split: 2] ;values => 2, keys => 3, delimiter => ',', split => 2
# => {:fruit => "apple", :weight => "320,"} ; note: cases to keep in mind
return super() if string_to_hash.empty?
if string_to_hash.is_a?(String)
string_to_hash = string_to_hash.split(delimiter, split).map(&:strip)
# if transform_keys is [], set the default keys
# in case of anything else, it uses transform_keys (fill with the alternative keys if transform_keys is shorter than string_to_hash)
num = string_to_hash.size - 1
transform_keys = transform_keys.empty? ? (0..num).to_a : (0..num).map { |n| transform_keys[n] || n }
string_to_hash = transform_keys.zip(string_to_hash)
elsif string_to_hash.is_a?(Hash)
string_to_hash
else
return string_to_hash
end
super(string_to_hash)
end
def combination(combination)
# all possible class combinations
# combination = {
# fruit: [String, NilClass],
# weight: [Integer, NilClass],
# harvest_date: [Time, NilClass]
# }
# ArgHash.combination(combination)
# => [[String, Integer, Time],
# [String, Integer, NilClass],
# [String, NilClass, Time],
# [String, NilClass, NilClass],
# [NilClass, Integer, Time],
# [NilClass, Integer, NilClass],
# [NilClass, NilClass, Time],
# [NilClass, NilClass, NilClass]]
# note:
# next case is pass
# combination = {fruit: String, weight: [Integer, NilClass]}
# ArgHash.combination(combination)
# => [[String, Integer], [String, NilClass]]
ini, *comb = combination.values.map { |v| v.is_a?(Array) ? v : [v] }
comb.inject(ini) { |result, c| result.product(c) }.map(&:flatten)
end
end
def convert
# convert the values of string_to_hash
# eg. arg_hash = ArgHash["apple, 320,", transform_keys: [:fruit, :weight, :harvest_date], delimiter: ',', split: 3]
# => {fruit: "apple", weight: "320", harvest_date: ""} note: harvest_date: ""
# arg_hash.convert
# => {fruit: "apple", weight: 320, harvest_date: nil} note: harvest_date: nil
return self if empty?
each do |(k, v)|
if v.is_a?(String)
self[k] = v.empty? ? nil : v.convert
end
end
self
end
def classes
# all classes of string_to_hash
# eg. ArgHash[{fruit: "apple", weight: 320, harvest_date: Time.parse("2024-02-10")}].classes
# => [String, Integer, Time]
# ArgHash[{fruit: "apple", weight: "320", harvest_date: "2024-02-10"}].convert.classes
# => [String, Integer, Time]
return [NilClass] if empty?
map { |(_, v)| v.class }
end
def classes_match?(*pattern)
# check if all classes match
# note: self is not destroyed
# "" in value is evaluated as NilClass
# each variable of a String class instance is attempted to be converted to other classes
# eg. ArgHash[{fruit: "apple"}].classes_match?(String) => true
# ArgHash[{fruit: "apple"}].classes_match?(String, NilClass) => true
# ArgHash[{fruit: nil}].classes_match?(String, NilClass) => true
# note:
# bad case!!
# ArgHash[{fruit: "apple"}].classes_match?([String], [String, Range]); this case dosen't match because pattern => [String]
# pass case
# ArgHash[{fruit: "apple"}].classes_match?(String, [String, Range]); this case match because pattern => String
# ArgHash[{fruit: "apple", weight: 320, harvest_date: Time.parse("2024-02-10")}].classes_match?([String, Integer, Time])
# => true
# ArgHash[{fruit: "apple", weight: "320", harvest_date: "2024-02-10"}].classes_match?([String, Integer, Time])
# => true
# ArgHash[{fruit: "apple", weight: "320", harvest_date: ""}].classes_match?([String, Integer, Time], [String, Integer, NilClass])
# => true
# pattern = [[String, Integer, Time], [String, Integer, NilClass]]
# ArgHash[{fruit: "apple", weight: "320", harvest_date: ""}].classes_match?(*pattern)
# => true
arg_hash = Marshal.load(Marshal.dump(self))
arg_hash[0] = nil if arg_hash.empty?
all_string = arg_hash.classes.map { |c| c == String }.all?
target = all_string ? arg_hash.convert.classes : arg_hash.classes
target = target.first if target.size == 1
pattern.include?(target)
end
def size(ignore_blank: true)
# the size of hash 'values'
# note: not count nil (and more doesn't count blank if ignore_blank: false)
# arg_hash = ArgHash[{fruit: "apple", weight: 320, harvest_date: ""}]
# arg_hash.size => 2
# arg_hash.size(ignore_blank: false) => 3
vals = values
vals = vals.map { |v| (v == '') ? nil : v } if ignore_blank
vals.compact.size
end
def size?(scope, ignore_blank: true)
# ArgHash[{0 => "arg 1", 1 => "arg 2"}].size?(2) => true ; evaluation => 2
# ArgHash[{0 => "arg 1", 1 => "arg 2", 2 => "arg 3"}].size?(2) => false ; evaluation => 3
# ArgHash[{}].size?(0..2) => true ; evaluation => 0
# ArgHash[{}].size?(1..2) => false ; evaluation => 0
# ArgHash[{0 => nil, 1 => nil, 2 => nil}].size?(1..2) => false ; evaluation => 0
# ArgHash[{0 => nil, 1 => "arg_2", 2 => nil}].size?(1..2) => true ; evaluation => 1
# note: in case of ignore_blank = true;
# ArgHash[{0 => "", 1 => "arg_2", 2 => ""}].size?(1..2) => true ; evaluation => 1
# note: in case of ignore_blank = false;
# ArgHash[{0 => "", 1 => "arg_2", 2 => ""}].size?(1..2, ignore_blank: false) => false ; evaluation => 3
case scope
when Integer
scope == size(ignore_blank: ignore_blank)
when Range
scope.cover?(size(ignore_blank: ignore_blank))
end
end
end
gadget.rb (ArgHashをサポートするmodule)
# frozen_string_literal: true
require 'date'
require 'time'
# small useful tool
# note: the effect only within Class
module Gadget
refine String do
def int_convertable?
# check if string can be converted to integer
# eg. str = 'I400' => false
# str = '400.0' => false
# str = '400' => true
# str = '' => false
to_i.to_s == delete_prefix('+') # delete '+' if prefix is '+'
end
end
refine String do
def time_convertable?
# check if string can be converted to Time
# eg. str = 'I400' => false
# str = '400.0' => false
# str = '' => false
# str = '400' => true
# str = '0' => true
raise Date::Error if /[^0-9\-\/+:\s]/.match?(self)
Date.parse(self)
true
rescue Date::Error
false
end
end
refine String do
def to_inum
# eg. '+1,234,567' => 1234567
str = delete_prefix('+') # delete '+' if prefix is '+'
str = str.delete(',') # delete ','
str.int_convertable? ? str.to_i : self
end
end
refine String do
def to_time
# eg. '2023-1-1' => 2023-01-01 00:00:00 +0900
# note: require 'date' and 'time'
return self unless time_convertable?
Time.parse(self)
end
end
refine String do
def to_range
# eg. '123..456' => 123..456
# '123...456' => 123...456
# '456..123' => 123..456
# '123.456' => '123.456' (nop)
return self unless include?('..') || include?('...')
r1, sep, r2 = partition(/\.{3}|\.{2}/)
return self if r1.start_with?('+', '-')
return self if r2.start_with?('+', '-')
return self if include?(',')
return self unless r1.int_convertable?
return self unless r2.int_convertable?
r1 = r1.to_i
r2 = r2.to_i
r1, r2 = r2, r1 if r1 > r2
(sep == '..') ? r1..r2 : r1...r2
end
end
refine String do
def to_range_time
# eg. '2023-1-1..2023-12-31' => 2023-1-1..2023-12-31
# '2023-1-1...20023-12-31' => 2023-1-1...2023-12-31
# '20023-12-31..2023-1-1' => 2023-1-1..2023-12-31
# '2023-1-1.2023-12-31' => '2023-1-1.2023-12-31' (nop)
return self unless include?('..') || include?('...')
r1, sep, r2 = partition(/\.{3}|\.{2}/)
return self unless r1.time_convertable?
return self unless r2.time_convertable?
r1 = r1.to_time
r2 = r2.to_time
r1, r2 = r2, r1 if r1 > r2
(sep == '..') ? r1...(r2 + 86400) : r1...r2
end
end
refine String do
def to_symbol(force: true)
# eg. 'symbol' => :symbol
# ':symbol' => :symbol
case [force, start_with?(':'), end_with?(':')]
when [true, true, false], [false, true, false] # ':symbol' => :symbol
delete_prefix(':').to_sym
when [true, false, true], [false, false, true] # 'symbol:' => :symbol
delete_suffix(':').to_sym
when [true, false, false] # 'symbol' => :symbol ; force == true
to_sym
else # 'symbol' => 'symbol'; force == false
self
end
end
end
refine String do
def convert
# after parsing str, change to Integer, Range, Time and Symbol
# return str if fail
retval = to_inum
retval = to_range if retval.is_a?(String)
retval = to_range_time if retval.is_a?(String)
retval = to_time if retval.is_a?(String)
retval = to_symbol(force: false) if retval.is_a?(String)
retval
end
end
end
# ------------------------------------------------------------------------------
if __FILE__ == $PROGRAM_NAME
include CommonModule
using Gadget
p 'I400'.int_convertable? # => false
p '400.0'.int_convertable? # => false
p '400'.int_convertable? # => true
p '+400'.int_convertable? # => true
p '-400'.int_convertable? # => true
p '0'.int_convertable? # => true
p ''.int_convertable? # => false
p '1234567'.to_amount # => '1,234,567'
p '1,234,567'.to_inum # => 1234567
p '+1,234,567'.to_inum # => 1234567
p '-1,234,567'.to_inum # => -1234567
p '123..456'.to_range # => 123..456
p '123...456'.to_range # => 123...456
p '456..123'.to_range # => 123..456
p '2023-12-31'.time_convertable? # => true
p '2023-12-32'.time_convertable? # => false
p '2023-13-01'.time_convertable? # => false
p '2023-12-31'.to_time # => 2023-12-31 00:00:00 +0900
p '2023-12-32'.to_time # => '2023-12-32'
p '2023-13-01'.to_time # => '2023-12-32'
p '2023-1-1..2023-12-31'.to_range_time
# notice: the return value is NOT wrong
# => 2023-1-1 00:00:00 +0900...2024-01-01 00:00:00 +0900
p '2023-1-1...2023-12-31'.to_range_time # => 2023-1-1 00:00:00 +0900...2023-12-31 00:00:00 +0900
p '2023-12-31..2023-1-1'.to_range_time # => 2023-1-1 00:00:00 +0900...2024-01-01 00:00:00 +0900
p 'symbol'.to_symbol # => :symbol
p ':symbol'.to_symbol # => :symbol
p 'symbol:'.to_symbol # => :symbol
p 'symbol'.to_symbol(force: false) # => 'symbol'
p ':symbol'.to_symbol(force: false) # => :symbol
p 'symbol:'.to_symbol(force: false) # => :symbol
p 'I400'.convert # => 'I400'
p '400'.convert # => 400
p '1,234,567'.convert # => 1234567
p '123..456'.convert # => 123..456
p '2023-12-31'.convert # => 2023-12-31 00:00:00 +0900
p '2023-1-1..2023-12-31'.convert # => 2023-1-1 00:00:00 +0900...2024-01-01 00:00:00 +0900
p ':symbol'.convert # => :symbol
p 'symbol:'.convert # => :symbol
p 'symbol'.convert # => 'symbol'
end
動作内容
夫々の動作内容は、
main.rbで、コマンドを受け付け、
commander.rbで各コマンドを実行しています。
各メソッド内の引数のチェックを統一的に解決するために
arg_hash.rbとgadget.rbで処理しています。
arg_hash.rbとgadget.rbは、サンプルの動作以外の挙動もするようにしてありますが、それは個々のコード内のコメントを参照願います。
お試しにいかがでしょう?
何かもっと良いアイデアがあれば、お待ちしています。
この記事が気に入ったらサポートをしてみませんか?