diff options
author | Adrien Hopkins <adrien.p.hopkins@gmail.com> | 2024-03-23 15:25:42 -0500 |
---|---|---|
committer | Adrien Hopkins <adrien.p.hopkins@gmail.com> | 2024-03-23 15:29:12 -0500 |
commit | 4db89a7e775921f4bb29db9f9b6bd939f115b631 (patch) | |
tree | 4b2a1feee3e5d08ff9fcfd95c8d77673964c4426 | |
parent | 4a17e32274f991014edcfa22402d7207361f69f1 (diff) |
Complete exponentiation of dimensions
Previously, you could only exponentiate individual dimensions in
expressions. For example, `Length^3` was valid, but `(Length / Time)^2`
was not. This is now fixed.
-rw-r--r-- | src/main/java/sevenUnits/unit/UnitDatabase.java | 44 | ||||
-rw-r--r-- | src/main/java/sevenUnits/utils/ExpressionParser.java | 156 | ||||
-rw-r--r-- | src/main/resources/about.txt | 2 | ||||
-rw-r--r-- | src/test/resources/test-dimensionfile-valid1.txt | 8 |
4 files changed, 167 insertions, 43 deletions
diff --git a/src/main/java/sevenUnits/unit/UnitDatabase.java b/src/main/java/sevenUnits/unit/UnitDatabase.java index ea0aa7f..7e76729 100644 --- a/src/main/java/sevenUnits/unit/UnitDatabase.java +++ b/src/main/java/sevenUnits/unit/UnitDatabase.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2018 Adrien Hopkins + * Copyright (C) 2018-2024 Adrien Hopkins * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -1274,7 +1274,11 @@ public final class UnitDatabase { private final ExpressionParser<ObjectProduct<BaseDimension>> unitDimensionParser = new ExpressionParser.Builder<>( this::getDimension).addBinaryOperator("*", (o1, o2) -> o1.times(o2), 0) .addSpaceFunction("*") - .addBinaryOperator("/", (o1, o2) -> o1.dividedBy(o2), 0).build(); + .addBinaryOperator("/", (o1, o2) -> o1.dividedBy(o2), 0) + .addNumericOperator("^", (o1, o2) -> { + int exponent = (int) Math.round(o2.value()); + return o1.toExponent(exponent); + }, 1).build(); /** * Creates the {@code UnitsDatabase}. @@ -1580,10 +1584,6 @@ public final class UnitDatabase { /** * Gets a unit dimension from the database using its name. * - * <p> - * This method accepts exponents, like "L^3" - * </p> - * * @param name dimension's name * @return dimension * @since 2019-03-14 @@ -1591,30 +1591,13 @@ public final class UnitDatabase { */ public ObjectProduct<BaseDimension> getDimension(final String name) { Objects.requireNonNull(name, "name must not be null."); - if (name.contains("^")) { - final String[] baseAndExponent = name.split("\\^"); - - final ObjectProduct<BaseDimension> 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); - } else { - final ObjectProduct<BaseDimension> dimension = this.dimensions - .get(name); - if (dimension == null) - throw new NoSuchElementException( - "No dimension with name \"" + name + "\"."); - else - return dimension; - } + final ObjectProduct<BaseDimension> dimension = this.dimensions + .get(name); + if (dimension == null) + throw new NoSuchElementException( + "No dimension with name \"" + name + "\"."); + else + return dimension; } /** @@ -1653,7 +1636,6 @@ public final class UnitDatabase { modifiedExpression = replacement.getKey().matcher(modifiedExpression) .replaceAll(replacement.getValue()); } - modifiedExpression = modifiedExpression.replaceAll(" *\\^ *", "\\^"); return this.unitDimensionParser.parseExpression(modifiedExpression); } diff --git a/src/main/java/sevenUnits/utils/ExpressionParser.java b/src/main/java/sevenUnits/utils/ExpressionParser.java index 941c2a4..a41f37d 100644 --- a/src/main/java/sevenUnits/utils/ExpressionParser.java +++ b/src/main/java/sevenUnits/utils/ExpressionParser.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2019 Adrien Hopkins + * Copyright (C) 2019, 2024 Adrien Hopkins * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -24,6 +24,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.function.BiFunction; import java.util.function.BinaryOperator; import java.util.function.Function; import java.util.function.UnaryOperator; @@ -83,6 +84,13 @@ public final class ExpressionParser<T> { private final Map<String, PriorityBinaryOperator<T>> binaryOperators; /** + * A map mapping operator strings to numeric functions. + * + * @since 2024-03-23 + */ + private final Map<String, PriorityBiFunction<T, UncertainDouble, T>> numericOperators; + + /** * Creates the {@code Builder}. * * @param objectObtainer a function that can turn strings into objects of @@ -96,6 +104,7 @@ public final class ExpressionParser<T> { "objectObtainer must not be null."); this.unaryOperators = new HashMap<>(); this.binaryOperators = new HashMap<>(); + this.numericOperators = new HashMap<>(); } /** @@ -131,6 +140,32 @@ public final class ExpressionParser<T> { } /** + * Adds a two-argument operator where the second operator is a number. This is used for operations like vector scaling and exponentation. + * @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 + */ + public Builder<T> addNumericOperator(final String text, final BiFunction<T, UncertainDouble, T> 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 PriorityBiFunction<T, UncertainDouble, T> priorityOperator = new PriorityBiFunction<>( + priority) { + @Override + public T apply(final T t, final UncertainDouble u) { + return operator.apply(t, u); + } + + }; + this.numericOperators.put(text, priorityOperator); + return this; + } + + /** * Adds a function for spaces. You must use the text of an existing binary * operator. * @@ -189,7 +224,7 @@ public final class ExpressionParser<T> { */ public ExpressionParser<T> build() { return new ExpressionParser<>(this.objectObtainer, this.unaryOperators, - this.binaryOperators, this.spaceFunction); + this.binaryOperators, this.numericOperators, this.spaceFunction); } } @@ -253,6 +288,67 @@ public final class ExpressionParser<T> { return this.priority; } } + + /** + * A binary operator with a priority field that determines which operators + * apply first. + * + * @author Adrien Hopkins + * @param <T> type of operand and result + * @since 2019-03-17 + * @since v0.2.0 + */ + private static abstract class PriorityBiFunction<T, U, R> + implements BiFunction<T, U, R>, Comparable<PriorityBiFunction<T, U, R>> { + /** + * The operator's priority. Higher-priority operators are applied before + * lower-priority operators + * + * @since 2019-03-17 + * @since v0.2.0 + */ + private final int priority; + + /** + * Creates the {@code PriorityBinaryOperator}. + * + * @param priority operator's priority + * @since 2019-03-17 + * @since v0.2.0 + */ + public PriorityBiFunction(final int priority) { + this.priority = priority; + } + + /** + * Compares this object to another by priority. + * + * <p> + * {@inheritDoc} + * </p> + * + * @since 2019-03-17 + * @since v0.2.0 + */ + @Override + public int compareTo(final PriorityBiFunction<T, U, R> 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 + * @since v0.2.0 + */ + public final int getPriority() { + return this.priority; + } + } /** * A unary operator with a priority field that determines which operators @@ -323,7 +419,7 @@ public final class ExpressionParser<T> { * @since v0.2.0 */ private static enum TokenType { - OBJECT, UNARY_OPERATOR, BINARY_OPERATOR; + OBJECT, UNARY_OPERATOR, BINARY_OPERATOR, NUMERIC_OPERATOR; } /** @@ -421,6 +517,13 @@ public final class ExpressionParser<T> { * @since v0.2.0 */ private final Map<String, PriorityBinaryOperator<T>> binaryOperators; + + /** + * A map mapping operator strings to numeric functions. + * + * @since 2024-03-23 + */ + private final Map<String, PriorityBiFunction<T, UncertainDouble, T>> numericOperators; /** * The operator for space, or null if spaces have no function. @@ -436,6 +539,7 @@ public final class ExpressionParser<T> { * @param objectObtainer function to get objects from strings * @param unaryOperators unary operators available to the parser * @param binaryOperators binary operators available to the parser + * @param numericOperators numeric operators available to the parser * @param spaceOperator operator used by spaces * @since 2019-03-14 * @since v0.2.0 @@ -443,10 +547,12 @@ public final class ExpressionParser<T> { private ExpressionParser(final Function<String, ? extends T> objectObtainer, final Map<String, PriorityUnaryOperator<T>> unaryOperators, final Map<String, PriorityBinaryOperator<T>> binaryOperators, + final Map<String, PriorityBiFunction<T, UncertainDouble, T>> numericOperators, final String spaceOperator) { this.objectObtainer = objectObtainer; this.unaryOperators = unaryOperators; this.binaryOperators = binaryOperators; + this.numericOperators = numericOperators; this.spaceOperator = spaceOperator; } @@ -554,6 +660,7 @@ public final class ExpressionParser<T> { operand + " " + unaryOperator); break; case BINARY_OPERATOR: + case NUMERIC_OPERATOR: if (components.size() < 3) throw new IllegalArgumentException( "Invalid expression \"" + expression + "\""); @@ -628,6 +735,16 @@ public final class ExpressionParser<T> { maxPriorityPosition = i; } break; + case NUMERIC_OPERATOR: + final PriorityBiFunction<T, UncertainDouble, T> numericOperator = this.numericOperators + .get(components.get(i)); + final int numericPriority = numericOperator.getPriority(); + + if (numericPriority > maxPriority) { + maxPriority = numericPriority; + maxPriorityPosition = i; + } + break; default: break; } @@ -653,6 +770,8 @@ public final class ExpressionParser<T> { return TokenType.UNARY_OPERATOR; else if (this.binaryOperators.containsKey(token)) return TokenType.BINARY_OPERATOR; + else if (this.numericOperators.containsKey(token)) + return TokenType.NUMERIC_OPERATOR; else return TokenType.OBJECT; } @@ -684,6 +803,7 @@ public final class ExpressionParser<T> { Objects.requireNonNull(expression, "expression must not be null."); final Deque<T> stack = new ArrayDeque<>(); + final Deque<UncertainDouble> doubleStack = new ArrayDeque<>(); // iterate over every item in the expression, then for (final String item : expression.split(" ")) { @@ -704,10 +824,38 @@ public final class ExpressionParser<T> { stack.push(binaryOperator.apply(o1, o2)); break; + + case NUMERIC_OPERATOR: + if (stack.size() < 1 || doubleStack.size() < 1) + throw new IllegalStateException(String.format( + "Attempted to call binary operator %s with insufficient arguments.", + item)); + + final T ot = stack.pop(); + final UncertainDouble on = doubleStack.pop(); + final BiFunction<T, UncertainDouble, T> op = this.numericOperators.get(item); + stack.push(op.apply(ot, on)); + break; case OBJECT: // just add it to the stack - stack.push(this.objectObtainer.apply(item)); + // these try-catch statements are necessary + // to make the code as generalizable as possible + // also they're required for number formatting code because + // that's the only way to tell if an expression is a number or not. + try { + stack.push(this.objectObtainer.apply(item)); + } catch (Exception e) { + try { + doubleStack.push(UncertainDouble.fromString(item)); + } catch (IllegalArgumentException e2) { + try { + doubleStack.push(UncertainDouble.of(Double.parseDouble(item), 0)); + } catch (NumberFormatException e3) { + throw e; + } + } + } break; case UNARY_OPERATOR: diff --git a/src/main/resources/about.txt b/src/main/resources/about.txt index 5cdcf67..07309e5 100644 --- a/src/main/resources/about.txt +++ b/src/main/resources/about.txt @@ -2,7 +2,7 @@ About 7Units Version [VERSION] Copyright Notice: -Unit Converter Copyright (C) 2018-2023 Adrien Hopkins +Unit Converter Copyright (C) 2018-2024 Adrien Hopkins This program comes with ABSOLUTELY NO WARRANTY; for details read the LICENSE file, section 15 diff --git a/src/test/resources/test-dimensionfile-valid1.txt b/src/test/resources/test-dimensionfile-valid1.txt index fc6a426..d51ffe0 100644 --- a/src/test/resources/test-dimensionfile-valid1.txt +++ b/src/test/resources/test-dimensionfile-valid1.txt @@ -3,10 +3,4 @@ MASS ! TIME ! ENERGY MASS * LENGTH^2 / TIME^2 -POWER ENERGY / TIME - -# doesn't work, but would require major changes to fix properly -# for now, just don't use brackets in dimension expressions -# (note that the unit/prefix expressions use a complete hack -# to enable this, one that doesn't work for dimensions) -# POWER MASS * (LENGTH / TIME)^2 / TIME
\ No newline at end of file +POWER MASS * (LENGTH / TIME)^2 / TIME
\ No newline at end of file |