見出し画像

ポンコツ・キャンプ-文字列の形式から適当なクラスに変換する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

refineusingを使って、一時的に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の時間情報の部分でほぼ確実に弾かれてしまいます。
この仕様が気持ち悪いと思われたなら、入力時に、その日の最後になるように変換してメソッドも改訂して下さい。

この記事が何かのお役に立てたのだとしたら、幸いであります。


この記事が気に入ったらサポートをしてみませんか?