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.

Authors

Andreas Zwinkau

  • Declaration

    struct currency(string currency_name, int dec_places = 4, roundingMode rmode = roundingMode.HALF_UP);

    Holds an amount of currency

    Examples

    Basic usage

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

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

    1. alias EUR = currency!("EUR", 2, roundingMode.UP); auto one = EUR(1); assert(one != one / 3);

    Examples

    Generic equality and order

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

    • max

      Declaration

      static immutable max;

      maximum amount depends on dec_places

    • min

      Declaration

      static immutable min;

      minimum amount depends on dec_places

    • Declaration

      const T opBinary(string op)(const T rhs);

      Can add and subtract money amounts of the same type.

    • Declaration

      const T opBinary(string op)(const long rhs);

      Can multiply, divide, and modulo with integer values.

    • Declaration

      const T opBinary(string op)(const real rhs);

      Can multiply, divide, and modulo floating point numbers.

    • Declaration

      void opOpAssign(string op)(const T rhs);

      Can add and subtract money amounts of the same type.

    • Declaration

      void opOpAssign(string op)(const long rhs);

      Can multiply, divide, and modulo with integer values.

    • Declaration

      void opOpAssign(string op)(const real rhs);

      Can multiply, divide, and modulo floating point numbers.

    • Declaration

      const bool opEquals(OT)(auto ref const OT other) 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 OT other) 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!char fmt);

      Can convert to string.

  • Declaration

    enum roundingMode: int;

    Specifies rounding behavior

    • UP

      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)(long x, int dec_place);

    Round an integer to a certain decimal place according to rounding mode

    Examples

    1. 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(real x, roundingMode m);

    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