/** * 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 sevenUnits.unit; 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 static org.junit.jupiter.api.Assertions.fail; import java.io.IOException; import java.io.InputStream; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.NoSuchElementException; import java.util.Objects; import java.util.Set; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; import sevenUnits.utils.NameSymbol; import sevenUnits.utils.UncertainDouble; /** * 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 { private static final class SimpleEntry implements Map.Entry { private final K key; private V value; /** * * @since 2021-10-07 */ public SimpleEntry(K key, V value) { this.key = key; this.value = value; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (!(obj instanceof Map.Entry)) return false; final Map.Entry other = (Map.Entry) obj; return Objects.equals(this.key, other.getKey()) && Objects.equals(this.value, other.getValue()); } @Override public K getKey() { return this.key; } @Override public V getValue() { return this.value; } @Override public int hashCode() { return (this.key == null ? 0 : this.key.hashCode()) ^ (this.value == null ? 0 : this.value.hashCode()); } @Override public V setValue(V value) { final V oldValue = this.value; this.value = value; return oldValue; } } // some linear units and one nonlinear private static final Unit U = Metric.METRE; private static final Unit V = Metric.KILOGRAM; private static final Unit W = Metric.SECOND; // used for testing expressions // J = U^2 * V / W^2 private static final LinearUnit J = Metric.KILOGRAM .times(Metric.METRE.toExponent(2)) .dividedBy(Metric.SECOND.toExponent(2)); private static final LinearUnit K = Metric.KELVIN; private static final Unit NONLINEAR = Unit.fromConversionFunctions( Metric.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) .withName(NameSymbol.ofName("A")); private static final UnitPrefix B = UnitPrefix.valueOf(3) .withName(NameSymbol.ofName("B")); private static final UnitPrefix C = UnitPrefix.valueOf(5) .withName(NameSymbol.ofName("C")); private static final UnitPrefix AB = UnitPrefix.valueOf(7); private static final UnitPrefix BC = UnitPrefix.valueOf(11); /** * Gets a map entry. * * @param type of key * @param type of value * @param key key in entry * @param value value in entry * @return entry * @since 2021-10-07 */ private static Map.Entry entry(K key, V value) { return new SimpleEntry<>(key, value); } /** * Loads the dimensionfile at src/test/resources/[path] to the database * {@code loadTo}. * * @param loadTo database to load to * @param path path of file to load * @since 2021-10-04 */ private static void loadDimensionFile(UnitDatabase loadTo, String path) { try (final InputStream testFile = UnitDatabaseTest.class .getResourceAsStream(path)) { loadTo.loadDimensionsFromStream(testFile); } catch (final IOException e) { fail(e.getClass() + " occurred upon loading file \"" + path + "\"."); } } /** * Loads the unitfile at src/test/resources/[path] to the database * {@code loadTo}. * * @param loadTo database to load to * @param path path of file to load * @since 2021-09-22 */ private static void loadUnitsFile(UnitDatabase loadTo, String path) { try (final InputStream testFile = UnitDatabaseTest.class .getResourceAsStream(path)) { loadTo.loadUnitsFromStream(testFile); } catch (final IOException e) { fail(e.getClass() + " occurred upon loading file \"" + path + "\"."); } } /** * A test for the {@link UnitDatabase#evaluateUnitExpression(String)} * function. Simple because the expression parser has its own test. * * @since 2021-09-27 */ @Test public void testEvaluateExpression() { 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 LinearUnitValue expected = LinearUnitValue.of(J, UncertainDouble.of(12, Math.sqrt(14.625))); // note: units are exact, each number has an uncertainty of 1 final LinearUnitValue actual = database .evaluateUnitExpression("J + (2 * 3) J + (20 / 4) J"); assertEquals(expected, actual); // check that negation works properly assertEquals(2, database.evaluateUnitExpression("J - -1 * J").getValueExact()); } /** * Test for {@link UnitDatabase#getUnit}, {@link UnitDatabase#getLinearUnit} * and {@link UnitDatabase#getLinearUnitValue}. * * @since 2021-10-07 */ @Test public void testGetUnit() { final UnitDatabase database = new UnitDatabase(); database.addUnit("m", Metric.METRE); database.addUnit("meter", Metric.METRE); database.addUnit("metre", Metric.METRE); database.addUnit("badname", Metric.METRE); database.addUnit("K", Metric.KELVIN); database.addUnit("degC", Metric.CELSIUS); // ensure getUnit returns units, regardless of whether the name is one of // the unit's names assertEquals(Metric.METRE, database.getUnit("m")); assertEquals(Metric.METRE, database.getUnit("metre")); assertEquals(Metric.METRE, database.getUnit("meter")); assertEquals(Metric.METRE, database.getUnit("badname")); assertThrows(NoSuchElementException.class, () -> database.getUnit("blabla")); assertEquals(Metric.KELVIN, database.getLinearUnit("K")); assertThrows(IllegalArgumentException.class, () -> database.getLinearUnit("degC")); assertEquals(Metric.KELVIN.times(373.15), database.getLinearUnit("degC(100)")); } /** * Confirms that operations that shouldn't function for infinite databases * throw an {@code IllegalStateException}. * * @since 2019-05-03 */ // @Test // @Timeout(value = 1, unit = TimeUnit.SECONDS) 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); final Set> entrySet = infiniteDatabase.unitMap() .entrySet(); final Set keySet = infiniteDatabase.unitMap().keySet(); assertThrows(IllegalStateException.class, () -> entrySet.toArray()); assertThrows(IllegalStateException.class, () -> keySet.toArray()); } /** * A bunch of tests for invalid dimension files * * @param num which file to test * @since 2021-10-04 */ @ParameterizedTest @ValueSource(ints = { 1, 2, 3 }) public void testLoadingInvalidDimensionFile(int num) { final UnitDatabase database = new UnitDatabase(); database.addDimension("LENGTH", Metric.Dimensions.LENGTH); database.addDimension("MASS", Metric.Dimensions.MASS); database.addDimension("TIME", Metric.Dimensions.TIME); final String filename = String.format("/test-dimensionfile-invalid%d.txt", num); final RuntimeException e = assertThrows(RuntimeException.class, () -> loadDimensionFile(database, filename)); assertTrue(e instanceof IllegalArgumentException || e instanceof NoSuchElementException); } /** * A bunch of tests for invalid unit files * * @param num which file to test * @since 2021-09-27 */ @ParameterizedTest @ValueSource(ints = { 1, 2, 3, 4, 5 }) public void testLoadingInvalidUnitFile(int num) { final UnitDatabase database = new UnitDatabase(); final String filename = String.format("/test-unitsfile-invalid%d.txt", num); final RuntimeException e = assertThrows(RuntimeException.class, () -> loadUnitsFile(database, filename)); assertTrue(e instanceof IllegalArgumentException || e instanceof NoSuchElementException); } /** * Tests loading a valid dimension-file with some derived dimensions. * * @since 2021-10-04 */ @Test public void testLoadingValidDimensions() { final UnitDatabase database = new UnitDatabase(); database.addDimension("LENGTH", Metric.Dimensions.LENGTH); database.addDimension("MASS", Metric.Dimensions.MASS); database.addDimension("TIME", Metric.Dimensions.TIME); loadDimensionFile(database, "/test-dimensionfile-valid1.txt"); assertEquals(Metric.Dimensions.ENERGY, database.getDimension("ENERGY")); assertEquals(Metric.Dimensions.POWER, database.getDimension("POWER")); } /** * Tests loading a valid unitfile with some prefixes and no units. * * @since 2021-09-22 */ @Test public void testLoadingValidPrefixes() { final UnitDatabase database = new UnitDatabase(); loadUnitsFile(database, "/test-unitsfile-valid2.txt"); assertEquals(7, database.getPrefix("A").getMultiplier()); assertEquals(11, database.getPrefix("B").getMultiplier()); assertEquals(13, database.getPrefix("C").getMultiplier()); } /** * Tests loading a valid unitfile with some units and preloaded prefixes * * @since 2021-09-22 */ @Test public void testLoadingValidUnits() { 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); loadUnitsFile(database, "/test-unitsfile-valid1.txt"); final Unit expected1 = ((LinearUnit) U).withPrefix(A).withPrefix(B) .withPrefix(C); final Unit actual1 = database.getUnit("test1"); assertEquals(expected1, actual1); final Unit expected2 = ((LinearUnit) W).withPrefix(B) .times(((LinearUnit) V).withPrefix(C)); final Unit actual2 = database.getUnit("test2"); assertEquals(expected2, actual2); final Unit expected3 = ((LinearUnit) U) .times(A.getMultiplier() + C.getMultiplier() - B.getMultiplier()); final Unit actual3 = database.getUnit("test3"); assertEquals(expected3, actual3); final UnitValue expected4 = UnitValue.of(U, 1); final UnitValue actual4 = database .evaluateUnitExpression("-5 * U + -3 * U + 12 * U - 3 * U") .asUnitValue(); assertEquals(expected4, actual4); assertTrue(System.err.toString().length() > 0); } /** * Tests the iterator of the prefixless unit map. These tests are simple, as * the unit map iterator is simple. * * @since 2021-10-07 */ @Test public void testPrefixedUnitMapIterator() { final UnitDatabase database1 = new UnitDatabase(); database1.addUnit("U", U); database1.addUnit("V", V); database1.addUnit("W", W); final Map map1 = database1.unitMap(); final Iterator keyIterator1 = map1.keySet().iterator(); final Iterator> entryIterator1 = map1.entrySet() .iterator(); final Set expectedKeys = Set.of("U", "V", "W"); final Set actualKeys = new HashSet<>(); while (keyIterator1.hasNext()) { actualKeys.add(keyIterator1.next()); } assertEquals(expectedKeys, actualKeys); assertEquals(expectedKeys, map1.keySet()); final Set> expectedEntries = Set.of(entry("U", U), entry("V", V), entry("W", W)); final Set> actualEntries = new HashSet<>(); while (entryIterator1.hasNext()) { actualEntries.add(entryIterator1.next()); } assertEquals(expectedEntries, actualEntries); assertEquals(expectedEntries, map1.entrySet()); } /** * 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); // test the getPrefixesFromName method final List expected = Arrays.asList(C, B, A); assertEquals(expected, database.getPrefixesFromName("ABCU")); // 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(true); 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")); assertThrows(NoSuchElementException.class, () -> database.getUnit("Z")); } @Test public void testRemovableDuplicates() { final Map unitMap = new HashMap<>(); unitMap.put("meter", Metric.METRE); unitMap.put("metre", Metric.METRE); unitMap.put("m", Metric.METRE); unitMap.put("second", Metric.SECOND); assertTrue(UnitDatabase.isRemovableDuplicate(unitMap, entry("m", Metric.METRE))); assertTrue(UnitDatabase.isRemovableDuplicate(unitMap, entry("meter", Metric.METRE))); assertFalse(UnitDatabase.isRemovableDuplicate(unitMap, entry("metre", Metric.METRE))); assertFalse(UnitDatabase.isRemovableDuplicate(unitMap, entry("second", Metric.SECOND))); } @Test public void testToString() { final UnitDatabase database = new UnitDatabase(); database.addUnit("J", J); database.addUnit("K", J); database.addPrefix("A", A); database.addPrefix("B", B); database.addPrefix("C", C); if ("Unit Database with 1 units, 3 unit prefixes and 0 dimensions" .equals(database.toString())) { fail("Database counts by number of units, not number of unit names."); } assertEquals( "Unit Database with 2 units, 3 unit prefixes and 0 dimensions", database.toString()); } /** * 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); // test incorrect expressions assertThrows(IllegalArgumentException.class, () -> database.getUnitFromExpression("U + V")); assertThrows(IllegalArgumentException.class, () -> database.getUnitFromExpression("U - V")); } /** * 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(true).size(); final int NUM_PREFIXES = database.prefixMap(true).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); } }