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