| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package org.apache.sis.measure; |
| |
| import java.util.Collections; |
| import java.util.Map; |
| import java.util.LinkedHashMap; |
| import java.io.Serializable; |
| import java.io.IOException; |
| import java.io.UncheckedIOException; |
| import java.io.ObjectStreamException; |
| import javax.measure.Dimension; |
| import org.apache.sis.math.Fraction; |
| import org.apache.sis.util.ObjectConverters; |
| import org.apache.sis.util.resources.Errors; |
| import org.apache.sis.util.UnsupportedImplementationException; |
| import org.apache.sis.internal.converter.FractionConverter; |
| import org.apache.sis.internal.util.CollectionsExt; |
| |
| |
| /** |
| * Dimension (length, mass, time, <i>etc.</i>) of a unit of measurement. |
| * Only two kind of dimensions are defined in Apache SIS: |
| * |
| * <ul> |
| * <li>Base dimensions are the 7 base dimensions specified by the SI system.</li> |
| * <li>Derived dimensions are products of base dimensions raised to some power.</li> |
| * </ul> |
| * |
| * The powers should be integers, but this implementation nevertheless accepts fractional power of dimensions. |
| * While quantities with dimension such as √M makes no sense physically, on a pragmatic point of view it is easier |
| * to write programs in which such units appear in intermediate calculations but become integers in the final result. |
| * Furthermore, some dimensions with fractional power actually exist. Examples: |
| * |
| * <ul> |
| * <li>Voltage noise density measured per √(Hz).</li> |
| * <li><a href="http://en.wikipedia.org/wiki/Specific_detectivity">Specific detectivity</a> |
| * as T^2.5 / (M⋅L) dimension.</li> |
| * </ul> |
| * |
| * All {@code UnitDimension} instances are immutable and thus inherently thread-safe. |
| * |
| * @author Martin Desruisseaux (Geomatys) |
| * @version 1.0 |
| * @since 0.8 |
| * @module |
| */ |
| final class UnitDimension implements Dimension, Serializable { |
| /** |
| * For cross-version compatibility. |
| */ |
| private static final long serialVersionUID = 2568769237612674235L; |
| |
| /** |
| * Pseudo-dimension for dimensionless units. |
| */ |
| static final UnitDimension NONE = new UnitDimension(Collections.emptyMap()); |
| // No need to store in UnitRegistry since UnitDimension performs special checks for dimensionless instances. |
| |
| /** |
| * The product of base dimensions that make this dimension. All keys in this map shall be base dimensions |
| * (base dimensions are identified by non-zero {@link #symbol}). If this {@code UnitDimension} is itself a |
| * base dimension, then the map contains {@code this} raised to power 1. The map shall never be {@code null}. |
| * |
| * @see #getBaseDimensions() |
| */ |
| final Map<UnitDimension,Fraction> components; |
| |
| /** |
| * If this {@code UnitDimension} is a base dimension, its symbol (not to be confused with unit symbol). |
| * Otherwise (i.e. if this {@code UnitDimension} is a derived dimension), zero. |
| */ |
| final char symbol; |
| |
| /** |
| * Creates a new base dimension with the given symbol, which shall not be zero. |
| * This constructor shall be invoked only during construction of {@link Units} constants. |
| * |
| * @param symbol the symbol of this base dimension (not to be confused with unit symbol). |
| */ |
| @SuppressWarnings("ThisEscapedInObjectConstruction") // Safe because this class is final. |
| UnitDimension(final char symbol) { |
| this.symbol = symbol; |
| components = Collections.singletonMap(this, new Fraction(1,1).unique()); |
| UnitRegistry.init(components, this); |
| } |
| |
| /** |
| * Creates a new derived dimension. This constructor shall never be invoked directly |
| * (except for {@link #NONE}); use {@link #create(Map)} instead. |
| * |
| * @param components the product of base dimensions together with their power. |
| */ |
| private UnitDimension(final Map<UnitDimension,Fraction> components) { |
| this.components = components; |
| this.symbol = 0; |
| } |
| |
| /** |
| * Creates a new derived dimension from the given product of base dimensions with their power. |
| * This method returns a shared instance if possible. |
| * |
| * @param components the product of base dimensions together with their power. |
| */ |
| private static UnitDimension create(Map<UnitDimension,Fraction> components) { |
| switch (components.size()) { |
| case 0: return NONE; |
| case 1: { |
| final Map.Entry<UnitDimension,Fraction> entry = components.entrySet().iterator().next(); |
| final Fraction power = entry.getValue(); |
| if (power.numerator == 1 && power.denominator == 1) { |
| return entry.getKey(); |
| } |
| break; |
| } |
| } |
| /* |
| * Implementation note: following code duplicates the functionality of Map.computeIfAbsent(…), |
| * but we had to do it because we compute not only the value, but also the 'components' key. |
| */ |
| UnitDimension dim = (UnitDimension) UnitRegistry.get(components); |
| if (dim == null) { |
| components.replaceAll((c, power) -> power.unique()); |
| components = CollectionsExt.unmodifiableOrCopy(components); |
| dim = new UnitDimension(components); |
| if (!Units.initialized) { |
| UnitRegistry.init(components, dim); |
| } else { |
| final UnitDimension c = (UnitDimension) UnitRegistry.putIfAbsent(components, dim); |
| if (c != null) { |
| return c; // UnitDimension created concurrently in another thread. |
| } |
| } |
| } |
| return dim; |
| } |
| |
| /** |
| * Invoked on deserialization for returning a unique instance of {@code UnitDimension}. |
| */ |
| Object readResolve() throws ObjectStreamException { |
| if (isDimensionless()) { |
| return NONE; |
| } |
| if (Units.initialized) { // Force Units class initialization. |
| final UnitDimension dim = (UnitDimension) UnitRegistry.putIfAbsent(components, this); |
| if (dim != null) { |
| return dim; |
| } |
| } |
| return this; |
| } |
| |
| /** |
| * Returns {@code true} if this {@code UnitDimension} has no components. |
| * Many dimensionless units exist for different quantities as angles, parts per million, <i>etc.</i> |
| */ |
| final boolean isDimensionless() { |
| return components.isEmpty(); |
| } |
| |
| /** |
| * Returns {@code true} if the given dimension has no components. |
| */ |
| static boolean isDimensionless(final Dimension dim) { |
| if (dim instanceof UnitDimension) { |
| return ((UnitDimension) dim).isDimensionless(); |
| } else if (dim != null) { |
| // Fallback for foreigner implementations. |
| final Map<? extends Dimension, Integer> bases = dim.getBaseDimensions(); |
| if (bases != null) return bases.isEmpty(); |
| } |
| return false; // Unit is a base unit (not a product of existing units). |
| } |
| |
| /** |
| * Returns {@code true} if the numerator is the dimension identified by the given symbol. |
| * This method returns {@code true} only if the numerator is not be raised to any exponent |
| * other than 1 and there is no other numerator. All denominator terms are ignored. |
| * |
| * <p>This method is used for identifying units like "kg", "kg/s", <i>etc</i> for handling |
| * the "kg" prefix in a special way.</p> |
| */ |
| final boolean numeratorIs(final char s) { |
| if (symbol == s) { // Optimization for a simple case. |
| assert components.keySet().equals(Collections.singleton(this)); |
| return true; |
| } |
| boolean found = false; |
| for (final Map.Entry<UnitDimension,Fraction> e : components.entrySet()) { |
| final Fraction value = e.getValue(); |
| if (e.getKey().symbol == s) { |
| if (value.numerator != value.denominator) { |
| return false; // Raised to a power different than 1. |
| } |
| found = true; |
| } else if (value.signum() >= 0) { |
| return false; // Found other numerators. |
| } |
| } |
| return found; |
| } |
| |
| /** |
| * Returns the (fundamental) base dimensions and their exponent whose product is this dimension, |
| * or null if this dimension is a base dimension. |
| */ |
| @Override |
| public Map<UnitDimension,Integer> getBaseDimensions() { |
| if (symbol != 0) { |
| return null; |
| } |
| return ObjectConverters.derivedValues(components, UnitDimension.class, FractionConverter.INSTANCE); |
| } |
| |
| /** |
| * Returns the base dimensions and their exponents whose product make the given dimension. |
| * If the given dimension is a base dimension, then this method returns {@code this} raised |
| * to power 1. This method never returns {@code null}. |
| */ |
| @SuppressWarnings("ReturnOfCollectionOrArrayField") // Safe because the map is unmodifiable. |
| private static Map<? extends Dimension, Fraction> getBaseDimensions(final Dimension dimension) { |
| if (dimension instanceof UnitDimension) { |
| return ((UnitDimension) dimension).components; |
| } |
| /* |
| * Fallback for non-SIS implementations. The cast from <? extends Dimension> to <Dimension> |
| * is safe if we use the 'components' map as a read-only map (no put operation allowed). |
| */ |
| @SuppressWarnings("unchecked") |
| Map<Dimension,Integer> components = (Map<Dimension,Integer>) dimension.getBaseDimensions(); |
| if (components == null) { |
| return Collections.singletonMap(dimension, new Fraction(1,1)); |
| } |
| return ObjectConverters.derivedValues(components, Dimension.class, FractionConverter.FromInteger.INSTANCE); |
| } |
| |
| /** |
| * Returns the product of this dimension with the one specified. |
| * |
| * @param multiplicand the dimension by which to multiply this dimension. |
| * @return {@code this} × {@code multiplicand} |
| */ |
| @Override |
| public UnitDimension multiply(final Dimension multiplicand) { |
| return combine(multiplicand, false); |
| } |
| |
| /** |
| * Returns the quotient of this dimension with the one specified. |
| * |
| * @param divisor the dimension by which to divide this dimension. |
| * @return {@code this} ∕ {@code divisor} |
| */ |
| @Override |
| public UnitDimension divide(final Dimension divisor) { |
| return combine(divisor, true); |
| } |
| |
| /** |
| * Returns the product or the quotient of this dimension with the specified one. |
| * |
| * @param other the dimension by which to multiply or divide this dimension. |
| * @param divide {@code false} for a multiplication, {@code true} for a division. |
| * @return the product or division of this dimension by the given dimension. |
| */ |
| private UnitDimension combine(final Dimension other, final boolean divide) { |
| final Map<UnitDimension,Fraction> product = new LinkedHashMap<>(components); |
| for (final Map.Entry<? extends Dimension, Fraction> entry : getBaseDimensions(other).entrySet()) { |
| final Dimension dim = entry.getKey(); |
| Fraction p = entry.getValue(); |
| if (divide) { |
| p = p.negate(); |
| } |
| if (dim instanceof UnitDimension) { |
| product.merge((UnitDimension) dim, p, (sum, toAdd) -> { |
| sum = sum.add(toAdd); |
| return (sum.numerator != 0) ? sum : null; |
| }); |
| } else if (p.numerator != 0) { |
| throw new UnsupportedImplementationException(Errors.format(Errors.Keys.UnsupportedImplementation_1, dim.getClass())); |
| } |
| } |
| return create(product); |
| } |
| |
| /** |
| * Returns this dimension raised to an exponent. |
| * |
| * @param n power to raise this dimension to (can be negative). |
| * @return {@code this}ⁿ |
| */ |
| private UnitDimension pow(final Fraction n) { |
| final Map<UnitDimension,Fraction> product = new LinkedHashMap<>(components); |
| product.replaceAll((dim, power) -> power.multiply(n)); |
| return create(product); |
| } |
| |
| /** |
| * Returns this dimension raised to an exponent. |
| * |
| * @param n power to raise this dimension to (can be negative). |
| * @return {@code this}ⁿ |
| */ |
| @Override |
| public UnitDimension pow(final int n) { |
| switch (n) { |
| case 0: return NONE; |
| case 1: return this; |
| default: return pow(new Fraction(n,1)); |
| } |
| } |
| |
| /** |
| * Returns the given root of this dimension. |
| * |
| * @param n the root's order. |
| * @return {@code this} raised to power 1/n. |
| */ |
| @Override |
| public UnitDimension root(final int n) { |
| switch (n) { |
| case 0: throw new ArithmeticException(Errors.format(Errors.Keys.IllegalArgumentValue_2, "n", 0)); |
| case 1: return this; |
| default: return pow(new Fraction(1,n)); |
| } |
| } |
| |
| /** |
| * Compares this dimension with the given object for equality. |
| */ |
| @Override |
| public boolean equals(final Object other) { |
| if (other == this) { |
| return true; |
| } |
| if (other instanceof UnitDimension) { |
| final UnitDimension that = (UnitDimension) other; |
| if (symbol == that.symbol) { |
| /* |
| * Do not compare 'components' if 'symbols' is non-zero because in such case |
| * the components map contains 'this', which would cause an infinite loop. |
| */ |
| return (symbol != 0) || components.equals(that.components); |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * Returns a hash code value for this dimension. |
| */ |
| @Override |
| public int hashCode() { |
| /* |
| * Do not use 'components' in hash code calculation if 'symbols' is non-zero |
| * beause in such case the map contains 'this', which would cause an infinite loop. |
| */ |
| return (symbol != 0) ? symbol ^ (int) serialVersionUID : components.hashCode(); |
| } |
| |
| /** |
| * Returns a string representation of this dimension. |
| */ |
| @Override |
| public String toString() { |
| final StringBuilder buffer = new StringBuilder(8); |
| try { |
| UnitFormat.formatComponents(components, UnitFormat.Style.SYMBOL, buffer); |
| } catch (IOException e) { |
| throw new UncheckedIOException(e); // Should never happen since we are writting to a StringBuilder. |
| } |
| return buffer.toString(); |
| } |
| } |