1 /++
2 Structs used to define units: rational numbers and dimensions.
3 
4 Copyright: Copyright 2013-2018, Nicolas Sicard
5 Authors: Nicolas Sicard
6 License: $(LINK www.boost.org/LICENSE_1_0.txt, Boost License 1.0)
7 Source: $(LINK https://github.com/biozic/quantities)
8 +/
9 module quantities.internal.dimensions;
10 
11 import std.algorithm;
12 import std.array;
13 import std.conv;
14 import std.exception;
15 import std.format;
16 import std.math;
17 import std.string;
18 import std.traits;
19 
20 /// Reduced implementation of a rational number
21 struct Rational
22 {
23 private:
24     int num = 0;
25     int den = 1;
26 
27     invariant()
28     {
29         assert(den != 0);
30     }
31 
32     void normalize() @safe pure nothrow
33     {
34         if (den == 1)
35             return;
36         if (den < 0)
37         {
38             num = -num;
39             den = -den;
40         }
41         immutable g = gcd(num, den);
42         num /= g;
43         den /= g;
44     }
45 
46     bool isNormalized() @safe pure nothrow const
47     {
48         return den >= 0 && gcd(num, den) == 1;
49     }
50 
51 public:
52     /++
53     Create a rational number.
54 
55     Params:
56         num = The numerator
57         den = The denominator
58     +/
59     this(int num, int den = 1) @safe pure nothrow
60     {
61         assert(den != 0, "Denominator is zero");
62         this.num = num;
63         this.den = den;
64         normalize();
65     }
66 
67     bool isInt() @property @safe pure nothrow const
68     {
69         return den == 1;
70     }
71 
72     Rational inverted() @property @safe pure nothrow const
73     {
74         Rational result;
75         result.num = den;
76         result.den = num;
77         assert(isNormalized);
78         return result;
79     }
80 
81     void opOpAssign(string op)(Rational other) @safe pure nothrow 
82             if (op == "+" || op == "-" || op == "*" || op == "/")
83     {
84         mixin("this = this" ~ op ~ "other;");
85         assert(isNormalized);
86     }
87 
88     void opOpAssign(string op)(int value) @safe pure nothrow 
89             if (op == "+" || op == "-" || op == "*" || op == "/")
90     {
91         mixin("this = this" ~ op ~ "value;");
92         assert(isNormalized);
93     }
94 
95     Rational opUnary(string op)() @safe pure nothrow const 
96             if (op == "+" || op == "-")
97     out (result)
98     {
99         assert(result.isNormalized);
100     }
101     body
102     {
103         return Rational(mixin(op ~ "num"), den);
104     }
105 
106     Rational opBinary(string op)(Rational other) @safe pure nothrow const 
107             if (op == "+" || op == "-")
108     {
109         auto ret = Rational(mixin("num * other.den" ~ op ~ "other.num * den"), den * other.den);
110         ret.normalize();
111         return ret;
112     }
113 
114     Rational opBinary(string op)(Rational other) @safe pure nothrow const 
115             if (op == "*")
116     {
117         auto ret = Rational(num * other.num, den * other.den);
118         ret.normalize();
119         return ret;
120     }
121 
122     Rational opBinary(string op)(Rational other) @safe pure nothrow const 
123             if (op == "/")
124     {
125         auto ret = Rational(num * other.den, den * other.num);
126         ret.normalize();
127         return ret;
128     }
129 
130     Rational opBinary(string op)(int value) @safe pure nothrow const 
131             if (op == "+" || op == "-" || op == "*" || op == "/")
132     out
133     {
134         assert(isNormalized);
135     }
136     body
137     {
138         return mixin("this" ~ op ~ "Rational(value)");
139     }
140 
141     bool opEquals(Rational other) @safe pure nothrow const
142     {
143         return num == other.num && den == other.den;
144     }
145 
146     bool opEquals(int value) @safe pure nothrow const
147     {
148         return num == value && den == 1;
149     }
150 
151     int opCmp(Rational other) @safe pure nothrow const
152     {
153         immutable diff = (num / cast(double) den) - (other.num / cast(double) other.den);
154         if (diff == 0)
155             return 0;
156         if (diff > 0)
157             return 1;
158         return -1;
159     }
160 
161     int opCmp(int value) @safe pure nothrow const
162     {
163         return opCmp(Rational(value));
164     }
165 
166     T opCast(T)() @safe pure nothrow const 
167             if (isNumeric!T)
168     {
169         return num / cast(T) den;
170     }
171 
172     void toString(scope void delegate(const(char)[]) sink) const
173     {
174         sink.formattedWrite!"%d"(num);
175         if (den != 1)
176         {
177             sink("/");
178             sink.formattedWrite!"%d"(den);
179         }
180     }
181 }
182 
183 private int gcd(int x, int y) @safe pure nothrow
184 {
185     if (x == 0 || y == 0)
186         return 1;
187 
188     int tmp;
189     int a = abs(x);
190     int b = abs(y);
191     while (a > 0)
192     {
193         tmp = a;
194         a = b % a;
195         b = tmp;
196     }
197     return b;
198 }
199 
200 /// Struct describing properties of a dimension in a dimension vector.
201 struct Dim
202 {
203     string symbol; /// The symbol of the dimension
204     Rational power; /// The power of the dimension
205     size_t rank = size_t.max; /// The rank of the dimension in the vector
206 
207     this(string symbol, Rational power, size_t rank = size_t.max) @safe pure nothrow
208     {
209         this.symbol = symbol;
210         this.power = power;
211         this.rank = rank;
212     }
213 
214     this(string symbol, int power, size_t rank = size_t.max) @safe pure nothrow
215     {
216         this(symbol, Rational(power), rank);
217     }
218 
219     int opCmp(Dim other) @safe pure nothrow const
220     {
221         if (rank == other.rank)
222         {
223             if (symbol < other.symbol)
224                 return -1;
225             else if (symbol > other.symbol)
226                 return 1;
227             else
228                 return 0;
229         }
230         else
231         {
232             if (rank < other.rank)
233                 return -1;
234             else if (rank > other.rank)
235                 return 1;
236             else
237                 assert(false);
238         }
239     }
240 
241     ///
242     void toString(scope void delegate(const(char)[]) sink) const
243     {
244         if (power == 0)
245             return;
246         if (power == 1)
247             sink(symbol);
248         else
249         {
250             sink.formattedWrite!"%s"(symbol);
251             sink("^");
252             sink.formattedWrite!"%s"(power);
253         }
254     }
255 }
256 
257 private immutable(Dim)[] inverted(immutable(Dim)[] source) @safe pure nothrow
258 {
259     Dim[] target = source.dup;
260     foreach (ref dim; target)
261         dim.power = -dim.power;
262     return target.immut;
263 }
264 
265 private void insertAndSort(ref Dim[] list, string symbol, Rational power, size_t rank) @safe pure nothrow
266 {
267     auto pos = list.countUntil!(d => d.symbol == symbol)();
268     if (pos >= 0)
269     {
270         // Merge the dimensions
271         list[pos].power += power;
272         if (list[pos].power == 0)
273         {
274             try
275                 list = list.remove(pos);
276             catch (Exception) // remove only throws when it has multiple arguments
277                 assert(false);
278 
279             // Necessary to compare dimensionless values
280             if (!list.length)
281                 list = null;
282         }
283     }
284     else
285     {
286         // Insert the new dimension
287         auto dim = Dim(symbol, power, rank);
288         pos = list.countUntil!(d => d > dim);
289         if (pos < 0)
290             pos = list.length;
291         list.insertInPlace(pos, dim);
292     }
293     assert(list.isSorted);
294 }
295 
296 private immutable(Dim)[] immut(Dim[] source) @trusted pure nothrow
297 {
298     if (__ctfe)
299         return source.idup;
300     else
301         return source.assumeUnique;
302 }
303 
304 private immutable(Dim)[] insertSorted(immutable(Dim)[] source, string symbol,
305         Rational power, size_t rank) @safe pure nothrow
306 {
307     if (power == 0)
308         return source;
309 
310     if (!source.length)
311         return [Dim(symbol, power, rank)].immut;
312 
313     Dim[] list = source.dup;
314     insertAndSort(list, symbol, power, rank);
315     return list.immut;
316 }
317 
318 private immutable(Dim)[] insertSorted(immutable(Dim)[] source, immutable(Dim)[] other) @safe pure nothrow
319 {
320     Dim[] list = source.dup;
321     foreach (dim; other)
322         insertAndSort(list, dim.symbol, dim.power, dim.rank);
323     return list.immut;
324 }
325 
326 /// A vector of dimensions
327 struct Dimensions
328 {
329 private:
330     immutable(Dim)[] _dims;
331 
332 package(quantities):
333     static Dimensions mono(string symbol, size_t rank) @safe pure nothrow
334     {
335         if (!symbol.length)
336             return Dimensions(null);
337         return Dimensions([Dim(symbol, 1, rank)].immut);
338     }
339 
340 public:
341     this(this) @safe pure nothrow
342     {
343         _dims = _dims.idup;
344     }
345 
346     ref Dimensions opAssign()(auto ref const Dimensions other) @safe pure nothrow
347     {
348         _dims = other._dims.idup;
349         return this;
350     }
351 
352     /// The dimensions stored in this vector
353     immutable(Dim)[] dims() @safe pure nothrow const
354     {
355         return _dims;
356     }
357 
358     alias dims this;
359 
360     bool empty() @safe pure nothrow const
361     {
362         return _dims.empty;
363     }
364 
365     Dimensions inverted() @safe pure nothrow const
366     {
367         return Dimensions(_dims.inverted);
368     }
369 
370     Dimensions opUnary(string op)() @safe pure nothrow const 
371             if (op == "~")
372     {
373         return Dimensions(_dims.inverted);
374     }
375 
376     Dimensions opBinary(string op)(const Dimensions other) @safe pure nothrow const 
377             if (op == "*")
378     {
379         return Dimensions(_dims.insertSorted(other._dims));
380     }
381 
382     Dimensions opBinary(string op)(const Dimensions other) @safe pure nothrow const 
383             if (op == "/")
384     {
385         return Dimensions(_dims.insertSorted(other._dims.inverted));
386     }
387 
388     Dimensions pow(Rational n) @safe pure nothrow const
389     {
390         if (n == 0)
391             return Dimensions.init;
392 
393         auto list = _dims.dup;
394         foreach (ref dim; list)
395             dim.power = dim.power * n;
396         return Dimensions(list.immut);
397     }
398 
399     Dimensions pow(int n) @safe pure nothrow const
400     {
401         return pow(Rational(n));
402     }
403 
404     Dimensions powinverse(Rational n) @safe pure nothrow const
405     {
406         import std.exception : enforce;
407         import std.string : format;
408 
409         auto list = _dims.dup;
410         foreach (ref dim; list)
411             dim.power = dim.power / n;
412         return Dimensions(list.immut);
413     }
414 
415     Dimensions powinverse(int n) @safe pure nothrow const
416     {
417         return powinverse(Rational(n));
418     }
419 
420     void toString(scope void delegate(const(char)[]) sink) const
421     {
422         sink.formattedWrite!"[%(%s %)]"(_dims);
423     }
424 }
425 
426 // Tests
427 
428 @("Rational")
429 unittest
430 {
431     const r = Rational(6, -8);
432     assert(r.text == "-3/4");
433     assert((+r).text == "-3/4");
434     assert((-r).text == "3/4");
435 
436     const r1 = Rational(4, 3) + Rational(2, 5);
437     assert(r1.text == "26/15");
438     const r2 = Rational(4, 3) - Rational(2, 5);
439     assert(r2.text == "14/15");
440     const r3 = Rational(8, 7) * Rational(3, -2);
441     assert(r3.text == "-12/7");
442     const r4 = Rational(8, 7) / Rational(3, -2);
443     assert(r4.text == "-16/21");
444 
445     auto r5 = Rational(4, 3);
446     r5 += Rational(2, 5);
447     assert(r5.text == "26/15");
448 
449     auto r6 = Rational(8, 7);
450     r6 /= Rational(2, -3);
451     assert(r6.text == "-12/7");
452 
453     assert(Rational(8, 7) == Rational(-16, -14));
454     assert(Rational(2, 5) < Rational(3, 7));
455 }
456 
457 @("Dim[].inverted")
458 @safe pure nothrow unittest
459 {
460     auto list = [Dim("A", 2), Dim("B", -2)].idup;
461     auto inv = [Dim("A", -2), Dim("B", 2)].idup;
462     assert(list.inverted == inv);
463 }
464 
465 @("Dim[].insertAndSort")
466 @safe pure nothrow unittest
467 {
468     Dim[] list;
469     list.insertAndSort("A", Rational(1), 1);
470     assert(list == [Dim("A", 1, 1)]);
471     list.insertAndSort("A", Rational(1), 1);
472     assert(list == [Dim("A", 2, 1)]);
473     list.insertAndSort("A", Rational(-2), 1);
474     assert(list.length == 0);
475     list.insertAndSort("B", Rational(1), 3);
476     assert(list == [Dim("B", 1, 3)]);
477     list.insertAndSort("C", Rational(1), 1);
478     assert(Dim("C", 1, 1) < Dim("B", 1, 3));
479     assert(list == [Dim("C", 1, 1), Dim("B", 1, 3)]);
480 }
481 
482 @("Dimensions *")
483 @safe pure nothrow unittest
484 {
485     auto dim1 = Dimensions([Dim("a", 1), Dim("b", -2)]);
486     auto dim2 = Dimensions([Dim("a", -1), Dim("c", 2)]);
487     assert(dim1 * dim2 == Dimensions([Dim("b", -2), Dim("c", 2)]));
488 }
489 
490 @("Dimensions /")
491 @safe pure nothrow unittest
492 {
493     auto dim1 = Dimensions([Dim("a", 1), Dim("b", -2)]);
494     auto dim2 = Dimensions([Dim("a", 1), Dim("c", 2)]);
495     assert(dim1 / dim2 == Dimensions([Dim("b", -2), Dim("c", -2)]));
496 }
497 
498 @("Dimensions pow")
499 @safe pure nothrow unittest
500 {
501     auto dim = Dimensions([Dim("a", 5), Dim("b", -2)]);
502     assert(dim.pow(Rational(2)) == Dimensions([Dim("a", 10), Dim("b", -4)]));
503     assert(dim.pow(Rational(0)) == Dimensions.init);
504 }
505 
506 @("Dimensions.powinverse")
507 @safe pure nothrow unittest
508 {
509     auto dim = Dimensions([Dim("a", 6), Dim("b", -2)]);
510     assert(dim.powinverse(Rational(2)) == Dimensions([Dim("a", 3), Dim("b", -1)]));
511 }
512 
513 @("Dimensions.toString")
514 unittest
515 {
516     auto dim = Dimensions([Dim("a", 1), Dim("b", -2)]);
517     assert(dim.text == "[a b^-2]");
518     assert(Dimensions.init.text == "[]");
519 }