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.io.Serializable; 033import java.util.ArrayList; 034import java.util.Collections; 035import java.util.Comparator; 036import java.util.List; 037import java.util.Objects; 038import java.util.Optional; 039import java.util.stream.Collectors; 040 041import javax.measure.UnitConverter; 042 043import tech.units.indriya.internal.function.Calculator; 044import tech.uom.lib.common.function.Converter; 045import tech.uom.lib.common.util.UnitComparator; 046 047/** 048 * <p> 049 * The base class for our {@link UnitConverter} implementations. 050 * </p> 051 * 052 * @author <a href="mailto:jean-marie@dautelle.com">Jean-Marie Dautelle</a> 053 * @author <a href="mailto:werner@units.tech">Werner Keil</a> 054 * @author Andi Huber 055 * @version 2.1, Mai 28, 2020 056 * @since 1.0 057 */ 058public abstract class AbstractConverter 059 implements UnitConverter, Converter<Number, Number>, Serializable, Comparable<UnitConverter> { 060 061 /** 062 * 063 */ 064 private static final long serialVersionUID = 5790242858468427131L; 065 066 /** 067 * Default identity converter implementing AbstractConverter. 068 * <p> 069 * Note: Checking whether a UnitConverter is an identity operator should be done with 070 * {@code UnitConverter.isIdentity()} rather than checking for object identity 071 * {@code unitConverter == AbstractConverter.IDENTITY}. 072 */ 073 public static final AbstractConverter IDENTITY = new Identity(); 074 075 /** 076 * Allows for plug in of a custom UnitCompositionHandler. 077 */ 078 public static ConverterCompositionHandler UNIT_COMPOSITION_HANDLER = ConverterCompositionHandler.yieldingNormalForm(); 079 080 /** 081 * memorization for getConversionSteps 082 */ 083 protected List<? extends UnitConverter> conversionSteps; 084 085 /** 086 * DefaultQuantityFactory constructor. 087 */ 088 protected AbstractConverter() { 089 } 090 091 @Override 092 public abstract boolean equals(Object cvtr); 093 094 @Override 095 public abstract int hashCode(); 096 097 // -- TO-STRING - CONTRACT AND INTERFACE IMPLEMENTATION (FINAL) 098 099 /** 100 * Non-API 101 * <p> 102 * Returns a String describing the transformation that is represented by this converter. 103 * Contributes to converter's {@code toString} method. If null or empty 104 * {@code toString} output becomes simplified. 105 * </p> 106 * @return 107 */ 108 protected abstract String transformationLiteral(); 109 110 @Override 111 public final String toString() { 112 String converterName = getClass().getSimpleName(); 113 // omit trailing 'Converter' 114 if(converterName.endsWith("Converter")) { 115 converterName = converterName.substring(0, converterName.length()-"Converter".length()); 116 } 117 if(isIdentity()) { 118 return String.format("%s(IDENTITY)", converterName); 119 } 120 final String transformationLiteral = transformationLiteral(); 121 if(transformationLiteral==null || transformationLiteral.length()==0) { 122 return String.format("%s", converterName); 123 } 124 return String.format("%s(%s)", converterName, transformationLiteral); 125 } 126 127 // -- INVERSION - CONTRACT AND INTERFACE IMPLEMENTATION (FINAL) 128 129 /** 130 * Non-API 131 * <p> 132 * Returns an AbstractConverter that represents the inverse transformation of this converter, 133 * for cases where the transformation is not the identity transformation. 134 * </p> 135 * @return 136 */ 137 protected abstract AbstractConverter inverseWhenNotIdentity(); 138 139 @Override 140 public final AbstractConverter inverse() { 141 if(isIdentity()) { 142 return this; 143 } 144 return inverseWhenNotIdentity(); 145 } 146 147 // -- COMPOSITION CONTRACTS (TO BE IMPLEMENTED BY SUB-CLASSES) 148 149 /** 150 * Non-API 151 * Guard for {@link #reduce(AbstractConverter)} 152 * @param that 153 * @return whether or not a composition with given {@code that} is possible, such 154 * that no additional conversion steps are required, with respect to the steps already 155 * in place by this converter 156 */ 157 protected abstract boolean canReduceWith(AbstractConverter that); 158 159 /** 160 * Non-API 161 * Guarded by {@link #canReduceWith(AbstractConverter)} 162 * @param that 163 * @return a new AbstractConverter that adds no additional conversion steps, with respect 164 * to the steps already in place by this converter 165 */ 166 protected AbstractConverter reduce(AbstractConverter that) { 167 throw new IllegalStateException( 168 String.format("Concrete UnitConverter '%s' does not implement reduce(...).", this)); 169 } 170 171 // -- COMPOSITION INTERFACE IMPLEMENTATION (FINAL) 172 173 @Override 174 public final UnitConverter concatenate(UnitConverter converter) { 175 Objects.requireNonNull(converter, "Cannot compose with converter that is null."); 176 177 if(converter instanceof AbstractConverter) { 178 final AbstractConverter other = (AbstractConverter) converter; 179 return UNIT_COMPOSITION_HANDLER.compose(this, other, 180 AbstractConverter::canReduceWith, 181 AbstractConverter::reduce); 182 } 183 // converter is not a sub-class of AbstractConverter, we do the best we can ... 184 if(converter.isIdentity()) { 185 return this; 186 } 187 if(this.isIdentity()) { 188 return converter; 189 } 190 //[ahuber] we don't know how to reduce to a 'normal-form' with 'foreign' converters, 191 // so we just return the straightforward composition, which no longer allows for proper 192 // composition equivalence test 193 return new Pair(this, converter); 194 } 195 196 @Override 197 public final List<? extends UnitConverter> getConversionSteps() { 198 if(conversionSteps != null) { 199 return conversionSteps; 200 } 201 if(this instanceof Pair) { 202 return conversionSteps = ((Pair)this).createConversionSteps(); 203 } 204 return conversionSteps = Collections.singletonList(this); 205 } 206 207 // -- CONVERSION CONTRACTS (TO BE IMPLEMENTED BY SUB-CLASSES) 208 209 /** 210 * Non-API 211 * @param value 212 * @return transformed value 213 */ 214 protected abstract Number convertWhenNotIdentity(Number value); 215 216 // -- CONVERSION INTERFACE IMPLEMENTATION (FINAL) 217 218 @Override 219 public final double convert(double value) { 220 if(isIdentity()) { 221 return value; 222 } 223 return convertWhenNotIdentity(value).doubleValue(); 224 } 225 226 /** 227 * @throws IllegalArgumentException 228 * if the value is <code>null</code>. 229 */ 230 @Override 231 public final Number convert(Number value) { 232 if(isIdentity()) { 233 return value; 234 } 235 if (value == null) { 236 throw new IllegalArgumentException("Value cannot be null"); 237 } 238 return convertWhenNotIdentity(value); 239 } 240 241 /** 242 * Even though transformations may be composed of addition and multiplication, the first 243 * derivative might just be a linear function. This is strictly required for Quantities that 244 * are expressed with RELATIVE scope. Eg. Δ2°C or Δ2°F. Otherwise such deltas cannot 245 * be converted to ABSOLUTE scope without additional information. 246 * 247 * @return optionally the linear factor of this transformation's first derivative, 248 * based on whether this transformation allows for RELATIVE scaled Quantities 249 */ 250 public Optional<Number> linearFactor() { 251 if(this instanceof AddConverter) { 252 return Identity.ONE; 253 } 254 if(this instanceof MultiplyConverter) { 255 return Optional.of(((MultiplyConverter)this).getFactor()); 256 } 257 return Optional.empty(); 258 } 259 260 // -- DEFAULT IMPLEMENTATION OF IDENTITY 261 262 /** 263 * This class represents the identity converter (singleton). 264 */ 265 private static final class Identity extends AbstractConverter { 266 267 /** 268 * 269 */ 270 private static final long serialVersionUID = -4460463244427587361L; 271 private static final Optional<Number> ONE = Optional.of(1); 272 273 @Override 274 public boolean isIdentity() { 275 return true; 276 } 277 278 @Override 279 protected Number convertWhenNotIdentity(Number value) { 280 throw unreachable(); 281 } 282 283 @Override 284 public boolean equals(Object cvtr) { 285 return (cvtr instanceof Identity); 286 } 287 288 @Override 289 public int hashCode() { 290 return 0; 291 } 292 293 @Override 294 public boolean isLinear() { 295 return true; 296 } 297 298 @Override 299 public Optional<Number> linearFactor() { 300 return ONE; 301 } 302 303 @Override 304 public int compareTo(UnitConverter o) { 305 if (o instanceof Identity) { 306 return 0; 307 } 308 return -1; 309 } 310 311 @Override 312 protected boolean canReduceWith(AbstractConverter that) { 313 throw unreachable(); 314 } 315 316 @Override 317 protected AbstractConverter reduce(AbstractConverter that) { 318 throw unreachable(); 319 } 320 321 @Override 322 protected AbstractConverter inverseWhenNotIdentity() { 323 throw unreachable(); 324 } 325 326 @Override 327 protected String transformationLiteral() { 328 return null; 329 } 330 331 private IllegalStateException unreachable() { 332 return new IllegalStateException("code was reached, that is expected unreachable"); 333 } 334 335 336 337 } 338 339 // -- BINARY TREE (PAIR) 340 341 /** 342 * This class represents converters made up of two or more separate converters 343 * (in matrix notation <code>[pair] = [left] x [right]</code>). 344 */ 345 public static final class Pair extends AbstractConverter implements Serializable { 346 347 @SuppressWarnings("rawtypes") 348 private final static Comparator unitComparator = new UnitComparator<>(); 349 350 /** 351 * 352 */ 353 private static final long serialVersionUID = -123063827821728331L; 354 355 /** 356 * Holds the first converter. 357 */ 358 private final UnitConverter left; 359 360 /** 361 * Holds the second converter. 362 */ 363 private final UnitConverter right; 364 365 /** 366 * Creates a pair converter resulting from the combined transformation of the 367 * specified converters. 368 * 369 * @param left 370 * the left converter, not <code>null</code>. 371 * @param right 372 * the right converter. 373 * @throws IllegalArgumentException 374 * if either the left or right converter are </code> null</code> 375 */ 376 public Pair(UnitConverter left, UnitConverter right) { 377 if (left != null && right != null) { 378 this.left = left; 379 this.right = right; 380 } else { 381 throw new IllegalArgumentException("Converters cannot be null"); 382 } 383 } 384 385 @Override 386 public boolean isLinear() { 387 return left.isLinear() && right.isLinear(); 388 } 389 390 @Override 391 public Optional<Number> linearFactor() { 392 // factors are composed by multiplying them, unless there is one absent linear-factor, 393 // then all breaks down and we return an empty optional 394 395 if(!(left instanceof AbstractConverter)) { 396 throw requiresAbstractConverter(); 397 } 398 399 if(!(right instanceof AbstractConverter)) { 400 throw requiresAbstractConverter(); 401 } 402 403 final Optional<Number> leftLinearFactor = ((AbstractConverter)left).linearFactor(); 404 final Optional<Number> rightLinearFactor = ((AbstractConverter)right).linearFactor(); 405 if(!leftLinearFactor.isPresent() || !leftLinearFactor.isPresent()) { 406 return Optional.empty(); 407 } 408 409 return Optional.of( 410 Calculator.of(leftLinearFactor.get()) 411 .multiply(rightLinearFactor.get()) 412 .peek()); 413 } 414 415 @Override 416 public boolean isIdentity() { 417 return false; 418 } 419 420 /* 421 * Non-API 422 */ 423 protected List<? extends UnitConverter> createConversionSteps(){ 424 final List<? extends UnitConverter> leftSteps = left.getConversionSteps(); 425 final List<? extends UnitConverter> rightSteps = right.getConversionSteps(); 426 // TODO we could use Lambdas here 427 final List<UnitConverter> steps = new ArrayList<>(leftSteps.size() + rightSteps.size()); 428 steps.addAll(leftSteps); 429 steps.addAll(rightSteps); 430 return steps; 431 } 432 433 @Override 434 public Pair inverseWhenNotIdentity() { 435 return new Pair(right.inverse(), left.inverse()); 436 } 437 438 @Override 439 protected Number convertWhenNotIdentity(Number value) { 440 441 if(!(left instanceof AbstractConverter)) { 442 throw requiresAbstractConverter(); 443 } 444 445 if(!(right instanceof AbstractConverter)) { 446 throw requiresAbstractConverter(); 447 } 448 final AbstractConverter absLeft = (AbstractConverter) left; 449 final AbstractConverter absRight = (AbstractConverter) right; 450 return absLeft.convertWhenNotIdentity(absRight.convertWhenNotIdentity(value)); 451 } 452 453 @Override 454 public boolean equals(Object obj) { 455 if (this == obj) { 456 return true; 457 } 458 if (obj instanceof Pair) { 459 Pair that = (Pair) obj; 460 return Objects.equals(left, that.left) && Objects.equals(right, that.right); 461 } 462 return false; 463 } 464 465 @Override 466 public int hashCode() { 467 return Objects.hash(left, right); 468 } 469 470 public UnitConverter getLeft() { 471 return left; 472 } 473 474 public UnitConverter getRight() { 475 return right; 476 } 477 478 @SuppressWarnings("unchecked") 479 @Override 480 public int compareTo(UnitConverter obj) { 481 if (this == obj) { 482 return 0; 483 } 484 if (obj instanceof Pair) { 485 Pair that = (Pair) obj; 486 487 return Objects.compare(left, that.left, unitComparator) 488 + Objects.compare(right, that.right, unitComparator); 489 } 490 return -1; 491 } 492 493 @Override 494 protected String transformationLiteral() { 495 return String.format("%s", 496 getConversionSteps().stream() 497 .map(UnitConverter::toString) 498 .collect(Collectors.joining(" ○ ")) ); 499 } 500 501 @Override 502 protected boolean canReduceWith(AbstractConverter that) { 503 return false; 504 } 505 506 private IllegalArgumentException requiresAbstractConverter() { 507 return new IllegalArgumentException("can only handle instances of AbstractConverter"); 508 } 509 510 511 512 513 } 514}