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