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.function;
031
032import java.util.ArrayList;
033import java.util.Arrays;
034import java.util.Collection;
035import java.util.Collections;
036import java.util.List;
037import java.util.Objects;
038
039import javax.measure.MeasurementException;
040import javax.measure.Quantity;
041import javax.measure.Quantity.Scale;
042import javax.measure.Unit;
043import javax.measure.UnitConverter;
044
045import tech.units.indriya.internal.function.calc.Calculator;
046import tech.units.indriya.internal.function.radix.MixedRadixSupport;
047import tech.units.indriya.internal.function.radix.Radix;
048import tech.units.indriya.quantity.CompoundQuantity;
049import tech.units.indriya.quantity.Quantities;
050
051/**
052 * Immutable class that represents mixed-radix units (like "hour:min:sec" or
053 * "ft, in")
054 * 
055 * 
056 * @author Andi Huber
057 * @author Werner Keil
058 * @version 1.10, Jun 25, 2019
059 * @since 2.0
060 * @see <a href="https://en.wikipedia.org/wiki/Mixed_radix">Wikipedia: Mixed
061 *      radix</a>
062 * @see <a href=
063 *      "https://reference.wolfram.com/language/ref/MixedUnit.html">Wolfram
064 *      Language & System: MixedUnit</a>
065 * @see <a href="https://en.wikipedia.org/wiki/Metrication">Wikipedia:
066 *      Metrication</a>
067 * @see CompoundQuantity
068 */
069public class MixedRadix<Q extends Quantity<Q>> {
070// TODO could it be final or is there a use case for extending it?
071
072        // -- PRIVATE FIELDS
073
074        @Override
075        public String toString() {
076                return "MixedRadix [units=" + mixedRadixUnits + "]";
077        }
078
079        private final PrimaryUnitPickState pickState;
080        private final Unit<Q> primaryUnit;
081        private final List<Unit<Q>> mixedRadixUnits;
082        private final MixedRadixSupport mixedRadixSupport;
083
084        // -- PRIMARY UNIT PICK CONVENTION
085
086        public static enum PrimaryUnitPick {
087                LEADING_UNIT, TRAILING_UNIT
088        }
089
090        public static final PrimaryUnitPick PRIMARY_UNIT_PICK_DEFAULT = PrimaryUnitPick.TRAILING_UNIT;
091
092        public static PrimaryUnitPick PRIMARY_UNIT_PICK = PRIMARY_UNIT_PICK_DEFAULT;
093
094        // -- FACTORIES
095
096        public static <X extends Quantity<X>> MixedRadix<X> of(Unit<X> leadingUnit) {
097                Objects.requireNonNull(leadingUnit);
098                return new MixedRadix<>(PrimaryUnitPickState.pickByConvention(), Collections.singletonList(leadingUnit));
099        }
100
101        @SafeVarargs
102        public static <X extends Quantity<X>> MixedRadix<X> of(Unit<X>... units) {
103                if (units == null || units.length < 1) {
104                        throw new IllegalArgumentException("at least the leading unit is required");
105                }
106                return of(Arrays.asList(units));
107        }
108
109        public static <X extends Quantity<X>> MixedRadix<X> of(Collection<Unit<X>> units) {
110                if (units == null || units.size() < 1) {
111                        throw new IllegalArgumentException("at least the leading unit is required");
112                }
113                MixedRadix<X> mixedRadix = null;
114                for (Unit<X> unit : units) {
115                        mixedRadix = mixedRadix == null ? of(unit) : mixedRadix.mix(unit);
116                }
117                return mixedRadix;
118        }
119
120        public static <X extends Quantity<X>> MixedRadix<X> ofPrimary(Unit<X> primaryUnit) {
121                Objects.requireNonNull(primaryUnit);
122                return new MixedRadix<>(PrimaryUnitPickState.pickLeading(), Collections.singletonList(primaryUnit));
123        }
124
125        public MixedRadix<Q> mix(Unit<Q> mixedRadixUnit) {
126                Objects.requireNonNull(mixedRadixUnit);
127                return append(pickState, mixedRadixUnit); // pickState is immutable, so reuse
128        }
129
130        public MixedRadix<Q> mixPrimary(Unit<Q> mixedRadixUnit) {
131                pickState.assertNotExplicitlyPicked();
132                Objects.requireNonNull(mixedRadixUnit);
133                return append(PrimaryUnitPickState.pickByExplicitIndex(getUnitCount()), mixedRadixUnit);
134        }
135
136        // -- GETTERS
137
138        public Unit<Q> getPrimaryUnit() {
139                return primaryUnit;
140        }
141
142        private Unit<Q> getTrailingUnit() {
143                return mixedRadixUnits.get(mixedRadixUnits.size() - 1);
144        }
145
146        public List<Unit<Q>> getUnits() {
147                return Collections.unmodifiableList(mixedRadixUnits);
148        }
149
150        private int getUnitCount() {
151                return mixedRadixUnits.size();
152        }
153
154        // -- QUANTITY FACTORY
155
156        /**
157         * Creates a {@link Quantity} from given {@code values} and {@code scale}.
158         * @param values - numbers corresponding to the radices in most significant first order, 
159     *      allowed to be of shorter length than the total count of radices of this {@code MixedRadix} instance
160         * @param scale - the {@link Scale} to be used for the returned {@link Quantity}
161         */
162        public Quantity<Q> createQuantity(final Number[] values, final Scale scale) {
163            Objects.requireNonNull(scale);
164            guardAgainstIllegalNumbersArgument(values);
165
166                Number sum = mixedRadixSupport.sumMostSignificant(values);
167
168                return Quantities.getQuantity(sum, getTrailingUnit(), scale).to(getPrimaryUnit());
169        }
170
171    public Quantity<Q> createQuantity(Number... values) {
172                return createQuantity(values, Scale.ABSOLUTE);
173        }
174
175    /**
176     * Creates a {@link CompoundQuantity} from given {@code values} and {@code scale}.
177     * <p>
178     * Note: Not every {@code CompoundQuantity} can be represented by a {@code MixedRadix}. 
179     * {@code MixedRadix} strictly requires its coefficients to be in decreasing order of significance, 
180     * while a {@code CompoundQuantity} in principle does not.
181     * 
182     * @param values - numbers corresponding to the radix coefficients in most significant first order, 
183     *      allowed to be of shorter length than the total count of radix coefficients of this 
184     *      {@code MixedRadix} instance
185     * @param scale - the {@link Scale} to be used for the elements of the returned {@link CompoundQuantity}
186     */
187        public CompoundQuantity<Q> createCompoundQuantity(final Number[] values, final Scale scale) {
188                Objects.requireNonNull(scale);
189                guardAgainstIllegalNumbersArgument(values);
190
191                List<Quantity<Q>> quantities = new ArrayList<>();
192                for (int i = 0; i < values.length; i++) {
193                        quantities.add(Quantities.getQuantity(values[i], mixedRadixUnits.get(i), scale));
194                }
195                return CompoundQuantity.of(quantities);
196        }
197
198        public CompoundQuantity<Q> createCompoundQuantity(Number... values) {
199                return createCompoundQuantity(values, Scale.ABSOLUTE);
200        }
201
202        // -- VALUE EXTRACTION
203
204        public Number[] extractValues(Quantity<Q> quantity) {
205                Objects.requireNonNull(quantity);
206                final Number[] target = new Number[mixedRadixUnits.size()];
207                return extractValuesInto(quantity, target);
208        }
209
210        public Number[] extractValuesInto(Quantity<Q> quantity, Number[] target) {
211                Objects.requireNonNull(quantity);
212                Objects.requireNonNull(target);
213
214                visitQuantity(quantity, target.length, (index, unit, value) -> {
215                        target[index] = value;
216                });
217
218                return target;
219        }
220
221        // -- THE VISITOR
222
223        @FunctionalInterface
224        public static interface MixedRadixVisitor<Q extends Quantity<Q>> {
225                public void accept(int index, Unit<Q> unit, Number value);
226        }
227
228        // -- IMPLEMENTATION DETAILS
229        
230   private void guardAgainstIllegalNumbersArgument(Number[] values) {
231        if (values == null || values.length < 1) {
232            throw new IllegalArgumentException("at least the leading unit's number is required");
233        }
234
235        int totalValuesGiven = values.length;
236        int totalValuesAllowed = mixedRadixUnits.size();
237
238        if (totalValuesGiven > totalValuesAllowed) {
239            String message = String.format(
240                    "number of values given <%d> exceeds the number of mixed-radix units available <%d>",
241                    totalValuesGiven, totalValuesAllowed);
242            throw new IllegalArgumentException(message);
243        }
244    }
245        
246        void visitQuantity(Quantity<Q> quantity, int maxPartsToVisit, MixedRadixVisitor<Q> partVisitor) {
247
248                final int partsToVisitCount = Math.min(maxPartsToVisit, getUnitCount());
249
250                // corner case (partsToVisitCount == 0)
251
252                if (partsToVisitCount == 0) {
253                        return;
254                }
255
256                // for partsToVisitCount >= 1
257
258                final Number value_inTrailingUnits = quantity.to(getTrailingUnit()).getValue();
259                final List<Number> extractedValues = new ArrayList<>(getUnitCount());
260
261                mixedRadixSupport.visitRadixNumbers(value_inTrailingUnits, extractedValues::add);
262
263                for (int i = 0; i < partsToVisitCount; ++i) {
264                        int invertedIndex = getUnitCount() - 1 - i;
265                        partVisitor.accept(i, mixedRadixUnits.get(i), extractedValues.get(invertedIndex));
266                }
267        }
268
269        /**
270         * 
271         * @param primaryUnitIndex - if negative, the index is relative to the number of
272         *                         units
273         * @param mixedRadixUnits
274         */
275        private MixedRadix(PrimaryUnitPickState pickState, final List<Unit<Q>> mixedRadixUnits) {
276                this.pickState = pickState;
277                this.mixedRadixUnits = mixedRadixUnits;
278                this.primaryUnit = mixedRadixUnits.get(pickState.nonNegativePrimaryUnitIndex(getUnitCount()));
279
280                final Radix[] radices = new Radix[getUnitCount() - 1];
281                for (int i = 0; i < radices.length; ++i) {
282                        Unit<Q> higher = mixedRadixUnits.get(i);
283                        Unit<Q> lesser = mixedRadixUnits.get(i + 1);
284                        radices[i] = toRadix(higher.getConverterTo(lesser));
285                }
286
287                this.mixedRadixSupport = new MixedRadixSupport(radices);
288
289        }
290
291        private Radix toRadix(UnitConverter converter) {
292                return Radix.ofMultiplyConverter(converter);
293        }
294
295        private MixedRadix<Q> append(PrimaryUnitPickState state, Unit<Q> mixedRadixUnit) {
296
297                Unit<Q> tail = getTrailingUnit();
298
299                assertDecreasingOrderOfSignificanceAndLinearity(tail, mixedRadixUnit);
300
301                final List<Unit<Q>> mixedRadixUnits = new ArrayList<>(this.mixedRadixUnits);
302                mixedRadixUnits.add(mixedRadixUnit);
303                return new MixedRadix<>(state, mixedRadixUnits);
304        }
305
306        private void assertDecreasingOrderOfSignificanceAndLinearity(Unit<Q> tail, Unit<Q> appended) {
307
308                final UnitConverter converter = appended.getConverterTo(tail);
309                if (!converter.isLinear()) {
310                        String message = String.format("the appended mixed-radix unit <%s> " + "must be linear",
311                                        appended.getClass());
312                        throw new IllegalArgumentException(message);
313                }
314
315                final Number factor = appended.getConverterTo(tail).convert(1);
316
317                if (Calculator.of(factor).abs().isLessThanOne()) {
318                        String message = String.format("the appended mixed-radix unit <%s> " + "must be of lesser significance "
319                                        + "than the one it is appended to: <%s>", appended.getClass(), tail.getClass());
320                        throw new MeasurementException(message);
321                }
322        }
323
324        private static class PrimaryUnitPickState {
325
326                private final static int LEADING_IS_PRIMARY_UNIT = 0;
327                private final static int TRAILING_IS_PRIMARY_UNIT = -1;
328                private final boolean explicitlyPicked;
329                private final int pickedIndex;
330
331                private static PrimaryUnitPickState pickByConvention() {
332
333                        final int pickedIndex_byConvention;
334
335                        switch (PRIMARY_UNIT_PICK) {
336                        case LEADING_UNIT:
337                                pickedIndex_byConvention = LEADING_IS_PRIMARY_UNIT;
338                                break;
339
340                        case TRAILING_UNIT:
341                                pickedIndex_byConvention = TRAILING_IS_PRIMARY_UNIT;
342                                break;
343
344                        default:
345                                throw new MeasurementException(
346                                                String.format("internal error: unmatched switch case <%s>", PRIMARY_UNIT_PICK));
347
348                        }
349
350                        return new PrimaryUnitPickState(false, pickedIndex_byConvention);
351                }
352
353                private void assertNotExplicitlyPicked() {
354                        if (explicitlyPicked) {
355                                throw new IllegalStateException("a primary unit was already picked");
356                        }
357                }
358
359                private static PrimaryUnitPickState pickByExplicitIndex(int explicitIndex) {
360                        return new PrimaryUnitPickState(true, explicitIndex);
361                }
362
363                private static PrimaryUnitPickState pickLeading() {
364                        return new PrimaryUnitPickState(true, LEADING_IS_PRIMARY_UNIT);
365                }
366
367                private PrimaryUnitPickState(boolean explicitlyPicked, int pickedIndex) {
368                        this.explicitlyPicked = explicitlyPicked;
369                        this.pickedIndex = pickedIndex;
370                }
371
372                private int nonNegativePrimaryUnitIndex(int unitCount) {
373                        return pickedIndex < 0 ? unitCount + pickedIndex : pickedIndex;
374                }
375
376        }
377}