summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAdrien Hopkins <adrien.p.hopkins@gmail.com>2019-04-14 14:32:48 -0400
committerAdrien Hopkins <adrien.p.hopkins@gmail.com>2019-04-14 14:32:48 -0400
commitfc1083454e4e9215140802602a17aafeef4515fa (patch)
tree020e5be4a384859ee2339e16e48e5778c25a5943
parent77051c4f70f450a4363be7ae587de36efc1fdd54 (diff)
Added a UnitDatabase test, and fixed some bugs using it.
-rwxr-xr-xsrc/org/unitConverter/UnitsDatabase.java395
-rw-r--r--src/test/java/UnitsDatabaseTest.java253
2 files changed, 512 insertions, 136 deletions
diff --git a/src/org/unitConverter/UnitsDatabase.java b/src/org/unitConverter/UnitsDatabase.java
index 901c6ef..959c151 100755
--- a/src/org/unitConverter/UnitsDatabase.java
+++ b/src/org/unitConverter/UnitsDatabase.java
@@ -30,6 +30,7 @@ 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;
@@ -83,6 +84,153 @@ public final class UnitsDatabase {
* @since 2019-04-13
*/
private static final class PrefixedUnitEntrySet extends AbstractSet<Map.Entry<String, Unit>> {
+ /**
+ * The entry for this set.
+ *
+ * @author Adrien Hopkins
+ * @since 2019-04-14
+ */
+ private static final class PrefixedUnitEntry implements Entry<String, Unit> {
+ private final String key;
+ private final Unit value;
+
+ /**
+ * Creates the {@code PrefixedUnitEntry}.
+ *
+ * @param key
+ * @param value
+ * @since 2019-04-14
+ */
+ public PrefixedUnitEntry(final String key, final Unit value) {
+ this.key = key;
+ this.value = value;
+ }
+
+ @Override
+ public String getKey() {
+ return this.key;
+ }
+
+ @Override
+ public Unit getValue() {
+ return this.value;
+ }
+
+ @Override
+ public Unit setValue(final Unit value) {
+ throw new UnsupportedOperationException();
+ }
+ }
+
+ /**
+ * An iterator that iterates over the units of a {@code PrefixedUnitNameSet}.
+ *
+ * @author Adrien Hopkins
+ * @since 2019-04-14
+ */
+ private static final class PrefixedUnitEntryIterator implements Iterator<Entry<String, Unit>> {
+ // position in the unit list
+ private int unitNamePosition = 0;
+ // the indices of the prefixes attached to the current unit
+ private final List<Integer> prefixCoordinates = new ArrayList<>();
+
+ private final Map<String, Unit> map;
+ private final List<String> unitNames;
+ private final List<String> prefixNames;
+
+ /**
+ * Creates the {@code UnitsDatabase.PrefixedUnitMap.PrefixedUnitNameSet.PrefixedUnitNameIterator}.
+ *
+ * @since 2019-04-14
+ */
+ public PrefixedUnitEntryIterator(final PrefixedUnitEntrySet set) {
+ this.map = set.map;
+ this.unitNames = new ArrayList<>(set.map.units.keySet());
+ this.prefixNames = new ArrayList<>(set.map.prefixes.keySet());
+ }
+
+ /**
+ * @return current unit name
+ * @since 2019-04-14
+ */
+ 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
+ */
+ private void incrementPosition() {
+ this.unitNamePosition++;
+
+ if (this.unitNamePosition >= this.unitNames.size()) {
+ // we have used all of our units, go to a different prefix
+ this.unitNamePosition = 0;
+
+ // if the prefix coordinates are empty, then set it to [0]
+ if (this.prefixCoordinates.isEmpty()) {
+ this.prefixCoordinates.add(0, 0);
+ } else {
+ // get the prefix coordinate to increment, then increment
+ int i = this.prefixCoordinates.size() - 1;
+ this.prefixCoordinates.set(i, this.prefixCoordinates.get(i) + 1);
+
+ // fix any carrying errors
+ while (i >= 0 && this.prefixCoordinates.get(i) >= this.prefixNames.size()) {
+ // carry over
+ this.prefixCoordinates.set(i--, 0); // null and decrement at the same time
+
+ if (i < 0) { // we need to add a new coordinate
+ this.prefixCoordinates.add(0, 0);
+ } else { // increment an existing one
+ this.prefixCoordinates.set(i, this.prefixCoordinates.get(i) + 1);
+ }
+ }
+ }
+ }
+ }
+
+ @Override
+ public Entry<String, Unit> next() {
+ 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();
+
+ this.incrementPosition();
+
+ return new PrefixedUnitEntry(nextName, this.map.get(nextName));
+ }
+ }
+
// the map that created this set
private final PrefixedUnitMap map;
@@ -143,83 +291,7 @@ public final class UnitsDatabase {
@Override
public Iterator<Entry<String, Unit>> iterator() {
- return new Iterator<Entry<String, Unit>>() {
- // position in the unit list
- int unitNamePosition = -1;
- // the indices of the prefixes attached to the current unit
- List<Integer> prefixCoordinates = new ArrayList<>();
-
- List<String> unitNames = new ArrayList<>(PrefixedUnitEntrySet.this.map.units.keySet());
- List<String> prefixNames = new ArrayList<>(PrefixedUnitEntrySet.this.map.prefixes.keySet());
-
- @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;
- }
- }
-
- @Override
- public Entry<String, Unit> next() {
- // increment unit name position
- this.unitNamePosition++;
-
- // 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 (!(PrefixedUnitEntrySet.this.map
- .get(this.unitNames.get(this.unitNamePosition)) instanceof LinearUnit)) {
- this.unitNames.remove(this.unitNamePosition);
- }
- }
-
- // carry over
- if (!this.prefixNames.isEmpty() && this.unitNamePosition >= this.unitNames.size() - 1) {
- // handle prefix position
- this.unitNamePosition = 0;
- int i = this.prefixCoordinates.size() - 1;
- this.prefixCoordinates.set(i, this.prefixCoordinates.get(i) + 1);
-
- while (this.prefixCoordinates.get(i) >= this.prefixNames.size() - 1) {
- this.prefixCoordinates.set(i, 0);
- i--;
- if (i < 0) {
- this.prefixCoordinates.add(0, 0);
- }
- }
- }
-
- // create the unit name
- final StringBuilder unitNameBuilder = new StringBuilder();
- for (final int i : this.prefixCoordinates) {
- unitNameBuilder.append(this.prefixNames.get(i));
- }
- unitNameBuilder.append(this.unitNames.get(this.unitNamePosition));
-
- final String unitName = unitNameBuilder.toString();
- return new Entry<String, Unit>() {
- @Override
- public String getKey() {
- return unitName;
- }
-
- @Override
- public Unit getValue() {
- return PrefixedUnitEntrySet.this.map.get(unitName);
- }
-
- @Override
- public Unit setValue(final Unit value) {
- throw new UnsupportedOperationException();
- }
- };
- }
- };
+ return new PrefixedUnitEntryIterator(this);
}
@Override
@@ -282,6 +354,115 @@ public final class UnitsDatabase {
* @since 2019-04-13
*/
private static final class PrefixedUnitNameSet extends AbstractSet<String> {
+ /**
+ * An iterator that iterates over the units of a {@code PrefixedUnitNameSet}.
+ *
+ * @author Adrien Hopkins
+ * @since 2019-04-14
+ */
+ private static final class PrefixedUnitNameIterator implements Iterator<String> {
+ // position in the unit list
+ private int unitNamePosition = 0;
+ // the indices of the prefixes attached to the current unit
+ private final List<Integer> prefixCoordinates = new ArrayList<>();
+
+ private final Map<String, Unit> map;
+ private final List<String> unitNames;
+ private final List<String> prefixNames;
+
+ /**
+ * Creates the {@code UnitsDatabase.PrefixedUnitMap.PrefixedUnitNameSet.PrefixedUnitNameIterator}.
+ *
+ * @since 2019-04-14
+ */
+ public PrefixedUnitNameIterator(final PrefixedUnitNameSet set) {
+ this.map = set.map;
+ this.unitNames = new ArrayList<>(set.map.units.keySet());
+ this.prefixNames = new ArrayList<>(set.map.prefixes.keySet());
+ }
+
+ /**
+ * @return current unit name
+ * @since 2019-04-14
+ */
+ 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
+ */
+ 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() {
+ 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();
+
+ this.incrementPosition();
+
+ return nextName;
+ }
+ }
+
// the map that created this set
private final PrefixedUnitMap map;
@@ -330,65 +511,7 @@ public final class UnitsDatabase {
@Override
public Iterator<String> iterator() {
- return new Iterator<String>() {
- // position in the unit list
- int unitNamePosition = -1;
- // the indices of the prefixes attached to the current unit
- List<Integer> prefixCoordinates = new ArrayList<>();
-
- List<String> unitNames = new ArrayList<>(PrefixedUnitNameSet.this.map.units.keySet());
- List<String> prefixNames = new ArrayList<>(PrefixedUnitNameSet.this.map.prefixes.keySet());
-
- @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;
- }
- }
-
- @Override
- public String next() {
- // increment unit name position
- this.unitNamePosition++;
-
- // 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 (!(PrefixedUnitNameSet.this.map
- .get(this.unitNames.get(this.unitNamePosition)) instanceof LinearUnit)) {
- this.unitNames.remove(this.unitNamePosition);
- }
- }
-
- // carry over
- if (!this.prefixNames.isEmpty() && this.unitNamePosition >= this.unitNames.size() - 1) {
- // handle prefix position
- this.unitNamePosition = 0;
- int i = this.prefixCoordinates.size() - 1;
- this.prefixCoordinates.set(i, this.prefixCoordinates.get(i) + 1);
-
- while (this.prefixCoordinates.get(i) >= this.prefixNames.size() - 1) {
- this.prefixCoordinates.set(i, 0);
- i--;
- if (i < 0) {
- this.prefixCoordinates.add(0, 0);
- }
- }
- }
-
- 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();
- }
- };
+ return new PrefixedUnitNameIterator(this);
}
@Override
diff --git a/src/test/java/UnitsDatabaseTest.java b/src/test/java/UnitsDatabaseTest.java
new file mode 100644
index 0000000..39f95a5
--- /dev/null
+++ b/src/test/java/UnitsDatabaseTest.java
@@ -0,0 +1,253 @@
+/**
+ * Copyright (C) 2019 Adrien Hopkins
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package test.java;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import org.junit.Test;
+import org.unitConverter.UnitsDatabase;
+import org.unitConverter.unit.AbstractUnit;
+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.
+ *
+ * @author Adrien Hopkins
+ * @since 2019-04-14
+ */
+public 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 Unit NONLINEAR = new AbstractUnit(SI.METRE) {
+
+ @Override
+ public double convertFromBase(final double value) {
+ return value + 1;
+ }
+
+ @Override
+ public double convertToBase(final double value) {
+ return value - 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);
+
+ /**
+ * Test that prefixes correctly apply to units.
+ *
+ * @since 2019-04-14
+ */
+ @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.
+ *
+ * <p>
+ * The map should be an auto-updating view of the units in the database.
+ * </p>
+ *
+ * @since 2019-04-14
+ */
+ @Test
+ public void testPrefixlessUnitMap() {
+ final UnitsDatabase database = new UnitsDatabase();
+ final Map<String, Unit> 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
+ */
+ @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
+ */
+ @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
+ */
+ @Test
+ public void testUnitIterator() {
+ // load units
+ final UnitsDatabase database = new UnitsDatabase();
+
+ database.addUnit("J", J);
+
+ database.addPrefix("A", A);
+ database.addPrefix("B", B);
+ database.addPrefix("C", C);
+
+ final Iterator<String> nameIterator = database.unitMap().keySet().iterator();
+ final Iterator<Entry<String, Unit>> 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 >= (int) Math.pow(3, expectedLength - 1)) {
+ expectedLength++;
+ unitsWithThisLengthSoFar = 0;
+ }
+
+ final String nextName = nameIterator.next();
+ final Unit nextUnit = database.getUnit(nextName);
+ final Entry<String, Unit> nextEntry = entryIterator.next();
+
+ assertEquals(expectedLength, nextName.length());
+ assertEquals(nextName, nextEntry.getKey());
+ assertEquals(nextUnit, nextEntry.getValue());
+
+ unitsWithThisLengthSoFar++;
+ }
+ }
+
+ /**
+ * Determine, given a unit name that could mean multiple things, which meaning is chosen.
+ * <p>
+ * 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.
+ * </p>
+ *
+ * @since 2019-04-14
+ */
+ @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);
+ }
+}