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: *

    - *
  • Any line that begins with the '#' character is considered a comment and ignored.
  • + *
  • Anything after a '#' 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 @@ -140,56 +156,7 @@ public final class UnitsDatabase { // 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; - } - - this.addUnit(name, unit); - } - } + this.addFromLine(reader.readLine(), ++lineCounter); } } catch (final FileNotFoundException e) { throw new IllegalArgumentException("Could not find file " + file, e); @@ -214,6 +181,67 @@ public final class UnitsDatabase { Objects.requireNonNull(dimension, "dimension must not be null.")); } + /** + * Adds to the list from a line in a unit file. + * + * @param line + * line to look at + * @param lineCounter + * number of line, for error messages + * @since 2019-04-10 + */ + private void addFromLine(final String line, final long lineCounter) { + // ignore lines that start with a # sign - they're comments + if (line.isEmpty()) + return; + if (line.contains("#")) { + this.addFromLine(line.substring(0, line.indexOf("#")), lineCounter); + return; + } + + // 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; + } + + this.addUnit(name, unit); + } + } + } + /** * Adds a unit prefix to the database. * @@ -316,12 +344,31 @@ public final class UnitsDatabase { /** * Gets a unit dimension from the database using its name. * + *

    + * This method accepts exponents, like "L^3" + *

    + * * @param name * dimension's name * @return dimension * @since 2019-03-14 */ public UnitDimension getDimension(final String name) { + Objects.requireNonNull(name, "name must not be null."); + if (name.contains("^")) { + final String[] baseAndExponent = name.split("\\^"); + + final UnitDimension base = this.getDimension(baseAndExponent[0]); + + final int exponent; + try { + exponent = Integer.parseInt(baseAndExponent[baseAndExponent.length - 1]); + } catch (final NumberFormatException e2) { + throw new IllegalArgumentException("Exponent must be an integer."); + } + + return base.toExponent(exponent); + } return this.dimensions.get(name); } -- cgit v1.2.3 From 8e613844ae19a4dea2089ac34c1f0ae650eaeae7 Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Sat, 13 Apr 2019 10:05:08 -0400 Subject: The dimension selector now loads dimensions from a file. The dimension selector does nothing, as its purpose is to filter a list which does not exist yet, but it does correctly load the options. --- CHANGELOG.org | 4 +- dimensionfile.txt | 13 + src/org/unitConverter/UnitsDatabase.java | 265 +++++++++++++++------ .../converterGUI/UnitConverterGUI.java | 30 ++- src/org/unitConverter/unit/BaseUnit.java | 24 ++ src/org/unitConverter/unit/LinearUnit.java | 10 + 6 files changed, 275 insertions(+), 71 deletions(-) create mode 100644 dimensionfile.txt (limited to 'src/org/unitConverter/UnitsDatabase.java') diff --git a/CHANGELOG.org b/CHANGELOG.org index 8a79c46..46197dc 100644 --- a/CHANGELOG.org +++ b/CHANGELOG.org @@ -6,12 +6,14 @@ 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 + - 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 - 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 ** v0.1.0 NOTE: At this stage, the API is subject to significant change. *** Added diff --git a/dimensionfile.txt b/dimensionfile.txt new file mode 100644 index 0000000..d3c068c --- /dev/null +++ b/dimensionfile.txt @@ -0,0 +1,13 @@ +# A file for the unit dimensions in my unit converter program + +# SI Base Dimensions +# ! 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. + +LENGTH ! +MASS ! +TIME ! +ELECTRIC_CURRENT ! +TEMPERATURE ! +QUANTITY ! +LUMINOUS_INTENSITY ! \ No newline at end of file diff --git a/src/org/unitConverter/UnitsDatabase.java b/src/org/unitConverter/UnitsDatabase.java index 69b25d8..626f145 100755 --- a/src/org/unitConverter/UnitsDatabase.java +++ b/src/org/unitConverter/UnitsDatabase.java @@ -125,46 +125,6 @@ public final class UnitsDatabase { 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: - *

      - *
    • Anything after a '#' 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()) { - this.addFromLine(reader.readLine(), ++lineCounter); - } - } 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. * @@ -182,7 +142,7 @@ public final class UnitsDatabase { } /** - * Adds to the list from a line in a unit file. + * Adds to the list from a line in a unit dimension file. * * @param line * line to look at @@ -190,12 +150,12 @@ public final class UnitsDatabase { * number of line, for error messages * @since 2019-04-10 */ - private void addFromLine(final String line, final long lineCounter) { + private void addDimensionFromLine(final String line, final long lineCounter) { // ignore lines that start with a # sign - they're comments if (line.isEmpty()) return; if (line.contains("#")) { - this.addFromLine(line.substring(0, line.indexOf("#")), lineCounter); + this.addDimensionFromLine(line.substring(0, line.indexOf("#")), lineCounter); return; } @@ -203,42 +163,32 @@ public final class UnitsDatabase { 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).", + "Lines must consist of a dimension 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); + System.err.printf("Warning - line %d's dimension name ends in a space", lineCounter); } - // if expression is "!", search for an existing unit + // if expression is "!", search for an existing dimension // 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)); + if (!this.containsDimensionName(name)) + throw new IllegalArgumentException( + String.format("! used but no dimension 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; - } - - this.addUnit(name, unit); + // it's a unit, get the unit + final UnitDimension dimension; + try { + dimension = this.getDimensionFromExpression(expression); + } catch (final IllegalArgumentException e) { + System.err.printf("Parsing error on line %d:%n", lineCounter); + throw e; } + + this.addDimension(name, dimension); } } @@ -276,6 +226,67 @@ public final class UnitsDatabase { Objects.requireNonNull(unit, "unit must not be null.")); } + /** + * Adds to the list from a line in a unit file. + * + * @param line + * line to look at + * @param lineCounter + * number of line, for error messages + * @since 2019-04-10 + */ + private void addUnitOrPrefixFromLine(final String line, final long lineCounter) { + // ignore lines that start with a # sign - they're comments + if (line.isEmpty()) + return; + if (line.contains("#")) { + this.addUnitOrPrefixFromLine(line.substring(0, line.indexOf("#")), lineCounter); + return; + } + + // 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; + } + + this.addUnit(name, unit); + } + } + } + /** * Tests if the database has a unit dimension with this name. * @@ -372,6 +383,44 @@ public final class UnitsDatabase { return this.dimensions.get(name); } + /** + * Uses the database's data to parse an expression into a unit dimension + *

    + * The expression is a series of any of the following: + *

      + *
    • The name of a unit dimension, which multiplies or divides the result based on preceding operators
    • + *
    • The operators '*' and '/', which multiply and divide (note that just putting two unit dimensions next to each + * other is equivalent to multiplication)
    • + *
    • The operator '^' which exponentiates. Exponents must be integers.
    • + *
    + * + * @param expression + * expression to parse + * @throws IllegalArgumentException + * if the expression cannot be parsed + * @throws NullPointerException + * if expression is null + * @since 2019-04-13 + */ + public UnitDimension getDimensionFromExpression(final String expression) { + Objects.requireNonNull(expression, "expression must not be null."); + + // attempt to get a dimension as an alias first + if (this.containsDimensionName(expression)) + return this.getDimension(expression); + + // force operators to have spaces + String modifiedExpression = expression; + modifiedExpression = modifiedExpression.replaceAll("\\*", " \\* "); + modifiedExpression = modifiedExpression.replaceAll("/", " / "); + modifiedExpression = modifiedExpression.replaceAll(" *\\^ *", "\\^"); + + // fix broken spaces + modifiedExpression = modifiedExpression.replaceAll(" +", " "); + + return this.unitDimensionParser.parseExpression(modifiedExpression); + } + /** * Gets a unit. If it is linear, cast it to a LinearUnit and return it. Otherwise, throw an * {@code IllegalArgumentException}. @@ -598,6 +647,86 @@ public final class UnitsDatabase { return this.unitExpressionParser.parseExpression(modifiedExpression); } + /** + * Adds all dimensions 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 getDimensionFromExpression) separated + * by any number of tab characters. + *

    + *

    + * Allowed exceptions: + *

      + *
    • Anything after a '#' 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 loadDimensionFile(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()) { + this.addDimensionFromLine(reader.readLine(), ++lineCounter); + } + } 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 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: + *

      + *
    • Anything after a '#' 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 loadUnitsFile(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()) { + this.addUnitOrPrefixFromLine(reader.readLine(), ++lineCounter); + } + } catch (final FileNotFoundException e) { + throw new IllegalArgumentException("Could not find file " + file, e); + } catch (final IOException e) { + throw new IllegalArgumentException("Could not read file " + file, e); + } + } + /** * @return an immutable set of all of the unit names in this database, ignoring prefixes * @since 2019-01-14 diff --git a/src/org/unitConverter/converterGUI/UnitConverterGUI.java b/src/org/unitConverter/converterGUI/UnitConverterGUI.java index fd40ff4..4f5ebeb 100755 --- a/src/org/unitConverter/converterGUI/UnitConverterGUI.java +++ b/src/org/unitConverter/converterGUI/UnitConverterGUI.java @@ -78,6 +78,9 @@ final class UnitConverterGUI { /** The names of all of the prefixes */ private final DelegateListModel prefixNamesFiltered; + /** The names of all of the dimensions */ + private final List dimensionNames; + private final Comparator prefixNameComparator; private int significantFigures = 6; @@ -109,7 +112,17 @@ final class UnitConverterGUI { this.units.addUnit("tempCelsius", NonlinearUnits.CELSIUS); this.units.addUnit("tempFahrenheit", NonlinearUnits.FAHRENHEIT); - this.units.addAllFromFile(new File("unitsfile.txt")); + // load initial dimensions + 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")); + this.units.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. @@ -143,6 +156,9 @@ final class UnitConverterGUI { this.prefixNamesFiltered = new DelegateListModel<>(new ArrayList<>(this.units.prefixNameSet())); this.prefixNamesFiltered.sort(this.prefixNameComparator); // sorts it using my comparator + this.dimensionNames = new DelegateListModel<>(new ArrayList<>(this.units.dimensionNameSet())); + this.dimensionNames.sort(null); // sorts it using Comparable + // a Predicate that returns true iff the argument is a full base unit final Predicate isFullBase = unit -> unit instanceof BaseUnit && ((BaseUnit) unit).isFullBase(); @@ -222,6 +238,14 @@ final class UnitConverterGUI { this.view.setOutputText(String.format("%s = %s %s", fromUnitString, output, toUnitString)); } + /** + * @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 * @@ -531,8 +555,10 @@ 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<>( - new String[] {"Select dimension..."}); + dimensionNameList.toArray(new String[0])); inBetweenPanel.add(dimensionSelector, BorderLayout.PAGE_START); } diff --git a/src/org/unitConverter/unit/BaseUnit.java b/src/org/unitConverter/unit/BaseUnit.java index 643272f..8bac866 100755 --- a/src/org/unitConverter/unit/BaseUnit.java +++ b/src/org/unitConverter/unit/BaseUnit.java @@ -18,11 +18,35 @@ package org.unitConverter.unit; import java.util.Objects; +import org.unitConverter.dimension.StandardDimensions; import org.unitConverter.dimension.UnitDimension; /** * A unit that is the base for its dimension. It does not have to be for a base dimension, so units like the Newton and * Joule are still base units. + *

    + * {@code BaseUnit} does not have any public constructors or static factories. There are two ways to obtain + * {@code BaseUnit} instances. + *

      + *
    1. The class {@link SI} in this package has constants for all of the SI base units. You can use these constants and + * multiply or divide them to get other units. For example: + * + *
      + * BaseUnit JOULE = SI.KILOGRAM.times(SI.METRE.toExponent(2)).dividedBy(SI.SECOND.toExponent(2));
      + * 
      + * + *
    2. + *
    3. You can also query a unit system for a base unit using a unit dimension. The previously mentioned {@link SI} + * class can do this for SI and SI-derived units (including imperial and USC), but if you want to use another system, + * this is the way to do it. {@link StandardDimensions} contains common unit dimensions that you can use for this. Here + * is an example: + * + *
      + * BaseUnit JOULE = SI.SI.getBaseUnit(StandardDimensions.ENERGY);
      + * 
      + * + *
    4. + *
    * * @author Adrien Hopkins * @since 2018-12-23 diff --git a/src/org/unitConverter/unit/LinearUnit.java b/src/org/unitConverter/unit/LinearUnit.java index c755f79..5b2680b 100644 --- a/src/org/unitConverter/unit/LinearUnit.java +++ b/src/org/unitConverter/unit/LinearUnit.java @@ -23,6 +23,16 @@ import org.unitConverter.math.DecimalComparison; /** * A unit that is equal to a certain number multiplied by its base. + *

    + * {@code LinearUnit} does not have any public constructors or static factories. In order to obtain a {@code LinearUnit} + * instance, multiply its base by the conversion factor. Example: + * + *

    + * LinearUnit foot = METRE.times(0.3048);
    + * 
    + * + * (where {@code METRE} is a {@code BaseUnit} instance) + *

    * * @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> 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> 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 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 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 remappingFunction) { + throw new UnsupportedOperationException(); + } + + @Override + public Unit computeIfAbsent(final String key, final Function mappingFunction) { + throw new UnsupportedOperationException(); + } + + @Override + public Unit computeIfPresent(final String key, + final BiFunction 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 remappingFunction) { + throw new UnsupportedOperationException(); + } + + @Override + public Unit put(final String key, final Unit value) { + throw new UnsupportedOperationException(); + } + + @Override + public void putAll(final Map 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 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