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 }