ずっと君のターン

2008-11-21 今日も晴れた

Array#one_hundred_thirty_two_thousand_six_hundred_and_forty_eighth とかできるよ

| 01:43 | Array#one_hundred_thirty_two_thousand_six_hundred_and_forty_eighth とかできるよを含むブックマーク

Rails2.2でなんとArrayオブジェクトのアクセサとして#second 〜 #tenthなんてのができちゃった。だいたい分かると思うけど ary.second は ary[1] と同じで、ary.tenth はary[9] と同じ。

正直これにはがっかりした。DHHにしては中途半端と言うほかない。なんで10thまでなんだよ。その程度じゃ全然たんねーよ。

やるならこうだろ!

$ irb
>> require 'array_ordinal_accessor'
=> true
>> array = (1..200000).to_a
..snip..
>> array.seventy_third
=> 73
>> array.three_hundred_and_nineth
=> 309 
>> array.one_hundred_thirty_two_thousand_six_hundred_and_forty_eighth
=> 132648

もちろん代入だってこうだ。

>> array.fifty_thousand_six_hundred_and_fifty_eighth
=> 50658
>> array.fifty_thousand_six_hundred_and_fifty_eighth = 'hello'
=> "hello"
>> array.fifty_thousand_six_hundred_and_fifty_eighth
=> "hello"

最大でだいたい一千兆くらい*1まで対応。試してないけど、アクセサの最大値はこんな感じになるはず。

>> array.nine_hundred_ninety_nine_trillion_nine_hundred_ninety_nine_billion_nine_hundred_ninety_nine_million_nine_hundred_ninety_nine_thousand_nine_hundred_and_ninety_ninth

さらにはおまけだけど、数値と英語を相互に変換もできる。

>> 90112.to_alphabetic
=> "ninety thousand one hundred and twelve"
>> 56115.to_ordinal
=> "fifty six thousand one hundred and fifteenth"
>> Integer.from_alphabetic "ninety thousand one hundred and twelve"
=> 90112
>> Integer.from_ordinal "fifty six thousand one hundred and fifteenth"
=> 56115

これでもう ary[123] みたいなだっさい記法とは金輪際さよならだ。男なら ary.one_hundred_and_twenty_fourth って書くよな。

進歩的なRubyistのみなさまにおかれましては是非ともこのライブラリを活用してソースの可読性向上に努めて頂きたい。

・・・

# array_ordinal_accessor.rb
class Integer
  LESS_THAN_20 = [
    :zero,    :one,     :two,       :three,    :four,
    :five,    :six,     :seven,     :eight,    :nine,
    :ten,     :eleven,  :twelve,    :thirteen, :fourteen,
    :fifteen, :sixteen, :seventeen, :eighteen, :nineteen,
  ]
  DOUBLE_FIGURES = [
    nil,    :ten,   :twenty,  :thirty, :forty,
    :fifty, :sixty, :seventy, :eighty, :ninety
  ]
  HUNDRED = :hundred
  BIG_FIGURES = [nil, :thousand, :million, :billion, :trillion]
  AND = :and

  SPECIAL_ORDINALS = [
    nil,    :first, :second, :third,  nil,
    :fifth, nil,    nil,     :eighth, :ninth,
  ]
  DOUBLE_FIGURES[2..-1].each_with_index do |fig, i|
    SPECIAL_ORDINALS[(i+2) * 10] = :"#{fig.to_s[0..-2]}ieth"
  end

  class <<self
    def from_alphabetic(str, sep=' ')
      ret, tmp = 0, 0
      (str.is_a?(String) ? str.split(sep) : str).map{|e| e.to_sym}.each do |num|
        case num
        when NilClass, AND;   # ignore
        when *LESS_THAN_20;   tmp += LESS_THAN_20.index num
        when *DOUBLE_FIGURES; tmp += DOUBLE_FIGURES.index(num) * 10
        when HUNDRED;         tmp *= 100
        when *BIG_FIGURES;    ret += tmp * 1000**BIG_FIGURES.index(num); tmp = 0
        else;                 raise ArgumentError.new("Invalid Format: #{num}")
        end
      end
      ret + tmp
    end

    def from_ordinal(str, sep=' ')
      array = (str.is_a?(String) ? str.split(sep) : str).map{|e| e.to_sym}
      array[-1] =
        if SPECIAL_ORDINALS.include?(array.last)
          if (idx = SPECIAL_ORDINALS.index(array.last)) < 20
            LESS_THAN_20[idx]
          else
            array.last.to_s.sub(/ieth$/, 'y').to_sym
          end
        else
          array[-1].to_s[0...-2].to_sym
        end
      from_alphabetic array
    end
  end

  def to_alphabetic(sep=' ')
    to_alphabetic_array.join sep
  end

  def to_ordinal(sep=' ')
    to_ordinal_array.join sep
  end

  private

  def to_alphabetic_array
    triples = []
    num = self
    while num != 0
      triples << num % 1000
      num /= 1000
    end
    raise RangeError.new('Too large') if BIG_FIGURES.size < triples.size

    ret = []
    triples.each_with_index do |n, i|
      array = to_alphabetic_array_less_than_1000 n, ret.empty?
      unless array == [:zero]
        array << BIG_FIGURES[i] unless i == 0
        ret.unshift array
      end
    end
    ret.flatten
  end

  def to_ordinal_array
    alphabetics = to_alphabetic_array
    alphabetics[-1] =
      if special = SPECIAL_ORDINALS[LESS_THAN_20.index(alphabetics.last) || 0]
        special
      elsif special = SPECIAL_ORDINALS[(DOUBLE_FIGURES.index(alphabetics.last) || 0) * 10]
        special
      else
        :"#{alphabetics.last}th"
      end
    alphabetics
  end

  def to_alphabetic_array_less_than_1000(num, last=false)
    case num
    when 0
      # ignore
    when 1..19
      [LESS_THAN_20[num]]
    when 20..99
      [DOUBLE_FIGURES[num / 10], *to_alphabetic_array_less_than_1000(num % 10)].compact
    else
      double = to_alphabetic_array_less_than_1000(num % 100)
      if not last
        [LESS_THAN_20[num / 100], HUNDRED, *double]
      elsif double == [:zero]
        [LESS_THAN_20[num / 100], HUNDRED]
      else
        [LESS_THAN_20[num / 100], HUNDRED, AND, *double]
      end
    end
  end
end

class Array
  def method_missing(symbol, *args, &block)
     name = symbol.to_s
    if name[-1] == ?=
      index = Integer.from_ordinal(name[0...-1], '_') - 1
      self[index] = args.first
    else
      index = Integer.from_ordinal(name, '_') - 1
      self[index]
    end
  rescue
    super
  end
end

*1:正確には九百九十九兆九千九百九十九億九千九百九十九万九千九百九十九まで対応

matakematake 2008/11/29 13:41 array.one_two_three => a[122] もできるといいかもw

technohippytechnohippy 2008/12/03 08:14 いやー、序数は死守でしょう。代わりと言っては何ですがgit版ではこっそり array.twenty_second_from_last (a[-22]) とか array.fifteenth_to_one_hundredth (a[14..99]) とかできるようになってます。