money
Handling amounts of money
safely and efficiently.
Discussion
An amount of money
is a number tagged with a currency id like "EUR"
or "USD". Precision and rounding mode can be chosen as template
parameters.
If you write code which handles money
, you have to choose a data type
for it. Out of the box, D offers you floating point, integer, and
std.bigint. All of these have their problems.
Floating point is inherently imprecise. If your dollar numbers become
too big, then you start getting too much or too little cents. This
is not acceptable as the errors accumulate. Also, floating point has
values like "infinity" and "not a number" and if those show up,
usually things break, if you did not prepare for it. Debugging then
means to work backwards how this happened, which is tedious and hard.
Integer numbers do not suffer from imprecision, but they can not
represent numbers as big as floating point. Worse, if your numbers
become too big, then your CPU silently wraps them into negative
numbers. Like the imprecision with floating point, your data is
now corrupted without anyone noticing it yet. Also, fixed point
arithmetic with integers is easy to get wrong and you need a
fractional part to represent cents, for example.
As a third option, there is std.bigint, which provides numbers
with arbitrary precision. Like floating point, the arithmetic is easy.
Like integer, precision is fine. The downside is performance.
Nevertheless, from the three options, this is the most safe one.
Can we do even better?
If we design a custom data type for money
, we can improve safety
even more. For example, certain arithmetics can be forbidden. What
does it mean to multiply two money
amounts, for example? There is no
such thing as $² which makes any sense. However, you can certainly
multiply a money
amount with a unitless number. A custom data type
can precisely allow and forbid this operations.
Here the design decision is to use an integer for the internal
representation. This limits the amounts you can use. For example,
if you decide to use 4 digits behind the comma, the maximum number
is 922,337,203,685,477.5807 or roughly 922 trillion. The US debt is
currently in the trillions, so there are certainly cases where
this representation is not applicable. However, we can check overflow,
so if it happens, you get an exception thrown and notice it
right away. The upside of using an integer is performance and
a deterministic arithmetic all programmers are familiar with.
License
-
Declaration
struct
currency
(string currency_name, int dec_places = 4, roundingMode rmode = roundingMode.HALF_UP);Holds an amount of
currency
Examples
Basic usage
alias EUR = currency!("EUR"); assert(EUR(100.0001) == EUR(100.00009)); assert(EUR(3.10) + EUR(1.40) == EUR(4.50)); assert(EUR(3.10) - EUR(1.40) == EUR(1.70)); assert(EUR(10.01) * 1.1 == EUR(11.011)); import std.format : format; // for writefln("%d", EUR(3.6)); assert(format("%d", EUR(3.6)) == "4EUR"); assert(format("%d", EUR(3.1)) == "3EUR"); // for writefln("%f", EUR(3.141592)); assert(format("%f", EUR(3.141592)) == "3.1416EUR"); assert(format("%.2f", EUR(3.145)) == "3.15EUR"); // From issue #5 assert(format("%.4f", EUR(0.01234)) == "0.0123EUR");
Examples
Overflow is an error, since silent corruption is worse
import std.exception : assertThrown; alias EUR = currency!("EUR"); auto one = EUR(1); assertThrown!OverflowException(EUR.max + one); assertThrown!OverflowException(EUR.min - one);
Examples
Arithmetic ignores rounding mode
alias EUR = currency!("EUR", 2, roundingMode.UP); auto one = EUR(1); assert(one != one / 3);
Examples
Generic equality and order
alias USD = currency!("USD", 2); alias EURa = currency!("EUR", 2); alias EURb = currency!("EUR", 4); alias EURc = currency!("EUR", 4, roundingMode.DOWN); // cannot compile with different currencies static assert(!__traits(compiles, EURa(1) == USD(1))); // cannot compile with different dec_places static assert(!__traits(compiles, EURa(1) == EURb(1))); // can check equality if only rounding mode differs assert(EURb(1.01) == EURc(1.01)); // cannot compare with different currencies static assert(!__traits(compiles, EURa(1) < USD(1)));
-
Declaration
this(double
x
);Floating point contructor. Uses rmode on
x
. -
Declaration
this(string
x
);String contructor.
Throws
ParseError or std.conv.ConvOverflowException for invalid inputs
-
Declaration
static immutable
init
;default initialisation value is zero
-
Declaration
static immutable
max
;maximum amount depends on dec_places
-
Declaration
static immutable
min
;minimum amount depends on dec_places
-
Declaration
const T
opBinary
(string op)(const Trhs
);Can add and subtract money amounts of the same type.
-
Declaration
const T
opBinary
(string op)(const longrhs
);Can multiply, divide, and modulo with integer values.
-
Declaration
const T
opBinary
(string op)(const realrhs
);Can multiply, divide, and modulo floating point numbers.
-
Declaration
void
opOpAssign
(string op)(const Trhs
);Can add and subtract money amounts of the same type.
-
Declaration
void
opOpAssign
(string op)(const longrhs
);Can multiply, divide, and modulo with integer values.
-
Declaration
void
opOpAssign
(string op)(const realrhs
);Can multiply, divide, and modulo floating point numbers.
-
Declaration
const bool
opEquals
(OT)(auto ref const OTother
) if (isCurrency!OT &&other
.__currency == currency_name &&other
.__dec_places == dec_places);Can check equality with money amounts of the same concurrency and decimal places.
-
Declaration
const int
opCmp
(OT)(const OTother
) if (isCurrency!OT &&other
.__currency == currency_name);Can compare with money amounts of the same concurrency.
-
Declaration
const void
toString
(scope void delegate(const(char)[])sink
, FormatSpec!charfmt
);Can convert to string.
-
Declaration
enum
roundingMode
: int;Specifies rounding behavior
-
Declaration
UP
Round upwards, e.g. 3.1 up to 4.
-
Declaration
DOWN
Round downwards, e.g. 3.9 down to 3.
-
Declaration
HALF_UP
Round to nearest number, half way between round up, e.g. 3.5 to 4.
-
Declaration
HALF_DOWN
Round to nearest number, half way between round dow, e.g. 3.5 to 3.
-
Declaration
HALF_EVEN
Round to nearest number, half way between round to even number, e.g. 3.5 to 4.
-
Declaration
HALF_ODD
Round to nearest number, half way between round to odd number, e.g. 3.5 to 3.
-
Declaration
HALF_TO_ZERO
Round to nearest number, half way between round towards zero, e.g. -3.5 to -3.
-
Declaration
HALF_FROM_ZERO
Round to nearest number, half way between round away from zero, e.g. -3.5 to -4.
-
Declaration
UNNECESSARY
Throw exception if rounding would be necessary
-
-
Declaration
long
round
(roundingMode m)(longx
, intdec_place
);Round an integer to a certain decimal place according to rounding mode
Examples
assert (round!(roundingMode.DOWN) (1009, 1) == 1000); assert (round!(roundingMode.UP) (1001, 1) == 1010); assert (round!(roundingMode.HALF_UP) (1005, 1) == 1010); assert (round!(roundingMode.HALF_DOWN)(1005, 1) == 1000);
-
Declaration
@trusted real
round
(realx
, roundingModem
);Round a float to an integer according to rounding mode
-
Declaration
class
ForbiddenRounding
: object.Exception;Exception is thrown if rounding would have to happen, but roundingMode.UNNECESSARY is specified.
-
Declaration
class
OverflowException
: object.Exception;Overflow can happen with money arithmetic.
-
Declaration
class
ParseError
: object.Exception;Failure to parse a money amount from string