001/* 002 * Units of Measurement Reference Implementation 003 * Copyright (c) 2005-2023, Jean-Marie Dautelle, Werner Keil, Otavio Santana. 004 * 005 * All rights reserved. 006 * 007 * Redistribution and use in source and binary forms, with or without modification, 008 * are permitted provided that the following conditions are met: 009 * 010 * 1. Redistributions of source code must retain the above copyright notice, 011 * this list of conditions and the following disclaimer. 012 * 013 * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions 014 * and the following disclaimer in the documentation and/or other materials provided with the distribution. 015 * 016 * 3. Neither the name of JSR-385, Indriya nor the names of their contributors may be used to endorse or promote products 017 * derived from this software without specific prior written permission. 018 * 019 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 020 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, 021 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 022 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 023 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 024 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 025 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED 026 * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 027 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 028 * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 029 */ 030package tech.units.indriya.quantity; 031 032import static org.apiguardian.api.API.Status.MAINTAINED; 033 034import java.io.Serializable; 035import java.util.ArrayList; 036import java.util.Arrays; 037import java.util.Collections; 038import java.util.List; 039import java.util.Objects; 040import java.util.stream.Collectors; 041 042import javax.measure.Quantity; 043import javax.measure.Quantity.Scale; 044 045import org.apiguardian.api.API; 046 047import javax.measure.Unit; 048 049import tech.units.indriya.format.SimpleQuantityFormat; 050import tech.units.indriya.function.Calculus; 051import tech.units.indriya.function.MixedRadix; 052import tech.units.indriya.internal.function.Calculator; 053import tech.units.indriya.spi.NumberSystem; 054import tech.uom.lib.common.function.QuantityConverter; 055 056/** 057 * <p> 058 * This class represents mixed-radix quantities (like "1 hour, 5 min, 30 sec" or "6 ft, 3 in"). 059 * </p> 060 * 061 * @param <Q> 062 * The type of the quantity. 063 * 064 * @author <a href="mailto:werner@units.tech">Werner Keil</a> 065 * @author Andi Huber 066 * @version 2.4, June 5, 2023 067 * @see <a href="https://www.wolfram.com/language/11/units-and-dates/mixed-quantities.html">Wolfram Language: Mixed Quantities</a> 068 * @see <a href="https://en.wikipedia.org/wiki/Fraction#Mixed_numbers">Wikipedia: Mixed Numbers</a> 069 * @see MixedRadix 070 * @since 2.1.2 071 */ 072@API(status=MAINTAINED) 073public final class MixedQuantity<Q extends Quantity<Q>> implements QuantityConverter<Q>, Serializable { 074 /** 075 * 076 */ 077 private static final long serialVersionUID = 5863961588282485676L; 078 079 private final List<Quantity<Q>> quantityList; 080 private final Object[] quantityArray; 081 private final List<Unit<Q>> unitList; 082 private Unit<Q> leastSignificantUnit; 083 private Scale commonScale; 084 085 // MixedRadix is optimized for best accuracy, when calculating the radix sum, so we try to use it if possible 086 private MixedRadix<Q> mixedRadixIfPossible; 087 088 /** 089 * @param quantities - the list of quantities to construct this MixedQuantity. 090 */ 091 protected MixedQuantity(final List<Quantity<Q>> quantities) { 092 final List<Unit<Q>> unitList = new ArrayList<>(); 093 094 for (Quantity<Q> q : quantities) { 095 final Unit<Q> unit = q.getUnit(); 096 unitList.add(unit); 097 commonScale = q.getScale(); 098 099 // keep track of the least significant unit, thats the one that should 'drive' arithmetic operations 100 101 if(leastSignificantUnit==null) { 102 leastSignificantUnit = unit; 103 } else { 104 final NumberSystem ns = Calculus.currentNumberSystem(); 105 final Number leastSignificantToCurrentFactor = leastSignificantUnit.getConverterTo(unit).convert(1); 106 final boolean isLessSignificant = ns.isLessThanOne(ns.abs(leastSignificantToCurrentFactor)); 107 if(isLessSignificant) { 108 leastSignificantUnit = unit; 109 } 110 } 111 } 112 113 this.quantityList = Collections.unmodifiableList(new ArrayList<>(quantities)); 114 this.quantityArray = quantities.toArray(); 115 this.unitList = Collections.unmodifiableList(unitList); 116 117 try { 118 // - will throw if units are not in decreasing order of significance 119 mixedRadixIfPossible = MixedRadix.of(getUnits()); 120 } catch (Exception e) { 121 mixedRadixIfPossible = null; 122 } 123 } 124 125 /** 126 * @param <Q> 127 * @param quantities 128 * @return a {@code MixedQuantity} with the specified {@code quantities} 129 * @throws IllegalArgumentException 130 * if given {@code quantities} is {@code null} or empty 131 * or contains any <code>null</code> values 132 * or contains quantities of mixed scale 133 */ 134 @SafeVarargs 135 public static <Q extends Quantity<Q>> MixedQuantity<Q> of(Quantity<Q>... quantities) { 136 guardAgainstIllegalQuantitiesArgument(quantities); 137 return new MixedQuantity<>(Arrays.asList(quantities)); 138 } 139 140 /** 141 * @param <Q> 142 * @param quantities 143 * @return a {@code MixedQuantity} with the specified {@code quantities} 144 * @throws IllegalArgumentException 145 * if given {@code quantities} is {@code null} or empty 146 * or contains any <code>null</code> values 147 * or contains quantities of mixed scale 148 */ 149 @SafeVarargs 150 public static <Q extends Quantity<Q>> MixedQuantity<Q> fromArray(Quantity<Q>... quantities) { 151 guardAgainstIllegalQuantitiesArgument(quantities); 152 return new MixedQuantity<>(Arrays.asList(quantities)); 153 } 154 155 /** 156 * @param <Q> 157 * @param quantities 158 * @return a {@code MixedQuantity} with the specified {@code quantities} 159 * @throws IllegalArgumentException 160 * if given {@code quantities} is {@code null} or empty 161 * or contains any <code>null</code> values 162 * or contains quantities of mixed scale 163 * 164 */ 165 public static <Q extends Quantity<Q>> MixedQuantity<Q> of(List<Quantity<Q>> quantities) { 166 guardAgainstIllegalQuantitiesArgument(quantities); 167 return new MixedQuantity<>(quantities); 168 } 169 170 /** 171 * Gets the list of units in this MixedQuantity. 172 * <p> 173 * This list can be used in conjunction with {@link #getQuantities()} to access the entire quantity. 174 * 175 * @return a list containing the units, not null 176 */ 177 public List<Unit<Q>> getUnits() { 178 return unitList; 179 } 180 181 /** 182 * Gets quantities in this MixedQuantity. 183 * 184 * @return a list containing the quantities, not null 185 */ 186 public List<Quantity<Q>> getQuantities() { 187 return quantityList; 188 } 189 190 /* 191 * (non-Javadoc) 192 * 193 * @see java.lang.Object#toString() 194 */ 195 @Override 196 public String toString() { 197 return SimpleQuantityFormat.getInstance().format(this); 198 } 199 200 /** 201 * Returns the <b>sum</b> of all quantity values in this MixedQuantity converted into another (compatible) unit. 202 * @param unit 203 * the {@code Unit unit} in which the returned quantity is stated. 204 * @return the sum of all quantities in this MixedQuantity or a new quantity stated in the specified unit. 205 * @throws ArithmeticException 206 * if the result is inexact and the quotient has a non-terminating decimal expansion. 207 */ 208 @Override 209 public Quantity<Q> to(Unit<Q> unit) { 210 211 // MixedRadix is optimized for best accuracy, when calculating the radix sum, so we use it if possible 212 if(mixedRadixIfPossible!=null) { 213 Number[] values = getQuantities() 214 .stream() 215 .map(Quantity::getValue) 216 .collect(Collectors.toList()) 217 .toArray(new Number[0]); 218 219 return mixedRadixIfPossible.createQuantity(values).to(unit); 220 } 221 222 // fallback 223 224 final Calculator calc = Calculator.of(0); 225 226 for (Quantity<Q> q : quantityList) { 227 228 final Number termInLeastSignificantUnits = 229 q.getUnit().getConverterTo(leastSignificantUnit).convert(q.getValue()); 230 231 calc.add(termInLeastSignificantUnits); 232 } 233 234 final Number sumInLeastSignificantUnits = calc.peek(); 235 236 return Quantities.getQuantity(sumInLeastSignificantUnits, leastSignificantUnit, commonScale).to(unit); 237 } 238 239 /** 240 * Indicates if this mixed quantity is considered equal to the specified object (both are mixed units with same composing units in the same order). 241 * 242 * @param obj 243 * the object to compare for equality. 244 * @return <code>true</code> if <code>this</code> and <code>obj</code> are considered equal; <code>false</code>otherwise. 245 */ 246 public boolean equals(Object obj) { 247 if (this == obj) { 248 return true; 249 } 250 if (obj instanceof MixedQuantity) { 251 MixedQuantity<?> c = (MixedQuantity<?>) obj; 252 return Arrays.equals(quantityArray, c.quantityArray); 253 } else { 254 return false; 255 } 256 } 257 258 @Override 259 public int hashCode() { 260 return Objects.hash(quantityArray); 261 } 262 263 // -- IMPLEMENTATION DETAILS 264 265 private static void guardAgainstIllegalQuantitiesArgument(Quantity<?>[] quantities) { 266 if (quantities == null || quantities.length < 1) { 267 throw new IllegalArgumentException("At least one quantity is required."); 268 } 269 Scale firstScale = null; 270 for(Quantity<?> q : quantities) { 271 if(q==null) { 272 throw new IllegalArgumentException("Quantities must not contain null."); 273 } 274 if(firstScale==null) { 275 firstScale = q.getScale(); 276 if(firstScale==null) { 277 throw new IllegalArgumentException("Quantities must have a scale."); 278 } 279 } 280 if (!firstScale.equals(q.getScale())) { 281 throw new IllegalArgumentException("Quantities do not have the same scale."); 282 } 283 } 284 } 285 286 // almost a duplicate of the above, this is to keep heap pollution at a minimum 287 private static <Q extends Quantity<Q>> void guardAgainstIllegalQuantitiesArgument(List<Quantity<Q>> quantities) { 288 if (quantities == null || quantities.size() < 1) { 289 throw new IllegalArgumentException("At least one quantity is required."); 290 } 291 Scale firstScale = null; 292 for(Quantity<Q> q : quantities) { 293 if(q==null) { 294 throw new IllegalArgumentException("Quantities must not contain null."); 295 } 296 if(firstScale==null) { 297 firstScale = q.getScale(); 298 if(firstScale==null) { 299 throw new IllegalArgumentException("Quantities must have a scale."); 300 } 301 } 302 if (!firstScale.equals(q.getScale())) { 303 throw new IllegalArgumentException("Quantities do not have the same scale."); 304 } 305 } 306 } 307}