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