stdxdecimal

This module defines an exact decimal type, Decimal, to a specific number of digits. This is designed to be a drop in replacement for built-in floating point numbers, allowing all the same possible operations.

Discussion

Floating point numbers (float, double, real) are inherently inaccurate because they cannot represent all possible numbers with a decimal part. Decimal on the other hand, is able to represent all possible numbers with a decimal part (limited by memory size and Hook.precision).

Adapted from the specification of the General Decimal Arithmetic.

Basic Example:

  1. import stdxdecimal; void main() { auto d1 = decimal("1.23E-10"); d1 -= decimal("2.00E-10"); assert(d1.toString() == "-0.000000000077"); }

Custom Behavior: The behavior of Decimal is controlled by the template parameter Hook, which can be a user defined type or one of the Hooks provided by this module.

The following behaviors are controlled by Hook:

  • The number of significant digits to store.
  • The rounding method.
  • The min and max exponents. (not implemented yet)
  • What to do when exceptional conditions arise.


The following predefined Hooks are available:

Abort precision is set to 9, rounding is HalfUp, and the program will assert(0) on divsion by zero, overflow, underflow, and invalid operations.
Throw precision is set to 9, rounding is HalfUp, and the program will throw an exception on divsion by zero, overflow, underflow, and invalid operations.
HighPrecision precision is set to 64, rounding is HalfUp, and the program will assert(0) on divsion by zero, overflow, underflow, and invalid operations.
NoOp precision is set to 9, rounding is HalfUp, and nothing will happen on exceptional conditions.

Percision and Rounding: Decimal accurately stores as many as Hook.precision significant digits. Once the number of digits > Hook.precision, then the number is rounded. Rounding is performed according to the rules laid out in .

By default, the precision is 9, and the rounding mode is RoundingMode.HalfUp.

Hook.precision must be <= uint.max - 1 and > 1.

Note: Increasing the number of possible significant digits can result in orders of magnitude slower behavior, as described below. Only ask for as many digits as you really need.

9 and below (default) Baseline
10-19 2x slower than baseline
20-1000 20x slower than baseline
1000 and above 200x slower than baseline

Exceptional Conditions: Certain operations will cause a Decimal to enter into an invalid state, e.g. dividing by zero. When this happens, Decimal does two things

  1. Sets a public bool variable to true.
  2. Calls a specific function in Hook, if it exists, with the operation's result as the only parameter.


The following table lists all of the conditions

Name Flag Method Description
Clamped clamped onClamped Occurs when the exponent has been altered to fit in-between Hook.maxExponent and Hook.minExponent.
Inexact inexact onInexact Occurs when the result of an operation is not perfectly accurate. Mostly occurs when rounding removed non-zero digits.
Invalid Operation invalidOperation onInvalidOperation Flagged when an operation makes no sense, e.g. multiplying 0 and Infinity or add -Infinity to Infinity.
Division by Zero divisionByZero onDivisionByZero Specific invalid operation. Occurs whenever the dividend of a division or modulo is equal to zero.
Rounded rounded onRounded Occurs when the Decimal's result had more than Hook.precision significant digits and was reduced.
Subnormal subnormal onSubnormal Flagged when the exponent is less than Hook.maxExponent but the digits of the Decimal are not inexact.
Overflow overflow onOverflow Not to be confused with integer overflow, this is flagged when the exponent of the result of an operation would have been above Hook.maxExponent and the result is inexact. Inexact and Rounded are always set with this flag.
Underflow underflow onUnderflow Not to be confused with integer underflow, this is flagged when the exponent of the result of an operation would have been below Hook.minExponent. Inexact, Rounded, and Subnormal are always set with this flag.


Each function documentation lists the specific states that will led to one of these flags.

Differences From The Specification:

  • There's no concept of a Signaling NaN in the module.
  • compare, implemented as opCmp, does not propagate NaN due to D's opCmp semantics.

Version

v0.5. Still work in progress. For missing features, see README.md

License

.

Authors

Jack Stouffer

  • Declaration

    struct Decimal(Hook = Abort);

    A exact decimal type, accurate to Hook.precision digits. Designed to be a drop in replacement for floating points.

    Discussion

    Behavior is defined by Hook. See the module overview for more information.

    • Declaration

      Hook hook;

      hook is a member variable if it has state, or an alias for Hook otherwise.

    • Declaration

      bool clamped;
      bool divisionByZero;
      bool inexact;
      bool invalidOperation;
      bool overflow;
      bool rounded;
      bool subnormal;
      bool underflow;

      Public flags

    • Declaration

      pure this(T)(const T num) if (isNumeric!T);

      Constructs an exact decimal type from a built in number

      Parameters

      T num

      the number to convert to exact decimal

      Note: Using float types for construction is less accurate than using a string representation due to floating point inaccuracy. If possible, it's always better to use string construction.

    • Declaration

      this(S)(S str) if (isForwardRange!S && isSomeChar!(ElementEncodingType!S) && !isInfinite!S && !isSomeString!S);
      pure this(S)(S str) if (isSomeString!S);

      Converts a string representing a number to an exact decimal.

      Discussion

      If the string does not represent a number, then the result is NaN and invalidOperation is true.

      Implements spec to-number.

      Parameters

      S str

      The string to convert from

      String Spec:

      1. sign ::= + | - digit ::= 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 indicator ::= e | E digits ::= digit [digit]... decimal-part ::= digits . [digits] | [.] digits exponent-part ::= indicator [sign] digits infinity ::= Infinity | Inf nan ::= NaN [digits] numeric-value ::= decimal-part [exponent-part] | infinity numeric-string ::= [sign] numeric-value | [sign] nan

      Exceptional Conditions: invalidOperation is flagged when str is not a valid string

    • Declaration

      const auto opBinary(string op, T)(T rhs) if (op == "+" || op == "-" || op == "*" || op == "/");
      ref Decimal!Hook opOpAssign(string op, T)(auto ref const T rhs) if (op == "+" || op == "-" || op == "*" || op == "/");

      Performs a binary operation between two decimals, or a decimal and a built in number.

      Discussion

      The result has the hook of the left hand side. On non-assignment operations invalid operations do not effect the left hand side of the operation.

      When the right hand side is a built-in numeric type, the default hook Abort is used for its decimal representation.

      Parameters

      T rhs

      the right-hand side of the operation

      Exceptional Conditions: invalidOperation is flagged under the following conditions

      • Adding Infinity and -Infinity, and vice-versa
      • Multiplying +/-Infinity by +/-zero
      • Dividing anything but zero by zero
      • Dividing +/-Infinity by +/-Infinity
      divisionByZero is flagged when dividing anything but zero by zero

    • Declaration

      const int opCmp(T)(T d) if (isNumeric!T || is(Unqual!T == Decimal));

      -Infinity is less than all numbers, -NaN is greater than -Infinity but less than all other numbers, NaN is greater than -NaN but less than all other numbers and Infinity is greater than all numbers. -NaN and NaN are equal to themselves.

      Parameters

      T d

      the decimal or built-in number to compare to

      Return Value

      Barring special values, 0 if subtracting the two numbers yields 0, -1 if the result is less than 0, and 1 if the result is greater than zero

    • Declaration

      const bool opEquals(T)(T d) if (isNumeric!T || is(Unqual!T == Decimal));

      Return Value

      true if opCmp would return 0

    • Declaration

      pure nothrow @nogc @safe void resetFlags();

      Convenience function to reset all exceptional condition flags to false at once

    • dup

      Declaration

      const Decimal!Hook dup()();

      Return Value

      A mutable copy of this Decimal. Also copies current flags.

    • Declaration

      const immutable(Decimal!Hook) idup()();

      Return Value

      An immutable copy of this Decimal. Also copies current flags.

    • Declaration

      const pure nothrow @nogc @property @safe bool isNaN();

      Return Value

      If this decimal represents a positive or negative NaN

    • Declaration

      const pure nothrow @nogc @property @safe bool isInfinity();

      Return Value

      If this decimal represents positive or negative infinity

    • nan

      Declaration

      @property Decimal!Hook nan()();

      Return Value

      A decimal representing a positive NaN

    • Declaration

      @property Decimal!Hook infinity()();

      Return Value

      A decimal representing positive Infinity

    • Declaration

      alias toString = toDecimalString;

    • Declaration

      const auto toDecimalString();
      const void toDecimalString(Writer)(auto ref Writer w) if (isOutputRange!(Writer, char));

      Return Value

      Returns the decimal string representation of this decimal.

  • Declaration

    auto decimal(Hook = Abort, R)(R r) if (isForwardRange!R && isSomeChar!(ElementEncodingType!R) && !isInfinite!R || isNumeric!R);

    Factory function

    Examples

    1. auto d1 = decimal(5.5); assert(d1.toString == "5.5"); auto d2 = decimal("500.555");

  • Declaration

    enum Rounding: int;

    Controls what happens when the number of significant digits exceeds Hook.precision

    • Declaration

      Down

      Round toward 0, a.k.a truncate. The discarded digits are ignored.

    • Declaration

      HalfUp

      If the discarded digits represent greater than or equal to half (0.5) of the value of a one in the next left position then the result coefficient should be incremented by 1 (rounded up). Otherwise the discarded digits are ignored.

    • Declaration

      HalfEven

      If the discarded digits represent greater than half (0.5) the value of a one in the next left position then the result coefficient should be incremented by 1 (rounded up). If they represent less than half, then the result coefficient is not adjusted (that is, the discarded digits are ignored).

      Discussion

      Otherwise (they represent exactly half) the result coefficient is unaltered if its rightmost digit is even, or incremented by 1 (rounded up) if its rightmost digit is odd (to make an even digit).

    • Declaration

      Ceiling

      If all of the discarded digits are zero or if the sign is 1 the result is unchanged. Otherwise, the result coefficient should be incremented by 1 (rounded up).

    • Declaration

      Floor

      If all of the discarded digits are zero or if the sign is 0 the result is unchanged. Otherwise, the sign is 1 and the result coefficient should be incremented by 1.

    • Declaration

      HalfDown

      If the discarded digits represent greater than half (0.5) of the value of a one in the next left position then the result coefficient should be incremented by 1 (rounded up). Otherwise (the discarded digits are 0.5 or less) the discarded digits are ignored.

    • Up

      Declaration

      Up

      (Round away from 0.) If all of the discarded digits are zero the result is unchanged. Otherwise, the result coefficient should be incremented by 1 (rounded up).

    • Declaration

      ZeroFiveUp

      (Round zero or five away from 0.) The same as round-up, except that rounding up only occurs if the digit to be rounded up is 0 or 5, and after overflow the result is the same as for round-down.

  • Declaration

    struct Abort;

    Will halt program on division by zero, invalid operations, overflows, and underflows.

    Discussion

    Has 9 significant digits, rounds half up

    • Declaration

      enum Rounding roundingMode;

    • Declaration

      enum uint precision;

      A precision of 9 allows all possible the results of +,-,*, and / to fit into a ulong with no issues.

    • Declaration

      void onDivisionByZero(T)(T d) if (isInstanceOf!(Decimal, T));

    • Declaration

      void onInvalidOperation(T)(T d) if (isInstanceOf!(Decimal, T));

    • Declaration

      void onOverflow(T)(T d) if (isInstanceOf!(Decimal, T));

    • Declaration

      void onUnderflow(T)(T d) if (isInstanceOf!(Decimal, T));

  • Declaration

    struct HighPrecision;

    Same as abort, but offers 64 significant digits

    Note: As noted in the module overview, using 64 significant digits is much slower than 9 or 19.

    • Declaration

      enum Rounding roundingMode;

    • Declaration

      enum uint precision;

    • Declaration

      void onDivisionByZero(T)(T d) if (isInstanceOf!(Decimal, T));

    • Declaration

      void onInvalidOperation(T)(T d) if (isInstanceOf!(Decimal, T));

    • Declaration

      void onOverflow(T)(T d) if (isInstanceOf!(Decimal, T));

    • Declaration

      void onUnderflow(T)(T d) if (isInstanceOf!(Decimal, T));

  • Declaration

    struct Throw;

    Will throw exceptions on division by zero, invalid operations, overflows, and underflows

    Discussion

    Has 9 significant digits, rounds half up

    • Declaration

      enum Rounding roundingMode;

    • Declaration

      enum uint precision;

    • Declaration

      void onDivisionByZero(T)(T d) if (isInstanceOf!(Decimal, T));

    • Declaration

      void onInvalidOperation(T)(T d) if (isInstanceOf!(Decimal, T));

    • Declaration

      void onOverflow(T)(T d) if (isInstanceOf!(Decimal, T));

    • Declaration

      void onUnderflow(T)(T d) if (isInstanceOf!(Decimal, T));

  • Declaration

    struct NoOp;

    Does nothing on invalid operations except the proper flags

    Discussion

    Has 9 significant digits, rounds half up

  • Declaration

    class DivisionByZero: object.Exception;

    Thrown when using and division by zero occurs

    • Declaration

      pure nothrow @nogc @safe this(string msg, string file = __FILE__, size_t line = __LINE__, Throwable next = null);

      Parameters

      string msg

      The message for the exception.

      string file

      The file where the exception occurred.

      size_t line

      The line number where the exception occurred.

      Throwable next

      The previous exception in the chain of exceptions, if any.

    • Declaration

      pure nothrow @nogc @safe this(string msg, Throwable next, string file = __FILE__, size_t line = __LINE__);

      Parameters

      string msg

      The message for the exception.

      Throwable next

      The previous exception in the chain of exceptions.

      string file

      The file where the exception occurred.

      size_t line

      The line number where the exception occurred.

  • Declaration

    class InvalidOperation: object.Exception;

    Thrown when using and an invalid operation occurs

    • Declaration

      pure nothrow @nogc @safe this(string msg, string file = __FILE__, size_t line = __LINE__, Throwable next = null);

      Parameters

      string msg

      The message for the exception.

      string file

      The file where the exception occurred.

      size_t line

      The line number where the exception occurred.

      Throwable next

      The previous exception in the chain of exceptions, if any.

    • Declaration

      pure nothrow @nogc @safe this(string msg, Throwable next, string file = __FILE__, size_t line = __LINE__);

      Parameters

      string msg

      The message for the exception.

      Throwable next

      The previous exception in the chain of exceptions.

      string file

      The file where the exception occurred.

      size_t line

      The line number where the exception occurred.

  • Declaration

    class Overflow: object.Exception;

    Thrown when using and overflow occurs

    • Declaration

      pure nothrow @nogc @safe this(string msg, string file = __FILE__, size_t line = __LINE__, Throwable next = null);

      Parameters

      string msg

      The message for the exception.

      string file

      The file where the exception occurred.

      size_t line

      The line number where the exception occurred.

      Throwable next

      The previous exception in the chain of exceptions, if any.

    • Declaration

      pure nothrow @nogc @safe this(string msg, Throwable next, string file = __FILE__, size_t line = __LINE__);

      Parameters

      string msg

      The message for the exception.

      Throwable next

      The previous exception in the chain of exceptions.

      string file

      The file where the exception occurred.

      size_t line

      The line number where the exception occurred.

  • Declaration

    class Underflow: object.Exception;

    Thrown when using and underflow occurs

    • Declaration

      pure nothrow @nogc @safe this(string msg, string file = __FILE__, size_t line = __LINE__, Throwable next = null);

      Parameters

      string msg

      The message for the exception.

      string file

      The file where the exception occurred.

      size_t line

      The line number where the exception occurred.

      Throwable next

      The previous exception in the chain of exceptions, if any.

    • Declaration

      pure nothrow @nogc @safe this(string msg, Throwable next, string file = __FILE__, size_t line = __LINE__);

      Parameters

      string msg

      The message for the exception.

      Throwable next

      The previous exception in the chain of exceptions.

      string file

      The file where the exception occurred.

      size_t line

      The line number where the exception occurred.