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}