1 /++ 2 This module defines the base types for unit and quantity handling. 3 4 Copyright: Copyright 2013-2015, 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.base; 10 11 import std.exception; 12 import std.format; 13 import std.string; 14 import std.traits; 15 16 version (unittest) 17 { 18 import std.math : approxEqual; 19 // import std.conv : text; 20 } 21 22 /// The type of a set of dimensions. 23 alias Dimensions = int[string]; 24 25 /++ 26 A quantity that can be represented as the product of a number and a set of 27 dimensions. The number is stored internally as a member of type N, which is 28 enforced to be a built-in numeric type (isNumeric!N is true). The dimensions 29 are stored as an associative array where keys are symbols and values are 30 integral powers. 31 For instance length and speed quantities can be stored as: 32 --- 33 alias Length = Quantity!(double, ["L": 1]); 34 alias Speed = Quantity!(double, ["L": 1, "T": -1]); 35 --- 36 where "L" is the symbol for the length dimension, "T" is the symbol of the time 37 dimensions, and 1 and -1 are the powers of those dimensions in the 38 representation of the quantity. 39 40 Units are just instances of a Quantity struct where the value is 1 and the 41 dimensions only contain one symbol, with the power 1. For instance, the meter 42 unit can be defined using the template `unit` as: 43 --- 44 enum meter = unit!(double, "L"); 45 static assert(is(typeof(meter) == Quantity!(double, ["L": 1]))); 46 --- 47 The main quantities compliant with the international system of units (SI) are 48 actually predefined in the module quantities.si. 49 50 Any quantity can be expressed as the product of a number ($(I n)) and a unit of 51 the right dimensions ($(I U)). For instance: 52 --- 53 auto size = 9.5 * meter; 54 auto time = 120 * milli(second); 55 --- 56 The unit $(I U) is not actually stored along with the number in a Quantity 57 struct, only the dimensions are. This is because the same quantity can be 58 expressed in an infinity of different units. The value of $(I n) is stored as 59 if the quantity was expressed in the base units of the same dimemsions. In the 60 example above, $(I n) = 9.5 for the variable size and $(I n) = 0.120 for the 61 variable time. 62 63 The method `value` can be used to extract the number $(I n) as if it was 64 expressed in any possible unit. The user must pass this unit to the method. 65 This way, the user makes it clear in which unit the value was expressed. 66 --- 67 auto size = 9.5 * meter; 68 auto valueMeter = size.value(meter); // valueMeter == 9.5 69 auto valueCentimeter = size.value(centi(meter)); // valueCentimeter == 950 70 --- 71 Arithmetic operators (+ - * /), as well as assignment and comparison operators, 72 are defined when the operations are dimensionally consistent, otherwise an 73 error occurs at compile-time: 74 --- 75 auto time = 2 * hour + 17 * minute; 76 auto frequency = time / second; 77 time = time + 2 * meter; // Compilation error 78 --- 79 Any kind of quantities and units can be defined with this module, not just 80 those from the SI. When a quantity that is not predefined has to be used, 81 instead of instantiating the Quantity template first, it is preferable to start 82 defining a new base unit (with only one dimension) using the unit template, and 83 then the quantity type with the typeof operator: 84 --- 85 enum euro = unit!"C"; // C for currency 86 alias Currency = typeof(euro); 87 assert(is(Currency == Quantity!(double, ["C": 1]))); 88 --- 89 This means that all currencies will be defined with respect to euro. 90 91 Params: 92 N = The numeric type of the quantity used to store the value internally (e.g. `double`). 93 dims = The dimensions of the quantity. 94 +/ 95 struct Quantity(N, Dimensions dims) 96 { 97 static assert(isNumeric!N, "Incompatible type: " ~ N.stringof); 98 99 private: 100 N _value; 101 102 static void checkDim(Dimensions dim)() 103 { 104 static assert(equals(dim, dimensions), 105 "Dimension error: %s is not compatible with %s" 106 .format(.toString(dim), .toString(dimensions))); 107 } 108 109 static void checkValueType(T)() 110 { 111 static assert(is(T : valueType), "%s is not implicitly convertible to %s" 112 .format(T.stringof, valueType.stringof)); 113 } 114 115 package: 116 // Should be a constructor 117 // Workaround for @@BUG 5770@@ 118 // (https://d.puremagic.com/issues/show_bug.cgi?id=5770) 119 // "Template constructor bypass access check" 120 package static Quantity make(T)(T value) 121 if (isNumeric!T) 122 { 123 checkValueType!T; 124 Quantity ret; 125 ret._value = value; 126 return ret; 127 } 128 129 // Gets the internal number of this quantity. 130 package N rawValue() const 131 { 132 return _value; 133 } 134 135 public: 136 /// The type of the underlying numeric value. 137 alias valueType = N; 138 139 // Implicitly convert a dimensionless value to the value type 140 static if (!dimensions.length) 141 { 142 // Gets the internal number of this quantity. 143 N get() const 144 { 145 return _value; 146 } 147 alias get this; 148 } 149 150 /// The dimensions of the quantity. 151 enum dimensions = dims; 152 153 /// Gets the base unit of this quantity. 154 static Quantity baseUnit() 155 { 156 N one = 1; 157 return Quantity.make(one); 158 } 159 160 // Creates a new quantity from another one with the same dimensions 161 this(Q)(Q other) 162 if (isQuantity!Q) 163 { 164 checkDim!(other.dimensions); 165 checkValueType!(Q.valueType); 166 _value = other._value; 167 } 168 169 // Creates a new dimensionless quantity from a number 170 this(T)(T value) 171 if (isNumeric!T && dimensions.length == 0) 172 { 173 checkValueType!T; 174 _value = value; 175 } 176 177 /++ 178 Gets the _value of this quantity expressed in the given target unit. 179 +/ 180 N value(Q)(Q target) const 181 if (isQuantity!Q) 182 { 183 checkDim!(target.dimensions); 184 checkValueType!(Q.valueType); 185 return _value / target._value; 186 } 187 /// 188 pure nothrow @nogc @safe unittest 189 { 190 import quantities.si : minute, hour; 191 192 auto time = 120 * minute; 193 assert(time.value(hour) == 2); 194 assert(time.value(minute) == 120); 195 } 196 197 /++ 198 Tests wheter this quantity has the same dimensions as another one. 199 +/ 200 bool isConsistentWith(Q)(Q other) const 201 if (isQuantity!Q) 202 { 203 enum ret = equals(dimensions, other.dimensions); 204 return ret; 205 } 206 /// 207 pure nothrow @nogc @safe unittest 208 { 209 import quantities.si : minute, second, meter; 210 211 assert(minute.isConsistentWith(second)); 212 assert(!meter.isConsistentWith(second)); 213 } 214 215 /// Overloaded operators. 216 /// Only dimensionally correct operations will compile. 217 218 // Cast a quantity to another quantity type with the same dimensions 219 Q opCast(Q)() const 220 if (isQuantity!Q) 221 { 222 checkDim!(Q.dimensions); 223 checkValueType!(Q.valueType); 224 return Q.make(_value); 225 } 226 227 // Cast a dimensionless quantity to a numeric type 228 T opCast(T)() const 229 if (isNumeric!T) 230 { 231 checkDim!(Dimensions.init); 232 checkValueType!T; 233 return _value; 234 } 235 236 237 // Assign from another quantity 238 void opAssign(Q)(Q other) 239 if (isQuantity!Q) 240 { 241 checkDim!(other.dimensions); 242 checkValueType!(Q.valueType); 243 _value = other._value; 244 } 245 246 // Assign from a numeric value if this quantity is dimensionless 247 /// ditto 248 void opAssign(T)(T other) 249 if (isNumeric!T) 250 { 251 checkDim!(Dimensions.init); 252 checkValueType!T; 253 _value = other; 254 } 255 256 // Unary + and - 257 /// ditto 258 auto opUnary(string op)() const 259 if (op == "+" || op == "-") 260 { 261 return Quantity.make(mixin(op ~ "_value")); 262 } 263 264 // Unary ++ and -- 265 /// ditto 266 auto opUnary(string op)() 267 if (op == "++" || op == "--") 268 { 269 mixin(op ~ "_value;"); 270 return this; 271 } 272 273 // Add (or substract) two quantities if they share the same dimensions 274 /// ditto 275 auto opBinary(string op, Q)(Q other) const 276 if (isQuantity!Q && (op == "+" || op == "-")) 277 { 278 checkDim!(other.dimensions); 279 checkValueType!(Q.valueType); 280 return Quantity.make(mixin("_value" ~ op ~ "other._value")); 281 } 282 283 // Add (or substract) a dimensionless quantity and a number 284 /// ditto 285 auto opBinary(string op, T)(T other) const 286 if (isNumeric!T && (op == "+" || op == "-")) 287 { 288 checkDim!(Dimensions.init); 289 checkValueType!T; 290 return Quantity.make(mixin("_value" ~ op ~ "other")); 291 } 292 293 /// ditto 294 auto opBinaryRight(string op, T)(T other) const 295 if (isNumeric!T && (op == "+" || op == "-")) 296 { 297 return opBinary!op(other); 298 } 299 300 // Multiply or divide two quantities 301 /// ditto 302 auto opBinary(string op, Q)(Q other) const 303 if (isQuantity!Q && (op == "*" || op == "/" || op == "%")) 304 { 305 checkValueType!(Q.valueType); 306 return Quantity!(N, binop!op(dimensions, other.dimensions)) 307 .make(mixin("(_value" ~ op ~ "other._value)")); 308 } 309 310 // Multiply or divide a quantity by a number 311 /// ditto 312 auto opBinary(string op, T)(T other) const 313 if (isNumeric!T && (op == "*" || op == "/" || op == "%")) 314 { 315 checkValueType!T; 316 return Quantity.make(mixin("_value" ~ op ~ "other")); 317 } 318 319 /// ditto 320 auto opBinaryRight(string op, T)(T other) const 321 if (isNumeric!T && op == "*") 322 { 323 checkValueType!T; 324 return this * other; 325 } 326 327 /// ditto 328 auto opBinaryRight(string op, T)(T other) const 329 if (isNumeric!T && (op == "/" || op == "%")) 330 { 331 checkValueType!T; 332 return Quantity!(N, invert(dimensions)).make(mixin("other" ~ op ~ "_value")); 333 } 334 335 auto opBinary(string op, T)(T power) const 336 if (op == "^^") 337 { 338 static assert(false, "Unsupporter operator: ^^"); 339 } 340 341 // Add/sub assign with a quantity that shares the same dimensions 342 /// ditto 343 void opOpAssign(string op, Q)(Q other) 344 if (isQuantity!Q && (op == "+" || op == "-")) 345 { 346 checkDim!(other.dimensions); 347 checkValueType!(Q.valueType); 348 mixin("_value " ~ op ~ "= other._value;"); 349 } 350 351 // Add/sub assign a number to a dimensionless quantity 352 /// ditto 353 void opOpAssign(string op, T)(T other) 354 if (isNumeric!T && (op == "+" || op == "-")) 355 { 356 checkDim!(Dimensions.init); 357 checkValueType!T; 358 mixin("_value " ~ op ~ "= other;"); 359 } 360 361 // Mul/div assign with a dimensionless quantity 362 /// ditto 363 void opOpAssign(string op, Q)(Q other) 364 if (isQuantity!Q && (op == "*" || op == "/" || op == "%")) 365 { 366 Q.checkDim!(Dimensions.init); 367 checkValueType!(Q.valueType); 368 mixin("_value" ~ op ~ "= other._value;"); 369 } 370 371 // Mul/div assign with a number 372 /// ditto 373 void opOpAssign(string op, T)(T other) 374 if (isNumeric!T && (op == "*" || op == "/" || op == "%")) 375 { 376 checkValueType!T; 377 mixin("_value" ~ op ~ "= other;"); 378 } 379 380 // Exact equality between quantities 381 /// ditto 382 bool opEquals(Q)(Q other) const 383 if (isQuantity!Q) 384 { 385 checkDim!(other.dimensions); 386 return _value == other._value; 387 } 388 389 // Exact equality between a dimensionless quantity and a number 390 /// ditto 391 bool opEquals(T)(T other) const 392 if (isNumeric!T) 393 { 394 checkValueType!T; 395 checkDim!(Dimensions.init); 396 return _value == other; 397 } 398 399 // Comparison between two quantities 400 /// ditto 401 int opCmp(Q)(Q other) const 402 if (isQuantity!Q) 403 { 404 checkDim!(other.dimensions); 405 if (_value == other._value) 406 return 0; 407 if (_value < other._value) 408 return -1; 409 return 1; 410 } 411 412 // Comparison between a dimensionless quantity and a number 413 /// ditto 414 int opCmp(T)(T other) const 415 if (isNumeric!T) 416 { 417 checkValueType!T; 418 checkDim!(Dimensions.init); 419 if (_value < other) 420 return -1; 421 if (_value > other) 422 return 1; 423 return 0; 424 } 425 426 // String formatting function 427 void toString(scope void delegate(const(char)[]) sink, FormatSpec!char fmt) const 428 { 429 sink.formatValue(_value, fmt); 430 sink(" "); 431 sink(dimensions.toString); 432 } 433 } 434 435 pure nothrow @nogc @safe unittest // Quantity.baseUnit 436 { 437 import quantities.si : minute, second; 438 439 assert(minute.baseUnit == second); 440 } 441 442 pure nothrow @nogc @safe unittest // Quantity constructor 443 { 444 import quantities.si : minute, second, radian; 445 446 auto time = typeof(second)(1 * minute); 447 assert(time.value(second) == 60); 448 449 450 auto angle = typeof(radian)(3.14); 451 assert(angle.value(radian) == 3.14); 452 } 453 454 pure nothrow @nogc @safe unittest // QVariant.alias this 455 { 456 import quantities.si : radian; 457 458 static double foo(double d) nothrow @nogc { return d; } 459 assert(foo(2 * radian) == 2); 460 } 461 462 pure nothrow @nogc @safe unittest // Quantity.opCast 463 { 464 import quantities.si : second, radian; 465 466 auto fsec = unit!(float, "T"); 467 assert((cast(typeof(second)) fsec).value(second) == 1); 468 auto angle = 12 * radian; 469 assert(cast(double) angle == 12); 470 } 471 472 pure nothrow @nogc @safe unittest // Quantity.opAssign Q = Q 473 { 474 import quantities.si : meter, radian; 475 476 auto length = meter; 477 length = 100 * meter; 478 assert(length.value(meter) == 100); 479 auto angle = radian; 480 angle = 2; 481 assert(angle.value(radian) == 2); 482 } 483 484 pure nothrow @nogc @safe unittest // Quantity.opUnary +Q -Q ++Q --Q 485 { 486 import quantities.si : meter; 487 488 auto length = + meter; 489 assert(length == 1 * meter); 490 auto length2 = - meter; 491 assert(length2 == -1 * meter); 492 493 auto len = ++meter; 494 assert(len.value(meter).approxEqual(2)); 495 len = --meter; 496 assert(len.value(meter).approxEqual(0)); 497 len++; 498 assert(len.value(meter).approxEqual(1)); 499 } 500 501 pure nothrow @nogc @safe unittest // Quantity.opBinary Q*N Q/N 502 { 503 import quantities.si : second; 504 505 auto time = second * 60; 506 assert(time.value(second) == 60); 507 auto time2 = second / 2; 508 assert(time2.value(second) == 1.0/2); 509 } 510 511 pure nothrow @nogc @safe unittest // Quantity.opBinary Q*Q Q/Q 512 { 513 import quantities.si : meter, minute, second; 514 515 auto hertz = 1 / second; 516 517 auto length = meter * 5; 518 auto surface = length * length; 519 assert(surface.value(meter * meter) == 5*5); 520 auto length2 = surface / length; 521 assert(length2.value(meter) == 5); 522 523 auto x = minute / second; 524 assert(x.rawValue == 60); 525 526 auto y = minute * hertz; 527 assert(y.rawValue == 60); 528 } 529 530 pure nothrow @nogc @safe unittest // Quantity.opBinaryRight N*Q 531 { 532 import quantities.si : meter; 533 534 auto length = 100 * meter; 535 assert(length == meter * 100); 536 } 537 538 pure nothrow @nogc @safe unittest // Quantity.opBinaryRight N/Q 539 { 540 import quantities.si : meter; 541 542 auto x = 1 / (2 * meter); 543 assert(x.value(1/meter) == 1.0/2); 544 } 545 546 pure nothrow @nogc @safe unittest // Quantity.opBinary Q%Q Q%N N%Q 547 { 548 import quantities.si : meter; 549 550 auto x = 258.1 * meter; 551 auto y1 = x % (50 * meter); 552 assert((cast(double) y1).approxEqual(8.1)); 553 auto y2 = x % 50; 554 assert(y2.value(meter).approxEqual(8.1)); 555 } 556 557 pure nothrow @nogc @safe unittest // Quantity.opBinary Q+Q Q-Q 558 { 559 import quantities.si : meter; 560 561 auto length = meter + meter; 562 assert(length.value(meter) == 2); 563 auto length2 = length - meter; 564 assert(length2.value(meter) == 1); 565 } 566 567 pure nothrow @nogc @safe unittest // Quantity.opBinary Q+N Q-N 568 { 569 import quantities.si : radian; 570 571 auto angle = radian + 1; 572 assert(angle.value(radian) == 2); 573 angle = angle - 1; 574 assert(angle.value(radian) == 1); 575 angle = 1 + angle; 576 assert(angle.value(radian) == 2); 577 } 578 579 pure nothrow @nogc @safe unittest // Quantity.opOpAssign Q+=Q Q-=Q 580 { 581 import quantities.si : second; 582 583 auto time = 10 * second; 584 time += 50 * second; 585 assert(time.value(second).approxEqual(60)); 586 time -= 40 * second; 587 assert(time.value(second).approxEqual(20)); 588 } 589 590 pure nothrow @nogc @safe unittest // Quantity.opBinary Q+N Q-N 591 { 592 import quantities.si : radian; 593 594 auto angle = 1 * radian; 595 angle += 1; 596 assert(angle.value(radian) == 2); 597 angle -= 1; 598 assert(angle.value(radian) == 1); 599 } 600 601 pure nothrow @nogc @safe unittest // Quantity.opOpAssign Q*=N Q/=N Q%=N 602 { 603 import quantities.si : second; 604 605 auto time = 20 * second; 606 time *= 2; 607 assert(time.value(second).approxEqual(40)); 608 time /= 4; 609 assert(time.value(second).approxEqual(10)); 610 time %= 3; 611 assert(time.value(second).approxEqual(1)); 612 } 613 614 pure nothrow @nogc @safe unittest // Quantity.opOpAssign Q*=N Q/=N Q%=N 615 { 616 import quantities.si : meter, second; 617 618 auto time = 20 * second; 619 time *= (2 * meter) / meter; 620 assert(time.value(second).approxEqual(40)); 621 time /= (4 * meter) / meter; 622 assert(time.value(second).approxEqual(10)); 623 time %= (3 * meter) / meter; 624 assert(time.value(second).approxEqual(1)); 625 } 626 627 pure nothrow @nogc @safe unittest // Quantity.opEquals 628 { 629 import quantities.si : radian, minute, second; 630 631 assert(1 * minute == 60 * second); 632 assert(1 * radian == 1); 633 } 634 635 pure nothrow @nogc @safe unittest // Quantity.opCmp 636 { 637 import quantities.si : minute, second; 638 639 auto hour = 60 * minute; 640 assert(second < minute); 641 assert(minute <= minute); 642 assert(hour > minute); 643 assert(hour >= hour); 644 } 645 646 pure nothrow @nogc @safe unittest // Quantity.opCmp 647 { 648 import quantities.si : radian; 649 650 auto angle = 2 * radian; 651 assert(angle < 4); 652 assert(angle <= 2); 653 assert(angle > 1); 654 assert(angle >= 2); 655 } 656 657 unittest // Quantity.toString 658 { 659 import quantities.si : meter; 660 import std.conv : text; 661 662 auto length = 12 * meter; 663 assert(length.text == "12 [L]", length.text); 664 } 665 666 pure nothrow @nogc @safe unittest // Compilation errors for incompatible dimensions 667 { 668 import quantities.si : meter, second; 669 670 auto m = meter; 671 static assert(!__traits(compiles, m.value(second))); 672 static assert(!__traits(compiles, m = second)); 673 static assert(!__traits(compiles, m + second)); 674 static assert(!__traits(compiles, m - second)); 675 static assert(!__traits(compiles, m + 1)); 676 static assert(!__traits(compiles, m - 1)); 677 static assert(!__traits(compiles, 1 + m)); 678 static assert(!__traits(compiles, 1 - m)); 679 static assert(!__traits(compiles, m += second)); 680 static assert(!__traits(compiles, m -= second)); 681 static assert(!__traits(compiles, m *= second)); 682 static assert(!__traits(compiles, m /= second)); 683 static assert(!__traits(compiles, m *= meter)); 684 static assert(!__traits(compiles, m /= meter)); 685 static assert(!__traits(compiles, m += 1)); 686 static assert(!__traits(compiles, m -= 1)); 687 static assert(!__traits(compiles, m == 1)); 688 static assert(!__traits(compiles, m == second)); 689 static assert(!__traits(compiles, m < second)); 690 static assert(!__traits(compiles, m < 1)); 691 } 692 693 pure nothrow @nogc @safe unittest // immutable Quantity 694 { 695 import quantities.si : meter, minute, second; 696 697 immutable length = 3e8 * meter; 698 immutable time = 1 * second; 699 immutable speedOfLight = length / time; 700 assert(speedOfLight == 3e8 * meter / second); 701 assert(speedOfLight > 1 * meter / minute); 702 } 703 704 /// Tests whether T is a quantity type 705 template isQuantity(T) 706 { 707 static if (is(Unqual!T == Quantity!X, X...)) 708 enum isQuantity = true; 709 else 710 enum isQuantity = false; 711 } 712 713 /// Creates a new monodimensional unit. 714 template unit(N, string symbol) 715 { 716 static assert(isNumeric!N, "Incompatible type: " ~ N.stringof); 717 enum N one = 1; 718 enum unit = Quantity!(N, [symbol: 1]).make(one); 719 } 720 /// 721 pure nothrow @nogc @safe unittest 722 { 723 auto euro = unit!(double, "C"); // C for Currency 724 assert(isQuantity!(typeof(euro))); 725 auto dollar = euro / 1.35; 726 assert((1.35 * dollar).value(euro).approxEqual(1)); 727 } 728 729 /// Check that two quantity types are dimensionally consistent. 730 template AreConsistent(Q1, Q2) 731 if (isQuantity!Q1 && isQuantity!Q2) 732 { 733 enum AreConsistent = Q1.dimensions == Q2.dimensions; 734 } 735 /// 736 pure nothrow @nogc @safe unittest 737 { 738 import quantities.si : meter, second; 739 740 alias Speed = typeof(meter/second); 741 alias Velocity = typeof((1/second * meter)); 742 static assert(AreConsistent!(Speed, Velocity)); 743 } 744 745 /++ 746 Creates a new prefix function that mutlpy a Quantity by _factor factor. 747 +/ 748 template prefix(alias factor) 749 { 750 alias N = typeof(factor); 751 static assert(isNumeric!N, "Incompatible type: " ~ N.stringof); 752 753 auto prefix(Q)(Q base) 754 if (isQuantity!Q) 755 { 756 return base * factor; 757 } 758 } 759 /// 760 pure nothrow @nogc @safe unittest 761 { 762 import quantities.si : meter; 763 764 alias milli = prefix!1e-3; 765 assert(milli(meter).value(meter).approxEqual(1e-3)); 766 } 767 768 package: 769 770 Dimensions dimdup(in Dimensions dim) @trusted pure 771 { 772 return cast(Dimensions) dim.dup; 773 } 774 775 // Necessary because of bugs with dim1 == dim2 at compile time. 776 bool equals(in Dimensions dim1, in Dimensions dim2) pure @safe 777 { 778 if (__ctfe) 779 { 780 if (dim1.length != dim2.length) 781 return false; 782 783 foreach (k, v1; dim1) 784 { 785 auto v2 = k in dim2; 786 if (v2 is null || v1 != *v2) 787 return false; 788 } 789 return true; 790 } 791 else 792 return dim1 == dim2; 793 } 794 pure @safe unittest 795 { 796 assert(equals(Dimensions.init, Dimensions.init)); 797 assert(equals(["a": 1, "b": 0], ["a": 1, "b": 0])); 798 assert(!equals(["a": 1, "b": 1], ["a": 1, "b": 0])); 799 assert(!equals(["a": 1], ["a": 1, "b": 0])); 800 assert(!equals(["a": 1, "b": 0], ["a": 1])); 801 } 802 803 Dimensions removeNull(in Dimensions dim) pure @safe 804 { 805 Dimensions ret; 806 foreach (k, v; dim) 807 if (v != 0) 808 ret[k] = v; 809 return ret; 810 } 811 pure @safe unittest 812 { 813 auto dim = ["a": 1, "b": 0, "c": 0, "d": 1]; 814 assert(dim.removeNull == ["a": 1, "d": 1]); 815 } 816 817 Dimensions invert(in Dimensions dim) pure @safe 818 { 819 Dimensions ret; 820 foreach (k, v; dim) 821 { 822 assert(v != 0); 823 ret[k] = -v; 824 } 825 return ret; 826 } 827 pure @safe unittest 828 { 829 auto dim = ["a": 5, "b": -2]; 830 assert(dim.invert == ["a": -5, "b": 2]); 831 } 832 833 Dimensions binop(string op)(in Dimensions dim1, in Dimensions dim2) pure @safe 834 if (op == "*") 835 { 836 auto ret = dim1.dimdup; 837 foreach (k, v2; dim2) 838 { 839 auto v1 = k in ret; 840 if (v1) 841 ret[k] = *v1 + v2; 842 else 843 ret[k] = v2; 844 } 845 return ret.removeNull; 846 } 847 pure @safe unittest 848 { 849 auto dim1 = ["a": 1, "b": -2]; 850 auto dim2 = ["a": -1, "c": 2]; 851 assert(binop!"*"(dim1, dim2) == ["b": -2, "c": 2]); 852 } 853 854 Dimensions binop(string op)(in Dimensions dim1, in Dimensions dim2) pure @safe 855 if (op == "/" || op == "%") 856 { 857 return binop!"*"(dim1, dim2.invert); 858 } 859 pure @safe unittest 860 { 861 auto dim1 = ["a": 1, "b": -2]; 862 auto dim2 = ["a": 1, "c": 2]; 863 assert(binop!"/"(dim1, dim2) == ["b": -2, "c": -2]); 864 } 865 866 Dimensions pow(in Dimensions dim, int power) pure @safe 867 { 868 if (dim.length == 0 || power == 0) 869 return Dimensions.init; 870 871 Dimensions ret; 872 foreach (k, v; dim) 873 { 874 assert(v != 0); 875 ret[k] = v * power; 876 } 877 return ret; 878 } 879 pure @safe unittest 880 { 881 auto dim = ["a": 5, "b": -2]; 882 assert(dim.pow(2) == ["a": 10, "b": -4]); 883 assert(dim.pow(0) is null); 884 } 885 886 Dimensions powinverse(in Dimensions dim, int n) pure @safe 887 { 888 assert(n != 0); 889 Dimensions ret; 890 foreach (k, v; dim) 891 { 892 assert(v != 0); 893 enforce(v % n == 0, "Dimension error: '%s^%s' is not divisible by %s".format(k, v, n)); 894 ret[k] = v / n; 895 } 896 return ret; 897 } 898 pure @safe unittest 899 { 900 auto dim = ["a": 6, "b": -2]; 901 assert(dim.powinverse(2) == ["a": 3, "b": -1]); 902 } 903 904 string toString(in Dimensions dim) pure @safe 905 { 906 import std.algorithm : filter; 907 import std.array : join; 908 import std.conv : to; 909 910 static string stringize(string symbol, int power) pure 911 { 912 if (power == 0) 913 return null; 914 if (power == 1) 915 return symbol; 916 return symbol ~ "^" ~ to!string(power); 917 } 918 919 string[] dimstrs; 920 foreach (sym, pow; dim) 921 dimstrs ~= stringize(sym, pow); 922 923 return "[%-(%s %)]".format(dimstrs.filter!"a !is null"); 924 } 925 unittest 926 { 927 assert(["a": 2, "b": -1, "c": 1, "d": 0].toString == "[a^2 b^-1 c]"); 928 }