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