From 69f2849d25a41ae7c0383636557deda1bc64247d Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Wed, 16 Oct 2019 12:29:19 -0400 Subject: Created more generalized objects for use in the new units. --- src/org/unitConverter/math/ObjectProduct.java | 261 +++++++++++++++++++++ src/org/unitConverter/math/ObjectProductTest.java | 79 +++++++ src/org/unitConverter/math/StandardDimensions.java | 86 +++++++ src/org/unitConverter/math/ZeroIsNullMap.java | 129 ++++++++++ src/org/unitConverter/math/ZeroIsNullMapTest.java | 113 +++++++++ 5 files changed, 668 insertions(+) create mode 100644 src/org/unitConverter/math/ObjectProduct.java create mode 100644 src/org/unitConverter/math/ObjectProductTest.java create mode 100644 src/org/unitConverter/math/StandardDimensions.java create mode 100644 src/org/unitConverter/math/ZeroIsNullMap.java create mode 100644 src/org/unitConverter/math/ZeroIsNullMapTest.java diff --git a/src/org/unitConverter/math/ObjectProduct.java b/src/org/unitConverter/math/ObjectProduct.java new file mode 100644 index 0000000..ec4d2d6 --- /dev/null +++ b/src/org/unitConverter/math/ObjectProduct.java @@ -0,0 +1,261 @@ +/** + * 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 . + */ +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; + +/** + * 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 { + /** + * Returns an empty ObjectProduct of a certain type + * + * @param + * type of objects that can be multiplied + * @return empty product + * @since 2019-10-16 + */ + public static final ObjectProduct empty() { + return new ObjectProduct<>(new HashMap<>()); + } + + /** + * Gets an {@code ObjectProduct} from an object-to-integer mapping + * + * @param + * type of object in product + * @param map + * map mapping objects to exponents + * @return object product + * @since 2019-10-16 + */ + public static final ObjectProduct fromExponentMapping(final Map 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 ObjectProduct oneOf(final T object) { + Objects.requireNonNull(object, "object must not be null."); + final Map 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 exponents; + + /** + * Creates the {@code ObjectProduct}. + * + * @param exponents + * objects that make up this product + * @since 2019-10-16 + */ + private ObjectProduct(final Map exponents) { + this.exponents = Collections.unmodifiableMap(ZeroIsNullMap.create(new HashMap<>(exponents))); + } + + /** + * 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 dividedBy(final ObjectProduct other) { + Objects.requireNonNull(other, "other must not be null."); + // get a list of all objects in both sets + final Set objects = new HashSet<>(); + objects.addAll(this.getBaseSet()); + objects.addAll(other.getBaseSet()); + + // get a list of all exponents + final Map 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 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 getBaseSet() { + final Set 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 "base", i.e. it has one exponent of one and no other nonzero exponents + * @since 2019-10-16 + */ + public boolean isBase() { + 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 times(final ObjectProduct other) { + Objects.requireNonNull(other, "other must not be null."); + // get a list of all objects in both sets + final Set objects = new HashSet<>(); + objects.addAll(this.getBaseSet()); + objects.addAll(other.getBaseSet()); + + // get a list of all exponents + final Map 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 toExponent(final int exponent) { + final Map 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() { + final List positiveStringComponents = new ArrayList<>(); + final List negativeStringComponents = new ArrayList<>(); + + // for each base dimension that makes up this dimension, add it and its exponent + for (final T dimension : this.getBaseSet()) { + final int exponent = this.exponents.get(dimension); + if (exponent > 0) { + positiveStringComponents.add(String.format("%s^%d", dimension, exponent)); + } else if (exponent < 0) { + negativeStringComponents.add(String.format("%s^%d", dimension, -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/ObjectProductTest.java b/src/org/unitConverter/math/ObjectProductTest.java new file mode 100644 index 0000000..03c767c --- /dev/null +++ b/src/org/unitConverter/math/ObjectProductTest.java @@ -0,0 +1,79 @@ +/** + * 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 . + */ +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 org.junit.jupiter.api.Test; +import org.unitConverter.dimension.SIBaseDimension; +import org.unitConverter.dimension.UnitDimension; + +/** + * 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 ObjectProductTest { + /** + * Tests {@link UnitDimension#equals} + * + * @since 2018-12-12 + * @since v0.1.0 + */ + @Test + public void testEquals() { + assertEquals(LENGTH, LENGTH); + assertFalse(LENGTH.equals(QUANTITY)); + } + + /** + * Tests {@code UnitDimension}'s exponentiation + * + * @since 2019-01-15 + * @since v0.1.0 + */ + @Test + public void testExponents() { + assertEquals(1, LENGTH.getExponent(SIBaseDimension.LENGTH)); + assertEquals(3, VOLUME.getExponent(SIBaseDimension.LENGTH)); + } + + /** + * Tests {@code UnitDimension}'s multiplication and division. + * + * @since 2018-12-12 + * @since v0.1.0 + */ + @Test + public void testMultiplicationAndDivision() { + assertEquals(AREA, LENGTH.times(LENGTH)); + assertEquals(MASS_DENSITY, MASS.dividedBy(VOLUME)); + assertEquals(ENERGY, AREA.times(MASS).dividedBy(TIME).dividedBy(TIME)); + assertEquals(LENGTH, LENGTH.times(TIME).dividedBy(TIME)); + } +} diff --git a/src/org/unitConverter/math/StandardDimensions.java b/src/org/unitConverter/math/StandardDimensions.java new file mode 100644 index 0000000..db5efc3 --- /dev/null +++ b/src/org/unitConverter/math/StandardDimensions.java @@ -0,0 +1,86 @@ +/** + * 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 . + */ +package org.unitConverter.math; + +import org.unitConverter.dimension.BaseDimension; +import org.unitConverter.dimension.OtherBaseDimension; +import org.unitConverter.dimension.SIBaseDimension; + +/** + * All of the dimensions that are used by the SI. (Test data for the ObjectProductTest) + * + * @author Adrien Hopkins + * @since 2018-12-11 + * @since v0.1.0 + */ +final class StandardDimensions { + // base dimensions + public static final ObjectProduct EMPTY = ObjectProduct.empty(); + public static final ObjectProduct LENGTH = ObjectProduct.oneOf(SIBaseDimension.LENGTH); + public static final ObjectProduct MASS = ObjectProduct.oneOf(SIBaseDimension.MASS); + public static final ObjectProduct TIME = ObjectProduct.oneOf(SIBaseDimension.TIME); + public static final ObjectProduct ELECTRIC_CURRENT = ObjectProduct + .oneOf(SIBaseDimension.ELECTRIC_CURRENT); + public static final ObjectProduct TEMPERATURE = ObjectProduct.oneOf(SIBaseDimension.TEMPERATURE); + public static final ObjectProduct QUANTITY = ObjectProduct.oneOf(SIBaseDimension.QUANTITY); + public static final ObjectProduct LUMINOUS_INTENSITY = ObjectProduct + .oneOf(SIBaseDimension.LUMINOUS_INTENSITY); + public static final ObjectProduct INFORMATION = ObjectProduct.oneOf(OtherBaseDimension.INFORMATION); + public static final ObjectProduct CURRENCY = ObjectProduct.oneOf(OtherBaseDimension.CURRENCY); + // derived dimensions without named SI units + public static final ObjectProduct AREA = LENGTH.times(LENGTH); + + public static final ObjectProduct VOLUME = AREA.times(LENGTH); + public static final ObjectProduct VELOCITY = LENGTH.dividedBy(TIME); + public static final ObjectProduct ACCELERATION = VELOCITY.dividedBy(TIME); + public static final ObjectProduct WAVENUMBER = EMPTY.dividedBy(LENGTH); + public static final ObjectProduct MASS_DENSITY = MASS.dividedBy(VOLUME); + public static final ObjectProduct SURFACE_DENSITY = MASS.dividedBy(AREA); + public static final ObjectProduct SPECIFIC_VOLUME = VOLUME.dividedBy(MASS); + public static final ObjectProduct CURRENT_DENSITY = ELECTRIC_CURRENT.dividedBy(AREA); + public static final ObjectProduct MAGNETIC_FIELD_STRENGTH = ELECTRIC_CURRENT.dividedBy(LENGTH); + public static final ObjectProduct CONCENTRATION = QUANTITY.dividedBy(VOLUME); + public static final ObjectProduct MASS_CONCENTRATION = CONCENTRATION.times(MASS); + public static final ObjectProduct LUMINANCE = LUMINOUS_INTENSITY.dividedBy(AREA); + public static final ObjectProduct REFRACTIVE_INDEX = VELOCITY.dividedBy(VELOCITY); + public static final ObjectProduct REFLACTIVE_PERMEABILITY = EMPTY.times(EMPTY); + public static final ObjectProduct ANGLE = LENGTH.dividedBy(LENGTH); + public static final ObjectProduct SOLID_ANGLE = AREA.dividedBy(AREA); + // derived dimensions with named SI units + public static final ObjectProduct FREQUENCY = EMPTY.dividedBy(TIME); + + public static final ObjectProduct FORCE = MASS.times(ACCELERATION); + public static final ObjectProduct ENERGY = FORCE.times(LENGTH); + public static final ObjectProduct POWER = ENERGY.dividedBy(TIME); + public static final ObjectProduct ELECTRIC_CHARGE = ELECTRIC_CURRENT.times(TIME); + public static final ObjectProduct VOLTAGE = ENERGY.dividedBy(ELECTRIC_CHARGE); + public static final ObjectProduct CAPACITANCE = ELECTRIC_CHARGE.dividedBy(VOLTAGE); + public static final ObjectProduct ELECTRIC_RESISTANCE = VOLTAGE.dividedBy(ELECTRIC_CURRENT); + public static final ObjectProduct ELECTRIC_CONDUCTANCE = ELECTRIC_CURRENT.dividedBy(VOLTAGE); + public static final ObjectProduct MAGNETIC_FLUX = VOLTAGE.times(TIME); + public static final ObjectProduct MAGNETIC_FLUX_DENSITY = MAGNETIC_FLUX.dividedBy(AREA); + public static final ObjectProduct INDUCTANCE = MAGNETIC_FLUX.dividedBy(ELECTRIC_CURRENT); + public static final ObjectProduct LUMINOUS_FLUX = LUMINOUS_INTENSITY.times(SOLID_ANGLE); + public static final ObjectProduct ILLUMINANCE = LUMINOUS_FLUX.dividedBy(AREA); + public static final ObjectProduct SPECIFIC_ENERGY = ENERGY.dividedBy(MASS); + public static final ObjectProduct CATALYTIC_ACTIVITY = QUANTITY.dividedBy(TIME); + + // You may NOT get StandardDimensions instances! + private StandardDimensions() { + throw new AssertionError(); + } +} diff --git a/src/org/unitConverter/math/ZeroIsNullMap.java b/src/org/unitConverter/math/ZeroIsNullMap.java new file mode 100644 index 0000000..10e4d52 --- /dev/null +++ b/src/org/unitConverter/math/ZeroIsNullMap.java @@ -0,0 +1,129 @@ +/** + * 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 . + */ +package org.unitConverter.math; + +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +/** + * A "wrapper" for an existing map that treats a zero value as equivalent to null. + * + * @author Adrien Hopkins + * @since 2019-10-16 + * @param + * type of key + */ +public final class ZeroIsNullMap extends AbstractMap { + /** + * Create a ZeroIsNullMap. This method always creates a new map. + * + * @param + * type of key + * @param map + * map to input + * @return map that treats zero as null + * @throws NullPointerException + * if map is null + * @since 2019-10-16 + */ + public static Map create(final Map map) { + return new ZeroIsNullMap<>(Objects.requireNonNull(map, "map must not be null.")); + } + + private final Map map; + + /** + * Creates the {@code ObjectProductMap}. + * + * @param map + * @since 2019-10-16 + */ + private ZeroIsNullMap(final Map map) { + this.map = map; + } + + @Override + public void clear() { + this.map.clear(); + } + + @Override + public boolean containsKey(final Object key) { + return this.map.containsKey(key) && this.map.get(key) != 0; + } + + @Override + public boolean containsValue(final Object value) { + return this.values().contains(value); + } + + @Override + public Set> entrySet() { + final Set> entrySet = new HashSet<>(this.map.entrySet()); + entrySet.removeIf(e -> e.getValue() == 0); + return entrySet; + } + + @Override + public Integer get(final Object key) { + final Integer i = this.map.get(key); + if (Objects.equals(i, 0)) + return null; + else + return i; + } + + @Override + public Set keySet() { + final Set keySet = new HashSet<>(this.map.keySet()); + keySet.removeIf(k -> this.map.get(k) == 0); + return keySet; + } + + @Override + public Integer put(final T key, final Integer value) { + if (value != 0) + return this.map.put(key, value); + else + return null; + } + + @Override + public void putAll(final Map m) { + for (final T key : m.keySet()) { + this.put(key, m.get(key)); + } + } + + @Override + public Integer remove(final Object key) { + return this.map.remove(key); + } + + @Override + public Collection values() { + final List values = new ArrayList<>(this.map.values()); + values.removeIf(i -> i == 0); + return values; + } +} \ No newline at end of file diff --git a/src/org/unitConverter/math/ZeroIsNullMapTest.java b/src/org/unitConverter/math/ZeroIsNullMapTest.java new file mode 100644 index 0000000..c3db951 --- /dev/null +++ b/src/org/unitConverter/math/ZeroIsNullMapTest.java @@ -0,0 +1,113 @@ +/** + * 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 . + */ +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.assertTrue; + +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; + +import org.junit.jupiter.api.Test; + +/** + * @author Adrien Hopkins + * @since 2019-10-16 + */ +class ZeroIsNullMapTest { + + /** + * @return map to be used for test data + * @since 2019-10-16 + */ + Map getTestMap() { + final Map map = new HashMap<>(); + map.put("one", 1); + map.put("two", 2); + map.put("zero", 0); + map.put("ten", 10); + return ZeroIsNullMap.create(map); + } + + /** + * Test method for {@link org.unitConverter.math.ZeroIsNullMap#containsKey(java.lang.Object)}. + */ + @Test + void testContainsKeyObject() { + final Map 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 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 map = this.getTestMap(); + for (final Entry e : map.entrySet()) { + assertTrue(e.getValue() != 0); + } + } + + /** + * Test method for {@link org.unitConverter.math.ZeroIsNullMap#get(java.lang.Object)}. + */ + @Test + void testGetObject() { + final Map map = this.getTestMap(); + assertEquals(1, map.get("one")); + assertEquals(10, map.get("ten")); + assertEquals(null, map.get("five")); + assertEquals(null, map.get("zero")); + } + + /** + * Test method for {@link org.unitConverter.math.ZeroIsNullMap#keySet()}. + */ + @Test + void testKeySet() { + final Map map = this.getTestMap(); + assertFalse(map.keySet().contains("zero")); + } + + /** + * Test method for {@link org.unitConverter.math.ZeroIsNullMap#values()}. + */ + @Test + void testValues() { + final Map map = this.getTestMap(); + assertFalse(map.values().contains(0)); + } + +} -- cgit v1.2.3 From 145f524f19b3d61cd12441c2f1a8de05d7dcfe60 Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Wed, 16 Oct 2019 12:31:09 -0400 Subject: Created the new BaseUnit, Unit and AbstractUnit implementations. --- src/org/unitConverter/newUnits/AbstractUnit.java | 76 +++++++++++ src/org/unitConverter/newUnits/BaseUnit.java | 102 +++++++++++++++ src/org/unitConverter/newUnits/FunctionalUnit.java | 98 ++++++++++++++ src/org/unitConverter/newUnits/Unit.java | 145 +++++++++++++++++++++ src/org/unitConverter/newUnits/package-info.java | 23 ++++ 5 files changed, 444 insertions(+) create mode 100644 src/org/unitConverter/newUnits/AbstractUnit.java create mode 100644 src/org/unitConverter/newUnits/BaseUnit.java create mode 100644 src/org/unitConverter/newUnits/FunctionalUnit.java create mode 100644 src/org/unitConverter/newUnits/Unit.java create mode 100644 src/org/unitConverter/newUnits/package-info.java diff --git a/src/org/unitConverter/newUnits/AbstractUnit.java b/src/org/unitConverter/newUnits/AbstractUnit.java new file mode 100644 index 0000000..909ea8b --- /dev/null +++ b/src/org/unitConverter/newUnits/AbstractUnit.java @@ -0,0 +1,76 @@ +/** + * 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 . + */ +package org.unitConverter.newUnits; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +import org.unitConverter.dimension.BaseDimension; +import org.unitConverter.math.ObjectProduct; + +/** + * @author Adrien Hopkins + * @since 2019-10-16 + */ +public abstract class AbstractUnit implements Unit { + /** + * The combination of units that this unit is based on. + */ + private final ObjectProduct unitBase; + + /** + * Cache storing the result of getDimension() + */ + private transient ObjectProduct dimension = null; + + /** + * Creates the {@code AbstractUnit}. + * + * @param unitBase + * @since 2019-10-16 + * @throws NullPointerException + * if unitBase is null + */ + public AbstractUnit(final ObjectProduct unitBase) { + this.unitBase = Objects.requireNonNull(unitBase, "unitBase must not be null."); + } + + /** + * @return unitBase + * @since 2019-10-16 + */ + @Override + public final ObjectProduct getBase() { + return this.unitBase; + } + + @Override + public ObjectProduct getDimension() { + if (this.dimension == null) { + final Map mapping = this.unitBase.exponentMap(); + final Map dimensionMap = new HashMap<>(); + + for (final BaseUnit key : mapping.keySet()) { + dimensionMap.put(key.getBaseDimension(), mapping.get(key)); + } + + this.dimension = ObjectProduct.fromExponentMapping(dimensionMap); + } + return this.dimension; + } +} diff --git a/src/org/unitConverter/newUnits/BaseUnit.java b/src/org/unitConverter/newUnits/BaseUnit.java new file mode 100644 index 0000000..69e8b8b --- /dev/null +++ b/src/org/unitConverter/newUnits/BaseUnit.java @@ -0,0 +1,102 @@ +/** + * 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 . + */ +package org.unitConverter.newUnits; + +import java.util.Objects; + +import org.unitConverter.dimension.BaseDimension; +import org.unitConverter.math.ObjectProduct; + +/** + * A unit that other units are defined by. + * + * @author Adrien Hopkins + * @since 2019-10-16 + */ +public final class BaseUnit implements Unit { + private final BaseDimension dimension; + private final String name; + private final String symbol; + + /** + * Creates the {@code BaseUnit}. + * + * @param dimension + * dimension of unit + * @param name + * name of unit + * @param symbol + * symbol of unit + * @throws NullPointerException + * if any argument is null + * @since 2019-10-16 + */ + private BaseUnit(final BaseDimension dimension, final String name, final String symbol) { + 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."); + } + + @Override + public double convertFromBase(final double value) { + return value; + } + + @Override + public double convertToBase(final double value) { + return value; + } + + @Override + public ObjectProduct getBase() { + return ObjectProduct.oneOf(this); + } + + /** + * @return dimension + * @since 2019-10-16 + */ + public final BaseDimension getBaseDimension() { + return this.dimension; + } + + @Override + public ObjectProduct getDimension() { + return ObjectProduct.oneOf(this.getBaseDimension()); + } + + /** + * @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/newUnits/FunctionalUnit.java b/src/org/unitConverter/newUnits/FunctionalUnit.java new file mode 100644 index 0000000..99ff833 --- /dev/null +++ b/src/org/unitConverter/newUnits/FunctionalUnit.java @@ -0,0 +1,98 @@ +/** + * 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 . + */ +package org.unitConverter.newUnits; + +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 { + /** + * Returns a unit from its base and the functions it uses to convert to and from its base. + * + * @param base + * unit's base + * @param converterFrom + * function that accepts a value expressed in the unit's base and returns that value expressed in this + * unit. + * @param converterTo + * function that accepts a value expressed in the unit and returns that value expressed in the unit's + * base. + * @return a unit that uses the provided functions to convert. + * @since 2019-05-22 + * @throws NullPointerException + * if any argument is null + */ + public static FunctionalUnit valueOf(final ObjectProduct base, final DoubleUnaryOperator converterFrom, + final DoubleUnaryOperator converterTo) { + return new FunctionalUnit(base, converterFrom, converterTo); + } + + /** + * A function that accepts a value expressed in the unit's base and returns that value expressed in this unit. + * + * @since 2019-05-22 + */ + private final DoubleUnaryOperator converterFrom; + + /** + * A function that accepts a value expressed in the unit and returns that value expressed in the unit's base. + * + * @since 2019-05-22 + */ + private final DoubleUnaryOperator converterTo; + + /** + * Creates the {@code FunctionalUnit}. + * + * @param base + * unit's base + * @param converterFrom + * function that accepts a value expressed in the unit's base and returns that value expressed in this + * unit. + * @param converterTo + * function that accepts a value expressed in the unit and returns that value expressed in the unit's + * base. + * @throws NullPointerException + * if any argument is null + * @since 2019-05-22 + */ + private FunctionalUnit(final ObjectProduct base, final DoubleUnaryOperator converterFrom, + final DoubleUnaryOperator converterTo) { + super(base); + this.converterFrom = Objects.requireNonNull(converterFrom, "converterFrom must not be null."); + this.converterTo = Objects.requireNonNull(converterTo, "converterTo must not be null."); + } + + @Override + public double convertFromBase(final double value) { + return this.converterFrom.applyAsDouble(value); + } + + @Override + public double convertToBase(final double value) { + return this.converterTo.applyAsDouble(value); + } + +} diff --git a/src/org/unitConverter/newUnits/Unit.java b/src/org/unitConverter/newUnits/Unit.java new file mode 100644 index 0000000..339afde --- /dev/null +++ b/src/org/unitConverter/newUnits/Unit.java @@ -0,0 +1,145 @@ +/** + * 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 . + */ +package org.unitConverter.newUnits; + +import java.util.Objects; +import java.util.function.DoubleUnaryOperator; + +import org.unitConverter.dimension.BaseDimension; +import org.unitConverter.math.ObjectProduct; + +/** + * @author Adrien Hopkins + * @since 2019-10-16 + */ +public interface Unit { + /** + * Returns a unit from its base and the functions it uses to convert to and from its base. + * + *

+ * For example, to get a unit representing the degree Celsius, the following code can be used: + * + * {@code Unit.fromConversionFunctions(SI.KELVIN, tempK -> tempK - 273.15, tempC -> tempC + 273.15);} + *

+ * + * @param base + * unit's base + * @param converterFrom + * function that accepts a value expressed in the unit's base and returns that value expressed in this + * unit. + * @param converterTo + * function that accepts a value expressed in the unit and returns that value expressed in the unit's + * base. + * @return a unit that uses the provided functions to convert. + * @since 2019-05-22 + * @throws NullPointerException + * if any argument is null + */ + public static Unit fromConversionFunctions(final ObjectProduct base, + final DoubleUnaryOperator converterFrom, final DoubleUnaryOperator converterTo) { + return FunctionalUnit.valueOf(base, converterFrom, converterTo); + } + + /** + * Checks if a value expressed in this unit can be converted to a value expressed in {@code other} + * + * @param other + * unit to test with + * @return true if the units are compatible + * @since 2019-01-13 + * @since v0.1.0 + * @throws NullPointerException + * if other is null + */ + default boolean canConvertTo(final Unit other) { + Objects.requireNonNull(other, "other must not be null."); + return Objects.equals(this.getBase(), other.getBase()); + } + + /** + * Converts from a value expressed in this unit's base unit to a value expressed in this unit. + *

+ * This must be the inverse of {@code convertToBase}, so {@code convertFromBase(convertToBase(value))} must be equal + * to {@code value} for any value, ignoring precision loss by roundoff error. + *

+ *

+ * If this unit is a base unit, this method should return {@code value}. + *

+ * + * @param value + * value expressed in base unit + * @return value expressed in this unit + * @since 2018-12-22 + * @since v0.1.0 + */ + double convertFromBase(double value); + + /** + * Converts a value expressed in this unit to a value expressed in {@code other}. + * + * @param other + * unit to convert to + * @param value + * value to convert + * @return converted value + * @since 2019-05-22 + * @throws IllegalArgumentException + * if {@code other} is incompatible for conversion with this unit (as tested by + * {@link Unit#canConvertTo}). + * @throws NullPointerException + * if other is null + */ + default double convertTo(final Unit other, final double value) { + Objects.requireNonNull(other, "other must not be null."); + if (this.canConvertTo(other)) + return other.convertFromBase(this.convertToBase(value)); + else + throw new IllegalArgumentException(String.format("Cannot convert from %s to %s.", this, other)); + } + + /** + * Converts from a value expressed in this unit to a value expressed in this unit's base unit. + *

+ * This must be the inverse of {@code convertFromBase}, so {@code convertToBase(convertFromBase(value))} must be + * equal to {@code value} for any value, ignoring precision loss by roundoff error. + *

+ *

+ * If this unit is a base unit, this method should return {@code value}. + *

+ * + * @param value + * value expressed in this unit + * @return value expressed in base unit + * @since 2018-12-22 + * @since v0.1.0 + */ + double convertToBase(double value); + + /** + * @return combination of units that this unit is based on + * @since 2018-12-22 + * @since v0.1.0 + */ + ObjectProduct getBase(); + + /** + * @return dimension measured by this unit + * @since 2018-12-22 + * @since v0.1.0 + */ + ObjectProduct getDimension(); +} \ No newline at end of file diff --git a/src/org/unitConverter/newUnits/package-info.java b/src/org/unitConverter/newUnits/package-info.java new file mode 100644 index 0000000..9cd0d1a --- /dev/null +++ b/src/org/unitConverter/newUnits/package-info.java @@ -0,0 +1,23 @@ +/** + * 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 . + */ +/** + * The new definition for units. + * + * @author Adrien Hopkins + * @since 2019-10-16 + */ +package org.unitConverter.newUnits; \ No newline at end of file -- cgit v1.2.3 From 45286c30f96f152f821098d878ede64ecbabe48a Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Wed, 16 Oct 2019 13:53:29 -0400 Subject: Added the LinearUnit to the new units definition. --- src/org/unitConverter/math/ObjectProduct.java | 28 ++- src/org/unitConverter/newUnits/AbstractUnit.java | 7 +- src/org/unitConverter/newUnits/BaseUnit.java | 11 + src/org/unitConverter/newUnits/LinearUnit.java | 261 +++++++++++++++++++++++ src/org/unitConverter/newUnits/Unit.java | 2 + 5 files changed, 301 insertions(+), 8 deletions(-) create mode 100644 src/org/unitConverter/newUnits/LinearUnit.java diff --git a/src/org/unitConverter/math/ObjectProduct.java b/src/org/unitConverter/math/ObjectProduct.java index ec4d2d6..21ab207 100644 --- a/src/org/unitConverter/math/ObjectProduct.java +++ b/src/org/unitConverter/math/ObjectProduct.java @@ -24,6 +24,7 @@ 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 @@ -177,10 +178,10 @@ public final class ObjectProduct { } /** - * @return true if this product is a "base", i.e. it has one exponent of one and no other nonzero 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 isBase() { + public boolean isSingleObject() { int oneCount = 0; boolean twoOrMore = false; // has exponents of 2 or more for (final T b : this.getBaseSet()) { @@ -238,16 +239,29 @@ public final class ObjectProduct { @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 objectToString) { final List positiveStringComponents = new ArrayList<>(); final List negativeStringComponents = new ArrayList<>(); - // for each base dimension that makes up this dimension, add it and its exponent - for (final T dimension : this.getBaseSet()) { - final int exponent = this.exponents.get(dimension); + // 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", dimension, exponent)); + positiveStringComponents.add(String.format("%s^%d", objectToString.apply(object), exponent)); } else if (exponent < 0) { - negativeStringComponents.add(String.format("%s^%d", dimension, -exponent)); + negativeStringComponents.add(String.format("%s^%d", objectToString.apply(object), -exponent)); } } diff --git a/src/org/unitConverter/newUnits/AbstractUnit.java b/src/org/unitConverter/newUnits/AbstractUnit.java index 909ea8b..bc4608e 100644 --- a/src/org/unitConverter/newUnits/AbstractUnit.java +++ b/src/org/unitConverter/newUnits/AbstractUnit.java @@ -60,7 +60,7 @@ public abstract class AbstractUnit implements Unit { } @Override - public ObjectProduct getDimension() { + public final ObjectProduct getDimension() { if (this.dimension == null) { final Map mapping = this.unitBase.exponentMap(); final Map dimensionMap = new HashMap<>(); @@ -73,4 +73,9 @@ public abstract class AbstractUnit implements Unit { } return this.dimension; } + + @Override + public String toString() { + return "Unit derived from base " + this.getBase().toString(); + } } diff --git a/src/org/unitConverter/newUnits/BaseUnit.java b/src/org/unitConverter/newUnits/BaseUnit.java index 69e8b8b..b7577ff 100644 --- a/src/org/unitConverter/newUnits/BaseUnit.java +++ b/src/org/unitConverter/newUnits/BaseUnit.java @@ -51,6 +51,17 @@ public final class BaseUnit implements Unit { this.symbol = Objects.requireNonNull(symbol, "symbol must not be null."); } + /** + * Returns a {@code LinearUnit} with this unit as a base and a conversion factor of 1. This operation must be done + * in order to allow units to be created with operations. + * + * @return this unit as a {@code LinearUnit} + * @since 2019-10-16 + */ + public LinearUnit asLinearUnit() { + return LinearUnit.valueOf(this.getBase(), 1); + } + @Override public double convertFromBase(final double value) { return value; diff --git a/src/org/unitConverter/newUnits/LinearUnit.java b/src/org/unitConverter/newUnits/LinearUnit.java new file mode 100644 index 0000000..5a589b7 --- /dev/null +++ b/src/org/unitConverter/newUnits/LinearUnit.java @@ -0,0 +1,261 @@ +/** + * 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 . + */ +package org.unitConverter.newUnits; + +import java.util.Objects; + +import org.unitConverter.math.DecimalComparison; +import org.unitConverter.math.ObjectProduct; + +/** + * A unit that can be expressed as a product of its base and a number. For example, kilometres, inches and pounds. + * + * @author Adrien Hopkins + * @since 2019-10-16 + */ +public final class LinearUnit extends AbstractUnit { + /** + * Gets a {@code LinearUnit} from a unit base and a conversion factor. In other words, gets the product of + * {@code unitBase} and {@code conversionFactor}, expressed as a {@code LinearUnit}. + * + * @param unitBase + * unit base to multiply by + * @param conversionFactor + * number to multiply base by + * @return product of base and conversion factor + * @since 2019-10-16 + */ + public static LinearUnit valueOf(final ObjectProduct unitBase, final double conversionFactor) { + return new LinearUnit(unitBase, conversionFactor); + } + + /** + * The value of this unit as represented in its base form. Mathematically, + * + *
+	 * this = conversionFactor * getBase()
+	 * 
+ * + * @since 2019-10-16 + */ + private final double conversionFactor; + + /** + * Creates the {@code LinearUnit}. + * + * @param unitBase + * base of linear unit + * @param conversionFactor + * conversion factor between base and unit + * @since 2019-10-16 + */ + private LinearUnit(final ObjectProduct unitBase, final double conversionFactor) { + super(unitBase); + this.conversionFactor = conversionFactor; + } + + @Override + public double convertFromBase(final double value) { + return value / this.getConversionFactor(); + } + + @Override + public double convertToBase(final double value) { + return value * this.getConversionFactor(); + } + + /** + * Divides this unit by a scalar. + * + * @param divisor + * scalar to divide by + * @return quotient + * @since 2018-12-23 + * @since v0.1.0 + */ + public LinearUnit dividedBy(final double divisor) { + return valueOf(this.getBase(), this.getConversionFactor() / divisor); + } + + /** + * Returns the quotient of this unit and another. + * + * @param divisor + * unit to divide by + * @return quotient of two units + * @throws NullPointerException + * if {@code divisor} is null + * @since 2018-12-22 + * @since v0.1.0 + */ + public LinearUnit dividedBy(final LinearUnit divisor) { + Objects.requireNonNull(divisor, "other must not be null"); + + // divide the units + final ObjectProduct base = this.getBase().dividedBy(divisor.getBase()); + return valueOf(base, this.getConversionFactor() / divisor.getConversionFactor()); + } + + @Override + public boolean equals(final Object obj) { + if (!(obj instanceof LinearUnit)) + return false; + final LinearUnit other = (LinearUnit) obj; + return Objects.equals(this.getBase(), other.getBase()) + && DecimalComparison.equals(this.getConversionFactor(), other.getConversionFactor()); + } + + /** + * @return conversion factor + * @since 2019-10-16 + */ + public double getConversionFactor() { + return this.conversionFactor; + } + + @Override + public int hashCode() { + return Objects.hash(this.getBase(), 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. + *

+ * 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. + *

+ * + * @param subtrahend + * unit to subtract + * @return difference of units + * @throws IllegalArgumentException + * if {@code subtrahend} is not compatible for subtraction as described above + * @throws NullPointerException + * if {@code subtrahend} is null + * @since 2019-03-17 + * @since v0.2.0 + */ + public LinearUnit minus(final LinearUnit subtrahendend) { + Objects.requireNonNull(subtrahendend, "addend must not be null."); + + // reject subtrahends that cannot be added to this unit + if (!this.getBase().equals(subtrahendend.getBase())) + throw new IllegalArgumentException( + String.format("Incompatible units for subtraction \"%s\" and \"%s\".", this, subtrahendend)); + + // add the units + return valueOf(this.getBase(), this.getConversionFactor() - subtrahendend.getConversionFactor()); + } + + /** + * Returns the sum of this unit and another. + *

+ * 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. + *

+ * + * @param addend + * unit to add + * @return sum of units + * @throws IllegalArgumentException + * if {@code addend} is not compatible for addition as described above + * @throws NullPointerException + * if {@code addend} is null + * @since 2019-03-17 + * @since v0.2.0 + */ + public LinearUnit plus(final LinearUnit addend) { + Objects.requireNonNull(addend, "addend must not be null."); + + // reject addends that cannot be added to this unit + if (!this.getBase().equals(addend.getBase())) + throw new IllegalArgumentException( + String.format("Incompatible units for addition \"%s\" and \"%s\".", this, addend)); + + // add the units + return valueOf(this.getBase(), this.getConversionFactor() + addend.getConversionFactor()); + } + + /** + * Multiplies this unit by a scalar. + * + * @param multiplier + * scalar to multiply by + * @return product + * @since 2018-12-23 + * @since v0.1.0 + */ + public LinearUnit times(final double multiplier) { + return valueOf(this.getBase(), this.getConversionFactor() * multiplier); + } + + /** + * Returns the product of this unit and another. + * + * @param multiplier + * unit to multiply by + * @return product of two units + * @throws NullPointerException + * if {@code multiplier} is null + * @since 2018-12-22 + * @since v0.1.0 + */ + public LinearUnit times(final LinearUnit multiplier) { + Objects.requireNonNull(multiplier, "other must not be null"); + + // multiply the units + final ObjectProduct base = this.getBase().times(multiplier.getBase()); + return valueOf(base, this.getConversionFactor() * multiplier.getConversionFactor()); + } + + /** + * Returns this unit but to an exponent. + * + * @param exponent + * exponent to exponentiate unit to + * @return exponentiated unit + * @since 2019-01-15 + * @since v0.1.0 + */ + public LinearUnit toExponent(final int exponent) { + return valueOf(this.getBase().toExponent(exponent), Math.pow(this.conversionFactor, exponent)); + } + + // returns a definition of the unit + @Override + public String toString() { + return Double.toString(this.conversionFactor) + " * " + this.getBase().toString(BaseUnit::getSymbol); + } + +} diff --git a/src/org/unitConverter/newUnits/Unit.java b/src/org/unitConverter/newUnits/Unit.java index 339afde..b50115d 100644 --- a/src/org/unitConverter/newUnits/Unit.java +++ b/src/org/unitConverter/newUnits/Unit.java @@ -23,6 +23,8 @@ import org.unitConverter.dimension.BaseDimension; import org.unitConverter.math.ObjectProduct; /** + * A unit described in terms of base units. + * * @author Adrien Hopkins * @since 2019-10-16 */ -- cgit v1.2.3 From 9eba2e72ecba1f33c73d358eb509f0a0816aa810 Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Wed, 16 Oct 2019 14:41:44 -0400 Subject: Added unit prefixes. --- src/org/unitConverter/math/DecimalComparison.java | 67 +++++++++++ src/org/unitConverter/newUnits/LinearUnit.java | 29 ++++- src/org/unitConverter/newUnits/UnitPrefix.java | 134 ++++++++++++++++++++++ 3 files changed, 229 insertions(+), 1 deletion(-) create mode 100644 src/org/unitConverter/newUnits/UnitPrefix.java 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}. + *

+ * WARNING: 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. + *

+ * If this does become a concern, some ways to solve this problem: + *

    + *
  1. 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) + *
  2. Use {@link BigDecimal} instead of {@code double} (this will make a violation of transitivity 100% impossible) + *
* * @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. * + *

+ * WARNING: 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. + *

+ * If this does become a concern, some ways to solve this problem: + *

    + *
  1. Raise the value of epsilon (this does not make a violation of transitivity impossible, it just significantly + * reduces the chances of it happening) + *
  2. Use {@link BigDecimal} instead of {@code double} (this will make a violation of transitivity 100% impossible) + *
+ * * @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}. * + *

+ * WARNING: 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. + *

+ * If this does become a concern, some ways to solve this problem: + *

    + *
  1. 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) + *
  2. Use {@link BigDecimal} instead of {@code float} (this will make a violation of transitivity 100% impossible) + *
+ * * @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. * + *

+ * WARNING: 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. + *

+ * If this does become a concern, some ways to solve this problem: + *

    + *
  1. Raise the value of epsilon (this does not make a violation of transitivity impossible, it just significantly + * reduces the chances of it happening) + *
  2. Use {@link BigDecimal} instead of {@code float} (this will make a violation of transitivity 100% impossible) + *
+ * * @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/newUnits/LinearUnit.java b/src/org/unitConverter/newUnits/LinearUnit.java index 5a589b7..7600dad 100644 --- a/src/org/unitConverter/newUnits/LinearUnit.java +++ b/src/org/unitConverter/newUnits/LinearUnit.java @@ -28,6 +28,21 @@ import org.unitConverter.math.ObjectProduct; * @since 2019-10-16 */ public final class LinearUnit extends AbstractUnit { + /** + * Gets a {@code LinearUnit} from a unit and a value. For example, converts '59 °F' to a linear unit with the value + * of '288.15 K' + * + * @param unit + * unit to convert + * @param value + * value to convert + * @return value expressed as a {@code LinearUnit} + * @since 2019-10-16 + */ + public static LinearUnit fromValue(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}. @@ -129,7 +144,7 @@ public final class LinearUnit extends AbstractUnit { @Override public int hashCode() { - return Objects.hash(this.getBase(), this.getConversionFactor()); + return 31 * this.getBase().hashCode() + DecimalComparison.hash(this.getConversionFactor()); } /** @@ -258,4 +273,16 @@ public final class LinearUnit extends AbstractUnit { return Double.toString(this.conversionFactor) + " * " + this.getBase().toString(BaseUnit::getSymbol); } + /** + * Returns the result of applying {@code prefix} to this unit. + * + * @param prefix + * prefix to apply + * @return unit with prefix + * @since 2019-03-18 + * @since v0.2.0 + */ + public LinearUnit withPrefix(final UnitPrefix prefix) { + return this.times(prefix.getMultiplier()); + } } diff --git a/src/org/unitConverter/newUnits/UnitPrefix.java b/src/org/unitConverter/newUnits/UnitPrefix.java new file mode 100644 index 0000000..905ca19 --- /dev/null +++ b/src/org/unitConverter/newUnits/UnitPrefix.java @@ -0,0 +1,134 @@ +/** + * 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 . + */ +package org.unitConverter.newUnits; + +import org.unitConverter.math.DecimalComparison; + +/** + * A prefix that can be applied to a {@code LinearUnit} to multiply it by some value + * + * @author Adrien Hopkins + * @since 2019-10-16 + */ +public final class UnitPrefix { + /** + * 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 new UnitPrefix(this.getMultiplier() / divisor); + } + + /** + * Divides this prefix by {@code other}. + * + * @param other + * prefix to divide by + * @return quotient of prefixes + * @since 2019-04-13 + * @since v0.2.0 + */ + public UnitPrefix dividedBy(final UnitPrefix other) { + return new UnitPrefix(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()); + } + + /** + * Multiplies this prefix by a scalar + * + * @param multiplicand + * number to multiply by + * @return product of prefix and scalar + * @since 2019-10-16 + */ + public UnitPrefix times(final double multiplicand) { + return new UnitPrefix(this.getMultiplier() * multiplicand); + } + + /** + * Multiplies this prefix by {@code other}. + * + * @param other + * prefix to multiply by + * @return product of prefixes + * @since 2019-04-13 + * @since v0.2.0 + */ + public UnitPrefix times(final UnitPrefix other) { + return new UnitPrefix(this.getMultiplier() * other.getMultiplier()); + } + + /** + * Raises this prefix to an exponent. + * + * @param exponent + * exponent to raise to + * @return result of exponentiation. + * @since 2019-04-13 + * @since v0.2.0 + */ + public UnitPrefix toExponent(final double exponent) { + return new UnitPrefix(Math.pow(this.getMultiplier(), exponent)); + } + + @Override + public String toString() { + return String.format("Unit prefix equal to %s", this.multiplier); + } +} -- cgit v1.2.3 From abe715a30844537693ae186308adcab62c66f121 Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Wed, 16 Oct 2019 15:35:30 -0400 Subject: Made Unit an abstract class. The abstract unit conversion methods are now protected. --- src/org/unitConverter/newUnits/AbstractUnit.java | 81 -------------------- src/org/unitConverter/newUnits/BaseUnit.java | 14 +--- src/org/unitConverter/newUnits/FunctionalUnit.java | 2 +- src/org/unitConverter/newUnits/LinearUnit.java | 8 +- src/org/unitConverter/newUnits/Unit.java | 88 ++++++++++++++++++---- 5 files changed, 79 insertions(+), 114 deletions(-) delete mode 100644 src/org/unitConverter/newUnits/AbstractUnit.java diff --git a/src/org/unitConverter/newUnits/AbstractUnit.java b/src/org/unitConverter/newUnits/AbstractUnit.java deleted file mode 100644 index bc4608e..0000000 --- a/src/org/unitConverter/newUnits/AbstractUnit.java +++ /dev/null @@ -1,81 +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 . - */ -package org.unitConverter.newUnits; - -import java.util.HashMap; -import java.util.Map; -import java.util.Objects; - -import org.unitConverter.dimension.BaseDimension; -import org.unitConverter.math.ObjectProduct; - -/** - * @author Adrien Hopkins - * @since 2019-10-16 - */ -public abstract class AbstractUnit implements Unit { - /** - * The combination of units that this unit is based on. - */ - private final ObjectProduct unitBase; - - /** - * Cache storing the result of getDimension() - */ - private transient ObjectProduct dimension = null; - - /** - * Creates the {@code AbstractUnit}. - * - * @param unitBase - * @since 2019-10-16 - * @throws NullPointerException - * if unitBase is null - */ - public AbstractUnit(final ObjectProduct unitBase) { - this.unitBase = Objects.requireNonNull(unitBase, "unitBase must not be null."); - } - - /** - * @return unitBase - * @since 2019-10-16 - */ - @Override - public final ObjectProduct getBase() { - return this.unitBase; - } - - @Override - public final ObjectProduct getDimension() { - if (this.dimension == null) { - final Map mapping = this.unitBase.exponentMap(); - final Map dimensionMap = new HashMap<>(); - - 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/newUnits/BaseUnit.java b/src/org/unitConverter/newUnits/BaseUnit.java index b7577ff..2c4d748 100644 --- a/src/org/unitConverter/newUnits/BaseUnit.java +++ b/src/org/unitConverter/newUnits/BaseUnit.java @@ -19,7 +19,6 @@ package org.unitConverter.newUnits; import java.util.Objects; import org.unitConverter.dimension.BaseDimension; -import org.unitConverter.math.ObjectProduct; /** * A unit that other units are defined by. @@ -27,7 +26,7 @@ import org.unitConverter.math.ObjectProduct; * @author Adrien Hopkins * @since 2019-10-16 */ -public final class BaseUnit implements Unit { +public final class BaseUnit extends Unit { private final BaseDimension dimension; private final String name; private final String symbol; @@ -46,6 +45,7 @@ public final class BaseUnit implements Unit { * @since 2019-10-16 */ 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."); @@ -72,11 +72,6 @@ public final class BaseUnit implements Unit { return value; } - @Override - public ObjectProduct getBase() { - return ObjectProduct.oneOf(this); - } - /** * @return dimension * @since 2019-10-16 @@ -85,11 +80,6 @@ public final class BaseUnit implements Unit { return this.dimension; } - @Override - public ObjectProduct getDimension() { - return ObjectProduct.oneOf(this.getBaseDimension()); - } - /** * @return name * @since 2019-10-16 diff --git a/src/org/unitConverter/newUnits/FunctionalUnit.java b/src/org/unitConverter/newUnits/FunctionalUnit.java index 99ff833..6bff3e8 100644 --- a/src/org/unitConverter/newUnits/FunctionalUnit.java +++ b/src/org/unitConverter/newUnits/FunctionalUnit.java @@ -27,7 +27,7 @@ import org.unitConverter.math.ObjectProduct; * @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. * diff --git a/src/org/unitConverter/newUnits/LinearUnit.java b/src/org/unitConverter/newUnits/LinearUnit.java index 7600dad..c8c610e 100644 --- a/src/org/unitConverter/newUnits/LinearUnit.java +++ b/src/org/unitConverter/newUnits/LinearUnit.java @@ -27,7 +27,7 @@ import org.unitConverter.math.ObjectProduct; * @author Adrien Hopkins * @since 2019-10-16 */ -public final class LinearUnit extends AbstractUnit { +public final class LinearUnit extends Unit { /** * Gets a {@code LinearUnit} from a unit and a value. For example, converts '59 °F' to a linear unit with the value * of '288.15 K' @@ -39,7 +39,7 @@ public final class LinearUnit extends AbstractUnit { * @return value expressed as a {@code LinearUnit} * @since 2019-10-16 */ - public static LinearUnit fromValue(final Unit unit, final double value) { + public static LinearUnit fromUnitValue(final Unit unit, final double value) { return new LinearUnit(unit.getBase(), unit.convertToBase(value)); } @@ -84,12 +84,12 @@ public final class LinearUnit extends AbstractUnit { } @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(); } diff --git a/src/org/unitConverter/newUnits/Unit.java b/src/org/unitConverter/newUnits/Unit.java index b50115d..feeb25e 100644 --- a/src/org/unitConverter/newUnits/Unit.java +++ b/src/org/unitConverter/newUnits/Unit.java @@ -19,16 +19,15 @@ package org.unitConverter.newUnits; import java.util.Objects; import java.util.function.DoubleUnaryOperator; -import org.unitConverter.dimension.BaseDimension; import org.unitConverter.math.ObjectProduct; /** - * A unit described in terms of base units. + * A unit that is composed of base units. * * @author Adrien Hopkins * @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. * @@ -56,6 +55,44 @@ public interface Unit { return FunctionalUnit.valueOf(base, converterFrom, converterTo); } + /** + * The combination of units that this unit is based on. + * + * @since 2019-10-16 + */ + private final ObjectProduct unitBase; + + // /** + // * Cache storing the result of getDimension() + // * + // * @since 2019-10-16 + // */ + // private transient ObjectProduct 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 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} * @@ -67,7 +104,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()); } @@ -88,7 +125,7 @@ public interface 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}. @@ -101,11 +138,11 @@ public interface Unit { * @since 2019-05-22 * @throws IllegalArgumentException * if {@code other} is incompatible for conversion with this unit (as tested by - * {@link Unit#canConvertTo}). + * {@link IUnit#canConvertTo}). * @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)); @@ -129,19 +166,38 @@ public interface Unit { * @since 2018-12-22 * @since v0.1.0 */ - double convertToBase(double value); + protected abstract double convertToBase(double value); /** * @return combination of units that this unit is based on * @since 2018-12-22 * @since v0.1.0 */ - ObjectProduct getBase(); + public final ObjectProduct getBase() { + return this.unitBase; + } - /** - * @return dimension measured by this unit - * @since 2018-12-22 - * @since v0.1.0 - */ - ObjectProduct getDimension(); -} \ No newline at end of file + // /** + // * @return dimension measured by this unit + // * @since 2018-12-22 + // * @since v0.1.0 + // */ + // private final ObjectProduct getDimension() { + // if (this.dimension == null) { + // final Map mapping = this.unitBase.exponentMap(); + // final Map dimensionMap = new HashMap<>(); + // + // 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(); + } +} -- cgit v1.2.3 From df06497dc4d7359de006c5885074f3356dbb81de Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Wed, 16 Oct 2019 16:50:04 -0400 Subject: Added new constructors for BaseUnit and BaseDimension. --- src/org/unitConverter/newUnits/BaseUnit.java | 18 ++++++++-- src/org/unitConverter/newUnits/Unit.java | 50 +++++++++++++------------- src/org/unitConverter/newUnits/UnitPrefix.java | 22 +++++++++--- 3 files changed, 59 insertions(+), 31 deletions(-) diff --git a/src/org/unitConverter/newUnits/BaseUnit.java b/src/org/unitConverter/newUnits/BaseUnit.java index 2c4d748..6a57faa 100644 --- a/src/org/unitConverter/newUnits/BaseUnit.java +++ b/src/org/unitConverter/newUnits/BaseUnit.java @@ -18,8 +18,6 @@ package org.unitConverter.newUnits; import java.util.Objects; -import org.unitConverter.dimension.BaseDimension; - /** * A unit that other units are defined by. * @@ -27,6 +25,22 @@ import org.unitConverter.dimension.BaseDimension; * @since 2019-10-16 */ public final class BaseUnit extends Unit { + /** + * Gets a base unit from the dimension it measures, its name and its symbol. + * + * @param dimension + * dimension measured by this unit + * @param name + * name of unit + * @param symbol + * symbol of unit + * @return base unit + * @since 2019-10-16 + */ + public static BaseUnit valueOf(final BaseDimension dimension, final String name, final String symbol) { + return new BaseUnit(dimension, name, symbol); + } + private final BaseDimension dimension; private final String name; private final String symbol; diff --git a/src/org/unitConverter/newUnits/Unit.java b/src/org/unitConverter/newUnits/Unit.java index feeb25e..339ab95 100644 --- a/src/org/unitConverter/newUnits/Unit.java +++ b/src/org/unitConverter/newUnits/Unit.java @@ -16,6 +16,8 @@ */ package org.unitConverter.newUnits; +import java.util.HashMap; +import java.util.Map; import java.util.Objects; import java.util.function.DoubleUnaryOperator; @@ -62,12 +64,12 @@ public abstract class Unit { */ private final ObjectProduct unitBase; - // /** - // * Cache storing the result of getDimension() - // * - // * @since 2019-10-16 - // */ - // private transient ObjectProduct dimension = null; + /** + * Cache storing the result of getDimension() + * + * @since 2019-10-16 + */ + private transient ObjectProduct dimension = null; /** * A constructor that constructs {@code BaseUnit} instances. @@ -177,24 +179,24 @@ public abstract class Unit { return this.unitBase; } - // /** - // * @return dimension measured by this unit - // * @since 2018-12-22 - // * @since v0.1.0 - // */ - // private final ObjectProduct getDimension() { - // if (this.dimension == null) { - // final Map mapping = this.unitBase.exponentMap(); - // final Map dimensionMap = new HashMap<>(); - // - // for (final BaseUnit key : mapping.keySet()) { - // dimensionMap.put(key.getBaseDimension(), mapping.get(key)); - // } - // - // this.dimension = ObjectProduct.fromExponentMapping(dimensionMap); - // } - // return this.dimension; - // } + /** + * @return dimension measured by this unit + * @since 2018-12-22 + * @since v0.1.0 + */ + public final ObjectProduct getDimension() { + if (this.dimension == null) { + final Map mapping = this.unitBase.exponentMap(); + final Map dimensionMap = new HashMap<>(); + + 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() { diff --git a/src/org/unitConverter/newUnits/UnitPrefix.java b/src/org/unitConverter/newUnits/UnitPrefix.java index 905ca19..5608098 100644 --- a/src/org/unitConverter/newUnits/UnitPrefix.java +++ b/src/org/unitConverter/newUnits/UnitPrefix.java @@ -25,6 +25,18 @@ import org.unitConverter.math.DecimalComparison; * @since 2019-10-16 */ public final class UnitPrefix { + /** + * Gets a {@code UnitPrefix} from a multiplier + * + * @param multiplier + * multiplier of prefix + * @return prefix + * @since 2019-10-16 + */ + public static UnitPrefix valueOf(final double multiplier) { + return new UnitPrefix(multiplier); + } + /** * The number that this prefix multiplies units by * @@ -52,7 +64,7 @@ public final class UnitPrefix { * @since 2019-10-16 */ public UnitPrefix dividedBy(final double divisor) { - return new UnitPrefix(this.getMultiplier() / divisor); + return valueOf(this.getMultiplier() / divisor); } /** @@ -65,7 +77,7 @@ public final class UnitPrefix { * @since v0.2.0 */ public UnitPrefix dividedBy(final UnitPrefix other) { - return new UnitPrefix(this.getMultiplier() / other.getMultiplier()); + return valueOf(this.getMultiplier() / other.getMultiplier()); } @Override @@ -98,7 +110,7 @@ public final class UnitPrefix { * @since 2019-10-16 */ public UnitPrefix times(final double multiplicand) { - return new UnitPrefix(this.getMultiplier() * multiplicand); + return valueOf(this.getMultiplier() * multiplicand); } /** @@ -111,7 +123,7 @@ public final class UnitPrefix { * @since v0.2.0 */ public UnitPrefix times(final UnitPrefix other) { - return new UnitPrefix(this.getMultiplier() * other.getMultiplier()); + return valueOf(this.getMultiplier() * other.getMultiplier()); } /** @@ -124,7 +136,7 @@ public final class UnitPrefix { * @since v0.2.0 */ public UnitPrefix toExponent(final double exponent) { - return new UnitPrefix(Math.pow(this.getMultiplier(), exponent)); + return valueOf(Math.pow(this.getMultiplier(), exponent)); } @Override -- cgit v1.2.3 From 2ff6c4ea25beeab58239ddf576fb89254ba98630 Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Wed, 16 Oct 2019 17:17:41 -0400 Subject: Added an 'SI' class with all units, dimensions and prefixes in SI. --- src/org/unitConverter/math/StandardDimensions.java | 86 -------- src/org/unitConverter/newUnits/BaseDimension.java | 81 ++++++++ src/org/unitConverter/newUnits/SI.java | 228 +++++++++++++++++++++ 3 files changed, 309 insertions(+), 86 deletions(-) delete mode 100644 src/org/unitConverter/math/StandardDimensions.java create mode 100644 src/org/unitConverter/newUnits/BaseDimension.java create mode 100644 src/org/unitConverter/newUnits/SI.java diff --git a/src/org/unitConverter/math/StandardDimensions.java b/src/org/unitConverter/math/StandardDimensions.java deleted file mode 100644 index db5efc3..0000000 --- a/src/org/unitConverter/math/StandardDimensions.java +++ /dev/null @@ -1,86 +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 . - */ -package org.unitConverter.math; - -import org.unitConverter.dimension.BaseDimension; -import org.unitConverter.dimension.OtherBaseDimension; -import org.unitConverter.dimension.SIBaseDimension; - -/** - * All of the dimensions that are used by the SI. (Test data for the ObjectProductTest) - * - * @author Adrien Hopkins - * @since 2018-12-11 - * @since v0.1.0 - */ -final class StandardDimensions { - // base dimensions - public static final ObjectProduct EMPTY = ObjectProduct.empty(); - public static final ObjectProduct LENGTH = ObjectProduct.oneOf(SIBaseDimension.LENGTH); - public static final ObjectProduct MASS = ObjectProduct.oneOf(SIBaseDimension.MASS); - public static final ObjectProduct TIME = ObjectProduct.oneOf(SIBaseDimension.TIME); - public static final ObjectProduct ELECTRIC_CURRENT = ObjectProduct - .oneOf(SIBaseDimension.ELECTRIC_CURRENT); - public static final ObjectProduct TEMPERATURE = ObjectProduct.oneOf(SIBaseDimension.TEMPERATURE); - public static final ObjectProduct QUANTITY = ObjectProduct.oneOf(SIBaseDimension.QUANTITY); - public static final ObjectProduct LUMINOUS_INTENSITY = ObjectProduct - .oneOf(SIBaseDimension.LUMINOUS_INTENSITY); - public static final ObjectProduct INFORMATION = ObjectProduct.oneOf(OtherBaseDimension.INFORMATION); - public static final ObjectProduct CURRENCY = ObjectProduct.oneOf(OtherBaseDimension.CURRENCY); - // derived dimensions without named SI units - public static final ObjectProduct AREA = LENGTH.times(LENGTH); - - public static final ObjectProduct VOLUME = AREA.times(LENGTH); - public static final ObjectProduct VELOCITY = LENGTH.dividedBy(TIME); - public static final ObjectProduct ACCELERATION = VELOCITY.dividedBy(TIME); - public static final ObjectProduct WAVENUMBER = EMPTY.dividedBy(LENGTH); - public static final ObjectProduct MASS_DENSITY = MASS.dividedBy(VOLUME); - public static final ObjectProduct SURFACE_DENSITY = MASS.dividedBy(AREA); - public static final ObjectProduct SPECIFIC_VOLUME = VOLUME.dividedBy(MASS); - public static final ObjectProduct CURRENT_DENSITY = ELECTRIC_CURRENT.dividedBy(AREA); - public static final ObjectProduct MAGNETIC_FIELD_STRENGTH = ELECTRIC_CURRENT.dividedBy(LENGTH); - public static final ObjectProduct CONCENTRATION = QUANTITY.dividedBy(VOLUME); - public static final ObjectProduct MASS_CONCENTRATION = CONCENTRATION.times(MASS); - public static final ObjectProduct LUMINANCE = LUMINOUS_INTENSITY.dividedBy(AREA); - public static final ObjectProduct REFRACTIVE_INDEX = VELOCITY.dividedBy(VELOCITY); - public static final ObjectProduct REFLACTIVE_PERMEABILITY = EMPTY.times(EMPTY); - public static final ObjectProduct ANGLE = LENGTH.dividedBy(LENGTH); - public static final ObjectProduct SOLID_ANGLE = AREA.dividedBy(AREA); - // derived dimensions with named SI units - public static final ObjectProduct FREQUENCY = EMPTY.dividedBy(TIME); - - public static final ObjectProduct FORCE = MASS.times(ACCELERATION); - public static final ObjectProduct ENERGY = FORCE.times(LENGTH); - public static final ObjectProduct POWER = ENERGY.dividedBy(TIME); - public static final ObjectProduct ELECTRIC_CHARGE = ELECTRIC_CURRENT.times(TIME); - public static final ObjectProduct VOLTAGE = ENERGY.dividedBy(ELECTRIC_CHARGE); - public static final ObjectProduct CAPACITANCE = ELECTRIC_CHARGE.dividedBy(VOLTAGE); - public static final ObjectProduct ELECTRIC_RESISTANCE = VOLTAGE.dividedBy(ELECTRIC_CURRENT); - public static final ObjectProduct ELECTRIC_CONDUCTANCE = ELECTRIC_CURRENT.dividedBy(VOLTAGE); - public static final ObjectProduct MAGNETIC_FLUX = VOLTAGE.times(TIME); - public static final ObjectProduct MAGNETIC_FLUX_DENSITY = MAGNETIC_FLUX.dividedBy(AREA); - public static final ObjectProduct INDUCTANCE = MAGNETIC_FLUX.dividedBy(ELECTRIC_CURRENT); - public static final ObjectProduct LUMINOUS_FLUX = LUMINOUS_INTENSITY.times(SOLID_ANGLE); - public static final ObjectProduct ILLUMINANCE = LUMINOUS_FLUX.dividedBy(AREA); - public static final ObjectProduct SPECIFIC_ENERGY = ENERGY.dividedBy(MASS); - public static final ObjectProduct CATALYTIC_ACTIVITY = QUANTITY.dividedBy(TIME); - - // You may NOT get StandardDimensions instances! - private StandardDimensions() { - throw new AssertionError(); - } -} diff --git a/src/org/unitConverter/newUnits/BaseDimension.java b/src/org/unitConverter/newUnits/BaseDimension.java new file mode 100644 index 0000000..a1cde46 --- /dev/null +++ b/src/org/unitConverter/newUnits/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 . + */ +package org.unitConverter.newUnits; + +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/newUnits/SI.java b/src/org/unitConverter/newUnits/SI.java new file mode 100644 index 0000000..b7a117a --- /dev/null +++ b/src/org/unitConverter/newUnits/SI.java @@ -0,0 +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 . + */ +package org.unitConverter.newUnits; + +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. + * + *

+ * This class does not include prefixed units. To obtain prefixed units, use {@link LinearUnit#withPrefix}: + * + *

+ * LinearUnit KILOMETRE = SI.METRE.withPrefix(SI.KILO);
+ * 
+ * + * + * @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 EMPTY = ObjectProduct.empty(); + public static final ObjectProduct LENGTH = ObjectProduct.oneOf(BaseDimensions.LENGTH); + public static final ObjectProduct MASS = ObjectProduct.oneOf(BaseDimensions.MASS); + public static final ObjectProduct TIME = ObjectProduct.oneOf(BaseDimensions.TIME); + public static final ObjectProduct ELECTRIC_CURRENT = ObjectProduct + .oneOf(BaseDimensions.ELECTRIC_CURRENT); + public static final ObjectProduct TEMPERATURE = ObjectProduct.oneOf(BaseDimensions.TEMPERATURE); + public static final ObjectProduct QUANTITY = ObjectProduct.oneOf(BaseDimensions.QUANTITY); + public static final ObjectProduct LUMINOUS_INTENSITY = ObjectProduct + .oneOf(BaseDimensions.LUMINOUS_INTENSITY); + public static final ObjectProduct INFORMATION = ObjectProduct.oneOf(BaseDimensions.INFORMATION); + public static final ObjectProduct CURRENCY = ObjectProduct.oneOf(BaseDimensions.CURRENCY); + // derived dimensions without named SI units + public static final ObjectProduct AREA = LENGTH.times(LENGTH); + + public static final ObjectProduct VOLUME = AREA.times(LENGTH); + public static final ObjectProduct VELOCITY = LENGTH.dividedBy(TIME); + public static final ObjectProduct ACCELERATION = VELOCITY.dividedBy(TIME); + public static final ObjectProduct WAVENUMBER = EMPTY.dividedBy(LENGTH); + public static final ObjectProduct MASS_DENSITY = MASS.dividedBy(VOLUME); + public static final ObjectProduct SURFACE_DENSITY = MASS.dividedBy(AREA); + public static final ObjectProduct SPECIFIC_VOLUME = VOLUME.dividedBy(MASS); + public static final ObjectProduct CURRENT_DENSITY = ELECTRIC_CURRENT.dividedBy(AREA); + public static final ObjectProduct MAGNETIC_FIELD_STRENGTH = ELECTRIC_CURRENT.dividedBy(LENGTH); + public static final ObjectProduct CONCENTRATION = QUANTITY.dividedBy(VOLUME); + public static final ObjectProduct MASS_CONCENTRATION = CONCENTRATION.times(MASS); + public static final ObjectProduct LUMINANCE = LUMINOUS_INTENSITY.dividedBy(AREA); + public static final ObjectProduct REFRACTIVE_INDEX = VELOCITY.dividedBy(VELOCITY); + public static final ObjectProduct REFLACTIVE_PERMEABILITY = EMPTY.times(EMPTY); + public static final ObjectProduct ANGLE = LENGTH.dividedBy(LENGTH); + public static final ObjectProduct SOLID_ANGLE = AREA.dividedBy(AREA); + // derived dimensions with named SI units + public static final ObjectProduct FREQUENCY = EMPTY.dividedBy(TIME); + + public static final ObjectProduct FORCE = MASS.times(ACCELERATION); + public static final ObjectProduct ENERGY = FORCE.times(LENGTH); + public static final ObjectProduct POWER = ENERGY.dividedBy(TIME); + public static final ObjectProduct ELECTRIC_CHARGE = ELECTRIC_CURRENT.times(TIME); + public static final ObjectProduct VOLTAGE = ENERGY.dividedBy(ELECTRIC_CHARGE); + public static final ObjectProduct CAPACITANCE = ELECTRIC_CHARGE.dividedBy(VOLTAGE); + public static final ObjectProduct ELECTRIC_RESISTANCE = VOLTAGE.dividedBy(ELECTRIC_CURRENT); + public static final ObjectProduct ELECTRIC_CONDUCTANCE = ELECTRIC_CURRENT.dividedBy(VOLTAGE); + public static final ObjectProduct MAGNETIC_FLUX = VOLTAGE.times(TIME); + public static final ObjectProduct MAGNETIC_FLUX_DENSITY = MAGNETIC_FLUX.dividedBy(AREA); + public static final ObjectProduct INDUCTANCE = MAGNETIC_FLUX.dividedBy(ELECTRIC_CURRENT); + public static final ObjectProduct LUMINOUS_FLUX = LUMINOUS_INTENSITY.times(SOLID_ANGLE); + public static final ObjectProduct ILLUMINANCE = LUMINOUS_FLUX.dividedBy(AREA); + public static final ObjectProduct SPECIFIC_ENERGY = ENERGY.dividedBy(MASS); + public static final ObjectProduct 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(); + } +} -- cgit v1.2.3 From f309ef0b9ed24629146d1d92a5c869946a6d65a2 Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Thu, 17 Oct 2019 14:10:35 -0400 Subject: Added a test for the new Unit classes --- src/org/unitConverter/newUnits/UnitTest.java | 102 +++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 src/org/unitConverter/newUnits/UnitTest.java diff --git a/src/org/unitConverter/newUnits/UnitTest.java b/src/org/unitConverter/newUnits/UnitTest.java new file mode 100644 index 0000000..33bd264 --- /dev/null +++ b/src/org/unitConverter/newUnits/UnitTest.java @@ -0,0 +1,102 @@ +/** + * 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 . + */ +package org.unitConverter.newUnits; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Random; +import java.util.concurrent.ThreadLocalRandom; + +import org.junit.jupiter.api.Test; +import org.unitConverter.math.DecimalComparison; + +/** + * Testing the various Unit classes. This is NOT part of this program's public API. + * + * @author Adrien Hopkins + * @since 2018-12-22 + * @since v0.1.0 + */ +class UnitTest { + /** A random number generator */ + private static final Random rng = ThreadLocalRandom.current(); + + @Test + public void testAdditionAndSubtraction() { + final LinearUnit inch = SI.METRE.times(0.0254); + final LinearUnit foot = SI.METRE.times(0.3048); + + assertEquals(inch.plus(foot), SI.METRE.times(0.3302)); + assertEquals(foot.minus(inch), SI.METRE.times(0.2794)); + } + + @Test + public void testConversion() { + final LinearUnit metre = SI.METRE; + final Unit inch = metre.times(0.0254); + + assertEquals(1.9, inch.convertTo(metre, 75), 0.01); + + // try random stuff + for (int i = 0; i < 1000; i++) { + // initiate random values + final double conversionFactor = rng.nextDouble() * 1000000; + final double testValue = rng.nextDouble() * 1000000; + final double expected = testValue * conversionFactor; + + // test + final Unit unit = SI.METRE.times(conversionFactor); + final double actual = unit.convertToBase(testValue); + + assertEquals(actual, expected, expected * DecimalComparison.DOUBLE_EPSILON); + } + } + + @Test + public void testEquals() { + final LinearUnit metre = SI.METRE; + final Unit meter = SI.BaseUnits.METRE.asLinearUnit(); + + assertEquals(metre, meter); + } + + @Test + 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.JOULE; + + assertEquals(generatedJoule, actualJoule); + + // test multiplication by conversion factors + final LinearUnit kilometre = SI.METRE.times(1000); + final LinearUnit hour = SI.SECOND.times(3600); + final LinearUnit generatedKPH = kilometre.dividedBy(hour); + + final LinearUnit actualKPH = SI.METRE.dividedBy(SI.SECOND).dividedBy(3.6); + + assertEquals(generatedKPH, actualKPH); + } + + @Test + public void testPrefixes() { + final LinearUnit generatedKilometre = SI.METRE.withPrefix(SI.KILO); + final LinearUnit actualKilometre = SI.METRE.times(1000); + + assertEquals(generatedKilometre, actualKilometre); + } +} -- cgit v1.2.3 From 54ab9c05234b09547e2a01b1eab812420c6a3dda Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Thu, 17 Oct 2019 14:25:17 -0400 Subject: Implemented the new Units system Fahrenheit has temporarily been removed; it will be back. --- src/org/unitConverter/UnitsDatabase.java | 1658 -------------------- src/org/unitConverter/UnitsDatabaseTest.java | 312 ---- .../converterGUI/UnitConverterGUI.java | 35 +- src/org/unitConverter/dimension/BaseDimension.java | 40 - .../dimension/OtherBaseDimension.java | 55 - .../unitConverter/dimension/SIBaseDimension.java | 57 - .../dimension/StandardDimensions.java | 80 - src/org/unitConverter/dimension/UnitDimension.java | 241 --- .../unitConverter/dimension/UnitDimensionTest.java | 77 - src/org/unitConverter/dimension/package-info.java | 24 - src/org/unitConverter/math/ObjectProductTest.java | 23 +- src/org/unitConverter/newUnits/BaseDimension.java | 81 - src/org/unitConverter/newUnits/BaseUnit.java | 117 -- src/org/unitConverter/newUnits/FunctionalUnit.java | 98 -- src/org/unitConverter/newUnits/LinearUnit.java | 288 ---- src/org/unitConverter/newUnits/SI.java | 228 --- src/org/unitConverter/newUnits/Unit.java | 205 --- src/org/unitConverter/newUnits/UnitPrefix.java | 146 -- src/org/unitConverter/newUnits/UnitTest.java | 102 -- src/org/unitConverter/newUnits/package-info.java | 23 - src/org/unitConverter/unit/AbstractUnit.java | 124 -- src/org/unitConverter/unit/BaseDimension.java | 81 + src/org/unitConverter/unit/BaseUnit.java | 163 +- src/org/unitConverter/unit/DefaultUnitPrefix.java | 68 - src/org/unitConverter/unit/FunctionalUnit.java | 8 +- src/org/unitConverter/unit/LinearUnit.java | 166 +- src/org/unitConverter/unit/NonlinearUnits.java | 37 - src/org/unitConverter/unit/SI.java | 302 +++- src/org/unitConverter/unit/SIPrefix.java | 54 - src/org/unitConverter/unit/Unit.java | 105 +- src/org/unitConverter/unit/UnitDatabase.java | 1653 +++++++++++++++++++ src/org/unitConverter/unit/UnitDatabaseTest.java | 307 ++++ src/org/unitConverter/unit/UnitPrefix.java | 104 +- src/org/unitConverter/unit/UnitSystem.java | 53 - src/org/unitConverter/unit/UnitTest.java | 21 +- src/org/unitConverter/unit/package-info.java | 5 +- unitsfile.txt | 4 +- 37 files changed, 2610 insertions(+), 4535 deletions(-) delete mode 100644 src/org/unitConverter/UnitsDatabase.java delete mode 100644 src/org/unitConverter/UnitsDatabaseTest.java delete mode 100644 src/org/unitConverter/dimension/BaseDimension.java delete mode 100644 src/org/unitConverter/dimension/OtherBaseDimension.java delete mode 100644 src/org/unitConverter/dimension/SIBaseDimension.java delete mode 100644 src/org/unitConverter/dimension/StandardDimensions.java delete mode 100644 src/org/unitConverter/dimension/UnitDimension.java delete mode 100644 src/org/unitConverter/dimension/UnitDimensionTest.java delete mode 100644 src/org/unitConverter/dimension/package-info.java delete mode 100644 src/org/unitConverter/newUnits/BaseDimension.java delete mode 100644 src/org/unitConverter/newUnits/BaseUnit.java delete mode 100644 src/org/unitConverter/newUnits/FunctionalUnit.java delete mode 100644 src/org/unitConverter/newUnits/LinearUnit.java delete mode 100644 src/org/unitConverter/newUnits/SI.java delete mode 100644 src/org/unitConverter/newUnits/Unit.java delete mode 100644 src/org/unitConverter/newUnits/UnitPrefix.java delete mode 100644 src/org/unitConverter/newUnits/UnitTest.java delete mode 100644 src/org/unitConverter/newUnits/package-info.java delete mode 100644 src/org/unitConverter/unit/AbstractUnit.java create mode 100644 src/org/unitConverter/unit/BaseDimension.java delete mode 100644 src/org/unitConverter/unit/DefaultUnitPrefix.java delete mode 100644 src/org/unitConverter/unit/NonlinearUnits.java delete mode 100644 src/org/unitConverter/unit/SIPrefix.java create mode 100644 src/org/unitConverter/unit/UnitDatabase.java create mode 100644 src/org/unitConverter/unit/UnitDatabaseTest.java delete mode 100644 src/org/unitConverter/unit/UnitSystem.java diff --git a/src/org/unitConverter/UnitsDatabase.java b/src/org/unitConverter/UnitsDatabase.java deleted file mode 100644 index 520195c..0000000 --- a/src/org/unitConverter/UnitsDatabase.java +++ /dev/null @@ -1,1658 +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 . - */ -package org.unitConverter; - -import java.io.BufferedReader; -import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileReader; -import java.io.IOException; -import java.util.AbstractSet; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.NoSuchElementException; -import java.util.Objects; -import java.util.Set; -import java.util.function.BiFunction; -import java.util.function.Function; -import java.util.function.Predicate; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import org.unitConverter.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; - -/** - * A database of units, prefixes and dimensions, and their names. - * - * @author Adrien Hopkins - * @since 2019-01-07 - * @since v0.1.0 - */ -public final class UnitsDatabase { - /** - * A map for units that allows the use of prefixes. - *

- * As this map implementation is intended to be used as a sort of "augmented view" of a unit and prefix map, it is - * unmodifiable but instead reflects the changes to the maps passed into it. Do not edit this map, instead edit the - * maps that were passed in during construction. - *

- *

- * The rules for applying prefixes onto units are the following: - *

    - *
  • Prefixes can only be applied to linear units.
  • - *
  • Before attempting to search for prefixes in a unit name, this map will first search for a unit name. So, if - * there are two units, "B" and "AB", and a prefix "A", this map will favour the unit "AB" over the unit "B" with - * the prefix "A", even though they have the same string.
  • - *
  • Longer prefixes are preferred to shorter prefixes. So, if you have units "BC" and "C", and prefixes "AB" and - * "A", inputting "ABC" will return the unit "C" with the prefix "AB", not "BC" with the prefix "A".
  • - *
- *

- *

- * This map is infinite in size if there is at least one unit and at least one prefix. If it is infinite, some - * operations that only work with finite collections, like converting name/entry sets to arrays, will throw an - * {@code IllegalStateException}. - *

- *

- * Because of ambiguities between prefixes (i.e. kilokilo = mega), {@link #containsValue} and {@link #values()} - * currently ignore prefixes. - *

- * - * @author Adrien Hopkins - * @since 2019-04-13 - * @since v0.2.0 - */ - private static final class PrefixedUnitMap implements Map { - /** - * The class used for entry sets. - * - *

- * If the map that created this set is infinite in size (has at least one unit and at least one prefix), this - * set is infinite as well. If this set is infinite in size, {@link #toArray} will fail with a - * {@code IllegalStateException} instead of creating an infinite-sized array. - *

- * - * @author Adrien Hopkins - * @since 2019-04-13 - * @since v0.2.0 - */ - private static final class PrefixedUnitEntrySet extends AbstractSet> { - /** - * The entry for this set. - * - * @author Adrien Hopkins - * @since 2019-04-14 - * @since v0.2.0 - */ - private static final class PrefixedUnitEntry implements Entry { - private final String key; - private final Unit value; - - /** - * Creates the {@code PrefixedUnitEntry}. - * - * @param key - * key - * @param value - * value - * @since 2019-04-14 - * @since v0.2.0 - */ - public PrefixedUnitEntry(final String key, final Unit value) { - this.key = key; - this.value = value; - } - - /** - * @since 2019-05-03 - */ - @Override - public boolean equals(final Object o) { - if (!(o instanceof Map.Entry)) - return false; - final Map.Entry other = (Map.Entry) o; - return Objects.equals(this.getKey(), other.getKey()) - && Objects.equals(this.getValue(), other.getValue()); - } - - @Override - public String getKey() { - return this.key; - } - - @Override - public Unit getValue() { - return this.value; - } - - /** - * @since 2019-05-03 - */ - @Override - public int hashCode() { - return (this.getKey() == null ? 0 : this.getKey().hashCode()) - ^ (this.getValue() == null ? 0 : this.getValue().hashCode()); - } - - @Override - public Unit setValue(final Unit value) { - throw new UnsupportedOperationException("Cannot set value in an immutable entry"); - } - - /** - * Returns a string representation of the entry. The format of the string is the string representation - * of the key, then the equals ({@code =}) character, then the string representation of the value. - * - * @since 2019-05-03 - */ - @Override - public String toString() { - return this.getKey() + "=" + this.getValue(); - } - } - - /** - * An iterator that iterates over the units of a {@code PrefixedUnitNameSet}. - * - * @author Adrien Hopkins - * @since 2019-04-14 - * @since v0.2.0 - */ - private static final class PrefixedUnitEntryIterator implements Iterator> { - // position in the unit list - private int unitNamePosition = 0; - // the indices of the prefixes attached to the current unit - private final List prefixCoordinates = new ArrayList<>(); - - // values from the unit entry set - private final Map map; - private transient final List unitNames; - private transient final List prefixNames; - - /** - * Creates the {@code UnitsDatabase.PrefixedUnitMap.PrefixedUnitNameSet.PrefixedUnitNameIterator}. - * - * @since 2019-04-14 - * @since v0.2.0 - */ - public PrefixedUnitEntryIterator(final PrefixedUnitMap map) { - this.map = map; - this.unitNames = new ArrayList<>(map.units.keySet()); - this.prefixNames = new ArrayList<>(map.prefixes.keySet()); - } - - /** - * @return current unit name - * @since 2019-04-14 - * @since v0.2.0 - */ - private String getCurrentUnitName() { - final StringBuilder unitName = new StringBuilder(); - for (final int i : this.prefixCoordinates) { - unitName.append(this.prefixNames.get(i)); - } - unitName.append(this.unitNames.get(this.unitNamePosition)); - - return unitName.toString(); - } - - @Override - public boolean hasNext() { - if (this.unitNames.isEmpty()) - return false; - else { - if (this.prefixNames.isEmpty()) - return this.unitNamePosition >= this.unitNames.size() - 1; - else - return true; - } - } - - /** - * Changes this iterator's position to the next available one. - * - * @since 2019-04-14 - * @since v0.2.0 - */ - private void incrementPosition() { - this.unitNamePosition++; - - if (this.unitNamePosition >= this.unitNames.size()) { - // we have used all of our units, go to a different prefix - this.unitNamePosition = 0; - - // if the prefix coordinates are empty, then set it to [0] - if (this.prefixCoordinates.isEmpty()) { - this.prefixCoordinates.add(0, 0); - } else { - // get the prefix coordinate to increment, then increment - int i = this.prefixCoordinates.size() - 1; - this.prefixCoordinates.set(i, this.prefixCoordinates.get(i) + 1); - - // fix any carrying errors - while (i >= 0 && this.prefixCoordinates.get(i) >= this.prefixNames.size()) { - // carry over - this.prefixCoordinates.set(i--, 0); // null and decrement at the same time - - if (i < 0) { // we need to add a new coordinate - this.prefixCoordinates.add(0, 0); - } else { // increment an existing one - this.prefixCoordinates.set(i, this.prefixCoordinates.get(i) + 1); - } - } - } - } - } - - @Override - public Entry next() { - // get next element - final Entry nextEntry = this.peek(); - - // iterate to next position - this.incrementPosition(); - - return nextEntry; - } - - /** - * @return the next element in the iterator, without iterating over it - * @since 2019-05-03 - */ - private Entry peek() { - if (!this.hasNext()) - throw new NoSuchElementException("No units left!"); - - // if I have prefixes, ensure I'm not using a nonlinear unit - // since all of the unprefixed stuff is done, just remove nonlinear units - if (!this.prefixCoordinates.isEmpty()) { - while (this.unitNamePosition < this.unitNames.size() - && !(this.map.get(this.unitNames.get(this.unitNamePosition)) instanceof LinearUnit)) { - this.unitNames.remove(this.unitNamePosition); - } - } - - final String nextName = this.getCurrentUnitName(); - - return new PrefixedUnitEntry(nextName, this.map.get(nextName)); - } - - /** - * Returns a string representation of the object. The exact details of the representation are - * unspecified and subject to change. - * - * @since 2019-05-03 - */ - @Override - public String toString() { - return String.format("Iterator iterating over name-unit entries; next value is \"%s\"", - this.peek()); - } - } - - // the map that created this set - private final PrefixedUnitMap map; - - /** - * Creates the {@code PrefixedUnitNameSet}. - * - * @param map - * map that created this set - * @since 2019-04-13 - * @since v0.2.0 - */ - public PrefixedUnitEntrySet(final PrefixedUnitMap map) { - this.map = map; - } - - @Override - public boolean add(final Map.Entry e) { - throw new UnsupportedOperationException("Cannot add to an immutable set"); - } - - @Override - public boolean addAll(final Collection> c) { - throw new UnsupportedOperationException("Cannot add to an immutable set"); - } - - @Override - public void clear() { - throw new UnsupportedOperationException("Cannot clear an immutable set"); - } - - @Override - public boolean contains(final Object o) { - // get the entry - final Entry entry; - - try { - // This is OK because I'm in a try-catch block, catching the exact exception that would be thrown. - @SuppressWarnings("unchecked") - final Entry tempEntry = (Entry) o; - entry = tempEntry; - } catch (final ClassCastException e) { - throw new IllegalArgumentException("Attempted to test for an entry using a non-entry."); - } - - return this.map.containsKey(entry.getKey()) && this.map.get(entry.getKey()).equals(entry.getValue()); - } - - @Override - public boolean containsAll(final Collection c) { - for (final Object o : c) - if (!this.contains(o)) - return false; - return true; - } - - @Override - public boolean isEmpty() { - return this.map.isEmpty(); - } - - @Override - public Iterator> iterator() { - return new PrefixedUnitEntryIterator(this.map); - } - - @Override - public boolean remove(final Object o) { - throw new UnsupportedOperationException("Cannot remove from an immutable set"); - } - - @Override - public boolean removeAll(final Collection c) { - throw new UnsupportedOperationException("Cannot remove from an immutable set"); - } - - @Override - public boolean removeIf(final Predicate> filter) { - throw new UnsupportedOperationException("Cannot remove from an immutable set"); - } - - @Override - public boolean retainAll(final Collection c) { - throw new UnsupportedOperationException("Cannot remove from an immutable set"); - } - - @Override - public int size() { - if (this.map.units.isEmpty()) - return 0; - else { - if (this.map.prefixes.isEmpty()) - return this.map.units.size(); - else - // infinite set - return Integer.MAX_VALUE; - } - } - - /** - * @throws IllegalStateException - * if the set is infinite in size - */ - @Override - public Object[] toArray() { - if (this.map.units.isEmpty() || this.map.prefixes.isEmpty()) - return super.toArray(); - else - // infinite set - throw new IllegalStateException("Cannot make an infinite set into an array."); - } - - /** - * @throws IllegalStateException - * if the set is infinite in size - */ - @Override - public T[] toArray(final T[] a) { - if (this.map.units.isEmpty() || this.map.prefixes.isEmpty()) - return super.toArray(a); - else - // infinite set - throw new IllegalStateException("Cannot make an infinite set into an array."); - } - - @Override - public String toString() { - if (this.map.units.isEmpty() || this.map.prefixes.isEmpty()) - return super.toString(); - else - return String.format("Infinite set of name-unit entries created from units %s and prefixes %s", - this.map.units, this.map.prefixes); - } - } - - /** - * The class used for unit name sets. - * - *

- * If the map that created this set is infinite in size (has at least one unit and at least one prefix), this - * set is infinite as well. If this set is infinite in size, {@link #toArray} will fail with a - * {@code IllegalStateException} instead of creating an infinite-sized array. - *

- * - * @author Adrien Hopkins - * @since 2019-04-13 - * @since v0.2.0 - */ - private static final class PrefixedUnitNameSet extends AbstractSet { - /** - * An iterator that iterates over the units of a {@code PrefixedUnitNameSet}. - * - * @author Adrien Hopkins - * @since 2019-04-14 - * @since v0.2.0 - */ - private static final class PrefixedUnitNameIterator implements Iterator { - // position in the unit list - private int unitNamePosition = 0; - // the indices of the prefixes attached to the current unit - private final List prefixCoordinates = new ArrayList<>(); - - // values from the unit name set - private final Map map; - private transient final List unitNames; - private transient final List prefixNames; - - /** - * Creates the {@code UnitsDatabase.PrefixedUnitMap.PrefixedUnitNameSet.PrefixedUnitNameIterator}. - * - * @since 2019-04-14 - * @since v0.2.0 - */ - public PrefixedUnitNameIterator(final PrefixedUnitMap map) { - this.map = map; - this.unitNames = new ArrayList<>(map.units.keySet()); - this.prefixNames = new ArrayList<>(map.prefixes.keySet()); - } - - /** - * @return current unit name - * @since 2019-04-14 - * @since v0.2.0 - */ - private String getCurrentUnitName() { - final StringBuilder unitName = new StringBuilder(); - for (final int i : this.prefixCoordinates) { - unitName.append(this.prefixNames.get(i)); - } - unitName.append(this.unitNames.get(this.unitNamePosition)); - - return unitName.toString(); - } - - @Override - public boolean hasNext() { - if (this.unitNames.isEmpty()) - return false; - else { - if (this.prefixNames.isEmpty()) - return this.unitNamePosition >= this.unitNames.size() - 1; - else - return true; - } - } - - /** - * Changes this iterator's position to the next available one. - * - * @since 2019-04-14 - * @since v0.2.0 - */ - private void incrementPosition() { - this.unitNamePosition++; - - if (this.unitNamePosition >= this.unitNames.size()) { - // we have used all of our units, go to a different prefix - this.unitNamePosition = 0; - - // if the prefix coordinates are empty, then set it to [0] - if (this.prefixCoordinates.isEmpty()) { - this.prefixCoordinates.add(0, 0); - } else { - // get the prefix coordinate to increment, then increment - int i = this.prefixCoordinates.size() - 1; - this.prefixCoordinates.set(i, this.prefixCoordinates.get(i) + 1); - - // fix any carrying errors - while (i >= 0 && this.prefixCoordinates.get(i) >= this.prefixNames.size()) { - // carry over - this.prefixCoordinates.set(i--, 0); // null and decrement at the same time - - if (i < 0) { // we need to add a new coordinate - this.prefixCoordinates.add(0, 0); - } else { // increment an existing one - this.prefixCoordinates.set(i, this.prefixCoordinates.get(i) + 1); - } - } - } - } - } - - @Override - public String next() { - final String nextName = this.peek(); - - this.incrementPosition(); - - return nextName; - } - - /** - * @return the next element in the iterator, without iterating over it - * @since 2019-05-03 - */ - private String peek() { - if (!this.hasNext()) - throw new NoSuchElementException("No units left!"); - // if I have prefixes, ensure I'm not using a nonlinear unit - // since all of the unprefixed stuff is done, just remove nonlinear units - if (!this.prefixCoordinates.isEmpty()) { - while (this.unitNamePosition < this.unitNames.size() - && !(this.map.get(this.unitNames.get(this.unitNamePosition)) instanceof LinearUnit)) { - this.unitNames.remove(this.unitNamePosition); - } - } - - return this.getCurrentUnitName(); - } - - /** - * Returns a string representation of the object. The exact details of the representation are - * unspecified and subject to change. - * - * @since 2019-05-03 - */ - @Override - public String toString() { - return String.format("Iterator iterating over unit names; next value is \"%s\"", this.peek()); - } - } - - // the map that created this set - private final PrefixedUnitMap map; - - /** - * Creates the {@code PrefixedUnitNameSet}. - * - * @param map - * map that created this set - * @since 2019-04-13 - * @since v0.2.0 - */ - public PrefixedUnitNameSet(final PrefixedUnitMap map) { - this.map = map; - } - - @Override - public boolean add(final String e) { - throw new UnsupportedOperationException("Cannot add to an immutable set"); - } - - @Override - public boolean addAll(final Collection c) { - throw new UnsupportedOperationException("Cannot add to an immutable set"); - } - - @Override - public void clear() { - throw new UnsupportedOperationException("Cannot clear an immutable set"); - } - - @Override - public boolean contains(final Object o) { - return this.map.containsKey(o); - } - - @Override - public boolean containsAll(final Collection c) { - for (final Object o : c) - if (!this.contains(o)) - return false; - return true; - } - - @Override - public boolean isEmpty() { - return this.map.isEmpty(); - } - - @Override - public Iterator iterator() { - return new PrefixedUnitNameIterator(this.map); - } - - @Override - public boolean remove(final Object o) { - throw new UnsupportedOperationException("Cannot remove from an immutable set"); - } - - @Override - public boolean removeAll(final Collection c) { - throw new UnsupportedOperationException("Cannot remove from an immutable set"); - } - - @Override - public boolean removeIf(final Predicate filter) { - throw new UnsupportedOperationException("Cannot remove from an immutable set"); - } - - @Override - public boolean retainAll(final Collection c) { - throw new UnsupportedOperationException("Cannot remove from an immutable set"); - } - - @Override - public int size() { - if (this.map.units.isEmpty()) - return 0; - else { - if (this.map.prefixes.isEmpty()) - return this.map.units.size(); - else - // infinite set - return Integer.MAX_VALUE; - } - } - - /** - * @throws IllegalStateException - * if the set is infinite in size - */ - @Override - public Object[] toArray() { - if (this.map.units.isEmpty() || this.map.prefixes.isEmpty()) - return super.toArray(); - else - // infinite set - throw new IllegalStateException("Cannot make an infinite set into an array."); - - } - - /** - * @throws IllegalStateException - * if the set is infinite in size - */ - @Override - public T[] toArray(final T[] a) { - if (this.map.units.isEmpty() || this.map.prefixes.isEmpty()) - return super.toArray(a); - else - // infinite set - throw new IllegalStateException("Cannot make an infinite set into an array."); - } - - @Override - public String toString() { - if (this.map.units.isEmpty() || this.map.prefixes.isEmpty()) - return super.toString(); - else - return String.format("Infinite set of name-unit entries created from units %s and prefixes %s", - this.map.units, this.map.prefixes); - } - } - - /** - * The units stored in this collection, without prefixes. - * - * @since 2019-04-13 - * @since v0.2.0 - */ - private final Map units; - - /** - * The available prefixes for use. - * - * @since 2019-04-13 - * @since v0.2.0 - */ - private final Map prefixes; - - // caches - private transient Collection values = null; - private transient Set keySet = null; - private transient Set> entrySet = null; - - /** - * Creates the {@code PrefixedUnitMap}. - * - * @param units - * map mapping unit names to units - * @param prefixes - * map mapping prefix names to prefixes - * @since 2019-04-13 - * @since v0.2.0 - */ - public PrefixedUnitMap(final Map units, final Map prefixes) { - // I am making unmodifiable maps to ensure I don't accidentally make changes. - this.units = Collections.unmodifiableMap(units); - this.prefixes = Collections.unmodifiableMap(prefixes); - } - - @Override - public void clear() { - throw new UnsupportedOperationException("Cannot clear an immutable map"); - } - - @Override - public Unit compute(final String key, - final BiFunction remappingFunction) { - throw new UnsupportedOperationException("Cannot edit an immutable map"); - } - - @Override - public Unit computeIfAbsent(final String key, final Function mappingFunction) { - throw new UnsupportedOperationException("Cannot edit an immutable map"); - } - - @Override - public Unit computeIfPresent(final String key, - final BiFunction remappingFunction) { - throw new UnsupportedOperationException("Cannot edit an immutable map"); - } - - @Override - public boolean containsKey(final Object key) { - // First, test if there is a unit with the key - if (this.units.containsKey(key)) - return true; - - // Next, try to cast it to String - if (!(key instanceof String)) - throw new IllegalArgumentException("Attempted to test for a unit using a non-string name."); - final String unitName = (String) key; - - // Then, look for the longest prefix that is attached to a valid unit - String longestPrefix = null; - int longestLength = 0; - - for (final String prefixName : this.prefixes.keySet()) { - // a prefix name is valid if: - // - it is prefixed (i.e. the unit name starts with it) - // - it is longer than the existing largest prefix (since I am looking for the longest valid prefix) - // - the part after the prefix is a valid unit name - // - the unit described that name is a linear unit (since only linear units can have prefixes) - if (unitName.startsWith(prefixName) && prefixName.length() > longestLength) { - final String rest = unitName.substring(prefixName.length()); - if (this.containsKey(rest) && this.get(rest) instanceof LinearUnit) { - longestPrefix = prefixName; - longestLength = prefixName.length(); - } - } - } - - return longestPrefix != null; - } - - /** - * {@inheritDoc} - * - *

- * Because of ambiguities between prefixes (i.e. kilokilo = mega), this method only tests for prefixless units. - *

- */ - @Override - public boolean containsValue(final Object value) { - return this.units.containsValue(value); - } - - @Override - public Set> entrySet() { - if (this.entrySet == null) { - this.entrySet = new PrefixedUnitEntrySet(this); - } - return this.entrySet; - } - - @Override - public Unit get(final Object key) { - // First, test if there is a unit with the key - if (this.units.containsKey(key)) - return this.units.get(key); - - // Next, try to cast it to String - if (!(key instanceof String)) - throw new IllegalArgumentException("Attempted to obtain a unit using a non-string name."); - final String unitName = (String) key; - - // Then, look for the longest prefix that is attached to a valid unit - String longestPrefix = null; - int longestLength = 0; - - for (final String prefixName : this.prefixes.keySet()) { - // a prefix name is valid if: - // - it is prefixed (i.e. the unit name starts with it) - // - it is longer than the existing largest prefix (since I am looking for the longest valid prefix) - // - the part after the prefix is a valid unit name - // - the unit described that name is a linear unit (since only linear units can have prefixes) - if (unitName.startsWith(prefixName) && prefixName.length() > longestLength) { - final String rest = unitName.substring(prefixName.length()); - if (this.containsKey(rest) && this.get(rest) instanceof LinearUnit) { - longestPrefix = prefixName; - longestLength = prefixName.length(); - } - } - } - - // if none found, returns null - if (longestPrefix == null) - return null; - else { - // get necessary data - final String rest = unitName.substring(longestLength); - // this cast will not fail because I verified that it would work before selecting this prefix - final LinearUnit unit = (LinearUnit) this.get(rest); - final UnitPrefix prefix = this.prefixes.get(longestPrefix); - - return unit.withPrefix(prefix); - } - } - - @Override - public boolean isEmpty() { - return this.units.isEmpty(); - } - - @Override - public Set keySet() { - if (this.keySet == null) { - this.keySet = new PrefixedUnitNameSet(this); - } - return this.keySet; - } - - @Override - public Unit merge(final String key, final Unit value, - final BiFunction remappingFunction) { - throw new UnsupportedOperationException("Cannot merge into an immutable map"); - } - - @Override - public Unit put(final String key, final Unit value) { - throw new UnsupportedOperationException("Cannot add entries to an immutable map"); - } - - @Override - public void putAll(final Map m) { - throw new UnsupportedOperationException("Cannot add entries to an immutable map"); - } - - @Override - public Unit putIfAbsent(final String key, final Unit value) { - throw new UnsupportedOperationException("Cannot add entries to an immutable map"); - } - - @Override - public Unit remove(final Object key) { - throw new UnsupportedOperationException("Cannot remove entries from an immutable map"); - } - - @Override - public boolean remove(final Object key, final Object value) { - throw new UnsupportedOperationException("Cannot remove entries from an immutable map"); - } - - @Override - public Unit replace(final String key, final Unit value) { - throw new UnsupportedOperationException("Cannot replace entries in an immutable map"); - } - - @Override - public boolean replace(final String key, final Unit oldValue, final Unit newValue) { - throw new UnsupportedOperationException("Cannot replace entries in an immutable map"); - } - - @Override - public void replaceAll(final BiFunction function) { - throw new UnsupportedOperationException("Cannot replace entries in an immutable map"); - } - - @Override - public int size() { - if (this.units.isEmpty()) - return 0; - else { - if (this.prefixes.isEmpty()) - return this.units.size(); - else - // infinite set - return Integer.MAX_VALUE; - } - } - - @Override - public String toString() { - if (this.units.isEmpty() || this.prefixes.isEmpty()) - return super.toString(); - else - return String.format("Infinite map of name-unit entries created from units %s and prefixes %s", - this.units, this.prefixes); - } - - /** - * {@inheritDoc} - * - *

- * Because of ambiguities between prefixes (i.e. kilokilo = mega), this method ignores prefixes. - *

- */ - @Override - public Collection values() { - if (this.values == null) { - this.values = Collections.unmodifiableCollection(this.units.values()); - } - return this.values; - } - } - - /** - * A regular expression that separates names and expressions in unit files. - */ - private static final Pattern NAME_EXPRESSION = Pattern.compile("(\\S+)\\s+(\\S.*)"); - - /** - * The exponent operator - * - * @param base - * base of exponentiation - * @param exponentUnit - * exponent - * @return result - * @since 2019-04-10 - * @since v0.2.0 - */ - private static final LinearUnit exponentiateUnits(final LinearUnit base, final LinearUnit exponentUnit) { - // exponent function - first check if o2 is a number, - if (exponentUnit.getBase().equals(SI.SI.getBaseUnit(UnitDimension.EMPTY))) { - // then check if it is an integer, - final double exponent = exponentUnit.getConversionFactor(); - if (DecimalComparison.equals(exponent % 1, 0)) - // then exponentiate - return base.toExponent((int) (exponent + 0.5)); - else - // not an integer - throw new UnsupportedOperationException("Decimal exponents are currently not supported."); - } else - // not a number - throw new IllegalArgumentException("Exponents must be numbers."); - } - - /** - * The units in this system, excluding prefixes. - * - * @since 2019-01-07 - * @since v0.1.0 - */ - private final Map prefixlessUnits; - - /** - * The unit prefixes in this system. - * - * @since 2019-01-14 - * @since v0.1.0 - */ - private final Map prefixes; - - /** - * The dimensions in this system. - * - * @since 2019-03-14 - * @since v0.2.0 - */ - private final Map dimensions; - - /** - * A map mapping strings to units (including prefixes) - * - * @since 2019-04-13 - * @since v0.2.0 - */ - private final Map units; - - /** - * A parser that can parse unit expressions. - * - * @since 2019-03-22 - * @since v0.2.0 - */ - private final ExpressionParser unitExpressionParser = new ExpressionParser.Builder<>( - this::getLinearUnit).addBinaryOperator("+", (o1, o2) -> o1.plus(o2), 0) - .addBinaryOperator("-", (o1, o2) -> o1.minus(o2), 0) - .addBinaryOperator("*", (o1, o2) -> o1.times(o2), 1).addSpaceFunction("*") - .addBinaryOperator("/", (o1, o2) -> o1.dividedBy(o2), 1) - .addBinaryOperator("^", UnitsDatabase::exponentiateUnits, 2).build(); - - /** - * A parser that can parse unit prefix expressions - * - * @since 2019-04-13 - * @since v0.2.0 - */ - private final ExpressionParser prefixExpressionParser = new ExpressionParser.Builder<>(this::getPrefix) - .addBinaryOperator("*", (o1, o2) -> o1.times(o2), 0).addSpaceFunction("*") - .addBinaryOperator("/", (o1, o2) -> o1.dividedBy(o2), 0) - .addBinaryOperator("^", (o1, o2) -> o1.toExponent(o2.getMultiplier()), 1).build(); - - /** - * A parser that can parse unit dimension expressions. - * - * @since 2019-04-13 - * @since v0.2.0 - */ - private final ExpressionParser unitDimensionParser = new ExpressionParser.Builder<>( - this::getDimension).addBinaryOperator("*", (o1, o2) -> o1.times(o2), 0).addSpaceFunction("*") - .addBinaryOperator("/", (o1, o2) -> o1.dividedBy(o2), 0).build(); - - /** - * Creates the {@code UnitsDatabase}. - * - * @since 2019-01-10 - * @since v0.1.0 - */ - public UnitsDatabase() { - this.prefixlessUnits = new HashMap<>(); - this.prefixes = new HashMap<>(); - this.dimensions = new HashMap<>(); - this.units = new PrefixedUnitMap(this.prefixlessUnits, this.prefixes); - } - - /** - * Adds a unit dimension to the database. - * - * @param name - * dimension's name - * @param dimension - * dimension to add - * @throws NullPointerException - * if name or dimension is null - * @since 2019-03-14 - * @since v0.2.0 - */ - public void addDimension(final String name, final UnitDimension dimension) { - this.dimensions.put(Objects.requireNonNull(name, "name must not be null."), - Objects.requireNonNull(dimension, "dimension must not be null.")); - } - - /** - * Adds to the list from a line in a unit dimension file. - * - * @param line - * line to look at - * @param lineCounter - * number of line, for error messages - * @since 2019-04-10 - * @since v0.2.0 - */ - private void addDimensionFromLine(final String line, final long lineCounter) { - // ignore lines that start with a # sign - they're comments - if (line.isEmpty()) - return; - if (line.contains("#")) { - this.addDimensionFromLine(line.substring(0, line.indexOf("#")), lineCounter); - return; - } - - // divide line into name and expression - final Matcher lineMatcher = NAME_EXPRESSION.matcher(line); - if (!lineMatcher.matches()) - throw new IllegalArgumentException(String.format( - "Error at line %d: Lines of a dimension file must consist of a dimension name, then spaces or tabs, then a dimension expression.", - lineCounter)); - final String name = lineMatcher.group(1); - final String expression = lineMatcher.group(2); - - if (name.endsWith(" ")) { - System.err.printf("Warning - line %d's dimension name ends in a space", lineCounter); - } - - // if expression is "!", search for an existing dimension - // if no unit found, throw an error - if (expression.equals("!")) { - if (!this.containsDimensionName(name)) - throw new IllegalArgumentException( - String.format("! used but no dimension found (line %d).", lineCounter)); - } else { - // it's a unit, get the unit - final UnitDimension dimension; - try { - dimension = this.getDimensionFromExpression(expression); - } catch (final IllegalArgumentException e) { - System.err.printf("Parsing error on line %d:%n", lineCounter); - throw e; - } - - this.addDimension(name, dimension); - } - } - - /** - * Adds a unit prefix to the database. - * - * @param name - * prefix's name - * @param prefix - * prefix to add - * @throws NullPointerException - * if name or prefix is null - * @since 2019-01-14 - * @since v0.1.0 - */ - public void addPrefix(final String name, final UnitPrefix prefix) { - this.prefixes.put(Objects.requireNonNull(name, "name must not be null."), - Objects.requireNonNull(prefix, "prefix must not be null.")); - } - - /** - * Adds a unit to the database. - * - * @param name - * unit's name - * @param unit - * unit to add - * @throws NullPointerException - * if unit is null - * @since 2019-01-10 - * @since v0.1.0 - */ - public void addUnit(final String name, final Unit unit) { - this.prefixlessUnits.put(Objects.requireNonNull(name, "name must not be null."), - Objects.requireNonNull(unit, "unit must not be null.")); - } - - /** - * Adds to the list from a line in a unit file. - * - * @param line - * line to look at - * @param lineCounter - * number of line, for error messages - * @since 2019-04-10 - * @since v0.2.0 - */ - private void addUnitOrPrefixFromLine(final String line, final long lineCounter) { - // ignore lines that start with a # sign - they're comments - if (line.isEmpty()) - return; - if (line.contains("#")) { - this.addUnitOrPrefixFromLine(line.substring(0, line.indexOf("#")), lineCounter); - return; - } - - // divide line into name and expression - final Matcher lineMatcher = NAME_EXPRESSION.matcher(line); - if (!lineMatcher.matches()) - throw new IllegalArgumentException(String.format( - "Error at line %d: Lines of a unit file must consist of a unit name, then spaces or tabs, then a unit expression.", - lineCounter)); - final String name = lineMatcher.group(1); - final String expression = lineMatcher.group(2); - - if (name.endsWith(" ")) { - System.err.printf("Warning - line %d's unit name ends in a space", lineCounter); - } - - // if expression is "!", search for an existing unit - // if no unit found, throw an error - if (expression.equals("!")) { - if (!this.containsUnitName(name)) - throw new IllegalArgumentException(String.format("! used but no unit found (line %d).", lineCounter)); - } else { - if (name.endsWith("-")) { - final UnitPrefix prefix; - try { - prefix = this.getPrefixFromExpression(expression); - } catch (final IllegalArgumentException e) { - System.err.printf("Parsing error on line %d:%n", lineCounter); - throw e; - } - this.addPrefix(name.substring(0, name.length() - 1), prefix); - } else { - // it's a unit, get the unit - final Unit unit; - try { - unit = this.getUnitFromExpression(expression); - } catch (final IllegalArgumentException e) { - System.err.printf("Parsing error on line %d:%n", lineCounter); - throw e; - } - - this.addUnit(name, unit); - } - } - } - - /** - * Tests if the database has a unit dimension with this name. - * - * @param name - * name to test - * @return if database contains name - * @since 2019-03-14 - * @since v0.2.0 - */ - public boolean containsDimensionName(final String name) { - return this.dimensions.containsKey(name); - } - - /** - * Tests if the database has a unit prefix with this name. - * - * @param name - * name to test - * @return if database contains name - * @since 2019-01-13 - * @since v0.1.0 - */ - public boolean containsPrefixName(final String name) { - return this.prefixes.containsKey(name); - } - - /** - * Tests if the database has a unit with this name, taking prefixes into consideration - * - * @param name - * name to test - * @return if database contains name - * @since 2019-01-13 - * @since v0.1.0 - */ - public boolean containsUnitName(final String name) { - return this.units.containsKey(name); - } - - /** - * @return a map mapping dimension names to dimensions - * @since 2019-04-13 - * @since v0.2.0 - */ - public Map dimensionMap() { - return Collections.unmodifiableMap(this.dimensions); - } - - /** - * Gets a unit dimension from the database using its name. - * - *

- * This method accepts exponents, like "L^3" - *

- * - * @param name - * dimension's name - * @return dimension - * @since 2019-03-14 - * @since v0.2.0 - */ - public UnitDimension 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 int exponent; - try { - exponent = Integer.parseInt(baseAndExponent[baseAndExponent.length - 1]); - } catch (final NumberFormatException e2) { - throw new IllegalArgumentException("Exponent must be an integer."); - } - - return base.toExponent(exponent); - } - return this.dimensions.get(name); - } - - /** - * Uses the database's data to parse an expression into a unit dimension - *

- * The expression is a series of any of the following: - *

    - *
  • The name of a unit dimension, which multiplies or divides the result based on preceding operators
  • - *
  • The operators '*' and '/', which multiply and divide (note that just putting two unit dimensions next to each - * other is equivalent to multiplication)
  • - *
  • The operator '^' which exponentiates. Exponents must be integers.
  • - *
- * - * @param expression - * expression to parse - * @throws IllegalArgumentException - * if the expression cannot be parsed - * @throws NullPointerException - * if expression is null - * @since 2019-04-13 - * @since v0.2.0 - */ - public UnitDimension getDimensionFromExpression(final String expression) { - Objects.requireNonNull(expression, "expression must not be null."); - - // attempt to get a dimension as an alias first - if (this.containsDimensionName(expression)) - return this.getDimension(expression); - - // force operators to have spaces - String modifiedExpression = expression; - modifiedExpression = modifiedExpression.replaceAll("\\*", " \\* "); - modifiedExpression = modifiedExpression.replaceAll("/", " / "); - modifiedExpression = modifiedExpression.replaceAll(" *\\^ *", "\\^"); - - // fix broken spaces - modifiedExpression = modifiedExpression.replaceAll(" +", " "); - - return this.unitDimensionParser.parseExpression(modifiedExpression); - } - - /** - * Gets a unit. If it is linear, cast it to a LinearUnit and return it. Otherwise, throw an - * {@code IllegalArgumentException}. - * - * @param name - * unit's name - * @return unit - * @since 2019-03-22 - * @since v0.2.0 - */ - private LinearUnit getLinearUnit(final String name) { - // see if I am using a function-unit like tempC(100) - if (name.contains("(") && name.contains(")")) { - // break it into function name and value - final List parts = Arrays.asList(name.split("\\(")); - if (parts.size() != 2) - throw new IllegalArgumentException("Format nonlinear units like: unit(value)."); - - // solve the function - final Unit unit = this.getUnit(parts.get(0)); - final double value = Double.parseDouble(parts.get(1).substring(0, parts.get(1).length() - 1)); - return unit.getBase().times(unit.convertToBase(value)); - } else { - // get a linear unit - final Unit unit = this.getUnit(name); - if (unit instanceof LinearUnit) - return (LinearUnit) unit; - else - throw new IllegalArgumentException(String.format("%s is not a linear unit.", name)); - } - } - - /** - * Gets a unit prefix from the database from its name - * - * @param name - * prefix's name - * @return prefix - * @since 2019-01-10 - * @since v0.1.0 - */ - public UnitPrefix getPrefix(final String name) { - try { - return new DefaultUnitPrefix(Double.parseDouble(name)); - } catch (final NumberFormatException e) { - return this.prefixes.get(name); - } - } - - /** - * Gets a unit prefix from a prefix expression - *

- * Currently, prefix expressions are much simpler than unit expressions: They are either a number or the name of - * another prefix - *

- * - * @param expression - * expression to input - * @return prefix - * @throws IllegalArgumentException - * if expression cannot be parsed - * @throws NullPointerException - * if any argument is null - * @since 2019-01-14 - * @since v0.1.0 - */ - public UnitPrefix getPrefixFromExpression(final String expression) { - Objects.requireNonNull(expression, "expression must not be null."); - - // attempt to get a unit as an alias first - if (this.containsUnitName(expression)) - return this.getPrefix(expression); - - // force operators to have spaces - String modifiedExpression = expression; - modifiedExpression = modifiedExpression.replaceAll("\\*", " \\* "); - modifiedExpression = modifiedExpression.replaceAll("/", " / "); - modifiedExpression = modifiedExpression.replaceAll("\\^", " \\^ "); - - // fix broken spaces - modifiedExpression = modifiedExpression.replaceAll(" +", " "); - - return this.prefixExpressionParser.parseExpression(modifiedExpression); - } - - /** - * Gets a unit from the database from its name, looking for prefixes. - * - * @param name - * unit's name - * @return unit - * @since 2019-01-10 - * @since v0.1.0 - */ - public Unit getUnit(final String name) { - try { - final double value = Double.parseDouble(name); - return SI.SI.getBaseUnit(UnitDimension.EMPTY).times(value); - } catch (final NumberFormatException e) { - return this.units.get(name); - } - - } - - /** - * Uses the database's unit data to parse an expression into a unit - *

- * The expression is a series of any of the following: - *

    - *
  • The name of a unit, which multiplies or divides the result based on preceding operators
  • - *
  • The operators '*' and '/', which multiply and divide (note that just putting two units or values next to each - * other is equivalent to multiplication)
  • - *
  • The operator '^' which exponentiates. Exponents must be integers.
  • - *
  • A number which is multiplied or divided
  • - *
- * This method only works with linear units. - * - * @param expression - * expression to parse - * @throws IllegalArgumentException - * if the expression cannot be parsed - * @throws NullPointerException - * if expression is null - * @since 2019-01-07 - * @since v0.1.0 - */ - public Unit getUnitFromExpression(final String expression) { - Objects.requireNonNull(expression, "expression must not be null."); - - // attempt to get a unit as an alias first - if (this.containsUnitName(expression)) - return this.getUnit(expression); - - // force operators to have spaces - String modifiedExpression = expression; - modifiedExpression = modifiedExpression.replaceAll("\\+", " \\+ "); - modifiedExpression = modifiedExpression.replaceAll("-", " - "); - modifiedExpression = modifiedExpression.replaceAll("\\*", " \\* "); - modifiedExpression = modifiedExpression.replaceAll("/", " / "); - modifiedExpression = modifiedExpression.replaceAll("\\^", " \\^ "); - - // fix broken spaces - modifiedExpression = modifiedExpression.replaceAll(" +", " "); - - // the previous operation breaks negative numbers, fix them! - // (i.e. -2 becomes - 2) - for (int i = 2; i < modifiedExpression.length(); i++) { - if (modifiedExpression.charAt(i) == '-' - && Arrays.asList('+', '-', '*', '/', '^').contains(modifiedExpression.charAt(i - 2))) { - // found a broken negative number - modifiedExpression = modifiedExpression.substring(0, i + 1) + modifiedExpression.substring(i + 2); - } - } - - return this.unitExpressionParser.parseExpression(modifiedExpression); - } - - /** - * Adds all dimensions from a file, using data from the database to parse them. - *

- * Each line in the file should consist of a name and an expression (parsed by getDimensionFromExpression) separated - * by any number of tab characters. - *

- *

- * Allowed exceptions: - *

    - *
  • Anything after a '#' character is considered a comment and ignored.
  • - *
  • Blank lines are also ignored
  • - *
  • If an expression consists of a single exclamation point, instead of parsing it, this method will search the - * database for an existing unit. If no unit is found, an IllegalArgumentException is thrown. This is used to define - * initial units and ensure that the database contains them.
  • - *
- * - * @param file - * file to read - * @throws IllegalArgumentException - * if the file cannot be parsed, found or read - * @throws NullPointerException - * if file is null - * @since 2019-01-13 - * @since v0.1.0 - */ - public void loadDimensionFile(final File file) { - Objects.requireNonNull(file, "file must not be null."); - try (FileReader fileReader = new FileReader(file); BufferedReader reader = new BufferedReader(fileReader)) { - // while the reader has lines to read, read a line, then parse it, then add it - long lineCounter = 0; - while (reader.ready()) { - this.addDimensionFromLine(reader.readLine(), ++lineCounter); - } - } catch (final FileNotFoundException e) { - throw new IllegalArgumentException("Could not find file " + file, e); - } catch (final IOException e) { - throw new IllegalArgumentException("Could not read file " + file, e); - } - } - - /** - * Adds all units from a file, using data from the database to parse them. - *

- * Each line in the file should consist of a name and an expression (parsed by getUnitFromExpression) separated by - * any number of tab characters. - *

- *

- * Allowed exceptions: - *

    - *
  • Anything after a '#' character is considered a comment and ignored.
  • - *
  • Blank lines are also ignored
  • - *
  • If an expression consists of a single exclamation point, instead of parsing it, this method will search the - * database for an existing unit. If no unit is found, an IllegalArgumentException is thrown. This is used to define - * initial units and ensure that the database contains them.
  • - *
- * - * @param file - * file to read - * @throws IllegalArgumentException - * if the file cannot be parsed, found or read - * @throws NullPointerException - * if file is null - * @since 2019-01-13 - * @since v0.1.0 - */ - public void loadUnitsFile(final File file) { - Objects.requireNonNull(file, "file must not be null."); - try (FileReader fileReader = new FileReader(file); BufferedReader reader = new BufferedReader(fileReader)) { - // while the reader has lines to read, read a line, then parse it, then add it - long lineCounter = 0; - while (reader.ready()) { - this.addUnitOrPrefixFromLine(reader.readLine(), ++lineCounter); - } - } catch (final FileNotFoundException e) { - throw new IllegalArgumentException("Could not find file " + file, e); - } catch (final IOException e) { - throw new IllegalArgumentException("Could not read file " + file, e); - } - } - - /** - * @return a map mapping prefix names to prefixes - * @since 2019-04-13 - * @since v0.2.0 - */ - public Map prefixMap() { - return Collections.unmodifiableMap(this.prefixes); - } - - @Override - public String toString() { - return String.format("Unit Database with %d units and %d unit prefixes", this.prefixlessUnits.size(), - this.prefixes.size()); - } - - /** - * Returns a map mapping unit names to units, including units with prefixes. - *

- * The returned map is infinite in size if there is at least one unit and at least one prefix. If it is infinite, - * some operations that only work with finite collections, like converting name/entry sets to arrays, will throw an - * {@code IllegalStateException}. - *

- *

- * Specifically, the operations that will throw an IllegalStateException if the map is infinite in size are: - *

    - *
  • {@code unitMap.entrySet().toArray()} (either overloading)
  • - *
  • {@code unitMap.keySet().toArray()} (either overloading)
  • - *
- *

- *

- * Because of ambiguities between prefixes (i.e. kilokilo = mega), the map's {@link PrefixedUnitMap#containsValue - * containsValue} and {@link PrefixedUnitMap#values() values()} methods currently ignore prefixes. - *

- * - * @return a map mapping unit names to units, including prefixed names - * @since 2019-04-13 - * @since v0.2.0 - */ - public Map unitMap() { - return this.units; // PrefixedUnitMap is immutable so I don't need to make an unmodifiable map. - } - - /** - * @return a map mapping unit names to units, ignoring prefixes - * @since 2019-04-13 - * @since v0.2.0 - */ - public Map unitMapPrefixless() { - return Collections.unmodifiableMap(this.prefixlessUnits); - } -} diff --git a/src/org/unitConverter/UnitsDatabaseTest.java b/src/org/unitConverter/UnitsDatabaseTest.java deleted file mode 100644 index c46d598..0000000 --- a/src/org/unitConverter/UnitsDatabaseTest.java +++ /dev/null @@ -1,312 +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 . - */ -package org.unitConverter; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assertions.fail; - -import java.util.Iterator; -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. - * - * @author Adrien Hopkins - * @since 2019-04-14 - * @since v0.2.0 - */ -class UnitsDatabaseTest { - // some linear units and one nonlinear - private static final Unit U = SI.METRE; - private static final Unit V = SI.KILOGRAM; - private static final Unit W = SI.SECOND; - - // used for testing expressions - // J = U^2 * V / W^2 - 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); - - // 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); - - /** - * Confirms that operations that shouldn't function for infinite databases throw an {@code IllegalStateException}. - * - * @since 2019-05-03 - */ - @Test - public void testInfiniteSetExceptions() { - // load units - final UnitsDatabase infiniteDatabase = new UnitsDatabase(); - - infiniteDatabase.addUnit("J", J); - infiniteDatabase.addUnit("K", K); - - infiniteDatabase.addPrefix("A", A); - infiniteDatabase.addPrefix("B", B); - infiniteDatabase.addPrefix("C", C); - - { - boolean exceptionThrown = false; - try { - infiniteDatabase.unitMap().entrySet().toArray(); - } catch (final IllegalStateException e) { - exceptionThrown = true; - // pass! - } finally { - if (!exceptionThrown) { - fail("No IllegalStateException thrown"); - } - } - } - - { - boolean exceptionThrown = false; - try { - infiniteDatabase.unitMap().keySet().toArray(); - } catch (final IllegalStateException e) { - exceptionThrown = true; - // pass! - } finally { - if (!exceptionThrown) { - fail("No IllegalStateException thrown"); - } - } - } - } - - /** - * Test that prefixes correctly apply to units. - * - * @since 2019-04-14 - * @since v0.2.0 - */ - @Test - public void testPrefixes() { - final UnitsDatabase database = new UnitsDatabase(); - - database.addUnit("U", U); - database.addUnit("V", V); - database.addUnit("W", W); - - database.addPrefix("A", A); - database.addPrefix("B", B); - database.addPrefix("C", C); - - // get the product - final Unit abcuNonlinear = database.getUnit("ABCU"); - assert abcuNonlinear instanceof LinearUnit; - - final LinearUnit abcu = (LinearUnit) abcuNonlinear; - assertEquals(A.getMultiplier() * B.getMultiplier() * C.getMultiplier(), abcu.getConversionFactor(), 1e-15); - } - - /** - * Tests the functionnalites of the prefixless unit map. - * - *

- * The map should be an auto-updating view of the units in the database. - *

- * - * @since 2019-04-14 - * @since v0.2.0 - */ - @Test - public void testPrefixlessUnitMap() { - final UnitsDatabase database = new UnitsDatabase(); - final Map prefixlessUnits = database.unitMapPrefixless(); - - database.addUnit("U", U); - database.addUnit("V", V); - database.addUnit("W", W); - - // this should work because the map should be an auto-updating view - assertTrue(prefixlessUnits.containsKey("U")); - assertFalse(prefixlessUnits.containsKey("Z")); - - assertTrue(prefixlessUnits.containsValue(U)); - assertFalse(prefixlessUnits.containsValue(NONLINEAR)); - } - - /** - * Tests that the database correctly stores and retrieves units, ignoring prefixes. - * - * @since 2019-04-14 - * @since v0.2.0 - */ - @Test - public void testPrefixlessUnits() { - final UnitsDatabase database = new UnitsDatabase(); - - database.addUnit("U", U); - database.addUnit("V", V); - database.addUnit("W", W); - - assertTrue(database.containsUnitName("U")); - assertFalse(database.containsUnitName("Z")); - - assertEquals(U, database.getUnit("U")); - assertEquals(null, database.getUnit("Z")); - } - - /** - * Test that unit expressions return the correct value. - * - * @since 2019-04-14 - * @since v0.2.0 - */ - @Test - public void testUnitExpressions() { - // load units - final UnitsDatabase database = new UnitsDatabase(); - - database.addUnit("U", U); - database.addUnit("V", V); - database.addUnit("W", W); - database.addUnit("fj", J.times(5)); - database.addUnit("ej", J.times(8)); - - database.addPrefix("A", A); - database.addPrefix("B", B); - database.addPrefix("C", C); - - // first test - test prefixes and operations - final Unit expected1 = J.withPrefix(A).withPrefix(B).withPrefix(C).withPrefix(C); - final Unit actual1 = database.getUnitFromExpression("ABV * CU^2 / W / W"); - - assertEquals(expected1, actual1); - - // second test - test addition and subtraction - final Unit expected2 = J.times(58); - final Unit actual2 = database.getUnitFromExpression("2 fj + 6 ej"); - - assertEquals(expected2, actual2); - } - - /** - * Tests both the unit name iterator and the name-unit entry iterator - * - * @since 2019-04-14 - * @since v0.2.0 - */ - @Test - public void testUnitIterator() { - // load units - final UnitsDatabase database = new UnitsDatabase(); - - database.addUnit("J", J); - database.addUnit("K", K); - - database.addPrefix("A", A); - database.addPrefix("B", B); - database.addPrefix("C", C); - - final int NUM_UNITS = database.unitMapPrefixless().size(); - final int NUM_PREFIXES = database.prefixMap().size(); - - final Iterator nameIterator = database.unitMap().keySet().iterator(); - final Iterator> entryIterator = database.unitMap().entrySet().iterator(); - - int expectedLength = 1; - int unitsWithThisLengthSoFar = 0; - - // loop 1000 times - for (int i = 0; i < 1000; i++) { - // expected length of next - if (unitsWithThisLengthSoFar >= NUM_UNITS * (int) Math.pow(NUM_PREFIXES, expectedLength - 1)) { - expectedLength++; - unitsWithThisLengthSoFar = 0; - } - - // test that stuff is valid - final String nextName = nameIterator.next(); - final Unit nextUnit = database.getUnit(nextName); - final Entry nextEntry = entryIterator.next(); - - assertEquals(expectedLength, nextName.length()); - assertEquals(nextName, nextEntry.getKey()); - assertEquals(nextUnit, nextEntry.getValue()); - - unitsWithThisLengthSoFar++; - } - - // test toString for consistency - final String entryIteratorString = entryIterator.toString(); - for (int i = 0; i < 3; i++) { - assertEquals(entryIteratorString, entryIterator.toString()); - } - - final String nameIteratorString = nameIterator.toString(); - for (int i = 0; i < 3; i++) { - assertEquals(nameIteratorString, nameIterator.toString()); - } - } - - /** - * Determine, given a unit name that could mean multiple things, which meaning is chosen. - *

- * For example, "ABCU" could mean "A-B-C-U", "AB-C-U", or "A-BC-U". In this case, "AB-C-U" is the correct choice. - *

- * - * @since 2019-04-14 - * @since v0.2.0 - */ - @Test - public void testUnitPrefixCombinations() { - // load units - final UnitsDatabase database = new UnitsDatabase(); - - database.addUnit("J", J); - - database.addPrefix("A", A); - database.addPrefix("B", B); - database.addPrefix("C", C); - database.addPrefix("AB", AB); - database.addPrefix("BC", BC); - - // test 1 - AB-C-J vs A-BC-J vs A-B-C-J - final Unit expected1 = J.withPrefix(AB).withPrefix(C); - final Unit actual1 = database.getUnit("ABCJ"); - - assertEquals(expected1, actual1); - - // test 2 - ABC-J vs AB-CJ vs AB-C-J - database.addUnit("CJ", J.times(13)); - database.addPrefix("ABC", new DefaultUnitPrefix(17)); - - final Unit expected2 = J.times(17); - final Unit actual2 = database.getUnit("ABCJ"); - - assertEquals(expected2, actual2); - } -} diff --git a/src/org/unitConverter/converterGUI/UnitConverterGUI.java b/src/org/unitConverter/converterGUI/UnitConverterGUI.java index 2d3d1a5..4598971 100644 --- a/src/org/unitConverter/converterGUI/UnitConverterGUI.java +++ b/src/org/unitConverter/converterGUI/UnitConverterGUI.java @@ -42,14 +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.LinearUnit; import org.unitConverter.unit.SI; import org.unitConverter.unit.Unit; import org.unitConverter.unit.UnitPrefix; +import org.unitConverter.unit.UnitDatabase; /** * @author Adrien Hopkins @@ -66,7 +65,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 +74,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", NonlinearUnits.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 unitNames; @@ -119,7 +118,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 +154,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 isFullBase = unit -> unit instanceof BaseUnit && ((BaseUnit) unit).isFullBase(); + final Predicate 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 +358,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 dimension = this.database.getDimension(dimensionName); return unit.getDimension().equals(dimension); } 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 . - */ -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 . - */ -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 . - */ -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 . - */ -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 . - */ -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 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 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 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 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 getBaseSet() { - final Set 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 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 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 positiveStringComponents = new ArrayList<>(); - final List 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/dimension/UnitDimensionTest.java b/src/org/unitConverter/dimension/UnitDimensionTest.java deleted file mode 100644 index 017e3d2..0000000 --- a/src/org/unitConverter/dimension/UnitDimensionTest.java +++ /dev/null @@ -1,77 +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 . - */ -package org.unitConverter.dimension; - -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 org.junit.jupiter.api.Test; - -/** - * Tests for {@link UnitDimension}. This is NOT part of this program's public API. - * - * @author Adrien Hopkins - * @since 2018-12-12 - * @since v0.1.0 - */ -class UnitDimensionTest { - /** - * Tests {@link UnitDimension#equals} - * - * @since 2018-12-12 - * @since v0.1.0 - */ - @Test - public void testEquals() { - assertEquals(LENGTH, LENGTH); - assertFalse(LENGTH.equals(QUANTITY)); - } - - /** - * Tests {@code UnitDimension}'s exponentiation - * - * @since 2019-01-15 - * @since v0.1.0 - */ - @Test - public void testExponents() { - assertEquals(1, LENGTH.getExponent(SIBaseDimension.LENGTH)); - assertEquals(3, VOLUME.getExponent(SIBaseDimension.LENGTH)); - } - - /** - * Tests {@code UnitDimension}'s multiplication and division. - * - * @since 2018-12-12 - * @since v0.1.0 - */ - @Test - public void testMultiplicationAndDivision() { - assertEquals(AREA, LENGTH.times(LENGTH)); - assertEquals(MASS_DENSITY, MASS.dividedBy(VOLUME)); - assertEquals(ENERGY, AREA.times(MASS).dividedBy(TIME).dividedBy(TIME)); - assertEquals(LENGTH, LENGTH.times(TIME).dividedBy(TIME)); - } -} diff --git a/src/org/unitConverter/dimension/package-info.java b/src/org/unitConverter/dimension/package-info.java deleted file mode 100644 index 8cb26b1..0000000 --- a/src/org/unitConverter/dimension/package-info.java +++ /dev/null @@ -1,24 +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 . - */ -/** - * Everything to do with what a unit measures, or its dimension. - * - * @author Adrien Hopkins - * @since 2018-12-22 - * @since v0.1.0 - */ -package org.unitConverter.dimension; \ No newline at end of file diff --git a/src/org/unitConverter/math/ObjectProductTest.java b/src/org/unitConverter/math/ObjectProductTest.java index 03c767c..afd18b7 100644 --- a/src/org/unitConverter/math/ObjectProductTest.java +++ b/src/org/unitConverter/math/ObjectProductTest.java @@ -18,18 +18,17 @@ 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.dimension.SIBaseDimension; -import org.unitConverter.dimension.UnitDimension; +import org.unitConverter.unit.SI; /** * Tests for {@link ObjectProduct} using BaseDimension as a test object. This is NOT part of this program's public API. @@ -59,8 +58,8 @@ class ObjectProductTest { */ @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/newUnits/BaseDimension.java b/src/org/unitConverter/newUnits/BaseDimension.java deleted file mode 100644 index a1cde46..0000000 --- a/src/org/unitConverter/newUnits/BaseDimension.java +++ /dev/null @@ -1,81 +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 . - */ -package org.unitConverter.newUnits; - -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/newUnits/BaseUnit.java b/src/org/unitConverter/newUnits/BaseUnit.java deleted file mode 100644 index 6a57faa..0000000 --- a/src/org/unitConverter/newUnits/BaseUnit.java +++ /dev/null @@ -1,117 +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 . - */ -package org.unitConverter.newUnits; - -import java.util.Objects; - -/** - * A unit that other units are defined by. - * - * @author Adrien Hopkins - * @since 2019-10-16 - */ -public final class BaseUnit extends Unit { - /** - * Gets a base unit from the dimension it measures, its name and its symbol. - * - * @param dimension - * dimension measured by this unit - * @param name - * name of unit - * @param symbol - * symbol of unit - * @return base unit - * @since 2019-10-16 - */ - public static BaseUnit valueOf(final BaseDimension dimension, final String name, final String symbol) { - return new BaseUnit(dimension, name, symbol); - } - - private final BaseDimension dimension; - private final String name; - private final String symbol; - - /** - * Creates the {@code BaseUnit}. - * - * @param dimension - * dimension of unit - * @param name - * name of unit - * @param symbol - * symbol of unit - * @throws NullPointerException - * if any argument is null - * @since 2019-10-16 - */ - private BaseUnit(final BaseDimension dimension, final String 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 a {@code LinearUnit} with this unit as a base and a conversion factor of 1. This operation must be done - * in order to allow units to be created with operations. - * - * @return this unit as a {@code LinearUnit} - * @since 2019-10-16 - */ - public LinearUnit asLinearUnit() { - return LinearUnit.valueOf(this.getBase(), 1); - } - - @Override - public double convertFromBase(final double value) { - return value; - } - - @Override - public double convertToBase(final double value) { - return value; - } - - /** - * @return dimension - * @since 2019-10-16 - */ - public final BaseDimension getBaseDimension() { - return this.dimension; - } - - /** - * @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/newUnits/FunctionalUnit.java b/src/org/unitConverter/newUnits/FunctionalUnit.java deleted file mode 100644 index 6bff3e8..0000000 --- a/src/org/unitConverter/newUnits/FunctionalUnit.java +++ /dev/null @@ -1,98 +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 . - */ -package org.unitConverter.newUnits; - -import java.util.Objects; -import java.util.function.DoubleUnaryOperator; - -import org.unitConverter.math.ObjectProduct; - -/** - * A unit that uses functional objects to convert to and from its base. - * - * @author Adrien Hopkins - * @since 2019-05-22 - */ -final class FunctionalUnit extends Unit { - /** - * Returns a unit from its base and the functions it uses to convert to and from its base. - * - * @param base - * unit's base - * @param converterFrom - * function that accepts a value expressed in the unit's base and returns that value expressed in this - * unit. - * @param converterTo - * function that accepts a value expressed in the unit and returns that value expressed in the unit's - * base. - * @return a unit that uses the provided functions to convert. - * @since 2019-05-22 - * @throws NullPointerException - * if any argument is null - */ - public static FunctionalUnit valueOf(final ObjectProduct base, final DoubleUnaryOperator converterFrom, - final DoubleUnaryOperator converterTo) { - return new FunctionalUnit(base, converterFrom, converterTo); - } - - /** - * A function that accepts a value expressed in the unit's base and returns that value expressed in this unit. - * - * @since 2019-05-22 - */ - private final DoubleUnaryOperator converterFrom; - - /** - * A function that accepts a value expressed in the unit and returns that value expressed in the unit's base. - * - * @since 2019-05-22 - */ - private final DoubleUnaryOperator converterTo; - - /** - * Creates the {@code FunctionalUnit}. - * - * @param base - * unit's base - * @param converterFrom - * function that accepts a value expressed in the unit's base and returns that value expressed in this - * unit. - * @param converterTo - * function that accepts a value expressed in the unit and returns that value expressed in the unit's - * base. - * @throws NullPointerException - * if any argument is null - * @since 2019-05-22 - */ - private FunctionalUnit(final ObjectProduct base, final DoubleUnaryOperator converterFrom, - final DoubleUnaryOperator converterTo) { - super(base); - this.converterFrom = Objects.requireNonNull(converterFrom, "converterFrom must not be null."); - this.converterTo = Objects.requireNonNull(converterTo, "converterTo must not be null."); - } - - @Override - public double convertFromBase(final double value) { - return this.converterFrom.applyAsDouble(value); - } - - @Override - public double convertToBase(final double value) { - return this.converterTo.applyAsDouble(value); - } - -} diff --git a/src/org/unitConverter/newUnits/LinearUnit.java b/src/org/unitConverter/newUnits/LinearUnit.java deleted file mode 100644 index c8c610e..0000000 --- a/src/org/unitConverter/newUnits/LinearUnit.java +++ /dev/null @@ -1,288 +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 . - */ -package org.unitConverter.newUnits; - -import java.util.Objects; - -import org.unitConverter.math.DecimalComparison; -import org.unitConverter.math.ObjectProduct; - -/** - * A unit that can be expressed as a product of its base and a number. For example, kilometres, inches and pounds. - * - * @author Adrien Hopkins - * @since 2019-10-16 - */ -public final class LinearUnit extends Unit { - /** - * Gets a {@code LinearUnit} from a unit and a value. For example, converts '59 °F' to a linear unit with the value - * of '288.15 K' - * - * @param unit - * unit to convert - * @param value - * value to convert - * @return value expressed as a {@code LinearUnit} - * @since 2019-10-16 - */ - 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}. - * - * @param unitBase - * unit base to multiply by - * @param conversionFactor - * number to multiply base by - * @return product of base and conversion factor - * @since 2019-10-16 - */ - public static LinearUnit valueOf(final ObjectProduct unitBase, final double conversionFactor) { - return new LinearUnit(unitBase, conversionFactor); - } - - /** - * The value of this unit as represented in its base form. Mathematically, - * - *
-	 * this = conversionFactor * getBase()
-	 * 
- * - * @since 2019-10-16 - */ - private final double conversionFactor; - - /** - * Creates the {@code LinearUnit}. - * - * @param unitBase - * base of linear unit - * @param conversionFactor - * conversion factor between base and unit - * @since 2019-10-16 - */ - private LinearUnit(final ObjectProduct unitBase, final double conversionFactor) { - super(unitBase); - this.conversionFactor = conversionFactor; - } - - @Override - protected double convertFromBase(final double value) { - return value / this.getConversionFactor(); - } - - @Override - protected double convertToBase(final double value) { - return value * this.getConversionFactor(); - } - - /** - * Divides this unit by a scalar. - * - * @param divisor - * scalar to divide by - * @return quotient - * @since 2018-12-23 - * @since v0.1.0 - */ - public LinearUnit dividedBy(final double divisor) { - return valueOf(this.getBase(), this.getConversionFactor() / divisor); - } - - /** - * Returns the quotient of this unit and another. - * - * @param divisor - * unit to divide by - * @return quotient of two units - * @throws NullPointerException - * if {@code divisor} is null - * @since 2018-12-22 - * @since v0.1.0 - */ - public LinearUnit dividedBy(final LinearUnit divisor) { - Objects.requireNonNull(divisor, "other must not be null"); - - // divide the units - final ObjectProduct base = this.getBase().dividedBy(divisor.getBase()); - return valueOf(base, this.getConversionFactor() / divisor.getConversionFactor()); - } - - @Override - public boolean equals(final Object obj) { - if (!(obj instanceof LinearUnit)) - return false; - final LinearUnit other = (LinearUnit) obj; - return Objects.equals(this.getBase(), other.getBase()) - && DecimalComparison.equals(this.getConversionFactor(), other.getConversionFactor()); - } - - /** - * @return conversion factor - * @since 2019-10-16 - */ - public double getConversionFactor() { - return this.conversionFactor; - } - - @Override - public int hashCode() { - return 31 * this.getBase().hashCode() + DecimalComparison.hash(this.getConversionFactor()); - } - - /** - * @return whether this unit is equivalent to a {@code BaseUnit} (i.e. there is a {@code BaseUnit b} where - * {@code b.asLinearUnit().equals(this)} returns {@code true}.) - * @since 2019-10-16 - */ - public boolean isBase() { - return this.isCoherent() && this.getBase().isSingleObject(); - } - - /** - * @return whether this unit is coherent (i.e. has conversion factor 1) - * @since 2019-10-16 - */ - public boolean isCoherent() { - return this.getConversionFactor() == 1; - } - - /** - * Returns the difference of this unit and another. - *

- * 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. - *

- * - * @param subtrahend - * unit to subtract - * @return difference of units - * @throws IllegalArgumentException - * if {@code subtrahend} is not compatible for subtraction as described above - * @throws NullPointerException - * if {@code subtrahend} is null - * @since 2019-03-17 - * @since v0.2.0 - */ - public LinearUnit minus(final LinearUnit subtrahendend) { - Objects.requireNonNull(subtrahendend, "addend must not be null."); - - // reject subtrahends that cannot be added to this unit - if (!this.getBase().equals(subtrahendend.getBase())) - throw new IllegalArgumentException( - String.format("Incompatible units for subtraction \"%s\" and \"%s\".", this, subtrahendend)); - - // add the units - return valueOf(this.getBase(), this.getConversionFactor() - subtrahendend.getConversionFactor()); - } - - /** - * Returns the sum of this unit and another. - *

- * 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. - *

- * - * @param addend - * unit to add - * @return sum of units - * @throws IllegalArgumentException - * if {@code addend} is not compatible for addition as described above - * @throws NullPointerException - * if {@code addend} is null - * @since 2019-03-17 - * @since v0.2.0 - */ - public LinearUnit plus(final LinearUnit addend) { - Objects.requireNonNull(addend, "addend must not be null."); - - // reject addends that cannot be added to this unit - if (!this.getBase().equals(addend.getBase())) - throw new IllegalArgumentException( - String.format("Incompatible units for addition \"%s\" and \"%s\".", this, addend)); - - // add the units - return valueOf(this.getBase(), this.getConversionFactor() + addend.getConversionFactor()); - } - - /** - * Multiplies this unit by a scalar. - * - * @param multiplier - * scalar to multiply by - * @return product - * @since 2018-12-23 - * @since v0.1.0 - */ - public LinearUnit times(final double multiplier) { - return valueOf(this.getBase(), this.getConversionFactor() * multiplier); - } - - /** - * Returns the product of this unit and another. - * - * @param multiplier - * unit to multiply by - * @return product of two units - * @throws NullPointerException - * if {@code multiplier} is null - * @since 2018-12-22 - * @since v0.1.0 - */ - public LinearUnit times(final LinearUnit multiplier) { - Objects.requireNonNull(multiplier, "other must not be null"); - - // multiply the units - final ObjectProduct base = this.getBase().times(multiplier.getBase()); - return valueOf(base, this.getConversionFactor() * multiplier.getConversionFactor()); - } - - /** - * Returns this unit but to an exponent. - * - * @param exponent - * exponent to exponentiate unit to - * @return exponentiated unit - * @since 2019-01-15 - * @since v0.1.0 - */ - public LinearUnit toExponent(final int exponent) { - return valueOf(this.getBase().toExponent(exponent), Math.pow(this.conversionFactor, exponent)); - } - - // returns a definition of the unit - @Override - public String toString() { - return Double.toString(this.conversionFactor) + " * " + this.getBase().toString(BaseUnit::getSymbol); - } - - /** - * Returns the result of applying {@code prefix} to this unit. - * - * @param prefix - * prefix to apply - * @return unit with prefix - * @since 2019-03-18 - * @since v0.2.0 - */ - public LinearUnit withPrefix(final UnitPrefix prefix) { - return this.times(prefix.getMultiplier()); - } -} diff --git a/src/org/unitConverter/newUnits/SI.java b/src/org/unitConverter/newUnits/SI.java deleted file mode 100644 index b7a117a..0000000 --- a/src/org/unitConverter/newUnits/SI.java +++ /dev/null @@ -1,228 +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 . - */ -package org.unitConverter.newUnits; - -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. - * - *

- * This class does not include prefixed units. To obtain prefixed units, use {@link LinearUnit#withPrefix}: - * - *

- * LinearUnit KILOMETRE = SI.METRE.withPrefix(SI.KILO);
- * 
- * - * - * @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 EMPTY = ObjectProduct.empty(); - public static final ObjectProduct LENGTH = ObjectProduct.oneOf(BaseDimensions.LENGTH); - public static final ObjectProduct MASS = ObjectProduct.oneOf(BaseDimensions.MASS); - public static final ObjectProduct TIME = ObjectProduct.oneOf(BaseDimensions.TIME); - public static final ObjectProduct ELECTRIC_CURRENT = ObjectProduct - .oneOf(BaseDimensions.ELECTRIC_CURRENT); - public static final ObjectProduct TEMPERATURE = ObjectProduct.oneOf(BaseDimensions.TEMPERATURE); - public static final ObjectProduct QUANTITY = ObjectProduct.oneOf(BaseDimensions.QUANTITY); - public static final ObjectProduct LUMINOUS_INTENSITY = ObjectProduct - .oneOf(BaseDimensions.LUMINOUS_INTENSITY); - public static final ObjectProduct INFORMATION = ObjectProduct.oneOf(BaseDimensions.INFORMATION); - public static final ObjectProduct CURRENCY = ObjectProduct.oneOf(BaseDimensions.CURRENCY); - // derived dimensions without named SI units - public static final ObjectProduct AREA = LENGTH.times(LENGTH); - - public static final ObjectProduct VOLUME = AREA.times(LENGTH); - public static final ObjectProduct VELOCITY = LENGTH.dividedBy(TIME); - public static final ObjectProduct ACCELERATION = VELOCITY.dividedBy(TIME); - public static final ObjectProduct WAVENUMBER = EMPTY.dividedBy(LENGTH); - public static final ObjectProduct MASS_DENSITY = MASS.dividedBy(VOLUME); - public static final ObjectProduct SURFACE_DENSITY = MASS.dividedBy(AREA); - public static final ObjectProduct SPECIFIC_VOLUME = VOLUME.dividedBy(MASS); - public static final ObjectProduct CURRENT_DENSITY = ELECTRIC_CURRENT.dividedBy(AREA); - public static final ObjectProduct MAGNETIC_FIELD_STRENGTH = ELECTRIC_CURRENT.dividedBy(LENGTH); - public static final ObjectProduct CONCENTRATION = QUANTITY.dividedBy(VOLUME); - public static final ObjectProduct MASS_CONCENTRATION = CONCENTRATION.times(MASS); - public static final ObjectProduct LUMINANCE = LUMINOUS_INTENSITY.dividedBy(AREA); - public static final ObjectProduct REFRACTIVE_INDEX = VELOCITY.dividedBy(VELOCITY); - public static final ObjectProduct REFLACTIVE_PERMEABILITY = EMPTY.times(EMPTY); - public static final ObjectProduct ANGLE = LENGTH.dividedBy(LENGTH); - public static final ObjectProduct SOLID_ANGLE = AREA.dividedBy(AREA); - // derived dimensions with named SI units - public static final ObjectProduct FREQUENCY = EMPTY.dividedBy(TIME); - - public static final ObjectProduct FORCE = MASS.times(ACCELERATION); - public static final ObjectProduct ENERGY = FORCE.times(LENGTH); - public static final ObjectProduct POWER = ENERGY.dividedBy(TIME); - public static final ObjectProduct ELECTRIC_CHARGE = ELECTRIC_CURRENT.times(TIME); - public static final ObjectProduct VOLTAGE = ENERGY.dividedBy(ELECTRIC_CHARGE); - public static final ObjectProduct CAPACITANCE = ELECTRIC_CHARGE.dividedBy(VOLTAGE); - public static final ObjectProduct ELECTRIC_RESISTANCE = VOLTAGE.dividedBy(ELECTRIC_CURRENT); - public static final ObjectProduct ELECTRIC_CONDUCTANCE = ELECTRIC_CURRENT.dividedBy(VOLTAGE); - public static final ObjectProduct MAGNETIC_FLUX = VOLTAGE.times(TIME); - public static final ObjectProduct MAGNETIC_FLUX_DENSITY = MAGNETIC_FLUX.dividedBy(AREA); - public static final ObjectProduct INDUCTANCE = MAGNETIC_FLUX.dividedBy(ELECTRIC_CURRENT); - public static final ObjectProduct LUMINOUS_FLUX = LUMINOUS_INTENSITY.times(SOLID_ANGLE); - public static final ObjectProduct ILLUMINANCE = LUMINOUS_FLUX.dividedBy(AREA); - public static final ObjectProduct SPECIFIC_ENERGY = ENERGY.dividedBy(MASS); - public static final ObjectProduct 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/newUnits/Unit.java b/src/org/unitConverter/newUnits/Unit.java deleted file mode 100644 index 339ab95..0000000 --- a/src/org/unitConverter/newUnits/Unit.java +++ /dev/null @@ -1,205 +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 . - */ -package org.unitConverter.newUnits; - -import java.util.HashMap; -import java.util.Map; -import java.util.Objects; -import java.util.function.DoubleUnaryOperator; - -import org.unitConverter.math.ObjectProduct; - -/** - * A unit that is composed of base units. - * - * @author Adrien Hopkins - * @since 2019-10-16 - */ -public abstract class Unit { - /** - * Returns a unit from its base and the functions it uses to convert to and from its base. - * - *

- * For example, to get a unit representing the degree Celsius, the following code can be used: - * - * {@code Unit.fromConversionFunctions(SI.KELVIN, tempK -> tempK - 273.15, tempC -> tempC + 273.15);} - *

- * - * @param base - * unit's base - * @param converterFrom - * function that accepts a value expressed in the unit's base and returns that value expressed in this - * unit. - * @param converterTo - * function that accepts a value expressed in the unit and returns that value expressed in the unit's - * base. - * @return a unit that uses the provided functions to convert. - * @since 2019-05-22 - * @throws NullPointerException - * if any argument is null - */ - public static Unit fromConversionFunctions(final ObjectProduct 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 unitBase; - - /** - * Cache storing the result of getDimension() - * - * @since 2019-10-16 - */ - private transient ObjectProduct 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 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 - * unit to test with - * @return true if the units are compatible - * @since 2019-01-13 - * @since v0.1.0 - * @throws NullPointerException - * if other is null - */ - public final boolean canConvertTo(final Unit other) { - Objects.requireNonNull(other, "other must not be null."); - return Objects.equals(this.getBase(), other.getBase()); - } - - /** - * Converts from a value expressed in this unit's base unit to a value expressed in this unit. - *

- * This must be the inverse of {@code convertToBase}, so {@code convertFromBase(convertToBase(value))} must be equal - * to {@code value} for any value, ignoring precision loss by roundoff error. - *

- *

- * If this unit is a base unit, this method should return {@code value}. - *

- * - * @param value - * value expressed in base unit - * @return value expressed in this unit - * @since 2018-12-22 - * @since v0.1.0 - */ - protected abstract double convertFromBase(double value); - - /** - * Converts a value expressed in this unit to a value expressed in {@code other}. - * - * @param other - * unit to convert to - * @param value - * value to convert - * @return converted value - * @since 2019-05-22 - * @throws IllegalArgumentException - * if {@code other} is incompatible for conversion with this unit (as tested by - * {@link IUnit#canConvertTo}). - * @throws NullPointerException - * if other is null - */ - public final double convertTo(final Unit other, final double value) { - Objects.requireNonNull(other, "other must not be null."); - if (this.canConvertTo(other)) - return other.convertFromBase(this.convertToBase(value)); - else - throw new IllegalArgumentException(String.format("Cannot convert from %s to %s.", this, other)); - } - - /** - * Converts from a value expressed in this unit to a value expressed in this unit's base unit. - *

- * This must be the inverse of {@code convertFromBase}, so {@code convertToBase(convertFromBase(value))} must be - * equal to {@code value} for any value, ignoring precision loss by roundoff error. - *

- *

- * If this unit is a base unit, this method should return {@code value}. - *

- * - * @param value - * value expressed in this unit - * @return value expressed in base unit - * @since 2018-12-22 - * @since v0.1.0 - */ - protected abstract double convertToBase(double value); - - /** - * @return combination of units that this unit is based on - * @since 2018-12-22 - * @since v0.1.0 - */ - public final ObjectProduct getBase() { - return this.unitBase; - } - - /** - * @return dimension measured by this unit - * @since 2018-12-22 - * @since v0.1.0 - */ - public final ObjectProduct getDimension() { - if (this.dimension == null) { - final Map mapping = this.unitBase.exponentMap(); - final Map dimensionMap = new HashMap<>(); - - 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/newUnits/UnitPrefix.java b/src/org/unitConverter/newUnits/UnitPrefix.java deleted file mode 100644 index 5608098..0000000 --- a/src/org/unitConverter/newUnits/UnitPrefix.java +++ /dev/null @@ -1,146 +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 . - */ -package org.unitConverter.newUnits; - -import org.unitConverter.math.DecimalComparison; - -/** - * A prefix that can be applied to a {@code LinearUnit} to multiply it by some value - * - * @author Adrien Hopkins - * @since 2019-10-16 - */ -public final class UnitPrefix { - /** - * Gets a {@code UnitPrefix} from a multiplier - * - * @param multiplier - * multiplier of prefix - * @return prefix - * @since 2019-10-16 - */ - public static UnitPrefix valueOf(final double multiplier) { - return new UnitPrefix(multiplier); - } - - /** - * 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}. - * - * @param other - * prefix to divide by - * @return quotient of prefixes - * @since 2019-04-13 - * @since v0.2.0 - */ - public UnitPrefix dividedBy(final UnitPrefix other) { - return valueOf(this.getMultiplier() / other.getMultiplier()); - } - - @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()); - } - - /** - * Multiplies this prefix by a scalar - * - * @param multiplicand - * number to multiply by - * @return product of prefix and scalar - * @since 2019-10-16 - */ - public UnitPrefix times(final double multiplicand) { - return valueOf(this.getMultiplier() * multiplicand); - } - - /** - * Multiplies this prefix by {@code other}. - * - * @param other - * prefix to multiply by - * @return product of prefixes - * @since 2019-04-13 - * @since v0.2.0 - */ - public UnitPrefix times(final UnitPrefix other) { - return valueOf(this.getMultiplier() * other.getMultiplier()); - } - - /** - * Raises this prefix to an exponent. - * - * @param exponent - * exponent to raise to - * @return result of exponentiation. - * @since 2019-04-13 - * @since v0.2.0 - */ - public UnitPrefix toExponent(final double exponent) { - return valueOf(Math.pow(this.getMultiplier(), exponent)); - } - - @Override - public String toString() { - return String.format("Unit prefix equal to %s", this.multiplier); - } -} diff --git a/src/org/unitConverter/newUnits/UnitTest.java b/src/org/unitConverter/newUnits/UnitTest.java deleted file mode 100644 index 33bd264..0000000 --- a/src/org/unitConverter/newUnits/UnitTest.java +++ /dev/null @@ -1,102 +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 . - */ -package org.unitConverter.newUnits; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import java.util.Random; -import java.util.concurrent.ThreadLocalRandom; - -import org.junit.jupiter.api.Test; -import org.unitConverter.math.DecimalComparison; - -/** - * Testing the various Unit classes. This is NOT part of this program's public API. - * - * @author Adrien Hopkins - * @since 2018-12-22 - * @since v0.1.0 - */ -class UnitTest { - /** A random number generator */ - private static final Random rng = ThreadLocalRandom.current(); - - @Test - public void testAdditionAndSubtraction() { - final LinearUnit inch = SI.METRE.times(0.0254); - final LinearUnit foot = SI.METRE.times(0.3048); - - assertEquals(inch.plus(foot), SI.METRE.times(0.3302)); - assertEquals(foot.minus(inch), SI.METRE.times(0.2794)); - } - - @Test - public void testConversion() { - final LinearUnit metre = SI.METRE; - final Unit inch = metre.times(0.0254); - - assertEquals(1.9, inch.convertTo(metre, 75), 0.01); - - // try random stuff - for (int i = 0; i < 1000; i++) { - // initiate random values - final double conversionFactor = rng.nextDouble() * 1000000; - final double testValue = rng.nextDouble() * 1000000; - final double expected = testValue * conversionFactor; - - // test - final Unit unit = SI.METRE.times(conversionFactor); - final double actual = unit.convertToBase(testValue); - - assertEquals(actual, expected, expected * DecimalComparison.DOUBLE_EPSILON); - } - } - - @Test - public void testEquals() { - final LinearUnit metre = SI.METRE; - final Unit meter = SI.BaseUnits.METRE.asLinearUnit(); - - assertEquals(metre, meter); - } - - @Test - 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.JOULE; - - assertEquals(generatedJoule, actualJoule); - - // test multiplication by conversion factors - final LinearUnit kilometre = SI.METRE.times(1000); - final LinearUnit hour = SI.SECOND.times(3600); - final LinearUnit generatedKPH = kilometre.dividedBy(hour); - - final LinearUnit actualKPH = SI.METRE.dividedBy(SI.SECOND).dividedBy(3.6); - - assertEquals(generatedKPH, actualKPH); - } - - @Test - public void testPrefixes() { - final LinearUnit generatedKilometre = SI.METRE.withPrefix(SI.KILO); - final LinearUnit actualKilometre = SI.METRE.times(1000); - - assertEquals(generatedKilometre, actualKilometre); - } -} diff --git a/src/org/unitConverter/newUnits/package-info.java b/src/org/unitConverter/newUnits/package-info.java deleted file mode 100644 index 9cd0d1a..0000000 --- a/src/org/unitConverter/newUnits/package-info.java +++ /dev/null @@ -1,23 +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 . - */ -/** - * The new definition for units. - * - * @author Adrien Hopkins - * @since 2019-10-16 - */ -package org.unitConverter.newUnits; \ 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 . - */ -package org.unitConverter.unit; - -import java.util.Objects; - -import org.unitConverter.dimension.UnitDimension; - -/** - * The default abstract implementation of the {@code Unit} interface. - * - *

- * 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}. - *

- * - * @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 . + */ +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. - *

- * {@code BaseUnit} does not have any public constructors or static factories. There are two ways to obtain - * {@code BaseUnit} instances. - *

    - *
  1. 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: - * - *
    - * BaseUnit JOULE = SI.KILOGRAM.times(SI.METRE.toExponent(2)).dividedBy(SI.SECOND.toExponent(2));
    - * 
    - * - *
  2. - *
  3. 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: - * - *
    - * BaseUnit JOULE = SI.SI.getBaseUnit(StandardDimensions.ENERGY);
    - * 
    - * - *
  4. - *
+ * 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. - *

- * 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. - *

+ * 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. - *

- * 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. - *

- * - * @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/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 . - */ -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 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 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..c397250 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. - *

- * {@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: - * - *

- * LinearUnit foot = METRE.times(0.3048);
- * 
- * - * (where {@code METRE} is a {@code BaseUnit} instance) - *

+ * 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 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 + *
+	 * this = conversionFactor * getBase()
+	 * 
+ * + * @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 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. - *

- * 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. - *

* * @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 base = this.getBase().dividedBy(divisor.getBase()); + return valueOf(base, this.getConversionFactor() / divisor.getConversionFactor()); } @Override @@ -136,28 +130,38 @@ 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; } /** @@ -186,7 +190,7 @@ 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()); } /** @@ -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. - *

- * 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. - *

* * @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 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/NonlinearUnits.java b/src/org/unitConverter/unit/NonlinearUnits.java deleted file mode 100644 index eda4a74..0000000 --- a/src/org/unitConverter/unit/NonlinearUnits.java +++ /dev/null @@ -1,37 +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 . - */ -package org.unitConverter.unit; - -/** - * Some major nonlinear units. - * - * @author Adrien Hopkins - * @since 2019-01-14 - * @since v0.1.0 - */ -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(); - } -} 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 . - */ -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 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 . + */ +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. + * + *

+ * This class does not include prefixed units. To obtain prefixed units, use {@link LinearUnit#withPrefix}: + * + *

+ * LinearUnit KILOMETRE = SI.METRE.withPrefix(SI.KILO);
+ * 
+ * + * + * @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 EMPTY = ObjectProduct.empty(); + public static final ObjectProduct LENGTH = ObjectProduct.oneOf(BaseDimensions.LENGTH); + public static final ObjectProduct MASS = ObjectProduct.oneOf(BaseDimensions.MASS); + public static final ObjectProduct TIME = ObjectProduct.oneOf(BaseDimensions.TIME); + public static final ObjectProduct ELECTRIC_CURRENT = ObjectProduct + .oneOf(BaseDimensions.ELECTRIC_CURRENT); + public static final ObjectProduct TEMPERATURE = ObjectProduct.oneOf(BaseDimensions.TEMPERATURE); + public static final ObjectProduct QUANTITY = ObjectProduct.oneOf(BaseDimensions.QUANTITY); + public static final ObjectProduct LUMINOUS_INTENSITY = ObjectProduct + .oneOf(BaseDimensions.LUMINOUS_INTENSITY); + public static final ObjectProduct INFORMATION = ObjectProduct.oneOf(BaseDimensions.INFORMATION); + public static final ObjectProduct CURRENCY = ObjectProduct.oneOf(BaseDimensions.CURRENCY); + // derived dimensions without named SI units + public static final ObjectProduct AREA = LENGTH.times(LENGTH); + + public static final ObjectProduct VOLUME = AREA.times(LENGTH); + public static final ObjectProduct VELOCITY = LENGTH.dividedBy(TIME); + public static final ObjectProduct ACCELERATION = VELOCITY.dividedBy(TIME); + public static final ObjectProduct WAVENUMBER = EMPTY.dividedBy(LENGTH); + public static final ObjectProduct MASS_DENSITY = MASS.dividedBy(VOLUME); + public static final ObjectProduct SURFACE_DENSITY = MASS.dividedBy(AREA); + public static final ObjectProduct SPECIFIC_VOLUME = VOLUME.dividedBy(MASS); + public static final ObjectProduct CURRENT_DENSITY = ELECTRIC_CURRENT.dividedBy(AREA); + public static final ObjectProduct MAGNETIC_FIELD_STRENGTH = ELECTRIC_CURRENT.dividedBy(LENGTH); + public static final ObjectProduct CONCENTRATION = QUANTITY.dividedBy(VOLUME); + public static final ObjectProduct MASS_CONCENTRATION = CONCENTRATION.times(MASS); + public static final ObjectProduct LUMINANCE = LUMINOUS_INTENSITY.dividedBy(AREA); + public static final ObjectProduct REFRACTIVE_INDEX = VELOCITY.dividedBy(VELOCITY); + public static final ObjectProduct REFLACTIVE_PERMEABILITY = EMPTY.times(EMPTY); + public static final ObjectProduct ANGLE = LENGTH.dividedBy(LENGTH); + public static final ObjectProduct SOLID_ANGLE = AREA.dividedBy(AREA); + // derived dimensions with named SI units + public static final ObjectProduct FREQUENCY = EMPTY.dividedBy(TIME); + + public static final ObjectProduct FORCE = MASS.times(ACCELERATION); + public static final ObjectProduct ENERGY = FORCE.times(LENGTH); + public static final ObjectProduct POWER = ENERGY.dividedBy(TIME); + public static final ObjectProduct ELECTRIC_CHARGE = ELECTRIC_CURRENT.times(TIME); + public static final ObjectProduct VOLTAGE = ENERGY.dividedBy(ELECTRIC_CHARGE); + public static final ObjectProduct CAPACITANCE = ELECTRIC_CHARGE.dividedBy(VOLTAGE); + public static final ObjectProduct ELECTRIC_RESISTANCE = VOLTAGE.dividedBy(ELECTRIC_CURRENT); + public static final ObjectProduct ELECTRIC_CONDUCTANCE = ELECTRIC_CURRENT.dividedBy(VOLTAGE); + public static final ObjectProduct MAGNETIC_FLUX = VOLTAGE.times(TIME); + public static final ObjectProduct MAGNETIC_FLUX_DENSITY = MAGNETIC_FLUX.dividedBy(AREA); + public static final ObjectProduct INDUCTANCE = MAGNETIC_FLUX.dividedBy(ELECTRIC_CURRENT); + public static final ObjectProduct LUMINOUS_FLUX = LUMINOUS_INTENSITY.times(SOLID_ANGLE); + public static final ObjectProduct ILLUMINANCE = LUMINOUS_FLUX.dividedBy(AREA); + public static final ObjectProduct SPECIFIC_ENERGY = ENERGY.dividedBy(MASS); + public static final ObjectProduct 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 . - */ -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/unit/Unit.java b/src/org/unitConverter/unit/Unit.java index 54f0ab5..d4eb86e 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,11 +52,49 @@ 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 Unit fromConversionFunctions(final ObjectProduct 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 unitBase; + + /** + * Cache storing the result of getDimension() + * + * @since 2019-10-16 + */ + private transient ObjectProduct 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 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} * @@ -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()); } @@ -88,7 +127,7 @@ public interface 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}. @@ -101,11 +140,11 @@ public interface Unit { * @since 2019-05-22 * @throws IllegalArgumentException * if {@code other} is incompatible for conversion with this unit (as tested by - * {@link Unit#canConvertTo}). + * {@link IUnit#canConvertTo}). * @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)); @@ -129,36 +168,38 @@ public interface Unit { * @since 2018-12-22 * @since v0.1.0 */ - double convertToBase(double value); + protected abstract double convertToBase(double value); /** - *

- * Returns the base unit associated with this unit. - *

- *

- * The dimension of this unit must be equal to the dimension of the returned unit. - *

- *

- * If this unit is a base unit, this method should return this unit.\ - *

- * - * @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 getBase() { + return this.unitBase; + } /** * @return dimension measured by this unit * @since 2018-12-22 * @since v0.1.0 */ - UnitDimension getDimension(); + public final ObjectProduct getDimension() { + if (this.dimension == null) { + final Map mapping = this.unitBase.exponentMap(); + final Map 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/unit/UnitDatabase.java b/src/org/unitConverter/unit/UnitDatabase.java new file mode 100644 index 0000000..a2b11c3 --- /dev/null +++ b/src/org/unitConverter/unit/UnitDatabase.java @@ -0,0 +1,1653 @@ +/** + * 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 . + */ +package org.unitConverter.unit; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.IOException; +import java.util.AbstractSet; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Objects; +import java.util.Set; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.unitConverter.math.DecimalComparison; +import org.unitConverter.math.ExpressionParser; +import org.unitConverter.math.ObjectProduct; + +/** + * A database of units, prefixes and dimensions, and their names. + * + * @author Adrien Hopkins + * @since 2019-01-07 + * @since v0.1.0 + */ +public final class UnitDatabase { + /** + * A map for units that allows the use of prefixes. + *

+ * As this map implementation is intended to be used as a sort of "augmented view" of a unit and prefix map, it is + * unmodifiable but instead reflects the changes to the maps passed into it. Do not edit this map, instead edit the + * maps that were passed in during construction. + *

+ *

+ * The rules for applying prefixes onto units are the following: + *

    + *
  • Prefixes can only be applied to linear units.
  • + *
  • Before attempting to search for prefixes in a unit name, this map will first search for a unit name. So, if + * there are two units, "B" and "AB", and a prefix "A", this map will favour the unit "AB" over the unit "B" with + * the prefix "A", even though they have the same string.
  • + *
  • Longer prefixes are preferred to shorter prefixes. So, if you have units "BC" and "C", and prefixes "AB" and + * "A", inputting "ABC" will return the unit "C" with the prefix "AB", not "BC" with the prefix "A".
  • + *
+ *

+ *

+ * This map is infinite in size if there is at least one unit and at least one prefix. If it is infinite, some + * operations that only work with finite collections, like converting name/entry sets to arrays, will throw an + * {@code IllegalStateException}. + *

+ *

+ * Because of ambiguities between prefixes (i.e. kilokilo = mega), {@link #containsValue} and {@link #values()} + * currently ignore prefixes. + *

+ * + * @author Adrien Hopkins + * @since 2019-04-13 + * @since v0.2.0 + */ + private static final class PrefixedUnitMap implements Map { + /** + * The class used for entry sets. + * + *

+ * If the map that created this set is infinite in size (has at least one unit and at least one prefix), this + * set is infinite as well. If this set is infinite in size, {@link #toArray} will fail with a + * {@code IllegalStateException} instead of creating an infinite-sized array. + *

+ * + * @author Adrien Hopkins + * @since 2019-04-13 + * @since v0.2.0 + */ + private static final class PrefixedUnitEntrySet extends AbstractSet> { + /** + * The entry for this set. + * + * @author Adrien Hopkins + * @since 2019-04-14 + * @since v0.2.0 + */ + private static final class PrefixedUnitEntry implements Entry { + private final String key; + private final Unit value; + + /** + * Creates the {@code PrefixedUnitEntry}. + * + * @param key + * key + * @param value + * value + * @since 2019-04-14 + * @since v0.2.0 + */ + public PrefixedUnitEntry(final String key, final Unit value) { + this.key = key; + this.value = value; + } + + /** + * @since 2019-05-03 + */ + @Override + public boolean equals(final Object o) { + if (!(o instanceof Map.Entry)) + return false; + final Map.Entry other = (Map.Entry) o; + return Objects.equals(this.getKey(), other.getKey()) + && Objects.equals(this.getValue(), other.getValue()); + } + + @Override + public String getKey() { + return this.key; + } + + @Override + public Unit getValue() { + return this.value; + } + + /** + * @since 2019-05-03 + */ + @Override + public int hashCode() { + return (this.getKey() == null ? 0 : this.getKey().hashCode()) + ^ (this.getValue() == null ? 0 : this.getValue().hashCode()); + } + + @Override + public Unit setValue(final Unit value) { + throw new UnsupportedOperationException("Cannot set value in an immutable entry"); + } + + /** + * Returns a string representation of the entry. The format of the string is the string representation + * of the key, then the equals ({@code =}) character, then the string representation of the value. + * + * @since 2019-05-03 + */ + @Override + public String toString() { + return this.getKey() + "=" + this.getValue(); + } + } + + /** + * An iterator that iterates over the units of a {@code PrefixedUnitNameSet}. + * + * @author Adrien Hopkins + * @since 2019-04-14 + * @since v0.2.0 + */ + private static final class PrefixedUnitEntryIterator implements Iterator> { + // position in the unit list + private int unitNamePosition = 0; + // the indices of the prefixes attached to the current unit + private final List prefixCoordinates = new ArrayList<>(); + + // values from the unit entry set + private final Map map; + private transient final List unitNames; + private transient final List prefixNames; + + /** + * Creates the {@code UnitsDatabase.PrefixedUnitMap.PrefixedUnitNameSet.PrefixedUnitNameIterator}. + * + * @since 2019-04-14 + * @since v0.2.0 + */ + public PrefixedUnitEntryIterator(final PrefixedUnitMap map) { + this.map = map; + this.unitNames = new ArrayList<>(map.units.keySet()); + this.prefixNames = new ArrayList<>(map.prefixes.keySet()); + } + + /** + * @return current unit name + * @since 2019-04-14 + * @since v0.2.0 + */ + private String getCurrentUnitName() { + final StringBuilder unitName = new StringBuilder(); + for (final int i : this.prefixCoordinates) { + unitName.append(this.prefixNames.get(i)); + } + unitName.append(this.unitNames.get(this.unitNamePosition)); + + return unitName.toString(); + } + + @Override + public boolean hasNext() { + if (this.unitNames.isEmpty()) + return false; + else { + if (this.prefixNames.isEmpty()) + return this.unitNamePosition >= this.unitNames.size() - 1; + else + return true; + } + } + + /** + * Changes this iterator's position to the next available one. + * + * @since 2019-04-14 + * @since v0.2.0 + */ + private void incrementPosition() { + this.unitNamePosition++; + + if (this.unitNamePosition >= this.unitNames.size()) { + // we have used all of our units, go to a different prefix + this.unitNamePosition = 0; + + // if the prefix coordinates are empty, then set it to [0] + if (this.prefixCoordinates.isEmpty()) { + this.prefixCoordinates.add(0, 0); + } else { + // get the prefix coordinate to increment, then increment + int i = this.prefixCoordinates.size() - 1; + this.prefixCoordinates.set(i, this.prefixCoordinates.get(i) + 1); + + // fix any carrying errors + while (i >= 0 && this.prefixCoordinates.get(i) >= this.prefixNames.size()) { + // carry over + this.prefixCoordinates.set(i--, 0); // null and decrement at the same time + + if (i < 0) { // we need to add a new coordinate + this.prefixCoordinates.add(0, 0); + } else { // increment an existing one + this.prefixCoordinates.set(i, this.prefixCoordinates.get(i) + 1); + } + } + } + } + } + + @Override + public Entry next() { + // get next element + final Entry nextEntry = this.peek(); + + // iterate to next position + this.incrementPosition(); + + return nextEntry; + } + + /** + * @return the next element in the iterator, without iterating over it + * @since 2019-05-03 + */ + private Entry peek() { + if (!this.hasNext()) + throw new NoSuchElementException("No units left!"); + + // if I have prefixes, ensure I'm not using a nonlinear unit + // since all of the unprefixed stuff is done, just remove nonlinear units + if (!this.prefixCoordinates.isEmpty()) { + while (this.unitNamePosition < this.unitNames.size() + && !(this.map.get(this.unitNames.get(this.unitNamePosition)) instanceof LinearUnit)) { + this.unitNames.remove(this.unitNamePosition); + } + } + + final String nextName = this.getCurrentUnitName(); + + return new PrefixedUnitEntry(nextName, this.map.get(nextName)); + } + + /** + * Returns a string representation of the object. The exact details of the representation are + * unspecified and subject to change. + * + * @since 2019-05-03 + */ + @Override + public String toString() { + return String.format("Iterator iterating over name-unit entries; next value is \"%s\"", + this.peek()); + } + } + + // the map that created this set + private final PrefixedUnitMap map; + + /** + * Creates the {@code PrefixedUnitNameSet}. + * + * @param map + * map that created this set + * @since 2019-04-13 + * @since v0.2.0 + */ + public PrefixedUnitEntrySet(final PrefixedUnitMap map) { + this.map = map; + } + + @Override + public boolean add(final Map.Entry e) { + throw new UnsupportedOperationException("Cannot add to an immutable set"); + } + + @Override + public boolean addAll(final Collection> c) { + throw new UnsupportedOperationException("Cannot add to an immutable set"); + } + + @Override + public void clear() { + throw new UnsupportedOperationException("Cannot clear an immutable set"); + } + + @Override + public boolean contains(final Object o) { + // get the entry + final Entry entry; + + try { + // This is OK because I'm in a try-catch block, catching the exact exception that would be thrown. + @SuppressWarnings("unchecked") + final Entry tempEntry = (Entry) o; + entry = tempEntry; + } catch (final ClassCastException e) { + throw new IllegalArgumentException("Attempted to test for an entry using a non-entry."); + } + + return this.map.containsKey(entry.getKey()) && this.map.get(entry.getKey()).equals(entry.getValue()); + } + + @Override + public boolean containsAll(final Collection c) { + for (final Object o : c) + if (!this.contains(o)) + return false; + return true; + } + + @Override + public boolean isEmpty() { + return this.map.isEmpty(); + } + + @Override + public Iterator> iterator() { + return new PrefixedUnitEntryIterator(this.map); + } + + @Override + public boolean remove(final Object o) { + throw new UnsupportedOperationException("Cannot remove from an immutable set"); + } + + @Override + public boolean removeAll(final Collection c) { + throw new UnsupportedOperationException("Cannot remove from an immutable set"); + } + + @Override + public boolean removeIf(final Predicate> filter) { + throw new UnsupportedOperationException("Cannot remove from an immutable set"); + } + + @Override + public boolean retainAll(final Collection c) { + throw new UnsupportedOperationException("Cannot remove from an immutable set"); + } + + @Override + public int size() { + if (this.map.units.isEmpty()) + return 0; + else { + if (this.map.prefixes.isEmpty()) + return this.map.units.size(); + else + // infinite set + return Integer.MAX_VALUE; + } + } + + /** + * @throws IllegalStateException + * if the set is infinite in size + */ + @Override + public Object[] toArray() { + if (this.map.units.isEmpty() || this.map.prefixes.isEmpty()) + return super.toArray(); + else + // infinite set + throw new IllegalStateException("Cannot make an infinite set into an array."); + } + + /** + * @throws IllegalStateException + * if the set is infinite in size + */ + @Override + public T[] toArray(final T[] a) { + if (this.map.units.isEmpty() || this.map.prefixes.isEmpty()) + return super.toArray(a); + else + // infinite set + throw new IllegalStateException("Cannot make an infinite set into an array."); + } + + @Override + public String toString() { + if (this.map.units.isEmpty() || this.map.prefixes.isEmpty()) + return super.toString(); + else + return String.format("Infinite set of name-unit entries created from units %s and prefixes %s", + this.map.units, this.map.prefixes); + } + } + + /** + * The class used for unit name sets. + * + *

+ * If the map that created this set is infinite in size (has at least one unit and at least one prefix), this + * set is infinite as well. If this set is infinite in size, {@link #toArray} will fail with a + * {@code IllegalStateException} instead of creating an infinite-sized array. + *

+ * + * @author Adrien Hopkins + * @since 2019-04-13 + * @since v0.2.0 + */ + private static final class PrefixedUnitNameSet extends AbstractSet { + /** + * An iterator that iterates over the units of a {@code PrefixedUnitNameSet}. + * + * @author Adrien Hopkins + * @since 2019-04-14 + * @since v0.2.0 + */ + private static final class PrefixedUnitNameIterator implements Iterator { + // position in the unit list + private int unitNamePosition = 0; + // the indices of the prefixes attached to the current unit + private final List prefixCoordinates = new ArrayList<>(); + + // values from the unit name set + private final Map map; + private transient final List unitNames; + private transient final List prefixNames; + + /** + * Creates the {@code UnitsDatabase.PrefixedUnitMap.PrefixedUnitNameSet.PrefixedUnitNameIterator}. + * + * @since 2019-04-14 + * @since v0.2.0 + */ + public PrefixedUnitNameIterator(final PrefixedUnitMap map) { + this.map = map; + this.unitNames = new ArrayList<>(map.units.keySet()); + this.prefixNames = new ArrayList<>(map.prefixes.keySet()); + } + + /** + * @return current unit name + * @since 2019-04-14 + * @since v0.2.0 + */ + private String getCurrentUnitName() { + final StringBuilder unitName = new StringBuilder(); + for (final int i : this.prefixCoordinates) { + unitName.append(this.prefixNames.get(i)); + } + unitName.append(this.unitNames.get(this.unitNamePosition)); + + return unitName.toString(); + } + + @Override + public boolean hasNext() { + if (this.unitNames.isEmpty()) + return false; + else { + if (this.prefixNames.isEmpty()) + return this.unitNamePosition >= this.unitNames.size() - 1; + else + return true; + } + } + + /** + * Changes this iterator's position to the next available one. + * + * @since 2019-04-14 + * @since v0.2.0 + */ + private void incrementPosition() { + this.unitNamePosition++; + + if (this.unitNamePosition >= this.unitNames.size()) { + // we have used all of our units, go to a different prefix + this.unitNamePosition = 0; + + // if the prefix coordinates are empty, then set it to [0] + if (this.prefixCoordinates.isEmpty()) { + this.prefixCoordinates.add(0, 0); + } else { + // get the prefix coordinate to increment, then increment + int i = this.prefixCoordinates.size() - 1; + this.prefixCoordinates.set(i, this.prefixCoordinates.get(i) + 1); + + // fix any carrying errors + while (i >= 0 && this.prefixCoordinates.get(i) >= this.prefixNames.size()) { + // carry over + this.prefixCoordinates.set(i--, 0); // null and decrement at the same time + + if (i < 0) { // we need to add a new coordinate + this.prefixCoordinates.add(0, 0); + } else { // increment an existing one + this.prefixCoordinates.set(i, this.prefixCoordinates.get(i) + 1); + } + } + } + } + } + + @Override + public String next() { + final String nextName = this.peek(); + + this.incrementPosition(); + + return nextName; + } + + /** + * @return the next element in the iterator, without iterating over it + * @since 2019-05-03 + */ + private String peek() { + if (!this.hasNext()) + throw new NoSuchElementException("No units left!"); + // if I have prefixes, ensure I'm not using a nonlinear unit + // since all of the unprefixed stuff is done, just remove nonlinear units + if (!this.prefixCoordinates.isEmpty()) { + while (this.unitNamePosition < this.unitNames.size() + && !(this.map.get(this.unitNames.get(this.unitNamePosition)) instanceof LinearUnit)) { + this.unitNames.remove(this.unitNamePosition); + } + } + + return this.getCurrentUnitName(); + } + + /** + * Returns a string representation of the object. The exact details of the representation are + * unspecified and subject to change. + * + * @since 2019-05-03 + */ + @Override + public String toString() { + return String.format("Iterator iterating over unit names; next value is \"%s\"", this.peek()); + } + } + + // the map that created this set + private final PrefixedUnitMap map; + + /** + * Creates the {@code PrefixedUnitNameSet}. + * + * @param map + * map that created this set + * @since 2019-04-13 + * @since v0.2.0 + */ + public PrefixedUnitNameSet(final PrefixedUnitMap map) { + this.map = map; + } + + @Override + public boolean add(final String e) { + throw new UnsupportedOperationException("Cannot add to an immutable set"); + } + + @Override + public boolean addAll(final Collection c) { + throw new UnsupportedOperationException("Cannot add to an immutable set"); + } + + @Override + public void clear() { + throw new UnsupportedOperationException("Cannot clear an immutable set"); + } + + @Override + public boolean contains(final Object o) { + return this.map.containsKey(o); + } + + @Override + public boolean containsAll(final Collection c) { + for (final Object o : c) + if (!this.contains(o)) + return false; + return true; + } + + @Override + public boolean isEmpty() { + return this.map.isEmpty(); + } + + @Override + public Iterator iterator() { + return new PrefixedUnitNameIterator(this.map); + } + + @Override + public boolean remove(final Object o) { + throw new UnsupportedOperationException("Cannot remove from an immutable set"); + } + + @Override + public boolean removeAll(final Collection c) { + throw new UnsupportedOperationException("Cannot remove from an immutable set"); + } + + @Override + public boolean removeIf(final Predicate filter) { + throw new UnsupportedOperationException("Cannot remove from an immutable set"); + } + + @Override + public boolean retainAll(final Collection c) { + throw new UnsupportedOperationException("Cannot remove from an immutable set"); + } + + @Override + public int size() { + if (this.map.units.isEmpty()) + return 0; + else { + if (this.map.prefixes.isEmpty()) + return this.map.units.size(); + else + // infinite set + return Integer.MAX_VALUE; + } + } + + /** + * @throws IllegalStateException + * if the set is infinite in size + */ + @Override + public Object[] toArray() { + if (this.map.units.isEmpty() || this.map.prefixes.isEmpty()) + return super.toArray(); + else + // infinite set + throw new IllegalStateException("Cannot make an infinite set into an array."); + + } + + /** + * @throws IllegalStateException + * if the set is infinite in size + */ + @Override + public T[] toArray(final T[] a) { + if (this.map.units.isEmpty() || this.map.prefixes.isEmpty()) + return super.toArray(a); + else + // infinite set + throw new IllegalStateException("Cannot make an infinite set into an array."); + } + + @Override + public String toString() { + if (this.map.units.isEmpty() || this.map.prefixes.isEmpty()) + return super.toString(); + else + return String.format("Infinite set of name-unit entries created from units %s and prefixes %s", + this.map.units, this.map.prefixes); + } + } + + /** + * The units stored in this collection, without prefixes. + * + * @since 2019-04-13 + * @since v0.2.0 + */ + private final Map units; + + /** + * The available prefixes for use. + * + * @since 2019-04-13 + * @since v0.2.0 + */ + private final Map prefixes; + + // caches + private transient Collection values = null; + private transient Set keySet = null; + private transient Set> entrySet = null; + + /** + * Creates the {@code PrefixedUnitMap}. + * + * @param units + * map mapping unit names to units + * @param prefixes + * map mapping prefix names to prefixes + * @since 2019-04-13 + * @since v0.2.0 + */ + public PrefixedUnitMap(final Map units, final Map prefixes) { + // I am making unmodifiable maps to ensure I don't accidentally make changes. + this.units = Collections.unmodifiableMap(units); + this.prefixes = Collections.unmodifiableMap(prefixes); + } + + @Override + public void clear() { + throw new UnsupportedOperationException("Cannot clear an immutable map"); + } + + @Override + public Unit compute(final String key, + final BiFunction remappingFunction) { + throw new UnsupportedOperationException("Cannot edit an immutable map"); + } + + @Override + public Unit computeIfAbsent(final String key, final Function mappingFunction) { + throw new UnsupportedOperationException("Cannot edit an immutable map"); + } + + @Override + public Unit computeIfPresent(final String key, + final BiFunction remappingFunction) { + throw new UnsupportedOperationException("Cannot edit an immutable map"); + } + + @Override + public boolean containsKey(final Object key) { + // First, test if there is a unit with the key + if (this.units.containsKey(key)) + return true; + + // Next, try to cast it to String + if (!(key instanceof String)) + throw new IllegalArgumentException("Attempted to test for a unit using a non-string name."); + final String unitName = (String) key; + + // Then, look for the longest prefix that is attached to a valid unit + String longestPrefix = null; + int longestLength = 0; + + for (final String prefixName : this.prefixes.keySet()) { + // a prefix name is valid if: + // - it is prefixed (i.e. the unit name starts with it) + // - it is longer than the existing largest prefix (since I am looking for the longest valid prefix) + // - the part after the prefix is a valid unit name + // - the unit described that name is a linear unit (since only linear units can have prefixes) + if (unitName.startsWith(prefixName) && prefixName.length() > longestLength) { + final String rest = unitName.substring(prefixName.length()); + if (this.containsKey(rest) && this.get(rest) instanceof LinearUnit) { + longestPrefix = prefixName; + longestLength = prefixName.length(); + } + } + } + + return longestPrefix != null; + } + + /** + * {@inheritDoc} + * + *

+ * Because of ambiguities between prefixes (i.e. kilokilo = mega), this method only tests for prefixless units. + *

+ */ + @Override + public boolean containsValue(final Object value) { + return this.units.containsValue(value); + } + + @Override + public Set> entrySet() { + if (this.entrySet == null) { + this.entrySet = new PrefixedUnitEntrySet(this); + } + return this.entrySet; + } + + @Override + public Unit get(final Object key) { + // First, test if there is a unit with the key + if (this.units.containsKey(key)) + return this.units.get(key); + + // Next, try to cast it to String + if (!(key instanceof String)) + throw new IllegalArgumentException("Attempted to obtain a unit using a non-string name."); + final String unitName = (String) key; + + // Then, look for the longest prefix that is attached to a valid unit + String longestPrefix = null; + int longestLength = 0; + + for (final String prefixName : this.prefixes.keySet()) { + // a prefix name is valid if: + // - it is prefixed (i.e. the unit name starts with it) + // - it is longer than the existing largest prefix (since I am looking for the longest valid prefix) + // - the part after the prefix is a valid unit name + // - the unit described that name is a linear unit (since only linear units can have prefixes) + if (unitName.startsWith(prefixName) && prefixName.length() > longestLength) { + final String rest = unitName.substring(prefixName.length()); + if (this.containsKey(rest) && this.get(rest) instanceof LinearUnit) { + longestPrefix = prefixName; + longestLength = prefixName.length(); + } + } + } + + // if none found, returns null + if (longestPrefix == null) + return null; + else { + // get necessary data + final String rest = unitName.substring(longestLength); + // this cast will not fail because I verified that it would work before selecting this prefix + final LinearUnit unit = (LinearUnit) this.get(rest); + final UnitPrefix prefix = this.prefixes.get(longestPrefix); + + return unit.withPrefix(prefix); + } + } + + @Override + public boolean isEmpty() { + return this.units.isEmpty(); + } + + @Override + public Set keySet() { + if (this.keySet == null) { + this.keySet = new PrefixedUnitNameSet(this); + } + return this.keySet; + } + + @Override + public Unit merge(final String key, final Unit value, + final BiFunction remappingFunction) { + throw new UnsupportedOperationException("Cannot merge into an immutable map"); + } + + @Override + public Unit put(final String key, final Unit value) { + throw new UnsupportedOperationException("Cannot add entries to an immutable map"); + } + + @Override + public void putAll(final Map m) { + throw new UnsupportedOperationException("Cannot add entries to an immutable map"); + } + + @Override + public Unit putIfAbsent(final String key, final Unit value) { + throw new UnsupportedOperationException("Cannot add entries to an immutable map"); + } + + @Override + public Unit remove(final Object key) { + throw new UnsupportedOperationException("Cannot remove entries from an immutable map"); + } + + @Override + public boolean remove(final Object key, final Object value) { + throw new UnsupportedOperationException("Cannot remove entries from an immutable map"); + } + + @Override + public Unit replace(final String key, final Unit value) { + throw new UnsupportedOperationException("Cannot replace entries in an immutable map"); + } + + @Override + public boolean replace(final String key, final Unit oldValue, final Unit newValue) { + throw new UnsupportedOperationException("Cannot replace entries in an immutable map"); + } + + @Override + public void replaceAll(final BiFunction function) { + throw new UnsupportedOperationException("Cannot replace entries in an immutable map"); + } + + @Override + public int size() { + if (this.units.isEmpty()) + return 0; + else { + if (this.prefixes.isEmpty()) + return this.units.size(); + else + // infinite set + return Integer.MAX_VALUE; + } + } + + @Override + public String toString() { + if (this.units.isEmpty() || this.prefixes.isEmpty()) + return super.toString(); + else + return String.format("Infinite map of name-unit entries created from units %s and prefixes %s", + this.units, this.prefixes); + } + + /** + * {@inheritDoc} + * + *

+ * Because of ambiguities between prefixes (i.e. kilokilo = mega), this method ignores prefixes. + *

+ */ + @Override + public Collection values() { + if (this.values == null) { + this.values = Collections.unmodifiableCollection(this.units.values()); + } + return this.values; + } + } + + /** + * A regular expression that separates names and expressions in unit files. + */ + private static final Pattern NAME_EXPRESSION = Pattern.compile("(\\S+)\\s+(\\S.*)"); + + /** + * The exponent operator + * + * @param base + * base of exponentiation + * @param exponentUnit + * exponent + * @return result + * @since 2019-04-10 + * @since v0.2.0 + */ + private static final LinearUnit exponentiateUnits(final LinearUnit base, final LinearUnit exponentUnit) { + // exponent function - first check if o2 is a number, + if (exponentUnit.getBase().equals(SI.ONE.getBase())) { + // then check if it is an integer, + final double exponent = exponentUnit.getConversionFactor(); + if (DecimalComparison.equals(exponent % 1, 0)) + // then exponentiate + return base.toExponent((int) (exponent + 0.5)); + else + // not an integer + throw new UnsupportedOperationException("Decimal exponents are currently not supported."); + } else + // not a number + throw new IllegalArgumentException("Exponents must be numbers."); + } + + /** + * The units in this system, excluding prefixes. + * + * @since 2019-01-07 + * @since v0.1.0 + */ + private final Map prefixlessUnits; + + /** + * The unit prefixes in this system. + * + * @since 2019-01-14 + * @since v0.1.0 + */ + private final Map prefixes; + + /** + * The dimensions in this system. + * + * @since 2019-03-14 + * @since v0.2.0 + */ + private final Map> dimensions; + + /** + * A map mapping strings to units (including prefixes) + * + * @since 2019-04-13 + * @since v0.2.0 + */ + private final Map units; + + /** + * A parser that can parse unit expressions. + * + * @since 2019-03-22 + * @since v0.2.0 + */ + private final ExpressionParser unitExpressionParser = new ExpressionParser.Builder<>( + this::getLinearUnit).addBinaryOperator("+", (o1, o2) -> o1.plus(o2), 0) + .addBinaryOperator("-", (o1, o2) -> o1.minus(o2), 0) + .addBinaryOperator("*", (o1, o2) -> o1.times(o2), 1).addSpaceFunction("*") + .addBinaryOperator("/", (o1, o2) -> o1.dividedBy(o2), 1) + .addBinaryOperator("^", UnitDatabase::exponentiateUnits, 2).build(); + + /** + * A parser that can parse unit prefix expressions + * + * @since 2019-04-13 + * @since v0.2.0 + */ + private final ExpressionParser prefixExpressionParser = new ExpressionParser.Builder<>(this::getPrefix) + .addBinaryOperator("*", (o1, o2) -> o1.times(o2), 0).addSpaceFunction("*") + .addBinaryOperator("/", (o1, o2) -> o1.dividedBy(o2), 0) + .addBinaryOperator("^", (o1, o2) -> o1.toExponent(o2.getMultiplier()), 1).build(); + + /** + * A parser that can parse unit dimension expressions. + * + * @since 2019-04-13 + * @since v0.2.0 + */ + private final ExpressionParser> unitDimensionParser = new ExpressionParser.Builder<>( + this::getDimension).addBinaryOperator("*", (o1, o2) -> o1.times(o2), 0).addSpaceFunction("*") + .addBinaryOperator("/", (o1, o2) -> o1.dividedBy(o2), 0).build(); + + /** + * Creates the {@code UnitsDatabase}. + * + * @since 2019-01-10 + * @since v0.1.0 + */ + public UnitDatabase() { + this.prefixlessUnits = new HashMap<>(); + this.prefixes = new HashMap<>(); + this.dimensions = new HashMap<>(); + this.units = new PrefixedUnitMap(this.prefixlessUnits, this.prefixes); + } + + /** + * Adds a unit dimension to the database. + * + * @param name + * dimension's name + * @param dimension + * dimension to add + * @throws NullPointerException + * if name or dimension is null + * @since 2019-03-14 + * @since v0.2.0 + */ + public void addDimension(final String name, final ObjectProduct dimension) { + this.dimensions.put(Objects.requireNonNull(name, "name must not be null."), + Objects.requireNonNull(dimension, "dimension must not be null.")); + } + + /** + * Adds to the list from a line in a unit dimension file. + * + * @param line + * line to look at + * @param lineCounter + * number of line, for error messages + * @since 2019-04-10 + * @since v0.2.0 + */ + private void addDimensionFromLine(final String line, final long lineCounter) { + // ignore lines that start with a # sign - they're comments + if (line.isEmpty()) + return; + if (line.contains("#")) { + this.addDimensionFromLine(line.substring(0, line.indexOf("#")), lineCounter); + return; + } + + // divide line into name and expression + final Matcher lineMatcher = NAME_EXPRESSION.matcher(line); + if (!lineMatcher.matches()) + throw new IllegalArgumentException(String.format( + "Error at line %d: Lines of a dimension file must consist of a dimension name, then spaces or tabs, then a dimension expression.", + lineCounter)); + final String name = lineMatcher.group(1); + final String expression = lineMatcher.group(2); + + if (name.endsWith(" ")) { + System.err.printf("Warning - line %d's dimension name ends in a space", lineCounter); + } + + // if expression is "!", search for an existing dimension + // if no unit found, throw an error + if (expression.equals("!")) { + if (!this.containsDimensionName(name)) + throw new IllegalArgumentException( + String.format("! used but no dimension found (line %d).", lineCounter)); + } else { + // it's a unit, get the unit + final ObjectProduct dimension; + try { + dimension = this.getDimensionFromExpression(expression); + } catch (final IllegalArgumentException e) { + System.err.printf("Parsing error on line %d:%n", lineCounter); + throw e; + } + + this.addDimension(name, dimension); + } + } + + /** + * Adds a unit prefix to the database. + * + * @param name + * prefix's name + * @param prefix + * prefix to add + * @throws NullPointerException + * if name or prefix is null + * @since 2019-01-14 + * @since v0.1.0 + */ + public void addPrefix(final String name, final UnitPrefix prefix) { + this.prefixes.put(Objects.requireNonNull(name, "name must not be null."), + Objects.requireNonNull(prefix, "prefix must not be null.")); + } + + /** + * Adds a unit to the database. + * + * @param name + * unit's name + * @param unit + * unit to add + * @throws NullPointerException + * if unit is null + * @since 2019-01-10 + * @since v0.1.0 + */ + public void addUnit(final String name, final Unit unit) { + this.prefixlessUnits.put(Objects.requireNonNull(name, "name must not be null."), + Objects.requireNonNull(unit, "unit must not be null.")); + } + + /** + * Adds to the list from a line in a unit file. + * + * @param line + * line to look at + * @param lineCounter + * number of line, for error messages + * @since 2019-04-10 + * @since v0.2.0 + */ + private void addUnitOrPrefixFromLine(final String line, final long lineCounter) { + // ignore lines that start with a # sign - they're comments + if (line.isEmpty()) + return; + if (line.contains("#")) { + this.addUnitOrPrefixFromLine(line.substring(0, line.indexOf("#")), lineCounter); + return; + } + + // divide line into name and expression + final Matcher lineMatcher = NAME_EXPRESSION.matcher(line); + if (!lineMatcher.matches()) + throw new IllegalArgumentException(String.format( + "Error at line %d: Lines of a unit file must consist of a unit name, then spaces or tabs, then a unit expression.", + lineCounter)); + final String name = lineMatcher.group(1); + final String expression = lineMatcher.group(2); + + if (name.endsWith(" ")) { + System.err.printf("Warning - line %d's unit name ends in a space", lineCounter); + } + + // if expression is "!", search for an existing unit + // if no unit found, throw an error + if (expression.equals("!")) { + if (!this.containsUnitName(name)) + throw new IllegalArgumentException(String.format("! used but no unit found (line %d).", lineCounter)); + } else { + if (name.endsWith("-")) { + final UnitPrefix prefix; + try { + prefix = this.getPrefixFromExpression(expression); + } catch (final IllegalArgumentException e) { + System.err.printf("Parsing error on line %d:%n", lineCounter); + throw e; + } + this.addPrefix(name.substring(0, name.length() - 1), prefix); + } else { + // it's a unit, get the unit + final Unit unit; + try { + unit = this.getUnitFromExpression(expression); + } catch (final IllegalArgumentException e) { + System.err.printf("Parsing error on line %d:%n", lineCounter); + throw e; + } + + this.addUnit(name, unit); + } + } + } + + /** + * Tests if the database has a unit dimension with this name. + * + * @param name + * name to test + * @return if database contains name + * @since 2019-03-14 + * @since v0.2.0 + */ + public boolean containsDimensionName(final String name) { + return this.dimensions.containsKey(name); + } + + /** + * Tests if the database has a unit prefix with this name. + * + * @param name + * name to test + * @return if database contains name + * @since 2019-01-13 + * @since v0.1.0 + */ + public boolean containsPrefixName(final String name) { + return this.prefixes.containsKey(name); + } + + /** + * Tests if the database has a unit with this name, taking prefixes into consideration + * + * @param name + * name to test + * @return if database contains name + * @since 2019-01-13 + * @since v0.1.0 + */ + public boolean containsUnitName(final String name) { + return this.units.containsKey(name); + } + + /** + * @return a map mapping dimension names to dimensions + * @since 2019-04-13 + * @since v0.2.0 + */ + public Map> dimensionMap() { + return Collections.unmodifiableMap(this.dimensions); + } + + /** + * Gets a unit dimension from the database using its name. + * + *

+ * This method accepts exponents, like "L^3" + *

+ * + * @param name + * dimension's name + * @return dimension + * @since 2019-03-14 + * @since v0.2.0 + */ + public ObjectProduct getDimension(final String name) { + Objects.requireNonNull(name, "name must not be null."); + if (name.contains("^")) { + final String[] baseAndExponent = name.split("\\^"); + + final ObjectProduct base = this.getDimension(baseAndExponent[0]); + + final int exponent; + try { + exponent = Integer.parseInt(baseAndExponent[baseAndExponent.length - 1]); + } catch (final NumberFormatException e2) { + throw new IllegalArgumentException("Exponent must be an integer."); + } + + return base.toExponent(exponent); + } + return this.dimensions.get(name); + } + + /** + * Uses the database's data to parse an expression into a unit dimension + *

+ * The expression is a series of any of the following: + *

    + *
  • The name of a unit dimension, which multiplies or divides the result based on preceding operators
  • + *
  • The operators '*' and '/', which multiply and divide (note that just putting two unit dimensions next to each + * other is equivalent to multiplication)
  • + *
  • The operator '^' which exponentiates. Exponents must be integers.
  • + *
+ * + * @param expression + * expression to parse + * @throws IllegalArgumentException + * if the expression cannot be parsed + * @throws NullPointerException + * if expression is null + * @since 2019-04-13 + * @since v0.2.0 + */ + public ObjectProduct getDimensionFromExpression(final String expression) { + Objects.requireNonNull(expression, "expression must not be null."); + + // attempt to get a dimension as an alias first + if (this.containsDimensionName(expression)) + return this.getDimension(expression); + + // force operators to have spaces + String modifiedExpression = expression; + modifiedExpression = modifiedExpression.replaceAll("\\*", " \\* "); + modifiedExpression = modifiedExpression.replaceAll("/", " / "); + modifiedExpression = modifiedExpression.replaceAll(" *\\^ *", "\\^"); + + // fix broken spaces + modifiedExpression = modifiedExpression.replaceAll(" +", " "); + + return this.unitDimensionParser.parseExpression(modifiedExpression); + } + + /** + * Gets a unit. If it is linear, cast it to a LinearUnit and return it. Otherwise, throw an + * {@code IllegalArgumentException}. + * + * @param name + * unit's name + * @return unit + * @since 2019-03-22 + * @since v0.2.0 + */ + private LinearUnit getLinearUnit(final String name) { + // see if I am using a function-unit like tempC(100) + if (name.contains("(") && name.contains(")")) { + // break it into function name and value + final List parts = Arrays.asList(name.split("\\(")); + if (parts.size() != 2) + throw new IllegalArgumentException("Format nonlinear units like: unit(value)."); + + // solve the function + final Unit unit = this.getUnit(parts.get(0)); + final double value = Double.parseDouble(parts.get(1).substring(0, parts.get(1).length() - 1)); + return LinearUnit.fromUnitValue(unit, value); + } else { + // get a linear unit + final Unit unit = this.getUnit(name); + if (unit instanceof LinearUnit) + return (LinearUnit) unit; + else + throw new IllegalArgumentException(String.format("%s is not a linear unit.", name)); + } + } + + /** + * Gets a unit prefix from the database from its name + * + * @param name + * prefix's name + * @return prefix + * @since 2019-01-10 + * @since v0.1.0 + */ + public UnitPrefix getPrefix(final String name) { + try { + return UnitPrefix.valueOf(Double.parseDouble(name)); + } catch (final NumberFormatException e) { + return this.prefixes.get(name); + } + } + + /** + * Gets a unit prefix from a prefix expression + *

+ * Currently, prefix expressions are much simpler than unit expressions: They are either a number or the name of + * another prefix + *

+ * + * @param expression + * expression to input + * @return prefix + * @throws IllegalArgumentException + * if expression cannot be parsed + * @throws NullPointerException + * if any argument is null + * @since 2019-01-14 + * @since v0.1.0 + */ + public UnitPrefix getPrefixFromExpression(final String expression) { + Objects.requireNonNull(expression, "expression must not be null."); + + // attempt to get a unit as an alias first + if (this.containsUnitName(expression)) + return this.getPrefix(expression); + + // force operators to have spaces + String modifiedExpression = expression; + modifiedExpression = modifiedExpression.replaceAll("\\*", " \\* "); + modifiedExpression = modifiedExpression.replaceAll("/", " / "); + modifiedExpression = modifiedExpression.replaceAll("\\^", " \\^ "); + + // fix broken spaces + modifiedExpression = modifiedExpression.replaceAll(" +", " "); + + return this.prefixExpressionParser.parseExpression(modifiedExpression); + } + + /** + * Gets a unit from the database from its name, looking for prefixes. + * + * @param name + * unit's name + * @return unit + * @since 2019-01-10 + * @since v0.1.0 + */ + public Unit getUnit(final String name) { + try { + final double value = Double.parseDouble(name); + return SI.ONE.times(value); + } catch (final NumberFormatException e) { + return this.units.get(name); + } + + } + + /** + * Uses the database's unit data to parse an expression into a unit + *

+ * The expression is a series of any of the following: + *

    + *
  • The name of a unit, which multiplies or divides the result based on preceding operators
  • + *
  • The operators '*' and '/', which multiply and divide (note that just putting two units or values next to each + * other is equivalent to multiplication)
  • + *
  • The operator '^' which exponentiates. Exponents must be integers.
  • + *
  • A number which is multiplied or divided
  • + *
+ * This method only works with linear units. + * + * @param expression + * expression to parse + * @throws IllegalArgumentException + * if the expression cannot be parsed + * @throws NullPointerException + * if expression is null + * @since 2019-01-07 + * @since v0.1.0 + */ + public Unit getUnitFromExpression(final String expression) { + Objects.requireNonNull(expression, "expression must not be null."); + + // attempt to get a unit as an alias first + if (this.containsUnitName(expression)) + return this.getUnit(expression); + + // force operators to have spaces + String modifiedExpression = expression; + modifiedExpression = modifiedExpression.replaceAll("\\+", " \\+ "); + modifiedExpression = modifiedExpression.replaceAll("-", " - "); + modifiedExpression = modifiedExpression.replaceAll("\\*", " \\* "); + modifiedExpression = modifiedExpression.replaceAll("/", " / "); + modifiedExpression = modifiedExpression.replaceAll("\\^", " \\^ "); + + // fix broken spaces + modifiedExpression = modifiedExpression.replaceAll(" +", " "); + + // the previous operation breaks negative numbers, fix them! + // (i.e. -2 becomes - 2) + for (int i = 2; i < modifiedExpression.length(); i++) { + if (modifiedExpression.charAt(i) == '-' + && Arrays.asList('+', '-', '*', '/', '^').contains(modifiedExpression.charAt(i - 2))) { + // found a broken negative number + modifiedExpression = modifiedExpression.substring(0, i + 1) + modifiedExpression.substring(i + 2); + } + } + + return this.unitExpressionParser.parseExpression(modifiedExpression); + } + + /** + * Adds all dimensions from a file, using data from the database to parse them. + *

+ * Each line in the file should consist of a name and an expression (parsed by getDimensionFromExpression) separated + * by any number of tab characters. + *

+ *

+ * Allowed exceptions: + *

    + *
  • Anything after a '#' character is considered a comment and ignored.
  • + *
  • Blank lines are also ignored
  • + *
  • If an expression consists of a single exclamation point, instead of parsing it, this method will search the + * database for an existing unit. If no unit is found, an IllegalArgumentException is thrown. This is used to define + * initial units and ensure that the database contains them.
  • + *
+ * + * @param file + * file to read + * @throws IllegalArgumentException + * if the file cannot be parsed, found or read + * @throws NullPointerException + * if file is null + * @since 2019-01-13 + * @since v0.1.0 + */ + public void loadDimensionFile(final File file) { + Objects.requireNonNull(file, "file must not be null."); + try (FileReader fileReader = new FileReader(file); BufferedReader reader = new BufferedReader(fileReader)) { + // while the reader has lines to read, read a line, then parse it, then add it + long lineCounter = 0; + while (reader.ready()) { + this.addDimensionFromLine(reader.readLine(), ++lineCounter); + } + } catch (final FileNotFoundException e) { + throw new IllegalArgumentException("Could not find file " + file, e); + } catch (final IOException e) { + throw new IllegalArgumentException("Could not read file " + file, e); + } + } + + /** + * Adds all units from a file, using data from the database to parse them. + *

+ * Each line in the file should consist of a name and an expression (parsed by getUnitFromExpression) separated by + * any number of tab characters. + *

+ *

+ * Allowed exceptions: + *

    + *
  • Anything after a '#' character is considered a comment and ignored.
  • + *
  • Blank lines are also ignored
  • + *
  • If an expression consists of a single exclamation point, instead of parsing it, this method will search the + * database for an existing unit. If no unit is found, an IllegalArgumentException is thrown. This is used to define + * initial units and ensure that the database contains them.
  • + *
+ * + * @param file + * file to read + * @throws IllegalArgumentException + * if the file cannot be parsed, found or read + * @throws NullPointerException + * if file is null + * @since 2019-01-13 + * @since v0.1.0 + */ + public void loadUnitsFile(final File file) { + Objects.requireNonNull(file, "file must not be null."); + try (FileReader fileReader = new FileReader(file); BufferedReader reader = new BufferedReader(fileReader)) { + // while the reader has lines to read, read a line, then parse it, then add it + long lineCounter = 0; + while (reader.ready()) { + this.addUnitOrPrefixFromLine(reader.readLine(), ++lineCounter); + } + } catch (final FileNotFoundException e) { + throw new IllegalArgumentException("Could not find file " + file, e); + } catch (final IOException e) { + throw new IllegalArgumentException("Could not read file " + file, e); + } + } + + /** + * @return a map mapping prefix names to prefixes + * @since 2019-04-13 + * @since v0.2.0 + */ + public Map prefixMap() { + return Collections.unmodifiableMap(this.prefixes); + } + + @Override + public String toString() { + return String.format("Unit Database with %d units and %d unit prefixes", this.prefixlessUnits.size(), + this.prefixes.size()); + } + + /** + * Returns a map mapping unit names to units, including units with prefixes. + *

+ * The returned map is infinite in size if there is at least one unit and at least one prefix. If it is infinite, + * some operations that only work with finite collections, like converting name/entry sets to arrays, will throw an + * {@code IllegalStateException}. + *

+ *

+ * Specifically, the operations that will throw an IllegalStateException if the map is infinite in size are: + *

    + *
  • {@code unitMap.entrySet().toArray()} (either overloading)
  • + *
  • {@code unitMap.keySet().toArray()} (either overloading)
  • + *
+ *

+ *

+ * Because of ambiguities between prefixes (i.e. kilokilo = mega), the map's {@link PrefixedUnitMap#containsValue + * containsValue} and {@link PrefixedUnitMap#values() values()} methods currently ignore prefixes. + *

+ * + * @return a map mapping unit names to units, including prefixed names + * @since 2019-04-13 + * @since v0.2.0 + */ + public Map unitMap() { + return this.units; // PrefixedUnitMap is immutable so I don't need to make an unmodifiable map. + } + + /** + * @return a map mapping unit names to units, ignoring prefixes + * @since 2019-04-13 + * @since v0.2.0 + */ + public Map unitMapPrefixless() { + return Collections.unmodifiableMap(this.prefixlessUnits); + } +} diff --git a/src/org/unitConverter/unit/UnitDatabaseTest.java b/src/org/unitConverter/unit/UnitDatabaseTest.java new file mode 100644 index 0000000..164172b --- /dev/null +++ b/src/org/unitConverter/unit/UnitDatabaseTest.java @@ -0,0 +1,307 @@ +/** + * 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 . + */ +package org.unitConverter.unit; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import java.util.Iterator; +import java.util.Map; +import java.util.Map.Entry; + +import org.junit.jupiter.api.Test; + +/** + * 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 UnitDatabaseTest { + // some linear units and one nonlinear + private static final Unit U = SI.METRE; + private static final Unit V = SI.KILOGRAM; + private static final Unit W = SI.SECOND; + + // used for testing expressions + // J = U^2 * V / W^2 + 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.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 = 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}. + * + * @since 2019-05-03 + */ + @Test + public void testInfiniteSetExceptions() { + // load units + final UnitDatabase infiniteDatabase = new UnitDatabase(); + + infiniteDatabase.addUnit("J", J); + infiniteDatabase.addUnit("K", K); + + infiniteDatabase.addPrefix("A", A); + infiniteDatabase.addPrefix("B", B); + infiniteDatabase.addPrefix("C", C); + + { + boolean exceptionThrown = false; + try { + infiniteDatabase.unitMap().entrySet().toArray(); + } catch (final IllegalStateException e) { + exceptionThrown = true; + // pass! + } finally { + if (!exceptionThrown) { + fail("No IllegalStateException thrown"); + } + } + } + + { + boolean exceptionThrown = false; + try { + infiniteDatabase.unitMap().keySet().toArray(); + } catch (final IllegalStateException e) { + exceptionThrown = true; + // pass! + } finally { + if (!exceptionThrown) { + fail("No IllegalStateException thrown"); + } + } + } + } + + /** + * Test that prefixes correctly apply to units. + * + * @since 2019-04-14 + * @since v0.2.0 + */ + @Test + public void testPrefixes() { + final UnitDatabase database = new UnitDatabase(); + + database.addUnit("U", U); + database.addUnit("V", V); + database.addUnit("W", W); + + database.addPrefix("A", A); + database.addPrefix("B", B); + database.addPrefix("C", C); + + // get the product + final Unit abcuNonlinear = database.getUnit("ABCU"); + assert abcuNonlinear instanceof LinearUnit; + + final LinearUnit abcu = (LinearUnit) abcuNonlinear; + assertEquals(A.getMultiplier() * B.getMultiplier() * C.getMultiplier(), abcu.getConversionFactor(), 1e-15); + } + + /** + * Tests the functionnalites of the prefixless unit map. + * + *

+ * The map should be an auto-updating view of the units in the database. + *

+ * + * @since 2019-04-14 + * @since v0.2.0 + */ + @Test + public void testPrefixlessUnitMap() { + final UnitDatabase database = new UnitDatabase(); + final Map prefixlessUnits = database.unitMapPrefixless(); + + database.addUnit("U", U); + database.addUnit("V", V); + database.addUnit("W", W); + + // this should work because the map should be an auto-updating view + assertTrue(prefixlessUnits.containsKey("U")); + assertFalse(prefixlessUnits.containsKey("Z")); + + assertTrue(prefixlessUnits.containsValue(U)); + assertFalse(prefixlessUnits.containsValue(NONLINEAR)); + } + + /** + * Tests that the database correctly stores and retrieves units, ignoring prefixes. + * + * @since 2019-04-14 + * @since v0.2.0 + */ + @Test + public void testPrefixlessUnits() { + final UnitDatabase database = new UnitDatabase(); + + database.addUnit("U", U); + database.addUnit("V", V); + database.addUnit("W", W); + + assertTrue(database.containsUnitName("U")); + assertFalse(database.containsUnitName("Z")); + + assertEquals(U, database.getUnit("U")); + assertEquals(null, database.getUnit("Z")); + } + + /** + * Test that unit expressions return the correct value. + * + * @since 2019-04-14 + * @since v0.2.0 + */ + @Test + public void testUnitExpressions() { + // load units + final UnitDatabase database = new UnitDatabase(); + + database.addUnit("U", U); + database.addUnit("V", V); + database.addUnit("W", W); + database.addUnit("fj", J.times(5)); + database.addUnit("ej", J.times(8)); + + database.addPrefix("A", A); + database.addPrefix("B", B); + database.addPrefix("C", C); + + // first test - test prefixes and operations + final Unit expected1 = J.withPrefix(A).withPrefix(B).withPrefix(C).withPrefix(C); + final Unit actual1 = database.getUnitFromExpression("ABV * CU^2 / W / W"); + + assertEquals(expected1, actual1); + + // second test - test addition and subtraction + final Unit expected2 = J.times(58); + final Unit actual2 = database.getUnitFromExpression("2 fj + 6 ej"); + + assertEquals(expected2, actual2); + } + + /** + * Tests both the unit name iterator and the name-unit entry iterator + * + * @since 2019-04-14 + * @since v0.2.0 + */ + @Test + public void testUnitIterator() { + // load units + final UnitDatabase database = new UnitDatabase(); + + database.addUnit("J", J); + database.addUnit("K", K); + + database.addPrefix("A", A); + database.addPrefix("B", B); + database.addPrefix("C", C); + + final int NUM_UNITS = database.unitMapPrefixless().size(); + final int NUM_PREFIXES = database.prefixMap().size(); + + final Iterator nameIterator = database.unitMap().keySet().iterator(); + final Iterator> entryIterator = database.unitMap().entrySet().iterator(); + + int expectedLength = 1; + int unitsWithThisLengthSoFar = 0; + + // loop 1000 times + for (int i = 0; i < 1000; i++) { + // expected length of next + if (unitsWithThisLengthSoFar >= NUM_UNITS * (int) Math.pow(NUM_PREFIXES, expectedLength - 1)) { + expectedLength++; + unitsWithThisLengthSoFar = 0; + } + + // test that stuff is valid + final String nextName = nameIterator.next(); + final Unit nextUnit = database.getUnit(nextName); + final Entry nextEntry = entryIterator.next(); + + assertEquals(expectedLength, nextName.length()); + assertEquals(nextName, nextEntry.getKey()); + assertEquals(nextUnit, nextEntry.getValue()); + + unitsWithThisLengthSoFar++; + } + + // test toString for consistency + final String entryIteratorString = entryIterator.toString(); + for (int i = 0; i < 3; i++) { + assertEquals(entryIteratorString, entryIterator.toString()); + } + + final String nameIteratorString = nameIterator.toString(); + for (int i = 0; i < 3; i++) { + assertEquals(nameIteratorString, nameIterator.toString()); + } + } + + /** + * Determine, given a unit name that could mean multiple things, which meaning is chosen. + *

+ * For example, "ABCU" could mean "A-B-C-U", "AB-C-U", or "A-BC-U". In this case, "AB-C-U" is the correct choice. + *

+ * + * @since 2019-04-14 + * @since v0.2.0 + */ + @Test + public void testUnitPrefixCombinations() { + // load units + final UnitDatabase database = new UnitDatabase(); + + database.addUnit("J", J); + + database.addPrefix("A", A); + database.addPrefix("B", B); + database.addPrefix("C", C); + database.addPrefix("AB", AB); + database.addPrefix("BC", BC); + + // test 1 - AB-C-J vs A-BC-J vs A-B-C-J + final Unit expected1 = J.withPrefix(AB).withPrefix(C); + final Unit actual1 = database.getUnit("ABCJ"); + + assertEquals(expected1, actual1); + + // test 2 - ABC-J vs AB-CJ vs AB-C-J + database.addUnit("CJ", J.times(13)); + database.addPrefix("ABC", UnitPrefix.valueOf(17)); + + final Unit expected2 = J.times(17); + final Unit actual2 = database.getUnit("ABCJ"); + + assertEquals(expected2, actual2); + } +} 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 . - */ -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; /** @@ -45,17 +44,9 @@ class UnitTest { assertEquals(foot.minus(inch), SI.METRE.times(0.2794)); } - @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..2d83e1f 100644 --- a/src/org/unitConverter/unit/package-info.java +++ b/src/org/unitConverter/unit/package-info.java @@ -15,10 +15,9 @@ * along with this program. If not, see . */ /** - * All of the classes that correspond to the units being converted. + * The new definition for units. * * @author Adrien Hopkins - * @since 2019-01-25 - * @since v0.1.0 + * @since 2019-10-16 */ package org.unitConverter.unit; \ No newline at end of file diff --git a/unitsfile.txt b/unitsfile.txt index bda9b81..73a29d1 100644 --- a/unitsfile.txt +++ b/unitsfile.txt @@ -146,9 +146,9 @@ deg degree # Use tempC(100) for 100 degrees Celsius tempCelsius ! -tempFahrenheit ! +#tempFahrenheit ! tempC tempCelsius -tempF tempFahrenheit +#tempF tempFahrenheit # Common time units minute 60 second -- cgit v1.2.3 From b491a1d67b513f6a6f549fe6fcb374d731e0beca Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Thu, 17 Oct 2019 16:31:38 -0400 Subject: Changed the ZeroIsNullMap to a more general ConditionalExistenceMap. --- .../math/ConditionalExistenceCollections.java | 322 +++++++++++++++++++++ .../math/ConditionalExistenceCollectionsTest.java | 159 ++++++++++ src/org/unitConverter/math/ObjectProduct.java | 3 +- src/org/unitConverter/math/ZeroIsNullMap.java | 129 --------- src/org/unitConverter/math/ZeroIsNullMapTest.java | 113 -------- 5 files changed, 483 insertions(+), 243 deletions(-) create mode 100644 src/org/unitConverter/math/ConditionalExistenceCollections.java create mode 100644 src/org/unitConverter/math/ConditionalExistenceCollectionsTest.java delete mode 100644 src/org/unitConverter/math/ZeroIsNullMap.java delete mode 100644 src/org/unitConverter/math/ZeroIsNullMapTest.java diff --git a/src/org/unitConverter/math/ConditionalExistenceCollections.java b/src/org/unitConverter/math/ConditionalExistenceCollections.java new file mode 100644 index 0000000..c0a69f0 --- /dev/null +++ b/src/org/unitConverter/math/ConditionalExistenceCollections.java @@ -0,0 +1,322 @@ +/** + * 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 . + */ +package org.unitConverter.math; + +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. + *

+ * 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. + *

+ * 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). + *

+ * The returned collections do not 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. + *

+ * Other than that, 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. + * + * + * @author Adrien Hopkins + * @since 2019-10-17 + */ +// TODO add conditional existence Collections, Lists and Sorted/Navigable Sets/Maps +public final class ConditionalExistenceCollections { + /** + * Elements in this wrapper iterator only exist if they pass a condition. + * + * @author Adrien Hopkins + * @since 2019-10-17 + * @param + * type of elements in iterator + */ + static final class ConditionalExistenceIterator implements Iterator { + final Iterator iterator; + final Predicate existenceCondition; + E nextElement; + boolean hasNext; + + /** + * Creates the {@code ConditionalExistenceIterator}. + * + * @param iterator + * @param condition + * @since 2019-10-17 + */ + private ConditionalExistenceIterator(final Iterator iterator, final Predicate 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 + * key type + * @param + * value type + */ + static final class ConditionalExistenceMap extends AbstractMap { + Map map; + Predicate> entryExistenceCondition; + + /** + * Creates the {@code ConditionalExistenceMap}. + * + * @param map + * @param entryExistenceCondition + * @since 2019-10-17 + */ + private ConditionalExistenceMap(final Map map, final Predicate> 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 entry = new SimpleEntry<>(keyAsK, value); + return this.entryExistenceCondition.test(entry); + } + + @Override + public Set> 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 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 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 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 + * type of element in set + */ + static final class ConditionalExistenceSet extends AbstractSet { + private final Set set; + private final Predicate existenceCondition; + + /** + * Creates the {@code ConditionalNonexistenceSet}. + * + * @param set + * set to use + * @param existenceCondition + * condition where element exists + * @since 2019-10-17 + */ + private ConditionalExistenceSet(final Set set, final Predicate existenceCondition) { + this.set = set; + this.existenceCondition = existenceCondition; + } + + /** + * {@inheritDoc} + *

+ * 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 iterator() { + return conditionalExistenceIterator(this.set.iterator(), this.existenceCondition); + } + + @Override + public boolean remove(final Object o) { + 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 iterator are ignored if they don't pass a condition. + * + * @param + * 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 Iterator conditionalExistenceIterator(final Iterator iterator, + final Predicate existenceCondition) { + return new ConditionalExistenceIterator<>(iterator, existenceCondition); + } + + /** + * Mappings in the returned wrapper map are ignored if the corresponding entry doesn't pass a condition + * + * @param + * type of key in map + * @param + * 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 Map conditionalExistenceMap(final Map map, + final Predicate> entryExistenceCondition) { + return new ConditionalExistenceMap<>(map, entryExistenceCondition); + } + + /** + * Elements in the returned wrapper set are ignored if they don't pass a condition. + * + * @param + * 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 Set conditionalExistenceSet(final Set set, final Predicate 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 . + */ +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 getTestIterator() { + final List items = Arrays.asList("aa", "ab", "ba"); + final Iterator it = items.iterator(); + final ConditionalExistenceIterator cit = (ConditionalExistenceIterator) 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 getTestMap() { + final Map map = new HashMap<>(); + map.put("one", 1); + map.put("two", 2); + map.put("zero", 0); + map.put("ten", 10); + final Map 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 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 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 map = this.getTestMap(); + for (final Entry e : map.entrySet()) { + assertTrue(e.getValue() != 0); + } + } + + /** + * Test method for {@link org.unitConverter.math.ZeroIsNullMap#get(java.lang.Object)}. + */ + @Test + void testGetObject() { + final Map 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 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 map = this.getTestMap(); + assertFalse(map.keySet().contains("zero")); + } + + /** + * Test method for {@link org.unitConverter.math.ZeroIsNullMap#values()}. + */ + @Test + void testValues() { + final Map map = this.getTestMap(); + assertFalse(map.values().contains(0)); + } + +} diff --git a/src/org/unitConverter/math/ObjectProduct.java b/src/org/unitConverter/math/ObjectProduct.java index 21ab207..0cf89ec 100644 --- a/src/org/unitConverter/math/ObjectProduct.java +++ b/src/org/unitConverter/math/ObjectProduct.java @@ -92,7 +92,8 @@ public final class ObjectProduct { * @since 2019-10-16 */ private ObjectProduct(final Map exponents) { - this.exponents = Collections.unmodifiableMap(ZeroIsNullMap.create(new HashMap<>(exponents))); + this.exponents = Collections.unmodifiableMap(ConditionalExistenceCollections + .conditionalExistenceMap(new HashMap<>(exponents), e -> !Integer.valueOf(0).equals(e.getValue()))); } /** diff --git a/src/org/unitConverter/math/ZeroIsNullMap.java b/src/org/unitConverter/math/ZeroIsNullMap.java deleted file mode 100644 index 10e4d52..0000000 --- a/src/org/unitConverter/math/ZeroIsNullMap.java +++ /dev/null @@ -1,129 +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 . - */ -package org.unitConverter.math; - -import java.util.AbstractMap; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Set; - -/** - * A "wrapper" for an existing map that treats a zero value as equivalent to null. - * - * @author Adrien Hopkins - * @since 2019-10-16 - * @param - * type of key - */ -public final class ZeroIsNullMap extends AbstractMap { - /** - * Create a ZeroIsNullMap. This method always creates a new map. - * - * @param - * type of key - * @param map - * map to input - * @return map that treats zero as null - * @throws NullPointerException - * if map is null - * @since 2019-10-16 - */ - public static Map create(final Map map) { - return new ZeroIsNullMap<>(Objects.requireNonNull(map, "map must not be null.")); - } - - private final Map map; - - /** - * Creates the {@code ObjectProductMap}. - * - * @param map - * @since 2019-10-16 - */ - private ZeroIsNullMap(final Map map) { - this.map = map; - } - - @Override - public void clear() { - this.map.clear(); - } - - @Override - public boolean containsKey(final Object key) { - return this.map.containsKey(key) && this.map.get(key) != 0; - } - - @Override - public boolean containsValue(final Object value) { - return this.values().contains(value); - } - - @Override - public Set> entrySet() { - final Set> entrySet = new HashSet<>(this.map.entrySet()); - entrySet.removeIf(e -> e.getValue() == 0); - return entrySet; - } - - @Override - public Integer get(final Object key) { - final Integer i = this.map.get(key); - if (Objects.equals(i, 0)) - return null; - else - return i; - } - - @Override - public Set keySet() { - final Set keySet = new HashSet<>(this.map.keySet()); - keySet.removeIf(k -> this.map.get(k) == 0); - return keySet; - } - - @Override - public Integer put(final T key, final Integer value) { - if (value != 0) - return this.map.put(key, value); - else - return null; - } - - @Override - public void putAll(final Map m) { - for (final T key : m.keySet()) { - this.put(key, m.get(key)); - } - } - - @Override - public Integer remove(final Object key) { - return this.map.remove(key); - } - - @Override - public Collection values() { - final List values = new ArrayList<>(this.map.values()); - values.removeIf(i -> i == 0); - return values; - } -} \ No newline at end of file diff --git a/src/org/unitConverter/math/ZeroIsNullMapTest.java b/src/org/unitConverter/math/ZeroIsNullMapTest.java deleted file mode 100644 index c3db951..0000000 --- a/src/org/unitConverter/math/ZeroIsNullMapTest.java +++ /dev/null @@ -1,113 +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 . - */ -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.assertTrue; - -import java.util.HashMap; -import java.util.Map; -import java.util.Map.Entry; - -import org.junit.jupiter.api.Test; - -/** - * @author Adrien Hopkins - * @since 2019-10-16 - */ -class ZeroIsNullMapTest { - - /** - * @return map to be used for test data - * @since 2019-10-16 - */ - Map getTestMap() { - final Map map = new HashMap<>(); - map.put("one", 1); - map.put("two", 2); - map.put("zero", 0); - map.put("ten", 10); - return ZeroIsNullMap.create(map); - } - - /** - * Test method for {@link org.unitConverter.math.ZeroIsNullMap#containsKey(java.lang.Object)}. - */ - @Test - void testContainsKeyObject() { - final Map 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 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 map = this.getTestMap(); - for (final Entry e : map.entrySet()) { - assertTrue(e.getValue() != 0); - } - } - - /** - * Test method for {@link org.unitConverter.math.ZeroIsNullMap#get(java.lang.Object)}. - */ - @Test - void testGetObject() { - final Map map = this.getTestMap(); - assertEquals(1, map.get("one")); - assertEquals(10, map.get("ten")); - assertEquals(null, map.get("five")); - assertEquals(null, map.get("zero")); - } - - /** - * Test method for {@link org.unitConverter.math.ZeroIsNullMap#keySet()}. - */ - @Test - void testKeySet() { - final Map map = this.getTestMap(); - assertFalse(map.keySet().contains("zero")); - } - - /** - * Test method for {@link org.unitConverter.math.ZeroIsNullMap#values()}. - */ - @Test - void testValues() { - final Map map = this.getTestMap(); - assertFalse(map.values().contains(0)); - } - -} -- cgit v1.2.3 From e28907d4bf4234cb988a5e76ebf7122c7c1ff6cb Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Sat, 19 Oct 2019 21:40:33 -0400 Subject: Added a generic ConditionalExistenceCollection --- .../math/ConditionalExistenceCollections.java | 87 +++++++++++++++++++++- 1 file changed, 86 insertions(+), 1 deletion(-) diff --git a/src/org/unitConverter/math/ConditionalExistenceCollections.java b/src/org/unitConverter/math/ConditionalExistenceCollections.java index c0a69f0..9522885 100644 --- a/src/org/unitConverter/math/ConditionalExistenceCollections.java +++ b/src/org/unitConverter/math/ConditionalExistenceCollections.java @@ -16,6 +16,7 @@ */ package org.unitConverter.math; +import java.util.AbstractCollection; import java.util.AbstractMap; import java.util.AbstractSet; import java.util.Collection; @@ -48,8 +49,74 @@ import java.util.function.Predicate; * @author Adrien Hopkins * @since 2019-10-17 */ -// TODO add conditional existence Collections, Lists and Sorted/Navigable Sets/Maps +// 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 + * type of element in collection + */ + static final class ConditionalExistenceCollection extends AbstractCollection { + final Collection collection; + final Predicate existenceCondition; + + /** + * Creates the {@code ConditionalExistenceCollection}. + * + * @param collection + * @param existenceCondition + * @since 2019-10-17 + */ + private ConditionalExistenceCollection(final Collection collection, final Predicate 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 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. * @@ -258,6 +325,7 @@ public final class ConditionalExistenceCollections { @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; } @@ -268,6 +336,23 @@ public final class ConditionalExistenceCollections { } } + /** + * Elements in the returned wrapper collection are ignored if they don't pass a condition. + * + * @param + * 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 Collection conditionalExistenceCollection(final Collection collection, + final Predicate existenceCondition) { + return new ConditionalExistenceCollection<>(collection, existenceCondition); + } + /** * Elements in the returned wrapper iterator are ignored if they don't pass a condition. * -- cgit v1.2.3 From 1bf43ad95e70019a69e91e09ff74f677082ed6f5 Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Mon, 21 Oct 2019 15:17:50 -0400 Subject: Made improvements and corrections to the documentation. --- src/org/unitConverter/converterGUI/UnitConverterGUI.java | 2 +- src/org/unitConverter/converterGUI/package-info.java | 2 +- src/org/unitConverter/math/package-info.java | 3 ++- src/org/unitConverter/unit/LinearUnit.java | 8 ++++---- src/org/unitConverter/unit/Unit.java | 14 ++++++++++++-- src/org/unitConverter/unit/package-info.java | 3 ++- 6 files changed, 22 insertions(+), 10 deletions(-) diff --git a/src/org/unitConverter/converterGUI/UnitConverterGUI.java b/src/org/unitConverter/converterGUI/UnitConverterGUI.java index 4598971..511e47b 100644 --- a/src/org/unitConverter/converterGUI/UnitConverterGUI.java +++ b/src/org/unitConverter/converterGUI/UnitConverterGUI.java @@ -47,8 +47,8 @@ import org.unitConverter.unit.BaseDimension; import org.unitConverter.unit.LinearUnit; import org.unitConverter.unit.SI; import org.unitConverter.unit.Unit; -import org.unitConverter.unit.UnitPrefix; import org.unitConverter.unit.UnitDatabase; +import org.unitConverter.unit.UnitPrefix; /** * @author Adrien Hopkins 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 . */ /** - * 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/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 . */ /** - * 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/LinearUnit.java b/src/org/unitConverter/unit/LinearUnit.java index c397250..1918d6b 100644 --- a/src/org/unitConverter/unit/LinearUnit.java +++ b/src/org/unitConverter/unit/LinearUnit.java @@ -167,8 +167,8 @@ public final class LinearUnit extends Unit { /** * Returns the difference of this unit and another. *

- * 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. *

* * @param subtrahend @@ -196,8 +196,8 @@ public final class LinearUnit extends Unit { /** * Returns the sum of this unit and another. *

- * 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. *

* * @param addend diff --git a/src/org/unitConverter/unit/Unit.java b/src/org/unitConverter/unit/Unit.java index d4eb86e..7971a41 100644 --- a/src/org/unitConverter/unit/Unit.java +++ b/src/org/unitConverter/unit/Unit.java @@ -52,7 +52,7 @@ public abstract class Unit { * @throws NullPointerException * if any argument is null */ - public static Unit fromConversionFunctions(final ObjectProduct base, + public static final Unit fromConversionFunctions(final ObjectProduct base, final DoubleUnaryOperator converterFrom, final DoubleUnaryOperator converterTo) { return FunctionalUnit.valueOf(base, converterFrom, converterTo); } @@ -121,6 +121,9 @@ public abstract class Unit { * If this unit is a base unit, this method should return {@code value}. *

* + * @implSpec This method is used by {@link #convertTo}, and its behaviour affects the behaviour of + * {@code convertTo}. + * * @param value * value expressed in base unit * @return value expressed in this unit @@ -132,6 +135,10 @@ public abstract class Unit { /** * 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 @@ -140,7 +147,7 @@ public abstract class Unit { * @since 2019-05-22 * @throws IllegalArgumentException * if {@code other} is incompatible for conversion with this unit (as tested by - * {@link IUnit#canConvertTo}). + * {@link Unit#canConvertTo}). * @throws NullPointerException * if other is null */ @@ -162,6 +169,9 @@ public abstract class Unit { * If this unit is a base unit, this method should return {@code value}. *

* + * @implSpec This method is used by {@link #convertTo}, and its behaviour affects the behaviour of + * {@code convertTo}. + * * @param value * value expressed in this unit * @return value expressed in base unit diff --git a/src/org/unitConverter/unit/package-info.java b/src/org/unitConverter/unit/package-info.java index 2d83e1f..2f0e097 100644 --- a/src/org/unitConverter/unit/package-info.java +++ b/src/org/unitConverter/unit/package-info.java @@ -15,9 +15,10 @@ * along with this program. If not, see . */ /** - * The new definition for units. + * Everything to do with the units that make up Unit Converter. * * @author Adrien Hopkins * @since 2019-10-16 + * @since v0.1.0 */ package org.unitConverter.unit; \ No newline at end of file -- cgit v1.2.3 From ce7402fb5e52d947b6b7c383fa96e3aaaf9da188 Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Mon, 21 Oct 2019 15:23:17 -0400 Subject: Added back Fahrenheit, with more Imperial/USC units to come. --- .../converterGUI/UnitConverterGUI.java | 3 ++- src/org/unitConverter/unit/BritishImperial.java | 28 ++++++++++++++++++++++ src/org/unitConverter/unit/USCustomary.java | 27 +++++++++++++++++++++ unitsfile.txt | 4 ++-- 4 files changed, 59 insertions(+), 3 deletions(-) create mode 100644 src/org/unitConverter/unit/BritishImperial.java create mode 100644 src/org/unitConverter/unit/USCustomary.java diff --git a/src/org/unitConverter/converterGUI/UnitConverterGUI.java b/src/org/unitConverter/converterGUI/UnitConverterGUI.java index 511e47b..0be6c9b 100644 --- a/src/org/unitConverter/converterGUI/UnitConverterGUI.java +++ b/src/org/unitConverter/converterGUI/UnitConverterGUI.java @@ -44,6 +44,7 @@ import javax.swing.JTextField; 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; @@ -78,7 +79,7 @@ final class UnitConverterGUI { database.addUnit("unit", SI.ONE); // nonlinear units - must be loaded manually database.addUnit("tempCelsius", SI.CELSIUS); - // database.addUnit("tempFahrenheit", NonlinearUnits.FAHRENHEIT); + database.addUnit("tempFahrenheit", BritishImperial.FAHRENHEIT); // load initial dimensions database.addDimension("LENGTH", SI.Dimensions.LENGTH); diff --git a/src/org/unitConverter/unit/BritishImperial.java b/src/org/unitConverter/unit/BritishImperial.java new file mode 100644 index 0000000..c9316f3 --- /dev/null +++ b/src/org/unitConverter/unit/BritishImperial.java @@ -0,0 +1,28 @@ +/** + * 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 . + */ +package org.unitConverter.unit; + +/** + * A static utility class that contains units in the British Imperial system. + * + * @author Adrien Hopkins + * @since 2019-10-21 + */ +public final class BritishImperial { + 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/USCustomary.java b/src/org/unitConverter/unit/USCustomary.java new file mode 100644 index 0000000..f5f9a7f --- /dev/null +++ b/src/org/unitConverter/unit/USCustomary.java @@ -0,0 +1,27 @@ +/** + * 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 . + */ +package org.unitConverter.unit; + +/** + * A static utility class that contains units in the US Customary system. + * + * @author Adrien Hopkins + * @since 2019-10-21 + */ +public final class USCustomary { + public static final Unit FAHRENHEIT = BritishImperial.FAHRENHEIT; +} diff --git a/unitsfile.txt b/unitsfile.txt index 73a29d1..bda9b81 100644 --- a/unitsfile.txt +++ b/unitsfile.txt @@ -146,9 +146,9 @@ deg degree # Use tempC(100) for 100 degrees Celsius tempCelsius ! -#tempFahrenheit ! +tempFahrenheit ! tempC tempCelsius -#tempF tempFahrenheit +tempF tempFahrenheit # Common time units minute 60 second -- cgit v1.2.3