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