1 /++
2 Structs used to define units: raional 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 < 0)
35         {
36             num = -num;
37             den = -den;
38         }
39         immutable g = gcd(num, den);
40         num /= g;
41         den /= g;
42     }
43 
44     private bool isNormalized() @safe pure nothrow const
45     {
46         return den >= 0 && gcd(num, den) == 1;
47     }
48 
49 public:
50     /++
51     Create a rational number.
52 
53     Params:
54         num = The numerator
55         den = The denominator
56     +/
57     this(int num, int den = 1) @safe pure nothrow
58     {
59         this.num = num;
60         this.den = den;
61         normalize();
62     }
63 
64     void opOpAssign(string op)(Rational other) @safe pure nothrow 
65             if (op == "+" || op == "-" || op == "*" || op == "/")
66     {
67         mixin("this = this" ~ op ~ "other;");
68         assert(isNormalized);
69     }
70 
71     void opOpAssign(string op)(int value) @safe pure nothrow 
72             if (op == "+" || op == "-" || op == "*" || op == "/")
73     {
74         mixin("this = this" ~ op ~ "value;");
75         assert(isNormalized);
76     }
77 
78     Rational opUnary(string op)() @safe pure nothrow const 
79             if (op == "+" || op == "-")
80     out (result)
81     {
82         assert(result.isNormalized);
83     }
84     body
85     {
86         return Rational(mixin(op ~ "num"), den);
87     }
88 
89     Rational opBinary(string op)(Rational other) @safe pure nothrow const 
90             if (op == "+" || op == "-")
91     {
92         auto ret = Rational(mixin("num * other.den" ~ op ~ "other.num * den"), den * other.den);
93         ret.normalize();
94         return ret;
95     }
96 
97     Rational opBinary(string op)(Rational other) @safe pure nothrow const 
98             if (op == "*")
99     {
100         auto ret = Rational(num * other.num, den * other.den);
101         ret.normalize();
102         return ret;
103     }
104 
105     Rational opBinary(string op)(Rational other) @safe pure nothrow const 
106             if (op == "/")
107     {
108         auto ret = Rational(num * other.den, den * other.num);
109         ret.normalize();
110         return ret;
111     }
112 
113     Rational opBinary(string op)(int value) @safe pure nothrow const 
114             if (op == "+" || op == "-" || op == "*" || op == "/")
115     out
116     {
117         assert(isNormalized);
118     }
119     body
120     {
121         return mixin("this" ~ op ~ "Rational(value)");
122     }
123 
124     bool opEquals(Rational other) @safe pure nothrow const
125     {
126         return num == other.num && den == other.den;
127     }
128 
129     bool opEquals(int value) @safe pure nothrow const
130     {
131         return num == value && den == 1;
132     }
133 
134     int opCmp(Rational other) @safe pure nothrow const
135     {
136         immutable diff = (num / cast(double) den) - (other.num / cast(double) other.den);
137         if (diff == 0)
138             return 0;
139         if (diff > 0)
140             return 1;
141         return -1;
142     }
143 
144     T opCast(T)() @safe pure nothrow const 
145             if (isNumeric!T)
146     {
147         return cast(T) num / cast(T) den;
148     }
149 
150     ///
151     void toString(scope void delegate(const(char)[]) sink) const
152     {
153         sink.formattedWrite!"%d"(num);
154         if (den != 1)
155         {
156             sink("/");
157             sink.formattedWrite!"%d"(den);
158         }
159     }
160 }
161 
162 private int gcd(int x, int y) @safe pure nothrow
163 {
164     if (x == 0 || y == 0)
165         return 1;
166 
167     int tmp;
168     int a = abs(x);
169     int b = abs(y);
170     while (a > 0)
171     {
172         tmp = a;
173         a = b % a;
174         b = tmp;
175     }
176     return b;
177 }
178 
179 unittest
180 {
181     const r = Rational(6, -8);
182     assert(r.text == "-3/4");
183     assert((+r).text == "-3/4");
184     assert((-r).text == "3/4");
185 
186     const r1 = Rational(4, 3) + Rational(2, 5);
187     assert(r1.text == "26/15");
188     const r2 = Rational(4, 3) - Rational(2, 5);
189     assert(r2.text == "14/15");
190     const r3 = Rational(8, 7) * Rational(3, -2);
191     assert(r3.text == "-12/7");
192     const r4 = Rational(8, 7) / Rational(3, -2);
193     assert(r4.text == "-16/21");
194 
195     auto r5 = Rational(4, 3);
196     r5 += Rational(2, 5);
197     assert(r5.text == "26/15");
198 
199     auto r6 = Rational(8, 7);
200     r6 /= Rational(2, -3);
201     assert(r6.text == "-12/7");
202 
203     assert(Rational(8, 7) == Rational(-16, -14));
204     assert(Rational(2, 5) < Rational(3, 7));
205 }
206 
207 /// Struct describind properties of a dimension in a dimension vector.
208 struct Dim
209 {
210     string symbol; /// The symbol of the dimension
211     Rational power; /// The power of the dimension
212 
213     this(string symbol, Rational power) @safe pure nothrow
214     {
215         this.symbol = symbol;
216         this.power = power;
217     }
218 
219     this(string symbol, int power) @safe pure nothrow
220     {
221         this(symbol, Rational(power));
222     }
223 
224     int opCmp(Dim other) @safe pure nothrow const
225     {
226         if (symbol < other.symbol)
227             return -1;
228         else if (symbol > other.symbol)
229             return 1;
230         else
231             return 0;
232     }
233 
234     ///
235     void toString(scope void delegate(const(char)[]) sink) const
236     {
237         if (power == 0)
238             return;
239         if (power == 1)
240             sink(symbol);
241         else
242         {
243             sink.formattedWrite!"%s"(symbol);
244             sink("^");
245             sink.formattedWrite!"%s"(power);
246         }
247     }
248 }
249 
250 private immutable(Dim)[] inverted(immutable(Dim)[] source) @safe pure nothrow
251 {
252     Dim[] target = source.dup;
253     foreach (ref dim; target)
254         dim.power = -dim.power;
255     return target.immut;
256 }
257 
258 @safe pure nothrow unittest
259 {
260     auto list = [Dim("A", 2), Dim("B", -2)].idup;
261     auto inv = [Dim("A", -2), Dim("B", 2)].idup;
262     assert(list.inverted == inv);
263 }
264 
265 private void insertAndSort(ref Dim[] list, string symbol, Rational power) @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         pos = list.countUntil!(d => d.symbol > symbol)();
288         if (pos < 0)
289             pos = list.length;
290         list.insertInPlace(pos, Dim(symbol, power));
291     }
292     //assert(list.isSorted);
293 }
294 
295 @safe pure nothrow unittest
296 {
297     Dim[] list;
298     list.insertAndSort("A", Rational(1));
299     assert(list == [Dim("A", 1)]);
300     list.insertAndSort("A", Rational(1));
301     assert(list == [Dim("A", 2)]);
302     list.insertAndSort("A", Rational(-2));
303     assert(list.length == 0);
304 }
305 
306 private immutable(Dim)[] immut(Dim[] source) @trusted pure nothrow
307 {
308     if (__ctfe)
309         return source.idup;
310     else
311         return source.assumeUnique;
312 }
313 
314 private immutable(Dim)[] insertSorted(immutable(Dim)[] source, string symbol, Rational power) @safe pure nothrow
315 {
316     if (power == 0)
317         return source;
318 
319     if (!source.length)
320         return [Dim(symbol, power)].immut;
321 
322     Dim[] list = source.dup;
323     insertAndSort(list, symbol, power);
324     return list.immut;
325 }
326 
327 private immutable(Dim)[] insertSorted(immutable(Dim)[] source, immutable(Dim)[] other) @safe pure nothrow
328 {
329     Dim[] list = source.dup;
330     foreach (dim; other)
331         insertAndSort(list, dim.symbol, dim.power);
332     return list.immut;
333 }
334 
335 /// A vector of dimensions
336 struct Dimensions
337 {
338 private:
339     immutable(Dim)[] _dims;
340 
341 package(quantities):
342     static Dimensions mono(string symbol) @safe pure nothrow
343     {
344         if (!symbol.length)
345             return Dimensions(null);
346         return Dimensions([Dim(symbol, 1)].immut);
347     }
348 
349 public:
350     this(this) @safe pure nothrow
351     {
352         _dims = _dims.idup;
353     }
354 
355     ref Dimensions opAssign()(auto ref const Dimensions other) @safe pure nothrow
356     {
357         _dims = other._dims.idup;
358         return this;
359     }
360 
361     /// The dimensions stored in this vector
362     immutable(Dim)[] dims() @safe pure nothrow const
363     {
364         return _dims;
365     }
366 
367     /// Test whether the dimension vector is empty.
368     bool empty() @safe pure nothrow const
369     {
370         return _dims.empty;
371     }
372 
373     Dimensions opUnary(string op)() @safe pure nothrow const 
374             if (op == "~")
375     {
376         return Dimensions(_dims.inverted);
377     }
378 
379     Dimensions opBinary(string op)(const Dimensions other) @safe pure nothrow const 
380             if (op == "*")
381     {
382         return Dimensions(_dims.insertSorted(other._dims));
383     }
384 
385     @safe pure nothrow unittest
386     {
387         auto dim1 = Dimensions([Dim("a", 1), Dim("b", -2)]);
388         auto dim2 = Dimensions([Dim("a", -1), Dim("c", 2)]);
389         assert(dim1 * dim2 == Dimensions([Dim("b", -2), Dim("c", 2)]));
390     }
391 
392     Dimensions opBinary(string op)(const Dimensions other) @safe pure nothrow const 
393             if (op == "/")
394     {
395         return Dimensions(_dims.insertSorted(other._dims.inverted));
396     }
397 
398     @safe pure nothrow unittest
399     {
400         auto dim1 = Dimensions([Dim("a", 1), Dim("b", -2)]);
401         auto dim2 = Dimensions([Dim("a", 1), Dim("c", 2)]);
402         assert(dim1 / dim2 == Dimensions([Dim("b", -2), Dim("c", -2)]));
403     }
404 
405     Dimensions pow(Rational n) @safe pure nothrow const
406     {
407         if (n == 0)
408             return Dimensions.init;
409 
410         auto list = _dims.dup;
411         foreach (ref dim; list)
412             dim.power = dim.power * n;
413         return Dimensions(list.immut);
414     }
415 
416     Dimensions pow(int n) @safe pure nothrow const
417     {
418         return pow(Rational(n));
419     }
420 
421     @safe pure nothrow unittest
422     {
423         auto dim = Dimensions([Dim("a", 5), Dim("b", -2)]);
424         assert(dim.pow(Rational(2)) == Dimensions([Dim("a", 10), Dim("b", -4)]));
425         assert(dim.pow(Rational(0)) == Dimensions.init);
426     }
427 
428     Dimensions powinverse(Rational n) @safe pure nothrow const
429     {
430         import std.exception : enforce;
431         import std.string : format;
432 
433         auto list = _dims.dup;
434         foreach (ref dim; list)
435             dim.power = dim.power / n;
436         return Dimensions(list.immut);
437     }
438 
439     Dimensions powinverse(int n) @safe pure nothrow const
440     {
441         return powinverse(Rational(n));
442     }
443 
444     @safe pure nothrow unittest
445     {
446         auto dim = Dimensions([Dim("a", 6), Dim("b", -2)]);
447         assert(dim.powinverse(Rational(2)) == Dimensions([Dim("a", 3), Dim("b", -1)]));
448     }
449 
450     ///
451     void toString(scope void delegate(const(char)[]) sink) const
452     {
453         sink.formattedWrite!"[%(%s %)]"(_dims);
454     }
455 
456     unittest
457     {
458         auto dim = Dimensions([Dim("a", 1), Dim("b", -2)]);
459         assert(dim.text == "[a b^-2]");
460     }
461 }
462 
463 @safe pure nothrow unittest  // Compile-time
464 {
465     enum a = Dimensions([Dim("a", 1)]);
466     enum b = Dimensions([Dim("b", 2)]);
467     enum c = a / b;
468     static assert(c.text == "[a b^-2]");
469 }