001/* 002 * Units of Measurement Reference Implementation 003 * Copyright (c) 2005-2021, 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.STABLE; 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.3, Feb 2, 2022 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=STABLE) 073public class MixedQuantity<Q extends Quantity<Q>> implements QuantityConverter<Q>, Serializable { 074 // TODO could it be final? 075 /** 076 * 077 */ 078 private static final long serialVersionUID = 5863961588282485676L; 079 080 private final List<Quantity<Q>> quantityList; 081 private final Object[] quantityArray; 082 private final List<Unit<Q>> unitList; 083 private Unit<Q> leastSignificantUnit; 084 private Scale commonScale; 085 086 // MixedRadix is optimized for best accuracy, when calculating the radix sum, so we try to use it if possible 087 private MixedRadix<Q> mixedRadixIfPossible; 088 089 /** 090 * @param quantities - the list of quantities to construct this MixedQuantity. 091 */ 092 protected MixedQuantity(final List<Quantity<Q>> quantities) { 093 094 final List<Unit<Q>> unitList = new ArrayList<>(); 095 096 for (Quantity<Q> q : quantities) { 097 final Unit<Q> unit = q.getUnit(); 098 unitList.add(unit); 099 commonScale = q.getScale(); 100 101 // keep track of the least significant unit, thats the one that should 'drive' arithmetic operations 102 103 if(leastSignificantUnit==null) { 104 leastSignificantUnit = unit; 105 } else { 106 final NumberSystem ns = Calculus.currentNumberSystem(); 107 final Number leastSignificantToCurrentFactor = leastSignificantUnit.getConverterTo(unit).convert(1); 108 final boolean isLessSignificant = ns.isLessThanOne(ns.abs(leastSignificantToCurrentFactor)); 109 if(isLessSignificant) { 110 leastSignificantUnit = unit; 111 } 112 } 113 } 114 115 this.quantityList = Collections.unmodifiableList(new ArrayList<>(quantities)); 116 this.quantityArray = quantities.toArray(); 117 118 this.unitList = Collections.unmodifiableList(unitList); 119 120 try { 121 // - will throw if units are not in decreasing order of significance 122 mixedRadixIfPossible = MixedRadix.of(getUnits()); 123 } catch (Exception e) { 124 mixedRadixIfPossible = null; 125 } 126 } 127 128 /** 129 * @param <Q> 130 * @param quantities 131 * @return a {@code MixedQuantity} with the specified {@code quantities} 132 * @throws IllegalArgumentException 133 * if given {@code quantities} is {@code null} or empty 134 * or contains any <code>null</code> values 135 * or contains quantities of mixed scale 136 */ 137 @SafeVarargs 138 public static <Q extends Quantity<Q>> MixedQuantity<Q> of(Quantity<Q>... quantities) { 139 guardAgainstIllegalQuantitiesArgument(quantities); 140 return new MixedQuantity<>(Arrays.asList(quantities)); 141 } 142 143 /** 144 * @param <Q> 145 * @param quantities 146 * @return a {@code MixedQuantity} with the specified {@code quantities} 147 * @throws IllegalArgumentException 148 * if given {@code quantities} is {@code null} or empty 149 * or contains any <code>null</code> values 150 * or contains quantities of mixed scale 151 */ 152 @SafeVarargs 153 public static <Q extends Quantity<Q>> MixedQuantity<Q> fromArray(Quantity<Q>... quantities) { 154 guardAgainstIllegalQuantitiesArgument(quantities); 155 return new MixedQuantity<>(Arrays.asList(quantities)); 156 } 157 158 /** 159 * @param <Q> 160 * @param quantities 161 * @return a {@code MixedQuantity} with the specified {@code quantities} 162 * @throws IllegalArgumentException 163 * if given {@code quantities} is {@code null} or empty 164 * or contains any <code>null</code> values 165 * or contains quantities of mixed scale 166 * 167 */ 168 public static <Q extends Quantity<Q>> MixedQuantity<Q> of(List<Quantity<Q>> quantities) { 169 guardAgainstIllegalQuantitiesArgument(quantities); 170 return new MixedQuantity<>(quantities); 171 } 172 173 /** 174 * Gets the list of units in this MixedQuantity. 175 * <p> 176 * This list can be used in conjunction with {@link #getQuantities()} to access the entire quantity. 177 * 178 * @return a list containing the units, not null 179 */ 180 public List<Unit<Q>> getUnits() { 181 return unitList; 182 } 183 184 /** 185 * Gets quantities in this MixedQuantity. 186 * 187 * @return a list containing the quantities, not null 188 */ 189 public List<Quantity<Q>> getQuantities() { 190 return quantityList; 191 } 192 193 /* 194 * (non-Javadoc) 195 * 196 * @see java.lang.Object#toString() 197 */ 198 @Override 199 public String toString() { 200 return SimpleQuantityFormat.getInstance().format(this); 201 } 202 203 /** 204 * Returns the <b>sum</b> of all quantity values in this MixedQuantity converted into another (compatible) unit. 205 * @param unit 206 * the {@code Unit unit} in which the returned quantity is stated. 207 * @return the sum of all quantities in this MixedQuantity or a new quantity stated in the specified unit. 208 * @throws ArithmeticException 209 * if the result is inexact and the quotient has a non-terminating decimal expansion. 210 */ 211 @Override 212 public Quantity<Q> to(Unit<Q> unit) { 213 214 // MixedRadix is optimized for best accuracy, when calculating the radix sum, so we use it if possible 215 if(mixedRadixIfPossible!=null) { 216 Number[] values = getQuantities() 217 .stream() 218 .map(Quantity::getValue) 219 .collect(Collectors.toList()) 220 .toArray(new Number[0]); 221 222 return mixedRadixIfPossible.createQuantity(values).to(unit); 223 } 224 225 // fallback 226 227 final Calculator calc = Calculator.of(0); 228 229 for (Quantity<Q> q : quantityList) { 230 231 final Number termInLeastSignificantUnits = 232 q.getUnit().getConverterTo(leastSignificantUnit).convert(q.getValue()); 233 234 calc.add(termInLeastSignificantUnits); 235 } 236 237 final Number sumInLeastSignificantUnits = calc.peek(); 238 239 return Quantities.getQuantity(sumInLeastSignificantUnits, leastSignificantUnit, commonScale).to(unit); 240 } 241 242 /** 243 * Indicates if this mixed quantity is considered equal to the specified object (both are mixed units with same composing units in the same order). 244 * 245 * @param obj 246 * the object to compare for equality. 247 * @return <code>true</code> if <code>this</code> and <code>obj</code> are considered equal; <code>false</code>otherwise. 248 */ 249 public boolean equals(Object obj) { 250 if (this == obj) { 251 return true; 252 } 253 if (obj instanceof MixedQuantity) { 254 MixedQuantity<?> c = (MixedQuantity<?>) obj; 255 return Arrays.equals(quantityArray, c.quantityArray); 256 } else { 257 return false; 258 } 259 } 260 261 @Override 262 public int hashCode() { 263 return Objects.hash(quantityArray); 264 } 265 266 // -- IMPLEMENTATION DETAILS 267 268 private static void guardAgainstIllegalQuantitiesArgument(Quantity<?>[] quantities) { 269 if (quantities == null || quantities.length < 1) { 270 throw new IllegalArgumentException("At least one quantity is required."); 271 } 272 Scale firstScale = null; 273 for(Quantity<?> q : quantities) { 274 if(q==null) { 275 throw new IllegalArgumentException("Quantities must not contain null."); 276 } 277 if(firstScale==null) { 278 firstScale = q.getScale(); 279 if(firstScale==null) { 280 throw new IllegalArgumentException("Quantities must have a scale."); 281 } 282 } 283 if (!firstScale.equals(q.getScale())) { 284 throw new IllegalArgumentException("Quantities do not have the same scale."); 285 } 286 } 287 } 288 289 // almost a duplicate of the above, this is to keep heap pollution at a minimum 290 private static <Q extends Quantity<Q>> void guardAgainstIllegalQuantitiesArgument(List<Quantity<Q>> quantities) { 291 if (quantities == null || quantities.size() < 1) { 292 throw new IllegalArgumentException("At least one quantity is required."); 293 } 294 Scale firstScale = null; 295 for(Quantity<Q> q : quantities) { 296 if(q==null) { 297 throw new IllegalArgumentException("Quantities must not contain null."); 298 } 299 if(firstScale==null) { 300 firstScale = q.getScale(); 301 if(firstScale==null) { 302 throw new IllegalArgumentException("Quantities must have a scale."); 303 } 304 } 305 if (!firstScale.equals(q.getScale())) { 306 throw new IllegalArgumentException("Quantities do not have the same scale."); 307 } 308 } 309 } 310}