diff options
author | Adrien Hopkins <adrien.p.hopkins@gmail.com> | 2019-04-14 14:32:48 -0400 |
---|---|---|
committer | Adrien Hopkins <adrien.p.hopkins@gmail.com> | 2019-04-14 14:32:48 -0400 |
commit | fc1083454e4e9215140802602a17aafeef4515fa (patch) | |
tree | 020e5be4a384859ee2339e16e48e5778c25a5943 | |
parent | 77051c4f70f450a4363be7ae587de36efc1fdd54 (diff) |
Added a UnitDatabase test, and fixed some bugs using it.
-rwxr-xr-x | src/org/unitConverter/UnitsDatabase.java | 395 | ||||
-rw-r--r-- | src/test/java/UnitsDatabaseTest.java | 253 |
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); + } +} |