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") 1
    actualValue := portfolio.Evaluate("KRW")

    assertEqual(t, expectedValue, actualValue)
}
1

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{ 1
        "EUR->USD": 1.2,
        "USD->KRW": 1100,
    }
1

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 1
    return money.amount * exchangeRates[key]
1

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{ 1
        // "EUR->USD": 1.2,
        // "USD->KRW": 1100,
    }
1

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}] 1
--- FAIL: TestAdditionOfDollarsAndEuros (0.00s)
=== RUN   TestAdditionOfDollarsAndWons
    ...   Expected  [{amount:2200 currency:KRW}] Got: [{amount:1100 currency:KRW}] 1
--- FAIL: TestAdditionOfDollarsAndWons (0.00s)
1

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"); 1
    assert.deepStrictEqual(portfolio.evaluate("KRW"), expectedValue);
  }
1

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(); 1
        exchangeRates.set("EUR->USD", 1.2);
        exchangeRates.set("USD->KRW", 1100);
1

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; 1
        return money.amount * exchangeRates.get(key);
1

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); 1
        // exchangeRates.set("USD->KRW", 1100);
1

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") 1
    actualValue = portfolio.evaluate("KRW")
    self.assertEqual(expectedValue, actualValue,
      "%s != %s"%(expectedValue, actualValue))
1

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} 1
1

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 1
            return aMoney.amount * exchangeRates[key]
1

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 = {} 1
1

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset