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}