diff options
author | Adrien Hopkins <masterofnumbers17@gmail.com> | 2019-10-21 15:25:24 -0400 |
---|---|---|
committer | Adrien Hopkins <masterofnumbers17@gmail.com> | 2019-10-21 15:25:24 -0400 |
commit | 8c8f900416981863607c3c39d737ab1be8540e1a (patch) | |
tree | a036180832095671027babc8b0fc16e3ca4eca47 | |
parent | 511fe144da142082a02b5a5b07e67bb76df1331e (diff) | |
parent | ce7402fb5e52d947b6b7c383fa96e3aaaf9da188 (diff) |
Merge branch 'feature-new-units-def' into develop
30 files changed, 1625 insertions, 1206 deletions
diff --git a/src/org/unitConverter/converterGUI/UnitConverterGUI.java b/src/org/unitConverter/converterGUI/UnitConverterGUI.java index 2d3d1a5..0be6c9b 100644 --- a/src/org/unitConverter/converterGUI/UnitConverterGUI.java +++ b/src/org/unitConverter/converterGUI/UnitConverterGUI.java @@ -42,13 +42,13 @@ import javax.swing.JTabbedPane; import javax.swing.JTextArea; import javax.swing.JTextField; -import org.unitConverter.UnitsDatabase; -import org.unitConverter.dimension.StandardDimensions; -import org.unitConverter.dimension.UnitDimension; -import org.unitConverter.unit.BaseUnit; -import org.unitConverter.unit.NonlinearUnits; +import org.unitConverter.math.ObjectProduct; +import org.unitConverter.unit.BaseDimension; +import org.unitConverter.unit.BritishImperial; +import org.unitConverter.unit.LinearUnit; import org.unitConverter.unit.SI; import org.unitConverter.unit.Unit; +import org.unitConverter.unit.UnitDatabase; import org.unitConverter.unit.UnitPrefix; /** @@ -66,7 +66,7 @@ final class UnitConverterGUI { * @since 2019-04-14 * @since v0.2.0 */ - private static void addDefaults(final UnitsDatabase database) { + private static void addDefaults(final UnitDatabase database) { database.addUnit("metre", SI.METRE); database.addUnit("kilogram", SI.KILOGRAM); database.addUnit("gram", SI.KILOGRAM.dividedBy(1000)); @@ -75,24 +75,24 @@ final class UnitConverterGUI { database.addUnit("kelvin", SI.KELVIN); database.addUnit("mole", SI.MOLE); database.addUnit("candela", SI.CANDELA); - database.addUnit("bit", SI.SI.getBaseUnit(StandardDimensions.INFORMATION)); - database.addUnit("unit", SI.SI.getBaseUnit(UnitDimension.EMPTY)); + database.addUnit("bit", SI.BIT); + database.addUnit("unit", SI.ONE); // nonlinear units - must be loaded manually - database.addUnit("tempCelsius", NonlinearUnits.CELSIUS); - database.addUnit("tempFahrenheit", NonlinearUnits.FAHRENHEIT); + database.addUnit("tempCelsius", SI.CELSIUS); + database.addUnit("tempFahrenheit", BritishImperial.FAHRENHEIT); // load initial dimensions - database.addDimension("LENGTH", StandardDimensions.LENGTH); - database.addDimension("MASS", StandardDimensions.MASS); - database.addDimension("TIME", StandardDimensions.TIME); - database.addDimension("TEMPERATURE", StandardDimensions.TEMPERATURE); + database.addDimension("LENGTH", SI.Dimensions.LENGTH); + database.addDimension("MASS", SI.Dimensions.MASS); + database.addDimension("TIME", SI.Dimensions.TIME); + database.addDimension("TEMPERATURE", SI.Dimensions.TEMPERATURE); } /** The presenter's associated view. */ private final View view; /** The units known by the program. */ - private final UnitsDatabase database; + private final UnitDatabase database; /** The names of all of the units */ private final List<String> unitNames; @@ -119,7 +119,7 @@ final class UnitConverterGUI { this.view = view; // load initial units - this.database = new UnitsDatabase(); + this.database = new UnitDatabase(); Presenter.addDefaults(this.database); this.database.loadUnitsFile(new File("unitsfile.txt")); @@ -155,7 +155,7 @@ final class UnitConverterGUI { this.dimensionNames.sort(null); // sorts it using Comparable // a Predicate that returns true iff the argument is a full base unit - final Predicate<Unit> isFullBase = unit -> unit instanceof BaseUnit && ((BaseUnit) unit).isFullBase(); + final Predicate<Unit> isFullBase = unit -> unit instanceof LinearUnit && ((LinearUnit) unit).isBase(); // print out unit counts System.out.printf("Successfully loaded %d units with %d unit names (%d base units).%n", @@ -359,7 +359,7 @@ final class UnitConverterGUI { */ public final boolean unitMatchesDimension(final String unitName, final String dimensionName) { final Unit unit = this.database.getUnit(unitName); - final UnitDimension dimension = this.database.getDimension(dimensionName); + final ObjectProduct<BaseDimension> dimension = this.database.getDimension(dimensionName); return unit.getDimension().equals(dimension); } diff --git a/src/org/unitConverter/converterGUI/package-info.java b/src/org/unitConverter/converterGUI/package-info.java index 1555291..d85ecab 100644 --- a/src/org/unitConverter/converterGUI/package-info.java +++ b/src/org/unitConverter/converterGUI/package-info.java @@ -15,7 +15,7 @@ * along with this program. If not, see <https://www.gnu.org/licenses/>. */ /** - * All classes that work to convert units. + * The GUI interface of the Unit Converter. * * @author Adrien Hopkins * @since 2019-01-25 diff --git a/src/org/unitConverter/dimension/BaseDimension.java b/src/org/unitConverter/dimension/BaseDimension.java deleted file mode 100644 index 5e3ddad..0000000 --- a/src/org/unitConverter/dimension/BaseDimension.java +++ /dev/null @@ -1,40 +0,0 @@ -/** - * 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.dimension; - -/** - * A base dimension that makes up {@code UnitDimension} objects. - * - * @author Adrien Hopkins - * @since 2018-12-22 - * @since v0.1.0 - */ -public interface BaseDimension { - /** - * @return the dimension's name - * @since 2018-12-22 - * @since v0.1.0 - */ - String getName(); - - /** - * @return a short string (usually one character) that represents this base dimension - * @since 2018-12-22 - * @since v0.1.0 - */ - String getSymbol(); -} diff --git a/src/org/unitConverter/dimension/OtherBaseDimension.java b/src/org/unitConverter/dimension/OtherBaseDimension.java deleted file mode 100644 index 8aea2b9..0000000 --- a/src/org/unitConverter/dimension/OtherBaseDimension.java +++ /dev/null @@ -1,55 +0,0 @@ -/** - * 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.dimension; - -import java.util.Objects; - -/** - * Non-SI base dimensions. - * - * @author Adrien Hopkins - * @since 2019-01-14 - * @since v0.1.0 - */ -public enum OtherBaseDimension implements BaseDimension { - INFORMATION("Info"), CURRENCY("$$"); - - /** The dimension's symbol */ - private final String symbol; - - /** - * Creates the {@code SIBaseDimension}. - * - * @param symbol - * dimension's symbol - * @since 2018-12-11 - * @since v0.1.0 - */ - private OtherBaseDimension(final String symbol) { - this.symbol = Objects.requireNonNull(symbol, "symbol must not be null."); - } - - @Override - public String getName() { - return this.toString(); - } - - @Override - public String getSymbol() { - return this.symbol; - } -} diff --git a/src/org/unitConverter/dimension/SIBaseDimension.java b/src/org/unitConverter/dimension/SIBaseDimension.java deleted file mode 100644 index c459963..0000000 --- a/src/org/unitConverter/dimension/SIBaseDimension.java +++ /dev/null @@ -1,57 +0,0 @@ -/**
- * 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.dimension;
-
-import java.util.Objects;
-
-/**
- * The seven base dimensions that make up the SI.
- *
- * @author Adrien Hopkins
- * @since 2018-12-11
- * @since v0.1.0
- */
-public enum SIBaseDimension implements BaseDimension {
- LENGTH("L"), MASS("M"), TIME("T"), ELECTRIC_CURRENT("I"), TEMPERATURE("\u0398"), // u0398 is the theta symbol
- QUANTITY("N"), LUMINOUS_INTENSITY("J");
-
- /** The dimension's symbol */
- private final String symbol;
-
- /**
- * Creates the {@code SIBaseDimension}.
- *
- * @param symbol
- * dimension's symbol
- * @since 2018-12-11
- * @since v0.1.0
- */
- private SIBaseDimension(final String symbol) {
- this.symbol = Objects.requireNonNull(symbol, "symbol must not be null.");
- }
-
- @Override
- public String getName() {
- return this.toString();
- }
-
- @Override
- public String getSymbol() {
- return this.symbol;
- }
-
-}
diff --git a/src/org/unitConverter/dimension/StandardDimensions.java b/src/org/unitConverter/dimension/StandardDimensions.java deleted file mode 100644 index 4b1b814..0000000 --- a/src/org/unitConverter/dimension/StandardDimensions.java +++ /dev/null @@ -1,80 +0,0 @@ -/**
- * 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.dimension;
-
-/**
- * All of the dimensions that are used by the SI.
- *
- * @author Adrien Hopkins
- * @since 2018-12-11
- * @since v0.1.0
- */
-public final class StandardDimensions {
- // base dimensions
- public static final UnitDimension EMPTY = UnitDimension.EMPTY;
- public static final UnitDimension LENGTH = UnitDimension.getBase(SIBaseDimension.LENGTH);
- public static final UnitDimension MASS = UnitDimension.getBase(SIBaseDimension.MASS);
- public static final UnitDimension TIME = UnitDimension.getBase(SIBaseDimension.TIME);
- public static final UnitDimension ELECTRIC_CURRENT = UnitDimension.getBase(SIBaseDimension.ELECTRIC_CURRENT);
- public static final UnitDimension TEMPERATURE = UnitDimension.getBase(SIBaseDimension.TEMPERATURE);
- public static final UnitDimension QUANTITY = UnitDimension.getBase(SIBaseDimension.QUANTITY);
- public static final UnitDimension LUMINOUS_INTENSITY = UnitDimension.getBase(SIBaseDimension.LUMINOUS_INTENSITY);
- public static final UnitDimension INFORMATION = UnitDimension.getBase(OtherBaseDimension.INFORMATION);
- public static final UnitDimension CURRENCY = UnitDimension.getBase(OtherBaseDimension.CURRENCY);
- // derived dimensions without named SI units
- public static final UnitDimension AREA = LENGTH.times(LENGTH);
-
- public static final UnitDimension VOLUME = AREA.times(LENGTH);
- public static final UnitDimension VELOCITY = LENGTH.dividedBy(TIME);
- public static final UnitDimension ACCELERATION = VELOCITY.dividedBy(TIME);
- public static final UnitDimension WAVENUMBER = EMPTY.dividedBy(LENGTH);
- public static final UnitDimension MASS_DENSITY = MASS.dividedBy(VOLUME);
- public static final UnitDimension SURFACE_DENSITY = MASS.dividedBy(AREA);
- public static final UnitDimension SPECIFIC_VOLUME = VOLUME.dividedBy(MASS);
- public static final UnitDimension CURRENT_DENSITY = ELECTRIC_CURRENT.dividedBy(AREA);
- public static final UnitDimension MAGNETIC_FIELD_STRENGTH = ELECTRIC_CURRENT.dividedBy(LENGTH);
- public static final UnitDimension CONCENTRATION = QUANTITY.dividedBy(VOLUME);
- public static final UnitDimension MASS_CONCENTRATION = CONCENTRATION.times(MASS);
- public static final UnitDimension LUMINANCE = LUMINOUS_INTENSITY.dividedBy(AREA);
- public static final UnitDimension REFRACTIVE_INDEX = VELOCITY.dividedBy(VELOCITY);
- public static final UnitDimension REFLACTIVE_PERMEABILITY = EMPTY.times(EMPTY);
- public static final UnitDimension ANGLE = LENGTH.dividedBy(LENGTH);
- public static final UnitDimension SOLID_ANGLE = AREA.dividedBy(AREA);
- // derived dimensions with named SI units
- public static final UnitDimension FREQUENCY = EMPTY.dividedBy(TIME);
-
- public static final UnitDimension FORCE = MASS.times(ACCELERATION);
- public static final UnitDimension ENERGY = FORCE.times(LENGTH);
- public static final UnitDimension POWER = ENERGY.dividedBy(TIME);
- public static final UnitDimension ELECTRIC_CHARGE = ELECTRIC_CURRENT.times(TIME);
- public static final UnitDimension VOLTAGE = ENERGY.dividedBy(ELECTRIC_CHARGE);
- public static final UnitDimension CAPACITANCE = ELECTRIC_CHARGE.dividedBy(VOLTAGE);
- public static final UnitDimension ELECTRIC_RESISTANCE = VOLTAGE.dividedBy(ELECTRIC_CURRENT);
- public static final UnitDimension ELECTRIC_CONDUCTANCE = ELECTRIC_CURRENT.dividedBy(VOLTAGE);
- public static final UnitDimension MAGNETIC_FLUX = VOLTAGE.times(TIME);
- public static final UnitDimension MAGNETIC_FLUX_DENSITY = MAGNETIC_FLUX.dividedBy(AREA);
- public static final UnitDimension INDUCTANCE = MAGNETIC_FLUX.dividedBy(ELECTRIC_CURRENT);
- public static final UnitDimension LUMINOUS_FLUX = LUMINOUS_INTENSITY.times(SOLID_ANGLE);
- public static final UnitDimension ILLUMINANCE = LUMINOUS_FLUX.dividedBy(AREA);
- public static final UnitDimension SPECIFIC_ENERGY = ENERGY.dividedBy(MASS);
- public static final UnitDimension CATALYTIC_ACTIVITY = QUANTITY.dividedBy(TIME);
-
- // You may NOT get StandardDimensions instances!
- private StandardDimensions() {
- throw new AssertionError();
- }
-}
diff --git a/src/org/unitConverter/dimension/UnitDimension.java b/src/org/unitConverter/dimension/UnitDimension.java deleted file mode 100644 index dbeaeff..0000000 --- a/src/org/unitConverter/dimension/UnitDimension.java +++ /dev/null @@ -1,241 +0,0 @@ -/**
- * 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.dimension;
-
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Set;
-
-/**
- * An object that represents what a unit measures, like length, mass, area, energy, etc.
- *
- * @author Adrien Hopkins
- * @since 2018-12-11
- * @since v0.1.0
- */
-public final class UnitDimension {
- /**
- * The unit dimension where every exponent is zero
- *
- * @since 2018-12-12
- * @since v0.1.0
- */
- public static final UnitDimension EMPTY = new UnitDimension(new HashMap<>());
-
- /**
- * Gets an UnitDimension that has 1 of a certain dimension and nothing else
- *
- * @param dimension
- * dimension to get
- * @return unit dimension
- * @since 2018-12-11
- * @since v0.1.0
- */
- public static final UnitDimension getBase(final BaseDimension dimension) {
- final Map<BaseDimension, Integer> map = new HashMap<>();
- map.put(dimension, 1);
- return new UnitDimension(map);
- }
-
- /**
- * The base dimensions that make up this dimension.
- *
- * @since 2018-12-11
- * @since v0.1.0
- */
- final Map<BaseDimension, Integer> exponents;
-
- /**
- * Creates the {@code UnitDimension}.
- *
- * @param exponents
- * base dimensions that make up this dimension
- * @since 2018-12-11
- * @since v0.1.0
- */
- private UnitDimension(final Map<BaseDimension, Integer> exponents) {
- this.exponents = new HashMap<>(exponents);
- }
-
- /**
- * Divides this dimension by another
- *
- * @param other
- * other dimension
- * @return quotient of two dimensions
- * @since 2018-12-11
- * @since v0.1.0
- */
- public UnitDimension dividedBy(final UnitDimension other) {
- final Map<BaseDimension, Integer> map = new HashMap<>(this.exponents);
-
- for (final BaseDimension key : other.exponents.keySet()) {
- if (map.containsKey(key)) {
- // add the dimensions
- map.put(key, map.get(key) - other.exponents.get(key));
- } else {
- map.put(key, -other.exponents.get(key));
- }
- }
- return new UnitDimension(map);
- }
-
- @Override
- public boolean equals(final Object obj) {
- if (this == obj)
- return true;
- if (obj == null)
- return false;
- if (!(obj instanceof UnitDimension))
- return false;
- final UnitDimension other = (UnitDimension) obj;
-
- // anything with a value of 0 is equal to a nonexistent value
- for (final BaseDimension b : this.getBaseSet()) {
- if (this.exponents.get(b) != other.exponents.get(b))
- if (!(this.exponents.get(b) == 0 && !other.exponents.containsKey(b)))
- return false;
- }
- for (final BaseDimension b : other.getBaseSet()) {
- if (this.exponents.get(b) != other.exponents.get(b))
- if (!(other.exponents.get(b) == 0 && !this.exponents.containsKey(b)))
- return false;
- }
- return true;
- }
-
- /**
- * @return a set of all of the base dimensions with non-zero exponents that make up this dimension.
- * @since 2018-12-12
- * @since v0.1.0
- */
- public final Set<BaseDimension> getBaseSet() {
- final Set<BaseDimension> dimensions = new HashSet<>();
-
- // add all dimensions with a nonzero exponent - they shouldn't be there in the first place
- for (final BaseDimension dimension : this.exponents.keySet()) {
- if (!this.exponents.get(dimension).equals(0)) {
- dimensions.add(dimension);
- }
- }
-
- return dimensions;
- }
-
- /**
- * Gets the exponent for a specific dimension.
- *
- * @param dimension
- * dimension to check
- * @return exponent for that dimension
- * @since 2018-12-12
- * @since v0.1.0
- */
- public int getExponent(final BaseDimension dimension) {
- return this.exponents.getOrDefault(dimension, 0);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(this.exponents);
- }
-
- /**
- * @return true if this dimension is a base, i.e. it has one exponent of one and no other nonzero exponents
- * @since 2019-01-15
- * @since v0.1.0
- */
- public boolean isBase() {
- int oneCount = 0;
- boolean twoOrMore = false; // has exponents of 2 or more
- for (final BaseDimension b : this.getBaseSet()) {
- if (this.exponents.get(b) == 1) {
- oneCount++;
- } else if (this.exponents.get(b) != 0) {
- twoOrMore = true;
- }
- }
- return (oneCount == 0 || oneCount == 1) && !twoOrMore;
- }
-
- /**
- * Multiplies this dimension by another
- *
- * @param other
- * other dimension
- * @return product of two dimensions
- * @since 2018-12-11
- * @since v0.1.0
- */
- public UnitDimension times(final UnitDimension other) {
- final Map<BaseDimension, Integer> map = new HashMap<>(this.exponents);
-
- for (final BaseDimension key : other.exponents.keySet()) {
- if (map.containsKey(key)) {
- // add the dimensions
- map.put(key, map.get(key) + other.exponents.get(key));
- } else {
- map.put(key, other.exponents.get(key));
- }
- }
- return new UnitDimension(map);
- }
-
- /**
- * Returns this dimension, but to an exponent
- *
- * @param exp
- * exponent
- * @return result of exponientation
- * @since 2019-01-15
- * @since v0.1.0
- */
- public UnitDimension toExponent(final int exp) {
- final Map<BaseDimension, Integer> map = new HashMap<>(this.exponents);
- for (final BaseDimension key : this.exponents.keySet()) {
- map.put(key, this.getExponent(key) * exp);
- }
- return new UnitDimension(map);
- }
-
- @Override
- public String toString() {
- final List<String> positiveStringComponents = new ArrayList<>();
- final List<String> negativeStringComponents = new ArrayList<>();
-
- // for each base dimension that makes up this dimension, add it and its exponent
- for (final BaseDimension dimension : this.getBaseSet()) {
- final int exponent = this.exponents.get(dimension);
- if (exponent > 0) {
- positiveStringComponents.add(String.format("%s^%d", dimension.getSymbol(), exponent));
- } else if (exponent < 0) {
- negativeStringComponents.add(String.format("%s^%d", dimension.getSymbol(), -exponent));
- }
- }
-
- final String positiveString = positiveStringComponents.isEmpty() ? "1"
- : String.join(" ", positiveStringComponents);
- final String negativeString = negativeStringComponents.isEmpty() ? ""
- : " / " + String.join(" ", negativeStringComponents);
-
- return positiveString + negativeString;
- }
-}
diff --git a/src/org/unitConverter/math/ConditionalExistenceCollections.java b/src/org/unitConverter/math/ConditionalExistenceCollections.java new file mode 100644 index 0000000..9522885 --- /dev/null +++ b/src/org/unitConverter/math/ConditionalExistenceCollections.java @@ -0,0 +1,407 @@ +/** + * 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.math; + +import java.util.AbstractCollection; +import java.util.AbstractMap; +import java.util.AbstractSet; +import java.util.Collection; +import java.util.Iterator; +import java.util.Map; +import java.util.Map.Entry; +import java.util.NoSuchElementException; +import java.util.Set; +import java.util.function.Predicate; + +/** + * Elements in these wrapper collections only exist if they pass a condition. + * <p> + * All of the collections in this class are "views" of the provided collections. They are mutable if the provided + * collections are mutable, they allow null if the provided collections allow null, they will reflect changes in the + * provided collection, etc. + * <p> + * The modification operations will always run the corresponding operations, even if the conditional existence + * collection doesn't change. For example, if you have a set that ignores even numbers, add(2) will still add a 2 to the + * backing set (but the conditional existence set will say it doesn't exist). + * <p> + * The returned collections do <i>not</i> pass the hashCode and equals operations through to the backing collections, + * but rely on {@code Object}'s {@code equals} and {@code hashCode} methods. This is necessary to preserve the contracts + * of these operations in the case that the backing collections are sets or lists. + * <p> + * Other than that, <i>the only difference between the provided collections and the returned collections are that + * elements don't exist if they don't pass the provided condition</i>. + * + * + * @author Adrien Hopkins + * @since 2019-10-17 + */ +// TODO add conditional existence Lists and Sorted/Navigable Sets/Maps +public final class ConditionalExistenceCollections { + /** + * Elements in this collection only exist if they meet a condition. + * + * @author Adrien Hopkins + * @since 2019-10-17 + * @param <E> + * type of element in collection + */ + static final class ConditionalExistenceCollection<E> extends AbstractCollection<E> { + final Collection<E> collection; + final Predicate<E> existenceCondition; + + /** + * Creates the {@code ConditionalExistenceCollection}. + * + * @param collection + * @param existenceCondition + * @since 2019-10-17 + */ + private ConditionalExistenceCollection(final Collection<E> collection, final Predicate<E> existenceCondition) { + this.collection = collection; + this.existenceCondition = existenceCondition; + } + + @Override + public boolean add(final E e) { + return this.collection.add(e) && this.existenceCondition.test(e); + } + + @Override + public void clear() { + this.collection.clear(); + } + + @Override + public boolean contains(final Object o) { + if (!this.collection.contains(o)) + return false; + + // this collection can only contain instances of E + // since the object is in the collection, we know that it must be an instance of E + // therefore this cast will always work + @SuppressWarnings("unchecked") + final E e = (E) o; + + return this.existenceCondition.test(e); + } + + @Override + public Iterator<E> iterator() { + return conditionalExistenceIterator(this.collection.iterator(), this.existenceCondition); + } + + @Override + public boolean remove(final Object o) { + // remove() must be first in the && statement, otherwise it may not execute + final boolean containedObject = this.contains(o); + return this.collection.remove(o) && containedObject; + } + + @Override + public int size() { + return (int) this.collection.stream().filter(this.existenceCondition).count(); + } + } + + /** + * Elements in this wrapper iterator only exist if they pass a condition. + * + * @author Adrien Hopkins + * @since 2019-10-17 + * @param <E> + * type of elements in iterator + */ + static final class ConditionalExistenceIterator<E> implements Iterator<E> { + final Iterator<E> iterator; + final Predicate<E> existenceCondition; + E nextElement; + boolean hasNext; + + /** + * Creates the {@code ConditionalExistenceIterator}. + * + * @param iterator + * @param condition + * @since 2019-10-17 + */ + private ConditionalExistenceIterator(final Iterator<E> iterator, final Predicate<E> condition) { + this.iterator = iterator; + this.existenceCondition = condition; + this.getAndSetNextElement(); + } + + /** + * Gets the next element, and sets nextElement and hasNext accordingly. + * + * @since 2019-10-17 + */ + private void getAndSetNextElement() { + do { + if (!this.iterator.hasNext()) { + this.nextElement = null; + this.hasNext = false; + return; + } + this.nextElement = this.iterator.next(); + } while (!this.existenceCondition.test(this.nextElement)); + this.hasNext = true; + } + + @Override + public boolean hasNext() { + return this.hasNext; + } + + @Override + public E next() { + if (this.hasNext()) { + final E next = this.nextElement; + this.getAndSetNextElement(); + return next; + } else + throw new NoSuchElementException(); + } + + @Override + public void remove() { + this.iterator.remove(); + } + } + + /** + * Mappings in this map only exist if the entry passes some condition. + * + * @author Adrien Hopkins + * @since 2019-10-17 + * @param <K> + * key type + * @param <V> + * value type + */ + static final class ConditionalExistenceMap<K, V> extends AbstractMap<K, V> { + Map<K, V> map; + Predicate<Entry<K, V>> entryExistenceCondition; + + /** + * Creates the {@code ConditionalExistenceMap}. + * + * @param map + * @param entryExistenceCondition + * @since 2019-10-17 + */ + private ConditionalExistenceMap(final Map<K, V> map, final Predicate<Entry<K, V>> entryExistenceCondition) { + this.map = map; + this.entryExistenceCondition = entryExistenceCondition; + } + + @Override + public boolean containsKey(final Object key) { + if (!this.map.containsKey(key)) + return false; + + // only instances of K have mappings in the backing map + // since we know that key is a valid key, it must be an instance of K + @SuppressWarnings("unchecked") + final K keyAsK = (K) key; + + // get and test entry + final V value = this.map.get(key); + final Entry<K, V> entry = new SimpleEntry<>(keyAsK, value); + return this.entryExistenceCondition.test(entry); + } + + @Override + public Set<Entry<K, V>> entrySet() { + return conditionalExistenceSet(this.map.entrySet(), this.entryExistenceCondition); + } + + @Override + public V get(final Object key) { + return this.containsKey(key) ? this.map.get(key) : null; + } + + @Override + public Set<K> keySet() { + // maybe change this to use ConditionalExistenceSet + return super.keySet(); + } + + @Override + public V put(final K key, final V value) { + final V oldValue = this.map.put(key, value); + + // get and test entry + final Entry<K, V> entry = new SimpleEntry<>(key, oldValue); + return this.entryExistenceCondition.test(entry) ? oldValue : null; + } + + @Override + public V remove(final Object key) { + final V oldValue = this.map.remove(key); + return this.containsKey(key) ? oldValue : null; + } + + @Override + public Collection<V> values() { + // maybe change this to use ConditionalExistenceCollection + return super.values(); + } + + } + + /** + * Elements in this set only exist if a certain condition is true. + * + * @author Adrien Hopkins + * @since 2019-10-17 + * @param <E> + * type of element in set + */ + static final class ConditionalExistenceSet<E> extends AbstractSet<E> { + private final Set<E> set; + private final Predicate<E> existenceCondition; + + /** + * Creates the {@code ConditionalNonexistenceSet}. + * + * @param set + * set to use + * @param existenceCondition + * condition where element exists + * @since 2019-10-17 + */ + private ConditionalExistenceSet(final Set<E> set, final Predicate<E> existenceCondition) { + this.set = set; + this.existenceCondition = existenceCondition; + } + + /** + * {@inheritDoc} + * <p> + * Note that this method returns {@code false} if {@code e} does not pass the existence condition. + */ + @Override + public boolean add(final E e) { + return this.set.add(e) && this.existenceCondition.test(e); + } + + @Override + public void clear() { + this.set.clear(); + } + + @Override + public boolean contains(final Object o) { + if (!this.set.contains(o)) + return false; + + // this set can only contain instances of E + // since the object is in the set, we know that it must be an instance of E + // therefore this cast will always work + @SuppressWarnings("unchecked") + final E e = (E) o; + + return this.existenceCondition.test(e); + } + + @Override + public Iterator<E> iterator() { + return conditionalExistenceIterator(this.set.iterator(), this.existenceCondition); + } + + @Override + public boolean remove(final Object o) { + // remove() must be first in the && statement, otherwise it may not execute + final boolean containedObject = this.contains(o); + return this.set.remove(o) && containedObject; + } + + @Override + public int size() { + return (int) this.set.stream().filter(this.existenceCondition).count(); + } + } + + /** + * Elements in the returned wrapper collection are ignored if they don't pass a condition. + * + * @param <E> + * type of elements in collection + * @param collection + * collection to wrap + * @param existenceCondition + * elements only exist if this returns true + * @return wrapper collection + * @since 2019-10-17 + */ + public static final <E> Collection<E> conditionalExistenceCollection(final Collection<E> collection, + final Predicate<E> existenceCondition) { + return new ConditionalExistenceCollection<>(collection, existenceCondition); + } + + /** + * Elements in the returned wrapper iterator are ignored if they don't pass a condition. + * + * @param <E> + * type of elements in iterator + * @param iterator + * iterator to wrap + * @param existenceCondition + * elements only exist if this returns true + * @return wrapper iterator + * @since 2019-10-17 + */ + public static final <E> Iterator<E> conditionalExistenceIterator(final Iterator<E> iterator, + final Predicate<E> existenceCondition) { + return new ConditionalExistenceIterator<>(iterator, existenceCondition); + } + + /** + * Mappings in the returned wrapper map are ignored if the corresponding entry doesn't pass a condition + * + * @param <K> + * type of key in map + * @param <V> + * type of value in map + * @param map + * map to wrap + * @param entryExistenceCondition + * mappings only exist if this returns true + * @return wrapper map + * @since 2019-10-17 + */ + public static final <K, V> Map<K, V> conditionalExistenceMap(final Map<K, V> map, + final Predicate<Entry<K, V>> entryExistenceCondition) { + return new ConditionalExistenceMap<>(map, entryExistenceCondition); + } + + /** + * Elements in the returned wrapper set are ignored if they don't pass a condition. + * + * @param <E> + * type of elements in set + * @param set + * set to wrap + * @param existenceCondition + * elements only exist if this returns true + * @return wrapper set + * @since 2019-10-17 + */ + public static final <E> Set<E> conditionalExistenceSet(final Set<E> set, final Predicate<E> existenceCondition) { + return new ConditionalExistenceSet<>(set, existenceCondition); + } +} diff --git a/src/org/unitConverter/math/ConditionalExistenceCollectionsTest.java b/src/org/unitConverter/math/ConditionalExistenceCollectionsTest.java new file mode 100644 index 0000000..311ace5 --- /dev/null +++ b/src/org/unitConverter/math/ConditionalExistenceCollectionsTest.java @@ -0,0 +1,159 @@ +/** + * 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.math; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.NoSuchElementException; + +import org.junit.jupiter.api.Test; +import org.unitConverter.math.ConditionalExistenceCollections.ConditionalExistenceIterator; + +/** + * Tests the {@link #ConditionalExistenceCollections}. + * + * @author Adrien Hopkins + * @since 2019-10-16 + */ +class ConditionalExistenceCollectionsTest { + + /** + * The returned iterator ignores elements that don't start with "a". + * + * @return test iterator + * @since 2019-10-17 + */ + ConditionalExistenceIterator<String> getTestIterator() { + final List<String> items = Arrays.asList("aa", "ab", "ba"); + final Iterator<String> it = items.iterator(); + final ConditionalExistenceIterator<String> cit = (ConditionalExistenceIterator<String>) ConditionalExistenceCollections + .conditionalExistenceIterator(it, s -> s.startsWith("a")); + return cit; + } + + /** + * The returned map ignores mappings where the value is zero. + * + * @return map to be used for test data + * @since 2019-10-16 + */ + Map<String, Integer> getTestMap() { + final Map<String, Integer> map = new HashMap<>(); + map.put("one", 1); + map.put("two", 2); + map.put("zero", 0); + map.put("ten", 10); + final Map<String, Integer> conditionalMap = ConditionalExistenceCollections.conditionalExistenceMap(map, + e -> !Integer.valueOf(0).equals(e.getValue())); + return conditionalMap; + } + + /** + * Test method for {@link org.unitConverter.math.ZeroIsNullMap#containsKey(java.lang.Object)}. + */ + @Test + void testContainsKeyObject() { + final Map<String, Integer> map = this.getTestMap(); + assertTrue(map.containsKey("one")); + assertTrue(map.containsKey("ten")); + assertFalse(map.containsKey("five")); + assertFalse(map.containsKey("zero")); + } + + /** + * Test method for {@link org.unitConverter.math.ZeroIsNullMap#containsValue(java.lang.Object)}. + */ + @Test + void testContainsValueObject() { + final Map<String, Integer> map = this.getTestMap(); + assertTrue(map.containsValue(1)); + assertTrue(map.containsValue(10)); + assertFalse(map.containsValue(5)); + assertFalse(map.containsValue(0)); + } + + /** + * Test method for {@link org.unitConverter.math.ZeroIsNullMap#entrySet()}. + */ + @Test + void testEntrySet() { + final Map<String, Integer> map = this.getTestMap(); + for (final Entry<String, Integer> e : map.entrySet()) { + assertTrue(e.getValue() != 0); + } + } + + /** + * Test method for {@link org.unitConverter.math.ZeroIsNullMap#get(java.lang.Object)}. + */ + @Test + void testGetObject() { + final Map<String, Integer> map = this.getTestMap(); + assertEquals(1, map.get("one")); + assertEquals(10, map.get("ten")); + assertEquals(null, map.get("five")); + assertEquals(null, map.get("zero")); + } + + @Test + void testIterator() { + final ConditionalExistenceIterator<String> testIterator = this.getTestIterator(); + + assertTrue(testIterator.hasNext); + assertTrue(testIterator.hasNext()); + assertEquals("aa", testIterator.nextElement); + assertEquals("aa", testIterator.next()); + + assertTrue(testIterator.hasNext); + assertTrue(testIterator.hasNext()); + assertEquals("ab", testIterator.nextElement); + assertEquals("ab", testIterator.next()); + + assertFalse(testIterator.hasNext); + assertFalse(testIterator.hasNext()); + assertEquals(null, testIterator.nextElement); + assertThrows(NoSuchElementException.class, testIterator::next); + } + + /** + * Test method for {@link org.unitConverter.math.ZeroIsNullMap#keySet()}. + */ + @Test + void testKeySet() { + final Map<String, Integer> map = this.getTestMap(); + assertFalse(map.keySet().contains("zero")); + } + + /** + * Test method for {@link org.unitConverter.math.ZeroIsNullMap#values()}. + */ + @Test + void testValues() { + final Map<String, Integer> map = this.getTestMap(); + assertFalse(map.values().contains(0)); + } + +} diff --git a/src/org/unitConverter/math/DecimalComparison.java b/src/org/unitConverter/math/DecimalComparison.java index 7cdbe5b..859e8da 100644 --- a/src/org/unitConverter/math/DecimalComparison.java +++ b/src/org/unitConverter/math/DecimalComparison.java @@ -16,6 +16,8 @@ */ package org.unitConverter.math; +import java.math.BigDecimal; + /** * A class that contains methods to compare float and double values. * @@ -44,6 +46,18 @@ public final class DecimalComparison { /** * Tests for equality of double values using {@link #DOUBLE_EPSILON}. + * <p> + * <strong>WARNING: </strong>this method is not technically transitive. If a and b are off by slightly less than + * {@code epsilon * max(abs(a), abs(b))}, and b and c are off by slightly less than + * {@code epsilon * max(abs(b), abs(c))}, then equals(a, b) and equals(b, c) will both return true, but equals(a, c) + * will return false. However, this situation is very unlikely to ever happen in a real programming situation. + * <p> + * If this does become a concern, some ways to solve this problem: + * <ol> + * <li>Raise the value of epsilon using {@link #equals(double, double, double)} (this does not make a violation of + * transitivity impossible, it just significantly reduces the chances of it happening) + * <li>Use {@link BigDecimal} instead of {@code double} (this will make a violation of transitivity 100% impossible) + * </ol> * * @param a * first value to test @@ -52,6 +66,7 @@ public final class DecimalComparison { * @return whether they are equal * @since 2019-03-18 * @since v0.2.0 + * @see #hashCode(double) */ public static final boolean equals(final double a, final double b) { return DecimalComparison.equals(a, b, DOUBLE_EPSILON); @@ -60,6 +75,19 @@ public final class DecimalComparison { /** * Tests for double equality using a custom epsilon value. * + * <p> + * <strong>WARNING: </strong>this method is not technically transitive. If a and b are off by slightly less than + * {@code epsilon * max(abs(a), abs(b))}, and b and c are off by slightly less than + * {@code epsilon * max(abs(b), abs(c))}, then equals(a, b) and equals(b, c) will both return true, but equals(a, c) + * will return false. However, this situation is very unlikely to ever happen in a real programming situation. + * <p> + * If this does become a concern, some ways to solve this problem: + * <ol> + * <li>Raise the value of epsilon (this does not make a violation of transitivity impossible, it just significantly + * reduces the chances of it happening) + * <li>Use {@link BigDecimal} instead of {@code double} (this will make a violation of transitivity 100% impossible) + * </ol> + * * @param a * first value to test * @param b @@ -77,6 +105,19 @@ public final class DecimalComparison { /** * Tests for equality of float values using {@link #FLOAT_EPSILON}. * + * <p> + * <strong>WARNING: </strong>this method is not technically transitive. If a and b are off by slightly less than + * {@code epsilon * max(abs(a), abs(b))}, and b and c are off by slightly less than + * {@code epsilon * max(abs(b), abs(c))}, then equals(a, b) and equals(b, c) will both return true, but equals(a, c) + * will return false. However, this situation is very unlikely to ever happen in a real programming situation. + * <p> + * If this does become a concern, some ways to solve this problem: + * <ol> + * <li>Raise the value of epsilon using {@link #equals(float, float, float)} (this does not make a violation of + * transitivity impossible, it just significantly reduces the chances of it happening) + * <li>Use {@link BigDecimal} instead of {@code float} (this will make a violation of transitivity 100% impossible) + * </ol> + * * @param a * first value to test * @param b @@ -92,6 +133,19 @@ public final class DecimalComparison { /** * Tests for float equality using a custom epsilon value. * + * <p> + * <strong>WARNING: </strong>this method is not technically transitive. If a and b are off by slightly less than + * {@code epsilon * max(abs(a), abs(b))}, and b and c are off by slightly less than + * {@code epsilon * max(abs(b), abs(c))}, then equals(a, b) and equals(b, c) will both return true, but equals(a, c) + * will return false. However, this situation is very unlikely to ever happen in a real programming situation. + * <p> + * If this does become a concern, some ways to solve this problem: + * <ol> + * <li>Raise the value of epsilon (this does not make a violation of transitivity impossible, it just significantly + * reduces the chances of it happening) + * <li>Use {@link BigDecimal} instead of {@code float} (this will make a violation of transitivity 100% impossible) + * </ol> + * * @param a * first value to test * @param b @@ -106,6 +160,19 @@ public final class DecimalComparison { return Math.abs(a - b) <= epsilon * Math.max(Math.abs(a), Math.abs(b)); } + /** + * Takes the hash code of doubles. Values that are equal according to {@link #equals(double, double)} will have the + * same hash code. + * + * @param d + * double to hash + * @return hash code of double + * @since 2019-10-16 + */ + public static final int hash(final double d) { + return Float.hashCode((float) d); + } + // You may NOT get any DecimalComparison instances private DecimalComparison() { throw new AssertionError(); diff --git a/src/org/unitConverter/math/ObjectProduct.java b/src/org/unitConverter/math/ObjectProduct.java new file mode 100644 index 0000000..0cf89ec --- /dev/null +++ b/src/org/unitConverter/math/ObjectProduct.java @@ -0,0 +1,276 @@ +/**
+ * 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.math;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.function.Function;
+
+/**
+ * An immutable product of multiple objects of a type, such as base units. The objects can be multiplied and
+ * exponentiated.
+ *
+ * @author Adrien Hopkins
+ * @since 2019-10-16
+ */
+public final class ObjectProduct<T> {
+ /**
+ * Returns an empty ObjectProduct of a certain type
+ *
+ * @param <T>
+ * type of objects that can be multiplied
+ * @return empty product
+ * @since 2019-10-16
+ */
+ public static final <T> ObjectProduct<T> empty() {
+ return new ObjectProduct<>(new HashMap<>());
+ }
+
+ /**
+ * Gets an {@code ObjectProduct} from an object-to-integer mapping
+ *
+ * @param <T>
+ * type of object in product
+ * @param map
+ * map mapping objects to exponents
+ * @return object product
+ * @since 2019-10-16
+ */
+ public static final <T> ObjectProduct<T> fromExponentMapping(final Map<T, Integer> map) {
+ return new ObjectProduct<>(new HashMap<>(map));
+ }
+
+ /**
+ * Gets an ObjectProduct that has one of the inputted argument, and nothing else.
+ *
+ * @param object
+ * object that will be in the product
+ * @return product
+ * @since 2019-10-16
+ * @throws NullPointerException
+ * if object is null
+ */
+ public static final <T> ObjectProduct<T> oneOf(final T object) {
+ Objects.requireNonNull(object, "object must not be null.");
+ final Map<T, Integer> map = new HashMap<>();
+ map.put(object, 1);
+ return new ObjectProduct<>(map);
+ }
+
+ /**
+ * The objects that make up the product, mapped to their exponents. This map treats zero as null, and is immutable.
+ *
+ * @since 2019-10-16
+ */
+ final Map<T, Integer> exponents;
+
+ /**
+ * Creates the {@code ObjectProduct}.
+ *
+ * @param exponents
+ * objects that make up this product
+ * @since 2019-10-16
+ */
+ private ObjectProduct(final Map<T, Integer> exponents) {
+ this.exponents = Collections.unmodifiableMap(ConditionalExistenceCollections
+ .conditionalExistenceMap(new HashMap<>(exponents), e -> !Integer.valueOf(0).equals(e.getValue())));
+ }
+
+ /**
+ * Calculates the quotient of two products
+ *
+ * @param other
+ * other product
+ * @return quotient of two products
+ * @since 2019-10-16
+ * @throws NullPointerException
+ * if other is null
+ */
+ public ObjectProduct<T> dividedBy(final ObjectProduct<T> other) {
+ Objects.requireNonNull(other, "other must not be null.");
+ // get a list of all objects in both sets
+ final Set<T> objects = new HashSet<>();
+ objects.addAll(this.getBaseSet());
+ objects.addAll(other.getBaseSet());
+
+ // get a list of all exponents
+ final Map<T, Integer> map = new HashMap<>(objects.size());
+ for (final T key : objects) {
+ map.put(key, this.getExponent(key) - other.getExponent(key));
+ }
+
+ // create the product
+ return new ObjectProduct<>(map);
+ }
+
+ // this method relies on the use of ZeroIsNullMap
+ @Override
+ public boolean equals(final Object obj) {
+ if (this == obj)
+ return true;
+ if (!(obj instanceof ObjectProduct))
+ return false;
+ final ObjectProduct<?> other = (ObjectProduct<?>) obj;
+ return Objects.equals(this.exponents, other.exponents);
+ }
+
+ /**
+ * @return immutable map mapping objects to exponents
+ * @since 2019-10-16
+ */
+ public Map<T, Integer> exponentMap() {
+ return this.exponents;
+ }
+
+ /**
+ * @return a set of all of the base objects with non-zero exponents that make up this dimension.
+ * @since 2018-12-12
+ * @since v0.1.0
+ */
+ public final Set<T> getBaseSet() {
+ final Set<T> dimensions = new HashSet<>();
+
+ // add all dimensions with a nonzero exponent - zero exponents shouldn't be there in the first place
+ for (final T dimension : this.exponents.keySet()) {
+ if (!this.exponents.get(dimension).equals(0)) {
+ dimensions.add(dimension);
+ }
+ }
+
+ return dimensions;
+ }
+
+ /**
+ * Gets the exponent for a specific dimension.
+ *
+ * @param dimension
+ * dimension to check
+ * @return exponent for that dimension
+ * @since 2018-12-12
+ * @since v0.1.0
+ */
+ public int getExponent(final T dimension) {
+ return this.exponents.getOrDefault(dimension, 0);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(this.exponents);
+ }
+
+ /**
+ * @return true if this product is a single object, i.e. it has one exponent of one and no other nonzero exponents
+ * @since 2019-10-16
+ */
+ public boolean isSingleObject() {
+ int oneCount = 0;
+ boolean twoOrMore = false; // has exponents of 2 or more
+ for (final T b : this.getBaseSet()) {
+ if (this.getExponent(b) == 1) {
+ oneCount++;
+ } else if (this.getExponent(b) != 0) {
+ twoOrMore = true;
+ }
+ }
+ return oneCount == 1 && !twoOrMore;
+ }
+
+ /**
+ * Multiplies this product by another
+ *
+ * @param other
+ * other product
+ * @return product of two products
+ * @since 2019-10-16
+ * @throws NullPointerException
+ * if other is null
+ */
+ public ObjectProduct<T> times(final ObjectProduct<T> other) {
+ Objects.requireNonNull(other, "other must not be null.");
+ // get a list of all objects in both sets
+ final Set<T> objects = new HashSet<>();
+ objects.addAll(this.getBaseSet());
+ objects.addAll(other.getBaseSet());
+
+ // get a list of all exponents
+ final Map<T, Integer> map = new HashMap<>(objects.size());
+ for (final T key : objects) {
+ map.put(key, this.getExponent(key) + other.getExponent(key));
+ }
+
+ // create the product
+ return new ObjectProduct<>(map);
+ }
+
+ /**
+ * Returns this product, but to an exponent
+ *
+ * @param exponent
+ * exponent
+ * @return result of exponentiation
+ * @since 2019-10-16
+ */
+ public ObjectProduct<T> toExponent(final int exponent) {
+ final Map<T, Integer> map = new HashMap<>(this.exponents);
+ for (final T key : this.exponents.keySet()) {
+ map.put(key, this.getExponent(key) * exponent);
+ }
+ return new ObjectProduct<>(map);
+ }
+
+ @Override
+ public String toString() {
+ return this.toString(Object::toString);
+ }
+
+ /**
+ * Converts this product to a string. The objects that make up this product are represented by
+ * {@code objectToString}
+ *
+ * @param objectToString
+ * function to convert objects to strings
+ * @return string representation of product
+ * @since 2019-10-16
+ */
+ public String toString(final Function<T, String> objectToString) {
+ final List<String> positiveStringComponents = new ArrayList<>();
+ final List<String> negativeStringComponents = new ArrayList<>();
+
+ // for each base object that makes up this object, add it and its exponent
+ for (final T object : this.getBaseSet()) {
+ final int exponent = this.exponents.get(object);
+ if (exponent > 0) {
+ positiveStringComponents.add(String.format("%s^%d", objectToString.apply(object), exponent));
+ } else if (exponent < 0) {
+ negativeStringComponents.add(String.format("%s^%d", objectToString.apply(object), -exponent));
+ }
+ }
+
+ final String positiveString = positiveStringComponents.isEmpty() ? "1"
+ : String.join(" * ", positiveStringComponents);
+ final String negativeString = negativeStringComponents.isEmpty() ? ""
+ : " / " + String.join(" * ", negativeStringComponents);
+
+ return positiveString + negativeString;
+ }
+}
diff --git a/src/org/unitConverter/dimension/UnitDimensionTest.java b/src/org/unitConverter/math/ObjectProductTest.java index 017e3d2..afd18b7 100644 --- a/src/org/unitConverter/dimension/UnitDimensionTest.java +++ b/src/org/unitConverter/math/ObjectProductTest.java @@ -14,29 +14,30 @@ * 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.dimension; +package org.unitConverter.math; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.unitConverter.dimension.StandardDimensions.AREA; -import static org.unitConverter.dimension.StandardDimensions.ENERGY; -import static org.unitConverter.dimension.StandardDimensions.LENGTH; -import static org.unitConverter.dimension.StandardDimensions.MASS; -import static org.unitConverter.dimension.StandardDimensions.MASS_DENSITY; -import static org.unitConverter.dimension.StandardDimensions.QUANTITY; -import static org.unitConverter.dimension.StandardDimensions.TIME; -import static org.unitConverter.dimension.StandardDimensions.VOLUME; +import static org.unitConverter.unit.SI.Dimensions.AREA; +import static org.unitConverter.unit.SI.Dimensions.ENERGY; +import static org.unitConverter.unit.SI.Dimensions.LENGTH; +import static org.unitConverter.unit.SI.Dimensions.MASS; +import static org.unitConverter.unit.SI.Dimensions.MASS_DENSITY; +import static org.unitConverter.unit.SI.Dimensions.QUANTITY; +import static org.unitConverter.unit.SI.Dimensions.TIME; +import static org.unitConverter.unit.SI.Dimensions.VOLUME; import org.junit.jupiter.api.Test; +import org.unitConverter.unit.SI; /** - * Tests for {@link UnitDimension}. This is NOT part of this program's public API. + * Tests for {@link ObjectProduct} using BaseDimension as a test object. This is NOT part of this program's public API. * * @author Adrien Hopkins * @since 2018-12-12 * @since v0.1.0 */ -class UnitDimensionTest { +class ObjectProductTest { /** * Tests {@link UnitDimension#equals} * @@ -57,8 +58,8 @@ class UnitDimensionTest { */ @Test public void testExponents() { - assertEquals(1, LENGTH.getExponent(SIBaseDimension.LENGTH)); - assertEquals(3, VOLUME.getExponent(SIBaseDimension.LENGTH)); + assertEquals(1, LENGTH.getExponent(SI.BaseDimensions.LENGTH)); + assertEquals(3, VOLUME.getExponent(SI.BaseDimensions.LENGTH)); } /** diff --git a/src/org/unitConverter/math/package-info.java b/src/org/unitConverter/math/package-info.java index 65d6b23..65727e4 100644 --- a/src/org/unitConverter/math/package-info.java +++ b/src/org/unitConverter/math/package-info.java @@ -15,9 +15,10 @@ * along with this program. If not, see <https://www.gnu.org/licenses/>. */ /** - * A module that is capable of parsing expressions of things, like mathematical expressions or unit expressions. + * Supplementary classes that are not related to units, but are necessary for their function. * * @author Adrien Hopkins * @since 2019-03-14 + * @since v0.2.0 */ package org.unitConverter.math;
\ No newline at end of file diff --git a/src/org/unitConverter/unit/AbstractUnit.java b/src/org/unitConverter/unit/AbstractUnit.java deleted file mode 100644 index 6045127..0000000 --- a/src/org/unitConverter/unit/AbstractUnit.java +++ /dev/null @@ -1,124 +0,0 @@ -/** - * 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.dimension.UnitDimension; - -/** - * The default abstract implementation of the {@code Unit} interface. - * - * <p> - * With the addition of {@link Unit#fromConversionFunctions}, there is no longer any reason to use {@code AbstractUnit} - * for any purpose other than making subclasses. Units should never be declared as {@code AbstractUnit}, they should be - * declared as {@code Unit}. Now that {@code Unit.fromConversionFunctions} exists, it is preferred to creating anonymous - * inner types of {@code AbstractUnit}. - * </p> - * - * @author Adrien Hopkins - * @since 2018-12-22 - * @since v0.1.0 - */ -public abstract class AbstractUnit implements Unit { - /** - * The dimension, or what the unit measures. - * - * @since 2018-12-22 - * @since v0.1.0 - */ - private final UnitDimension dimension; - - /** - * The unit's base unit. Values converted by {@code convertFromBase} and {@code convertToBase} are expressed in this - * unit. - * - * @since 2018-12-22 - * @since v0.1.0 - */ - private final BaseUnit base; - - /** - * The system that this unit is a part of. - * - * @since 2018-12-23 - * @since v0.1.0 - */ - private final UnitSystem system; - - /** - * Creates the {@code AbstractUnit}. - * - * @param base - * unit's base - * @throws NullPointerException - * if name, symbol or base is null - * @since 2018-12-22 - * @since v0.1.0 - */ - public AbstractUnit(final BaseUnit base) { - this.base = Objects.requireNonNull(base, "base must not be null."); - this.dimension = this.base.getDimension(); - this.system = this.base.getSystem(); - } - - /** - * Creates the {@code AbstractUnit} using a unique dimension. This constructor is for making base units and should - * only be used by {@code BaseUnit}. - * - * @param dimension - * dimension measured by unit - * @param system - * system that unit is a part of - * @throws AssertionError - * if this constructor is not run by {@code BaseUnit} or a subclass - * @throws NullPointerException - * if name, symbol or dimension is null - * @since 2018-12-23 - * @since v0.1.0 - */ - AbstractUnit(final UnitDimension dimension, final UnitSystem system) { - // try to set this as a base unit - if (this instanceof BaseUnit) { - this.base = (BaseUnit) this; - } else - throw new AssertionError(); - - this.dimension = Objects.requireNonNull(dimension, "dimension must not be null."); - this.system = Objects.requireNonNull(system, "system must not be null."); - } - - @Override - public final BaseUnit getBase() { - return this.base; - } - - @Override - public final UnitDimension getDimension() { - return this.dimension; - } - - @Override - public final UnitSystem getSystem() { - return this.system; - } - - @Override - public String toString() { - return String.format("%s-derived unit of dimension %s", this.getSystem(), this.getDimension()); - } -} diff --git a/src/org/unitConverter/unit/BaseDimension.java b/src/org/unitConverter/unit/BaseDimension.java new file mode 100644 index 0000000..35acd18 --- /dev/null +++ b/src/org/unitConverter/unit/BaseDimension.java @@ -0,0 +1,81 @@ +/** + * 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); + } + + private final String name; + 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/org/unitConverter/unit/BaseUnit.java b/src/org/unitConverter/unit/BaseUnit.java index 67309cf..8f44861 100644 --- a/src/org/unitConverter/unit/BaseUnit.java +++ b/src/org/unitConverter/unit/BaseUnit.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2018 Adrien Hopkins + * 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 @@ -18,151 +18,100 @@ package org.unitConverter.unit; import java.util.Objects; -import org.unitConverter.dimension.StandardDimensions; -import org.unitConverter.dimension.UnitDimension; - /** - * A unit that is the base for its dimension. It does not have to be for a base dimension, so units like the Newton and - * Joule are still base units. - * <p> - * {@code BaseUnit} does not have any public constructors or static factories. There are two ways to obtain - * {@code BaseUnit} instances. - * <ol> - * <li>The class {@link SI} in this package has constants for all of the SI base units. You can use these constants and - * multiply or divide them to get other units. For example: - * - * <pre> - * BaseUnit JOULE = SI.KILOGRAM.times(SI.METRE.toExponent(2)).dividedBy(SI.SECOND.toExponent(2)); - * </pre> - * - * </li> - * <li>You can also query a unit system for a base unit using a unit dimension. The previously mentioned {@link SI} - * class can do this for SI and SI-derived units (including imperial and USC), but if you want to use another system, - * this is the way to do it. {@link StandardDimensions} contains common unit dimensions that you can use for this. Here - * is an example: - * - * <pre> - * BaseUnit JOULE = SI.SI.getBaseUnit(StandardDimensions.ENERGY); - * </pre> - * - * </li> - * </ol> + * A unit that other units are defined by. * * @author Adrien Hopkins - * @since 2018-12-23 - * @since v0.1.0 + * @since 2019-10-16 */ -public final class BaseUnit extends LinearUnit { +public final class BaseUnit extends Unit { /** - * Is this unit a full base (i.e. m, s, ... but not N, J, ...) + * Gets a base unit from the dimension it measures, its name and its symbol. * - * @since 2019-01-15 - * @since v0.1.0 + * @param dimension + * dimension measured by this unit + * @param name + * name of unit + * @param symbol + * symbol of unit + * @return base unit + * @since 2019-10-16 */ - private final boolean isFullBase; + public static BaseUnit valueOf(final BaseDimension dimension, final String name, final String symbol) { + return new BaseUnit(dimension, name, symbol); + } + + private final BaseDimension dimension; + private final String name; + private final String symbol; /** * Creates the {@code BaseUnit}. * * @param dimension - * dimension measured by unit - * @param system - * system that unit is a part of + * dimension of unit * @param name * name of unit * @param symbol * symbol of unit - * @since 2018-12-23 - * @since v0.1.0 + * @throws NullPointerException + * if any argument is null + * @since 2019-10-16 */ - BaseUnit(final UnitDimension dimension, final UnitSystem system) { - super(dimension, system, 1); - this.isFullBase = dimension.isBase(); + private BaseUnit(final BaseDimension dimension, final String name, final String symbol) { + super(); + this.dimension = Objects.requireNonNull(dimension, "dimension must not be null."); + this.name = Objects.requireNonNull(name, "name must not be null."); + this.symbol = Objects.requireNonNull(symbol, "symbol must not be null."); } /** - * Returns the quotient of this unit and another. - * <p> - * Two units can be divided if they are part of the same unit system. If {@code divisor} does not meet this - * condition, an {@code IllegalArgumentException} should be thrown. - * </p> + * 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. * - * @param divisor - * unit to divide by - * @return quotient of two units - * @throws IllegalArgumentException - * if {@code divisor} is not compatible for division as described above - * @throws NullPointerException - * if {@code divisor} is null - * @since 2018-12-22 - * @since v0.1.0 + * @return this unit as a {@code LinearUnit} + * @since 2019-10-16 */ - public BaseUnit dividedBy(final BaseUnit divisor) { - Objects.requireNonNull(divisor, "other must not be null."); + public LinearUnit asLinearUnit() { + return LinearUnit.valueOf(this.getBase(), 1); + } - // check that these units can be multiplied - if (!this.getSystem().equals(divisor.getSystem())) - throw new IllegalArgumentException( - String.format("Incompatible units for division \"%s\" and \"%s\".", this, divisor)); + @Override + public double convertFromBase(final double value) { + return value; + } - return new BaseUnit(this.getDimension().dividedBy(divisor.getDimension()), this.getSystem()); + @Override + public double convertToBase(final double value) { + return value; } /** - * @return true if the unit is a "full base" unit like the metre or second. - * @since 2019-04-10 - * @since v0.2.0 + * @return dimension + * @since 2019-10-16 */ - public final boolean isFullBase() { - return this.isFullBase; + public final BaseDimension getBaseDimension() { + return this.dimension; } /** - * Returns the product of this unit and another. - * <p> - * Two units can be multiplied if they are part of the same unit system. If {@code multiplier} does not meet this - * condition, an {@code IllegalArgumentException} should be thrown. - * </p> - * - * @param multiplier - * unit to multiply by - * @return product of two units - * @throws IllegalArgumentException - * if {@code multiplier} is not compatible for multiplication as described above - * @throws NullPointerException - * if {@code multiplier} is null - * @since 2018-12-22 - * @since v0.1.0 + * @return name + * @since 2019-10-16 */ - public BaseUnit times(final BaseUnit multiplier) { - Objects.requireNonNull(multiplier, "other must not be null"); - - // check that these units can be multiplied - if (!this.getSystem().equals(multiplier.getSystem())) - throw new IllegalArgumentException( - String.format("Incompatible units for multiplication \"%s\" and \"%s\".", this, multiplier)); - - // multiply the units - return new BaseUnit(this.getDimension().times(multiplier.getDimension()), this.getSystem()); + public final String getName() { + return this.name; } /** - * Returns this unit, but to an exponent. - * - * @param exponent - * exponent - * @return result of exponentiation - * @since 2019-01-15 - * @since v0.1.0 + * @return symbol + * @since 2019-10-16 */ - @Override - public BaseUnit toExponent(final int exponent) { - return this.getSystem().getBaseUnit(this.getDimension().toExponent(exponent)); + public final String getSymbol() { + return this.symbol; } @Override public String toString() { - return String.format("%s base unit of%s dimension %s", this.getSystem(), this.isFullBase ? " base" : "", - this.getDimension()); + return String.format("%s (%s)", this.getName(), this.getSymbol()); } } diff --git a/src/org/unitConverter/unit/NonlinearUnits.java b/src/org/unitConverter/unit/BritishImperial.java index eda4a74..c9316f3 100644 --- a/src/org/unitConverter/unit/NonlinearUnits.java +++ b/src/org/unitConverter/unit/BritishImperial.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2018 Adrien Hopkins + * 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 @@ -17,21 +17,12 @@ package org.unitConverter.unit; /** - * Some major nonlinear units. + * A static utility class that contains units in the British Imperial system. * * @author Adrien Hopkins - * @since 2019-01-14 - * @since v0.1.0 + * @since 2019-10-21 */ -public final class NonlinearUnits { - public static final Unit CELSIUS = Unit.fromConversionFunctions(SI.KELVIN, tempK -> tempK - 273.15, - tempC -> tempC + 273.15); - - public static final Unit FAHRENHEIT = Unit.fromConversionFunctions(SI.KELVIN, tempK -> tempK * 1.8 - 459.67, - tempF -> (tempF + 459.67) / 1.8); - - // You may NOT get a NonlinearUnits instance. - private NonlinearUnits() { - throw new AssertionError(); - } +public final class BritishImperial { + 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/org/unitConverter/unit/DefaultUnitPrefix.java b/src/org/unitConverter/unit/DefaultUnitPrefix.java deleted file mode 100644 index 4a9e487..0000000 --- a/src/org/unitConverter/unit/DefaultUnitPrefix.java +++ /dev/null @@ -1,68 +0,0 @@ -/** - * 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.Objects; - -/** - * The default implementation of {@code UnitPrefix}, which contains a multiplier and nothing else. - * - * @author Adrien Hopkins - * @since 2019-01-14 - * @since v0.1.0 - */ -public final class DefaultUnitPrefix implements UnitPrefix { - private final double multiplier; - - /** - * Creates the {@code DefaultUnitPrefix}. - * - * @param multiplier - * @since 2019-01-14 - * @since v0.2.0 - */ - public DefaultUnitPrefix(final double multiplier) { - this.multiplier = multiplier; - } - - @Override - public boolean equals(final Object obj) { - if (this == obj) - return true; - if (obj == null) - return false; - if (!(obj instanceof DefaultUnitPrefix)) - return false; - final DefaultUnitPrefix other = (DefaultUnitPrefix) obj; - return Double.doubleToLongBits(this.multiplier) == Double.doubleToLongBits(other.multiplier); - } - - @Override - public double getMultiplier() { - return this.multiplier; - } - - @Override - public int hashCode() { - return Objects.hash(this.multiplier); - } - - @Override - public String toString() { - return String.format("Unit prefix equal to %s", this.multiplier); - } -} diff --git a/src/org/unitConverter/unit/FunctionalUnit.java b/src/org/unitConverter/unit/FunctionalUnit.java index e3db43a..7ddd876 100644 --- a/src/org/unitConverter/unit/FunctionalUnit.java +++ b/src/org/unitConverter/unit/FunctionalUnit.java @@ -19,13 +19,15 @@ 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 AbstractUnit { +final class FunctionalUnit extends Unit { /** * Returns a unit from its base and the functions it uses to convert to and from its base. * @@ -42,7 +44,7 @@ final class FunctionalUnit extends AbstractUnit { * @throws NullPointerException * if any argument is null */ - public static FunctionalUnit valueOf(final BaseUnit base, final DoubleUnaryOperator converterFrom, + public static FunctionalUnit valueOf(final ObjectProduct<BaseUnit> base, final DoubleUnaryOperator converterFrom, final DoubleUnaryOperator converterTo) { return new FunctionalUnit(base, converterFrom, converterTo); } @@ -76,7 +78,7 @@ final class FunctionalUnit extends AbstractUnit { * if any argument is null * @since 2019-05-22 */ - private FunctionalUnit(final BaseUnit base, final DoubleUnaryOperator converterFrom, + private FunctionalUnit(final ObjectProduct<BaseUnit> base, final DoubleUnaryOperator converterFrom, final DoubleUnaryOperator converterTo) { super(base); this.converterFrom = Objects.requireNonNull(converterFrom, "converterFrom must not be null."); diff --git a/src/org/unitConverter/unit/LinearUnit.java b/src/org/unitConverter/unit/LinearUnit.java index 1b1ac97..1918d6b 100644 --- a/src/org/unitConverter/unit/LinearUnit.java +++ b/src/org/unitConverter/unit/LinearUnit.java @@ -18,73 +18,78 @@ package org.unitConverter.unit; import java.util.Objects; -import org.unitConverter.dimension.UnitDimension; import org.unitConverter.math.DecimalComparison; +import org.unitConverter.math.ObjectProduct; /** - * A unit that is equal to a certain number multiplied by its base. - * <p> - * {@code LinearUnit} does not have any public constructors or static factories. In order to obtain a {@code LinearUnit} - * instance, multiply its base by the conversion factor. Example: - * - * <pre> - * LinearUnit foot = METRE.times(0.3048); - * </pre> - * - * (where {@code METRE} is a {@code BaseUnit} instance) - * </p> + * 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 2018-12-22 - * @since v0.1.0 + * @since 2019-10-16 */ -public class LinearUnit extends AbstractUnit { +public final class LinearUnit extends Unit { /** - * The value of one of this unit in this unit's base 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' * - * @since 2018-12-22 - * @since v0.1.0 + * @param unit + * unit to convert + * @param value + * value to convert + * @return value expressed as a {@code LinearUnit} + * @since 2019-10-16 */ - private final double conversionFactor; + public static LinearUnit fromUnitValue(final Unit unit, final double value) { + return new LinearUnit(unit.getBase(), unit.convertToBase(value)); + } /** + * 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}. * - * Creates the {@code LinearUnit}. - * - * @param base - * unit's base + * @param unitBase + * unit base to multiply by * @param conversionFactor - * value of one of this unit in its base - * @since 2018-12-23 - * @since v0.1.0 + * number to multiply base by + * @return product of base and conversion factor + * @since 2019-10-16 */ - LinearUnit(final BaseUnit base, final double conversionFactor) { - super(base); - this.conversionFactor = conversionFactor; + public static LinearUnit valueOf(final ObjectProduct<BaseUnit> unitBase, final double conversionFactor) { + return new LinearUnit(unitBase, conversionFactor); } /** - * Creates the {@code LinearUnit} as a base unit. + * The value of this unit as represented in its base form. Mathematically, * - * @param dimension - * dimension measured by unit - * @param system - * system unit is part of - * @since 2019-01-25 - * @since v0.1.0 + * <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 */ - LinearUnit(final UnitDimension dimension, final UnitSystem system, final double conversionFactor) { - super(dimension, system); + private LinearUnit(final ObjectProduct<BaseUnit> unitBase, final double conversionFactor) { + super(unitBase); this.conversionFactor = conversionFactor; } @Override - public double convertFromBase(final double value) { + protected double convertFromBase(final double value) { return value / this.getConversionFactor(); } @Override - public double convertToBase(final double value) { + protected double convertToBase(final double value) { return value * this.getConversionFactor(); } @@ -98,21 +103,15 @@ public class LinearUnit extends AbstractUnit { * @since v0.1.0 */ public LinearUnit dividedBy(final double divisor) { - return new LinearUnit(this.getBase(), this.getConversionFactor() / divisor); + return valueOf(this.getBase(), this.getConversionFactor() / divisor); } /** * Returns the quotient of this unit and another. - * <p> - * Two units can be divided if they are part of the same unit system. If {@code divisor} does not meet this - * condition, an {@code IllegalArgumentException} should be thrown. - * </p> * * @param divisor * unit to divide by * @return quotient of two units - * @throws IllegalArgumentException - * if {@code divisor} is not compatible for division as described above * @throws NullPointerException * if {@code divisor} is null * @since 2018-12-22 @@ -121,14 +120,9 @@ public class LinearUnit extends AbstractUnit { public LinearUnit dividedBy(final LinearUnit divisor) { Objects.requireNonNull(divisor, "other must not be null"); - // check that these units can be multiplied - if (!this.getSystem().equals(divisor.getSystem())) - throw new IllegalArgumentException( - String.format("Incompatible units for division \"%s\" and \"%s\".", this, divisor)); - // divide the units - final BaseUnit base = this.getBase().dividedBy(divisor.getBase()); - return new LinearUnit(base, this.getConversionFactor() / divisor.getConversionFactor()); + final ObjectProduct<BaseUnit> base = this.getBase().dividedBy(divisor.getBase()); + return valueOf(base, this.getConversionFactor() / divisor.getConversionFactor()); } @Override @@ -136,35 +130,45 @@ public class LinearUnit extends AbstractUnit { if (!(obj instanceof LinearUnit)) return false; final LinearUnit other = (LinearUnit) obj; - return Objects.equals(this.getSystem(), other.getSystem()) - && Objects.equals(this.getDimension(), other.getDimension()) + return Objects.equals(this.getBase(), other.getBase()) && DecimalComparison.equals(this.getConversionFactor(), other.getConversionFactor()); } /** - * @return conversion factor between this unit and its base - * @since 2018-12-22 - * @since v0.1.0 + * @return conversion factor + * @since 2019-10-16 */ - public final double getConversionFactor() { + public double getConversionFactor() { return this.conversionFactor; } @Override public int hashCode() { - final int prime = 31; - int result = 1; - result = result * prime + this.getSystem().hashCode(); - result = result * prime + this.getDimension().hashCode(); - result = result * prime + Double.hashCode(this.getConversionFactor()); - return result; + 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. If {@code subtrahend} does not meet this condition, an - * {@code IllegalArgumentException} will be thrown. + * 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 @@ -186,14 +190,14 @@ public class LinearUnit extends AbstractUnit { String.format("Incompatible units for subtraction \"%s\" and \"%s\".", this, subtrahendend)); // add the units - return new LinearUnit(this.getBase(), this.getConversionFactor() - subtrahendend.getConversionFactor()); + return valueOf(this.getBase(), this.getConversionFactor() - subtrahendend.getConversionFactor()); } /** * Returns the sum of this unit and another. * <p> - * Two units can be added if they have the same base. If {@code addend} does not meet this condition, an - * {@code IllegalArgumentException} will be thrown. + * 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 @@ -215,7 +219,7 @@ public class LinearUnit extends AbstractUnit { String.format("Incompatible units for addition \"%s\" and \"%s\".", this, addend)); // add the units - return new LinearUnit(this.getBase(), this.getConversionFactor() + addend.getConversionFactor()); + return valueOf(this.getBase(), this.getConversionFactor() + addend.getConversionFactor()); } /** @@ -228,21 +232,15 @@ public class LinearUnit extends AbstractUnit { * @since v0.1.0 */ public LinearUnit times(final double multiplier) { - return new LinearUnit(this.getBase(), this.getConversionFactor() * multiplier); + return valueOf(this.getBase(), this.getConversionFactor() * multiplier); } /** * Returns the product of this unit and another. - * <p> - * Two units can be multiplied if they are part of the same unit system. If {@code multiplier} does not meet this - * condition, an {@code IllegalArgumentException} should be thrown. - * </p> * * @param multiplier * unit to multiply by * @return product of two units - * @throws IllegalArgumentException - * if {@code multiplier} is not compatible for multiplication as described above * @throws NullPointerException * if {@code multiplier} is null * @since 2018-12-22 @@ -251,32 +249,28 @@ public class LinearUnit extends AbstractUnit { public LinearUnit times(final LinearUnit multiplier) { Objects.requireNonNull(multiplier, "other must not be null"); - // check that these units can be multiplied - if (!this.getSystem().equals(multiplier.getSystem())) - throw new IllegalArgumentException( - String.format("Incompatible units for multiplication \"%s\" and \"%s\".", this, multiplier)); - // multiply the units - final BaseUnit base = this.getBase().times(multiplier.getBase()); - return new LinearUnit(base, this.getConversionFactor() * multiplier.getConversionFactor()); + 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 exponientate unit to - * @return exponientated unit + * exponent to exponentiate unit to + * @return exponentiated unit * @since 2019-01-15 * @since v0.1.0 */ public LinearUnit toExponent(final int exponent) { - return new LinearUnit(this.getBase().toExponent(exponent), Math.pow(this.conversionFactor, exponent)); + return valueOf(this.getBase().toExponent(exponent), Math.pow(this.conversionFactor, exponent)); } + // returns a definition of the unit @Override public String toString() { - return super.toString() + String.format(" (equal to %s * base)", this.getConversionFactor()); + return Double.toString(this.conversionFactor) + " * " + this.getBase().toString(BaseUnit::getSymbol); } /** diff --git a/src/org/unitConverter/unit/SI.java b/src/org/unitConverter/unit/SI.java index 46e6ff1..f623179 100644 --- a/src/org/unitConverter/unit/SI.java +++ b/src/org/unitConverter/unit/SI.java @@ -1,74 +1,228 @@ -/** - * 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.HashSet; -import java.util.Objects; -import java.util.Set; - -import org.unitConverter.dimension.StandardDimensions; -import org.unitConverter.dimension.UnitDimension; - -/** - * The SI, which holds all SI units - * - * @author Adrien Hopkins - * @since 2018-12-23 - * @since v0.1.0 - */ -public enum SI implements UnitSystem { - SI; - - /** - * This system's base units. - * - * @since 2019-01-25 - * @since v0.1.0 - */ - private static final Set<BaseUnit> baseUnits = new HashSet<>(); - - // base units - public static final BaseUnit METRE = SI.getBaseUnit(StandardDimensions.LENGTH); - public static final BaseUnit KILOGRAM = SI.getBaseUnit(StandardDimensions.MASS); - public static final BaseUnit SECOND = SI.getBaseUnit(StandardDimensions.TIME); - public static final BaseUnit AMPERE = SI.getBaseUnit(StandardDimensions.ELECTRIC_CURRENT); - public static final BaseUnit KELVIN = SI.getBaseUnit(StandardDimensions.TEMPERATURE); - public static final BaseUnit MOLE = SI.getBaseUnit(StandardDimensions.QUANTITY); - public static final BaseUnit CANDELA = SI.getBaseUnit(StandardDimensions.LUMINOUS_INTENSITY); - - @Override - public BaseUnit getBaseUnit(final UnitDimension dimension) { - // try to find an existing unit before creating a new one - - Objects.requireNonNull(dimension, "dimension must not be null."); - for (final BaseUnit unit : baseUnits) { - // it will be equal since the conditions for equality are dimension and system, - // and system is always SI. - if (unit.getDimension().equals(dimension)) - return unit; - } - // could not find an existing base unit - final BaseUnit unit = new BaseUnit(dimension, this); - baseUnits.add(unit); - return unit; - } - - @Override - public String getName() { - return "SI"; - } -} +/**
+ * 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 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", "$");
+
+ // You may NOT get SI.BaseUnits instances!
+ private BaseUnits() {
+ throw new AssertionError();
+ }
+ }
+
+ // 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> REFLACTIVE_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();
+ public static final LinearUnit KILOGRAM = BaseUnits.KILOGRAM.asLinearUnit();
+ public static final LinearUnit SECOND = BaseUnits.SECOND.asLinearUnit();
+ public static final LinearUnit AMPERE = BaseUnits.AMPERE.asLinearUnit();
+ public static final LinearUnit KELVIN = BaseUnits.KELVIN.asLinearUnit();
+ public static final LinearUnit MOLE = BaseUnits.MOLE.asLinearUnit();
+ public static final LinearUnit CANDELA = BaseUnits.CANDELA.asLinearUnit();
+ public static final LinearUnit BIT = BaseUnits.BIT.asLinearUnit();
+ public static final LinearUnit DOLLAR = BaseUnits.DOLLAR.asLinearUnit();
+
+ // Non-base units
+ public static final LinearUnit RADIAN = METRE.dividedBy(METRE);
+ public static final LinearUnit STERADIAN = RADIAN.times(RADIAN);
+ public static final LinearUnit HERTZ = ONE.dividedBy(SECOND); // for periodic phenomena
+ public static final LinearUnit NEWTON = KILOGRAM.times(METRE).dividedBy(SECOND.times(SECOND));
+ public static final LinearUnit PASCAL = NEWTON.dividedBy(METRE.times(METRE));
+ public static final LinearUnit JOULE = NEWTON.times(METRE);
+ public static final LinearUnit WATT = JOULE.dividedBy(SECOND);
+ public static final LinearUnit COULOMB = AMPERE.times(SECOND);
+ public static final LinearUnit VOLT = JOULE.dividedBy(COULOMB);
+ public static final LinearUnit FARAD = COULOMB.dividedBy(VOLT);
+ public static final LinearUnit OHM = VOLT.dividedBy(AMPERE);
+ public static final LinearUnit SIEMENS = ONE.dividedBy(OHM);
+ public static final LinearUnit WEBER = VOLT.times(SECOND);
+ public static final LinearUnit TESLA = WEBER.dividedBy(METRE.times(METRE));
+ public static final LinearUnit HENRY = WEBER.dividedBy(AMPERE);
+ public static final LinearUnit LUMEN = CANDELA.times(STERADIAN);
+ public static final LinearUnit LUX = LUMEN.dividedBy(METRE.times(METRE));
+ public static final LinearUnit BEQUEREL = ONE.dividedBy(SECOND); // for activity referred to a nucleotide
+ public static final LinearUnit GRAY = JOULE.dividedBy(KILOGRAM); // for absorbed dose
+ public static final LinearUnit SIEVERT = JOULE.dividedBy(KILOGRAM); // for dose equivalent
+ public static final LinearUnit KATAL = MOLE.dividedBy(SECOND);
+
+ // Non-SI units included for convenience
+ public static final Unit CELSIUS = Unit.fromConversionFunctions(KELVIN.getBase(), tempK -> tempK - 273.15,
+ tempC -> tempC + 273.15);
+ public static final LinearUnit MINUTE = SECOND.times(60);
+ public static final LinearUnit HOUR = MINUTE.times(60);
+ public static final LinearUnit DAY = HOUR.times(60);
+ public static final LinearUnit DEGREE = RADIAN.times(360 / (2 * Math.PI));
+ public static final LinearUnit ARCMINUTE = DEGREE.dividedBy(60);
+ public static final LinearUnit ARCSECOND = ARCMINUTE.dividedBy(60);
+ public static final LinearUnit ASTRONOMICAL_UNIT = METRE.times(149597870700.0);
+ public static final LinearUnit PARSEC = ASTRONOMICAL_UNIT.times(ARCSECOND);
+ public static final LinearUnit HECTARE = METRE.times(METRE).times(10000.0);
+ public static final LinearUnit LITRE = METRE.times(METRE).times(METRE).dividedBy(1000.0);
+ public static final LinearUnit TONNE = KILOGRAM.times(1000.0);
+ public static final LinearUnit DALTON = KILOGRAM.times(1.660539040e-27); // approximate value
+ public static final LinearUnit ELECTRONVOLT = JOULE.times(1.602176634e-19);
+ public static final Unit NEPER = Unit.fromConversionFunctions(ONE.getBase(), pr -> 0.5 * Math.log(pr),
+ Np -> Math.exp(2 * Np));
+ public static final Unit BEL = Unit.fromConversionFunctions(ONE.getBase(), pr -> Math.log10(pr),
+ dB -> Math.pow(10, dB));
+ public static final Unit DECIBEL = Unit.fromConversionFunctions(ONE.getBase(), pr -> 10 * Math.log10(pr),
+ dB -> Math.pow(10, dB / 10));
+
+ /// The prefixes of the SI
+ // expanding decimal prefixes
+ public static final UnitPrefix KILO = UnitPrefix.valueOf(1e3);
+ public static final UnitPrefix MEGA = UnitPrefix.valueOf(1e6);
+ public static final UnitPrefix GIGA = UnitPrefix.valueOf(1e9);
+ public static final UnitPrefix TERA = UnitPrefix.valueOf(1e12);
+ public static final UnitPrefix PETA = UnitPrefix.valueOf(1e15);
+ public static final UnitPrefix EXA = UnitPrefix.valueOf(1e18);
+ public static final UnitPrefix ZETTA = UnitPrefix.valueOf(1e21);
+ public static final UnitPrefix YOTTA = UnitPrefix.valueOf(1e24);
+
+ // contracting decimal prefixes
+ public static final UnitPrefix MILLI = UnitPrefix.valueOf(1e-3);
+ public static final UnitPrefix MICRO = UnitPrefix.valueOf(1e-6);
+ public static final UnitPrefix NANO = UnitPrefix.valueOf(1e-9);
+ public static final UnitPrefix PICO = UnitPrefix.valueOf(1e-12);
+ public static final UnitPrefix FEMTO = UnitPrefix.valueOf(1e-15);
+ public static final UnitPrefix ATTO = UnitPrefix.valueOf(1e-18);
+ public static final UnitPrefix ZEPTO = UnitPrefix.valueOf(1e-21);
+ public static final UnitPrefix YOCTO = UnitPrefix.valueOf(1e-24);
+
+ // prefixes that don't match the pattern of thousands
+ public static final UnitPrefix DEKA = UnitPrefix.valueOf(1e1);
+ public static final UnitPrefix HECTO = UnitPrefix.valueOf(1e2);
+ public static final UnitPrefix DECI = UnitPrefix.valueOf(1e-1);
+ public static final UnitPrefix CENTI = UnitPrefix.valueOf(1e-2);
+ public static final UnitPrefix KIBI = UnitPrefix.valueOf(1024);
+ public static final UnitPrefix MEBI = KIBI.times(1024);
+ public static final UnitPrefix GIBI = MEBI.times(1024);
+ public static final UnitPrefix TEBI = GIBI.times(1024);
+ public static final UnitPrefix PEBI = TEBI.times(1024);
+ public static final UnitPrefix EXBI = PEBI.times(1024);
+
+ // You may NOT get SI instances!
+ private SI() {
+ throw new AssertionError();
+ }
+}
diff --git a/src/org/unitConverter/unit/SIPrefix.java b/src/org/unitConverter/unit/SIPrefix.java deleted file mode 100644 index 31d7ff2..0000000 --- a/src/org/unitConverter/unit/SIPrefix.java +++ /dev/null @@ -1,54 +0,0 @@ -/** - * 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; - -/** - * The SI prefixes. - * - * @author Adrien Hopkins - * @since 2019-01-14 - * @since v0.1.0 - */ -public enum SIPrefix implements UnitPrefix { - DECA(10), HECTO(100), KILO(1e3), MEGA(1e6), GIGA(1e9), TERA(1e12), PETA(1e15), EXA(1e18), ZETTA(1e21), YOTTA( - 1e24), DECI(0.1), CENTI(0.01), MILLI( - 1e-3), MICRO(1e-6), NANO(1e-9), PICO(1e-12), FEMTO(1e-15), ATTO(1e-18), ZEPTO(1e-21), YOCTO(1e-24); - - private final double multiplier; - - /** - * Creates the {@code SIPrefix}. - * - * @param multiplier - * prefix's multiplier - * @since 2019-01-14 - * @since v0.1.0 - */ - private SIPrefix(final double multiplier) { - this.multiplier = multiplier; - } - - /** - * @return value - * @since 2019-01-14 - * @since v0.1.0 - */ - @Override - public final double getMultiplier() { - return this.multiplier; - } -} diff --git a/src/org/unitConverter/dimension/package-info.java b/src/org/unitConverter/unit/USCustomary.java index 8cb26b1..f5f9a7f 100644 --- a/src/org/unitConverter/dimension/package-info.java +++ b/src/org/unitConverter/unit/USCustomary.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2018 Adrien Hopkins + * 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 @@ -14,11 +14,14 @@ * 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; + /** - * Everything to do with what a unit measures, or its dimension. + * A static utility class that contains units in the US Customary system. * * @author Adrien Hopkins - * @since 2018-12-22 - * @since v0.1.0 + * @since 2019-10-21 */ -package org.unitConverter.dimension;
\ No newline at end of file +public final class USCustomary { + public static final Unit FAHRENHEIT = BritishImperial.FAHRENHEIT; +} diff --git a/src/org/unitConverter/unit/Unit.java b/src/org/unitConverter/unit/Unit.java index 54f0ab5..7971a41 100644 --- a/src/org/unitConverter/unit/Unit.java +++ b/src/org/unitConverter/unit/Unit.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2018 Adrien Hopkins + * 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 @@ -16,19 +16,20 @@ */ package org.unitConverter.unit; +import java.util.HashMap; +import java.util.Map; import java.util.Objects; import java.util.function.DoubleUnaryOperator; -import org.unitConverter.dimension.UnitDimension; +import org.unitConverter.math.ObjectProduct; /** - * A unit that has an associated base unit, and can convert a value expressed in it to and from that base. + * A unit that is composed of base units. * * @author Adrien Hopkins - * @since 2018-12-22 - * @since v0.1.0 + * @since 2019-10-16 */ -public interface Unit { +public abstract class Unit { /** * Returns a unit from its base and the functions it uses to convert to and from its base. * @@ -51,12 +52,50 @@ public interface Unit { * @throws NullPointerException * if any argument is null */ - public static Unit fromConversionFunctions(final BaseUnit base, final DoubleUnaryOperator converterFrom, - final DoubleUnaryOperator converterTo) { + public static final Unit fromConversionFunctions(final ObjectProduct<BaseUnit> base, + final DoubleUnaryOperator converterFrom, final DoubleUnaryOperator converterTo) { return FunctionalUnit.valueOf(base, converterFrom, converterTo); } /** + * The combination of units that this unit is based on. + * + * @since 2019-10-16 + */ + private final ObjectProduct<BaseUnit> unitBase; + + /** + * Cache storing the result of getDimension() + * + * @since 2019-10-16 + */ + private transient ObjectProduct<BaseDimension> dimension = null; + + /** + * A constructor that constructs {@code BaseUnit} instances. + * + * @since 2019-10-16 + */ + Unit() { + if (this instanceof BaseUnit) { + this.unitBase = ObjectProduct.oneOf((BaseUnit) this); + } else + throw new AssertionError(); + } + + /** + * Creates the {@code AbstractUnit}. + * + * @param unitBase + * @since 2019-10-16 + * @throws NullPointerException + * if unitBase is null + */ + protected Unit(final ObjectProduct<BaseUnit> unitBase) { + this.unitBase = Objects.requireNonNull(unitBase, "unitBase must not be null."); + } + + /** * Checks if a value expressed in this unit can be converted to a value expressed in {@code other} * * @param other @@ -67,7 +106,7 @@ public interface Unit { * @throws NullPointerException * if other is null */ - default boolean canConvertTo(final Unit other) { + public final boolean canConvertTo(final Unit other) { Objects.requireNonNull(other, "other must not be null."); return Objects.equals(this.getBase(), other.getBase()); } @@ -82,17 +121,24 @@ public interface Unit { * 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 */ - double convertFromBase(double value); + 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 @@ -105,7 +151,7 @@ public interface Unit { * @throws NullPointerException * if other is null */ - default double convertTo(final Unit other, final double value) { + 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)); @@ -123,42 +169,47 @@ public interface Unit { * 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 */ - double convertToBase(double value); + protected abstract double convertToBase(double value); /** - * <p> - * Returns the base unit associated with this unit. - * </p> - * <p> - * The dimension of this unit must be equal to the dimension of the returned unit. - * </p> - * <p> - * If this unit <i>is</i> a base unit, this method should return this unit.\ - * </p> - * - * @return base unit associated with this unit + * @return combination of units that this unit is based on * @since 2018-12-22 * @since v0.1.0 */ - BaseUnit getBase(); + public final ObjectProduct<BaseUnit> getBase() { + return this.unitBase; + } /** * @return dimension measured by this unit * @since 2018-12-22 * @since v0.1.0 */ - UnitDimension getDimension(); + public final ObjectProduct<BaseDimension> getDimension() { + if (this.dimension == null) { + final Map<BaseUnit, Integer> mapping = this.unitBase.exponentMap(); + final Map<BaseDimension, Integer> dimensionMap = new HashMap<>(); - /** - * @return system that this unit is a part of - * @since 2018-12-23 - * @since v0.1.0 - */ - UnitSystem getSystem(); + for (final BaseUnit key : mapping.keySet()) { + dimensionMap.put(key.getBaseDimension(), mapping.get(key)); + } + + this.dimension = ObjectProduct.fromExponentMapping(dimensionMap); + } + return this.dimension; + } + + @Override + public String toString() { + return "Unit derived from base " + this.getBase().toString(); + } } diff --git a/src/org/unitConverter/UnitsDatabase.java b/src/org/unitConverter/unit/UnitDatabase.java index 520195c..a2b11c3 100644 --- a/src/org/unitConverter/UnitsDatabase.java +++ b/src/org/unitConverter/unit/UnitDatabase.java @@ -14,7 +14,7 @@ * 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; +package org.unitConverter.unit; import java.io.BufferedReader; import java.io.File; @@ -39,14 +39,9 @@ import java.util.function.Predicate; import java.util.regex.Matcher; import java.util.regex.Pattern; -import org.unitConverter.dimension.UnitDimension; import org.unitConverter.math.DecimalComparison; import org.unitConverter.math.ExpressionParser; -import org.unitConverter.unit.DefaultUnitPrefix; -import org.unitConverter.unit.LinearUnit; -import org.unitConverter.unit.SI; -import org.unitConverter.unit.Unit; -import org.unitConverter.unit.UnitPrefix; +import org.unitConverter.math.ObjectProduct; /** * A database of units, prefixes and dimensions, and their names. @@ -55,7 +50,7 @@ import org.unitConverter.unit.UnitPrefix; * @since 2019-01-07 * @since v0.1.0 */ -public final class UnitsDatabase { +public final class UnitDatabase { /** * A map for units that allows the use of prefixes. * <p> @@ -991,7 +986,7 @@ public final class UnitsDatabase { */ 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.SI.getBaseUnit(UnitDimension.EMPTY))) { + 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)) @@ -1027,7 +1022,7 @@ public final class UnitsDatabase { * @since 2019-03-14 * @since v0.2.0 */ - private final Map<String, UnitDimension> dimensions; + private final Map<String, ObjectProduct<BaseDimension>> dimensions; /** * A map mapping strings to units (including prefixes) @@ -1048,7 +1043,7 @@ public final class UnitsDatabase { .addBinaryOperator("-", (o1, o2) -> o1.minus(o2), 0) .addBinaryOperator("*", (o1, o2) -> o1.times(o2), 1).addSpaceFunction("*") .addBinaryOperator("/", (o1, o2) -> o1.dividedBy(o2), 1) - .addBinaryOperator("^", UnitsDatabase::exponentiateUnits, 2).build(); + .addBinaryOperator("^", UnitDatabase::exponentiateUnits, 2).build(); /** * A parser that can parse unit prefix expressions @@ -1067,7 +1062,7 @@ public final class UnitsDatabase { * @since 2019-04-13 * @since v0.2.0 */ - private final ExpressionParser<UnitDimension> unitDimensionParser = new ExpressionParser.Builder<>( + 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(); @@ -1077,7 +1072,7 @@ public final class UnitsDatabase { * @since 2019-01-10 * @since v0.1.0 */ - public UnitsDatabase() { + public UnitDatabase() { this.prefixlessUnits = new HashMap<>(); this.prefixes = new HashMap<>(); this.dimensions = new HashMap<>(); @@ -1096,7 +1091,7 @@ public final class UnitsDatabase { * @since 2019-03-14 * @since v0.2.0 */ - public void addDimension(final String name, final UnitDimension dimension) { + 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.")); } @@ -1141,7 +1136,7 @@ public final class UnitsDatabase { String.format("! used but no dimension found (line %d).", lineCounter)); } else { // it's a unit, get the unit - final UnitDimension dimension; + final ObjectProduct<BaseDimension> dimension; try { dimension = this.getDimensionFromExpression(expression); } catch (final IllegalArgumentException e) { @@ -1293,7 +1288,7 @@ public final class UnitsDatabase { * @since 2019-04-13 * @since v0.2.0 */ - public Map<String, UnitDimension> dimensionMap() { + public Map<String, ObjectProduct<BaseDimension>> dimensionMap() { return Collections.unmodifiableMap(this.dimensions); } @@ -1310,12 +1305,12 @@ public final class UnitsDatabase { * @since 2019-03-14 * @since v0.2.0 */ - public UnitDimension getDimension(final String name) { + public ObjectProduct<BaseDimension> getDimension(final String name) { Objects.requireNonNull(name, "name must not be null."); if (name.contains("^")) { final String[] baseAndExponent = name.split("\\^"); - final UnitDimension base = this.getDimension(baseAndExponent[0]); + final ObjectProduct<BaseDimension> base = this.getDimension(baseAndExponent[0]); final int exponent; try { @@ -1349,7 +1344,7 @@ public final class UnitsDatabase { * @since 2019-04-13 * @since v0.2.0 */ - public UnitDimension getDimensionFromExpression(final String expression) { + public ObjectProduct<BaseDimension> getDimensionFromExpression(final String expression) { Objects.requireNonNull(expression, "expression must not be null."); // attempt to get a dimension as an alias first @@ -1389,7 +1384,7 @@ public final class UnitsDatabase { // 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 unit.getBase().times(unit.convertToBase(value)); + return LinearUnit.fromUnitValue(unit, value); } else { // get a linear unit final Unit unit = this.getUnit(name); @@ -1411,7 +1406,7 @@ public final class UnitsDatabase { */ public UnitPrefix getPrefix(final String name) { try { - return new DefaultUnitPrefix(Double.parseDouble(name)); + return UnitPrefix.valueOf(Double.parseDouble(name)); } catch (final NumberFormatException e) { return this.prefixes.get(name); } @@ -1465,7 +1460,7 @@ public final class UnitsDatabase { public Unit getUnit(final String name) { try { final double value = Double.parseDouble(name); - return SI.SI.getBaseUnit(UnitDimension.EMPTY).times(value); + return SI.ONE.times(value); } catch (final NumberFormatException e) { return this.units.get(name); } diff --git a/src/org/unitConverter/UnitsDatabaseTest.java b/src/org/unitConverter/unit/UnitDatabaseTest.java index c46d598..164172b 100644 --- a/src/org/unitConverter/UnitsDatabaseTest.java +++ b/src/org/unitConverter/unit/UnitDatabaseTest.java @@ -14,7 +14,7 @@ * 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; +package org.unitConverter.unit; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -26,20 +26,15 @@ import java.util.Map; import java.util.Map.Entry; import org.junit.jupiter.api.Test; -import org.unitConverter.unit.DefaultUnitPrefix; -import org.unitConverter.unit.LinearUnit; -import org.unitConverter.unit.SI; -import org.unitConverter.unit.Unit; -import org.unitConverter.unit.UnitPrefix; /** - * A test for the {@link UnitsDatabase} class. This is NOT part of this program's public API. + * A test for the {@link UnitDatabase} class. This is NOT part of this program's public API. * * @author Adrien Hopkins * @since 2019-04-14 * @since v0.2.0 */ -class UnitsDatabaseTest { +class UnitDatabaseTest { // some linear units and one nonlinear private static final Unit U = SI.METRE; private static final Unit V = SI.KILOGRAM; @@ -50,14 +45,14 @@ class UnitsDatabaseTest { private static final LinearUnit J = SI.KILOGRAM.times(SI.METRE.toExponent(2)).dividedBy(SI.SECOND.toExponent(2)); private static final LinearUnit K = SI.KELVIN; - private static final Unit NONLINEAR = Unit.fromConversionFunctions(SI.METRE, o -> o + 1, o -> o - 1); + private static final Unit NONLINEAR = Unit.fromConversionFunctions(SI.METRE.getBase(), o -> o + 1, o -> o - 1); // make the prefix values prime so I can tell which multiplications were made - private static final UnitPrefix A = new DefaultUnitPrefix(2); - private static final UnitPrefix B = new DefaultUnitPrefix(3); - private static final UnitPrefix C = new DefaultUnitPrefix(5); - private static final UnitPrefix AB = new DefaultUnitPrefix(7); - private static final UnitPrefix BC = new DefaultUnitPrefix(11); + private static final UnitPrefix A = UnitPrefix.valueOf(2); + private static final UnitPrefix B = UnitPrefix.valueOf(3); + private static final UnitPrefix C = UnitPrefix.valueOf(5); + private static final UnitPrefix AB = UnitPrefix.valueOf(7); + private static final UnitPrefix BC = UnitPrefix.valueOf(11); /** * Confirms that operations that shouldn't function for infinite databases throw an {@code IllegalStateException}. @@ -67,7 +62,7 @@ class UnitsDatabaseTest { @Test public void testInfiniteSetExceptions() { // load units - final UnitsDatabase infiniteDatabase = new UnitsDatabase(); + final UnitDatabase infiniteDatabase = new UnitDatabase(); infiniteDatabase.addUnit("J", J); infiniteDatabase.addUnit("K", K); @@ -113,7 +108,7 @@ class UnitsDatabaseTest { */ @Test public void testPrefixes() { - final UnitsDatabase database = new UnitsDatabase(); + final UnitDatabase database = new UnitDatabase(); database.addUnit("U", U); database.addUnit("V", V); @@ -143,7 +138,7 @@ class UnitsDatabaseTest { */ @Test public void testPrefixlessUnitMap() { - final UnitsDatabase database = new UnitsDatabase(); + final UnitDatabase database = new UnitDatabase(); final Map<String, Unit> prefixlessUnits = database.unitMapPrefixless(); database.addUnit("U", U); @@ -166,7 +161,7 @@ class UnitsDatabaseTest { */ @Test public void testPrefixlessUnits() { - final UnitsDatabase database = new UnitsDatabase(); + final UnitDatabase database = new UnitDatabase(); database.addUnit("U", U); database.addUnit("V", V); @@ -188,7 +183,7 @@ class UnitsDatabaseTest { @Test public void testUnitExpressions() { // load units - final UnitsDatabase database = new UnitsDatabase(); + final UnitDatabase database = new UnitDatabase(); database.addUnit("U", U); database.addUnit("V", V); @@ -222,7 +217,7 @@ class UnitsDatabaseTest { @Test public void testUnitIterator() { // load units - final UnitsDatabase database = new UnitsDatabase(); + final UnitDatabase database = new UnitDatabase(); database.addUnit("J", J); database.addUnit("K", K); @@ -284,7 +279,7 @@ class UnitsDatabaseTest { @Test public void testUnitPrefixCombinations() { // load units - final UnitsDatabase database = new UnitsDatabase(); + final UnitDatabase database = new UnitDatabase(); database.addUnit("J", J); @@ -302,7 +297,7 @@ class UnitsDatabaseTest { // test 2 - ABC-J vs AB-CJ vs AB-C-J database.addUnit("CJ", J.times(13)); - database.addPrefix("ABC", new DefaultUnitPrefix(17)); + database.addPrefix("ABC", UnitPrefix.valueOf(17)); final Unit expected2 = J.times(17); final Unit actual2 = database.getUnit("ABCJ"); diff --git a/src/org/unitConverter/unit/UnitPrefix.java b/src/org/unitConverter/unit/UnitPrefix.java index 9f9645d..514fa1c 100644 --- a/src/org/unitConverter/unit/UnitPrefix.java +++ b/src/org/unitConverter/unit/UnitPrefix.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2018 Adrien Hopkins + * 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 @@ -16,14 +16,57 @@ */ package org.unitConverter.unit; +import org.unitConverter.math.DecimalComparison; + /** - * A prefix that can be attached onto the front of any unit, which multiplies it by a certain value + * A prefix that can be applied to a {@code LinearUnit} to multiply it by some value * * @author Adrien Hopkins - * @since 2019-01-14 - * @since v0.1.0 + * @since 2019-10-16 */ -public interface UnitPrefix { +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); + } + + /** + * 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) { + this.multiplier = multiplier; + } + + /** + * 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}. * @@ -33,16 +76,42 @@ public interface UnitPrefix { * @since 2019-04-13 * @since v0.2.0 */ - default UnitPrefix dividedBy(final UnitPrefix other) { - return new DefaultUnitPrefix(this.getMultiplier() / other.getMultiplier()); + public UnitPrefix dividedBy(final UnitPrefix other) { + return valueOf(this.getMultiplier() / other.getMultiplier()); + } + + @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()); + } + + public double getMultiplier() { + return this.multiplier; + } + + @Override + public int hashCode() { + return DecimalComparison.hash(this.getMultiplier()); } /** - * @return this prefix's multiplier - * @since 2019-01-14 - * @since v0.1.0 + * Multiplies this prefix by a scalar + * + * @param multiplicand + * number to multiply by + * @return product of prefix and scalar + * @since 2019-10-16 */ - double getMultiplier(); + public UnitPrefix times(final double multiplicand) { + return valueOf(this.getMultiplier() * multiplicand); + } /** * Multiplies this prefix by {@code other}. @@ -53,8 +122,8 @@ public interface UnitPrefix { * @since 2019-04-13 * @since v0.2.0 */ - default UnitPrefix times(final UnitPrefix other) { - return new DefaultUnitPrefix(this.getMultiplier() * other.getMultiplier()); + public UnitPrefix times(final UnitPrefix other) { + return valueOf(this.getMultiplier() * other.getMultiplier()); } /** @@ -66,7 +135,12 @@ public interface UnitPrefix { * @since 2019-04-13 * @since v0.2.0 */ - default UnitPrefix toExponent(final double exponent) { - return new DefaultUnitPrefix(Math.pow(getMultiplier(), exponent)); + public UnitPrefix toExponent(final double exponent) { + return valueOf(Math.pow(this.getMultiplier(), exponent)); + } + + @Override + public String toString() { + return String.format("Unit prefix equal to %s", this.multiplier); } } diff --git a/src/org/unitConverter/unit/UnitSystem.java b/src/org/unitConverter/unit/UnitSystem.java deleted file mode 100644 index 550eff6..0000000 --- a/src/org/unitConverter/unit/UnitSystem.java +++ /dev/null @@ -1,53 +0,0 @@ -/** - * 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.Objects; - -import org.unitConverter.dimension.UnitDimension; - -/** - * A system of units. Each unit should be aware of its system. - * - * @author Adrien Hopkins - * @since 2018-12-23 - * @since v0.1.0 - */ -public interface UnitSystem { - /** - * Gets a base unit for this system and the provided dimension. - * - * @param dimension - * dimension used by base unit - * @return base unit - * @throws NullPointerException - * if dimension is null - * @since 2019-01-25 - * @since v0.1.0 - */ - default BaseUnit getBaseUnit(final UnitDimension dimension) { - Objects.requireNonNull(dimension, "dimension must not be null."); - return new BaseUnit(dimension, this); - } - - /** - * @return name of system - * @since 2018-12-23 - * @since v0.1.0 - */ - String getName(); -} diff --git a/src/org/unitConverter/unit/UnitTest.java b/src/org/unitConverter/unit/UnitTest.java index 7ae5fbf..c078cfc 100644 --- a/src/org/unitConverter/unit/UnitTest.java +++ b/src/org/unitConverter/unit/UnitTest.java @@ -22,7 +22,6 @@ import java.util.Random; import java.util.concurrent.ThreadLocalRandom; import org.junit.jupiter.api.Test; -import org.unitConverter.dimension.StandardDimensions; import org.unitConverter.math.DecimalComparison; /** @@ -46,16 +45,8 @@ class UnitTest { } @Test - public void testBaseUnitExclusives() { - // this test should have a compile error if I am doing something wrong - final BaseUnit metrePerSecondSquared = SI.METRE.dividedBy(SI.SECOND.toExponent(2)); - - assertEquals(metrePerSecondSquared, SI.SI.getBaseUnit(StandardDimensions.ACCELERATION)); - } - - @Test public void testConversion() { - final BaseUnit metre = SI.METRE; + final LinearUnit metre = SI.METRE; final Unit inch = metre.times(0.0254); assertEquals(1.9, inch.convertTo(metre, 75), 0.01); @@ -77,8 +68,8 @@ class UnitTest { @Test public void testEquals() { - final BaseUnit metre = SI.METRE; - final Unit meter = SI.SI.getBaseUnit(StandardDimensions.LENGTH); + final LinearUnit metre = SI.METRE; + final Unit meter = SI.BaseUnits.METRE.asLinearUnit(); assertEquals(metre, meter); } @@ -87,7 +78,7 @@ class UnitTest { public void testMultiplicationAndDivision() { // test unit-times-unit multiplication final LinearUnit generatedJoule = SI.KILOGRAM.times(SI.METRE.toExponent(2)).dividedBy(SI.SECOND.toExponent(2)); - final LinearUnit actualJoule = SI.SI.getBaseUnit(StandardDimensions.ENERGY); + final LinearUnit actualJoule = SI.JOULE; assertEquals(generatedJoule, actualJoule); @@ -96,14 +87,14 @@ class UnitTest { final LinearUnit hour = SI.SECOND.times(3600); final LinearUnit generatedKPH = kilometre.dividedBy(hour); - final LinearUnit actualKPH = SI.SI.getBaseUnit(StandardDimensions.VELOCITY).dividedBy(3.6); + final LinearUnit actualKPH = SI.METRE.dividedBy(SI.SECOND).dividedBy(3.6); assertEquals(generatedKPH, actualKPH); } @Test public void testPrefixes() { - final LinearUnit generatedKilometre = SI.METRE.withPrefix(SIPrefix.KILO); + final LinearUnit generatedKilometre = SI.METRE.withPrefix(SI.KILO); final LinearUnit actualKilometre = SI.METRE.times(1000); assertEquals(generatedKilometre, actualKilometre); diff --git a/src/org/unitConverter/unit/package-info.java b/src/org/unitConverter/unit/package-info.java index dd5a939..2f0e097 100644 --- a/src/org/unitConverter/unit/package-info.java +++ b/src/org/unitConverter/unit/package-info.java @@ -15,10 +15,10 @@ * along with this program. If not, see <https://www.gnu.org/licenses/>. */ /** - * All of the classes that correspond to the units being converted. + * Everything to do with the units that make up Unit Converter. * * @author Adrien Hopkins - * @since 2019-01-25 + * @since 2019-10-16 * @since v0.1.0 */ package org.unitConverter.unit;
\ No newline at end of file |