見出し画像

ポンコツ・キャンプ -金種計算から思う、 明日の自分に負債を作らぬ方法-

もう、ネタも尽きてきたので、金種計算で遊んでみることにします。

ゴール:コードは、常に変更に晒されており、そのことを意識して備える
    習慣を身に着け、明日の自分に負債を負わせないようにする

手にすることが出来る成果:
    相変わらずの泥臭い手法
    絡み合ったコードを分ける、まとめるを意識すること
    読み易い、分かり易いコードは、絶対の正義である

Step 1. ベッタベタのコード

ある日、上司の指示で金種計算プログラムを作ることに。(と云う設定で、話しは進む。あはは。) 任意の金額に対して、紙幣が何枚、硬貨が何個ってのを表示するあれです。
ベッタベタにコードを書くとまずはこんな感じです。

class Money
  def initialize(money)
    @money = money
  end

  def denominations
    result_10000, r = @money.divmod(10_000)
    result_5000, r  = r.divmod(5_000)
    result_1000, r  = r.divmod(1_000)
    result_500, r   = r.divmod(500)
    result_100, r   = r.divmod(100)
    result_50, r    = r.divmod(50)
    result_10, r    = r.divmod(10)
    result_5, r     = r.divmod(5)
    result_1, r     = r.divmod(1)

    { 10000 => result_10000, 5000  => result_5000, 1000  => result_1000,
      500   => result_500,   100   => result_100,  50    => result_50,
      10    => result_10,    5     => result_5,    1     => result_1 }
  end
end

p Money.new(186_543).denominations
p Money.new(7_543).denominations
p Money.new(10_598).denominations

実行すると結果は、

{10000=>18, 5000=>1, 1000=>1, 500=>1, 100=>0, 50=>0, 10=>4, 5=>0, 1=>3}
{10000=>0, 5000=>1, 1000=>2, 500=>1, 100=>0, 50=>0, 10=>4, 5=>0, 1=>3}
{10000=>1, 5000=>0, 1000=>0, 500=>1, 100=>0, 50=>1, 10=>4, 5=>1, 1=>3}

ハッシュにしておくと後々処理が便利なので、今のとここうしておきます。
ちゃんと動くのだけど、何故か違和感アリアリです。
理由は、result_なんとかの中間変数がテンコモリなのと、金種のデータとコードが絡みあっているからです。

Step 2. 上司に自分の能力を疑われるとマズイのでまとめる

  • 共通の手順をメソッド化、中間変数の削減、設定値とロジックの分離

ぱっと見て、不細工なコードはロクなもんじゃないです。
だから、直さないとね。上司に能力を疑われてボーナスの査定にひびいても困ります。
一連の
 result_10000, r = @money.divmod(10_000)
 …
は、メッソッドにして、最終結果がハッシュなので、返り値もハッシュにして返すようにしてみる。

  def pieces_of_money(money, kind)
    q, r = money.divmod(kind)
    { kind => q, remaining: r }
  end

この返り値をドンドンとハッシュの集計値にマージしていけば良さそう。
そうすれば、一度に、kind => qは、新規に登録され、remaining => rは、更新されていきます。
あと、金種の通貨の種類は、配列にしまっときましょ。

    @kinds_of_money = [10_000, 5000, 1000, 500, 100, 50, 10, 5, 1]

@kinds_of_moneyをeachで回せばこうなるね。

class Money
  def initialize(money)
    @money = money
    @kinds_of_money = [10_000, 5000, 1000, 500, 100, 50, 10, 5, 1]
  end

  def denominations
    result = { remaining: @money }

    @kinds_of_money.each do |kind|
      result.merge!(pieces_of_money(result[:remaining], kind))
    end

    result.reject { |key, _| key == :remaining }
  end

  def pieces_of_money(money, kind)
    q, r = money.divmod(kind)
    { kind => q, remaining: r }
  end
end

p Money.new(186_543).denominations

Step 3. 上司の後出しジャンケン

これで完成と喜んでいたら、上司が表示用のメソッドも付けなさいと云ってきました。更に、数量の後に、紙幣の場合は'枚'、硬貨の場合は、'個'としなさいとのこと。(硬貨の場合でも'枚'って云うだろ?でも上司なので逆らえません。)
(そう云うことは、全部まとめて最初に云って欲しいものです。だぁほ!)

とにかく、直さねば。

class Money
  def initialize(money)
    @money = money
    @kinds_of_money = [10_000, 5000, 1000, 500, 100, 50, 10, 5, 1]
    @units_of_money = { 10_000 => '枚', 5000 => '枚', 1000 => '枚', 500 => '個',
                        100 => '個', 50 => '個', 10 => '個', 5 => '個', 1 => '個' }
  end

  def denominations
    result = { remaining: @money }

    @kinds_of_money.each do |kind|
      result.merge!(pieces_of_money(result[:remaining], kind))
    end

    result.reject { |key, _| key == :remaining }
  end

  def pieces_of_money(money, kind)
    q, r = money.divmod(kind)
    { kind => q, remaining: r }
  end

  def show_denominations
    print "--- #{@money} ---\n"
    denominations.each do |(kind, pieces)|
      print "#{kind}#{pieces} #{@units_of_money[kind]}\n"
    end
  end
end

p Money.new(186_543).denominations
Money.new(186_543).show_denominations

こんなショボイ表示メソッドでもOKがでました。(とにかく、OKなんだからいいよね。)

{10000=>18, 5000=>1, 1000=>1, 500=>1, 100=>0, 50=>0, 10=>4, 5=>0, 1=>3}
--- 186543 ---
10000185000110001500110005001045013

でも、コードのこの部分
def initialize(money)
 @money = money
 @kinds_of_money = [10_000, 5000, 1000, 500, 100, 50, 10, 5, 1]
 @units_of_money = { 10_000 => '枚', 5000 => '枚', 1000 => '枚',
           500 => '個', 100 => '個', 50 => '個', 10 => '個',
           5 => '個', 1 => '個' }
end
ぱっと見、@kinds_of_moneyと@units_of_moneyが不細工です。
理由は、通貨の種類が重複しているからです。

Step 4. 更なる後出しジャンケンに備えて

  • 明日の自分に負債を背負わさないために備えて

上司は、また後出しジャンケンを云ってくるよな。今の内に手直ししておいた方がいいに決まってる。
通貨の基本情報のデータから@kinds_of_moneyと@units_of_moneyを作りだすようにしておこう。
@money_setに各金種の情報をハッシュの配列にしまえばいいかな。
こんな感じだろ。

class Money
  def initialize(money)
    @money = money
    money_set = [
      { kind: 10_000, unit: '枚' },
      { kind: 5000,   unit: '枚' },
      { kind: 1000,   unit: '枚' },
      { kind: 500,    unit: '個' },
      { kind: 100,    unit: '個' },
      { kind: 50,     unit: '個' },
      { kind: 10,     unit: '個' },
      { kind: 5,      unit: '個' },
      { kind: 1,      unit: '個' }
    ]
 
    @kinds_of_money = money_set.map { |h| h[:kind] }    # @kinds_of_money = [10000, 5000, ...]
    @units_of_money = money_set.map(&:values).to_h      # eg. @units_of_money[500] => '個'
  end

  def denominations
    result = { remaining: @money }

    @kinds_of_money.each do |kind|    # result = { remaining: r, 10000: 999, 5000: 999, ... }
      result.merge!(pieces_of_money(result[:remaining], kind))
    end

    result.reject { |key, _| key == :remaining }
  end

  def pieces_of_money(money, kind)
    q, r = money.divmod(kind)
    { kind => q, remaining: r }
  end

  def show_denominations
    print "--- #{@money} ---\n"
    denominations.each do |(kind, pieces)|
      print "#{kind}#{pieces} #{@units_of_money[kind]}\n"
    end
  end
end

p Money.new(186_543).denominations
Money.new(186_543).show_denominations

更にこの後、ポンコツ上司が、全く流通してない2,000円も計算に含めなければいけないとか、とうとう100,000円紙幣が発行されることになったとか、金属資源の枯渇から500円紙幣、100円紙幣が復活することになったとか、円だけでなくドルでもこの計算をしなければいけないとか、何か云ってきても上手く対応できるでしょ。

あぁ~、あとコメントも入れとこう

@kinds_of_money = @money_set.map { |h| h[:kind] }   # @kinds_of_money = [10000, 5000, ...]

@units_of_money = @money_set.map(&:values).to_h     # eg. @units_of_money[500] => '個'

@kinds_of_money.each do |kind|    # result = { remaining: r, 10000: 999, 5000: 999, ... }@kinds_of_money = @money_set.map { |h| h[:kind] }   # @kinds_of_money = [10000, 5000, ...]

プログラミングなんて、3日経てば自分の書いたコードも他人のコードと大して変わらない。一旦作ったプログラムの読み直しは大変だ。

コーディングなんて、明日の自分に負債を背負わせているようなもんだし。

明日の自分のために読みやすいコードを書きましょ。
それが、せめてもの救いです。

Step 5. あなたへの課題

課題1.
Step 3.のコード中に
result = { remaining: @money }
@kinds_of_money.each do |kind|
 result.merge!(pieces_of_money(result[:remaining], kind))
end
とありますが、このコードはeachの代わりにinjectを使うことができます。
inject版をコーディングされたし。また、そのコードは、明日の自分にとってどのくらいの負債になるかも検討されたし。

課題2.
会社が、給与を現金支給しています。銀行から調達する金種の連絡をしなければいけません。それぞれの社員の給与は、
  salaries = [254_862, 370_249, 188_624, 662_060, 159_991]
と配列で与えられています。Money#denominationsのメッセージを送り、
  total_money_denominations = { 10000 => 999, 5000 => 888, … }
の形で銀行に連絡する金種を求められたし。
ヒント:salariesの要素をMoney#denominationsで置き換えた配列が計算の起点になります。

エピローグ

殊更、強調していませんでしたが、.denominationsは、中間出力のようなハッシュで計算を終えています。これも分けると云う意味で重要だと思います。この中間出力の形式が保証されることで、様々な用途に対して次の起点になっています。.show_denominationsや課題2.がそれです。上手く切り分けることが、拡張性を高めるのだと思います。

こんなショボイ例題で随分と偉そうなことを吹いてんなぁ。
と思われた方も大勢いらっしゃるでしょうが、ここは一つ寛大なお心で見守ってやってくださいな。よしなに。

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