From 5c4cd6d206e195d0c5efce747e8670f8e77cb59c Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Thu, 14 Mar 2019 18:07:12 -0400 Subject: Added unit dimensions to the unit database. --- CHANGELOG.org | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'CHANGELOG.org') diff --git a/CHANGELOG.org b/CHANGELOG.org index 1dbe268..280ceb3 100644 --- a/CHANGELOG.org +++ b/CHANGELOG.org @@ -1,6 +1,10 @@ * Changelog All notable changes in this project will be shown in this file. +** Unreleased +*** Added + - GUI for a selection-based unit converter + - The UnitDatabase now stores dimensions. ** v0.1.0 NOTE: At this stage, the API is subject to significant change. *** Added -- cgit v1.2.3 From 5f06f688ee0de31125682a9a0b1d05b4b5edf66c Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Sat, 16 Mar 2019 15:09:07 -0400 Subject: Updated changelog and fixed tests. --- CHANGELOG.org | 3 +++ pom.xml | 7 +++++++ 2 files changed, 10 insertions(+) (limited to 'CHANGELOG.org') diff --git a/CHANGELOG.org b/CHANGELOG.org index 280ceb3..b9c87c9 100644 --- a/CHANGELOG.org +++ b/CHANGELOG.org @@ -2,6 +2,9 @@ All notable changes in this project will be shown in this file. ** Unreleased +*** Changed + - Moved project to Maven + - Downgraded JUnit to 4.11 *** Added - GUI for a selection-based unit converter - The UnitDatabase now stores dimensions. diff --git a/pom.xml b/pom.xml index 3aede3b..8b3bc0d 100644 --- a/pom.xml +++ b/pom.xml @@ -18,6 +18,13 @@ 1.8 + + org.apache.maven.plugins + maven-surefire-plugin + + false + + org.codehaus.mojo exec-maven-plugin -- cgit v1.2.3 From 6dbd32cd208c164e9c818b48b0b9bf823a152d71 Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Sun, 17 Mar 2019 20:04:56 -0400 Subject: Added an expression parser that can parse RPN expressions. --- .gitignore | 1 + CHANGELOG.org | 1 + .../expressionParser/ExpressionParser.java | 398 +++++++++++++++++++++ .../expressionParser/package-info.java | 23 ++ 4 files changed, 423 insertions(+) create mode 100644 src/org/unitConverter/expressionParser/ExpressionParser.java create mode 100644 src/org/unitConverter/expressionParser/package-info.java (limited to 'CHANGELOG.org') diff --git a/.gitignore b/.gitignore index 866d01d..52a523a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ bin/ +target/ *.class *~ \ No newline at end of file diff --git a/CHANGELOG.org b/CHANGELOG.org index b9c87c9..87e26e0 100644 --- a/CHANGELOG.org +++ b/CHANGELOG.org @@ -8,6 +8,7 @@ All notable changes in this project will be shown in this file. *** Added - GUI for a selection-based unit converter - The UnitDatabase now stores dimensions. + - A system to parse mathematical expressions, used to parse unit expressions. ** v0.1.0 NOTE: At this stage, the API is subject to significant change. *** Added diff --git a/src/org/unitConverter/expressionParser/ExpressionParser.java b/src/org/unitConverter/expressionParser/ExpressionParser.java new file mode 100644 index 0000000..804ea87 --- /dev/null +++ b/src/org/unitConverter/expressionParser/ExpressionParser.java @@ -0,0 +1,398 @@ +/** + * 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 new file mode 100644 index 0000000..28f0cae --- /dev/null +++ b/src/org/unitConverter/expressionParser/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.expressionParser; \ No newline at end of file -- 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 'CHANGELOG.org') 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 'CHANGELOG.org') 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 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 'CHANGELOG.org') 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 'CHANGELOG.org') 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 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 'CHANGELOG.org') 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 'CHANGELOG.org') 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 b43c399c21bddd3cb8af42c109940564c3890cf7 Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Sun, 14 Apr 2019 17:41:10 -0400 Subject: Bumped the version number to v0.2.0 --- CHANGELOG.org | 4 ++-- README.org | 23 +++++++++++++---------- src/org/unitConverter/package-info.java | 1 + 3 files changed, 16 insertions(+), 12 deletions(-) (limited to 'CHANGELOG.org') diff --git a/CHANGELOG.org b/CHANGELOG.org index db9766b..77e7593 100644 --- a/CHANGELOG.org +++ b/CHANGELOG.org @@ -1,7 +1,7 @@ * Changelog All notable changes in this project will be shown in this file. -** Unreleased +** v0.2.0 - [2019-04-14] *** Changed - When searching for units, units with no prefixes are searched for before prefixed units - Smaller prefixes are searched for before larger prefixes @@ -17,7 +17,7 @@ All notable changes in this project will be shown in this file. - 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 +** v0.1.0 - [2019-02-01] NOTE: At this stage, the API is subject to significant change. *** Added - Unit interface, implemented and supporting classes diff --git a/README.org b/README.org index 5500d68..2a6fa95 100644 --- a/README.org +++ b/README.org @@ -1,25 +1,27 @@ -* What is it? +* Unit Converter v0.2.0 +(this project uses Semantic Versioning) +** What is it? This is a unit converter, which allows you to convert between different units, and includes a GUI which can read unit data from a file (using some unit math) and convert between units that you type in, and has a unit and prefix viewer to check the units that have been loaded in. -* Features +** Features - Convert between units and expressions of units - linear or base unit can use unit prefixes (including non-metric units!) - and prefixes are defined in an editable data file, in a simple and intuitive format. - Viewer and Prefix Viewer which allow you to search through all of the available units and prefixes and learn details about them. - All of SI included in default text file - Choose your precision -* How to Convert (in "Convert Units") +** How to Convert (in "Convert Units") To convert units, simply: 1. Select the kind of units to convert (length/mass/time, etc.). 2. Select the units to convert from and to (you can use the text boxes above to search) 3. Enter a value to convert 4. Press "Convert" -* How to Convert (in "Convert Expressions") +** How to Convert (in "Convert Expressions") To convert units, simply enter the first unit in the From box and the second unit in the To box. Press Convert and a result will appear in the output box. Both boxes accept unit expressions, so you can input things like ‘1.7 m’, ‘85 km/h’ and ‘35 A * 14 s’ into either box. *Warning*: The space between the number and the unit name is required, or else the whole expression will be interpreted as a unit name. Spaces are not required for operators. Use the slider at the bottom to choose the maximum precision of the result, in significant digits, not decimal places. -* Units Files and Expressions +** Units Files and Expressions As mentioned previously, all units are loaded from a units file. The format for lines consists of a name for the unit, some tabs (not spaces), and an expression. The following operations are supported in expressions: - Addition using the plus sign (+), which only works on compatible units - Subtraction using the minus sign (-), which only works on compatible units @@ -44,14 +46,14 @@ Unit prefixes are defined differently. When a unit name ends with the dash (-) - Division using the forward slash (/) - Exponentiation using the caret (^) Every argument can be a number or a prefix name, except exponents which much be integers -* Unit Prefixes +** Unit Prefixes In SI, you can have a unit prefix, which you attach to the start of a unit and it multiplies the unit by a certain value. For example, milli-metre = 0.001 * metre. This unit converter supports unit prefixes, and you can put any number of prefixes before a linear or base unit to transform it (this includes non-SI units). You can use more than one prefix at once (yottayottametre = really really long distance), but you may not convert prefixes alone. If you want to do that, use ‘unit’ or ‘u’ (ku = 1000000 mu). You can also use u for converting from numbers (pi = 3.141593 u). The default unit file allows you to use D-, H- and K- in place of da-, h- and k- if you want. *Warning*: The standard prefixes will never use 1024 instead of 1000, even when operating on bits and bytes. Use ‘Ki’, ‘Mi’, ‘Gi’, ‘Ti’ and so on instead. -* Nonlinear Units +** Nonlinear Units Sometimes, units cannot be converted from and to by simply multiplying and dividing. A common example of this is the Celsius and Fahrenheit temperature scales, which require multiplication and addition to convert to each other (and to their base, Kelvin). To use nonlinear units, use the following syntax: @@ -64,9 +66,9 @@ Nonlinear units cannot: - be defined by unit files To define a nonlinear unit, make an anonymous inner type (or any other subclass) of AbstractUnit, and define the conversion methods. -* Unit and Prefix Viewers +** Unit and Prefix Viewers The unit and prefix viewers can be used to see the available units (without prefixes) and prefixes. Upon opening them, you will see a list of units or prefixes on your left. Using the text box above, the list can be filtered. When a unit is clicked on, details about will be displayed on the right. -* Copyright and Licences +** Copyright and Licences The Unit Converter program is Copyright (C) 2018, 2019 Adrien Hopkins. It is released under the terms of the Aferro GNU General Public License, version 3.0 or any later version published by the Free Software Foundation. A copy of this license should be provided with this program, and a human-readable summary of the very similar GNU General Public License can be found at the following link: https://www.gnu.org/licenses/quick-guide-gplv3.html, although this summary is NOT a replacement for the actual license. This document is Copyright (C) 2019 Adrien Hopkins. This document is dual-licensed under the terms of the GNU Free Documentation License and the Creative Commons Attribution-ShareAlike License. More details are in the next paragraphs: @@ -74,7 +76,7 @@ This document is Copyright (C) 2019 Adrien Hopkins. This document is dual-licen Permission is granted to copy, distribute and/or modify this document under the terms of the GNU Free Documentation License, Version 1.3 or any later version published by the Free Software Foundation; with no Invariant Sections, no Front-Cover Texts, and no Back-Cover Texts. A copy of the license is included in the section entitled "GNU Free Documentation License". This work is licensed under the Creative Commons Attribution-ShareAlike 4.0 International License. To view a copy of this license, visit https://creativecommons.org/licenses/by-sa/4.0/ or send a letter to Creative Commons, PO Box 1866, Mountain View, CA 94042, USA. -** GNU Free Documentation License +*** GNU Free Documentation License GNU Free Documentation License Version 1.3, 3 November 2008 @@ -526,3 +528,4 @@ If your document contains nontrivial examples of program code, we recommend releasing these examples in parallel under your choice of free software license, such as the GNU General Public License, to permit their use in free software. + diff --git a/src/org/unitConverter/package-info.java b/src/org/unitConverter/package-info.java index 4f51ad0..23dd165 100644 --- a/src/org/unitConverter/package-info.java +++ b/src/org/unitConverter/package-info.java @@ -18,6 +18,7 @@ * A program that converts units. * * @author Adrien Hopkins + * @version v0.2.0 * @since 2019-01-25 */ package org.unitConverter; \ No newline at end of file -- cgit v1.2.3