From 6f1bbc1024eae98f1815ab5f9e9cb3399f5eef9c Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Thu, 22 Aug 2024 10:12:37 -0500 Subject: Allow fractional exponents --- docs/roadmap.org | 1 - 1 file changed, 1 deletion(-) (limited to 'docs') diff --git a/docs/roadmap.org b/docs/roadmap.org index 5a3888f..bd0ccee 100644 --- a/docs/roadmap.org +++ b/docs/roadmap.org @@ -8,7 +8,6 @@ Feature Requirements: (It should not be required to handle features that aren't in 7Units; those definitions should be ignored with a warning) - 7Units's expression converter should support most or all of the conversion features supported by GNU Units: - Converting to sums of units (it should also be possible to do this in the unit converter with preset combinations) - - Non-integer exponents - (/Optional/) Inverse nonlinear conversion with the tilde prefix - (/Optional/) Nonlinear units should be specifiable in unit files. - /Any other feature not listed should be considered optional./ -- cgit v1.2.3 From ea3e2bf07939926e43c7abe3fd13a7c4e93f69d1 Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Thu, 22 Aug 2024 11:41:04 -0500 Subject: Show unit/dim file errors as popup Previously, any error in the unit or dimension file(s) crashes the program. Instead, 7Units now ignores any invalid lines, still parsing the correct ones, and shows a popup in case any errors happen. --- docs/roadmap.org | 3 +- .../java/sevenUnits/unit/LoadingException.java | 96 +++++++++ src/main/java/sevenUnits/unit/UnitDatabase.java | 138 ++++++------- src/main/java/sevenUnitsGUI/Presenter.java | 30 ++- .../java/sevenUnits/unit/UnitDatabaseTest.java | 221 +++++++++++---------- src/test/java/sevenUnitsGUI/TabbedViewTest.java | 28 +-- 6 files changed, 323 insertions(+), 193 deletions(-) create mode 100644 src/main/java/sevenUnits/unit/LoadingException.java (limited to 'docs') diff --git a/docs/roadmap.org b/docs/roadmap.org index bd0ccee..04ea0a5 100644 --- a/docs/roadmap.org +++ b/docs/roadmap.org @@ -5,9 +5,8 @@ These requirements are subject to change. I intend to finish version 1.0.0 by [ Feature Requirements: - 7Units should be able to parse unit files from [[https://www.gnu.org/software/units/][GNU Units]], the program that inspired it. - (It should not be required to handle features that aren't in 7Units; those definitions should be ignored with a warning) - 7Units's expression converter should support most or all of the conversion features supported by GNU Units: - - Converting to sums of units (it should also be possible to do this in the unit converter with preset combinations) + - (/Mostly Done/) Converting to sums of units (it should also be possible to do this in the unit converter with preset combinations) - (/Optional/) Inverse nonlinear conversion with the tilde prefix - (/Optional/) Nonlinear units should be specifiable in unit files. - /Any other feature not listed should be considered optional./ diff --git a/src/main/java/sevenUnits/unit/LoadingException.java b/src/main/java/sevenUnits/unit/LoadingException.java new file mode 100644 index 0000000..9376ed7 --- /dev/null +++ b/src/main/java/sevenUnits/unit/LoadingException.java @@ -0,0 +1,96 @@ +/** + * Copyright (C) 2024 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 java.nio.file.Path; +import java.util.Optional; + +/** + * An exception that occurred when loading a file. This wrapper class adds more + * info about the error. + * + * @author Adrien Hopkins + * @since 2024-08-22 + */ +public final class LoadingException extends RuntimeException { + public static enum FileType { + UNIT, DIMENSION + } + + private static final long serialVersionUID = -8167971828216907607L; + + private final long lineNumber; + private final String line; + private final Optional file; + + private final FileType fileType; + + private final RuntimeException problem; + + public LoadingException(long lineNumber, String line, FileType fileType, + RuntimeException problem) { + super(problem); + this.lineNumber = lineNumber; + this.line = line; + this.file = Optional.empty(); + this.fileType = fileType; + this.problem = problem; + } + + public LoadingException(long lineNumber, String line, Path file, + FileType fileType, RuntimeException problem) { + super(problem); + this.lineNumber = lineNumber; + this.line = line; + this.file = Optional.of(file); + this.fileType = fileType; + this.problem = problem; + } + + public Optional file() { + return this.file; + } + + public FileType fileType() { + return this.fileType; + } + + @Override + public String getMessage() { + return this.file + .map(f -> String.format( + "Error parsing line %d of %s file '%s' (\"%s\"): %s", + this.lineNumber, this.fileType.toString().toLowerCase(), f, + this.line, this.problem)) + .orElse(String.format( + "Error parsing line %d of %s stream (\"%s\"): %s", + this.lineNumber, this.fileType.toString().toLowerCase(), + this.line, this.problem)); + } + + public String line() { + return this.line; + } + + public long lineNumber() { + return this.lineNumber; + } + + public RuntimeException problem() { + return this.problem; + } +} diff --git a/src/main/java/sevenUnits/unit/UnitDatabase.java b/src/main/java/sevenUnits/unit/UnitDatabase.java index 514b27d..dc81aca 100644 --- a/src/main/java/sevenUnits/unit/UnitDatabase.java +++ b/src/main/java/sevenUnits/unit/UnitDatabase.java @@ -1364,16 +1364,7 @@ public final class UnitDatabase { 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 | NoSuchElementException e) { - System.err.printf("Parsing error on line %d:%n", lineCounter); - throw e; - } - - this.addDimension(name, dimension); + this.addDimension(name, this.getDimensionFromExpression(expression)); } } @@ -1454,56 +1445,15 @@ public final class UnitDatabase { .format("! used but no unit found (line %d).", lineCounter)); } else { if (name.endsWith("-")) { - final UnitPrefix prefix; - try { - prefix = this.getPrefixFromExpression(expression); - } catch (final IllegalArgumentException - | NoSuchElementException e) { - System.err.printf("Parsing error on line %d:%n", lineCounter); - throw e; - } final String prefixName = name.substring(0, name.length() - 1); - this.addPrefix(prefixName, prefix); + this.addPrefix(prefixName, + this.getPrefixFromExpression(expression)); } else if (expression.contains(";")) { // it's a multi-unit - final String[] parts = expression.split(";"); - final List units = new ArrayList<>(parts.length); - for (final String unitName : parts) { - final Unit unit; - try { - unit = this.getUnitFromExpression(unitName.trim()); - } catch (final NoSuchElementException e) { - System.err.printf("Parsing error on line %d:%n", lineCounter); - throw e; - } - - if (unit instanceof LinearUnit) { - units.add((LinearUnit) unit); - } else { - System.err.printf("Parsing error on line %d:%n", lineCounter); - throw new IllegalArgumentException(String.format( - "Unit '%s' is in a unit-set expression, but is not linear.", - unitName)); - } - } - - try { - this.addUnitSet(name, units); - } catch (final IllegalArgumentException e) { - System.err.printf("Parsing error on line %d:%n", lineCounter); - throw e; - } + this.addUnitSet(name, this.getUnitSetFromExpression(expression)); } else { // it's a unit, get the unit - final Unit unit; - try { - unit = this.getUnitFromExpression(expression); - } catch (final IllegalArgumentException - | NoSuchElementException e) { - System.err.printf("Parsing error on line %d:%n", lineCounter); - throw e; - } - this.addUnit(name, unit); + this.addUnit(name, this.getUnitFromExpression(expression)); } } } @@ -1950,6 +1900,27 @@ public final class UnitDatabase { return unitSet; } + /** + * Parses a semicolon-separated expression to get the unit set being used. + * + * @since 2024-08-22 + */ + private List getUnitSetFromExpression(String expression) { + final String[] parts = expression.split(";"); + final List units = new ArrayList<>(parts.length); + for (final String unitName : parts) { + final Unit unit = this.getUnitFromExpression(unitName.trim()); + + if (unit instanceof LinearUnit) { + units.add((LinearUnit) unit); + } else + throw new IllegalArgumentException(String.format( + "Unit '%s' is in a unit-set expression, but is not linear.", + unitName)); + } + return units; + } + /** * Adds all dimensions from a file, using data from the database to parse * them. @@ -1970,24 +1941,30 @@ public final class UnitDatabase { * * * @param file file to read - * @throws IllegalArgumentException if the file cannot be parsed, found or - * read - * @throws NullPointerException if file is null + * @throws NullPointerException if file is null + * @returns list of errors that happened when loading file * @since 2019-01-13 * @since v0.1.0 */ - public void loadDimensionFile(final Path file) { + public List loadDimensionFile(final Path file) { Objects.requireNonNull(file, "file must not be null."); + final List errors = new ArrayList<>(); try { long lineCounter = 0; for (final String line : Files.readAllLines(file)) { - this.addDimensionFromLine(line, ++lineCounter); + try { + this.addDimensionFromLine(line, ++lineCounter); + } catch (IllegalArgumentException | NoSuchElementException e) { + errors.add(new LoadingException(lineCounter, line, file, + LoadingException.FileType.DIMENSION, e)); + } } } 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 errors; } /** @@ -1997,13 +1974,22 @@ public final class UnitDatabase { * @param stream stream to load from * @since 2021-03-27 */ - public void loadDimensionsFromStream(final InputStream stream) { + public List loadDimensionsFromStream( + final InputStream stream) { + final List errors = new ArrayList<>(); try (final Scanner scanner = new Scanner(stream)) { long lineCounter = 0; while (scanner.hasNextLine()) { - this.addDimensionFromLine(scanner.nextLine(), ++lineCounter); + final String line = scanner.nextLine(); + try { + this.addDimensionFromLine(line, ++lineCounter); + } catch (IllegalArgumentException | NoSuchElementException e) { + errors.add(new LoadingException(lineCounter, line, + LoadingException.FileType.DIMENSION, e)); + } } } + return errors; } /** @@ -2025,24 +2011,30 @@ public final class UnitDatabase { * * * @param file file to read - * @throws IllegalArgumentException if the file cannot be parsed, found or - * read - * @throws NullPointerException if file is null + * @throws NullPointerException if file is null + * @returns list of errors that happened when loading file * @since 2019-01-13 * @since v0.1.0 */ - public void loadUnitsFile(final Path file) { + public List loadUnitsFile(final Path file) { Objects.requireNonNull(file, "file must not be null."); + final List errors = new ArrayList<>(); try { long lineCounter = 0; for (final String line : Files.readAllLines(file)) { - this.addUnitOrPrefixFromLine(line, ++lineCounter); + try { + this.addUnitOrPrefixFromLine(line, ++lineCounter); + } catch (IllegalArgumentException | NoSuchElementException e) { + errors.add(new LoadingException(lineCounter, line, file, + LoadingException.FileType.UNIT, e)); + } } } 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 errors; } /** @@ -2052,13 +2044,21 @@ public final class UnitDatabase { * @param stream stream to load from * @since 2021-03-27 */ - public void loadUnitsFromStream(InputStream stream) { + public List loadUnitsFromStream(InputStream stream) { + final List errors = new ArrayList<>(); try (final Scanner scanner = new Scanner(stream)) { long lineCounter = 0; while (scanner.hasNextLine()) { - this.addUnitOrPrefixFromLine(scanner.nextLine(), ++lineCounter); + final String line = scanner.nextLine(); + try { + this.addUnitOrPrefixFromLine(line, ++lineCounter); + } catch (IllegalArgumentException | NoSuchElementException e) { + errors.add(new LoadingException(lineCounter, line, + LoadingException.FileType.UNIT, e)); + } } } + return errors; } /** diff --git a/src/main/java/sevenUnitsGUI/Presenter.java b/src/main/java/sevenUnitsGUI/Presenter.java index 68f0fcb..4ff2d65 100644 --- a/src/main/java/sevenUnitsGUI/Presenter.java +++ b/src/main/java/sevenUnitsGUI/Presenter.java @@ -40,6 +40,7 @@ import sevenUnits.unit.BaseUnit; import sevenUnits.unit.BritishImperial; import sevenUnits.unit.LinearUnit; import sevenUnits.unit.LinearUnitValue; +import sevenUnits.unit.LoadingException; import sevenUnits.unit.Metric; import sevenUnits.unit.Unit; import sevenUnits.unit.UnitDatabase; @@ -311,7 +312,7 @@ public final class Presenter { // load units and prefixes try (final InputStream units = inputStream(DEFAULT_UNITS_FILEPATH)) { - this.database.loadUnitsFromStream(units); + this.handleLoadErrors(this.database.loadUnitsFromStream(units)); } catch (final IOException e) { throw new AssertionError("Loading of unitsfile.txt failed.", e); } @@ -319,7 +320,8 @@ public final class Presenter { // load dimensions try (final InputStream dimensions = inputStream( DEFAULT_DIMENSIONS_FILEPATH)) { - this.database.loadDimensionsFromStream(dimensions); + this.handleLoadErrors( + this.database.loadDimensionsFromStream(dimensions)); } catch (final IOException e) { throw new AssertionError("Loading of dimensionfile.txt failed.", e); } @@ -771,6 +773,24 @@ public final class Presenter { return this.view; } + /** + * Accepts a list of errors. If that list is non-empty, prints an error + * message and alerts the user. + * + * @since 2024-08-22 + */ + private void handleLoadErrors(List errors) { + if (!errors.isEmpty()) { + final String errorMessage = String.format( + "%d error(s) happened while loading file:\n%s\n", errors.size(), + errors.stream().map(Throwable::getMessage) + .collect(Collectors.joining("\n"))); + System.err.print(errorMessage); + this.view.showErrorMessage(errors.size() + "Loading Error(s)", + errorMessage); + } + } + /** * @return whether or not the provided unit is semi-metric (i.e. an * exception) @@ -832,13 +852,15 @@ public final class Presenter { // set manually to avoid the unnecessary saving of the non-manual // methods case "custom_dimension_file": - this.database.loadDimensionFile(pathFromConfig(value)); + this.handleLoadErrors( + this.database.loadDimensionFile(pathFromConfig(value))); break; case "custom_exception_file": this.loadExceptionFile(pathFromConfig(value)); break; case "custom_unit_file": - this.database.loadUnitsFile(pathFromConfig(value)); + this.handleLoadErrors( + this.database.loadUnitsFile(pathFromConfig(value))); break; case "number_display_rule": this.setDisplayRuleFromString(value); diff --git a/src/test/java/sevenUnits/unit/UnitDatabaseTest.java b/src/test/java/sevenUnits/unit/UnitDatabaseTest.java index 9d650f0..e7f3ccf 100644 --- a/src/test/java/sevenUnits/unit/UnitDatabaseTest.java +++ b/src/test/java/sevenUnits/unit/UnitDatabaseTest.java @@ -25,6 +25,7 @@ import static org.junit.jupiter.api.Assertions.fail; import java.io.IOException; import java.io.InputStream; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; @@ -53,9 +54,9 @@ import sevenUnits.utils.UncertainDouble; class UnitDatabaseTest { private static final class SimpleEntry implements Map.Entry { private final K key; - + private V value; - + /** * * @since 2021-10-07 @@ -64,7 +65,7 @@ class UnitDatabaseTest { this.key = key; this.value = value; } - + @Override public boolean equals(Object obj) { if (this == obj) @@ -75,23 +76,23 @@ class UnitDatabaseTest { 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; @@ -99,20 +100,20 @@ class UnitDatabaseTest { 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 @@ -123,9 +124,9 @@ class UnitDatabaseTest { 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. * @@ -139,41 +140,47 @@ class UnitDatabaseTest { 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 + * @return exceptions returned by file loading * @since 2021-10-04 */ - private static void loadDimensionFile(UnitDatabase loadTo, String path) { + private static List loadDimensionFile(UnitDatabase loadTo, + String path) { try (final InputStream testFile = UnitDatabaseTest.class .getResourceAsStream(path)) { - loadTo.loadDimensionsFromStream(testFile); + return loadTo.loadDimensionsFromStream(testFile); } catch (final IOException e) { fail(e.getClass() + " occurred upon loading file \"" + path + "\"."); + return Collections.emptyList(); } } - + /** * 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 + * @return exceptions returned by file loading * @since 2021-09-22 */ - private static void loadUnitsFile(UnitDatabase loadTo, String path) { + private static List loadUnitsFile(UnitDatabase loadTo, + String path) { try (final InputStream testFile = UnitDatabaseTest.class .getResourceAsStream(path)) { - loadTo.loadUnitsFromStream(testFile); + return loadTo.loadUnitsFromStream(testFile); } catch (final IOException e) { fail(e.getClass() + " occurred upon loading file \"" + path + "\"."); + return Collections.emptyList(); } } - + /** * A test for the {@link UnitDatabase#evaluateUnitExpression(String)} * function. Simple because the expression parser has its own test. @@ -183,26 +190,26 @@ class UnitDatabaseTest { @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}. @@ -212,14 +219,14 @@ class UnitDatabaseTest { @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")); @@ -228,14 +235,14 @@ class UnitDatabaseTest { 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}. @@ -247,21 +254,21 @@ class UnitDatabaseTest { 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 * @@ -277,12 +284,13 @@ class UnitDatabaseTest { 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)); + final List errs = loadDimensionFile(database, filename); + assertFalse(errs.isEmpty(), "no error from invalid file " + filename); + final RuntimeException e = errs.get(0).problem(); assertTrue(e instanceof IllegalArgumentException || e instanceof NoSuchElementException); } - + /** * A bunch of tests for invalid unit files * @@ -295,12 +303,13 @@ class UnitDatabaseTest { 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)); + final List errs = loadUnitsFile(database, filename); + assertFalse(errs.isEmpty(), "no error from invalid file " + filename); + final RuntimeException e = errs.get(0).problem(); assertTrue(e instanceof IllegalArgumentException || e instanceof NoSuchElementException); } - + /** * Tests loading a valid dimension-file with some derived dimensions. * @@ -312,13 +321,13 @@ class UnitDatabaseTest { 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. * @@ -327,13 +336,13 @@ class UnitDatabaseTest { @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 * @@ -342,43 +351,43 @@ class UnitDatabaseTest { @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. @@ -388,16 +397,16 @@ class UnitDatabaseTest { @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()) { @@ -405,7 +414,7 @@ class UnitDatabaseTest { } 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<>(); @@ -415,7 +424,7 @@ class UnitDatabaseTest { assertEquals(expectedEntries, actualEntries); assertEquals(expectedEntries, map1.entrySet()); } - + /** * Test that prefixes correctly apply to units. * @@ -425,28 +434,28 @@ class UnitDatabaseTest { @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. * @@ -462,19 +471,19 @@ class UnitDatabaseTest { 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. @@ -485,18 +494,18 @@ class UnitDatabaseTest { @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<>(); @@ -504,7 +513,7 @@ class UnitDatabaseTest { 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, @@ -514,28 +523,28 @@ class UnitDatabaseTest { 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. * @@ -546,37 +555,37 @@ class UnitDatabaseTest { 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 * @@ -587,25 +596,25 @@ class UnitDatabaseTest { 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 @@ -614,31 +623,31 @@ class UnitDatabaseTest { 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. @@ -654,28 +663,28 @@ class UnitDatabaseTest { 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/test/java/sevenUnitsGUI/TabbedViewTest.java b/src/test/java/sevenUnitsGUI/TabbedViewTest.java index 165718f..017e9ea 100644 --- a/src/test/java/sevenUnitsGUI/TabbedViewTest.java +++ b/src/test/java/sevenUnitsGUI/TabbedViewTest.java @@ -18,14 +18,18 @@ package sevenUnitsGUI; import static org.junit.jupiter.api.Assertions.assertEquals; +import java.util.concurrent.TimeUnit; + import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; /** - * Test for the TabbedView + * Test for the TabbedView. * * @since v0.4.0 * @since 2022-07-17 */ +@Timeout(value = 10, unit = TimeUnit.SECONDS) class TabbedViewTest { /** * @return a view with all settings set to standard values @@ -36,17 +40,17 @@ class TabbedViewTest { private static final TabbedView setupView() { final var view = new TabbedView(); final var presenter = view.getPresenter(); - + presenter.setNumberDisplayRule(StandardDisplayRules.uncertaintyBased()); presenter.setPrefixRepetitionRule( DefaultPrefixRepetitionRule.NO_RESTRICTION); presenter.setSearchRule(PrefixSearchRule.COMMON_PREFIXES); presenter.setOneWayConversionEnabled(false); presenter.setShowDuplicates(true); - + return view; } - + /** * Simulates an expression conversion operation, and ensures it works * properly. @@ -57,18 +61,18 @@ class TabbedViewTest { @Test void testExpressionConversion() { final var view = setupView(); - + // prepare for unit conversion view.masterPane.setSelectedIndex(1); view.fromEntry.setText("250.0 inch"); view.toEntry.setText("metre"); - + view.convertExpressionButton.doClick(); - + // check result of conversion assertEquals("250.0 inch = 6.350 metre", view.expressionOutput.getText()); } - + /** * Simulates a unit conversion operation, and ensures it works properly. * @@ -78,18 +82,18 @@ class TabbedViewTest { @Test void testUnitConversion() { final var view = setupView(); - + // prepare for unit conversion view.masterPane.setSelectedIndex(0); view.dimensionSelector.setSelectedItem("Length"); view.fromSearch.getSearchList().setSelectedValue("inch", true); view.toSearch.getSearchList().setSelectedValue("metre", true); view.valueInput.setText("250.0"); - + view.convertUnitButton.doClick(); - + // check result of conversion assertEquals("250.0 inch = 6.350 metre", view.unitOutput.getText()); } - + } -- cgit v1.2.3 From dfdcf58c8751db95f024528aa38dd81eb2364f39 Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Sun, 23 Feb 2025 20:53:06 -0500 Subject: Bump version number to 1.0.0b1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Compared to version 0.5.0, this release: - allows conversion to sums of units (e.g. 4/3 ft → 1 ft + 4 in) - allows non-integer exponents in expressions - adds the ability to change the UI language - gracefully handles datafile errors - adds more information to the loading-success message, and adds it to the About tab - allows the user to not use the default datafiles No new features will be added until the release of version 1.0.0. --- CHANGELOG.org | 3 +++ README.org | 2 +- docs/roadmap.org | 9 --------- src/main/java/sevenUnits/ProgramInfo.java | 4 ++-- 4 files changed, 6 insertions(+), 12 deletions(-) (limited to 'docs') diff --git a/CHANGELOG.org b/CHANGELOG.org index 78bb9a1..0798904 100644 --- a/CHANGELOG.org +++ b/CHANGELOG.org @@ -5,7 +5,10 @@ All notable changes in this project will be shown in this file. - *Allowed conversion to a sum of units (e.g. 4/3 ft \rightarrow 1 ft + 4 in).* - *Allowed exponents on units to be non-integer numbers.* The resulting exponents are rounded to the nearest integer, and the user is warned if this rounding changes the value by more than normal floating-point error. +- *Added the ability to change the language of 7Units's UI.* + This does not affect the names of units, prefixes and dimensions. - Added more information to the loading-success message, and added it to the about tab. +- Added the ability to not use the default data files. *** Changed - *Errors in unit/dimension files are shown in popups, rather than crashing the program.* *** Fixed diff --git a/README.org b/README.org index cf8de11..279c378 100644 --- a/README.org +++ b/README.org @@ -1,4 +1,4 @@ -* 7Units Version 1.0.0-alpha.1 +* 7Units Version 1.0.0-beta.1 (this project uses Semantic Versioning) ** What is it? This is a unit converter, which allows you to convert between different units, and includes a GUI which can read unit data from a file (using some unit math) and convert between units that you type in, and has a unit and prefix viewer to check the units that have been loaded in. diff --git a/docs/roadmap.org b/docs/roadmap.org index 04ea0a5..963c17d 100644 --- a/docs/roadmap.org +++ b/docs/roadmap.org @@ -3,15 +3,6 @@ Here is a list of the unfinished requirements for version 1.0.0. When everythin These requirements are subject to change. I intend to finish version 1.0.0 by [2025-04-27 Sun]. -Feature Requirements: -- 7Units should be able to parse unit files from [[https://www.gnu.org/software/units/][GNU Units]], the program that inspired it. -- 7Units's expression converter should support most or all of the conversion features supported by GNU Units: - - (/Mostly Done/) Converting to sums of units (it should also be possible to do this in the unit converter with preset combinations) - - (/Optional/) Inverse nonlinear conversion with the tilde prefix - - (/Optional/) Nonlinear units should be specifiable in unit files. - - /Any other feature not listed should be considered optional./ -- (/Optional/) It should be possible to add, edit and remove units and prefixes from the GUI unit and prefix viewers. - Documentation/Testing Requirements: - 7Units should be fully documented. - 7Units should have automated testing with a code coverage of at least 2/3 (ideally at least 5/6). diff --git a/src/main/java/sevenUnits/ProgramInfo.java b/src/main/java/sevenUnits/ProgramInfo.java index 573c5c7..fee3cea 100644 --- a/src/main/java/sevenUnits/ProgramInfo.java +++ b/src/main/java/sevenUnits/ProgramInfo.java @@ -26,9 +26,9 @@ import sevenUnits.utils.SemanticVersionNumber; */ public final class ProgramInfo { - /** The version number (1.0.0-alpha.1) */ + /** The version number (1.0.0-beta.1) */ public static final SemanticVersionNumber VERSION = SemanticVersionNumber - .preRelease(1, 0, 0, "alpha", 1); + .preRelease(1, 0, 0, "beta", 1); private ProgramInfo() { // this class is only for static variables, you shouldn't be able to -- cgit v1.2.3 From 7812a0f6a219cb817a5e6b9db34012f7eca373b8 Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Fri, 30 May 2025 20:26:22 -0500 Subject: Update roadmap --- docs/roadmap.org | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) (limited to 'docs') diff --git a/docs/roadmap.org b/docs/roadmap.org index 963c17d..fdd12ac 100644 --- a/docs/roadmap.org +++ b/docs/roadmap.org @@ -1,11 +1,6 @@ * Version 1.0.0 Roadmap Here is a list of the unfinished requirements for version 1.0.0. When everything here is met, I intend to release version 1.0.0 and consider 7Units complete (for the most part). -These requirements are subject to change. I intend to finish version 1.0.0 by [2025-04-27 Sun]. +These requirements are subject to change. I intend to finish version 1.0.0 by [2025-06-01 Sun]. -Documentation/Testing Requirements: -- 7Units should be fully documented. -- 7Units should have automated testing with a code coverage of at least 2/3 (ideally at least 5/6). - -Other Requirements -- The public API of 7Units should be finalized and well-designed. +Once the documentation is up to date, 7Units 1.0.0 can be released. -- cgit v1.2.3 From ae0559a9432f85f9147eeb80b35f1b2947889acd Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Sun, 1 Jun 2025 20:05:18 -0500 Subject: Update documentation to version 1.0.0 --- docs/data_spec.org | 8 ++- docs/data_spec.pdf | Bin 90604 -> 92033 bytes docs/data_spec.tex | 27 ++++++----- docs/design.org | 5 +- docs/design.pdf | Bin 352153 -> 350221 bytes docs/design.tex | 73 ++++++++++++++-------------- docs/manual.org | 140 ++++++++++++++++++++++++++++------------------------- docs/manual.pdf | Bin 185625 -> 186518 bytes docs/manual.tex | 48 ++++++++++-------- docs/roadmap.org | 6 --- 10 files changed, 160 insertions(+), 147 deletions(-) delete mode 100644 docs/roadmap.org (limited to 'docs') diff --git a/docs/data_spec.org b/docs/data_spec.org index c75eaa7..d251230 100644 --- a/docs/data_spec.org +++ b/docs/data_spec.org @@ -1,6 +1,6 @@ #+TITLE: 7Units Datafile Specification -#+SUBTITLE: For Version 0.5.0 -#+DATE: 2024 March 23 +#+SUBTITLE: For Version 1.0.0 +#+DATE: 2025 June 1 #+LaTeX_HEADER: \usepackage[a4paper, lmargin=25mm, rmargin=25mm, tmargin=25mm, bmargin=25mm]{geometry} #+LaTeX: \newpage @@ -35,5 +35,9 @@ If a line's name part ends in the ASCII dash (~-~, 0x2D), it defines a prefix in Dimension files give names to unit dimensions, so they can be selected in the unit converter to determine which units are shown. Dimension files are similar to unit and prefix files, except that they use a different set of base /dimensions/ (defined with ! as usual), and that addition and subtraction are not supported in dimension expressions. * Metric Exception Files Metric exception files list exceptions to the One-Way Conversion rule. Units included in these files are always shown on both sides of the unit converter, even if One Way Conversion is enabled. They are just a list of units, one per line. Ignored lines and comments work the same way as in other data files. +* Locale Files +A locale file contains the data to translate 7Units into another language. Each line is in the form ~key=value~. A sample locale file, with all available keys, can be found in ~src/resources/locales/en.txt~. + +To translate 7Units, simply create another locale file in the ~locales~ subdirectory of your 7Units configuration directory. * Configuration Files A configuration file contains one line for each configuration setting, in the format ~key=value~. Check the user manual for the list of configurable setting keys. Ignored lines and comments work the same way as in other data files. diff --git a/docs/data_spec.pdf b/docs/data_spec.pdf index 35e01f8..cae6592 100644 Binary files a/docs/data_spec.pdf and b/docs/data_spec.pdf differ diff --git a/docs/data_spec.tex b/docs/data_spec.tex index 879d62c..4e2475d 100644 --- a/docs/data_spec.tex +++ b/docs/data_spec.tex @@ -1,4 +1,4 @@ -% Created 2024-03-24 Sun 13:16 +% Created 2025-06-01 Sun 19:42 % Intended LaTeX compiler: pdflatex \documentclass[11pt]{article} \usepackage[utf8]{inputenc} @@ -13,15 +13,15 @@ \usepackage{capt-of} \usepackage{hyperref} \usepackage[a4paper, lmargin=25mm, rmargin=25mm, tmargin=25mm, bmargin=25mm]{geometry} -\date{2024 March 23} +\date{2025 June 1} \title{7Units Datafile Specification\\\medskip -\large For Version 0.5.0} +\large For Version 1.0.0} \hypersetup{ pdfauthor={}, pdftitle={7Units Datafile Specification}, pdfkeywords={}, pdfsubject={}, - pdfcreator={Emacs 29.2 (Org mode 9.6.15)}, + pdfcreator={Emacs 29.3 (Org mode 9.6.15)}, pdflang={English}} \begin{document} @@ -30,7 +30,7 @@ \newpage \section{Unit and Prefix Files} -\label{sec:org3f0bfdf} +\label{sec:orgc529fa1} Unit and prefix files specify the units and prefixes that are available to 7Units. Their format is intended to be compatible with 7Units's inspiration, \href{https://www.gnu.org/software/units/}{GNU Units}. Each unit or prefix is specified by a line in the following format: @@ -40,7 +40,7 @@ Each unit or prefix is specified by a line in the following format: The name may not contain whitespace, but the definition can and often will. Lines can be separated by either a line feed (0x0A) or a carriage return followed by a line feed (0x0D 0x0A). \subsection{Ignored Lines \& Comments} -\label{sec:org7396c6b} +\label{sec:orga9c97a7} All of the following should be ignored: \begin{itemize} \item Any line containing only whitespace @@ -50,7 +50,7 @@ All of the following should be ignored: This allows unit and prefix files to be easily organized and divided. \subsection{Unit Expressions} -\label{sec:orga56737c} +\label{sec:orgebaa825} The definition part of a unit's line is a unit expression - the same sort of expression you would use in the complex unit converter. Expressions should be a standard mathematical expression, which can operate on either numbers or units. The following operators are supported: addition (+), subtraction (-), multiplication (\texttt{*} or no operator), division (\texttt{/} or \texttt{|}), exponentation (\texttt{\textasciicircum{}}; exponent must be a number). Brackets (\texttt{(} and \texttt{)}) may be used to change order of operations, but otherwise standard BEDMAS order is followed (exponentation first, then multiplication and division, then addition and subtraction), with two exceptions: if a number is multiplied by a unit using spaces, the multiplication will have precedence over division, and division with \texttt{|} has higher precedence than any other operator. For example, "2 m / 1 m" is equal to the dimensionless value 2, not 2 m\textsuperscript{2}. An example of a line defining a unit is: @@ -60,15 +60,20 @@ yard 9 dm + 1.4 cm + 4 mm^2 / 10 mm If the definition is an exclamation mark (\texttt{!}), this defines a base unit, which is expected to already be stored in the system. \subsection{Prefixes \& Prefix Expressions} -\label{sec:org28c98b9} +\label{sec:orgd0ef1d8} If a line's name part ends in the ASCII dash (\texttt{-}, 0x2D), it defines a prefix instead of a unit (this dash is not included in the prefix's name). Prefix expressions are like unit expressions, but the operands are prefixes and numbers, not units and numbers. Because prefixes do not have a dimension, there are no base prefixes - the exclamation mark is forbidden in prefix expressions. \section{Dimension Files} -\label{sec:org9530301} +\label{sec:orgdc0ddd9} Dimension files give names to unit dimensions, so they can be selected in the unit converter to determine which units are shown. Dimension files are similar to unit and prefix files, except that they use a different set of base \emph{dimensions} (defined with ! as usual), and that addition and subtraction are not supported in dimension expressions. \section{Metric Exception Files} -\label{sec:org79afe10} +\label{sec:org0587104} Metric exception files list exceptions to the One-Way Conversion rule. Units included in these files are always shown on both sides of the unit converter, even if One Way Conversion is enabled. They are just a list of units, one per line. Ignored lines and comments work the same way as in other data files. +\section{Locale Files} +\label{sec:orgdc95be9} +A locale file contains the data to translate 7Units into another language. Each line is in the form \texttt{key=value}. A sample locale file, with all available keys, can be found in \texttt{src/resources/locales/en.txt}. + +To translate 7Units, simply create another locale file in the \texttt{locales} subdirectory of your 7Units configuration directory. \section{Configuration Files} -\label{sec:orgf3f6b9a} +\label{sec:orgad1bd81} A configuration file contains one line for each configuration setting, in the format \texttt{key=value}. Check the user manual for the list of configurable setting keys. Ignored lines and comments work the same way as in other data files. \end{document} diff --git a/docs/design.org b/docs/design.org index 65ee651..3764adb 100644 --- a/docs/design.org +++ b/docs/design.org @@ -1,6 +1,6 @@ #+TITLE: 7Units Design Document -#+SUBTITLE: For version 0.5.0 -#+DATE: 2024 March 23 +#+SUBTITLE: For version 1.0.0 +#+DATE: 2025 June 1 #+LaTeX_HEADER: \usepackage[a4paper, lmargin=25mm, rmargin=25mm, tmargin=25mm, bmargin=25mm]{geometry} #+LaTeX_HEADER: \usepackage{xurl} #+LaTeX: \newpage @@ -66,7 +66,6 @@ - Note that any operations will return a unit without name(s) or a symbol. All unit classes have a ~withName~ method that returns a copy of them with different names and/or a different symbol (all of this info is contained in the ~NameSymbol~ class) There are a few more classes which play small roles in the unit system: - - Unitlike :: A class that is like a unit, but its "value" can be any class. The only use of this class right now is to implement ~MultiUnit~, a combination of units (like "foot + inch", commonly used in North America for measuring height); its "value" is a list of numbers. - FunctionalUnit :: A convenience class that implements the two conversion functions of ~Unit~ using ~DoubleUnaryOperator~ instances. This is used internally to implement degrees Celsius and Fahrenheit. There is also a version of this for ~Unitlike~, ~FunctionalUnitlike~. - UnitValue :: A value expressed as a certain unit (such as "7 inches"). This class is used by the simple unit converter to represent units. You can convert them between units. There are also versions of this for ~LinearUnit~ and ~Unitlike~. - Metric :: A static utility class with instances of all of the SI named units, the 9 base dimensions, SI prefixes, some common prefixed units like the kilometre, and a few non-SI units used commonly with them. diff --git a/docs/design.pdf b/docs/design.pdf index 00703d8..764bf3b 100644 Binary files a/docs/design.pdf and b/docs/design.pdf differ diff --git a/docs/design.tex b/docs/design.tex index c850412..9779a9e 100644 --- a/docs/design.tex +++ b/docs/design.tex @@ -1,4 +1,4 @@ -% Created 2024-03-24 Sun 13:15 +% Created 2025-06-01 Sun 19:45 % Intended LaTeX compiler: pdflatex \documentclass[11pt]{article} \usepackage[utf8]{inputenc} @@ -14,15 +14,15 @@ \usepackage{hyperref} \usepackage[a4paper, lmargin=25mm, rmargin=25mm, tmargin=25mm, bmargin=25mm]{geometry} \usepackage{xurl} -\date{2024 March 23} +\date{2025 June 1} \title{7Units Design Document\\\medskip -\large For version 0.5.0} +\large For version 1.0.0} \hypersetup{ pdfauthor={}, pdftitle={7Units Design Document}, pdfkeywords={}, pdfsubject={}, - pdfcreator={Emacs 29.2 (Org mode 9.6.15)}, + pdfcreator={Emacs 29.3 (Org mode 9.6.15)}, pdflang={English}} \begin{document} @@ -32,35 +32,35 @@ \newpage \section{Introduction} -\label{sec:orgdac8c13} +\label{sec:orge8f18c8} 7Units is a program that can convert between units. This document details the internal design of 7Units, intended to be used by current and future developers. \section{System Overview} -\label{sec:org2a8e77a} +\label{sec:org3dec32e} \begin{figure}[htbp] \centering \includegraphics[height=144px]{./diagrams/overview-diagram.plantuml.png} \caption{A big-picture diagram of 7Units, containing all of the major classes.} \end{figure} \subsection{Packages of 7Units} -\label{sec:org80ac84e} +\label{sec:org5f2f6d8} 7Units splits its code into three main packages: \begin{description} -\item[{\texttt{sevenUnits.unit}}] The \hyperref[sec:orgc2400d6]{unit system} +\item[{\texttt{sevenUnits.unit}}] The \hyperref[sec:orgdc040fb]{unit system} \item[{\texttt{sevenUnits.utils}}] Extra classes that aid the unit system. -\item[{\texttt{sevenUnitsGUI}}] The \hyperref[sec:org261b06e]{front end} code +\item[{\texttt{sevenUnitsGUI}}] The \hyperref[sec:org1879a96]{front end} code \end{description} \texttt{sevenUnits.unit} depends on \texttt{sevenUnits.utils}, while \texttt{sevenUnitsGUI} depends on both \texttt{sevenUnits} packages. There is only one class that isn't in any of these packages, \texttt{sevenUnits.VersionInfo}. \subsection{Major Classes of 7Units} -\label{sec:org5910307} +\label{sec:orgea198c1} \begin{description} -\item[{\hyperref[sec:org946a4e5]{sevenUnits.unit.Unit}}] The class representing a unit -\item[{\hyperref[sec:orgac71770]{sevenUnits.unit.UnitDatabase}}] A class that stores collections of units, prefixes and dimensions. -\item[{\hyperref[sec:org57b8a42]{sevenUnitsGUI.View}}] The class that handles interaction between the user and the program. -\item[{\hyperref[sec:orga668171]{sevenUnitsGUI.Presenter}}] The class that handles communication between the \texttt{View} and the unit system. +\item[{\hyperref[sec:org097d2bc]{sevenUnits.unit.Unit}}] The class representing a unit +\item[{\hyperref[sec:orgdd255ff]{sevenUnits.unit.UnitDatabase}}] A class that stores collections of units, prefixes and dimensions. +\item[{\hyperref[sec:org1047a59]{sevenUnitsGUI.View}}] The class that handles interaction between the user and the program. +\item[{\hyperref[sec:org17c3fce]{sevenUnitsGUI.Presenter}}] The class that handles communication between the \texttt{View} and the unit system. \end{description} \newpage \subsection{Process of Unit Conversion} -\label{sec:orgbbad9d5} +\label{sec:orgfe8e937} \begin{figure}[htbp] \centering \includegraphics[width=.9\linewidth]{./diagrams/convert-units.plantuml.png} @@ -75,7 +75,7 @@ \end{enumerate} \newpage \subsection{Process of Expression Conversion} -\label{sec:org52749d5} +\label{sec:orgbdb360e} The process of expression conversion is similar to that of unit conversion. \begin{figure}[htbp] \centering @@ -91,7 +91,7 @@ The process of expression conversion is similar to that of unit conversion. \end{enumerate} \newpage \section{Unit System Design} -\label{sec:orgc2400d6} +\label{sec:orgdc040fb} Any code related to the backend unit system is stored in the \texttt{sevenUnits.unit} package. Here is a class diagram of the system. Unimportant methods, methods inherited from Object, getters and setters have been omitted. @@ -102,11 +102,11 @@ Here is a class diagram of the system. Unimportant methods, methods inherited f \end{figure} \newpage \subsection{Dimensions} -\label{sec:orgb21aaed} -Dimensions represent what a unit is measuring, such as length, time, or energy. Dimensions are represented as an \hyperref[sec:org16bc96f]{ObjectProduct}, where \texttt{BaseDimension} is a very simple class (its only properties are a name and a symbol) which represents the dimension of a base unit; these base dimensions can be multiplied to create all other Dimensions. +\label{sec:orgd0c0232} +Dimensions represent what a unit is measuring, such as length, time, or energy. Dimensions are represented as an \hyperref[sec:org5bd9128]{ObjectProduct}, where \texttt{BaseDimension} is a very simple class (its only properties are a name and a symbol) which represents the dimension of a base unit; these base dimensions can be multiplied to create all other Dimensions. \subsection{Unit Classes} -\label{sec:org946a4e5} -Units are internally represented by the abstract class \texttt{Unit}. All units have an \hyperref[sec:org16bc96f]{ObjectProduct} (referred to as the base) that they are based on, a dimension (ObjectProduct), one or more names and a symbol (these last two bits of data are contained in the \texttt{NameSymbol} class). The dimension is calculated from the base unit when needed; the variable is just a cache. It has two constructors: a package-private one used to make \texttt{BaseUnit} instances, and a protected one used to make general units (for other subclasses of \texttt{Unit}). All unit classes are immutable. +\label{sec:org097d2bc} +Units are internally represented by the abstract class \texttt{Unit}. All units have an \hyperref[sec:org5bd9128]{ObjectProduct} (referred to as the base) that they are based on, a dimension (ObjectProduct), one or more names and a symbol (these last two bits of data are contained in the \texttt{NameSymbol} class). The dimension is calculated from the base unit when needed; the variable is just a cache. It has two constructors: a package-private one used to make \texttt{BaseUnit} instances, and a protected one used to make general units (for other subclasses of \texttt{Unit}). All unit classes are immutable. Units also have two conversion functions - one which converts from a value expressed in this unit to its base unit, and another which converts from a value expressed in the base unit to this unit. In \texttt{Unit}, they are defined as two abstract methods. This allows you to convert from any unit to any other (as long as they have the same base, i.e. you aren't converting metres to pounds). To convert from A to B, first convert from A to its base, then convert from the base to B. @@ -123,7 +123,6 @@ However, most units are instances of \texttt{LinearUnit}, another subclass of \t There are a few more classes which play small roles in the unit system: \begin{description} -\item[{Unitlike}] A class that is like a unit, but its "value" can be any class. The only use of this class right now is to implement \texttt{MultiUnit}, a combination of units (like "foot + inch", commonly used in North America for measuring height); its "value" is a list of numbers. \item[{FunctionalUnit}] A convenience class that implements the two conversion functions of \texttt{Unit} using \texttt{DoubleUnaryOperator} instances. This is used internally to implement degrees Celsius and Fahrenheit. There is also a version of this for \texttt{Unitlike}, \texttt{FunctionalUnitlike}. \item[{UnitValue}] A value expressed as a certain unit (such as "7 inches"). This class is used by the simple unit converter to represent units. You can convert them between units. There are also versions of this for \texttt{LinearUnit} and \texttt{Unitlike}. \item[{Metric}] A static utility class with instances of all of the SI named units, the 9 base dimensions, SI prefixes, some common prefixed units like the kilometre, and a few non-SI units used commonly with them. @@ -131,20 +130,20 @@ There are a few more classes which play small roles in the unit system: \item[{USCustomary}] A static utility class with instances of common units in the US Customary system (not to be confused with the British Imperial system; it has the same unit names but the values of a few units are different). \end{description} \subsection{Prefixes} -\label{sec:orgede1b85} +\label{sec:orge1a285c} A \texttt{UnitPrefix} is a simple object that can multiply a \texttt{LinearUnit} by a value. It can calculate a new name for the unit by combining its name and the unit's name (symbols are done similarly). It can do multiplication, division and exponentation with a number, as well as multiplication and division with another prefix; all of these work by changing the prefix's multiplier. \subsection{The Unit Database} -\label{sec:orgac71770} +\label{sec:orgdd255ff} The \texttt{UnitDatabase} class stores all of the unit, prefix and dimension data used by this program. It is not a representation of an actual database, just a class that stores lots of data. Units are stored using a custom \texttt{Map} implementation (\texttt{PrefixedUnitMap}) which maps unit names to units. It is backed by two maps: one for units (without prefixes) and one for prefixes. It is programmed to include prefixes (so if units includes "metre" and prefixes includes "kilo", this map will include "kilometre", mapping it to a unit representing a kilometre). It is immutable, but you can modify the underlying maps, which is reflected in the \texttt{PrefixedUnitMap}. Other than that, it is a normal map implementation. Prefixes and dimensions are stored in normal maps. \subsubsection{Parsing Expressions} -\label{sec:org02e3ff1} -Each \texttt{UnitDatabase} instance has four \hyperref[sec:org7296a14]{ExpressionParser} instances associated with it, for four types of expressions: unit, unit value, prefix and dimension. They are mostly similar, with operators corresponding to each operation of the corresponding class (\texttt{LinearUnit}, \texttt{LinearUnitValue}, \texttt{UnitPrefix}, \texttt{ObjectProduct}). Unit and unit value expressions use linear units; nonlinear units can be used with a special syntax (like "degC(20)") and are immediately converted to a linear unit representing their base (Kelvin in this case) before operating. +\label{sec:orgd4e45a3} +Each \texttt{UnitDatabase} instance has four \hyperref[sec:org6a2c74c]{ExpressionParser} instances associated with it, for four types of expressions: unit, unit value, prefix and dimension. They are mostly similar, with operators corresponding to each operation of the corresponding class (\texttt{LinearUnit}, \texttt{LinearUnitValue}, \texttt{UnitPrefix}, \texttt{ObjectProduct}). Unit and unit value expressions use linear units; nonlinear units can be used with a special syntax (like "degC(20)") and are immediately converted to a linear unit representing their base (Kelvin in this case) before operating. \subsubsection{Parsing Files} -\label{sec:org676148d} +\label{sec:org6cff6b9} There are two types of data files: unit and dimension. Unit files contain data about units and prefixes. Each line contains the name of a unit or prefix (prefixes end in a dash, units don't) followed by an expression which defines it, separated by one or more space characters (this behaviour is defined by the static regular expression \texttt{NAME\_EXPRESSION}). Unit files are parsed line by line, each line being run through the \texttt{addUnitOrPrefixFromLine} method, which splits a line into name and expression, determines whether it's a unit or a prefix, and parses the expression. Because all units are defined by others, base units need to be defined with a special expression "!"; \textbf{these units should be added to the database before parsing the file}. @@ -152,10 +151,10 @@ Unit files contain data about units and prefixes. Each line contains the name o Dimension files are similar, only for dimensions instead of units and prefixes. \newpage \section{Front-End Design} -\label{sec:org261b06e} +\label{sec:org1879a96} The front-end of 7Units is based on an MVP model. There are two major frontend classes, the \textbf{View} and the \textbf{Presenter}. \subsection{The View} -\label{sec:org57b8a42} +\label{sec:org1047a59} The \texttt{View} is the part of the frontend code that directly interacts with the user. It handles input and output, but does not do any processing. Processing is handled by the presenter and the backend code. The \texttt{View} is an interface, not a single class, so that I can easily create multiple views without having to rewrite any processing code. This allows me to easily prototype changes to the GUI without messing with existing code. @@ -169,10 +168,10 @@ There are currently two implementations of the \texttt{View}: \end{description} Both of these \texttt{View} implementations implement \texttt{UnitConversionView} and \texttt{ExpressionConversionView}. \subsection{The Presenter} -\label{sec:orga668171} +\label{sec:org17c3fce} The \texttt{Presenter} is an intermediary between the \texttt{View} and the backend code. It accepts the user's input and passes it to the backend, then accepts the backend's output and passes it to the frontend for user viewing. Its main functions do not have arguments or return values; instead it takes input from and provides output to the \texttt{View} via its public methods. \subsubsection{Rules} -\label{sec:org81f6f8a} +\label{sec:org159ba12} The \texttt{Presenter} has a set of function-object rules that determine some of its behaviours. Each corresponds to a setting in the \texttt{View}, but they can be set to other values via the \texttt{Presenter}'s setters (although nonstandard rules cannot be saved and loaded): \begin{description} \item[{numberDisplayRule}] A function that determines how numbers are displayed. This controls the rounding rules. @@ -182,7 +181,7 @@ The \texttt{Presenter} has a set of function-object rules that determine some of These rules have been made this way to enable an incredible level of customization of these behaviours. Because any function object with the correct arguments and return type is accepted, these rules (especially the number display rule) can do much more than the default behaviours. \subsection{Utility Classes} -\label{sec:orga5b57ce} +\label{sec:org4d2c502} The frontend has many miscellaneous utility classes. Many of them are package-private. Here is a list of them, with a brief description of what they do and where they are used: \begin{description} \item[{DefaultPrefixRepetitionRule}] An enum containing the available rules determining when you can repeat prefixes. Used by the \texttt{TabbedView} for selecting the rule and by the \texttt{Presenter} for loading it from a file. @@ -195,15 +194,15 @@ The frontend has many miscellaneous utility classes. Many of them are package-p \end{description} \newpage \section{Utility Classes} -\label{sec:org3686e64} +\label{sec:org97c6f74} 7Units has a few general "utility" classes. They aren't directly related to units, but are used in the units system. \subsection{ObjectProduct} -\label{sec:org16bc96f} +\label{sec:org5bd9128} An \texttt{ObjectProduct} represents a "product" of elements of some type. The units system uses them to represent coherent units as a product of base units, and dimensions as a product of base dimensions. Internally, it is represented using a map mapping objects to their exponents in the product. For example, the unit "kg m\textsuperscript{2} / s\textsuperscript{2}" (i.e. a Joule) would be represented with a map like \texttt{[kg: 1, m: 2, s: -2]}. \subsection{ExpressionParser} -\label{sec:org7296a14} +\label{sec:org6a2c74c} The \texttt{ExpressionParser} class is used to parse the unit, prefix and dimension expressions that are used throughout 7Units. An expression is something like "(2 m + 30 J / N) * 8 s)". Each instance represents a type of expression, containing a way to obtain values (such as numbers or units) from the text and operations that can be done on these values (such as addition, subtraction or multiplication). Each operation also has a priority, which controls the order of operations (i.e. multiplication gets a higher priority than addition). \texttt{ExpressionParser} has a parameterized type \texttt{T}, which represents the type of the value used in the expression. The expression parser currently only supports one type of value per expression; in the expressions used by 7Units numbers are treated as a kind of unit or prefix. Operators are represented by internal types; the system distinguishes between unary operators (those that take a single value, like negation) and binary operators (those that take 2 values, like +, -, * or /). @@ -224,13 +223,13 @@ Expressions are parsed in 2 steps: After evaluating the last token, there should be one value left in the stack - the answer. If there isn't, the original expression was malformed. \end{enumerate} \subsection{Math Classes} -\label{sec:orgfd8c723} +\label{sec:orgfec3253} There are two simple math classes in 7Units: \begin{description} \item[{\texttt{UncertainDouble}}] Like a \texttt{double}, but with an uncertainty (e.g. \(2.0 \pm 0.4\)). The operations are like those of the regular Double, only they also calculate the uncertainty of the final value. They also have "exact" versions to help interoperation between \texttt{double} and \texttt{UncertainDouble}. It is used by the converter's Scientific Precision setting. \item[{\texttt{DecimalComparison}}] A static utility class that contains a few alternate equals() methods for \texttt{double} and \texttt{UncertainDouble}. These methods allow a slight (configurable) difference between values to still be considered equal, to fight roundoff error. \end{description} \subsection{Collection Classes} -\label{sec:org32d7d09} +\label{sec:orgc8ae9c7} The \texttt{ConditionalExistenceCollections} class contains wrapper implementations of \texttt{Collection}, \texttt{Iterator}, \texttt{Map} and \texttt{Set}. These implementations ignore elements that do not pass a certain condition - if an element fails the condition, \texttt{contains} will return false, the iterator will skip past it, it won't be counted in \texttt{size}, etc. even if it exists in the original collection. Effectively, any element of the original collection that fails the test does not exist. \end{document} diff --git a/docs/manual.org b/docs/manual.org index 3c6de1c..92160c3 100644 --- a/docs/manual.org +++ b/docs/manual.org @@ -1,78 +1,81 @@ #+TITLE: 7Units User Manual -#+SUBTITLE: For Version 0.5.0 -#+DATE: 2024 March 23 +#+SUBTITLE: For Version 1.0.0 +#+DATE: 2025 June 1 #+LaTeX_HEADER: \usepackage[a4paper, lmargin=25mm, rmargin=25mm, tmargin=25mm, bmargin=25mm]{geometry} #+LaTeX: \newpage * Introduction and Purpose - 7Units is a program that can be used to convert units. This document outlines how to use the program. +7Units is a program that can be used to convert units. This document outlines how to use the program. * System Requirements - - Works on all major operating systems \\ - *NOTE:* All screenshots in this document were taken on Windows 10. If you use a different operating system, the program will probably look different than what is shown. - - Java version 11+ required -# installation instructions go here - wait until git repository is fixed/set up +- Works on all major operating systems \\ + *NOTE:* All screenshots in this document were taken on Windows 10. If you use a different operating system, the program will probably look different than what is shown. +- Java version 11+ required +- Gradle required +- To run the software, simply run './gradlew run' in the main directory. #+LaTeX: \newpage * How to Use 7Units ** Simple Unit Conversion - 1. Select the "Convert Units" tab if it is not already selected. You should see a screen like in figure [[main-interface-dimension]]: - #+CAPTION: Taken in version 0.3.0 - #+ATTR_LaTeX: :height 250px - #+name: main-interface-dimension - [[../screenshots/main-interface-dimension-converter.png]] - 2. Use the dropdown box at the top to select what kind of unit to convert (length, mass, speed, etc.) - 3. Select the unit to convert /from/ on the left. - 4. Select the unit to convert /to/ on the right. - 5. Enter the value to convert in the box above the convert button. The program should look something like in figure [[sample-conversion-dimension]]: - #+CAPTION: This image, taken in version 0.3.0, shows the user about to convert 35 miles to kilometres. - #+attr_latex: :height 250px - #+name: sample-conversion-dimension - [[../screenshots/sample-conversion-dimension-converter.png]] - 6. Press the "Convert" button. The result will be shown below the "Convert" button. This is shown in figure [[sample-results-dimension]] - #+CAPTION: The result of the above conversion - #+attr_latex: :height 250px - #+name: sample-results-dimension - [[../screenshots/sample-conversion-results-dimension-converter.png]] +1. Select the "Convert Units" tab if it is not already selected. You should see a screen like in figure [[main-interface-dimension]]: + #+CAPTION: Taken in version 0.3.0 + #+ATTR_LaTeX: :height 250px + #+name: main-interface-dimension + [[../screenshots/main-interface-dimension-converter.png]] +2. Use the dropdown box at the top to select what kind of unit to convert (length, mass, speed, etc.) +3. Select the unit to convert /from/ on the left. +4. Select the unit to convert /to/ on the right. +5. Enter the value to convert in the box above the convert button. The program should look something like in figure [[sample-conversion-dimension]]: + #+CAPTION: This image, taken in version 0.3.0, shows the user about to convert 35 miles to kilometres. + #+attr_latex: :height 250px + #+name: sample-conversion-dimension + [[../screenshots/sample-conversion-dimension-converter.png]] +6. Press the "Convert" button. The result will be shown below the "Convert" button. This is shown in figure [[sample-results-dimension]] + #+CAPTION: The result of the above conversion + #+attr_latex: :height 250px + #+name: sample-results-dimension + [[../screenshots/sample-conversion-results-dimension-converter.png]] ** Complex Unit Conversion - 1. Select the "Convert Unit Expressions" if it is not already selected. You should see a screen like in figure [[main-interface-expression]]: - #+CAPTION: Taken in version 0.3.0 - #+attr_latex: :height 250px - #+name: main-interface-expression - [[../screenshots/main-interface-expression-converter.png]] - 2. Enter a [[*Unit Expressions][unit expression]] in the From box. This can be something like "~7 km~" or "~6 ft - 2 in~" or "~3 kg m + 9 lb ft + (35 mm)^2 * (85 oz) / (20 in)~". - 3. Enter a unit name (or another unit expression) in the To box. - 4. Press the Convert button. This will calculate the value of the first expression, and convert it to a multiple of the second unit (or expression). - #+CAPTION: A sample calculation. Divides ~100 km~ by ~35 km/h~ and converts the result to minutes. This could be used to calculate how long (in minutes) it takes to go 100 kilometres at a speed of 35 km/h. - #+attr_latex: :height 250px - #+name: sample-results-expression - [[../screenshots/sample-conversion-results-expression-converter.png]] +1. Select the "Convert Unit Expressions" if it is not already selected. You should see a screen like in figure [[main-interface-expression]]: + #+CAPTION: Taken in version 0.3.0 + #+attr_latex: :height 250px + #+name: main-interface-expression + [[../screenshots/main-interface-expression-converter.png]] +2. Enter a [[*Unit Expressions][unit expression]] in the From box. This can be something like "~7 km~" or "~6 ft - 2 in~" or "~3 kg m + 9 lb ft + (35 mm)^2 * (85 oz) / (20 in)~". +3. Enter a unit name (or another unit expression) in the To box. +4. Press the Convert button. This will calculate the value of the first expression, and convert it to a multiple of the second unit (or expression). + #+CAPTION: A sample calculation. Divides ~100 km~ by ~35 km/h~ and converts the result to minutes. This could be used to calculate how long (in minutes) it takes to go 100 kilometres at a speed of 35 km/h. + #+attr_latex: :height 250px + #+name: sample-results-expression + [[../screenshots/sample-conversion-results-expression-converter.png]] * 7Units Settings - All settings can be accessed in the tab with the gear icon. - #+CAPTION: The settings menu, as of version 0.4.0 - #+ATTR_LaTeX: :height 250px - [[../screenshots/main-interface-settings.png]] +All settings can be accessed in the tab with the gear icon. +#+CAPTION: The settings menu, as of version 0.4.0 +#+ATTR_LaTeX: :height 250px +[[../screenshots/main-interface-settings.png]] ** Rounding Settings - These settings control how the output of a unit conversion is rounded. - - Fixed Precision :: Round to a fixed number of [[https://en.wikipedia.org/wiki/Significant_figures][significant digits]]. The number of significant digits is controlled by the precision slider below. - - Fixed Decimal Places :: Round to a fixed number of digits after the decimal point. The number of decimal places is also controlled by the precision slider below. - - Scientific Precision :: Intelligent rounding which uses the precision of the input value(s) to determine the output precision. Not affected by the precision slider. +These settings control how the output of a unit conversion is rounded. +- Fixed Precision :: Round to a fixed number of [[https://en.wikipedia.org/wiki/Significant_figures][significant digits]]. The number of significant digits is controlled by the precision slider below. +- Fixed Decimal Places :: Round to a fixed number of digits after the decimal point. The number of decimal places is also controlled by the precision slider below. +- Scientific Precision :: Intelligent rounding which uses the precision of the input value(s) to determine the output precision. Not affected by the precision slider. ** Prefix Repetition Settings - These settings control when you are allowed to repeat unit prefixes (e.g. kilokilometre) - - No Repetition :: Units may only have one prefix. - - No Restriction :: Units may have any number of prefixes. - - Complex Repetition :: A complex rule which makes it so that each power of 10 has one and only one prefix combination. Units may have the following prefixes: - - one of: centi, deci, deca, hecto - - one of: zepto, atto, femto, pico, nano, micro, milli, kilo, mega, giga, tera, peta, exa, zetta - - any number of yocto or yotta - - they must be in this order - - all prefixes must be of the same sign (either all magnifying or all reducing) +These settings control when you are allowed to repeat unit prefixes (e.g. kilokilometre) +- No Repetition :: Units may only have one prefix. +- No Restriction :: Units may have any number of prefixes. +- Complex Repetition :: A complex rule which makes it so that each power of 10 has one and only one prefix combination. Units may have the following prefixes: + - one of: centi, deci, deca, hecto + - one of: zepto, atto, femto, pico, nano, micro, milli, kilo, mega, giga, tera, peta, exa, zetta + - any number of yocto or yotta + - they must be in this order + - all prefixes must be of the same sign (either all magnifying or all reducing) ** Search Settings - These settings control which prefixes are shown in the "Convert Units" tab. Only coherent SI units (e.g. metre, second, newton, joule) will get prefixes. Some prefixed units are created in the unitfile, and will stay regardless of this setting (though they can be removed from the unitfile). - - Never Include Prefixed Units :: Prefixed units will only be shown if they are explicitly added to the unitfile. - - Include Common Prefixes :: Every coherent unit will have its kilo- and milli- versions included in the list. - - Include All Single Prefixes :: Every coherent unit will have every prefixed version of it included in the list. +These settings control which prefixes are shown in the "Convert Units" tab. Only coherent SI units (e.g. metre, second, newton, joule) will get prefixes. Some prefixed units are created in the unitfile, and will stay regardless of this setting (though they can be removed from the unitfile). +- Never Include Prefixed Units :: Prefixed units will only be shown if they are explicitly added to the unitfile. +- Include Common Prefixes :: Every coherent unit will have its kilo- and milli- versions included in the list. +- Include All Single Prefixes :: Every coherent unit will have every prefixed version of it included in the list. ** Miscellaneous Settings - - Convert One Way Only :: In the simple conversion tab, only imperial/customary units will be shown on the left, and only metric units[fn:1] will be shown on the right. Units listed in the exceptions file (~src/main/resources/metric_exceptions.txt~) will be shown on both sides. This is a way to reduce the number of options you must search through if you only convert one way. The expressions tab is unaffected. - - Show Duplicates in "Convert Units" :: If unchecked, any unit that has multiple names will only have one included in the Convert Units lists. The selected name will be the longest; if there are multiple longest names one is selected arbitrarily. You will still be able to use these alternate names in the expressions tab. +- Convert One Way Only :: In the simple conversion tab, only imperial/customary units will be shown on the left, and only metric units[fn:1] will be shown on the right. Units listed in the exceptions file (~src/main/resources/metric_exceptions.txt~) will be shown on both sides. This is a way to reduce the number of options you must search through if you only convert one way. The expressions tab is unaffected. +- Show Duplicates in "Convert Units" :: If unchecked, any unit that has multiple names will only have one included in the Convert Units lists. The selected name will be the longest; if there are multiple longest names one is selected arbitrarily. You will still be able to use these alternate names in the expressions tab. +- Use Default Datafiles :: If unchecked, the default units, prefixes and diension names will not be loaded (except for SI base units and their dimensions). +- Locale :: Which language is used for the interface of 7Units. Custom locales can be added. ** Configuration File The settings are saved in a configuration file. On Windows, this is located at \\ ~%USERPROFILE%/AppData/Local/SevenUnits/config.txt~. On other operating systems, this is located at ~$HOME/.config/SevenUnits/config.txt~. The directory containing the ~SevenUnits~ directory can be overridden with the environment variables ~$LOCALAPPDATA~ on Windows or ~$XDG_CONFIG_HOME~ elsewhere. @@ -87,19 +90,22 @@ The possible setting names are: - ~include_duplicates~ :: Whether duplicate units should be shown; can be either ~true~ or ~false~. - ~search_prefix_rule~ :: The prefix search rule; can be ~NO_PREFIXES~, ~COMMON_PREFIXES~, \\ or ~ALL_METRIC_PREFIXES~. +- ~use_default_datafiles~ :: Whether default datafiles should be used; can be either ~true~ or ~false~. +- ~locale~ :: The name of the locale to use. + This is the name of a locale file, excluding the extension, in one of the directories specified in the data specification. For example, if this is 'en', 7Units will look for a file called 'en.txt' in either ~src/main/resources/locales/~ or ~[CONFIG]/locales/~. You can also use the special setting names ~custom_unit_file~, ~custom_dimension_file~ and ~custom_exception_file~ to add custom units, dimensions and metric exceptions to the system. These files use the same format as the standard files. These setting names can be used more than once to include multiple unit, dimension or exception files. * Appendices ** Unit Expressions - A unit expression is simply a math expression where the values being operated on are units or numbers. The operations that can be used are (in order of precedence): - - Exponentiation (^); the exponent must be an integer. Both units and numbers can be raised to an exponent - - Multiplication (*) and division (/). Multiplication can also be done with a space (so "15 meter" is the same thing as "15 * meter"). - You can also divide with ~|~ to create fractions. Using ~|~ instead of ~/~ gives the division a higher precedence than any other operator. For example, "2|5^2" evaluates to 4/25, not 2/25. - - Addition (+) and subtraction (-). They can only be done between units of the same dimension (measuring the same thing). So you can add metres, inches and feet together, and you can add joules and calories together, but you can't add metres to seconds, or feet to calories, or watts to pounds. +A unit expression is simply a math expression where the values being operated on are units or numbers. The operations that can be used are (in order of precedence): +- Exponentiation (^); the exponent must be an integer. Both units and numbers can be raised to an exponent +- Multiplication (*) and division (/). Multiplication can also be done with a space (so "15 meter" is the same thing as "15 * meter"). + You can also divide with ~|~ to create fractions. Using ~|~ instead of ~/~ gives the division a higher precedence than any other operator. For example, "2|5^2" evaluates to 4/25, not 2/25. +- Addition (+) and subtraction (-). They can only be done between units of the same dimension (measuring the same thing). So you can add metres, inches and feet together, and you can add joules and calories together, but you can't add metres to seconds, or feet to calories, or watts to pounds. Brackets can be used to manipulate the order of operations, and nonlinear units like Celsius and Fahrenheit cannot be used in expressions. You can use a value in a nonlinear unit by putting brackets after it - for example, degC(12) represents the value 12 \deg C ** Other Expressions - There are also a simplified version of expressions for prefixes and dimensions. Only multiplication, division and exponentation are supported. Currently, exponentation is not supported for dimensions, but that may be fixed in the future. +There are also a simplified version of expressions for prefixes and dimensions. Only multiplication, division and exponentation are supported. Currently, exponentation is not supported for dimensions, but that may be fixed in the future. * Footnotes -[fn:1] 7Units's definition of "metric" is stricter than the SI, but all of the common units that are commonly considered metric but not included in 7Units's definition are included in the exceptions file. +[fn:1] 7Units's definition of "metric" is stricter than the SI, but all of the common units that are commonly considered metric but not included in 7Units's definition are included in the exceptions file. diff --git a/docs/manual.pdf b/docs/manual.pdf index 5fcc115..38d5c66 100644 Binary files a/docs/manual.pdf and b/docs/manual.pdf differ diff --git a/docs/manual.tex b/docs/manual.tex index e16198f..a1f7c63 100644 --- a/docs/manual.tex +++ b/docs/manual.tex @@ -1,4 +1,4 @@ -% Created 2024-03-24 Sun 13:16 +% Created 2025-06-01 Sun 20:01 % Intended LaTeX compiler: pdflatex \documentclass[11pt]{article} \usepackage[utf8]{inputenc} @@ -13,15 +13,15 @@ \usepackage{capt-of} \usepackage{hyperref} \usepackage[a4paper, lmargin=25mm, rmargin=25mm, tmargin=25mm, bmargin=25mm]{geometry} -\date{2024 March 23} +\date{2025 June 1} \title{7Units User Manual\\\medskip -\large For Version 0.5.0} +\large For Version 1.0.0} \hypersetup{ pdfauthor={}, pdftitle={7Units User Manual}, pdfkeywords={}, pdfsubject={}, - pdfcreator={Emacs 29.2 (Org mode 9.6.15)}, + pdfcreator={Emacs 29.3 (Org mode 9.6.15)}, pdflang={English}} \begin{document} @@ -30,21 +30,22 @@ \newpage \section{Introduction and Purpose} -\label{sec:orgc09fcc7} +\label{sec:org6fb5a20} 7Units is a program that can be used to convert units. This document outlines how to use the program. \section{System Requirements} -\label{sec:orga902335} +\label{sec:org9d01e55} \begin{itemize} \item Works on all major operating systems \\[0pt] \textbf{NOTE:} All screenshots in this document were taken on Windows 10. If you use a different operating system, the program will probably look different than what is shown. \item Java version 11+ required +\item Gradle required +\item To run the software, simply run './gradlew run' in the main directory. \end{itemize} - \newpage \section{How to Use 7Units} -\label{sec:orgdec078f} +\label{sec:org1fd2398} \subsection{Simple Unit Conversion} -\label{sec:org785ebcb} +\label{sec:orga7e83a8} \begin{enumerate} \item Select the "Convert Units" tab if it is not already selected. You should see a screen like in figure \ref{main-interface-dimension}: \begin{figure}[htbp] @@ -69,7 +70,7 @@ \end{figure} \end{enumerate} \subsection{Complex Unit Conversion} -\label{sec:org75a0192} +\label{sec:orgf923c07} \begin{enumerate} \item Select the "Convert Unit Expressions" if it is not already selected. You should see a screen like in figure \ref{main-interface-expression}: \begin{figure}[htbp] @@ -77,7 +78,7 @@ \includegraphics[height=250px]{../screenshots/main-interface-expression-converter.png} \caption{\label{main-interface-expression}Taken in version 0.3.0} \end{figure} -\item Enter a \hyperref[sec:org3724d84]{unit expression} in the From box. This can be something like "\texttt{7 km}" or "\texttt{6 ft - 2 in}" or "\texttt{3 kg m + 9 lb ft + (35 mm)\textasciicircum{}2 * (85 oz) / (20 in)}". +\item Enter a \hyperref[sec:orga2dae79]{unit expression} in the From box. This can be something like "\texttt{7 km}" or "\texttt{6 ft - 2 in}" or "\texttt{3 kg m + 9 lb ft + (35 mm)\textasciicircum{}2 * (85 oz) / (20 in)}". \item Enter a unit name (or another unit expression) in the To box. \item Press the Convert button. This will calculate the value of the first expression, and convert it to a multiple of the second unit (or expression). \begin{figure}[htbp] @@ -87,7 +88,7 @@ \end{figure} \end{enumerate} \section{7Units Settings} -\label{sec:orgae2806f} +\label{sec:org69dc7d4} All settings can be accessed in the tab with the gear icon. \begin{figure}[htbp] \centering @@ -95,7 +96,7 @@ All settings can be accessed in the tab with the gear icon. \caption{The settings menu, as of version 0.4.0} \end{figure} \subsection{Rounding Settings} -\label{sec:org6d3e49c} +\label{sec:org8d63000} These settings control how the output of a unit conversion is rounded. \begin{description} \item[{Fixed Precision}] Round to a fixed number of \href{https://en.wikipedia.org/wiki/Significant\_figures}{significant digits}. The number of significant digits is controlled by the precision slider below. @@ -103,7 +104,7 @@ These settings control how the output of a unit conversion is rounded. \item[{Scientific Precision}] Intelligent rounding which uses the precision of the input value(s) to determine the output precision. Not affected by the precision slider. \end{description} \subsection{Prefix Repetition Settings} -\label{sec:org9aa98f8} +\label{sec:org1f1263f} These settings control when you are allowed to repeat unit prefixes (e.g. kilokilometre) \begin{description} \item[{No Repetition}] Units may only have one prefix. @@ -118,7 +119,7 @@ These settings control when you are allowed to repeat unit prefixes (e.g. kiloki \end{itemize} \end{description} \subsection{Search Settings} -\label{sec:org2745ba0} +\label{sec:org03df615} These settings control which prefixes are shown in the "Convert Units" tab. Only coherent SI units (e.g. metre, second, newton, joule) will get prefixes. Some prefixed units are created in the unitfile, and will stay regardless of this setting (though they can be removed from the unitfile). \begin{description} \item[{Never Include Prefixed Units}] Prefixed units will only be shown if they are explicitly added to the unitfile. @@ -126,13 +127,15 @@ These settings control which prefixes are shown in the "Convert Units" tab. Onl \item[{Include All Single Prefixes}] Every coherent unit will have every prefixed version of it included in the list. \end{description} \subsection{Miscellaneous Settings} -\label{sec:orgeabb2df} +\label{sec:org159a151} \begin{description} \item[{Convert One Way Only}] In the simple conversion tab, only imperial/customary units will be shown on the left, and only metric units\footnote{7Units's definition of "metric" is stricter than the SI, but all of the common units that are commonly considered metric but not included in 7Units's definition are included in the exceptions file.} will be shown on the right. Units listed in the exceptions file (\texttt{src/main/resources/metric\_exceptions.txt}) will be shown on both sides. This is a way to reduce the number of options you must search through if you only convert one way. The expressions tab is unaffected. \item[{Show Duplicates in "Convert Units"}] If unchecked, any unit that has multiple names will only have one included in the Convert Units lists. The selected name will be the longest; if there are multiple longest names one is selected arbitrarily. You will still be able to use these alternate names in the expressions tab. +\item[{Use Default Datafiles}] If unchecked, the default units, prefixes and diension names will not be loaded (except for SI base units and their dimensions). +\item[{Locale}] Which language is used for the interface of 7Units. Custom locales can be added. \end{description} \subsection{Configuration File} -\label{sec:org4cc2874} +\label{sec:org398eb26} The settings are saved in a configuration file. On Windows, this is located at \\[0pt] \texttt{\%USERPROFILE\%/AppData/Local/SevenUnits/config.txt}. On other operating systems, this is located at \texttt{\$HOME/.config/SevenUnits/config.txt}. The directory containing the \texttt{SevenUnits} directory can be overridden with the environment variables \texttt{\$LOCALAPPDATA} on Windows or \texttt{\$XDG\_CONFIG\_HOME} elsewhere. @@ -147,23 +150,26 @@ or \texttt{COMPLEX\_REPETITION}. \item[{\texttt{include\_duplicates}}] Whether duplicate units should be shown; can be either \texttt{true} or \texttt{false}. \item[{\texttt{search\_prefix\_rule}}] The prefix search rule; can be \texttt{NO\_PREFIXES}, \texttt{COMMON\_PREFIXES}, \\[0pt] or \texttt{ALL\_METRIC\_PREFIXES}. +\item[{\texttt{use\_default\_datafiles}}] Whether default datafiles should be used; can be either \texttt{true} or \texttt{false}. +\item[{\texttt{locale}}] The name of the locale to use. +This is the name of a locale file, excluding the extension, in one of the directories specified in the data specification. For example, if this is 'en', 7Units will look for a file called 'en.txt' in either \texttt{src/main/resources/locales/} or \texttt{[CONFIG]/locales/}. \end{description} You can also use the special setting names \texttt{custom\_unit\_file}, \texttt{custom\_dimension\_file} and \texttt{custom\_exception\_file} to add custom units, dimensions and metric exceptions to the system. These files use the same format as the standard files. These setting names can be used more than once to include multiple unit, dimension or exception files. \section{Appendices} -\label{sec:org60385a7} +\label{sec:org89d72bb} \subsection{Unit Expressions} -\label{sec:org3724d84} +\label{sec:orga2dae79} A unit expression is simply a math expression where the values being operated on are units or numbers. The operations that can be used are (in order of precedence): \begin{itemize} \item Exponentiation (\^{}); the exponent must be an integer. Both units and numbers can be raised to an exponent \item Multiplication (*) and division (/). Multiplication can also be done with a space (so "15 meter" is the same thing as "15 * meter"). You can also divide with \texttt{|} to create fractions. Using \texttt{|} instead of \texttt{/} gives the division a higher precedence than any other operator. For example, "2|5\textsuperscript{2}" evaluates to 4/25, not 2/25. \item Addition (+) and subtraction (-). They can only be done between units of the same dimension (measuring the same thing). So you can add metres, inches and feet together, and you can add joules and calories together, but you can't add metres to seconds, or feet to calories, or watts to pounds. -\end{itemize} Brackets can be used to manipulate the order of operations, and nonlinear units like Celsius and Fahrenheit cannot be used in expressions. You can use a value in a nonlinear unit by putting brackets after it - for example, degC(12) represents the value 12 \textdegree{} C +\end{itemize} \subsection{Other Expressions} -\label{sec:orgc72a672} +\label{sec:orgf67cbc9} There are also a simplified version of expressions for prefixes and dimensions. Only multiplication, division and exponentation are supported. Currently, exponentation is not supported for dimensions, but that may be fixed in the future. \end{document} diff --git a/docs/roadmap.org b/docs/roadmap.org deleted file mode 100644 index fdd12ac..0000000 --- a/docs/roadmap.org +++ /dev/null @@ -1,6 +0,0 @@ -* Version 1.0.0 Roadmap -Here is a list of the unfinished requirements for version 1.0.0. When everything here is met, I intend to release version 1.0.0 and consider 7Units complete (for the most part). - -These requirements are subject to change. I intend to finish version 1.0.0 by [2025-06-01 Sun]. - -Once the documentation is up to date, 7Units 1.0.0 can be released. -- cgit v1.2.3 From a00ad7ca48928a30ae577aeaed0345680df0a3fe Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Sat, 7 Jun 2025 22:11:45 -0500 Subject: Fix e-notation & consolidate expression parsing This commit moves all of the expression formatting code to one method, and changes it so that it works with things like '1e+2'. This does mean that I had to require spaces for addition and subtraction, but without that, the rules would be complicated. --- docs/manual.org | 8 +- docs/manual.pdf | Bin 186518 -> 188582 bytes docs/manual.tex | 40 +- src/main/java/sevenUnits/unit/UnitDatabase.java | 574 +++++++++------------ .../java/sevenUnits/utils/ExpressionParser.java | 3 +- .../java/sevenUnits/unit/UnitDatabaseTest.java | 29 +- 6 files changed, 305 insertions(+), 349 deletions(-) (limited to 'docs') diff --git a/docs/manual.org b/docs/manual.org index 92160c3..bc58ceb 100644 --- a/docs/manual.org +++ b/docs/manual.org @@ -98,12 +98,12 @@ You can also use the special setting names ~custom_unit_file~, ~custom_dimension * Appendices ** Unit Expressions A unit expression is simply a math expression where the values being operated on are units or numbers. The operations that can be used are (in order of precedence): -- Exponentiation (^); the exponent must be an integer. Both units and numbers can be raised to an exponent -- Multiplication (*) and division (/). Multiplication can also be done with a space (so "15 meter" is the same thing as "15 * meter"). +- Exponentiation (^); the exponent must be an integer. Both units and numbers can be raised to an exponent. +- Multiplication (*) and division (/). Multiplication can also be done with a space (so "15 meter" is the same thing as "15 * meter"), and multiplication using spaces has higher precedence than division. Therefore, "2 m / 1 m" is the number 2, while "2 * m / 1 * m" is equal to 2 m^2. You can also divide with ~|~ to create fractions. Using ~|~ instead of ~/~ gives the division a higher precedence than any other operator. For example, "2|5^2" evaluates to 4/25, not 2/25. -- Addition (+) and subtraction (-). They can only be done between units of the same dimension (measuring the same thing). So you can add metres, inches and feet together, and you can add joules and calories together, but you can't add metres to seconds, or feet to calories, or watts to pounds. +- Addition (+) and subtraction (-). They can only be done between units of the same dimension (measuring the same thing). So you can add metres, inches and feet together, and you can add joules and calories together, but you can't add metres to seconds, or feet to calories, or watts to pounds. *You must use spaces between terms when adding or subtracting - "1 + 2" is valid, "1+2" is not. This only applies to addition and subtraction - other operators do not need spaces between them.* This is done to avoid complex rules when working with negative numbers and e-notation. - Brackets can be used to manipulate the order of operations, and nonlinear units like Celsius and Fahrenheit cannot be used in expressions. You can use a value in a nonlinear unit by putting brackets after it - for example, degC(12) represents the value 12 \deg C + Brackets can be used to manipulate the order of operations, and nonlinear units like Celsius and Fahrenheit cannot be used in expressions. You can use a value in a nonlinear unit by putting brackets after it - for example, degC(12) represents the value 12 \deg{}C ** Other Expressions There are also a simplified version of expressions for prefixes and dimensions. Only multiplication, division and exponentation are supported. Currently, exponentation is not supported for dimensions, but that may be fixed in the future. * Footnotes diff --git a/docs/manual.pdf b/docs/manual.pdf index 38d5c66..ca31104 100644 Binary files a/docs/manual.pdf and b/docs/manual.pdf differ diff --git a/docs/manual.tex b/docs/manual.tex index a1f7c63..8ab09d5 100644 --- a/docs/manual.tex +++ b/docs/manual.tex @@ -1,4 +1,4 @@ -% Created 2025-06-01 Sun 20:01 +% Created 2025-06-07 Sat 18:16 % Intended LaTeX compiler: pdflatex \documentclass[11pt]{article} \usepackage[utf8]{inputenc} @@ -30,10 +30,10 @@ \newpage \section{Introduction and Purpose} -\label{sec:org6fb5a20} +\label{sec:orgf5013f4} 7Units is a program that can be used to convert units. This document outlines how to use the program. \section{System Requirements} -\label{sec:org9d01e55} +\label{sec:org9c3bf6a} \begin{itemize} \item Works on all major operating systems \\[0pt] \textbf{NOTE:} All screenshots in this document were taken on Windows 10. If you use a different operating system, the program will probably look different than what is shown. @@ -43,9 +43,9 @@ \end{itemize} \newpage \section{How to Use 7Units} -\label{sec:org1fd2398} +\label{sec:org6a030cf} \subsection{Simple Unit Conversion} -\label{sec:orga7e83a8} +\label{sec:org4406ca2} \begin{enumerate} \item Select the "Convert Units" tab if it is not already selected. You should see a screen like in figure \ref{main-interface-dimension}: \begin{figure}[htbp] @@ -70,7 +70,7 @@ \end{figure} \end{enumerate} \subsection{Complex Unit Conversion} -\label{sec:orgf923c07} +\label{sec:org721af5f} \begin{enumerate} \item Select the "Convert Unit Expressions" if it is not already selected. You should see a screen like in figure \ref{main-interface-expression}: \begin{figure}[htbp] @@ -78,7 +78,7 @@ \includegraphics[height=250px]{../screenshots/main-interface-expression-converter.png} \caption{\label{main-interface-expression}Taken in version 0.3.0} \end{figure} -\item Enter a \hyperref[sec:orga2dae79]{unit expression} in the From box. This can be something like "\texttt{7 km}" or "\texttt{6 ft - 2 in}" or "\texttt{3 kg m + 9 lb ft + (35 mm)\textasciicircum{}2 * (85 oz) / (20 in)}". +\item Enter a \hyperref[sec:orgfffe912]{unit expression} in the From box. This can be something like "\texttt{7 km}" or "\texttt{6 ft - 2 in}" or "\texttt{3 kg m + 9 lb ft + (35 mm)\textasciicircum{}2 * (85 oz) / (20 in)}". \item Enter a unit name (or another unit expression) in the To box. \item Press the Convert button. This will calculate the value of the first expression, and convert it to a multiple of the second unit (or expression). \begin{figure}[htbp] @@ -88,7 +88,7 @@ \end{figure} \end{enumerate} \section{7Units Settings} -\label{sec:org69dc7d4} +\label{sec:org437288e} All settings can be accessed in the tab with the gear icon. \begin{figure}[htbp] \centering @@ -96,7 +96,7 @@ All settings can be accessed in the tab with the gear icon. \caption{The settings menu, as of version 0.4.0} \end{figure} \subsection{Rounding Settings} -\label{sec:org8d63000} +\label{sec:orgc275b0f} These settings control how the output of a unit conversion is rounded. \begin{description} \item[{Fixed Precision}] Round to a fixed number of \href{https://en.wikipedia.org/wiki/Significant\_figures}{significant digits}. The number of significant digits is controlled by the precision slider below. @@ -104,7 +104,7 @@ These settings control how the output of a unit conversion is rounded. \item[{Scientific Precision}] Intelligent rounding which uses the precision of the input value(s) to determine the output precision. Not affected by the precision slider. \end{description} \subsection{Prefix Repetition Settings} -\label{sec:org1f1263f} +\label{sec:org1789e5a} These settings control when you are allowed to repeat unit prefixes (e.g. kilokilometre) \begin{description} \item[{No Repetition}] Units may only have one prefix. @@ -119,7 +119,7 @@ These settings control when you are allowed to repeat unit prefixes (e.g. kiloki \end{itemize} \end{description} \subsection{Search Settings} -\label{sec:org03df615} +\label{sec:orgfcf1ac1} These settings control which prefixes are shown in the "Convert Units" tab. Only coherent SI units (e.g. metre, second, newton, joule) will get prefixes. Some prefixed units are created in the unitfile, and will stay regardless of this setting (though they can be removed from the unitfile). \begin{description} \item[{Never Include Prefixed Units}] Prefixed units will only be shown if they are explicitly added to the unitfile. @@ -127,7 +127,7 @@ These settings control which prefixes are shown in the "Convert Units" tab. Onl \item[{Include All Single Prefixes}] Every coherent unit will have every prefixed version of it included in the list. \end{description} \subsection{Miscellaneous Settings} -\label{sec:org159a151} +\label{sec:org2a48ec0} \begin{description} \item[{Convert One Way Only}] In the simple conversion tab, only imperial/customary units will be shown on the left, and only metric units\footnote{7Units's definition of "metric" is stricter than the SI, but all of the common units that are commonly considered metric but not included in 7Units's definition are included in the exceptions file.} will be shown on the right. Units listed in the exceptions file (\texttt{src/main/resources/metric\_exceptions.txt}) will be shown on both sides. This is a way to reduce the number of options you must search through if you only convert one way. The expressions tab is unaffected. \item[{Show Duplicates in "Convert Units"}] If unchecked, any unit that has multiple names will only have one included in the Convert Units lists. The selected name will be the longest; if there are multiple longest names one is selected arbitrarily. You will still be able to use these alternate names in the expressions tab. @@ -135,7 +135,7 @@ These settings control which prefixes are shown in the "Convert Units" tab. Onl \item[{Locale}] Which language is used for the interface of 7Units. Custom locales can be added. \end{description} \subsection{Configuration File} -\label{sec:org398eb26} +\label{sec:orgd976cbf} The settings are saved in a configuration file. On Windows, this is located at \\[0pt] \texttt{\%USERPROFILE\%/AppData/Local/SevenUnits/config.txt}. On other operating systems, this is located at \texttt{\$HOME/.config/SevenUnits/config.txt}. The directory containing the \texttt{SevenUnits} directory can be overridden with the environment variables \texttt{\$LOCALAPPDATA} on Windows or \texttt{\$XDG\_CONFIG\_HOME} elsewhere. @@ -157,19 +157,19 @@ This is the name of a locale file, excluding the extension, in one of the direct You can also use the special setting names \texttt{custom\_unit\_file}, \texttt{custom\_dimension\_file} and \texttt{custom\_exception\_file} to add custom units, dimensions and metric exceptions to the system. These files use the same format as the standard files. These setting names can be used more than once to include multiple unit, dimension or exception files. \section{Appendices} -\label{sec:org89d72bb} +\label{sec:orgec4e8a9} \subsection{Unit Expressions} -\label{sec:orga2dae79} +\label{sec:orgfffe912} A unit expression is simply a math expression where the values being operated on are units or numbers. The operations that can be used are (in order of precedence): \begin{itemize} -\item Exponentiation (\^{}); the exponent must be an integer. Both units and numbers can be raised to an exponent -\item Multiplication (*) and division (/). Multiplication can also be done with a space (so "15 meter" is the same thing as "15 * meter"). +\item Exponentiation (\^{}); the exponent must be an integer. Both units and numbers can be raised to an exponent. +\item Multiplication (*) and division (/). Multiplication can also be done with a space (so "15 meter" is the same thing as "15 * meter"), and multiplication using spaces has higher precedence than division. Therefore, "2 m / 1 m" is the number 2, while "2 * m / 1 * m" is equal to 2 m\textsuperscript{2}. You can also divide with \texttt{|} to create fractions. Using \texttt{|} instead of \texttt{/} gives the division a higher precedence than any other operator. For example, "2|5\textsuperscript{2}" evaluates to 4/25, not 2/25. -\item Addition (+) and subtraction (-). They can only be done between units of the same dimension (measuring the same thing). So you can add metres, inches and feet together, and you can add joules and calories together, but you can't add metres to seconds, or feet to calories, or watts to pounds. +\item Addition (+) and subtraction (-). They can only be done between units of the same dimension (measuring the same thing). So you can add metres, inches and feet together, and you can add joules and calories together, but you can't add metres to seconds, or feet to calories, or watts to pounds. \textbf{You must use spaces between terms when adding or subtracting - "1 + 2" is valid, "1+2" is not. This only applies to addition and subtraction - other operators do not need spaces between them.} This is done to avoid complex rules when working with negative numbers and e-notation. -Brackets can be used to manipulate the order of operations, and nonlinear units like Celsius and Fahrenheit cannot be used in expressions. You can use a value in a nonlinear unit by putting brackets after it - for example, degC(12) represents the value 12 \textdegree{} C +Brackets can be used to manipulate the order of operations, and nonlinear units like Celsius and Fahrenheit cannot be used in expressions. You can use a value in a nonlinear unit by putting brackets after it - for example, degC(12) represents the value 12 \textdegree{}C \end{itemize} \subsection{Other Expressions} -\label{sec:orgf67cbc9} +\label{sec:org5d2d129} There are also a simplified version of expressions for prefixes and dimensions. Only multiplication, division and exponentation are supported. Currently, exponentation is not supported for dimensions, but that may be fixed in the future. \end{document} diff --git a/src/main/java/sevenUnits/unit/UnitDatabase.java b/src/main/java/sevenUnits/unit/UnitDatabase.java index b0d026f..a85ec5f 100644 --- a/src/main/java/sevenUnits/unit/UnitDatabase.java +++ b/src/main/java/sevenUnits/unit/UnitDatabase.java @@ -122,7 +122,7 @@ public final class UnitDatabase { implements Entry { private final String key; private final Unit value; - + /** * Creates the {@code PrefixedUnitEntry}. * @@ -135,7 +135,7 @@ public final class UnitDatabase { this.key = key; this.value = value; } - + /** * @since 2019-05-03 * @since v0.3.0 @@ -148,17 +148,17 @@ public final class UnitDatabase { 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 * @since v0.3.0 @@ -169,13 +169,13 @@ public final class UnitDatabase { ^ (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 @@ -190,7 +190,7 @@ public final class UnitDatabase { return this.getKey() + "=" + this.getValue(); } } - + /** * An iterator that iterates over the units of a * {@code PrefixedUnitNameSet}. @@ -205,12 +205,12 @@ public final class UnitDatabase { 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}. @@ -225,7 +225,7 @@ public final class UnitDatabase { this.unitNames = new ArrayList<>(map.units.keySet()); this.prefixNames = new ArrayList<>(map.prefixes.keySet()); } - + /** * @return current unit name * @since 2019-04-14 @@ -237,10 +237,10 @@ public final class UnitDatabase { unitName.append(this.prefixNames.get(i)); } unitName.append(this.unitNames.get(this.unitNamePosition)); - + return unitName.toString(); } - + @Override public boolean hasNext() { if (this.unitNames.isEmpty()) @@ -253,7 +253,7 @@ public final class UnitDatabase { return true; } } - + /** * Changes this iterator's position to the next available one. * @@ -262,11 +262,11 @@ public final class UnitDatabase { */ 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); @@ -275,7 +275,7 @@ public final class UnitDatabase { 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()) { @@ -283,7 +283,7 @@ public final class UnitDatabase { 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 @@ -294,18 +294,18 @@ public final class UnitDatabase { } } } - + @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 @@ -315,7 +315,7 @@ public final class UnitDatabase { 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 @@ -326,12 +326,12 @@ public final class UnitDatabase { 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. @@ -346,10 +346,10 @@ public final class UnitDatabase { this.peek()); } } - + // the map that created this set private final PrefixedUnitMap map; - + /** * Creates the {@code PrefixedUnitNameSet}. * @@ -360,31 +360,31 @@ public final class UnitDatabase { 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. @@ -395,11 +395,11 @@ public final class UnitDatabase { 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) @@ -407,42 +407,42 @@ public final class UnitDatabase { 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()) @@ -455,7 +455,7 @@ public final class UnitDatabase { return Integer.MAX_VALUE; } } - + /** * @throws IllegalStateException if the set is infinite in size */ @@ -468,7 +468,7 @@ public final class UnitDatabase { throw new IllegalStateException( "Cannot make an infinite set into an array."); } - + /** * @throws IllegalStateException if the set is infinite in size */ @@ -481,7 +481,7 @@ public final class UnitDatabase { throw new IllegalStateException( "Cannot make an infinite set into an array."); } - + @Override public String toString() { if (this.map.units.isEmpty() || this.map.prefixes.isEmpty()) @@ -492,7 +492,7 @@ public final class UnitDatabase { this.map.units, this.map.prefixes); } } - + /** * The class used for unit name sets. * @@ -524,12 +524,12 @@ public final class UnitDatabase { 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}. @@ -544,7 +544,7 @@ public final class UnitDatabase { this.unitNames = new ArrayList<>(map.units.keySet()); this.prefixNames = new ArrayList<>(map.prefixes.keySet()); } - + /** * @return current unit name * @since 2019-04-14 @@ -556,10 +556,10 @@ public final class UnitDatabase { unitName.append(this.prefixNames.get(i)); } unitName.append(this.unitNames.get(this.unitNamePosition)); - + return unitName.toString(); } - + @Override public boolean hasNext() { if (this.unitNames.isEmpty()) @@ -572,7 +572,7 @@ public final class UnitDatabase { return true; } } - + /** * Changes this iterator's position to the next available one. * @@ -581,11 +581,11 @@ public final class UnitDatabase { */ 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); @@ -594,7 +594,7 @@ public final class UnitDatabase { 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()) { @@ -602,7 +602,7 @@ public final class UnitDatabase { 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 @@ -613,16 +613,16 @@ public final class UnitDatabase { } } } - + @Override public String next() { final String nextName = this.peek(); - + this.incrementPosition(); - + return nextName; } - + /** * @return the next element in the iterator, without iterating over * it @@ -642,10 +642,10 @@ public final class UnitDatabase { 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. @@ -660,10 +660,10 @@ public final class UnitDatabase { this.peek()); } } - + // the map that created this set private final PrefixedUnitMap map; - + /** * Creates the {@code PrefixedUnitNameSet}. * @@ -674,30 +674,30 @@ public final class UnitDatabase { 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) @@ -705,41 +705,41 @@ public final class UnitDatabase { 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()) @@ -752,7 +752,7 @@ public final class UnitDatabase { return Integer.MAX_VALUE; } } - + /** * @throws IllegalStateException if the set is infinite in size */ @@ -764,9 +764,9 @@ public final class UnitDatabase { // infinite set throw new IllegalStateException( "Cannot make an infinite set into an array."); - + } - + /** * @throws IllegalStateException if the set is infinite in size */ @@ -779,7 +779,7 @@ public final class UnitDatabase { throw new IllegalStateException( "Cannot make an infinite set into an array."); } - + @Override public String toString() { if (this.map.units.isEmpty() || this.map.prefixes.isEmpty()) @@ -790,7 +790,7 @@ public final class UnitDatabase { this.map.units, this.map.prefixes); } } - + /** * The units stored in this collection, without prefixes. * @@ -798,7 +798,7 @@ public final class UnitDatabase { * @since v0.2.0 */ private final Map units; - + /** * The available prefixes for use. * @@ -806,12 +806,12 @@ public final class UnitDatabase { * @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}. * @@ -827,50 +827,50 @@ public final class UnitDatabase { 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) @@ -889,10 +889,10 @@ public final class UnitDatabase { } } } - + return longestPrefix != null; } - + /** * {@inheritDoc} * @@ -905,7 +905,7 @@ public final class UnitDatabase { public boolean containsValue(final Object value) { return this.units.containsValue(value); } - + @Override public Set> entrySet() { if (this.entrySet == null) { @@ -913,23 +913,23 @@ public final class UnitDatabase { } 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) @@ -948,7 +948,7 @@ public final class UnitDatabase { } } } - + // if none found, returns null if (longestPrefix == null) return null; @@ -959,16 +959,16 @@ public final class UnitDatabase { // 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) { @@ -976,64 +976,64 @@ public final class UnitDatabase { } 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()) @@ -1046,7 +1046,7 @@ public final class UnitDatabase { return Integer.MAX_VALUE; } } - + @Override public String toString() { if (this.units.isEmpty() || this.prefixes.isEmpty()) @@ -1056,7 +1056,7 @@ public final class UnitDatabase { "Infinite map of name-unit entries created from units %s and prefixes %s", this.units, this.prefixes); } - + /** * {@inheritDoc} * @@ -1074,48 +1074,20 @@ public final class UnitDatabase { return this.values; } } - - /** - * Replacements done to *all* expression types - */ - private static final Map EXPRESSION_REPLACEMENTS = new HashMap<>(); - - // add data to expression replacements - static { - // add spaces around operators - for (final String operator : Arrays.asList("\\*", "/", "\\|", "\\^")) { - EXPRESSION_REPLACEMENTS.put(Pattern.compile(operator), - " " + operator + " "); - } - - // replace multiple spaces with a single space - EXPRESSION_REPLACEMENTS.put(Pattern.compile(" +"), " "); - // place brackets around any expression of the form "number unit", with or - // without the space - EXPRESSION_REPLACEMENTS.put(Pattern.compile("((?:-?[1-9]\\d*|0)" // integer - + "(?:\\.\\d+(?:[eE]\\d+))?)" // optional decimal point with numbers - // after it - + "\\s*" // optional space(s) - + "([a-zA-Z]+(?:\\^\\d+)?" // any string of letters - + "(?:\\s+[a-zA-Z]+(?:\\^\\d+)?))" // optional other letters - + "(?!-?\\d)" // no number directly afterwards (avoids matching - // "1e3") - ), "\\($1 $2\\)"); - } - + /** * A regular expression that separates names and expressions in unit files. */ private static final Pattern NAME_EXPRESSION = Pattern .compile("(\\S+)\\s+(\\S.*)"); - + /** * Like normal string comparisons, but shorter strings are always less than * longer strings. */ private static final Comparator lengthFirstComparator = Comparator .comparingInt(String::length).thenComparing(Comparator.naturalOrder()); - + /** * The exponent operator * @@ -1131,11 +1103,11 @@ public final class UnitDatabase { throw new IllegalArgumentException(String.format( "Tried to exponentiate %s^%s, but exponents must be dimensionless numbers.", base, exponentUnit)); - + final double exponent = exponentUnit.getConversionFactor(); return base.toExponentRounded(exponent); } - + /** * The exponent operator * @@ -1151,11 +1123,33 @@ public final class UnitDatabase { throw new IllegalArgumentException(String.format( "Tried to exponentiate %s^%s, but exponents must be dimensionless numbers.", base, exponentValue)); - + final double exponent = exponentValue.getValueExact(); return base.toExponentRounded(exponent); } - + + /** + * Formats an expression so it can be parsed by the expression parser. + * + * Specifically, puts spaces around all operators so they can be parsed as + * words. + * + * @param expression expression to format + * @return formatted expression + * @since 2025-06-07 + * @since v1.0.0 + */ + static final String formatExpression(String expression) { + String modifiedExpression = expression; + for (final String operator : Arrays.asList("\\*", "/", "\\|", "\\^")) { + modifiedExpression = modifiedExpression.replaceAll( + operator, " " + operator + " "); + } + + modifiedExpression = modifiedExpression.replaceAll("\\s+", " "); + return modifiedExpression; + } + /** * @return true if entry represents a removable duplicate entry of map. * @since 2021-05-22 @@ -1172,7 +1166,7 @@ public final class UnitDatabase { } return false; } - + /** * The units in this system, excluding prefixes. * @@ -1180,7 +1174,7 @@ public final class UnitDatabase { * @since v0.1.0 */ private final Map prefixlessUnits; - + /** * The unit prefixes in this system. * @@ -1188,7 +1182,7 @@ public final class UnitDatabase { * @since v0.1.0 */ private final Map prefixes; - + /** * The dimensions in this system. * @@ -1196,7 +1190,7 @@ public final class UnitDatabase { * @since v0.2.0 */ private final Map> dimensions; - + /** * A map mapping strings to units (including prefixes) * @@ -1204,7 +1198,7 @@ public final class UnitDatabase { * @since v0.2.0 */ private final Map units; - + /** * A map mapping strings to unit sets * @@ -1212,7 +1206,7 @@ public final class UnitDatabase { * @since v1.0.0 */ private final Map> unitSets; - + /** * The rule that specifies when prefix repetition is allowed. It takes in one * argument: a list of the prefixes being applied to the unit @@ -1224,7 +1218,7 @@ public final class UnitDatabase { * {@code prefixRepetitionRule.test(Arrays.asList(giga, mega, kilo))} */ private Predicate> prefixRepetitionRule; - + /** * A parser that can parse unit expressions. * @@ -1233,14 +1227,14 @@ public final class UnitDatabase { */ 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("|", (o1, o2) -> o1.dividedBy(o2), 3) - .addBinaryOperator("^", UnitDatabase::exponentiateUnits, 2) - .build(); - + .addBinaryOperator("-", (o1, o2) -> o1.minus(o2), 0) + .addBinaryOperator("*", (o1, o2) -> o1.times(o2), 1) + .addBinaryOperator("space_times", (o1, o2) -> o1.times(o2), 2) + .addSpaceFunction("space_times") + .addBinaryOperator("/", (o1, o2) -> o1.dividedBy(o2), 1) + .addBinaryOperator("|", (o1, o2) -> o1.dividedBy(o2), 4) + .addBinaryOperator("^", UnitDatabase::exponentiateUnits, 3).build(); + /** * A parser that can parse unit value expressions. * @@ -1249,15 +1243,16 @@ public final class UnitDatabase { */ private final ExpressionParser unitValueExpressionParser = new ExpressionParser.Builder<>( this::getLinearUnitValue) - .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("|", (o1, o2) -> o1.dividedBy(o2), 3) - .addBinaryOperator("^", UnitDatabase::exponentiateUnitValues, 2) - .build(); - + .addBinaryOperator("+", (o1, o2) -> o1.plus(o2), 0) + .addBinaryOperator("-", (o1, o2) -> o1.minus(o2), 0) + .addBinaryOperator("*", (o1, o2) -> o1.times(o2), 1) + .addBinaryOperator("space_times", (o1, o2) -> o1.times(o2), 2) + .addSpaceFunction("space_times") + .addBinaryOperator("/", (o1, o2) -> o1.dividedBy(o2), 1) + .addBinaryOperator("|", (o1, o2) -> o1.dividedBy(o2), 4) + .addBinaryOperator("^", UnitDatabase::exponentiateUnitValues, 3) + .build(); + /** * A parser that can parse unit prefix expressions * @@ -1266,15 +1261,15 @@ public final class UnitDatabase { */ private final ExpressionParser prefixExpressionParser = new ExpressionParser.Builder<>( this::getPrefix).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("|", (o1, o2) -> o1.dividedBy(o2), 3) - .addBinaryOperator("^", - (o1, o2) -> o1.toExponent(o2.getMultiplier()), 2) - .build(); - + .addBinaryOperator("-", (o1, o2) -> o1.minus(o2), 0) + .addBinaryOperator("*", (o1, o2) -> o1.times(o2), 1) + .addSpaceFunction("*") + .addBinaryOperator("/", (o1, o2) -> o1.dividedBy(o2), 1) + .addBinaryOperator("|", (o1, o2) -> o1.dividedBy(o2), 3) + .addBinaryOperator("^", (o1, o2) -> o1.toExponent(o2.getMultiplier()), + 2) + .build(); + /** * A parser that can parse unit dimension expressions. * @@ -1283,14 +1278,14 @@ public final class UnitDatabase { */ private final ExpressionParser> unitDimensionParser = new ExpressionParser.Builder<>( this::getDimension).addBinaryOperator("*", (o1, o2) -> o1.times(o2), 0) - .addSpaceFunction("*") - .addBinaryOperator("/", (o1, o2) -> o1.dividedBy(o2), 0) - .addBinaryOperator("|", (o1, o2) -> o1.dividedBy(o2), 2) - .addNumericOperator("^", (o1, o2) -> { - final int exponent = (int) Math.round(o2.value()); - return o1.toExponent(exponent); - }, 1).build(); - + .addSpaceFunction("*") + .addBinaryOperator("/", (o1, o2) -> o1.dividedBy(o2), 0) + .addBinaryOperator("|", (o1, o2) -> o1.dividedBy(o2), 2) + .addNumericOperator("^", (o1, o2) -> { + final int exponent = (int) Math.round(o2.value()); + return o1.toExponent(exponent); + }, 1).build(); + /** * Creates the {@code UnitsDatabase}. * @@ -1300,7 +1295,7 @@ public final class UnitDatabase { public UnitDatabase() { this(prefixes -> true); } - + /** * Creates the {@code UnitsDatabase} * @@ -1320,7 +1315,7 @@ public final class UnitDatabase { .test(this.getPrefixesFromName(entry.getKey()))); this.unitSets = new HashMap<>(); } - + /** * Adds a unit dimension to the database. * @@ -1338,7 +1333,7 @@ public final class UnitDatabase { .withName(dimension.getNameSymbol().withExtraName(name)); this.dimensions.put(name, namedDimension); } - + /** * Adds to the list from a line in a unit dimension file. * @@ -1357,7 +1352,7 @@ public final class UnitDatabase { lineCounter); return; } - + // divide line into name and expression final Matcher lineMatcher = NAME_EXPRESSION.matcher(line); if (!lineMatcher.matches()) @@ -1366,12 +1361,12 @@ public final class UnitDatabase { 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("!")) { @@ -1382,7 +1377,7 @@ public final class UnitDatabase { this.addDimension(name, this.getDimensionFromExpression(expression)); } } - + /** * Adds a unit prefix to the database. * @@ -1399,7 +1394,7 @@ public final class UnitDatabase { this.prefixes.put(Objects.requireNonNull(name, "name must not be null."), namedPrefix); } - + /** * Adds a unit to the database. * @@ -1416,7 +1411,7 @@ public final class UnitDatabase { this.prefixlessUnits.put( Objects.requireNonNull(name, "name must not be null."), namedUnit); } - + /** * Adds to the list from a line in a unit file. * @@ -1435,7 +1430,7 @@ public final class UnitDatabase { lineCounter); return; } - + // divide line into name and expression final Matcher lineMatcher = NAME_EXPRESSION.matcher(line); if (!lineMatcher.matches()) @@ -1443,15 +1438,15 @@ public final class UnitDatabase { "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); - + // this code should never occur // 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("!")) { @@ -1472,7 +1467,7 @@ public final class UnitDatabase { } } } - + /** * Add a unit set to the database. * @@ -1490,10 +1485,10 @@ public final class UnitDatabase { "Unit sets must be all the same dimension, " + value + " is not."); } - + this.unitSets.put(name, value); } - + /** * Removes all units, unit sets, prefixes and dimensions from this database. * @@ -1506,7 +1501,7 @@ public final class UnitDatabase { this.prefixlessUnits.clear(); this.unitSets.clear(); } - + /** * Tests if the database has a unit dimension with this name. * @@ -1518,7 +1513,7 @@ public final class UnitDatabase { public boolean containsDimensionName(final String name) { return this.dimensions.containsKey(name); } - + /** * Tests if the database has a unit prefix with this name. * @@ -1530,7 +1525,7 @@ public final class UnitDatabase { 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 @@ -1543,7 +1538,7 @@ public final class UnitDatabase { public boolean containsUnitName(final String name) { return this.units.containsKey(name); } - + /** * Returns true iff there is a unit set with this name. * @@ -1556,7 +1551,7 @@ public final class UnitDatabase { public boolean containsUnitSetName(String name) { return this.unitSets.containsKey(name); } - + /** * @return a map mapping dimension names to dimensions * @since 2019-04-13 @@ -1565,7 +1560,7 @@ public final class UnitDatabase { public Map> dimensionMap() { return Collections.unmodifiableMap(this.dimensions); } - + /** * Evaluates a unit expression, following the same rules as * {@link #getUnitFromExpression}. @@ -1577,39 +1572,15 @@ public final class UnitDatabase { */ public LinearUnitValue evaluateUnitExpression(final String expression) { Objects.requireNonNull(expression, "expression must not be null."); - + // attempt to get a unit as an alias, or a number with precision first if (this.containsUnitName(expression)) return this.getLinearUnitValue(expression); - - // force operators to have spaces - String modifiedExpression = expression; - modifiedExpression = modifiedExpression.replaceAll("\\+", " \\+ "); - modifiedExpression = modifiedExpression.replaceAll("-", " - "); - - // format expression - for (final Entry replacement : EXPRESSION_REPLACEMENTS - .entrySet()) { - modifiedExpression = replacement.getKey().matcher(modifiedExpression) - .replaceAll(replacement.getValue()); - } - - // the previous operation breaks negative numbers, fix them! - // (i.e. -2 becomes - 2) - // FIXME the previous operation also breaks stuff like "1e-5" - for (int i = 0; i < modifiedExpression.length(); i++) { - if (modifiedExpression.charAt(i) == '-' - && (i < 2 || Arrays.asList('+', '-', '*', '/', '|', '^') - .contains(modifiedExpression.charAt(i - 2)))) { - // found a broken negative number - modifiedExpression = modifiedExpression.substring(0, i + 1) - + modifiedExpression.substring(i + 2); - } - } - - return this.unitValueExpressionParser.parseExpression(modifiedExpression); + + return this.unitValueExpressionParser + .parseExpression(formatExpression(expression)); } - + /** * Gets a unit dimension from the database using its name. * @@ -1627,7 +1598,7 @@ public final class UnitDatabase { else return dimension; } - + /** * Uses the database's data to parse an expression into a unit dimension *

@@ -1651,24 +1622,14 @@ public final class UnitDatabase { 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; - - // format expression - for (final Entry replacement : EXPRESSION_REPLACEMENTS - .entrySet()) { - modifiedExpression = replacement.getKey().matcher(modifiedExpression) - .replaceAll(replacement.getValue()); - } - - return this.unitDimensionParser.parseExpression(modifiedExpression); + + return this.unitDimensionParser.parseExpression(formatExpression(expression)); } - + /** * Gets a unit. If it is linear, cast it to a LinearUnit and return it. * Otherwise, throw an {@code IllegalArgumentException}. @@ -1687,7 +1648,7 @@ public final class UnitDatabase { 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( @@ -1696,7 +1657,7 @@ public final class UnitDatabase { } else { // get a linear unit final Unit unit = this.getUnit(name); - + if (unit instanceof LinearUnit) return (LinearUnit) unit; else @@ -1704,7 +1665,7 @@ public final class UnitDatabase { String.format("%s is not a linear unit.", name)); } } - + /** * Gets a {@code LinearUnitValue} from a unit name. Nonlinear units will be * converted to their base units. @@ -1723,7 +1684,7 @@ public final class UnitDatabase { return LinearUnitValue.getExact(this.getLinearUnit(name), 1); } } - + /** * Gets a unit prefix from the database from its name * @@ -1744,7 +1705,7 @@ public final class UnitDatabase { return prefix; } } - + /** * Gets all of the prefixes that are on a unit name, in application order. * @@ -1756,12 +1717,12 @@ public final class UnitDatabase { List getPrefixesFromName(final String unitName) { final List prefixes = new ArrayList<>(); String name = unitName; - + while (!this.prefixlessUnits.containsKey(name)) { // find the longest prefix String longestPrefixName = null; int longestLength = name.length(); - + while (longestPrefixName == null) { longestLength--; if (longestLength <= 0) @@ -1771,7 +1732,7 @@ public final class UnitDatabase { longestPrefixName = name.substring(0, longestLength); } } - + // longest prefix found! final UnitPrefix prefix = this.getPrefix(longestPrefixName); prefixes.add(0, prefix); @@ -1779,7 +1740,7 @@ public final class UnitDatabase { } return prefixes; } - + /** * Gets a unit prefix from a prefix expression *

@@ -1796,24 +1757,14 @@ public final class UnitDatabase { */ 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; - - // format expression - for (final Entry replacement : EXPRESSION_REPLACEMENTS - .entrySet()) { - modifiedExpression = replacement.getKey().matcher(modifiedExpression) - .replaceAll(replacement.getValue()); - } - - return this.prefixExpressionParser.parseExpression(modifiedExpression); + + return this.prefixExpressionParser.parseExpression(formatExpression(expression)); } - + /** * @return the prefixRepetitionRule * @since 2020-08-26 @@ -1822,7 +1773,7 @@ public final class UnitDatabase { public final Predicate> getPrefixRepetitionRule() { return this.prefixRepetitionRule; } - + /** * Gets a unit from the database from its name, looking for prefixes. * @@ -1855,9 +1806,9 @@ public final class UnitDatabase { } else return unit; } - + } - + /** * Uses the database's unit data to parse an expression into a unit *

@@ -1882,38 +1833,15 @@ public final class UnitDatabase { */ 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("-", " - "); - - // format expression - for (final Entry replacement : EXPRESSION_REPLACEMENTS - .entrySet()) { - modifiedExpression = replacement.getKey().matcher(modifiedExpression) - .replaceAll(replacement.getValue()); - } - - // the previous operation breaks negative numbers, fix them! - // (i.e. -2 becomes - 2) - for (int i = 0; i < modifiedExpression.length(); i++) { - if (modifiedExpression.charAt(i) == '-' - && (i < 2 || 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); + + return this.unitExpressionParser + .parseExpression(formatExpression(expression)); } - + /** * Get a unit set from its name, throwing a {@link NoSuchElementException} if * there is none. @@ -1930,7 +1858,7 @@ public final class UnitDatabase { throw new NoSuchElementException("No unit set with name " + name); return unitSet; } - + /** * Parses a semicolon-separated expression to get the unit set being used. * @@ -1942,7 +1870,7 @@ public final class UnitDatabase { final List units = new ArrayList<>(parts.length); for (final String unitName : parts) { final Unit unit = this.getUnitFromExpression(unitName.trim()); - + if (!(unit instanceof LinearUnit)) { throw new IllegalArgumentException(String.format( "Unit '%s' is in a unit-set expression, but is not linear.", @@ -1957,7 +1885,7 @@ public final class UnitDatabase { } return units; } - + /** * Adds all dimensions from a file, using data from the database to parse * them. @@ -2003,7 +1931,7 @@ public final class UnitDatabase { } return errors; } - + /** * Adds all dimensions from a {@code InputStream}. Otherwise, works like * {@link #loadDimensionFile}. @@ -2030,7 +1958,7 @@ public final class UnitDatabase { } return errors; } - + /** * Adds all units from a file, using data from the database to parse them. *

@@ -2075,7 +2003,7 @@ public final class UnitDatabase { } return errors; } - + /** * Adds all units from a {@code InputStream}. Otherwise, works like * {@link #loadUnitsFile}. @@ -2101,7 +2029,7 @@ public final class UnitDatabase { } return errors; } - + /** * @param includeDuplicates if false, duplicates are removed from the map * @return a map mapping prefix names to prefixes @@ -2116,7 +2044,7 @@ public final class UnitDatabase { .conditionalExistenceMap(this.prefixes, entry -> !isRemovableDuplicate(this.prefixes, entry))); } - + /** * @param prefixRepetitionRule the prefixRepetitionRule to set * @since 2020-08-26 @@ -2126,7 +2054,7 @@ public final class UnitDatabase { Predicate> prefixRepetitionRule) { this.prefixRepetitionRule = prefixRepetitionRule; } - + /** * @return a string stating the number of units, prefixes and dimensions in * the database @@ -2138,7 +2066,7 @@ public final class UnitDatabase { this.prefixlessUnits.size(), this.prefixes.size(), this.dimensions.size()); } - + /** * Returns a map mapping unit names to units, including units with prefixes. *

@@ -2170,7 +2098,7 @@ public final class UnitDatabase { return this.units; // PrefixedUnitMap is immutable so I don't need to make // an unmodifiable map. } - + /** * @param includeDuplicates if true, duplicate units will all exist in the * map; if false, only one of each unit will exist, @@ -2188,7 +2116,7 @@ public final class UnitDatabase { entry -> !isRemovableDuplicate(this.prefixlessUnits, entry))); } - + /** * @return an unmodifiable map mapping names to unit sets * @since 2024-08-16 diff --git a/src/main/java/sevenUnits/utils/ExpressionParser.java b/src/main/java/sevenUnits/utils/ExpressionParser.java index 1c8df9f..051082d 100644 --- a/src/main/java/sevenUnits/utils/ExpressionParser.java +++ b/src/main/java/sevenUnits/utils/ExpressionParser.java @@ -578,7 +578,8 @@ public final class ExpressionParser { * @since 2019-03-17 * @since v0.2.0 */ - String convertExpressionToReversePolish(final String expression) { + // TODO revert to package private + public String convertExpressionToReversePolish(final String expression) { Objects.requireNonNull(expression, "expression must not be null."); final List components = new ArrayList<>(); diff --git a/src/test/java/sevenUnits/unit/UnitDatabaseTest.java b/src/test/java/sevenUnits/unit/UnitDatabaseTest.java index c78837f..800d13d 100644 --- a/src/test/java/sevenUnits/unit/UnitDatabaseTest.java +++ b/src/test/java/sevenUnits/unit/UnitDatabaseTest.java @@ -190,14 +190,35 @@ class UnitDatabaseTest { } private static final Stream testEvaluateExpressionValid() { + UncertainDouble uncertainTwoThirds = UncertainDouble.of(2.0, 1.0) + .dividedBy(UncertainDouble.of(3.0, 1.0)); return Stream.of( Arguments.of("J + (2 * 3) J + (20 / 4) J", LinearUnitValue.of(J, UncertainDouble.of(12, Math.sqrt(14.625)))), + Arguments.of("J + 2 * 3 * J + 20 / 4 * J", + LinearUnitValue.of(J, + UncertainDouble.of(12, Math.sqrt(14.625)))), Arguments.of("J - -1 * J", LinearUnitValue.of(J, UncertainDouble.of(2, 1))), Arguments.of("K^2", - LinearUnitValue.of(K.times(K), UncertainDouble.of(1, 0)))); + LinearUnitValue.of(K.times(K), UncertainDouble.of(1, 0))), + Arguments.of("2 J / 3 J", + LinearUnitValue.of(J.dividedBy(J), uncertainTwoThirds))); + } + + private static final Stream testFormatExpression() { + return Stream.of( + Arguments.of("1*2", "1 * 2"), + Arguments.of("1/2", "1 / 2"), + Arguments.of("1|2", "1 | 2"), + Arguments.of("1^2", "1 ^ 2"), + Arguments.of("1 * 2", "1 * 2"), + Arguments.of("+1", "+1"), + Arguments.of("-1", "-1"), + Arguments.of("1.1e+5", "1.1e+5"), + Arguments.of("1.25e-5", "1.25e-5") + ); } /** @@ -253,6 +274,12 @@ class UnitDatabaseTest { final var actualU = database.getUnitFromExpression(expression); assertEquals(expectedU, actualU); } + + @ParameterizedTest + @MethodSource + public void testFormatExpression(String expression, String expected) { + assertEquals(expected, UnitDatabase.formatExpression(expression)); + } /** * Test for {@link UnitDatabase#getUnit}, {@link UnitDatabase#getLinearUnit} -- cgit v1.2.3