diff options
Diffstat (limited to 'src/main/java/org/unitConverter/unit')
19 files changed, 5652 insertions, 0 deletions
diff --git a/src/main/java/org/unitConverter/unit/BaseDimension.java b/src/main/java/org/unitConverter/unit/BaseDimension.java new file mode 100644 index 0000000..8e63a17 --- /dev/null +++ b/src/main/java/org/unitConverter/unit/BaseDimension.java @@ -0,0 +1,87 @@ +/** + * Copyright (C) 2019 Adrien Hopkins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ +package org.unitConverter.unit; + +import java.util.Objects; + +/** + * A dimension that defines a {@code BaseUnit} + * + * @author Adrien Hopkins + * @since 2019-10-16 + */ +public final class BaseDimension { + /** + * Gets a {@code BaseDimension} with the provided name and symbol. + * + * @param name + * name of dimension + * @param symbol + * symbol used for dimension + * @return dimension + * @since 2019-10-16 + */ + public static BaseDimension valueOf(final String name, final String symbol) { + return new BaseDimension(name, symbol); + } + + /** + * The name of the dimension. + */ + private final String name; + /** + * The symbol used by the dimension. Symbols should be short, generally one or two characters. + */ + private final String symbol; + + /** + * Creates the {@code BaseDimension}. + * + * @param name + * name of unit + * @param symbol + * symbol of unit + * @throws NullPointerException + * if any argument is null + * @since 2019-10-16 + */ + private BaseDimension(final String name, final String symbol) { + this.name = Objects.requireNonNull(name, "name must not be null."); + this.symbol = Objects.requireNonNull(symbol, "symbol must not be null."); + } + + /** + * @return name + * @since 2019-10-16 + */ + public final String getName() { + return this.name; + } + + /** + * @return symbol + * @since 2019-10-16 + */ + public final String getSymbol() { + return this.symbol; + } + + @Override + public String toString() { + return String.format("%s (%s)", this.getName(), this.getSymbol()); + } +} diff --git a/src/main/java/org/unitConverter/unit/BaseUnit.java b/src/main/java/org/unitConverter/unit/BaseUnit.java new file mode 100644 index 0000000..6757bd0 --- /dev/null +++ b/src/main/java/org/unitConverter/unit/BaseUnit.java @@ -0,0 +1,133 @@ +/** + * Copyright (C) 2019 Adrien Hopkins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ +package org.unitConverter.unit; + +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; + +/** + * A unit that other units are defined by. + * <p> + * Note that BaseUnits <b>must</b> have names and symbols. This is because they + * are used for toString code. Therefore, the Optionals provided by + * {@link #getPrimaryName} and {@link #getSymbol} will always contain a value. + * + * @author Adrien Hopkins + * @since 2019-10-16 + */ +public final class BaseUnit extends Unit { + /** + * Gets a base unit from the dimension it measures, its name and its symbol. + * + * @param dimension dimension measured by this unit + * @param name name of unit + * @param symbol symbol of unit + * @return base unit + * @since 2019-10-16 + */ + public static BaseUnit valueOf(final BaseDimension dimension, + final String name, final String symbol) { + return new BaseUnit(dimension, name, symbol, new HashSet<>()); + } + + /** + * Gets a base unit from the dimension it measures, its name and its symbol. + * + * @param dimension dimension measured by this unit + * @param name name of unit + * @param symbol symbol of unit + * @return base unit + * @since 2019-10-21 + */ + public static BaseUnit valueOf(final BaseDimension dimension, + final String name, final String symbol, final Set<String> otherNames) { + return new BaseUnit(dimension, name, symbol, otherNames); + } + + /** + * The dimension measured by this base unit. + */ + private final BaseDimension dimension; + + /** + * Creates the {@code BaseUnit}. + * + * @param dimension dimension of unit + * @param primaryName name of unit + * @param symbol symbol of unit + * @throws NullPointerException if any argument is null + * @since 2019-10-16 + */ + private BaseUnit(final BaseDimension dimension, final String primaryName, + final String symbol, final Set<String> otherNames) { + super(primaryName, symbol, otherNames); + this.dimension = Objects.requireNonNull(dimension, + "dimension must not be null."); + } + + /** + * Returns a {@code LinearUnit} with this unit as a base and a conversion + * factor of 1. This operation must be done in order to allow units to be + * created with operations. + * + * @return this unit as a {@code LinearUnit} + * @since 2019-10-16 + */ + public LinearUnit asLinearUnit() { + return LinearUnit.valueOf(this.getBase(), 1); + } + + @Override + protected double convertFromBase(final double value) { + return value; + } + + @Override + protected double convertToBase(final double value) { + return value; + } + + /** + * @return dimension + * @since 2019-10-16 + */ + public final BaseDimension getBaseDimension() { + return this.dimension; + } + + @Override + public String toString() { + return this.getPrimaryName().orElse("Unnamed unit") + + (this.getSymbol().isPresent() + ? String.format(" (%s)", this.getSymbol().get()) + : ""); + } + + @Override + public BaseUnit withName(final NameSymbol ns) { + Objects.requireNonNull(ns, "ns must not be null."); + if (!ns.getPrimaryName().isPresent()) + throw new IllegalArgumentException( + "BaseUnits must have primary names."); + if (!ns.getSymbol().isPresent()) + throw new IllegalArgumentException("BaseUnits must have symbols."); + return BaseUnit.valueOf(this.getBaseDimension(), + ns.getPrimaryName().get(), ns.getSymbol().get(), + ns.getOtherNames()); + } +} diff --git a/src/main/java/org/unitConverter/unit/BritishImperial.java b/src/main/java/org/unitConverter/unit/BritishImperial.java new file mode 100644 index 0000000..ea23cd1 --- /dev/null +++ b/src/main/java/org/unitConverter/unit/BritishImperial.java @@ -0,0 +1,116 @@ +/** + * Copyright (C) 2019 Adrien Hopkins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ +package org.unitConverter.unit; + +/** + * A static utility class that contains units in the British Imperial system. + * + * @author Adrien Hopkins + * @since 2019-10-21 + */ +public final class BritishImperial { + /** + * Imperial units that measure area + * + * @author Adrien Hopkins + * @since 2019-11-08 + */ + public static final class Area { + public static final LinearUnit SQUARE_FOOT = Length.FOOT.toExponent(2); + public static final LinearUnit SQUARE_YARD = Length.YARD.toExponent(2); + public static final LinearUnit SQUARE_MILE = Length.MILE.toExponent(2); + public static final LinearUnit PERCH = Length.ROD.times(Length.ROD); + public static final LinearUnit ROOD = Length.ROD.times(Length.FURLONG); + public static final LinearUnit ACRE = Length.FURLONG.times(Length.CHAIN); + } + + /** + * Imperial units that measure length + * + * @author Adrien Hopkins + * @since 2019-10-28 + */ + public static final class Length { + /** + * According to the International Yard and Pound of 1959, a yard is defined as exactly 0.9144 metres. + */ + public static final LinearUnit YARD = SI.METRE.times(0.9144); + public static final LinearUnit FOOT = YARD.dividedBy(3); + public static final LinearUnit INCH = FOOT.dividedBy(12); + public static final LinearUnit THOU = INCH.dividedBy(1000); + public static final LinearUnit CHAIN = YARD.times(22); + public static final LinearUnit FURLONG = CHAIN.times(10); + public static final LinearUnit MILE = FURLONG.times(8); + public static final LinearUnit LEAGUE = MILE.times(3); + + public static final LinearUnit NAUTICAL_MILE = SI.METRE.times(1852); + public static final LinearUnit CABLE = NAUTICAL_MILE.dividedBy(10); + public static final LinearUnit FATHOM = CABLE.dividedBy(100); + + public static final LinearUnit ROD = YARD.times(5.5); + public static final LinearUnit LINK = ROD.dividedBy(25); + } + + /** + * British Imperial units that measure mass. + * + * @author Adrien Hopkins + * @since 2019-11-08 + */ + public static final class Mass { + public static final LinearUnit POUND = SI.GRAM.times(453.59237); + public static final LinearUnit OUNCE = POUND.dividedBy(16); + public static final LinearUnit DRACHM = POUND.dividedBy(256); + public static final LinearUnit GRAIN = POUND.dividedBy(7000); + public static final LinearUnit STONE = POUND.times(14); + public static final LinearUnit QUARTER = STONE.times(2); + public static final LinearUnit HUNDREDWEIGHT = QUARTER.times(4); + public static final LinearUnit LONG_TON = HUNDREDWEIGHT.times(20); + public static final LinearUnit SLUG = SI.KILOGRAM.times(14.59390294); + } + + /** + * British Imperial units that measure volume + * + * @author Adrien Hopkins + * @since 2019-11-08 + */ + public static final class Volume { + public static final LinearUnit FLUID_OUNCE = SI.LITRE.withPrefix(SI.MILLI).times(28.4130625); + public static final LinearUnit GILL = FLUID_OUNCE.times(5); + public static final LinearUnit PINT = FLUID_OUNCE.times(20); + public static final LinearUnit QUART = PINT.times(2); + public static final LinearUnit GALLON = QUART.times(4); + public static final LinearUnit PECK = GALLON.times(2); + public static final LinearUnit BUSHEL = PECK.times(4); + + public static final LinearUnit CUBIC_INCH = Length.INCH.toExponent(3); + public static final LinearUnit CUBIC_FOOT = Length.FOOT.toExponent(3); + public static final LinearUnit CUBIC_YARD = Length.YARD.toExponent(3); + public static final LinearUnit ACRE_FOOT = Area.ACRE.times(Length.FOOT); + } + + public static final LinearUnit OUNCE_FORCE = Mass.OUNCE.times(SI.Constants.EARTH_GRAVITY); + public static final LinearUnit POUND_FORCE = Mass.POUND.times(SI.Constants.EARTH_GRAVITY); + + public static final LinearUnit BRITISH_THERMAL_UNIT = SI.JOULE.times(1055.06); + public static final LinearUnit CALORIE = SI.JOULE.times(4.184); + public static final LinearUnit KILOCALORIE = SI.JOULE.times(4184); + + public static final Unit FAHRENHEIT = Unit.fromConversionFunctions(SI.KELVIN.getBase(), + tempK -> tempK * 1.8 - 459.67, tempF -> (tempF + 459.67) / 1.8); +} diff --git a/src/main/java/org/unitConverter/unit/FunctionalUnit.java b/src/main/java/org/unitConverter/unit/FunctionalUnit.java new file mode 100644 index 0000000..586e0d7 --- /dev/null +++ b/src/main/java/org/unitConverter/unit/FunctionalUnit.java @@ -0,0 +1,109 @@ +/** + * Copyright (C) 2019 Adrien Hopkins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ +package org.unitConverter.unit; + +import java.util.Objects; +import java.util.function.DoubleUnaryOperator; + +import org.unitConverter.math.ObjectProduct; + +/** + * A unit that uses functional objects to convert to and from its base. + * + * @author Adrien Hopkins + * @since 2019-05-22 + */ +final class FunctionalUnit extends Unit { + /** + * A function that accepts a value expressed in the unit's base and returns that value expressed in this unit. + * + * @since 2019-05-22 + */ + private final DoubleUnaryOperator converterFrom; + + /** + * A function that accepts a value expressed in the unit and returns that value expressed in the unit's base. + * + * @since 2019-05-22 + */ + private final DoubleUnaryOperator converterTo; + + /** + * Creates the {@code FunctionalUnit}. + * + * @param base + * unit's base + * @param converterFrom + * function that accepts a value expressed in the unit's base and returns that value expressed in this + * unit. + * @param converterTo + * function that accepts a value expressed in the unit and returns that value expressed in the unit's + * base. + * @throws NullPointerException + * if any argument is null + * @since 2019-05-22 + */ + public FunctionalUnit(final ObjectProduct<BaseUnit> base, final DoubleUnaryOperator converterFrom, + final DoubleUnaryOperator converterTo) { + super(base, NameSymbol.EMPTY); + this.converterFrom = Objects.requireNonNull(converterFrom, "converterFrom must not be null."); + this.converterTo = Objects.requireNonNull(converterTo, "converterTo must not be null."); + } + + /** + * Creates the {@code FunctionalUnit}. + * + * @param base + * unit's base + * @param converterFrom + * function that accepts a value expressed in the unit's base and returns that value expressed in this + * unit. + * @param converterTo + * function that accepts a value expressed in the unit and returns that value expressed in the unit's + * base. + * @throws NullPointerException + * if any argument is null + * @since 2019-05-22 + */ + public FunctionalUnit(final ObjectProduct<BaseUnit> base, final DoubleUnaryOperator converterFrom, + final DoubleUnaryOperator converterTo, final NameSymbol ns) { + super(base, ns); + this.converterFrom = Objects.requireNonNull(converterFrom, "converterFrom must not be null."); + this.converterTo = Objects.requireNonNull(converterTo, "converterTo must not be null."); + } + + /** + * {@inheritDoc} + * + * Uses {@code converterFrom} to convert. + */ + @Override + public double convertFromBase(final double value) { + return this.converterFrom.applyAsDouble(value); + } + + /** + * {@inheritDoc} + * + * Uses {@code converterTo} to convert. + */ + @Override + public double convertToBase(final double value) { + return this.converterTo.applyAsDouble(value); + } + +} diff --git a/src/main/java/org/unitConverter/unit/FunctionalUnitlike.java b/src/main/java/org/unitConverter/unit/FunctionalUnitlike.java new file mode 100644 index 0000000..21c1fca --- /dev/null +++ b/src/main/java/org/unitConverter/unit/FunctionalUnitlike.java @@ -0,0 +1,72 @@ +/** + * Copyright (C) 2020 Adrien Hopkins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ +package org.unitConverter.unit; + +import java.util.function.DoubleFunction; +import java.util.function.ToDoubleFunction; + +import org.unitConverter.math.ObjectProduct; + +/** + * A unitlike form that converts using two conversion functions. + * + * @since 2020-09-07 + */ +final class FunctionalUnitlike<V> extends Unitlike<V> { + /** + * A function that accepts a value in the unitlike form's base and returns a + * value in the unitlike form. + * + * @since 2020-09-07 + */ + private final DoubleFunction<V> converterFrom; + + /** + * A function that accepts a value in the unitlike form and returns a value + * in the unitlike form's base. + */ + private final ToDoubleFunction<V> converterTo; + + /** + * Creates the {@code FunctionalUnitlike}. + * + * @param base unitlike form's base + * @param converterFrom function that accepts a value in the unitlike form's + * base and returns a value in the unitlike form. + * @param converterTo function that accepts a value in the unitlike form + * and returns a value in the unitlike form's base. + * @throws NullPointerException if any argument is null + * @since 2019-05-22 + */ + protected FunctionalUnitlike(ObjectProduct<BaseUnit> unitBase, NameSymbol ns, + DoubleFunction<V> converterFrom, ToDoubleFunction<V> converterTo) { + super(unitBase, ns); + this.converterFrom = converterFrom; + this.converterTo = converterTo; + } + + @Override + protected V convertFromBase(double value) { + return this.converterFrom.apply(value); + } + + @Override + protected double convertToBase(V value) { + return this.converterTo.applyAsDouble(value); + } + +} diff --git a/src/main/java/org/unitConverter/unit/LinearUnit.java b/src/main/java/org/unitConverter/unit/LinearUnit.java new file mode 100644 index 0000000..b7f33d5 --- /dev/null +++ b/src/main/java/org/unitConverter/unit/LinearUnit.java @@ -0,0 +1,441 @@ +/** + * Copyright (C) 2019 Adrien Hopkins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ +package org.unitConverter.unit; + +import java.util.Objects; + +import org.unitConverter.math.DecimalComparison; +import org.unitConverter.math.ObjectProduct; +import org.unitConverter.math.UncertainDouble; + +/** + * A unit that can be expressed as a product of its base and a number. For + * example, kilometres, inches and pounds. + * + * @author Adrien Hopkins + * @since 2019-10-16 + */ +public final class LinearUnit extends Unit { + /** + * Gets a {@code LinearUnit} from a unit and a value. For example, converts + * '59 °F' to a linear unit with the value of '288.15 K' + * + * @param unit unit to convert + * @param value value to convert + * @return value expressed as a {@code LinearUnit} + * @since 2019-10-16 + * @throws NullPointerException if unit is null + */ + public static LinearUnit fromUnitValue(final Unit unit, final double value) { + return new LinearUnit( + Objects.requireNonNull(unit, "unit must not be null.").getBase(), + unit.convertToBase(value), NameSymbol.EMPTY); + } + + /** + * Gets a {@code LinearUnit} from a unit and a value. For example, converts + * '59 °F' to a linear unit with the value of '288.15 K' + * + * @param unit unit to convert + * @param value value to convert + * @param ns name(s) and symbol of unit + * @return value expressed as a {@code LinearUnit} + * @since 2019-10-21 + * @throws NullPointerException if unit or ns is null + */ + public static LinearUnit fromUnitValue(final Unit unit, final double value, + final NameSymbol ns) { + return new LinearUnit( + Objects.requireNonNull(unit, "unit must not be null.").getBase(), + unit.convertToBase(value), ns); + } + + /** + * @return the base unit associated with {@code unit}, as a + * {@code LinearUnit}. + * @since 2020-10-02 + */ + public static LinearUnit getBase(final Unit unit) { + return new LinearUnit(unit.getBase(), 1, NameSymbol.EMPTY); + } + + /** + * @return the base unit associated with {@code unitlike}, as a + * {@code LinearUnit}. + * @since 2020-10-02 + */ + public static LinearUnit getBase(final Unitlike<?> unit) { + return new LinearUnit(unit.getBase(), 1, NameSymbol.EMPTY); + } + + /** + * Gets a {@code LinearUnit} from a unit base and a conversion factor. In + * other words, gets the product of {@code unitBase} and + * {@code conversionFactor}, expressed as a {@code LinearUnit}. + * + * @param unitBase unit base to multiply by + * @param conversionFactor number to multiply base by + * @return product of base and conversion factor + * @since 2019-10-16 + * @throws NullPointerException if unitBase is null + */ + public static LinearUnit valueOf(final ObjectProduct<BaseUnit> unitBase, + final double conversionFactor) { + return new LinearUnit(unitBase, conversionFactor, NameSymbol.EMPTY); + } + + /** + * Gets a {@code LinearUnit} from a unit base and a conversion factor. In + * other words, gets the product of {@code unitBase} and + * {@code conversionFactor}, expressed as a {@code LinearUnit}. + * + * @param unitBase unit base to multiply by + * @param conversionFactor number to multiply base by + * @param ns name(s) and symbol of unit + * @return product of base and conversion factor + * @since 2019-10-21 + * @throws NullPointerException if unitBase is null + */ + public static LinearUnit valueOf(final ObjectProduct<BaseUnit> unitBase, + final double conversionFactor, final NameSymbol ns) { + return new LinearUnit(unitBase, conversionFactor, ns); + } + + /** + * The value of this unit as represented in its base form. Mathematically, + * + * <pre> + * this = conversionFactor * getBase() + * </pre> + * + * @since 2019-10-16 + */ + private final double conversionFactor; + + /** + * Creates the {@code LinearUnit}. + * + * @param unitBase base of linear unit + * @param conversionFactor conversion factor between base and unit + * @since 2019-10-16 + */ + private LinearUnit(final ObjectProduct<BaseUnit> unitBase, + final double conversionFactor, final NameSymbol ns) { + super(unitBase, ns); + this.conversionFactor = conversionFactor; + } + + /** + * {@inheritDoc} + * + * Converts by dividing by {@code conversionFactor} + */ + @Override + protected double convertFromBase(final double value) { + return value / this.getConversionFactor(); + } + + /** + * Converts an {@code UncertainDouble} value expressed in this unit to an + * {@code UncertainValue} value expressed in {@code other}. + * + * @param other unit to convert to + * @param value value to convert + * @return converted value + * @since 2019-09-07 + * @throws IllegalArgumentException if {@code other} is incompatible for + * conversion with this unit (as tested by + * {@link Unit#canConvertTo}). + * @throws NullPointerException if value or other is null + */ + public UncertainDouble convertTo(LinearUnit other, UncertainDouble value) { + Objects.requireNonNull(other, "other must not be null."); + Objects.requireNonNull(value, "value may not be null."); + if (this.canConvertTo(other)) + return value.timesExact( + this.getConversionFactor() / other.getConversionFactor()); + else + throw new IllegalArgumentException( + String.format("Cannot convert from %s to %s.", this, other)); + + } + + /** + * {@inheritDoc} + * + * Converts by multiplying by {@code conversionFactor} + */ + @Override + protected double convertToBase(final double value) { + return value * this.getConversionFactor(); + } + + /** + * Converts an {@code UncertainDouble} to the base unit. + * + * @since 2020-09-07 + */ + UncertainDouble convertToBase(final UncertainDouble value) { + return value.timesExact(this.getConversionFactor()); + } + + /** + * Divides this unit by a scalar. + * + * @param divisor scalar to divide by + * @return quotient + * @since 2018-12-23 + * @since v0.1.0 + */ + public LinearUnit dividedBy(final double divisor) { + return valueOf(this.getBase(), this.getConversionFactor() / divisor); + } + + /** + * Returns the quotient of this unit and another. + * + * @param divisor unit to divide by + * @return quotient of two units + * @throws NullPointerException if {@code divisor} is null + * @since 2018-12-22 + * @since v0.1.0 + */ + public LinearUnit dividedBy(final LinearUnit divisor) { + Objects.requireNonNull(divisor, "other must not be null"); + + // divide the units + final ObjectProduct<BaseUnit> base = this.getBase() + .dividedBy(divisor.getBase()); + return valueOf(base, + this.getConversionFactor() / divisor.getConversionFactor()); + } + + /** + * {@inheritDoc} + * + * Uses the base and conversion factor of units to test for equality. + */ + @Override + public boolean equals(final Object obj) { + if (!(obj instanceof LinearUnit)) + return false; + final LinearUnit other = (LinearUnit) obj; + return Objects.equals(this.getBase(), other.getBase()) + && DecimalComparison.equals(this.getConversionFactor(), + other.getConversionFactor()); + } + + /** + * @return conversion factor + * @since 2019-10-16 + */ + public double getConversionFactor() { + return this.conversionFactor; + } + + /** + * {@inheritDoc} + * + * Uses the base and conversion factor to compute a hash code. + */ + @Override + public int hashCode() { + return 31 * this.getBase().hashCode() + + DecimalComparison.hash(this.getConversionFactor()); + } + + /** + * @return whether this unit is equivalent to a {@code BaseUnit} (i.e. there + * is a {@code BaseUnit b} where + * {@code b.asLinearUnit().equals(this)} returns {@code true}.) + * @since 2019-10-16 + */ + public boolean isBase() { + return this.isCoherent() && this.getBase().isSingleObject(); + } + + /** + * @return whether this unit is coherent (i.e. has conversion factor 1) + * @since 2019-10-16 + */ + public boolean isCoherent() { + return this.getConversionFactor() == 1; + } + + /** + * Returns the difference of this unit and another. + * <p> + * Two units can be subtracted if they have the same base. Note that + * {@link #canConvertTo} can be used to determine this. If {@code subtrahend} + * does not meet this condition, an {@code IllegalArgumentException} will be + * thrown. + * </p> + * + * @param subtrahend unit to subtract + * @return difference of units + * @throws IllegalArgumentException if {@code subtrahend} is not compatible + * for subtraction as described above + * @throws NullPointerException if {@code subtrahend} is null + * @since 2019-03-17 + * @since v0.2.0 + */ + public LinearUnit minus(final LinearUnit subtrahend) { + Objects.requireNonNull(subtrahend, "addend must not be null."); + + // reject subtrahends that cannot be added to this unit + if (!this.getBase().equals(subtrahend.getBase())) + throw new IllegalArgumentException(String.format( + "Incompatible units for subtraction \"%s\" and \"%s\".", this, + subtrahend)); + + // subtract the units + return valueOf(this.getBase(), + this.getConversionFactor() - subtrahend.getConversionFactor()); + } + + /** + * Returns the sum of this unit and another. + * <p> + * Two units can be added if they have the same base. Note that + * {@link #canConvertTo} can be used to determine this. If {@code addend} + * does not meet this condition, an {@code IllegalArgumentException} will be + * thrown. + * </p> + * + * @param addend unit to add + * @return sum of units + * @throws IllegalArgumentException if {@code addend} is not compatible for + * addition as described above + * @throws NullPointerException if {@code addend} is null + * @since 2019-03-17 + * @since v0.2.0 + */ + public LinearUnit plus(final LinearUnit addend) { + Objects.requireNonNull(addend, "addend must not be null."); + + // reject addends that cannot be added to this unit + if (!this.getBase().equals(addend.getBase())) + throw new IllegalArgumentException(String.format( + "Incompatible units for addition \"%s\" and \"%s\".", this, + addend)); + + // add the units + return valueOf(this.getBase(), + this.getConversionFactor() + addend.getConversionFactor()); + } + + /** + * Multiplies this unit by a scalar. + * + * @param multiplier scalar to multiply by + * @return product + * @since 2018-12-23 + * @since v0.1.0 + */ + public LinearUnit times(final double multiplier) { + return valueOf(this.getBase(), this.getConversionFactor() * multiplier); + } + + /** + * Returns the product of this unit and another. + * + * @param multiplier unit to multiply by + * @return product of two units + * @throws NullPointerException if {@code multiplier} is null + * @since 2018-12-22 + * @since v0.1.0 + */ + public LinearUnit times(final LinearUnit multiplier) { + Objects.requireNonNull(multiplier, "other must not be null"); + + // multiply the units + final ObjectProduct<BaseUnit> base = this.getBase() + .times(multiplier.getBase()); + return valueOf(base, + this.getConversionFactor() * multiplier.getConversionFactor()); + } + + /** + * Returns this unit but to an exponent. + * + * @param exponent exponent to exponentiate unit to + * @return exponentiated unit + * @since 2019-01-15 + * @since v0.1.0 + */ + public LinearUnit toExponent(final int exponent) { + return valueOf(this.getBase().toExponent(exponent), + Math.pow(this.conversionFactor, exponent)); + } + + /** + * @return a string providing a definition of this unit + * @since 2019-10-21 + */ + @Override + public String toString() { + return this.getPrimaryName().orElse("Unnamed unit") + + (this.getSymbol().isPresent() + ? String.format(" (%s)", this.getSymbol().get()) + : "") + + ", " + Double.toString(this.conversionFactor) + " * " + + this.getBase().toString(u -> u.getSymbol().get()); + } + + @Override + public LinearUnit withName(final NameSymbol ns) { + return valueOf(this.getBase(), this.getConversionFactor(), ns); + } + + /** + * Returns the result of applying {@code prefix} to this unit. + * <p> + * If this unit and the provided prefix have a primary name, the returned + * unit will have a primary name (prefix's name + unit's name). <br> + * If this unit and the provided prefix have a symbol, the returned unit will + * have a symbol. <br> + * This method ignores alternate names of both this unit and the provided + * prefix. + * + * @param prefix prefix to apply + * @return unit with prefix + * @since 2019-03-18 + * @since v0.2.0 + * @throws NullPointerException if prefix is null + */ + public LinearUnit withPrefix(final UnitPrefix prefix) { + final LinearUnit unit = this.times(prefix.getMultiplier()); + + // create new name and symbol, if possible + final String name; + if (this.getPrimaryName().isPresent() + && prefix.getPrimaryName().isPresent()) { + name = prefix.getPrimaryName().get() + this.getPrimaryName().get(); + } else { + name = null; + } + + final String symbol; + if (this.getSymbol().isPresent() && prefix.getSymbol().isPresent()) { + symbol = prefix.getSymbol().get() + this.getSymbol().get(); + } else { + symbol = null; + } + + return unit.withName(NameSymbol.ofNullable(name, symbol)); + } +} diff --git a/src/main/java/org/unitConverter/unit/LinearUnitValue.java b/src/main/java/org/unitConverter/unit/LinearUnitValue.java new file mode 100644 index 0000000..8de734e --- /dev/null +++ b/src/main/java/org/unitConverter/unit/LinearUnitValue.java @@ -0,0 +1,341 @@ +/** + * Copyright (C) 2019 Adrien Hopkins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ +package org.unitConverter.unit; + +import java.util.Objects; +import java.util.Optional; + +import org.unitConverter.math.DecimalComparison; +import org.unitConverter.math.UncertainDouble; + +/** + * A possibly uncertain value expressed in a linear unit. + * + * Unless otherwise indicated, all methods in this class throw a + * {@code NullPointerException} when an argument is null. + * + * @author Adrien Hopkins + * @since 2020-07-26 + */ +public final class LinearUnitValue { + public static final LinearUnitValue ONE = getExact(SI.ONE, 1); + + /** + * Gets an exact {@code LinearUnitValue} + * + * @param unit unit to express with + * @param value value to express + * @return exact {@code LinearUnitValue} instance + * @since 2020-07-26 + */ + public static final LinearUnitValue getExact(final LinearUnit unit, + final double value) { + return new LinearUnitValue( + Objects.requireNonNull(unit, "unit must not be null"), + UncertainDouble.of(value, 0)); + } + + /** + * Gets an uncertain {@code LinearUnitValue} + * + * @param unit unit to express with + * @param value value to express + * @param uncertainty absolute uncertainty of value + * @return uncertain {@code LinearUnitValue} instance + * @since 2020-07-26 + */ + public static final LinearUnitValue of(final LinearUnit unit, + final UncertainDouble value) { + return new LinearUnitValue( + Objects.requireNonNull(unit, "unit must not be null"), + Objects.requireNonNull(value, "value may not be null")); + } + + private final LinearUnit unit; + + private final UncertainDouble value; + + /** + * @param unit unit to express as + * @param value value to express + * @since 2020-07-26 + */ + private LinearUnitValue(final LinearUnit unit, final UncertainDouble value) { + this.unit = unit; + this.value = value; + } + + /** + * @return this value as a {@code UnitValue}. All uncertainty information is + * removed from the returned value. + * @since 2020-08-04 + */ + public final UnitValue asUnitValue() { + return UnitValue.of(this.unit, this.value.value()); + } + + /** + * @param other a {@code LinearUnit} + * @return true iff this value can be represented with {@code other}. + * @since 2020-07-26 + */ + public final boolean canConvertTo(final LinearUnit other) { + return this.unit.canConvertTo(other); + } + + /** + * Returns a LinearUnitValue that represents the same value expressed in a + * different unit + * + * @param other new unit to express value in + * @return value expressed in {@code other} + * @since 2020-07-26 + */ + public final LinearUnitValue convertTo(final LinearUnit other) { + return LinearUnitValue.of(other, this.unit.convertTo(other, this.value)); + } + + /** + * Divides this value by a scalar + * + * @param divisor value to divide by + * @return multiplied value + * @since 2020-07-28 + */ + public LinearUnitValue dividedBy(final double divisor) { + return LinearUnitValue.of(this.unit, this.value.dividedByExact(divisor)); + } + + /** + * Divides this value by another value + * + * @param divisor value to multiply by + * @return quotient + * @since 2020-07-28 + */ + public LinearUnitValue dividedBy(final LinearUnitValue divisor) { + return LinearUnitValue.of(this.unit.dividedBy(divisor.unit), + this.value.dividedBy(divisor.value)); + } + + /** + * Returns true if this and obj represent the same value, regardless of + * whether or not they are expressed in the same unit. So (1000 m).equals(1 + * km) returns true. + * + * @since 2020-07-26 + * @see #equals(Object, boolean) + */ + @Override + public boolean equals(final Object obj) { + if (!(obj instanceof LinearUnitValue)) + return false; + final LinearUnitValue other = (LinearUnitValue) obj; + return Objects.equals(this.unit.getBase(), other.unit.getBase()) + && this.unit.convertToBase(this.value) + .equals(other.unit.convertToBase(other.value)); + } + + /** + * Returns true if this and obj represent the same value, regardless of + * whether or not they are expressed in the same unit. So (1000 m).equals(1 + * km) returns true. + * <p> + * If avoidFPErrors is true, this method will attempt to avoid floating-point + * errors, at the cost of not always being transitive. + * + * @since 2020-07-28 + */ + public boolean equals(final Object obj, final boolean avoidFPErrors) { + if (!avoidFPErrors) + return this.equals(obj); + if (!(obj instanceof LinearUnitValue)) + return false; + final LinearUnitValue other = (LinearUnitValue) obj; + return Objects.equals(this.unit.getBase(), other.unit.getBase()) + && DecimalComparison.equals(this.unit.convertToBase(this.value), + other.unit.convertToBase(other.value)); + } + + /** + * @param other another {@code LinearUnitValue} + * @return true iff this and other are within each other's uncertainty range + * + * @since 2020-07-26 + */ + public boolean equivalent(final LinearUnitValue other) { + if (other == null + || !Objects.equals(this.unit.getBase(), other.unit.getBase())) + return false; + final LinearUnit base = LinearUnit.valueOf(this.unit.getBase(), 1); + final LinearUnitValue thisBase = this.convertTo(base); + final LinearUnitValue otherBase = other.convertTo(base); + + return thisBase.value.equivalent(otherBase.value); + } + + /** + * @return the unit + * @since 2020-09-29 + */ + public final LinearUnit getUnit() { + return this.unit; + } + + /** + * @return the value + * @since 2020-09-29 + */ + public final UncertainDouble getValue() { + return this.value; + } + + /** + * @return the exact value + * @since 2020-09-07 + */ + public final double getValueExact() { + return this.value.value(); + } + + @Override + public int hashCode() { + return Objects.hash(this.unit.getBase(), + this.unit.convertToBase(this.getValue())); + } + + /** + * Returns the difference of this value and another, expressed in this + * value's unit + * + * @param subtrahend value to subtract + * @return difference of values + * @throws IllegalArgumentException if {@code subtrahend} has a unit that is + * not compatible for addition + * @since 2020-07-26 + */ + public LinearUnitValue minus(final LinearUnitValue subtrahend) { + Objects.requireNonNull(subtrahend, "subtrahend may not be null"); + + if (!this.canConvertTo(subtrahend.unit)) + throw new IllegalArgumentException(String.format( + "Incompatible units for subtraction \"%s\" and \"%s\".", + this.unit, subtrahend.unit)); + + final LinearUnitValue otherConverted = subtrahend.convertTo(this.unit); + return LinearUnitValue.of(this.unit, + this.value.minus(otherConverted.value)); + } + + /** + * Returns the sum of this value and another, expressed in this value's unit + * + * @param addend value to add + * @return sum of values + * @throws IllegalArgumentException if {@code addend} has a unit that is not + * compatible for addition + * @since 2020-07-26 + */ + public LinearUnitValue plus(final LinearUnitValue addend) { + Objects.requireNonNull(addend, "addend may not be null"); + + if (!this.canConvertTo(addend.unit)) + throw new IllegalArgumentException(String.format( + "Incompatible units for addition \"%s\" and \"%s\".", this.unit, + addend.unit)); + + final LinearUnitValue otherConverted = addend.convertTo(this.unit); + return LinearUnitValue.of(this.unit, + this.value.plus(otherConverted.value)); + } + + /** + * Multiplies this value by a scalar + * + * @param multiplier value to multiply by + * @return multiplied value + * @since 2020-07-28 + */ + public LinearUnitValue times(final double multiplier) { + return LinearUnitValue.of(this.unit, this.value.timesExact(multiplier)); + } + + /** + * Multiplies this value by another value + * + * @param multiplier value to multiply by + * @return product + * @since 2020-07-28 + */ + public LinearUnitValue times(final LinearUnitValue multiplier) { + return LinearUnitValue.of(this.unit.times(multiplier.unit), + this.value.times(multiplier.value)); + } + + /** + * Raises a value to an exponent + * + * @param exponent exponent to raise to + * @return result of exponentiation + * @since 2020-07-28 + */ + public LinearUnitValue toExponent(final int exponent) { + return LinearUnitValue.of(this.unit.toExponent(exponent), + this.value.toExponentExact(exponent)); + } + + @Override + public String toString() { + return this.toString(!this.value.isExact()); + } + + /** + * Returns a string representing the object. <br> + * If the attached unit has a name or symbol, the string looks like "12 km". + * Otherwise, it looks like "13 unnamed unit (= 2 m/s)". + * <p> + * If showUncertainty is true, strings like "35 ± 8" are shown instead of + * single numbers. + * <p> + * Non-exact values are rounded intelligently based on their uncertainty. + * + * @since 2020-07-26 + */ + public String toString(final boolean showUncertainty) { + final Optional<String> primaryName = this.unit.getPrimaryName(); + final Optional<String> symbol = this.unit.getSymbol(); + final String chosenName = symbol.orElse(primaryName.orElse(null)); + + final UncertainDouble baseValue = this.unit.convertToBase(this.value); + + // get rounded strings + // if showUncertainty is true, add brackets around the string + final String valueString = showUncertainty ? "(" + : "" + this.value.toString(showUncertainty) + + (showUncertainty ? ")" : ""); + final String baseValueString = showUncertainty ? "(" + : "" + baseValue.toString(showUncertainty) + + (showUncertainty ? ")" : ""); + + // create string + if (primaryName.isEmpty() && symbol.isEmpty()) + return String.format("%s unnamed unit (= %s %s)", valueString, + baseValueString, this.unit.getBase()); + else + return String.format("%s %s", valueString, chosenName); + } +} diff --git a/src/main/java/org/unitConverter/unit/MultiUnit.java b/src/main/java/org/unitConverter/unit/MultiUnit.java new file mode 100644 index 0000000..a1623f8 --- /dev/null +++ b/src/main/java/org/unitConverter/unit/MultiUnit.java @@ -0,0 +1,160 @@ +/** + * Copyright (C) 2020 Adrien Hopkins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ +package org.unitConverter.unit; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.unitConverter.math.ObjectProduct; + +/** + * A combination of units, like "5 foot + 7 inch". All but the last units should + * have a whole number value associated with them. + * + * @since 2020-10-02 + */ +public final class MultiUnit extends Unitlike<List<Double>> { + /** + * Creates a {@code MultiUnit} from its units. It will not have a name or + * symbol. + * + * @since 2020-10-03 + */ + public static final MultiUnit of(LinearUnit... units) { + return of(Arrays.asList(units)); + } + + /** + * Creates a {@code MultiUnit} from its units. It will not have a name or + * symbol. + * + * @since 2020-10-03 + */ + public static final MultiUnit of(List<LinearUnit> units) { + if (units.size() < 1) + throw new IllegalArgumentException("Must have at least one unit"); + final ObjectProduct<BaseUnit> unitBase = units.get(0).getBase(); + for (final LinearUnit unit : units) { + if (!unitBase.equals(unit.getBase())) + throw new IllegalArgumentException( + "All units must have the same base."); + } + return new MultiUnit(new ArrayList<>(units), unitBase, NameSymbol.EMPTY); + } + + /** + * The units that make up this value. + */ + private final List<LinearUnit> units; + + /** + * Creates a {@code MultiUnit}. + * + * @since 2020-10-03 + */ + private MultiUnit(List<LinearUnit> units, ObjectProduct<BaseUnit> unitBase, + NameSymbol ns) { + super(unitBase, ns); + this.units = units; + } + + @Override + protected List<Double> convertFromBase(double value) { + final List<Double> values = new ArrayList<>(this.units.size()); + double temp = value; + + for (final LinearUnit unit : this.units.subList(0, + this.units.size() - 1)) { + values.add(Math.floor(temp / unit.getConversionFactor())); + temp %= unit.getConversionFactor(); + } + + values.add(this.units.size() - 1, + this.units.get(this.units.size() - 1).convertFromBase(temp)); + + return values; + } + + /** + * Converts a value expressed in this unitlike form to a value expressed in + * {@code other}. + * + * @implSpec If conversion is possible, this implementation returns + * {@code other.convertFromBase(this.convertToBase(value))}. + * Therefore, overriding either of those methods will change the + * output of this method. + * + * @param other unit to convert to + * @param value value to convert + * @return converted value + * @since 2020-10-03 + * @throws IllegalArgumentException if {@code other} is incompatible for + * conversion with this unitlike form (as + * tested by {@link Unit#canConvertTo}). + * @throws NullPointerException if other is null + */ + public final <U extends Unitlike<V>, V> V convertTo(U other, + double... values) { + final List<Double> valueList = new ArrayList<>(values.length); + for (final double d : values) { + valueList.add(d); + } + + return this.convertTo(other, valueList); + } + + /** + * Converts a value expressed in this unitlike form to a value expressed in + * {@code other}. + * + * @implSpec If conversion is possible, this implementation returns + * {@code other.convertFromBase(this.convertToBase(value))}. + * Therefore, overriding either of those methods will change the + * output of this method. + * + * @param other unit to convert to + * @param value value to convert + * @return converted value + * @since 2020-10-03 + * @throws IllegalArgumentException if {@code other} is incompatible for + * conversion with this unitlike form (as + * tested by {@link Unit#canConvertTo}). + * @throws NullPointerException if other is null + */ + public final double convertTo(Unit other, double... values) { + final List<Double> valueList = new ArrayList<>(values.length); + for (final double d : values) { + valueList.add(d); + } + + return this.convertTo(other, valueList); + } + + @Override + protected double convertToBase(List<Double> value) { + if (value.size() != this.units.size()) + throw new IllegalArgumentException("Wrong number of values for " + + this.units.size() + "-unit MultiUnit."); + + double baseValue = 0; + for (int i = 0; i < this.units.size(); i++) { + baseValue += value.get(i) * this.units.get(i).getConversionFactor(); + } + return baseValue; + } +} diff --git a/src/main/java/org/unitConverter/unit/NameSymbol.java b/src/main/java/org/unitConverter/unit/NameSymbol.java new file mode 100644 index 0000000..8d8302a --- /dev/null +++ b/src/main/java/org/unitConverter/unit/NameSymbol.java @@ -0,0 +1,280 @@ +/** + * Copyright (C) 2019 Adrien Hopkins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ +package org.unitConverter.unit; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; + +/** + * A class that can be used to specify names and a symbol for a unit. + * + * @author Adrien Hopkins + * @since 2019-10-21 + */ +public final class NameSymbol { + public static final NameSymbol EMPTY = new NameSymbol(Optional.empty(), + Optional.empty(), new HashSet<>()); + + /** + * Creates a {@code NameSymbol}, ensuring that if primaryName is null and + * otherNames is not empty, one name is moved from otherNames to primaryName + * + * Ensure that otherNames is a copy of the inputted argument. + */ + private static final NameSymbol create(final String name, + final String symbol, final Set<String> otherNames) { + final Optional<String> primaryName; + + if (name == null && !otherNames.isEmpty()) { + // get primary name and remove it from savedNames + final Iterator<String> it = otherNames.iterator(); + assert it.hasNext(); + primaryName = Optional.of(it.next()); + otherNames.remove(primaryName.get()); + } else { + primaryName = Optional.ofNullable(name); + } + + return new NameSymbol(primaryName, Optional.ofNullable(symbol), + otherNames); + } + + /** + * Gets a {@code NameSymbol} with a primary name, a symbol and no other + * names. + * + * @param name name to use + * @param symbol symbol to use + * @return NameSymbol instance + * @since 2019-10-21 + * @throws NullPointerException if name or symbol is null + */ + public static final NameSymbol of(final String name, final String symbol) { + return new NameSymbol(Optional.of(name), Optional.of(symbol), + new HashSet<>()); + } + + /** + * Gets a {@code NameSymbol} with a primary name, a symbol and additional + * names. + * + * @param name name to use + * @param symbol symbol to use + * @param otherNames other names to use + * @return NameSymbol instance + * @since 2019-10-21 + * @throws NullPointerException if any argument is null + */ + public static final NameSymbol of(final String name, final String symbol, + final Set<String> otherNames) { + return new NameSymbol(Optional.of(name), Optional.of(symbol), + new HashSet<>(Objects.requireNonNull(otherNames, + "otherNames must not be null."))); + } + + /** + * h * Gets a {@code NameSymbol} with a primary name, a symbol and additional + * names. + * + * @param name name to use + * @param symbol symbol to use + * @param otherNames other names to use + * @return NameSymbol instance + * @since 2019-10-21 + * @throws NullPointerException if any argument is null + */ + public static final NameSymbol of(final String name, final String symbol, + final String... otherNames) { + return new NameSymbol(Optional.of(name), Optional.of(symbol), + new HashSet<>(Arrays.asList(Objects.requireNonNull(otherNames, + "otherNames must not be null.")))); + } + + /** + * Gets a {@code NameSymbol} with a primary name, no symbol, and no other + * names. + * + * @param name name to use + * @return NameSymbol instance + * @since 2019-10-21 + * @throws NullPointerException if name is null + */ + public static final NameSymbol ofName(final String name) { + return new NameSymbol(Optional.of(name), Optional.empty(), + new HashSet<>()); + } + + /** + * Gets a {@code NameSymbol} with a primary name, a symbol and additional + * names. + * <p> + * If any argument is null, this static factory replaces it with an empty + * Optional or empty Set. + * <p> + * If {@code name} is null and {@code otherNames} is not empty, a primary + * name will be picked from {@code otherNames}. This name will not appear in + * getOtherNames(). + * + * @param name name to use + * @param symbol symbol to use + * @param otherNames other names to use + * @return NameSymbol instance + * @since 2019-11-26 + */ + public static final NameSymbol ofNullable(final String name, + final String symbol, final Set<String> otherNames) { + return NameSymbol.create(name, symbol, + otherNames == null ? new HashSet<>() : new HashSet<>(otherNames)); + } + + /** + * h * Gets a {@code NameSymbol} with a primary name, a symbol and additional + * names. + * <p> + * If any argument is null, this static factory replaces it with an empty + * Optional or empty Set. + * <p> + * If {@code name} is null and {@code otherNames} is not empty, a primary + * name will be picked from {@code otherNames}. This name will not appear in + * getOtherNames(). + * + * @param name name to use + * @param symbol symbol to use + * @param otherNames other names to use + * @return NameSymbol instance + * @since 2019-11-26 + */ + public static final NameSymbol ofNullable(final String name, + final String symbol, final String... otherNames) { + return create(name, symbol, otherNames == null ? new HashSet<>() + : new HashSet<>(Arrays.asList(otherNames))); + } + + /** + * Gets a {@code NameSymbol} with a symbol and no names. + * + * @param symbol symbol to use + * @return NameSymbol instance + * @since 2019-10-21 + * @throws NullPointerException if symbol is null + */ + public static final NameSymbol ofSymbol(final String symbol) { + return new NameSymbol(Optional.empty(), Optional.of(symbol), + new HashSet<>()); + } + + private final Optional<String> primaryName; + private final Optional<String> symbol; + + private final Set<String> otherNames; + + /** + * Creates the {@code NameSymbol}. + * + * @param primaryName primary name of unit + * @param symbol symbol used to represent unit + * @param otherNames other names and/or spellings, should be a mutable copy + * of the argument + * @since 2019-10-21 + */ + private NameSymbol(final Optional<String> primaryName, + final Optional<String> symbol, final Set<String> otherNames) { + this.primaryName = primaryName; + this.symbol = symbol; + otherNames.remove(null); + this.otherNames = Collections.unmodifiableSet(otherNames); + + if (this.primaryName.isEmpty()) { + assert this.otherNames.isEmpty(); + } + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (!(obj instanceof NameSymbol)) + return false; + final NameSymbol other = (NameSymbol) obj; + if (this.otherNames == null) { + if (other.otherNames != null) + return false; + } else if (!this.otherNames.equals(other.otherNames)) + return false; + if (this.primaryName == null) { + if (other.primaryName != null) + return false; + } else if (!this.primaryName.equals(other.primaryName)) + return false; + if (this.symbol == null) { + if (other.symbol != null) + return false; + } else if (!this.symbol.equals(other.symbol)) + return false; + return true; + } + + /** + * @return otherNames + * @since 2019-10-21 + */ + public final Set<String> getOtherNames() { + return this.otherNames; + } + + /** + * @return primaryName + * @since 2019-10-21 + */ + public final Optional<String> getPrimaryName() { + return this.primaryName; + } + + /** + * @return symbol + * @since 2019-10-21 + */ + public final Optional<String> getSymbol() { + return this.symbol; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + + (this.otherNames == null ? 0 : this.otherNames.hashCode()); + result = prime * result + + (this.primaryName == null ? 0 : this.primaryName.hashCode()); + result = prime * result + + (this.symbol == null ? 0 : this.symbol.hashCode()); + return result; + } + + /** + * @return true iff this {@code NameSymbol} contains no names or symbols. + */ + public final boolean isEmpty() { + // if primaryName is empty, otherNames must also be empty + return this.primaryName.isEmpty() && this.symbol.isEmpty(); + } +}
\ No newline at end of file diff --git a/src/main/java/org/unitConverter/unit/Nameable.java b/src/main/java/org/unitConverter/unit/Nameable.java new file mode 100644 index 0000000..36740ab --- /dev/null +++ b/src/main/java/org/unitConverter/unit/Nameable.java @@ -0,0 +1,59 @@ +/** + * Copyright (C) 2020 Adrien Hopkins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ +package org.unitConverter.unit; + +import java.util.Optional; +import java.util.Set; + +/** + * An object that can hold one or more names, and possibly a symbol. The name + * and symbol data should be immutable. + * + * @since 2020-09-07 + */ +public interface Nameable { + /** + * @return a {@code NameSymbol} that contains this object's primary name, + * symbol and other names + * @since 2020-09-07 + */ + NameSymbol getNameSymbol(); + + /** + * @return set of alternate names + * @since 2020-09-07 + */ + default Set<String> getOtherNames() { + return this.getNameSymbol().getOtherNames(); + } + + /** + * @return preferred name of object + * @since 2020-09-07 + */ + default Optional<String> getPrimaryName() { + return this.getNameSymbol().getPrimaryName(); + } + + /** + * @return short symbol representing object + * @since 2020-09-07 + */ + default Optional<String> getSymbol() { + return this.getNameSymbol().getSymbol(); + } +} diff --git a/src/main/java/org/unitConverter/unit/SI.java b/src/main/java/org/unitConverter/unit/SI.java new file mode 100644 index 0000000..81736f3 --- /dev/null +++ b/src/main/java/org/unitConverter/unit/SI.java @@ -0,0 +1,479 @@ +/** + * Copyright (C) 2018 Adrien Hopkins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ +package org.unitConverter.unit; + +import java.util.Set; + +import org.unitConverter.math.ObjectProduct; + +/** + * All of the units, prefixes and dimensions that are used by the SI, as well as + * some outside the SI. + * + * <p> + * This class does not include prefixed units. To obtain prefixed units, use + * {@link LinearUnit#withPrefix}: + * + * <pre> + * LinearUnit KILOMETRE = SI.METRE.withPrefix(SI.KILO); + * </pre> + * + * + * @author Adrien Hopkins + * @since 2019-10-16 + */ +public final class SI { + /// dimensions used by SI units + // base dimensions, as BaseDimensions + public static final class BaseDimensions { + public static final BaseDimension LENGTH = BaseDimension.valueOf("Length", + "L"); + public static final BaseDimension MASS = BaseDimension.valueOf("Mass", + "M"); + public static final BaseDimension TIME = BaseDimension.valueOf("Time", + "T"); + public static final BaseDimension ELECTRIC_CURRENT = BaseDimension + .valueOf("Electric Current", "I"); + public static final BaseDimension TEMPERATURE = BaseDimension + .valueOf("Temperature", "\u0398"); // theta symbol + public static final BaseDimension QUANTITY = BaseDimension + .valueOf("Quantity", "N"); + public static final BaseDimension LUMINOUS_INTENSITY = BaseDimension + .valueOf("Luminous Intensity", "J"); + public static final BaseDimension INFORMATION = BaseDimension + .valueOf("Information", "Info"); // non-SI + public static final BaseDimension CURRENCY = BaseDimension + .valueOf("Currency", "$$"); // non-SI + + // You may NOT get SI.BaseDimensions instances! + private BaseDimensions() { + throw new AssertionError(); + } + } + + /// base units of the SI + // suppressing warnings since these are the same object, but in a different + /// form (class) + @SuppressWarnings("hiding") + public static final class BaseUnits { + public static final BaseUnit METRE = BaseUnit + .valueOf(BaseDimensions.LENGTH, "metre", "m"); + public static final BaseUnit KILOGRAM = BaseUnit + .valueOf(BaseDimensions.MASS, "kilogram", "kg"); + public static final BaseUnit SECOND = BaseUnit + .valueOf(BaseDimensions.TIME, "second", "s"); + public static final BaseUnit AMPERE = BaseUnit + .valueOf(BaseDimensions.ELECTRIC_CURRENT, "ampere", "A"); + public static final BaseUnit KELVIN = BaseUnit + .valueOf(BaseDimensions.TEMPERATURE, "kelvin", "K"); + public static final BaseUnit MOLE = BaseUnit + .valueOf(BaseDimensions.QUANTITY, "mole", "mol"); + public static final BaseUnit CANDELA = BaseUnit + .valueOf(BaseDimensions.LUMINOUS_INTENSITY, "candela", "cd"); + public static final BaseUnit BIT = BaseUnit + .valueOf(BaseDimensions.INFORMATION, "bit", "b"); + public static final BaseUnit DOLLAR = BaseUnit + .valueOf(BaseDimensions.CURRENCY, "dollar", "$"); + + public static final Set<BaseUnit> BASE_UNITS = Set.of(METRE, KILOGRAM, + SECOND, AMPERE, KELVIN, MOLE, CANDELA, BIT); + + // You may NOT get SI.BaseUnits instances! + private BaseUnits() { + throw new AssertionError(); + } + } + + /** + * Constants that relate to the SI or other systems. + * + * @author Adrien Hopkins + * @since 2019-11-08 + */ + public static final class Constants { + public static final LinearUnit EARTH_GRAVITY = METRE.dividedBy(SECOND) + .dividedBy(SECOND).times(9.80665); + } + + // dimensions used in the SI, as ObjectProducts + public static final class Dimensions { + public static final ObjectProduct<BaseDimension> EMPTY = ObjectProduct + .empty(); + public static final ObjectProduct<BaseDimension> LENGTH = ObjectProduct + .oneOf(BaseDimensions.LENGTH); + public static final ObjectProduct<BaseDimension> MASS = ObjectProduct + .oneOf(BaseDimensions.MASS); + public static final ObjectProduct<BaseDimension> TIME = ObjectProduct + .oneOf(BaseDimensions.TIME); + public static final ObjectProduct<BaseDimension> ELECTRIC_CURRENT = ObjectProduct + .oneOf(BaseDimensions.ELECTRIC_CURRENT); + public static final ObjectProduct<BaseDimension> TEMPERATURE = ObjectProduct + .oneOf(BaseDimensions.TEMPERATURE); + public static final ObjectProduct<BaseDimension> QUANTITY = ObjectProduct + .oneOf(BaseDimensions.QUANTITY); + public static final ObjectProduct<BaseDimension> LUMINOUS_INTENSITY = ObjectProduct + .oneOf(BaseDimensions.LUMINOUS_INTENSITY); + public static final ObjectProduct<BaseDimension> INFORMATION = ObjectProduct + .oneOf(BaseDimensions.INFORMATION); + public static final ObjectProduct<BaseDimension> CURRENCY = ObjectProduct + .oneOf(BaseDimensions.CURRENCY); + + // derived dimensions without named SI units + public static final ObjectProduct<BaseDimension> AREA = LENGTH + .times(LENGTH); + public static final ObjectProduct<BaseDimension> VOLUME = AREA + .times(LENGTH); + public static final ObjectProduct<BaseDimension> VELOCITY = LENGTH + .dividedBy(TIME); + public static final ObjectProduct<BaseDimension> ACCELERATION = VELOCITY + .dividedBy(TIME); + public static final ObjectProduct<BaseDimension> WAVENUMBER = EMPTY + .dividedBy(LENGTH); + public static final ObjectProduct<BaseDimension> MASS_DENSITY = MASS + .dividedBy(VOLUME); + public static final ObjectProduct<BaseDimension> SURFACE_DENSITY = MASS + .dividedBy(AREA); + public static final ObjectProduct<BaseDimension> SPECIFIC_VOLUME = VOLUME + .dividedBy(MASS); + public static final ObjectProduct<BaseDimension> CURRENT_DENSITY = ELECTRIC_CURRENT + .dividedBy(AREA); + public static final ObjectProduct<BaseDimension> MAGNETIC_FIELD_STRENGTH = ELECTRIC_CURRENT + .dividedBy(LENGTH); + public static final ObjectProduct<BaseDimension> CONCENTRATION = QUANTITY + .dividedBy(VOLUME); + public static final ObjectProduct<BaseDimension> MASS_CONCENTRATION = CONCENTRATION + .times(MASS); + public static final ObjectProduct<BaseDimension> LUMINANCE = LUMINOUS_INTENSITY + .dividedBy(AREA); + public static final ObjectProduct<BaseDimension> REFRACTIVE_INDEX = VELOCITY + .dividedBy(VELOCITY); + public static final ObjectProduct<BaseDimension> REFRACTIVE_PERMEABILITY = EMPTY + .times(EMPTY); + public static final ObjectProduct<BaseDimension> ANGLE = LENGTH + .dividedBy(LENGTH); + public static final ObjectProduct<BaseDimension> SOLID_ANGLE = AREA + .dividedBy(AREA); + + // derived dimensions with named SI units + public static final ObjectProduct<BaseDimension> FREQUENCY = EMPTY + .dividedBy(TIME); + public static final ObjectProduct<BaseDimension> FORCE = MASS + .times(ACCELERATION); + public static final ObjectProduct<BaseDimension> ENERGY = FORCE + .times(LENGTH); + public static final ObjectProduct<BaseDimension> POWER = ENERGY + .dividedBy(TIME); + public static final ObjectProduct<BaseDimension> ELECTRIC_CHARGE = ELECTRIC_CURRENT + .times(TIME); + public static final ObjectProduct<BaseDimension> VOLTAGE = ENERGY + .dividedBy(ELECTRIC_CHARGE); + public static final ObjectProduct<BaseDimension> CAPACITANCE = ELECTRIC_CHARGE + .dividedBy(VOLTAGE); + public static final ObjectProduct<BaseDimension> ELECTRIC_RESISTANCE = VOLTAGE + .dividedBy(ELECTRIC_CURRENT); + public static final ObjectProduct<BaseDimension> ELECTRIC_CONDUCTANCE = ELECTRIC_CURRENT + .dividedBy(VOLTAGE); + public static final ObjectProduct<BaseDimension> MAGNETIC_FLUX = VOLTAGE + .times(TIME); + public static final ObjectProduct<BaseDimension> MAGNETIC_FLUX_DENSITY = MAGNETIC_FLUX + .dividedBy(AREA); + public static final ObjectProduct<BaseDimension> INDUCTANCE = MAGNETIC_FLUX + .dividedBy(ELECTRIC_CURRENT); + public static final ObjectProduct<BaseDimension> LUMINOUS_FLUX = LUMINOUS_INTENSITY + .times(SOLID_ANGLE); + public static final ObjectProduct<BaseDimension> ILLUMINANCE = LUMINOUS_FLUX + .dividedBy(AREA); + public static final ObjectProduct<BaseDimension> SPECIFIC_ENERGY = ENERGY + .dividedBy(MASS); + public static final ObjectProduct<BaseDimension> CATALYTIC_ACTIVITY = QUANTITY + .dividedBy(TIME); + + // You may NOT get SI.Dimension instances! + private Dimensions() { + throw new AssertionError(); + } + } + + /// The units of the SI + public static final LinearUnit ONE = LinearUnit + .valueOf(ObjectProduct.empty(), 1); + + public static final LinearUnit METRE = BaseUnits.METRE.asLinearUnit() + .withName(NameSymbol.of("metre", "m", "meter")); + public static final LinearUnit KILOGRAM = BaseUnits.KILOGRAM.asLinearUnit() + .withName(NameSymbol.of("kilogram", "kg")); + public static final LinearUnit SECOND = BaseUnits.SECOND.asLinearUnit() + .withName(NameSymbol.of("second", "s", "sec")); + public static final LinearUnit AMPERE = BaseUnits.AMPERE.asLinearUnit() + .withName(NameSymbol.of("ampere", "A")); + public static final LinearUnit KELVIN = BaseUnits.KELVIN.asLinearUnit() + .withName(NameSymbol.of("kelvin", "K")); + public static final LinearUnit MOLE = BaseUnits.MOLE.asLinearUnit() + .withName(NameSymbol.of("mole", "mol")); + public static final LinearUnit CANDELA = BaseUnits.CANDELA.asLinearUnit() + .withName(NameSymbol.of("candela", "cd")); + public static final LinearUnit BIT = BaseUnits.BIT.asLinearUnit() + .withName(NameSymbol.of("bit", "b")); + public static final LinearUnit DOLLAR = BaseUnits.DOLLAR.asLinearUnit() + .withName(NameSymbol.of("dollar", "$")); + // Non-base units + public static final LinearUnit RADIAN = METRE.dividedBy(METRE) + .withName(NameSymbol.of("radian", "rad")); + + public static final LinearUnit STERADIAN = RADIAN.times(RADIAN) + .withName(NameSymbol.of("steradian", "sr")); + public static final LinearUnit HERTZ = ONE.dividedBy(SECOND) + .withName(NameSymbol.of("hertz", "Hz")); + // for periodic phenomena + public static final LinearUnit NEWTON = KILOGRAM.times(METRE) + .dividedBy(SECOND.times(SECOND)) + .withName(NameSymbol.of("newton", "N")); + public static final LinearUnit PASCAL = NEWTON.dividedBy(METRE.times(METRE)) + .withName(NameSymbol.of("pascal", "Pa")); + public static final LinearUnit JOULE = NEWTON.times(METRE) + .withName(NameSymbol.of("joule", "J")); + public static final LinearUnit WATT = JOULE.dividedBy(SECOND) + .withName(NameSymbol.of("watt", "W")); + public static final LinearUnit COULOMB = AMPERE.times(SECOND) + .withName(NameSymbol.of("coulomb", "C")); + public static final LinearUnit VOLT = JOULE.dividedBy(COULOMB) + .withName(NameSymbol.of("volt", "V")); + public static final LinearUnit FARAD = COULOMB.dividedBy(VOLT) + .withName(NameSymbol.of("farad", "F")); + public static final LinearUnit OHM = VOLT.dividedBy(AMPERE) + .withName(NameSymbol.of("ohm", "\u03A9")); // omega + public static final LinearUnit SIEMENS = ONE.dividedBy(OHM) + .withName(NameSymbol.of("siemens", "S")); + public static final LinearUnit WEBER = VOLT.times(SECOND) + .withName(NameSymbol.of("weber", "Wb")); + public static final LinearUnit TESLA = WEBER.dividedBy(METRE.times(METRE)) + .withName(NameSymbol.of("tesla", "T")); + public static final LinearUnit HENRY = WEBER.dividedBy(AMPERE) + .withName(NameSymbol.of("henry", "H")); + public static final LinearUnit LUMEN = CANDELA.times(STERADIAN) + .withName(NameSymbol.of("lumen", "lm")); + public static final LinearUnit LUX = LUMEN.dividedBy(METRE.times(METRE)) + .withName(NameSymbol.of("lux", "lx")); + public static final LinearUnit BEQUEREL = ONE.dividedBy(SECOND) + .withName(NameSymbol.of("bequerel", "Bq")); + // for activity referred to a nucleotide + public static final LinearUnit GRAY = JOULE.dividedBy(KILOGRAM) + .withName(NameSymbol.of("grey", "Gy")); + // for absorbed dose + public static final LinearUnit SIEVERT = JOULE.dividedBy(KILOGRAM) + .withName(NameSymbol.of("sievert", "Sv")); + // for dose equivalent + public static final LinearUnit KATAL = MOLE.dividedBy(SECOND) + .withName(NameSymbol.of("katal", "kat")); + // common derived units included for convenience + public static final LinearUnit GRAM = KILOGRAM.dividedBy(1000) + .withName(NameSymbol.of("gram", "g")); + + public static final LinearUnit SQUARE_METRE = METRE.toExponent(2) + .withName(NameSymbol.of("square metre", "m^2", "square meter", + "metre squared", "meter squared")); + public static final LinearUnit CUBIC_METRE = METRE.toExponent(3) + .withName(NameSymbol.of("cubic metre", "m^3", "cubic meter", + "metre cubed", "meter cubed")); + public static final LinearUnit METRE_PER_SECOND = METRE.dividedBy(SECOND) + .withName( + NameSymbol.of("metre per second", "m/s", "meter per second")); + // Non-SI units included for convenience + public static final Unit CELSIUS = Unit + .fromConversionFunctions(KELVIN.getBase(), tempK -> tempK - 273.15, + tempC -> tempC + 273.15) + .withName(NameSymbol.of("degree Celsius", "\u00B0C")); + + public static final LinearUnit MINUTE = SECOND.times(60) + .withName(NameSymbol.of("minute", "min")); + public static final LinearUnit HOUR = MINUTE.times(60) + .withName(NameSymbol.of("hour", "h", "hr")); + public static final LinearUnit DAY = HOUR.times(60) + .withName(NameSymbol.of("day", "d")); + public static final LinearUnit KILOMETRE_PER_HOUR = METRE.times(1000) + .dividedBy(HOUR).withName(NameSymbol.of("kilometre per hour", "km/h", + "kilometer per hour")); + public static final LinearUnit DEGREE = RADIAN.times(360 / (2 * Math.PI)) + .withName(NameSymbol.of("degree", "\u00B0", "deg")); + public static final LinearUnit ARCMINUTE = DEGREE.dividedBy(60) + .withName(NameSymbol.of("arcminute", "arcmin")); + public static final LinearUnit ARCSECOND = ARCMINUTE.dividedBy(60) + .withName(NameSymbol.of("arcsecond", "arcsec")); + public static final LinearUnit ASTRONOMICAL_UNIT = METRE + .times(149597870700.0) + .withName(NameSymbol.of("astronomical unit", "au")); + public static final LinearUnit PARSEC = ASTRONOMICAL_UNIT + .dividedBy(ARCSECOND).withName(NameSymbol.of("parsec", "pc")); + public static final LinearUnit HECTARE = METRE.times(METRE).times(10000.0) + .withName(NameSymbol.of("hectare", "ha")); + public static final LinearUnit LITRE = METRE.times(METRE).times(METRE) + .dividedBy(1000.0).withName(NameSymbol.of("litre", "L", "l", "liter")); + public static final LinearUnit TONNE = KILOGRAM.times(1000.0) + .withName(NameSymbol.of("tonne", "t", "metric ton")); + public static final LinearUnit DALTON = KILOGRAM.times(1.660539040e-27) + .withName(NameSymbol.of("dalton", "Da", "atomic unit", "u")); // approximate + // value + public static final LinearUnit ELECTRONVOLT = JOULE.times(1.602176634e-19) + .withName(NameSymbol.of("electron volt", "eV")); + public static final LinearUnit BYTE = BIT.times(8) + .withName(NameSymbol.of("byte", "B")); + public static final Unit NEPER = Unit.fromConversionFunctions(ONE.getBase(), + pr -> 0.5 * Math.log(pr), Np -> Math.exp(2 * Np)) + .withName(NameSymbol.of("neper", "Np")); + public static final Unit BEL = Unit.fromConversionFunctions(ONE.getBase(), + pr -> Math.log10(pr), dB -> Math.pow(10, dB)) + .withName(NameSymbol.of("bel", "B")); + public static final Unit DECIBEL = Unit + .fromConversionFunctions(ONE.getBase(), pr -> 10 * Math.log10(pr), + dB -> Math.pow(10, dB / 10)) + .withName(NameSymbol.of("decibel", "dB")); + + /// The prefixes of the SI + // expanding decimal prefixes + public static final UnitPrefix KILO = UnitPrefix.valueOf(1e3) + .withName(NameSymbol.of("kilo", "k", "K")); + public static final UnitPrefix MEGA = UnitPrefix.valueOf(1e6) + .withName(NameSymbol.of("mega", "M")); + public static final UnitPrefix GIGA = UnitPrefix.valueOf(1e9) + .withName(NameSymbol.of("giga", "G")); + public static final UnitPrefix TERA = UnitPrefix.valueOf(1e12) + .withName(NameSymbol.of("tera", "T")); + public static final UnitPrefix PETA = UnitPrefix.valueOf(1e15) + .withName(NameSymbol.of("peta", "P")); + public static final UnitPrefix EXA = UnitPrefix.valueOf(1e18) + .withName(NameSymbol.of("exa", "E")); + public static final UnitPrefix ZETTA = UnitPrefix.valueOf(1e21) + .withName(NameSymbol.of("zetta", "Z")); + public static final UnitPrefix YOTTA = UnitPrefix.valueOf(1e24) + .withName(NameSymbol.of("yotta", "Y")); + + // contracting decimal prefixes + public static final UnitPrefix MILLI = UnitPrefix.valueOf(1e-3) + .withName(NameSymbol.of("milli", "m")); + public static final UnitPrefix MICRO = UnitPrefix.valueOf(1e-6) + .withName(NameSymbol.of("micro", "\u03BC", "u")); // mu + public static final UnitPrefix NANO = UnitPrefix.valueOf(1e-9) + .withName(NameSymbol.of("nano", "n")); + public static final UnitPrefix PICO = UnitPrefix.valueOf(1e-12) + .withName(NameSymbol.of("pico", "p")); + public static final UnitPrefix FEMTO = UnitPrefix.valueOf(1e-15) + .withName(NameSymbol.of("femto", "f")); + public static final UnitPrefix ATTO = UnitPrefix.valueOf(1e-18) + .withName(NameSymbol.of("atto", "a")); + public static final UnitPrefix ZEPTO = UnitPrefix.valueOf(1e-21) + .withName(NameSymbol.of("zepto", "z")); + public static final UnitPrefix YOCTO = UnitPrefix.valueOf(1e-24) + .withName(NameSymbol.of("yocto", "y")); + + // prefixes that don't match the pattern of thousands + public static final UnitPrefix DEKA = UnitPrefix.valueOf(1e1) + .withName(NameSymbol.of("deka", "da", "deca", "D")); + public static final UnitPrefix HECTO = UnitPrefix.valueOf(1e2) + .withName(NameSymbol.of("hecto", "h", "H", "hekto")); + public static final UnitPrefix DECI = UnitPrefix.valueOf(1e-1) + .withName(NameSymbol.of("deci", "d")); + public static final UnitPrefix CENTI = UnitPrefix.valueOf(1e-2) + .withName(NameSymbol.of("centi", "c")); + public static final UnitPrefix KIBI = UnitPrefix.valueOf(1024) + .withName(NameSymbol.of("kibi", "Ki")); + public static final UnitPrefix MEBI = KIBI.times(1024) + .withName(NameSymbol.of("mebi", "Mi")); + public static final UnitPrefix GIBI = MEBI.times(1024) + .withName(NameSymbol.of("gibi", "Gi")); + public static final UnitPrefix TEBI = GIBI.times(1024) + .withName(NameSymbol.of("tebi", "Ti")); + public static final UnitPrefix PEBI = TEBI.times(1024) + .withName(NameSymbol.of("pebi", "Pi")); + public static final UnitPrefix EXBI = PEBI.times(1024) + .withName(NameSymbol.of("exbi", "Ei")); + + // a few prefixed units + public static final LinearUnit MICROMETRE = SI.METRE.withPrefix(SI.MICRO); + public static final LinearUnit MILLIMETRE = SI.METRE.withPrefix(SI.MILLI); + public static final LinearUnit KILOMETRE = SI.METRE.withPrefix(SI.KILO); + public static final LinearUnit MEGAMETRE = SI.METRE.withPrefix(SI.MEGA); + + public static final LinearUnit MICROLITRE = SI.LITRE.withPrefix(SI.MICRO); + public static final LinearUnit MILLILITRE = SI.LITRE.withPrefix(SI.MILLI); + public static final LinearUnit KILOLITRE = SI.LITRE.withPrefix(SI.KILO); + public static final LinearUnit MEGALITRE = SI.LITRE.withPrefix(SI.MEGA); + + public static final LinearUnit MICROSECOND = SI.SECOND.withPrefix(SI.MICRO); + public static final LinearUnit MILLISECOND = SI.SECOND.withPrefix(SI.MILLI); + public static final LinearUnit KILOSECOND = SI.SECOND.withPrefix(SI.KILO); + public static final LinearUnit MEGASECOND = SI.SECOND.withPrefix(SI.MEGA); + + public static final LinearUnit MICROGRAM = SI.GRAM.withPrefix(SI.MICRO); + public static final LinearUnit MILLIGRAM = SI.GRAM.withPrefix(SI.MILLI); + public static final LinearUnit MEGAGRAM = SI.GRAM.withPrefix(SI.MEGA); + + public static final LinearUnit MICRONEWTON = SI.NEWTON.withPrefix(SI.MICRO); + public static final LinearUnit MILLINEWTON = SI.NEWTON.withPrefix(SI.MILLI); + public static final LinearUnit KILONEWTON = SI.NEWTON.withPrefix(SI.KILO); + public static final LinearUnit MEGANEWTON = SI.NEWTON.withPrefix(SI.MEGA); + + public static final LinearUnit MICROJOULE = SI.JOULE.withPrefix(SI.MICRO); + public static final LinearUnit MILLIJOULE = SI.JOULE.withPrefix(SI.MILLI); + public static final LinearUnit KILOJOULE = SI.JOULE.withPrefix(SI.KILO); + public static final LinearUnit MEGAJOULE = SI.JOULE.withPrefix(SI.MEGA); + + public static final LinearUnit MICROWATT = SI.WATT.withPrefix(SI.MICRO); + public static final LinearUnit MILLIWATT = SI.WATT.withPrefix(SI.MILLI); + public static final LinearUnit KILOWATT = SI.WATT.withPrefix(SI.KILO); + public static final LinearUnit MEGAWATT = SI.WATT.withPrefix(SI.MEGA); + + public static final LinearUnit MICROCOULOMB = SI.COULOMB + .withPrefix(SI.MICRO); + public static final LinearUnit MILLICOULOMB = SI.COULOMB + .withPrefix(SI.MILLI); + public static final LinearUnit KILOCOULOMB = SI.COULOMB.withPrefix(SI.KILO); + public static final LinearUnit MEGACOULOMB = SI.COULOMB.withPrefix(SI.MEGA); + + public static final LinearUnit MICROAMPERE = SI.AMPERE.withPrefix(SI.MICRO); + public static final LinearUnit MILLIAMPERE = SI.AMPERE.withPrefix(SI.MILLI); + + public static final LinearUnit MICROVOLT = SI.VOLT.withPrefix(SI.MICRO); + public static final LinearUnit MILLIVOLT = SI.VOLT.withPrefix(SI.MILLI); + public static final LinearUnit KILOVOLT = SI.VOLT.withPrefix(SI.KILO); + public static final LinearUnit MEGAVOLT = SI.VOLT.withPrefix(SI.MEGA); + + public static final LinearUnit KILOOHM = SI.OHM.withPrefix(SI.KILO); + public static final LinearUnit MEGAOHM = SI.OHM.withPrefix(SI.MEGA); + + // sets of prefixes + public static final Set<UnitPrefix> ALL_PREFIXES = Set.of(DEKA, HECTO, KILO, + MEGA, GIGA, TERA, PETA, EXA, ZETTA, YOTTA, DECI, CENTI, MILLI, MICRO, + NANO, PICO, FEMTO, ATTO, ZEPTO, YOCTO, KIBI, MEBI, GIBI, TEBI, PEBI, + EXBI); + + public static final Set<UnitPrefix> DECIMAL_PREFIXES = Set.of(DEKA, HECTO, + KILO, MEGA, GIGA, TERA, PETA, EXA, ZETTA, YOTTA, DECI, CENTI, MILLI, + MICRO, NANO, PICO, FEMTO, ATTO, ZEPTO, YOCTO); + public static final Set<UnitPrefix> THOUSAND_PREFIXES = Set.of(KILO, MEGA, + GIGA, TERA, PETA, EXA, ZETTA, YOTTA, MILLI, MICRO, NANO, PICO, FEMTO, + ATTO, ZEPTO, YOCTO); + public static final Set<UnitPrefix> MAGNIFYING_PREFIXES = Set.of(DEKA, HECTO, + KILO, MEGA, GIGA, TERA, PETA, EXA, ZETTA, YOTTA, KIBI, MEBI, GIBI, + TEBI, PEBI, EXBI); + public static final Set<UnitPrefix> REDUCING_PREFIXES = Set.of(DECI, CENTI, + MILLI, MICRO, NANO, PICO, FEMTO, ATTO, ZEPTO, YOCTO); + + // You may NOT get SI instances! + private SI() { + throw new AssertionError(); + } +} diff --git a/src/main/java/org/unitConverter/unit/USCustomary.java b/src/main/java/org/unitConverter/unit/USCustomary.java new file mode 100644 index 0000000..1c4bcfe --- /dev/null +++ b/src/main/java/org/unitConverter/unit/USCustomary.java @@ -0,0 +1,135 @@ +/** + * Copyright (C) 2019 Adrien Hopkins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ +package org.unitConverter.unit; + +/** + * A static utility class that contains units in the US Customary system. + * + * @author Adrien Hopkins + * @since 2019-10-21 + */ +public final class USCustomary { + /** + * US Customary units that measure area + * + * @author Adrien Hopkins + * @since 2019-11-08 + */ + public static final class Area { + public static final LinearUnit SQUARE_SURVEY_FOOT = Length.SURVEY_FOOT.times(Length.SURVEY_FOOT); + public static final LinearUnit SQUARE_CHAIN = Length.SURVEY_CHAIN.times(Length.SURVEY_CHAIN); + public static final LinearUnit ACRE = Length.SURVEY_CHAIN.times(Length.SURVEY_FURLONG); + public static final LinearUnit SECTION = Length.SURVEY_MILE.times(Length.SURVEY_MILE); + public static final LinearUnit SURVEY_TOWNSHIP = SECTION.times(36); + } + + /** + * US Customary units that measure length + * + * @author Adrien Hopkins + * @since 2019-10-28 + */ + public static final class Length { + public static final LinearUnit FOOT = BritishImperial.Length.FOOT; + public static final LinearUnit INCH = BritishImperial.Length.INCH; + public static final LinearUnit HAND = INCH.times(4); + public static final LinearUnit PICA = INCH.dividedBy(6); + public static final LinearUnit POINT = PICA.dividedBy(12); + public static final LinearUnit YARD = BritishImperial.Length.YARD; + public static final LinearUnit MILE = BritishImperial.Length.MILE; + + public static final LinearUnit SURVEY_FOOT = SI.METRE.times(1200.0 / 3937.0); + public static final LinearUnit SURVEY_LINK = SURVEY_FOOT.times(33.0 / 50.0); + public static final LinearUnit SURVEY_ROD = SURVEY_FOOT.times(16.5); + public static final LinearUnit SURVEY_CHAIN = SURVEY_ROD.times(4); + public static final LinearUnit SURVEY_FURLONG = SURVEY_CHAIN.times(10); + public static final LinearUnit SURVEY_MILE = SURVEY_FURLONG.times(8); + public static final LinearUnit SURVEY_LEAGUE = SURVEY_MILE.times(3); + + public static final LinearUnit NAUTICAL_MILE = BritishImperial.Length.NAUTICAL_MILE; + public static final LinearUnit FATHOM = YARD.times(2); + public static final LinearUnit CABLE = FATHOM.times(120); + } + + /** + * mass units + * + * @author Adrien Hopkins + * @since 2019-11-08 + */ + public static final class Mass { + public static final LinearUnit GRAIN = BritishImperial.Mass.GRAIN; + public static final LinearUnit DRAM = BritishImperial.Mass.DRACHM; + public static final LinearUnit OUNCE = BritishImperial.Mass.OUNCE; + public static final LinearUnit POUND = BritishImperial.Mass.POUND; + public static final LinearUnit HUNDREDWEIGHT = POUND.times(100); + public static final LinearUnit SHORT_TON = HUNDREDWEIGHT.times(20); + + // troy system for precious metals + public static final LinearUnit PENNYWEIGHT = GRAIN.times(24); + public static final LinearUnit TROY_OUNCE = PENNYWEIGHT.times(20); + public static final LinearUnit TROY_POUND = TROY_OUNCE.times(12); + } + + /** + * Volume units + * + * @author Adrien Hopkins + * @since 2019-11-08 + */ + public static final class Volume { + public static final LinearUnit CUBIC_INCH = Length.INCH.toExponent(3); + public static final LinearUnit CUBIC_FOOT = Length.FOOT.toExponent(3); + public static final LinearUnit CUBIC_YARD = Length.YARD.toExponent(3); + public static final LinearUnit ACRE_FOOT = Area.ACRE.times(Length.FOOT); + + public static final LinearUnit MINIM = SI.LITRE.withPrefix(SI.MICRO).times(61.611519921875); + public static final LinearUnit FLUID_DRAM = MINIM.times(60); + public static final LinearUnit TEASPOON = MINIM.times(80); + public static final LinearUnit TABLESPOON = TEASPOON.times(3); + public static final LinearUnit FLUID_OUNCE = TABLESPOON.times(2); + public static final LinearUnit SHOT = TABLESPOON.times(3); + public static final LinearUnit GILL = FLUID_OUNCE.times(4); + public static final LinearUnit CUP = GILL.times(2); + public static final LinearUnit PINT = CUP.times(2); + public static final LinearUnit QUART = PINT.times(2); + public static final LinearUnit GALLON = QUART.times(4); + public static final LinearUnit BARREL = GALLON.times(31.5); + public static final LinearUnit OIL_BARREL = GALLON.times(42); + public static final LinearUnit HOGSHEAD = GALLON.times(63); + + public static final LinearUnit DRY_PINT = SI.LITRE.times(0.5506104713575); + public static final LinearUnit DRY_QUART = DRY_PINT.times(2); + public static final LinearUnit DRY_GALLON = DRY_QUART.times(4); + public static final LinearUnit PECK = DRY_GALLON.times(2); + public static final LinearUnit BUSHEL = PECK.times(4); + public static final LinearUnit DRY_BARREL = CUBIC_INCH.times(7056); + } + + public static final LinearUnit OUNCE_FORCE = BritishImperial.OUNCE_FORCE; + public static final LinearUnit POUND_FORCE = BritishImperial.POUND_FORCE; + + public static final LinearUnit BRITISH_THERMAL_UNIT = BritishImperial.BRITISH_THERMAL_UNIT; + public static final LinearUnit CALORIE = BritishImperial.CALORIE; + public static final LinearUnit KILOCALORIE = BritishImperial.KILOCALORIE; + public static final LinearUnit FOOT_POUND = POUND_FORCE.times(Length.FOOT); + + public static final LinearUnit HORSEPOWER = Length.FOOT.times(POUND_FORCE).dividedBy(SI.MINUTE).times(33000); + public static final LinearUnit POUND_PER_SQUARE_INCH = POUND_FORCE.dividedBy(Length.INCH.toExponent(2)); + + public static final Unit FAHRENHEIT = BritishImperial.FAHRENHEIT; +} diff --git a/src/main/java/org/unitConverter/unit/Unit.java b/src/main/java/org/unitConverter/unit/Unit.java new file mode 100644 index 0000000..0a3298f --- /dev/null +++ b/src/main/java/org/unitConverter/unit/Unit.java @@ -0,0 +1,377 @@ +/** + * Copyright (C) 2019 Adrien Hopkins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ +package org.unitConverter.unit; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.DoubleUnaryOperator; + +import org.unitConverter.math.DecimalComparison; +import org.unitConverter.math.ObjectProduct; + +/** + * A unit that is composed of base units. + * + * @author Adrien Hopkins + * @since 2019-10-16 + */ +public abstract class Unit implements Nameable { + /** + * Returns a unit from its base and the functions it uses to convert to and + * from its base. + * + * <p> + * For example, to get a unit representing the degree Celsius, the following + * code can be used: + * + * {@code Unit.fromConversionFunctions(SI.KELVIN, tempK -> tempK - 273.15, tempC -> tempC + 273.15);} + * </p> + * + * @param base unit's base + * @param converterFrom function that accepts a value expressed in the unit's + * base and returns that value expressed in this unit. + * @param converterTo function that accepts a value expressed in the unit + * and returns that value expressed in the unit's base. + * @return a unit that uses the provided functions to convert. + * @since 2019-05-22 + * @throws NullPointerException if any argument is null + */ + public static final Unit fromConversionFunctions( + final ObjectProduct<BaseUnit> base, + final DoubleUnaryOperator converterFrom, + final DoubleUnaryOperator converterTo) { + return new FunctionalUnit(base, converterFrom, converterTo); + } + + /** + * Returns a unit from its base and the functions it uses to convert to and + * from its base. + * + * <p> + * For example, to get a unit representing the degree Celsius, the following + * code can be used: + * + * {@code Unit.fromConversionFunctions(SI.KELVIN, tempK -> tempK - 273.15, tempC -> tempC + 273.15);} + * </p> + * + * @param base unit's base + * @param converterFrom function that accepts a value expressed in the unit's + * base and returns that value expressed in this unit. + * @param converterTo function that accepts a value expressed in the unit + * and returns that value expressed in the unit's base. + * @param ns names and symbol of unit + * @return a unit that uses the provided functions to convert. + * @since 2019-05-22 + * @throws NullPointerException if any argument is null + */ + public static final Unit fromConversionFunctions( + final ObjectProduct<BaseUnit> base, + final DoubleUnaryOperator converterFrom, + final DoubleUnaryOperator converterTo, final NameSymbol ns) { + return new FunctionalUnit(base, converterFrom, converterTo, ns); + } + + /** + * The combination of units that this unit is based on. + * + * @since 2019-10-16 + */ + private final ObjectProduct<BaseUnit> unitBase; + + /** + * This unit's name(s) and symbol + * + * @since 2020-09-07 + */ + private final NameSymbol nameSymbol; + + /** + * Cache storing the result of getDimension() + * + * @since 2019-10-16 + */ + private transient ObjectProduct<BaseDimension> dimension = null; + + /** + * Creates the {@code Unit}. + * + * @param unitBase base of unit + * @param ns names and symbol of unit + * @since 2019-10-16 + * @throws NullPointerException if unitBase or ns is null + */ + Unit(ObjectProduct<BaseUnit> unitBase, NameSymbol ns) { + this.unitBase = Objects.requireNonNull(unitBase, + "unitBase may not be null"); + this.nameSymbol = Objects.requireNonNull(ns, "ns may not be null"); + } + + /** + * A constructor that constructs {@code BaseUnit} instances. + * + * @since 2019-10-16 + */ + Unit(final String primaryName, final String symbol, + final Set<String> otherNames) { + if (this instanceof BaseUnit) { + this.unitBase = ObjectProduct.oneOf((BaseUnit) this); + } else + throw new AssertionError(); + this.nameSymbol = NameSymbol.of(primaryName, symbol, + new HashSet<>(otherNames)); + } + + /** + * @return this unit as a {@link Unitlike} + * @since 2020-09-07 + */ + public final Unitlike<Double> asUnitlike() { + return Unitlike.fromConversionFunctions(this.getBase(), + this::convertFromBase, this::convertToBase, this.getNameSymbol()); + } + + /** + * Checks if a value expressed in this unit can be converted to a value + * expressed in {@code other} + * + * @param other unit or unitlike form to test with + * @return true if they are compatible + * @since 2019-01-13 + * @since v0.1.0 + * @throws NullPointerException if other is null + */ + public final boolean canConvertTo(final Unit other) { + Objects.requireNonNull(other, "other must not be null."); + return Objects.equals(this.getBase(), other.getBase()); + } + + /** + * Checks if a value expressed in this unit can be converted to a value + * expressed in {@code other} + * + * @param other unit or unitlike form to test with + * @return true if they are compatible + * @since 2019-01-13 + * @since v0.1.0 + * @throws NullPointerException if other is null + */ + public final <W> boolean canConvertTo(final Unitlike<W> other) { + Objects.requireNonNull(other, "other must not be null."); + return Objects.equals(this.getBase(), other.getBase()); + } + + /** + * Converts from a value expressed in this unit's base unit to a value + * expressed in this unit. + * <p> + * This must be the inverse of {@code convertToBase}, so + * {@code convertFromBase(convertToBase(value))} must be equal to + * {@code value} for any value, ignoring precision loss by roundoff error. + * </p> + * <p> + * If this unit <i>is</i> a base unit, this method should return + * {@code value}. + * </p> + * + * @implSpec This method is used by {@link #convertTo}, and its behaviour + * affects the behaviour of {@code convertTo}. + * + * @param value value expressed in <b>base</b> unit + * @return value expressed in <b>this</b> unit + * @since 2018-12-22 + * @since v0.1.0 + */ + protected abstract double convertFromBase(double value); + + /** + * Converts a value expressed in this unit to a value expressed in + * {@code other}. + * + * @implSpec If unit conversion is possible, this implementation returns + * {@code other.convertFromBase(this.convertToBase(value))}. + * Therefore, overriding either of those methods will change the + * output of this method. + * + * @param other unit to convert to + * @param value value to convert + * @return converted value + * @since 2019-05-22 + * @throws IllegalArgumentException if {@code other} is incompatible for + * conversion with this unit (as tested by + * {@link Unit#canConvertTo}). + * @throws NullPointerException if other is null + */ + public final double convertTo(final Unit other, final double value) { + Objects.requireNonNull(other, "other must not be null."); + if (this.canConvertTo(other)) + return other.convertFromBase(this.convertToBase(value)); + else + throw new IllegalArgumentException( + String.format("Cannot convert from %s to %s.", this, other)); + } + + /** + * Converts a value expressed in this unit to a value expressed in + * {@code other}. + * + * @implSpec If conversion is possible, this implementation returns + * {@code other.convertFromBase(this.convertToBase(value))}. + * Therefore, overriding either of those methods will change the + * output of this method. + * + * @param other unitlike form to convert to + * @param value value to convert + * @param <W> type of value to convert to + * @return converted value + * @since 2020-09-07 + * @throws IllegalArgumentException if {@code other} is incompatible for + * conversion with this unit (as tested by + * {@link Unit#canConvertTo}). + * @throws NullPointerException if other is null + */ + public final <W> W convertTo(final Unitlike<W> other, final double value) { + Objects.requireNonNull(other, "other must not be null."); + if (this.canConvertTo(other)) + return other.convertFromBase(this.convertToBase(value)); + else + throw new IllegalArgumentException( + String.format("Cannot convert from %s to %s.", this, other)); + } + + /** + * Converts from a value expressed in this unit to a value expressed in this + * unit's base unit. + * <p> + * This must be the inverse of {@code convertFromBase}, so + * {@code convertToBase(convertFromBase(value))} must be equal to + * {@code value} for any value, ignoring precision loss by roundoff error. + * </p> + * <p> + * If this unit <i>is</i> a base unit, this method should return + * {@code value}. + * </p> + * + * @implSpec This method is used by {@link #convertTo}, and its behaviour + * affects the behaviour of {@code convertTo}. + * + * @param value value expressed in <b>this</b> unit + * @return value expressed in <b>base</b> unit + * @since 2018-12-22 + * @since v0.1.0 + */ + protected abstract double convertToBase(double value); + + /** + * @return combination of units that this unit is based on + * @since 2018-12-22 + * @since v0.1.0 + */ + public final ObjectProduct<BaseUnit> getBase() { + return this.unitBase; + } + + /** + * @return dimension measured by this unit + * @since 2018-12-22 + * @since v0.1.0 + */ + public final ObjectProduct<BaseDimension> getDimension() { + if (this.dimension == null) { + final Map<BaseUnit, Integer> mapping = this.unitBase.exponentMap(); + final Map<BaseDimension, Integer> dimensionMap = new HashMap<>(); + + for (final BaseUnit key : mapping.keySet()) { + dimensionMap.put(key.getBaseDimension(), mapping.get(key)); + } + + this.dimension = ObjectProduct.fromExponentMapping(dimensionMap); + } + return this.dimension; + } + + /** + * @return the nameSymbol + * @since 2020-09-07 + */ + @Override + public final NameSymbol getNameSymbol() { + return this.nameSymbol; + } + + /** + * Returns true iff this unit is metric. + * <p> + * "Metric" is defined by three conditions: + * <ul> + * <li>Must be an instance of {@link LinearUnit}.</li> + * <li>Must be based on the SI base units (as determined by getBase())</li> + * <li>The conversion factor must be a power of 10.</li> + * </ul> + * <p> + * Note that this definition excludes some units that many would consider + * "metric", such as the degree Celsius (fails the first condition), + * calories, minutes and hours (fail the third condition). + * <p> + * All SI units (as designated by the BIPM) except the degree Celsius are + * considered "metric" by this definition. + * + * @since 2020-08-27 + */ + public final boolean isMetric() { + // first condition - check that it is a linear unit + if (!(this instanceof LinearUnit)) + return false; + final LinearUnit linear = (LinearUnit) this; + + // second condition - check that + for (final BaseUnit b : linear.getBase().getBaseSet()) { + if (!SI.BaseUnits.BASE_UNITS.contains(b)) + return false; + } + + // third condition - check that conversion factor is a power of 10 + return DecimalComparison + .equals(Math.log10(linear.getConversionFactor()) % 1.0, 0); + } + + @Override + public String toString() { + return this.getPrimaryName().orElse("Unnamed unit") + + (this.getSymbol().isPresent() + ? String.format(" (%s)", this.getSymbol().get()) + : "") + + ", derived from " + + this.getBase().toString(u -> u.getSymbol().get()) + + (this.getOtherNames().isEmpty() ? "" + : ", also called " + String.join(", ", this.getOtherNames())); + } + + /** + * @param ns name(s) and symbol to use + * @return a copy of this unit with provided name(s) and symbol + * @since 2019-10-21 + * @throws NullPointerException if ns is null + */ + public Unit withName(final NameSymbol ns) { + return fromConversionFunctions(this.getBase(), this::convertFromBase, + this::convertToBase, + Objects.requireNonNull(ns, "ns must not be null.")); + } +} diff --git a/src/main/java/org/unitConverter/unit/UnitDatabase.java b/src/main/java/org/unitConverter/unit/UnitDatabase.java new file mode 100644 index 0000000..000acf5 --- /dev/null +++ b/src/main/java/org/unitConverter/unit/UnitDatabase.java @@ -0,0 +1,1991 @@ +/** + * Copyright (C) 2018 Adrien Hopkins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ +package org.unitConverter.unit; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.math.BigDecimal; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.AbstractSet; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.NoSuchElementException; +import java.util.Objects; +import java.util.Set; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.unitConverter.math.ConditionalExistenceCollections; +import org.unitConverter.math.DecimalComparison; +import org.unitConverter.math.ExpressionParser; +import org.unitConverter.math.ObjectProduct; +import org.unitConverter.math.UncertainDouble; + +/** + * A database of units, prefixes and dimensions, and their names. + * + * @author Adrien Hopkins + * @since 2019-01-07 + * @since v0.1.0 + */ +public final class UnitDatabase { + /** + * A map for units that allows the use of prefixes. + * <p> + * As this map implementation is intended to be used as a sort of "augmented + * view" of a unit and prefix map, it is unmodifiable but instead reflects + * the changes to the maps passed into it. Do not edit this map, instead edit + * the maps that were passed in during construction. + * </p> + * <p> + * The rules for applying prefixes onto units are the following: + * <ul> + * <li>Prefixes can only be applied to linear units.</li> + * <li>Before attempting to search for prefixes in a unit name, this map will + * first search for a unit name. So, if there are two units, "B" and "AB", + * and a prefix "A", this map will favour the unit "AB" over the unit "B" + * with the prefix "A", even though they have the same string.</li> + * <li>Longer prefixes are preferred to shorter prefixes. So, if you have + * units "BC" and "C", and prefixes "AB" and "A", inputting "ABC" will return + * the unit "C" with the prefix "AB", not "BC" with the prefix "A".</li> + * </ul> + * </p> + * <p> + * This map is infinite in size if there is at least one unit and at least + * one prefix. If it is infinite, some operations that only work with finite + * collections, like converting name/entry sets to arrays, will throw an + * {@code IllegalStateException}. + * </p> + * <p> + * Because of ambiguities between prefixes (i.e. kilokilo = mega), + * {@link #containsValue} and {@link #values()} currently ignore prefixes. + * </p> + * + * @author Adrien Hopkins + * @since 2019-04-13 + * @since v0.2.0 + */ + private static final class PrefixedUnitMap implements Map<String, Unit> { + /** + * The class used for entry sets. + * + * <p> + * If the map that created this set is infinite in size (has at least one + * unit and at least one prefix), this set is infinite as well. If this + * set is infinite in size, {@link #toArray} will fail with a + * {@code IllegalStateException} instead of creating an infinite-sized + * array. + * </p> + * + * @author Adrien Hopkins + * @since 2019-04-13 + * @since v0.2.0 + */ + private static final class PrefixedUnitEntrySet + extends AbstractSet<Map.Entry<String, Unit>> { + /** + * The entry for this set. + * + * @author Adrien Hopkins + * @since 2019-04-14 + * @since v0.2.0 + */ + private static final class PrefixedUnitEntry + implements Entry<String, Unit> { + private final String key; + private final Unit value; + + /** + * Creates the {@code PrefixedUnitEntry}. + * + * @param key key + * @param value value + * @since 2019-04-14 + * @since v0.2.0 + */ + public PrefixedUnitEntry(final String key, final Unit value) { + this.key = key; + this.value = value; + } + + /** + * @since 2019-05-03 + */ + @Override + public boolean equals(final Object o) { + if (!(o instanceof Map.Entry)) + return false; + final Map.Entry<?, ?> other = (Map.Entry<?, ?>) o; + return Objects.equals(this.getKey(), other.getKey()) + && Objects.equals(this.getValue(), other.getValue()); + } + + @Override + public String getKey() { + return this.key; + } + + @Override + public Unit getValue() { + return this.value; + } + + /** + * @since 2019-05-03 + */ + @Override + public int hashCode() { + return (this.getKey() == null ? 0 : this.getKey().hashCode()) + ^ (this.getValue() == null ? 0 + : this.getValue().hashCode()); + } + + @Override + public Unit setValue(final Unit value) { + throw new UnsupportedOperationException( + "Cannot set value in an immutable entry"); + } + + /** + * Returns a string representation of the entry. The format of the + * string is the string representation of the key, then the equals + * ({@code =}) character, then the string representation of the + * value. + * + * @since 2019-05-03 + */ + @Override + public String toString() { + return this.getKey() + "=" + this.getValue(); + } + } + + /** + * An iterator that iterates over the units of a + * {@code PrefixedUnitNameSet}. + * + * @author Adrien Hopkins + * @since 2019-04-14 + * @since v0.2.0 + */ + private static final class PrefixedUnitEntryIterator + implements Iterator<Entry<String, Unit>> { + // position in the unit list + private int unitNamePosition = 0; + // the indices of the prefixes attached to the current unit + private final List<Integer> prefixCoordinates = new ArrayList<>(); + + // values from the unit entry set + private final Map<String, Unit> map; + private transient final List<String> unitNames; + private transient final List<String> prefixNames; + + /** + * Creates the + * {@code UnitsDatabase.PrefixedUnitMap.PrefixedUnitNameSet.PrefixedUnitNameIterator}. + * + * @since 2019-04-14 + * @since v0.2.0 + */ + public PrefixedUnitEntryIterator(final PrefixedUnitMap map) { + this.map = map; + this.unitNames = new ArrayList<>(map.units.keySet()); + this.prefixNames = new ArrayList<>(map.prefixes.keySet()); + } + + /** + * @return current unit name + * @since 2019-04-14 + * @since v0.2.0 + */ + private String getCurrentUnitName() { + final StringBuilder unitName = new StringBuilder(); + for (final int i : this.prefixCoordinates) { + unitName.append(this.prefixNames.get(i)); + } + unitName.append(this.unitNames.get(this.unitNamePosition)); + + return unitName.toString(); + } + + @Override + public boolean hasNext() { + if (this.unitNames.isEmpty()) + return false; + else { + if (this.prefixNames.isEmpty()) + return this.unitNamePosition >= this.unitNames.size() - 1; + else + return true; + } + } + + /** + * Changes this iterator's position to the next available one. + * + * @since 2019-04-14 + * @since v0.2.0 + */ + private void incrementPosition() { + this.unitNamePosition++; + + if (this.unitNamePosition >= this.unitNames.size()) { + // we have used all of our units, go to a different prefix + this.unitNamePosition = 0; + + // if the prefix coordinates are empty, then set it to [0] + if (this.prefixCoordinates.isEmpty()) { + this.prefixCoordinates.add(0, 0); + } else { + // get the prefix coordinate to increment, then increment + int i = this.prefixCoordinates.size() - 1; + this.prefixCoordinates.set(i, + this.prefixCoordinates.get(i) + 1); + + // fix any carrying errors + while (i >= 0 && this.prefixCoordinates + .get(i) >= this.prefixNames.size()) { + // carry over + this.prefixCoordinates.set(i--, 0); // null and + // decrement at the + // same time + + if (i < 0) { // we need to add a new coordinate + this.prefixCoordinates.add(0, 0); + } else { // increment an existing one + this.prefixCoordinates.set(i, + this.prefixCoordinates.get(i) + 1); + } + } + } + } + } + + @Override + public Entry<String, Unit> next() { + // get next element + final Entry<String, Unit> nextEntry = this.peek(); + + // iterate to next position + this.incrementPosition(); + + return nextEntry; + } + + /** + * @return the next element in the iterator, without iterating over + * it + * @since 2019-05-03 + */ + private Entry<String, Unit> peek() { + if (!this.hasNext()) + throw new NoSuchElementException("No units left!"); + + // if I have prefixes, ensure I'm not using a nonlinear unit + // since all of the unprefixed stuff is done, just remove + // nonlinear units + if (!this.prefixCoordinates.isEmpty()) { + while (this.unitNamePosition < this.unitNames.size() + && !(this.map.get(this.unitNames.get( + this.unitNamePosition)) instanceof LinearUnit)) { + this.unitNames.remove(this.unitNamePosition); + } + } + + final String nextName = this.getCurrentUnitName(); + + return new PrefixedUnitEntry(nextName, this.map.get(nextName)); + } + + /** + * Returns a string representation of the object. The exact details + * of the representation are unspecified and subject to change. + * + * @since 2019-05-03 + */ + @Override + public String toString() { + return String.format( + "Iterator iterating over name-unit entries; next value is \"%s\"", + this.peek()); + } + } + + // the map that created this set + private final PrefixedUnitMap map; + + /** + * Creates the {@code PrefixedUnitNameSet}. + * + * @param map map that created this set + * @since 2019-04-13 + * @since v0.2.0 + */ + public PrefixedUnitEntrySet(final PrefixedUnitMap map) { + this.map = map; + } + + @Override + public boolean add(final Map.Entry<String, Unit> e) { + throw new UnsupportedOperationException( + "Cannot add to an immutable set"); + } + + @Override + public boolean addAll( + final Collection<? extends Map.Entry<String, Unit>> c) { + throw new UnsupportedOperationException( + "Cannot add to an immutable set"); + } + + @Override + public void clear() { + throw new UnsupportedOperationException( + "Cannot clear an immutable set"); + } + + @Override + public boolean contains(final Object o) { + // get the entry + final Entry<String, Unit> entry; + + try { + // This is OK because I'm in a try-catch block, catching the + // exact exception that would be thrown. + @SuppressWarnings("unchecked") + final Entry<String, Unit> tempEntry = (Entry<String, Unit>) o; + entry = tempEntry; + } catch (final ClassCastException e) { + throw new IllegalArgumentException( + "Attempted to test for an entry using a non-entry."); + } + + return this.map.containsKey(entry.getKey()) + && this.map.get(entry.getKey()).equals(entry.getValue()); + } + + @Override + public boolean containsAll(final Collection<?> c) { + for (final Object o : c) + if (!this.contains(o)) + return false; + return true; + } + + @Override + public boolean isEmpty() { + return this.map.isEmpty(); + } + + @Override + public Iterator<Entry<String, Unit>> iterator() { + return new PrefixedUnitEntryIterator(this.map); + } + + @Override + public boolean remove(final Object o) { + throw new UnsupportedOperationException( + "Cannot remove from an immutable set"); + } + + @Override + public boolean removeAll(final Collection<?> c) { + throw new UnsupportedOperationException( + "Cannot remove from an immutable set"); + } + + @Override + public boolean removeIf( + final Predicate<? super Entry<String, Unit>> filter) { + throw new UnsupportedOperationException( + "Cannot remove from an immutable set"); + } + + @Override + public boolean retainAll(final Collection<?> c) { + throw new UnsupportedOperationException( + "Cannot remove from an immutable set"); + } + + @Override + public int size() { + if (this.map.units.isEmpty()) + return 0; + else { + if (this.map.prefixes.isEmpty()) + return this.map.units.size(); + else + // infinite set + return Integer.MAX_VALUE; + } + } + + /** + * @throws IllegalStateException if the set is infinite in size + */ + @Override + public Object[] toArray() { + if (this.map.units.isEmpty() || this.map.prefixes.isEmpty()) + return super.toArray(); + else + // infinite set + throw new IllegalStateException( + "Cannot make an infinite set into an array."); + } + + /** + * @throws IllegalStateException if the set is infinite in size + */ + @Override + public <T> T[] toArray(final T[] a) { + if (this.map.units.isEmpty() || this.map.prefixes.isEmpty()) + return super.toArray(a); + else + // infinite set + throw new IllegalStateException( + "Cannot make an infinite set into an array."); + } + + @Override + public String toString() { + if (this.map.units.isEmpty() || this.map.prefixes.isEmpty()) + return super.toString(); + else + return String.format( + "Infinite set of name-unit entries created from units %s and prefixes %s", + this.map.units, this.map.prefixes); + } + } + + /** + * The class used for unit name sets. + * + * <p> + * If the map that created this set is infinite in size (has at least one + * unit and at least one prefix), this set is infinite as well. If this + * set is infinite in size, {@link #toArray} will fail with a + * {@code IllegalStateException} instead of creating an infinite-sized + * array. + * </p> + * + * @author Adrien Hopkins + * @since 2019-04-13 + * @since v0.2.0 + */ + private static final class PrefixedUnitNameSet + extends AbstractSet<String> { + /** + * An iterator that iterates over the units of a + * {@code PrefixedUnitNameSet}. + * + * @author Adrien Hopkins + * @since 2019-04-14 + * @since v0.2.0 + */ + private static final class PrefixedUnitNameIterator + implements Iterator<String> { + // position in the unit list + private int unitNamePosition = 0; + // the indices of the prefixes attached to the current unit + private final List<Integer> prefixCoordinates = new ArrayList<>(); + + // values from the unit name set + private final Map<String, Unit> map; + private transient final List<String> unitNames; + private transient final List<String> prefixNames; + + /** + * Creates the + * {@code UnitsDatabase.PrefixedUnitMap.PrefixedUnitNameSet.PrefixedUnitNameIterator}. + * + * @since 2019-04-14 + * @since v0.2.0 + */ + public PrefixedUnitNameIterator(final PrefixedUnitMap map) { + this.map = map; + this.unitNames = new ArrayList<>(map.units.keySet()); + this.prefixNames = new ArrayList<>(map.prefixes.keySet()); + } + + /** + * @return current unit name + * @since 2019-04-14 + * @since v0.2.0 + */ + private String getCurrentUnitName() { + final StringBuilder unitName = new StringBuilder(); + for (final int i : this.prefixCoordinates) { + unitName.append(this.prefixNames.get(i)); + } + unitName.append(this.unitNames.get(this.unitNamePosition)); + + return unitName.toString(); + } + + @Override + public boolean hasNext() { + if (this.unitNames.isEmpty()) + return false; + else { + if (this.prefixNames.isEmpty()) + return this.unitNamePosition >= this.unitNames.size() - 1; + else + return true; + } + } + + /** + * Changes this iterator's position to the next available one. + * + * @since 2019-04-14 + * @since v0.2.0 + */ + private void incrementPosition() { + this.unitNamePosition++; + + if (this.unitNamePosition >= this.unitNames.size()) { + // we have used all of our units, go to a different prefix + this.unitNamePosition = 0; + + // if the prefix coordinates are empty, then set it to [0] + if (this.prefixCoordinates.isEmpty()) { + this.prefixCoordinates.add(0, 0); + } else { + // get the prefix coordinate to increment, then increment + int i = this.prefixCoordinates.size() - 1; + this.prefixCoordinates.set(i, + this.prefixCoordinates.get(i) + 1); + + // fix any carrying errors + while (i >= 0 && this.prefixCoordinates + .get(i) >= this.prefixNames.size()) { + // carry over + this.prefixCoordinates.set(i--, 0); // null and + // decrement at the + // same time + + if (i < 0) { // we need to add a new coordinate + this.prefixCoordinates.add(0, 0); + } else { // increment an existing one + this.prefixCoordinates.set(i, + this.prefixCoordinates.get(i) + 1); + } + } + } + } + } + + @Override + public String next() { + final String nextName = this.peek(); + + this.incrementPosition(); + + return nextName; + } + + /** + * @return the next element in the iterator, without iterating over + * it + * @since 2019-05-03 + */ + private String peek() { + if (!this.hasNext()) + throw new NoSuchElementException("No units left!"); + // if I have prefixes, ensure I'm not using a nonlinear unit + // since all of the unprefixed stuff is done, just remove + // nonlinear units + if (!this.prefixCoordinates.isEmpty()) { + while (this.unitNamePosition < this.unitNames.size() + && !(this.map.get(this.unitNames.get( + this.unitNamePosition)) instanceof LinearUnit)) { + this.unitNames.remove(this.unitNamePosition); + } + } + + return this.getCurrentUnitName(); + } + + /** + * Returns a string representation of the object. The exact details + * of the representation are unspecified and subject to change. + * + * @since 2019-05-03 + */ + @Override + public String toString() { + return String.format( + "Iterator iterating over unit names; next value is \"%s\"", + this.peek()); + } + } + + // the map that created this set + private final PrefixedUnitMap map; + + /** + * Creates the {@code PrefixedUnitNameSet}. + * + * @param map map that created this set + * @since 2019-04-13 + * @since v0.2.0 + */ + public PrefixedUnitNameSet(final PrefixedUnitMap map) { + this.map = map; + } + + @Override + public boolean add(final String e) { + throw new UnsupportedOperationException( + "Cannot add to an immutable set"); + } + + @Override + public boolean addAll(final Collection<? extends String> c) { + throw new UnsupportedOperationException( + "Cannot add to an immutable set"); + } + + @Override + public void clear() { + throw new UnsupportedOperationException( + "Cannot clear an immutable set"); + } + + @Override + public boolean contains(final Object o) { + return this.map.containsKey(o); + } + + @Override + public boolean containsAll(final Collection<?> c) { + for (final Object o : c) + if (!this.contains(o)) + return false; + return true; + } + + @Override + public boolean isEmpty() { + return this.map.isEmpty(); + } + + @Override + public Iterator<String> iterator() { + return new PrefixedUnitNameIterator(this.map); + } + + @Override + public boolean remove(final Object o) { + throw new UnsupportedOperationException( + "Cannot remove from an immutable set"); + } + + @Override + public boolean removeAll(final Collection<?> c) { + throw new UnsupportedOperationException( + "Cannot remove from an immutable set"); + } + + @Override + public boolean removeIf(final Predicate<? super String> filter) { + throw new UnsupportedOperationException( + "Cannot remove from an immutable set"); + } + + @Override + public boolean retainAll(final Collection<?> c) { + throw new UnsupportedOperationException( + "Cannot remove from an immutable set"); + } + + @Override + public int size() { + if (this.map.units.isEmpty()) + return 0; + else { + if (this.map.prefixes.isEmpty()) + return this.map.units.size(); + else + // infinite set + return Integer.MAX_VALUE; + } + } + + /** + * @throws IllegalStateException if the set is infinite in size + */ + @Override + public Object[] toArray() { + if (this.map.units.isEmpty() || this.map.prefixes.isEmpty()) + return super.toArray(); + else + // infinite set + throw new IllegalStateException( + "Cannot make an infinite set into an array."); + + } + + /** + * @throws IllegalStateException if the set is infinite in size + */ + @Override + public <T> T[] toArray(final T[] a) { + if (this.map.units.isEmpty() || this.map.prefixes.isEmpty()) + return super.toArray(a); + else + // infinite set + throw new IllegalStateException( + "Cannot make an infinite set into an array."); + } + + @Override + public String toString() { + if (this.map.units.isEmpty() || this.map.prefixes.isEmpty()) + return super.toString(); + else + return String.format( + "Infinite set of name-unit entries created from units %s and prefixes %s", + this.map.units, this.map.prefixes); + } + } + + /** + * The units stored in this collection, without prefixes. + * + * @since 2019-04-13 + * @since v0.2.0 + */ + private final Map<String, Unit> units; + + /** + * The available prefixes for use. + * + * @since 2019-04-13 + * @since v0.2.0 + */ + private final Map<String, UnitPrefix> prefixes; + + // caches + private transient Collection<Unit> values = null; + private transient Set<String> keySet = null; + private transient Set<Entry<String, Unit>> entrySet = null; + + /** + * Creates the {@code PrefixedUnitMap}. + * + * @param units map mapping unit names to units + * @param prefixes map mapping prefix names to prefixes + * @since 2019-04-13 + * @since v0.2.0 + */ + public PrefixedUnitMap(final Map<String, Unit> units, + final Map<String, UnitPrefix> prefixes) { + // I am making unmodifiable maps to ensure I don't accidentally make + // changes. + this.units = Collections.unmodifiableMap(units); + this.prefixes = Collections.unmodifiableMap(prefixes); + } + + @Override + public void clear() { + throw new UnsupportedOperationException( + "Cannot clear an immutable map"); + } + + @Override + public Unit compute(final String key, + final BiFunction<? super String, ? super Unit, ? extends Unit> remappingFunction) { + throw new UnsupportedOperationException( + "Cannot edit an immutable map"); + } + + @Override + public Unit computeIfAbsent(final String key, + final Function<? super String, ? extends Unit> mappingFunction) { + throw new UnsupportedOperationException( + "Cannot edit an immutable map"); + } + + @Override + public Unit computeIfPresent(final String key, + final BiFunction<? super String, ? super Unit, ? extends Unit> remappingFunction) { + throw new UnsupportedOperationException( + "Cannot edit an immutable map"); + } + + @Override + public boolean containsKey(final Object key) { + // First, test if there is a unit with the key + if (this.units.containsKey(key)) + return true; + + // Next, try to cast it to String + if (!(key instanceof String)) + throw new IllegalArgumentException( + "Attempted to test for a unit using a non-string name."); + final String unitName = (String) key; + + // Then, look for the longest prefix that is attached to a valid unit + String longestPrefix = null; + int longestLength = 0; + + for (final String prefixName : this.prefixes.keySet()) { + // a prefix name is valid if: + // - it is prefixed (i.e. the unit name starts with it) + // - it is longer than the existing largest prefix (since I am + // looking for the longest valid prefix) + // - the part after the prefix is a valid unit name + // - the unit described that name is a linear unit (since only + // linear units can have prefixes) + if (unitName.startsWith(prefixName) + && prefixName.length() > longestLength) { + final String rest = unitName.substring(prefixName.length()); + if (this.containsKey(rest) + && this.get(rest) instanceof LinearUnit) { + longestPrefix = prefixName; + longestLength = prefixName.length(); + } + } + } + + return longestPrefix != null; + } + + /** + * {@inheritDoc} + * + * <p> + * Because of ambiguities between prefixes (i.e. kilokilo = mega), this + * method only tests for prefixless units. + * </p> + */ + @Override + public boolean containsValue(final Object value) { + return this.units.containsValue(value); + } + + @Override + public Set<Entry<String, Unit>> entrySet() { + if (this.entrySet == null) { + this.entrySet = new PrefixedUnitEntrySet(this); + } + return this.entrySet; + } + + @Override + public Unit get(final Object key) { + // First, test if there is a unit with the key + if (this.units.containsKey(key)) + return this.units.get(key); + + // Next, try to cast it to String + if (!(key instanceof String)) + throw new IllegalArgumentException( + "Attempted to obtain a unit using a non-string name."); + final String unitName = (String) key; + + // Then, look for the longest prefix that is attached to a valid unit + String longestPrefix = null; + int longestLength = 0; + + for (final String prefixName : this.prefixes.keySet()) { + // a prefix name is valid if: + // - it is prefixed (i.e. the unit name starts with it) + // - it is longer than the existing largest prefix (since I am + // looking for the longest valid prefix) + // - the part after the prefix is a valid unit name + // - the unit described that name is a linear unit (since only + // linear units can have prefixes) + if (unitName.startsWith(prefixName) + && prefixName.length() > longestLength) { + final String rest = unitName.substring(prefixName.length()); + if (this.containsKey(rest) + && this.get(rest) instanceof LinearUnit) { + longestPrefix = prefixName; + longestLength = prefixName.length(); + } + } + } + + // if none found, returns null + if (longestPrefix == null) + return null; + else { + // get necessary data + final String rest = unitName.substring(longestLength); + // this cast will not fail because I verified that it would work + // before selecting this prefix + final LinearUnit unit = (LinearUnit) this.get(rest); + final UnitPrefix prefix = this.prefixes.get(longestPrefix); + + return unit.withPrefix(prefix); + } + } + + @Override + public boolean isEmpty() { + return this.units.isEmpty(); + } + + @Override + public Set<String> keySet() { + if (this.keySet == null) { + this.keySet = new PrefixedUnitNameSet(this); + } + return this.keySet; + } + + @Override + public Unit merge(final String key, final Unit value, + final BiFunction<? super Unit, ? super Unit, ? extends Unit> remappingFunction) { + throw new UnsupportedOperationException( + "Cannot merge into an immutable map"); + } + + @Override + public Unit put(final String key, final Unit value) { + throw new UnsupportedOperationException( + "Cannot add entries to an immutable map"); + } + + @Override + public void putAll(final Map<? extends String, ? extends Unit> m) { + throw new UnsupportedOperationException( + "Cannot add entries to an immutable map"); + } + + @Override + public Unit putIfAbsent(final String key, final Unit value) { + throw new UnsupportedOperationException( + "Cannot add entries to an immutable map"); + } + + @Override + public Unit remove(final Object key) { + throw new UnsupportedOperationException( + "Cannot remove entries from an immutable map"); + } + + @Override + public boolean remove(final Object key, final Object value) { + throw new UnsupportedOperationException( + "Cannot remove entries from an immutable map"); + } + + @Override + public Unit replace(final String key, final Unit value) { + throw new UnsupportedOperationException( + "Cannot replace entries in an immutable map"); + } + + @Override + public boolean replace(final String key, final Unit oldValue, + final Unit newValue) { + throw new UnsupportedOperationException( + "Cannot replace entries in an immutable map"); + } + + @Override + public void replaceAll( + final BiFunction<? super String, ? super Unit, ? extends Unit> function) { + throw new UnsupportedOperationException( + "Cannot replace entries in an immutable map"); + } + + @Override + public int size() { + if (this.units.isEmpty()) + return 0; + else { + if (this.prefixes.isEmpty()) + return this.units.size(); + else + // infinite set + return Integer.MAX_VALUE; + } + } + + @Override + public String toString() { + if (this.units.isEmpty() || this.prefixes.isEmpty()) + return super.toString(); + else + return String.format( + "Infinite map of name-unit entries created from units %s and prefixes %s", + this.units, this.prefixes); + } + + /** + * {@inheritDoc} + * + * <p> + * Because of ambiguities between prefixes (i.e. kilokilo = mega), this + * method ignores prefixes. + * </p> + */ + @Override + public Collection<Unit> values() { + if (this.values == null) { + this.values = Collections + .unmodifiableCollection(this.units.values()); + } + return this.values; + } + } + + /** + * Replacements done to *all* expression types + */ + private static final Map<Pattern, String> EXPRESSION_REPLACEMENTS = new HashMap<>(); + + // add data to expression replacements + static { + // add spaces around operators + for (final String operator : Arrays.asList("\\*", "/", "\\^")) { + EXPRESSION_REPLACEMENTS.put(Pattern.compile(operator), + " " + operator + " "); + } + + // replace multiple spaces with a single space + EXPRESSION_REPLACEMENTS.put(Pattern.compile(" +"), " "); + // place brackets around any expression of the form "number unit", with or + // without the space + EXPRESSION_REPLACEMENTS.put(Pattern.compile("((?:-?[1-9]\\d*|0)" // integer + + "(?:\\.\\d+(?:[eE]\\d+))?)" // optional decimal point with numbers + // after it + + "\\s*" // optional space(s) + + "([a-zA-Z]+(?:\\^\\d+)?" // any string of letters + + "(?:\\s+[a-zA-Z]+(?:\\^\\d+)?))" // optional other letters + + "(?!-?\\d)" // no number directly afterwards (avoids matching + // "1e3") + ), "\\($1 $2\\)"); + } + + /** + * A regular expression that separates names and expressions in unit files. + */ + private static final Pattern NAME_EXPRESSION = Pattern + .compile("(\\S+)\\s+(\\S.*)"); + + /** + * The exponent operator + * + * @param base base of exponentiation + * @param exponentUnit exponent + * @return result + * @since 2019-04-10 + * @since v0.2.0 + */ + private static final LinearUnit exponentiateUnits(final LinearUnit base, + final LinearUnit exponentUnit) { + // exponent function - first check if o2 is a number, + if (exponentUnit.getBase().equals(SI.ONE.getBase())) { + // then check if it is an integer, + final double exponent = exponentUnit.getConversionFactor(); + if (DecimalComparison.equals(exponent % 1, 0)) + // then exponentiate + return base.toExponent((int) (exponent + 0.5)); + else + // not an integer + throw new UnsupportedOperationException( + "Decimal exponents are currently not supported."); + } else + // not a number + throw new IllegalArgumentException("Exponents must be numbers."); + } + + /** + * The exponent operator + * + * @param base base of exponentiation + * @param exponentUnit exponent + * @return result + * @since 2020-08-04 + */ + private static final LinearUnitValue exponentiateUnitValues( + final LinearUnitValue base, final LinearUnitValue exponentValue) { + // exponent function - first check if o2 is a number, + if (exponentValue.canConvertTo(SI.ONE)) { + // then check if it is an integer, + final double exponent = exponentValue.getValueExact(); + if (DecimalComparison.equals(exponent % 1, 0)) + // then exponentiate + return base.toExponent((int) (exponent + 0.5)); + else + // not an integer + throw new UnsupportedOperationException( + "Decimal exponents are currently not supported."); + } else + // not a number + throw new IllegalArgumentException("Exponents must be numbers."); + } + + /** + * The units in this system, excluding prefixes. + * + * @since 2019-01-07 + * @since v0.1.0 + */ + private final Map<String, Unit> prefixlessUnits; + + /** + * The unit prefixes in this system. + * + * @since 2019-01-14 + * @since v0.1.0 + */ + private final Map<String, UnitPrefix> prefixes; + + /** + * The dimensions in this system. + * + * @since 2019-03-14 + * @since v0.2.0 + */ + private final Map<String, ObjectProduct<BaseDimension>> dimensions; + + /** + * A map mapping strings to units (including prefixes) + * + * @since 2019-04-13 + * @since v0.2.0 + */ + private final Map<String, Unit> units; + + /** + * The rule that specifies when prefix repetition is allowed. It takes in one + * argument: a list of the prefixes being applied to the unit + * <p> + * The prefixes are inputted in <em>application order</em>. This means that + * testing whether "kilomegagigametre" is a valid unit is equivalent to + * running the following code (assuming all variables are defined correctly): + * <br> + * {@code prefixRepetitionRule.test(Arrays.asList(giga, mega, kilo))} + */ + private Predicate<List<UnitPrefix>> prefixRepetitionRule; + + /** + * A parser that can parse unit expressions. + * + * @since 2019-03-22 + * @since v0.2.0 + */ + private final ExpressionParser<LinearUnit> unitExpressionParser = new ExpressionParser.Builder<>( + this::getLinearUnit).addBinaryOperator("+", (o1, o2) -> o1.plus(o2), 0) + .addBinaryOperator("-", (o1, o2) -> o1.minus(o2), 0) + .addBinaryOperator("*", (o1, o2) -> o1.times(o2), 1) + .addSpaceFunction("*") + .addBinaryOperator("/", (o1, o2) -> o1.dividedBy(o2), 1) + .addBinaryOperator("^", UnitDatabase::exponentiateUnits, 2) + .build(); + + /** + * A parser that can parse unit value expressions. + * + * @since 2020-08-04 + */ + private final ExpressionParser<LinearUnitValue> unitValueExpressionParser = new ExpressionParser.Builder<>( + this::getLinearUnitValue) + .addBinaryOperator("+", (o1, o2) -> o1.plus(o2), 0) + .addBinaryOperator("-", (o1, o2) -> o1.minus(o2), 0) + .addBinaryOperator("*", (o1, o2) -> o1.times(o2), 1) + .addSpaceFunction("*") + .addBinaryOperator("/", (o1, o2) -> o1.dividedBy(o2), 1) + .addBinaryOperator("^", UnitDatabase::exponentiateUnitValues, 2) + .build(); + + /** + * A parser that can parse unit prefix expressions + * + * @since 2019-04-13 + * @since v0.2.0 + */ + private final ExpressionParser<UnitPrefix> prefixExpressionParser = new ExpressionParser.Builder<>( + this::getPrefix).addBinaryOperator("*", (o1, o2) -> o1.times(o2), 0) + .addSpaceFunction("*") + .addBinaryOperator("/", (o1, o2) -> o1.dividedBy(o2), 0) + .addBinaryOperator("^", + (o1, o2) -> o1.toExponent(o2.getMultiplier()), 1) + .build(); + + /** + * A parser that can parse unit dimension expressions. + * + * @since 2019-04-13 + * @since v0.2.0 + */ + private final ExpressionParser<ObjectProduct<BaseDimension>> unitDimensionParser = new ExpressionParser.Builder<>( + this::getDimension).addBinaryOperator("*", (o1, o2) -> o1.times(o2), 0) + .addSpaceFunction("*") + .addBinaryOperator("/", (o1, o2) -> o1.dividedBy(o2), 0).build(); + + /** + * Creates the {@code UnitsDatabase}. + * + * @since 2019-01-10 + * @since v0.1.0 + */ + public UnitDatabase() { + this(prefixes -> true); + } + + /** + * Creates the {@code UnitsDatabase} + * + * @param prefixRepetitionRule the rule that determines when prefix + * repetition is allowed + * @since 2020-08-26 + */ + public UnitDatabase(Predicate<List<UnitPrefix>> prefixRepetitionRule) { + this.prefixlessUnits = new HashMap<>(); + this.prefixes = new HashMap<>(); + this.dimensions = new HashMap<>(); + this.prefixRepetitionRule = prefixRepetitionRule; + this.units = ConditionalExistenceCollections.conditionalExistenceMap( + new PrefixedUnitMap(this.prefixlessUnits, this.prefixes), + entry -> this.prefixRepetitionRule + .test(this.getPrefixesFromName(entry.getKey()))); + } + + /** + * Adds a unit dimension to the database. + * + * @param name dimension's name + * @param dimension dimension to add + * @throws NullPointerException if name or dimension is null + * @since 2019-03-14 + * @since v0.2.0 + */ + public void addDimension(final String name, + final ObjectProduct<BaseDimension> dimension) { + this.dimensions.put( + Objects.requireNonNull(name, "name must not be null."), + Objects.requireNonNull(dimension, "dimension must not be null.")); + } + + /** + * Adds to the list from a line in a unit dimension file. + * + * @param line line to look at + * @param lineCounter number of line, for error messages + * @since 2019-04-10 + * @since v0.2.0 + */ + private void addDimensionFromLine(final String line, + final long lineCounter) { + // ignore lines that start with a # sign - they're comments + if (line.isEmpty()) + return; + if (line.contains("#")) { + this.addDimensionFromLine(line.substring(0, line.indexOf("#")), + lineCounter); + return; + } + + // divide line into name and expression + final Matcher lineMatcher = NAME_EXPRESSION.matcher(line); + if (!lineMatcher.matches()) + throw new IllegalArgumentException(String.format( + "Error at line %d: Lines of a dimension file must consist of a dimension name, then spaces or tabs, then a dimension expression.", + lineCounter)); + final String name = lineMatcher.group(1); + final String expression = lineMatcher.group(2); + + if (name.endsWith(" ")) { + System.err.printf("Warning - line %d's dimension name ends in a space", + lineCounter); + } + + // if expression is "!", search for an existing dimension + // if no unit found, throw an error + if (expression.equals("!")) { + if (!this.containsDimensionName(name)) + throw new IllegalArgumentException(String.format( + "! used but no dimension found (line %d).", lineCounter)); + } else { + // it's a unit, get the unit + final ObjectProduct<BaseDimension> dimension; + try { + dimension = this.getDimensionFromExpression(expression); + } catch (final IllegalArgumentException e) { + System.err.printf("Parsing error on line %d:%n", lineCounter); + throw e; + } + + this.addDimension(name, dimension); + } + } + + /** + * Adds a unit prefix to the database. + * + * @param name prefix's name + * @param prefix prefix to add + * @throws NullPointerException if name or prefix is null + * @since 2019-01-14 + * @since v0.1.0 + */ + public void addPrefix(final String name, final UnitPrefix prefix) { + this.prefixes.put(Objects.requireNonNull(name, "name must not be null."), + Objects.requireNonNull(prefix, "prefix must not be null.")); + } + + /** + * Adds a unit to the database. + * + * @param name unit's name + * @param unit unit to add + * @throws NullPointerException if unit is null + * @since 2019-01-10 + * @since v0.1.0 + */ + public void addUnit(final String name, final Unit unit) { + this.prefixlessUnits.put( + Objects.requireNonNull(name, "name must not be null."), + Objects.requireNonNull(unit, "unit must not be null.")); + } + + /** + * Adds to the list from a line in a unit file. + * + * @param line line to look at + * @param lineCounter number of line, for error messages + * @since 2019-04-10 + * @since v0.2.0 + */ + private void addUnitOrPrefixFromLine(final String line, + final long lineCounter) { + // ignore lines that start with a # sign - they're comments + if (line.isEmpty()) + return; + if (line.contains("#")) { + this.addUnitOrPrefixFromLine(line.substring(0, line.indexOf("#")), + lineCounter); + return; + } + + // divide line into name and expression + final Matcher lineMatcher = NAME_EXPRESSION.matcher(line); + if (!lineMatcher.matches()) + throw new IllegalArgumentException(String.format( + "Error at line %d: Lines of a unit file must consist of a unit name, then spaces or tabs, then a unit expression.", + lineCounter)); + final String name = lineMatcher.group(1); + + final String expression = lineMatcher.group(2); + + if (name.endsWith(" ")) { + System.err.printf("Warning - line %d's unit name ends in a space", + lineCounter); + } + + // if expression is "!", search for an existing unit + // if no unit found, throw an error + if (expression.equals("!")) { + if (!this.containsUnitName(name)) + throw new IllegalArgumentException(String + .format("! used but no unit found (line %d).", lineCounter)); + } else { + if (name.endsWith("-")) { + final UnitPrefix prefix; + try { + prefix = this.getPrefixFromExpression(expression); + } catch (final IllegalArgumentException e) { + System.err.printf("Parsing error on line %d:%n", lineCounter); + throw e; + } + this.addPrefix(name.substring(0, name.length() - 1), prefix); + } else { + // it's a unit, get the unit + final Unit unit; + try { + unit = this.getUnitFromExpression(expression); + } catch (final IllegalArgumentException e) { + System.err.printf("Parsing error on line %d:%n", lineCounter); + throw e; + } + + this.addUnit(name, unit); + } + } + } + + /** + * Tests if the database has a unit dimension with this name. + * + * @param name name to test + * @return if database contains name + * @since 2019-03-14 + * @since v0.2.0 + */ + public boolean containsDimensionName(final String name) { + return this.dimensions.containsKey(name); + } + + /** + * Tests if the database has a unit prefix with this name. + * + * @param name name to test + * @return if database contains name + * @since 2019-01-13 + * @since v0.1.0 + */ + public boolean containsPrefixName(final String name) { + return this.prefixes.containsKey(name); + } + + /** + * Tests if the database has a unit with this name, taking prefixes into + * consideration + * + * @param name name to test + * @return if database contains name + * @since 2019-01-13 + * @since v0.1.0 + */ + public boolean containsUnitName(final String name) { + return this.units.containsKey(name); + } + + /** + * @return a map mapping dimension names to dimensions + * @since 2019-04-13 + * @since v0.2.0 + */ + public Map<String, ObjectProduct<BaseDimension>> dimensionMap() { + return Collections.unmodifiableMap(this.dimensions); + } + + /** + * Evaluates a unit expression, following the same rules as + * {@link #getUnitFromExpression}. + * + * @param expression expression to parse + * @return {@code LinearUnitValue} representing value of expression + * @since 2020-08-04 + */ + public LinearUnitValue evaluateUnitExpression(final String expression) { + Objects.requireNonNull(expression, "expression must not be null."); + + // attempt to get a unit as an alias, or a number with precision first + if (this.containsUnitName(expression)) + return this.getLinearUnitValue(expression); + + // force operators to have spaces + String modifiedExpression = expression; + modifiedExpression = modifiedExpression.replaceAll("\\+", " \\+ "); + modifiedExpression = modifiedExpression.replaceAll("-", " - "); + + // format expression + for (final Entry<Pattern, String> replacement : EXPRESSION_REPLACEMENTS + .entrySet()) { + modifiedExpression = replacement.getKey().matcher(modifiedExpression) + .replaceAll(replacement.getValue()); + } + + // the previous operation breaks negative numbers, fix them! + // (i.e. -2 becomes - 2) + // FIXME the previous operaton also breaks stuff like "1e-5" + for (int i = 0; i < modifiedExpression.length(); i++) { + if (modifiedExpression.charAt(i) == '-' + && (i < 2 || Arrays.asList('+', '-', '*', '/', '^') + .contains(modifiedExpression.charAt(i - 2)))) { + // found a broken negative number + modifiedExpression = modifiedExpression.substring(0, i + 1) + + modifiedExpression.substring(i + 2); + } + } + + return this.unitValueExpressionParser.parseExpression(modifiedExpression); + } + + /** + * Gets a unit dimension from the database using its name. + * + * <p> + * This method accepts exponents, like "L^3" + * </p> + * + * @param name dimension's name + * @return dimension + * @since 2019-03-14 + * @since v0.2.0 + */ + public ObjectProduct<BaseDimension> getDimension(final String name) { + Objects.requireNonNull(name, "name must not be null."); + if (name.contains("^")) { + final String[] baseAndExponent = name.split("\\^"); + + final ObjectProduct<BaseDimension> base = this + .getDimension(baseAndExponent[0]); + + final int exponent; + try { + exponent = Integer + .parseInt(baseAndExponent[baseAndExponent.length - 1]); + } catch (final NumberFormatException e2) { + throw new IllegalArgumentException("Exponent must be an integer."); + } + + return base.toExponent(exponent); + } + return this.dimensions.get(name); + } + + /** + * Uses the database's data to parse an expression into a unit dimension + * <p> + * The expression is a series of any of the following: + * <ul> + * <li>The name of a unit dimension, which multiplies or divides the result + * based on preceding operators</li> + * <li>The operators '*' and '/', which multiply and divide (note that just + * putting two unit dimensions next to each other is equivalent to + * multiplication)</li> + * <li>The operator '^' which exponentiates. Exponents must be integers.</li> + * </ul> + * + * @param expression expression to parse + * @throws IllegalArgumentException if the expression cannot be parsed + * @throws NullPointerException if expression is null + * @since 2019-04-13 + * @since v0.2.0 + */ + public ObjectProduct<BaseDimension> getDimensionFromExpression( + final String expression) { + Objects.requireNonNull(expression, "expression must not be null."); + + // attempt to get a dimension as an alias first + if (this.containsDimensionName(expression)) + return this.getDimension(expression); + + // force operators to have spaces + String modifiedExpression = expression; + + // format expression + for (final Entry<Pattern, String> replacement : EXPRESSION_REPLACEMENTS + .entrySet()) { + modifiedExpression = replacement.getKey().matcher(modifiedExpression) + .replaceAll(replacement.getValue()); + } + modifiedExpression = modifiedExpression.replaceAll(" *\\^ *", "\\^"); + + return this.unitDimensionParser.parseExpression(modifiedExpression); + } + + /** + * Gets a unit. If it is linear, cast it to a LinearUnit and return it. + * Otherwise, throw an {@code IllegalArgumentException}. + * + * @param name unit's name + * @return unit + * @since 2019-03-22 + * @since v0.2.0 + */ + private LinearUnit getLinearUnit(final String name) { + // see if I am using a function-unit like tempC(100) + Objects.requireNonNull(name, "name may not be null"); + if (name.contains("(") && name.contains(")")) { + // break it into function name and value + final List<String> parts = Arrays.asList(name.split("\\(")); + if (parts.size() != 2) + throw new IllegalArgumentException( + "Format nonlinear units like: unit(value)."); + + // solve the function + final Unit unit = this.getUnit(parts.get(0)); + final double value = Double.parseDouble( + parts.get(1).substring(0, parts.get(1).length() - 1)); + return LinearUnit.fromUnitValue(unit, value); + } else { + // get a linear unit + final Unit unit = this.getUnit(name); + + if (unit instanceof LinearUnit) + return (LinearUnit) unit; + else + throw new IllegalArgumentException( + String.format("%s is not a linear unit.", name)); + } + } + + /** + * Gets a {@code LinearUnitValue} from a unit name. Nonlinear units will be + * converted to their base units. + * + * @param name name of unit + * @return {@code LinearUnitValue} instance + * @since 2020-08-04 + */ + private LinearUnitValue getLinearUnitValue(final String name) { + try { + // try to parse it as a number - otherwise it is not a number! + final BigDecimal number = new BigDecimal(name); + + final double uncertainty = Math.pow(10, -number.scale()); + return LinearUnitValue.of(SI.ONE, + UncertainDouble.of(number.doubleValue(), uncertainty)); + } catch (final NumberFormatException e) { + return LinearUnitValue.getExact(this.getLinearUnit(name), 1); + } + } + + /** + * Gets a unit prefix from the database from its name + * + * @param name prefix's name + * @return prefix + * @since 2019-01-10 + * @since v0.1.0 + */ + public UnitPrefix getPrefix(final String name) { + try { + return UnitPrefix.valueOf(Double.parseDouble(name)); + } catch (final NumberFormatException e) { + return this.prefixes.get(name); + } + } + + /** + * Gets all of the prefixes that are on a unit name, in application order. + * + * @param unitName name of unit + * @return prefixes + * @since 2020-08-26 + */ + List<UnitPrefix> getPrefixesFromName(final String unitName) { + final List<UnitPrefix> prefixes = new ArrayList<>(); + String name = unitName; + + while (!this.prefixlessUnits.containsKey(name)) { + // find the longest prefix + String longestPrefixName = null; + int longestLength = name.length(); + + while (longestPrefixName == null) { + longestLength--; + if (longestLength <= 0) + throw new AssertionError( + "No prefix found in " + name + ", but it is not a unit!"); + if (this.prefixes.containsKey(name.substring(0, longestLength))) { + longestPrefixName = name.substring(0, longestLength); + } + } + + // longest prefix found! + final UnitPrefix prefix = this.getPrefix(longestPrefixName); + prefixes.add(0, prefix); + name = name.substring(longestLength); + } + return prefixes; + } + + /** + * Gets a unit prefix from a prefix expression + * <p> + * Currently, prefix expressions are much simpler than unit expressions: They + * are either a number or the name of another prefix + * </p> + * + * @param expression expression to input + * @return prefix + * @throws IllegalArgumentException if expression cannot be parsed + * @throws NullPointerException if any argument is null + * @since 2019-01-14 + * @since v0.1.0 + */ + public UnitPrefix getPrefixFromExpression(final String expression) { + Objects.requireNonNull(expression, "expression must not be null."); + + // attempt to get a unit as an alias first + if (this.containsUnitName(expression)) + return this.getPrefix(expression); + + // force operators to have spaces + String modifiedExpression = expression; + + // format expression + for (final Entry<Pattern, String> replacement : EXPRESSION_REPLACEMENTS + .entrySet()) { + modifiedExpression = replacement.getKey().matcher(modifiedExpression) + .replaceAll(replacement.getValue()); + } + + return this.prefixExpressionParser.parseExpression(modifiedExpression); + } + + /** + * @return the prefixRepetitionRule + * @since 2020-08-26 + */ + public final Predicate<List<UnitPrefix>> getPrefixRepetitionRule() { + return this.prefixRepetitionRule; + } + + /** + * Gets a unit from the database from its name, looking for prefixes. + * + * @param name unit's name + * @return unit + * @since 2019-01-10 + * @since v0.1.0 + */ + public Unit getUnit(final String name) { + try { + final double value = Double.parseDouble(name); + return SI.ONE.times(value); + } catch (final NumberFormatException e) { + final Unit unit = this.units.get(name); + if (unit == null) + throw new NoSuchElementException("No unit " + name); + else if (unit.getPrimaryName().isEmpty()) + return unit.withName(NameSymbol.ofName(name)); + else if (!unit.getPrimaryName().get().equals(name)) { + final Set<String> otherNames = new HashSet<>(unit.getOtherNames()); + otherNames.add(unit.getPrimaryName().get()); + return unit.withName(NameSymbol.ofNullable(name, + unit.getSymbol().orElse(null), otherNames)); + } else if (!unit.getOtherNames().contains(name)) { + final Set<String> otherNames = new HashSet<>(unit.getOtherNames()); + otherNames.add(name); + return unit.withName( + NameSymbol.ofNullable(unit.getPrimaryName().orElse(null), + unit.getSymbol().orElse(null), otherNames)); + } else + return unit; + } + + } + + /** + * Uses the database's unit data to parse an expression into a unit + * <p> + * The expression is a series of any of the following: + * <ul> + * <li>The name of a unit, which multiplies or divides the result based on + * preceding operators</li> + * <li>The operators '*' and '/', which multiply and divide (note that just + * putting two units or values next to each other is equivalent to + * multiplication)</li> + * <li>The operator '^' which exponentiates. Exponents must be integers.</li> + * <li>A number which is multiplied or divided</li> + * </ul> + * This method only works with linear units. + * + * @param expression expression to parse + * @throws IllegalArgumentException if the expression cannot be parsed + * @throws NullPointerException if expression is null + * @since 2019-01-07 + * @since v0.1.0 + */ + public Unit getUnitFromExpression(final String expression) { + Objects.requireNonNull(expression, "expression must not be null."); + + // attempt to get a unit as an alias first + if (this.containsUnitName(expression)) + return this.getUnit(expression); + + // force operators to have spaces + String modifiedExpression = expression; + modifiedExpression = modifiedExpression.replaceAll("\\+", " \\+ "); + modifiedExpression = modifiedExpression.replaceAll("-", " - "); + + // format expression + for (final Entry<Pattern, String> replacement : EXPRESSION_REPLACEMENTS + .entrySet()) { + modifiedExpression = replacement.getKey().matcher(modifiedExpression) + .replaceAll(replacement.getValue()); + } + + // the previous operation breaks negative numbers, fix them! + // (i.e. -2 becomes - 2) + for (int i = 0; i < modifiedExpression.length(); i++) { + if (modifiedExpression.charAt(i) == '-' + && (i < 2 || Arrays.asList('+', '-', '*', '/', '^') + .contains(modifiedExpression.charAt(i - 2)))) { + // found a broken negative number + modifiedExpression = modifiedExpression.substring(0, i + 1) + + modifiedExpression.substring(i + 2); + } + } + + return this.unitExpressionParser.parseExpression(modifiedExpression); + } + + /** + * Adds all dimensions from a file, using data from the database to parse + * them. + * <p> + * Each line in the file should consist of a name and an expression (parsed + * by getDimensionFromExpression) separated by any number of tab characters. + * <p> + * <p> + * Allowed exceptions: + * <ul> + * <li>Anything after a '#' character is considered a comment and + * ignored.</li> + * <li>Blank lines are also ignored</li> + * <li>If an expression consists of a single exclamation point, instead of + * parsing it, this method will search the database for an existing unit. If + * no unit is found, an IllegalArgumentException is thrown. This is used to + * define initial units and ensure that the database contains them.</li> + * </ul> + * + * @param file file to read + * @throws IllegalArgumentException if the file cannot be parsed, found or + * read + * @throws NullPointerException if file is null + * @since 2019-01-13 + * @since v0.1.0 + */ + public void loadDimensionFile(final Path file) { + Objects.requireNonNull(file, "file must not be null."); + try { + long lineCounter = 0; + for (final String line : Files.readAllLines(file)) { + this.addDimensionFromLine(line, ++lineCounter); + } + } catch (final FileNotFoundException e) { + throw new IllegalArgumentException("Could not find file " + file, e); + } catch (final IOException e) { + throw new IllegalArgumentException("Could not read file " + file, e); + } + } + + /** + * Adds all units from a file, using data from the database to parse them. + * <p> + * Each line in the file should consist of a name and an expression (parsed + * by getUnitFromExpression) separated by any number of tab characters. + * <p> + * <p> + * Allowed exceptions: + * <ul> + * <li>Anything after a '#' character is considered a comment and + * ignored.</li> + * <li>Blank lines are also ignored</li> + * <li>If an expression consists of a single exclamation point, instead of + * parsing it, this method will search the database for an existing unit. If + * no unit is found, an IllegalArgumentException is thrown. This is used to + * define initial units and ensure that the database contains them.</li> + * </ul> + * + * @param file file to read + * @throws IllegalArgumentException if the file cannot be parsed, found or + * read + * @throws NullPointerException if file is null + * @since 2019-01-13 + * @since v0.1.0 + */ + public void loadUnitsFile(final Path file) { + Objects.requireNonNull(file, "file must not be null."); + try { + long lineCounter = 0; + for (final String line : Files.readAllLines(file)) { + this.addUnitOrPrefixFromLine(line, ++lineCounter); + } + } catch (final FileNotFoundException e) { + throw new IllegalArgumentException("Could not find file " + file, e); + } catch (final IOException e) { + throw new IllegalArgumentException("Could not read file " + file, e); + } + } + + /** + * @return a map mapping prefix names to prefixes + * @since 2019-04-13 + * @since v0.2.0 + */ + public Map<String, UnitPrefix> prefixMap() { + return Collections.unmodifiableMap(this.prefixes); + } + + /** + * @param prefixRepetitionRule the prefixRepetitionRule to set + * @since 2020-08-26 + */ + public final void setPrefixRepetitionRule( + Predicate<List<UnitPrefix>> prefixRepetitionRule) { + this.prefixRepetitionRule = prefixRepetitionRule; + } + + /** + * @return a string stating the number of units, prefixes and dimensions in + * the database + */ + @Override + public String toString() { + return String.format( + "Unit Database with %d units, %d unit prefixes and %d dimensions", + this.prefixlessUnits.size(), this.prefixes.size(), + this.dimensions.size()); + } + + /** + * Returns a map mapping unit names to units, including units with prefixes. + * <p> + * The returned map is infinite in size if there is at least one unit and at + * least one prefix. If it is infinite, some operations that only work with + * finite collections, like converting name/entry sets to arrays, will throw + * an {@code IllegalStateException}. + * </p> + * <p> + * Specifically, the operations that will throw an IllegalStateException if + * the map is infinite in size are: + * <ul> + * <li>{@code unitMap.entrySet().toArray()} (either overloading)</li> + * <li>{@code unitMap.keySet().toArray()} (either overloading)</li> + * </ul> + * </p> + * <p> + * Because of ambiguities between prefixes (i.e. kilokilo = mega), the map's + * {@link PrefixedUnitMap#containsValue containsValue} and + * {@link PrefixedUnitMap#values() values()} methods currently ignore + * prefixes. + * </p> + * + * @return a map mapping unit names to units, including prefixed names + * @since 2019-04-13 + * @since v0.2.0 + */ + public Map<String, Unit> unitMap() { + return this.units; // PrefixedUnitMap is immutable so I don't need to make + // an unmodifiable map. + } + + /** + * @return a map mapping unit names to units, ignoring prefixes + * @since 2019-04-13 + * @since v0.2.0 + */ + public Map<String, Unit> unitMapPrefixless() { + return Collections.unmodifiableMap(this.prefixlessUnits); + } +} diff --git a/src/main/java/org/unitConverter/unit/UnitPrefix.java b/src/main/java/org/unitConverter/unit/UnitPrefix.java new file mode 100644 index 0000000..31cc0b3 --- /dev/null +++ b/src/main/java/org/unitConverter/unit/UnitPrefix.java @@ -0,0 +1,242 @@ +/** + * Copyright (C) 2019 Adrien Hopkins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ +package org.unitConverter.unit; + +import java.util.Objects; +import java.util.Optional; +import java.util.Set; + +import org.unitConverter.math.DecimalComparison; + +/** + * A prefix that can be applied to a {@code LinearUnit} to multiply it by some value + * + * @author Adrien Hopkins + * @since 2019-10-16 + */ +public final class UnitPrefix { + /** + * Gets a {@code UnitPrefix} from a multiplier + * + * @param multiplier + * multiplier of prefix + * @return prefix + * @since 2019-10-16 + */ + public static UnitPrefix valueOf(final double multiplier) { + return new UnitPrefix(multiplier, NameSymbol.EMPTY); + } + + /** + * Gets a {@code UnitPrefix} from a multiplier and a name + * + * @param multiplier + * multiplier of prefix + * @param ns + * name(s) and symbol of prefix + * @return prefix + * @since 2019-10-16 + * @throws NullPointerException + * if ns is null + */ + public static UnitPrefix valueOf(final double multiplier, final NameSymbol ns) { + return new UnitPrefix(multiplier, Objects.requireNonNull(ns, "ns must not be null.")); + } + + /** + * This prefix's primary name + */ + private final Optional<String> primaryName; + + /** + * This prefix's symbol + */ + private final Optional<String> symbol; + + /** + * Other names and symbols used by this prefix + */ + private final Set<String> otherNames; + + /** + * The number that this prefix multiplies units by + * + * @since 2019-10-16 + */ + private final double multiplier; + + /** + * Creates the {@code DefaultUnitPrefix}. + * + * @param multiplier + * @since 2019-01-14 + * @since v0.2.0 + */ + private UnitPrefix(final double multiplier, final NameSymbol ns) { + this.multiplier = multiplier; + this.primaryName = ns.getPrimaryName(); + this.symbol = ns.getSymbol(); + this.otherNames = ns.getOtherNames(); + } + + /** + * Divides this prefix by a scalar + * + * @param divisor + * number to divide by + * @return quotient of prefix and scalar + * @since 2019-10-16 + */ + public UnitPrefix dividedBy(final double divisor) { + return valueOf(this.getMultiplier() / divisor); + } + + /** + * Divides this prefix by {@code other}. + * + * @param other + * prefix to divide by + * @return quotient of prefixes + * @since 2019-04-13 + * @since v0.2.0 + */ + public UnitPrefix dividedBy(final UnitPrefix other) { + return valueOf(this.getMultiplier() / other.getMultiplier()); + } + + /** + * {@inheritDoc} + * + * Uses the prefix's multiplier to determine equality. + */ + @Override + public boolean equals(final Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (!(obj instanceof UnitPrefix)) + return false; + final UnitPrefix other = (UnitPrefix) obj; + return DecimalComparison.equals(this.getMultiplier(), other.getMultiplier()); + } + + /** + * @return prefix's multiplier + * @since 2019-11-26 + */ + public double getMultiplier() { + return this.multiplier; + } + + /** + * @return other names + * @since 2019-11-26 + */ + public final Set<String> getOtherNames() { + return this.otherNames; + } + + /** + * @return primary name + * @since 2019-11-26 + */ + public final Optional<String> getPrimaryName() { + return this.primaryName; + } + + /** + * @return symbol + * @since 2019-11-26 + */ + public final Optional<String> getSymbol() { + return this.symbol; + } + + /** + * {@inheritDoc} + * + * Uses the prefix's multiplier to determine a hash code. + */ + @Override + public int hashCode() { + return DecimalComparison.hash(this.getMultiplier()); + } + + /** + * Multiplies this prefix by a scalar + * + * @param multiplicand + * number to multiply by + * @return product of prefix and scalar + * @since 2019-10-16 + */ + public UnitPrefix times(final double multiplicand) { + return valueOf(this.getMultiplier() * multiplicand); + } + + /** + * Multiplies this prefix by {@code other}. + * + * @param other + * prefix to multiply by + * @return product of prefixes + * @since 2019-04-13 + * @since v0.2.0 + */ + public UnitPrefix times(final UnitPrefix other) { + return valueOf(this.getMultiplier() * other.getMultiplier()); + } + + /** + * Raises this prefix to an exponent. + * + * @param exponent + * exponent to raise to + * @return result of exponentiation. + * @since 2019-04-13 + * @since v0.2.0 + */ + public UnitPrefix toExponent(final double exponent) { + return valueOf(Math.pow(this.getMultiplier(), exponent)); + } + + /** + * @return a string describing the prefix and its multiplier + */ + @Override + public String toString() { + if (this.primaryName.isPresent()) + return String.format("%s (\u00D7 %s)", this.primaryName.get(), this.multiplier); + else if (this.symbol.isPresent()) + return String.format("%s (\u00D7 %s)", this.symbol.get(), this.multiplier); + else + return String.format("Unit Prefix (\u00D7 %s)", this.multiplier); + } + + /** + * @param ns + * name(s) and symbol to use + * @return copy of this prefix with provided name(s) and symbol + * @since 2019-11-26 + * @throws NullPointerException + * if ns is null + */ + public UnitPrefix withName(final NameSymbol ns) { + return valueOf(this.multiplier, ns); + } +} diff --git a/src/main/java/org/unitConverter/unit/UnitValue.java b/src/main/java/org/unitConverter/unit/UnitValue.java new file mode 100644 index 0000000..c138332 --- /dev/null +++ b/src/main/java/org/unitConverter/unit/UnitValue.java @@ -0,0 +1,172 @@ +/** + * Copyright (C) 2019 Adrien Hopkins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ +package org.unitConverter.unit; + +import java.util.Objects; +import java.util.Optional; + +/** + * A value expressed in a unit. + * + * Unless otherwise indicated, all methods in this class throw a + * {@code NullPointerException} when an argument is null. + * + * @author Adrien Hopkins + * @since 2020-07-26 + */ +public final class UnitValue { + /** + * Creates a {@code UnitValue} from a unit and the associated value. + * + * @param unit unit to use + * @param value value to use + * @return {@code UnitValue} instance + */ + public static UnitValue of(Unit unit, double value) { + return new UnitValue( + Objects.requireNonNull(unit, "unit must not be null"), value); + } + + private final Unit unit; + private final double value; + + /** + * @param unit the unit being used + * @param value the value being represented + */ + private UnitValue(Unit unit, Double value) { + this.unit = unit; + this.value = value; + } + + /** + * @return true if this value can be converted to {@code other}. + * @since 2020-10-01 + */ + public final boolean canConvertTo(Unit other) { + return this.unit.canConvertTo(other); + } + + /** + * @return true if this value can be converted to {@code other}. + * @since 2020-10-01 + */ + public final <W> boolean canConvertTo(Unitlike<W> other) { + return this.unit.canConvertTo(other); + } + + /** + * Returns a UnitlikeValue that represents the same value expressed in a + * different unitlike form. + * + * @param other new unit to express value in + * @return value expressed in {@code other} + */ + public final <U extends Unitlike<W>, W> UnitlikeValue<U, W> convertTo( + U other) { + return UnitlikeValue.of(other, + this.unit.convertTo(other, this.getValue())); + } + + /** + * Returns a UnitValue that represents the same value expressed in a + * different unit + * + * @param other new unit to express value in + * @return value expressed in {@code other} + */ + public final UnitValue convertTo(Unit other) { + return UnitValue.of(other, + this.getUnit().convertTo(other, this.getValue())); + } + + /** + * Returns this unit value represented as a {@code LinearUnitValue} with this + * unit's base unit as the base. + * + * @param ns name and symbol for the base unit, use NameSymbol.EMPTY if not + * needed. + * @since 2020-09-29 + */ + public final LinearUnitValue convertToBase(NameSymbol ns) { + final LinearUnit base = LinearUnit.getBase(this.unit).withName(ns); + return this.convertToLinear(base); + } + + /** + * @return a {@code LinearUnitValue} that is equivalent to this value. It + * will have zero uncertainty. + * @since 2020-09-29 + */ + public final LinearUnitValue convertToLinear(LinearUnit other) { + return LinearUnitValue.getExact(other, + this.getUnit().convertTo(other, this.getValue())); + } + + /** + * Returns true if this and obj represent the same value, regardless of + * whether or not they are expressed in the same unit. So (1000 m).equals(1 + * km) returns true. + */ + @Override + public boolean equals(Object obj) { + if (!(obj instanceof UnitValue)) + return false; + final UnitValue other = (UnitValue) obj; + return Objects.equals(this.getUnit().getBase(), other.getUnit().getBase()) + && Double.doubleToLongBits( + this.getUnit().convertToBase(this.getValue())) == Double + .doubleToLongBits( + other.getUnit().convertToBase(other.getValue())); + } + + /** + * @return the unit + * @since 2020-09-29 + */ + public final Unit getUnit() { + return this.unit; + } + + /** + * @return the value + * @since 2020-09-29 + */ + public final double getValue() { + return this.value; + } + + @Override + public int hashCode() { + return Objects.hash(this.getUnit().getBase(), + this.getUnit().convertFromBase(this.getValue())); + } + + @Override + public String toString() { + final Optional<String> primaryName = this.getUnit().getPrimaryName(); + final Optional<String> symbol = this.getUnit().getSymbol(); + if (primaryName.isEmpty() && symbol.isEmpty()) { + final double baseValue = this.getUnit().convertToBase(this.getValue()); + return String.format("%s unnamed unit (= %s %s)", this.getValue(), + baseValue, this.getUnit().getBase()); + } else { + final String unitName = symbol.orElse(primaryName.get()); + return this.getValue() + " " + unitName; + } + } +} diff --git a/src/main/java/org/unitConverter/unit/Unitlike.java b/src/main/java/org/unitConverter/unit/Unitlike.java new file mode 100644 index 0000000..8077771 --- /dev/null +++ b/src/main/java/org/unitConverter/unit/Unitlike.java @@ -0,0 +1,260 @@ +/** + * Copyright (C) 2020 Adrien Hopkins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ +package org.unitConverter.unit; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.function.DoubleFunction; +import java.util.function.ToDoubleFunction; + +import org.unitConverter.math.ObjectProduct; + +/** + * An object that can convert a value between multiple forms (instances of the + * object); like a unit but the "converted value" can be any type. + * + * @since 2020-09-07 + */ +public abstract class Unitlike<V> implements Nameable { + /** + * Returns a unitlike form from its base and the functions it uses to convert + * to and from its base. + * + * @param base unitlike form's base + * @param converterFrom function that accepts a value expressed in the + * unitlike form's base and returns that value expressed + * in this unitlike form. + * @param converterTo function that accepts a value expressed in the + * unitlike form and returns that value expressed in the + * unit's base. + * @return a unitlike form that uses the provided functions to convert. + * @since 2020-09-07 + * @throws NullPointerException if any argument is null + */ + public static final <W> Unitlike<W> fromConversionFunctions( + final ObjectProduct<BaseUnit> base, + final DoubleFunction<W> converterFrom, + final ToDoubleFunction<W> converterTo) { + return new FunctionalUnitlike<>(base, NameSymbol.EMPTY, converterFrom, + converterTo); + } + + /** + * Returns a unitlike form from its base and the functions it uses to convert + * to and from its base. + * + * @param base unitlike form's base + * @param converterFrom function that accepts a value expressed in the + * unitlike form's base and returns that value expressed + * in this unitlike form. + * @param converterTo function that accepts a value expressed in the + * unitlike form and returns that value expressed in the + * unit's base. + * @param ns names and symbol of unit + * @return a unitlike form that uses the provided functions to convert. + * @since 2020-09-07 + * @throws NullPointerException if any argument is null + */ + public static final <W> Unitlike<W> fromConversionFunctions( + final ObjectProduct<BaseUnit> base, + final DoubleFunction<W> converterFrom, + final ToDoubleFunction<W> converterTo, final NameSymbol ns) { + return new FunctionalUnitlike<>(base, ns, converterFrom, converterTo); + } + + /** + * The combination of units that this unit is based on. + * + * @since 2019-10-16 + */ + private final ObjectProduct<BaseUnit> unitBase; + + /** + * This unit's name(s) and symbol + * + * @since 2020-09-07 + */ + private final NameSymbol nameSymbol; + + /** + * Cache storing the result of getDimension() + * + * @since 2019-10-16 + */ + private transient ObjectProduct<BaseDimension> dimension = null; + + /** + * @param unitBase + * @since 2020-09-07 + */ + protected Unitlike(ObjectProduct<BaseUnit> unitBase, NameSymbol ns) { + this.unitBase = Objects.requireNonNull(unitBase, + "unitBase may not be null"); + this.nameSymbol = Objects.requireNonNull(ns, "ns may not be null"); + } + + /** + * Checks if a value expressed in this unitlike form can be converted to a + * value expressed in {@code other} + * + * @param other unit or unitlike form to test with + * @return true if they are compatible + * @since 2019-01-13 + * @since v0.1.0 + * @throws NullPointerException if other is null + */ + public final boolean canConvertTo(final Unit other) { + Objects.requireNonNull(other, "other must not be null."); + return Objects.equals(this.getBase(), other.getBase()); + } + + /** + * Checks if a value expressed in this unitlike form can be converted to a + * value expressed in {@code other} + * + * @param other unit or unitlike form to test with + * @return true if they are compatible + * @since 2019-01-13 + * @since v0.1.0 + * @throws NullPointerException if other is null + */ + public final <W> boolean canConvertTo(final Unitlike<W> other) { + Objects.requireNonNull(other, "other must not be null."); + return Objects.equals(this.getBase(), other.getBase()); + } + + protected abstract V convertFromBase(double value); + + /** + * Converts a value expressed in this unitlike form to a value expressed in + * {@code other}. + * + * @implSpec If conversion is possible, this implementation returns + * {@code other.convertFromBase(this.convertToBase(value))}. + * Therefore, overriding either of those methods will change the + * output of this method. + * + * @param other unit to convert to + * @param value value to convert + * @return converted value + * @since 2019-05-22 + * @throws IllegalArgumentException if {@code other} is incompatible for + * conversion with this unitlike form (as + * tested by {@link Unit#canConvertTo}). + * @throws NullPointerException if other is null + */ + public final double convertTo(final Unit other, final V value) { + Objects.requireNonNull(other, "other must not be null."); + if (this.canConvertTo(other)) + return other.convertFromBase(this.convertToBase(value)); + else + throw new IllegalArgumentException( + String.format("Cannot convert from %s to %s.", this, other)); + } + + /** + * Converts a value expressed in this unitlike form to a value expressed in + * {@code other}. + * + * @implSpec If conversion is possible, this implementation returns + * {@code other.convertFromBase(this.convertToBase(value))}. + * Therefore, overriding either of those methods will change the + * output of this method. + * + * @param other unitlike form to convert to + * @param value value to convert + * @param <W> type of value to convert to + * @return converted value + * @since 2020-09-07 + * @throws IllegalArgumentException if {@code other} is incompatible for + * conversion with this unitlike form (as + * tested by {@link Unit#canConvertTo}). + * @throws NullPointerException if other is null + */ + public final <W> W convertTo(final Unitlike<W> other, final V value) { + Objects.requireNonNull(other, "other must not be null."); + if (this.canConvertTo(other)) + return other.convertFromBase(this.convertToBase(value)); + else + throw new IllegalArgumentException( + String.format("Cannot convert from %s to %s.", this, other)); + } + + protected abstract double convertToBase(V value); + + /** + * @return combination of units that this unit is based on + * @since 2018-12-22 + * @since v0.1.0 + */ + public final ObjectProduct<BaseUnit> getBase() { + return this.unitBase; + } + + /** + * @return dimension measured by this unit + * @since 2018-12-22 + * @since v0.1.0 + */ + public final ObjectProduct<BaseDimension> getDimension() { + if (this.dimension == null) { + final Map<BaseUnit, Integer> mapping = this.unitBase.exponentMap(); + final Map<BaseDimension, Integer> dimensionMap = new HashMap<>(); + + for (final BaseUnit key : mapping.keySet()) { + dimensionMap.put(key.getBaseDimension(), mapping.get(key)); + } + + this.dimension = ObjectProduct.fromExponentMapping(dimensionMap); + } + return this.dimension; + } + + /** + * @return the nameSymbol + * @since 2020-09-07 + */ + @Override + public final NameSymbol getNameSymbol() { + return this.nameSymbol; + } + + @Override + public String toString() { + return this.getPrimaryName().orElse("Unnamed unitlike form") + + (this.getSymbol().isPresent() + ? String.format(" (%s)", this.getSymbol().get()) + : "") + + ", derived from " + + this.getBase().toString(u -> u.getSymbol().get()) + + (this.getOtherNames().isEmpty() ? "" + : ", also called " + String.join(", ", this.getOtherNames())); + } + + /** + * @param ns name(s) and symbol to use + * @return a copy of this unitlike form with provided name(s) and symbol + * @since 2020-09-07 + * @throws NullPointerException if ns is null + */ + public Unitlike<V> withName(final NameSymbol ns) { + return fromConversionFunctions(this.getBase(), this::convertFromBase, + this::convertToBase, + Objects.requireNonNull(ns, "ns must not be null.")); + } +} diff --git a/src/main/java/org/unitConverter/unit/UnitlikeValue.java b/src/main/java/org/unitConverter/unit/UnitlikeValue.java new file mode 100644 index 0000000..79201c4 --- /dev/null +++ b/src/main/java/org/unitConverter/unit/UnitlikeValue.java @@ -0,0 +1,174 @@ +/** + * Copyright (C) 2020 Adrien Hopkins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ +package org.unitConverter.unit; + +import java.util.Optional; + +/** + * + * @since 2020-09-07 + */ +final class UnitlikeValue<T extends Unitlike<V>, V> { + /** + * Gets a {@code UnitlikeValue<V>}. + * + * @since 2020-10-02 + */ + public static <T extends Unitlike<V>, V> UnitlikeValue<T, V> of(T unitlike, + V value) { + return new UnitlikeValue<>(unitlike, value); + } + + private final T unitlike; + private final V value; + + /** + * @param unitlike + * @param value + * @since 2020-09-07 + */ + private UnitlikeValue(T unitlike, V value) { + this.unitlike = unitlike; + this.value = value; + } + + /** + * @return true if this value can be converted to {@code other}. + * @since 2020-10-01 + */ + public final boolean canConvertTo(Unit other) { + return this.unitlike.canConvertTo(other); + } + + /** + * @return true if this value can be converted to {@code other}. + * @since 2020-10-01 + */ + public final <W> boolean canConvertTo(Unitlike<W> other) { + return this.unitlike.canConvertTo(other); + } + + /** + * Returns a UnitlikeValue that represents the same value expressed in a + * different unitlike form. + * + * @param other new unit to express value in + * @return value expressed in {@code other} + */ + public final <U extends Unitlike<W>, W> UnitlikeValue<U, W> convertTo( + U other) { + return UnitlikeValue.of(other, + this.unitlike.convertTo(other, this.getValue())); + } + + /** + * Returns a UnitValue that represents the same value expressed in a + * different unit + * + * @param other new unit to express value in + * @return value expressed in {@code other} + */ + public final UnitValue convertTo(Unit other) { + return UnitValue.of(other, + this.unitlike.convertTo(other, this.getValue())); + } + + /** + * Returns this unit value represented as a {@code LinearUnitValue} with this + * unit's base unit as the base. + * + * @param ns name and symbol for the base unit, use NameSymbol.EMPTY if not + * needed. + * @since 2020-09-29 + */ + public final LinearUnitValue convertToBase(NameSymbol ns) { + final LinearUnit base = LinearUnit.getBase(this.unitlike).withName(ns); + return this.convertToLinear(base); + } + + /** + * @return a {@code LinearUnitValue} that is equivalent to this value. It + * will have zero uncertainty. + * @since 2020-09-29 + */ + public final LinearUnitValue convertToLinear(LinearUnit other) { + return LinearUnitValue.getExact(other, + this.getUnitlike().convertTo(other, this.getValue())); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (!(obj instanceof UnitlikeValue)) + return false; + final UnitlikeValue<?, ?> other = (UnitlikeValue<?, ?>) obj; + if (this.getUnitlike() == null) { + if (other.getUnitlike() != null) + return false; + } else if (!this.getUnitlike().equals(other.getUnitlike())) + return false; + if (this.getValue() == null) { + if (other.getValue() != null) + return false; + } else if (!this.getValue().equals(other.getValue())) + return false; + return true; + } + + /** + * @return the unitlike + * @since 2020-09-29 + */ + public final Unitlike<V> getUnitlike() { + return this.unitlike; + } + + /** + * @return the value + * @since 2020-09-29 + */ + public final V getValue() { + return this.value; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + + (this.getUnitlike() == null ? 0 : this.getUnitlike().hashCode()); + result = prime * result + + (this.getValue() == null ? 0 : this.getValue().hashCode()); + return result; + } + + @Override + public String toString() { + final Optional<String> primaryName = this.getUnitlike().getPrimaryName(); + final Optional<String> symbol = this.getUnitlike().getSymbol(); + if (primaryName.isEmpty() && symbol.isEmpty()) { + final double baseValue = this.getUnitlike() + .convertToBase(this.getValue()); + return String.format("%s unnamed unit (= %s %s)", this.getValue(), + baseValue, this.getUnitlike().getBase()); + } else { + final String unitName = symbol.orElse(primaryName.get()); + return this.getValue() + " " + unitName; + } + } +} diff --git a/src/main/java/org/unitConverter/unit/package-info.java b/src/main/java/org/unitConverter/unit/package-info.java new file mode 100644 index 0000000..2f0e097 --- /dev/null +++ b/src/main/java/org/unitConverter/unit/package-info.java @@ -0,0 +1,24 @@ +/** + * Copyright (C) 2019 Adrien Hopkins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ +/** + * Everything to do with the units that make up Unit Converter. + * + * @author Adrien Hopkins + * @since 2019-10-16 + * @since v0.1.0 + */ +package org.unitConverter.unit;
\ No newline at end of file |