From b20cd4223b4ffc03e334627a82ca4eff9738912c Mon Sep 17 00:00:00 2001
From: Adrien Hopkins
Date: Sat, 16 Mar 2019 14:55:07 -0400
Subject: Moved project to Maven.
---
src/org/unitConverter/UnitsDatabase.java | 641 +++++++++++++++++++++++++++++++
1 file changed, 641 insertions(+)
create mode 100755 src/org/unitConverter/UnitsDatabase.java
(limited to 'src/org/unitConverter/UnitsDatabase.java')
diff --git a/src/org/unitConverter/UnitsDatabase.java b/src/org/unitConverter/UnitsDatabase.java
new file mode 100755
index 0000000..4d41735
--- /dev/null
+++ b/src/org/unitConverter/UnitsDatabase.java
@@ -0,0 +1,641 @@
+/**
+ * Copyright (C) 2018 Adrien Hopkins
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.unitConverter;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileReader;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+import org.unitConverter.dimension.UnitDimension;
+import org.unitConverter.unit.AbstractUnit;
+import org.unitConverter.unit.BaseUnit;
+import org.unitConverter.unit.DefaultUnitPrefix;
+import org.unitConverter.unit.LinearUnit;
+import org.unitConverter.unit.SI;
+import org.unitConverter.unit.Unit;
+import org.unitConverter.unit.UnitPrefix;
+
+/**
+ * A database of units and prefixes, and their names.
+ *
+ * @author Adrien Hopkins
+ * @since 2019-01-07
+ * @since v0.1.0
+ */
+public final class UnitsDatabase {
+ /**
+ * The units in this system.
+ *
+ * @since 2019-01-07
+ * @since v0.1.0
+ */
+ private final Map units;
+
+ /**
+ * The unit prefixes in this system.
+ *
+ * @since 2019-01-14
+ * @since v0.1.0
+ */
+ private final Map prefixes;
+
+ /**
+ * The dimensions in this system.
+ *
+ * @since 2019-03-14
+ */
+ private final Map dimensions;
+
+ /**
+ * Creates the {@code UnitsDatabase}.
+ *
+ * @since 2019-01-10
+ * @since v0.1.0
+ */
+ public UnitsDatabase() {
+ this.units = new HashMap<>();
+ this.prefixes = new HashMap<>();
+ this.dimensions = new HashMap<>();
+ }
+
+ /**
+ * Adds all units from a file, using data from the database to parse them.
+ *
+ * Each line in the file should consist of a name and an expression (parsed by getUnitFromExpression) separated by
+ * any number of tab characters.
+ *
+ *
+ * Allowed exceptions:
+ *
+ * - Any line that begins with the '#' character is considered a comment and ignored.
+ * - Blank lines are also ignored
+ * - If an expression consists of a single exclamation point, instead of parsing it, this method will search the
+ * database for an existing unit. If no unit is found, an IllegalArgumentException is thrown. This is used to define
+ * initial units and ensure that the database contains them.
+ *
+ *
+ * @param file
+ * file to read
+ * @throws IllegalArgumentException
+ * if the file cannot be parsed, found or read
+ * @throws NullPointerException
+ * if file is null
+ * @since 2019-01-13
+ * @since v0.1.0
+ */
+ public void addAllFromFile(final File file) {
+ Objects.requireNonNull(file, "file must not be null.");
+ try (FileReader fileReader = new FileReader(file); BufferedReader reader = new BufferedReader(fileReader)) {
+ // while the reader has lines to read, read a line, then parse it, then add it
+ long lineCounter = 0;
+ while (reader.ready()) {
+ final String line = reader.readLine();
+ lineCounter++;
+
+ // ignore lines that start with a # sign - they're comments
+ if (line.startsWith("#") || line.isEmpty()) {
+ continue;
+ }
+
+ // divide line into name and expression
+ final String[] parts = line.split("\t");
+ if (parts.length < 2)
+ throw new IllegalArgumentException(String.format(
+ "Lines must consist of a unit name and its definition, separated by tab(s) (line %d).",
+ lineCounter));
+ final String name = parts[0];
+ final String expression = parts[parts.length - 1];
+
+ if (name.endsWith(" ")) {
+ System.err.printf("Warning - line %d's unit name ends in a space", lineCounter);
+ }
+
+ // if expression is "!", search for an existing unit
+ // if no unit found, throw an error
+ if (expression.equals("!")) {
+ if (!this.containsUnitName(name))
+ throw new IllegalArgumentException(
+ String.format("! used but no unit found (line %d).", lineCounter));
+ } else {
+ if (name.endsWith("-")) {
+ final UnitPrefix prefix;
+ try {
+ prefix = this.getPrefixFromExpression(expression);
+ } catch (final IllegalArgumentException e) {
+ System.err.printf("Parsing error on line %d:%n", lineCounter);
+ throw e;
+ }
+ this.addPrefix(name.substring(0, name.length() - 1), prefix);
+ } else {
+ // it's a unit, get the unit
+ final Unit unit;
+ try {
+ unit = this.getUnitFromExpression(expression);
+ } catch (final IllegalArgumentException e) {
+ System.err.printf("Parsing error on line %d:%n", lineCounter);
+ throw e;
+ }
+ AbstractUnit.incrementUnitCounter();
+ if (unit instanceof BaseUnit) {
+ AbstractUnit.incrementBaseUnitCounter();
+ }
+ this.addUnit(name, unit);
+ }
+ }
+ }
+ } catch (final FileNotFoundException e) {
+ throw new IllegalArgumentException("Could not find file " + file, e);
+ } catch (final IOException e) {
+ throw new IllegalArgumentException("Could not read file " + file, e);
+ }
+ }
+
+ /**
+ * Adds a unit dimension to the database.
+ *
+ * @param name
+ * dimension's name
+ * @param dimension
+ * dimension to add
+ * @throws NullPointerException
+ * if name or dimension is null
+ * @since 2019-03-14
+ */
+ public void addDimension(final String name, final UnitDimension dimension) {
+ this.dimensions.put(Objects.requireNonNull(name, "name must not be null."),
+ Objects.requireNonNull(dimension, "dimension must not be null."));
+ }
+
+ /**
+ * Adds a unit prefix to the database.
+ *
+ * @param name
+ * prefix's name
+ * @param prefix
+ * prefix to add
+ * @throws NullPointerException
+ * if name or prefix is null
+ * @since 2019-01-14
+ * @since v0.1.0
+ */
+ public void addPrefix(final String name, final UnitPrefix prefix) {
+ this.prefixes.put(Objects.requireNonNull(name, "name must not be null."),
+ Objects.requireNonNull(prefix, "prefix must not be null."));
+ }
+
+ /**
+ * Adds a unit to the database.
+ *
+ * @param name
+ * unit's name
+ * @param unit
+ * unit to add
+ * @throws NullPointerException
+ * if unit is null
+ * @since 2019-01-10
+ * @since v0.1.0
+ */
+ public void addUnit(final String name, final Unit unit) {
+ this.units.put(Objects.requireNonNull(name, "name must not be null."),
+ Objects.requireNonNull(unit, "unit must not be null."));
+ }
+
+ /**
+ * Tests if the database has a unit dimension with this name.
+ *
+ * @param name
+ * name to test
+ * @return if database contains name
+ * @since 2019-03-14
+ */
+ public boolean containsDimensionName(final String name) {
+ return this.dimensions.containsKey(name);
+ }
+
+ /**
+ * Tests if the database has a unit with this name, ignoring prefixes
+ *
+ * @param name
+ * name to test
+ * @return if database contains name
+ * @since 2019-01-13
+ * @since v0.1.0
+ */
+ public boolean containsPrefixlessUnitName(final String name) {
+ return this.units.containsKey(name);
+ }
+
+ /**
+ * Tests if the database has a unit prefix with this name.
+ *
+ * @param name
+ * name to test
+ * @return if database contains name
+ * @since 2019-01-13
+ * @since v0.1.0
+ */
+ public boolean containsPrefixName(final String name) {
+ return this.prefixes.containsKey(name);
+ }
+
+ /**
+ * Tests if the database has a unit with this name, taking prefixes into consideration
+ *
+ * @param name
+ * name to test
+ * @return if database contains name
+ * @since 2019-01-13
+ * @since v0.1.0
+ */
+ public boolean containsUnitName(final String name) {
+ // check for prefixes
+ for (final String prefixName : this.prefixNameSet()) {
+ if (name.startsWith(prefixName))
+ if (this.containsUnitName(name.substring(prefixName.length())))
+ return true;
+ }
+ return this.units.containsKey(name);
+ }
+
+ /**
+ * @return an immutable set of all of the dimension names in this database.
+ * @since 2019-03-14
+ */
+ public Set dimensionNameSet() {
+ return Collections.unmodifiableSet(this.dimensions.keySet());
+ }
+
+ /**
+ * Gets a unit dimension from the database using its name.
+ *
+ * @param name
+ * dimension's name
+ * @return dimension
+ * @since 2019-03-14
+ */
+ public UnitDimension getDimension(final String name) {
+ return this.dimensions.get(name);
+ }
+
+ /**
+ * Gets a unit prefix from the database from its name
+ *
+ * @param name
+ * prefix's name
+ * @return prefix
+ * @since 2019-01-10
+ * @since v0.1.0
+ */
+ public UnitPrefix getPrefix(final String name) {
+ return this.prefixes.get(name);
+ }
+
+ /**
+ * Gets a unit prefix from a prefix expression
+ *
+ * Currently, prefix expressions are much simpler than unit expressions: They are either a number or the name of
+ * another prefix
+ *
+ *
+ * @param expression
+ * expression to input
+ * @return prefix
+ * @throws IllegalArgumentException
+ * if expression cannot be parsed
+ * @throws NullPointerException
+ * if any argument is null
+ * @since 2019-01-14
+ * @since v0.1.0
+ */
+ public UnitPrefix getPrefixFromExpression(final String expression) {
+ Objects.requireNonNull(expression, "expression must not be null.");
+
+ try {
+ return new DefaultUnitPrefix(Double.parseDouble(expression));
+ } catch (final NumberFormatException e) {
+ if (expression.contains("^")) {
+ final String[] baseAndExponent = expression.split("\\^");
+
+ final double base;
+ try {
+ base = Double.parseDouble(baseAndExponent[0]);
+ } catch (final NumberFormatException e2) {
+ throw new IllegalArgumentException("Base of exponientation must be a number.");
+ }
+
+ final int exponent;
+ try {
+ exponent = Integer.parseInt(baseAndExponent[baseAndExponent.length - 1]);
+ } catch (final NumberFormatException e2) {
+ throw new IllegalArgumentException("Exponent must be an integer.");
+ }
+
+ return new DefaultUnitPrefix(Math.pow(base, exponent));
+ } else {
+ if (!this.containsPrefixName(expression))
+ throw new IllegalArgumentException("Unrecognized prefix name \"" + expression + "\".");
+ return this.getPrefix(expression);
+ }
+ }
+ }
+
+ /**
+ * Gets a unit from the database from its name, ignoring prefixes.
+ *
+ * @param name
+ * unit's name
+ * @return unit
+ * @since 2019-01-10
+ * @since v0.1.0
+ */
+ public Unit getPrefixlessUnit(final String name) {
+ return this.units.get(name);
+ }
+
+ /**
+ * Gets a unit from the database from its name, looking for prefixes.
+ *
+ * @param name
+ * unit's name
+ * @return unit
+ * @since 2019-01-10
+ * @since v0.1.0
+ */
+ public Unit getUnit(final String name) {
+ if (name.contains("^")) {
+ final String[] baseAndExponent = name.split("\\^");
+
+ LinearUnit base;
+ try {
+ base = SI.SI.getBaseUnit(UnitDimension.EMPTY).times(Double.parseDouble(baseAndExponent[0]));
+ } catch (final NumberFormatException e2) {
+ final Unit unit = this.getUnit(baseAndExponent[0]);
+ if (unit instanceof LinearUnit) {
+ base = (LinearUnit) unit;
+ } else if (unit instanceof BaseUnit) {
+ base = ((BaseUnit) unit).asLinearUnit();
+ } else
+ throw new IllegalArgumentException("Base of exponientation must be a linear or base unit.");
+ }
+
+ final int exponent;
+ try {
+ exponent = Integer.parseInt(baseAndExponent[baseAndExponent.length - 1]);
+ } catch (final NumberFormatException e2) {
+ throw new IllegalArgumentException("Exponent must be an integer.");
+ }
+
+ final LinearUnit exponentiated = base.toExponent(exponent);
+ if (exponentiated.getConversionFactor() == 1)
+ return exponentiated.getSystem().getBaseUnit(exponentiated.getDimension());
+ else
+ return exponentiated;
+ } else {
+ for (final String prefixName : this.prefixNameSet()) {
+ // check for a prefix
+ if (name.startsWith(prefixName)) {
+ // prefix found! Make sure what comes after it is actually a unit!
+ final String prefixless = name.substring(prefixName.length());
+ if (this.containsUnitName(prefixless)) {
+ // yep, it's a proper prefix! Get the unit!
+ final Unit unit = this.getUnit(prefixless);
+ final UnitPrefix prefix = this.getPrefix(prefixName);
+
+ // Prefixes only work with linear and base units, so make sure it's one of those
+ if (unit instanceof LinearUnit) {
+ final LinearUnit linearUnit = (LinearUnit) unit;
+ return linearUnit.times(prefix.getMultiplier());
+ } else if (unit instanceof BaseUnit) {
+ final BaseUnit baseUnit = (BaseUnit) unit;
+ return baseUnit.times(prefix.getMultiplier());
+ }
+ }
+ }
+ }
+ return this.units.get(name);
+ }
+ }
+
+ /**
+ * Uses the database's unit data to parse an expression into a unit
+ *
+ * The expression is a series of any of the following:
+ *
+ * - The name of a unit, which multiplies or divides the result based on preceding operators
+ * - The operators '*' and '/', which multiply and divide (note that just putting two units or values next to each
+ * other is equivalent to multiplication)
+ * - The operator '^' which exponentiates. Exponents must be integers.
+ * - A number which is multiplied or divided
+ *
+ * This method only works with linear units.
+ *
+ * @param expression
+ * expression to parse
+ * @throws IllegalArgumentException
+ * if the expression cannot be parsed
+ * @throws NullPointerException
+ * if any argument is null
+ * @since 2019-01-07
+ * @since v0.1.0
+ */
+ public Unit getUnitFromExpression(final String expression) {
+ Objects.requireNonNull(expression, "expression must not be null.");
+
+ // parse the expression
+ // start with an "empty" unit then apply operations on it
+ LinearUnit unit = SI.SI.getBaseUnit(UnitDimension.EMPTY).asLinearUnit();
+ boolean dividing = false;
+
+ // if I'm just creating an alias, just create one instead of going through the parsing process
+ if (!expression.contains(" ") && !expression.contains("*") && !expression.contains("/")
+ && !expression.contains("(") && !expression.contains(")") && !expression.contains("^")) {
+ try {
+ return SI.SI.getBaseUnit(UnitDimension.EMPTY).times(Double.parseDouble(expression));
+ } catch (final NumberFormatException e) {
+ if (!this.containsUnitName(expression))
+ throw new IllegalArgumentException("Unrecognized unit name \"" + expression + "\".");
+ return this.getUnit(expression);
+ }
+ }
+
+ // \\* means "asterisk", * is reserved
+ for (final String part : expression.replaceAll("\\*", " \\* ").replaceAll("/", " / ").replaceAll(" \\^", "\\^")
+ .replaceAll("\\^ ", "\\^").split(" ")) {
+ if ("".equals(part)) {
+ continue;
+ }
+ // "unit1 unit2" is the same as "unit1 * unit2", so multiplication signs do nothing
+ if ("*".equals(part)) {
+ continue;
+ }
+ // When I reach a division sign, don't parse a unit, instead tell myself I'm going to divide the next
+ // thing
+ if ("/".equals(part) || "per".equals(part)) {
+ dividing = true;
+ continue;
+ }
+
+ try {
+ final double partAsNumber = Double.parseDouble(part); // if this works, it's a number
+ // this code should not throw any exceptions, so I'm going to throw AssertionErrors if it does
+ try {
+ if (dividing) {
+ unit = unit.dividedBy(partAsNumber);
+ dividing = false;
+ } else {
+ unit = unit.times(partAsNumber);
+ }
+ } catch (final Exception e) {
+ throw new AssertionError(e);
+ }
+ } catch (final NumberFormatException e) {
+ // it's a unit, try that
+
+ if (part.contains("(") && part.endsWith(")")) {
+ // the unitsfile is looking for a nonlinear unit
+ final String[] unitAndValue = part.split("\\(");
+
+ // this will work because I've checked that it contains a (
+ final String unitName = unitAndValue[0];
+ final String valueStringWithBracket = unitAndValue[unitAndValue.length - 1];
+ final String valueString = valueStringWithBracket.substring(0, valueStringWithBracket.length() - 1);
+ final double value;
+
+ // try to get the value - else throw an error
+ try {
+ value = Double.parseDouble(valueString);
+ } catch (final NumberFormatException e2) {
+ throw new IllegalArgumentException("Unparseable value " + valueString);
+ }
+
+ // get this unit in a linear form
+ if (!this.containsPrefixlessUnitName(unitName))
+ throw new IllegalArgumentException("Unrecognized unit name \"" + part + "\".");
+ final Unit partUnit = this.getPrefixlessUnit(unitName);
+ final LinearUnit multiplier = partUnit.getBase().times(partUnit.convertToBase(value));
+
+ // finally, add it to the expression
+ if (dividing) {
+ unit = unit.dividedBy(multiplier);
+ dividing = false;
+ } else {
+ unit = unit.times(multiplier);
+ }
+ } else {
+ // check for exponientation
+ if (part.contains("^")) {
+ final String[] valueAndExponent = part.split("\\^");
+ // this will always work because of the contains check
+ final String valueString = valueAndExponent[0];
+ final String exponentString = valueAndExponent[valueAndExponent.length - 1];
+
+ LinearUnit value;
+
+ // first, try to get the value
+ try {
+ final double valueAsNumber = Double.parseDouble(valueString);
+
+ value = SI.SI.getBaseUnit(UnitDimension.EMPTY).times(valueAsNumber);
+ } catch (final NumberFormatException e2) {
+
+ // look for a unit
+ if (!this.containsUnitName(valueString))
+ throw new IllegalArgumentException("Unrecognized unit name \"" + valueString + "\".");
+ final Unit valueUnit = this.getUnit(valueString);
+
+ // try to turn the value into a linear unit
+ if (valueUnit instanceof LinearUnit) {
+ value = (LinearUnit) valueUnit;
+ } else if (valueUnit instanceof BaseUnit) {
+ value = ((BaseUnit) valueUnit).asLinearUnit();
+ } else
+ throw new IllegalArgumentException("Only linear and base units can be exponientated.");
+ }
+
+ // now, try to get the exponent
+ final int exponent;
+ try {
+ exponent = Integer.parseInt(exponentString);
+ } catch (final NumberFormatException e2) {
+ throw new IllegalArgumentException("Exponents must be integers.");
+ }
+
+ final LinearUnit exponientated = value.toExponent(exponent);
+
+ if (dividing) {
+ unit = unit.dividedBy(exponientated);
+ dividing = false;
+ } else {
+ unit = unit.times(exponientated);
+ }
+ } else {
+ // no exponent - look for a unit
+ // the unitsfile is looking for a linear unit
+ if (!this.containsUnitName(part))
+ throw new IllegalArgumentException("Unrecognized unit name \"" + part + "\".");
+ Unit other = this.getUnit(part);
+ if (other instanceof BaseUnit) {
+ other = ((BaseUnit) other).asLinearUnit();
+ }
+ if (other instanceof LinearUnit) {
+ if (dividing) {
+ unit = unit.dividedBy((LinearUnit) other);
+ dividing = false;
+ } else {
+ unit = unit.times((LinearUnit) other);
+ }
+ } else
+ throw new IllegalArgumentException(
+ "Only linear or base units can be multiplied and divided. If you want to use a non-linear unit, try the format UNITNAME(VALUE).");
+ }
+ }
+ }
+ }
+
+ // replace conversion-factor-1 linear units with base units
+ // this improves the autogenerated names, allowing them to use derived SI names
+ if (unit != null && unit.getConversionFactor() == 1)
+ return unit.getSystem().getBaseUnit(unit.getDimension());
+ else
+ return unit;
+ }
+
+ /**
+ * @return an immutable set of all of the unit names in this database, ignoring prefixes
+ * @since 2019-01-14
+ * @since v0.1.0
+ */
+ public Set prefixlessUnitNameSet() {
+ return Collections.unmodifiableSet(this.units.keySet());
+ }
+
+ /**
+ * @return an immutable set of all of the prefix names in this database
+ * @since 2019-01-14
+ * @since v0.1.0
+ */
+ public Set prefixNameSet() {
+ return Collections.unmodifiableSet(this.prefixes.keySet());
+ }
+}
--
cgit v1.2.3
From 943496888d18b031be19ba8e7348ec188dc8eb6b Mon Sep 17 00:00:00 2001
From: Adrien Hopkins
Date: Fri, 22 Mar 2019 17:00:58 -0400
Subject: Made BaseUnit a subclass of LinearUnit and made an expression parser
---
CHANGELOG.org | 1 +
src/org/unitConverter/UnitsDatabase.java | 11 +-
.../expressionParser/ExpressionParser.java | 398 -------------
.../expressionParser/package-info.java | 23 -
src/org/unitConverter/math/DecimalComparison.java | 107 ++++
src/org/unitConverter/math/ExpressionParser.java | 627 +++++++++++++++++++++
src/org/unitConverter/math/package-info.java | 23 +
src/org/unitConverter/unit/BaseUnit.java | 151 +----
src/org/unitConverter/unit/LinearUnit.java | 164 ++++--
src/org/unitConverter/unit/OperatableUnit.java | 169 ------
src/test/java/ExpressionParserTest.java | 50 ++
src/test/java/UnitTest.java | 66 +++
12 files changed, 1009 insertions(+), 781 deletions(-)
delete mode 100644 src/org/unitConverter/expressionParser/ExpressionParser.java
delete mode 100644 src/org/unitConverter/expressionParser/package-info.java
create mode 100644 src/org/unitConverter/math/DecimalComparison.java
create mode 100644 src/org/unitConverter/math/ExpressionParser.java
create mode 100644 src/org/unitConverter/math/package-info.java
delete mode 100644 src/org/unitConverter/unit/OperatableUnit.java
create mode 100644 src/test/java/ExpressionParserTest.java
(limited to 'src/org/unitConverter/UnitsDatabase.java')
diff --git a/CHANGELOG.org b/CHANGELOG.org
index 87e26e0..5baf980 100644
--- a/CHANGELOG.org
+++ b/CHANGELOG.org
@@ -5,6 +5,7 @@ All notable changes in this project will be shown in this file.
*** Changed
- Moved project to Maven
- Downgraded JUnit to 4.11
+ - BaseUnit is now a subclass of LinearUnit
*** Added
- GUI for a selection-based unit converter
- The UnitDatabase now stores dimensions.
diff --git a/src/org/unitConverter/UnitsDatabase.java b/src/org/unitConverter/UnitsDatabase.java
index 4d41735..3af1c8d 100755
--- a/src/org/unitConverter/UnitsDatabase.java
+++ b/src/org/unitConverter/UnitsDatabase.java
@@ -393,8 +393,6 @@ public final class UnitsDatabase {
final Unit unit = this.getUnit(baseAndExponent[0]);
if (unit instanceof LinearUnit) {
base = (LinearUnit) unit;
- } else if (unit instanceof BaseUnit) {
- base = ((BaseUnit) unit).asLinearUnit();
} else
throw new IllegalArgumentException("Base of exponientation must be a linear or base unit.");
}
@@ -464,7 +462,7 @@ public final class UnitsDatabase {
// parse the expression
// start with an "empty" unit then apply operations on it
- LinearUnit unit = SI.SI.getBaseUnit(UnitDimension.EMPTY).asLinearUnit();
+ LinearUnit unit = SI.SI.getBaseUnit(UnitDimension.EMPTY);
boolean dividing = false;
// if I'm just creating an alias, just create one instead of going through the parsing process
@@ -567,8 +565,6 @@ public final class UnitsDatabase {
// try to turn the value into a linear unit
if (valueUnit instanceof LinearUnit) {
value = (LinearUnit) valueUnit;
- } else if (valueUnit instanceof BaseUnit) {
- value = ((BaseUnit) valueUnit).asLinearUnit();
} else
throw new IllegalArgumentException("Only linear and base units can be exponientated.");
}
@@ -594,10 +590,7 @@ public final class UnitsDatabase {
// the unitsfile is looking for a linear unit
if (!this.containsUnitName(part))
throw new IllegalArgumentException("Unrecognized unit name \"" + part + "\".");
- Unit other = this.getUnit(part);
- if (other instanceof BaseUnit) {
- other = ((BaseUnit) other).asLinearUnit();
- }
+ final Unit other = this.getUnit(part);
if (other instanceof LinearUnit) {
if (dividing) {
unit = unit.dividedBy((LinearUnit) other);
diff --git a/src/org/unitConverter/expressionParser/ExpressionParser.java b/src/org/unitConverter/expressionParser/ExpressionParser.java
deleted file mode 100644
index 804ea87..0000000
--- a/src/org/unitConverter/expressionParser/ExpressionParser.java
+++ /dev/null
@@ -1,398 +0,0 @@
-/**
- * Copyright (C) 2019 Adrien Hopkins
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see .
- */
-package org.unitConverter.expressionParser;
-
-import java.util.ArrayDeque;
-import java.util.Deque;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Objects;
-import java.util.function.BinaryOperator;
-import java.util.function.Function;
-import java.util.function.UnaryOperator;
-
-/**
- * An object that can parse expressions with unary or binary operators.
- *
- * @author Adrien Hopkins
- * @param
- * type of object that exists in parsed expressions
- * @since 2019-03-14
- */
-// TODO: possibly make this class non-final?
-public final class ExpressionParser {
- /**
- * A builder that can create {@code ExpressionParser} instances.
- *
- * @author Adrien Hopkins
- * @param
- * type of object that exists in parsed expressions
- * @since 2019-03-17
- */
- public static final class Builder {
- /**
- * A function that obtains a parseable object from a string. For example, an integer {@code ExpressionParser}
- * would use {@code Integer::parseInt}.
- *
- * @since 2019-03-14
- */
- private final Function objectObtainer;
-
- /**
- * A map mapping operator strings to operator functions, for unary operators.
- *
- * @since 2019-03-14
- */
- private final Map> unaryOperators;
-
- /**
- * A map mapping operator strings to operator functions, for binary operators.
- *
- * @since 2019-03-14
- */
- private final Map> binaryOperators;
-
- /**
- * Creates the {@code Builder}.
- *
- * @param objectObtainer
- * a function that can turn strings into objects of the type handled by the parser.
- * @throws NullPointerException
- * if {@code objectObtainer} is null
- * @since 2019-03-17
- */
- public Builder(final Function objectObtainer) {
- this.objectObtainer = Objects.requireNonNull(objectObtainer, "objectObtainer must not be null.");
- this.unaryOperators = new HashMap<>();
- this.binaryOperators = new HashMap<>();
- }
-
- /**
- * Adds a binary operator to the builder.
- *
- * @param text
- * text used to reference the operator, like '+'
- * @param operator
- * operator to add
- * @param priority
- * operator's priority, which determines which operators are applied first
- * @return this builder
- * @throws NullPointerException
- * if {@code text} or {@code operator} is null
- * @since 2019-03-17
- */
- public Builder addBinaryOperator(final String text, final BinaryOperator operator, final int priority) {
- Objects.requireNonNull(text, "text must not be null.");
- Objects.requireNonNull(operator, "operator must not be null.");
-
- // Unfortunately, I cannot use a lambda because the PriorityBinaryOperator requires arguments.
- final PriorityBinaryOperator priorityOperator = new PriorityBinaryOperator(priority) {
- @Override
- public T apply(final T t, final T u) {
- return operator.apply(t, u);
- }
-
- };
- this.binaryOperators.put(text, priorityOperator);
- return this;
- }
-
- /**
- * Adds a unary operator to the builder.
- *
- * @param text
- * text used to reference the operator, like '-'
- * @param operator
- * operator to add
- * @param priority
- * operator's priority, which determines which operators are applied first
- * @return this builder
- * @throws NullPointerException
- * if {@code text} or {@code operator} is null
- * @since 2019-03-17
- */
- public Builder addUnaryOperator(final String text, final UnaryOperator operator, final int priority) {
- Objects.requireNonNull(text, "text must not be null.");
- Objects.requireNonNull(operator, "operator must not be null.");
-
- // Unfortunately, I cannot use a lambda because the PriorityUnaryOperator requires arguments.
- final PriorityUnaryOperator priorityOperator = new PriorityUnaryOperator(priority) {
- @Override
- public T apply(final T t) {
- return operator.apply(t);
- }
- };
- this.unaryOperators.put(text, priorityOperator);
- return this;
- }
-
- /**
- * @return an {@code ExpressionParser} instance with the properties given to this builder
- * @since 2019-03-17
- */
- public ExpressionParser build() {
- return new ExpressionParser<>(this.objectObtainer, this.unaryOperators, this.binaryOperators);
- }
- }
-
- /**
- * A binary operator with a priority field that determines which operators apply first.
- *
- * @author Adrien Hopkins
- * @param
- * type of operand and result
- * @since 2019-03-17
- */
- private static abstract class PriorityBinaryOperator
- implements BinaryOperator, Comparable> {
- /**
- * The operator's priority. Higher-priority operators are applied before lower-priority operators
- */
- private final int priority;
-
- /**
- * Creates the {@code PriorityBinaryOperator}.
- *
- * @param priority
- * operator's priority
- * @since 2019-03-17
- */
- public PriorityBinaryOperator(final int priority) {
- this.priority = priority;
- }
-
- /**
- * Compares this object to another by priority.
- */
- @Override
- public int compareTo(final PriorityBinaryOperator o) {
- if (this.priority < o.priority)
- return -1;
- else if (this.priority > o.priority)
- return 1;
- else
- return 0;
- }
- }
-
- /**
- * A unary operator with a priority field that determines which operators apply first.
- *
- * @author Adrien Hopkins
- * @param
- * type of operand and result
- * @since 2019-03-17
- */
- private static abstract class PriorityUnaryOperator
- implements UnaryOperator, Comparable> {
- /**
- * The operator's priority. Higher-priority operators are applied before lower-priority operators
- */
- private final int priority;
-
- /**
- * Creates the {@code PriorityUnaryOperator}.
- *
- * @param priority
- * operator's priority
- * @since 2019-03-17
- */
- public PriorityUnaryOperator(final int priority) {
- this.priority = priority;
- }
-
- /**
- * Compares this object to another by priority.
- */
- @Override
- public int compareTo(final PriorityUnaryOperator o) {
- if (this.priority < o.priority)
- return -1;
- else if (this.priority > o.priority)
- return 1;
- else
- return 0;
- }
- }
-
- /**
- * The types of tokens that are available.
- *
- * @author Adrien Hopkins
- * @since 2019-03-14
- */
- private static enum TokenType {
- OBJECT, UNARY_OPERATOR, BINARY_OPERATOR;
- }
-
- /**
- * A function that obtains a parseable object from a string. For example, an integer {@code ExpressionParser} would
- * use {@code Integer::parseInt}.
- *
- * @since 2019-03-14
- */
- private final Function objectObtainer;
-
- /**
- * A map mapping operator strings to operator functions, for unary operators.
- *
- * @since 2019-03-14
- */
- private final Map> unaryOperators;
-
- /**
- * A map mapping operator strings to operator functions, for binary operators.
- *
- * @since 2019-03-14
- */
- private final Map> binaryOperators;
-
- /**
- * Creates the {@code ExpressionParser}.
- *
- * @param objectObtainer
- * function to get objects from strings
- * @param unaryOperators
- * operators available to the parser
- * @since 2019-03-14
- */
- private ExpressionParser(final Function objectObtainer,
- final Map> unaryOperators,
- final Map> binaryOperators) {
- this.objectObtainer = objectObtainer;
- this.unaryOperators = unaryOperators;
- this.binaryOperators = binaryOperators;
- }
-
- /**
- * Converts a given mathematical expression to reverse Polish notation (operators after operands).
- *
- * For example,
- * {@code 2 * (3 + 4)}
- * becomes
- * {@code 2 3 4 + *}.
- *
- * @param expression
- * @return
- * @since 2019-03-17
- */
- private String convertExpressionToReversePolish(final String expression) {
- Objects.requireNonNull(expression, "expression must not be null.");
-
- // TODO method stub org.unitConverter.expressionParser.ExpressionParser.convertExpressionToPolish(expression)
- throw new UnsupportedOperationException();
- }
-
- /**
- * Determines whether an inputted string is an object or an operator
- *
- * @param token
- * string to input
- * @return type of token it is
- * @throws NullPointerException
- * if {@code expression} is null
- * @since 2019-03-14
- */
- private TokenType getTokenType(final String token) {
- Objects.requireNonNull(token, "token must not be null.");
-
- if (this.unaryOperators.containsKey(token))
- return TokenType.UNARY_OPERATOR;
- else if (this.binaryOperators.containsKey(token))
- return TokenType.BINARY_OPERATOR;
- else
- return TokenType.OBJECT;
- }
-
- /**
- * Parses an expression.
- *
- * @param expression
- * expression to parse
- * @return result
- * @throws NullPointerException
- * if {@code expression} is null
- * @since 2019-03-14
- */
- public T parseExpression(final String expression) {
- return this.parseReversePolishExpression(this.convertExpressionToReversePolish(expression));
- }
-
- /**
- * Parses an expression expressed in reverse Polish notation.
- *
- * @param expression
- * expression to parse
- * @return result
- * @throws NullPointerException
- * if {@code expression} is null
- * @since 2019-03-14
- */
- private T parseReversePolishExpression(final String expression) {
- Objects.requireNonNull(expression, "expression must not be null.");
-
- final Deque stack = new ArrayDeque<>();
-
- // iterate over every item in the expression, then
- for (final String item : expression.split(" ")) {
- // choose a path based on what kind of thing was just read
- switch (this.getTokenType(item)) {
-
- case BINARY_OPERATOR:
- if (stack.size() < 2)
- throw new IllegalStateException(String.format(
- "Attempted to call binary operator %s with only %d arguments.", item, stack.size()));
-
- // get two arguments and operator, then apply!
- final T o1 = stack.pop();
- final T o2 = stack.pop();
- final BinaryOperator binaryOperator = this.binaryOperators.get(item);
-
- stack.push(binaryOperator.apply(o1, o2));
- break;
-
- case OBJECT:
- // just add it to the stack
- stack.push(this.objectObtainer.apply(item));
- break;
-
- case UNARY_OPERATOR:
- if (stack.size() < 1)
- throw new IllegalStateException(String.format(
- "Attempted to call unary operator %s with only %d arguments.", item, stack.size()));
-
- // get one argument and operator, then apply!
- final T o = stack.pop();
- final UnaryOperator unaryOperator = this.unaryOperators.get(item);
-
- stack.push(unaryOperator.apply(o));
- break;
- default:
- throw new AssertionError(
- String.format("Internal error: Invalid token type %s.", this.getTokenType(item)));
-
- }
- }
-
- // return answer, or throw an exception if I can't
- if (stack.size() > 1)
- throw new IllegalStateException("Computation ended up with more than one answer.");
- else if (stack.size() == 0)
- throw new IllegalStateException("Computation ended up without an answer.");
- return stack.pop();
- }
-}
diff --git a/src/org/unitConverter/expressionParser/package-info.java b/src/org/unitConverter/expressionParser/package-info.java
deleted file mode 100644
index 28f0cae..0000000
--- a/src/org/unitConverter/expressionParser/package-info.java
+++ /dev/null
@@ -1,23 +0,0 @@
-/**
- * Copyright (C) 2019 Adrien Hopkins
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see .
- */
-/**
- * A module that is capable of parsing expressions of things, like mathematical expressions or unit expressions.
- *
- * @author Adrien Hopkins
- * @since 2019-03-14
- */
-package org.unitConverter.expressionParser;
\ No newline at end of file
diff --git a/src/org/unitConverter/math/DecimalComparison.java b/src/org/unitConverter/math/DecimalComparison.java
new file mode 100644
index 0000000..e6fb733
--- /dev/null
+++ b/src/org/unitConverter/math/DecimalComparison.java
@@ -0,0 +1,107 @@
+/**
+ * Copyright (C) 2019 Adrien Hopkins
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.unitConverter.math;
+
+/**
+ * A class that contains methods to compare float and double values.
+ *
+ * @author Adrien Hopkins
+ * @since 2019-03-18
+ */
+public final class DecimalComparison {
+ /**
+ * The value used for double comparison. If two double values are within this value multiplied by the larger value,
+ * they are considered equal.
+ *
+ * @since 2019-03-18
+ */
+ public static final double DOUBLE_EPSILON = 1.0e-15;
+
+ /**
+ * The value used for float comparison. If two float values are within this value multiplied by the larger value,
+ * they are considered equal.
+ *
+ * @since 2019-03-18
+ */
+ public static final float FLOAT_EPSILON = 1.0e-6f;
+
+ /**
+ * Tests for equality of double values using {@link #DOUBLE_EPSILON}.
+ *
+ * @param a
+ * first value to test
+ * @param b
+ * second value to test
+ * @return whether they are equal
+ * @since 2019-03-18
+ */
+ public static final boolean equals(final double a, final double b) {
+ return DecimalComparison.equals(a, b, DOUBLE_EPSILON);
+ }
+
+ /**
+ * Tests for double equality using a custom epsilon value.
+ *
+ * @param a
+ * first value to test
+ * @param b
+ * second value to test
+ * @param epsilon
+ * allowed difference
+ * @return whether they are equal
+ * @since 2019-03-18
+ */
+ public static final boolean equals(final double a, final double b, final double epsilon) {
+ return Math.abs(a - b) <= epsilon * Math.max(Math.abs(a), Math.abs(b));
+ }
+
+ /**
+ * Tests for equality of float values using {@link #FLOAT_EPSILON}.
+ *
+ * @param a
+ * first value to test
+ * @param b
+ * second value to test
+ * @return whether they are equal
+ * @since 2019-03-18
+ */
+ public static final boolean equals(final float a, final float b) {
+ return DecimalComparison.equals(a, b, FLOAT_EPSILON);
+ }
+
+ /**
+ * Tests for float equality using a custom epsilon value.
+ *
+ * @param a
+ * first value to test
+ * @param b
+ * second value to test
+ * @param epsilon
+ * allowed difference
+ * @return whether they are equal
+ * @since 2019-03-18
+ */
+ public static final boolean equals(final float a, final float b, final float epsilon) {
+ return Math.abs(a - b) <= epsilon * Math.max(Math.abs(a), Math.abs(b));
+ }
+
+ // You may NOT get any DecimalComparison instances
+ private DecimalComparison() {
+ throw new AssertionError();
+ }
+
+}
diff --git a/src/org/unitConverter/math/ExpressionParser.java b/src/org/unitConverter/math/ExpressionParser.java
new file mode 100644
index 0000000..e06a58b
--- /dev/null
+++ b/src/org/unitConverter/math/ExpressionParser.java
@@ -0,0 +1,627 @@
+/**
+ * Copyright (C) 2019 Adrien Hopkins
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.unitConverter.math;
+
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Deque;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.function.BinaryOperator;
+import java.util.function.Function;
+import java.util.function.UnaryOperator;
+
+/**
+ * An object that can parse expressions with unary or binary operators.
+ *
+ * @author Adrien Hopkins
+ * @param
+ * type of object that exists in parsed expressions
+ * @since 2019-03-14
+ */
+// TODO: possibly make this class non-final?
+public final class ExpressionParser {
+ /**
+ * A builder that can create {@code ExpressionParser} instances.
+ *
+ * @author Adrien Hopkins
+ * @param
+ * type of object that exists in parsed expressions
+ * @since 2019-03-17
+ */
+ public static final class Builder {
+ /**
+ * A function that obtains a parseable object from a string. For example, an integer {@code ExpressionParser}
+ * would use {@code Integer::parseInt}.
+ *
+ * @since 2019-03-14
+ */
+ private final Function objectObtainer;
+
+ /**
+ * A map mapping operator strings to operator functions, for unary operators.
+ *
+ * @since 2019-03-14
+ */
+ private final Map> unaryOperators;
+
+ /**
+ * A map mapping operator strings to operator functions, for binary operators.
+ *
+ * @since 2019-03-14
+ */
+ private final Map> binaryOperators;
+
+ /**
+ * Creates the {@code Builder}.
+ *
+ * @param objectObtainer
+ * a function that can turn strings into objects of the type handled by the parser.
+ * @throws NullPointerException
+ * if {@code objectObtainer} is null
+ * @since 2019-03-17
+ */
+ public Builder(final Function objectObtainer) {
+ this.objectObtainer = Objects.requireNonNull(objectObtainer, "objectObtainer must not be null.");
+ this.unaryOperators = new HashMap<>();
+ this.binaryOperators = new HashMap<>();
+ }
+
+ /**
+ * Adds a binary operator to the builder.
+ *
+ * @param text
+ * text used to reference the operator, like '+'
+ * @param operator
+ * operator to add
+ * @param priority
+ * operator's priority, which determines which operators are applied first
+ * @return this builder
+ * @throws NullPointerException
+ * if {@code text} or {@code operator} is null
+ * @since 2019-03-17
+ */
+ public Builder addBinaryOperator(final String text, final BinaryOperator operator, final int priority) {
+ Objects.requireNonNull(text, "text must not be null.");
+ Objects.requireNonNull(operator, "operator must not be null.");
+
+ // Unfortunately, I cannot use a lambda because the PriorityBinaryOperator requires arguments.
+ final PriorityBinaryOperator priorityOperator = new PriorityBinaryOperator(priority) {
+ @Override
+ public T apply(final T t, final T u) {
+ return operator.apply(t, u);
+ }
+
+ };
+ this.binaryOperators.put(text, priorityOperator);
+ return this;
+ }
+
+ /**
+ * Adds a unary operator to the builder.
+ *
+ * @param text
+ * text used to reference the operator, like '-'
+ * @param operator
+ * operator to add
+ * @param priority
+ * operator's priority, which determines which operators are applied first
+ * @return this builder
+ * @throws NullPointerException
+ * if {@code text} or {@code operator} is null
+ * @since 2019-03-17
+ */
+ public Builder addUnaryOperator(final String text, final UnaryOperator operator, final int priority) {
+ Objects.requireNonNull(text, "text must not be null.");
+ Objects.requireNonNull(operator, "operator must not be null.");
+
+ // Unfortunately, I cannot use a lambda because the PriorityUnaryOperator requires arguments.
+ final PriorityUnaryOperator priorityOperator = new PriorityUnaryOperator(priority) {
+ @Override
+ public T apply(final T t) {
+ return operator.apply(t);
+ }
+ };
+ this.unaryOperators.put(text, priorityOperator);
+ return this;
+ }
+
+ /**
+ * @return an {@code ExpressionParser} instance with the properties given to this builder
+ * @since 2019-03-17
+ */
+ public ExpressionParser build() {
+ return new ExpressionParser<>(this.objectObtainer, this.unaryOperators, this.binaryOperators);
+ }
+ }
+
+ /**
+ * A binary operator with a priority field that determines which operators apply first.
+ *
+ * @author Adrien Hopkins
+ * @param
+ * type of operand and result
+ * @since 2019-03-17
+ */
+ private static abstract class PriorityBinaryOperator
+ implements BinaryOperator, Comparable> {
+ /**
+ * The operator's priority. Higher-priority operators are applied before lower-priority operators
+ */
+ private final int priority;
+
+ /**
+ * Creates the {@code PriorityBinaryOperator}.
+ *
+ * @param priority
+ * operator's priority
+ * @since 2019-03-17
+ */
+ public PriorityBinaryOperator(final int priority) {
+ this.priority = priority;
+ }
+
+ /**
+ * Compares this object to another by priority.
+ *
+ *
+ * {@inheritDoc}
+ */
+ @Override
+ public int compareTo(final PriorityBinaryOperator o) {
+ if (this.priority < o.priority)
+ return -1;
+ else if (this.priority > o.priority)
+ return 1;
+ else
+ return 0;
+ }
+
+ /**
+ * @return priority
+ * @since 2019-03-22
+ */
+ public final int getPriority() {
+ return this.priority;
+ }
+ }
+
+ /**
+ * A unary operator with a priority field that determines which operators apply first.
+ *
+ * @author Adrien Hopkins
+ * @param
+ * type of operand and result
+ * @since 2019-03-17
+ */
+ private static abstract class PriorityUnaryOperator
+ implements UnaryOperator, Comparable> {
+ /**
+ * The operator's priority. Higher-priority operators are applied before lower-priority operators
+ */
+ private final int priority;
+
+ /**
+ * Creates the {@code PriorityUnaryOperator}.
+ *
+ * @param priority
+ * operator's priority
+ * @since 2019-03-17
+ */
+ public PriorityUnaryOperator(final int priority) {
+ this.priority = priority;
+ }
+
+ /**
+ * Compares this object to another by priority.
+ *
+ *
+ * {@inheritDoc}
+ */
+ @Override
+ public int compareTo(final PriorityUnaryOperator o) {
+ if (this.priority < o.priority)
+ return -1;
+ else if (this.priority > o.priority)
+ return 1;
+ else
+ return 0;
+ }
+
+ /**
+ * @return priority
+ * @since 2019-03-22
+ */
+ public final int getPriority() {
+ return this.priority;
+ }
+ }
+
+ /**
+ * The types of tokens that are available.
+ *
+ * @author Adrien Hopkins
+ * @since 2019-03-14
+ */
+ private static enum TokenType {
+ OBJECT, UNARY_OPERATOR, BINARY_OPERATOR;
+ }
+
+ /**
+ * The opening bracket.
+ *
+ * @since 2019-03-22
+ */
+ public static final char OPENING_BRACKET = '(';
+
+ /**
+ * The closing bracket.
+ *
+ * @since 2019-03-22
+ */
+ public static final char CLOSING_BRACKET = ')';
+
+ /**
+ * Finds the other bracket in a pair of brackets, given the position of one.
+ *
+ * @param string
+ * string that contains brackets
+ * @param bracketPosition
+ * position of first bracket
+ * @return position of matching bracket
+ * @throws NullPointerException
+ * if string is null
+ * @since 2019-03-22
+ */
+ private static int findBracketPair(final String string, final int bracketPosition) {
+ Objects.requireNonNull(string, "string must not be null.");
+
+ final char openingBracket = string.charAt(bracketPosition);
+
+ // figure out what closing bracket to look for
+ final char closingBracket;
+ switch (openingBracket) {
+ case '(':
+ closingBracket = ')';
+ break;
+ case '[':
+ closingBracket = ']';
+ break;
+ case '{':
+ closingBracket = '}';
+ break;
+ default:
+ throw new IllegalArgumentException(String.format("Invalid bracket '%s'", openingBracket));
+ }
+
+ // level of brackets. every opening bracket increments this; every closing bracket decrements it
+ int bracketLevel = 0;
+
+ // iterate over the string to find the closing bracket
+ for (int currentPosition = bracketPosition; currentPosition < string.length(); currentPosition++) {
+ final char currentCharacter = string.charAt(currentPosition);
+
+ if (currentCharacter == openingBracket) {
+ bracketLevel++;
+ } else if (currentCharacter == closingBracket) {
+ bracketLevel--;
+ if (bracketLevel == 0)
+ return currentPosition;
+ }
+ }
+
+ throw new IllegalArgumentException("No matching bracket found.");
+ }
+
+ public static void main(final String[] args) {
+ final ExpressionParser numberParser = new ExpressionParser.Builder<>(Integer::parseInt)
+ .addBinaryOperator("+", (o1, o2) -> o1 + o2, 0).addBinaryOperator("*", (o1, o2) -> o1 * o2, 1)
+ .addBinaryOperator("^", (o1, o2) -> (int) Math.pow(o1, o2), 2).build();
+ System.out.println(numberParser.convertExpressionToReversePolish("(1 + 2) ^ 5 * 3"));
+ System.out.println(numberParser.parseExpression("(1 + 2) ^ 5 * 3")); // 729
+ }
+
+ /**
+ * Swaps two elements in a list. Modifies the list passed in instead of returning a modified list.
+ *
+ * @param list
+ * list to swap elements
+ * @param firstIndex
+ * index of first element to swap
+ * @param otherIndex
+ * index of other element to swap
+ * @throws NullPointerException
+ * if list is null
+ * @since 2019-03-20
+ */
+ private static void swap(final List list, final int firstIndex, final int otherIndex) {
+ Objects.requireNonNull(list, "list must not be null.");
+ final E temp = list.get(firstIndex);
+ list.set(firstIndex, list.get(otherIndex));
+ list.set(otherIndex, temp);
+ }
+
+ /**
+ * A function that obtains a parseable object from a string. For example, an integer {@code ExpressionParser} would
+ * use {@code Integer::parseInt}.
+ *
+ * @since 2019-03-14
+ */
+ private final Function objectObtainer;
+
+ /**
+ * A map mapping operator strings to operator functions, for unary operators.
+ *
+ * @since 2019-03-14
+ */
+ private final Map> unaryOperators;
+
+ /**
+ * A map mapping operator strings to operator functions, for binary operators.
+ *
+ * @since 2019-03-14
+ */
+ private final Map> binaryOperators;
+
+ /**
+ * Creates the {@code ExpressionParser}.
+ *
+ * @param objectObtainer
+ * function to get objects from strings
+ * @param unaryOperators
+ * operators available to the parser
+ * @since 2019-03-14
+ */
+ private ExpressionParser(final Function objectObtainer,
+ final Map> unaryOperators,
+ final Map> binaryOperators) {
+ this.objectObtainer = objectObtainer;
+ this.unaryOperators = unaryOperators;
+ this.binaryOperators = binaryOperators;
+ }
+
+ /**
+ * Converts a given mathematical expression to reverse Polish notation (operators after operands).
+ *
+ * For example,
+ * {@code 2 * (3 + 4)}
+ * becomes
+ * {@code 2 3 4 + *}.
+ *
+ * @param expression
+ * expression
+ * @return expression in RPN
+ * @since 2019-03-17
+ */
+ private String convertExpressionToReversePolish(final String expression) {
+ Objects.requireNonNull(expression, "expression must not be null.");
+
+ final List components = new ArrayList<>();
+
+ // the part of the expression remaining to parse
+ String partialExpression = expression;
+
+ // find and deal with brackets
+ while (partialExpression.indexOf(OPENING_BRACKET) != -1) {
+ final int openingBracketPosition = partialExpression.indexOf(OPENING_BRACKET);
+ final int closingBracketPosition = findBracketPair(partialExpression, openingBracketPosition);
+ components.addAll(Arrays.asList(partialExpression.substring(0, openingBracketPosition).split(" ")));
+ components.add(this.convertExpressionToReversePolish(
+ partialExpression.substring(openingBracketPosition + 1, closingBracketPosition)));
+ partialExpression = partialExpression.substring(closingBracketPosition + 1);
+ }
+
+ // add everything else
+ components.addAll(Arrays.asList(partialExpression.split(" ")));
+
+ // remove empty entries
+ while (components.contains("")) {
+ components.remove("");
+ }
+
+ // turn the expression into reverse Polish
+ while (true) {
+ final int highestPriorityOperatorPosition = this.findHighestPriorityOperatorPosition(components);
+ if (highestPriorityOperatorPosition == -1) {
+ break;
+ }
+
+ switch (this.getTokenType(components.get(highestPriorityOperatorPosition))) {
+ case UNARY_OPERATOR:
+ final String unaryOperator = components.remove(highestPriorityOperatorPosition);
+ final String operand = components.remove(highestPriorityOperatorPosition);
+ components.add(highestPriorityOperatorPosition, operand + " " + unaryOperator);
+ break;
+ case BINARY_OPERATOR:
+ final String binaryOperator = components.remove(highestPriorityOperatorPosition);
+ final String operand1 = components.remove(highestPriorityOperatorPosition - 1);
+ final String operand2 = components.remove(highestPriorityOperatorPosition - 1);
+ components.add(highestPriorityOperatorPosition - 1,
+ operand2 + " " + operand1 + " " + binaryOperator);
+ break;
+ default:
+ throw new AssertionError("Expected operator, found non-operator.");
+ }
+ }
+
+ // join all of the components together, then ensure there is only one space in a row
+ String expressionRPN = String.join(" ", components).replaceAll(" +", " ");
+
+ while (expressionRPN.charAt(0) == ' ') {
+ expressionRPN = expressionRPN.substring(1);
+ }
+ while (expressionRPN.charAt(expressionRPN.length() - 1) == ' ') {
+ expressionRPN = expressionRPN.substring(0, expressionRPN.length() - 1);
+ }
+ return expressionRPN;
+
+ // TODO method stub org.unitConverter.expressionParser.ExpressionParser.convertExpressionToPolish(expression)
+ }
+
+ /**
+ * Finds the position of the highest-priority operator in a list
+ *
+ * @param components
+ * components to test
+ * @param blacklist
+ * positions of operators that should be ignored
+ * @return position of highest priority, or -1 if the list contains no operators
+ * @throws NullPointerException
+ * if components is null
+ * @since 2019-03-22
+ */
+ private int findHighestPriorityOperatorPosition(final List components) {
+ Objects.requireNonNull(components, "components must not be null.");
+ // find highest priority
+ int maxPriority = Integer.MIN_VALUE;
+ int maxPriorityPosition = -1;
+
+ // go over components one by one
+ // if it is an operator, test its priority to see if it's max
+ // if it is, update maxPriority and maxPriorityPosition
+ for (int i = 0; i < components.size(); i++) {
+
+ switch (this.getTokenType(components.get(i))) {
+ case UNARY_OPERATOR:
+ final PriorityUnaryOperator unaryOperator = this.unaryOperators.get(components.get(i));
+ final int unaryPriority = unaryOperator.getPriority();
+
+ if (unaryPriority > maxPriority) {
+ maxPriority = unaryPriority;
+ maxPriorityPosition = i;
+ }
+ break;
+ case BINARY_OPERATOR:
+ final PriorityBinaryOperator binaryOperator = this.binaryOperators.get(components.get(i));
+ final int binaryPriority = binaryOperator.getPriority();
+
+ if (binaryPriority > maxPriority) {
+ maxPriority = binaryPriority;
+ maxPriorityPosition = i;
+ }
+ break;
+ default:
+ break;
+ }
+ }
+
+ // max priority position found
+ return maxPriorityPosition;
+ }
+
+ /**
+ * Determines whether an inputted string is an object or an operator
+ *
+ * @param token
+ * string to input
+ * @return type of token it is
+ * @throws NullPointerException
+ * if {@code expression} is null
+ * @since 2019-03-14
+ */
+ private TokenType getTokenType(final String token) {
+ Objects.requireNonNull(token, "token must not be null.");
+
+ if (this.unaryOperators.containsKey(token))
+ return TokenType.UNARY_OPERATOR;
+ else if (this.binaryOperators.containsKey(token))
+ return TokenType.BINARY_OPERATOR;
+ else
+ return TokenType.OBJECT;
+ }
+
+ /**
+ * Parses an expression.
+ *
+ * @param expression
+ * expression to parse
+ * @return result
+ * @throws NullPointerException
+ * if {@code expression} is null
+ * @since 2019-03-14
+ */
+ public T parseExpression(final String expression) {
+ return this.parseReversePolishExpression(this.convertExpressionToReversePolish(expression));
+ }
+
+ /**
+ * Parses an expression expressed in reverse Polish notation.
+ *
+ * @param expression
+ * expression to parse
+ * @return result
+ * @throws NullPointerException
+ * if {@code expression} is null
+ * @since 2019-03-14
+ */
+ private T parseReversePolishExpression(final String expression) {
+ Objects.requireNonNull(expression, "expression must not be null.");
+
+ final Deque stack = new ArrayDeque<>();
+
+ // iterate over every item in the expression, then
+ for (final String item : expression.split(" ")) {
+ // choose a path based on what kind of thing was just read
+ switch (this.getTokenType(item)) {
+
+ case BINARY_OPERATOR:
+ if (stack.size() < 2)
+ throw new IllegalStateException(String.format(
+ "Attempted to call binary operator %s with only %d arguments.", item, stack.size()));
+
+ // get two arguments and operator, then apply!
+ final T o1 = stack.pop();
+ final T o2 = stack.pop();
+ final BinaryOperator binaryOperator = this.binaryOperators.get(item);
+
+ stack.push(binaryOperator.apply(o1, o2));
+ break;
+
+ case OBJECT:
+ // just add it to the stack
+ stack.push(this.objectObtainer.apply(item));
+ break;
+
+ case UNARY_OPERATOR:
+ if (stack.size() < 1)
+ throw new IllegalStateException(String.format(
+ "Attempted to call unary operator %s with only %d arguments.", item, stack.size()));
+
+ // get one argument and operator, then apply!
+ final T o = stack.pop();
+ final UnaryOperator unaryOperator = this.unaryOperators.get(item);
+
+ stack.push(unaryOperator.apply(o));
+ break;
+ default:
+ throw new AssertionError(
+ String.format("Internal error: Invalid token type %s.", this.getTokenType(item)));
+
+ }
+ }
+
+ // return answer, or throw an exception if I can't
+ if (stack.size() > 1)
+ throw new IllegalStateException("Computation ended up with more than one answer.");
+ else if (stack.size() == 0)
+ throw new IllegalStateException("Computation ended up without an answer.");
+ return stack.pop();
+ }
+}
diff --git a/src/org/unitConverter/math/package-info.java b/src/org/unitConverter/math/package-info.java
new file mode 100644
index 0000000..65d6b23
--- /dev/null
+++ b/src/org/unitConverter/math/package-info.java
@@ -0,0 +1,23 @@
+/**
+ * Copyright (C) 2019 Adrien Hopkins
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+/**
+ * A module that is capable of parsing expressions of things, like mathematical expressions or unit expressions.
+ *
+ * @author Adrien Hopkins
+ * @since 2019-03-14
+ */
+package org.unitConverter.math;
\ No newline at end of file
diff --git a/src/org/unitConverter/unit/BaseUnit.java b/src/org/unitConverter/unit/BaseUnit.java
index 894d338..2def48e 100755
--- a/src/org/unitConverter/unit/BaseUnit.java
+++ b/src/org/unitConverter/unit/BaseUnit.java
@@ -28,7 +28,7 @@ import org.unitConverter.dimension.UnitDimension;
* @since 2018-12-23
* @since v0.1.0
*/
-public final class BaseUnit extends AbstractUnit implements OperatableUnit {
+public final class BaseUnit extends LinearUnit {
/**
* Is this unit a full base (i.e. m, s, ... but not N, J, ...)
*
@@ -52,156 +52,65 @@ public final class BaseUnit extends AbstractUnit implements OperatableUnit {
* @since v0.1.0
*/
BaseUnit(final UnitDimension dimension, final UnitSystem system) {
- super(dimension, system);
+ super(dimension, system, 1);
this.isFullBase = dimension.isBase();
}
/**
- * @return this unit as a {@code LinearUnit}
- * @since 2019-01-25
- * @since v0.1.0
- */
- public LinearUnit asLinearUnit() {
- return this.times(1);
- }
-
- @Override
- public double convertFromBase(final double value) {
- return value;
- }
-
- @Override
- public double convertToBase(final double value) {
- return value;
- }
-
- /**
- * Divides this unit by another unit.
+ * Returns the quotient of this unit and another.
+ *
+ * Two units can be divided if they are part of the same unit system. If {@code divisor} does not meet this
+ * condition, an {@code IllegalArgumentException} should be thrown.
+ *
*
- * @param other
+ * @param divisor
* unit to divide by
* @return quotient of two units
* @throws IllegalArgumentException
- * if this unit's system is not other's system
+ * if {@code divisor} is not compatible for division as described above
* @throws NullPointerException
- * if other is null
+ * if {@code divisor} is null
* @since 2018-12-22
* @since v0.1.0
*/
- public BaseUnit dividedBy(final BaseUnit other) {
- Objects.requireNonNull(other, "other must not be null.");
- if (!this.getSystem().equals(other.getSystem()))
- throw new IllegalArgumentException("Incompatible base units for division.");
- return new BaseUnit(this.getDimension().dividedBy(other.getDimension()), this.getSystem());
- }
+ public BaseUnit dividedBy(final BaseUnit divisor) {
+ Objects.requireNonNull(divisor, "other must not be null.");
- /**
- * Divides this unit by a divisor
- *
- * @param divisor
- * amount to divide by
- * @return quotient
- * @since 2018-12-23
- * @since v0.1.0
- */
- public LinearUnit dividedBy(final double divisor) {
- return new LinearUnit(this, 1 / divisor);
- }
-
- @Override
- public boolean equals(final Object obj) {
- if (!(obj instanceof BaseUnit))
- return false;
- final BaseUnit other = (BaseUnit) obj;
- return Objects.equals(this.getSystem(), other.getSystem())
- && Objects.equals(this.getDimension(), other.getDimension());
- }
-
- @Override
- public int hashCode() {
- final int prime = 31;
- int result = 1;
- result = result * prime + this.getSystem().hashCode();
- result = result * prime + this.getDimension().hashCode();
- return result;
- }
-
- @Override
- public LinearUnit negated() {
- return this.times(-1);
- }
-
- @Override
- public OperatableUnit plus(final OperatableUnit addend) {
- Objects.requireNonNull(addend, "addend must not be null.");
-
- // reject addends that cannot be added to this unit
- if (!this.getSystem().equals(addend.getSystem()))
- throw new IllegalArgumentException(
- String.format("Incompatible units for addition or subtraction \"%s\" and \"%s\".", this, addend));
- if (!this.getDimension().equals(addend.getDimension()))
+ // check that these units can be multiplied
+ if (!this.getSystem().equals(divisor.getSystem()))
throw new IllegalArgumentException(
- String.format("Incompatible units for addition or subtraction \"%s\" and \"%s\".", this, addend));
-
- // add them together
- if (addend instanceof BaseUnit)
- return this.times(2);
- else
- return addend.plus(this);
- }
+ String.format("Incompatible units for division \"%s\" and \"%s\".", this, divisor));
- @Override
- public BaseUnit reciprocal() {
- return this.toExponent(-1);
+ return new BaseUnit(this.getDimension().dividedBy(divisor.getDimension()), this.getSystem());
}
/**
- * Multiplies this unit by another unit.
+ * Returns the product of this unit and another.
+ *
+ * Two units can be multiplied if they are part of the same unit system. If {@code multiplier} does not meet this
+ * condition, an {@code IllegalArgumentException} should be thrown.
+ *
*
- * @param other
+ * @param multiplier
* unit to multiply by
* @return product of two units
* @throws IllegalArgumentException
- * if this unit's system is not other's system
+ * if {@code multiplier} is not compatible for multiplication as described above
* @throws NullPointerException
- * if other is null
+ * if {@code multiplier} is null
* @since 2018-12-22
* @since v0.1.0
*/
- public BaseUnit times(final BaseUnit other) {
- Objects.requireNonNull(other, "other must not be null.");
- if (!this.getSystem().equals(other.getSystem()))
- throw new IllegalArgumentException("Incompatible base units for multiplication.");
- return new BaseUnit(this.getDimension().times(other.getDimension()), this.getSystem());
- }
-
- /**
- * Multiplies this unit by a multiplier.
- *
- * @param multiplier
- * amount to multiply by
- * @return product
- * @since 2018-12-23
- * @since v0.1.0
- */
- public LinearUnit times(final double multiplier) {
- return new LinearUnit(this, multiplier);
- }
+ public BaseUnit times(final BaseUnit multiplier) {
+ Objects.requireNonNull(multiplier, "other must not be null");
- @Override
- public OperatableUnit times(final OperatableUnit multiplier) {
- Objects.requireNonNull(multiplier, "multiplier must not be null.");
-
- // reject multipliers that cannot be muliplied by this unit
+ // check that these units can be multiplied
if (!this.getSystem().equals(multiplier.getSystem()))
- throw new IllegalArgumentException(String
- .format("Incompatible units for multiplication or division \"%s\" and \"%s\".", this, multiplier));
+ throw new IllegalArgumentException(
+ String.format("Incompatible units for multiplication \"%s\" and \"%s\".", this, multiplier));
// multiply the units
- if (multiplier instanceof BaseUnit)
- return new BaseUnit(this.getDimension().times(multiplier.getDimension()), this.getSystem());
- else
- return multiplier.times(this);
+ return new BaseUnit(this.getDimension().times(multiplier.getDimension()), this.getSystem());
}
/**
diff --git a/src/org/unitConverter/unit/LinearUnit.java b/src/org/unitConverter/unit/LinearUnit.java
index 64eff1f..c755f79 100644
--- a/src/org/unitConverter/unit/LinearUnit.java
+++ b/src/org/unitConverter/unit/LinearUnit.java
@@ -19,6 +19,7 @@ package org.unitConverter.unit;
import java.util.Objects;
import org.unitConverter.dimension.UnitDimension;
+import org.unitConverter.math.DecimalComparison;
/**
* A unit that is equal to a certain number multiplied by its base.
@@ -27,7 +28,7 @@ import org.unitConverter.dimension.UnitDimension;
* @since 2018-12-22
* @since v0.1.0
*/
-public final class LinearUnit extends AbstractUnit implements OperatableUnit {
+public class LinearUnit extends AbstractUnit {
/**
* The value of one of this unit in this unit's base unit
*
@@ -91,20 +92,33 @@ public final class LinearUnit extends AbstractUnit implements OperatableUnit {
}
/**
- * Divides this unit by another unit.
+ * Returns the quotient of this unit and another.
+ *
+ * Two units can be divided if they are part of the same unit system. If {@code divisor} does not meet this
+ * condition, an {@code IllegalArgumentException} should be thrown.
+ *
*
- * @param other
+ * @param divisor
* unit to divide by
* @return quotient of two units
+ * @throws IllegalArgumentException
+ * if {@code divisor} is not compatible for division as described above
* @throws NullPointerException
- * if other is null
+ * if {@code divisor} is null
* @since 2018-12-22
* @since v0.1.0
*/
- public LinearUnit dividedBy(final LinearUnit other) {
- Objects.requireNonNull(other, "other must not be null");
- final BaseUnit base = this.getBase().dividedBy(other.getBase());
- return new LinearUnit(base, this.getConversionFactor() / other.getConversionFactor());
+ public LinearUnit dividedBy(final LinearUnit divisor) {
+ Objects.requireNonNull(divisor, "other must not be null");
+
+ // check that these units can be multiplied
+ if (!this.getSystem().equals(divisor.getSystem()))
+ throw new IllegalArgumentException(
+ String.format("Incompatible units for division \"%s\" and \"%s\".", this, divisor));
+
+ // divide the units
+ final BaseUnit base = this.getBase().dividedBy(divisor.getBase());
+ return new LinearUnit(base, this.getConversionFactor() / divisor.getConversionFactor());
}
@Override
@@ -112,12 +126,13 @@ public final class LinearUnit extends AbstractUnit implements OperatableUnit {
if (!(obj instanceof LinearUnit))
return false;
final LinearUnit other = (LinearUnit) obj;
- return Objects.equals(this.getBase(), other.getBase())
- && Objects.equals(this.getConversionFactor(), other.getConversionFactor());
+ return Objects.equals(this.getSystem(), other.getSystem())
+ && Objects.equals(this.getDimension(), other.getDimension())
+ && DecimalComparison.equals(this.getConversionFactor(), other.getConversionFactor());
}
/**
- * @return conversionFactor
+ * @return conversion factor between this unit and its base
* @since 2018-12-22
* @since v0.1.0
*/
@@ -129,43 +144,66 @@ public final class LinearUnit extends AbstractUnit implements OperatableUnit {
public int hashCode() {
final int prime = 31;
int result = 1;
- result = result * prime + this.getBase().hashCode();
+ result = result * prime + this.getSystem().hashCode();
+ result = result * prime + this.getDimension().hashCode();
result = result * prime + Double.hashCode(this.getConversionFactor());
return result;
}
- @Override
- public LinearUnit negated() {
- return new LinearUnit(this.getBase(), -this.getConversionFactor());
+ /**
+ * Returns the difference of this unit and another.
+ *
+ * Two units can be subtracted if they have the same base. If {@code subtrahend} does not meet this condition, an
+ * {@code IllegalArgumentException} will be thrown.
+ *
+ *
+ * @param subtrahend
+ * unit to subtract
+ * @return difference of units
+ * @throws IllegalArgumentException
+ * if {@code subtrahend} is not compatible for subtraction as described above
+ * @throws NullPointerException
+ * if {@code subtrahend} is null
+ * @since 2019-03-17
+ */
+ public LinearUnit minus(final LinearUnit subtrahendend) {
+ Objects.requireNonNull(subtrahendend, "addend must not be null.");
+
+ // reject subtrahends that cannot be added to this unit
+ if (!this.getBase().equals(subtrahendend.getBase()))
+ throw new IllegalArgumentException(
+ String.format("Incompatible units for subtraction \"%s\" and \"%s\".", this, subtrahendend));
+
+ // add the units
+ return new LinearUnit(this.getBase(), this.getConversionFactor() - subtrahendend.getConversionFactor());
}
- @Override
- public OperatableUnit plus(final OperatableUnit addend) {
+ /**
+ * Returns the sum of this unit and another.
+ *
+ * Two units can be added if they have the same base. If {@code addend} does not meet this condition, an
+ * {@code IllegalArgumentException} will be thrown.
+ *
+ *
+ * @param addend
+ * unit to add
+ * @return sum of units
+ * @throws IllegalArgumentException
+ * if {@code addend} is not compatible for addition as described above
+ * @throws NullPointerException
+ * if {@code addend} is null
+ * @since 2019-03-17
+ */
+ public LinearUnit plus(final LinearUnit addend) {
Objects.requireNonNull(addend, "addend must not be null.");
// reject addends that cannot be added to this unit
- if (!this.getSystem().equals(addend.getSystem()))
- throw new IllegalArgumentException(
- String.format("Incompatible units for addition or subtraction \"%s\" and \"%s\".", this, addend));
- if (!this.getDimension().equals(addend.getDimension()))
+ if (!this.getBase().equals(addend.getBase()))
throw new IllegalArgumentException(
- String.format("Incompatible units for addition or subtraction \"%s\" and \"%s\".", this, addend));
+ String.format("Incompatible units for addition \"%s\" and \"%s\".", this, addend));
// add the units
- if (addend instanceof BaseUnit)
- // since addend's dimension is equal to this unit's dimension, and there is only one base unit per
- // system-dimension, addend must be this unit's base.
- return new LinearUnit(this.getBase(), this.getConversionFactor() + 1);
- else if (addend instanceof LinearUnit)
- return new LinearUnit(this.getBase(),
- this.getConversionFactor() + ((LinearUnit) addend).getConversionFactor());
- else
- return addend.times(this);
- }
-
- @Override
- public LinearUnit reciprocal() {
- return this.toExponent(-1);
+ return new LinearUnit(this.getBase(), this.getConversionFactor() + addend.getConversionFactor());
}
/**
@@ -182,40 +220,33 @@ public final class LinearUnit extends AbstractUnit implements OperatableUnit {
}
/**
- * Multiplies this unit by another unit.
+ * Returns the product of this unit and another.
+ *
+ * Two units can be multiplied if they are part of the same unit system. If {@code multiplier} does not meet this
+ * condition, an {@code IllegalArgumentException} should be thrown.
+ *
*
- * @param other
- * unit to multiply by=
+ * @param multiplier
+ * unit to multiply by
* @return product of two units
+ * @throws IllegalArgumentException
+ * if {@code multiplier} is not compatible for multiplication as described above
* @throws NullPointerException
- * if other is null
+ * if {@code multiplier} is null
* @since 2018-12-22
* @since v0.1.0
*/
- public LinearUnit times(final LinearUnit other) {
- Objects.requireNonNull(other, "other must not be null");
- final BaseUnit base = this.getBase().times(other.getBase());
- return new LinearUnit(base, this.getConversionFactor() * other.getConversionFactor());
- }
-
- @Override
- public OperatableUnit times(final OperatableUnit multiplier) {
- Objects.requireNonNull(multiplier, "multiplier must not be null.");
+ public LinearUnit times(final LinearUnit multiplier) {
+ Objects.requireNonNull(multiplier, "other must not be null");
- // reject multipliers that cannot be muliplied by this unit
+ // check that these units can be multiplied
if (!this.getSystem().equals(multiplier.getSystem()))
- throw new IllegalArgumentException(String
- .format("Incompatible units for multiplication or division \"%s\" and \"%s\".", this, multiplier));
+ throw new IllegalArgumentException(
+ String.format("Incompatible units for multiplication \"%s\" and \"%s\".", this, multiplier));
// multiply the units
- if (multiplier instanceof BaseUnit) {
- final BaseUnit newBase = this.getBase().times((BaseUnit) multiplier);
- return new LinearUnit(newBase, this.getConversionFactor());
- } else if (multiplier instanceof LinearUnit) {
- final BaseUnit base = this.getBase().times(multiplier.getBase());
- return new LinearUnit(base, this.getConversionFactor() * ((LinearUnit) multiplier).getConversionFactor());
- } else
- return multiplier.times(this);
+ final BaseUnit base = this.getBase().times(multiplier.getBase());
+ return new LinearUnit(base, this.getConversionFactor() * multiplier.getConversionFactor());
}
/**
@@ -227,7 +258,6 @@ public final class LinearUnit extends AbstractUnit implements OperatableUnit {
* @since 2019-01-15
* @since v0.1.0
*/
- @Override
public LinearUnit toExponent(final int exponent) {
return new LinearUnit(this.getBase().toExponent(exponent), Math.pow(this.conversionFactor, exponent));
}
@@ -236,4 +266,16 @@ public final class LinearUnit extends AbstractUnit implements OperatableUnit {
public String toString() {
return super.toString() + String.format(" (equal to %s * base)", this.getConversionFactor());
}
+
+ /**
+ * Returns the result of applying {@code prefix} to this unit.
+ *
+ * @param prefix
+ * prefix to apply
+ * @return unit with prefix
+ * @since 2019-03-18
+ */
+ public LinearUnit withPrefix(final UnitPrefix prefix) {
+ return this.times(prefix.getMultiplier());
+ }
}
diff --git a/src/org/unitConverter/unit/OperatableUnit.java b/src/org/unitConverter/unit/OperatableUnit.java
deleted file mode 100644
index ae11c41..0000000
--- a/src/org/unitConverter/unit/OperatableUnit.java
+++ /dev/null
@@ -1,169 +0,0 @@
-/**
- * Copyright (C) 2019 Adrien Hopkins
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see .
- */
-package org.unitConverter.unit;
-
-/**
- * A unit that can be added, subtracted, multiplied or divided by another operatable unit, and raised to an integer
- * exponent.
- *
- * In order to use two units in an operation, they must be part of the same unit system. In addition, in order for two
- * units to add or subtract, they must measure the same dimension.
- *
- *
- * It is okay for an operation to throw a {@code ClassCastException} if the operator's class cannot operate with another
- * class. However, all classes that implement this interface should be able to interoperate with {@code BaseUnit} and
- * {@code LinearUnit}.
- *
- *
- * @author Adrien Hopkins
- * @since 2019-03-17
- */
-public interface OperatableUnit extends Unit {
- /**
- * Returns the quotient of this unit and another.
- *
- * Two units can be divided if they are part of the same unit system. If {@code divisor} does not meet this
- * condition, an {@code IllegalArgumentException} should be thrown.
- *
- *
- * It is okay for a unit to throw a {@code ClassCastException} if it cannot operate with {@code divisor}'s class.
- * However, all classes that implement this interface should be able to interoperate with {@code BaseUnit} and
- * {@code LinearUnit}.
- *
- *
- * @param divisor
- * unit to divide by
- * @return quotient
- * @throws IllegalArgumentException
- * if {@code divisor} is not compatible for division as described above
- * @throws NullPointerException
- * if {@code divisor} is null
- * @throws ClassCastException
- * if {@code divisor}'s class is incompatible with this unit's class
- * @since 2019-03-17
- */
- default OperatableUnit dividedBy(final OperatableUnit divisor) {
- return this.times(divisor.reciprocal());
- }
-
- /**
- * Returns the difference of this unit and another.
- *
- * Two units can be subtracted if they meet the following conditions:
- *
- * - The two units are part of the same UnitSystem.
- * - The two units have the same {@code dimension}.
- *
- * If {@code subtrahend} does not meet these conditions, an {@code IllegalArgumentException} should be thrown.
- *
- *
- * It is okay for a unit to throw a {@code ClassCastException} if it cannot operate with {@code subtrahend}'s class.
- * However, all classes that implement this interface should be able to interoperate with {@code BaseUnit} and
- * {@code LinearUnit}.
- *
- *
- * @param subtrahend
- * unit to subtract
- * @return difference
- * @throws IllegalArgumentException
- * if {@code subtrahend} is not compatible for subtraction as described above
- * @throws NullPointerException
- * if {@code subtrahend} is null
- * @throws ClassCastException
- * if {@code subtrahend}'s class is incompatible with this unit's class
- * @since 2019-03-17
- */
- default OperatableUnit minus(final OperatableUnit subtrahend) {
- return this.plus(subtrahend.negated());
- }
-
- /**
- * @return this unit negated, i.e. -this
- * @since 2019-03-17
- */
- OperatableUnit negated();
-
- /**
- * Returns the sum of this unit and another.
- *
- * Two units can be added if they meet the following conditions:
- *
- * - The two units are part of the same UnitSystem.
- * - The two units have the same {@code dimension}.
- *
- * If {@code addend} does not meet these conditions, an {@code IllegalArgumentException} should be thrown.
- *
- *
- * It is okay for a unit to throw a {@code ClassCastException} if it cannot operate with {@code addend}'s class.
- * However, all classes that implement this interface should be able to interoperate with {@code BaseUnit} and
- * {@code LinearUnit}.
- *
- *
- * @param addend
- * unit to add
- * @return sum
- * @throws IllegalArgumentException
- * if {@code addend} is not compatible for addition as described above
- * @throws NullPointerException
- * if {@code addend} is null
- * @throws ClassCastException
- * if {@code addend}'s class is incompatible with this unit's class
- * @since 2019-03-17
- */
- OperatableUnit plus(OperatableUnit addend);
-
- /**
- * @return reciprocal of this unit
- * @since 2019-03-17
- */
- OperatableUnit reciprocal();
-
- /**
- * Returns the product of this unit and another.
- *
- * Two units can be multiplied if they are part of the same unit system. If {@code multiplier} does not meet this
- * condition, an {@code IllegalArgumentException} should be thrown.
- *
- *
- * It is okay for a unit to throw a {@code ClassCastException} if it cannot operate with {@code multiplier}'s class.
- * However, all classes that implement this interface should be able to interoperate with {@code BaseUnit} and
- * {@code LinearUnit}.
- *
- *
- * @param multiplier
- * unit to multiply by
- * @return product
- * @throws IllegalArgumentException
- * if {@code multiplier} is not compatible for multiplication as described above
- * @throws NullPointerException
- * if {@code multiplier} is null
- * @throws ClassCastException
- * if {@code multiplier}'s class is incompatible with this unit's class
- * @since 2019-03-17
- */
- OperatableUnit times(OperatableUnit multiplier);
-
- /**
- * Returns the result of raising this unit to the exponent {@code exponent}.
- *
- * @param exponent
- * exponent to exponentiate by
- * @return result of exponentiation
- * @since 2019-03-17
- */
- OperatableUnit toExponent(int exponent);
-}
diff --git a/src/test/java/ExpressionParserTest.java b/src/test/java/ExpressionParserTest.java
new file mode 100644
index 0000000..e81ca40
--- /dev/null
+++ b/src/test/java/ExpressionParserTest.java
@@ -0,0 +1,50 @@
+/**
+ * Copyright (C) 2019 Adrien Hopkins
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package test.java;
+
+import static org.junit.Assert.assertEquals;
+
+import org.junit.Test;
+import org.unitConverter.math.ExpressionParser;
+
+/**
+ * @author Adrien Hopkins
+ * @since 2019-03-22
+ */
+public class ExpressionParserTest {
+ private static final ExpressionParser numberParser = new ExpressionParser.Builder<>(Integer::parseInt)
+ .addBinaryOperator("+", (o1, o2) -> o1 + o2, 0).addBinaryOperator("-", (o1, o2) -> o1 - o2, 0)
+ .addBinaryOperator("*", (o1, o2) -> o1 * o2, 1).addBinaryOperator("/", (o1, o2) -> o1 / o2, 1)
+ .addBinaryOperator("^", (o1, o2) -> (int) Math.pow(o1, o2), 2).build();
+
+ /**
+ * Test method for {@link org.unitConverter.math.ExpressionParser#parseExpression(java.lang.String)}.
+ */
+ @Test
+ public void testParseExpression() {
+ // test parsing of expressions
+ assertEquals((int) numberParser.parseExpression("1 + 2 ^ 5 * 3"), 97);
+ assertEquals((int) numberParser.parseExpression("(1 + 2) ^ 5 * 3"), 729);
+
+ // ensure it normally goes left to right
+ assertEquals((int) numberParser.parseExpression("1 + 2 + 3 + 4"), 10);
+ assertEquals((int) numberParser.parseExpression("12 - 4 - 3"), 5);
+ assertEquals((int) numberParser.parseExpression("12 - (4 - 3)"), 11);
+ assertEquals((int) numberParser.parseExpression("1 / 2 + 3"), 3);
+ }
+
+}
diff --git a/src/test/java/UnitTest.java b/src/test/java/UnitTest.java
index 45f890f..79bc3d1 100755
--- a/src/test/java/UnitTest.java
+++ b/src/test/java/UnitTest.java
@@ -18,10 +18,16 @@ package test.java;
import static org.junit.Assert.assertEquals;
+import java.util.Random;
+import java.util.concurrent.ThreadLocalRandom;
+
import org.junit.Test;
import org.unitConverter.dimension.StandardDimensions;
+import org.unitConverter.math.DecimalComparison;
import org.unitConverter.unit.BaseUnit;
+import org.unitConverter.unit.LinearUnit;
import org.unitConverter.unit.SI;
+import org.unitConverter.unit.SIPrefix;
import org.unitConverter.unit.Unit;
/**
@@ -31,12 +37,46 @@ import org.unitConverter.unit.Unit;
* @since 2018-12-22
*/
public class UnitTest {
+ /** A random number generator */
+ private static final Random rng = ThreadLocalRandom.current();
+
+ @Test
+ public void testAdditionAndSubtraction() {
+ final LinearUnit inch = SI.METRE.times(0.0254);
+ final LinearUnit foot = SI.METRE.times(0.3048);
+
+ assertEquals(inch.plus(foot), SI.METRE.times(0.3302));
+ assertEquals(foot.minus(inch), SI.METRE.times(0.2794));
+ }
+
+ @Test
+ public void testBaseUnitExclusives() {
+ // this test should have a compile error if I am doing something wrong
+ final BaseUnit metrePerSecondSquared = SI.METRE.dividedBy(SI.SECOND.toExponent(2));
+
+ assertEquals(metrePerSecondSquared, SI.SI.getBaseUnit(StandardDimensions.ACCELERATION));
+ }
+
@Test
public void testConversion() {
final BaseUnit metre = SI.METRE;
final Unit inch = metre.times(0.0254);
assertEquals(1.9, inch.convertToBase(75), 0.01);
+
+ // try random stuff
+ for (int i = 0; i < 1000; i++) {
+ // initiate random values
+ final double conversionFactor = rng.nextDouble() * 1000000;
+ final double testValue = rng.nextDouble() * 1000000;
+ final double expected = testValue * conversionFactor;
+
+ // test
+ final Unit unit = SI.METRE.times(conversionFactor);
+ final double actual = unit.convertToBase(testValue);
+
+ assertEquals(actual, expected, expected * DecimalComparison.DOUBLE_EPSILON);
+ }
}
@Test
@@ -46,4 +86,30 @@ public class UnitTest {
assertEquals(metre, meter);
}
+
+ @Test
+ public void testMultiplicationAndDivision() {
+ // test unit-times-unit multiplication
+ final LinearUnit generatedJoule = SI.KILOGRAM.times(SI.METRE.toExponent(2)).dividedBy(SI.SECOND.toExponent(2));
+ final LinearUnit actualJoule = SI.SI.getBaseUnit(StandardDimensions.ENERGY);
+
+ assertEquals(generatedJoule, actualJoule);
+
+ // test multiplication by conversion factors
+ final LinearUnit kilometre = SI.METRE.times(1000);
+ final LinearUnit hour = SI.SECOND.times(3600);
+ final LinearUnit generatedKPH = kilometre.dividedBy(hour);
+
+ final LinearUnit actualKPH = SI.SI.getBaseUnit(StandardDimensions.VELOCITY).dividedBy(3.6);
+
+ assertEquals(generatedKPH, actualKPH);
+ }
+
+ @Test
+ public void testPrefixes() {
+ final LinearUnit generatedKilometre = SI.METRE.withPrefix(SIPrefix.KILO);
+ final LinearUnit actualKilometre = SI.METRE.times(1000);
+
+ assertEquals(generatedKilometre, actualKilometre);
+ }
}
--
cgit v1.2.3
From bfe1f266922bffd3c0c8d8906535be7621217e7a Mon Sep 17 00:00:00 2001
From: Adrien Hopkins
Date: Fri, 22 Mar 2019 19:35:30 -0400
Subject: Unit Expressions are now parsed with the expression parser.
Addition and subtraction are now possible.
---
CHANGELOG.org | 1 +
src/org/unitConverter/UnitsDatabase.java | 326 +++++++++------------
.../converterGUI/UnitConverterGUI.java | 7 +-
src/org/unitConverter/math/ExpressionParser.java | 108 ++++---
unitsfile.txt | 2 +-
5 files changed, 213 insertions(+), 231 deletions(-)
(limited to 'src/org/unitConverter/UnitsDatabase.java')
diff --git a/CHANGELOG.org b/CHANGELOG.org
index 5baf980..95dc57a 100644
--- a/CHANGELOG.org
+++ b/CHANGELOG.org
@@ -10,6 +10,7 @@ All notable changes in this project will be shown in this file.
- GUI for a selection-based unit converter
- The UnitDatabase now stores dimensions.
- A system to parse mathematical expressions, used to parse unit expressions.
+ - You can now add and subtract in unit expressions!
** v0.1.0
NOTE: At this stage, the API is subject to significant change.
*** Added
diff --git a/src/org/unitConverter/UnitsDatabase.java b/src/org/unitConverter/UnitsDatabase.java
index 3af1c8d..481ce93 100755
--- a/src/org/unitConverter/UnitsDatabase.java
+++ b/src/org/unitConverter/UnitsDatabase.java
@@ -21,13 +21,17 @@ import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
+import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import org.unitConverter.dimension.UnitDimension;
+import org.unitConverter.math.DecimalComparison;
+import org.unitConverter.math.ExpressionParser;
import org.unitConverter.unit.AbstractUnit;
import org.unitConverter.unit.BaseUnit;
import org.unitConverter.unit.DefaultUnitPrefix;
@@ -67,6 +71,32 @@ public final class UnitsDatabase {
*/
private final Map dimensions;
+ /**
+ * A parser that can parse unit expressions.
+ *
+ * @since 2019-03-22
+ */
+ 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) -> {
+ // exponent function - first check if o2 is a number,
+ if (o2.getBase().equals(SI.SI.getBaseUnit(UnitDimension.EMPTY))) {
+ // then check if it is an integer,
+ final double exponent = o2.getConversionFactor();
+ if (DecimalComparison.equals(exponent % 1, 0))
+ // then exponentiate
+ return o1.toExponent((int) (exponent % 1 + 0.5));
+ else
+ // not an integer
+ throw new UnsupportedOperationException(
+ "Decimal exponents are currently not supported.");
+ } else
+ // not a number
+ throw new IllegalArgumentException("Exponents must be numbers.");
+ }, 2).build();
+
/**
* Creates the {@code UnitsDatabase}.
*
@@ -298,6 +328,37 @@ public final class UnitsDatabase {
return this.dimensions.get(name);
}
+ /**
+ * Gets a unit. If it is linear, cast it to a LinearUnit and return it. Otherwise, throw an
+ * {@code IllegalArgumentException}.
+ *
+ * @param name
+ * unit's name
+ * @return unit
+ * @since 2019-03-22
+ */
+ private LinearUnit getLinearUnit(final String name) {
+ // see if I am using a function-unit like tempC(100)
+ if (name.contains("(") && name.contains(")")) {
+ // break it into function name and value
+ final List parts = Arrays.asList(name.split("\\("));
+ if (parts.size() != 2)
+ throw new IllegalArgumentException("Format nonlinear units like: unit(value).");
+
+ // solve the function
+ final Unit unit = this.getUnit(parts.get(0));
+ final double value = Double.parseDouble(parts.get(1).substring(0, parts.get(1).length() - 1));
+ return unit.getBase().times(unit.convertToBase(value));
+ } else {
+ // get a linear unit
+ final Unit unit = this.getUnit(name);
+ if (unit instanceof LinearUnit)
+ return (LinearUnit) unit;
+ else
+ throw new IllegalArgumentException(String.format("%s is not a linear unit.", name));
+ }
+ }
+
/**
* Gets a unit prefix from the database from its name
*
@@ -383,55 +444,60 @@ public final class UnitsDatabase {
* @since v0.1.0
*/
public Unit getUnit(final String name) {
- if (name.contains("^")) {
- final String[] baseAndExponent = name.split("\\^");
-
- LinearUnit base;
- try {
- base = SI.SI.getBaseUnit(UnitDimension.EMPTY).times(Double.parseDouble(baseAndExponent[0]));
- } catch (final NumberFormatException e2) {
- final Unit unit = this.getUnit(baseAndExponent[0]);
- if (unit instanceof LinearUnit) {
- base = (LinearUnit) unit;
- } else
- throw new IllegalArgumentException("Base of exponientation must be a linear or base unit.");
- }
+ try {
+ final double value = Double.parseDouble(name);
+ return SI.SI.getBaseUnit(UnitDimension.EMPTY).times(value);
+ } catch (final NumberFormatException e) {
+ if (name.contains("^")) {
+ final String[] baseAndExponent = name.split("\\^");
- final int exponent;
- try {
- exponent = Integer.parseInt(baseAndExponent[baseAndExponent.length - 1]);
- } catch (final NumberFormatException e2) {
- throw new IllegalArgumentException("Exponent must be an integer.");
- }
+ LinearUnit base;
+ try {
+ base = SI.SI.getBaseUnit(UnitDimension.EMPTY).times(Double.parseDouble(baseAndExponent[0]));
+ } catch (final NumberFormatException e2) {
+ final Unit unit = this.getUnit(baseAndExponent[0]);
+ if (unit instanceof LinearUnit) {
+ base = (LinearUnit) unit;
+ } else
+ throw new IllegalArgumentException("Base of exponientation must be a linear or base unit.");
+ }
- final LinearUnit exponentiated = base.toExponent(exponent);
- if (exponentiated.getConversionFactor() == 1)
- return exponentiated.getSystem().getBaseUnit(exponentiated.getDimension());
- else
- return exponentiated;
- } else {
- for (final String prefixName : this.prefixNameSet()) {
- // check for a prefix
- if (name.startsWith(prefixName)) {
- // prefix found! Make sure what comes after it is actually a unit!
- final String prefixless = name.substring(prefixName.length());
- if (this.containsUnitName(prefixless)) {
- // yep, it's a proper prefix! Get the unit!
- final Unit unit = this.getUnit(prefixless);
- final UnitPrefix prefix = this.getPrefix(prefixName);
-
- // Prefixes only work with linear and base units, so make sure it's one of those
- if (unit instanceof LinearUnit) {
- final LinearUnit linearUnit = (LinearUnit) unit;
- return linearUnit.times(prefix.getMultiplier());
- } else if (unit instanceof BaseUnit) {
- final BaseUnit baseUnit = (BaseUnit) unit;
- return baseUnit.times(prefix.getMultiplier());
+ final int exponent;
+ try {
+ exponent = Integer.parseInt(baseAndExponent[baseAndExponent.length - 1]);
+ } catch (final NumberFormatException e2) {
+ throw new IllegalArgumentException("Exponent must be an integer.");
+ }
+
+ final LinearUnit exponentiated = base.toExponent(exponent);
+ if (exponentiated.getConversionFactor() == 1)
+ return exponentiated.getSystem().getBaseUnit(exponentiated.getDimension());
+ else
+ return exponentiated;
+ } else {
+ for (final String prefixName : this.prefixNameSet()) {
+ // check for a prefix
+ if (name.startsWith(prefixName)) {
+ // prefix found! Make sure what comes after it is actually a unit!
+ final String prefixless = name.substring(prefixName.length());
+ if (this.containsUnitName(prefixless)) {
+ // yep, it's a proper prefix! Get the unit!
+ final Unit unit = this.getUnit(prefixless);
+ final UnitPrefix prefix = this.getPrefix(prefixName);
+
+ // Prefixes only work with linear and base units, so make sure it's one of those
+ if (unit instanceof LinearUnit) {
+ final LinearUnit linearUnit = (LinearUnit) unit;
+ return linearUnit.times(prefix.getMultiplier());
+ } else if (unit instanceof BaseUnit) {
+ final BaseUnit baseUnit = (BaseUnit) unit;
+ return baseUnit.times(prefix.getMultiplier());
+ }
}
}
}
+ return this.units.get(name);
}
- return this.units.get(name);
}
}
@@ -453,165 +519,39 @@ public final class UnitsDatabase {
* @throws IllegalArgumentException
* if the expression cannot be parsed
* @throws NullPointerException
- * if any argument is null
+ * if expression is null
* @since 2019-01-07
* @since v0.1.0
*/
public Unit getUnitFromExpression(final String expression) {
Objects.requireNonNull(expression, "expression must not be null.");
- // parse the expression
- // start with an "empty" unit then apply operations on it
- LinearUnit unit = SI.SI.getBaseUnit(UnitDimension.EMPTY);
- boolean dividing = false;
-
- // if I'm just creating an alias, just create one instead of going through the parsing process
- if (!expression.contains(" ") && !expression.contains("*") && !expression.contains("/")
- && !expression.contains("(") && !expression.contains(")") && !expression.contains("^")) {
- try {
- return SI.SI.getBaseUnit(UnitDimension.EMPTY).times(Double.parseDouble(expression));
- } catch (final NumberFormatException e) {
- if (!this.containsUnitName(expression))
- throw new IllegalArgumentException("Unrecognized unit name \"" + expression + "\".");
- return this.getUnit(expression);
- }
- }
-
- // \\* means "asterisk", * is reserved
- for (final String part : expression.replaceAll("\\*", " \\* ").replaceAll("/", " / ").replaceAll(" \\^", "\\^")
- .replaceAll("\\^ ", "\\^").split(" ")) {
- if ("".equals(part)) {
- continue;
- }
- // "unit1 unit2" is the same as "unit1 * unit2", so multiplication signs do nothing
- if ("*".equals(part)) {
- continue;
- }
- // When I reach a division sign, don't parse a unit, instead tell myself I'm going to divide the next
- // thing
- if ("/".equals(part) || "per".equals(part)) {
- dividing = true;
- continue;
- }
-
- try {
- final double partAsNumber = Double.parseDouble(part); // if this works, it's a number
- // this code should not throw any exceptions, so I'm going to throw AssertionErrors if it does
- try {
- if (dividing) {
- unit = unit.dividedBy(partAsNumber);
- dividing = false;
- } else {
- unit = unit.times(partAsNumber);
- }
- } catch (final Exception e) {
- throw new AssertionError(e);
- }
- } catch (final NumberFormatException e) {
- // it's a unit, try that
-
- if (part.contains("(") && part.endsWith(")")) {
- // the unitsfile is looking for a nonlinear unit
- final String[] unitAndValue = part.split("\\(");
-
- // this will work because I've checked that it contains a (
- final String unitName = unitAndValue[0];
- final String valueStringWithBracket = unitAndValue[unitAndValue.length - 1];
- final String valueString = valueStringWithBracket.substring(0, valueStringWithBracket.length() - 1);
- final double value;
-
- // try to get the value - else throw an error
- try {
- value = Double.parseDouble(valueString);
- } catch (final NumberFormatException e2) {
- throw new IllegalArgumentException("Unparseable value " + valueString);
- }
-
- // get this unit in a linear form
- if (!this.containsPrefixlessUnitName(unitName))
- throw new IllegalArgumentException("Unrecognized unit name \"" + part + "\".");
- final Unit partUnit = this.getPrefixlessUnit(unitName);
- final LinearUnit multiplier = partUnit.getBase().times(partUnit.convertToBase(value));
-
- // finally, add it to the expression
- if (dividing) {
- unit = unit.dividedBy(multiplier);
- dividing = false;
- } else {
- unit = unit.times(multiplier);
- }
- } else {
- // check for exponientation
- if (part.contains("^")) {
- final String[] valueAndExponent = part.split("\\^");
- // this will always work because of the contains check
- final String valueString = valueAndExponent[0];
- final String exponentString = valueAndExponent[valueAndExponent.length - 1];
-
- LinearUnit value;
-
- // first, try to get the value
- try {
- final double valueAsNumber = Double.parseDouble(valueString);
-
- value = SI.SI.getBaseUnit(UnitDimension.EMPTY).times(valueAsNumber);
- } catch (final NumberFormatException e2) {
-
- // look for a unit
- if (!this.containsUnitName(valueString))
- throw new IllegalArgumentException("Unrecognized unit name \"" + valueString + "\".");
- final Unit valueUnit = this.getUnit(valueString);
-
- // try to turn the value into a linear unit
- if (valueUnit instanceof LinearUnit) {
- value = (LinearUnit) valueUnit;
- } else
- throw new IllegalArgumentException("Only linear and base units can be exponientated.");
- }
-
- // now, try to get the exponent
- final int exponent;
- try {
- exponent = Integer.parseInt(exponentString);
- } catch (final NumberFormatException e2) {
- throw new IllegalArgumentException("Exponents must be integers.");
- }
-
- final LinearUnit exponientated = value.toExponent(exponent);
-
- if (dividing) {
- unit = unit.dividedBy(exponientated);
- dividing = false;
- } else {
- unit = unit.times(exponientated);
- }
- } else {
- // no exponent - look for a unit
- // the unitsfile is looking for a linear unit
- if (!this.containsUnitName(part))
- throw new IllegalArgumentException("Unrecognized unit name \"" + part + "\".");
- final Unit other = this.getUnit(part);
- if (other instanceof LinearUnit) {
- if (dividing) {
- unit = unit.dividedBy((LinearUnit) other);
- dividing = false;
- } else {
- unit = unit.times((LinearUnit) other);
- }
- } else
- throw new IllegalArgumentException(
- "Only linear or base units can be multiplied and divided. If you want to use a non-linear unit, try the format UNITNAME(VALUE).");
- }
- }
+ // attempt to get a unit as an alias first
+ if (this.containsUnitName(expression))
+ return this.getUnit(expression);
+
+ // force operators to have spaces
+ String modifiedExpression = expression;
+ modifiedExpression = modifiedExpression.replaceAll("\\+", " \\+ ");
+ modifiedExpression = modifiedExpression.replaceAll("-", " - ");
+ modifiedExpression = modifiedExpression.replaceAll("\\*", " \\* ");
+ modifiedExpression = modifiedExpression.replaceAll("/", " / ");
+ modifiedExpression = modifiedExpression.replaceAll("\\^", " \\^ ");
+
+ // fix broken spaces
+ modifiedExpression = modifiedExpression.replaceAll(" +", " ");
+
+ // the previous operation breaks negative numbers, fix them!
+ // (i.e. -2 becomes - 2)
+ for (int i = 2; i < modifiedExpression.length(); i++) {
+ if (modifiedExpression.charAt(i) == '-'
+ && Arrays.asList('+', '-', '*', '/', '^').contains(modifiedExpression.charAt(i - 2))) {
+ // found a broken negative number
+ modifiedExpression = modifiedExpression.substring(0, i + 1) + modifiedExpression.substring(i + 2);
}
}
- // replace conversion-factor-1 linear units with base units
- // this improves the autogenerated names, allowing them to use derived SI names
- if (unit != null && unit.getConversionFactor() == 1)
- return unit.getSystem().getBaseUnit(unit.getDimension());
- else
- return unit;
+ return this.unitExpressionParser.parseExpression(modifiedExpression);
}
/**
diff --git a/src/org/unitConverter/converterGUI/UnitConverterGUI.java b/src/org/unitConverter/converterGUI/UnitConverterGUI.java
index a70e971..867211c 100755
--- a/src/org/unitConverter/converterGUI/UnitConverterGUI.java
+++ b/src/org/unitConverter/converterGUI/UnitConverterGUI.java
@@ -175,7 +175,12 @@ final class UnitConverterGUI {
// try to parse to
final Unit to;
try {
- to = this.units.getUnitFromExpression(toUnitString);
+ // if it's a unit, convert to that
+ if (this.units.containsUnitName(toUnitString)) {
+ to = this.units.getUnit(toUnitString);
+ } else {
+ to = this.units.getUnitFromExpression(toUnitString);
+ }
} catch (final IllegalArgumentException e) {
this.view.showErrorDialog("Parse Error", "Could not recognize text in To entry: " + e.getMessage());
return;
diff --git a/src/org/unitConverter/math/ExpressionParser.java b/src/org/unitConverter/math/ExpressionParser.java
index e06a58b..f34a0c2 100644
--- a/src/org/unitConverter/math/ExpressionParser.java
+++ b/src/org/unitConverter/math/ExpressionParser.java
@@ -55,6 +55,13 @@ public final class ExpressionParser {
*/
private final Function objectObtainer;
+ /**
+ * The function of the space as an operator (like 3 x y)
+ *
+ * @since 2019-03-22
+ */
+ private String spaceFunction = null;
+
/**
* A map mapping operator strings to operator functions, for unary operators.
*
@@ -114,6 +121,24 @@ public final class ExpressionParser {
return this;
}
+ /**
+ * Adds a function for spaces. You must use the text of an existing binary operator.
+ *
+ * @param operator
+ * text of operator to use
+ * @return this builder
+ * @since 2019-03-22
+ */
+ public Builder addSpaceFunction(final String operator) {
+ Objects.requireNonNull(operator, "operator must not be null.");
+
+ if (!this.binaryOperators.containsKey(operator))
+ throw new IllegalArgumentException(String.format("Could not find binary operator '%s'", operator));
+
+ this.spaceFunction = operator;
+ return this;
+ }
+
/**
* Adds a unary operator to the builder.
*
@@ -148,7 +173,8 @@ public final class ExpressionParser {
* @since 2019-03-17
*/
public ExpressionParser build() {
- return new ExpressionParser<>(this.objectObtainer, this.unaryOperators, this.binaryOperators);
+ return new ExpressionParser<>(this.objectObtainer, this.unaryOperators, this.binaryOperators,
+ this.spaceFunction);
}
}
@@ -330,34 +356,6 @@ public final class ExpressionParser {
throw new IllegalArgumentException("No matching bracket found.");
}
- public static void main(final String[] args) {
- final ExpressionParser numberParser = new ExpressionParser.Builder<>(Integer::parseInt)
- .addBinaryOperator("+", (o1, o2) -> o1 + o2, 0).addBinaryOperator("*", (o1, o2) -> o1 * o2, 1)
- .addBinaryOperator("^", (o1, o2) -> (int) Math.pow(o1, o2), 2).build();
- System.out.println(numberParser.convertExpressionToReversePolish("(1 + 2) ^ 5 * 3"));
- System.out.println(numberParser.parseExpression("(1 + 2) ^ 5 * 3")); // 729
- }
-
- /**
- * Swaps two elements in a list. Modifies the list passed in instead of returning a modified list.
- *
- * @param list
- * list to swap elements
- * @param firstIndex
- * index of first element to swap
- * @param otherIndex
- * index of other element to swap
- * @throws NullPointerException
- * if list is null
- * @since 2019-03-20
- */
- private static void swap(final List list, final int firstIndex, final int otherIndex) {
- Objects.requireNonNull(list, "list must not be null.");
- final E temp = list.get(firstIndex);
- list.set(firstIndex, list.get(otherIndex));
- list.set(otherIndex, temp);
- }
-
/**
* A function that obtains a parseable object from a string. For example, an integer {@code ExpressionParser} would
* use {@code Integer::parseInt}.
@@ -380,21 +378,33 @@ public final class ExpressionParser {
*/
private final Map> binaryOperators;
+ /**
+ * The operator for space, or null if spaces have no function.
+ *
+ * @since 2019-03-22
+ */
+ private final String spaceOperator;
+
/**
* Creates the {@code ExpressionParser}.
*
* @param objectObtainer
* function to get objects from strings
* @param unaryOperators
- * operators available to the parser
+ * unary operators available to the parser
+ * @param binaryOperators
+ * binary operators available to the parser
+ * @param spaceOperator
+ * operator used by spaces
* @since 2019-03-14
*/
private ExpressionParser(final Function objectObtainer,
final Map> unaryOperators,
- final Map> binaryOperators) {
+ final Map> binaryOperators, final String spaceOperator) {
this.objectObtainer = objectObtainer;
this.unaryOperators = unaryOperators;
this.binaryOperators = binaryOperators;
+ this.spaceOperator = spaceOperator;
}
/**
@@ -422,10 +432,26 @@ public final class ExpressionParser {
while (partialExpression.indexOf(OPENING_BRACKET) != -1) {
final int openingBracketPosition = partialExpression.indexOf(OPENING_BRACKET);
final int closingBracketPosition = findBracketPair(partialExpression, openingBracketPosition);
- components.addAll(Arrays.asList(partialExpression.substring(0, openingBracketPosition).split(" ")));
- components.add(this.convertExpressionToReversePolish(
- partialExpression.substring(openingBracketPosition + 1, closingBracketPosition)));
- partialExpression = partialExpression.substring(closingBracketPosition + 1);
+
+ // check for function
+ if (openingBracketPosition > 0 && partialExpression.charAt(openingBracketPosition - 1) != ' ') {
+ // function like sin(2) or tempF(32)
+ // find the position of the last space
+ int spacePosition = openingBracketPosition;
+ while (spacePosition >= 0 && partialExpression.charAt(spacePosition) != ' ') {
+ spacePosition--;
+ }
+ // then split the function into pre-function and function, using the space position
+ components.addAll(Arrays.asList(partialExpression.substring(0, spacePosition + 1).split(" ")));
+ components.add(partialExpression.substring(spacePosition + 1, closingBracketPosition + 1));
+ partialExpression = partialExpression.substring(closingBracketPosition + 1);
+ } else {
+ // normal brackets like (1 + 2) * (3 / 5)
+ components.addAll(Arrays.asList(partialExpression.substring(0, openingBracketPosition).split(" ")));
+ components.add(this.convertExpressionToReversePolish(
+ partialExpression.substring(openingBracketPosition + 1, closingBracketPosition)));
+ partialExpression = partialExpression.substring(closingBracketPosition + 1);
+ }
}
// add everything else
@@ -436,6 +462,16 @@ public final class ExpressionParser {
components.remove("");
}
+ // deal with space multiplication (x y)
+ if (this.spaceOperator != null) {
+ for (int i = 0; i < components.size() - 1; i++) {
+ if (this.getTokenType(components.get(i)) == TokenType.OBJECT
+ && this.getTokenType(components.get(i + 1)) == TokenType.OBJECT) {
+ components.add(++i, this.spaceOperator);
+ }
+ }
+ }
+
// turn the expression into reverse Polish
while (true) {
final int highestPriorityOperatorPosition = this.findHighestPriorityOperatorPosition(components);
@@ -472,7 +508,7 @@ public final class ExpressionParser {
}
return expressionRPN;
- // TODO method stub org.unitConverter.expressionParser.ExpressionParser.convertExpressionToPolish(expression)
+ // TODO document org.unitConverter.expressionParser.ExpressionParser.convertExpressionToPolish(expression)
}
/**
diff --git a/unitsfile.txt b/unitsfile.txt
index 78e51f7..2455c8a 100755
--- a/unitsfile.txt
+++ b/unitsfile.txt
@@ -136,7 +136,7 @@ steradian m^2 / m^2
sr steradian
degree 360 / tau * radian
deg degree
-° degree
+° degree
# Nonlinear units, which are not supported by the file reader and must be defined manually
# Use tempC(100) for 100 degrees Celsius
--
cgit v1.2.3
From 91ee53876aeeb52e980dd1fa976fae06d890ba19 Mon Sep 17 00:00:00 2001
From: Adrien Hopkins
Date: Wed, 10 Apr 2019 19:31:59 -0400
Subject: Removed AbstractUnit's unit counting functionnality.
The startup unit count is now performed by the UnitDatabase.
---
src/org/unitConverter/UnitsDatabase.java | 15 ++++--
.../converterGUI/UnitConverterGUI.java | 18 ++++++--
src/org/unitConverter/math/ExpressionParser.java | 3 ++
src/org/unitConverter/unit/AbstractUnit.java | 54 ----------------------
src/org/unitConverter/unit/BaseUnit.java | 8 ++++
5 files changed, 35 insertions(+), 63 deletions(-)
(limited to 'src/org/unitConverter/UnitsDatabase.java')
diff --git a/src/org/unitConverter/UnitsDatabase.java b/src/org/unitConverter/UnitsDatabase.java
index 481ce93..290a425 100755
--- a/src/org/unitConverter/UnitsDatabase.java
+++ b/src/org/unitConverter/UnitsDatabase.java
@@ -24,6 +24,7 @@ import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -32,7 +33,6 @@ import java.util.Set;
import org.unitConverter.dimension.UnitDimension;
import org.unitConverter.math.DecimalComparison;
import org.unitConverter.math.ExpressionParser;
-import org.unitConverter.unit.AbstractUnit;
import org.unitConverter.unit.BaseUnit;
import org.unitConverter.unit.DefaultUnitPrefix;
import org.unitConverter.unit.LinearUnit;
@@ -186,10 +186,7 @@ public final class UnitsDatabase {
System.err.printf("Parsing error on line %d:%n", lineCounter);
throw e;
}
- AbstractUnit.incrementUnitCounter();
- if (unit instanceof BaseUnit) {
- AbstractUnit.incrementBaseUnitCounter();
- }
+
this.addUnit(name, unit);
}
}
@@ -563,6 +560,14 @@ public final class UnitsDatabase {
return Collections.unmodifiableSet(this.units.keySet());
}
+ /**
+ * @return an immutable set of all of the units in this database, ignoring prefixes.
+ * @since 2019-04-10
+ */
+ public Set prefixlessUnitSet() {
+ return Collections.unmodifiableSet(new HashSet<>(this.units.values()));
+ }
+
/**
* @return an immutable set of all of the prefix names in this database
* @since 2019-01-14
diff --git a/src/org/unitConverter/converterGUI/UnitConverterGUI.java b/src/org/unitConverter/converterGUI/UnitConverterGUI.java
index 867211c..fd40ff4 100755
--- a/src/org/unitConverter/converterGUI/UnitConverterGUI.java
+++ b/src/org/unitConverter/converterGUI/UnitConverterGUI.java
@@ -47,7 +47,7 @@ import javax.swing.ListSelectionModel;
import org.unitConverter.UnitsDatabase;
import org.unitConverter.dimension.StandardDimensions;
import org.unitConverter.dimension.UnitDimension;
-import org.unitConverter.unit.AbstractUnit;
+import org.unitConverter.unit.BaseUnit;
import org.unitConverter.unit.NonlinearUnits;
import org.unitConverter.unit.SI;
import org.unitConverter.unit.Unit;
@@ -143,8 +143,13 @@ final class UnitConverterGUI {
this.prefixNamesFiltered = new DelegateListModel<>(new ArrayList<>(this.units.prefixNameSet()));
this.prefixNamesFiltered.sort(this.prefixNameComparator); // sorts it using my comparator
- System.out.printf("Successfully loaded %d units (%d base units)", AbstractUnit.getUnitCount(),
- AbstractUnit.getBaseUnitCount());
+ // a Predicate that returns true iff the argument is a full base unit
+ final Predicate isFullBase = unit -> unit instanceof BaseUnit && ((BaseUnit) unit).isFullBase();
+
+ // print out unit counts
+ System.out.printf("Successfully loaded %d units with %d unit names (%d base units).%n",
+ this.units.prefixlessUnitSet().size(), this.units.prefixlessUnitNameSet().size(),
+ this.units.prefixlessUnitSet().stream().filter(isFullBase).count());
}
/**
@@ -162,6 +167,11 @@ final class UnitConverterGUI {
final String fromUnitString = this.view.getFromText();
final String toUnitString = this.view.getToText();
+ if (fromUnitString.isEmpty()) {
+ this.view.showErrorDialog("Parse Error", "Please enter a unit expression in the From: box.");
+ return;
+ }
+
// try to parse from
final Unit from;
try {
@@ -175,8 +185,8 @@ final class UnitConverterGUI {
// try to parse to
final Unit to;
try {
- // if it's a unit, convert to that
if (this.units.containsUnitName(toUnitString)) {
+ // if it's a unit, convert to that
to = this.units.getUnit(toUnitString);
} else {
to = this.units.getUnitFromExpression(toUnitString);
diff --git a/src/org/unitConverter/math/ExpressionParser.java b/src/org/unitConverter/math/ExpressionParser.java
index f34a0c2..b56fa71 100644
--- a/src/org/unitConverter/math/ExpressionParser.java
+++ b/src/org/unitConverter/math/ExpressionParser.java
@@ -479,6 +479,9 @@ public final class ExpressionParser {
break;
}
+ // swap components based on what kind of operator there is
+ // 1 + 2 becomes 2 1 +
+ // - 1 becomes 1 -
switch (this.getTokenType(components.get(highestPriorityOperatorPosition))) {
case UNARY_OPERATOR:
final String unaryOperator = components.remove(highestPriorityOperatorPosition);
diff --git a/src/org/unitConverter/unit/AbstractUnit.java b/src/org/unitConverter/unit/AbstractUnit.java
index 6088960..a0d6f7e 100644
--- a/src/org/unitConverter/unit/AbstractUnit.java
+++ b/src/org/unitConverter/unit/AbstractUnit.java
@@ -28,60 +28,6 @@ import org.unitConverter.dimension.UnitDimension;
* @since v0.1.0
*/
public abstract class AbstractUnit implements Unit {
- /**
- * The number of units created, including base units.
- *
- * @since 2019-01-02
- * @since v0.1.0
- */
- private static long unitCount = 0;
-
- /**
- * The number of base units created.
- *
- * @since 2019-01-02
- * @since v0.1.0
- */
- private static long baseUnitCount = 0;
-
- /**
- * @return number of base units created
- * @since 2019-01-02
- * @since v0.1.0
- */
- public static final long getBaseUnitCount() {
- return baseUnitCount;
- }
-
- /**
- * @return number of units created
- * @since 2019-01-02
- * @since v0.1.0
- */
- public static final long getUnitCount() {
- return unitCount;
- }
-
- /**
- * Increments the number of base units.
- *
- * @since 2019-01-15
- * @since v0.1.0
- */
- public static final void incrementBaseUnitCounter() {
- baseUnitCount++;
- }
-
- /**
- * Increments the number of units.
- *
- * @since 2019-01-15
- * @since v0.1.0
- */
- public static final void incrementUnitCounter() {
- unitCount++;
- }
-
/**
* The dimension, or what the unit measures.
*
diff --git a/src/org/unitConverter/unit/BaseUnit.java b/src/org/unitConverter/unit/BaseUnit.java
index 2def48e..643272f 100755
--- a/src/org/unitConverter/unit/BaseUnit.java
+++ b/src/org/unitConverter/unit/BaseUnit.java
@@ -84,6 +84,14 @@ public final class BaseUnit extends LinearUnit {
return new BaseUnit(this.getDimension().dividedBy(divisor.getDimension()), this.getSystem());
}
+ /**
+ * @return true if the unit is a "full base" unit like the metre or second.
+ * @since 2019-04-10
+ */
+ public final boolean isFullBase() {
+ return this.isFullBase;
+ }
+
/**
* Returns the product of this unit and another.
*
--
cgit v1.2.3
From ec036fdad931fbbd7dec28b864150f8668e91b41 Mon Sep 17 00:00:00 2001
From: Adrien Hopkins
Date: Wed, 10 Apr 2019 21:00:49 -0400
Subject: Edited dimension database code and improved comments.
getDimension() now works with exponents,
Added a dimension parser,
comments can now be in the middle of lines
---
CHANGELOG.org | 1 +
src/org/unitConverter/UnitsDatabase.java | 181 +++++++++++++++++++------------
2 files changed, 115 insertions(+), 67 deletions(-)
(limited to 'src/org/unitConverter/UnitsDatabase.java')
diff --git a/CHANGELOG.org b/CHANGELOG.org
index 95dc57a..8a79c46 100644
--- a/CHANGELOG.org
+++ b/CHANGELOG.org
@@ -6,6 +6,7 @@ All notable changes in this project will be shown in this file.
- Moved project to Maven
- Downgraded JUnit to 4.11
- BaseUnit is now a subclass of LinearUnit
+ - Comments can now start in the middle of lines
*** Added
- GUI for a selection-based unit converter
- The UnitDatabase now stores dimensions.
diff --git a/src/org/unitConverter/UnitsDatabase.java b/src/org/unitConverter/UnitsDatabase.java
index 290a425..69b25d8 100755
--- a/src/org/unitConverter/UnitsDatabase.java
+++ b/src/org/unitConverter/UnitsDatabase.java
@@ -48,6 +48,32 @@ import org.unitConverter.unit.UnitPrefix;
* @since v0.1.0
*/
public final class UnitsDatabase {
+ /**
+ * The exponent operator
+ *
+ * @param base
+ * base of exponentiation
+ * @param exponentUnit
+ * exponent
+ * @return result
+ * @since 2019-04-10
+ */
+ private static final LinearUnit exponent(final LinearUnit base, final LinearUnit exponentUnit) {
+ // exponent function - first check if o2 is a number,
+ if (exponentUnit.getBase().equals(SI.SI.getBaseUnit(UnitDimension.EMPTY))) {
+ // then check if it is an integer,
+ final double exponent = exponentUnit.getConversionFactor();
+ if (DecimalComparison.equals(exponent % 1, 0))
+ // then exponentiate
+ return base.toExponent((int) (exponent % 1 + 0.5));
+ else
+ // not an integer
+ throw new UnsupportedOperationException("Decimal exponents are currently not supported.");
+ } else
+ // not a number
+ throw new IllegalArgumentException("Exponents must be numbers.");
+ }
+
/**
* The units in this system.
*
@@ -80,22 +106,12 @@ public final class UnitsDatabase {
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) -> {
- // exponent function - first check if o2 is a number,
- if (o2.getBase().equals(SI.SI.getBaseUnit(UnitDimension.EMPTY))) {
- // then check if it is an integer,
- final double exponent = o2.getConversionFactor();
- if (DecimalComparison.equals(exponent % 1, 0))
- // then exponentiate
- return o1.toExponent((int) (exponent % 1 + 0.5));
- else
- // not an integer
- throw new UnsupportedOperationException(
- "Decimal exponents are currently not supported.");
- } else
- // not a number
- throw new IllegalArgumentException("Exponents must be numbers.");
- }, 2).build();
+ .addBinaryOperator("/", (o1, o2) -> o1.dividedBy(o2), 1)
+ .addBinaryOperator("^", UnitsDatabase::exponent, 2).build();
+
+ private final ExpressionParser unitDimensionParser = new ExpressionParser.Builder<>(
+ this::getDimension).addBinaryOperator("*", (o1, o2) -> o1.times(o2), 0).addSpaceFunction("*")
+ .addBinaryOperator("/", (o1, o2) -> o1.dividedBy(o2), 0).build();
/**
* Creates the {@code UnitsDatabase}.
@@ -118,7 +134,7 @@ public final class UnitsDatabase {
*
* Allowed exceptions:
*
*
* @author Adrien Hopkins
* @since 2018-12-22
--
cgit v1.2.3
From e0c5021a9ba85debf0c0722d78f75a0dbcc8376b Mon Sep 17 00:00:00 2001
From: Adrien Hopkins
Date: Sat, 13 Apr 2019 12:17:14 -0400
Subject: Implemented the dimension-based converter.
Also added a search box list, and fixed a bug with dimension exponentiation.
---
dimensionfile.txt | 10 +-
src/org/unitConverter/UnitsDatabase.java | 2 +-
.../converterGUI/DelegateListModel.java | 10 +
.../converterGUI/MutablePredicate.java | 60 ++++++
.../unitConverter/converterGUI/SearchBoxList.java | 205 +++++++++++++++++++++
.../converterGUI/UnitConverterGUI.java | 180 ++++++++++++++----
unitsfile.txt | 20 +-
7 files changed, 444 insertions(+), 43 deletions(-)
create mode 100644 src/org/unitConverter/converterGUI/MutablePredicate.java
create mode 100644 src/org/unitConverter/converterGUI/SearchBoxList.java
(limited to 'src/org/unitConverter/UnitsDatabase.java')
diff --git a/dimensionfile.txt b/dimensionfile.txt
index d3c068c..7a1da10 100644
--- a/dimensionfile.txt
+++ b/dimensionfile.txt
@@ -4,10 +4,14 @@
# ! means "look for an existing dimension which I will load at the start"
# This is necessary because every dimension must be defined by others, and I need somewhere to start.
+# I have excluded electric current and quantity since their units are exclusively SI.
+
LENGTH !
MASS !
TIME !
-ELECTRIC_CURRENT !
TEMPERATURE !
-QUANTITY !
-LUMINOUS_INTENSITY !
\ No newline at end of file
+LUMINOUS_INTENSITY !
+
+# Derived Dimensions
+VELOCITY LENGTH / TIME
+ENERGY MASS * VELOCITY^2
\ No newline at end of file
diff --git a/src/org/unitConverter/UnitsDatabase.java b/src/org/unitConverter/UnitsDatabase.java
index 626f145..c3d3131 100755
--- a/src/org/unitConverter/UnitsDatabase.java
+++ b/src/org/unitConverter/UnitsDatabase.java
@@ -65,7 +65,7 @@ public final class UnitsDatabase {
final double exponent = exponentUnit.getConversionFactor();
if (DecimalComparison.equals(exponent % 1, 0))
// then exponentiate
- return base.toExponent((int) (exponent % 1 + 0.5));
+ return base.toExponent((int) (exponent + 0.5));
else
// not an integer
throw new UnsupportedOperationException("Decimal exponents are currently not supported.");
diff --git a/src/org/unitConverter/converterGUI/DelegateListModel.java b/src/org/unitConverter/converterGUI/DelegateListModel.java
index e375126..b80f63d 100755
--- a/src/org/unitConverter/converterGUI/DelegateListModel.java
+++ b/src/org/unitConverter/converterGUI/DelegateListModel.java
@@ -16,6 +16,7 @@
*/
package org.unitConverter.converterGUI;
+import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
@@ -49,6 +50,15 @@ final class DelegateListModel extends AbstractListModel implements List
*/
private final List delegate;
+ /**
+ * Creates an empty {@code DelegateListModel}.
+ *
+ * @since 2019-04-13
+ */
+ public DelegateListModel() {
+ this(new ArrayList<>());
+ }
+
/**
* Creates the {@code DelegateListModel}.
*
diff --git a/src/org/unitConverter/converterGUI/MutablePredicate.java b/src/org/unitConverter/converterGUI/MutablePredicate.java
new file mode 100644
index 0000000..157903c
--- /dev/null
+++ b/src/org/unitConverter/converterGUI/MutablePredicate.java
@@ -0,0 +1,60 @@
+/**
+ * Copyright (C) 2019 Adrien Hopkins
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.unitConverter.converterGUI;
+
+import java.util.function.Predicate;
+
+/**
+ * A container for a predicate, which can be changed later.
+ *
+ * @author Adrien Hopkins
+ * @since 2019-04-13
+ */
+final class MutablePredicate implements Predicate {
+ private Predicate predicate;
+
+ /**
+ * Creates the {@code MutablePredicate}.
+ *
+ * @since 2019-04-13
+ */
+ public MutablePredicate(final Predicate predicate) {
+ this.predicate = predicate;
+ }
+
+ /**
+ * @return predicate
+ * @since 2019-04-13
+ */
+ public final Predicate getPredicate() {
+ return this.predicate;
+ }
+
+ /**
+ * @param predicate
+ * new value of predicate
+ * @since 2019-04-13
+ */
+ public final void setPredicate(final Predicate predicate) {
+ this.predicate = predicate;
+ }
+
+ @Override
+ public boolean test(final T t) {
+ return this.predicate.test(t);
+ }
+}
diff --git a/src/org/unitConverter/converterGUI/SearchBoxList.java b/src/org/unitConverter/converterGUI/SearchBoxList.java
new file mode 100644
index 0000000..7d3b748
--- /dev/null
+++ b/src/org/unitConverter/converterGUI/SearchBoxList.java
@@ -0,0 +1,205 @@
+/**
+ * Copyright (C) 2019 Adrien Hopkins
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.unitConverter.converterGUI;
+
+import java.awt.BorderLayout;
+import java.awt.Color;
+import java.awt.event.FocusEvent;
+import java.awt.event.FocusListener;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.function.Predicate;
+
+import javax.swing.JList;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.JTextField;
+
+/**
+ * @author Adrien Hopkins
+ * @since 2019-04-13
+ */
+final class SearchBoxList extends JPanel {
+
+ /**
+ * @since 2019-04-13
+ */
+ private static final long serialVersionUID = 6226930279415983433L;
+
+ /**
+ * The text to place in an empty search box.
+ */
+ private static final String EMPTY_TEXT = "Search...";
+ /**
+ * The color to use for an empty foreground.
+ */
+ private static final Color EMPTY_FOREGROUND = new Color(192, 192, 192);
+
+ // the components
+ private final Collection itemsToFilter;
+ private final DelegateListModel listModel;
+ private final JTextField searchBox;
+ private final JList searchItems;
+
+ private boolean searchBoxEmpty = true;
+
+ // I need to do this because, for some reason, Swing is auto-focusing my search box without triggering a focus
+ // event.
+ private boolean searchBoxFocused = false;
+
+ private Predicate searchFilter = o -> true;
+
+ /**
+ * Creates the {@code SearchBoxList}.
+ *
+ * @since 2019-04-13
+ */
+ public SearchBoxList(final Collection itemsToFilter) {
+ super(new BorderLayout(), true);
+ this.itemsToFilter = itemsToFilter;
+
+ // create the components
+ this.listModel = new DelegateListModel<>(new ArrayList<>(itemsToFilter));
+ this.searchItems = new JList<>(this.listModel);
+
+ this.searchBox = new JTextField(EMPTY_TEXT);
+ this.searchBox.setForeground(EMPTY_FOREGROUND);
+
+ // add them to the panel
+ this.add(this.searchBox, BorderLayout.PAGE_START);
+ this.add(new JScrollPane(this.searchItems), BorderLayout.CENTER);
+
+ // set up the search box
+ this.searchBox.addFocusListener(new FocusListener() {
+ @Override
+ public void focusGained(final FocusEvent e) {
+ SearchBoxList.this.searchBoxFocusGained(e);
+ }
+
+ @Override
+ public void focusLost(final FocusEvent e) {
+ SearchBoxList.this.searchBoxFocusLost(e);
+ }
+ });
+
+ this.searchBox.addCaretListener(e -> this.searchBoxTextChanged());
+ this.searchBoxEmpty = true;
+ }
+
+ /**
+ * Adds an additional filter for searching.
+ *
+ * @param filter
+ * filter to add.
+ * @since 2019-04-13
+ */
+ public void addSearchFilter(final Predicate filter) {
+ this.searchFilter = this.searchFilter.and(filter);
+ }
+
+ /**
+ * Resets the search filter.
+ *
+ * @since 2019-04-13
+ */
+ public void clearSearchFilters() {
+ this.searchFilter = o -> true;
+ }
+
+ /**
+ * @return value selected in item list
+ * @since 2019-04-13
+ */
+ public String getSelectedValue() {
+ return this.searchItems.getSelectedValue();
+ }
+
+ /**
+ * Re-applies the filters.
+ *
+ * @since 2019-04-13
+ */
+ public void reapplyFilter() {
+ final String searchText = this.searchBoxEmpty ? "" : this.searchBox.getText();
+ final FilterComparator comparator = new FilterComparator(searchText);
+
+ this.listModel.clear();
+ this.itemsToFilter.forEach(string -> {
+ if (string.toLowerCase().contains(searchText.toLowerCase())) {
+ this.listModel.add(string);
+ }
+ });
+
+ // applies the custom filters
+ this.listModel.removeIf(this.searchFilter.negate());
+
+ // sorts the remaining items
+ this.listModel.sort(comparator);
+ }
+
+ /**
+ * Runs whenever the search box gains focus.
+ *
+ * @param e
+ * focus event
+ * @since 2019-04-13
+ */
+ private void searchBoxFocusGained(final FocusEvent e) {
+ this.searchBoxFocused = true;
+ if (this.searchBoxEmpty) {
+ this.searchBox.setText("");
+ this.searchBox.setForeground(Color.BLACK);
+ }
+ }
+
+ /**
+ * Runs whenever the search box loses focus.
+ *
+ * @param e
+ * focus event
+ * @since 2019-04-13
+ */
+ private void searchBoxFocusLost(final FocusEvent e) {
+ this.searchBoxFocused = false;
+ if (this.searchBoxEmpty) {
+ this.searchBox.setText(EMPTY_TEXT);
+ this.searchBox.setForeground(EMPTY_FOREGROUND);
+ }
+ }
+
+ private void searchBoxTextChanged() {
+ if (this.searchBoxFocused) {
+ this.searchBoxEmpty = this.searchBox.getText().equals("");
+ }
+ final String searchText = this.searchBoxEmpty ? "" : this.searchBox.getText();
+ final FilterComparator comparator = new FilterComparator(searchText);
+
+ // initialize list with items that match the filter then sort
+ this.listModel.clear();
+ this.itemsToFilter.forEach(string -> {
+ if (string.toLowerCase().contains(searchText.toLowerCase())) {
+ this.listModel.add(string);
+ }
+ });
+
+ // applies the custom filters
+ this.listModel.removeIf(this.searchFilter.negate());
+
+ // sorts the remaining items
+ this.listModel.sort(comparator);
+ }
+}
diff --git a/src/org/unitConverter/converterGUI/UnitConverterGUI.java b/src/org/unitConverter/converterGUI/UnitConverterGUI.java
index 4f5ebeb..9314510 100755
--- a/src/org/unitConverter/converterGUI/UnitConverterGUI.java
+++ b/src/org/unitConverter/converterGUI/UnitConverterGUI.java
@@ -25,6 +25,7 @@ import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
+import java.util.Set;
import java.util.function.Predicate;
import javax.swing.BorderFactory;
@@ -116,9 +117,7 @@ final class UnitConverterGUI {
this.units.addDimension("LENGTH", StandardDimensions.LENGTH);
this.units.addDimension("MASS", StandardDimensions.MASS);
this.units.addDimension("TIME", StandardDimensions.TIME);
- this.units.addDimension("ELECTRIC_CURRENT", StandardDimensions.ELECTRIC_CURRENT);
this.units.addDimension("TEMPERATURE", StandardDimensions.TEMPERATURE);
- this.units.addDimension("QUANTITY", StandardDimensions.QUANTITY);
this.units.addDimension("LUMINOUS_INTENSITY", StandardDimensions.LUMINOUS_INTENSITY);
this.units.loadUnitsFile(new File("unitsfile.txt"));
@@ -238,6 +237,52 @@ final class UnitConverterGUI {
this.view.setOutputText(String.format("%s = %s %s", fromUnitString, output, toUnitString));
}
+ /**
+ * Converts in the dimension-based converter
+ *
+ * @since 2019-04-13
+ */
+ public final void convertDimensionBased() {
+ final String fromSelection = this.view.getFromSelection();
+ if (fromSelection == null) {
+ this.view.showErrorDialog("Error", "No unit selected in From field");
+ return;
+ }
+ final String toSelection = this.view.getToSelection();
+ if (toSelection == null) {
+ this.view.showErrorDialog("Error", "No unit selected in To field");
+ return;
+ }
+
+ final Unit from = this.units.getUnit(fromSelection);
+ final Unit to = this.units.getUnit(toSelection);
+
+ final String input = this.view.getDimensionBasedInput();
+ if (input.equals("")) {
+ this.view.showErrorDialog("Error", "No value to convert entered.");
+ return;
+ }
+ final double beforeValue = Double.parseDouble(input);
+ final double value = to.convertFromBase(from.convertToBase(beforeValue));
+
+ // round value
+ final BigDecimal bigValue = new BigDecimal(value).round(new MathContext(this.significantFigures));
+ String output = bigValue.toString();
+
+ // remove trailing zeroes
+ if (output.contains(".")) {
+ while (output.endsWith("0")) {
+ output = output.substring(0, output.length() - 1);
+ }
+ if (output.endsWith(".")) {
+ output = output.substring(0, output.length() - 1);
+ }
+ }
+
+ this.view.setDimensionBasedOutputText(
+ String.format("%s %s = %s %s", input, fromSelection, output, toSelection));
+ }
+
/**
* @return a list of all of the unit dimensions
* @since 2019-04-13
@@ -365,6 +410,23 @@ final class UnitConverterGUI {
this.unitNamesFiltered.sort(new FilterComparator(filter));
}
+ /**
+ * Returns true if and only if the unit represented by {@code unitName} has the dimension represented by
+ * {@code dimensionName}.
+ *
+ * @param unitName
+ * name of unit to test
+ * @param dimensionName
+ * name of dimension to test
+ * @return whether unit has dimenision
+ * @since 2019-04-13
+ */
+ public boolean unitMatchesDimension(final String unitName, final String dimensionName) {
+ final Unit unit = this.units.getUnit(unitName);
+ final UnitDimension dimension = this.units.getDimension(dimensionName);
+ return unit.getDimension().equals(dimension);
+ }
+
/**
* Runs whenever a unit is selected in the viewer.
*
@@ -385,6 +447,10 @@ final class UnitConverterGUI {
this.view.setUnitTextBoxText(unit.toString());
}
}
+
+ public final Set unitNameSet() {
+ return this.units.prefixlessUnitNameSet();
+ }
}
private static class View {
@@ -405,6 +471,14 @@ final class UnitConverterGUI {
private final JTextField prefixFilterEntry;
/** The text box for prefix data in the prefix viewer */
private final JTextArea prefixTextBox;
+ /** The panel for "From" in the dimension-based converter */
+ private final SearchBoxList fromSearch;
+ /** The panel for "To" in the dimension-based converter */
+ private final SearchBoxList toSearch;
+ /** The panel for inputting values in the dimension-based converter */
+ private final JTextField valueInput;
+ /** The output area in the dimension-based converter */
+ private final JTextArea dimensionBasedOutput;
/** The "From" entry in the conversion panel */
private final JTextField fromEntry;
/** The "To" entry in the conversion panel */
@@ -430,6 +504,10 @@ final class UnitConverterGUI {
this.unitTextBox = new JTextArea();
this.prefixFilterEntry = new JTextField();
this.prefixTextBox = new JTextArea();
+ this.fromSearch = new SearchBoxList(this.presenter.unitNameSet());
+ this.toSearch = new SearchBoxList(this.presenter.unitNameSet());
+ this.valueInput = new JFormattedTextField(new DecimalFormat("###############0.################"));
+ this.dimensionBasedOutput = new JTextArea(2, 32);
this.fromEntry = new JTextField();
this.toEntry = new JTextField();
this.output = new JTextArea(2, 32);
@@ -440,6 +518,22 @@ final class UnitConverterGUI {
this.frame.pack();
}
+ /**
+ * @return value in dimension-based converter
+ * @since 2019-04-13
+ */
+ public String getDimensionBasedInput() {
+ return this.valueInput.getText();
+ }
+
+ /**
+ * @return selection in "From" selector in dimension-based converter
+ * @since 2019-04-13
+ */
+ public String getFromSelection() {
+ return this.fromSearch.getSelectedValue();
+ }
+
/**
* @return text in "From" box in converter panel
* @since 2019-01-15
@@ -467,6 +561,14 @@ final class UnitConverterGUI {
return this.prefixNameList.getSelectedIndex();
}
+ /**
+ * @return selection in "To" selector in dimension-based converter
+ * @since 2019-04-13
+ */
+ public String getToSelection() {
+ return this.toSearch.getSelectedValue();
+ }
+
/**
* @return text in "To" box in converter panel
* @since 2019-01-26
@@ -531,22 +633,17 @@ final class UnitConverterGUI {
inputPanel.setLayout(new GridLayout(1, 3));
- { // panel for From things
- final JPanel fromPanel = new JPanel();
- inputPanel.add(fromPanel);
+ final JComboBox dimensionSelector = new JComboBox<>(
+ this.presenter.dimensionNameList().toArray(new String[0]));
+ dimensionSelector.setSelectedItem("LENGTH");
- fromPanel.setLayout(new BorderLayout());
+ // handle dimension filter
+ final MutablePredicate dimensionFilter = new MutablePredicate<>(s -> true);
- { // search box for from
- final JTextField fromSearch = new JTextField("Search...");
- fromPanel.add(fromSearch, BorderLayout.PAGE_START);
- }
+ // panel for From things
+ inputPanel.add(this.fromSearch);
- { // list for From units
- final JList fromList = new JList<>();
- fromPanel.add(fromList, BorderLayout.CENTER);
- }
- }
+ this.fromSearch.addSearchFilter(dimensionFilter);
{ // for dimension selector and arrow that represents conversion
final JPanel inBetweenPanel = new JPanel();
@@ -555,10 +652,6 @@ final class UnitConverterGUI {
inBetweenPanel.setLayout(new BorderLayout());
{ // dimension selector
- final List dimensionNameList = this.presenter.dimensionNameList();
- dimensionNameList.add(0, "Select a dimension...");
- final JComboBox dimensionSelector = new JComboBox<>(
- dimensionNameList.toArray(new String[0]));
inBetweenPanel.add(dimensionSelector, BorderLayout.PAGE_START);
}
@@ -568,23 +661,25 @@ final class UnitConverterGUI {
}
}
- { // panel for To things
- final JPanel toPanel = new JPanel();
- inputPanel.add(toPanel);
+ // panel for To things
- toPanel.setLayout(new BorderLayout());
+ inputPanel.add(this.toSearch);
- { // search box for to
- final JTextField toSearch = new JTextField("Search...");
- toPanel.add(toSearch, BorderLayout.PAGE_START);
- }
+ this.toSearch.addSearchFilter(dimensionFilter);
- { // list for To units
- final JList toList = new JList<>();
- toPanel.add(toList, BorderLayout.CENTER);
- }
- }
+ // code for dimension filter
+ dimensionSelector.addItemListener(e -> {
+ dimensionFilter.setPredicate(string -> View.this.presenter.unitMatchesDimension(string,
+ (String) dimensionSelector.getSelectedItem()));
+ this.fromSearch.reapplyFilter();
+ this.toSearch.reapplyFilter();
+ });
+ // apply the item listener once because I have a default selection
+ dimensionFilter.setPredicate(string -> View.this.presenter.unitMatchesDimension(string,
+ (String) dimensionSelector.getSelectedItem()));
+ this.fromSearch.reapplyFilter();
+ this.toSearch.reapplyFilter();
}
{ // panel for submit and output, and also value entry
@@ -605,20 +700,20 @@ final class UnitConverterGUI {
}
{ // value to convert
- final JTextField valueInput = new JFormattedTextField(
- new DecimalFormat("###############0.################"));
- valueInputPanel.add(valueInput, BorderLayout.CENTER);
+ valueInputPanel.add(this.valueInput, BorderLayout.CENTER);
}
}
{ // button to convert
final JButton convertButton = new JButton("Convert");
outputPanel.add(convertButton);
+
+ convertButton.addActionListener(e -> this.presenter.convertDimensionBased());
}
{ // output of conversion
- final JLabel outputLabel = new JLabel();
- outputPanel.add(outputLabel);
+ outputPanel.add(this.dimensionBasedOutput);
+ this.dimensionBasedOutput.setEditable(false);
}
}
}
@@ -762,6 +857,17 @@ final class UnitConverterGUI {
}
}
+ /**
+ * Sets the text in the output of the dimension-based converter.
+ *
+ * @param text
+ * text to set
+ * @since 2019-04-13
+ */
+ public void setDimensionBasedOutputText(final String text) {
+ this.dimensionBasedOutput.setText(text);
+ }
+
/**
* Sets the text in the output of the conversion panel.
*
diff --git a/unitsfile.txt b/unitsfile.txt
index 2455c8a..14fb6fb 100755
--- a/unitsfile.txt
+++ b/unitsfile.txt
@@ -171,7 +171,7 @@ arcsecond 1 / 60 arcminute
arcsec arcsecond
# constants
-waterdensity 1 kilogram / litre
+waterdensity kilogram / litre
# Imperial length units
foot 0.3048 m
@@ -183,6 +183,10 @@ yd yard
mile 1760 yard
mi mile
+# Compressed notation
+kph km / hour
+mph mile / hour
+
# Imperial weight units
pound 0.45359237 kg
lb pound
@@ -231,4 +235,16 @@ metricpint 2 metriccup
pint metricpint
metricquart 2 metricpint
quart metricquart
-metricgallon 4 metricquart
\ No newline at end of file
+metricgallon 4 metricquart
+
+# Energy units
+calorie 4.18 J
+cal calorie
+Calorie kilocalorie
+Cal Calorie
+
+# Extra units to only include in the dimension-based converter
+m/s m / s
+km/h km / h
+ft/s foot / s
+mi/h mile / hour
\ No newline at end of file
--
cgit v1.2.3
From f0f4898f796b9cc26294ba9feb22692143d00a9e Mon Sep 17 00:00:00 2001
From: Adrien Hopkins
Date: Sat, 13 Apr 2019 15:55:49 -0400
Subject: Unit prefixes now have math methods, and use the expression parser.
---
CHANGELOG.org | 3 +-
src/org/unitConverter/UnitsDatabase.java | 119 ++++++++++++-----------------
src/org/unitConverter/unit/UnitPrefix.java | 36 +++++++++
3 files changed, 85 insertions(+), 73 deletions(-)
(limited to 'src/org/unitConverter/UnitsDatabase.java')
diff --git a/CHANGELOG.org b/CHANGELOG.org
index 46197dc..e7748ba 100644
--- a/CHANGELOG.org
+++ b/CHANGELOG.org
@@ -9,11 +9,12 @@ All notable changes in this project will be shown in this file.
- In unit files, Comments can now start in the middle of lines
- UnitsDatabase.addAllFromFile() has been renamed to loadUnitsFile()
*** Added
- - GUI for a selection-based unit converter
+ - A selection-based unit converter which allows you to select two units, input a value, and convert.
- The UnitDatabase now stores dimensions.
- A system to parse mathematical expressions, used to parse unit expressions.
- You can now add and subtract in unit expressions!
- Instructions for obtaining unit instances are provided in the relevant classes
+ - The UnitPrefix interface now provides default times, dividedBy and toExponent methods.
** v0.1.0
NOTE: At this stage, the API is subject to significant change.
*** Added
diff --git a/src/org/unitConverter/UnitsDatabase.java b/src/org/unitConverter/UnitsDatabase.java
index c3d3131..a7e6047 100755
--- a/src/org/unitConverter/UnitsDatabase.java
+++ b/src/org/unitConverter/UnitsDatabase.java
@@ -33,7 +33,6 @@ import java.util.Set;
import org.unitConverter.dimension.UnitDimension;
import org.unitConverter.math.DecimalComparison;
import org.unitConverter.math.ExpressionParser;
-import org.unitConverter.unit.BaseUnit;
import org.unitConverter.unit.DefaultUnitPrefix;
import org.unitConverter.unit.LinearUnit;
import org.unitConverter.unit.SI;
@@ -109,6 +108,21 @@ public final class UnitsDatabase {
.addBinaryOperator("/", (o1, o2) -> o1.dividedBy(o2), 1)
.addBinaryOperator("^", UnitsDatabase::exponent, 2).build();
+ /**
+ * A parser that can parse unit prefix expressions
+ *
+ * @since 2019-04-13
+ */
+ private final ExpressionParser prefixExpressionParser = new ExpressionParser.Builder<>(this::getPrefix)
+ .addBinaryOperator("*", (o1, o2) -> o1.times(o2), 0).addSpaceFunction("*")
+ .addBinaryOperator("/", (o1, o2) -> o1.dividedBy(o2), 0)
+ .addBinaryOperator("^", (o1, o2) -> o1.toExponent(o2.getMultiplier()), 1).build();
+
+ /**
+ * A parser that can parse unit dimension expressions.
+ *
+ * @since 2019-04-13
+ */
private final ExpressionParser unitDimensionParser = new ExpressionParser.Builder<>(
this::getDimension).addBinaryOperator("*", (o1, o2) -> o1.times(o2), 0).addSpaceFunction("*")
.addBinaryOperator("/", (o1, o2) -> o1.dividedBy(o2), 0).build();
@@ -462,7 +476,11 @@ public final class UnitsDatabase {
* @since v0.1.0
*/
public UnitPrefix getPrefix(final String name) {
- return this.prefixes.get(name);
+ try {
+ return new DefaultUnitPrefix(Double.parseDouble(name));
+ } catch (final NumberFormatException e) {
+ return this.prefixes.get(name);
+ }
}
/**
@@ -485,33 +503,20 @@ public final class UnitsDatabase {
public UnitPrefix getPrefixFromExpression(final String expression) {
Objects.requireNonNull(expression, "expression must not be null.");
- try {
- return new DefaultUnitPrefix(Double.parseDouble(expression));
- } catch (final NumberFormatException e) {
- if (expression.contains("^")) {
- final String[] baseAndExponent = expression.split("\\^");
+ // attempt to get a unit as an alias first
+ if (this.containsUnitName(expression))
+ return this.getPrefix(expression);
- final double base;
- try {
- base = Double.parseDouble(baseAndExponent[0]);
- } catch (final NumberFormatException e2) {
- throw new IllegalArgumentException("Base of exponientation must be a number.");
- }
+ // force operators to have spaces
+ String modifiedExpression = expression;
+ modifiedExpression = modifiedExpression.replaceAll("\\*", " \\* ");
+ modifiedExpression = modifiedExpression.replaceAll("/", " / ");
+ modifiedExpression = modifiedExpression.replaceAll("\\^", " \\^ ");
- final int exponent;
- try {
- exponent = Integer.parseInt(baseAndExponent[baseAndExponent.length - 1]);
- } catch (final NumberFormatException e2) {
- throw new IllegalArgumentException("Exponent must be an integer.");
- }
+ // fix broken spaces
+ modifiedExpression = modifiedExpression.replaceAll(" +", " ");
- return new DefaultUnitPrefix(Math.pow(base, exponent));
- } else {
- if (!this.containsPrefixName(expression))
- throw new IllegalArgumentException("Unrecognized prefix name \"" + expression + "\".");
- return this.getPrefix(expression);
- }
- }
+ return this.prefixExpressionParser.parseExpression(modifiedExpression);
}
/**
@@ -541,57 +546,27 @@ public final class UnitsDatabase {
final double value = Double.parseDouble(name);
return SI.SI.getBaseUnit(UnitDimension.EMPTY).times(value);
} catch (final NumberFormatException e) {
- if (name.contains("^")) {
- final String[] baseAndExponent = name.split("\\^");
-
- LinearUnit base;
- try {
- base = SI.SI.getBaseUnit(UnitDimension.EMPTY).times(Double.parseDouble(baseAndExponent[0]));
- } catch (final NumberFormatException e2) {
- final Unit unit = this.getUnit(baseAndExponent[0]);
- if (unit instanceof LinearUnit) {
- base = (LinearUnit) unit;
- } else
- throw new IllegalArgumentException("Base of exponientation must be a linear or base unit.");
- }
-
- final int exponent;
- try {
- exponent = Integer.parseInt(baseAndExponent[baseAndExponent.length - 1]);
- } catch (final NumberFormatException e2) {
- throw new IllegalArgumentException("Exponent must be an integer.");
- }
-
- final LinearUnit exponentiated = base.toExponent(exponent);
- if (exponentiated.getConversionFactor() == 1)
- return exponentiated.getSystem().getBaseUnit(exponentiated.getDimension());
- else
- return exponentiated;
- } else {
- for (final String prefixName : this.prefixNameSet()) {
- // check for a prefix
- if (name.startsWith(prefixName)) {
- // prefix found! Make sure what comes after it is actually a unit!
- final String prefixless = name.substring(prefixName.length());
- if (this.containsUnitName(prefixless)) {
- // yep, it's a proper prefix! Get the unit!
- final Unit unit = this.getUnit(prefixless);
- final UnitPrefix prefix = this.getPrefix(prefixName);
-
- // Prefixes only work with linear and base units, so make sure it's one of those
- if (unit instanceof LinearUnit) {
- final LinearUnit linearUnit = (LinearUnit) unit;
- return linearUnit.times(prefix.getMultiplier());
- } else if (unit instanceof BaseUnit) {
- final BaseUnit baseUnit = (BaseUnit) unit;
- return baseUnit.times(prefix.getMultiplier());
- }
+ for (final String prefixName : this.prefixNameSet()) {
+ // check for a prefix
+ if (name.startsWith(prefixName)) {
+ // prefix found! Make sure what comes after it is actually a unit!
+ final String prefixless = name.substring(prefixName.length());
+ if (this.containsUnitName(prefixless)) {
+ // yep, it's a proper prefix! Get the unit!
+ final Unit unit = this.getUnit(prefixless);
+ final UnitPrefix prefix = this.getPrefix(prefixName);
+
+ // Prefixes only work with linear and base units, so make sure it's one of those
+ if (unit instanceof LinearUnit) {
+ final LinearUnit linearUnit = (LinearUnit) unit;
+ return linearUnit.withPrefix(prefix);
}
}
}
- return this.units.get(name);
}
+ return this.units.get(name);
}
+
}
/**
diff --git a/src/org/unitConverter/unit/UnitPrefix.java b/src/org/unitConverter/unit/UnitPrefix.java
index 289e60f..a1609c6 100755
--- a/src/org/unitConverter/unit/UnitPrefix.java
+++ b/src/org/unitConverter/unit/UnitPrefix.java
@@ -24,10 +24,46 @@ package org.unitConverter.unit;
* @since v0.1.0
*/
public interface UnitPrefix {
+ /**
+ * Divides this prefix by {@code other}.
+ *
+ * @param other
+ * prefix to divide by
+ * @return quotient of prefixes
+ * @since 2019-04-13
+ */
+ default UnitPrefix dividedBy(final UnitPrefix other) {
+ return new DefaultUnitPrefix(this.getMultiplier() / other.getMultiplier());
+ }
+
/**
* @return this prefix's multiplier
* @since 2019-01-14
* @since v0.1.0
*/
double getMultiplier();
+
+ /**
+ * Multiplies this prefix by {@code other}.
+ *
+ * @param other
+ * prefix to multiply by
+ * @return product of prefixes
+ * @since 2019-04-13
+ */
+ default UnitPrefix times(final UnitPrefix other) {
+ return new DefaultUnitPrefix(this.getMultiplier() * other.getMultiplier());
+ }
+
+ /**
+ * Raises this prefix to an exponent.
+ *
+ * @param exponent
+ * exponent to raise to
+ * @return result of exponentiation.
+ * @since 2019-04-13
+ */
+ default UnitPrefix toExponent(final double exponent) {
+ return new DefaultUnitPrefix(Math.pow(getMultiplier(), exponent));
+ }
}
--
cgit v1.2.3
From 63dd50e5d7a5daa0bcbdd00608543d4572c870ea Mon Sep 17 00:00:00 2001
From: Adrien Hopkins
Date: Sat, 13 Apr 2019 19:48:25 -0400
Subject: Edited the UnitsDatabase API; it now favours prefixless units.
---
CHANGELOG.org | 2 +
src/org/unitConverter/UnitsDatabase.java | 753 ++++++++++++++++++---
.../converterGUI/UnitConverterGUI.java | 18 +-
src/org/unitConverter/math/ExpressionParser.java | 2 -
4 files changed, 689 insertions(+), 86 deletions(-)
(limited to 'src/org/unitConverter/UnitsDatabase.java')
diff --git a/CHANGELOG.org b/CHANGELOG.org
index e7748ba..db9766b 100644
--- a/CHANGELOG.org
+++ b/CHANGELOG.org
@@ -3,6 +3,8 @@ All notable changes in this project will be shown in this file.
** Unreleased
*** Changed
+ - When searching for units, units with no prefixes are searched for before prefixed units
+ - Smaller prefixes are searched for before larger prefixes
- Moved project to Maven
- Downgraded JUnit to 4.11
- BaseUnit is now a subclass of LinearUnit
diff --git a/src/org/unitConverter/UnitsDatabase.java b/src/org/unitConverter/UnitsDatabase.java
index a7e6047..9749e9c 100755
--- a/src/org/unitConverter/UnitsDatabase.java
+++ b/src/org/unitConverter/UnitsDatabase.java
@@ -21,14 +21,20 @@ import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
+import java.util.AbstractSet;
+import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
-import java.util.HashSet;
+import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+import java.util.function.Predicate;
import org.unitConverter.dimension.UnitDimension;
import org.unitConverter.math.DecimalComparison;
@@ -40,13 +46,652 @@ import org.unitConverter.unit.Unit;
import org.unitConverter.unit.UnitPrefix;
/**
- * A database of units and prefixes, and their names.
+ * A database of units, prefixes and dimensions, and their names.
*
* @author Adrien Hopkins
* @since 2019-01-07
* @since v0.1.0
*/
public final class UnitsDatabase {
+ /**
+ * A map for units that allows the use of prefixes.
+ *
+ * As this map implementation is intended to be used as a sort of "augmented view" of a unit and prefix map, it is
+ * unmodifiable but instead reflects the changes to the maps passed into it. Do not edit this map, instead edit the
+ * maps that were passed in during construction.
+ *
+ *
+ * The rules for applying prefixes onto units are the following:
+ *
+ * - Prefixes can only be applied to linear units.
+ * - Before attempting to search for prefixes in a unit name, this map will first search for a unit name. So, if
+ * there are two units, "B" and "AB", and a prefix "A", this map will favour the unit "AB" over the unit "B" with
+ * the prefix "A", even though they have the same string.
+ * - Shorter prefixes are preferred to longer prefixes. So, if you have units "BC" and "C", and prefixes "AB" and
+ * "A", inputting "ABC" will return the unit "BC" with the prefix "A", not "C" with the prefix "AB".
+ *
+ *
+ *
+ * @author Adrien Hopkins
+ * @since 2019-04-13
+ */
+ private static final class PrefixedUnitMap implements Map {
+ /**
+ * The class used for entry sets.
+ *
+ * @author Adrien Hopkins
+ * @since 2019-04-13
+ */
+ private static final class PrefixedUnitEntrySet extends AbstractSet> {
+ // the map that created this set
+ private final PrefixedUnitMap map;
+
+ /**
+ * Creates the {@code PrefixedUnitNameSet}.
+ *
+ * @param map
+ * @since 2019-04-13
+ */
+ public PrefixedUnitEntrySet(final PrefixedUnitMap map) {
+ this.map = map;
+ }
+
+ @Override
+ public boolean add(final Map.Entry e) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean addAll(final Collection extends Map.Entry> c) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void clear() {
+ throw new UnsupportedOperationException();
+ }
+
+ @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.
+ @SuppressWarnings("unchecked")
+ final Entry tempEntry = (Entry) o;
+ entry = tempEntry;
+ } catch (final ClassCastException e) {
+ throw new IllegalArgumentException("Attempted to test for an entry using a non-entry.");
+ }
+
+ return this.map.containsKey(entry.getKey()) && this.map.get(entry.getKey()).equals(entry.getValue());
+ }
+
+ @Override
+ public boolean containsAll(final Collection> c) {
+ for (final Object o : c)
+ if (!this.contains(o))
+ return false;
+ return true;
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return this.map.isEmpty();
+ }
+
+ @Override
+ public Iterator> iterator() {
+ return new Iterator>() {
+ // position in the unit list
+ int unitNamePosition = -1;
+ // the indices of the prefixes attached to the current unit
+ List prefixCoordinates = new ArrayList<>();
+
+ List unitNames = new ArrayList<>(PrefixedUnitEntrySet.this.map.units.keySet());
+ List prefixNames = new ArrayList<>(PrefixedUnitEntrySet.this.map.prefixes.keySet());
+
+ @Override
+ public boolean hasNext() {
+ if (this.unitNames.isEmpty())
+ return false;
+ else {
+ if (this.prefixNames.isEmpty())
+ return this.unitNamePosition >= this.unitNames.size() - 1;
+ else
+ return true;
+ }
+ }
+
+ @Override
+ public Entry next() {
+ // increment unit name position
+ this.unitNamePosition++;
+
+ // if I have prefixes, ensure I'm not using a nonlinear unit
+ // since all of the unprefixed stuff is done, just remove nonlinear units
+ if (!this.prefixCoordinates.isEmpty()) {
+ while (!(PrefixedUnitEntrySet.this.map
+ .get(this.unitNames.get(this.unitNamePosition)) instanceof LinearUnit)) {
+ this.unitNames.remove(this.unitNamePosition);
+ }
+ }
+
+ // carry over
+ if (!this.prefixNames.isEmpty() && this.unitNamePosition >= this.unitNames.size() - 1) {
+ // handle prefix position
+ this.unitNamePosition = 0;
+ int i = this.prefixCoordinates.size() - 1;
+ this.prefixCoordinates.set(i, this.prefixCoordinates.get(i) + 1);
+
+ while (this.prefixCoordinates.get(i) >= this.prefixNames.size() - 1) {
+ this.prefixCoordinates.set(i, 0);
+ i--;
+ if (i < 0) {
+ this.prefixCoordinates.add(0, 0);
+ }
+ }
+ }
+
+ final StringBuilder unitNameBuilder = new StringBuilder();
+ for (final int i : this.prefixCoordinates) {
+ unitNameBuilder.append(this.prefixNames.get(i));
+ }
+ unitNameBuilder.append(this.unitNames.get(this.unitNamePosition));
+
+ final String unitName = unitNameBuilder.toString();
+ return new Entry() {
+ @Override
+ public String getKey() {
+ return unitName;
+ }
+
+ @Override
+ public Unit getValue() {
+ return PrefixedUnitEntrySet.this.map.get(unitName);
+ }
+
+ @Override
+ public Unit setValue(final Unit value) {
+ throw new UnsupportedOperationException();
+ }
+ };
+ }
+ };
+ }
+
+ @Override
+ public boolean remove(final Object o) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean removeAll(final Collection> c) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean removeIf(final Predicate super Entry> filter) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean retainAll(final Collection> c) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public int size() {
+ if (this.map.units.isEmpty())
+ return 0;
+ else {
+ if (this.map.prefixes.isEmpty())
+ return this.map.units.size();
+ else
+ // infinite set
+ return Integer.MAX_VALUE;
+ }
+ }
+
+ @Override
+ public Object[] toArray() {
+ if (this.map.units.isEmpty())
+ // finite, it will work
+ return super.toArray();
+ else {
+ if (this.map.prefixes.isEmpty())
+ // finite, it will work
+ return super.toArray();
+ else
+ // infinite set
+ throw new UnsupportedOperationException("Cannot make an infinite set into an array.");
+ }
+ }
+
+ @Override
+ public T[] toArray(final T[] a) {
+ if (this.map.units.isEmpty())
+ // finite, it will work
+ return super.toArray(a);
+ else {
+ if (this.map.prefixes.isEmpty())
+ // finite, it will work
+ return super.toArray(a);
+ else
+ // infinite set
+ throw new UnsupportedOperationException("Cannot make an infinite set into an array.");
+ }
+ }
+
+ }
+
+ /**
+ * The class used for unit name sets.
+ *
+ * @author Adrien Hopkins
+ * @since 2019-04-13
+ */
+ private static final class PrefixedUnitNameSet extends AbstractSet {
+ // the map that created this set
+ private final PrefixedUnitMap map;
+
+ /**
+ * Creates the {@code PrefixedUnitNameSet}.
+ *
+ * @param map
+ * @since 2019-04-13
+ */
+ public PrefixedUnitNameSet(final PrefixedUnitMap map) {
+ this.map = map;
+ }
+
+ @Override
+ public boolean add(final String e) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean addAll(final Collection extends String> c) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void clear() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean contains(final Object o) {
+ return this.map.containsKey(o);
+ }
+
+ @Override
+ public boolean containsAll(final Collection> c) {
+ for (final Object o : c)
+ if (!this.contains(o))
+ return false;
+ return true;
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return this.map.isEmpty();
+ }
+
+ @Override
+ public Iterator iterator() {
+ return new Iterator() {
+ // position in the unit list
+ int unitNamePosition = -1;
+ // the indices of the prefixes attached to the current unit
+ List prefixCoordinates = new ArrayList<>();
+
+ List unitNames = new ArrayList<>(PrefixedUnitNameSet.this.map.units.keySet());
+ List prefixNames = new ArrayList<>(PrefixedUnitNameSet.this.map.prefixes.keySet());
+
+ @Override
+ public boolean hasNext() {
+ if (this.unitNames.isEmpty())
+ return false;
+ else {
+ if (this.prefixNames.isEmpty())
+ return this.unitNamePosition >= this.unitNames.size() - 1;
+ else
+ return true;
+ }
+ }
+
+ @Override
+ public String next() {
+ // increment unit name position
+ this.unitNamePosition++;
+
+ // if I have prefixes, ensure I'm not using a nonlinear unit
+ // since all of the unprefixed stuff is done, just remove nonlinear units
+ if (!this.prefixCoordinates.isEmpty()) {
+ while (!(PrefixedUnitNameSet.this.map
+ .get(this.unitNames.get(this.unitNamePosition)) instanceof LinearUnit)) {
+ this.unitNames.remove(this.unitNamePosition);
+ }
+ }
+
+ // carry over
+ if (!this.prefixNames.isEmpty() && this.unitNamePosition >= this.unitNames.size() - 1) {
+ // handle prefix position
+ this.unitNamePosition = 0;
+ int i = this.prefixCoordinates.size() - 1;
+ this.prefixCoordinates.set(i, this.prefixCoordinates.get(i) + 1);
+
+ while (this.prefixCoordinates.get(i) >= this.prefixNames.size() - 1) {
+ this.prefixCoordinates.set(i, 0);
+ i--;
+ if (i < 0) {
+ this.prefixCoordinates.add(0, 0);
+ }
+ }
+ }
+
+ final StringBuilder unitName = new StringBuilder();
+ for (final int i : this.prefixCoordinates) {
+ unitName.append(this.prefixNames.get(i));
+ }
+ unitName.append(this.unitNames.get(this.unitNamePosition));
+ return unitName.toString();
+ }
+ };
+ }
+
+ @Override
+ public boolean remove(final Object o) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean removeAll(final Collection> c) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean removeIf(final Predicate super String> filter) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean retainAll(final Collection> c) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public int size() {
+ if (this.map.units.isEmpty())
+ return 0;
+ else {
+ if (this.map.prefixes.isEmpty())
+ return this.map.units.size();
+ else
+ // infinite set
+ return Integer.MAX_VALUE;
+ }
+ }
+
+ @Override
+ public Object[] toArray() {
+ if (this.map.units.isEmpty())
+ // finite, it will work
+ return super.toArray();
+ else {
+ if (this.map.prefixes.isEmpty())
+ // finite, it will work
+ return super.toArray();
+ else
+ // infinite set
+ throw new UnsupportedOperationException("Cannot make an infinite set into an array.");
+ }
+ }
+
+ @Override
+ public T[] toArray(final T[] a) {
+ if (this.map.units.isEmpty())
+ // finite, it will work
+ return super.toArray(a);
+ else {
+ if (this.map.prefixes.isEmpty())
+ // finite, it will work
+ return super.toArray(a);
+ else
+ // infinite set
+ throw new UnsupportedOperationException("Cannot make an infinite set into an array.");
+ }
+ }
+
+ }
+
+ /**
+ * The units stored in this collection, without prefixes.
+ *
+ * @since 2019-04-13
+ */
+ private final Map units;
+
+ /**
+ * The available prefixes for use.
+ *
+ * @since 2019-04-13
+ */
+ private final Map prefixes;
+
+ // caches
+ private Collection values = null;
+ private Set keySet = null;
+ private Set> entrySet = null;
+
+ /**
+ * Creates the {@code PrefixedUnitMap}.
+ *
+ * @param units
+ * @param prefixes
+ * @since 2019-04-13
+ */
+ public PrefixedUnitMap(final Map units, final Map prefixes) {
+ // I am making unmodifiable maps to ensure I don't accidentally make changes.
+ this.units = Collections.unmodifiableMap(units);
+ this.prefixes = Collections.unmodifiableMap(prefixes);
+ }
+
+ @Override
+ public void clear() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Unit compute(final String key,
+ final BiFunction super String, ? super Unit, ? extends Unit> remappingFunction) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Unit computeIfAbsent(final String key, final Function super String, ? extends Unit> mappingFunction) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Unit computeIfPresent(final String key,
+ final BiFunction super String, ? super Unit, ? extends Unit> remappingFunction) {
+ throw new UnsupportedOperationException();
+ }
+
+ @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 shortest prefix that is attached to a valid unit
+ String shortestPrefix = null;
+ int shortestLength = Integer.MAX_VALUE;
+
+ for (final String prefixName : this.prefixes.keySet()) {
+ // a prefix name is valid if:
+ // - it is prefixed (i.e. the unit name starts with it)
+ // - it is shorter than the existing largest prefix (since I am looking for the smallest valid prefix)
+ // - the part after the prefix is a valid unit name
+ // - the unit described that name is a linear unit (since only linear units can have prefixes)
+ if (unitName.startsWith(prefixName) && prefixName.length() < shortestLength) {
+ final String rest = unitName.substring(prefixName.length());
+ if (this.containsKey(rest) && this.get(rest) instanceof LinearUnit) {
+ shortestPrefix = prefixName;
+ shortestLength = prefixName.length();
+ }
+ }
+ }
+
+ return shortestPrefix != null;
+ }
+
+ @Override
+ public boolean containsValue(final Object value) {
+ return this.units.containsValue(value);
+ }
+
+ @Override
+ public Set> entrySet() {
+ if (this.entrySet == null) {
+ this.entrySet = new PrefixedUnitEntrySet(this);
+ }
+ return this.entrySet;
+ }
+
+ @Override
+ public Unit get(final Object key) {
+ // First, test if there is a unit with the key
+ if (this.units.containsKey(key))
+ return this.units.get(key);
+
+ // Next, try to cast it to String
+ if (!(key instanceof String))
+ throw new IllegalArgumentException("Attempted to obtain a unit using a non-string name.");
+ final String unitName = (String) key;
+
+ // Then, look for the shortest prefix that is attached to a valid unit
+ String shortestPrefix = null;
+ int shortestLength = Integer.MAX_VALUE;
+
+ for (final String prefixName : this.prefixes.keySet()) {
+ // a prefix name is valid if:
+ // - it is prefixed (i.e. the unit name starts with it)
+ // - it is shorter than the existing largest prefix (since I am looking for the smallest valid prefix)
+ // - the part after the prefix is a valid unit name
+ // - the unit described that name is a linear unit (since only linear units can have prefixes)
+ if (unitName.startsWith(prefixName) && prefixName.length() < shortestLength) {
+ final String rest = unitName.substring(prefixName.length());
+ if (this.containsKey(rest) && this.get(rest) instanceof LinearUnit) {
+ shortestPrefix = prefixName;
+ shortestLength = prefixName.length();
+ }
+ }
+ }
+
+ // if none found, returns null
+ if (shortestPrefix == null)
+ return null;
+ else {
+ // get necessary data
+ final String rest = unitName.substring(shortestLength);
+ // this cast will not fail because I verified that it would work before selecting this prefix
+ final LinearUnit unit = (LinearUnit) this.get(rest);
+ final UnitPrefix prefix = this.prefixes.get(shortestPrefix);
+
+ return unit.withPrefix(prefix);
+ }
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return this.units.isEmpty();
+ }
+
+ @Override
+ public Set keySet() {
+ if (this.keySet == null) {
+ this.keySet = new PrefixedUnitNameSet(this);
+ }
+ return this.keySet;
+ }
+
+ @Override
+ public Unit merge(final String key, final Unit value,
+ final BiFunction super Unit, ? super Unit, ? extends Unit> remappingFunction) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Unit put(final String key, final Unit value) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void putAll(final Map extends String, ? extends Unit> m) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Unit putIfAbsent(final String key, final Unit value) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Unit remove(final Object key) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean remove(final Object key, final Object value) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Unit replace(final String key, final Unit value) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean replace(final String key, final Unit oldValue, final Unit newValue) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void replaceAll(final BiFunction super String, ? super Unit, ? extends Unit> function) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public int size() {
+ if (this.units.isEmpty())
+ return 0;
+ else {
+ if (this.prefixes.isEmpty())
+ return this.units.size();
+ else
+ // infinite set
+ return Integer.MAX_VALUE;
+ }
+ }
+
+ @Override
+ public Collection values() {
+ if (this.values == null) {
+ this.values = Collections.unmodifiableCollection(this.units.values());
+ }
+ return this.values;
+ }
+ }
+
/**
* The exponent operator
*
@@ -57,7 +702,7 @@ public final class UnitsDatabase {
* @return result
* @since 2019-04-10
*/
- private static final LinearUnit exponent(final LinearUnit base, final LinearUnit exponentUnit) {
+ private static final LinearUnit exponentiateUnits(final LinearUnit base, final LinearUnit exponentUnit) {
// exponent function - first check if o2 is a number,
if (exponentUnit.getBase().equals(SI.SI.getBaseUnit(UnitDimension.EMPTY))) {
// then check if it is an integer,
@@ -74,12 +719,12 @@ public final class UnitsDatabase {
}
/**
- * The units in this system.
+ * The units in this system, excluding prefixes.
*
* @since 2019-01-07
* @since v0.1.0
*/
- private final Map units;
+ private final Map prefixlessUnits;
/**
* The unit prefixes in this system.
@@ -96,6 +741,13 @@ public final class UnitsDatabase {
*/
private final Map dimensions;
+ /**
+ * A map mapping strings to units (including prefixes)
+ *
+ * @since 2019-04-13
+ */
+ private final Map units;
+
/**
* A parser that can parse unit expressions.
*
@@ -106,7 +758,7 @@ public final class UnitsDatabase {
.addBinaryOperator("-", (o1, o2) -> o1.minus(o2), 0)
.addBinaryOperator("*", (o1, o2) -> o1.times(o2), 1).addSpaceFunction("*")
.addBinaryOperator("/", (o1, o2) -> o1.dividedBy(o2), 1)
- .addBinaryOperator("^", UnitsDatabase::exponent, 2).build();
+ .addBinaryOperator("^", UnitsDatabase::exponentiateUnits, 2).build();
/**
* A parser that can parse unit prefix expressions
@@ -134,9 +786,10 @@ public final class UnitsDatabase {
* @since v0.1.0
*/
public UnitsDatabase() {
- this.units = new HashMap<>();
+ this.prefixlessUnits = new HashMap<>();
this.prefixes = new HashMap<>();
this.dimensions = new HashMap<>();
+ this.units = new PrefixedUnitMap(this.prefixlessUnits, this.prefixes);
}
/**
@@ -236,7 +889,7 @@ public final class UnitsDatabase {
* @since v0.1.0
*/
public void addUnit(final String name, final Unit unit) {
- this.units.put(Objects.requireNonNull(name, "name must not be null."),
+ this.prefixlessUnits.put(Objects.requireNonNull(name, "name must not be null."),
Objects.requireNonNull(unit, "unit must not be null."));
}
@@ -313,19 +966,6 @@ public final class UnitsDatabase {
return this.dimensions.containsKey(name);
}
- /**
- * Tests if the database has a unit with this name, ignoring prefixes
- *
- * @param name
- * name to test
- * @return if database contains name
- * @since 2019-01-13
- * @since v0.1.0
- */
- public boolean containsPrefixlessUnitName(final String name) {
- return this.units.containsKey(name);
- }
-
/**
* Tests if the database has a unit prefix with this name.
*
@@ -349,21 +989,15 @@ public final class UnitsDatabase {
* @since v0.1.0
*/
public boolean containsUnitName(final String name) {
- // check for prefixes
- for (final String prefixName : this.prefixNameSet()) {
- if (name.startsWith(prefixName))
- if (this.containsUnitName(name.substring(prefixName.length())))
- return true;
- }
return this.units.containsKey(name);
}
/**
- * @return an immutable set of all of the dimension names in this database.
- * @since 2019-03-14
+ * @return a map mapping dimension names to dimensions
+ * @since 2019-04-13
*/
- public Set dimensionNameSet() {
- return Collections.unmodifiableSet(this.dimensions.keySet());
+ public Map dimensionMap() {
+ return Collections.unmodifiableMap(this.dimensions);
}
/**
@@ -519,19 +1153,6 @@ public final class UnitsDatabase {
return this.prefixExpressionParser.parseExpression(modifiedExpression);
}
- /**
- * Gets a unit from the database from its name, ignoring prefixes.
- *
- * @param name
- * unit's name
- * @return unit
- * @since 2019-01-10
- * @since v0.1.0
- */
- public Unit getPrefixlessUnit(final String name) {
- return this.units.get(name);
- }
-
/**
* Gets a unit from the database from its name, looking for prefixes.
*
@@ -546,24 +1167,6 @@ public final class UnitsDatabase {
final double value = Double.parseDouble(name);
return SI.SI.getBaseUnit(UnitDimension.EMPTY).times(value);
} catch (final NumberFormatException e) {
- for (final String prefixName : this.prefixNameSet()) {
- // check for a prefix
- if (name.startsWith(prefixName)) {
- // prefix found! Make sure what comes after it is actually a unit!
- final String prefixless = name.substring(prefixName.length());
- if (this.containsUnitName(prefixless)) {
- // yep, it's a proper prefix! Get the unit!
- final Unit unit = this.getUnit(prefixless);
- final UnitPrefix prefix = this.getPrefix(prefixName);
-
- // Prefixes only work with linear and base units, so make sure it's one of those
- if (unit instanceof LinearUnit) {
- final LinearUnit linearUnit = (LinearUnit) unit;
- return linearUnit.withPrefix(prefix);
- }
- }
- }
- }
return this.units.get(name);
}
@@ -703,28 +1306,26 @@ public final class UnitsDatabase {
}
/**
- * @return an immutable set of all of the unit names in this database, ignoring prefixes
- * @since 2019-01-14
- * @since v0.1.0
+ * @return a map mapping prefix names to prefixes
+ * @since 2019-04-13
*/
- public Set prefixlessUnitNameSet() {
- return Collections.unmodifiableSet(this.units.keySet());
+ public Map prefixMap() {
+ return Collections.unmodifiableMap(this.prefixes);
}
/**
- * @return an immutable set of all of the units in this database, ignoring prefixes.
- * @since 2019-04-10
+ * @return a map mapping unit names to units, including prefixed names
+ * @since 2019-04-13
*/
- public Set prefixlessUnitSet() {
- return Collections.unmodifiableSet(new HashSet<>(this.units.values()));
+ public Map unitMap() {
+ return this.units; // PrefixedUnitMap is immutable so I don't need to make an unmodifiable map.
}
/**
- * @return an immutable set of all of the prefix names in this database
- * @since 2019-01-14
- * @since v0.1.0
+ * @return a map mapping unit names to units, ignoring prefixes
+ * @since 2019-04-13
*/
- public Set prefixNameSet() {
- return Collections.unmodifiableSet(this.prefixes.keySet());
+ public Map unitMapPrefixless() {
+ return Collections.unmodifiableMap(this.prefixlessUnits);
}
}
diff --git a/src/org/unitConverter/converterGUI/UnitConverterGUI.java b/src/org/unitConverter/converterGUI/UnitConverterGUI.java
index 9314510..49a40d6 100755
--- a/src/org/unitConverter/converterGUI/UnitConverterGUI.java
+++ b/src/org/unitConverter/converterGUI/UnitConverterGUI.java
@@ -24,6 +24,7 @@ import java.math.MathContext;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Comparator;
+import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Predicate;
@@ -143,19 +144,19 @@ final class UnitConverterGUI {
return o1.compareTo(o2);
};
- this.unitNames = new ArrayList<>(this.units.prefixlessUnitNameSet());
+ this.unitNames = new ArrayList<>(this.units.unitMapPrefixless().keySet());
this.unitNames.sort(null); // sorts it using Comparable
- this.unitNamesFiltered = new DelegateListModel<>(new ArrayList<>(this.units.prefixlessUnitNameSet()));
+ this.unitNamesFiltered = new DelegateListModel<>(new ArrayList<>(this.units.unitMapPrefixless().keySet()));
this.unitNamesFiltered.sort(null); // sorts it using Comparable
- this.prefixNames = new ArrayList<>(this.units.prefixNameSet());
+ this.prefixNames = new ArrayList<>(this.units.prefixMap().keySet());
this.prefixNames.sort(this.prefixNameComparator); // sorts it using my comparator
- this.prefixNamesFiltered = new DelegateListModel<>(new ArrayList<>(this.units.prefixNameSet()));
+ this.prefixNamesFiltered = new DelegateListModel<>(new ArrayList<>(this.units.prefixMap().keySet()));
this.prefixNamesFiltered.sort(this.prefixNameComparator); // sorts it using my comparator
- this.dimensionNames = new DelegateListModel<>(new ArrayList<>(this.units.dimensionNameSet()));
+ this.dimensionNames = new DelegateListModel<>(new ArrayList<>(this.units.dimensionMap().keySet()));
this.dimensionNames.sort(null); // sorts it using Comparable
// a Predicate that returns true iff the argument is a full base unit
@@ -163,8 +164,9 @@ final class UnitConverterGUI {
// print out unit counts
System.out.printf("Successfully loaded %d units with %d unit names (%d base units).%n",
- this.units.prefixlessUnitSet().size(), this.units.prefixlessUnitNameSet().size(),
- this.units.prefixlessUnitSet().stream().filter(isFullBase).count());
+ new HashSet<>(this.units.unitMapPrefixless().values()).size(),
+ this.units.unitMapPrefixless().size(),
+ new HashSet<>(this.units.unitMapPrefixless().values()).stream().filter(isFullBase).count());
}
/**
@@ -449,7 +451,7 @@ final class UnitConverterGUI {
}
public final Set unitNameSet() {
- return this.units.prefixlessUnitNameSet();
+ return this.units.unitMapPrefixless().keySet();
}
}
diff --git a/src/org/unitConverter/math/ExpressionParser.java b/src/org/unitConverter/math/ExpressionParser.java
index b56fa71..d01afaa 100644
--- a/src/org/unitConverter/math/ExpressionParser.java
+++ b/src/org/unitConverter/math/ExpressionParser.java
@@ -510,8 +510,6 @@ public final class ExpressionParser {
expressionRPN = expressionRPN.substring(0, expressionRPN.length() - 1);
}
return expressionRPN;
-
- // TODO document org.unitConverter.expressionParser.ExpressionParser.convertExpressionToPolish(expression)
}
/**
--
cgit v1.2.3
From 4cef115e3fbd228a84ad48eed7af5403e8c8c46e Mon Sep 17 00:00:00 2001
From: Adrien Hopkins
Date: Sat, 13 Apr 2019 20:01:26 -0400
Subject: Longer prefixes are now favoured over shorter prefixes.
Added 'da-' to the unit file, which was previously missing because it
was interpreted as 'deciatto'. 'D-' can still be used.
---
src/org/unitConverter/UnitsDatabase.java | 98 +++++++++++++-------------------
unitsfile.txt | 1 +
2 files changed, 39 insertions(+), 60 deletions(-)
(limited to 'src/org/unitConverter/UnitsDatabase.java')
diff --git a/src/org/unitConverter/UnitsDatabase.java b/src/org/unitConverter/UnitsDatabase.java
index 9749e9c..901c6ef 100755
--- a/src/org/unitConverter/UnitsDatabase.java
+++ b/src/org/unitConverter/UnitsDatabase.java
@@ -67,8 +67,8 @@ public final class UnitsDatabase {
* Before attempting to search for prefixes in a unit name, this map will first search for a unit name. So, if
* there are two units, "B" and "AB", and a prefix "A", this map will favour the unit "AB" over the unit "B" with
* the prefix "A", even though they have the same string.
- * Shorter prefixes are preferred to longer prefixes. So, if you have units "BC" and "C", and prefixes "AB" and
- * "A", inputting "ABC" will return the unit "BC" with the prefix "A", not "C" with the prefix "AB".
+ * Longer prefixes are preferred to shorter prefixes. So, if you have units "BC" and "C", and prefixes "AB" and
+ * "A", inputting "ABC" will return the unit "C" with the prefix "AB", not "BC" with the prefix "A".
*
*
*
@@ -194,6 +194,7 @@ public final class UnitsDatabase {
}
}
+ // create the unit name
final StringBuilder unitNameBuilder = new StringBuilder();
for (final int i : this.prefixCoordinates) {
unitNameBuilder.append(this.prefixNames.get(i));
@@ -256,32 +257,20 @@ public final class UnitsDatabase {
@Override
public Object[] toArray() {
- if (this.map.units.isEmpty())
- // finite, it will work
+ if (this.map.units.isEmpty() || this.map.prefixes.isEmpty())
return super.toArray();
- else {
- if (this.map.prefixes.isEmpty())
- // finite, it will work
- return super.toArray();
- else
- // infinite set
- throw new UnsupportedOperationException("Cannot make an infinite set into an array.");
- }
+ else
+ // infinite set
+ throw new UnsupportedOperationException("Cannot make an infinite set into an array.");
}
@Override
public T[] toArray(final T[] a) {
- if (this.map.units.isEmpty())
- // finite, it will work
+ if (this.map.units.isEmpty() || this.map.prefixes.isEmpty())
return super.toArray(a);
- else {
- if (this.map.prefixes.isEmpty())
- // finite, it will work
- return super.toArray(a);
- else
- // infinite set
- throw new UnsupportedOperationException("Cannot make an infinite set into an array.");
- }
+ else
+ // infinite set
+ throw new UnsupportedOperationException("Cannot make an infinite set into an array.");
}
}
@@ -437,32 +426,21 @@ public final class UnitsDatabase {
@Override
public Object[] toArray() {
- if (this.map.units.isEmpty())
- // finite, it will work
+ if (this.map.units.isEmpty() || this.map.prefixes.isEmpty())
return super.toArray();
- else {
- if (this.map.prefixes.isEmpty())
- // finite, it will work
- return super.toArray();
- else
- // infinite set
- throw new UnsupportedOperationException("Cannot make an infinite set into an array.");
- }
+ else
+ // infinite set
+ throw new UnsupportedOperationException("Cannot make an infinite set into an array.");
+
}
@Override
public T[] toArray(final T[] a) {
- if (this.map.units.isEmpty())
- // finite, it will work
+ if (this.map.units.isEmpty() || this.map.prefixes.isEmpty())
return super.toArray(a);
- else {
- if (this.map.prefixes.isEmpty())
- // finite, it will work
- return super.toArray(a);
- else
- // infinite set
- throw new UnsupportedOperationException("Cannot make an infinite set into an array.");
- }
+ else
+ // infinite set
+ throw new UnsupportedOperationException("Cannot make an infinite set into an array.");
}
}
@@ -532,26 +510,26 @@ public final class UnitsDatabase {
throw new IllegalArgumentException("Attempted to test for a unit using a non-string name.");
final String unitName = (String) key;
- // Then, look for the shortest prefix that is attached to a valid unit
- String shortestPrefix = null;
- int shortestLength = Integer.MAX_VALUE;
+ // Then, look for the longest prefix that is attached to a valid unit
+ String longestPrefix = null;
+ int longestLength = 0;
for (final String prefixName : this.prefixes.keySet()) {
// a prefix name is valid if:
// - it is prefixed (i.e. the unit name starts with it)
- // - it is shorter than the existing largest prefix (since I am looking for the smallest valid prefix)
+ // - it is longer than the existing largest prefix (since I am looking for the longest valid prefix)
// - the part after the prefix is a valid unit name
// - the unit described that name is a linear unit (since only linear units can have prefixes)
- if (unitName.startsWith(prefixName) && prefixName.length() < shortestLength) {
+ if (unitName.startsWith(prefixName) && prefixName.length() > longestLength) {
final String rest = unitName.substring(prefixName.length());
if (this.containsKey(rest) && this.get(rest) instanceof LinearUnit) {
- shortestPrefix = prefixName;
- shortestLength = prefixName.length();
+ longestPrefix = prefixName;
+ longestLength = prefixName.length();
}
}
}
- return shortestPrefix != null;
+ return longestPrefix != null;
}
@Override
@@ -578,34 +556,34 @@ public final class UnitsDatabase {
throw new IllegalArgumentException("Attempted to obtain a unit using a non-string name.");
final String unitName = (String) key;
- // Then, look for the shortest prefix that is attached to a valid unit
- String shortestPrefix = null;
- int shortestLength = Integer.MAX_VALUE;
+ // Then, look for the longest prefix that is attached to a valid unit
+ String longestPrefix = null;
+ int longestLength = 0;
for (final String prefixName : this.prefixes.keySet()) {
// a prefix name is valid if:
// - it is prefixed (i.e. the unit name starts with it)
- // - it is shorter than the existing largest prefix (since I am looking for the smallest valid prefix)
+ // - it is longer than the existing largest prefix (since I am looking for the longest valid prefix)
// - the part after the prefix is a valid unit name
// - the unit described that name is a linear unit (since only linear units can have prefixes)
- if (unitName.startsWith(prefixName) && prefixName.length() < shortestLength) {
+ if (unitName.startsWith(prefixName) && prefixName.length() > longestLength) {
final String rest = unitName.substring(prefixName.length());
if (this.containsKey(rest) && this.get(rest) instanceof LinearUnit) {
- shortestPrefix = prefixName;
- shortestLength = prefixName.length();
+ longestPrefix = prefixName;
+ longestLength = prefixName.length();
}
}
}
// if none found, returns null
- if (shortestPrefix == null)
+ if (longestPrefix == null)
return null;
else {
// get necessary data
- final String rest = unitName.substring(shortestLength);
+ final String rest = unitName.substring(longestLength);
// this cast will not fail because I verified that it would work before selecting this prefix
final LinearUnit unit = (LinearUnit) this.get(rest);
- final UnitPrefix prefix = this.prefixes.get(shortestPrefix);
+ final UnitPrefix prefix = this.prefixes.get(longestPrefix);
return unit.withPrefix(prefix);
}
diff --git a/unitsfile.txt b/unitsfile.txt
index 14fb6fb..78f8117 100755
--- a/unitsfile.txt
+++ b/unitsfile.txt
@@ -55,6 +55,7 @@ atto- 1e-18
zepto- 1e-21
yocto- 1e-24
+da- deca
D- deca
h- hecto
H- hecto
--
cgit v1.2.3
From fc1083454e4e9215140802602a17aafeef4515fa Mon Sep 17 00:00:00 2001
From: Adrien Hopkins
Date: Sun, 14 Apr 2019 14:32:48 -0400
Subject: Added a UnitDatabase test, and fixed some bugs using it.
---
src/org/unitConverter/UnitsDatabase.java | 395 ++++++++++++++++++++-----------
src/test/java/UnitsDatabaseTest.java | 253 ++++++++++++++++++++
2 files changed, 512 insertions(+), 136 deletions(-)
create mode 100644 src/test/java/UnitsDatabaseTest.java
(limited to 'src/org/unitConverter/UnitsDatabase.java')
diff --git a/src/org/unitConverter/UnitsDatabase.java b/src/org/unitConverter/UnitsDatabase.java
index 901c6ef..959c151 100755
--- a/src/org/unitConverter/UnitsDatabase.java
+++ b/src/org/unitConverter/UnitsDatabase.java
@@ -30,6 +30,7 @@ import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
+import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Set;
import java.util.function.BiFunction;
@@ -83,6 +84,153 @@ public final class UnitsDatabase {
* @since 2019-04-13
*/
private static final class PrefixedUnitEntrySet extends AbstractSet> {
+ /**
+ * The entry for this set.
+ *
+ * @author Adrien Hopkins
+ * @since 2019-04-14
+ */
+ private static final class PrefixedUnitEntry implements Entry {
+ private final String key;
+ private final Unit value;
+
+ /**
+ * Creates the {@code PrefixedUnitEntry}.
+ *
+ * @param key
+ * @param value
+ * @since 2019-04-14
+ */
+ public PrefixedUnitEntry(final String key, final Unit value) {
+ this.key = key;
+ this.value = value;
+ }
+
+ @Override
+ public String getKey() {
+ return this.key;
+ }
+
+ @Override
+ public Unit getValue() {
+ return this.value;
+ }
+
+ @Override
+ public Unit setValue(final Unit value) {
+ throw new UnsupportedOperationException();
+ }
+ }
+
+ /**
+ * An iterator that iterates over the units of a {@code PrefixedUnitNameSet}.
+ *
+ * @author Adrien Hopkins
+ * @since 2019-04-14
+ */
+ private static final class PrefixedUnitEntryIterator implements Iterator> {
+ // position in the unit list
+ private int unitNamePosition = 0;
+ // the indices of the prefixes attached to the current unit
+ private final List prefixCoordinates = new ArrayList<>();
+
+ private final Map map;
+ private final List unitNames;
+ private final List prefixNames;
+
+ /**
+ * Creates the {@code UnitsDatabase.PrefixedUnitMap.PrefixedUnitNameSet.PrefixedUnitNameIterator}.
+ *
+ * @since 2019-04-14
+ */
+ public PrefixedUnitEntryIterator(final PrefixedUnitEntrySet set) {
+ this.map = set.map;
+ this.unitNames = new ArrayList<>(set.map.units.keySet());
+ this.prefixNames = new ArrayList<>(set.map.prefixes.keySet());
+ }
+
+ /**
+ * @return current unit name
+ * @since 2019-04-14
+ */
+ private String getCurrentUnitName() {
+ final StringBuilder unitName = new StringBuilder();
+ for (final int i : this.prefixCoordinates) {
+ unitName.append(this.prefixNames.get(i));
+ }
+ unitName.append(this.unitNames.get(this.unitNamePosition));
+
+ return unitName.toString();
+ }
+
+ @Override
+ public boolean hasNext() {
+ if (this.unitNames.isEmpty())
+ return false;
+ else {
+ if (this.prefixNames.isEmpty())
+ return this.unitNamePosition >= this.unitNames.size() - 1;
+ else
+ return true;
+ }
+ }
+
+ /**
+ * Changes this iterator's position to the next available one.
+ *
+ * @since 2019-04-14
+ */
+ private void incrementPosition() {
+ this.unitNamePosition++;
+
+ if (this.unitNamePosition >= this.unitNames.size()) {
+ // we have used all of our units, go to a different prefix
+ this.unitNamePosition = 0;
+
+ // if the prefix coordinates are empty, then set it to [0]
+ if (this.prefixCoordinates.isEmpty()) {
+ this.prefixCoordinates.add(0, 0);
+ } else {
+ // get the prefix coordinate to increment, then increment
+ int i = this.prefixCoordinates.size() - 1;
+ this.prefixCoordinates.set(i, this.prefixCoordinates.get(i) + 1);
+
+ // fix any carrying errors
+ while (i >= 0 && this.prefixCoordinates.get(i) >= this.prefixNames.size()) {
+ // carry over
+ this.prefixCoordinates.set(i--, 0); // null and decrement at the same time
+
+ if (i < 0) { // we need to add a new coordinate
+ this.prefixCoordinates.add(0, 0);
+ } else { // increment an existing one
+ this.prefixCoordinates.set(i, this.prefixCoordinates.get(i) + 1);
+ }
+ }
+ }
+ }
+ }
+
+ @Override
+ public Entry next() {
+ if (!this.hasNext())
+ throw new NoSuchElementException("No units left!");
+ // if I have prefixes, ensure I'm not using a nonlinear unit
+ // since all of the unprefixed stuff is done, just remove nonlinear units
+ if (!this.prefixCoordinates.isEmpty()) {
+ while (this.unitNamePosition < this.unitNames.size()
+ && !(this.map.get(this.unitNames.get(this.unitNamePosition)) instanceof LinearUnit)) {
+ this.unitNames.remove(this.unitNamePosition);
+ }
+ }
+
+ final String nextName = this.getCurrentUnitName();
+
+ this.incrementPosition();
+
+ return new PrefixedUnitEntry(nextName, this.map.get(nextName));
+ }
+ }
+
// the map that created this set
private final PrefixedUnitMap map;
@@ -143,83 +291,7 @@ public final class UnitsDatabase {
@Override
public Iterator> iterator() {
- return new Iterator>() {
- // position in the unit list
- int unitNamePosition = -1;
- // the indices of the prefixes attached to the current unit
- List prefixCoordinates = new ArrayList<>();
-
- List unitNames = new ArrayList<>(PrefixedUnitEntrySet.this.map.units.keySet());
- List prefixNames = new ArrayList<>(PrefixedUnitEntrySet.this.map.prefixes.keySet());
-
- @Override
- public boolean hasNext() {
- if (this.unitNames.isEmpty())
- return false;
- else {
- if (this.prefixNames.isEmpty())
- return this.unitNamePosition >= this.unitNames.size() - 1;
- else
- return true;
- }
- }
-
- @Override
- public Entry next() {
- // increment unit name position
- this.unitNamePosition++;
-
- // if I have prefixes, ensure I'm not using a nonlinear unit
- // since all of the unprefixed stuff is done, just remove nonlinear units
- if (!this.prefixCoordinates.isEmpty()) {
- while (!(PrefixedUnitEntrySet.this.map
- .get(this.unitNames.get(this.unitNamePosition)) instanceof LinearUnit)) {
- this.unitNames.remove(this.unitNamePosition);
- }
- }
-
- // carry over
- if (!this.prefixNames.isEmpty() && this.unitNamePosition >= this.unitNames.size() - 1) {
- // handle prefix position
- this.unitNamePosition = 0;
- int i = this.prefixCoordinates.size() - 1;
- this.prefixCoordinates.set(i, this.prefixCoordinates.get(i) + 1);
-
- while (this.prefixCoordinates.get(i) >= this.prefixNames.size() - 1) {
- this.prefixCoordinates.set(i, 0);
- i--;
- if (i < 0) {
- this.prefixCoordinates.add(0, 0);
- }
- }
- }
-
- // create the unit name
- final StringBuilder unitNameBuilder = new StringBuilder();
- for (final int i : this.prefixCoordinates) {
- unitNameBuilder.append(this.prefixNames.get(i));
- }
- unitNameBuilder.append(this.unitNames.get(this.unitNamePosition));
-
- final String unitName = unitNameBuilder.toString();
- return new Entry() {
- @Override
- public String getKey() {
- return unitName;
- }
-
- @Override
- public Unit getValue() {
- return PrefixedUnitEntrySet.this.map.get(unitName);
- }
-
- @Override
- public Unit setValue(final Unit value) {
- throw new UnsupportedOperationException();
- }
- };
- }
- };
+ return new PrefixedUnitEntryIterator(this);
}
@Override
@@ -282,6 +354,115 @@ public final class UnitsDatabase {
* @since 2019-04-13
*/
private static final class PrefixedUnitNameSet extends AbstractSet {
+ /**
+ * An iterator that iterates over the units of a {@code PrefixedUnitNameSet}.
+ *
+ * @author Adrien Hopkins
+ * @since 2019-04-14
+ */
+ private static final class PrefixedUnitNameIterator implements Iterator {
+ // position in the unit list
+ private int unitNamePosition = 0;
+ // the indices of the prefixes attached to the current unit
+ private final List prefixCoordinates = new ArrayList<>();
+
+ private final Map map;
+ private final List unitNames;
+ private final List prefixNames;
+
+ /**
+ * Creates the {@code UnitsDatabase.PrefixedUnitMap.PrefixedUnitNameSet.PrefixedUnitNameIterator}.
+ *
+ * @since 2019-04-14
+ */
+ public PrefixedUnitNameIterator(final PrefixedUnitNameSet set) {
+ this.map = set.map;
+ this.unitNames = new ArrayList<>(set.map.units.keySet());
+ this.prefixNames = new ArrayList<>(set.map.prefixes.keySet());
+ }
+
+ /**
+ * @return current unit name
+ * @since 2019-04-14
+ */
+ private String getCurrentUnitName() {
+ final StringBuilder unitName = new StringBuilder();
+ for (final int i : this.prefixCoordinates) {
+ unitName.append(this.prefixNames.get(i));
+ }
+ unitName.append(this.unitNames.get(this.unitNamePosition));
+
+ return unitName.toString();
+ }
+
+ @Override
+ public boolean hasNext() {
+ if (this.unitNames.isEmpty())
+ return false;
+ else {
+ if (this.prefixNames.isEmpty())
+ return this.unitNamePosition >= this.unitNames.size() - 1;
+ else
+ return true;
+ }
+ }
+
+ /**
+ * Changes this iterator's position to the next available one.
+ *
+ * @since 2019-04-14
+ */
+ private void incrementPosition() {
+ this.unitNamePosition++;
+
+ if (this.unitNamePosition >= this.unitNames.size()) {
+ // we have used all of our units, go to a different prefix
+ this.unitNamePosition = 0;
+
+ // if the prefix coordinates are empty, then set it to [0]
+ if (this.prefixCoordinates.isEmpty()) {
+ this.prefixCoordinates.add(0, 0);
+ } else {
+ // get the prefix coordinate to increment, then increment
+ int i = this.prefixCoordinates.size() - 1;
+ this.prefixCoordinates.set(i, this.prefixCoordinates.get(i) + 1);
+
+ // fix any carrying errors
+ while (i >= 0 && this.prefixCoordinates.get(i) >= this.prefixNames.size()) {
+ // carry over
+ this.prefixCoordinates.set(i--, 0); // null and decrement at the same time
+
+ if (i < 0) { // we need to add a new coordinate
+ this.prefixCoordinates.add(0, 0);
+ } else { // increment an existing one
+ this.prefixCoordinates.set(i, this.prefixCoordinates.get(i) + 1);
+ }
+ }
+ }
+ }
+ }
+
+ @Override
+ public String next() {
+ if (!this.hasNext())
+ throw new NoSuchElementException("No units left!");
+ // if I have prefixes, ensure I'm not using a nonlinear unit
+ // since all of the unprefixed stuff is done, just remove nonlinear units
+ if (!this.prefixCoordinates.isEmpty()) {
+ while (this.unitNamePosition < this.unitNames.size()
+ && !(this.map.get(this.unitNames.get(this.unitNamePosition)) instanceof LinearUnit)) {
+ this.unitNames.remove(this.unitNamePosition);
+ }
+ }
+
+ final String nextName = this.getCurrentUnitName();
+
+ this.incrementPosition();
+
+ return nextName;
+ }
+ }
+
// the map that created this set
private final PrefixedUnitMap map;
@@ -330,65 +511,7 @@ public final class UnitsDatabase {
@Override
public Iterator iterator() {
- return new Iterator() {
- // position in the unit list
- int unitNamePosition = -1;
- // the indices of the prefixes attached to the current unit
- List prefixCoordinates = new ArrayList<>();
-
- List unitNames = new ArrayList<>(PrefixedUnitNameSet.this.map.units.keySet());
- List prefixNames = new ArrayList<>(PrefixedUnitNameSet.this.map.prefixes.keySet());
-
- @Override
- public boolean hasNext() {
- if (this.unitNames.isEmpty())
- return false;
- else {
- if (this.prefixNames.isEmpty())
- return this.unitNamePosition >= this.unitNames.size() - 1;
- else
- return true;
- }
- }
-
- @Override
- public String next() {
- // increment unit name position
- this.unitNamePosition++;
-
- // if I have prefixes, ensure I'm not using a nonlinear unit
- // since all of the unprefixed stuff is done, just remove nonlinear units
- if (!this.prefixCoordinates.isEmpty()) {
- while (!(PrefixedUnitNameSet.this.map
- .get(this.unitNames.get(this.unitNamePosition)) instanceof LinearUnit)) {
- this.unitNames.remove(this.unitNamePosition);
- }
- }
-
- // carry over
- if (!this.prefixNames.isEmpty() && this.unitNamePosition >= this.unitNames.size() - 1) {
- // handle prefix position
- this.unitNamePosition = 0;
- int i = this.prefixCoordinates.size() - 1;
- this.prefixCoordinates.set(i, this.prefixCoordinates.get(i) + 1);
-
- while (this.prefixCoordinates.get(i) >= this.prefixNames.size() - 1) {
- this.prefixCoordinates.set(i, 0);
- i--;
- if (i < 0) {
- this.prefixCoordinates.add(0, 0);
- }
- }
- }
-
- final StringBuilder unitName = new StringBuilder();
- for (final int i : this.prefixCoordinates) {
- unitName.append(this.prefixNames.get(i));
- }
- unitName.append(this.unitNames.get(this.unitNamePosition));
- return unitName.toString();
- }
- };
+ return new PrefixedUnitNameIterator(this);
}
@Override
diff --git a/src/test/java/UnitsDatabaseTest.java b/src/test/java/UnitsDatabaseTest.java
new file mode 100644
index 0000000..39f95a5
--- /dev/null
+++ b/src/test/java/UnitsDatabaseTest.java
@@ -0,0 +1,253 @@
+/**
+ * Copyright (C) 2019 Adrien Hopkins
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package test.java;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import org.junit.Test;
+import org.unitConverter.UnitsDatabase;
+import org.unitConverter.unit.AbstractUnit;
+import org.unitConverter.unit.DefaultUnitPrefix;
+import org.unitConverter.unit.LinearUnit;
+import org.unitConverter.unit.SI;
+import org.unitConverter.unit.Unit;
+import org.unitConverter.unit.UnitPrefix;
+
+/**
+ * A test for the {@link UnitsDatabase} class.
+ *
+ * @author Adrien Hopkins
+ * @since 2019-04-14
+ */
+public class UnitsDatabaseTest {
+ // some linear units and one nonlinear
+ private static final Unit U = SI.METRE;
+ private static final Unit V = SI.KILOGRAM;
+ private static final Unit W = SI.SECOND;
+
+ // used for testing expressions
+ // J = U^2 * V / W^2
+ private static final LinearUnit J = SI.KILOGRAM.times(SI.METRE.toExponent(2)).dividedBy(SI.SECOND.toExponent(2));
+ private static final Unit NONLINEAR = new AbstractUnit(SI.METRE) {
+
+ @Override
+ public double convertFromBase(final double value) {
+ return value + 1;
+ }
+
+ @Override
+ public double convertToBase(final double value) {
+ return value - 1;
+ }
+ };
+
+ // make the prefix values prime so I can tell which multiplications were made
+ private static final UnitPrefix A = new DefaultUnitPrefix(2);
+ private static final UnitPrefix B = new DefaultUnitPrefix(3);
+ private static final UnitPrefix C = new DefaultUnitPrefix(5);
+ private static final UnitPrefix AB = new DefaultUnitPrefix(7);
+ private static final UnitPrefix BC = new DefaultUnitPrefix(11);
+
+ /**
+ * Test that prefixes correctly apply to units.
+ *
+ * @since 2019-04-14
+ */
+ @Test
+ public void testPrefixes() {
+ final UnitsDatabase database = new UnitsDatabase();
+
+ database.addUnit("U", U);
+ database.addUnit("V", V);
+ database.addUnit("W", W);
+
+ database.addPrefix("A", A);
+ database.addPrefix("B", B);
+ database.addPrefix("C", C);
+
+ // get the product
+ final Unit abcuNonlinear = database.getUnit("ABCU");
+ assert abcuNonlinear instanceof LinearUnit;
+
+ final LinearUnit abcu = (LinearUnit) abcuNonlinear;
+ assertEquals(A.getMultiplier() * B.getMultiplier() * C.getMultiplier(), abcu.getConversionFactor(), 1e-15);
+ }
+
+ /**
+ * Tests the functionnalites of the prefixless unit map.
+ *
+ *
+ * The map should be an auto-updating view of the units in the database.
+ *
+ *
+ * @since 2019-04-14
+ */
+ @Test
+ public void testPrefixlessUnitMap() {
+ final UnitsDatabase database = new UnitsDatabase();
+ final Map prefixlessUnits = database.unitMapPrefixless();
+
+ database.addUnit("U", U);
+ database.addUnit("V", V);
+ database.addUnit("W", W);
+
+ // this should work because the map should be an auto-updating view
+ assertTrue(prefixlessUnits.containsKey("U"));
+ assertFalse(prefixlessUnits.containsKey("Z"));
+
+ assertTrue(prefixlessUnits.containsValue(U));
+ assertFalse(prefixlessUnits.containsValue(NONLINEAR));
+ }
+
+ /**
+ * Tests that the database correctly stores and retrieves units, ignoring prefixes.
+ *
+ * @since 2019-04-14
+ */
+ @Test
+ public void testPrefixlessUnits() {
+ final UnitsDatabase database = new UnitsDatabase();
+
+ database.addUnit("U", U);
+ database.addUnit("V", V);
+ database.addUnit("W", W);
+
+ assertTrue(database.containsUnitName("U"));
+ assertFalse(database.containsUnitName("Z"));
+
+ assertEquals(U, database.getUnit("U"));
+ assertEquals(null, database.getUnit("Z"));
+ }
+
+ /**
+ * Test that unit expressions return the correct value.
+ *
+ * @since 2019-04-14
+ */
+ @Test
+ public void testUnitExpressions() {
+ // load units
+ final UnitsDatabase database = new UnitsDatabase();
+
+ database.addUnit("U", U);
+ database.addUnit("V", V);
+ database.addUnit("W", W);
+ database.addUnit("fj", J.times(5));
+ database.addUnit("ej", J.times(8));
+
+ database.addPrefix("A", A);
+ database.addPrefix("B", B);
+ database.addPrefix("C", C);
+
+ // first test - test prefixes and operations
+ final Unit expected1 = J.withPrefix(A).withPrefix(B).withPrefix(C).withPrefix(C);
+ final Unit actual1 = database.getUnitFromExpression("ABV * CU^2 / W / W");
+
+ assertEquals(expected1, actual1);
+
+ // second test - test addition and subtraction
+ final Unit expected2 = J.times(58);
+ final Unit actual2 = database.getUnitFromExpression("2 fj + 6 ej");
+
+ assertEquals(expected2, actual2);
+ }
+
+ /**
+ * Tests both the unit name iterator and the name-unit entry iterator
+ *
+ * @since 2019-04-14
+ */
+ @Test
+ public void testUnitIterator() {
+ // load units
+ final UnitsDatabase database = new UnitsDatabase();
+
+ database.addUnit("J", J);
+
+ database.addPrefix("A", A);
+ database.addPrefix("B", B);
+ database.addPrefix("C", C);
+
+ final Iterator nameIterator = database.unitMap().keySet().iterator();
+ final Iterator> entryIterator = database.unitMap().entrySet().iterator();
+
+ int expectedLength = 1;
+ int unitsWithThisLengthSoFar = 0;
+
+ // loop 1000 times
+ for (int i = 0; i < 1000; i++) {
+ // expected length of next
+ if (unitsWithThisLengthSoFar >= (int) Math.pow(3, expectedLength - 1)) {
+ expectedLength++;
+ unitsWithThisLengthSoFar = 0;
+ }
+
+ 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++;
+ }
+ }
+
+ /**
+ * Determine, given a unit name that could mean multiple things, which meaning is chosen.
+ *
+ * For example, "ABCU" could mean "A-B-C-U", "AB-C-U", or "A-BC-U". In this case, "AB-C-U" is the correct choice.
+ *
+ *
+ * @since 2019-04-14
+ */
+ @Test
+ public void testUnitPrefixCombinations() {
+ // load units
+ final UnitsDatabase database = new UnitsDatabase();
+
+ database.addUnit("J", J);
+
+ database.addPrefix("A", A);
+ database.addPrefix("B", B);
+ database.addPrefix("C", C);
+ database.addPrefix("AB", AB);
+ database.addPrefix("BC", BC);
+
+ // test 1 - AB-C-J vs A-BC-J vs A-B-C-J
+ final Unit expected1 = J.withPrefix(AB).withPrefix(C);
+ final Unit actual1 = database.getUnit("ABCJ");
+
+ assertEquals(expected1, actual1);
+
+ // test 2 - ABC-J vs AB-CJ vs AB-C-J
+ database.addUnit("CJ", J.times(13));
+ database.addPrefix("ABC", new DefaultUnitPrefix(17));
+
+ final Unit expected2 = J.times(17);
+ final Unit actual2 = database.getUnit("ABCJ");
+
+ assertEquals(expected2, actual2);
+ }
+}
--
cgit v1.2.3
From 54b9f00faba367e1ef325c9d0bc75a848fadb906 Mon Sep 17 00:00:00 2001
From: Adrien Hopkins
Date: Sun, 14 Apr 2019 15:39:38 -0400
Subject: The unit and prefix viewers now use SearchBoxList.
---
src/org/unitConverter/UnitsDatabase.java | 2 +
.../converterGUI/FilterComparator.java | 50 ++-
.../unitConverter/converterGUI/SearchBoxList.java | 77 +++-
.../converterGUI/UnitConverterGUI.java | 401 +++++++--------------
unitsfile.txt | 2 +-
5 files changed, 247 insertions(+), 285 deletions(-)
(limited to 'src/org/unitConverter/UnitsDatabase.java')
diff --git a/src/org/unitConverter/UnitsDatabase.java b/src/org/unitConverter/UnitsDatabase.java
index 959c151..abe6546 100755
--- a/src/org/unitConverter/UnitsDatabase.java
+++ b/src/org/unitConverter/UnitsDatabase.java
@@ -134,6 +134,7 @@ public final class UnitsDatabase {
// 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 final List unitNames;
private final List prefixNames;
@@ -366,6 +367,7 @@ public final class UnitsDatabase {
// 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 final List unitNames;
private final List prefixNames;
diff --git a/src/org/unitConverter/converterGUI/FilterComparator.java b/src/org/unitConverter/converterGUI/FilterComparator.java
index ad8d0b0..ef94602 100755
--- a/src/org/unitConverter/converterGUI/FilterComparator.java
+++ b/src/org/unitConverter/converterGUI/FilterComparator.java
@@ -41,6 +41,12 @@ public final class FilterComparator implements Comparator {
* @since v0.1.0
*/
private final Comparator comparator;
+ /**
+ * Whether or not the comparison is case-sensitive.
+ *
+ * @since 2019-04-14
+ */
+ private final boolean caseSensitive;
/**
* Creates the {@code FilterComparator}.
@@ -60,38 +66,62 @@ public final class FilterComparator implements Comparator {
* string to filter by
* @param comparator
* comparator to fall back to if all else fails, null is compareTo.
+ * @throws NullPointerException
+ * if filter is null
* @since 2019-01-15
* @since v0.1.0
+ */
+ public FilterComparator(final String filter, final Comparator comparator) {
+ this(filter, comparator, false);
+ }
+
+ /**
+ * Creates the {@code FilterComparator}.
+ *
+ * @param filter
+ * string to filter by
+ * @param comparator
+ * comparator to fall back to if all else fails, null is compareTo.
+ * @param caseSensitive
+ * whether or not the comparator is case-sensitive
* @throws NullPointerException
* if filter is null
+ * @since 2019-04-14
*/
- public FilterComparator(final String filter, final Comparator comparator) {
+ public FilterComparator(final String filter, final Comparator comparator, final boolean caseSensitive) {
this.filter = Objects.requireNonNull(filter, "filter must not be null.");
this.comparator = comparator;
+ this.caseSensitive = caseSensitive;
}
@Override
public int compare(final String arg0, final String arg1) {
- // this is case insensitive, so make them lowercase
- final String arg0lower = arg0.toLowerCase();
- final String arg1lower = arg1.toLowerCase();
+ // if this is case insensitive, make them lowercase
+ final String str0, str1;
+ if (this.caseSensitive) {
+ str0 = arg0;
+ str1 = arg1;
+ } else {
+ str0 = arg0.toLowerCase();
+ str1 = arg1.toLowerCase();
+ }
// elements that start with the filter always go first
- if (arg0lower.startsWith(this.filter) && !arg1lower.startsWith(this.filter))
+ if (str0.startsWith(this.filter) && !str1.startsWith(this.filter))
return -1;
- else if (!arg0lower.startsWith(this.filter) && arg1lower.startsWith(this.filter))
+ else if (!str0.startsWith(this.filter) && str1.startsWith(this.filter))
return 1;
// elements that contain the filter but don't start with them go next
- if (arg0lower.contains(this.filter) && !arg1lower.contains(this.filter))
+ if (str0.contains(this.filter) && !str1.contains(this.filter))
return -1;
- else if (!arg0lower.contains(this.filter) && !arg1lower.contains(this.filter))
+ else if (!str0.contains(this.filter) && !str1.contains(this.filter))
return 1;
// other elements go last
if (this.comparator == null)
- return arg0lower.compareTo(arg1lower);
+ return str0.compareTo(str1);
else
- return this.comparator.compare(arg0lower, arg1lower);
+ return this.comparator.compare(str0, str1);
}
}
diff --git a/src/org/unitConverter/converterGUI/SearchBoxList.java b/src/org/unitConverter/converterGUI/SearchBoxList.java
index 7d3b748..35cc347 100644
--- a/src/org/unitConverter/converterGUI/SearchBoxList.java
+++ b/src/org/unitConverter/converterGUI/SearchBoxList.java
@@ -22,6 +22,7 @@ import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.util.ArrayList;
import java.util.Collection;
+import java.util.Comparator;
import java.util.function.Predicate;
import javax.swing.JList;
@@ -61,16 +62,25 @@ final class SearchBoxList extends JPanel {
// event.
private boolean searchBoxFocused = false;
- private Predicate searchFilter = o -> true;
+ private Predicate customSearchFilter = o -> true;
+ private final Comparator defaultOrdering;
+ private final boolean caseSensitive;
+
+ public SearchBoxList(final Collection itemsToFilter) {
+ this(itemsToFilter, null, false);
+ }
/**
* Creates the {@code SearchBoxList}.
*
* @since 2019-04-13
*/
- public SearchBoxList(final Collection itemsToFilter) {
+ public SearchBoxList(final Collection itemsToFilter, final Comparator defaultOrdering,
+ final boolean caseSensitive) {
super(new BorderLayout(), true);
this.itemsToFilter = itemsToFilter;
+ this.defaultOrdering = defaultOrdering;
+ this.caseSensitive = caseSensitive;
// create the components
this.listModel = new DelegateListModel<>(new ArrayList<>(itemsToFilter));
@@ -108,7 +118,7 @@ final class SearchBoxList extends JPanel {
* @since 2019-04-13
*/
public void addSearchFilter(final Predicate filter) {
- this.searchFilter = this.searchFilter.and(filter);
+ this.customSearchFilter = this.customSearchFilter.and(filter);
}
/**
@@ -117,7 +127,44 @@ final class SearchBoxList extends JPanel {
* @since 2019-04-13
*/
public void clearSearchFilters() {
- this.searchFilter = o -> true;
+ this.customSearchFilter = o -> true;
+ }
+
+ /**
+ * @return this component's search box component
+ * @since 2019-04-14
+ */
+ public final JTextField getSearchBox() {
+ return this.searchBox;
+ }
+
+ /**
+ * @param searchText
+ * text to search for
+ * @return a filter that filters out that text, based on this list's case sensitive setting
+ * @since 2019-04-14
+ */
+ private Predicate getSearchFilter(final String searchText) {
+ if (this.caseSensitive)
+ return string -> string.contains(searchText);
+ else
+ return string -> string.toLowerCase().contains(searchText.toLowerCase());
+ }
+
+ /**
+ * @return this component's list component
+ * @since 2019-04-14
+ */
+ public final JList getSearchList() {
+ return this.searchItems;
+ }
+
+ /**
+ * @return index selected in item list
+ * @since 2019-04-14
+ */
+ public int getSelectedIndex() {
+ return this.searchItems.getSelectedIndex();
}
/**
@@ -135,17 +182,18 @@ final class SearchBoxList extends JPanel {
*/
public void reapplyFilter() {
final String searchText = this.searchBoxEmpty ? "" : this.searchBox.getText();
- final FilterComparator comparator = new FilterComparator(searchText);
+ final FilterComparator comparator = new FilterComparator(searchText, this.defaultOrdering, this.caseSensitive);
+ final Predicate searchFilter = this.getSearchFilter(searchText);
this.listModel.clear();
this.itemsToFilter.forEach(string -> {
- if (string.toLowerCase().contains(searchText.toLowerCase())) {
+ if (searchFilter.test(string)) {
this.listModel.add(string);
}
});
// applies the custom filters
- this.listModel.removeIf(this.searchFilter.negate());
+ this.listModel.removeIf(this.customSearchFilter.negate());
// sorts the remaining items
this.listModel.sort(comparator);
@@ -181,23 +229,32 @@ final class SearchBoxList extends JPanel {
}
}
+ /**
+ * Runs whenever the text in the search box is changed.
+ *
+ * Reapplies the search filter, and custom filters.
+ *
+ *
+ * @since 2019-04-14
+ */
private void searchBoxTextChanged() {
if (this.searchBoxFocused) {
this.searchBoxEmpty = this.searchBox.getText().equals("");
}
final String searchText = this.searchBoxEmpty ? "" : this.searchBox.getText();
- final FilterComparator comparator = new FilterComparator(searchText);
+ final FilterComparator comparator = new FilterComparator(searchText, this.defaultOrdering, this.caseSensitive);
+ final Predicate searchFilter = this.getSearchFilter(searchText);
// initialize list with items that match the filter then sort
this.listModel.clear();
this.itemsToFilter.forEach(string -> {
- if (string.toLowerCase().contains(searchText.toLowerCase())) {
+ if (searchFilter.test(string)) {
this.listModel.add(string);
}
});
// applies the custom filters
- this.listModel.removeIf(this.searchFilter.negate());
+ this.listModel.removeIf(this.customSearchFilter.negate());
// sorts the remaining items
this.listModel.sort(comparator);
diff --git a/src/org/unitConverter/converterGUI/UnitConverterGUI.java b/src/org/unitConverter/converterGUI/UnitConverterGUI.java
index 34cbef9..cacc3b7 100755
--- a/src/org/unitConverter/converterGUI/UnitConverterGUI.java
+++ b/src/org/unitConverter/converterGUI/UnitConverterGUI.java
@@ -35,16 +35,12 @@ import javax.swing.JComboBox;
import javax.swing.JFormattedTextField;
import javax.swing.JFrame;
import javax.swing.JLabel;
-import javax.swing.JList;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
-import javax.swing.JScrollPane;
import javax.swing.JSlider;
import javax.swing.JTabbedPane;
import javax.swing.JTextArea;
import javax.swing.JTextField;
-import javax.swing.ListModel;
-import javax.swing.ListSelectionModel;
import org.unitConverter.UnitsDatabase;
import org.unitConverter.dimension.StandardDimensions;
@@ -95,20 +91,14 @@ final class UnitConverterGUI {
private final View view;
/** The units known by the program. */
- private final UnitsDatabase units;
+ private final UnitsDatabase database;
/** The names of all of the units */
private final List unitNames;
- /** The names of all of the units, but filtered */
- private final DelegateListModel unitNamesFiltered;
-
/** The names of all of the prefixes */
private final List prefixNames;
- /** The names of all of the prefixes */
- private final DelegateListModel prefixNamesFiltered;
-
/** The names of all of the dimensions */
private final List dimensionNames;
@@ -128,23 +118,23 @@ final class UnitConverterGUI {
this.view = view;
// load initial units
- this.units = new UnitsDatabase();
- Presenter.addDefaults(this.units);
+ this.database = new UnitsDatabase();
+ Presenter.addDefaults(this.database);
- this.units.loadUnitsFile(new File("unitsfile.txt"));
- this.units.loadDimensionFile(new File("dimensionfile.txt"));
+ this.database.loadUnitsFile(new File("unitsfile.txt"));
+ this.database.loadDimensionFile(new File("dimensionfile.txt"));
// a comparator that can be used to compare prefix names
// any name that does not exist is less than a name that does.
// otherwise, they are compared by value
this.prefixNameComparator = (o1, o2) -> {
- if (!Presenter.this.units.containsPrefixName(o1))
+ if (!Presenter.this.database.containsPrefixName(o1))
return -1;
- else if (!Presenter.this.units.containsPrefixName(o2))
+ else if (!Presenter.this.database.containsPrefixName(o2))
return 1;
- final UnitPrefix p1 = Presenter.this.units.getPrefix(o1);
- final UnitPrefix p2 = Presenter.this.units.getPrefix(o2);
+ final UnitPrefix p1 = Presenter.this.database.getPrefix(o1);
+ final UnitPrefix p2 = Presenter.this.database.getPrefix(o2);
if (p1.getMultiplier() < p2.getMultiplier())
return -1;
@@ -154,19 +144,13 @@ final class UnitConverterGUI {
return o1.compareTo(o2);
};
- this.unitNames = new ArrayList<>(this.units.unitMapPrefixless().keySet());
+ this.unitNames = new ArrayList<>(this.database.unitMapPrefixless().keySet());
this.unitNames.sort(null); // sorts it using Comparable
- this.unitNamesFiltered = new DelegateListModel<>(new ArrayList<>(this.units.unitMapPrefixless().keySet()));
- this.unitNamesFiltered.sort(null); // sorts it using Comparable
-
- this.prefixNames = new ArrayList<>(this.units.prefixMap().keySet());
+ this.prefixNames = new ArrayList<>(this.database.prefixMap().keySet());
this.prefixNames.sort(this.prefixNameComparator); // sorts it using my comparator
- this.prefixNamesFiltered = new DelegateListModel<>(new ArrayList<>(this.units.prefixMap().keySet()));
- this.prefixNamesFiltered.sort(this.prefixNameComparator); // sorts it using my comparator
-
- this.dimensionNames = new DelegateListModel<>(new ArrayList<>(this.units.dimensionMap().keySet()));
+ this.dimensionNames = new DelegateListModel<>(new ArrayList<>(this.database.dimensionMap().keySet()));
this.dimensionNames.sort(null); // sorts it using Comparable
// a Predicate that returns true iff the argument is a full base unit
@@ -174,9 +158,43 @@ final class UnitConverterGUI {
// print out unit counts
System.out.printf("Successfully loaded %d units with %d unit names (%d base units).%n",
- new HashSet<>(this.units.unitMapPrefixless().values()).size(),
- this.units.unitMapPrefixless().size(),
- new HashSet<>(this.units.unitMapPrefixless().values()).stream().filter(isFullBase).count());
+ new HashSet<>(this.database.unitMapPrefixless().values()).size(),
+ this.database.unitMapPrefixless().size(),
+ new HashSet<>(this.database.unitMapPrefixless().values()).stream().filter(isFullBase).count());
+ }
+
+ /**
+ * Converts in the dimension-based converter
+ *
+ * @since 2019-04-13
+ */
+ public final void convertDimensionBased() {
+ final String fromSelection = this.view.getFromSelection();
+ if (fromSelection == null) {
+ this.view.showErrorDialog("Error", "No unit selected in From field");
+ return;
+ }
+ final String toSelection = this.view.getToSelection();
+ if (toSelection == null) {
+ this.view.showErrorDialog("Error", "No unit selected in To field");
+ return;
+ }
+
+ final Unit from = this.database.getUnit(fromSelection);
+ final Unit to = this.database.getUnit(toSelection);
+
+ final String input = this.view.getDimensionConverterInput();
+ if (input.equals("")) {
+ this.view.showErrorDialog("Error", "No value to convert entered.");
+ return;
+ }
+ final double beforeValue = Double.parseDouble(input);
+ final double value = to.convertFromBase(from.convertToBase(beforeValue));
+
+ final String output = this.getRoundedString(value);
+
+ this.view.setDimensionConverterOutputText(
+ String.format("%s %s = %s %s", input, fromSelection, output, toSelection));
}
/**
@@ -190,7 +208,7 @@ final class UnitConverterGUI {
* @since 2019-01-26
* @since v0.1.0
*/
- public final void convert() {
+ public final void convertExpressions() {
final String fromUnitString = this.view.getFromText();
final String toUnitString = this.view.getToText();
@@ -202,7 +220,7 @@ final class UnitConverterGUI {
// try to parse from
final Unit from;
try {
- from = this.units.getUnitFromExpression(fromUnitString);
+ from = this.database.getUnitFromExpression(fromUnitString);
} catch (final IllegalArgumentException e) {
this.view.showErrorDialog("Parse Error", "Could not recognize text in From entry: " + e.getMessage());
return;
@@ -212,11 +230,11 @@ final class UnitConverterGUI {
// try to parse to
final Unit to;
try {
- if (this.units.containsUnitName(toUnitString)) {
+ if (this.database.containsUnitName(toUnitString)) {
// if it's a unit, convert to that
- to = this.units.getUnit(toUnitString);
+ to = this.database.getUnit(toUnitString);
} else {
- to = this.units.getUnitFromExpression(toUnitString);
+ to = this.database.getUnitFromExpression(toUnitString);
}
} catch (final IllegalArgumentException e) {
this.view.showErrorDialog("Parse Error", "Could not recognize text in To entry: " + e.getMessage());
@@ -233,50 +251,35 @@ final class UnitConverterGUI {
value = to.convertFromBase(from.convertToBase(1));
// round value
- final BigDecimal bigValue = new BigDecimal(value).round(new MathContext(this.significantFigures));
- String output = bigValue.toString();
+ final String output = this.getRoundedString(value);
- // remove trailing zeroes
- if (output.contains(".")) {
- while (output.endsWith("0")) {
- output = output.substring(0, output.length() - 1);
- }
- if (output.endsWith(".")) {
- output = output.substring(0, output.length() - 1);
- }
- }
-
- this.view.setOutputText(String.format("%s = %s %s", fromUnitString, output, toUnitString));
+ this.view.setExpressionConverterOutputText(
+ String.format("%s = %s %s", fromUnitString, output, toUnitString));
}
/**
- * Converts in the dimension-based converter
- *
+ * @return a list of all of the unit dimensions
* @since 2019-04-13
*/
- public final void convertDimensionBased() {
- final String fromSelection = this.view.getFromSelection();
- if (fromSelection == null) {
- this.view.showErrorDialog("Error", "No unit selected in From field");
- return;
- }
- final String toSelection = this.view.getToSelection();
- if (toSelection == null) {
- this.view.showErrorDialog("Error", "No unit selected in To field");
- return;
- }
-
- final Unit from = this.units.getUnit(fromSelection);
- final Unit to = this.units.getUnit(toSelection);
+ public final List dimensionNameList() {
+ return this.dimensionNames;
+ }
- final String input = this.view.getDimensionBasedInput();
- if (input.equals("")) {
- this.view.showErrorDialog("Error", "No value to convert entered.");
- return;
- }
- final double beforeValue = Double.parseDouble(input);
- final double value = to.convertFromBase(from.convertToBase(beforeValue));
+ /**
+ * @return a comparator to compare prefix names
+ * @since 2019-04-14
+ */
+ public final Comparator getPrefixNameComparator() {
+ return this.prefixNameComparator;
+ }
+ /**
+ * @param value
+ * value to round
+ * @return string of that value rounded to {@code significantDigits} significant digits.
+ * @since 2019-04-14
+ */
+ private final String getRoundedString(final double value) {
// round value
final BigDecimal bigValue = new BigDecimal(value).round(new MathContext(this.significantFigures));
String output = bigValue.toString();
@@ -291,86 +294,15 @@ final class UnitConverterGUI {
}
}
- this.view.setDimensionBasedOutputText(
- String.format("%s %s = %s %s", input, fromSelection, output, toSelection));
- }
-
- /**
- * @return a list of all of the unit dimensions
- * @since 2019-04-13
- */
- public final List dimensionNameList() {
- return this.dimensionNames;
- }
-
- /**
- * Filters the filtered model for units
- *
- * @param filter
- * filter to use
- * @since 2019-01-15
- * @since v0.1.0
- */
- private final void filterFilteredPrefixModel(final Predicate filter) {
- this.prefixNamesFiltered.clear();
- for (final String prefixName : this.prefixNames) {
- if (filter.test(prefixName)) {
- this.prefixNamesFiltered.add(prefixName);
- }
- }
- }
-
- /**
- * Filters the filtered model for units
- *
- * @param filter
- * filter to use
- * @since 2019-01-15
- * @since v0.1.0
- */
- private final void filterFilteredUnitModel(final Predicate filter) {
- this.unitNamesFiltered.clear();
- for (final String unitName : this.unitNames) {
- if (filter.test(unitName)) {
- this.unitNamesFiltered.add(unitName);
- }
- }
+ return output;
}
/**
- * @return a list model of all of the unit keys
- * @since 2019-01-14
- * @since v0.1.0
- */
- public final ListModel keyListModel() {
- return this.unitNamesFiltered;
- }
-
- /**
- * Runs whenever the prefix filter is changed.
- *
- * Filters the prefix list then sorts it using a {@code FilterComparator}.
- *
- *
- * @since 2019-01-15
- * @since v0.1.0
- */
- public final void prefixFilterUpdated() {
- final String filter = this.view.getPrefixFilterText();
- if (filter.equals("")) {
- this.filterFilteredPrefixModel(t -> true);
- } else {
- this.filterFilteredPrefixModel(t -> t.contains(filter));
- }
- this.prefixNamesFiltered.sort(new FilterComparator(filter));
- }
-
- /**
- * @return a list model of all of the prefix names
- * @since 2019-01-15
+ * @return a set of all prefix names in the database
+ * @since 2019-04-14
*/
- public final ListModel prefixNameListModel() {
- return this.prefixNamesFiltered;
+ public final Set prefixNameSet() {
+ return this.database.prefixMap().keySet();
}
/**
@@ -383,12 +315,11 @@ final class UnitConverterGUI {
* @since v0.1.0
*/
public final void prefixSelected() {
- final int index = this.view.getPrefixListSelection();
- if (index == -1)
+ final String prefixName = this.view.getPrefixViewerSelection();
+ if (prefixName == null)
return;
else {
- final String prefixName = this.prefixNamesFiltered.get(index);
- final UnitPrefix prefix = this.units.getPrefix(prefixName);
+ final UnitPrefix prefix = this.database.getPrefix(prefixName);
this.view.setPrefixTextBoxText(String.format("%s%nMultiplier: %s", prefixName, prefix.getMultiplier()));
}
@@ -403,25 +334,6 @@ final class UnitConverterGUI {
this.significantFigures = significantFigures;
}
- /**
- * Runs whenever the unit filter is changed.
- *
- * Filters the unit list then sorts it using a {@code FilterComparator}.
- *
- *
- * @since 2019-01-15
- * @since v0.1.0
- */
- public final void unitFilterUpdated() {
- final String filter = this.view.getUnitFilterText();
- if (filter.equals("")) {
- this.filterFilteredUnitModel(t -> true);
- } else {
- this.filterFilteredUnitModel(t -> t.contains(filter));
- }
- this.unitNamesFiltered.sort(new FilterComparator(filter));
- }
-
/**
* Returns true if and only if the unit represented by {@code unitName} has the dimension represented by
* {@code dimensionName}.
@@ -433,9 +345,9 @@ final class UnitConverterGUI {
* @return whether unit has dimenision
* @since 2019-04-13
*/
- public boolean unitMatchesDimension(final String unitName, final String dimensionName) {
- final Unit unit = this.units.getUnit(unitName);
- final UnitDimension dimension = this.units.getDimension(dimensionName);
+ public final boolean unitMatchesDimension(final String unitName, final String dimensionName) {
+ final Unit unit = this.database.getUnit(unitName);
+ final UnitDimension dimension = this.database.getDimension(dimensionName);
return unit.getDimension().equals(dimension);
}
@@ -448,20 +360,23 @@ final class UnitConverterGUI {
* @since 2019-01-15
* @since v0.1.0
*/
- public void unitNameSelected() {
- final int index = this.view.getUnitListSelection();
- if (index == -1)
+ public final void unitNameSelected() {
+ final String unitName = this.view.getUnitViewerSelection();
+ if (unitName == null)
return;
else {
- final String unitName = this.unitNamesFiltered.get(index);
- final Unit unit = this.units.getUnit(unitName);
+ final Unit unit = this.database.getUnit(unitName);
this.view.setUnitTextBoxText(unit.toString());
}
}
+ /**
+ * @return a set of all of the unit names
+ * @since 2019-04-14
+ */
public final Set unitNameSet() {
- return this.units.unitMapPrefixless().keySet();
+ return this.database.unitMapPrefixless().keySet();
}
}
@@ -471,26 +386,17 @@ final class UnitConverterGUI {
/** The view's associated presenter. */
private final Presenter presenter;
- /** The list of unit names in the unit viewer */
- private final JList unitNameList;
- /** The list of prefix names in the prefix viewer */
- private final JList prefixNameList;
- /** The unit search box in the unit viewer */
- private final JTextField unitFilterEntry;
- /** The text box for unit data in the unit viewer */
- private final JTextArea unitTextBox;
- /** The prefix search box in the prefix viewer */
- private final JTextField prefixFilterEntry;
- /** The text box for prefix data in the prefix viewer */
- private final JTextArea prefixTextBox;
+ // DIMENSION-BASED CONVERTER
+ /** The panel for inputting values in the dimension-based converter */
+ private final JTextField valueInput;
/** The panel for "From" in the dimension-based converter */
private final SearchBoxList fromSearch;
/** The panel for "To" in the dimension-based converter */
private final SearchBoxList toSearch;
- /** The panel for inputting values in the dimension-based converter */
- private final JTextField valueInput;
/** The output area in the dimension-based converter */
private final JTextArea dimensionBasedOutput;
+
+ // EXPRESSION-BASED CONVERTER
/** The "From" entry in the conversion panel */
private final JTextField fromEntry;
/** The "To" entry in the conversion panel */
@@ -498,6 +404,16 @@ final class UnitConverterGUI {
/** The output area in the conversion panel */
private final JTextArea output;
+ // UNIT AND PREFIX VIEWERS
+ /** The searchable list of unit names in the unit viewer */
+ private final SearchBoxList unitNameList;
+ /** The searchable list of prefix names in the prefix viewer */
+ private final SearchBoxList prefixNameList;
+ /** The text box for unit data in the unit viewer */
+ private final JTextArea unitTextBox;
+ /** The text box for prefix data in the prefix viewer */
+ private final JTextArea prefixTextBox;
+
/**
* Creates the {@code View}.
*
@@ -510,11 +426,10 @@ final class UnitConverterGUI {
this.frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
// create the components
- this.unitNameList = new JList<>(this.presenter.keyListModel());
- this.prefixNameList = new JList<>(this.presenter.prefixNameListModel());
- this.unitFilterEntry = new JTextField();
+ this.unitNameList = new SearchBoxList(this.presenter.unitNameSet());
+ this.prefixNameList = new SearchBoxList(this.presenter.prefixNameSet(),
+ this.presenter.getPrefixNameComparator(), true);
this.unitTextBox = new JTextArea();
- this.prefixFilterEntry = new JTextField();
this.prefixTextBox = new JTextArea();
this.fromSearch = new SearchBoxList(this.presenter.unitNameSet());
this.toSearch = new SearchBoxList(this.presenter.unitNameSet());
@@ -534,7 +449,7 @@ final class UnitConverterGUI {
* @return value in dimension-based converter
* @since 2019-04-13
*/
- public String getDimensionBasedInput() {
+ public String getDimensionConverterInput() {
return this.valueInput.getText();
}
@@ -556,21 +471,12 @@ final class UnitConverterGUI {
}
/**
- * @return text in prefix filter
+ * @return index of selected prefix in prefix viewer
* @since 2019-01-15
* @since v0.1.0
*/
- public String getPrefixFilterText() {
- return this.prefixFilterEntry.getText();
- }
-
- /**
- * @return index of selected prefix
- * @since 2019-01-15
- * @since v0.1.0
- */
- public int getPrefixListSelection() {
- return this.prefixNameList.getSelectedIndex();
+ public String getPrefixViewerSelection() {
+ return this.prefixNameList.getSelectedValue();
}
/**
@@ -591,20 +497,12 @@ final class UnitConverterGUI {
}
/**
- * @return text in unit filter
- * @see javax.swing.text.JTextComponent#getText()
- */
- public String getUnitFilterText() {
- return this.unitFilterEntry.getText();
- }
-
- /**
- * @return index of selected unit
+ * @return index of selected unit in unit viewer
* @since 2019-01-15
* @since v0.1.0
*/
- public int getUnitListSelection() {
- return this.unitNameList.getSelectedIndex();
+ public String getUnitViewerSelection() {
+ return this.unitNameList.getSelectedValue();
}
/**
@@ -764,7 +662,7 @@ final class UnitConverterGUI {
final JButton convertButton = new JButton("Convert!");
convertExpressionPanel.add(convertButton);
- convertButton.addActionListener(e -> this.presenter.convert());
+ convertButton.addActionListener(e -> this.presenter.convertExpressions());
}
{ // output of conversion
@@ -808,24 +706,11 @@ final class UnitConverterGUI {
unitLookupPanel.setLayout(new GridLayout());
- { // panel for listing and searching
- final JPanel listPanel = new JPanel();
- unitLookupPanel.add(listPanel);
-
- listPanel.setLayout(new BorderLayout());
+ { // search panel
+ unitLookupPanel.add(this.unitNameList);
- { // unit search box
- listPanel.add(this.unitFilterEntry, BorderLayout.PAGE_START);
- this.unitFilterEntry.addCaretListener(e -> this.presenter.unitFilterUpdated());
- }
-
- { // a list of units
- listPanel.add(new JScrollPane(this.unitNameList), BorderLayout.CENTER);
- this.unitNameList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); // temp
- this.unitNameList.addListSelectionListener(e -> {
- this.presenter.unitNameSelected();
- });
- }
+ this.unitNameList.getSearchList()
+ .addListSelectionListener(e -> this.presenter.unitNameSelected());
}
{ // the text box for unit's toString
@@ -842,23 +727,10 @@ final class UnitConverterGUI {
prefixLookupPanel.setLayout(new GridLayout(1, 2));
{ // panel for listing and seaching
- final JPanel prefixListPanel = new JPanel();
- prefixLookupPanel.add(prefixListPanel);
-
- prefixListPanel.setLayout(new BorderLayout());
-
- { // prefix search box
- prefixListPanel.add(this.prefixFilterEntry, BorderLayout.PAGE_START);
- this.prefixFilterEntry.addCaretListener(e -> this.presenter.prefixFilterUpdated());
- }
+ prefixLookupPanel.add(this.prefixNameList);
- { // a list of prefixes
- prefixListPanel.add(new JScrollPane(this.prefixNameList), BorderLayout.CENTER);
- this.prefixNameList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); // temp
- this.prefixNameList.addListSelectionListener(e -> {
- this.presenter.prefixSelected();
- });
- }
+ this.prefixNameList.getSearchList()
+ .addListSelectionListener(e -> this.presenter.prefixSelected());
}
{ // the text box for prefix's toString
@@ -876,7 +748,7 @@ final class UnitConverterGUI {
* text to set
* @since 2019-04-13
*/
- public void setDimensionBasedOutputText(final String text) {
+ public void setDimensionConverterOutputText(final String text) {
this.dimensionBasedOutput.setText(text);
}
@@ -888,12 +760,12 @@ final class UnitConverterGUI {
* @since 2019-01-15
* @since v0.1.0
*/
- public void setOutputText(final String text) {
+ public void setExpressionConverterOutputText(final String text) {
this.output.setText(text);
}
/**
- * Sets the text of the prefix text box.
+ * Sets the text of the prefix text box in the prefix viewer.
*
* @param text
* text to set
@@ -905,14 +777,15 @@ final class UnitConverterGUI {
}
/**
- * Sets the text of the unit text box.
+ * Sets the text of the unit text box in the unit viewer.
*
- * @param t
+ * @param text
* text to set
- * @see javax.swing.text.JTextComponent#setText(java.lang.String)
+ * @since 2019-01-15
+ * @since v0.1.0
*/
- public void setUnitTextBoxText(final String t) {
- this.unitTextBox.setText(t);
+ public void setUnitTextBoxText(final String text) {
+ this.unitTextBox.setText(text);
}
/**
diff --git a/unitsfile.txt b/unitsfile.txt
index 553fd5e..bda9b81 100755
--- a/unitsfile.txt
+++ b/unitsfile.txt
@@ -138,7 +138,7 @@ radian m / m
rad radian
steradian m^2 / m^2
sr steradian
-degree 360 / tau * radian
+degree tau / 360 radian
deg degree
° degree
--
cgit v1.2.3
From 73d305684d3549d17ebd95a5fdb7d366849db226 Mon Sep 17 00:00:00 2001
From: Adrien Hopkins
Date: Sun, 14 Apr 2019 17:29:50 -0400
Subject: Added @since tags to all classes and methods from v0.2.0
---
src/org/unitConverter/UnitsDatabase.java | 47 +++++++++++++++++++++-
.../converterGUI/FilterComparator.java | 2 +
.../converterGUI/MutablePredicate.java | 10 +++++
.../unitConverter/converterGUI/SearchBoxList.java | 35 ++++++++++++++++
.../converterGUI/UnitConverterGUI.java | 13 ++++++
.../unitConverter/converterGUI/package-info.java | 1 +
src/org/unitConverter/dimension/package-info.java | 1 +
src/org/unitConverter/math/DecimalComparison.java | 7 ++++
src/org/unitConverter/math/ExpressionParser.java | 46 ++++++++++++++++++++-
src/org/unitConverter/unit/AbstractUnit.java | 1 -
src/org/unitConverter/unit/BaseUnit.java | 1 +
src/org/unitConverter/unit/DefaultUnitPrefix.java | 1 +
src/org/unitConverter/unit/LinearUnit.java | 3 ++
src/org/unitConverter/unit/UnitPrefix.java | 3 ++
src/org/unitConverter/unit/package-info.java | 1 +
src/test/java/ExpressionParserTest.java | 1 +
src/test/java/UnitTest.java | 1 +
src/test/java/UnitsDatabaseTest.java | 7 ++++
src/test/java/package-info.java | 1 +
19 files changed, 179 insertions(+), 3 deletions(-)
(limited to 'src/org/unitConverter/UnitsDatabase.java')
diff --git a/src/org/unitConverter/UnitsDatabase.java b/src/org/unitConverter/UnitsDatabase.java
index abe6546..e5d2f67 100755
--- a/src/org/unitConverter/UnitsDatabase.java
+++ b/src/org/unitConverter/UnitsDatabase.java
@@ -72,9 +72,15 @@ public final class UnitsDatabase {
* "A", inputting "ABC" will return the unit "C" with the prefix "AB", not "BC" with the prefix "A".
*
*
+ *
+ * This map is infinite in size if there is at least one unit and at least one prefix. If it is infinite, some
+ * operations that only work with finite collections, like converting name/entry sets to arrays, will throw an
+ * {@code UnsupportedOperationException}.
+ *
*
* @author Adrien Hopkins
* @since 2019-04-13
+ * @since v0.2.0
*/
private static final class PrefixedUnitMap implements Map {
/**
@@ -82,6 +88,7 @@ public final class UnitsDatabase {
*
* @author Adrien Hopkins
* @since 2019-04-13
+ * @since v0.2.0
*/
private static final class PrefixedUnitEntrySet extends AbstractSet> {
/**
@@ -89,6 +96,7 @@ public final class UnitsDatabase {
*
* @author Adrien Hopkins
* @since 2019-04-14
+ * @since v0.2.0
*/
private static final class PrefixedUnitEntry implements Entry {
private final String key;
@@ -98,8 +106,11 @@ public final class UnitsDatabase {
* Creates the {@code PrefixedUnitEntry}.
*
* @param key
+ * key
* @param value
+ * value
* @since 2019-04-14
+ * @since v0.2.0
*/
public PrefixedUnitEntry(final String key, final Unit value) {
this.key = key;
@@ -127,6 +138,7 @@ public final class UnitsDatabase {
*
* @author Adrien Hopkins
* @since 2019-04-14
+ * @since v0.2.0
*/
private static final class PrefixedUnitEntryIterator implements Iterator> {
// position in the unit list
@@ -143,6 +155,7 @@ public final class UnitsDatabase {
* Creates the {@code UnitsDatabase.PrefixedUnitMap.PrefixedUnitNameSet.PrefixedUnitNameIterator}.
*
* @since 2019-04-14
+ * @since v0.2.0
*/
public PrefixedUnitEntryIterator(final PrefixedUnitEntrySet set) {
this.map = set.map;
@@ -153,6 +166,7 @@ public final class UnitsDatabase {
/**
* @return current unit name
* @since 2019-04-14
+ * @since v0.2.0
*/
private String getCurrentUnitName() {
final StringBuilder unitName = new StringBuilder();
@@ -180,6 +194,7 @@ public final class UnitsDatabase {
* Changes this iterator's position to the next available one.
*
* @since 2019-04-14
+ * @since v0.2.0
*/
private void incrementPosition() {
this.unitNamePosition++;
@@ -239,7 +254,9 @@ public final class UnitsDatabase {
* Creates the {@code PrefixedUnitNameSet}.
*
* @param map
+ * map that created this set
* @since 2019-04-13
+ * @since v0.2.0
*/
public PrefixedUnitEntrySet(final PrefixedUnitMap map) {
this.map = map;
@@ -353,6 +370,7 @@ public final class UnitsDatabase {
*
* @author Adrien Hopkins
* @since 2019-04-13
+ * @since v0.2.0
*/
private static final class PrefixedUnitNameSet extends AbstractSet {
/**
@@ -360,6 +378,7 @@ public final class UnitsDatabase {
*
* @author Adrien Hopkins
* @since 2019-04-14
+ * @since v0.2.0
*/
private static final class PrefixedUnitNameIterator implements Iterator {
// position in the unit list
@@ -376,6 +395,7 @@ public final class UnitsDatabase {
* Creates the {@code UnitsDatabase.PrefixedUnitMap.PrefixedUnitNameSet.PrefixedUnitNameIterator}.
*
* @since 2019-04-14
+ * @since v0.2.0
*/
public PrefixedUnitNameIterator(final PrefixedUnitNameSet set) {
this.map = set.map;
@@ -386,6 +406,7 @@ public final class UnitsDatabase {
/**
* @return current unit name
* @since 2019-04-14
+ * @since v0.2.0
*/
private String getCurrentUnitName() {
final StringBuilder unitName = new StringBuilder();
@@ -413,6 +434,7 @@ public final class UnitsDatabase {
* Changes this iterator's position to the next available one.
*
* @since 2019-04-14
+ * @since v0.2.0
*/
private void incrementPosition() {
this.unitNamePosition++;
@@ -472,7 +494,9 @@ public final class UnitsDatabase {
* Creates the {@code PrefixedUnitNameSet}.
*
* @param map
+ * map that created this set
* @since 2019-04-13
+ * @since v0.2.0
*/
public PrefixedUnitNameSet(final PrefixedUnitMap map) {
this.map = map;
@@ -567,13 +591,13 @@ public final class UnitsDatabase {
// infinite set
throw new UnsupportedOperationException("Cannot make an infinite set into an array.");
}
-
}
/**
* The units stored in this collection, without prefixes.
*
* @since 2019-04-13
+ * @since v0.2.0
*/
private final Map units;
@@ -581,6 +605,7 @@ public final class UnitsDatabase {
* The available prefixes for use.
*
* @since 2019-04-13
+ * @since v0.2.0
*/
private final Map prefixes;
@@ -593,8 +618,11 @@ public final class UnitsDatabase {
* Creates the {@code PrefixedUnitMap}.
*
* @param units
+ * map mapping unit names to units
* @param prefixes
+ * map mapping prefix names to prefixes
* @since 2019-04-13
+ * @since v0.2.0
*/
public PrefixedUnitMap(final Map units, final Map prefixes) {
// I am making unmodifiable maps to ensure I don't accidentally make changes.
@@ -804,6 +832,7 @@ public final class UnitsDatabase {
* exponent
* @return result
* @since 2019-04-10
+ * @since v0.2.0
*/
private static final LinearUnit exponentiateUnits(final LinearUnit base, final LinearUnit exponentUnit) {
// exponent function - first check if o2 is a number,
@@ -841,6 +870,7 @@ public final class UnitsDatabase {
* The dimensions in this system.
*
* @since 2019-03-14
+ * @since v0.2.0
*/
private final Map dimensions;
@@ -848,6 +878,7 @@ public final class UnitsDatabase {
* A map mapping strings to units (including prefixes)
*
* @since 2019-04-13
+ * @since v0.2.0
*/
private final Map units;
@@ -855,6 +886,7 @@ public final class UnitsDatabase {
* A parser that can parse unit expressions.
*
* @since 2019-03-22
+ * @since v0.2.0
*/
private final ExpressionParser unitExpressionParser = new ExpressionParser.Builder<>(
this::getLinearUnit).addBinaryOperator("+", (o1, o2) -> o1.plus(o2), 0)
@@ -867,6 +899,7 @@ public final class UnitsDatabase {
* A parser that can parse unit prefix expressions
*
* @since 2019-04-13
+ * @since v0.2.0
*/
private final ExpressionParser prefixExpressionParser = new ExpressionParser.Builder<>(this::getPrefix)
.addBinaryOperator("*", (o1, o2) -> o1.times(o2), 0).addSpaceFunction("*")
@@ -877,6 +910,7 @@ public final class UnitsDatabase {
* A parser that can parse unit dimension expressions.
*
* @since 2019-04-13
+ * @since v0.2.0
*/
private final ExpressionParser unitDimensionParser = new ExpressionParser.Builder<>(
this::getDimension).addBinaryOperator("*", (o1, o2) -> o1.times(o2), 0).addSpaceFunction("*")
@@ -905,6 +939,7 @@ public final class UnitsDatabase {
* @throws NullPointerException
* if name or dimension is null
* @since 2019-03-14
+ * @since v0.2.0
*/
public void addDimension(final String name, final UnitDimension dimension) {
this.dimensions.put(Objects.requireNonNull(name, "name must not be null."),
@@ -919,6 +954,7 @@ public final class UnitsDatabase {
* @param lineCounter
* number of line, for error messages
* @since 2019-04-10
+ * @since v0.2.0
*/
private void addDimensionFromLine(final String line, final long lineCounter) {
// ignore lines that start with a # sign - they're comments
@@ -1004,6 +1040,7 @@ public final class UnitsDatabase {
* @param lineCounter
* number of line, for error messages
* @since 2019-04-10
+ * @since v0.2.0
*/
private void addUnitOrPrefixFromLine(final String line, final long lineCounter) {
// ignore lines that start with a # sign - they're comments
@@ -1064,6 +1101,7 @@ public final class UnitsDatabase {
* name to test
* @return if database contains name
* @since 2019-03-14
+ * @since v0.2.0
*/
public boolean containsDimensionName(final String name) {
return this.dimensions.containsKey(name);
@@ -1098,6 +1136,7 @@ public final class UnitsDatabase {
/**
* @return a map mapping dimension names to dimensions
* @since 2019-04-13
+ * @since v0.2.0
*/
public Map dimensionMap() {
return Collections.unmodifiableMap(this.dimensions);
@@ -1114,6 +1153,7 @@ public final class UnitsDatabase {
* dimension's name
* @return dimension
* @since 2019-03-14
+ * @since v0.2.0
*/
public UnitDimension getDimension(final String name) {
Objects.requireNonNull(name, "name must not be null.");
@@ -1152,6 +1192,7 @@ public final class UnitsDatabase {
* @throws NullPointerException
* if expression is null
* @since 2019-04-13
+ * @since v0.2.0
*/
public UnitDimension getDimensionFromExpression(final String expression) {
Objects.requireNonNull(expression, "expression must not be null.");
@@ -1180,6 +1221,7 @@ public final class UnitsDatabase {
* unit's name
* @return unit
* @since 2019-03-22
+ * @since v0.2.0
*/
private LinearUnit getLinearUnit(final String name) {
// see if I am using a function-unit like tempC(100)
@@ -1411,6 +1453,7 @@ public final class UnitsDatabase {
/**
* @return a map mapping prefix names to prefixes
* @since 2019-04-13
+ * @since v0.2.0
*/
public Map prefixMap() {
return Collections.unmodifiableMap(this.prefixes);
@@ -1419,6 +1462,7 @@ public final class UnitsDatabase {
/**
* @return a map mapping unit names to units, including prefixed names
* @since 2019-04-13
+ * @since v0.2.0
*/
public Map unitMap() {
return this.units; // PrefixedUnitMap is immutable so I don't need to make an unmodifiable map.
@@ -1427,6 +1471,7 @@ public final class UnitsDatabase {
/**
* @return a map mapping unit names to units, ignoring prefixes
* @since 2019-04-13
+ * @since v0.2.0
*/
public Map unitMapPrefixless() {
return Collections.unmodifiableMap(this.prefixlessUnits);
diff --git a/src/org/unitConverter/converterGUI/FilterComparator.java b/src/org/unitConverter/converterGUI/FilterComparator.java
index 2d0e7f9..7b17bfc 100755
--- a/src/org/unitConverter/converterGUI/FilterComparator.java
+++ b/src/org/unitConverter/converterGUI/FilterComparator.java
@@ -45,6 +45,7 @@ final class FilterComparator implements Comparator {
* Whether or not the comparison is case-sensitive.
*
* @since 2019-04-14
+ * @since v0.2.0
*/
private final boolean caseSensitive;
@@ -87,6 +88,7 @@ final class FilterComparator implements Comparator {
* @throws NullPointerException
* if filter is null
* @since 2019-04-14
+ * @since v0.2.0
*/
public FilterComparator(final String filter, final Comparator comparator, final boolean caseSensitive) {
this.filter = Objects.requireNonNull(filter, "filter must not be null.");
diff --git a/src/org/unitConverter/converterGUI/MutablePredicate.java b/src/org/unitConverter/converterGUI/MutablePredicate.java
index 157903c..e15b3cd 100644
--- a/src/org/unitConverter/converterGUI/MutablePredicate.java
+++ b/src/org/unitConverter/converterGUI/MutablePredicate.java
@@ -23,14 +23,22 @@ import java.util.function.Predicate;
*
* @author Adrien Hopkins
* @since 2019-04-13
+ * @since v0.2.0
*/
final class MutablePredicate implements Predicate {
+ /**
+ * The predicate stored in this {@code MutablePredicate}
+ *
+ * @since 2019-04-13
+ * @since v0.2.0
+ */
private Predicate predicate;
/**
* Creates the {@code MutablePredicate}.
*
* @since 2019-04-13
+ * @since v0.2.0
*/
public MutablePredicate(final Predicate predicate) {
this.predicate = predicate;
@@ -39,6 +47,7 @@ final class MutablePredicate implements Predicate {
/**
* @return predicate
* @since 2019-04-13
+ * @since v0.2.0
*/
public final Predicate getPredicate() {
return this.predicate;
@@ -48,6 +57,7 @@ final class MutablePredicate implements Predicate {
* @param predicate
* new value of predicate
* @since 2019-04-13
+ * @since v0.2.0
*/
public final void setPredicate(final Predicate predicate) {
this.predicate = predicate;
diff --git a/src/org/unitConverter/converterGUI/SearchBoxList.java b/src/org/unitConverter/converterGUI/SearchBoxList.java
index 35cc347..1995466 100644
--- a/src/org/unitConverter/converterGUI/SearchBoxList.java
+++ b/src/org/unitConverter/converterGUI/SearchBoxList.java
@@ -33,20 +33,29 @@ import javax.swing.JTextField;
/**
* @author Adrien Hopkins
* @since 2019-04-13
+ * @since v0.2.0
*/
final class SearchBoxList extends JPanel {
/**
* @since 2019-04-13
+ * @since v0.2.0
*/
private static final long serialVersionUID = 6226930279415983433L;
/**
* The text to place in an empty search box.
+ *
+ * @since 2019-04-13
+ * @since v0.2.0
*/
private static final String EMPTY_TEXT = "Search...";
+
/**
* The color to use for an empty foreground.
+ *
+ * @since 2019-04-13
+ * @since v0.2.0
*/
private static final Color EMPTY_FOREGROUND = new Color(192, 192, 192);
@@ -66,6 +75,13 @@ final class SearchBoxList extends JPanel {
private final Comparator defaultOrdering;
private final boolean caseSensitive;
+ /**
+ * Creates the {@code SearchBoxList}.
+ *
+ * @param itemsToFilter
+ * items to put in the list
+ * @since 2019-04-14
+ */
public SearchBoxList(final Collection itemsToFilter) {
this(itemsToFilter, null, false);
}
@@ -73,7 +89,15 @@ final class SearchBoxList extends JPanel {
/**
* Creates the {@code SearchBoxList}.
*
+ * @param itemsToFilter
+ * items to put in the list
+ * @param defaultOrdering
+ * default ordering of items after filtration (null=Comparable)
+ * @param caseSensitive
+ * whether or not the filtration is case-sensitive
+ *
* @since 2019-04-13
+ * @since v0.2.0
*/
public SearchBoxList(final Collection itemsToFilter, final Comparator defaultOrdering,
final boolean caseSensitive) {
@@ -116,6 +140,7 @@ final class SearchBoxList extends JPanel {
* @param filter
* filter to add.
* @since 2019-04-13
+ * @since v0.2.0
*/
public void addSearchFilter(final Predicate filter) {
this.customSearchFilter = this.customSearchFilter.and(filter);
@@ -125,6 +150,7 @@ final class SearchBoxList extends JPanel {
* Resets the search filter.
*
* @since 2019-04-13
+ * @since v0.2.0
*/
public void clearSearchFilters() {
this.customSearchFilter = o -> true;
@@ -133,6 +159,7 @@ final class SearchBoxList extends JPanel {
/**
* @return this component's search box component
* @since 2019-04-14
+ * @since v0.2.0
*/
public final JTextField getSearchBox() {
return this.searchBox;
@@ -143,6 +170,7 @@ final class SearchBoxList extends JPanel {
* text to search for
* @return a filter that filters out that text, based on this list's case sensitive setting
* @since 2019-04-14
+ * @since v0.2.0
*/
private Predicate getSearchFilter(final String searchText) {
if (this.caseSensitive)
@@ -154,6 +182,7 @@ final class SearchBoxList extends JPanel {
/**
* @return this component's list component
* @since 2019-04-14
+ * @since v0.2.0
*/
public final JList getSearchList() {
return this.searchItems;
@@ -162,6 +191,7 @@ final class SearchBoxList extends JPanel {
/**
* @return index selected in item list
* @since 2019-04-14
+ * @since v0.2.0
*/
public int getSelectedIndex() {
return this.searchItems.getSelectedIndex();
@@ -170,6 +200,7 @@ final class SearchBoxList extends JPanel {
/**
* @return value selected in item list
* @since 2019-04-13
+ * @since v0.2.0
*/
public String getSelectedValue() {
return this.searchItems.getSelectedValue();
@@ -179,6 +210,7 @@ final class SearchBoxList extends JPanel {
* Re-applies the filters.
*
* @since 2019-04-13
+ * @since v0.2.0
*/
public void reapplyFilter() {
final String searchText = this.searchBoxEmpty ? "" : this.searchBox.getText();
@@ -205,6 +237,7 @@ final class SearchBoxList extends JPanel {
* @param e
* focus event
* @since 2019-04-13
+ * @since v0.2.0
*/
private void searchBoxFocusGained(final FocusEvent e) {
this.searchBoxFocused = true;
@@ -220,6 +253,7 @@ final class SearchBoxList extends JPanel {
* @param e
* focus event
* @since 2019-04-13
+ * @since v0.2.0
*/
private void searchBoxFocusLost(final FocusEvent e) {
this.searchBoxFocused = false;
@@ -236,6 +270,7 @@ final class SearchBoxList extends JPanel {
*
*
* @since 2019-04-14
+ * @since v0.2.0
*/
private void searchBoxTextChanged() {
if (this.searchBoxFocused) {
diff --git a/src/org/unitConverter/converterGUI/UnitConverterGUI.java b/src/org/unitConverter/converterGUI/UnitConverterGUI.java
index 1f59e3a..e258c6f 100755
--- a/src/org/unitConverter/converterGUI/UnitConverterGUI.java
+++ b/src/org/unitConverter/converterGUI/UnitConverterGUI.java
@@ -64,6 +64,7 @@ final class UnitConverterGUI {
* @param database
* database to add to
* @since 2019-04-14
+ * @since v0.2.0
*/
private static void addDefaults(final UnitsDatabase database) {
database.addUnit("metre", SI.METRE);
@@ -167,6 +168,7 @@ final class UnitConverterGUI {
* Converts in the dimension-based converter
*
* @since 2019-04-13
+ * @since v0.2.0
*/
public final void convertDimensionBased() {
final String fromSelection = this.view.getFromSelection();
@@ -264,6 +266,7 @@ final class UnitConverterGUI {
/**
* @return a list of all of the unit dimensions
* @since 2019-04-13
+ * @since v0.2.0
*/
public final List dimensionNameList() {
return this.dimensionNames;
@@ -272,6 +275,7 @@ final class UnitConverterGUI {
/**
* @return a comparator to compare prefix names
* @since 2019-04-14
+ * @since v0.2.0
*/
public final Comparator getPrefixNameComparator() {
return this.prefixNameComparator;
@@ -282,6 +286,7 @@ final class UnitConverterGUI {
* value to round
* @return string of that value rounded to {@code significantDigits} significant digits.
* @since 2019-04-14
+ * @since v0.2.0
*/
private final String getRoundedString(final double value) {
// round value
@@ -304,6 +309,7 @@ final class UnitConverterGUI {
/**
* @return a set of all prefix names in the database
* @since 2019-04-14
+ * @since v0.2.0
*/
public final Set prefixNameSet() {
return this.database.prefixMap().keySet();
@@ -333,6 +339,7 @@ final class UnitConverterGUI {
* @param significantFigures
* new value of significantFigures
* @since 2019-01-15
+ * @since v0.1.0
*/
public final void setSignificantFigures(final int significantFigures) {
this.significantFigures = significantFigures;
@@ -348,6 +355,7 @@ final class UnitConverterGUI {
* name of dimension to test
* @return whether unit has dimenision
* @since 2019-04-13
+ * @since v0.2.0
*/
public final boolean unitMatchesDimension(final String unitName, final String dimensionName) {
final Unit unit = this.database.getUnit(unitName);
@@ -378,6 +386,7 @@ final class UnitConverterGUI {
/**
* @return a set of all of the unit names
* @since 2019-04-14
+ * @since v0.2.0
*/
public final Set unitNameSet() {
return this.database.unitMapPrefixless().keySet();
@@ -452,6 +461,7 @@ final class UnitConverterGUI {
/**
* @return value in dimension-based converter
* @since 2019-04-13
+ * @since v0.2.0
*/
public String getDimensionConverterInput() {
return this.valueInput.getText();
@@ -460,6 +470,7 @@ final class UnitConverterGUI {
/**
* @return selection in "From" selector in dimension-based converter
* @since 2019-04-13
+ * @since v0.2.0
*/
public String getFromSelection() {
return this.fromSearch.getSelectedValue();
@@ -486,6 +497,7 @@ final class UnitConverterGUI {
/**
* @return selection in "To" selector in dimension-based converter
* @since 2019-04-13
+ * @since v0.2.0
*/
public String getToSelection() {
return this.toSearch.getSelectedValue();
@@ -752,6 +764,7 @@ final class UnitConverterGUI {
* @param text
* text to set
* @since 2019-04-13
+ * @since v0.2.0
*/
public void setDimensionConverterOutputText(final String text) {
this.dimensionBasedOutput.setText(text);
diff --git a/src/org/unitConverter/converterGUI/package-info.java b/src/org/unitConverter/converterGUI/package-info.java
index d899f97..1555291 100644
--- a/src/org/unitConverter/converterGUI/package-info.java
+++ b/src/org/unitConverter/converterGUI/package-info.java
@@ -19,5 +19,6 @@
*
* @author Adrien Hopkins
* @since 2019-01-25
+ * @since v0.2.0
*/
package org.unitConverter.converterGUI;
\ No newline at end of file
diff --git a/src/org/unitConverter/dimension/package-info.java b/src/org/unitConverter/dimension/package-info.java
index db363df..8cb26b1 100755
--- a/src/org/unitConverter/dimension/package-info.java
+++ b/src/org/unitConverter/dimension/package-info.java
@@ -19,5 +19,6 @@
*
* @author Adrien Hopkins
* @since 2018-12-22
+ * @since v0.1.0
*/
package org.unitConverter.dimension;
\ No newline at end of file
diff --git a/src/org/unitConverter/math/DecimalComparison.java b/src/org/unitConverter/math/DecimalComparison.java
index e6fb733..7cdbe5b 100644
--- a/src/org/unitConverter/math/DecimalComparison.java
+++ b/src/org/unitConverter/math/DecimalComparison.java
@@ -21,6 +21,7 @@ package org.unitConverter.math;
*
* @author Adrien Hopkins
* @since 2019-03-18
+ * @since v0.2.0
*/
public final class DecimalComparison {
/**
@@ -28,6 +29,7 @@ public final class DecimalComparison {
* they are considered equal.
*
* @since 2019-03-18
+ * @since v0.2.0
*/
public static final double DOUBLE_EPSILON = 1.0e-15;
@@ -36,6 +38,7 @@ public final class DecimalComparison {
* they are considered equal.
*
* @since 2019-03-18
+ * @since v0.2.0
*/
public static final float FLOAT_EPSILON = 1.0e-6f;
@@ -48,6 +51,7 @@ public final class DecimalComparison {
* second value to test
* @return whether they are equal
* @since 2019-03-18
+ * @since v0.2.0
*/
public static final boolean equals(final double a, final double b) {
return DecimalComparison.equals(a, b, DOUBLE_EPSILON);
@@ -64,6 +68,7 @@ public final class DecimalComparison {
* allowed difference
* @return whether they are equal
* @since 2019-03-18
+ * @since v0.2.0
*/
public static final boolean equals(final double a, final double b, final double epsilon) {
return Math.abs(a - b) <= epsilon * Math.max(Math.abs(a), Math.abs(b));
@@ -78,6 +83,7 @@ public final class DecimalComparison {
* second value to test
* @return whether they are equal
* @since 2019-03-18
+ * @since v0.2.0
*/
public static final boolean equals(final float a, final float b) {
return DecimalComparison.equals(a, b, FLOAT_EPSILON);
@@ -94,6 +100,7 @@ public final class DecimalComparison {
* allowed difference
* @return whether they are equal
* @since 2019-03-18
+ * @since v0.2.0
*/
public static final boolean equals(final float a, final float b, final float epsilon) {
return Math.abs(a - b) <= epsilon * Math.max(Math.abs(a), Math.abs(b));
diff --git a/src/org/unitConverter/math/ExpressionParser.java b/src/org/unitConverter/math/ExpressionParser.java
index d01afaa..b2261ed 100644
--- a/src/org/unitConverter/math/ExpressionParser.java
+++ b/src/org/unitConverter/math/ExpressionParser.java
@@ -35,8 +35,8 @@ import java.util.function.UnaryOperator;
* @param
* type of object that exists in parsed expressions
* @since 2019-03-14
+ * @since v0.2.0
*/
-// TODO: possibly make this class non-final?
public final class ExpressionParser {
/**
* A builder that can create {@code ExpressionParser} instances.
@@ -45,6 +45,7 @@ public final class ExpressionParser {
* @param
* type of object that exists in parsed expressions
* @since 2019-03-17
+ * @since v0.2.0
*/
public static final class Builder {
/**
@@ -52,6 +53,7 @@ public final class ExpressionParser {
* would use {@code Integer::parseInt}.
*
* @since 2019-03-14
+ * @since v0.2.0
*/
private final Function objectObtainer;
@@ -59,6 +61,7 @@ public final class ExpressionParser {
* The function of the space as an operator (like 3 x y)
*
* @since 2019-03-22
+ * @since v0.2.0
*/
private String spaceFunction = null;
@@ -66,6 +69,7 @@ public final class ExpressionParser {
* A map mapping operator strings to operator functions, for unary operators.
*
* @since 2019-03-14
+ * @since v0.2.0
*/
private final Map> unaryOperators;
@@ -73,6 +77,7 @@ public final class ExpressionParser {
* A map mapping operator strings to operator functions, for binary operators.
*
* @since 2019-03-14
+ * @since v0.2.0
*/
private final Map> binaryOperators;
@@ -84,6 +89,7 @@ public final class ExpressionParser {
* @throws NullPointerException
* if {@code objectObtainer} is null
* @since 2019-03-17
+ * @since v0.2.0
*/
public Builder(final Function objectObtainer) {
this.objectObtainer = Objects.requireNonNull(objectObtainer, "objectObtainer must not be null.");
@@ -104,6 +110,7 @@ public final class ExpressionParser {
* @throws NullPointerException
* if {@code text} or {@code operator} is null
* @since 2019-03-17
+ * @since v0.2.0
*/
public Builder addBinaryOperator(final String text, final BinaryOperator operator, final int priority) {
Objects.requireNonNull(text, "text must not be null.");
@@ -128,6 +135,7 @@ public final class ExpressionParser {
* text of operator to use
* @return this builder
* @since 2019-03-22
+ * @since v0.2.0
*/
public Builder addSpaceFunction(final String operator) {
Objects.requireNonNull(operator, "operator must not be null.");
@@ -152,6 +160,7 @@ public final class ExpressionParser {
* @throws NullPointerException
* if {@code text} or {@code operator} is null
* @since 2019-03-17
+ * @since v0.2.0
*/
public Builder addUnaryOperator(final String text, final UnaryOperator operator, final int priority) {
Objects.requireNonNull(text, "text must not be null.");
@@ -171,6 +180,7 @@ public final class ExpressionParser {
/**
* @return an {@code ExpressionParser} instance with the properties given to this builder
* @since 2019-03-17
+ * @since v0.2.0
*/
public ExpressionParser build() {
return new ExpressionParser<>(this.objectObtainer, this.unaryOperators, this.binaryOperators,
@@ -185,11 +195,15 @@ public final class ExpressionParser {
* @param
* type of operand and result
* @since 2019-03-17
+ * @since v0.2.0
*/
private static abstract class PriorityBinaryOperator
implements BinaryOperator, Comparable> {
/**
* The operator's priority. Higher-priority operators are applied before lower-priority operators
+ *
+ * @since 2019-03-17
+ * @since v0.2.0
*/
private final int priority;
@@ -199,6 +213,7 @@ public final class ExpressionParser {
* @param priority
* operator's priority
* @since 2019-03-17
+ * @since v0.2.0
*/
public PriorityBinaryOperator(final int priority) {
this.priority = priority;
@@ -209,6 +224,10 @@ public final class ExpressionParser {
*
*
* {@inheritDoc}
+ *
+ *
+ * @since 2019-03-17
+ * @since v0.2.0
*/
@Override
public int compareTo(final PriorityBinaryOperator o) {
@@ -223,6 +242,7 @@ public final class ExpressionParser {
/**
* @return priority
* @since 2019-03-22
+ * @since v0.2.0
*/
public final int getPriority() {
return this.priority;
@@ -236,11 +256,15 @@ public final class ExpressionParser {
* @param
* type of operand and result
* @since 2019-03-17
+ * @since v0.2.0
*/
private static abstract class PriorityUnaryOperator
implements UnaryOperator, Comparable> {
/**
* The operator's priority. Higher-priority operators are applied before lower-priority operators
+ *
+ * @since 2019-03-17
+ * @since v0.2.0
*/
private final int priority;
@@ -250,6 +274,7 @@ public final class ExpressionParser {
* @param priority
* operator's priority
* @since 2019-03-17
+ * @since v0.2.0
*/
public PriorityUnaryOperator(final int priority) {
this.priority = priority;
@@ -260,6 +285,10 @@ public final class ExpressionParser {
*
*
* {@inheritDoc}
+ *
+ *
+ * @since 2019-03-17
+ * @since v0.2.0
*/
@Override
public int compareTo(final PriorityUnaryOperator o) {
@@ -274,6 +303,7 @@ public final class ExpressionParser {
/**
* @return priority
* @since 2019-03-22
+ * @since v0.2.0
*/
public final int getPriority() {
return this.priority;
@@ -285,6 +315,7 @@ public final class ExpressionParser {
*
* @author Adrien Hopkins
* @since 2019-03-14
+ * @since v0.2.0
*/
private static enum TokenType {
OBJECT, UNARY_OPERATOR, BINARY_OPERATOR;
@@ -294,6 +325,7 @@ public final class ExpressionParser {
* The opening bracket.
*
* @since 2019-03-22
+ * @since v0.2.0
*/
public static final char OPENING_BRACKET = '(';
@@ -301,6 +333,7 @@ public final class ExpressionParser {
* The closing bracket.
*
* @since 2019-03-22
+ * @since v0.2.0
*/
public static final char CLOSING_BRACKET = ')';
@@ -315,6 +348,7 @@ public final class ExpressionParser {
* @throws NullPointerException
* if string is null
* @since 2019-03-22
+ * @since v0.2.0
*/
private static int findBracketPair(final String string, final int bracketPosition) {
Objects.requireNonNull(string, "string must not be null.");
@@ -361,6 +395,7 @@ public final class ExpressionParser {
* use {@code Integer::parseInt}.
*
* @since 2019-03-14
+ * @since v0.2.0
*/
private final Function objectObtainer;
@@ -368,6 +403,7 @@ public final class ExpressionParser {
* A map mapping operator strings to operator functions, for unary operators.
*
* @since 2019-03-14
+ * @since v0.2.0
*/
private final Map> unaryOperators;
@@ -375,6 +411,7 @@ public final class ExpressionParser {
* A map mapping operator strings to operator functions, for binary operators.
*
* @since 2019-03-14
+ * @since v0.2.0
*/
private final Map> binaryOperators;
@@ -382,6 +419,7 @@ public final class ExpressionParser {
* The operator for space, or null if spaces have no function.
*
* @since 2019-03-22
+ * @since v0.2.0
*/
private final String spaceOperator;
@@ -397,6 +435,7 @@ public final class ExpressionParser {
* @param spaceOperator
* operator used by spaces
* @since 2019-03-14
+ * @since v0.2.0
*/
private ExpressionParser(final Function objectObtainer,
final Map> unaryOperators,
@@ -419,6 +458,7 @@ public final class ExpressionParser {
* expression
* @return expression in RPN
* @since 2019-03-17
+ * @since v0.2.0
*/
private String convertExpressionToReversePolish(final String expression) {
Objects.requireNonNull(expression, "expression must not be null.");
@@ -523,6 +563,7 @@ public final class ExpressionParser {
* @throws NullPointerException
* if components is null
* @since 2019-03-22
+ * @since v0.2.0
*/
private int findHighestPriorityOperatorPosition(final List components) {
Objects.requireNonNull(components, "components must not be null.");
@@ -572,6 +613,7 @@ public final class ExpressionParser {
* @throws NullPointerException
* if {@code expression} is null
* @since 2019-03-14
+ * @since v0.2.0
*/
private TokenType getTokenType(final String token) {
Objects.requireNonNull(token, "token must not be null.");
@@ -593,6 +635,7 @@ public final class ExpressionParser {
* @throws NullPointerException
* if {@code expression} is null
* @since 2019-03-14
+ * @since v0.2.0
*/
public T parseExpression(final String expression) {
return this.parseReversePolishExpression(this.convertExpressionToReversePolish(expression));
@@ -607,6 +650,7 @@ public final class ExpressionParser {
* @throws NullPointerException
* if {@code expression} is null
* @since 2019-03-14
+ * @since v0.2.0
*/
private T parseReversePolishExpression(final String expression) {
Objects.requireNonNull(expression, "expression must not be null.");
diff --git a/src/org/unitConverter/unit/AbstractUnit.java b/src/org/unitConverter/unit/AbstractUnit.java
index a0d6f7e..05a6c17 100644
--- a/src/org/unitConverter/unit/AbstractUnit.java
+++ b/src/org/unitConverter/unit/AbstractUnit.java
@@ -110,7 +110,6 @@ public abstract class AbstractUnit implements Unit {
return this.system;
}
- // TODO document and revise units' toString methods
@Override
public String toString() {
return String.format("%s-derived unit of dimension %s", this.getSystem(), this.getDimension());
diff --git a/src/org/unitConverter/unit/BaseUnit.java b/src/org/unitConverter/unit/BaseUnit.java
index 8bac866..67309cf 100755
--- a/src/org/unitConverter/unit/BaseUnit.java
+++ b/src/org/unitConverter/unit/BaseUnit.java
@@ -111,6 +111,7 @@ public final class BaseUnit extends LinearUnit {
/**
* @return true if the unit is a "full base" unit like the metre or second.
* @since 2019-04-10
+ * @since v0.2.0
*/
public final boolean isFullBase() {
return this.isFullBase;
diff --git a/src/org/unitConverter/unit/DefaultUnitPrefix.java b/src/org/unitConverter/unit/DefaultUnitPrefix.java
index c0e8dcc..4a9e487 100755
--- a/src/org/unitConverter/unit/DefaultUnitPrefix.java
+++ b/src/org/unitConverter/unit/DefaultUnitPrefix.java
@@ -33,6 +33,7 @@ public final class DefaultUnitPrefix implements UnitPrefix {
*
* @param multiplier
* @since 2019-01-14
+ * @since v0.2.0
*/
public DefaultUnitPrefix(final double multiplier) {
this.multiplier = multiplier;
diff --git a/src/org/unitConverter/unit/LinearUnit.java b/src/org/unitConverter/unit/LinearUnit.java
index 5b2680b..1b1ac97 100644
--- a/src/org/unitConverter/unit/LinearUnit.java
+++ b/src/org/unitConverter/unit/LinearUnit.java
@@ -175,6 +175,7 @@ public class LinearUnit extends AbstractUnit {
* @throws NullPointerException
* if {@code subtrahend} is null
* @since 2019-03-17
+ * @since v0.2.0
*/
public LinearUnit minus(final LinearUnit subtrahendend) {
Objects.requireNonNull(subtrahendend, "addend must not be null.");
@@ -203,6 +204,7 @@ public class LinearUnit extends AbstractUnit {
* @throws NullPointerException
* if {@code addend} is null
* @since 2019-03-17
+ * @since v0.2.0
*/
public LinearUnit plus(final LinearUnit addend) {
Objects.requireNonNull(addend, "addend must not be null.");
@@ -284,6 +286,7 @@ public class LinearUnit extends AbstractUnit {
* prefix to apply
* @return unit with prefix
* @since 2019-03-18
+ * @since v0.2.0
*/
public LinearUnit withPrefix(final UnitPrefix prefix) {
return this.times(prefix.getMultiplier());
diff --git a/src/org/unitConverter/unit/UnitPrefix.java b/src/org/unitConverter/unit/UnitPrefix.java
index a1609c6..9f9645d 100755
--- a/src/org/unitConverter/unit/UnitPrefix.java
+++ b/src/org/unitConverter/unit/UnitPrefix.java
@@ -31,6 +31,7 @@ public interface UnitPrefix {
* prefix to divide by
* @return quotient of prefixes
* @since 2019-04-13
+ * @since v0.2.0
*/
default UnitPrefix dividedBy(final UnitPrefix other) {
return new DefaultUnitPrefix(this.getMultiplier() / other.getMultiplier());
@@ -50,6 +51,7 @@ public interface UnitPrefix {
* prefix to multiply by
* @return product of prefixes
* @since 2019-04-13
+ * @since v0.2.0
*/
default UnitPrefix times(final UnitPrefix other) {
return new DefaultUnitPrefix(this.getMultiplier() * other.getMultiplier());
@@ -62,6 +64,7 @@ public interface UnitPrefix {
* exponent to raise to
* @return result of exponentiation.
* @since 2019-04-13
+ * @since v0.2.0
*/
default UnitPrefix toExponent(final double exponent) {
return new DefaultUnitPrefix(Math.pow(getMultiplier(), exponent));
diff --git a/src/org/unitConverter/unit/package-info.java b/src/org/unitConverter/unit/package-info.java
index c4493ae..dd5a939 100644
--- a/src/org/unitConverter/unit/package-info.java
+++ b/src/org/unitConverter/unit/package-info.java
@@ -19,5 +19,6 @@
*
* @author Adrien Hopkins
* @since 2019-01-25
+ * @since v0.1.0
*/
package org.unitConverter.unit;
\ No newline at end of file
diff --git a/src/test/java/ExpressionParserTest.java b/src/test/java/ExpressionParserTest.java
index 62fa964..40c91ac 100644
--- a/src/test/java/ExpressionParserTest.java
+++ b/src/test/java/ExpressionParserTest.java
@@ -26,6 +26,7 @@ import org.unitConverter.math.ExpressionParser;
*
* @author Adrien Hopkins
* @since 2019-03-22
+ * @since v0.2.0
*/
public class ExpressionParserTest {
private static final ExpressionParser numberParser = new ExpressionParser.Builder<>(Integer::parseInt)
diff --git a/src/test/java/UnitTest.java b/src/test/java/UnitTest.java
index 952b6f2..00fcf3c 100755
--- a/src/test/java/UnitTest.java
+++ b/src/test/java/UnitTest.java
@@ -35,6 +35,7 @@ import org.unitConverter.unit.Unit;
*
* @author Adrien Hopkins
* @since 2018-12-22
+ * @since v0.1.0
*/
public class UnitTest {
/** A random number generator */
diff --git a/src/test/java/UnitsDatabaseTest.java b/src/test/java/UnitsDatabaseTest.java
index 8429561..9222740 100644
--- a/src/test/java/UnitsDatabaseTest.java
+++ b/src/test/java/UnitsDatabaseTest.java
@@ -38,6 +38,7 @@ import org.unitConverter.unit.UnitPrefix;
*
* @author Adrien Hopkins
* @since 2019-04-14
+ * @since v0.2.0
*/
public class UnitsDatabaseTest {
// some linear units and one nonlinear
@@ -72,6 +73,7 @@ public class UnitsDatabaseTest {
* Test that prefixes correctly apply to units.
*
* @since 2019-04-14
+ * @since v0.2.0
*/
@Test
public void testPrefixes() {
@@ -101,6 +103,7 @@ public class UnitsDatabaseTest {
*
*
* @since 2019-04-14
+ * @since v0.2.0
*/
@Test
public void testPrefixlessUnitMap() {
@@ -123,6 +126,7 @@ public class UnitsDatabaseTest {
* Tests that the database correctly stores and retrieves units, ignoring prefixes.
*
* @since 2019-04-14
+ * @since v0.2.0
*/
@Test
public void testPrefixlessUnits() {
@@ -143,6 +147,7 @@ public class UnitsDatabaseTest {
* Test that unit expressions return the correct value.
*
* @since 2019-04-14
+ * @since v0.2.0
*/
@Test
public void testUnitExpressions() {
@@ -176,6 +181,7 @@ public class UnitsDatabaseTest {
* Tests both the unit name iterator and the name-unit entry iterator
*
* @since 2019-04-14
+ * @since v0.2.0
*/
@Test
public void testUnitIterator() {
@@ -221,6 +227,7 @@ public class UnitsDatabaseTest {
*
*
* @since 2019-04-14
+ * @since v0.2.0
*/
@Test
public void testUnitPrefixCombinations() {
diff --git a/src/test/java/package-info.java b/src/test/java/package-info.java
index 87b4a06..3da7fcb 100644
--- a/src/test/java/package-info.java
+++ b/src/test/java/package-info.java
@@ -19,5 +19,6 @@
*
* @author Adrien Hopkins
* @since 2019-03-16
+ * @since v0.2.0
*/
package test.java;
\ No newline at end of file
--
cgit v1.2.3