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
structcurrency(string currency_name, int dec_places = 4, roundingMode rmode = roundingMode.HALF_UP);Holds an amount of
currencyExamples
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(doublex);Floating point contructor. Uses rmode on
x. -
Declaration
this(stringx);String contructor.
Throws
ParseError or std.conv.ConvOverflowException for invalid inputs
-
Declaration
static immutableinit;default initialisation value is zero
-
Declaration
static immutablemax;maximum amount depends on dec_places
-
Declaration
static immutablemin;minimum amount depends on dec_places
-
Declaration
const TopBinary(string op)(const Trhs);Can add and subtract money amounts of the same type.
-
Declaration
const TopBinary(string op)(const longrhs);Can multiply, divide, and modulo with integer values.
-
Declaration
const TopBinary(string op)(const realrhs);Can multiply, divide, and modulo floating point numbers.
-
Declaration
voidopOpAssign(string op)(const Trhs);Can add and subtract money amounts of the same type.
-
Declaration
voidopOpAssign(string op)(const longrhs);Can multiply, divide, and modulo with integer values.
-
Declaration
voidopOpAssign(string op)(const realrhs);Can multiply, divide, and modulo floating point numbers.
-
Declaration
const boolopEquals(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 intopCmp(OT)(const OTother) if (isCurrency!OT &&other.__currency == currency_name);Can compare with money amounts of the same concurrency.
-
Declaration
const voidtoString(scope void delegate(const(char)[])sink, FormatSpec!charfmt);Can convert to string.
-
Declaration
enumroundingMode: int;Specifies rounding behavior
-
Declaration
UPRound upwards, e.g. 3.1 up to 4.
-
Declaration
DOWNRound downwards, e.g. 3.9 down to 3.
-
Declaration
HALF_UPRound to nearest number, half way between round up, e.g. 3.5 to 4.
-
Declaration
HALF_DOWNRound to nearest number, half way between round dow, e.g. 3.5 to 3.
-
Declaration
HALF_EVENRound to nearest number, half way between round to even number, e.g. 3.5 to 4.
-
Declaration
HALF_ODDRound to nearest number, half way between round to odd number, e.g. 3.5 to 3.
-
Declaration
HALF_TO_ZERORound to nearest number, half way between round towards zero, e.g. -3.5 to -3.
-
Declaration
HALF_FROM_ZERORound to nearest number, half way between round away from zero, e.g. -3.5 to -4.
-
Declaration
UNNECESSARYThrow exception if rounding would be necessary
-
-
Declaration
longround(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 realround(realx, roundingModem);Round a float to an integer according to rounding mode
-
Declaration
classForbiddenRounding: object.Exception;Exception is thrown if rounding would have to happen, but roundingMode.UNNECESSARY is specified.
-
Declaration
classOverflowException: object.Exception;Overflow can happen with money arithmetic.
-
Declaration
classParseError: object.Exception;Failure to parse a money amount from string