1 /**
2 Decimal module.
3 */
4 module karasux.decimal;
5 
6 import std.algorithm : max;
7 import std.ascii : isDigit;
8 import std.math : floor, log10, abs;
9 import std.string : format;
10 import std.traits : isFloatingPoint;
11 import std.typecons : Nullable, nullable, Tuple, tuple;
12 
13 @safe:
14 
15 /**
16 Decimal type.
17 */
18 struct Decimal
19 {
20     /**
21     Min price number.
22     */
23     static immutable(Decimal) min = Decimal(long.min, ubyte.min);
24 
25     /**
26     price number.
27     */
28     static immutable(Decimal) max = Decimal(long.max, ubyte.max);
29 
30     /**
31     Number mantissa.
32     */
33     long mantissa;
34 
35     /**
36     Number exponent.
37     */
38     ubyte exponent;
39 
40     /**
41     Add exponent and shift mantissa.
42 
43     Params:
44         n = shift value.
45     Returns:
46         shifted number.
47     */
48     Decimal addExponent(ubyte n) const @nogc nothrow pure
49     in ((cast(uint) exponent) + n <= ubyte.max)
50     out (r; (mantissa > 0) ? r.mantissa >= mantissa : r.mantissa <= mantissa)
51     {
52         if (n == 0)
53         {
54             return this;
55         }
56 
57         return Decimal(mantissa * (10 ^^ n), cast(ubyte)(exponent + n));
58     }
59 
60     ///
61     @nogc nothrow pure unittest
62     {
63         auto price = Decimal(123456, 3);
64         assert(price.addExponent(0) == Decimal(123456, 3));
65         assert(price.addExponent(1) == Decimal(1234560, 4));
66         assert(price.addExponent(2) == Decimal(12345600, 5));
67     }
68 
69     ///
70     @nogc nothrow pure unittest
71     {
72         auto price = Decimal(-123456, 3);
73         assert(price.addExponent(0) == Decimal(-123456, 3));
74         assert(price.addExponent(1) == Decimal(-1234560, 4));
75         assert(price.addExponent(2) == Decimal(-12345600, 5));
76     }
77 
78     /**
79     Compare other price number.
80 
81     Params:
82         other = other value.
83     Returns:
84         true if equal value.
85     */
86     bool opEquals()(auto scope ref const(Decimal) other) const @nogc nothrow pure
87     {
88         if (exponent == other.exponent)
89         {
90             return mantissa == other.mantissa;
91         }
92 
93         return this.normalized.mantissa == other.normalized.mantissa;
94     }
95 
96     ///
97     @nogc nothrow pure unittest
98     {
99         immutable a = Decimal(123456, 3);
100         assert(a == a);
101 
102         immutable b = Decimal(1234560, 4);
103         assert(a == b);
104 
105         assert(a != Decimal(123457, 3));
106         assert(a != Decimal(1234567, 4));
107     }
108 
109     /**
110     Returns:
111         value hash.
112     */
113     size_t toHash() const @nogc nothrow pure @safe
114     {
115         immutable n = normalized;
116         return (31 * n.mantissa) ^ (131 * n.exponent);
117     }
118 
119     ///
120     @nogc nothrow pure unittest
121     {
122         assert(Decimal(12300, 2).toHash == Decimal(12300, 2).toHash);
123         assert(Decimal(12300, 2).toHash == Decimal(123000, 3).toHash);
124         assert(Decimal(12300, 2).toHash != Decimal(12300, 1).toHash);
125     }
126 
127     /**
128     Compare two prices.
129 
130     Params:
131         other = other price number.
132     Returns:
133         opCmp result.
134     */
135     int opCmp()(auto ref const(Decimal) other) const @nogc nothrow pure scope
136     {
137         if (exponent == other.exponent)
138         {
139             return mantissa.cmp(other.mantissa);
140         }
141 
142         immutable m = matchExponent(this, other);
143         return m[0].mantissa.cmp(m[1].mantissa);
144     }
145 
146     ///
147     @nogc nothrow pure unittest
148     {
149         immutable a = Decimal(123456, 3);
150         assert(!(a < a));
151         assert(!(a > a));
152         assert(a <= a);
153         assert(a >= a);
154 
155         immutable b = Decimal(123457, 3);
156         assert(a < b);
157         assert(a <= b);
158         assert(!(a > b));
159         assert(!(a >= b));
160 
161         assert(b > a);
162         assert(b >= a);
163         assert(!(b < a));
164         assert(!(b <= a));
165 
166         immutable c = Decimal(1234560, 4);
167         assert(!(a < c));
168         assert(!(a > c));
169         assert(a <= c);
170         assert(a >= c);
171 
172         assert(!(c < a));
173         assert(!(c > a));
174         assert(c <= a);
175         assert(c >= a);
176 
177         immutable d = Decimal(1234561, 4);
178         assert(a < d);
179         assert(!(a > d));
180         assert(a <= d);
181         assert(!(a >= d));
182 
183         assert(!(d < a));
184         assert(d > a);
185         assert(!(d <= a));
186         assert(d >= a);
187     }
188 
189     ///
190     @nogc nothrow pure unittest
191     {
192         immutable a = Decimal(1234559, 4); // 123.4559
193         immutable b = Decimal(123456, 3); // 123.456
194         assert(a < b);
195         assert(a <= b);
196         assert(!(b < a));
197 
198         assert(b > a);
199         assert(b >= a);
200         assert(!(a > b));
201     }
202 
203     /**
204     Calculate other price.
205 
206     Params:
207         rhs = other hand price.
208     Returns:
209         calculated price.
210     */
211     Decimal opBinary(string op)(auto scope ref const(Decimal) rhs) const @nogc nothrow pure
212         if (op == "+" || op == "-")
213     {
214         immutable matched = matchExponent(this, rhs);
215         immutable resultMantissa = mixin("matched[0].mantissa " ~ op ~ "matched[1].mantissa");
216         return Decimal(resultMantissa, matched[0].exponent);
217     }
218 
219     ///
220     @nogc nothrow pure unittest
221     {
222         immutable a = Decimal(123000, 3);
223         immutable b = Decimal(456, 3);
224         assert(a + b == Decimal(123456, 3));
225         assert(a - b == Decimal(122544, 3));
226 
227         immutable c = Decimal(-456, 3);
228         assert(a + c == Decimal(122544, 3));
229         assert(a - c == Decimal(123456, 3));
230 
231         assert(b - a == Decimal(-122544, 3));
232     }
233 
234     /**
235     Calculate mul and div.
236 
237     Params:
238         rhs = other hand value.
239     Returns:
240         calculated price.
241     */
242     Decimal opBinary(string op)(long rhs) const @nogc nothrow pure
243         if (op == "*" || op == "/")
244     {
245         return Decimal(mixin("mantissa " ~ op ~ " rhs"), exponent);
246     }
247 
248     ///
249     @nogc nothrow pure unittest
250     {
251         immutable a = Decimal(123456, 3);
252         assert(a * 2 == Decimal(246912, 3));
253         assert(a / 2 == Decimal(123456 / 2, 3));
254     }
255 
256     /**
257     Calculate mul.
258 
259     Params:
260         rhs = other hand value.
261     Returns:
262         calculated price.
263     */
264     Decimal opBinary(string op)(auto scope ref const(Decimal) rhs) const @nogc nothrow pure
265         if (op == "*")
266     {
267         return Decimal(mantissa * rhs.mantissa, cast(ubyte)(exponent + rhs.exponent));
268     }
269 
270     ///
271     @nogc nothrow pure unittest
272     {
273         immutable a = Decimal(123456, 3);
274         assert(a * Decimal(2, 0) == Decimal(246912, 3));
275         assert(a * Decimal(2, 1) == Decimal(246912, 4));
276     }
277 
278     /**
279     Convert from floating point.
280 
281     Params:
282         value = floating point value.
283         exponent = price number exponent.
284     Returns:
285         price number.
286     */
287     static Decimal from(T)(auto scope ref const(T) value, ubyte exponent) @nogc nothrow pure @safe
288         if (isFloatingPoint!T)
289     {
290         return Decimal(cast(long) .floor(value * (10.0 ^^ exponent) + 0.5), exponent);
291     }
292 
293     ///
294     @nogc nothrow pure @safe unittest
295     {
296         assert(Decimal.from(123.456, 3) == Decimal(123456, 3));
297         assert(Decimal.from(123.001, 3) == Decimal(123001, 3));
298         assert(Decimal.from(123.0019, 3) == Decimal(123002, 3));
299 
300         assert(Decimal.from(-123.456, 3) == Decimal(-123456, 3));
301         assert(Decimal.from(-123.001, 3) == Decimal(-123001, 3));
302         assert(Decimal.from(-123.0019, 3) == Decimal(-123002, 3));
303     }
304 
305     /**
306     Convert to floating point.
307 
308     Params:
309         price = price value.
310     Returns:
311         floating point value.
312     */
313     auto opCast(T)() const @nogc nothrow pure scope
314         if (isFloatingPoint!T)
315     {
316         return (cast(T) mantissa) / (cast(T) 10.0) ^^ exponent;
317     }
318 
319     ///
320     @nogc nothrow pure unittest
321     {
322         import std.math : isClose;
323         assert(isClose(cast(double) Decimal(123456, 0), 123456.0));
324         assert(isClose(cast(double) Decimal(123456, 3), 123.456));
325         assert(isClose(cast(double) Decimal(-123456, 3), -123.456));
326     }
327 
328     /**
329     Returns:
330         String representation.
331     */
332     string toString() const pure
333     {
334         if (exponent == 0)
335         {
336             return format("%d", mantissa);
337         }
338 
339         immutable unit = 10L ^^ exponent;
340         immutable m = mantissa / unit;
341         immutable f = abs(mantissa % unit);
342         return format("%d.%0*d", m, exponent, f);
343     }
344 
345     ///
346     pure unittest
347     {
348         assert(Decimal(123456, 3).toString == "123.456");
349         assert(Decimal(123000, 3).toString == "123.000");
350         assert(Decimal(123456, 0).toString == "123456");
351         assert(Decimal(123456, 6).toString == "0.123456");
352         assert(Decimal(1, 3).toString == "0.001");
353         assert(Decimal(123001, 3).toString == "123.001");
354         assert(Decimal(-123001, 3).toString == "-123.001", Decimal(-123001, 3).toString);
355     }
356 
357     /**
358     Parse price number from string.
359 
360     Params:
361         s = target string
362     Returns:
363         Decimal if succceeded.
364     */
365     static Nullable!Decimal fromString(scope const(char)[] s) nothrow @nogc pure @safe
366     {
367         if (s.length == 0)
368         {
369             return typeof(return).init;
370         }
371 
372         // read sign.
373         bool plus = true;
374         size_t start = 0;
375         if (s[0] == '-')
376         {
377             plus = false;
378             ++start;
379         }
380         else if (s[0] == '+')
381         {
382             ++start;
383         }
384 
385         ulong n = 0;
386         ptrdiff_t dotIndex = s.length;
387         foreach (i, c; s[start .. $])
388         {
389             if (c == '.')
390             {
391                 dotIndex = start + i;
392                 continue;
393             }
394 
395             if (!c.isDigit)
396             {
397                 return typeof(return).init;
398             }
399 
400             n *= 10;
401             n += (c - '0');
402         }
403 
404         immutable exponent = .max(
405             0, (cast(ptrdiff_t) s.length) - dotIndex - 1);
406         if (exponent > ubyte.max)
407         {
408             return typeof(return).init;
409         }
410 
411         return Decimal(plus ? n : -n, cast(ubyte) exponent).nullable;
412     }
413 
414     ///
415     nothrow pure @safe unittest
416     {
417         import std.exception : assertThrown;
418         import std.range : array, repeat;
419 
420         assert(Decimal.fromString(".123456") == Decimal(123456, 6));
421         assert(Decimal.fromString("123.456") == Decimal(123456, 3));
422         assert(Decimal.fromString("12345.6") == Decimal(123456, 1));
423         assert(Decimal.fromString("123456.") == Decimal(123456, 0));
424         assert(Decimal.fromString("123456") == Decimal(123456, 0));
425         assert(Decimal.fromString("0.001") == Decimal(1, 3));
426         assert(Decimal.fromString("100.000") == Decimal(100000, 3));
427 
428         assert(Decimal.fromString("a123456").isNull);
429         assert(Decimal.fromString("123.456a").isNull);
430 
431         string zeros = '0'.repeat(ubyte.max - 1).array;
432         assert(Decimal.fromString("." ~ zeros ~ "1") == Decimal(1, ubyte.max));
433         assert(Decimal.fromString("." ~ zeros ~ "01").isNull);
434 
435         assert(Decimal.fromString("+123.456") == Decimal(+123456, 3));
436         assert(Decimal.fromString("-123.456") == Decimal(-123456, 3));
437         assert(Decimal.fromString("+.123456") == Decimal(123456, 6));
438         assert(Decimal.fromString("-.123456") == Decimal(-123456, 6));
439     }
440 
441     /**
442     Calcurate floor value.
443 
444     Returns:
445         floor value.
446     */
447     Decimal floor() const @nogc nothrow pure scope
448     {
449         immutable scale = (cast(long) 10) ^^ exponent;
450         return Decimal(mantissa / scale, 0);
451     }
452 
453     ///
454     @nogc nothrow pure @safe unittest
455     {
456         assert(Decimal(10, 1).floor == Decimal(1, 0));
457         assert(Decimal(11, 1).floor == Decimal(1, 0));
458         assert(Decimal(10001, 4).floor == Decimal(1, 0));
459         assert(Decimal(20001, 4).floor == Decimal(2, 0));
460     }
461 
462     /**
463     Calcurate ceil value.
464 
465     Returns:
466         ceil value.
467     */
468     Decimal ceil() const @nogc nothrow pure scope
469     {
470         immutable scale = (cast(long) 10) ^^ exponent;
471         immutable rest = mantissa % scale;
472         return Decimal((mantissa / scale) + (rest > 0 ? 1 : 0), 0);
473     }
474 
475     ///
476     @nogc nothrow pure @safe unittest
477     {
478         assert(Decimal(10, 1).ceil == Decimal(1, 0));
479         assert(Decimal(11, 1).ceil == Decimal(2, 0));
480         assert(Decimal(10001, 4).ceil == Decimal(2, 0));
481     }
482 
483     /**
484     Normalize mantissa and exponent.
485 
486     Returns:
487         normalized value.
488     */
489     @property Decimal normalized() const @nogc nothrow pure scope
490     {
491         Decimal result = this;
492         while (result.mantissa % 10 == 0 && result.exponent > 0)
493         {
494             result.mantissa /= 10;
495             --result.exponent;
496         }
497 
498         return result;
499     }
500 
501     ///
502     @nogc nothrow pure @safe unittest
503     {
504         assert(Decimal.init.normalized.mantissa == 0);
505         assert(Decimal.init.normalized.exponent == 0);
506 
507         assert(Decimal(100, 0).normalized.mantissa == 100);
508         assert(Decimal(100, 0).normalized.exponent == 0);
509 
510         assert(Decimal(100, 2).normalized.mantissa == 1);
511         assert(Decimal(100, 2).normalized.exponent == 0);
512 
513         assert(Decimal(120, 2).normalized.mantissa == 12);
514         assert(Decimal(120, 2).normalized.exponent == 1);
515 
516         assert(Decimal(123, 2).normalized.mantissa == 123);
517         assert(Decimal(123, 2).normalized.exponent == 2);
518 
519         assert(Decimal(12340000, 2).normalized.mantissa == 123400);
520         assert(Decimal(12340000, 2).normalized.exponent == 0);
521     }
522 }
523 
524 /**
525 Match two exponent.
526 
527 Params:
528     a = price number 1.
529     b = price number 2.
530 Returns:
531     matched exponent.
532 */
533 auto matchExponent()(
534         auto scope ref const(Decimal) a,
535         auto scope ref const(Decimal) b) @nogc nothrow pure
536 out(r; r[0].exponent == r[1].exponent)
537 {
538     immutable maxExponent = max(a.exponent, b.exponent);
539     return tuple(
540         a.addExponent(cast(ubyte)(maxExponent - a.exponent)),
541         b.addExponent(cast(ubyte)(maxExponent - b.exponent)));
542 }
543 
544 ///
545 @nogc nothrow pure unittest
546 {
547     immutable a = Decimal(123456, 3);
548     assert(matchExponent(a, a) == tuple(a, a));
549 
550     immutable b = Decimal(7890120, 4);
551     assert(matchExponent(a, b) == tuple(Decimal(1234560, 4), b));
552     assert(matchExponent(b, a) == tuple(b, Decimal(1234560, 4)));
553 }
554 
555 private:
556 
557 int cmp(T)(auto ref const(T) a, auto ref const(T) b)
558 {
559     if (a < b)
560     {
561         return -1;
562     }
563 
564     if (a > b)
565     {
566         return 1;
567     }
568 
569     return 0;
570 }
571 
572 ///
573 @nogc nothrow pure unittest
574 {
575     assert(cmp(0, 0) == 0);
576     assert(cmp(1, 0) == 1);
577     assert(cmp(0, 1) == -1);
578 }
579