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 }