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}