1 /++ 2 This module defines dimensionally variant quantities, mainly for use at run-time. 3 4 The dimensions are stored in a field, along with the numerical value of the 5 quantity. Operations and function calls fail if they are not dimensionally 6 consistent, by throwing a `DimensionException`. 7 8 Copyright: Copyright 2013-2018, Nicolas Sicard 9 Authors: Nicolas Sicard 10 License: $(LINK www.boost.org/LICENSE_1_0.txt, Boost License 1.0) 11 Source: $(LINK https://github.com/biozic/quantities) 12 +/ 13 module quantities.runtime; 14 15 /// 16 unittest 17 { 18 import quantities.runtime; 19 import quantities.si; 20 import std.format : format; 21 import std.math : approxEqual; 22 23 // Note: the types of the predefined SI units (gram, mole, liter...) 24 // are Quantity instances, not QVariant instance. 25 26 // Introductory example 27 { 28 // I have to make a new solution at the concentration of 5 mmol/L 29 QVariant!double concentration = 5.0 * milli(mole) / liter; 30 31 // The final volume is 100 ml. 32 QVariant!double volume = 100.0 * milli(liter); 33 34 // The molar mass of my compound is 118.9 g/mol 35 QVariant!double molarMass = 118.9 * gram / mole; 36 37 // What mass should I weigh? 38 QVariant!double mass = concentration * volume * molarMass; 39 assert(format("%s", mass) == "5.945e-05 [M]"); 40 // Wait! That's not really useful! 41 assert(siFormat!"%.1f mg"(mass) == "59.5 mg"); 42 } 43 44 // Working with predefined units 45 { 46 QVariant!double distance = 384_400 * kilo(meter); // From Earth to Moon 47 QVariant!double speed = 299_792_458 * meter / second; // Speed of light 48 QVariant!double time = distance / speed; 49 assert(time.siFormat!"%.3f s" == "1.282 s"); 50 } 51 52 // Dimensional correctness 53 { 54 import std.exception : assertThrown; 55 56 QVariant!double mass = 4 * kilogram; 57 assertThrown!DimensionException(mass + meter); 58 assertThrown!DimensionException(mass == 1.2); 59 } 60 61 // Create a new unit from the predefined ones 62 { 63 QVariant!double inch = 2.54 * centi(meter); 64 QVariant!double mile = 1609 * meter; 65 assert(mile.value(inch).approxEqual(63_346)); // inches in a mile 66 // NB. Cannot use siFormatter, because inches are not SI units 67 } 68 69 // Create a new unit with new dimensions 70 { 71 // Create a new base unit of currency 72 QVariant!double euro = unit!double("C"); // C is the chosen dimension symol (for currency...) 73 74 QVariant!double dollar = euro / 1.35; 75 QVariant!double price = 2000 * dollar; 76 assert(price.value(euro).approxEqual(1481)); // Price in euros 77 } 78 79 // Run-time parsing 80 { 81 auto data = ["distance-to-the-moon" : "384_400 km", "speed-of-light" : "299_792_458 m/s"]; 82 QVariant!double distance = parseSI(data["distance-to-the-moon"]); 83 QVariant!double speed = parseSI(data["speed-of-light"]); 84 QVariant!double time = distance / speed; 85 } 86 } 87 88 import quantities.internal.dimensions; 89 import quantities.common; 90 import quantities.compiletime; 91 92 import std.conv; 93 import std.exception; 94 import std.format; 95 import std.math; 96 import std.string; 97 import std.traits; 98 99 /++ 100 Exception thrown when operating on two units that are not interconvertible. 101 +/ 102 class DimensionException : Exception 103 { 104 /// Holds the dimensions of the quantity currently operated on 105 Dimensions thisDim; 106 /// Holds the dimensions of the eventual other operand 107 Dimensions otherDim; 108 109 mixin basicExceptionCtors; 110 111 this(string msg, Dimensions thisDim, Dimensions otherDim, 112 string file = __FILE__, size_t line = __LINE__, Throwable next = null) @safe pure nothrow 113 { 114 super(msg, file, line, next); 115 this.thisDim = thisDim; 116 this.otherDim = otherDim; 117 } 118 } 119 /// 120 unittest 121 { 122 import std.exception : assertThrown; 123 124 enum meter = unit!double("L"); 125 enum second = unit!double("T"); 126 assertThrown!DimensionException(meter + second); 127 } 128 129 /++ 130 A dimensionnaly variant quantity. 131 132 Params: 133 N = the numeric type of the quantity. 134 135 See_Also: 136 QVariant has the same public members and overloaded operators as Quantity. 137 +/ 138 struct QVariant(N) 139 { 140 static assert(isNumeric!N, "Incompatible type: " ~ N.stringof); 141 142 private: 143 N _value; 144 Dimensions _dimensions; 145 146 void checkDim(Dimensions dim) @safe pure const 147 { 148 enforce(_dimensions == dim, 149 new DimensionException("Incompatible dimensions", _dimensions, dim)); 150 } 151 152 void checkDimensionless() @safe pure const 153 { 154 enforce(_dimensions.empty, new DimensionException("Not dimensionless", 155 _dimensions, Dimensions.init)); 156 } 157 158 package(quantities): 159 alias valueType = N; 160 161 N rawValue() const 162 { 163 return _value; 164 } 165 166 public: 167 // Creates a new quantity with non-empty dimensions 168 this(T)(T scalar, const Dimensions dim) 169 if (isNumeric!T) 170 { 171 _value = scalar; 172 _dimensions = dim; 173 } 174 175 /// Creates a new quantity from another one with the same dimensions 176 this(Q)(auto ref const Q qty) 177 if (isQVariant!Q) 178 { 179 _value = qty._value; 180 _dimensions = qty._dimensions; 181 } 182 183 /// ditto 184 this(Q)(auto ref const Q qty) 185 if (isQuantity!Q) 186 { 187 import quantities.compiletime : qVariant; 188 189 this = qty.qVariant; 190 } 191 192 /// Creates a new dimensionless quantity from a number 193 this(T)(T scalar) 194 if (isNumeric!T) 195 { 196 _dimensions = Dimensions.init; 197 _value = scalar; 198 } 199 200 /// Returns the dimensions of the quantity 201 Dimensions dimensions() @property const 202 { 203 return _dimensions; 204 } 205 206 /++ 207 Implicitly convert a dimensionless value to the value type. 208 209 Calling get will throw DimensionException if the quantity is not 210 dimensionless. 211 +/ 212 N get() const 213 { 214 checkDimensionless; 215 return _value; 216 } 217 218 alias get this; 219 220 /++ 221 Gets the _value of this quantity when expressed in the given target unit. 222 +/ 223 N value(Q)(auto ref const Q target) const 224 if (isQVariantOrQuantity!Q) 225 { 226 checkDim(target.dimensions); 227 return _value / target.rawValue; 228 } 229 /// 230 @safe pure unittest 231 { 232 auto minute = unit!int("T"); 233 auto hour = 60 * minute; 234 235 QVariant!int time = 120 * minute; 236 assert(time.value(hour) == 2); 237 assert(time.value(minute) == 120); 238 } 239 240 /++ 241 Test whether this quantity is dimensionless 242 +/ 243 bool isDimensionless() @property const 244 { 245 return _dimensions.empty; 246 } 247 248 /++ 249 Tests wheter this quantity has the same dimensions as another one. 250 +/ 251 bool isConsistentWith(Q)(auto ref const Q qty) const 252 if (isQVariantOrQuantity!Q) 253 { 254 return _dimensions == qty.dimensions; 255 } 256 /// 257 @safe pure unittest 258 { 259 auto second = unit!double("T"); 260 auto minute = 60 * second; 261 auto meter = unit!double("L"); 262 263 assert(minute.isConsistentWith(second)); 264 assert(!meter.isConsistentWith(second)); 265 } 266 267 /++ 268 Returns the base unit of this quantity. 269 +/ 270 QVariant baseUnit() @property const 271 { 272 return QVariant(1, _dimensions); 273 } 274 275 /++ 276 Cast a dimensionless quantity to a numeric type. 277 278 The cast operation will throw DimensionException if the quantity is not 279 dimensionless. 280 +/ 281 T opCast(T)() const 282 if (isNumeric!T) 283 { 284 checkDimensionless; 285 return _value; 286 } 287 288 // Assign from another quantity 289 /// Operator overloading 290 ref QVariant opAssign(Q)(auto ref const Q qty) 291 if (isQVariantOrQuantity!Q) 292 { 293 _dimensions = qty.dimensions; 294 _value = qty.rawValue; 295 return this; 296 } 297 298 // Assign from a numeric value if this quantity is dimensionless 299 /// ditto 300 ref QVariant opAssign(T)(T scalar) 301 if (isNumeric!T) 302 { 303 _dimensions = Dimensions.init; 304 _value = scalar; 305 return this; 306 } 307 308 // Unary + and - 309 /// ditto 310 QVariant!N opUnary(string op)() const 311 if (op == "+" || op == "-") 312 { 313 return QVariant(mixin(op ~ "_value"), _dimensions); 314 } 315 316 // Unary ++ and -- 317 /// ditto 318 QVariant!N opUnary(string op)() 319 if (op == "++" || op == "--") 320 { 321 mixin(op ~ "_value;"); 322 return this; 323 } 324 325 // Add (or substract) two quantities if they share the same dimensions 326 /// ditto 327 QVariant!N opBinary(string op, Q)(auto ref const Q qty) const 328 if (isQVariantOrQuantity!Q && (op == "+" || op == "-")) 329 { 330 checkDim(qty.dimensions); 331 return QVariant(mixin("_value" ~ op ~ "qty.rawValue"), _dimensions); 332 } 333 334 /// ditto 335 QVariant!N opBinaryRight(string op, Q)(auto ref const Q qty) const 336 if (isQVariantOrQuantity!Q && (op == "+" || op == "-")) 337 { 338 checkDim(qty.dimensions); 339 return QVariant(mixin("qty.rawValue" ~ op ~ "_value"), _dimensions); 340 } 341 342 // Add (or substract) a dimensionless quantity and a number 343 /// ditto 344 QVariant!N opBinary(string op, T)(T scalar) const 345 if (isNumeric!T && (op == "+" || op == "-")) 346 { 347 checkDimensionless; 348 return QVariant(mixin("_value" ~ op ~ "scalar"), _dimensions); 349 } 350 351 /// ditto 352 QVariant!N opBinaryRight(string op, T)(T scalar) const 353 if (isNumeric!T && (op == "+" || op == "-")) 354 { 355 checkDimensionless; 356 return QVariant(mixin("scalar" ~ op ~ "_value"), _dimensions); 357 } 358 359 // Multiply or divide a quantity by a number 360 /// ditto 361 QVariant!N opBinary(string op, T)(T scalar) const 362 if (isNumeric!T && (op == "*" || op == "/" || op == "%")) 363 { 364 return QVariant(mixin("_value" ~ op ~ "scalar"), _dimensions); 365 } 366 367 /// ditto 368 QVariant!N opBinaryRight(string op, T)(T scalar) const 369 if (isNumeric!T && op == "*") 370 { 371 return QVariant(mixin("scalar" ~ op ~ "_value"), _dimensions); 372 } 373 374 /// ditto 375 QVariant!N opBinaryRight(string op, T)(T scalar) const 376 if (isNumeric!T && (op == "/" || op == "%")) 377 { 378 return QVariant(mixin("scalar" ~ op ~ "_value"), ~_dimensions); 379 } 380 381 // Multiply or divide two quantities 382 /// ditto 383 QVariant!N opBinary(string op, Q)(auto ref const Q qty) const 384 if (isQVariantOrQuantity!Q && (op == "*" || op == "/")) 385 { 386 return QVariant(mixin("_value" ~ op ~ "qty.rawValue"), 387 mixin("_dimensions" ~ op ~ "qty.dimensions")); 388 } 389 390 /// ditto 391 QVariant!N opBinaryRight(string op, Q)(auto ref const Q qty) const 392 if (isQVariantOrQuantity!Q && (op == "*" || op == "/")) 393 { 394 return QVariant(mixin("qty.rawValue" ~ op ~ "_value"), 395 mixin("qty.dimensions" ~ op ~ "_dimensions")); 396 } 397 398 /// ditto 399 QVariant!N opBinary(string op, Q)(auto ref const Q qty) const 400 if (isQVariantOrQuantity!Q && (op == "%")) 401 { 402 checkDim(qty.dimensions); 403 return QVariant(_value % qty.rawValue, _dimensions); 404 } 405 406 /// ditto 407 QVariant!N opBinaryRight(string op, Q)(auto ref const Q qty) const 408 if (isQVariantOrQuantity!Q && (op == "%")) 409 { 410 checkDim(qty.dimensions); 411 return QVariant(qty.rawValue % _value, _dimensions); 412 } 413 414 /// ditto 415 QVariant!N opBinary(string op, T)(T power) const 416 if (isIntegral!T && op == "^^") 417 { 418 return QVariant(_value ^^ power, _dimensions.pow(Rational(power))); 419 } 420 421 /// ditto 422 QVariant!N opBinary(string op)(Rational power) const 423 if (op == "^^") 424 { 425 static if (isIntegral!N) 426 auto newValue = std.math.pow(_value, cast(real) power).roundTo!N; 427 else static if (isFloatingPoint!N) 428 auto newValue = std.math.pow(_value, cast(real) power); 429 else 430 static assert(false, "Operation not defined for " ~ QVariant!N.stringof); 431 return QVariant(newValue, _dimensions.pow(power)); 432 } 433 434 // Add/sub assign with a quantity that shares the same dimensions 435 /// ditto 436 void opOpAssign(string op, Q)(auto ref const Q qty) 437 if (isQVariantOrQuantity!Q && (op == "+" || op == "-")) 438 { 439 checkDim(qty.dimensions); 440 mixin("_value " ~ op ~ "= qty.rawValue;"); 441 } 442 443 // Add/sub assign a number to a dimensionless quantity 444 /// ditto 445 void opOpAssign(string op, T)(T scalar) 446 if (isNumeric!T && (op == "+" || op == "-")) 447 { 448 checkDimensionless; 449 mixin("_value " ~ op ~ "= scalar;"); 450 } 451 452 // Mul/div assign another quantity to a quantity 453 /// ditto 454 void opOpAssign(string op, Q)(auto ref const Q qty) 455 if (isQVariantOrQuantity!Q && (op == "*" || op == "/" || op == "%")) 456 { 457 mixin("_value" ~ op ~ "= qty.rawValue;"); 458 static if (op == "*") 459 _dimensions = _dimensions * qty.dimensions; 460 else 461 _dimensions = _dimensions / qty.dimensions; 462 } 463 464 // Mul/div assign a number to a quantity 465 /// ditto 466 void opOpAssign(string op, T)(T scalar) 467 if (isNumeric!T && (op == "*" || op == "/")) 468 { 469 mixin("_value" ~ op ~ "= scalar;"); 470 } 471 472 /// ditto 473 void opOpAssign(string op, T)(T scalar) 474 if (isNumeric!T && op == "%") 475 { 476 checkDimensionless; 477 mixin("_value" ~ op ~ "= scalar;"); 478 } 479 480 // Exact equality between quantities 481 /// ditto 482 bool opEquals(Q)(auto ref const Q qty) const 483 if (isQVariantOrQuantity!Q) 484 { 485 checkDim(qty.dimensions); 486 return _value == qty.rawValue; 487 } 488 489 // Exact equality between a dimensionless quantity and a number 490 /// ditto 491 bool opEquals(T)(T scalar) const 492 if (isNumeric!T) 493 { 494 checkDimensionless; 495 return _value == scalar; 496 } 497 498 // Comparison between two quantities 499 /// ditto 500 int opCmp(Q)(auto ref const Q qty) const 501 if (isQVariantOrQuantity!Q) 502 { 503 checkDim(qty.dimensions); 504 if (_value == qty.rawValue) 505 return 0; 506 if (_value < qty.rawValue) 507 return -1; 508 return 1; 509 } 510 511 // Comparison between a dimensionless quantity and a number 512 /// ditto 513 int opCmp(T)(T scalar) const 514 if (isNumeric!T) 515 { 516 checkDimensionless; 517 if (_value < scalar) 518 return -1; 519 if (_value > scalar) 520 return 1; 521 return 0; 522 } 523 524 void toString(scope void delegate(const(char)[]) sink, FormatSpec!char fmt) const 525 { 526 sink.formatValue(_value, fmt); 527 sink(" "); 528 sink.formattedWrite!"%s"(_dimensions); 529 } 530 } 531 532 /++ 533 Creates a new monodimensional unit as a QVariant. 534 535 Params: 536 N = The numeric type of the value part of the quantity. 537 538 dimSymbol = The symbol of the dimension of this quantity. 539 540 rank = The rank of the dimensions of this quantity in the dimension vector, 541 when combining this quantity with other oned. 542 +/ 543 QVariant!N unit(N)(string dimSymbol, size_t rank = size_t.max) 544 { 545 return QVariant!N(N(1), Dimensions.mono(dimSymbol, rank)); 546 } 547 /// 548 unittest 549 { 550 enum meter = unit!double("L", 1); 551 enum kilogram = unit!double("M", 2); 552 // Dimensions will be in this order: L M 553 } 554 555 // Tests whether T is a quantity type 556 template isQVariant(T) 557 { 558 alias U = Unqual!T; 559 static if (is(U == QVariant!X, X...)) 560 enum isQVariant = true; 561 else 562 enum isQVariant = false; 563 } 564 565 enum isQVariantOrQuantity(T) = isQVariant!T || isQuantity!T; 566 567 /// Turns a Quantity into a QVariant 568 auto qVariant(Q)(auto ref const Q qty) 569 if (isQuantity!Q) 570 { 571 return QVariant!(Q.valueType)(qty.rawValue, qty.dimensions); 572 } 573 574 /// Turns a scalar into a dimensionless QVariant 575 auto qVariant(N)(N scalar) 576 if (isNumeric!N) 577 { 578 return QVariant!N(scalar, Dimensions.init); 579 } 580 581 /// Basic math functions that work with QVariant. 582 auto square(Q)(auto ref const Q quantity) 583 if (isQVariant!Q) 584 { 585 return Q(quantity._value ^^ 2, quantity._dimensions.pow(2)); 586 } 587 588 /// ditto 589 auto sqrt(Q)(auto ref const Q quantity) 590 if (isQVariant!Q) 591 { 592 return Q(std.math.sqrt(quantity._value), quantity._dimensions.powinverse(2)); 593 } 594 595 /// ditto 596 auto cubic(Q)(auto ref const Q quantity) 597 if (isQVariant!Q) 598 { 599 return Q(quantity._value ^^ 3, quantity._dimensions.pow(3)); 600 } 601 602 /// ditto 603 auto cbrt(Q)(auto ref const Q quantity) 604 if (isQVariant!Q) 605 { 606 return Q(std.math.cbrt(quantity._value), quantity._dimensions.powinverse(3)); 607 } 608 609 /// ditto 610 auto pow(Q)(auto ref const Q quantity, Rational r) 611 if (isQVariant!Q) 612 { 613 return quantity ^^ r; 614 } 615 616 auto pow(Q, I)(auto ref const Q quantity, I n) 617 if (isQVariant!Q && isIntegral!I) 618 { 619 return quantity ^^ Rational(n); 620 } 621 622 /// ditto 623 auto nthRoot(Q)(auto ref const Q quantity, Rational r) 624 if (isQVariant!Q) 625 { 626 return quantity ^^ r.inverted; 627 } 628 629 auto nthRoot(Q, I)(auto ref const Q quantity, I n) 630 if (isQVariant!Q && isIntegral!I) 631 { 632 return nthRoot(quantity, Rational(n)); 633 } 634 635 /// ditto 636 Q abs(Q)(auto ref const Q quantity) 637 if (isQVariant!Q) 638 { 639 return Q(std.math.fabs(quantity._value), quantity._dimensions); 640 }