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.internal.function;
031
032import java.util.Optional;
033import java.util.function.BinaryOperator;
034import java.util.function.UnaryOperator;
035
036import javax.measure.Quantity;
037import javax.measure.Quantity.Scale;
038
039import org.apiguardian.api.API;
040
041import javax.measure.Unit;
042import javax.measure.UnitConverter;
043
044import static javax.measure.Quantity.Scale.ABSOLUTE;
045import static javax.measure.Quantity.Scale.RELATIVE;
046import static org.apiguardian.api.API.Status.INTERNAL;
047
048import tech.units.indriya.ComparableQuantity;
049import tech.units.indriya.function.AbstractConverter;
050import tech.units.indriya.quantity.Quantities;
051
052/**
053 * Encapsulates scale-honoring quantity arithmetics.
054 * 
055 * @author Andi Huber
056 */
057@API(status=INTERNAL)
058public final class ScaleHelper {
059
060    public static boolean isAbsolute(final Quantity<?> quantity) {
061        return ABSOLUTE == quantity.getScale();
062    }
063
064    public static boolean isRelative(final Quantity<?> quantity) {
065        return RELATIVE == quantity.getScale();
066    }
067
068    public static <Q extends Quantity<Q>> ComparableQuantity<Q> convertTo(
069            final Quantity<Q> quantity, 
070            final Unit<Q> anotherUnit) {
071
072        final UnitConverter converter = quantity.getUnit().getConverterTo(anotherUnit);
073        
074        if (isRelative(quantity)) {
075            final Number linearFactor = linearFactorOf(converter).orElse(null);
076            if(linearFactor==null) {
077                throw unsupportedRelativeScaleConversion(quantity, anotherUnit);
078            }
079            final Number valueInOtherUnit = Calculator.of(linearFactor).multiply(quantity.getValue()).peek();
080            return Quantities.getQuantity(valueInOtherUnit, anotherUnit, RELATIVE);
081        }
082        
083        final Number convertedValue = converter.convert(quantity.getValue());
084        return Quantities.getQuantity(convertedValue, anotherUnit, ABSOLUTE);
085    }
086
087    public static <Q extends Quantity<Q>> ComparableQuantity<Q> addition(
088            final Quantity<Q> q1,
089            final Quantity<Q> q2, 
090            final BinaryOperator<Number> operator) {
091
092        final boolean yieldsRelativeScale = OperandMode.get(q1, q2).isAllRelative(); 
093
094        // converting almost all, except system units and those that are shifted and relative like eg. Δ2°C == Δ2K
095        final ToSystemUnitConverter thisConverter = toSystemUnitConverterForAdd(q1, q1);
096        final ToSystemUnitConverter thatConverter = toSystemUnitConverterForAdd(q1, q2);
097
098        final Number thisValueInSystemUnit = thisConverter.apply(q1.getValue());
099        final Number thatValueInSystemUnit = thatConverter.apply(q2.getValue());
100
101        final Number resultValueInSystemUnit = operator.apply(thisValueInSystemUnit, thatValueInSystemUnit);
102
103        if (yieldsRelativeScale) {
104            return Quantities.getQuantity(thisConverter.invert(resultValueInSystemUnit), q1.getUnit(), RELATIVE);
105        }
106
107        final boolean needsInvering = !thisConverter.isNoop() || !thatConverter.isNoop();
108        final Number resultValueInThisUnit = needsInvering 
109                ? q1.getUnit().getConverterTo(q1.getUnit().getSystemUnit()).inverse().convert(resultValueInSystemUnit)
110                : resultValueInSystemUnit;
111
112        return Quantities.getQuantity(resultValueInThisUnit, q1.getUnit(), ABSOLUTE);
113    }
114
115    public static <Q extends Quantity<Q>> ComparableQuantity<Q> scalarMultiplication(
116            final Quantity<Q> quantity, 
117            final UnaryOperator<Number> operator) {
118
119        // if operand has scale RELATIVE, multiplication is trivial
120        if (isRelative(quantity)) {
121            return Quantities.getQuantity(
122                    operator.apply(quantity.getValue()), 
123                    quantity.getUnit(), 
124                    RELATIVE);
125        }
126
127        final ToSystemUnitConverter toSystemUnits = toSystemUnitConverterForMul(quantity);
128
129        final Number thisValueWithAbsoluteScale = toSystemUnits.apply(quantity.getValue());
130        final Number resultValueInAbsUnits = operator.apply(thisValueWithAbsoluteScale);
131        final boolean needsInvering = !toSystemUnits.isNoop();
132
133        final Number resultValueInThisUnit = needsInvering 
134                ? quantity.getUnit().getConverterTo(quantity.getUnit().getSystemUnit()).inverse().convert(resultValueInAbsUnits)
135                : resultValueInAbsUnits;
136
137        return Quantities.getQuantity(resultValueInThisUnit, quantity.getUnit(), quantity.getScale());
138    }
139
140    public static ComparableQuantity<?> multiplication(
141            final Quantity<?> q1,
142            final Quantity<?> q2, 
143            final BinaryOperator<Number> amountOperator, 
144            final BinaryOperator<Unit<?>> unitOperator) {
145        
146        final Quantity<?> absQ1 = toAbsoluteLinear(q1);
147        final Quantity<?> absQ2 = toAbsoluteLinear(q2);
148        return Quantities.getQuantity(
149                amountOperator.apply(absQ1.getValue(), absQ2.getValue()), 
150                unitOperator.apply(absQ1.getUnit(), absQ2.getUnit()));
151    }
152
153    // -- HELPER
154
155    private static <Q extends Quantity<Q>> Quantity<Q> toAbsoluteLinear(Quantity<Q> quantity) {
156        final Unit<Q> systemUnit = quantity.getUnit().getSystemUnit();
157        final UnitConverter toSystemUnit = quantity.getUnit().getConverterTo(systemUnit);
158        if(toSystemUnit.isLinear()) {
159            if(isAbsolute(quantity)) {
160                return quantity;
161            }
162            return Quantities.getQuantity(quantity.getValue(), quantity.getUnit());
163        }
164        // convert to system units
165        if(isAbsolute(quantity)) {
166            return Quantities.getQuantity(toSystemUnit.convert(quantity.getValue()), systemUnit, Scale.ABSOLUTE);
167        } else {
168            final Number linearFactor = linearFactorOf(toSystemUnit).orElse(null);
169            if(linearFactor==null) {
170                throw unsupportedRelativeScaleConversion(quantity, systemUnit);
171            }
172            final Number valueInSystemUnits = Calculator.of(linearFactor).multiply(quantity.getValue()).peek();
173            return Quantities.getQuantity(valueInSystemUnits, systemUnit, ABSOLUTE);
174        }
175    }
176
177    // used for addition, honors RELATIVE scale
178    private static <Q extends Quantity<Q>> ToSystemUnitConverter toSystemUnitConverterForAdd(
179            final Quantity<Q> q1,
180            final Quantity<Q> q2) {
181        final Unit<Q> systemUnit = q1.getUnit().getSystemUnit();
182        return ToSystemUnitConverter.forQuantity(q2, systemUnit);
183    }
184
185    // used for multiplication, honors RELATIVE scale
186    private static <T extends Quantity<T>> 
187    ToSystemUnitConverter toSystemUnitConverterForMul(Quantity<T> quantity) {
188        final Unit<T> systemUnit = quantity.getUnit().getSystemUnit();
189        return ToSystemUnitConverter.forQuantity(quantity, systemUnit);
190    }
191
192    private static Optional<Number> linearFactorOf(UnitConverter converter) {
193        return (converter instanceof AbstractConverter)
194                ? ((AbstractConverter)converter).linearFactor()
195                : Optional.empty();
196    }
197
198    // honors RELATIVE scale
199    private static class ToSystemUnitConverter implements UnaryOperator<Number> {
200        private final UnaryOperator<Number> unaryOperator;
201        private final UnaryOperator<Number> inverseOperator;
202
203        public static <Q extends Quantity<Q>>  
204        ToSystemUnitConverter forQuantity(Quantity<Q> quantity, Unit<Q> systemUnit) {
205            if(quantity.getUnit().equals(systemUnit)) {
206                return ToSystemUnitConverter.noop(); // no conversion required
207            }
208
209            final UnitConverter converter = quantity.getUnit().getConverterTo(systemUnit);
210
211            if(isAbsolute(quantity)) {
212
213                return ToSystemUnitConverter.of(converter::convert); // convert to system units
214
215            } else {
216                final Number linearFactor = linearFactorOf(converter).orElse(null);
217                if(linearFactor!=null) {
218                    // conversion by factor required ... Δ2°C -> Δ2K , Δ2°F -> 5/9 * Δ2K
219                    return ToSystemUnitConverter.factor(linearFactor); 
220                }
221                // convert any other cases of RELATIVE scale to system unit (ABSOLUTE) ...
222                throw unsupportedConverter(converter, quantity.getUnit());
223            }
224        }
225
226        public Number invert(Number x) {
227            return isNoop() 
228                    ? x
229                    : inverseOperator.apply(x); 
230        }
231
232        public static ToSystemUnitConverter of(UnaryOperator<Number> unaryOperator) {
233            return new ToSystemUnitConverter(unaryOperator, null);
234        }
235        public static ToSystemUnitConverter noop() {
236            return new ToSystemUnitConverter(null, null);
237        }
238        public static ToSystemUnitConverter factor(Number factor) {
239            return new ToSystemUnitConverter(
240                    number->Calculator.of(number).multiply(factor).peek(),
241                    number->Calculator.of(number).divide(factor).peek());
242        }
243        private ToSystemUnitConverter(
244                UnaryOperator<Number> unaryOperator, 
245                UnaryOperator<Number> inverseOperator) {
246            this.unaryOperator = unaryOperator;
247            this.inverseOperator = inverseOperator;
248        }
249        public boolean isNoop() {
250            return unaryOperator==null;
251        }
252        @Override
253        public Number apply(Number x) {
254            return isNoop() 
255                    ? x
256                    : unaryOperator.apply(x); 
257        }
258
259    }
260    
261    // -- OPERANDS
262    
263    private static enum OperandMode {
264        ALL_ABSOLUTE,
265        ALL_RELATIVE,
266        MIXED;
267        public static OperandMode get(
268                final Quantity<?> q1,
269                final Quantity<?> q2) {
270            if(q1.getScale()!=q2.getScale()) {
271                return OperandMode.MIXED;
272            }
273            return isAbsolute(q1)
274                    ? OperandMode.ALL_ABSOLUTE
275                    : OperandMode.ALL_RELATIVE;
276        }
277//        public boolean isAllAbsolute() {
278//            return this==ALL_ABSOLUTE;
279//        }
280        public boolean isAllRelative() {
281            return this==ALL_RELATIVE;
282        }
283    }
284        
285    // -- EXCEPTIONS
286    
287    private static <Q extends Quantity<Q>> UnsupportedOperationException unsupportedRelativeScaleConversion(
288            Quantity<Q> quantity, 
289            Unit<Q> anotherUnit) {
290        return new UnsupportedOperationException(
291                String.format(
292                        "Conversion of Quantitity %s to Unit %s is not supported for realtive scale.", 
293                        quantity, anotherUnit));
294    }
295    
296    private static UnsupportedOperationException unsupportedConverter(UnitConverter converter, Unit<?> unit) {
297        return new UnsupportedOperationException(
298                String.format(
299                        "Scale conversion from RELATIVE to ABSOLUTE for Unit %s having Converter %s is not implemented.", 
300                        unit, converter));
301    }   
302}