1 package org.djunits.quantity.def;
2
3 import java.util.Locale;
4 import java.util.Objects;
5
6 import org.djunits.formatter.QuantityFormat;
7 import org.djunits.formatter.QuantityFormatter;
8 import org.djunits.quantity.Area;
9 import org.djunits.quantity.Length;
10 import org.djunits.quantity.SIQuantity;
11 import org.djunits.quantity.Speed;
12 import org.djunits.unit.UnitInterface;
13 import org.djunits.unit.Unitless;
14 import org.djunits.unit.Units;
15 import org.djunits.unit.si.SIUnit;
16 import org.djunits.value.Additive;
17 import org.djunits.value.Scalable;
18 import org.djunits.value.Value;
19 import org.djutils.base.NumberParser;
20 import org.djutils.exceptions.Throw;
21
22 /**
23 * Quantity is an abstract class that stores the basic information about a quantity. A physical quantity can be expressed as a
24 * value, which is the combination of a numerical value and a unit of measurement. The type of physical quantity is encoded in
25 * the class ({@link Length}, {@link Speed}, {@link Area}, etc.) with its associated (base) unit of measurement, whereas the
26 * numerical value is stored in the si field. Additionally, each quantity has a displayUnit that gives the preference for the
27 * (scaled) display of the quantity, e.g., in a toString() method.
28 * <p>
29 * Copyright (c) 2025-2026 Delft University of Technology, Jaffalaan 5, 2628 BX Delft, the Netherlands. All rights reserved. See
30 * for project information <a href="https://djunits.org" target="_blank">https://djunits.org</a>. The DJUNITS project is
31 * distributed under a <a href="https://djunits.org/docs/license.html" target="_blank">three-clause BSD-style license</a>.
32 * @author Alexander Verbraeck
33 * @param <Q> the quantity type
34 */
35 public abstract class Quantity<Q extends Quantity<Q>> extends Number
36 implements Value<Q>, Comparable<Q>, Additive<Q>, Scalable<Q>
37 {
38 /** */
39 private static final long serialVersionUID = 600L;
40
41 /** The si value. */
42 @SuppressWarnings("checkstyle:visibilitymodifier")
43 public final double si;
44
45 /** The display unit. */
46 private final UnitInterface<Q> displayUnit;
47
48 /**
49 * Instantiate a quantity with an SI or base value and a display unit.
50 * @param siValue the quantity value expressed in the SI or base unit
51 * @param displayUnit the display unit to use
52 * @param useSi use SI value when true, value in unit when false
53 */
54 public Quantity(final double siValue, final UnitInterface<Q> displayUnit, final boolean useSi)
55 {
56 Throw.whenNull(displayUnit, "displayUnit");
57 this.si = useSi ? siValue : displayUnit.toBaseValue(siValue);
58 this.displayUnit = displayUnit;
59 }
60
61 /**********************************************************************************/
62 /******************************* UNIT-RELATED METHODS *****************************/
63 /**********************************************************************************/
64
65 @Override
66 public UnitInterface<Q> getDisplayUnit()
67 {
68 return this.displayUnit;
69 }
70
71 /**
72 * Retrieve the value in the current display unit.
73 * @return the value in the current display unit
74 */
75 public double getInUnit()
76 {
77 return getDisplayUnit().getScale().fromIdentityScale(si());
78 }
79
80 /**
81 * Retrieve the value converted into some specified unit.
82 * @param targetUnit the unit to convert the value into
83 * @return the double value of this quantity expressed in the target unit
84 */
85 public double getInUnit(final UnitInterface<Q> targetUnit)
86 {
87 return targetUnit.getScale().fromIdentityScale(si());
88 }
89
90 /**
91 * Return the "pretty" name of the quantity.
92 * @return the "pretty" name of the quantity
93 */
94 public String getName()
95 {
96 String name = Units.localizedQuantityName(Locale.getDefault(), getClass().getSimpleName());
97 final StringBuilder sb = new StringBuilder(name.length() + 8);
98 sb.append(name.charAt(0)); // keep first character exactly as-is
99 for (int i = 1; i < name.length(); i++)
100 {
101 final char c = name.charAt(i);
102 if (Character.isUpperCase(c))
103 {
104 if (sb.length() > 0 && sb.charAt(sb.length() - 1) != ' ')
105 {
106 sb.append(' ');
107 }
108 sb.append(Character.toLowerCase(c));
109 }
110 else
111 {
112 sb.append(c);
113 }
114 }
115 return sb.toString();
116 }
117
118 /**********************************************************************************/
119 /******************************** SI-RELATED METHODS ******************************/
120 /**********************************************************************************/
121
122 /**
123 * Return the SI unit of this quantity.
124 * @return the SI unit of this quantity
125 */
126 public SIUnit siUnit()
127 {
128 return getDisplayUnit().siUnit();
129 }
130
131 /**
132 * Return the SI value of the quantity.
133 * @return the SI value of the quantity
134 */
135 public double si()
136 {
137 return this.si;
138 }
139
140 /**
141 * Instantiate a quantity with an SI or base value, and a display unit.
142 * @param siValue the value expressed in the base (SI) unit
143 * @param displayUnit the display unit to use for the quantity
144 * @return a quantity with the given SI or base value, and the given display unit
145 */
146 @SuppressWarnings("checkstyle:hiddenfield")
147 public abstract Q instantiateSi(double siValue, UnitInterface<Q> displayUnit);
148
149 /**
150 * Instantiate a quantity with an SI or base value, with the SI or base display unit.
151 * @param siValue the value expressed in the base (SI) unit
152 * @return a quantity with the given SI or base value, with the SI or base display unit
153 */
154 public Q instantiateSi(final double siValue)
155 {
156 return instantiateSi(siValue, getDisplayUnit().getBaseUnit());
157 }
158
159 /**
160 * Instantiate a quantity with a value and a unit.
161 * @param valueInUnit the double value, expressed in the unit
162 * @param unit the unit
163 * @return a quantity with the given value and display unit
164 */
165 public Q instantiate(final double valueInUnit, final UnitInterface<Q> unit)
166 {
167 return instantiateSi(unit.toBaseValue(valueInUnit), unit);
168 }
169
170 /**********************************************************************************/
171 /********************************* NUMBER METHODS *********************************/
172 /**********************************************************************************/
173
174 @Override
175 public double doubleValue()
176 {
177 return si();
178 }
179
180 @Override
181 public int intValue()
182 {
183 return (int) Math.round(si());
184 }
185
186 @Override
187 public long longValue()
188 {
189 return Math.round(si());
190 }
191
192 @Override
193 public float floatValue()
194 {
195 return (float) si();
196 }
197
198 /**
199 * Test if this Quantity is less than another Quantity.
200 * @param other the right hand side operand of the comparison
201 * @return true if this is less than o; false otherwise
202 */
203 public boolean lt(final Q other)
204 {
205 return si() < other.si();
206 }
207
208 /**
209 * Test if this Quantity is less than or equal to another Quantity.
210 * @param other the right hand side operand of the comparison
211 * @return true if this is less than or equal to o; false otherwise
212 */
213 public boolean le(final Q other)
214 {
215 return si() <= other.si();
216 }
217
218 /**
219 * Test if this Quantity is greater than another Quantity.
220 * @param other the right hand side operand of the comparison
221 * @return true if this is greater than o; false otherwise
222 */
223 public boolean gt(final Q other)
224 {
225 return si() > other.si();
226 }
227
228 /**
229 * Test if this Quantity is greater than or equal to another Quantity.
230 * @param other the right hand side operand of the comparison
231 * @return true if this is greater than or equal to o; false otherwise
232 */
233 public boolean ge(final Q other)
234 {
235 return si() >= other.si();
236 }
237
238 /**
239 * Test if this Quantity is equal to another Quantity.
240 * @param other the right hand side operand of the comparison
241 * @return true if this is equal to o; false otherwise
242 */
243 public boolean eq(final Q other)
244 {
245 return si() == other.si();
246 }
247
248 /**
249 * Test if this Quantity is not equal to another Quantity.
250 * @param other the right hand side operand of the comparison
251 * @return true if this is not equal to o; false otherwise
252 */
253 public boolean ne(final Q other)
254 {
255 return si() != other.si();
256 }
257
258 /**
259 * Test if this Quantity is less than 0.0.
260 * @return true if this is less than 0.0; false if this is not less than 0.0
261 */
262 public boolean lt0()
263 {
264 return si() < 0.0;
265 }
266
267 /**
268 * Test if this Quantity is less than or equal to 0.0.
269 * @return true if this is less than or equal to 0.0; false if this is not less than or equal to 0.0
270 */
271 public boolean le0()
272 {
273 return si() <= 0.0;
274 }
275
276 /**
277 * Test if this Quantity is greater than 0.0.
278 * @return true if this is greater than 0.0; false if this is not greater than 0.0
279 */
280 public boolean gt0()
281 {
282 return si() > 0.0;
283 }
284
285 /**
286 * Test if this Quantity is greater than or equal to 0.0.
287 * @return true if this is greater than or equal to 0.0; false if this is not greater than or equal to 0.0
288 */
289 public boolean ge0()
290 {
291 return si() >= 0.0;
292 }
293
294 /**
295 * Test if this Quantity is equal to 0.0.
296 * @return true if this is equal to 0.0; false if this is not equal to 0.0
297 */
298 public boolean eq0()
299 {
300 return si() == 0.0;
301 }
302
303 /**
304 * Test if this Quantity is not equal to 0.0.
305 * @return true if this is not equal to 0.0; false if this is equal to 0.0
306 */
307 public boolean ne0()
308 {
309 return si() != 0.0;
310 }
311
312 @Override
313 public int compareTo(final Q other)
314 {
315 return Double.compare(this.si(), other.si());
316 }
317
318 @Override
319 public int hashCode()
320 {
321 return Objects.hash(this.displayUnit, this.si);
322 }
323
324 @SuppressWarnings("checkstyle:needbraces")
325 @Override
326 public boolean equals(final Object obj)
327 {
328 if (this == obj)
329 return true;
330 if (obj == null)
331 return false;
332 if (getClass() != obj.getClass())
333 return false;
334 Quantity<?> other = (Quantity<?>) obj;
335 return Objects.equals(this.displayUnit, other.displayUnit)
336 && Double.doubleToLongBits(this.si) == Double.doubleToLongBits(other.si);
337 }
338
339 /**********************************************************************************/
340 /********************************** PARSING METHODS *******************************/
341 /**********************************************************************************/
342
343 /**
344 * Returns a quantity for the textual representation of a value with a unit. The String representation that can be parsed is
345 * the double value in the unit, followed by a localized or English abbreviation of the unit. Spaces are allowed, but not
346 * required, between the value and the unit.
347 * @param text the textual representation to parse into the quantity
348 * @param example an example instance to deliver
349 * @return the quantity representation of the value with its unit
350 * @throws IllegalArgumentException when the text cannot be parsed
351 * @throws NullPointerException when the text argument is null
352 * @param <Q> the quantity type
353 */
354 @SuppressWarnings("unchecked")
355 public static <Q extends Quantity<Q>> Q valueOf(final String text, final Q example)
356 {
357 Throw.whenNull(example, "Error parsing Quantity: example is null");
358 String quantityClass = example.getClass().getSimpleName();
359 Throw.whenNull(text, "Error parsing Quantity: text to parse is null");
360 Throw.when(text.length() == 0, IllegalArgumentException.class, "Error parsing %s: empty text to parse", quantityClass);
361 try
362 {
363 NumberParser numberParser = new NumberParser().lenient().trailing();
364 double d = numberParser.parseDouble(text);
365
366 // Everything after the parsed number is considered the unit token.
367 String unitStringRaw = text.substring(numberParser.getTrailingPosition());
368 String unitString = unitStringRaw.trim();
369
370 Class<? extends UnitInterface<Q>> unitClass = (Class<UnitInterface<Q>>) example.getDisplayUnit().getClass();
371
372 UnitInterface<Q> unit = null;
373 if (unitString.isEmpty())
374 {
375 // Special-case: DIMENSIONLESS can omit the unit entirely ("" or all whitespace).
376 if (Unitless.class.isAssignableFrom(unitClass))
377 {
378 unit = (UnitInterface<Q>) Unitless.BASE;
379 }
380 else
381 {
382 throw new IllegalArgumentException(
383 String.format("Error parsing %s: missing unit in '%s'", quantityClass, text));
384 }
385 }
386 else
387 {
388 // Normal path: resolve the unit string for the quantity's unit class.
389 UnitInterface<Q> resolved = (UnitInterface<Q>) Units.resolve(unitClass, unitString);
390 Throw.when(resolved == null, IllegalArgumentException.class, "Unit '%s' not found for quantity %s", unitString,
391 quantityClass);
392 unit = resolved;
393 }
394
395 return example.instantiate(d, unit);
396 }
397 catch (Exception exception)
398 {
399 throw new IllegalArgumentException("Error parsing " + quantityClass + " from " + text + " using Locale "
400 + Locale.getDefault(Locale.Category.FORMAT), exception);
401 }
402 }
403
404 /**
405 * Returns a quantity based on a value and the textual representation of the unit, which can be localized.
406 * @param valueInUnit the value, expressed in the unit as given by unitString
407 * @param unitString the textual representation of the unit
408 * @param example an example instance to deliver
409 * @return the quantity representation of the value in its unit
410 * @throws IllegalArgumentException when the unit cannot be parsed or is incorrect
411 * @throws NullPointerException when the unitString argument is null
412 * @param <Q> the quantity type
413 */
414 public static <Q extends Quantity<Q>> Q of(final double valueInUnit, final String unitString, final Q example)
415 {
416 Throw.whenNull(example, "Error parsing Quantity: example is null");
417 String quantityClass = example.getClass().getSimpleName();
418 Throw.whenNull(unitString, "Error parsing %s: unitString is null", quantityClass);
419 Throw.when(unitString.length() == 0, IllegalArgumentException.class, "Error parsing %s: empty unitString",
420 quantityClass);
421 @SuppressWarnings("unchecked")
422 UnitInterface<Q> unit = (UnitInterface<Q>) Units.resolve(example.getDisplayUnit().getClass(), unitString);
423 Throw.when(unit == null, IllegalArgumentException.class, "Error parsing %s with unit %s", quantityClass, unitString);
424 return example.instantiate(valueInUnit, unit);
425 }
426
427 /**********************************************************************************/
428 /*************************** STRING AND FORMATTING METHODS ************************/
429 /**********************************************************************************/
430
431 /**
432 * Description of this quantity with default formatting.
433 * @return a String with the value of the quantity, with the unit attached.
434 */
435 @Override
436 public String toString()
437 {
438 return format();
439 }
440
441 /**
442 * Concise description of this quantity.
443 * @return a String with the value of the quantity, with the unit attached.
444 */
445 @Override
446 public String format()
447 {
448 return format(QuantityFormat.instance());
449 }
450
451 /**
452 * String representation of this quantity after applying the format.
453 * @param format the format to apply for the quantity
454 * @return a String representation of this quantity, formatted according to the given format
455 */
456 public String format(final QuantityFormat format)
457 {
458 return QuantityFormatter.format(this, format);
459 }
460
461 /**
462 * String representation of this quantity, expressed in the specified unit.
463 * @param targetUnit the unit into which the quantity is converted for display
464 * @return printable string with the quantity value expressed in the specified unit
465 */
466 @Override
467 public String format(final UnitInterface<Q> targetUnit)
468 {
469 return format(QuantityFormat.instance().setDisplayUnit(targetUnit));
470 }
471
472 /**********************************************************************************/
473 /********************* STATIC OPERATIONS ON MULTIPLE QUANTITIES *******************/
474 /**********************************************************************************/
475
476 /**
477 * Interpolate between two quantities. Note that the first quantities does not have to be smaller than the second.
478 * @param zero the quantity at a ratio of zero
479 * @param one the quantity at a ratio of one
480 * @param ratio the ratio between 0 and 1, inclusive
481 * @return a Quantity at the given ratio between 0 and 1
482 * @param <Q> the quantity type
483 */
484 public static <Q extends Quantity<Q>> Q interpolate(final Q zero, final Q one, final double ratio)
485 {
486 Throw.when(ratio < 0.0 || ratio > 1.0, IllegalArgumentException.class,
487 "ratio for interpolation should be between 0 and 1, but is %f", ratio);
488 return zero.instantiateSi(zero.si() * (1 - ratio) + one.si() * ratio, zero.getDisplayUnit());
489 }
490
491 /**
492 * Return the maximum value of one or more quantities.
493 * @param quantity1 the first quantity
494 * @param quantities the other quantities
495 * @return the maximum value of the quantities
496 * @param <Q> the quantity type
497 */
498 @SafeVarargs
499 public static <Q extends Quantity<Q>> Q max(final Q quantity1, final Q... quantities)
500 {
501 Q maxQ = quantity1;
502 for (Q quantity : quantities)
503 {
504 if (quantity.gt(maxQ))
505 {
506 maxQ = quantity;
507 }
508 }
509 return maxQ;
510 }
511
512 /**
513 * Return the minimum value of one or more quantities.
514 * @param quantity1 the first quantity
515 * @param quantities the other quantities
516 * @return the minimum value of more than two quantities
517 * @param <Q> the quantity type
518 */
519 @SafeVarargs
520 public static <Q extends Quantity<Q>> Q min(final Q quantity1, final Q... quantities)
521 {
522 Q minQ = quantity1;
523 for (Q quantity : quantities)
524 {
525 if (quantity.lt(minQ))
526 {
527 minQ = quantity;
528 }
529 }
530 return minQ;
531 }
532
533 /**
534 * Return the sum of one or more quantities.
535 * @param quantity1 the first quantity
536 * @param quantities the other quantities
537 * @return the sum of the quantities
538 * @param <Q> the quantity type
539 */
540 @SafeVarargs
541 public static <Q extends Quantity<Q>> Q sum(final Q quantity1, final Q... quantities)
542 {
543 double sum = quantity1.si();
544 for (Q quantity : quantities)
545 {
546 sum += quantity.si();
547 }
548 return quantity1.instantiateSi(sum, quantity1.getDisplayUnit());
549 }
550
551 /**
552 * Return the product of one or more quantities.
553 * @param quantity1 the first quantity
554 * @param quantities the other quantities
555 * @return the product of the quantities
556 */
557 @SafeVarargs
558 public static SIQuantity product(final Quantity<?> quantity1, final Quantity<?>... quantities)
559 {
560 double product = quantity1.si();
561 SIUnit unit = quantity1.siUnit();
562 for (var quantity : quantities)
563 {
564 product *= quantity.si();
565 unit = unit.plus(quantity.siUnit());
566 }
567 return new SIQuantity(product, unit);
568 }
569
570 /**
571 * Return the mean of one or more quantities.
572 * @param quantity1 the first quantity
573 * @param quantities the other quantities
574 * @return the mean of the quantities
575 * @param <Q> the quantity type
576 */
577 @SafeVarargs
578 public static <Q extends Quantity<Q>> Q mean(final Q quantity1, final Q... quantities)
579 {
580 int n = 1 + quantities.length;
581 return sum(quantity1, quantities).divideBy(n);
582 }
583
584 /***********************************************************************************/
585 /********************************* RELATIVE METHODS ********************************/
586 /***********************************************************************************/
587
588 @Override
589 public Q add(final Q increment)
590 {
591 return instantiateSi(si() + increment.si(), getDisplayUnit());
592 }
593
594 @Override
595 public Q subtract(final Q decrement)
596 {
597 return instantiateSi(si() - decrement.si(), getDisplayUnit());
598 }
599
600 @Override
601 public Q abs()
602 {
603 return instantiateSi(Math.abs(si()), getDisplayUnit());
604 }
605
606 @Override
607 public Q negate()
608 {
609 return instantiateSi(-si(), getDisplayUnit());
610 }
611
612 @Override
613 public Q scaleBy(final double factor)
614 {
615 return instantiateSi(si() * factor, getDisplayUnit());
616 }
617
618 /**
619 * Multiply this quantity with another quantity, and return a SIQuantity as the result.
620 * @param quantity the quantity to multiply with
621 * @return the multiplication of this quantity and the given quantity
622 */
623 public SIQuantity multiply(final Quantity<?> quantity)
624 {
625 SIUnit siUnit = SIUnit.add(siUnit(), quantity.siUnit());
626 return new SIQuantity(si() * quantity.si(), siUnit);
627 }
628
629 /**
630 * Divide this quantity by another quantity, and return a SIQuantity as the result.
631 * @param quantity the quantity to divide by
632 * @return the division of this quantity and the given quantity
633 */
634 public SIQuantity divide(final Quantity<?> quantity)
635 {
636 SIUnit siUnit = SIUnit.subtract(siUnit(), quantity.siUnit());
637 return new SIQuantity(si() / quantity.si(), siUnit);
638 }
639
640 /**
641 * Return the reciprocal of this quantity (1/q).
642 * @return the reciprocal of this quantity, with the correct SI units
643 */
644 public Quantity<?> reciprocal()
645 {
646 return new SIQuantity(1.0 / si(), this.siUnit().invert());
647 }
648
649 /**
650 * Return the quantity 'as' a known quantity, using a unit to express the result in. Throw a Runtime exception when the SI
651 * units of this quantity and the target quantity do not match.
652 * @param targetUnit the unit to convert the quantity to
653 * @return a quantity typed in the target quantity class
654 * @throws IllegalArgumentException when the units do not match
655 * @param <TQ> target quantity type
656 */
657 public <TQ extends Quantity<TQ>> TQ as(final UnitInterface<TQ> targetUnit) throws IllegalArgumentException
658 {
659 Throw.when(!siUnit().equals(targetUnit.siUnit()), IllegalArgumentException.class,
660 "Quantity.as(%s) called, but units do not match: %s <> %s", targetUnit, siUnit().getDisplayAbbreviation(),
661 targetUnit.siUnit().getDisplayAbbreviation());
662 return targetUnit.ofSi(si(), targetUnit);
663 }
664
665 @Override
666 public boolean isRelative()
667 {
668 return true;
669 }
670
671 }