View Javadoc
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 }