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