/*
 * This file is part of the Amalthea library.
 * Copyright (C) 2020-2021, 2026 Eugene 'Vindex' Stulin
 * Distributed under the BSL 1.0 or the GNU LGPL 3.0 or later.
 */

/**
 * The module contains the an implementation of the Decimal data type.
 */
module amalthea.decimal;

import std.array : empty;
import std.algorithm : all, among;
import std.bigint : BigInt, divMod, toDecimalString;
static import std.conv;
import std.conv : to;
import std.format : format, FormatSpec;
static import std.math;
static import std.string;
import std.string : replace, split;

import amalthea.libcore : RealizeException;
import amalthea.dataprocessing : makeFilledArray, stripRight;
import amalthea.crypto : fnv1a64;


private shared size_t defaultFixedPoint = 18;


/*******************************************************************************
 * Sets default value of fixed point positon for all new Decimal structures.
 */
void setDefaultFixedPoint(size_t fixedPoint) {
    defaultFixedPoint = fixedPoint;
}


/*******************************************************************************
 * Gets current value of fixed point position by default.
 * If the value was not changed by setDefaultFixedPoint, the value is 18.
 */
size_t getDefaultFixedPoint() {
    return defaultFixedPoint;
}


/*******************************************************************************
 * Transforms number or string to Decimal.
 */
Decimal toDecimal(T)(T value) {
    return Decimal(value);
}


/// Short alias for main structure.
alias D = Decimal;


/*******************************************************************************
 * A struct representing a decimal fixed-point number.
 *
 * By default, the number of digits after the decimal point is 18.
 * The decimal point position is limited
 * by the maximum value of the size_t data type.
 * Decimal is based on std.bigint.BigInt and have no limitation
 * for the number of significant digits.
 */
struct Decimal {
    private BigInt value;
    private size_t fixedPoint;

    /***************************************************************************
     * Constucts an object from a decimal string.
     *
     * Params:
     *     number = Decimal string with comma/point or without it.
     *     fixedPointPosition = Position of decimal point in new object.
     */
    this(string number, size_t fixedPointPosition = defaultFixedPoint) {
        fixedPoint = fixedPointPosition;
        number = number.replace(",", ".");
        auto twoParts = number.split(".");
        if (twoParts.length > 2 || !std.string.isNumeric(number)) {
            throw new DecimalException("Wrong data format");
        }

        auto integer = twoParts[0].to!(char[]);
        int sign = 1;
        if (integer[0] == '-') {
            sign = -1;
        }
        if (fixedPoint != 0) {
            integer.length += fixedPoint;
            integer[$-fixedPoint .. $] = '0';
        }
        value = BigInt(integer);

        if (twoParts.length == 2 && fixedPoint != 0) {
            auto fraction = twoParts[1].to!(char[]);
            if (fraction.length > fixedPoint) {
                fraction.length = fixedPoint;
            } else {
                size_t oldLength = fraction.length;
                fraction.length = fixedPoint;
                fraction[oldLength .. $] = '0';
            }
            value += sign * BigInt(fraction);
        }
    }

    /***************************************************************************
     * Constucts an object from another Decimal object.
     * This constructor is used to transforming precision.
     *
     * Params:
     *     number = Decimal object.
     *     fixedPointPosition = Position of decimal point in new object.
     */
    this(Decimal number, size_t fixedPointPosition = defaultFixedPoint) {
        fixedPoint = fixedPointPosition;
        if (fixedPoint == number.fixedPoint) {
            value = number.value;
        } else if (fixedPoint > number.fixedPoint) {
            auto diff = fixedPoint - number.fixedPoint;
            value = number.value * (BigInt(10)^^diff);
        } else if (number.fixedPoint > fixedPoint) {
            auto diff = number.fixedPoint - fixedPoint;
            BigInt rem;
            BigInt divisor = BigInt(10)^^diff;
            divMod(number.value, divisor, value, rem);
            // if the remainder is one order of magnitude less than the divisor
            if (divisor < absBigInt(rem*10)) {
                value += calcRoundingCompensation(rem);
            }
        }
    }

    /***************************************************************************
     * Constucts an object from a BigInt object.
     *
     * Params:
     *     number = BigInt
     *     fixedPointPosition = Position of decimal point in new object.
     */
    this(BigInt number, size_t fixedPointPosition = defaultFixedPoint) {
        fixedPoint = fixedPointPosition;
        value = number * BigInt(10)^^fixedPoint;
    }

    /***************************************************************************
     * Constucts an object from an integer number (int/uint, long/ulong, etc.).
     *
     * Params:
     *     number = Integer number of a built-in data type.
     *     fixedPointPosition = Position of decimal point in new object.
     */
    this(T)(T number, size_t fixedPointPosition = defaultFixedPoint)
    if (is(T: long)) {
        fixedPoint = fixedPointPosition;
        value = BigInt(number) * BigInt(10)^^fixedPoint;
    }

    /***************************************************************************
     * Constucts an object from a floating point number.
     * (For good precision,
     * give preference to forming a decimal number from a string.)
     *
     * Params:
     *     number = Floating point number of a built-in data type.
     *     fixedPointPosition = Position of decimal point in new object.
     */
    this(real number, size_t fixedPointPosition = defaultFixedPoint) {
        if (std.math.isNaN(number) || std.math.isInfinity(number)) {
            throw new DecimalException("Unsupported floating value");
        }
        string fmt = "%f";
        string strValue = format(fmt, number);
        this(strValue, fixedPointPosition);
    }

    /***************************************************************************
     * Converts to a human readable decimal string.
     * All characters after the dot (with all zeros) will be shown according
     * to the configured fixed point position.
     */
    string toFullString() const pure @safe {
        auto strValue = format!"%d"(value).to!(char[]);
        int sign = 1;
        if (strValue[0] == '-') {
            sign = -1;
            strValue = strValue[1 .. $];
        }
        if (strValue.length <= fixedPoint) {
            char[] zeros;
            zeros.length = fixedPoint - strValue.length;
            zeros[] = '0';
            strValue = "0" ~ zeros ~ strValue;
        }
        auto res = strValue[0 .. $-fixedPoint];
        if (fixedPoint != 0) {
             res ~= "." ~ strValue[$-fixedPoint .. $];
        }
        if (sign == -1) {
            res = "-" ~ res;
        }
        return res.to!string;
    }

    /***************************************************************************
     * Convert the `Decimal` to `string`, passing it to the given sink.
     *
     * Available format indicators: `%D` (or `%f` instead it) and `%s`.
     * For `%D` (or `%f`) there are available '+', '0' and width with precision
     * like for floating. For example, `"%+08.5D"`.
     *
     * Params:
     *     sink = An OutputRange for accepting possibly piecewise
     *            segments of the  formatted string.
     *     fmt = A format string specifying the output format.
     */
    void toString(scope void delegate(const(char)[]) sink, FormatSpec!char fmt)
    const {
        immutable defaultFormatPrecision = fmt.UNSPECIFIED;
        if (fmt.spec == 'D' || fmt.spec == 'f') {
            auto prec = fmt.precision;
            if (prec == defaultFormatPrecision && this.fixedPoint < int.max) {
                prec = cast(int)this.fixedPoint;
            }
            string[] parts = this.toFullString().split('.');
            if (parts.length == 1) {
                parts ~= makeFilledArray(prec, '0').idup;
            } else {
                if (prec > this.fixedPoint) {
                    parts[1] ~= makeFilledArray(prec-this.fixedPoint, '0');
                } else {
                    auto rounded = this.round(prec);
                    string[] roundedParts = rounded.toFullString().split('.');
                    parts[0] = roundedParts[0];
                    if (parts.length == 2) {
                        parts[1] = roundedParts[1][0 .. prec];
                    } else {
                        parts[1] = "";
                    }
                }
            }
            string result = parts[1] != "" ? parts[0] ~ "." ~ parts[1]
                                           : parts[0];
            if (result.length < fmt.width) {
                char filler = fmt.flZero ? '0' : ' ';
                char[] s = makeFilledArray(fmt.width-result.length, filler);
                if (fmt.flPlus) s[0] = '+';
                result = s.idup ~ result;
            } else if (fmt.flPlus) {
                result = "+" ~ result;
            }
            sink(result);
        } else if (fmt.spec == 's') {
            sink(this.toFullString.stripRight('0').stripRight('.'));
        } else {
            throw new Exception("Unknown format specifier: %" ~ fmt.spec);
        }
    }

    /***************************************************************************
     * Converts to a boolean value.
     */
    T opCast(T:bool)() const {
        return !isZero();
    }

    /***************************************************************************
     * Converts to a floating type, if possible.
     */
    T opCast(T:real)() const {
        return this.toFullString.stripRight('0').to!T;
    }

    /***************************************************************************
     * Converts to an integer type, if possible.
     */
    T opCast(T:long)() const {
        string integerPart = this.toFullString().split(".")[0];
        return integerPart.to!T;
    }

    /***************************************************************************
     * Gets the modulus.
     */
    Decimal abs() pure const {
        auto copy = this;
        if (this.value < 0) {
            return -copy;
        }
        return copy;
    }

    /***************************************************************************
     * Calculates and returns the rounded value.
     * The fixed point position in the return value remains the original.
     * Params:
     *     lastDigitPosition = The number of decimal places to save.
     *                         By default, 0.
     */
    Decimal round(size_t lastDigitPosition = 0) pure const {
        Decimal copy = this;
        if (fixedPoint == 0 || lastDigitPosition >= fixedPoint) {
            return copy;
        }
        auto diff = fixedPoint - lastDigitPosition;
        BigInt rem;
        BigInt divisor = BigInt(10)^^diff;
        divMod(copy.value, divisor, copy.value, rem);
        if (divisor < absBigInt(rem*10)) {
            copy.value += calcRoundingCompensation(rem);
        }
        copy.value *= BigInt(10)^^diff;
        return copy;
    }

    /***************************************************************************
     * Gets a sign of the decimal number (-1, 0 or 1).
     */
    int getSign() pure const {
        if (this.value < 0) {
            return -1;
        } else if (this.value == 0) {
            return 0;
        }
        return 1;
    }

    /***************************************************************************
     * Implements Decimal equality test with other Decimal.
     */
    bool opEquals(Decimal rhs) const {
        string left = this.toFullString.stripRight('0').stripRight('.');
        string right = rhs.toFullString.stripRight('0').stripRight('.');
        return left == right;
    }

    /***************************************************************************
     * Implements Decimal equality test with buil-in numeric types and strings.
     */
    bool opEquals(T)(T rhs) const {
        auto right = Decimal(rhs);
        return this.opEquals(right);
    }

    /***************************************************************************
     * Returns hash of the current object.
     */
    ulong toHash() const @trusted pure {
        return fnv1a64(cast(ubyte[]) this.toFullString());
    }

    /***************************************************************************
     * Implements comparison of Decimal with other Decimal.
     */
    int opCmp(Decimal rhs) const {
        if (this.opEquals(rhs)) {
            return 0;
        }
        if (this.fixedPoint == rhs.fixedPoint && this.value > rhs.value) {
            return 1;
        } else if (this.fixedPoint != rhs.fixedPoint) {
            Decimal lhs = this;
            Decimal rhsCopy = rhs;
            leadToUnifiedFixedPoint(lhs, rhsCopy);
            if (lhs.value > rhs.value) {
                return 1;
            }
        }
        return -1;
    }

    /***************************************************************************
     * Implements comparison of Decimal with other numeric types and strings.
     */
    int opCmp(T)(T rhs) const {
        return this.opCmp(Decimal(rhs));
    }

    /***************************************************************************
     * Implements Decimal unary operator "++".
     */
    ref Decimal opUnary(string op)() if (op == "++") {
        auto appendum = BigInt(1) * BigInt(10)^^fixedPoint;
        value += appendum;
        return this;
    }

    /***************************************************************************
     * Implements Decimal unary operator "--".
     */
    ref Decimal opUnary(string op)() if (op == "--") {
        auto subtrahend = BigInt(1) * BigInt(10)^^fixedPoint;
        value -= subtrahend;
        return this;
    }

    /***************************************************************************
     * Implements Decimal unary operator "+".
     */
    Decimal opUnary(string op)() if (op == "+") {
        return this;
    }

    /***************************************************************************
     * Implements Decimal unary operator "-".
     */
    Decimal opUnary(string op)() pure const if (op == "-") {
        Decimal res = this;
        res.value = -this.value;
        return res;
    }

    /***************************************************************************
     * Implements binary operators between Decimals.
     * Returns an object with the maximum precision of both original objects.
     */
    Decimal opBinary(string op)(Decimal rhs) const
    if (op.among("+", "-", "*", "/", "^^")) {
        static if (op == "^^") {
            return pow(this, rhs);
        }

        Decimal lhs = this; //copied current state
        size_t bestPrecision = leadToUnifiedFixedPoint(lhs, rhs);
        auto res = Decimal(0, bestPrecision);

        static if (op == "+") {
            res.value = lhs.value + rhs.value;
        }
        static if (op == "-") {
            res.value = lhs.value - rhs.value;
        }
        static if (op == "*") {
            res.value = lhs.value * rhs.value;
            BigInt rem;
            BigInt divisor = BigInt(10)^^bestPrecision;
            divMod(res.value, divisor, res.value, rem);
            if (divisor < absBigInt(rem*10)) {
                res.value += calcRoundingCompensation(rem);
            }
        }
        static if (op == "/") {
            if (rhs.value == BigInt(0)) {
                throw new DecimalException("Division by zero");
            }
            BigInt rem;
            lhs.value *= BigInt(10)^^bestPrecision;
            divMod(lhs.value, rhs.value, res.value, rem);
            if (rhs.value < absBigInt(rem*10)) {
                res.value += calcRoundingCompensation(rem);
            }
        }
        return res;
    }

    /***************************************************************************
     * Implements binary operators between Decimal and build-in type values.
     *
     * The precision of the decimal result is equal
     * to the precision of the decimal argument.
     */
    Decimal opBinary(string op, T)(T rhs) const
    if (op.among("+", "-", "*", "/", "^^")) {
        static if (op == "^^") {
            return pow(this, Decimal(rhs, this.fixedPoint));
        }
        return this.opBinary!op(Decimal(rhs, this.fixedPoint));
    }

    /***************************************************************************
     * Implements operators with built-in integers on the left-hand side
     * and `Decimal` on the right-hand side.
     *
     * The precision of the decimal result is equal
     * to the precision of the decimal argument.
     */
    Decimal opBinaryRight(string op, T)(T value) const
    if (op.among("+", "*", "-", "/", "^^")) {
        auto decValue = Decimal(value, this.fixedPoint);
        static if (op == "^^") {
            return pow(decValue, this);
        }
        return decValue.opBinary!op(this);
    }

    /***************************************************************************
     * Implements assignment operators of the form `Decimal op= integer`:
     * "+=", "-=", "*=", "/=", "^^=".
     * Position of the fixed point (precision) doesn't change.
     */
    Decimal opOpAssign(string op, T)(T value)
    if (op.among("+", "-", "*", "/", "^^")) {
        Decimal rhs = Decimal(value, this.fixedPoint);
        static if (op == "+") this.value += rhs.value;
        static if (op == "-") this.value -= rhs.value;
        static if (op == "*") {
            this.value *= rhs.value;
            BigInt rem;
            BigInt divisor = BigInt(10)^^this.fixedPoint;
            divMod(this.value, divisor, this.value, rem);
            if (divisor < absBigInt(rem*10)) {
                this.value += calcRoundingCompensation(rem);
            }
        }
        static if (op == "/") {
            if (rhs.value == BigInt(0)) {
                throw new DecimalException("Division by zero");
            }
            this.value *= BigInt(10)^^this.fixedPoint;
            BigInt rem;
            divMod(this.value, rhs.value, this.value, rem);
            if (rhs.value < absBigInt(rem*10)) {
                this.value += calcRoundingCompensation(rem);
            }
        }
        static if (op == "^^") {
            Decimal res = pow(this, rhs);
            this = Decimal(res, this.fixedPoint);
        }
        return this;
    }

    /***************************************************************************
     * Check if Decimal has zero fractional part.
     */
    bool isInteger() const {
        if (fixedPoint == 0) {
            return true;
        }
        string fractionalPart = toFullString().split(".")[1];
        return all!"a=='0'"(fractionalPart);
    }

    /***************************************************************************
     * Check if Decimal has zero value.
     */
    bool isZero() const {
        return this.value == 0;
    }

private:

    size_t leadToUnifiedFixedPoint(ref Decimal lhs, ref Decimal rhs)
    pure const {
        size_t bestPrecision, worstPrecision;
        if (lhs.fixedPoint > rhs.fixedPoint) {
            bestPrecision = lhs.fixedPoint;
            worstPrecision = rhs.fixedPoint;
            rhs.value *= BigInt(10)^^(bestPrecision-worstPrecision);
        } else {
            worstPrecision = this.fixedPoint;
            bestPrecision = rhs.fixedPoint;
            lhs.value *= BigInt(10)^^(bestPrecision-worstPrecision);
        }
        return bestPrecision;
    }

    BigInt calcSignForBigInt(BigInt value) pure const {
        if (value > 0) return BigInt(1);
        if (value == 0) return BigInt(0);
        return BigInt(-1);
    }

    BigInt absBigInt(BigInt value) pure const {
        return value >= 0 ? value : -value;
    }

    BigInt calcRoundingCompensation(BigInt remainder) pure const {
        BigInt absRemainder = absBigInt(remainder);
        BigInt sign = calcSignForBigInt(remainder);
        if (remainder % 10 == remainder) {
            if (remainder >= 0) {
                if (remainder % 10 >= 5) {
                    return BigInt(1);
                }
            } else {
                if (-remainder % 10 >= 5) {
                    return BigInt(-1);
                }
            }
            return BigInt(0);
        }
        size_t calcDigits(BigInt value) {
            return absBigInt(value).toDecimalString.length;
        }
        if (absRemainder % 10 >= 5) {
            if (calcDigits(absRemainder) != calcDigits(absRemainder + 1)) {
                return sign; // overflow
            }
            remainder = (remainder + sign) / 10;
        } else {
            remainder = remainder / 10;
        }
        return calcRoundingCompensation(remainder);
    }
}


/*******************************************************************************
 * Calculates e^x for Decimal.
 */
Decimal exp(Decimal x) {
    auto precision = x.fixedPoint;
    auto additPrecision = precision * 2 + 3;
    Decimal res = x + Decimal(1, additPrecision);
    Decimal numerator = Decimal(x, additPrecision);
    Decimal denominator = Decimal(1, additPrecision);
    foreach (i; 2 .. additPrecision) {
        numerator = numerator * x;
        denominator = denominator * i;
        res = res + numerator/denominator;
    }
    return Decimal(res, precision);
}


/*******************************************************************************
 * Calculates the natural logarithm x (for `real`).
 * The precision is 18 decimal places.
 */
real ln(real x) {
    auto numerator = (x-1)/(x+1);
    auto n2 = numerator * numerator;
    auto res = numerator;
    real elem; // row element
    real denominator = 3;
    do {
        numerator = numerator * n2;
        elem = numerator / denominator;
        res = res + elem;
        denominator = denominator + 2;
    } while (elem > 0.000000000000000001);

    return res * 2;
}


/*******************************************************************************
 * Calculates the natural logarithm x of the Decimal type.
 */
Decimal ln(Decimal x) {
    auto precision = x.fixedPoint;
    auto additPrecision = x.fixedPoint * 2 + 3;
    auto extendedX = Decimal(x, additPrecision);
    auto numerator = (extendedX-1)/(extendedX+1);
    auto n2 = numerator * numerator;
    auto res = numerator;
    Decimal elem = Decimal(0, additPrecision); // row element
    Decimal denominator = Decimal(3, additPrecision);
    char[] zeros = makeFilledArray(precision, '0');
    string strMin = "0." ~ zeros.idup ~ "1";
    auto minValue = Decimal(strMin, additPrecision);
    do {
        numerator = numerator * n2;
        elem = numerator / denominator;
        res = res + elem;
        denominator = denominator + 2;
    } while (elem > minValue);

    res.value *= 2;
    return Decimal(res, x.fixedPoint);
}


/*******************************************************************************
 * Calculates the logarithm.
 *
 * Params:
 *     b = Decimal value
 *     a = base of the logarithm
 */
Decimal log(Decimal b, Decimal a) {
    auto precision = b.fixedPoint > a.fixedPoint ? b.fixedPoint : a.fixedPoint;
    auto additPrecison = precision * 2 + 3;
    b = Decimal(b, additPrecison);
    a = Decimal(a, additPrecison);
    return Decimal(ln(b) / ln(a), precision);
}


/*******************************************************************************
 * Returns the value of x raised to the power of y.
 *
 * Params:
 *     x = Decimal raised to a power
 *     y = exponent of the ulong type
 */
Decimal pow(Decimal x, ulong y) {
    if (y == 0) {
        return Decimal(1, x.fixedPoint);
    } else if (y == 1) {
        return x;
    }
    Decimal res = x;
    foreach(i; 1 .. y) {
        res = res * x;
    }
    return res;
}


/*******************************************************************************
 * Returns the value of x raised to the power of y.
 *
 * Params:
 *    x = Decimal raised to a power
 *    y = Decimal exponent
 */
Decimal pow(Decimal x, Decimal y) {
    size_t digits = x.fixedPoint > y.fixedPoint ? x.fixedPoint : y.fixedPoint;
    x = Decimal(x, digits*2+8);
    y = Decimal(y, digits*2+8);
    auto res = exp(y * ln(x));
    return Decimal(res, digits);
}


/*******************************************************************************
 * Base exeption for this module
 */
class DecimalException : Exception { mixin RealizeException; }



/*******************************************************************************
********************************** UNITTESTS ***********************************
*******************************************************************************/

// constructors
unittest {
    auto x = Decimal("36");
    assert(x.value == BigInt("36000000000000000000"));
    assert(x.toFullString == "36.000000000000000000");

    x = Decimal("36.6");
    assert(x.value == BigInt("36600000000000000000"));
    assert(x.toFullString == "36.600000000000000000");

    x = Decimal("36,6");
    assert(x.value == BigInt("36600000000000000000"));
    assert(x.toFullString == "36.600000000000000000");

    x = Decimal("-36,6");
    assert(x.value == BigInt("-36600000000000000000"));
    assert(x.toFullString == "-36.600000000000000000");

    x = Decimal(36);
    assert(x.value == BigInt("36000000000000000000"));
    assert(x.toFullString == "36.000000000000000000");

    x = Decimal(-36);
    assert(x.value == BigInt("-36000000000000000000"));
    assert(x.toFullString == "-36.000000000000000000");

    x = Decimal(-36.6, 4);
    assert(x.value == BigInt("-366000"));
    assert(x.toFullString == "-36.6000");

    x = Decimal("0");
    assert(x.value == BigInt("0"));
    assert(x.toFullString == "0.000000000000000000");

    x = Decimal(0);
    assert(x.value == BigInt("0"));
    assert(x.toFullString == "0.000000000000000000");

    x = Decimal(0.0);
    assert(x.value == BigInt("0"));
    assert(x.toFullString == "0.000000000000000000");

    x = Decimal(-0.0);
    assert(x.value == BigInt("0"));
    assert(x.toFullString == "0.000000000000000000");

    x = Decimal("0.12");
    assert(x.value == BigInt("120000000000000000"));
    assert(x.toFullString == "0.120000000000000000");

    x = Decimal(0.12);
    assert(x.value == BigInt("120000000000000000"));
    assert(x.toFullString == "0.120000000000000000");

    x = Decimal("-0.12");
    assert(x.value == BigInt("-120000000000000000"));
    assert(x.toFullString == "-0.120000000000000000");

    x = Decimal(-0.12);
    assert(x.value == BigInt("-120000000000000000"));
    assert(x.toFullString == "-0.120000000000000000");

    x = Decimal("-0.02");
    assert(x.value == BigInt("-20000000000000000"));
    assert(x.toFullString == "-0.020000000000000000");

    x = Decimal(-0.02);
    assert(x.value == BigInt("-20000000000000000"));
    assert(x.toFullString == "-0.020000000000000000");

    x = Decimal("0.1");
    assert(x.value == BigInt("100000000000000000"));
    assert(x.toFullString == "0.100000000000000000");

    x = Decimal(0.1);
    assert(x.value == BigInt("100000000000000000"));
    assert(x.toFullString == "0.100000000000000000");

    x = Decimal("-0.00123");
    assert(x.value == BigInt("-1230000000000000"));
    assert(x.toFullString == "-0.001230000000000000");

    x = Decimal(-0.00123);
    assert(x.value == BigInt("-1230000000000000"));
    assert(x.toFullString == "-0.001230000000000000");

    x = Decimal(BigInt(36));
    assert(x.value == BigInt("36000000000000000000"));
    assert(x.toFullString == "36.000000000000000000");
}

// error data
unittest {
    import std.stdio : writefln;

    bool error = false;
    Decimal x;
    try x = Decimal("34.2.55");
    catch (DecimalException e) error = true;
    assert(error);

    error = false;
    try x = Decimal("");
    catch (DecimalException e) error = true;
    assert(error);

    error = false;
    try x = Decimal("23.1A");
    catch (DecimalException e) error = true;
    assert(error);

    error = false;
    try x = Decimal(float.infinity);
    catch(DecimalException e) error = true;
    assert(error);

    error = false;
    try x = Decimal(float.nan);
    catch(DecimalException e) error = true;
    assert(error);

    error = false;
    try x = Decimal(2)/Decimal(0);
    catch(DecimalException e) error = true;
    assert(error);

    error = false;
    x = Decimal("9999999999999999999999999999999999");
    try writefln("%d", x.to!long);
    catch(std.conv.ConvOverflowException e) error = true;
    assert(error);

    error = false;
    x = Decimal(65_536);
    try writefln("%d", x.to!ushort);
    catch(std.conv.ConvOverflowException e) error = true;
    assert(error);
}

//transform/copy constructor
unittest {
    auto x = Decimal("6.54321", 6);
    assert(x.toFullString == "6.543210");
    auto y = Decimal(x, 4);
    assert(x != y);
    assert(y.toFullString == "6.5432");
    auto z = Decimal(x, 7);
    assert(x == z);
    assert(z.toFullString == "6.5432100");
}


//convertation
unittest {
    auto x = Decimal("5", 4);
    assert(x.toFullString == "5.0000");
    assert(x.to!string == "5");
    assert(x.to!real == 5.0);
    assert(x.to!long == 5);
    x = Decimal("5.8");
    assert(x.to!long == 5);
    x = Decimal("-5.8");
    assert(x.to!long == -5);
    assert(x.to!bool == true);
    x = Decimal("0.0000000000000000000000000000001", 50);
    assert(x.to!bool == true);
    x = Decimal(0);
    assert(x.to!bool == false);
}

// usage of output format
unittest {
    Decimal x = Decimal("5.123456789");
    assert(format!"%2.7D"(x) == "5.1234568");
    assert(format!"%.7D"(x) == "5.1234568");
    assert(format!"%.12D"(x) == "5.123456789000");
    assert(format!"%D"(x) == "5.123456789000000000");
    assert(format!"%8.2D"(x) == "    5.12");
    assert(format!"%08.2D"(x) == "00005.12");
    assert(format!"%+08.2D"(x) == "+0005.12");
    assert(format!"%+.2D"(x) == "+5.12");
    assert(format!"%+.0D"(x) == "+5");
    Decimal zero = Decimal(0);
    assert(format!"%.8D"(zero) == "0.00000000");
    assert(format!"%D"(zero) == "0.000000000000000000");
    assert(format!"%s"(zero) == "0");
}


// comparison
unittest {
    auto x = Decimal(-36.6, 4);
    assert(Decimal("-36.6") == x);

    assert(Decimal("0") == Decimal(-0.0));

    assert(Decimal(-0.0002) < Decimal(-0.0001));
    assert(Decimal(-0.0001) >= Decimal(-0.0001));
    assert(Decimal(-0.0001, 8) == Decimal(-0.0001, 5));
    assert(Decimal(-0.0003, 17) == Decimal(-0.0003, 18));
    assert(Decimal(0.0004, 5) > Decimal(0.00035, 8));
    assert(Decimal(0.0004, 8) > Decimal(0.00035, 5));
    assert(Decimal(-0.0004, 8) < Decimal(0.00035, 5));
    assert(Decimal(-0.0004, 5) < Decimal(0.00035, 8));

    assert(Decimal(1) == 1);
    assert(1 == Decimal(1));

    assert(Decimal(100.5) == 100.5);
    assert(Decimal(100.6) > 100.5);
}


// precision
unittest {
    auto x = Decimal("36.6", 4);
    assert(x.toFullString == "36.6000");

    x = Decimal("36,654321", 3);
    assert(x.toFullString == "36.654");

    x = Decimal("-36,654321", 3);
    assert(x.toFullString == "-36.654");

    x = Decimal(-36.654321, 3);
    assert(x.toFullString == "-36.654");

    x = Decimal("36,654321", 0);
    assert(x.toFullString == "36");

    x = Decimal("-36,654321", 0);
    assert(x.toFullString == "-36");

    x = Decimal(-36.654321, 0);
    assert(x.toFullString == "-36");

    x = Decimal("-36.654321", 0);
    assert(x.toFullString == "-36");

    x = Decimal("-0.03", 0);
    assert(x.toFullString == "0");

    x = Decimal(100, 2);
    assert(x.toFullString == "100.00");

    x = Decimal(100, 0);
    assert(x.toFullString == "100");

    // round
    x = Decimal(99.79, 2);
    assert(Decimal(x, 1).toFullString == "99.8");
}

//unary operators
unittest {
    auto x = Decimal(0);
    x++;
    assert(x == Decimal(1));

    x = Decimal(-0.4);
    x++;
    assert(x == Decimal(0.6));
    x--;
    assert(x == Decimal(-0.4));

    x = Decimal(0);
    x--;
    assert(x == Decimal(-1));

    x = Decimal(-9);
    assert(x == -Decimal(9));

    x = -Decimal(0.3);
    assert(x == Decimal(-0.3));
}

// plus/minus
unittest {
    assert(2.3.D + 7.8.D == 10.1.D);
    assert(Decimal("0.5") + Decimal("-0.7") == Decimal("-0.2"));
    assert(Decimal("20.9") - Decimal("5.87") == Decimal("15.03"));
    assert(Decimal("20.39", 2) + Decimal(11.125, 3) == Decimal("31.515"));

    assert(Decimal("0.6") + 0.6 == Decimal("1.2"));
    assert(0.6 + Decimal("0.6") == Decimal("1.2"));
    assert(Decimal("1.5") - 1.7 == Decimal("-0.2"));
    assert(1.5 - Decimal(1.7) == Decimal("-0.2"));
}

// +=
unittest {
    auto x = Decimal(5);
    auto y = Decimal(7);
    x += y;
    assert(x == Decimal(12));
    assert(y == Decimal(7)); // no effects

    x = Decimal("-0.1");
    y = Decimal("2.05");
    y += x;
    assert(y == Decimal("1.95"));
    assert(x == Decimal("-0.1"));

    x = Decimal("4.12345", 5);
    y = Decimal("0.0000001");
    x += y;
    assert(x == Decimal("4.12345")); // no effects
    assert(x.fixedPoint == 5); // no effects

    x = Decimal("4.12345", 5);
    y = Decimal("0.000009");
    x += y;
    assert(x == Decimal("4.12346"));
    assert(x.fixedPoint == 5); // no effects

    // other data types
    x = Decimal(7);
    x += 2;
    assert(x == Decimal(9));
    x -= 1.7;
    assert(x == Decimal(7.3));
    x += "11.1";
    assert(x == Decimal(18.4));
}

// -=
unittest {
    auto x = Decimal(5);
    auto y = Decimal(7);
    x -= y;
    assert(x == Decimal(-2));
    assert(y == Decimal(7)); // no effects

    x = Decimal("-0.1");
    y = Decimal("2.05");
    y -= x;
    assert(y == Decimal("2.15"));
    assert(x == Decimal("-0.1")); // no effects

    x = Decimal("4.12345", 5);
    y = Decimal("0.0000001");
    x -= y;
    assert(x == Decimal("4.12345")); // no effects
    assert(x.fixedPoint == 5); // no effects
    assert(y.fixedPoint == defaultFixedPoint); // no effects

    x = Decimal("4.12345", 5);
    y = Decimal("0.000009");
    x -= y;
    assert(x == Decimal("4.12344"));
    assert(x.fixedPoint == 5); // no effects
}

//mutliplication
unittest {
    auto x = 3.D;
    auto y = 2.D;
    assert(x*y == 6.D);
    x = 2809.9324.D;
    y = 2.D;
    assert(x*y == "5619.8648".D);
    y = 2.7.D;
    assert(x*y == "7586,81748".D);
    y = 2.75.D;
    assert(x*y == "7727.3141".D);
    y = 2.05.D;
    assert(x*y == "5760.36142".D);
    x = "3.08".D;
    y = -2.D;
    assert(x*y == "-6.16".D);
    auto pi = "3.141592653589793238".D;
    assert(pi*pi == "9.869604401089358616".D);
    auto e = "2.718281828459045".D;
    assert(e*e == "7.389056098930648948".D);
    x = "0.3".D;
    y = 10.D;
    assert(x*y == 3.D);
    assert(100.D == 10.D * 10.D);
    assert(0.01.D == 0.1.D * 0.1.D);
    assert(0.001.D == "0.01".D * "0.1".D);
    assert(0.001.D == "0.1".D * "0.01".D);

    assert(2.5.D * 2 == 5.D);
    assert(2 * 2.5.D * 2 == 10.D);

    // check rounding
    x = Decimal("0.1", 2);
    y = Decimal("2.05", 2);
    auto z = x * y;
    assert(z == Decimal("0.21", 2));
    x = Decimal("-0.1", 2);
    y = Decimal("2.05", 2);
    z = x * y;
    assert(z == Decimal("-0.21", 2));
}

//division
unittest {
    assert(5.D / 2.D == 2.5.D);
    assert(5 / 2.D == 2.5.D);
    assert(5.D / 2 == 2.5.D);
    assert(0.D / 2.D == 0.D);
    assert("-13.50".D / "2.25".D == "-6".D);
    assert(Decimal("-13.50", 10) / Decimal("2.25", 4) == "-6".D);
    assert(1.D == 1.D/BigInt(1));
    assert(1.D == Decimal(1, 10)/BigInt(1));
    assert(1.D == Decimal(1, 25)/BigInt(1));
    assert(0.5.D == 1.D/BigInt(2));

    auto x = Decimal(5);
    auto y = Decimal(7);
    assert(x/y == Decimal("0.714285714285714286"));
}

// *=
unittest {
    auto x = Decimal(5);
    auto y = Decimal(7);
    x *= y;
    assert(x == Decimal(35));
    assert(y == Decimal(7)); // no effects

    x = Decimal("-0.1");
    y = Decimal("2.05");
    y *= x;
    assert(y == Decimal("-0.205"));
    assert(x == Decimal("-0.1")); // no effects

    x = Decimal("4.12345", 5);
    y = Decimal("0.0000001");
    x *= y;
    assert(x == Decimal(0));
    assert(x.fixedPoint == 5); // no effects
    assert(y.fixedPoint == defaultFixedPoint); // no effects

    x = Decimal("4.12345", 5);
    y = Decimal("0.000009");
    x *= y;
    assert(x == Decimal(0.00004));
    assert(x.fixedPoint == 5); // no effects

    x = Decimal("-0.1");
    y = Decimal("2.05");
    y *= x;
    assert(y == Decimal("-0.205"));
    assert(x == Decimal("-0.1")); // no effects

    // check rounding
    x = Decimal("0.1", 2);
    y = Decimal("2.05", 2);
    y *= x;
    assert(y == Decimal("0.21", 2));
    assert(x == Decimal("0.1", 2)); // no effects
    x = Decimal("-0.1", 2);
    y = Decimal("2.05", 2);
    y *= x;
    assert(y == Decimal("-0.21", 2));
    assert(x == Decimal("-0.1", 2)); // no effects

    x = Decimal("4.12345", 5);
    y = Decimal("0.0000001");
    x *= y;
    assert(x == Decimal(0));
    assert(x.fixedPoint == 5); // no effects
    assert(y.fixedPoint == defaultFixedPoint); // no effects

    x = Decimal("4.12345", 5);
    y = Decimal("0.000009");
    x *= y;
    assert(x == Decimal(0.00004));
    assert(x.fixedPoint == 5); // no effects
}

// /=
unittest {
    auto x = Decimal(5);
    auto y = Decimal(7);
    x /= y;
    assert(x == Decimal("0.714285714285714286"));
    assert(y == Decimal(7)); // no effects

    x = Decimal(-5);
    y = Decimal(7);
    x /= y;
    assert(x == Decimal("-0.714285714285714286"));
    assert(y == Decimal(7)); // no effects

}

//abs, round
unittest {
    auto x = Decimal("-0.725");
    assert(x.abs == Decimal("0.725"));
    x = Decimal("4.9");
    assert(x.abs == Decimal("4.9"));

    x = Decimal("43.5");
    assert(x.round() == Decimal("44"));
    x = Decimal("-35.1");
    assert(x.round() == Decimal("-35"));
    x = Decimal("-35.9");
    assert(x.round() == Decimal("-36"));

    x = Decimal("77.383838");
    auto rounded = x.round();
    assert(rounded == Decimal("77"));
    assert(rounded.fixedPoint == Decimal(x.fixedPoint));
    rounded = x.round(1);
    assert(rounded == Decimal("77.4"));
    assert(rounded.fixedPoint == Decimal(x.fixedPoint));
    rounded = x.round(2);
    assert(rounded == Decimal("77.38"));
    assert(rounded.fixedPoint == Decimal(x.fixedPoint));
    rounded = x.round(3);
    assert(rounded == Decimal("77.384"));
    assert(rounded.fixedPoint == Decimal(x.fixedPoint));
    x = Decimal("-77.383838");
    rounded = x.round(3);
    assert(rounded == Decimal("-77.384"));
    assert(rounded.fixedPoint == Decimal(x.fixedPoint));
}

//exp
unittest {
    string e = "2.71828182845904523536028747135266249775724709369995";
    assert(exp(Decimal(1, 18)).toFullString() == e[0 .. 18+2]);
    assert(exp(Decimal(0.5, 6)).toFullString() == "1.648721");
    assert(exp(Decimal(1, 3)) == Decimal(2.718));
    assert(exp(Decimal(1, 2)) == Decimal(2.72));
    assert(exp(Decimal(1, 1)) == Decimal(2.7));
    assert(exp(Decimal(1, 0)) == Decimal(3));
}

//log and ln
unittest {
    string e = "2.71828182845904523536028747135266249775724709369995";
    assert(ln(Decimal(e)).toFullString == "1.000000000000000000");
    assert(ln(Decimal(3)).toFullString == "1.098612288668109691");
    assert(ln(Decimal(3, 9)) == "1.098612289".D);
    assert(ln(Decimal(3, 5)).toFullString == "1.09861");
    assert(ln(Decimal(3, 4)).toFullString == "1.0986");
    assert(ln(Decimal(3, 3)).toFullString == "1.099");
    assert(ln(Decimal(3, 2)).toFullString == "1.10");
    assert(ln(Decimal(3, 1)).toFullString == "1.1");
    assert(ln(Decimal(3, 0)).toFullString == "1");
    assert(format!"%.8D"(ln(Decimal(e, 5))) == "1.00000000");
    assert(format!"%.8D"(ln(Decimal(e, 1))) == "1.00000000");

    assert(log(100.D, 10.D) == 2.D);
}

//pow
unittest {
    auto x = Decimal(2);
    assert(pow(x, 2) == 4.D);
    assert(pow(3.D, 3) == 27.D);
    assert(pow(2.D, 0.1.D) == "1.071773462536293164".D);
    auto pi = Decimal(
        "3.1415926535897932384626433832795028841971693993751" ~
        "05820974944592307816406286208998628034825342117",
        96 //precision
    );
    auto piSquared = pow(pi, 2);
    auto expectedResult = Decimal(
        "9.86960440108935861883449099987615113531369940724" ~
        "0790626413349376220044822419205243001773403718552",
        96
    );
    assert(piSquared == expectedResult);
    assert(piSquared.fixedPoint == expectedResult.fixedPoint);

    //overloading ^^
    assert(2.D ^^ 3.D == 8.D);
    assert(2.D ^^ 3 == 8.D);
    assert(2 ^^ 3.D == 8.D);
    assert(Decimal(2.0, 2) ^^ Decimal(0.1, 18) == "1.071773462536293164".D);
    assert(Decimal(2.0, 2) ^^ Decimal(0.1, 16) == "1.0717734625362932".D);
    assert(2.D ^^ 0.1.D == "1.071773462536293164".D);
    assert(2.D ^^ 0.1 == "1.071773462536293164".D);
    assert(2.0 ^^ 0.1.D == "1.071773462536293164".D);

    auto a = Decimal(4.0, 2);
    auto b = Decimal(2.0, 3);
    auto z = a ^^ b;
    assert(z.fixedPoint == 3);
    assert(z == Decimal(16));

    a = Decimal(4, 0);
    b = Decimal(2, 0);
    z = a ^^ b;
    assert(z.fixedPoint == 0);
    assert(z == Decimal(16));

    a = Decimal(4.5, 1);
    b = Decimal(5.25, 1);
    z = a ^^ b;
    assert(z == Decimal("2492.9"));

    a = Decimal(4.5, 1);
    b = Decimal(5.25, 2);
    z = a ^^ b;
    assert(z == Decimal(2687.61));

    // ^^=
    a = Decimal(4.5, 2);
    b = Decimal(5.25, 2);
    a ^^= b;
    assert(a == Decimal(2687.61));

    a = Decimal(4.5, 3);
    b = Decimal(5.25, 3);
    a ^^= b;
    assert(a == Decimal("2687.607"));
}

