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 name of the currency. string name; /// 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 defining all available currencies. This is generated at * compile time by reading currency data from CSV files and generating a list * of currency declarations. */ mixin("enum Currencies : Currency {\n" ~ getCurrenciesEnumMembers() ~ "\n}"); /** * A list of all currencies, as a convenience for getting all members of the * `Currencies` enum. */ immutable(Currency[]) ALL_CURRENCIES = cast(Currency[]) [EnumMembers!Currencies]; private Currency[] readCurrenciesFromFile() { import std.csv; import std.stdio; import std.algorithm; import std.typecons; import std.array; import std.string; import std.conv; // First read the list of known currency symbols and use it as a lookup table. string[string] knownCurrencySymbols; const string currencySymbolsFile = import("currency_symbols.csv"); foreach (record; currencySymbolsFile.csvReader!(Tuple!(string, string))) { string code = record[0].strip(); string symbol = record[1].strip(); knownCurrencySymbols[code] = symbol; } // Then read the list of currencies. auto app = appender!(Currency[]); auto codes = appender!(string[]); const string currenciesFile = import("currency_codes_ISO4217.csv"); foreach (record; currenciesFile.csvReader!(Tuple!(string, string, string, string, string, string))) { string currencyName = record[1].strip(); string code = record[2].strip(); string numericCode = record[3].strip(); string minorUnit = record[4].strip(); string withdrawalDate = record[5].strip(); string symbol; if (code in knownCurrencySymbols) { symbol = knownCurrencySymbols[code]; } else { symbol = "$"; } if ( withdrawalDate.length > 0 || canFind(codes[], code) || code.length != 3 ) { continue; } if (minorUnit == "-") { minorUnit = "0"; } app ~= Currency(currencyName, code[0..3], minorUnit.to!ubyte, numericCode.to!ushort, symbol); codes ~= code; } return app[]; } private string getCurrenciesEnumMembers() { import std.algorithm; import std.array; import std.format; import std.conv; auto currencies = readCurrenciesFromFile(); return currencies .map!(c => format!"%s = Currency(\"%s\", \"%s\", %d, %d, \"%s\")"( c.code, c.name, c.code, c.fractionalDigits, c.numericCode, c.symbol )) .joiner(",\n") .array.to!string; } 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."); } }