/** * Copyright (C) 2018 Adrien Hopkins * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ package org.unitConverter; import java.io.BufferedReader; import java.io.File; import java.io.FileNotFoundException; import java.io.FileReader; import java.io.IOException; import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Objects; import java.util.Set; import org.unitConverter.dimension.UnitDimension; import org.unitConverter.unit.AbstractUnit; import org.unitConverter.unit.BaseUnit; import org.unitConverter.unit.DefaultUnitPrefix; import org.unitConverter.unit.LinearUnit; import org.unitConverter.unit.SI; import org.unitConverter.unit.Unit; import org.unitConverter.unit.UnitPrefix; /** * A database of units and prefixes, and their names. * * @author Adrien Hopkins * @since 2019-01-07 * @since v0.1.0 */ public final class UnitsDatabase { /** * The units in this system. * * @since 2019-01-07 * @since v0.1.0 */ private final Map units; /** * The unit prefixes in this system. * * @since 2019-01-14 * @since v0.1.0 */ private final Map prefixes; /** * The dimensions in this system. * * @since 2019-03-14 */ private final Map dimensions; /** * Creates the {@code UnitsDatabase}. * * @since 2019-01-10 * @since v0.1.0 */ public UnitsDatabase() { this.units = new HashMap<>(); this.prefixes = new HashMap<>(); this.dimensions = new HashMap<>(); } /** * Adds all units from a file, using data from the database to parse them. *

* Each line in the file should consist of a name and an expression (parsed by getUnitFromExpression) separated by * any number of tab characters. *

*

* Allowed exceptions: *

    *
  • Any line that begins with the '#' character is considered a comment and ignored.
  • *
  • Blank lines are also ignored
  • *
  • If an expression consists of a single exclamation point, instead of parsing it, this method will search the * database for an existing unit. If no unit is found, an IllegalArgumentException is thrown. This is used to define * initial units and ensure that the database contains them.
  • *
* * @param file * file to read * @throws IllegalArgumentException * if the file cannot be parsed, found or read * @throws NullPointerException * if file is null * @since 2019-01-13 * @since v0.1.0 */ public void addAllFromFile(final File file) { Objects.requireNonNull(file, "file must not be null."); try (FileReader fileReader = new FileReader(file); BufferedReader reader = new BufferedReader(fileReader)) { // while the reader has lines to read, read a line, then parse it, then add it long lineCounter = 0; while (reader.ready()) { final String line = reader.readLine(); lineCounter++; // ignore lines that start with a # sign - they're comments if (line.startsWith("#") || line.isEmpty()) { continue; } // divide line into name and expression final String[] parts = line.split("\t"); if (parts.length < 2) throw new IllegalArgumentException(String.format( "Lines must consist of a unit name and its definition, separated by tab(s) (line %d).", lineCounter)); final String name = parts[0]; final String expression = parts[parts.length - 1]; if (name.endsWith(" ")) { System.err.printf("Warning - line %d's unit name ends in a space", lineCounter); } // if expression is "!", search for an existing unit // if no unit found, throw an error if (expression.equals("!")) { if (!this.containsUnitName(name)) throw new IllegalArgumentException( String.format("! used but no unit found (line %d).", lineCounter)); } else { if (name.endsWith("-")) { final UnitPrefix prefix; try { prefix = this.getPrefixFromExpression(expression); } catch (final IllegalArgumentException e) { System.err.printf("Parsing error on line %d:%n", lineCounter); throw e; } this.addPrefix(name.substring(0, name.length() - 1), prefix); } else { // it's a unit, get the unit final Unit unit; try { unit = this.getUnitFromExpression(expression); } catch (final IllegalArgumentException e) { System.err.printf("Parsing error on line %d:%n", lineCounter); throw e; } AbstractUnit.incrementUnitCounter(); if (unit instanceof BaseUnit) { AbstractUnit.incrementBaseUnitCounter(); } this.addUnit(name, unit); } } } } catch (final FileNotFoundException e) { throw new IllegalArgumentException("Could not find file " + file, e); } catch (final IOException e) { throw new IllegalArgumentException("Could not read file " + file, e); } } /** * Adds a unit dimension to the database. * * @param name * dimension's name * @param dimension * dimension to add * @throws NullPointerException * if name or dimension is null * @since 2019-03-14 */ public void addDimension(final String name, final UnitDimension dimension) { this.dimensions.put(Objects.requireNonNull(name, "name must not be null."), Objects.requireNonNull(dimension, "dimension must not be null.")); } /** * Adds a unit prefix to the database. * * @param name * prefix's name * @param prefix * prefix to add * @throws NullPointerException * if name or prefix is null * @since 2019-01-14 * @since v0.1.0 */ public void addPrefix(final String name, final UnitPrefix prefix) { this.prefixes.put(Objects.requireNonNull(name, "name must not be null."), Objects.requireNonNull(prefix, "prefix must not be null.")); } /** * Adds a unit to the database. * * @param name * unit's name * @param unit * unit to add * @throws NullPointerException * if unit is null * @since 2019-01-10 * @since v0.1.0 */ public void addUnit(final String name, final Unit unit) { this.units.put(Objects.requireNonNull(name, "name must not be null."), Objects.requireNonNull(unit, "unit must not be null.")); } /** * Tests if the database has a unit dimension with this name. * * @param name * name to test * @return if database contains name * @since 2019-03-14 */ public boolean containsDimensionName(final String name) { return this.dimensions.containsKey(name); } /** * Tests if the database has a unit with this name, ignoring prefixes * * @param name * name to test * @return if database contains name * @since 2019-01-13 * @since v0.1.0 */ public boolean containsPrefixlessUnitName(final String name) { return this.units.containsKey(name); } /** * Tests if the database has a unit prefix with this name. * * @param name * name to test * @return if database contains name * @since 2019-01-13 * @since v0.1.0 */ 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 * * @param name * name to test * @return if database contains name * @since 2019-01-13 * @since v0.1.0 */ public boolean containsUnitName(final String name) { // check for prefixes for (final String prefixName : this.prefixNameSet()) { if (name.startsWith(prefixName)) if (this.containsUnitName(name.substring(prefixName.length()))) return true; } return this.units.containsKey(name); } /** * @return an immutable set of all of the dimension names in this database. * @since 2019-03-14 */ public Set dimensionNameSet() { return Collections.unmodifiableSet(this.dimensions.keySet()); } /** * Gets a unit dimension from the database using its name. * * @param name * dimension's name * @return dimension * @since 2019-03-14 */ public UnitDimension getDimension(final String name) { return this.dimensions.get(name); } /** * Gets a unit prefix from the database from its name * * @param name * prefix's name * @return prefix * @since 2019-01-10 * @since v0.1.0 */ public UnitPrefix getPrefix(final String name) { return this.prefixes.get(name); } /** * Gets a unit prefix from a prefix expression *

* Currently, prefix expressions are much simpler than unit expressions: They are either a number or the name of * another prefix *

* * @param expression * expression to input * @return prefix * @throws IllegalArgumentException * if expression cannot be parsed * @throws NullPointerException * if any argument is null * @since 2019-01-14 * @since v0.1.0 */ public UnitPrefix getPrefixFromExpression(final String expression) { Objects.requireNonNull(expression, "expression must not be null."); try { return new DefaultUnitPrefix(Double.parseDouble(expression)); } catch (final NumberFormatException e) { if (expression.contains("^")) { final String[] baseAndExponent = expression.split("\\^"); final double base; try { base = Double.parseDouble(baseAndExponent[0]); } catch (final NumberFormatException e2) { throw new IllegalArgumentException("Base of exponientation must be a number."); } final int exponent; try { exponent = Integer.parseInt(baseAndExponent[baseAndExponent.length - 1]); } catch (final NumberFormatException e2) { throw new IllegalArgumentException("Exponent must be an integer."); } return new DefaultUnitPrefix(Math.pow(base, exponent)); } else { if (!this.containsPrefixName(expression)) throw new IllegalArgumentException("Unrecognized prefix name \"" + expression + "\"."); return this.getPrefix(expression); } } } /** * Gets a unit from the database from its name, ignoring prefixes. * * @param name * unit's name * @return unit * @since 2019-01-10 * @since v0.1.0 */ public Unit getPrefixlessUnit(final String name) { return this.units.get(name); } /** * Gets a unit from the database from its name, looking for prefixes. * * @param name * unit's name * @return unit * @since 2019-01-10 * @since v0.1.0 */ public Unit getUnit(final String name) { if (name.contains("^")) { final String[] baseAndExponent = name.split("\\^"); LinearUnit base; try { base = SI.SI.getBaseUnit(UnitDimension.EMPTY).times(Double.parseDouble(baseAndExponent[0])); } catch (final NumberFormatException e2) { final Unit unit = this.getUnit(baseAndExponent[0]); if (unit instanceof LinearUnit) { base = (LinearUnit) unit; } else throw new IllegalArgumentException("Base of exponientation must be a linear or base unit."); } final int exponent; try { exponent = Integer.parseInt(baseAndExponent[baseAndExponent.length - 1]); } catch (final NumberFormatException e2) { throw new IllegalArgumentException("Exponent must be an integer."); } final LinearUnit exponentiated = base.toExponent(exponent); if (exponentiated.getConversionFactor() == 1) return exponentiated.getSystem().getBaseUnit(exponentiated.getDimension()); else return exponentiated; } else { for (final String prefixName : this.prefixNameSet()) { // check for a prefix if (name.startsWith(prefixName)) { // prefix found! Make sure what comes after it is actually a unit! final String prefixless = name.substring(prefixName.length()); if (this.containsUnitName(prefixless)) { // yep, it's a proper prefix! Get the unit! final Unit unit = this.getUnit(prefixless); final UnitPrefix prefix = this.getPrefix(prefixName); // Prefixes only work with linear and base units, so make sure it's one of those if (unit instanceof LinearUnit) { final LinearUnit linearUnit = (LinearUnit) unit; return linearUnit.times(prefix.getMultiplier()); } else if (unit instanceof BaseUnit) { final BaseUnit baseUnit = (BaseUnit) unit; return baseUnit.times(prefix.getMultiplier()); } } } } return this.units.get(name); } } /** * Uses the database's unit data to parse an expression into a unit *

* The expression is a series of any of the following: *

    *
  • The name of a unit, which multiplies or divides the result based on preceding operators
  • *
  • The operators '*' and '/', which multiply and divide (note that just putting two units or values next to each * other is equivalent to multiplication)
  • *
  • The operator '^' which exponentiates. Exponents must be integers.
  • *
  • A number which is multiplied or divided
  • *
* This method only works with linear units. * * @param expression * expression to parse * @throws IllegalArgumentException * if the expression cannot be parsed * @throws NullPointerException * if any argument is null * @since 2019-01-07 * @since v0.1.0 */ public Unit getUnitFromExpression(final String expression) { Objects.requireNonNull(expression, "expression must not be null."); // parse the expression // start with an "empty" unit then apply operations on it LinearUnit unit = SI.SI.getBaseUnit(UnitDimension.EMPTY); boolean dividing = false; // if I'm just creating an alias, just create one instead of going through the parsing process if (!expression.contains(" ") && !expression.contains("*") && !expression.contains("/") && !expression.contains("(") && !expression.contains(")") && !expression.contains("^")) { try { return SI.SI.getBaseUnit(UnitDimension.EMPTY).times(Double.parseDouble(expression)); } catch (final NumberFormatException e) { if (!this.containsUnitName(expression)) throw new IllegalArgumentException("Unrecognized unit name \"" + expression + "\"."); return this.getUnit(expression); } } // \\* means "asterisk", * is reserved for (final String part : expression.replaceAll("\\*", " \\* ").replaceAll("/", " / ").replaceAll(" \\^", "\\^") .replaceAll("\\^ ", "\\^").split(" ")) { if ("".equals(part)) { continue; } // "unit1 unit2" is the same as "unit1 * unit2", so multiplication signs do nothing if ("*".equals(part)) { continue; } // When I reach a division sign, don't parse a unit, instead tell myself I'm going to divide the next // thing if ("/".equals(part) || "per".equals(part)) { dividing = true; continue; } try { final double partAsNumber = Double.parseDouble(part); // if this works, it's a number // this code should not throw any exceptions, so I'm going to throw AssertionErrors if it does try { if (dividing) { unit = unit.dividedBy(partAsNumber); dividing = false; } else { unit = unit.times(partAsNumber); } } catch (final Exception e) { throw new AssertionError(e); } } catch (final NumberFormatException e) { // it's a unit, try that if (part.contains("(") && part.endsWith(")")) { // the unitsfile is looking for a nonlinear unit final String[] unitAndValue = part.split("\\("); // this will work because I've checked that it contains a ( final String unitName = unitAndValue[0]; final String valueStringWithBracket = unitAndValue[unitAndValue.length - 1]; final String valueString = valueStringWithBracket.substring(0, valueStringWithBracket.length() - 1); final double value; // try to get the value - else throw an error try { value = Double.parseDouble(valueString); } catch (final NumberFormatException e2) { throw new IllegalArgumentException("Unparseable value " + valueString); } // get this unit in a linear form if (!this.containsPrefixlessUnitName(unitName)) throw new IllegalArgumentException("Unrecognized unit name \"" + part + "\"."); final Unit partUnit = this.getPrefixlessUnit(unitName); final LinearUnit multiplier = partUnit.getBase().times(partUnit.convertToBase(value)); // finally, add it to the expression if (dividing) { unit = unit.dividedBy(multiplier); dividing = false; } else { unit = unit.times(multiplier); } } else { // check for exponientation if (part.contains("^")) { final String[] valueAndExponent = part.split("\\^"); // this will always work because of the contains check final String valueString = valueAndExponent[0]; final String exponentString = valueAndExponent[valueAndExponent.length - 1]; LinearUnit value; // first, try to get the value try { final double valueAsNumber = Double.parseDouble(valueString); value = SI.SI.getBaseUnit(UnitDimension.EMPTY).times(valueAsNumber); } catch (final NumberFormatException e2) { // look for a unit if (!this.containsUnitName(valueString)) throw new IllegalArgumentException("Unrecognized unit name \"" + valueString + "\"."); final Unit valueUnit = this.getUnit(valueString); // try to turn the value into a linear unit if (valueUnit instanceof LinearUnit) { value = (LinearUnit) valueUnit; } else throw new IllegalArgumentException("Only linear and base units can be exponientated."); } // now, try to get the exponent final int exponent; try { exponent = Integer.parseInt(exponentString); } catch (final NumberFormatException e2) { throw new IllegalArgumentException("Exponents must be integers."); } final LinearUnit exponientated = value.toExponent(exponent); if (dividing) { unit = unit.dividedBy(exponientated); dividing = false; } else { unit = unit.times(exponientated); } } else { // no exponent - look for a unit // the unitsfile is looking for a linear unit if (!this.containsUnitName(part)) throw new IllegalArgumentException("Unrecognized unit name \"" + part + "\"."); final Unit other = this.getUnit(part); if (other instanceof LinearUnit) { if (dividing) { unit = unit.dividedBy((LinearUnit) other); dividing = false; } else { unit = unit.times((LinearUnit) other); } } else throw new IllegalArgumentException( "Only linear or base units can be multiplied and divided. If you want to use a non-linear unit, try the format UNITNAME(VALUE)."); } } } } // replace conversion-factor-1 linear units with base units // this improves the autogenerated names, allowing them to use derived SI names if (unit != null && unit.getConversionFactor() == 1) return unit.getSystem().getBaseUnit(unit.getDimension()); else return unit; } /** * @return an immutable set of all of the unit names in this database, ignoring prefixes * @since 2019-01-14 * @since v0.1.0 */ public Set prefixlessUnitNameSet() { return Collections.unmodifiableSet(this.units.keySet()); } /** * @return an immutable set of all of the prefix names in this database * @since 2019-01-14 * @since v0.1.0 */ public Set prefixNameSet() { return Collections.unmodifiableSet(this.prefixes.keySet()); } }