summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAdrien Hopkins <adrien.p.hopkins@gmail.com>2025-06-07 22:11:45 -0500
committerAdrien Hopkins <adrien.p.hopkins@gmail.com>2025-06-07 22:26:49 -0500
commita00ad7ca48928a30ae577aeaed0345680df0a3fe (patch)
tree02a096b9f5c54775605cd648d8cd1c0a5e65a742
parent06192835ea97a657da1e5bd160686d21c097cbf5 (diff)
Fix e-notation & consolidate expression parsing
This commit moves all of the expression formatting code to one method, and changes it so that it works with things like '1e+2'. This does mean that I had to require spaces for addition and subtraction, but without that, the rules would be complicated.
-rw-r--r--docs/manual.org8
-rw-r--r--docs/manual.pdfbin186518 -> 188582 bytes
-rw-r--r--docs/manual.tex40
-rw-r--r--src/main/java/sevenUnits/unit/UnitDatabase.java574
-rw-r--r--src/main/java/sevenUnits/utils/ExpressionParser.java3
-rw-r--r--src/test/java/sevenUnits/unit/UnitDatabaseTest.java29
6 files changed, 305 insertions, 349 deletions
diff --git a/docs/manual.org b/docs/manual.org
index 92160c3..bc58ceb 100644
--- a/docs/manual.org
+++ b/docs/manual.org
@@ -98,12 +98,12 @@ You can also use the special setting names ~custom_unit_file~, ~custom_dimension
* Appendices
** Unit Expressions
A unit expression is simply a math expression where the values being operated on are units or numbers. The operations that can be used are (in order of precedence):
-- Exponentiation (^); the exponent must be an integer. Both units and numbers can be raised to an exponent
-- Multiplication (*) and division (/). Multiplication can also be done with a space (so "15 meter" is the same thing as "15 * meter").
+- Exponentiation (^); the exponent must be an integer. Both units and numbers can be raised to an exponent.
+- Multiplication (*) and division (/). Multiplication can also be done with a space (so "15 meter" is the same thing as "15 * meter"), and multiplication using spaces has higher precedence than division. Therefore, "2 m / 1 m" is the number 2, while "2 * m / 1 * m" is equal to 2 m^2.
You can also divide with ~|~ to create fractions. Using ~|~ instead of ~/~ gives the division a higher precedence than any other operator. For example, "2|5^2" evaluates to 4/25, not 2/25.
-- Addition (+) and subtraction (-). They can only be done between units of the same dimension (measuring the same thing). So you can add metres, inches and feet together, and you can add joules and calories together, but you can't add metres to seconds, or feet to calories, or watts to pounds.
+- Addition (+) and subtraction (-). They can only be done between units of the same dimension (measuring the same thing). So you can add metres, inches and feet together, and you can add joules and calories together, but you can't add metres to seconds, or feet to calories, or watts to pounds. *You must use spaces between terms when adding or subtracting - "1 + 2" is valid, "1+2" is not. This only applies to addition and subtraction - other operators do not need spaces between them.* This is done to avoid complex rules when working with negative numbers and e-notation.
- Brackets can be used to manipulate the order of operations, and nonlinear units like Celsius and Fahrenheit cannot be used in expressions. You can use a value in a nonlinear unit by putting brackets after it - for example, degC(12) represents the value 12 \deg C
+ Brackets can be used to manipulate the order of operations, and nonlinear units like Celsius and Fahrenheit cannot be used in expressions. You can use a value in a nonlinear unit by putting brackets after it - for example, degC(12) represents the value 12 \deg{}C
** Other Expressions
There are also a simplified version of expressions for prefixes and dimensions. Only multiplication, division and exponentation are supported. Currently, exponentation is not supported for dimensions, but that may be fixed in the future.
* Footnotes
diff --git a/docs/manual.pdf b/docs/manual.pdf
index 38d5c66..ca31104 100644
--- a/docs/manual.pdf
+++ b/docs/manual.pdf
Binary files differ
diff --git a/docs/manual.tex b/docs/manual.tex
index a1f7c63..8ab09d5 100644
--- a/docs/manual.tex
+++ b/docs/manual.tex
@@ -1,4 +1,4 @@
-% Created 2025-06-01 Sun 20:01
+% Created 2025-06-07 Sat 18:16
% Intended LaTeX compiler: pdflatex
\documentclass[11pt]{article}
\usepackage[utf8]{inputenc}
@@ -30,10 +30,10 @@
\newpage
\section{Introduction and Purpose}
-\label{sec:org6fb5a20}
+\label{sec:orgf5013f4}
7Units is a program that can be used to convert units. This document outlines how to use the program.
\section{System Requirements}
-\label{sec:org9d01e55}
+\label{sec:org9c3bf6a}
\begin{itemize}
\item Works on all major operating systems \\[0pt]
\textbf{NOTE:} All screenshots in this document were taken on Windows 10. If you use a different operating system, the program will probably look different than what is shown.
@@ -43,9 +43,9 @@
\end{itemize}
\newpage
\section{How to Use 7Units}
-\label{sec:org1fd2398}
+\label{sec:org6a030cf}
\subsection{Simple Unit Conversion}
-\label{sec:orga7e83a8}
+\label{sec:org4406ca2}
\begin{enumerate}
\item Select the "Convert Units" tab if it is not already selected. You should see a screen like in figure \ref{main-interface-dimension}:
\begin{figure}[htbp]
@@ -70,7 +70,7 @@
\end{figure}
\end{enumerate}
\subsection{Complex Unit Conversion}
-\label{sec:orgf923c07}
+\label{sec:org721af5f}
\begin{enumerate}
\item Select the "Convert Unit Expressions" if it is not already selected. You should see a screen like in figure \ref{main-interface-expression}:
\begin{figure}[htbp]
@@ -78,7 +78,7 @@
\includegraphics[height=250px]{../screenshots/main-interface-expression-converter.png}
\caption{\label{main-interface-expression}Taken in version 0.3.0}
\end{figure}
-\item Enter a \hyperref[sec:orga2dae79]{unit expression} in the From box. This can be something like "\texttt{7 km}" or "\texttt{6 ft - 2 in}" or "\texttt{3 kg m + 9 lb ft + (35 mm)\textasciicircum{}2 * (85 oz) / (20 in)}".
+\item Enter a \hyperref[sec:orgfffe912]{unit expression} in the From box. This can be something like "\texttt{7 km}" or "\texttt{6 ft - 2 in}" or "\texttt{3 kg m + 9 lb ft + (35 mm)\textasciicircum{}2 * (85 oz) / (20 in)}".
\item Enter a unit name (or another unit expression) in the To box.
\item Press the Convert button. This will calculate the value of the first expression, and convert it to a multiple of the second unit (or expression).
\begin{figure}[htbp]
@@ -88,7 +88,7 @@
\end{figure}
\end{enumerate}
\section{7Units Settings}
-\label{sec:org69dc7d4}
+\label{sec:org437288e}
All settings can be accessed in the tab with the gear icon.
\begin{figure}[htbp]
\centering
@@ -96,7 +96,7 @@ All settings can be accessed in the tab with the gear icon.
\caption{The settings menu, as of version 0.4.0}
\end{figure}
\subsection{Rounding Settings}
-\label{sec:org8d63000}
+\label{sec:orgc275b0f}
These settings control how the output of a unit conversion is rounded.
\begin{description}
\item[{Fixed Precision}] Round to a fixed number of \href{https://en.wikipedia.org/wiki/Significant\_figures}{significant digits}. The number of significant digits is controlled by the precision slider below.
@@ -104,7 +104,7 @@ These settings control how the output of a unit conversion is rounded.
\item[{Scientific Precision}] Intelligent rounding which uses the precision of the input value(s) to determine the output precision. Not affected by the precision slider.
\end{description}
\subsection{Prefix Repetition Settings}
-\label{sec:org1f1263f}
+\label{sec:org1789e5a}
These settings control when you are allowed to repeat unit prefixes (e.g. kilokilometre)
\begin{description}
\item[{No Repetition}] Units may only have one prefix.
@@ -119,7 +119,7 @@ These settings control when you are allowed to repeat unit prefixes (e.g. kiloki
\end{itemize}
\end{description}
\subsection{Search Settings}
-\label{sec:org03df615}
+\label{sec:orgfcf1ac1}
These settings control which prefixes are shown in the "Convert Units" tab. Only coherent SI units (e.g. metre, second, newton, joule) will get prefixes. Some prefixed units are created in the unitfile, and will stay regardless of this setting (though they can be removed from the unitfile).
\begin{description}
\item[{Never Include Prefixed Units}] Prefixed units will only be shown if they are explicitly added to the unitfile.
@@ -127,7 +127,7 @@ These settings control which prefixes are shown in the "Convert Units" tab. Onl
\item[{Include All Single Prefixes}] Every coherent unit will have every prefixed version of it included in the list.
\end{description}
\subsection{Miscellaneous Settings}
-\label{sec:org159a151}
+\label{sec:org2a48ec0}
\begin{description}
\item[{Convert One Way Only}] In the simple conversion tab, only imperial/customary units will be shown on the left, and only metric units\footnote{7Units's definition of "metric" is stricter than the SI, but all of the common units that are commonly considered metric but not included in 7Units's definition are included in the exceptions file.} will be shown on the right. Units listed in the exceptions file (\texttt{src/main/resources/metric\_exceptions.txt}) will be shown on both sides. This is a way to reduce the number of options you must search through if you only convert one way. The expressions tab is unaffected.
\item[{Show Duplicates in "Convert Units"}] If unchecked, any unit that has multiple names will only have one included in the Convert Units lists. The selected name will be the longest; if there are multiple longest names one is selected arbitrarily. You will still be able to use these alternate names in the expressions tab.
@@ -135,7 +135,7 @@ These settings control which prefixes are shown in the "Convert Units" tab. Onl
\item[{Locale}] Which language is used for the interface of 7Units. Custom locales can be added.
\end{description}
\subsection{Configuration File}
-\label{sec:org398eb26}
+\label{sec:orgd976cbf}
The settings are saved in a configuration file. On Windows, this is located at \\[0pt]
\texttt{\%USERPROFILE\%/AppData/Local/SevenUnits/config.txt}. On other operating systems, this is located at \texttt{\$HOME/.config/SevenUnits/config.txt}. The directory containing the \texttt{SevenUnits} directory can be overridden with the environment variables \texttt{\$LOCALAPPDATA} on Windows or \texttt{\$XDG\_CONFIG\_HOME} elsewhere.
@@ -157,19 +157,19 @@ This is the name of a locale file, excluding the extension, in one of the direct
You can also use the special setting names \texttt{custom\_unit\_file}, \texttt{custom\_dimension\_file} and \texttt{custom\_exception\_file} to add custom units, dimensions and metric exceptions to the system. These files use the same format as the standard files. These setting names can be used more than once to include multiple unit, dimension or exception files.
\section{Appendices}
-\label{sec:org89d72bb}
+\label{sec:orgec4e8a9}
\subsection{Unit Expressions}
-\label{sec:orga2dae79}
+\label{sec:orgfffe912}
A unit expression is simply a math expression where the values being operated on are units or numbers. The operations that can be used are (in order of precedence):
\begin{itemize}
-\item Exponentiation (\^{}); the exponent must be an integer. Both units and numbers can be raised to an exponent
-\item Multiplication (*) and division (/). Multiplication can also be done with a space (so "15 meter" is the same thing as "15 * meter").
+\item Exponentiation (\^{}); the exponent must be an integer. Both units and numbers can be raised to an exponent.
+\item Multiplication (*) and division (/). Multiplication can also be done with a space (so "15 meter" is the same thing as "15 * meter"), and multiplication using spaces has higher precedence than division. Therefore, "2 m / 1 m" is the number 2, while "2 * m / 1 * m" is equal to 2 m\textsuperscript{2}.
You can also divide with \texttt{|} to create fractions. Using \texttt{|} instead of \texttt{/} gives the division a higher precedence than any other operator. For example, "2|5\textsuperscript{2}" evaluates to 4/25, not 2/25.
-\item Addition (+) and subtraction (-). They can only be done between units of the same dimension (measuring the same thing). So you can add metres, inches and feet together, and you can add joules and calories together, but you can't add metres to seconds, or feet to calories, or watts to pounds.
+\item Addition (+) and subtraction (-). They can only be done between units of the same dimension (measuring the same thing). So you can add metres, inches and feet together, and you can add joules and calories together, but you can't add metres to seconds, or feet to calories, or watts to pounds. \textbf{You must use spaces between terms when adding or subtracting - "1 + 2" is valid, "1+2" is not. This only applies to addition and subtraction - other operators do not need spaces between them.} This is done to avoid complex rules when working with negative numbers and e-notation.
-Brackets can be used to manipulate the order of operations, and nonlinear units like Celsius and Fahrenheit cannot be used in expressions. You can use a value in a nonlinear unit by putting brackets after it - for example, degC(12) represents the value 12 \textdegree{} C
+Brackets can be used to manipulate the order of operations, and nonlinear units like Celsius and Fahrenheit cannot be used in expressions. You can use a value in a nonlinear unit by putting brackets after it - for example, degC(12) represents the value 12 \textdegree{}C
\end{itemize}
\subsection{Other Expressions}
-\label{sec:orgf67cbc9}
+\label{sec:org5d2d129}
There are also a simplified version of expressions for prefixes and dimensions. Only multiplication, division and exponentation are supported. Currently, exponentation is not supported for dimensions, but that may be fixed in the future.
\end{document}
diff --git a/src/main/java/sevenUnits/unit/UnitDatabase.java b/src/main/java/sevenUnits/unit/UnitDatabase.java
index b0d026f..a85ec5f 100644
--- a/src/main/java/sevenUnits/unit/UnitDatabase.java
+++ b/src/main/java/sevenUnits/unit/UnitDatabase.java
@@ -122,7 +122,7 @@ public final class UnitDatabase {
implements Entry<String, Unit> {
private final String key;
private final Unit value;
-
+
/**
* Creates the {@code PrefixedUnitEntry}.
*
@@ -135,7 +135,7 @@ public final class UnitDatabase {
this.key = key;
this.value = value;
}
-
+
/**
* @since 2019-05-03
* @since v0.3.0
@@ -148,17 +148,17 @@ public final class UnitDatabase {
return Objects.equals(this.getKey(), other.getKey())
&& Objects.equals(this.getValue(), other.getValue());
}
-
+
@Override
public String getKey() {
return this.key;
}
-
+
@Override
public Unit getValue() {
return this.value;
}
-
+
/**
* @since 2019-05-03
* @since v0.3.0
@@ -169,13 +169,13 @@ public final class UnitDatabase {
^ (this.getValue() == null ? 0
: this.getValue().hashCode());
}
-
+
@Override
public Unit setValue(final Unit value) {
throw new UnsupportedOperationException(
"Cannot set value in an immutable entry");
}
-
+
/**
* Returns a string representation of the entry. The format of the
* string is the string representation of the key, then the equals
@@ -190,7 +190,7 @@ public final class UnitDatabase {
return this.getKey() + "=" + this.getValue();
}
}
-
+
/**
* An iterator that iterates over the units of a
* {@code PrefixedUnitNameSet}.
@@ -205,12 +205,12 @@ public final class UnitDatabase {
private int unitNamePosition = 0;
// the indices of the prefixes attached to the current unit
private final List<Integer> prefixCoordinates = new ArrayList<>();
-
+
// values from the unit entry set
private final Map<String, Unit> map;
private transient final List<String> unitNames;
private transient final List<String> prefixNames;
-
+
/**
* Creates the
* {@code UnitsDatabase.PrefixedUnitMap.PrefixedUnitNameSet.PrefixedUnitNameIterator}.
@@ -225,7 +225,7 @@ public final class UnitDatabase {
this.unitNames = new ArrayList<>(map.units.keySet());
this.prefixNames = new ArrayList<>(map.prefixes.keySet());
}
-
+
/**
* @return current unit name
* @since 2019-04-14
@@ -237,10 +237,10 @@ public final class UnitDatabase {
unitName.append(this.prefixNames.get(i));
}
unitName.append(this.unitNames.get(this.unitNamePosition));
-
+
return unitName.toString();
}
-
+
@Override
public boolean hasNext() {
if (this.unitNames.isEmpty())
@@ -253,7 +253,7 @@ public final class UnitDatabase {
return true;
}
}
-
+
/**
* Changes this iterator's position to the next available one.
*
@@ -262,11 +262,11 @@ public final class UnitDatabase {
*/
private void incrementPosition() {
this.unitNamePosition++;
-
+
if (this.unitNamePosition >= this.unitNames.size()) {
// we have used all of our units, go to a different prefix
this.unitNamePosition = 0;
-
+
// if the prefix coordinates are empty, then set it to [0]
if (this.prefixCoordinates.isEmpty()) {
this.prefixCoordinates.add(0, 0);
@@ -275,7 +275,7 @@ public final class UnitDatabase {
int i = this.prefixCoordinates.size() - 1;
this.prefixCoordinates.set(i,
this.prefixCoordinates.get(i) + 1);
-
+
// fix any carrying errors
while (i >= 0 && this.prefixCoordinates
.get(i) >= this.prefixNames.size()) {
@@ -283,7 +283,7 @@ public final class UnitDatabase {
this.prefixCoordinates.set(i--, 0); // null and
// decrement at the
// same time
-
+
if (i < 0) { // we need to add a new coordinate
this.prefixCoordinates.add(0, 0);
} else { // increment an existing one
@@ -294,18 +294,18 @@ public final class UnitDatabase {
}
}
}
-
+
@Override
public Entry<String, Unit> next() {
// get next element
final Entry<String, Unit> nextEntry = this.peek();
-
+
// iterate to next position
this.incrementPosition();
-
+
return nextEntry;
}
-
+
/**
* @return the next element in the iterator, without iterating over
* it
@@ -315,7 +315,7 @@ public final class UnitDatabase {
private Entry<String, Unit> peek() {
if (!this.hasNext())
throw new NoSuchElementException("No units left!");
-
+
// if I have prefixes, ensure I'm not using a nonlinear unit
// since all of the unprefixed stuff is done, just remove
// nonlinear units
@@ -326,12 +326,12 @@ public final class UnitDatabase {
this.unitNames.remove(this.unitNamePosition);
}
}
-
+
final String nextName = this.getCurrentUnitName();
-
+
return new PrefixedUnitEntry(nextName, this.map.get(nextName));
}
-
+
/**
* Returns a string representation of the object. The exact details
* of the representation are unspecified and subject to change.
@@ -346,10 +346,10 @@ public final class UnitDatabase {
this.peek());
}
}
-
+
// the map that created this set
private final PrefixedUnitMap map;
-
+
/**
* Creates the {@code PrefixedUnitNameSet}.
*
@@ -360,31 +360,31 @@ public final class UnitDatabase {
public PrefixedUnitEntrySet(final PrefixedUnitMap map) {
this.map = map;
}
-
+
@Override
public boolean add(final Map.Entry<String, Unit> e) {
throw new UnsupportedOperationException(
"Cannot add to an immutable set");
}
-
+
@Override
public boolean addAll(
final Collection<? extends Map.Entry<String, Unit>> c) {
throw new UnsupportedOperationException(
"Cannot add to an immutable set");
}
-
+
@Override
public void clear() {
throw new UnsupportedOperationException(
"Cannot clear an immutable set");
}
-
+
@Override
public boolean contains(final Object o) {
// get the entry
final Entry<String, Unit> entry;
-
+
try {
// This is OK because I'm in a try-catch block, catching the
// exact exception that would be thrown.
@@ -395,11 +395,11 @@ public final class UnitDatabase {
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)
@@ -407,42 +407,42 @@ public final class UnitDatabase {
return false;
return true;
}
-
+
@Override
public boolean isEmpty() {
return this.map.isEmpty();
}
-
+
@Override
public Iterator<Entry<String, Unit>> iterator() {
return new PrefixedUnitEntryIterator(this.map);
}
-
+
@Override
public boolean remove(final Object o) {
throw new UnsupportedOperationException(
"Cannot remove from an immutable set");
}
-
+
@Override
public boolean removeAll(final Collection<?> c) {
throw new UnsupportedOperationException(
"Cannot remove from an immutable set");
}
-
+
@Override
public boolean removeIf(
final Predicate<? super Entry<String, Unit>> filter) {
throw new UnsupportedOperationException(
"Cannot remove from an immutable set");
}
-
+
@Override
public boolean retainAll(final Collection<?> c) {
throw new UnsupportedOperationException(
"Cannot remove from an immutable set");
}
-
+
@Override
public int size() {
if (this.map.units.isEmpty())
@@ -455,7 +455,7 @@ public final class UnitDatabase {
return Integer.MAX_VALUE;
}
}
-
+
/**
* @throws IllegalStateException if the set is infinite in size
*/
@@ -468,7 +468,7 @@ public final class UnitDatabase {
throw new IllegalStateException(
"Cannot make an infinite set into an array.");
}
-
+
/**
* @throws IllegalStateException if the set is infinite in size
*/
@@ -481,7 +481,7 @@ public final class UnitDatabase {
throw new IllegalStateException(
"Cannot make an infinite set into an array.");
}
-
+
@Override
public String toString() {
if (this.map.units.isEmpty() || this.map.prefixes.isEmpty())
@@ -492,7 +492,7 @@ public final class UnitDatabase {
this.map.units, this.map.prefixes);
}
}
-
+
/**
* The class used for unit name sets.
*
@@ -524,12 +524,12 @@ public final class UnitDatabase {
private int unitNamePosition = 0;
// the indices of the prefixes attached to the current unit
private final List<Integer> prefixCoordinates = new ArrayList<>();
-
+
// values from the unit name set
private final Map<String, Unit> map;
private transient final List<String> unitNames;
private transient final List<String> prefixNames;
-
+
/**
* Creates the
* {@code UnitsDatabase.PrefixedUnitMap.PrefixedUnitNameSet.PrefixedUnitNameIterator}.
@@ -544,7 +544,7 @@ public final class UnitDatabase {
this.unitNames = new ArrayList<>(map.units.keySet());
this.prefixNames = new ArrayList<>(map.prefixes.keySet());
}
-
+
/**
* @return current unit name
* @since 2019-04-14
@@ -556,10 +556,10 @@ public final class UnitDatabase {
unitName.append(this.prefixNames.get(i));
}
unitName.append(this.unitNames.get(this.unitNamePosition));
-
+
return unitName.toString();
}
-
+
@Override
public boolean hasNext() {
if (this.unitNames.isEmpty())
@@ -572,7 +572,7 @@ public final class UnitDatabase {
return true;
}
}
-
+
/**
* Changes this iterator's position to the next available one.
*
@@ -581,11 +581,11 @@ public final class UnitDatabase {
*/
private void incrementPosition() {
this.unitNamePosition++;
-
+
if (this.unitNamePosition >= this.unitNames.size()) {
// we have used all of our units, go to a different prefix
this.unitNamePosition = 0;
-
+
// if the prefix coordinates are empty, then set it to [0]
if (this.prefixCoordinates.isEmpty()) {
this.prefixCoordinates.add(0, 0);
@@ -594,7 +594,7 @@ public final class UnitDatabase {
int i = this.prefixCoordinates.size() - 1;
this.prefixCoordinates.set(i,
this.prefixCoordinates.get(i) + 1);
-
+
// fix any carrying errors
while (i >= 0 && this.prefixCoordinates
.get(i) >= this.prefixNames.size()) {
@@ -602,7 +602,7 @@ public final class UnitDatabase {
this.prefixCoordinates.set(i--, 0); // null and
// decrement at the
// same time
-
+
if (i < 0) { // we need to add a new coordinate
this.prefixCoordinates.add(0, 0);
} else { // increment an existing one
@@ -613,16 +613,16 @@ public final class UnitDatabase {
}
}
}
-
+
@Override
public String next() {
final String nextName = this.peek();
-
+
this.incrementPosition();
-
+
return nextName;
}
-
+
/**
* @return the next element in the iterator, without iterating over
* it
@@ -642,10 +642,10 @@ public final class UnitDatabase {
this.unitNames.remove(this.unitNamePosition);
}
}
-
+
return this.getCurrentUnitName();
}
-
+
/**
* Returns a string representation of the object. The exact details
* of the representation are unspecified and subject to change.
@@ -660,10 +660,10 @@ public final class UnitDatabase {
this.peek());
}
}
-
+
// the map that created this set
private final PrefixedUnitMap map;
-
+
/**
* Creates the {@code PrefixedUnitNameSet}.
*
@@ -674,30 +674,30 @@ public final class UnitDatabase {
public PrefixedUnitNameSet(final PrefixedUnitMap map) {
this.map = map;
}
-
+
@Override
public boolean add(final String e) {
throw new UnsupportedOperationException(
"Cannot add to an immutable set");
}
-
+
@Override
public boolean addAll(final Collection<? extends String> c) {
throw new UnsupportedOperationException(
"Cannot add to an immutable set");
}
-
+
@Override
public void clear() {
throw new UnsupportedOperationException(
"Cannot clear an immutable set");
}
-
+
@Override
public boolean contains(final Object o) {
return this.map.containsKey(o);
}
-
+
@Override
public boolean containsAll(final Collection<?> c) {
for (final Object o : c)
@@ -705,41 +705,41 @@ public final class UnitDatabase {
return false;
return true;
}
-
+
@Override
public boolean isEmpty() {
return this.map.isEmpty();
}
-
+
@Override
public Iterator<String> iterator() {
return new PrefixedUnitNameIterator(this.map);
}
-
+
@Override
public boolean remove(final Object o) {
throw new UnsupportedOperationException(
"Cannot remove from an immutable set");
}
-
+
@Override
public boolean removeAll(final Collection<?> c) {
throw new UnsupportedOperationException(
"Cannot remove from an immutable set");
}
-
+
@Override
public boolean removeIf(final Predicate<? super String> filter) {
throw new UnsupportedOperationException(
"Cannot remove from an immutable set");
}
-
+
@Override
public boolean retainAll(final Collection<?> c) {
throw new UnsupportedOperationException(
"Cannot remove from an immutable set");
}
-
+
@Override
public int size() {
if (this.map.units.isEmpty())
@@ -752,7 +752,7 @@ public final class UnitDatabase {
return Integer.MAX_VALUE;
}
}
-
+
/**
* @throws IllegalStateException if the set is infinite in size
*/
@@ -764,9 +764,9 @@ public final class UnitDatabase {
// infinite set
throw new IllegalStateException(
"Cannot make an infinite set into an array.");
-
+
}
-
+
/**
* @throws IllegalStateException if the set is infinite in size
*/
@@ -779,7 +779,7 @@ public final class UnitDatabase {
throw new IllegalStateException(
"Cannot make an infinite set into an array.");
}
-
+
@Override
public String toString() {
if (this.map.units.isEmpty() || this.map.prefixes.isEmpty())
@@ -790,7 +790,7 @@ public final class UnitDatabase {
this.map.units, this.map.prefixes);
}
}
-
+
/**
* The units stored in this collection, without prefixes.
*
@@ -798,7 +798,7 @@ public final class UnitDatabase {
* @since v0.2.0
*/
private final Map<String, Unit> units;
-
+
/**
* The available prefixes for use.
*
@@ -806,12 +806,12 @@ public final class UnitDatabase {
* @since v0.2.0
*/
private final Map<String, UnitPrefix> prefixes;
-
+
// caches
private transient Collection<Unit> values = null;
private transient Set<String> keySet = null;
private transient Set<Entry<String, Unit>> entrySet = null;
-
+
/**
* Creates the {@code PrefixedUnitMap}.
*
@@ -827,50 +827,50 @@ public final class UnitDatabase {
this.units = Collections.unmodifiableMap(units);
this.prefixes = Collections.unmodifiableMap(prefixes);
}
-
+
@Override
public void clear() {
throw new UnsupportedOperationException(
"Cannot clear an immutable map");
}
-
+
@Override
public Unit compute(final String key,
final BiFunction<? super String, ? super Unit, ? extends Unit> remappingFunction) {
throw new UnsupportedOperationException(
"Cannot edit an immutable map");
}
-
+
@Override
public Unit computeIfAbsent(final String key,
final Function<? super String, ? extends Unit> mappingFunction) {
throw new UnsupportedOperationException(
"Cannot edit an immutable map");
}
-
+
@Override
public Unit computeIfPresent(final String key,
final BiFunction<? super String, ? super Unit, ? extends Unit> remappingFunction) {
throw new UnsupportedOperationException(
"Cannot edit an immutable map");
}
-
+
@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 longest prefix that is attached to a valid unit
String longestPrefix = null;
int longestLength = 0;
-
+
for (final String prefixName : this.prefixes.keySet()) {
// a prefix name is valid if:
// - it is prefixed (i.e. the unit name starts with it)
@@ -889,10 +889,10 @@ public final class UnitDatabase {
}
}
}
-
+
return longestPrefix != null;
}
-
+
/**
* {@inheritDoc}
*
@@ -905,7 +905,7 @@ public final class UnitDatabase {
public boolean containsValue(final Object value) {
return this.units.containsValue(value);
}
-
+
@Override
public Set<Entry<String, Unit>> entrySet() {
if (this.entrySet == null) {
@@ -913,23 +913,23 @@ public final class UnitDatabase {
}
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 longest prefix that is attached to a valid unit
String longestPrefix = null;
int longestLength = 0;
-
+
for (final String prefixName : this.prefixes.keySet()) {
// a prefix name is valid if:
// - it is prefixed (i.e. the unit name starts with it)
@@ -948,7 +948,7 @@ public final class UnitDatabase {
}
}
}
-
+
// if none found, returns null
if (longestPrefix == null)
return null;
@@ -959,16 +959,16 @@ public final class UnitDatabase {
// before selecting this prefix
final LinearUnit unit = (LinearUnit) this.get(rest);
final UnitPrefix prefix = this.prefixes.get(longestPrefix);
-
+
return unit.withPrefix(prefix);
}
}
-
+
@Override
public boolean isEmpty() {
return this.units.isEmpty();
}
-
+
@Override
public Set<String> keySet() {
if (this.keySet == null) {
@@ -976,64 +976,64 @@ public final class UnitDatabase {
}
return this.keySet;
}
-
+
@Override
public Unit merge(final String key, final Unit value,
final BiFunction<? super Unit, ? super Unit, ? extends Unit> remappingFunction) {
throw new UnsupportedOperationException(
"Cannot merge into an immutable map");
}
-
+
@Override
public Unit put(final String key, final Unit value) {
throw new UnsupportedOperationException(
"Cannot add entries to an immutable map");
}
-
+
@Override
public void putAll(final Map<? extends String, ? extends Unit> m) {
throw new UnsupportedOperationException(
"Cannot add entries to an immutable map");
}
-
+
@Override
public Unit putIfAbsent(final String key, final Unit value) {
throw new UnsupportedOperationException(
"Cannot add entries to an immutable map");
}
-
+
@Override
public Unit remove(final Object key) {
throw new UnsupportedOperationException(
"Cannot remove entries from an immutable map");
}
-
+
@Override
public boolean remove(final Object key, final Object value) {
throw new UnsupportedOperationException(
"Cannot remove entries from an immutable map");
}
-
+
@Override
public Unit replace(final String key, final Unit value) {
throw new UnsupportedOperationException(
"Cannot replace entries in an immutable map");
}
-
+
@Override
public boolean replace(final String key, final Unit oldValue,
final Unit newValue) {
throw new UnsupportedOperationException(
"Cannot replace entries in an immutable map");
}
-
+
@Override
public void replaceAll(
final BiFunction<? super String, ? super Unit, ? extends Unit> function) {
throw new UnsupportedOperationException(
"Cannot replace entries in an immutable map");
}
-
+
@Override
public int size() {
if (this.units.isEmpty())
@@ -1046,7 +1046,7 @@ public final class UnitDatabase {
return Integer.MAX_VALUE;
}
}
-
+
@Override
public String toString() {
if (this.units.isEmpty() || this.prefixes.isEmpty())
@@ -1056,7 +1056,7 @@ public final class UnitDatabase {
"Infinite map of name-unit entries created from units %s and prefixes %s",
this.units, this.prefixes);
}
-
+
/**
* {@inheritDoc}
*
@@ -1074,48 +1074,20 @@ public final class UnitDatabase {
return this.values;
}
}
-
- /**
- * Replacements done to *all* expression types
- */
- private static final Map<Pattern, String> EXPRESSION_REPLACEMENTS = new HashMap<>();
-
- // add data to expression replacements
- static {
- // add spaces around operators
- for (final String operator : Arrays.asList("\\*", "/", "\\|", "\\^")) {
- EXPRESSION_REPLACEMENTS.put(Pattern.compile(operator),
- " " + operator + " ");
- }
-
- // replace multiple spaces with a single space
- EXPRESSION_REPLACEMENTS.put(Pattern.compile(" +"), " ");
- // place brackets around any expression of the form "number unit", with or
- // without the space
- EXPRESSION_REPLACEMENTS.put(Pattern.compile("((?:-?[1-9]\\d*|0)" // integer
- + "(?:\\.\\d+(?:[eE]\\d+))?)" // optional decimal point with numbers
- // after it
- + "\\s*" // optional space(s)
- + "([a-zA-Z]+(?:\\^\\d+)?" // any string of letters
- + "(?:\\s+[a-zA-Z]+(?:\\^\\d+)?))" // optional other letters
- + "(?!-?\\d)" // no number directly afterwards (avoids matching
- // "1e3")
- ), "\\($1 $2\\)");
- }
-
+
/**
* A regular expression that separates names and expressions in unit files.
*/
private static final Pattern NAME_EXPRESSION = Pattern
.compile("(\\S+)\\s+(\\S.*)");
-
+
/**
* Like normal string comparisons, but shorter strings are always less than
* longer strings.
*/
private static final Comparator<String> lengthFirstComparator = Comparator
.comparingInt(String::length).thenComparing(Comparator.naturalOrder());
-
+
/**
* The exponent operator
*
@@ -1131,11 +1103,11 @@ public final class UnitDatabase {
throw new IllegalArgumentException(String.format(
"Tried to exponentiate %s^%s, but exponents must be dimensionless numbers.",
base, exponentUnit));
-
+
final double exponent = exponentUnit.getConversionFactor();
return base.toExponentRounded(exponent);
}
-
+
/**
* The exponent operator
*
@@ -1151,11 +1123,33 @@ public final class UnitDatabase {
throw new IllegalArgumentException(String.format(
"Tried to exponentiate %s^%s, but exponents must be dimensionless numbers.",
base, exponentValue));
-
+
final double exponent = exponentValue.getValueExact();
return base.toExponentRounded(exponent);
}
-
+
+ /**
+ * Formats an expression so it can be parsed by the expression parser.
+ *
+ * Specifically, puts spaces around all operators so they can be parsed as
+ * words.
+ *
+ * @param expression expression to format
+ * @return formatted expression
+ * @since 2025-06-07
+ * @since v1.0.0
+ */
+ static final String formatExpression(String expression) {
+ String modifiedExpression = expression;
+ for (final String operator : Arrays.asList("\\*", "/", "\\|", "\\^")) {
+ modifiedExpression = modifiedExpression.replaceAll(
+ operator, " " + operator + " ");
+ }
+
+ modifiedExpression = modifiedExpression.replaceAll("\\s+", " ");
+ return modifiedExpression;
+ }
+
/**
* @return true if entry represents a removable duplicate entry of map.
* @since 2021-05-22
@@ -1172,7 +1166,7 @@ public final class UnitDatabase {
}
return false;
}
-
+
/**
* The units in this system, excluding prefixes.
*
@@ -1180,7 +1174,7 @@ public final class UnitDatabase {
* @since v0.1.0
*/
private final Map<String, Unit> prefixlessUnits;
-
+
/**
* The unit prefixes in this system.
*
@@ -1188,7 +1182,7 @@ public final class UnitDatabase {
* @since v0.1.0
*/
private final Map<String, UnitPrefix> prefixes;
-
+
/**
* The dimensions in this system.
*
@@ -1196,7 +1190,7 @@ public final class UnitDatabase {
* @since v0.2.0
*/
private final Map<String, ObjectProduct<BaseDimension>> dimensions;
-
+
/**
* A map mapping strings to units (including prefixes)
*
@@ -1204,7 +1198,7 @@ public final class UnitDatabase {
* @since v0.2.0
*/
private final Map<String, Unit> units;
-
+
/**
* A map mapping strings to unit sets
*
@@ -1212,7 +1206,7 @@ public final class UnitDatabase {
* @since v1.0.0
*/
private final Map<String, List<LinearUnit>> unitSets;
-
+
/**
* The rule that specifies when prefix repetition is allowed. It takes in one
* argument: a list of the prefixes being applied to the unit
@@ -1224,7 +1218,7 @@ public final class UnitDatabase {
* {@code prefixRepetitionRule.test(Arrays.asList(giga, mega, kilo))}
*/
private Predicate<List<UnitPrefix>> prefixRepetitionRule;
-
+
/**
* A parser that can parse unit expressions.
*
@@ -1233,14 +1227,14 @@ public final class UnitDatabase {
*/
private final ExpressionParser<LinearUnit> 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) -> o1.dividedBy(o2), 3)
- .addBinaryOperator("^", UnitDatabase::exponentiateUnits, 2)
- .build();
-
+ .addBinaryOperator("-", (o1, o2) -> o1.minus(o2), 0)
+ .addBinaryOperator("*", (o1, o2) -> o1.times(o2), 1)
+ .addBinaryOperator("space_times", (o1, o2) -> o1.times(o2), 2)
+ .addSpaceFunction("space_times")
+ .addBinaryOperator("/", (o1, o2) -> o1.dividedBy(o2), 1)
+ .addBinaryOperator("|", (o1, o2) -> o1.dividedBy(o2), 4)
+ .addBinaryOperator("^", UnitDatabase::exponentiateUnits, 3).build();
+
/**
* A parser that can parse unit value expressions.
*
@@ -1249,15 +1243,16 @@ public final class UnitDatabase {
*/
private final ExpressionParser<LinearUnitValue> unitValueExpressionParser = new ExpressionParser.Builder<>(
this::getLinearUnitValue)
- .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) -> o1.dividedBy(o2), 3)
- .addBinaryOperator("^", UnitDatabase::exponentiateUnitValues, 2)
- .build();
-
+ .addBinaryOperator("+", (o1, o2) -> o1.plus(o2), 0)
+ .addBinaryOperator("-", (o1, o2) -> o1.minus(o2), 0)
+ .addBinaryOperator("*", (o1, o2) -> o1.times(o2), 1)
+ .addBinaryOperator("space_times", (o1, o2) -> o1.times(o2), 2)
+ .addSpaceFunction("space_times")
+ .addBinaryOperator("/", (o1, o2) -> o1.dividedBy(o2), 1)
+ .addBinaryOperator("|", (o1, o2) -> o1.dividedBy(o2), 4)
+ .addBinaryOperator("^", UnitDatabase::exponentiateUnitValues, 3)
+ .build();
+
/**
* A parser that can parse unit prefix expressions
*
@@ -1266,15 +1261,15 @@ public final class UnitDatabase {
*/
private final ExpressionParser<UnitPrefix> prefixExpressionParser = new ExpressionParser.Builder<>(
this::getPrefix).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) -> o1.dividedBy(o2), 3)
- .addBinaryOperator("^",
- (o1, o2) -> o1.toExponent(o2.getMultiplier()), 2)
- .build();
-
+ .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) -> o1.dividedBy(o2), 3)
+ .addBinaryOperator("^", (o1, o2) -> o1.toExponent(o2.getMultiplier()),
+ 2)
+ .build();
+
/**
* A parser that can parse unit dimension expressions.
*
@@ -1283,14 +1278,14 @@ 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)
- .addBinaryOperator("|", (o1, o2) -> o1.dividedBy(o2), 2)
- .addNumericOperator("^", (o1, o2) -> {
- final int exponent = (int) Math.round(o2.value());
- return o1.toExponent(exponent);
- }, 1).build();
-
+ .addSpaceFunction("*")
+ .addBinaryOperator("/", (o1, o2) -> o1.dividedBy(o2), 0)
+ .addBinaryOperator("|", (o1, o2) -> o1.dividedBy(o2), 2)
+ .addNumericOperator("^", (o1, o2) -> {
+ final int exponent = (int) Math.round(o2.value());
+ return o1.toExponent(exponent);
+ }, 1).build();
+
/**
* Creates the {@code UnitsDatabase}.
*
@@ -1300,7 +1295,7 @@ public final class UnitDatabase {
public UnitDatabase() {
this(prefixes -> true);
}
-
+
/**
* Creates the {@code UnitsDatabase}
*
@@ -1320,7 +1315,7 @@ public final class UnitDatabase {
.test(this.getPrefixesFromName(entry.getKey())));
this.unitSets = new HashMap<>();
}
-
+
/**
* Adds a unit dimension to the database.
*
@@ -1338,7 +1333,7 @@ public final class UnitDatabase {
.withName(dimension.getNameSymbol().withExtraName(name));
this.dimensions.put(name, namedDimension);
}
-
+
/**
* Adds to the list from a line in a unit dimension file.
*
@@ -1357,7 +1352,7 @@ public final class UnitDatabase {
lineCounter);
return;
}
-
+
// divide line into name and expression
final Matcher lineMatcher = NAME_EXPRESSION.matcher(line);
if (!lineMatcher.matches())
@@ -1366,12 +1361,12 @@ public final class UnitDatabase {
lineCounter));
final String name = lineMatcher.group(1);
final String expression = lineMatcher.group(2);
-
+
// if (name.endsWith(" ")) {
// System.err.printf("Warning - line %d's dimension name ends in a space",
// lineCounter);
// }
-
+
// if expression is "!", search for an existing dimension
// if no unit found, throw an error
if (expression.equals("!")) {
@@ -1382,7 +1377,7 @@ public final class UnitDatabase {
this.addDimension(name, this.getDimensionFromExpression(expression));
}
}
-
+
/**
* Adds a unit prefix to the database.
*
@@ -1399,7 +1394,7 @@ public final class UnitDatabase {
this.prefixes.put(Objects.requireNonNull(name, "name must not be null."),
namedPrefix);
}
-
+
/**
* Adds a unit to the database.
*
@@ -1416,7 +1411,7 @@ public final class UnitDatabase {
this.prefixlessUnits.put(
Objects.requireNonNull(name, "name must not be null."), namedUnit);
}
-
+
/**
* Adds to the list from a line in a unit file.
*
@@ -1435,7 +1430,7 @@ public final class UnitDatabase {
lineCounter);
return;
}
-
+
// divide line into name and expression
final Matcher lineMatcher = NAME_EXPRESSION.matcher(line);
if (!lineMatcher.matches())
@@ -1443,15 +1438,15 @@ public final class UnitDatabase {
"Error at line %d: Lines of a unit file must consist of a unit name, then spaces or tabs, then a unit expression.",
lineCounter));
final String name = lineMatcher.group(1);
-
+
final String expression = lineMatcher.group(2);
-
+
// this code should never occur
// 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("!")) {
@@ -1472,7 +1467,7 @@ public final class UnitDatabase {
}
}
}
-
+
/**
* Add a unit set to the database.
*
@@ -1490,10 +1485,10 @@ public final class UnitDatabase {
"Unit sets must be all the same dimension, " + value
+ " is not.");
}
-
+
this.unitSets.put(name, value);
}
-
+
/**
* Removes all units, unit sets, prefixes and dimensions from this database.
*
@@ -1506,7 +1501,7 @@ public final class UnitDatabase {
this.prefixlessUnits.clear();
this.unitSets.clear();
}
-
+
/**
* Tests if the database has a unit dimension with this name.
*
@@ -1518,7 +1513,7 @@ public final class UnitDatabase {
public boolean containsDimensionName(final String name) {
return this.dimensions.containsKey(name);
}
-
+
/**
* Tests if the database has a unit prefix with this name.
*
@@ -1530,7 +1525,7 @@ public final class UnitDatabase {
public boolean containsPrefixName(final String name) {
return this.prefixes.containsKey(name);
}
-
+
/**
* Tests if the database has a unit with this name, taking prefixes into
* consideration
@@ -1543,7 +1538,7 @@ public final class UnitDatabase {
public boolean containsUnitName(final String name) {
return this.units.containsKey(name);
}
-
+
/**
* Returns true iff there is a unit set with this name.
*
@@ -1556,7 +1551,7 @@ public final class UnitDatabase {
public boolean containsUnitSetName(String name) {
return this.unitSets.containsKey(name);
}
-
+
/**
* @return a map mapping dimension names to dimensions
* @since 2019-04-13
@@ -1565,7 +1560,7 @@ public final class UnitDatabase {
public Map<String, ObjectProduct<BaseDimension>> dimensionMap() {
return Collections.unmodifiableMap(this.dimensions);
}
-
+
/**
* Evaluates a unit expression, following the same rules as
* {@link #getUnitFromExpression}.
@@ -1577,39 +1572,15 @@ public final class UnitDatabase {
*/
public LinearUnitValue evaluateUnitExpression(final String expression) {
Objects.requireNonNull(expression, "expression must not be null.");
-
+
// attempt to get a unit as an alias, or a number with precision first
if (this.containsUnitName(expression))
return this.getLinearUnitValue(expression);
-
- // force operators to have spaces
- String modifiedExpression = expression;
- modifiedExpression = modifiedExpression.replaceAll("\\+", " \\+ ");
- modifiedExpression = modifiedExpression.replaceAll("-", " - ");
-
- // format expression
- for (final Entry<Pattern, String> replacement : EXPRESSION_REPLACEMENTS
- .entrySet()) {
- modifiedExpression = replacement.getKey().matcher(modifiedExpression)
- .replaceAll(replacement.getValue());
- }
-
- // the previous operation breaks negative numbers, fix them!
- // (i.e. -2 becomes - 2)
- // FIXME the previous operation also breaks stuff like "1e-5"
- for (int i = 0; i < modifiedExpression.length(); i++) {
- if (modifiedExpression.charAt(i) == '-'
- && (i < 2 || Arrays.asList('+', '-', '*', '/', '|', '^')
- .contains(modifiedExpression.charAt(i - 2)))) {
- // found a broken negative number
- modifiedExpression = modifiedExpression.substring(0, i + 1)
- + modifiedExpression.substring(i + 2);
- }
- }
-
- return this.unitValueExpressionParser.parseExpression(modifiedExpression);
+
+ return this.unitValueExpressionParser
+ .parseExpression(formatExpression(expression));
}
-
+
/**
* Gets a unit dimension from the database using its name.
*
@@ -1627,7 +1598,7 @@ public final class UnitDatabase {
else
return dimension;
}
-
+
/**
* Uses the database's data to parse an expression into a unit dimension
* <p>
@@ -1651,24 +1622,14 @@ public final class UnitDatabase {
public ObjectProduct<BaseDimension> 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;
-
- // format expression
- for (final Entry<Pattern, String> replacement : EXPRESSION_REPLACEMENTS
- .entrySet()) {
- modifiedExpression = replacement.getKey().matcher(modifiedExpression)
- .replaceAll(replacement.getValue());
- }
-
- return this.unitDimensionParser.parseExpression(modifiedExpression);
+
+ return this.unitDimensionParser.parseExpression(formatExpression(expression));
}
-
+
/**
* Gets a unit. If it is linear, cast it to a LinearUnit and return it.
* Otherwise, throw an {@code IllegalArgumentException}.
@@ -1687,7 +1648,7 @@ public final class UnitDatabase {
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(
@@ -1696,7 +1657,7 @@ public final class UnitDatabase {
} else {
// get a linear unit
final Unit unit = this.getUnit(name);
-
+
if (unit instanceof LinearUnit)
return (LinearUnit) unit;
else
@@ -1704,7 +1665,7 @@ public final class UnitDatabase {
String.format("%s is not a linear unit.", name));
}
}
-
+
/**
* Gets a {@code LinearUnitValue} from a unit name. Nonlinear units will be
* converted to their base units.
@@ -1723,7 +1684,7 @@ public final class UnitDatabase {
return LinearUnitValue.getExact(this.getLinearUnit(name), 1);
}
}
-
+
/**
* Gets a unit prefix from the database from its name
*
@@ -1744,7 +1705,7 @@ public final class UnitDatabase {
return prefix;
}
}
-
+
/**
* Gets all of the prefixes that are on a unit name, in application order.
*
@@ -1756,12 +1717,12 @@ public final class UnitDatabase {
List<UnitPrefix> getPrefixesFromName(final String unitName) {
final List<UnitPrefix> prefixes = new ArrayList<>();
String name = unitName;
-
+
while (!this.prefixlessUnits.containsKey(name)) {
// find the longest prefix
String longestPrefixName = null;
int longestLength = name.length();
-
+
while (longestPrefixName == null) {
longestLength--;
if (longestLength <= 0)
@@ -1771,7 +1732,7 @@ public final class UnitDatabase {
longestPrefixName = name.substring(0, longestLength);
}
}
-
+
// longest prefix found!
final UnitPrefix prefix = this.getPrefix(longestPrefixName);
prefixes.add(0, prefix);
@@ -1779,7 +1740,7 @@ public final class UnitDatabase {
}
return prefixes;
}
-
+
/**
* Gets a unit prefix from a prefix expression
* <p>
@@ -1796,24 +1757,14 @@ public final class UnitDatabase {
*/
public UnitPrefix getPrefixFromExpression(final String expression) {
Objects.requireNonNull(expression, "expression must not be null.");
-
+
// attempt to get a unit as an alias first
if (this.containsUnitName(expression))
return this.getPrefix(expression);
-
- // force operators to have spaces
- String modifiedExpression = expression;
-
- // format expression
- for (final Entry<Pattern, String> replacement : EXPRESSION_REPLACEMENTS
- .entrySet()) {
- modifiedExpression = replacement.getKey().matcher(modifiedExpression)
- .replaceAll(replacement.getValue());
- }
-
- return this.prefixExpressionParser.parseExpression(modifiedExpression);
+
+ return this.prefixExpressionParser.parseExpression(formatExpression(expression));
}
-
+
/**
* @return the prefixRepetitionRule
* @since 2020-08-26
@@ -1822,7 +1773,7 @@ public final class UnitDatabase {
public final Predicate<List<UnitPrefix>> getPrefixRepetitionRule() {
return this.prefixRepetitionRule;
}
-
+
/**
* Gets a unit from the database from its name, looking for prefixes.
*
@@ -1855,9 +1806,9 @@ public final class UnitDatabase {
} else
return unit;
}
-
+
}
-
+
/**
* Uses the database's unit data to parse an expression into a unit
* <p>
@@ -1882,38 +1833,15 @@ public final class UnitDatabase {
*/
public Unit getUnitFromExpression(final String expression) {
Objects.requireNonNull(expression, "expression must not be null.");
-
+
// 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("-", " - ");
-
- // format expression
- for (final Entry<Pattern, String> replacement : EXPRESSION_REPLACEMENTS
- .entrySet()) {
- modifiedExpression = replacement.getKey().matcher(modifiedExpression)
- .replaceAll(replacement.getValue());
- }
-
- // the previous operation breaks negative numbers, fix them!
- // (i.e. -2 becomes - 2)
- for (int i = 0; i < modifiedExpression.length(); i++) {
- if (modifiedExpression.charAt(i) == '-'
- && (i < 2 || Arrays.asList('+', '-', '*', '/', '|', '^')
- .contains(modifiedExpression.charAt(i - 2)))) {
- // found a broken negative number
- modifiedExpression = modifiedExpression.substring(0, i + 1)
- + modifiedExpression.substring(i + 2);
- }
- }
-
- return this.unitExpressionParser.parseExpression(modifiedExpression);
+
+ return this.unitExpressionParser
+ .parseExpression(formatExpression(expression));
}
-
+
/**
* Get a unit set from its name, throwing a {@link NoSuchElementException} if
* there is none.
@@ -1930,7 +1858,7 @@ public final class UnitDatabase {
throw new NoSuchElementException("No unit set with name " + name);
return unitSet;
}
-
+
/**
* Parses a semicolon-separated expression to get the unit set being used.
*
@@ -1942,7 +1870,7 @@ public final class UnitDatabase {
final List<LinearUnit> units = new ArrayList<>(parts.length);
for (final String unitName : parts) {
final Unit unit = this.getUnitFromExpression(unitName.trim());
-
+
if (!(unit instanceof LinearUnit)) {
throw new IllegalArgumentException(String.format(
"Unit '%s' is in a unit-set expression, but is not linear.",
@@ -1957,7 +1885,7 @@ public final class UnitDatabase {
}
return units;
}
-
+
/**
* Adds all dimensions from a file, using data from the database to parse
* them.
@@ -2003,7 +1931,7 @@ public final class UnitDatabase {
}
return errors;
}
-
+
/**
* Adds all dimensions from a {@code InputStream}. Otherwise, works like
* {@link #loadDimensionFile}.
@@ -2030,7 +1958,7 @@ public final class UnitDatabase {
}
return errors;
}
-
+
/**
* Adds all units from a file, using data from the database to parse them.
* <p>
@@ -2075,7 +2003,7 @@ public final class UnitDatabase {
}
return errors;
}
-
+
/**
* Adds all units from a {@code InputStream}. Otherwise, works like
* {@link #loadUnitsFile}.
@@ -2101,7 +2029,7 @@ public final class UnitDatabase {
}
return errors;
}
-
+
/**
* @param includeDuplicates if false, duplicates are removed from the map
* @return a map mapping prefix names to prefixes
@@ -2116,7 +2044,7 @@ public final class UnitDatabase {
.conditionalExistenceMap(this.prefixes,
entry -> !isRemovableDuplicate(this.prefixes, entry)));
}
-
+
/**
* @param prefixRepetitionRule the prefixRepetitionRule to set
* @since 2020-08-26
@@ -2126,7 +2054,7 @@ public final class UnitDatabase {
Predicate<List<UnitPrefix>> prefixRepetitionRule) {
this.prefixRepetitionRule = prefixRepetitionRule;
}
-
+
/**
* @return a string stating the number of units, prefixes and dimensions in
* the database
@@ -2138,7 +2066,7 @@ public final class UnitDatabase {
this.prefixlessUnits.size(), this.prefixes.size(),
this.dimensions.size());
}
-
+
/**
* Returns a map mapping unit names to units, including units with prefixes.
* <p>
@@ -2170,7 +2098,7 @@ public final class UnitDatabase {
return this.units; // PrefixedUnitMap is immutable so I don't need to make
// an unmodifiable map.
}
-
+
/**
* @param includeDuplicates if true, duplicate units will all exist in the
* map; if false, only one of each unit will exist,
@@ -2188,7 +2116,7 @@ public final class UnitDatabase {
entry -> !isRemovableDuplicate(this.prefixlessUnits,
entry)));
}
-
+
/**
* @return an unmodifiable map mapping names to unit sets
* @since 2024-08-16
diff --git a/src/main/java/sevenUnits/utils/ExpressionParser.java b/src/main/java/sevenUnits/utils/ExpressionParser.java
index 1c8df9f..051082d 100644
--- a/src/main/java/sevenUnits/utils/ExpressionParser.java
+++ b/src/main/java/sevenUnits/utils/ExpressionParser.java
@@ -578,7 +578,8 @@ public final class ExpressionParser<T> {
* @since 2019-03-17
* @since v0.2.0
*/
- String convertExpressionToReversePolish(final String expression) {
+ // TODO revert to package private
+ public String convertExpressionToReversePolish(final String expression) {
Objects.requireNonNull(expression, "expression must not be null.");
final List<String> components = new ArrayList<>();
diff --git a/src/test/java/sevenUnits/unit/UnitDatabaseTest.java b/src/test/java/sevenUnits/unit/UnitDatabaseTest.java
index c78837f..800d13d 100644
--- a/src/test/java/sevenUnits/unit/UnitDatabaseTest.java
+++ b/src/test/java/sevenUnits/unit/UnitDatabaseTest.java
@@ -190,14 +190,35 @@ class UnitDatabaseTest {
}
private static final Stream<Arguments> testEvaluateExpressionValid() {
+ UncertainDouble uncertainTwoThirds = UncertainDouble.of(2.0, 1.0)
+ .dividedBy(UncertainDouble.of(3.0, 1.0));
return Stream.of(
Arguments.of("J + (2 * 3) J + (20 / 4) J",
LinearUnitValue.of(J,
UncertainDouble.of(12, Math.sqrt(14.625)))),
+ Arguments.of("J + 2 * 3 * J + 20 / 4 * J",
+ LinearUnitValue.of(J,
+ UncertainDouble.of(12, Math.sqrt(14.625)))),
Arguments.of("J - -1 * J",
LinearUnitValue.of(J, UncertainDouble.of(2, 1))),
Arguments.of("K^2",
- LinearUnitValue.of(K.times(K), UncertainDouble.of(1, 0))));
+ LinearUnitValue.of(K.times(K), UncertainDouble.of(1, 0))),
+ Arguments.of("2 J / 3 J",
+ LinearUnitValue.of(J.dividedBy(J), uncertainTwoThirds)));
+ }
+
+ private static final Stream<Arguments> testFormatExpression() {
+ return Stream.of(
+ Arguments.of("1*2", "1 * 2"),
+ Arguments.of("1/2", "1 / 2"),
+ Arguments.of("1|2", "1 | 2"),
+ Arguments.of("1^2", "1 ^ 2"),
+ Arguments.of("1 * 2", "1 * 2"),
+ Arguments.of("+1", "+1"),
+ Arguments.of("-1", "-1"),
+ Arguments.of("1.1e+5", "1.1e+5"),
+ Arguments.of("1.25e-5", "1.25e-5")
+ );
}
/**
@@ -253,6 +274,12 @@ class UnitDatabaseTest {
final var actualU = database.getUnitFromExpression(expression);
assertEquals(expectedU, actualU);
}
+
+ @ParameterizedTest
+ @MethodSource
+ public void testFormatExpression(String expression, String expected) {
+ assertEquals(expected, UnitDatabase.formatExpression(expression));
+ }
/**
* Test for {@link UnitDatabase#getUnit}, {@link UnitDatabase#getLinearUnit}