Money itself isn’t lost or made, it’s simply transferred from one perception to another. Like magic.
Gordon Gekko, Wall Street (the movie)
We’ve dallied around the question of how to convert the several Money
s in a Portfolio
into a single currency
. Let’s dally no longer!
The next feature on our list is the one dealing with mixed currencies:
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 |
A heterogeneous combination of currencies demands that we create a new abstraction in our code: the conversion of money from one currency to another. This requires establishing some ground rules about currency conversions, drawn from our problem domain:
Conversion always relates a pair of currencies. This is important because we want all conversions to be independent. It does happen in reality that multiple currencies are “pegged” to one single currency — which means that a particular exchange rate is fixed de jure. 1 Even in such cases, it’s important to treat each pegged relationship as a distinct pair.
Conversion is from one currency to another with a well defined exchange rate. The exchange rate — the number of units of the “to” currency we get for one unit of the “from” currency — is a key component of currency conversion. The exchange rate is represented by a fractional number.
The two exchange rates between a pair currencies may or may not be arithmetical reciprocals of each other. For example: the exchange rate from EUR to USD may or may not be the mathematical reciprocal (i.e. 1/x) of the exchange rate from USD to EUR.
It is possible for a currency to have no defined exchange rate to another currency. This could be because one of the two currencies is an inconvertible currency. 2
Given that currency conversion involves all the above considerations, how should we implement it? The answer is: one test-driven scenario at a time!
We’ll start by test-driving the scenario listed in the next item on our feature list: the conversion from EUR to USD. This will help us set up the scaffolding for the “convert” method and a single exchange rate from EUR to USD. Because exchange rates are unidirectional, we’ll represent this particular one as “EUR→USD”.
Starting with this one scenario means it’s likely that we’ll add more items to our feature list. That’s all right — making controlled progress at a measured pace isn’t a bad deal!
Let’s write our new test in money_test.go
to represent addition of Dollars and Euros:
func
TestAdditionOfDollarsAndEuros
(
t
*
testing
.
T
)
{
var
portfolio
s
.
Portfolio
fiveDollars
:=
s
.
NewMoney
(
5
,
"USD"
)
tenEuros
:=
s
.
NewMoney
(
10
,
"EUR"
)
portfolio
=
portfolio
.
Add
(
fiveDollars
)
portfolio
=
portfolio
.
Add
(
tenEuros
)
expectedValue
:=
s
.
NewMoney
(
17
,
"USD"
)
actualValue
:=
portfolio
.
Evaluate
(
"USD"
)
assertEqual
(
t
,
expectedValue
,
actualValue
)
}
The test creates two Money
struct`s representing 5 USD and 10 EUR, respectively. They are added to a newly created `Portfolio
struct
. The actualValue
from evaluating the Portfolio in dollars is compared with the expectedValue
struct
of 17 USD.
The test fails as expected:
...
Expected
[{
amount
:
17
currency
:
USD
}]
Got
:
[{
amount
:
15
currency
:
USD
}]
This validates what we know: the evaluate
method simply adds the amounts of all Money
`struct`s (5 and 10 in our test) to get the result, regardless of the currencies involved (USD and EUR, respectively, in our test).
What we need is to first convert the amount of each Money into the target currency and then add it.
for
_
,
m
:=
range
p
{
total
=
total
+
convert
(
m
,
currency
)
}
How should we write the convert
method? The simplest thing that works is to return the amount
when the currencies match and to arbitrarily multiply by the conversion rate required by our test otherwise:
func
convert
(
money
Money
,
currency
string
)
float64
{
if
money
.
currency
==
currency
{
return
money
.
amount
}
return
money
.
amount
*
1.2
}
The test turns green, but something doesn’t seem right about our code! Specifically:
The exchange rate is hard-coded. It should be declared as a variable.
The exchange rate isn’t dependent on the currency. It should be looked up based on the two currencies involved.
The exchange rate should be modifiable.
Let’s address the first of these and add the remaining two to our feature list. We define a variable named eurToUsd
in our convert
method and use it:
func
convert
(
money
Money
,
currency
string
)
float64
{
eurToUsd
:=
1.2
if
money
.
currency
==
currency
{
return
money
.
amount
}
return
money
.
amount
*
eurToUsd
}
Exchange rate is defined as an appropriately named variable
The exchange rate variable is used to convert currency
The test is still green.
Let’s start by adding a new test in MoneyTest
to test the addition of Dollars and Euros:
testAdditionOfDollarsAndEuros
(
)
{
let
fiveDollars
=
new
Money
(
5
,
"USD"
)
;
let
tenEuros
=
new
Money
(
10
,
"EUR"
)
;
let
portfolio
=
new
Portfolio
(
)
;
portfolio
.
add
(
fiveDollars
,
tenEuros
)
;
let
expectedValue
=
new
Money
(
17
,
"USD"
)
;
assert
.
deepStrictEqual
(
portfolio
.
evaluate
(
"USD"
)
,
expectedValue
)
;
}
The test creates two Money
objects representing 5 USD and 10 EUR each. These are added to a Portfolio
object. The value from evaluating the Portfolio
in USD is compared with a Money
object representing 17 USD.
The test fails as expected:
AssertionError
[
ERR_ASSERTION
]
:
Expected
values
to
be
strictly
deep
-
equal
:
+
actual
-
expected
Money
{
+
amount
:
15
,
-
amount
:
17
,
currency
:
'USD'
}
We expect this failure because the current implementation of evaluate
method simply adds the amount
attribute of all Money
objects, regardless of their currencies.
We need to first convert the amount of each Money into the target currency and then sum it.
evaluate
(
currency
)
{
let
total
=
this
.
moneys
.
reduce
((
sum
,
money
)
=>
{
return
sum
+
this
.
convert
(
money
,
currency
);
},
0
);
return
new
Money
(
total
,
currency
);
}
How should the convert
method work? For now, the simplest implementation that works is one that returns amount
when the currencies match and otherwise multiplies the amount by the conversion rate require by our test:
convert
(
money
,
currency
)
{
if
(
money
.
currency
===
currency
)
{
return
money
.
amount
;
}
return
money
.
amount
*
1.2
;
}
The test is now green. That’s progress, but not everything is hunky-dory. In particular:
The exchange rate is hard-coded. It should be declared as a variable.
The exchange rate isn’t dependent on the currency. It should be looked up based on the two currencies involved.
The exchange rate should be modifiable.
Let’s address the first of these right away and add the remaining to our feature list.
We define a variable named eurToUsd
and use it in our convert
method:
convert
(
money
,
currency
)
{
let
eurToUsd
=
1.2
;
if
(
money
.
currency
===
currency
)
{
return
money
.
amount
;
}
return
money
.
amount
*
eurToUsd
;
}
Exchange rate is defined as an appropriately named variable
The exchange rate variable is used to convert currency
All tests are green.
Let’s write a new test in test_money.py
that validates adding Dollars to Euros:
def
testAdditionOfDollarsAndEuros
(
self
)
:
fiveDollars
=
Money
(
5
,
"
USD
"
)
tenEuros
=
Money
(
10
,
"
EUR
"
)
portfolio
=
Portfolio
(
)
portfolio
.
add
(
fiveDollars
,
tenEuros
)
expectedValue
=
Money
(
17
,
"
USD
"
)
actualValue
=
portfolio
.
evaluate
(
"
USD
"
)
self
.
assertEqual
(
expectedValue
,
actualValue
)
The test creates two Money
objects representing 5 USD and 10 EUR, respectively. They are added to a pristine Portfolio
object. The actualValue
from evaluating the Portfolio
in dollars is compared with a newly minted expectedValue
of 17 USD.
We expect the test to fail, of course, because we are in the RED phase of our of RGR cycle. However, the error message from the assertion failure is rather cryptic:
AssertionError
:
<
money
.
Money
object
at
0x10f3c3280
>
!=
<
money
.
Money
object
at
0x10f3c33a0
>
Who on earth knows what mysterious goblins reside at those obscure memory addresses!
This is one of those times where we must slow down and write a better failing test before we attempt to get to GREEN. Can we make the assertion statement print a more helpful error message?
The assertEqual
method — like most other assertion methods in the unittest
package — takes an optional third parameter, which is a custom error message. Let’s provide a formatted string showing the stringified representation of expectedValue
and actualValue
:
self
.
assertEqual
(
expectedValue
,
actualValue
,
"
%s
!=
%s
"
%
(
expectedValue
,
actualValue
)
)
Nope! That simply prints the obscure memory addresses twice:
AssertionError
:
<
money
.
Money
object
at
0x1081111f0
>
!=
<
money
.
Money
object
at
0x108111310
>
:
<
money
.
Money
object
at
0x1081111f0
>
!=
<
money
.
Money
object
at
0x108111310
>
What we need to do is to override the __str__
method in the Money
class and make it return a more human-readable representation. Something like “USD 17.00”.
def
__str__
(
self
)
:
return
f
"
{self.currency} {self.amount:0.2f}
"
We format Money
’s currency
and amount
fields, printing the latter up to two decimal places.
After adding the __str__
method, let’s run our test suite again:
AssertionError
:
...
USD
17.00
!=
USD
15.00
Ah, much better! Seventeen dollars certainly aren’t the same thing as 15 dollars!
Python’s F-strings interpolation provides a succinct and neat way to format strings with a mixture of fixed text and variables. F-strings were defined in [PEP-498]https://www.python.org/dev/peps/pep-0498/ and have been a part of Python since version 3.6.
This validates our belief that the evaluate
method, as currently implemented, mindlessly adds the amounts of all Money
objects (5 and 10 in our test) to get the result, with no regard to the currencies (USD and EUR, respectively, in our test).
A closer examination of the evaluate
method shows that the mindlessness is in the lambda expression. It maps every Money
object to its amount
, regardless of its currency. These amounts are then added up by the reduce
function using the add
operator.
What if the lambda expression mapped every Money
object to its converted value? The target currency for the conversion would be the currency in which the Portfolio
is being evaluated.
total
=
functools
.
reduce
(
operator
.
add
,
map
(
lambda
m
:
self
.
__convert
(
m
,
currency
)
,
self
.
moneys
)
,
0
)
Python doesn’t have truly “Private” scope for variables or functions. The naming convention and something called “name mangling” ensure that names with two leading underscores are treated as private.
How should we implement the __convert
method? Converting to the same currency
as that of the Money
is trivial: the Money
’s amount doesn’t change in this case. When converting to a different currency, we’ll multiply Money
’s amount with the (for now) hard-coded exchange rate between USD and EUR:
def
__convert
(
self
,
aMoney
,
aCurrency
)
:
if
aMoney
.
currency
==
aCurrency
:
return
aMoney
.
amount
else
:
return
aMoney
.
amount
*
1.2
The test is green. Yay … and hmm! We should do the refactoring to remove the ugliness of this code. Here are some problems with it:
The exchange rate is hard-coded. It should be declared as a variable.
The exchange rate isn’t dependent on the currency. It should be looked up based on the two currencies involved.
The exchange rate should be modifiable.
Let’s address the first of these three items in the REFACTOR phase and add the remaining two to our feature list.
We define a private variable named _eur_to_usd
in the __init__
method and use it instead of the hard-coded value in the __convert
method:
class
Portfolio
:
def
__init__
(
self
)
:
self
.
moneys
=
[
]
self
.
_eur_to_usd
=
1.2
.
.
.
def
__convert
(
self
,
aMoney
,
aCurrency
)
:
if
aMoney
.
currency
==
aCurrency
:
return
aMoney
.
amount
else
:
return
aMoney
.
amount
*
self
.
_eur_to_usd
Exchange rate is defined as an appropriately named variable
The exchange rate variable is used to convert currency
All tests are green.
We have our first implementation of converting money between two different currencies, specifically USD → EUR. Let’s commit our changes to our local Git repository:
git add .
git commit -m "feat: convert from EUR to USD"
We have solved the conversion of Money
s in different currencies for the scenario of converting USD to EUR. However, we cut a few corners while doing so. The conversion only works for one specific case (USD → EUR). Furthermore, there is no way to add or modify exchange rates.
Let’s update our feature list to cross out the accomplished items and add the new ones.
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 |
1 For an economic discussion of currency pegging, see https://www.investopedia.com/terms/c/currency-peg.asp
2 Currencies can be inconvertible for a variety of reasons: political, economic, or even military. https://www.investopedia.com/terms/i/inconvertible_currency.asp
3.138.69.45