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.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.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.MixedQuantity;
050import tech.units.indriya.quantity.Quantities;
051
052/**
053 * Immutable class that represents mixed-radix units (like "hour:min:sec" or
054 * "ft, in")
055 * 
056 * 
057 * @author Andi Huber
058 * @author Werner Keil
059 * @version 2.1, Feb 21, 2021
060 * @since 2.0
061 * @see <a href="https://en.wikipedia.org/wiki/Mixed_radix">Wikipedia: Mixed
062 *      radix</a>
063 * @see <a href=
064 *      "https://reference.wolfram.com/language/ref/MixedUnit.html">Wolfram
065 *      Language & System: MixedUnit</a>
066 * @see <a href="https://en.wikipedia.org/wiki/Metrication">Wikipedia:
067 *      Metrication</a>
068 * @see MixedQuantity
069 */
070public final class MixedRadix<Q extends Quantity<Q>> {
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        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 MixedQuantity} from given {@code values} and {@code scale}.
177     * <p>
178     * Note: Not every {@code MixedQuantity} 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 MixedQuantity} 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 MixedQuantity}
186     */
187        public MixedQuantity<Q> createMixedQuantity(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 MixedQuantity.of(quantities);
196        }
197
198        public MixedQuantity<Q> createMixedQuantity(Number... values) {
199                return createMixedQuantity(values, Scale.ABSOLUTE);
200        }
201        
202    /**
203     * Creates a {@link CompoundQuantity} from given {@code values} and {@code scale}.
204     * <p>
205     * Note: Not every {@code CompoundQuantity} can be represented by a {@code MixedRadix}. 
206     * {@code MixedRadix} strictly requires its coefficients to be in decreasing order of significance, 
207     * while a {@code CompoundQuantity} in principle does not.
208     * 
209     * @param values - numbers corresponding to the radix coefficients in most significant first order, 
210     *      allowed to be of shorter length than the total count of radix coefficients of this 
211     *      {@code MixedRadix} instance
212     * @param scale - the {@link Scale} to be used for the elements of the returned {@link CompoundQuantity}
213     * @deprecated use #getMixedQuantity
214     */
215        public CompoundQuantity<Q> createCompoundQuantity(final Number[] values, final Scale scale) {
216                Objects.requireNonNull(scale);
217                guardAgainstIllegalNumbersArgument(values);
218
219                List<Quantity<Q>> quantities = new ArrayList<>();
220                for (int i = 0; i < values.length; i++) {
221                        quantities.add(Quantities.getQuantity(values[i], mixedRadixUnits.get(i), scale));
222                }
223                return CompoundQuantity.of(quantities);
224        }
225
226        @Deprecated
227        public CompoundQuantity<Q> createCompoundQuantity(Number... values) {
228                return createCompoundQuantity(values, Scale.ABSOLUTE);
229        }
230
231        // -- VALUE EXTRACTION
232
233        public Number[] extractValues(Quantity<Q> quantity) {
234                Objects.requireNonNull(quantity);
235                final Number[] target = new Number[mixedRadixUnits.size()];
236                return extractValuesInto(quantity, target);
237        }
238
239        public Number[] extractValuesInto(Quantity<Q> quantity, Number[] target) {
240                Objects.requireNonNull(quantity);
241                Objects.requireNonNull(target);
242
243                visitQuantity(quantity, target.length, (index, unit, value) -> {
244                        target[index] = value;
245                });
246
247                return target;
248        }
249
250        // -- THE VISITOR
251
252        @FunctionalInterface
253        public static interface MixedRadixVisitor<Q extends Quantity<Q>> {
254                public void accept(int index, Unit<Q> unit, Number value);
255        }
256
257        // -- IMPLEMENTATION DETAILS
258        
259   private void guardAgainstIllegalNumbersArgument(Number[] values) {
260        if (values == null || values.length < 1) {
261            throw new IllegalArgumentException("at least the leading unit's number is required");
262        }
263
264        int totalValuesGiven = values.length;
265        int totalValuesAllowed = mixedRadixUnits.size();
266
267        if (totalValuesGiven > totalValuesAllowed) {
268            String message = String.format(
269                    "number of values given <%d> exceeds the number of mixed-radix units available <%d>",
270                    totalValuesGiven, totalValuesAllowed);
271            throw new IllegalArgumentException(message);
272        }
273    }
274        
275        void visitQuantity(Quantity<Q> quantity, int maxPartsToVisit, MixedRadixVisitor<Q> partVisitor) {
276                final int partsToVisitCount = Math.min(maxPartsToVisit, getUnitCount());
277
278                // corner case (partsToVisitCount == 0)
279
280                if (partsToVisitCount == 0) {
281                        return;
282                }
283
284                // for partsToVisitCount >= 1
285
286                final Number value_inTrailingUnits = quantity.to(getTrailingUnit()).getValue();
287                final List<Number> extractedValues = new ArrayList<>(getUnitCount());
288
289                mixedRadixSupport.visitRadixNumbers(value_inTrailingUnits, extractedValues::add);
290
291                for (int i = 0; i < partsToVisitCount; ++i) {
292                        int invertedIndex = getUnitCount() - 1 - i;
293                        partVisitor.accept(i, mixedRadixUnits.get(i), extractedValues.get(invertedIndex));
294                }
295        }
296
297        /**
298         * 
299         * @param primaryUnitIndex - if negative, the index is relative to the number of
300         *                         units
301         * @param mixedRadixUnits
302         */
303        private MixedRadix(PrimaryUnitPickState pickState, final List<Unit<Q>> mixedRadixUnits) {
304                this.pickState = pickState;
305                this.mixedRadixUnits = mixedRadixUnits;
306                this.primaryUnit = mixedRadixUnits.get(pickState.nonNegativePrimaryUnitIndex(getUnitCount()));
307
308                final Radix[] radices = new Radix[getUnitCount() - 1];
309                for (int i = 0; i < radices.length; ++i) {
310                        Unit<Q> higher = mixedRadixUnits.get(i);
311                        Unit<Q> lesser = mixedRadixUnits.get(i + 1);
312                        radices[i] = toRadix(higher.getConverterTo(lesser));
313                }
314
315                this.mixedRadixSupport = new MixedRadixSupport(radices);
316
317        }
318
319        private Radix toRadix(UnitConverter converter) {
320                return Radix.ofMultiplyConverter(converter);
321        }
322
323        private MixedRadix<Q> append(PrimaryUnitPickState state, Unit<Q> mixedRadixUnit) {
324                Unit<Q> tail = getTrailingUnit();
325                assertDecreasingOrderOfSignificanceAndLinearity(tail, mixedRadixUnit);
326
327                final List<Unit<Q>> mixedRadixUnits = new ArrayList<>(this.mixedRadixUnits);
328                mixedRadixUnits.add(mixedRadixUnit);
329                return new MixedRadix<>(state, mixedRadixUnits);
330        }
331
332        private void assertDecreasingOrderOfSignificanceAndLinearity(Unit<Q> tail, Unit<Q> appended) {
333                final UnitConverter converter = appended.getConverterTo(tail);
334                if (!converter.isLinear()) {
335                        String message = String.format("the appended mixed-radix unit <%s> " + "must be linear",
336                                        appended.getClass());
337                        throw new IllegalArgumentException(message);
338                }
339
340                final Number factor = tail.getConverterTo(appended).convert(1);
341
342                if (Calculator.of(factor).abs().isLessThanOne()) {
343                        String message = String.format("the appended mixed-radix unit <%s> " + "must be of lesser significance "
344                                        + "than the one it is appended to: <%s>", appended.getClass(), tail.getClass());
345                        throw new MeasurementException(message);
346                }
347        }
348
349        private static class PrimaryUnitPickState {
350
351                private final static int LEADING_IS_PRIMARY_UNIT = 0;
352                private final static int TRAILING_IS_PRIMARY_UNIT = -1;
353                private final boolean explicitlyPicked;
354                private final int pickedIndex;
355
356                private static PrimaryUnitPickState pickByConvention() {
357                        final int pickedIndex_byConvention;
358
359                        switch (PRIMARY_UNIT_PICK) {
360                        case LEADING_UNIT:
361                                pickedIndex_byConvention = LEADING_IS_PRIMARY_UNIT;
362                                break;
363
364                        case TRAILING_UNIT:
365                                pickedIndex_byConvention = TRAILING_IS_PRIMARY_UNIT;
366                                break;
367
368                        default:
369                                throw new MeasurementException(
370                                                String.format("internal error: unmatched switch case <%s>", PRIMARY_UNIT_PICK));
371                        }
372
373                        return new PrimaryUnitPickState(false, pickedIndex_byConvention);
374                }
375
376                private void assertNotExplicitlyPicked() {
377                        if (explicitlyPicked) {
378                                throw new IllegalStateException("a primary unit was already picked");
379                        }
380                }
381
382                private static PrimaryUnitPickState pickByExplicitIndex(int explicitIndex) {
383                        return new PrimaryUnitPickState(true, explicitIndex);
384                }
385
386                private static PrimaryUnitPickState pickLeading() {
387                        return new PrimaryUnitPickState(true, LEADING_IS_PRIMARY_UNIT);
388                }
389
390                private PrimaryUnitPickState(boolean explicitlyPicked, int pickedIndex) {
391                        this.explicitlyPicked = explicitlyPicked;
392                        this.pickedIndex = pickedIndex;
393                }
394
395                private int nonNegativePrimaryUnitIndex(int unitCount) {
396                        return pickedIndex < 0 ? unitCount + pickedIndex : pickedIndex;
397                }
398        }
399}