finnow/finnow-api/source/util/money.d

152 lines
5.2 KiB
D

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.");
}
}