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