新訳版『テスト駆動開発』の第Ⅰ部の多国通貨をネタに Smalltalk を学ぶシリーズの第8回です。同書と似た仕様を目指しますが、好みを優先する等の都合で写経にはなっていませんのでどうぞあしからず。(12/24追記:なるべく忠実な写経は別に書きました→こちら )
前回 は、同書第17章でちょっとだけ触れられている“MoneyBag”を、きっとこんな感じなのではないかな…と勝手な想像を巡らしながら実装してみました。
今回は MoneyBag に手を加えて、気持ち Bag らしく振舞うよう細工を施します。
#####*参考: 多国通貨実装のおおまかな流れ* 1. Dollar を定義 1. Dollar をコピーして Franc を定義 1. Dollar、Franc の重複を新しく作ったスーパークラスの Money にプルアップ 1. **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 すると式を実行して結果を見ることができます。
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 クラスを作成してくれます。
しかし自動生成した場合、Tests タグというものが付いて同名の別フォルダに分類されしまうため、ブラウザでの切り替えが面倒になります。
もとよりこういうときのためにクラスブラウザは複数開くことができるのですが、ここでは簡単のため MoneyTest と同様に、パッケージ内の他のクラスと一覧できるように Tests タグは付けずにおきましょう。
直近に示したクラス定義式のように package: の引数部分を 'TDD-Money-Tests' から 'TDD-Money' に変更して改めて Accept すると、Tests フォルダが消えて、MoneyBagTest が TDD-Money パッケージ内の他のクラスと同じ場所に表示されます。
Testsタグ(フォルダ)内の MoneyBagTest をドラッグして TDD-Money フォルダにドロップインしても同じことができます。
同様の方法でメソッドも別クラスに簡単に移動できます。MoneyTest のメソッドである testPlusDifferentCurrencyReturnsMoneyBag もこの機に MoneyBagTest に移動してしまいましょう。
では MoneyBagTest にテストメソッドを追加してゆきましょう。プロトコルは tests です。
MoneyBagTest >> testPrintOn
self assert: (5 USD + 10 CHF) printString equals: '(5 USD + 10 CHF)'
MoneyBagTest の ○印 をクリックして全テストを走らせるとイエローバーが出ます。
printing プロトコルを MoneyBag に新たに作成し、仮実装のくだりは省略して重複を排除した MoneyBag>>printOn: を次ように書いてコンパイル(Accept)します。
MoneyBag >> printOn: aStream
aStream nextPut: $(.
elements
do: [ :aMoney | aStream print: aMoney ]
separatedBy: [ aStream nextPutAll: ' + ' ].
aStream nextPut: $)
とりあえずグリーンになりますが、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
結果はイエローです。
MoneyBag に = とついでに hash も(再)定義します。プロトコルは comparing です。
MoneyBag >> = other
^ other class == self class and: [ other elements = elements ]
MoneyBag >> hash
^ self class hash bitXor: elements hash
グリーンになります。
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: で受け取った配列をそのまま使うよう実装されているのでこれはもちろん失敗します。
参考まで現時点の MoneyBag class>>withAll: とそれがコールしている MoneyBag>>setElements: の実装は次のとおりです。
MoneyBag class >> withAll: aCollectionOfMoney
^ self new setElements: aCollectionOfMoney; yourself
MoneyBag >> setElements: aCollectionOfMoney
elements := aCollectionOfMoney
まず失敗しているテストを通すため、MoneyMag>> = を仮実装にします。
MoneyMag >> = other
^ true
MoneyBag の要素は配列ではなく、辞書で管理しましょう。currency を key に、Money を value として取るようにします。
まず、それに相応しい名前に変更するため、MoneyBag 右クリック → Rename inst var で elements を elemsDict に変えます。
…とここで、確認のために出てきたリファクタリング作業の項目の多さがこれからの困難を予感させますね。いったん Cancel してこの作業はやめておきます。
elements への直接アクセス(ベパp.99、実パp.58)が良くなかったパターンっぽいですね。辞書による管理に切り替える前に、間接アクセス(ベパp.101、実パp.59)に変更しておくのが良さそうです。
MoneyBag 右クリック → Analyze → Inst var references... を選択すると elements を参照しているメソッドの一覧が表示されます。
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.
あとは、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 でこれまでの書き換え履歴の一覧が表示されます。
仮実装にするひとつ手前(2番目)の実装を選択して Revert します。バージョンブラウザは不要なので閉じてしまいます。
元に戻ってはいるのですが、elemsDict に名前を変える前の elements に、しかも直接アクセスしていた時代のコードに戻ってしまうので、改めて self elements(間接アクセス)に変えてから再度コンパイルします。
MoneyBag >> = other
^ other class == self class and: [ other elements = self elements ]
少々歩幅が広すぎましたが、全テストの実行結果はグリーンとなり作業完了です。
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 しましょう。
プロトコルは arithmetic です。
MoneyBag >> + addend
仮引数を addend に変更し、self shouldBeImplemented を消して何もない(self を返す)メソッドをコンパイル(Accept)したら、
デバッガーは閉じてしまいます。
テストは失敗します(イエロー)。テストが期待する結果をそのまま返す仮実装に変えてコンパイルし直します。
MoneyBag >> + addend
^ 5 USD + 10 CHF
期待通り、グリーンが出ます。重複の排除は次のようにします。
MoneyBag >> + addend
^ self class withAll: (self elements copyWith: addend)
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 に答えられずエラー(レッド)になっていることが分かります。
そのままデバッガーのコード枠で、addend が Money の時の処理と条件分けして仮実装を書きます。
Money >> + addend
addend isMoney ifTrue: [
addend currency ~~ currency
ifTrue: [ ^ MoneyBag withAll: {self. addend} ].
^ amount + addend amount perform: currency ].
^ 5 USD + 10 CHF
いったんデバッガーを閉じて、テストが通ることを確認したら、MoneyBag である addend に委譲する形に書き直して重複の排除とします。
Money >> + addend
addend isMoney ifTrue: [
addend currency ~~ currency
ifTrue: [ ^ MoneyBag withAll: {self. addend} ].
^ amount + addend amount perform: currency ].
^ addend + self
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>> + で起こっています。
そのままデバッガー内で、addend が Money か否かで条件分けをした仮実装を書きます。
MoneyBag >> + addend
addend isMoney ifTrue: [ ^ self class withAll: (self elements copyWith: addend) ].
^ 5 USD + 10 CHF
addend の MoneyBag の elements を一つずつ足す作業にして重複を排除します。
MoneyBag >> + addend
addend isMoney ifTrue: [ ^ self class withAll: (self elements copyWith: addend) ].
^ addend elements inject: self into: [ :acc :aMoney | acc + aMoney ]
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}
Squeak や Pharo には、APL や R ライクな…などと言うとおこがましいので、それらから見れば実にささやかな配列演算が組み込みで用意されています。
#(1 2 3) * 4 "=> #(4 8 12) "
重複の排除には、これを使いましょう。
ManeyBag >> * multiplier
^ self class withAll: self elements * multiplier
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)'! !