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}