• Search in book...
• Toggle Font Controls

# Chapter 9. Currencies, Currencies, Everywhere

Small change, small wonders — these are the currency of my endurance and ultimately of my life.

Barbara Kingsolver

Here’s the current state of our evaluate feature vis-à-vis `Money` s in a `Portfolio`:

1. When “converting” a `Money` in a currency to the same currency, it returns the `amount` of the `Money`. This is correct: the exchange rate for any currency to itself is 1.

2. In all other cases, the `amount` of the `Money` is multiplied by a fixed number (1.2). This is correct in a very limited sense: this rate ensures conversions from USD to EUR only. There is no way to modify this exchange rate or specify any other rate.

Our currency conversion code does one thing correctly and another thing almost correctly. It’s time to make it work correctly in both cases. In this chapter, we’ll introduce — at long last — the conversion of money from one currency into another using currency-specific exchange rates.

# Making a Hash(map) of Things

What we need is a hashmap that allows us to look up exchange rates given a “from” currency and a “to” currency. The hashmap would be a representation of an exchange rate table we regularly see in banks and currency exchange counters at airports, as shown in Table 9-1.

Table 9-1. Exchange rate table
From To Rate

EUR

USD

1.2

USD

EUR

0.82

USD

KRW

1100

KRW

USD

0.00090

EUR

KRW

1344

KRW

EUR

0.00073

To read this table, use this pattern: given an amount in “From” currency, multiply with the “Rate” to get the equivalent amount in “To” currency.

As noted in Chapter 8, the mutual rates for any pair of currencies are not arithmetical reciprocals of each other. 1 Let’s use an example to illustrate this point: based on the rates given in Table 9-1, if we convert 100 EUR to USD and back to EUR, we’ll get 98.4 EUR; not the original 100 EUR we started with. This is common for exchange rate tables; it’s one way how banks make money! 2

The next couple of items on our feature list give us the opportunity to build out an implementation of the exchange rate table in our code. We’ll do this by introducing a new currency.

 5 USD x 2 = 10 USD 10 EUR x 2 = 20 EUR 4002 KRW / 4 = 1000.5 KRW 5 USD + 10 USD = 15 USD Separate test code from production code Remove redundant tests 5 USD + 10 EUR = 17 USD 1 USD + 1100 KRW = 2200 KRW Determine exchange rate based on the currencies involved (from → to) Allow exchange rates to be modified

As we introduce the additional currency, we’ll see the Transformation Priority Premise in action. That is, instead of adding more conditional code in a Tower of Babel style if-else chain, we’ll introduce a new data structure that allows us to look up the exchange rate.3

###### Tip

The Transformation Priority Premise states that as tests get more specific, the production code gets more generic through a series of transformations.

## Go

Let’s write a new test. This test will also involve multiple currencies — like our last one. We’ll name it after the two currencies used in this case.

````func`` ``TestAdditionOfDollarsAndWons``(``t`` ``*``testing``.``T``)`` ``{````
````    ``var`` ``portfolio`` ``s``.``Portfolio````
``````
````    ``oneDollar`` ``:=`` ``s``.``NewMoney``(``1``,`` ``"USD"``)````
````    ``elevenHundredWon`` ``:=`` ``s``.``NewMoney``(``1100``,`` ``"KRW"``)````
``````
````    ``portfolio`` ``=`` ``portfolio``.``Add``(``oneDollar``)````
````    ``portfolio`` ``=`` ``portfolio``.``Add``(``elevenHundredWon``)````
``````
````    ``expectedValue`` ``:=`` ``s``.``NewMoney``(``2200``,`` ``"KRW"``)`` ````
````actualValue`` ``:=`` ``portfolio``.``Evaluate``(``"KRW"``)````
``````
````    ``assertEqual``(``t``,`` ``expectedValue``,`` ``actualValue``)````
````}````

The expected value of 2200 KRW assumes that we get 1100 won for every dollar we convert

The test fails, of course. The error message is interesting:

``...` `Expected`  `[{``amount``:``2200` `currency``:``KRW``}]` `Got``:` `[{``amount``:``1101.2` `currency``:``KRW``}]``

Since we don’t yet have any mechanism to choose the correct exchange rates, our `convert` method chose the incorrect `eurToUsd` rate, producing the odd result of “1101.2 KRW”.

Let’s introduce a `map[string]float64` to represent the exchange rates. We will initialize this map with the two exchange rates needed by our tests: EUR→USD = 1.2 and USD→KRW = 1100. For now, let’s keep this map local to the `convert` method:

````    ``exchangeRates`` ``:=`` ``map``[``string``]``float64``{`` ````
````"EUR->USD"``:`` ``1.2``,````
````        ``"USD->KRW"``:`` ``1100``,````
````    ``}````

In `convert` method, at the very top

Instead of always multiplying `money.amount` by `eurToUsd` (which is 1.2) in `convert`, we can use the “from” and “to” currencies to create a key and look up the exchange rate. We delete the line defining the `eurToUsd` variable and replace the final `return` statement with this lookup and calculation:

````    ``key`` ``:=`` ``money``.``currency`` ``+`` ``"->"`` ``+`` ``currency`` ````
````return`` ``money``.``amount`` ``*`` ``exchangeRates``[``key``]````

In `convert` method, at the very bottom

With these changes to the `convert` method, all our tests pass.

Out of curiosity: what will happen if we try to evaluate a `Portfolio` in a currency for which the relevant exchange rates are not specified? Let’s momentarily comment out both the entries in the exchangeRates map:

````    ``exchangeRates`` ``:=`` ``map``[``string``]``float64``{`` ````
``````// "EUR->USD": 1.2,
````        ````// "USD->KRW": 1100,
````    ``}````

Temporarily comment out all entries in `exchangeRates` as an experiment

When we run the tests now, we get assertion errors in both of our addition tests:

````==``=`` ``RUN``   ``TestAdditionOfDollarsAndEuros````
````    ``...``   ``Expected``  ``[``{``amount``:``17`` ``currency``:``USD``}``]`` ``Got``:`` ``[``{``amount``:``5`` ``currency``:``USD``}``]`` ````
````--``-`` ``FAIL``:`` ``TestAdditionOfDollarsAndEuros`` ``(``0.00``s``)````
````==``=`` ``RUN``   ``TestAdditionOfDollarsAndWons````
````    ``...``   ``Expected``  ``[``{``amount``:``2200`` ``currency``:``KRW``}``]`` ``Got``:`` ``[``{``amount``:``1100`` ``currency``:``KRW``}``]`` ````
````--``-`` ``FAIL``:`` ``TestAdditionOfDollarsAndWons`` ``(``0.00``s``)````

With no entries in `exchangeRates` a value of 0 is used in every call to `convert` method

It’s clear from the actual values (printed after `Got:`) that when an entry isn’t found in our map, an exchange rate of 0 is used; effectively burning the `Money` that needs to be converted into an ash pile!

###### Important

In Golang, an attempt to get a map entry with a non-existent key will return the “default zero” value. E.g. 0 (or 0.0) for `int` or `float`, `false` for `boolean`, empty string for `string`, etc.

Looks like we need better error handling. We’ll add this to our feature list. (Let’s not forget to revert the two commented out lines of code!)

## JavaScript

Let’s write a test in `test_money.js` for our new scenario, converting Dollars to Wons.

````  ``testAdditionOfDollarsAndWons``(``)`` ``{````
````let`` ``oneDollar`` ``=`` ``new`` ``Money``(``1``,`` ``"USD"``)``;````
````let`` ``elevenHundredWon`` ``=`` ``new`` ``Money``(``1100``,`` ``"KRW"``)``;````
````let`` ``portfolio`` ``=`` ``new`` ``Portfolio``(``)``;````
````portfolio``.``add``(``oneDollar``,`` ``elevenHundredWon``)``;````
````let`` ``expectedValue`` ``=`` ``new`` ``Money``(``2200``,`` ``"KRW"``)``;`` ````
````assert``.``deepStrictEqual``(``portfolio``.``evaluate``(``"KRW"``)``,`` ``expectedValue``)``;````
````}````

The expected value of 2200 KRW assumes that we get 1100 won for every dollar we convert

The test fails with an interesting error message:

````Running``:` `testAdditionOfDollarsAndWons``()`
`AssertionError` `[``ERR_ASSERTION``]``:` `Expected` `values` `to` `be` `strictly` `deep``-``equal``:`
`+` `actual` `-` `expected`

`Money` `{`
`+`   `amount``:` `1101.2``,`
`-`   `amount``:` `2200``,`
`currency``:` `'KRW'`
`}````

The `convert` method is using the incorrect `eurToUSD` rate, even though we don’t have any Euros in our test. That’s how we ended up with the funny amount of “1101.2”.

Let’s introduce a `Map` to represent the exchange rates. The two entries we define in the map are those needed by our tests: EUR→USD = 1.2 and USD→KRW = 1100. For now, we’ll keep this map inside the `convert` method:

````        ``let`` ``exchangeRates`` ``=`` ``new`` ``Map``(``)``;`` ````
````exchangeRates``.``set``(``"EUR->USD"``,`` ``1.2``)``;````
````exchangeRates``.``set``(``"USD->KRW"``,`` ``1100``)``;````

In `convert` method, at the top

We can delete the line defining the `eurToUsd` variable and use this `exchangeRates` map instead. We use the “from” and “to” currencies to create a key and look up the exchange rate. The last two lines of `convert` embody this logic:

````        ``let`` ``key`` ``=`` ``money``.``currency`` ``+`` ``"->"`` ``+`` ``currency``;`` ````
````return`` ``money``.``amount`` ``*`` ``exchangeRates``.``get``(``key``)``;````

In `convert` method, at the bottom

With this improvement, all our tests are green again.

What if we try to evaluate a `Portfolio` in a currency for which the relevant exchange rates are unspecified? Let’s momentarily comment out both the entries in the exchangeRates Map:

````        ``// exchangeRates.set("EUR->USD", 1.2); ````
````        ``// exchangeRates.set("USD->KRW", 1100);````

Temporarily comment out all entries in `exchangeRates` as an experiment

Both of our addition tests fail with assertion errors.

````Running``:` `testAdditionOfDollarsAndEuros``()`
`AssertionError` `[``ERR_ASSERTION``]``:` `Expected` `values` `to` `be` `strictly` `deep``-``equal``:`
`+` `actual` `-` `expected`

`Money` `{`
`+`   `amount``:` `NaN``,`
`-`   `amount``:` `17``,`
`currency``:` `'USD'`
`}`
`...`
`Running``:` `testAdditionOfDollarsAndWons``()`
`AssertionError` `[``ERR_ASSERTION``]``:` `Expected` `values` `to` `be` `strictly` `deep``-``equal``:`
`+` `actual` `-` `expected`

`Money` `{`
`+`   `amount``:` `NaN``,`
`-`   `amount``:` `2200``,`
`currency``:` `'KRW'`
`}````

When an entry isn’t found in our Map, the exchangeRate lookup value is `undefined`. The arithmetic operation of multiplying a number (`money.amount`) with this `undefined` is “not a number” (i.e. `NaN`).

###### Important

In JavaScript, an attempt to get a map entry with a non-existent key will always return `undefined` as the value.

Let’s revert the two commented out lines to get back to a green test suite. We’ll add the need for better error handling to our feature list.

## Python

Let’s write a test in `test_money.py` to reflect our new feature: converting Dollars to Wons.

````  ``def`` ``testAdditionOfDollarsAndWons``(``self``)``:````
````    ``oneDollar`` ``=`` ``Money``(``1``,`` ``"``USD``"``)````
````    ``elevenHundredWon`` ``=`` ``Money``(``1100``,`` ``"``KRW``"``)````
````    ``portfolio`` ``=`` ``Portfolio``(``)````
````    ``portfolio``.``add``(``oneDollar``,`` ``elevenHundredWon``)````
````    ``expectedValue`` ``=`` ``Money``(``2200``,`` ``"``KRW``"``)`` ````
````    ``actualValue`` ``=`` ``portfolio``.``evaluate``(``"``KRW``"``)````
````    ``self``.``assertEqual``(``expectedValue``,`` ``actualValue``,````
````      ``"``%s`` != ``%s``"``%``(``expectedValue``,`` ``actualValue``)``)````

The expected value of 2200 KRW assumes that we get 1100 won for every dollar we convert

This test predictably fails. The error message gives an insight as to what’s wrong:

``AssertionError``:` `...` `KRW` `2200.00` `!=` `KRW` `1101.20``

The `__convert` method is using the rate `eurToUsd`, which is incorrect for this case. That’s where the peculiar amount `1101.20` comes from.

Let’s introduce a dictionary to store exchange rates. We’ll add the two entries we need currently: EUR→USD = 1.2 and USD→KRW = 1100. We’ll keep this dictionary in the `__convert` method to begin with:

``        ``exchangeRates`` ``=`` ``{``'``EUR->USD``'``:`` ``1.2``,`` ``'``USD->KRW``'``:`` ``1100``}`` ``

In `__convert` method, at the top

We can delete the `self.eur_to_usd` variable and use the values in this dictionary instead. We create a key using the “from” and “to” currencies and look up the exchange rate. The `else:` block in `__convert` changes to the code shown below:

````        ``else``:````
````            ``key`` ``=`` ``aMoney``.``currency`` ``+`` ``'``->``'`` ``+`` ``aCurrency`` ````
````            ``return`` ``aMoney``.``amount`` ``*`` ``exchangeRates``[``key``]````

In `__convert` method, at the bottom

With these changes, all our tests turn green again.

Out of curiosity: what if we try to evaluate a `Portfolio` in a currency when the necessary exchange rates are not specified? Let’s temporarily remove all entries from the `exchangeRates` map in the `convert` method, making it empty:

``        ``exchangeRates`` ``=`` ``{``}`` ``

Temporarily delete all entries in `exchangeRates` as an experiment

When we run our tests, both the addition tests fail with `KeyError`s:

````ERROR``:` `testAdditionOfDollarsAndEuros` `(``__main__``.``TestMoney``)`
`...`
`KeyError``:` `'EUR->USD'`
`...`
`ERROR``:` `testAdditionOfDollarsAndWons` `(``__main__``.``TestMoney``)`
`...`
`KeyError``:` `'USD->KRW'````

In Python, missing keys in dictionary cause KeyErrors when a lookup is performed.

###### Important

In Python, an attempt to get a dictionary entry via the key-lookup operator `[]` with a non-existent key will always raise a KeyError.

We need to improve error handling in our code. We’ll add this to our feature list. (Let’s not forget to restore the `exchangeRates` dictionary with the two values!)

# Committing our changes

We now have the ability to define multiple exchange rates and convert between arbitrary currencies accordingly. Our Git commit message should reflect this new feature:

```git add .
git commit -m `"feat: convert between any currencies with defined exchange rates"````

# Where We Are

Our code has progressed to the point where we can maintain a `Portfolio` of disparate `Money` s and evaluate it in multiple currencies, as long as the necessary exchange rates are known. That’s nothing to sneer at!

We’ve also identified a need for more robust error handling, particularly when exchange rates are not specified. We’ll add this to our list and turn our attention to it in Chapter 10.

 5 USD x 2 = 10 USD 10 EUR x 2 = 20 EUR 4002 KRW / 4 = 1000.5 KRW 5 USD + 10 USD = 15 USD Separate test code from production code Remove redundant tests 5 USD + 10 EUR = 17 USD 1 USD + 1100 KRW = 2200 KRW# Determine exchange rate based on the currencies involved (from → to) Improve error handling when exchange rates are unspecified Allow exchange rates to be modified

1 The arithmetic reciprocal of a fraction a/b is the fraction b/a, assuming that neither a nor b is zero. For example: the reciprocal of 6/5 (i.e. 1.2) is 5/6 (~0.833).

2 This is putatively more legal than the “penny rounding subroutine” that the three protagonists use to make money in the now-classic movie Office Space!

3 Fred Brooks has analyzed the Biblical Tower of Babel narrative in a chapter of his classical book “The Mythical Man Month”. Brooks said that the Tower project failed because of lack of clear communication and organization — two things that are also missing from a long chain of if-else statements.

• No Comment
..................Content has been hidden....................