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