152 lines
5.2 KiB
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.");
|
|
}
|
|
} |