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