テスト駆動開発でお試しする Pharo Smalltalk・第7回 MoneyBagに異種通貨同士の加算を保持

[新訳版『テスト駆動開発』](https://estore.ohmsha.co.jp/titles/978427421788P)の第Ⅰ部の多国通貨をネタに Smalltalk を学ぶシリーズの第7回です。同書と似た仕様を目指しますが、好みを優先する等の都合で写経にはなっていませんのでどうぞあしからず。（12/24追記：なるべく忠実な写経は別に書きました→[こちら](https://qiita.com/sumim/items/791984a4702bce91dacd) ）

- [テスト駆動開発でお試しする Pharo Smalltalk・目次](https://qiita.com/sumim/items/3a36c03ab41519077b37)

[前回](https://qiita.com/sumim/items/07f543956904c3cfcf2d) は、同書の流れからは外れ、換算レートのオブジェクト化（MoneyXRate, money exchange rate）とそれを用いた換算を試しました。

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

<br />
#####*参考： 多国通貨実装のおおまかな流れ*
1. Dollar を定義
1. Dollar をコピーして Franc を定義
1. Dollar、Franc の重複を新しく作ったスーパークラスの Money にプルアップ
1. **Sum（Expression）、Bank を定義して reduce を実現** ← 今ここ

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

# Bank に通貨換算レートの登録と問い合わせの機能を持たせる

###### **TODO リスト**
- [ ] 5 USD + 10 CHF = 10 USD （レートが 2 : 1 の場合）
- [x] 10 CHF * (1 USD / 2 CHF) = 5 USD
- [x] 2 USD / 2 = 1 USD
- [x] 2 USD / 1 USD = 2
- [x] 1 USD / 2 CHF は MoneyXRate を返す
- [ ] **Bank>>addXRate: 、xRateOf:**

前回、通貨と換算レートで換算は可能になりましたが、換算レートは本来 Bank に管理される情報なので、そこに蓄積・問い合わせが可能になるようにします。

テストを書きます。

```Smalltalk
MoneyTest >> testBankSettingGettingXRate
	| xRate bank |
	xRate := 1 USD / 2 CHF.
	bank := Bank new.
	bank addXRate: xRate.
	self assert: (bank xRateOf: xRate fromToCurrency) equals: xRate
```

![1027.png](https://qiita-image-store.s3.amazonaws.com/0/49275/c3e1cef9-3bb9-207b-2e08-aaa2142e5d1d.png)

コンパイル（Accept）に際し、Bank の定義を求められますが Define new calss を選んで、下のクラス定義式のまま OK します。

```Smalltalk
Object subclass: #Bank
	instanceVariableNames: ''
	classVariableNames: ''
	package: 'TDD-Money'
```

![1029.png](https://qiita-image-store.s3.amazonaws.com/0/49275/84ea0bc9-a00a-67ce-f90c-5d45f68b77b8.png)

MoneyText 全テストを走らせる（○印クリック）と結果はレッド（エラー）になるので、

![1030.png](https://qiita-image-store.s3.amazonaws.com/0/49275/823e5d6c-1d4c-7bce-07de-6a059e29b4f3.png)

続けて、testBankSettingGettingXRate のみを走らせて、指摘された未定義メソッドごとに Create を押して定義してゆきます。。

![1031.png](https://qiita-image-store.s3.amazonaws.com/0/49275/15b1f4c9-c77e-8745-54d4-ceced149b976.png)

addXRate: は Bank に定義、プロトコルは adding にでもしておきましょう。メソッドは空でもよいのですが、adding 系のメソッドは add した要素をそのまま返すのが慣習なのでとりあえずそうしておきます。

```Smalltalk
Bank >> addXRate: aMoneyXRate 
	^ aMoneyXRate
```

![1032.png](https://qiita-image-store.s3.amazonaws.com/0/49275/242b5111-f7d6-daef-e60d-2e328b91ca3a.png)

fromToCurrency は MoneyXRate に定義します。fromCurrency と toCurrency は実装済みなので表現できなくもないですが、fromToCurrency 自体もあった方が便利ですね（と、しれっと言う）。

プロトコルは accessing です。前回の xRate 同様、インスタンス変数名にメソッド名が一致するので自動でそれを返すコードにしてくれていますので、そのまま Proceed します。


```Smalltalk
MoneyXRate >> fromToCurrency
	^ fromToCurrency
```

![1033.png](https://qiita-image-store.s3.amazonaws.com/0/49275/abc11463-cdc9-278d-0306-6844aa161db7.png)

次に未定義を指摘される xRateOf: は Bank に、プロトコルは accessing で定義します。テストを失敗させるため、1 USD / 1 CHF を返すコードにするのをお忘れなく。

```Smallltalk
Bank >> xRateOf: anAssociation 
	^ 1 USD / 1 CHF
```

![1034.png](https://qiita-image-store.s3.amazonaws.com/0/49275/ff0ad7d5-a927-9277-44f3-a6e82fa72ec2.png)

全テストを実行して失敗（イエロー）を確認できたら 1 USD / 2 CHF を返す仮実装に変えてテストを通します。

```Smallltalk
Bank >> xRateOf: anAssociation 
	^ 1 USD / 2 CHF
```

![1035.png](https://qiita-image-store.s3.amazonaws.com/0/49275/6113ac8d-da75-54c1-cf47-67568831bcf4.png)

Bank>>addXRate: 、xRateOf: は、辞書をインスタンス変数 xRateDict として持たせておき、それに対して格納と問い合わせることで実現しましょう。

```Smalltalk
Bank >> addXRate: aMoneyXRate
	^ xRateDict at: aMoneyXRate fromToCurrency put: aMoneyXRate
```

コンパイルに際して xRateDict の未宣言を指摘されるので Declare new instance variable します。

![1036.png](https://qiita-image-store.s3.amazonaws.com/0/49275/187e639d-d93f-2b3d-bd85-a0074ae677bd.png)

忘れないうちに、Bank の右クリック → Analyze → Generate initialize method で初期化メソッドを定義し、Dictionay new をセットしておきます。

![1037.png](https://qiita-image-store.s3.amazonaws.com/0/49275/3ad1dd04-50b7-d599-d32a-f73edffecbd2.png)

```Smalltalk
Bank >> initialize

	super initialize.

	xRateDict := Dictionary new.
```

![1038.png](https://qiita-image-store.s3.amazonaws.com/0/49275/c1e8a932-c2ef-6862-06ad-992c09fbf4ed.png)

xRateOf: は引数として与えられた「換算前の通貨 -> 換算後の通貨」を保持する key-value 値（anAssociation）をキーにして、辞書 xRateDict から該当する換算レート（MoneyXRate）を探して返します。同種通貨換算の場合（anAssociation の key と value が一致する場合）は通貨変換は不要なので 1 を返します。

```Smalltalk
Bank >> xRateOf: anAssociation
	anAssociation key == anAssociation value ifTrue: [ ^ 1 ]. 
	^ xRateDict at: anAssociation
```

![1040.png](https://qiita-image-store.s3.amazonaws.com/0/49275/b60947f2-e458-6669-7f15-837fb152e978.png)

テストはグリーンのままです。

###### **TODO リスト**
- [ ] 5 USD + 10 CHF = 10 USD （レートが 2 : 1 の場合）
- [x] 10 CHF * (1 USD / 2 CHF) = 5 USD
- [x] 2 USD / 2 = 1 USD
- [x] 2 USD / 1 USD = 2
- [x] 1 USD / 2 CHF は MoneyXRate を返す
- [x] **Bank>>addXRate: 、xRateOf:**


# 通貨変換を Bank に任せる

###### **TODO リスト**
- [ ] 5 USD + 10 CHF = 10 USD （レートが 2 : 1 の場合）
- [x] 10 CHF * (1 USD / 2 CHF) = 5 USD
- [x] 2 USD / 2 = 1 USD
- [x] 2 USD / 1 USD = 2
- [x] 1 USD / 2 CHF は MoneyXRate を返す
- [x] Bank>>addXRate: 、xRateOf:
- [ ] **Bank>>exchange: to:**

testExchangeMoney を書き換えます。

```Smalltalk
MoneyTest >> testExchangeMoney
	| bank |
	bank := Bank new.
	bank addXRate: 1 USD / 2 CHF.
	self assert: (bank exchange: 10 CHF to: #USD) equals: 5 USD
```

reduce というのはピンと来なかったので（知らないだけで、通貨換算をそう表現する言い回しがあるのかもしれませんが…）ここではテストのメソッド名は testReduceMoney には変えずに前回のとおり testExchangeMoney のままで、Bank のメソッド名も exchange: to: にしてみました。

![1041.png](https://qiita-image-store.s3.amazonaws.com/0/49275/6b37f952-38e2-99f8-1144-c5aa2bedf82a.png)

コンパイルは通りますが、テストはレッド（エラー）です。testExchangeMoney のみ実行して exchange: to: を Bank に追加します。プロトコルは適当に exchanging とでもします。

```Smalltalk
Bank >> exchange: aMoney to: toCurrency
	^ 1 USD
```

テストを失敗させるため、返値は 1 USD にします。

![1043.png](https://qiita-image-store.s3.amazonaws.com/0/49275/84859db3-1d03-a8d0-f685-62a708c5f4cb.png)

失敗が確認できたら、5 USD を返す仮実装に書き換えます。

```Smalltalk
Bank >> exchange: aMoney to: toCurrency
	^ 5 USD
```

![1044.png](https://qiita-image-store.s3.amazonaws.com/0/49275/680caaa3-3282-34aa-bd9c-dd385279aaa1.png)

重複の排除はこのようにします。

```Smalltalk
Bank >> exchange: aMoney to: toCurrency 
	^ aMoney * (self xRateOf: aMoney currency -> toCurrency)
```

![1045.png](https://qiita-image-store.s3.amazonaws.com/0/49275/a1f147ca-0a90-73c9-6df9-70547f20703d.png)

###### **TODO リスト**
- [ ] 5 USD + 10 CHF = 10 USD （レートが 2 : 1 の場合）
- [x] 10 CHF * (1 USD / 2 CHF) = 5 USD
- [x] 2 USD / 2 = 1 USD
- [x] 2 USD / 1 USD = 2
- [x] 1 USD / 2 CHF は MoneyXRate を返す
- [x] Bank>>addXRate: 、xRateOf:
- [x] **Bank>>exchange: to:**

# 同種通貨同士の加算

###### **TODO リスト**
- [ ] 5 USD + 10 CHF = 10 USD （レートが 2 : 1 の場合）
- [x] 10 CHF * (1 USD / 2 CHF) = 5 USD
- [x] 2 USD / 2 = 1 USD
- [x] 2 USD / 1 USD = 2
- [x] 1 USD / 2 CHF は MoneyXRate を返す
- [x] Bank>>addXRate: 、xRateOf:
- [x] Bank>>exchange: to:
- [ ] **3 USD + 4 USD = 7 USD**

次に異種通貨同士の加算の準備として、同種通貨の加算を実装します。まずはテストです。

```Smalltalk
MoneyTest >> testSimpleAddition
	self assert: 3 USD + 4 USD equals: 7 USD
```

全テストを実行するとエラー（レッド）になるので、

![1047.png](https://qiita-image-store.s3.amazonaws.com/0/49275/d00fc7e7-faac-efa0-2bf8-79ba3ed82974.png)

testSimpleAddition を個別に実行して Money>> + をプロトコル arithmetic に追加します。仮引数は addend に変更し、テストを失敗させるためにメソッド本体は空にしてコンパイルします（self が返ります）。

```Smalltalk
Money >> + addend
```

![1048.png](https://qiita-image-store.s3.amazonaws.com/0/49275/e97bf82b-5af9-255f-1770-fa682bfd5c57.png)

仮実装として、7 USD が返るように Money>> + を書き直してコンパイルします。

```Smalltalk
Money >> + addend
	^ 7 USD
```

![1049.png](https://qiita-image-store.s3.amazonaws.com/0/49275/fe18f3d2-6f10-e451-7806-db6ccf88c4b7.png)

重複の排除は次のようにします。

```Smalltalk
Money >> + addend
	^ amount + addend amount perform: currency
```

![1050.png](https://qiita-image-store.s3.amazonaws.com/0/49275/b07ce69e-de59-b227-baa6-0d363323dcf2.png)

###### **TODO リスト**
- [ ] 5 USD + 10 CHF = 10 USD （レートが 2 : 1 の場合）
- [x] 10 CHF * (1 USD / 2 CHF) = 5 USD
- [x] 2 USD / 2 = 1 USD
- [x] 2 USD / 1 USD = 2
- [x] 1 USD / 2 CHF は MoneyXRate を返す
- [x] Bank>>addXRate: 、xRateOf:
- [x] Bank>>exchange: to:
- [x] **3 USD + 4 USD = 7 USD**


# 異種通貨の加算は MoneyBag を返す

###### **TODO リスト**
- [ ] 5 USD + 10 CHF = 10 USD （レートが 2 : 1 の場合）
- [x] 10 CHF * (1 USD / 2 CHF) = 5 USD
- [x] 2 USD / 2 = 1 USD
- [x] 2 USD / 1 USD = 2
- [x] 1 USD / 2 CHF は MoneyXRate を返す
- [x] Bank>>addXRate: 、xRateOf:
- [x] Bank>>exchange: to:
- [x] 3 USD + 4 USD = 7 USD
- [ ] **5 USD + 10 CHF は MoneyBag を返す**

では次に、異種通貨の加算の場合です。いよいよ MoneyBag の登場です。まずテストを書きます。

```Smalltalk
MoneyTest >> testPlusDifferentCurrencyReturnsMoneyBag
	| sum |
	sum := 5 USD + 10 CHF.
	self assert: sum class equals: MoneyBag
```

コンパイルに際して、MoneyBag の定義を要求されますので Declear new class を選んでそのまま OK します。

```Smalltalk
Object subclass: #MoneyBag
		instanceVariableNames: '' 
		classVariableNames: ''
		category: 'TDD-Money'
```

![1051.png](https://qiita-image-store.s3.amazonaws.com/0/49275/18f06632-b453-8c48-9ea9-f9fd54a38a67.png)

全テスト実行すると失敗するので、Money>> + を次の仮実装に書き換えてコンパイルし直します。

```Smalltalk
Money >> + addend
	addend currency ~~ currency
		ifTrue: [ ^ MoneyBag new ].
	^ amount + addend amount perform: currency
```

![1052.png](https://qiita-image-store.s3.amazonaws.com/0/49275/998d01fd-a615-f1fb-96cc-026154d67a3c.png)


テストは通りますが、まだ 何の情報も持たない MoneyBag が返ってきているだけです。

MoneyBag は elements として加算の要素を持ちます。set で始まるコンストラクターパラメーターメソッド（[ベパ](https://www.amazon.co.jp/dp/4894717549/)p27）を private プロトコルに追加し、インスタンス生成時に使いましょう。

![1053.png](https://qiita-image-store.s3.amazonaws.com/0/49275/70eea740-6489-1017-a80a-cabf1c45ff4f.png)

![1054.png](https://qiita-image-store.s3.amazonaws.com/0/49275/b972d734-685e-62e9-dda2-e75d847f1f1a.png)

```Smalltalk
MoneyBag >> setElements: aCollectionOfMoney
	elements := aCollectionOfMoney
```

![1055.png](https://qiita-image-store.s3.amazonaws.com/0/49275/e01c6012-66ec-540d-b053-421b0b051c3f.png)

コンパイル時に elements が未宣言であることを指摘されますので、Declare new instance variable でインスタンス変数に追加してもらいます。

![1056.png](https://qiita-image-store.s3.amazonaws.com/0/49275/093e85e9-f026-f133-1a2b-58bbdfe1d340.png)

ゲッターも定義しておきましょう。普通にコード枠に次のコードを入力してコンパイルしてもよいですし、MoneyBag を右クリック → Refactoring → Inst Var Refactoring → Accessors → elements をクリック → Ok で自動生成するのもよしです。

![1057.png](https://qiita-image-store.s3.amazonaws.com/0/49275/82bc13d5-68e2-8801-cafa-d24ee975ba71.png)

```Smalltalk
MoneyBag >> elements
	^ elements
```

あと、private な setElements: を MoneyBag の外で使わずに済むように、MoneyBag class（MoneyBag のメタクラス）に withAll: というインスタンス生成用のクラスメソッドも定義しておきます。

クラスブラウザのクラスリスト枠下にある Class ボタンをクリックしてクラスサイドに切り替え、プロトコルは instance creation を設け、そこに定義します。

```Smalltalk
MoneyBag class >> withAll: aCollectionOfMoney
	^ self new setElements: aCollectionOfMoney; yourself
```

![1027.png](https://qiita-image-store.s3.amazonaws.com/0/49275/520d659b-5cf6-ebd2-a639-d0329e81e693.png)

再び Class ボタンを押してインスタンスサイドに忘れずに戻しておきます。

準備が整ったので MoneyBag 生成時に必要な情報を付加するように Money>> + を書き換えます。

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


![1028.png](https://qiita-image-store.s3.amazonaws.com/0/49275/e12bbaf6-d410-4aaf-fd86-1768fe792c40.png)


###### **TODO リスト**
- [ ] 5 USD + 10 CHF = 10 USD （レートが 2 : 1 の場合）
- [x] 10 CHF * (1 USD / 2 CHF) = 5 USD
- [x] 2 USD / 2 = 1 USD
- [x] 2 USD / 1 USD = 2
- [x] 1 USD / 2 CHF は MoneyXRate を返す
- [x] Bank>>addXRate: 、xRateOf:
- [x] Bank>>exchange: to:
- [x] 3 USD + 4 USD = 7 USD
- [x] **5 USD + 10 CHF は MoneyBag を返す**


# 異種通貨の加算を Bank に簡約・換算させる

当初の目的である、異種通貨の和を指定した通貨で換算しつつ簡約する処理を実装します。

###### **TODO リスト**
- [ ] **5 USD + 10 CHF = 10 USD （レートが 2 : 1 の場合）**
- [x] 10 CHF * (1 USD / 2 CHF) = 5 USD
- [x] 2 USD / 2 = 1 USD
- [x] 2 USD / 1 USD = 2
- [x] 1 USD / 2 CHF は MoneyXRate を返す
- [x] Bank>>addXRate: 、xRateOf:
- [x] Bank>>exchange: to:
- [x] 3 USD + 4 USD = 7 USD
- [x] 5 USD + 10 CHF は MoneyBag を返す

テストを書きます。

```Smalltalk
MoneyTest >> 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
```

エラー（レッド）になります。

testExchangeMoneyDifferentCurrency のみ走らせて出てきたノーティファイアーで（Create ではなく） Dubug をクリックして開くデバッガーを見ると、Money を想定した仮引数 aMoney に MoneyBag が渡り、それに currency の問い合わせが行って MessageNotUnderstood になっているのが分かります。

![1060.png](https://qiita-image-store.s3.amazonaws.com/0/49275/65db3ff6-e4bc-a55b-8617-29a147e52a8a.png)

このまま Bank>>exchange: to: を次のように適切に書き換えましょう。

```Smalltalk
Bank >> exchange: aMoneyOrBag to: toCurrency 
	^ aMoneyOrBag exchangeTo: toCurrency with: self
```

![1061.png](https://qiita-image-store.s3.amazonaws.com/0/49275/9c601d29-39f6-80d7-d0ee-1277e2c80ca7.png)

Proceed すると、先ほどの定義に含まれた exchangeTo:with: が未定義との指摘（MessageNotUnderstood: MoneyBag>>exchangeTo:with:）を受けるので（今度は Debug ボタンではなく） Create ボタンをクリックします。プロトコルは exchanging でよいでしょう。


```Smalltalk
MoneyBag >> exchangeTo: toCurrency with: aBank
	^ elements
		inject: (0 perform: toCurrency)
		into: [ :acc :aMoney | acc + (aMoney exchangeTo: toCurrency with: aBank) ]
```

Proceed するとさらに Money にも同名メソッドの定義を促されますので同様に Create します。プロトコルは同じく exchanging 。

```Smalltalk
Money >> exchangeTo: toCurrency with: aBank 
	^ self * (aBank xRateOf: currency -> toCurrency)
```

![1062.png](https://qiita-image-store.s3.amazonaws.com/0/49275/b7447a0a-c005-55e3-5297-dfa6f085da1d.png)

###### **TODO リスト**
- [x] **5 USD + 10 CHF = 10 USD （レートが 2 : 1 の場合）**
- [x] 10 CHF * (1 USD / 2 CHF) = 5 USD
- [x] 2 USD / 2 = 1 USD
- [x] 2 USD / 1 USD = 2
- [x] 1 USD / 2 CHF は MoneyXRate を返す
- [x] Bank>>addXRate: 、xRateOf:
- [x] Bank>>exchange: to:
- [x] 3 USD + 4 USD = 7 USD
- [x] 5 USD + 10 CHF は MoneyBag を返す


#ここまでのまとめと補足
- 5 USD + 5 USD（同種通貨の和）は常に Money を返す
- 未定義メソッドのコールがあるメソッドのコンパイル時、細かく警告を出してその都度候補を提示してくる Squeak と違い、何も言わずにコンパイルを通す Pharo は、開発の軽快なテンポを保つのに役立ち、TDD とも相性がよい。しかしタイプミスのチェックくらいはやって欲しくもあり、双方を両立できないのがもどかしい。
- Bank>>exchange: to: から exchangeTo:with: の流れはダブルディスパッチ。
- inject:into: は、たとえば #(1 2 3) inject: 0 into: [ :sum :x | sum + x ] "=> 6 " のように動作する、他言語では reduce とか fold（fold left）に相当するメソッドで、第一引数に初期値を第二引数に二変数の無名関数（Pharo は不可だが、Squeak ように二項メッセージセレクターを許す処理系もある →  #(1 2 3) inject: 0 into: #+ ）を与えて使う。
- MoneyBag は、実は Bag になっていない。（次回、直す予定）


#この時点のコード
ファイルアウト形式のソースです。（この形式で Smalltalk のソースを読むことはあまり推奨されません。為念）

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

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

[[前回からの差分はこちら](https://github.com/sumim/tdd-money/commit/34b22d6f9c3bc9bfb08b348cbeb57e94b2ae17f3)]

```Smalltalk
!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/14/2017 18: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/16/2017 08:59'!
+ addend
	addend currency ~~ currency
		ifTrue: [ ^ MoneyBag withAll: {self. addend} ].
	^ amount + addend amount perform: currency! !


!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: 'elements'
	classVariableNames: ''
	poolDictionaries: ''
	category: 'TDD-Money'!


!MoneyBag methodsFor: 'accessing' stamp: 'sumim 12/16/2017 08:58'!
elements
	^ elements! !


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


!MoneyBag methodsFor: 'private' stamp: 'sumim 12/16/2017 08:56'!
setElements: aCollectionOfMoney
	elements := aCollectionOfMoney! !


!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'!
xRate
	^ xRate! !


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


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


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


!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/16/2017 08:51'!
testPlusDifferentCurrencyReturnsMoneyBag
	| sum |
	sum := 5 USD + 10 CHF.
	self assert: sum class equals: MoneyBag! !


!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! !
```