Smalltalk
Pharo
テスト駆動開発
SmalltalkDay 18

テスト駆動開発でお試しする Pharo Smalltalk・第8回 MoneyBagを少しだけBagらしくする

新訳版『テスト駆動開発』の第Ⅰ部の多国通貨をネタに Smalltalk を学ぶシリーズの第8回です。同書と似た仕様を目指しますが、好みを優先する等の都合で写経にはなっていませんのでどうぞあしからず。(12/24追記:なるべく忠実な写経は別に書きました→こちら

前回 は、同書第17章でちょっとだけ触れられている“MoneyBag”を、きっとこんな感じなのではないかな…と勝手な想像を巡らしながら実装してみました。

今回は MoneyBag に手を加えて、気持ち Bag らしく振舞うよう細工を施します。


参考: 多国通貨実装のおおまかな流れ
  1. Dollar を定義
  2. Dollar をコピーして Franc を定義
  3. Dollar、Franc の重複を新しく作ったスーパークラスの Money にプルアップ
  4. Sum(Expression)、Bank を定義して reduce を実現 ← 今ここ

同書を読んでいない方向けに補足:Sum(当シリーズでは MoneyBag)は換算前の異種通貨同士の加算(式, expression)の保持を、Bank は異種通貨間の換算レートの管理、および、Sum の換算を伴う簡約(reduce)それぞれ担当します。

Bag について

Smalltalk で Bag というのは多重集合(multiset)を扱うための組み込みのコレクション・クラスで、要素を種類ごとに分類してその数で管理します。

#(1 2 2 3) asBag = #(2 3 2 1) asBag "=> true "

クラスブラウザのコード枠の空欄や、Playground などに式を入力(あるいはコピペ)してから右クリック → Print it すると式を実行して結果を見ることができます。

1030.png

MoneyBag の場合、通貨の種類とその小計で管理することになるでしょう。

(5 USD + 10 CHF) = (10 CHF + 5 USD) "=> true"

つまり、MoneyBag class>>withAll: で受け取った配列内の順序が違っていても

(MoneyBag withAll: {5 USD. 10 CHF}) = ((MoneyBag withAll: {10 CHF. 5 USD})) "=> true"

というように、両者は等価な MoneyBag を返すことを意味まします。

Bag に似せると言っても、MoneyBag は Money 同様イミュータブルなバリューオブジェクトなので adding プロトコルなどはサポートしない最低限の実装に止めます。

MoneyBag に対する Money の加算にはこんな感じの(しかし新たに作られた別の)MoneyBag を返す振舞いを期待します。

(3 USD + 10 CHF) + 2 USD "=> 5 USD + 10 CHF "

MoneyBag の printing、comparing プロトコルを整える

TODO リスト
  • 5 USD + 10 CHF = 10 USD (レートが 2 : 1 の場合)
  • 3 USD + 4 USD = 7 USD
  • 5 USD + 10 CHF は MoneyBag を返す
  • MoneyBag に printOn: を再定義
  • MoneyBag に = と hash を再定義

…とその前に、テストも増えて来て MoneyTest のメソッドリスト枠も見にくくなってしまった(そもそも MoneyTest に盛り込みすぎな)ので、MoneyBag 関連のテストメソッドは独立したテストクラスである MoneyBagTest を新たに作成してそこにまとめてしまいましょう。

クラスリスト枠内の選択を解除し、コード枠に現れるクラス定義式を次のように編集して Accept します。

TestCase subclass: #MoneyBagTest
    instanceVariableNames: ''
    classVariableNames: ''
    package: 'TDD-Money'

MoneyBag を右クリックして Jump to test class を選択しても、自動的に MoneyBagTest クラスを作成してくれます。

1031.png

しかし自動生成した場合、Tests タグというものが付いて同名の別フォルダに分類されしまうため、ブラウザでの切り替えが面倒になります。

1032.png

もとよりこういうときのためにクラスブラウザは複数開くことができるのですが、ここでは簡単のため MoneyTest と同様に、パッケージ内の他のクラスと一覧できるように Tests タグは付けずにおきましょう。

直近に示したクラス定義式のように package: の引数部分を 'TDD-Money-Tests' から 'TDD-Money' に変更して改めて Accept すると、Tests フォルダが消えて、MoneyBagTest が TDD-Money パッケージ内の他のクラスと同じ場所に表示されます。

Testsタグ(フォルダ)内の MoneyBagTest をドラッグして TDD-Money フォルダにドロップインしても同じことができます。

1035.png

1038.png

同様の方法でメソッドも別クラスに簡単に移動できます。MoneyTest のメソッドである testPlusDifferentCurrencyReturnsMoneyBag もこの機に MoneyBagTest に移動してしまいましょう。

1070.png

1074.png

では MoneyBagTest にテストメソッドを追加してゆきましょう。プロトコルは tests です。

MoneyBagTest >> testPrintOn
    self assert: (5 USD + 10 CHF) printString equals: '(5 USD + 10 CHF)'

MoneyBagTest の ○印 をクリックして全テストを走らせるとイエローバーが出ます。

1075.png

printing プロトコルを MoneyBag に新たに作成し、仮実装のくだりは省略して重複を排除した MoneyBag>>printOn: を次ように書いてコンパイル(Accept)します。

MoneyBag >> printOn: aStream
    aStream nextPut: $(.
    elements
        do: [ :aMoney | aStream print: aMoney ]
        separatedBy: [ aStream nextPutAll: ' + ' ].
    aStream nextPut: $)

1076.png

とりあえずグリーンになりますが、MoneyBag を Bag っぽく振舞うようになるとテストが通らなくなるかもしれない(要素が順不同になる)ので、このテストは後で消してしまってもよいと思います。

TODO リスト
  • 5 USD + 10 CHF = 10 USD (レートが 2 : 1 の場合)
  • 3 USD + 4 USD = 7 USD
  • 5 USD + 10 CHF は MoneyBag を返す
  • MoneyBag に printOn: を再定義
  • MoneyBag に = と hash を再定義

次に等価性チェックを実装(再定義)します。

MoneyBagTest >> testEquality
    self assert: 5 USD + 10 CHF equals: 5 USD + 10 CHF

1077.png

結果はイエローです。

MoneyBag に = とついでに hash も(再)定義します。プロトコルは comparing です。

MoneyBag >> = other
    ^ other class == self class and: [ other elements = elements ]
MoneyBag >> hash
    ^ self class hash bitXor: elements hash

1078.png

グリーンになります。

MoneyBag を Bag っぽく振舞わせるために、テストを足しましょう。

MoneyBagTest >> testEquality
    self assert: 5 USD + 10 CHF equals: 5 USD + 10 CHF.
    self assert: 5 USD + 10 CHF equals: 10 CHF + 5 USD

MoneyBag の加算の要素を収める elements は、生成時に withAll: で受け取った配列をそのまま使うよう実装されているのでこれはもちろん失敗します。

1080.png

参考まで現時点の MoneyBag class>>withAll: とそれがコールしている MoneyBag>>setElements: の実装は次のとおりです。

MoneyBag class >> withAll: aCollectionOfMoney
    ^ self new setElements: aCollectionOfMoney; yourself
MoneyBag >> setElements: aCollectionOfMoney
    elements := aCollectionOfMoney

まず失敗しているテストを通すため、MoneyMag>> = を仮実装にします。

MoneyMag >> = other
    ^ true

1081.png

MoneyBag の要素は配列ではなく、辞書で管理しましょう。currency を key に、Money を value として取るようにします。

まず、それに相応しい名前に変更するため、MoneyBag 右クリック → Rename inst var で elements を elemsDict に変えます。

1082.png

1083.png

1084.png

…とここで、確認のために出てきたリファクタリング作業の項目の多さがこれからの困難を予感させますね。いったん Cancel してこの作業はやめておきます。

1086.png

elements への直接アクセス(ベパp.99、実パp.58)が良くなかったパターンっぽいですね。辞書による管理に切り替える前に、間接アクセス(ベパp.101、実パp.59)に変更しておくのが良さそうです。

MoneyBag 右クリック → Analyze → Inst var references... を選択すると elements を参照しているメソッドの一覧が表示されます。

1087.png

1088.png

1089.png

elements の間接アクセスの際にこれから用いる MoneyBag>>elements と elements に代入している MoneyBag>>setElements: 、配列でも辞書でも影響の無い MoneyBag>>hash を除く次の2つのメソッドが書き換えの対象になります。

それぞれのメソッドをクリックして定義を下のコード枠に呼び出し、コード中の elements への直接アクセスを self elements(間接アクセス)に書き換えてコンパイルし直します。

MoneyBag >> exchangeTo: toCurrency with: aBank
    ^ self elements
        inject: (0 perform: toCurrency)
        into: [ :acc :aMoney | acc + (aMoney exchangeTo: toCurrency with: aBank) ]
MoneyBag >> printOn: aStream
    aStream nextPut: $(.
    self elements
        do: [ :aMoney | aStream print: aMoney ]
        separatedBy: [ aStream nextPutAll: ' + ' ].
    aStream nextPut: $)

書き換えと再コンパイルが済んだらこのウインドウは閉じてしまいましょう。

準備は整いましたので、改めてインスタンス変数 elements を、MoneyBag 右クリック → Rename inst var で elemsDict に変更します。

続けて、この elements 改め elemsDict の初期化のためのメソッドを定義します。

MoneyBag を右リック → Analyze → Generate initialize method で MoneyBag>>initialize というメソッドを自動生成して追加し、elemsDict への代入を nil から Dictionary new に変えてコンパイルします。

MoneyBag >> initialize

    super initialize.

    elemsDict := Dictionary new.

1091.png

あとは、MoneyBag>>elements に elemsDict をそのままではなく、値の配列である values を返させます。

こうしておくことで、self elements で配列が返ってくることを期待している先ほど書き換えた2つのメソッド(exchangeTo:with: 、printOn: )も問題なく動作するはずです。

MoneyBag >> elements
    ^ elemsDict values

最後に配列を受け取る setElements: は、受け取った配列を重複なく辞書(elemsDict)に登録するよう書き換えておしまいです。

MoneyBag >> setElements: aCollectionOfMoney
    aCollectionOfMoney do: [ :aMoney | 
            elemsDict
                at: aMoney currency
                put: (elemsDict
                        at: aMoney currency
                        ifPresent: [ :pMoney | pMoney + aMoney ]
                        ifAbsent: [ aMoney ]) ]

ああ、ひとつ忘れていました。このタイミングで MoneyBag>> = の仮実装もきちんと元に戻しておかねば…。

Smalltalk には古来より、メソッド単位でのバージョン履歴機能が備わっています。MoneyBag>> = を右クリック → Versions でこれまでの書き換え履歴の一覧が表示されます。

1092.png

1105.png

仮実装にするひとつ手前(2番目)の実装を選択して Revert します。バージョンブラウザは不要なので閉じてしまいます。

元に戻ってはいるのですが、elemsDict に名前を変える前の elements に、しかも直接アクセスしていた時代のコードに戻ってしまうので、改めて self elements(間接アクセス)に変えてから再度コンパイルします。

MoneyBag >> = other
    ^ other class == self class and: [ other elements = self elements ]

1093.png

少々歩幅が広すぎましたが、全テストの実行結果はグリーンとなり作業完了です。

TODO リスト
  • 5 USD + 10 CHF = 10 USD (レートが 2 : 1 の場合)
  • 3 USD + 4 USD = 7 USD
  • 5 USD + 10 CHF は MoneyBag を返す
  • MoneyBag に printOn: を再定義
  • MoneyBag に = と hash を再定義

MoneyBag + Money と Money + MoneyBag と MoneyBag + MoneyBag

TODO リスト
  • 5 USD + 10 CHF = 10 USD (レートが 2 : 1 の場合)
  • 3 USD + 4 USD = 7 USD
  • 5 USD + 10 CHF は MoneyBag を返す
  • MoneyBag に printOn: を再定義
  • MoneyBag に = と hash を再定義
  •  (3 USD + 10 CHF) + 2 USD = (5 USD + 10 CHF)
  •  3 USD + (10 CHF + 2 USD) = (5 USD + 10 CHF)
  •  (3 USD + 4 CHF) + (2 USD + 6 CHF) = (5 USD + 10 CHF)

次は、MoneyBag と Money の加算をサポートします。

MoneyBagTest >> testPlusMoney
    self assert: (3 USD + 10 CHF) + 2 USD equals: 5 USD + 10 CHF

結果はもちろんレッド(エラー)です。testPlusMoney のみ走らせて、足りない MoneyBag>> + をいつも通り Create しましょう。

1094.png

プロトコルは arithmetic です。

MoneyBag >> + addend

仮引数を addend に変更し、self shouldBeImplemented を消して何もない(self を返す)メソッドをコンパイル(Accept)したら、

1095.png

デバッガーは閉じてしまいます。

テストは失敗します(イエロー)。テストが期待する結果をそのまま返す仮実装に変えてコンパイルし直します。

MoneyBag >> + addend
    ^ 5 USD + 10 CHF

1096.png

期待通り、グリーンが出ます。重複の排除は次のようにします。

MoneyBag >> + addend
    ^ self class withAll: (self elements copyWith: addend)

1097.png

TODO リスト
  • 5 USD + 10 CHF = 10 USD (レートが 2 : 1 の場合)
  • 3 USD + 4 USD = 7 USD
  • 5 USD + 10 CHF は MoneyBag を返す
  • MoneyBag に printOn: を再定義
  • MoneyBag に = と hash を再定義
  •  (3 USD + 10 CHF) + 2 USD = (5 USD + 10 CHF)
  •  3 USD + (10 CHF + 2 USD) = (5 USD + 10 CHF)
  •  (3 USD + 4 CHF) + (2 USD + 6 CHF) = (5 USD + 10 CHF)

引き続き、Money に MoneyBag を加算する場合を考えましょう。

MoneyBagTest >> testMoneyPlusMoneyBag
    self assert: 3 USD + (10 CHF + 2 USD) equals: 5 USD + 10 CHF

結果はレッドです。testMoneyPlusMoneyBag のみを走らせて出てきたノーティファイアーの Debug をクリックしてデバッガーを起動すると、Money>> + の今の実装は addend に MoneyBag を考えていないため、MoneyBag は currency に答えられずエラー(レッド)になっていることが分かります。

1098.png

そのままデバッガーのコード枠で、addend が Money の時の処理と条件分けして仮実装を書きます。

Money >> + addend
    addend isMoney ifTrue: [
        addend currency ~~ currency
            ifTrue: [ ^ MoneyBag withAll: {self. addend} ].
        ^ amount + addend amount perform: currency ].
    ^ 5 USD + 10 CHF

1099.png

いったんデバッガーを閉じて、テストが通ることを確認したら、MoneyBag である addend に委譲する形に書き直して重複の排除とします。

Money >> + addend
    addend isMoney ifTrue: [
        addend currency ~~ currency
            ifTrue: [ ^ MoneyBag withAll: {self. addend} ].
        ^ amount + addend amount perform: currency ].
    ^ addend + self

1100.png

Money の加算について、結合則が成り立つようになったみたいで面白いですね。

((3 USD + 10 CHF) + 2 USD) = (3 USD + (10 CHF + 2 USD)) "=> true "
TODO リスト
  • 5 USD + 10 CHF = 10 USD (レートが 2 : 1 の場合)
  • 3 USD + 4 USD = 7 USD
  • 5 USD + 10 CHF は MoneyBag を返す
  • MoneyBag に printOn: を再定義
  • MoneyBag に = と hash を再定義
  •  (3 USD + 10 CHF) + 2 USD = (5 USD + 10 CHF)
  •  3 USD + (10 CHF + 2 USD) = (5 USD + 10 CHF)
  •  (3 USD + 4 CHF) + (2 USD + 6 CHF) = (5 USD + 10 CHF)

引き続き、MoneyBag 同士の加算もサポートします。

MoneyBagTest >> testPlusMoneyBag
    self assert: (3 USD + 4 CHF) + (2 USD + 6 CHF) equals: 5 USD + 10 CHF

スタックトレースを見ると、MoneyBag>>setElements: まで進んでから currency でエラーを出していますが、問題は上流の(一覧の下の方にある) MoneyBag>> + で起こっています。

1107.png

そのままデバッガー内で、addend が Money か否かで条件分けをした仮実装を書きます。

MoneyBag >> + addend
    addend isMoney ifTrue: [ ^ self class withAll: (self elements copyWith: addend) ].
    ^ 5 USD + 10 CHF

1101.png

addend の MoneyBag の elements を一つずつ足す作業にして重複を排除します。

MoneyBag >> + addend
    addend isMoney ifTrue: [ ^ self class withAll: (self elements copyWith: addend) ].
    ^ addend elements inject: self into: [ :acc :aMoney | acc + aMoney ]

1102.png

TODO リスト
  • 5 USD + 10 CHF = 10 USD (レートが 2 : 1 の場合)
  • 3 USD + 4 USD = 7 USD
  • 5 USD + 10 CHF は MoneyBag を返す
  • MoneyBag に printOn: を再定義
  • MoneyBag に = と hash を再定義
  •  (3 USD + 10 CHF) + 2 USD = (5 USD + 10 CHF)
  •  3 USD + (10 CHF + 2 USD) = (5 USD + 10 CHF)
  •  (3 USD + 4 CHF) + (2 USD + 6 CHF) = (5 USD + 10 CHF)

MoneyBag の乗算サポート

TODO リスト
  • 5 USD + 10 CHF = 10 USD (レートが 2 : 1 の場合)
  • 3 USD + 4 USD = 7 USD
  • 5 USD + 10 CHF は MoneyBag を返す
  • MoneyBag に printOn: を再定義
  • MoneyBag に = と hash を再定義
  •  (3 USD + 10 CHF) + 2 USD = (5 USD + 10 CHF)
  •  3 USD + (10 CHF + 2 USD) = (5 USD + 10 CHF)
  •  (3 USD + 4 CHF) + (2 USD + 6 CHF) = (5 USD + 10 CHF)
  •  (5 USD + 10 CHF) * 2 = (10 USD + 20 CHF)
MoneyBagTest >> testMoneyBagTimes
    self assert: (5 USD + 10 CHF) * 2 equals: 10 USD + 20 CHF

テストの結果はレッドなので、いつもどおり testMoneyBagTimes のみ走らせてノーティファイアーから MoneyBag>> * を Create します。プロトコルは arithmetic です。

MoneyBag >> * multiplier

仮引数を multiplier に変えて、self shouldBeImplemented を削除したらコンパイルし、デバッガーは閉じてしまいます。

MoneyBagTest の全テストを走らせると結果はイエローです。

仮実装は次のようにしましょう。

MoneyBag >> * multiplier
    ^ self class withAll: {10 USD. 20 CHF}

1103.png

Squeak や Pharo には、APL や R ライクな…などと言うとおこがましいので、それらから見れば実にささやかな配列演算が組み込みで用意されています。

#(1 2 3) * 4 "=> #(4 8 12) "

重複の排除には、これを使いましょう。

ManeyBag >> * multiplier
    ^ self class withAll: self elements * multiplier

1104.png

TODO リスト
  • 5 USD + 10 CHF = 10 USD (レートが 2 : 1 の場合)
  • 3 USD + 4 USD = 7 USD
  • 5 USD + 10 CHF は MoneyBag を返す
  • MoneyBag に printOn: を再定義
  • MoneyBag に = と hash を再定義
  •  (3 USD + 10 CHF) + 2 USD = (5 USD + 10 CHF)
  •  3 USD + (10 CHF + 2 USD) = (5 USD + 10 CHF)
  •  (3 USD + 4 CHF) + (2 USD + 6 CHF) = (5 USD + 10 CHF)
  •  (5 USD + 10 CHF) * 2 = (10 USD + 20 CHF)

ここまでのまとめと補足

  • Bag は多重集合を扱うためのコレクション
  • Analyze → Inst var references... でインスタンス変数を参照しているメソッド群を、Inst var assignments... でインスタンス変数に代入しているメソッドを呼び出せる。
  • APLライクな配列演算は便利。ただし効率は良くないので注意。

この時点のコード

ファイルアウト形式のソースです。(この形式で Smalltalk のソースを読むことはあまり推奨されません。為念)

Pharo では、いったんこのコードをテキストファイルに保存してから、そのファイルを Pharo 内から Tools → File Browser で開き、Filein ボタンをクリックすると読み込めます。

Squeak でも読み込みや実行が可能なコードにもなっています。Pharo 同様に File List で開くか、デスクトップクリック → Workspace で開いたワークスペースなどにコピペし、alt/cmd + shift + g などで file it in するとクラスブラウザから閲覧したり、デスクトップクリック → Test Runner でテストも試せます(ただし使い勝手は Pharo に比べるとかなり落ちます^^;)。

[前回からの差分はこちら]

!Number methodsFor: '*TDD-Money' stamp: 'sumim 12/7/2017 18:16'!
USD
    ^ Money amount: self currency: #USD! !


!Number methodsFor: '*TDD-Money' stamp: 'sumim 12/7/2017 18:16'!
CHF
    ^ Money amount: self currency: #CHF! !


!Object methodsFor: '*TDD-Money' stamp: 'sumim 12/14/2017 18:03'!
isMoney
    ^ false! !


Object subclass: #Bank
    instanceVariableNames: 'xRateDict'
    classVariableNames: ''
    poolDictionaries: ''
    category: 'TDD-Money'!


!Bank methodsFor: 'accessing' stamp: 'sumim 12/16/2017 08:39'!
addXRate: aMoneyXRate
    ^ xRateDict at: aMoneyXRate fromToCurrency put: aMoneyXRate! !


!Bank methodsFor: 'accessing' stamp: 'sumim 12/16/2017 08:42'!
xRateOf: anAssociation
    anAssociation key == anAssociation value ifTrue: [ ^ 1 ]. 
    ^ xRateDict at: anAssociation! !


!Bank methodsFor: 'exchanging' stamp: 'sumim 12/16/2017 09:05'!
exchange: aMoneyOrBag to: toCurrency 
    ^ aMoneyOrBag exchangeTo: toCurrency with: self! !


!Bank methodsFor: 'initialization' stamp: 'sumim 12/16/2017 08:42'!
initialize

    super initialize.

    xRateDict := Dictionary new.! !


Object subclass: #Money
    instanceVariableNames: 'amount currency'
    classVariableNames: ''
    poolDictionaries: ''
    category: 'TDD-Money'!


!Money methodsFor: 'accessing' stamp: 'sumim 12/7/2017 17:32'!
currency
    ^ currency! !


!Money methodsFor: 'accessing' stamp: 'sumim 12/5/2017 17:28'!
amount
    ^ amount! !


!Money methodsFor: 'arithmetic' stamp: 'sumim 12/15/2017 15:06'!
* multiplier
    multiplier isNumber
        ifTrue: [ ^ amount * multiplier perform: currency ].
    self assert: multiplier fromCurrency == currency.
    ^ amount * multiplier xRate perform: multiplier toCurrency! !


!Money methodsFor: 'arithmetic' stamp: 'sumim 12/14/2017 18:05'!
/ divisor
    divisor isNumber
        ifTrue: [ ^ amount / divisor perform: currency ].
    divisor isMoney
        ifFalse: [ ^ self error: 'unsupported divisor' ].
    ^ divisor currency == currency
        ifTrue: [ amount / divisor amount ]
        ifFalse: [ MoneyXRate new
                setFromToCurrency: divisor currency -> currency
                xRate: amount / divisor amount ]! !


!Money methodsFor: 'arithmetic' stamp: 'sumim 12/19/2017 18:05'!
+ addend
    addend isMoney ifTrue: [
        addend currency ~~ currency
            ifTrue: [ ^ MoneyBag withAll: {self. addend} ].
        ^ amount + addend amount perform: currency ].
    ^ addend + self! !


!Money methodsFor: 'comparing' stamp: 'sumim 12/7/2017 17:50'!
= other
    ^ other currency == currency and: [ other amount = amount ]! !


!Money methodsFor: 'comparing' stamp: 'sumim 12/14/2017 18:08'!
hash
    ^ currency hash bitXor: amount hash! !


!Money methodsFor: 'exchanging' stamp: 'sumim 12/16/2017 09:08'!
exchangeTo: toCurrency with: aBank 
    ^ self * (aBank xRateOf: currency -> toCurrency)! !


!Money methodsFor: 'printing' stamp: 'sumim 12/7/2017 18:08'!
printOn: aStream
    aStream print: amount; space; nextPutAll: currency! !


!Money methodsFor: 'private' stamp: 'sumim 12/7/2017 17:41'!
setAmount: aNumber currency: aSymbol
    amount := aNumber.
    currency := aSymbol! !


!Money methodsFor: 'testing' stamp: 'sumim 12/14/2017 18:04'!
isMoney
    ^ true! !


!Money class methodsFor: 'instance creation' stamp: 'sumim 12/7/2017 18:15'!
amount: aNumber currency: aSymbol
    ^ self new setAmount: aNumber currency: aSymbol; yourself! !


Object subclass: #MoneyBag
    instanceVariableNames: 'elemsDict'
    classVariableNames: ''
    poolDictionaries: ''
    category: 'TDD-Money'!


!MoneyBag methodsFor: 'accessing' stamp: 'sumim 12/19/2017 17:55'!
elements
    ^ elemsDict values! !


!MoneyBag methodsFor: 'arithmetic' stamp: 'sumim 12/19/2017 18:14'!
+ addend
    addend isMoney ifTrue: [ ^ self class withAll: (self elements copyWith: addend) ].
    ^ addend elements inject: self into: [ :acc :aMoney | acc + aMoney ]! !


!MoneyBag methodsFor: 'arithmetic' stamp: 'sumim 12/19/2017 18:15'!
* multiplier
    ^ self class withAll: self elements * multiplier! !


!MoneyBag methodsFor: 'comparing' stamp: 'sumim 12/19/2017 18:00'!
= other
    ^ other class == self class and: [ other elements = self elements ]! !


!MoneyBag methodsFor: 'comparing' stamp: 'sumim 12/19/2017 17:55'!
hash
    ^ self class hash bitXor: elemsDict hash! !


!MoneyBag methodsFor: 'exchanging' stamp: 'sumim 12/19/2017 17:53'!
exchangeTo: toCurrency with: aBank
    ^ self elements
        inject: (0 perform: toCurrency)
        into: [ :acc :aMoney | acc + (aMoney exchangeTo: toCurrency with: aBank) ]! !


!MoneyBag methodsFor: 'printing' stamp: 'sumim 12/19/2017 17:53'!
printOn: aStream
    aStream nextPut: $(.
    self elements
        do: [ :aMoney | aStream print: aMoney ]
        separatedBy: [ aStream nextPutAll: ' + ' ].
    aStream nextPut: $)! !


!MoneyBag methodsFor: 'private' stamp: 'sumim 12/19/2017 17:57'!
setElements: aCollectionOfMoney
    aCollectionOfMoney do: [ :aMoney | 
            elemsDict
                at: aMoney currency
                put: (elemsDict
                        at: aMoney currency
                        ifPresent: [ :pMoney | pMoney + aMoney ]
                        ifAbsent: [ aMoney ]) ]! !


!MoneyBag methodsFor: 'initialization' stamp: 'sumim 12/19/2017 17:55'!
initialize

    super initialize.

    elemsDict := Dictionary new.! !


!MoneyBag class methodsFor: 'instance creation' stamp: 'sumim 12/17/2017 16:47:06'!
withAll: aCollectionOfMoney
    ^ self new setElements: aCollectionOfMoney; yourself! !


Object subclass: #MoneyXRate
    instanceVariableNames: 'fromToCurrency xRate'
    classVariableNames: ''
    poolDictionaries: ''
    category: 'TDD-Money'!


!MoneyXRate methodsFor: 'accessing' stamp: 'sumim 12/14/2017 18:06'!
fromCurrency
    ^ fromToCurrency key! !


!MoneyXRate methodsFor: 'accessing' stamp: 'sumim 12/14/2017 18:07'!
toCurrency
    ^ fromToCurrency value! !


!MoneyXRate methodsFor: 'accessing' stamp: 'sumim 12/14/2017 18:06'!
xRate
    ^ xRate! !


!MoneyXRate methodsFor: 'accessing' stamp: 'sumim 12/16/2017 08:36'!
fromToCurrency
    ^ fromToCurrency! !


!MoneyXRate methodsFor: 'comparing' stamp: 'sumim 12/14/2017 18:08'!
= other
    ^ other class == self class
        and: [ other fromToCurrency = fromToCurrency and: [ other xRate = xRate ] ]! !


!MoneyXRate methodsFor: 'comparing' stamp: 'sumim 12/14/2017 18:08'!
hash
    ^ fromToCurrency hash bitXor: xRate hash! !


!MoneyXRate methodsFor: 'printing' stamp: 'sumim 12/14/2017 18:08'!
printOn: aStream
    aStream
        print: xRate;
        space;
        nextPutAll: self toCurrency;
        nextPutAll: ' / ';
        print: 1;
        space;
        nextPutAll: self fromCurrency! !


!MoneyXRate methodsFor: 'private' stamp: 'sumim 12/14/2017 18:05'!
setFromToCurrency: anAssociation xRate: aNumber
    fromToCurrency := anAssociation.
    xRate := aNumber! !


TestCase subclass: #MoneyTest
    instanceVariableNames: ''
    classVariableNames: ''
    poolDictionaries: ''
    category: 'TDD-Money'!


!MoneyTest methodsFor: 'tests' stamp: 'sumim 12/16/2017 08:28'!
testBankSettingGettingXRate
    | xRate bank |
    xRate := 1 USD / 2 CHF.
    bank := Bank new.
    bank addXRate: xRate.
    self assert: (bank xRateOf: xRate fromToCurrency) equals: xRate! !


!MoneyTest methodsFor: 'tests' stamp: 'sumim 12/7/2017 17:28'!
testCurrency
    self assert: 5 USD currency equals: #USD.
    self assert: 5 CHF currency equals: #CHF! !


!MoneyTest methodsFor: 'tests' stamp: 'sumim 12/14/2017 18:04'!
testDivideDifferentCurrencyReturnsMoneyXRate
    | aMoneyXRate |
    aMoneyXRate := 1 USD / 2 CHF.
    self assert: aMoneyXRate class equals: MoneyXRate! !


!MoneyTest methodsFor: 'tests' stamp: 'sumim 12/14/2017 18:00'!
testDividedByNum
    self assert: 2 USD / 2 equals: 1 USD! !


!MoneyTest methodsFor: 'tests' stamp: 'sumim 12/14/2017 18:02'!
testDividedBySameCurrency
    self assert: 2 USD / 1 USD equals: 2! !


!MoneyTest methodsFor: 'tests' stamp: 'sumim 12/5/2017 12:55'!
testEquality
    self assert: 5 USD = 5 USD.
    self assert: 5 USD ~= 6 USD.
    self assert: 5 CHF = 5 CHF.
    self assert: 5 CHF ~= 6 CHF.
    self assert: 5 USD ~= 5 CHF! !


!MoneyTest methodsFor: 'tests' stamp: 'sumim 12/16/2017 08:44'!
testExchangeMoney
    | bank |
    bank := Bank new.
    bank addXRate: 1 USD / 2 CHF.
    self assert: (bank exchange: 10 CHF to: #USD) equals: 5 USD! !


!MoneyTest methodsFor: 'tests' stamp: 'sumim 12/16/2017 09:00'!
testExchangeMoneyDifferentCurrency
    | bank result |
    bank := Bank new.
    bank addXRate: 1 USD / 2 CHF.
    result := bank exchange: 5 USD + 10 CHF to: #USD.
    self assert: result equals: 10 USD! !


!MoneyTest methodsFor: 'tests' stamp: 'sumim 12/7/2017 17:30'!
testFiveCHFReturnsAMoney
    self assert: 5 CHF class equals: Money! !


!MoneyTest methodsFor: 'tests' stamp: 'sumim 12/7/2017 17:30'!
testFiveUSDReturnsAMoney
    self assert: 5 USD class equals: Money! !


!MoneyTest methodsFor: 'tests' stamp: 'sumim 12/14/2017 18:03'!
testIsMoney
    self deny: 1 isMoney.
    self assert: 1 USD isMoney! !


!MoneyTest methodsFor: 'tests' stamp: 'sumim 12/5/2017 12:56'!
testMultiplication
    self assert: 5 USD * 2 equals: 10 USD.
    self assert: 5 CHF * 2 equals: 10 CHF! !


!MoneyTest methodsFor: 'tests' stamp: 'sumim 12/7/2017 17:28'!
testPrintString
    self assert: 5 USD printString equals: '5 USD'.
    self assert: 5 CHF printString equals: '5 CHF'! !


!MoneyTest methodsFor: 'tests' stamp: 'sumim 12/16/2017 08:47'!
testSimpleAddition
    self assert: 3 USD + 4 USD equals: 7 USD! !


TestCase subclass: #MoneyBagTest
    instanceVariableNames: ''
    classVariableNames: ''
    poolDictionaries: ''
    category: 'TDD-Money'!


!MoneyBagTest methodsFor: 'tests' stamp: 'sumim 12/19/2017 17:52'!
testEquality
    self assert: 5 USD + 10 CHF equals: 5 USD + 10 CHF.
    self assert: 5 USD + 10 CHF equals: 10 CHF + 5 USD! !


!MoneyBagTest methodsFor: 'tests' stamp: 'sumim 12/19/2017 18:15'!
testMoneyBagTimes
    self assert: (5 USD + 10 CHF) * 2 equals: 10 USD + 20 CHF! !


!MoneyBagTest methodsFor: 'tests' stamp: 'sumim 12/19/2017 18:03'!
testMoneyPlusMoneyBag
    self assert: 3 USD + (10 CHF + 2 USD) equals: 5 USD + 10 CHF! !


!MoneyBagTest methodsFor: 'tests' stamp: 'sumim 12/19/2017 17:49'!
testPlusDifferentCurrencyReturnsMoneyBag
    | sum |
    sum := 5 USD + 10 CHF.
    self assert: sum class equals: MoneyBag! !


!MoneyBagTest methodsFor: 'tests' stamp: 'sumim 12/19/2017 18:01'!
testPlusMoney
    self assert: (3 USD + 10 CHF) + 2 USD equals: 5 USD + 10 CHF! !


!MoneyBagTest methodsFor: 'tests' stamp: 'sumim 12/19/2017 18:05'!
testPlusMoneyBag
    self assert: (3 USD + 4 CHF) + (2 USD + 6 CHF) equals: 5 USD + 10 CHF! !


!MoneyBagTest methodsFor: 'tests' stamp: 'sumim 12/19/2017 17:49'!
testPrintOn
    self assert: (5 USD + 10 CHF) printString equals: '(5 USD + 10 CHF)'! !