Skip to content

Commit

Permalink
Require precision for precious metals.
Browse files Browse the repository at this point in the history
  • Loading branch information
TotalVerb committed Dec 10, 2015
1 parent 66df397 commit 0528533
Show file tree
Hide file tree
Showing 5 changed files with 49 additions and 12 deletions.
28 changes: 22 additions & 6 deletions src/monetary.jl
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ currency used is part of the type and not the object. The value is internally
represented as a quantity of some integer type. The usual way to construct a
`Monetary` directly, if needed, is:
Monetary(:USD, 100) # 1.00 USD
Monetary(:USD) # 1.00 USD
Monetary(:USD, 325) # 3.25 USD
Be careful about the decimal point, as the `Monetary` constructor takes an
integer, representing the number of smallest denominations of the currency.
Expand All @@ -32,8 +33,22 @@ number of decimal points to keep after the major denomination:
immutable Monetary{T, U, V} <: AbstractMonetary
amt::U
end
Monetary(T::Symbol, x; precision=decimals(T)) =
Monetary{T, typeof(x), precision}(x)

function Monetary(T::Symbol, x; precision=decimals(T))
if precision == -1
throw(ArgumentError("Must provide precision for currency $T."))
else
Monetary{T, typeof(x), precision}(x)
end
end

function Monetary(T::Symbol; precision=decimals(T), storage=Int)
if precision == -1
throw(ArgumentError("Must provide precision for currency $T."))
else
one(Monetary{T, storage, precision})
end
end

"""
filltype(typ) → typ
Expand All @@ -58,9 +73,10 @@ currency{T}(m::Monetary{T}) = T
decimals(money) → Int
Get the precision, in terms of the number of decimal places after the major
currency unit, of the given `Monetary` value or type. Alternatively, if given
a symbol, gets the default decimal value (the number of decimal places to
represent the minor currency unit) for that symbol.
currency unit, of the given `Monetary` value or type. Alternatively, if given a
symbol, gets the default exponent (the number of decimal places to represent the
minor currency unit) for that symbol. Return `-1` if there is no sane minor
unit, such as for several kinds of precious metal.
"""
decimals(c::Symbol) = DATA[c][1]
decimals{T,U,V}(::Monetary{T,U,V}) = V
Expand Down
7 changes: 6 additions & 1 deletion src/usingcurrencies.jl
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ currency unit worth 1€, not a currency unit worth 0.01€.
@usingcurrencies EUR, GBP, AUD
7AUD # 7.00 AUD
There is no sane unit for certain currencies like `XAU` or `XAG`, so this macro
does not work for those. Instead, define them manually:
const XAU = Monetary(:XAU; precision=4)
"""
macro usingcurrencies(curs)
if isexpr(curs, Symbol)
Expand All @@ -14,7 +19,7 @@ macro usingcurrencies(curs)
@assert isexpr(curs, :tuple)

quote
$([:(const $cur = one(Monetary{$(Expr(:quote, cur))}))
$([:(const $cur = Monetary($(Expr(:quote, cur))))
for cur in curs.args]...)
end |> esc
end
3 changes: 3 additions & 0 deletions test/currencies.jl
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,7 @@ end
@test decimals(:JPY) == 0
@test decimals(Monetary{:USD, BigInt}) == 2
@test decimals(Monetary{:JPY, Int, 4}) == 4
@test decimals(:XAU) == -1
@test decimals(Monetary(:XAU; precision=4)) == 4
@test decimals(Monetary{:XAU, Int, 3}) == 3
end
2 changes: 2 additions & 0 deletions test/display.jl
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
"1.00000 USD"
@test stringmime("text/plain", -one(Monetary{:JPY, Int, 2})) ==
"−1.00 JPY"
@test stringmime("text/plain", 7.2512Monetary(:XAU; precision=4)) ==
"7.2512 XAU"
end

@testset "text/latex" begin
Expand Down
21 changes: 16 additions & 5 deletions test/monetary.jl
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@
@test 10USD / 1USD == 10.0
@test 10USD ÷ 3USD == 3
@test 10USD % 3USD == 1USD
@test one(Monetary{:USD}) == USD
@test zero(Monetary{:USD}) == 0USD
@test one(Monetary{:USD}) USD Monetary(:USD)
@test zero(Monetary{:USD}) 0USD
end

# Type safety
Expand Down Expand Up @@ -62,12 +62,13 @@ end
# Big int monetary
BI_USD = Monetary(:USD, BigInt(100))
BI_USD2 = one(Monetary{:USD, BigInt})
BI_USD3 = Monetary(:USD; storage=BigInt)
I128_USD = one(Monetary{:USD, Int128})
@testset "Monetary — Representation" begin
@test BigInt(2)^100 * BI_USD + 10BI_USD == (BigInt(2)^100 + 10) * BI_USD

# test **equality** — note equivalence is untrue because BigInt
@test BI_USD == BI_USD2
@test BI_USD == BI_USD2 == BI_USD3

# wrapping behaviour (strange but documented)
@test typemin(Int128) * I128_USD typemax(Int128) * I128_USD + I128_USD
Expand All @@ -84,8 +85,10 @@ end
millusd = one(Monetary{:USD, Int, 3})

# Constructor equivalence
@test flatusd == Monetary(:USD, 1; precision=0)
@test millusd == Monetary(:USD, 1000; precision=3)
@test flatusd Monetary(:USD, 1; precision=0)
@test flatusd Monetary(:USD; precision=0)
@test millusd Monetary(:USD, 1000; precision=3)
@test millusd Monetary(:USD; precision=3, storage=Int)
@test flatusd millusd

# Custom precision arithmetic
Expand All @@ -98,4 +101,12 @@ end
@test_throws MethodError flatusd USD
@test_throws ArgumentError flatusd + millusd
@test_throws MethodError flatusd / millusd

# Special metals — precision required
@test_throws ArgumentError @usingcurrencies XAU
@test_throws ArgumentError Monetary(:XAU)
@test_throws ArgumentError Monetary(:XAU, 100)

@test Monetary(:XAU; precision=2) one(Monetary{:XAU,Int,2})
@test int(Monetary(:XSU; precision=0)) == 1
end

0 comments on commit 0528533

Please sign in to comment.