1 /++ 2 This module defines dimensionnaly variant quantities. 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.qvariant; 10 11 import quantities.base; 12 import quantities.parsing : DimensionException; 13 import std.exception; 14 import std.format; 15 import std.string; 16 import std.traits; 17 18 version (unittest) import std.math : approxEqual; 19 20 alias Dimensions = int[string]; 21 22 /++ 23 QVariant is analog to Quantity except that the dimensions are stored in a 24 private field instead of a type parameter. This makes QVariant "dimensionnaly 25 variant", so that a variable of type QVariant can hold quantities of variable 26 dimensions. Yet, only operations that are dimensionnaly consistent can be 27 performed on QVariant variables. 28 29 Warning: Contrary to Quantity, where all dimensional operations are done at 30 compile-time, the dimensions of QVariant ate computed at runtime for each 31 operation. This has a significant performance cost. Only use QVariant in 32 situation where using Quantity is not possible, that is when the dimensions of 33 the quantities are only known at runtime. 34 35 Params: 36 N = the numeric type of the quantity. 37 38 See_Also: 39 QVariant has the same public members and overloaded operators as Quantity. 40 +/ 41 struct QVariant(N) 42 { 43 static assert(isNumeric!N, "Incompatible type: " ~ N.stringof); 44 45 private: 46 this(N value, in Dimensions dim) pure @safe 47 { 48 _value = value; 49 dimensions = dim.dimdup; 50 } 51 52 void checkDim(in Dimensions dim) const pure @safe 53 { 54 enforceEx!DimensionException(equals(dim, dimensions), 55 "Dimension error: %s is not compatible with %s" 56 .format(.toString(dim), .toString(dimensions))); 57 } 58 59 static void checkValueType(T)() 60 { 61 static assert(is(T : valueType), "%s is not implicitly convertible to %s" 62 .format(T.stringof, valueType.stringof)); 63 } 64 65 package: 66 N _value; 67 Dimensions dimensions; 68 69 // Should be a constructor 70 // Workaround for @@BUG 5770@@ 71 // (https://d.puremagic.com/issues/show_bug.cgi?id=5770) 72 // "Template constructor bypass access check" 73 static QVariant make(T)(T value, in Dimensions dim) 74 if (isNumeric!T) 75 { 76 checkValueType!T; 77 return QVariant(cast(N) value, dim); 78 } 79 80 // Gets the internal number of this quantity. 81 N rawValue() const pure nothrow @nogc @safe 82 { 83 return _value; 84 } 85 86 public: 87 alias valueType = N; 88 89 // Gets the base unit of this quantity. 90 QVariant baseUnit() pure @safe 91 { 92 N one = 1; 93 return QVariant.make(one, dimensions.dup); 94 } 95 96 // Creates a new quantity from another one with the same dimensions 97 this(Q)(Q other) 98 if (isQVariant!Q || isQuantity!Q) 99 { 100 checkValueType!(Q.valueType); 101 _value = other.rawValue; 102 dimensions = other.dimensions; 103 } 104 105 // Creates a new dimensionless quantity from a number 106 this(T)(T value) 107 if (isNumeric!T) 108 { 109 checkValueType!T; 110 dimensions = Dimensions.init; 111 _value = value; 112 } 113 114 // Implicitly convert a dimensionless value to the value type 115 N get() const pure @safe 116 { 117 checkDim(Dimensions.init); 118 return _value; 119 } 120 alias get this; 121 122 /+ 123 Gets the _value of this quantity expressed in the given target unit. 124 +/ 125 N value(Q)(Q target) const 126 if (isQVariant!Q || isQuantity!Q) 127 { 128 checkDim(target.dimensions); 129 checkValueType!(Q.valueType); 130 return _value / target.rawValue; 131 } 132 // 133 pure @safe unittest 134 { 135 import quantities.si : minute, hour; 136 137 QVariant!double time = 120 * minute; 138 assert(time.value(hour) == 2); 139 assert(time.value(minute) == 120); 140 } 141 142 /+ 143 Tests wheter this quantity has the same dimensions as another one. 144 +/ 145 bool isConsistentWith(Q)(Q other) const 146 if (isQVariant!Q || isQuantity!Q) 147 { 148 return equals(dimensions, other.dimensions); 149 } 150 // 151 pure @safe unittest 152 { 153 import quantities.si : minute, second, meter; 154 155 assert(minute.qVariant.isConsistentWith(second)); 156 assert(!meter.qVariant.isConsistentWith(second)); 157 } 158 159 // Cast a QVariant to an equivalent Quantity 160 Q opCast(Q)() const 161 if (isQuantity!Q) 162 { 163 checkDim(Q.dimensions); 164 checkValueType!(Q.valueType); 165 return Q.make(_value); 166 } 167 168 // Cast a dimensionless quantity to a numeric type 169 T opCast(T)() const 170 if (isNumeric!T) 171 { 172 checkDim(Dimensions.init); 173 checkValueType!T; 174 return _value; 175 } 176 177 // Overloaded operators. 178 // Only dimensionally correct operations will compile. 179 180 // Assign from another quantity 181 void opAssign(Q)(Q other) 182 if (isQVariant!Q || isQuantity!Q) 183 { 184 checkValueType!(Q.valueType); 185 dimensions = other.dimensions; 186 _value = other.rawValue; 187 } 188 189 // Assign from a numeric value if this quantity is dimensionless 190 // ditto 191 void opAssign(T)(T other) 192 if (isNumeric!T) 193 { 194 checkValueType!T; 195 dimensions = Dimensions.init; 196 _value = other; 197 } 198 199 // Unary + and - 200 // ditto 201 auto opUnary(string op)() const 202 if (op == "+" || op == "-") 203 { 204 return QVariant.make(mixin(op ~ "_value"), dimensions); 205 } 206 207 // Unary ++ and -- 208 // ditto 209 auto opUnary(string op)() 210 if (op == "++" || op == "--") 211 { 212 mixin(op ~ "_value;"); 213 return this; 214 } 215 216 // Add (or substract) two quantities if they share the same dimensions 217 // ditto 218 auto opBinary(string op, Q)(Q other) const 219 if ((isQVariant!Q || isQuantity!Q) && (op == "+" || op == "-")) 220 { 221 checkDim(other.dimensions); 222 checkValueType!(Q.valueType); 223 return QVariant.make(mixin("_value" ~ op ~ "other.rawValue"), dimensions); 224 } 225 226 // ditto 227 auto opBinaryRight(string op, Q)(Q other) const 228 if ((isQVariant!Q || isQuantity!Q) && (op == "+" || op == "-")) 229 { 230 return opBinary!op(other); 231 } 232 233 // Add (or substract) a dimensionless quantity and a number 234 // ditto 235 auto opBinary(string op, T)(T other) const 236 if (isNumeric!T && (op == "+" || op == "-")) 237 { 238 checkDim(Dimensions.init); 239 checkValueType!T; 240 return QVariant.make(mixin("_value" ~ op ~ "other"), dimensions); 241 } 242 243 // ditto 244 auto opBinaryRight(string op, T)(T other) const 245 if (isNumeric!T && (op == "+" || op == "-")) 246 { 247 return opBinary!op(other); 248 } 249 250 // Multiply or divide two quantities 251 // ditto 252 auto opBinary(string op, Q)(Q other) const 253 if ((isQVariant!Q || isQuantity!Q) && (op == "*" || op == "/" || op == "%")) 254 { 255 checkValueType!(Q.valueType); 256 return QVariant.make(mixin("(_value" ~ op ~ "other.rawValue)"), 257 binop!op(dimensions, other.dimensions)); 258 } 259 260 // ditto 261 auto opBinaryRight(string op, Q)(Q other) const 262 if ((isQVariant!Q || isQuantity!Q) && (op == "*" || op == "/" || op == "%")) 263 { 264 return this * other; 265 } 266 267 // Multiply or divide a quantity by a number 268 // ditto 269 auto opBinary(string op, T)(T other) const 270 if (isNumeric!T && (op == "*" || op == "/" || op == "%")) 271 { 272 checkValueType!T; 273 return QVariant.make(mixin("_value" ~ op ~ "other"), dimensions); 274 } 275 276 // ditto 277 auto opBinaryRight(string op, T)(T other) const 278 if (isNumeric!T && op == "*") 279 { 280 checkValueType!T; 281 return this * other; 282 } 283 284 // ditto 285 auto opBinaryRight(string op, T)(T other) const 286 if (isNumeric!T && (op == "/" || op == "%")) 287 { 288 checkValueType!T; 289 return QVariant.make(mixin("other" ~ op ~ "_value"), invert(dimensions)); 290 } 291 292 // ditto 293 auto opBinary(string op, T)(T power) const 294 if (op == "^^") 295 { 296 if (__ctfe) 297 assert(false, "QVariant operator ^^ is not supported at compile-time"); 298 299 checkValueType!T; 300 return QVariant.make(_value^^power, pow(dimensions, power)); 301 } 302 303 // Add/sub assign with a quantity that shares the same dimensions 304 // ditto 305 void opOpAssign(string op, Q)(Q other) 306 if ((isQVariant!Q || isQuantity!Q) && (op == "+" || op == "-")) 307 { 308 checkDim(other.dimensions); 309 checkValueType!(Q.valueType); 310 mixin("_value " ~ op ~ "= other.rawValue;"); 311 } 312 313 // Add/sub assign a number to a dimensionless quantity 314 // ditto 315 void opOpAssign(string op, T)(T other) 316 if (isNumeric!T && (op == "+" || op == "-")) 317 { 318 checkDim(Dimensions.init); 319 checkValueType!T; 320 mixin("_value " ~ op ~ "= other;"); 321 } 322 323 // Mul/div assign with a dimensionless quantity 324 // ditto 325 void opOpAssign(string op, Q)(Q other) 326 if ((isQVariant!Q || isQuantity!Q) && (op == "*" || op == "/" || op == "%")) 327 { 328 checkValueType!(Q.valueType); 329 mixin("_value" ~ op ~ "= other.rawValue;"); 330 dimensions = binop!op(dimensions, other.dimensions); 331 } 332 333 // Mul/div assign with a number 334 // ditto 335 void opOpAssign(string op, T)(T other) 336 if (isNumeric!T && (op == "*" || op == "/" || op == "%")) 337 { 338 checkValueType!T; 339 mixin("_value" ~ op ~ "= other;"); 340 } 341 342 // Exact equality between quantities 343 // ditto 344 bool opEquals(Q)(Q other) const 345 if (isQVariant!Q || isQuantity!Q) 346 { 347 checkDim(other.dimensions); 348 return _value == other.rawValue; 349 } 350 351 // Exact equality between a dimensionless quantity and a number 352 // ditto 353 bool opEquals(T)(T other) const 354 if (isNumeric!T) 355 { 356 checkDim(Dimensions.init); 357 return _value == other; 358 } 359 360 // Comparison between two quantities 361 // ditto 362 int opCmp(Q)(Q other) const 363 if (isQVariant!Q || isQuantity!Q) 364 { 365 checkDim(other.dimensions); 366 if (_value == other.rawValue) 367 return 0; 368 if (_value < other.rawValue) 369 return -1; 370 return 1; 371 } 372 373 // Comparison between a dimensionless quantity and a number 374 // ditto 375 int opCmp(T)(T other) const 376 if (isNumeric!T) 377 { 378 checkDim(Dimensions.init); 379 if (_value < other) 380 return -1; 381 if (_value > other) 382 return 1; 383 return 0; 384 } 385 386 // String formatting function 387 void toString(scope void delegate(const(char)[]) sink, FormatSpec!char fmt) const 388 { 389 sink.formatValue(_value, fmt); 390 sink(" "); 391 sink(dimensions.toString); 392 } 393 } 394 395 /// Converts a Quantity to an equivalent QVariant 396 auto qVariant(Q)(Q quantity) 397 { 398 return QVariant!(Q.valueType).make(quantity.rawValue, quantity.dimensions); 399 } 400 /// 401 pure @safe unittest 402 { 403 import quantities.si : meter, second; 404 405 auto speed = 42 * meter/second; 406 auto qspeed = speed.qVariant; 407 assert(qspeed.value(meter/second) == 42); 408 } 409 410 // Tests whether T is a quantity type 411 template isQVariant(T) 412 { 413 alias U = Unqual!T; 414 static if (is(U == QVariant!X, X...)) 415 enum isQVariant = true; 416 else 417 enum isQVariant = false; 418 } 419 420 pure @safe unittest // QVariant constructor 421 { 422 import quantities.si : minute, second, radian; 423 424 QVariant!double time = typeof(second)(1 * minute); 425 assert(time.value(second) == 60); 426 assert(time.baseUnit == qVariant(second)); 427 428 QVariant!double angle = 2; 429 assert(angle.value(radian) == 2); 430 } 431 432 pure @safe unittest // QVariant.alias this 433 { 434 import quantities.si : radian; 435 436 static double foo(double d) { return d; } 437 assert(foo(2 * qVariant(radian)) == 2); 438 } 439 440 pure @safe unittest // QVariant.opCast 441 { 442 import quantities.si : meter, radian, Angle; 443 444 auto angle = cast(Angle) (12 * radian.qVariant); 445 assert(angle.value(radian) == 12); 446 447 QVariant!double angle2 = 12 * radian; 448 assert(cast(double) angle2 == 12); 449 } 450 451 pure @safe unittest // QVariant.opAssign Q = Q 452 { 453 import quantities.si : meter, second, radian; 454 455 QVariant!double var = meter; 456 var = 100 * meter; 457 assert(var.value(meter) == 100); 458 459 var /= 5 * second; 460 assert(var.value(meter/second).approxEqual(20)); 461 462 var = 3.14 * radian; 463 } 464 465 pure @safe unittest // QVariant.opAssign Q = N 466 { 467 import quantities.si : radian; 468 469 QVariant!double angle = radian; 470 angle = 2; 471 assert(angle.value(radian) == 2); 472 } 473 474 pure @safe unittest // QVariant.opUnary +Q -Q ++Q --Q 475 { 476 import quantities.si : meter; 477 478 QVariant!double length = + meter.qVariant; 479 assert(length == 1 * meter); 480 QVariant!double length2 = - meter.qVariant; 481 assert(length2 == -1 * meter); 482 483 QVariant!double len = ++meter; 484 assert(len.value(meter).approxEqual(2)); 485 len = --meter; 486 assert(len.value(meter).approxEqual(0)); 487 ++len; 488 assert(len.value(meter).approxEqual(1)); 489 len++; 490 assert(len.value(meter).approxEqual(2)); 491 } 492 493 pure @safe unittest // QVariant.opBinary Q*N Q/N 494 { 495 import quantities.si : second; 496 497 QVariant!double time = second * 60; 498 assert(time.value(second) == 60); 499 QVariant!double time2 = second / 2; 500 assert(time2.value(second) == 1.0/2); 501 } 502 503 pure @safe unittest // QVariant.opBinary Q+Q Q-Q 504 { 505 import quantities.si : meter; 506 507 QVariant!double length = meter + meter; 508 assert(length.value(meter) == 2); 509 QVariant!double length2 = length - meter; 510 assert(length2.value(meter) == 1); 511 } 512 513 pure @safe unittest // QVariant.opBinary Q+N Q-N 514 { 515 import quantities.si : radian; 516 517 QVariant!double angle = radian + 1; 518 assert(angle.value(radian) == 2); 519 QVariant!double angle2 = angle - 1; 520 assert(angle2.value(radian) == 1); 521 } 522 523 pure @safe unittest // QVariant.opBinary Q*Q Q/Q 524 { 525 import quantities.si : meter, minute, second; 526 527 QVariant!double hertz = 1 / second; 528 529 QVariant!double length = meter * 5; 530 QVariant!double surface = length * length; 531 assert(surface.value(meter * meter) == 5*5); 532 QVariant!double length2 = surface / length; 533 assert(length2.value(meter) == 5); 534 535 QVariant!double x = minute / second; 536 assert(x.rawValue == 60); 537 538 QVariant!double y = minute * hertz; 539 assert(y.rawValue == 60); 540 } 541 542 pure @safe unittest // QVariant.opBinaryRight N*Q 543 { 544 import quantities.si : meter; 545 546 QVariant!double length = 100 * meter; 547 assert(length == meter * 100); 548 } 549 550 pure @safe unittest // QVariant.opBinaryRight N/Q 551 { 552 import quantities.si : meter; 553 554 QVariant!double x = 1 / (2 * meter); 555 assert(x.value(1/meter) == 1.0/2); 556 } 557 558 pure @safe unittest // QVariant.opBinary Q%Q Q%N N%Q 559 { 560 import quantities.si : meter; 561 562 QVariant!double x = 258.1 * meter; 563 QVariant!double y1 = x % (50 * meter); 564 assert((cast(double) y1).approxEqual(8.1)); 565 QVariant!double y2 = x % 50; 566 assert(y2.value(meter).approxEqual(8.1)); 567 } 568 569 pure @safe unittest // QVariant.opBinary Q^^N 570 { 571 import quantities.si : meter; 572 import quantities.si : cubic; 573 574 QVariant!double x = 2 * meter; 575 assert((x^^3).value(cubic(meter)).approxEqual(8)); 576 } 577 578 pure @safe unittest // QVariant.opOpAssign Q+=Q Q-=Q 579 { 580 import quantities.si : second; 581 582 QVariant!double time = 10 * second; 583 time += 50 * second; 584 assert(time.value(second).approxEqual(60)); 585 time -= 40 * second; 586 assert(time.value(second).approxEqual(20)); 587 } 588 589 pure @safe unittest // QVariant.opOpAssign Q*=N Q/=N Q%=N 590 { 591 import quantities.si : second; 592 593 QVariant!double time = 20 * second; 594 time *= 2; 595 assert(time.value(second).approxEqual(40)); 596 time /= 4; 597 assert(time.value(second).approxEqual(10)); 598 time %= 3; 599 assert(time.value(second).approxEqual(1)); 600 } 601 602 pure @safe unittest // QVariant.opEquals 603 { 604 import quantities.si : meter, minute, second, radian; 605 606 assert(qVariant(1 * minute) == qVariant(60 * second)); 607 assert(qVariant((1 / second) * meter) == qVariant(meter / second)); 608 assert(radian.qVariant == 1); 609 } 610 611 pure @safe unittest // QVariant.opCmp 612 { 613 import quantities.si : minute, second; 614 615 QVariant!double hour = 60 * minute; 616 assert(second.qVariant < minute.qVariant); 617 assert(minute.qVariant <= minute.qVariant); 618 assert(hour > minute); 619 assert(hour >= hour); 620 } 621 622 pure @safe unittest // Quantity.opCmp 623 { 624 import quantities.si : radian; 625 626 QVariant!double angle = 2 * radian; 627 assert(angle < 4); 628 assert(angle <= 2); 629 assert(angle > 1); 630 assert(angle >= 2); 631 } 632 633 unittest // Quantity.toString 634 { 635 import quantities.si : meter; 636 import std.conv : text; 637 638 QVariant!double length = 12 * meter; 639 assert(length.text == "12 [L]", length.text); 640 } 641 642 pure @safe unittest // Exceptions for incompatible dimensions 643 { 644 import quantities.si : meter, second; 645 import std.exception; 646 647 QVariant!double m = meter; 648 assertThrown!DimensionException(m.value(second)); 649 assertThrown!DimensionException(m + second); 650 assertThrown!DimensionException(m - second); 651 assertThrown!DimensionException(m + 1); 652 assertThrown!DimensionException(m - 1); 653 assertThrown!DimensionException(1 + m); 654 assertThrown!DimensionException(1 - m); 655 assertThrown!DimensionException(m += second); 656 assertThrown!DimensionException(m -= second); 657 assertThrown!DimensionException(m += 1); 658 assertThrown!DimensionException(m -= 1); 659 assertThrown!DimensionException(m == 1); 660 assertThrown!DimensionException(m == second); 661 assertThrown!DimensionException(m < second); 662 assertThrown!DimensionException(m < 1); 663 } 664 665 pure @safe unittest // Compile-time 666 { 667 import quantities.si : meter, second, radian, cubic; 668 669 enum length = 100 * meter.qVariant; 670 enum time = 5 * second.qVariant; 671 enum speed = length / time; 672 enum val = speed.value(meter/second); 673 static assert(val.approxEqual(20)); 674 675 version (none) 676 { 677 enum volume = length^^3; 678 static assert(volume.value(cubic(meter)).approxEqual(1e6)); 679 } 680 }