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 this = qty.qVariant; 188 } 189 190 /// Creates a new dimensionless quantity from a number 191 this(T)(T scalar) 192 if (isNumeric!T) 193 { 194 _dimensions = Dimensions.init; 195 _value = scalar; 196 } 197 198 /// Returns the dimensions of the quantity 199 Dimensions dimensions() @property const 200 { 201 return _dimensions; 202 } 203 204 /++ 205 Implicitly convert a dimensionless value to the value type. 206 207 Calling get will throw DimensionException if the quantity is not 208 dimensionless. 209 +/ 210 N get() const 211 { 212 checkDimensionless; 213 return _value; 214 } 215 216 alias get this; 217 218 /++ 219 Gets the _value of this quantity when expressed in the given target unit. 220 +/ 221 N value(Q)(auto ref const Q target) const 222 if (isQVariantOrQuantity!Q) 223 { 224 checkDim(target.dimensions); 225 return _value / target.rawValue; 226 } 227 /// 228 @safe pure unittest 229 { 230 auto minute = unit!int("T"); 231 auto hour = 60 * minute; 232 233 QVariant!int time = 120 * minute; 234 assert(time.value(hour) == 2); 235 assert(time.value(minute) == 120); 236 } 237 238 /++ 239 Test whether this quantity is dimensionless 240 +/ 241 bool isDimensionless() @property const 242 { 243 return _dimensions.empty; 244 } 245 246 /++ 247 Tests wheter this quantity has the same dimensions as another one. 248 +/ 249 bool isConsistentWith(Q)(auto ref const Q qty) const 250 if (isQVariantOrQuantity!Q) 251 { 252 return _dimensions == qty.dimensions; 253 } 254 /// 255 @safe pure unittest 256 { 257 auto second = unit!double("T"); 258 auto minute = 60 * second; 259 auto meter = unit!double("L"); 260 261 assert(minute.isConsistentWith(second)); 262 assert(!meter.isConsistentWith(second)); 263 } 264 265 /++ 266 Returns the base unit of this quantity. 267 +/ 268 QVariant baseUnit() @property const 269 { 270 return QVariant(1, _dimensions); 271 } 272 273 /++ 274 Cast a dimensionless quantity to a numeric type. 275 276 The cast operation will throw DimensionException if the quantity is not 277 dimensionless. 278 +/ 279 T opCast(T)() const 280 if (isNumeric!T) 281 { 282 checkDimensionless; 283 return _value; 284 } 285 286 // Assign from another quantity 287 /// Operator overloading 288 ref QVariant opAssign(Q)(auto ref const Q qty) 289 if (isQVariantOrQuantity!Q) 290 { 291 _dimensions = qty.dimensions; 292 _value = qty.rawValue; 293 return this; 294 } 295 296 // Assign from a numeric value if this quantity is dimensionless 297 /// ditto 298 ref QVariant opAssign(T)(T scalar) 299 if (isNumeric!T) 300 { 301 _dimensions = Dimensions.init; 302 _value = scalar; 303 return this; 304 } 305 306 // Unary + and - 307 /// ditto 308 QVariant!N opUnary(string op)() const 309 if (op == "+" || op == "-") 310 { 311 return QVariant(mixin(op ~ "_value"), _dimensions); 312 } 313 314 // Unary ++ and -- 315 /// ditto 316 QVariant!N opUnary(string op)() 317 if (op == "++" || op == "--") 318 { 319 mixin(op ~ "_value;"); 320 return this; 321 } 322 323 // Add (or substract) two quantities if they share the same dimensions 324 /// ditto 325 QVariant!N opBinary(string op, Q)(auto ref const Q qty) const 326 if (isQVariantOrQuantity!Q && (op == "+" || op == "-")) 327 { 328 checkDim(qty.dimensions); 329 return QVariant(mixin("_value" ~ op ~ "qty.rawValue"), _dimensions); 330 } 331 332 /// ditto 333 QVariant!N opBinaryRight(string op, Q)(auto ref const Q qty) const 334 if (isQVariantOrQuantity!Q && (op == "+" || op == "-")) 335 { 336 checkDim(qty.dimensions); 337 return QVariant(mixin("qty.rawValue" ~ op ~ "_value"), _dimensions); 338 } 339 340 // Add (or substract) a dimensionless quantity and a number 341 /// ditto 342 QVariant!N opBinary(string op, T)(T scalar) const 343 if (isNumeric!T && (op == "+" || op == "-")) 344 { 345 checkDimensionless; 346 return QVariant(mixin("_value" ~ op ~ "scalar"), _dimensions); 347 } 348 349 /// ditto 350 QVariant!N opBinaryRight(string op, T)(T scalar) const 351 if (isNumeric!T && (op == "+" || op == "-")) 352 { 353 checkDimensionless; 354 return QVariant(mixin("scalar" ~ op ~ "_value"), _dimensions); 355 } 356 357 // Multiply or divide a quantity by a number 358 /// ditto 359 QVariant!N opBinary(string op, T)(T scalar) const 360 if (isNumeric!T && (op == "*" || op == "/" || op == "%")) 361 { 362 return QVariant(mixin("_value" ~ op ~ "scalar"), _dimensions); 363 } 364 365 /// ditto 366 QVariant!N opBinaryRight(string op, T)(T scalar) const 367 if (isNumeric!T && op == "*") 368 { 369 return QVariant(mixin("scalar" ~ op ~ "_value"), _dimensions); 370 } 371 372 /// ditto 373 QVariant!N opBinaryRight(string op, T)(T scalar) const 374 if (isNumeric!T && (op == "/" || op == "%")) 375 { 376 return QVariant(mixin("scalar" ~ op ~ "_value"), ~_dimensions); 377 } 378 379 // Multiply or divide two quantities 380 /// ditto 381 QVariant!N opBinary(string op, Q)(auto ref const Q qty) const 382 if (isQVariantOrQuantity!Q && (op == "*" || op == "/")) 383 { 384 return QVariant(mixin("_value" ~ op ~ "qty.rawValue"), 385 mixin("_dimensions" ~ op ~ "qty.dimensions")); 386 } 387 388 /// ditto 389 QVariant!N opBinaryRight(string op, Q)(auto ref const Q qty) const 390 if (isQVariantOrQuantity!Q && (op == "*" || op == "/")) 391 { 392 return QVariant(mixin("qty.rawValue" ~ op ~ "_value"), 393 mixin("qty.dimensions" ~ op ~ "_dimensions")); 394 } 395 396 /// ditto 397 QVariant!N opBinary(string op, Q)(auto ref const Q qty) const 398 if (isQVariantOrQuantity!Q && (op == "%")) 399 { 400 checkDim(qty.dimensions); 401 return QVariant(_value % qty.rawValue, _dimensions); 402 } 403 404 /// ditto 405 QVariant!N opBinaryRight(string op, Q)(auto ref const Q qty) const 406 if (isQVariantOrQuantity!Q && (op == "%")) 407 { 408 checkDim(qty.dimensions); 409 return QVariant(qty.rawValue % _value, _dimensions); 410 } 411 412 /// ditto 413 QVariant!N opBinary(string op, T)(T power) const 414 if (isIntegral!T && op == "^^") 415 { 416 return QVariant(_value ^^ power, _dimensions.pow(Rational(power))); 417 } 418 419 /// ditto 420 QVariant!N opBinary(string op)(Rational power) const 421 if (op == "^^") 422 { 423 static if (isIntegral!N) 424 auto newValue = std.math.pow(_value, cast(real) power).roundTo!N; 425 else static if (isFloatingPoint!N) 426 auto newValue = std.math.pow(_value, cast(real) power); 427 else 428 static assert(false, "Operation not defined for " ~ QVariant!N.stringof); 429 return QVariant(newValue, _dimensions.pow(power)); 430 } 431 432 // Add/sub assign with a quantity that shares the same dimensions 433 /// ditto 434 void opOpAssign(string op, Q)(auto ref const Q qty) 435 if (isQVariantOrQuantity!Q && (op == "+" || op == "-")) 436 { 437 checkDim(qty.dimensions); 438 mixin("_value " ~ op ~ "= qty.rawValue;"); 439 } 440 441 // Add/sub assign a number to a dimensionless quantity 442 /// ditto 443 void opOpAssign(string op, T)(T scalar) 444 if (isNumeric!T && (op == "+" || op == "-")) 445 { 446 checkDimensionless; 447 mixin("_value " ~ op ~ "= scalar;"); 448 } 449 450 // Mul/div assign another quantity to a quantity 451 /// ditto 452 void opOpAssign(string op, Q)(auto ref const Q qty) 453 if (isQVariantOrQuantity!Q && (op == "*" || op == "/" || op == "%")) 454 { 455 mixin("_value" ~ op ~ "= qty.rawValue;"); 456 static if (op == "*") 457 _dimensions = _dimensions * qty.dimensions; 458 else 459 _dimensions = _dimensions / qty.dimensions; 460 } 461 462 // Mul/div assign a number to a quantity 463 /// ditto 464 void opOpAssign(string op, T)(T scalar) 465 if (isNumeric!T && (op == "*" || op == "/")) 466 { 467 mixin("_value" ~ op ~ "= scalar;"); 468 } 469 470 /// ditto 471 void opOpAssign(string op, T)(T scalar) 472 if (isNumeric!T && op == "%") 473 { 474 checkDimensionless; 475 mixin("_value" ~ op ~ "= scalar;"); 476 } 477 478 // Exact equality between quantities 479 /// ditto 480 bool opEquals(Q)(auto ref const Q qty) const 481 if (isQVariantOrQuantity!Q) 482 { 483 checkDim(qty.dimensions); 484 return _value == qty.rawValue; 485 } 486 487 // Exact equality between a dimensionless quantity and a number 488 /// ditto 489 bool opEquals(T)(T scalar) const 490 if (isNumeric!T) 491 { 492 checkDimensionless; 493 return _value == scalar; 494 } 495 496 // Comparison between two quantities 497 /// ditto 498 int opCmp(Q)(auto ref const Q qty) const 499 if (isQVariantOrQuantity!Q) 500 { 501 checkDim(qty.dimensions); 502 if (_value == qty.rawValue) 503 return 0; 504 if (_value < qty.rawValue) 505 return -1; 506 return 1; 507 } 508 509 // Comparison between a dimensionless quantity and a number 510 /// ditto 511 int opCmp(T)(T scalar) const 512 if (isNumeric!T) 513 { 514 checkDimensionless; 515 if (_value < scalar) 516 return -1; 517 if (_value > scalar) 518 return 1; 519 return 0; 520 } 521 522 void toString(scope void delegate(const(char)[]) sink, FormatSpec!char fmt) const 523 { 524 sink.formatValue(_value, fmt); 525 sink(" "); 526 sink.formattedWrite!"%s"(_dimensions); 527 } 528 } 529 530 /++ 531 Creates a new monodimensional unit as a QVariant. 532 533 Params: 534 N = The numeric type of the value part of the quantity. 535 536 dimSymbol = The symbol of the dimension of this quantity. 537 538 rank = The rank of the dimensions of this quantity in the dimension vector, 539 when combining this quantity with other oned. 540 +/ 541 QVariant!N unit(N)(string dimSymbol, size_t rank = size_t.max) 542 { 543 return QVariant!N(N(1), Dimensions.mono(dimSymbol, rank)); 544 } 545 /// 546 unittest 547 { 548 enum meter = unit!double("L", 1); 549 enum kilogram = unit!double("M", 2); 550 // Dimensions will be in this order: L M 551 } 552 553 // Tests whether T is a quantity type 554 template isQVariant(T) 555 { 556 alias U = Unqual!T; 557 static if (is(U == QVariant!X, X...)) 558 enum isQVariant = true; 559 else 560 enum isQVariant = false; 561 } 562 563 enum isQVariantOrQuantity(T) = isQVariant!T || isQuantity!T; 564 565 /// Turns a Quantity into a QVariant 566 auto qVariant(Q)(auto ref const Q qty) 567 if (isQuantity!Q) 568 { 569 return QVariant!(Q.valueType)(qty.rawValue, qty.dimensions); 570 } 571 572 /// Turns a scalar into a dimensionless QVariant 573 auto qVariant(N)(N scalar) 574 if (isNumeric!N) 575 { 576 return QVariant!N(scalar, Dimensions.init); 577 } 578 579 /// Basic math functions that work with QVariant. 580 auto square(Q)(auto ref const Q quantity) 581 if (isQVariant!Q) 582 { 583 return Q(quantity._value ^^ 2, quantity._dimensions.pow(2)); 584 } 585 586 /// ditto 587 auto sqrt(Q)(auto ref const Q quantity) 588 if (isQVariant!Q) 589 { 590 return Q(std.math.sqrt(quantity._value), quantity._dimensions.powinverse(2)); 591 } 592 593 /// ditto 594 auto cubic(Q)(auto ref const Q quantity) 595 if (isQVariant!Q) 596 { 597 return Q(quantity._value ^^ 3, quantity._dimensions.pow(3)); 598 } 599 600 /// ditto 601 auto cbrt(Q)(auto ref const Q quantity) 602 if (isQVariant!Q) 603 { 604 return Q(std.math.cbrt(quantity._value), quantity._dimensions.powinverse(3)); 605 } 606 607 /// ditto 608 auto pow(Q)(auto ref const Q quantity, Rational r) 609 if (isQVariant!Q) 610 { 611 return quantity ^^ r; 612 } 613 614 auto pow(Q, I)(auto ref const Q quantity, I n) 615 if (isQVariant!Q && isIntegral!I) 616 { 617 return quantity ^^ Rational(n); 618 } 619 620 /// ditto 621 auto nthRoot(Q)(auto ref const Q quantity, Rational r) 622 if (isQVariant!Q) 623 { 624 return quantity ^^ r.inverted; 625 } 626 627 auto nthRoot(Q, I)(auto ref const Q quantity, I n) 628 if (isQVariant!Q && isIntegral!I) 629 { 630 return nthRoot(quantity, Rational(n)); 631 } 632 633 /// ditto 634 Q abs(Q)(auto ref const Q quantity) 635 if (isQVariant!Q) 636 { 637 return Q(std.math.fabs(quantity._value), quantity._dimensions); 638 }