Commit 5dedae16 authored by Robert Zenz's avatar Robert Zenz

#13 Added unit conversion support.

parent 3c3e4079
......@@ -31,6 +31,8 @@ import org.bonsaimind.jmathpaper.core.evaluatedexpressions.BooleanEvaluatedExpre
import org.bonsaimind.jmathpaper.core.evaluatedexpressions.FunctionEvaluatedExpression;
import org.bonsaimind.jmathpaper.core.evaluatedexpressions.NumberEvaluatedExpression;
import org.bonsaimind.jmathpaper.core.resources.ResourceLoader;
import org.bonsaimind.jmathpaper.core.units.PrefixedUnit;
import org.bonsaimind.jmathpaper.core.units.UnitConverter;
import com.udojava.evalex.Expression;
......@@ -45,14 +47,21 @@ public class Evaluator {
private static final Pattern ID = ResourceLoader.compileRegex("id");
private static final Pattern LAST_REFERENCE = ResourceLoader.compileRegex("last-reference");
private static final Pattern OCTAL_NUMBER = ResourceLoader.compileRegex("octal-number");
private static final Pattern UNIT_CONVERSION = ResourceLoader.compileRegex("unit-conversion");
private int expressionCounter = 0;
private String lastVariableAdded = null;
private MathContext mathContext = DEFAULT_MATH_CONTEXT;
private List<EvaluatedExpression> previousEvaluatedExpressions = new ArrayList<>();
private UnitConverter unitConverter = new UnitConverter();
public Evaluator() {
super();
ResourceLoader.processResource("units/iec.prefixes", unitConverter::loadPrefix);
ResourceLoader.processResource("units/si.prefixes", unitConverter::loadPrefix);
ResourceLoader.processResource("units/default.units", unitConverter::loadUnit);
ResourceLoader.processResource("units/default.conversions", unitConverter::loadConversion);
reset();
}
......@@ -71,6 +80,8 @@ public class Evaluator {
public EvaluatedExpression evaluate(String expression) throws InvalidExpressionException {
String preProcessedExpression = preProcess(expression);
String processedExpression = stripComments(preProcessedExpression);
processedExpression = replaceAliases(processedExpression);
processedExpression = convertNumbers(processedExpression);
Matcher functionMatcher = FUNCTION.matcher(processedExpression);
......@@ -99,11 +110,36 @@ public class Evaluator {
processedExpression = idMatcher.group("EXPRESSION");
}
PrefixedUnit unitFrom = null;
PrefixedUnit unitTo = null;
Matcher unitConversionMatcher = UNIT_CONVERSION.matcher(processedExpression);
if (unitConversionMatcher.matches()) {
processedExpression = unitConversionMatcher.group("EXPRESSION");
unitFrom = unitConverter.getPrefixedUnit(unitConversionMatcher.group("FROM"));
if (unitFrom == null) {
throw new InvalidExpressionException("No such unit: " + unitConversionMatcher.group("FROM"));
}
unitTo = unitConverter.getPrefixedUnit(unitConversionMatcher.group("TO"));
if (unitTo == null) {
throw new InvalidExpressionException("No such unit: " + unitConversionMatcher.group("TO"));
}
}
try {
Expression mathExpression = prepareExpression(processedExpression);
BigDecimal result = mathExpression.eval();
if (unitFrom != null && unitTo != null) {
result = unitConverter.convert(unitFrom, unitTo, result, mathContext).stripTrailingZeros();
}
if (id == null) {
id = getNextId();
}
......@@ -129,23 +165,6 @@ public class Evaluator {
return new Expression("0");
}
expression = expression.replace(" and ", " && ");
expression = expression.replace(" or ", " || ");
expression = expression.replace(" equal ", " == ");
expression = expression.replace(" equals ", " == ");
expression = expression.replace(" notequal ", " != ");
expression = expression.replace(" notequals ", " != ");
expression = expression.replace(" greater ", " > ");
expression = expression.replace(" greaterequal ", " >= ");
expression = expression.replace(" greaterequals ", " >= ");
expression = expression.replace(" less ", " < ");
expression = expression.replace(" lessequal ", " <= ");
expression = expression.replace(" lessequals ", " <= ");
expression = applyPattern(expression, BINARY_NUMBER, Evaluator::convertFromBinary);
expression = applyPattern(expression, OCTAL_NUMBER, Evaluator::convertFromOctal);
expression = applyPattern(expression, HEX_NUMBER, Evaluator::convertFromHex);
String processedExpression = expression.replace('#', 'R');
EvaluatorAwareExpression mathExpression = new EvaluatorAwareExpression(
......@@ -206,6 +225,14 @@ public class Evaluator {
return buffer.toString();
}
private String convertNumbers(String expression) {
expression = applyPattern(expression, BINARY_NUMBER, Evaluator::convertFromBinary);
expression = applyPattern(expression, OCTAL_NUMBER, Evaluator::convertFromOctal);
expression = applyPattern(expression, HEX_NUMBER, Evaluator::convertFromHex);
return expression;
}
private String getNextId() {
expressionCounter = expressionCounter + 1;
......@@ -216,6 +243,23 @@ public class Evaluator {
return applyPattern(expression.trim(), LAST_REFERENCE, this::replaceLastReference);
}
private String replaceAliases(String expression) {
expression = expression.replace(" and ", " && ");
expression = expression.replace(" or ", " || ");
expression = expression.replace(" equal ", " == ");
expression = expression.replace(" equals ", " == ");
expression = expression.replace(" notequal ", " != ");
expression = expression.replace(" notequals ", " != ");
expression = expression.replace(" greater ", " > ");
expression = expression.replace(" greaterequal ", " >= ");
expression = expression.replace(" greaterequals ", " >= ");
expression = expression.replace(" less ", " < ");
expression = expression.replace(" lessequal ", " <= ");
expression = expression.replace(" lessequals ", " <= ");
return expression;
}
private String replaceLastReference(String value) {
if (lastVariableAdded == null) {
return "0";
......
......@@ -21,17 +21,21 @@ import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.function.Consumer;
import java.util.regex.Pattern;
/**
* {@link ResourceLoader} is a static utility for loading embedded resources.
*/
public final class ResourceLoader {
/** The {@link String} with which a comment in a regex file starts. */
private static final String REGEX_COMMENT_START = "#";
/** The package which contains the resources. */
private static final String BASE_PACKAGE = "/" + ResourceLoader.class.getPackage().getName().replace(".", "/");
/** The {@link String} with which a comment in a file starts. */
private static final String COMMENT_START = "#";
/** The package which contains the regex files/resources. */
private static final String REGEX_PACKAGE = "/" + ResourceLoader.class.getPackage().getName().replace(".", "/") + "/regex";
private static final String REGEX_PACKAGE = "regex";
/**
* No instance required.
......@@ -47,29 +51,62 @@ public final class ResourceLoader {
* @return The {@link Pattern} compiled from the regex.
*/
public static final Pattern compileRegex(String name) {
return Pattern.compile(loadRegex(name));
return Pattern.compile(loadResource(REGEX_PACKAGE + "/" + name + ".regex", null));
}
/**
* Loads the regex content with the given name.
* Loads the content of the given resource with the specified line endings.
* <p>
* This function will strip empty lines and also comments (see the
* {@link #COMMENT_START} string.
*
* @param name The name of the regex file, without path or extension.
* @return The content of the regex file with the given name.
* @param relativePath The path to the resource relative to this class.
* @param lineEnding The string to use as line-ending.
* @return The content of the given resource file.
*/
public static final String loadRegex(String name) {
String regexFile = REGEX_PACKAGE + "/" + name + ".regex";
public static final String loadResource(String relativePath, String lineEnding) {
StringBuilder content = new StringBuilder();
processResource(relativePath, content::append, lineEnding);
return content.toString();
}
/**
* Iterates over each line of the given resource.
* <p>
* This function will strip empty lines and also comments (see the
* {@link #COMMENT_START} string.
*
* @param relativePath The path to the resource relative to this class.
* @param lineProcessor The function to execute for every line.
*/
public static final void processResource(String relativePath, Consumer<String> lineProcessor) {
processResource(relativePath, lineProcessor, null);
}
/**
* Iterates over each line of the given resource.
* <p>
* This function will strip empty lines and also comments (see the
* {@link #COMMENT_START} string.
*
* @param relativePath The path to the resource relative to this class.
* @param lineProcessor The function to execute for every line.
* @param lineEnding The line ending to append to each line.
*/
public static final void processResource(String relativePath, Consumer<String> lineProcessor, String lineEnding) {
String file = BASE_PACKAGE + "/" + relativePath;
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(
ResourceLoader.class.getResourceAsStream(regexFile),
ResourceLoader.class.getResourceAsStream(file),
StandardCharsets.UTF_8))) {
StringBuilder content = new StringBuilder();
String line = reader.readLine();
while (line != null) {
int commentIndex = line.indexOf(REGEX_COMMENT_START);
int commentIndex = line.indexOf(COMMENT_START);
if (commentIndex >= 0) {
line = line.substring(0, commentIndex);
......@@ -77,16 +114,20 @@ public final class ResourceLoader {
line = line.trim();
content.append(line);
if (line.length() > 0) {
if (lineEnding != null) {
line = line + lineEnding;
}
lineProcessor.accept(line);
}
line = reader.readLine();
}
return content.toString();
} catch (IOException e) {
e.printStackTrace();
return null;
return;
}
}
}
#
# Copyright 2018, Robert 'Bobby' Zenz
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# Regular Expression for finding a function definition and expression.
#
# Matched samples:
# 1+1*4 m to in
^
(?<EXPRESSION>
.*?
[0-9]
)
( )*
(?<FROM>
([a-zA-Z]+|1)
(\^[0-9]+)?
)
(
( to )
|( in )
|( )
)
(?<TO>
([a-zA-Z]+|1)
(\^[0-9]+)?
)
$ # End of string.
\ No newline at end of file
# Metric volume
litre 1cudm
# Imperial length, https://en.wikipedia.org/wiki/Imperial_units#Length
thou 1/12000inch
thou 0.0254mm
inch 1000thou
inch 25.4mm
foot 12inches
foot 304.8mm
yard 3ft
yard 914.4mm
chain 22yards
furlong 10chain
mile 8furlongs
league 3miles
fathom 2.02667yards
cable 100fathoms
nauticalmile 10cables
link 7.92inches
rod 25links
britishnauticalmile 6080feet
# Imperial area, https://en.wikipedia.org/wiki/Imperial_units#Area
perch 272.25sqft
rood 10890sqft
acre 43560sqft
# Imperial volume, https://en.wikipedia.org/wiki/Imperial_units#Volume
fluidounce 28.4130625ml
gill 5floz
pint 20floz
quart 40floz
gallon 160floz
# Force https://en.wikipedia.org/wiki/Force#Units_of_measurement
dyne 0.00001N
kilogramforce 9.80665N
poundforce 4.4482216152605N
poundal 0.138254954376N
# Power, https://en.wikipedia.org/wiki/Horsepower
horsepower 735.49875W
imperialhorsepower 745.69987158227022W
electricalhorsepower 746W
boilerhorsepower 9812.5W
hydraulichorsepower 745.69987158227022W
airhorsepower 745.69987158227022W
# Digital
byte 8bit
# Temperature, https://en.wikipedia.org/wiki/Conversion_of_units_of_temperature
celsius (x * (9/5) + 32)°F
fahrenheit ((x - 32) * (5/9))°C
celsius (x + 273.15)K
kelvin (x - 273.15)°C
celsius ((x + 273.15) * (9/5))°R
rankine ((x - 491.67) * (5/9))°C
celsius ((100 - x) * (3/2))°De
delisle (100 - x * (2/3))°C
celsius (x * 33/100)°N
newtonscale (x * 100/33)°C
celsius (x * 4/5)°Re
reaumur (x * 5/4)°C
celsius (x * 21/40 + 7.5)°Ro
romer ((x - 7.5) * 40/21)°C
# Time
minute 60seconds
hour 60minutes
day 24hours
week 7days
month 30.436875days
year 12months
decade 10years
century 100years
millennium 1000years
# Aliases
klick 1km
\ No newline at end of file
# Metric length
meter 1 Metre, metre, m
# Metric force, https://en.wikipedia.org/wiki/Force#Units_of_measurement
newton 1 N
# Metric power
watt 1 W
# Metric volume
litre 3 Liter, liter, l
# Imperial length, https://en.wikipedia.org/wiki/Imperial_units#Length
thou 1 th, mil
inch 1 Inches, inches, in
foot 1 Feet, feet, ft
yard 1 yd
chain 1 ch
furlong 1 Stade, stade
mile 1 ml
league 1 lea
fathom 1 ftm
cable 1
nauticalmile 1
link 1
rod 1 Perch, perch, Pole, pole
britishnauticalmile 1
# Imperial area, https://en.wikipedia.org/wiki/Imperial_units#Area
perch 2
rood 2
acre 2
# Imperial volume, https://en.wikipedia.org/wiki/Imperial_units#Volume
fluidounce 3 floz
gill 3 gi
pint 3 pt
quart 3 qt
gallon 3 gal
# English
poppyseed 1
line 1
barleycorn 1
digit 1
finger 1
nail 1
palm 1
hand 1
shaftment 1
span 1
cubit 1
ell 1
# Other force, https://en.wikipedia.org/wiki/Force#Units_of_measurement
dyne 1 dyn
kilogramforce 1 kilopond, kp
poundforce 1 lbf
poundal 1 pdl
# Other power, https://en.wikipedia.org/wiki/Horsepower
horsepower 1 hp,PS, cv, hk, pk, ks, ch
imperialhorsepower 1 mechanicalhorsepower,hpI
electricalhorsepower 1 hpE
boilerhorsepower 1 hpS
hydraulichorsepower 1 hpH
airhorsepower 1 hpA
# Digital
bit 1 b
byte 1 B
# Temperature
celsius 1 C,°C
fahrenheit 1 F,°F
kelvin 1 K
rankine 1 R,°R
delisle 1 De,°De
newtonscale 1 °N
reaumur 1 Re,°Re
romer 1 Ro,°Ro
#Time
second 1 sec
minute 1 min
hour 1
day 1
week 1
month 1
year 1
decade 1
century 1
millennium 1
# Aliases
klick 1
\ No newline at end of file
# IEC/Binary prefixes
# See https://en.wikipedia.org/wiki/Binary_prefix
kibi Ki 1024 1
mebi Mi 1024 2
gibi Gi 1024 3
tebi Ti 1024 4
pebi Pi 1024 5
exbi Ei 1024 6
zebi Zi 1024 7
yobi Yi 1024 8
\ No newline at end of file
# SI prefixes
# See https://en.wikipedia.org/wiki/Metric_prefix
atto a 10 -18
femto f 10 -15
pico p 10 -12
nano n 10 -9
micro μ 10 -6
milli m 10 -3
centi c 10 -2
deci d 10 -1
deca da 10 1
hecto h 10 2
kilo k 10 3
mega M 10 6
giga G 10 9
tera T 10 12
peta P 10 15
exa E 10 18
\ No newline at end of file
/*
* Copyright 2018, Robert 'Bobby' Zenz
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.bonsaimind.jmathpaper.core.units;
import java.math.BigDecimal;
import java.math.MathContext;
/**
* A {@link Prefix} is a representation of a unit prefix which denotes multiples
* or fractions of a unit.
* <p>
* Two {@link Prefix}es are considered {@link #equals(Object) equal} when they
* have the same {@link #getName()} (case-insensitive).
*
* @see <a href="https://en.wikipedia.org/wiki/Unit_prefix">Wikipedia: Unit
* Prefix</a>
*/
public class Prefix {
/** An instance which denotes no prefix. */
public static final Prefix BASE = new Prefix("", "", 1, 1);
protected int base = 0;
protected BigDecimal factor = null;
protected String name = null;
protected int power = 0;
protected String symbol = null;
/**
* Creates a new instance of {@link Prefix}.
*
* @param name The (unique) name for this {@link Prefix}, case-insensitive.
* @param symbol The (unique) symbol for this {@link Prefix},
* case-sensitive.
* @param base The base for this {@link Prefix}.
* @param power The power for this {@link Prefix}.
*/
public Prefix(String name, String symbol, int base, int power) {
this.name = name;
this.symbol = symbol;
this.base = base;
this.power = power;
this.factor = new BigDecimal(base).pow(power, MathContext.DECIMAL128).stripTrailingZeros();
}
/**
* {@inheritDoc}
*/
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
Prefix other = (Prefix)obj;
if (name == null) {
if (other.name != null) {
return false;
}
} else if (!name.equalsIgnoreCase(other.name)) {
return false;
}
return true;
}
/**
* Gets the base of this {@link Prefix}.
*
* @return The base of this {@link Prefix}.
*/
public int getBase() {
return base;
}
/**
* Gets the factor of this {@link Prefix}.
* <p>
* The factor is the factor which can be directly appled to the value.
*
* @return The factor of this {@link Prefix}.
*/
public BigDecimal getFactor() {
return factor;
}
/**
* Gets the name of this {@link Prefix}.
* <p>
* The name uniquely identifies this {@link Prefix} and should be treated
* case insensitive.
*
* @return The name of this {@link Prefix}.
*/
public String getName() {
return name;
}
/**
* Gets the power of this {@link Prefix}.
*
* @return The power of this {@link Prefix}.
*/
public int getPower() {
return power;
}
/**
* Gets the symbol that denotes this {@link Prefix}.
* <p>
* The symbol uniquely identifies this {@link Prefix}, this should be
* treated case sensitive.
*
* @return The symbol that denotes this {@link Prefix}.
*/
public String getSymbol() {
return symbol;
}
/**
* {@inheritDoc}
*/
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
// Use the lower-case name to make sure that name is treated
// case-insensitive.
result = prime * result + ((name == null) ? 0 : name.toLowerCase().hashCode());
return result;
}
/**
* {@inheritDoc}
*/
@Override
public String toString() {
return name.toLowerCase();
}
}
/*
* Copyright 2018, Robert 'Bobby' Zenz
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.bonsaimind.jmathpaper.core.units;
/**
* A {@link PrefixedUnit} is the combination of an {@link Unit} with a
* {@link Prefix}.
* <p>
* Two {@link PrefixedUnit}s are considered {@link #equals(Object) equal} if the
* {@link Unit} and {@link Prefix} are equal to each other.
*/
public class PrefixedUnit {
/** An instance which denotes neither an unit nor a prefix. */
public static final PrefixedUnit NONE = new PrefixedUnit(Prefix.BASE, Unit.ONE);
protected Prefix prefix = null;
protected Unit unit = null;
/**
* Creates a new instance of {@link PrefixedUnit}.