module util.money;

import std.traits : isSomeString, EnumMembers;

/**
 * Basic information about a monetary currency, as defined by ISO 4217.
 * https://en.wikipedia.org/wiki/ISO_4217
 */
struct Currency {
    /// The common 3-character code for the currency, like "USD".
    char[3] code;
    /// The number of digits after the decimal place that the currency supports.
    ubyte fractionalDigits;
    /// The ISO 4217 numeric code for the currency.
    ushort numericCode;
    /// The symbol used when writing monetary values of this currency.
    string symbol;

    static Currency ofCode(S)(in S code) if (isSomeString!S) {
        if (code.length != 3) {
            throw new Exception("Invalid currency code: " ~ code);
        }
        static foreach (c; ALL_CURRENCIES) {
            if (c.code == code) return c;
        }
        throw new Exception("Unknown currency code: " ~ code);
    }
}

/// An enumeration of all available currencies.
enum Currencies : Currency {
    AUD = Currency("AUD", 2, 36, "$"),
    USD = Currency("USD", 2, 840, "$"),
    CAD = Currency("CAD", 2, 124, "$"),
    GBP = Currency("GBP", 2, 826, "£"),
    EUR = Currency("EUR", 2, 978, "€"),
    CHF = Currency("CHF", 2, 756, "Fr"),
    ZAR = Currency("ZAR", 2, 710, "R"),
    JPY = Currency("JPY", 0, 392, "¥"),
    INR = Currency("INR", 2, 356, "₹")
}

immutable(Currency[]) ALL_CURRENCIES = cast(Currency[]) [EnumMembers!Currencies];

unittest {
    assert(Currency.ofCode("USD") == Currencies.USD);
}

/**
 * A monetary value consisting of an integer value, and a currency. The value
 * is interpreted as a multiple of the smallest denomination of the currency,
 * so for example, with USD currency, a value of 123 indicates $1.23.
 */
struct MoneyValue {
    immutable Currency currency;
    immutable long value;

    int opCmp(in MoneyValue other) const {
        if (other.currency != this.currency) return 0;
        if (this.value < other.value) return -1;
        if (this.value > other.value) return 1;
        return 0;
    }

    MoneyValue opBinary(string op)(in MoneyValue rhs) const {
        if (rhs.currency != this.currency)
            throw new Exception("Cannot perform binary operations on MoneyValues with different currencies.");
        static if (op == "+") return MoneyValue(currency, this.value + rhs.value);
        static if (op == "-") return MoneyValue(currency, this.value - rhs.value);
        static assert(false, "Operator " ~ op ~ " is not supported.");
    }

    MoneyValue opBinary(string op)(int rhs) const {
        static if (op == "+") return MoneyValue(currency, this.value + rhs);
        static if (op == "-") return MoneyValue(currency, this.value - rhs);
        static if (op == "*") return MoneyValue(currency, this.value * rhs);
        static if (op == "/") return MoneyValue(currency, this.value / rhs);
        static assert(false, "Operator " ~ op ~ " is not supported.");
    }

    MoneyValue opUnary(string op)() const {
        static if (op == "-") return MoneyValue(currency, -this.value);
        static assert(false, "Operator " ~ op ~ " is not supported.");
    }
}