1 module tagion.script.Currency; 2 @safe: 3 import std.algorithm; 4 import std.algorithm.searching : canFind; 5 import std.format; 6 import std.range; 7 import std.traits : isFloatingPoint, isIntegral, isNumeric; 8 import std.traits; 9 import tagion.hibon.HiBONRecord : HiBONRecord, label, recordType; 10 import tagion.script.ScriptException : ScriptException, scriptCheck = check; 11 12 const(V) totalAmount(R, V = ElementType!R)(R r) if (isInputRange!R && isCurrency!V) { 13 scriptCheck(r.all!(v => v.units >= 0), format("Negative currency unit %s ", V.UNIT)); 14 return r.sum; 15 } 16 17 template isCurrency(alias T) { 18 enum isCurrency = __traits(hasMember, T, "units") && is(typeof(T.units()) == long); 19 } 20 21 version (unittest) { 22 alias MyCurrency = Currency!"My"; 23 } 24 static unittest { 25 static assert(isCurrency!MyCurrency); 26 } 27 28 unittest { 29 import std.exception; 30 31 auto list = [MyCurrency(12.0), MyCurrency(120.0), MyCurrency(1300.0)]; 32 import std.stdio; 33 34 assert(list.totalAmount == MyCurrency(1432.0)); 35 assertThrown!ScriptException((list ~ MyCurrency(-1.3)).totalAmount); 36 37 } 38 39 struct Currency(string _UNIT, long _BASE_UNIT = 1_000_000_000, long MAX_VALUE_IN_BASE_UNITS = 1_000_000_000) { 40 static assert(_BASE_UNIT > 0, "Base unit must be positive"); 41 static assert(UNIT_MAX > 0, "Max unit mist be positive"); 42 enum long BASE_UNIT = _BASE_UNIT; 43 enum long UNIT_MAX = MAX_VALUE_IN_BASE_UNITS * BASE_UNIT; 44 enum UNIT = _UNIT; 45 // enum type_name = _UNIT; 46 protected { 47 @label("$") long _units; 48 } 49 50 mixin HiBONRecord!( 51 q{ 52 this(T)(T whole) pure if (isFloatingPoint!T) { 53 scope(exit) { 54 check_range; 55 } 56 _units = cast(long)(whole * BASE_UNIT); 57 } 58 59 this(T)(const T units) pure if (isIntegral!T) { 60 scope(exit) { 61 check_range; 62 } 63 _units = units; 64 } 65 }); 66 67 bool verify() const pure nothrow { 68 return _units >= -UNIT_MAX && _units <= UNIT_MAX; 69 } 70 71 void check_range() const pure { 72 73 scriptCheck(_units >= -UNIT_MAX && _units <= UNIT_MAX, 74 format("Value out of range [%s:%s] value is %s", 75 toValue(-UNIT_MAX), 76 toValue(UNIT_MAX), 77 toValue(_units))); 78 } 79 80 Currency opBinary(string OP)(const Currency rhs) const pure 81 if ( 82 ["+", "-", "%"].canFind(OP)) { 83 enum code = format(q{return Currency(_units %1$s rhs._units);}, OP); 84 mixin(code); 85 } 86 87 Currency opBinary(string OP, T)(T rhs) const pure 88 if (isIntegral!T && (["+", "-", "*", "%", "/"].canFind(OP))) { 89 enum code = format(q{return Currency(_units %s rhs);}, OP); 90 mixin(code); 91 } 92 93 Currency opBinaryRight(string OP, T)(T left) const pure 94 if (isIntegral!T && (["+", "-", "*"].canFind(OP))) { 95 enum code = format(q{return Currency(left %s _units);}, OP); 96 mixin(code); 97 } 98 99 Currency opUnary(string OP)() const pure if (OP == "-" || OP == "-") { 100 static if (OP == "-") { 101 return Currency(-_units); 102 } 103 else { 104 return Currency(_units); 105 } 106 } 107 108 void opOpAssign(string OP)(const Currency rhs) pure 109 if (["+", "-", "%"].canFind(OP)) { 110 scope (exit) { 111 check_range; 112 } 113 enum code = format(q{_units %s= rhs._units;}, OP); 114 mixin(code); 115 } 116 117 void opOpAssign(string OP, T)(const T rhs) pure 118 if (isIntegral!T && (["+", "-", "*", "%", "/"].canFind(OP))) { 119 scope (exit) { 120 check_range; 121 } 122 enum code = format(q{_units %s= rhs;}, OP); 123 mixin(code); 124 } 125 126 void opOpAssign(string OP, T)(const T rhs) pure 127 if (isFloatingPoint!T && (["*", "%", "/"].canFind(OP))) { 128 scope (exit) { 129 check_range; 130 } 131 enum code = format(q{_units %s= rhs;}, OP); 132 mixin(code); 133 } 134 135 pure const nothrow @nogc { 136 137 bool opEquals(const Currency x) { 138 return _units == x._units; 139 } 140 141 bool opEquals(T)(T x) if (isNumeric!T) { 142 import std.math; 143 144 static if (isFloatingPoint!T) { 145 return isClose(value, x, 1e-9); 146 } 147 else { 148 return _units == x; 149 } 150 } 151 152 int opCmp(const Currency x) { 153 if (_units < x._units) { 154 return -1; 155 } 156 else if (_units > x._units) { 157 return 1; 158 } 159 return 0; 160 } 161 162 int opCmp(T)(T x) if (isNumeric!T) { 163 if (_units < x) { 164 return -1; 165 } 166 else if (_units > x) { 167 return 1; 168 } 169 return 0; 170 } 171 172 @property 173 long units() { 174 return _units; 175 } 176 177 @property 178 long axios() { 179 if (_units < 0) { 180 return -(-_units % BASE_UNIT); 181 } 182 return _units % BASE_UNIT; 183 } 184 185 @property 186 long whole() { 187 if (_units < 0) { 188 return -(-_units / BASE_UNIT); 189 } 190 return _units / BASE_UNIT; 191 } 192 193 @property 194 double value() { 195 return double(_units) / BASE_UNIT; 196 } 197 198 T opCast(T)() { 199 static if (is(Unqual!T == double)) { 200 return value; 201 } 202 else { 203 static assert(0, format("%s casting is not supported", T.stringof)); 204 } 205 } 206 207 } 208 209 static string toValue(const long units) pure nothrow { 210 long value = units; 211 if (units < 0) { 212 value = -value; 213 } 214 const sign = (units < 0) ? "-" : ""; 215 return only(sign, (value / BASE_UNIT).to!string, ".", (value % BASE_UNIT).to!string).join; 216 } 217 218 string toString() pure const nothrow { 219 return toValue(_units); 220 } 221 }