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