ポンコツ・キャンプ-文字列の形式から適当なクラスに変換するGadget
「ポンコツ・キャンプ-シンプルなコマンド入力プログラム」の付録です。
コマンド入力をする際にパラメータの入力が付随する場合、パラメータを適当なクラスに変換する必要が出てきたので、作っていく内に膨らんできたので、moduleにまとめたものです。railsで既に対応しているかもですが。。。
例えば、こんな状況があったとします。
delete 1592
delete 1592..1599
コマンド入力したdeleteとパラメータのフレーズを
command, argument = phrase
のように分離した後、文字列のままではargumentは認識されないので、”1592”をInteger、”1592..1599"をRangeに変換する必要があります。
個別のメソッドの呼び出しにも応じますが、最終的に、
"1592".convert => 1592
"1592..1599".convert => 1592..1599
とすれば変換してくれます。
サンプルコード
# frozen_string_literal: true
require 'date'
require 'time'
# small useful tool
# note: the effect only within Class
# see the bottom code for the reference
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 == self
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_amount
# eg. '1234567' => '1,234,567'
int_convertable? ? Gadget.format_with_comma(self) : self
end
end
def self.format_with_comma(num)
# insert comma to the numbers or the numeric character
# 1230000.56 => '1,230,000.56'
# '1230000.56' => '1,230,000.56'
num = num.to_s if num.is_a?(Numeric)
return nil if Float(num, exception: false).nil?
int, frac = num.split('.') # separate the integer part from the fractional part
_, sign, int = int.rpartition(/[-+]/) # separate the sign and the integer part
int = int.reverse # reverse the integer part
.scan(/.{1,3}/) # separate by 3 letters
.join(',') # join the block with ','
.reverse # reverse the string
frac.nil? ? [sign, int].join : [sign, int, '.', frac].join # join all strings
end
refine String do
def to_inum
# eg. '1,234,567' => 1234567
without_comma = delete(',')
without_comma.int_convertable? ? without_comma.to_i : self
end
end
refine String do
def to_time
# eg. '2023-1-1' => 2023-01-01 00:00:00 +0900
# 'Hello!' => 'Hello!'
# 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(/\.\.\.|\.\./)
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...2024-01-01
# '2023-1-1...20023-12-31' => 2023-1-1...2023-12-31
# '20023-12-31..2023-1-1' => 2023-1-1..2024-01-01
# '2023-1-1.2023-12-31' => '2023-1-1.2023-12-31' (nop)
return self unless include?('..') || include?('...')
r1, sep, r2 = partition(/\.\.\.|\.\./)
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 convert
# after parsing str, change to Integer, Range and Time
# if fail, return str
return to_i if int_convertable?
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
end
end
end
# ------------------------------------------------------------------------------
if __FILE__ == $PROGRAM_NAME
using Gadget
p 'I400'.int_convertable? # 'I400' => false
p '400.0'.int_convertable? # '400.0' => false
p '400'.int_convertable? # '400' => true
p '0'.int_convertable? # '0' => true
p '1234567'.to_amount # '1234567' => '1,234,567'
p '1,234,567'.to_inum # '1,234,567' => 1234567
p '123..456'.to_range # '123..456' => 123..456
p '123...456'.to_range # '123...456' => 123...456
p '456..123'.to_range # '456..123' => 123..456
p '2023-12-31'.time_convertable? # '2023-12-31' => true
p '2023-12-32'.time_convertable? # '2023-12-32' => false
p '2023-13-01'.time_convertable? # '2023-12-32' => false
p '2023-12-31'.to_time # '2023-12-31' => 2023-12-31 00:00:00 +0900
p '2023-12-32'.to_time # '2023-12-32' => '2023-12-32'
p '2023-13-01'.to_time # '2023-12-32' => '2023-12-32'
p '2023-1-1..2023-12-31'.to_range_time
# notice: not wrong
# '2023-1-1..2023-12-31' => 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..2023-12-31' => 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-12-31..2023-1-1' => 2023-1-1 00:00:00 +0900...2024-01-01 00:00:00 +0900
p 'I400'.convert # 'I400' => 'I400'
p '400'.convert # '400' => 400
p '1,234,567'.convert # '1,234,567' => 1234567
p '123..456'.convert # '123..456' => 123..456
p '2023-12-31'.convert # '2023-12-31' => 2023-12-31 00:00:00 +0900
p '2023-1-1..2023-12-31'.convert
# '2023-1-1..2023-12-31' => 2023-1-1 00:00:00 +0900...2024-01-01 00:00:00 +0900
end
簡単な説明1
refineとusingを使って、一時的にStringクラスを拡張しています。
usingの使用場所で影響範囲が変わるようですので、using Gadgetの置き場所は、class直下かincludeを使用しているのなら、その後が望ましいと思われます。
if __FILE__ == $PROGRAM_NAMEの直下にテスト・サンプルがありますので、使用法、結果等は、そこから把握してください。ファイルを作って直接実行すれば、動作確認できます。
to_amountメソッドとself.format_with_commaとの絡みが不細工な作りになっていますが、これはformat_with_commaは、別module内のメソッドで使用していたものを無理矢理このmoduleに収めてまとめたせいです。すんません。
簡単な説明2
to_range_timeに於いて、
p '2023-1-1..2023-12-31'.to_range_time
# notice: not wrong
# '2023-1-1..2023-12-31' => 2023-1-1 00:00:00 +0900...2024-01-01 00:00:00 +0900
'2023-1-1..2023-12-31'を変換すると後半部分が…2024-01-01 00:00:00 +0900となっていますが、これは間違いではなく仕様です。手入力の日付を素直に変換するとその日の最初の日時に変換されるからです。例えば、2023-12-31のtime_stampを
'2023-1-1..2023-12-31'.to_range_time.cover?(time_stamp)
などとと判定した場合、変換結果を..2023-12-31 00:00:00 +0900としてしまうとtime_stampの時間情報の部分でほぼ確実に弾かれてしまいます。
この仕様が気持ち悪いと思われたなら、入力時に、その日の最後になるように変換してメソッドも改訂して下さい。
この記事が何かのお役に立てたのだとしたら、幸いであります。
この記事が気に入ったらサポートをしてみませんか?