summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAdrien Hopkins <adrien.p.hopkins@gmail.com>2024-03-23 15:25:42 -0500
committerAdrien Hopkins <adrien.p.hopkins@gmail.com>2024-03-23 15:29:12 -0500
commit4db89a7e775921f4bb29db9f9b6bd939f115b631 (patch)
tree4b2a1feee3e5d08ff9fcfd95c8d77673964c4426
parent4a17e32274f991014edcfa22402d7207361f69f1 (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.java44
-rw-r--r--src/main/java/sevenUnits/utils/ExpressionParser.java156
-rw-r--r--src/main/resources/about.txt2
-rw-r--r--src/test/resources/test-dimensionfile-valid1.txt8
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