ポンコツ・キャンプ -金種計算から思う、 明日の自分に負債を作らぬ方法-
もう、ネタも尽きてきたので、金種計算で遊んでみることにします。
ゴール:コードは、常に変更に晒されており、そのことを意識して備える
習慣を身に着け、明日の自分に負債を負わせないようにする
手にすることが出来る成果:
相変わらずの泥臭い手法
絡み合ったコードを分ける、まとめるを意識すること
読み易い、分かり易いコードは、絶対の正義である
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 ---
10000円 18 枚
5000円 1 枚
1000円 1 枚
500円 1 個
100円 0 個
50円 0 個
10円 4 個
5円 0 個
1円 3 個
でも、コードのこの部分
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.がそれです。上手く切り分けることが、拡張性を高めるのだと思います。
こんなショボイ例題で随分と偉そうなことを吹いてんなぁ。
と思われた方も大勢いらっしゃるでしょうが、ここは一つ寛大なお心で見守ってやってくださいな。よしなに。
この記事が気に入ったらサポートをしてみませんか?