units;
+
+ /**
+ * The rule that specifies when prefix repetition is allowed. It takes in one
+ * argument: a list of the prefixes being applied to the unit
+ *
+ * The prefixes are inputted in application order. This means that
+ * testing whether "kilomegagigametre" is a valid unit is equivalent to
+ * running the following code (assuming all variables are defined correctly):
+ *
+ * {@code prefixRepetitionRule.test(Arrays.asList(giga, mega, kilo))}
+ */
+ private Predicate> prefixRepetitionRule;
+
+ /**
+ * A parser that can parse unit expressions.
+ *
+ * @since 2019-03-22
+ * @since v0.2.0
+ */
+ private final ExpressionParser 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("^", UnitDatabase::exponentiateUnits, 2)
+ .build();
+
+ /**
+ * A parser that can parse unit value expressions.
+ *
+ * @since 2020-08-04
+ */
+ private final ExpressionParser 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("^", UnitDatabase::exponentiateUnitValues, 2)
+ .build();
+
+ /**
+ * A parser that can parse unit prefix expressions
+ *
+ * @since 2019-04-13
+ * @since v0.2.0
+ */
+ private final ExpressionParser prefixExpressionParser = new ExpressionParser.Builder<>(
+ this::getPrefix).addBinaryOperator("*", (o1, o2) -> o1.times(o2), 0)
+ .addSpaceFunction("*")
+ .addBinaryOperator("/", (o1, o2) -> o1.dividedBy(o2), 0)
+ .addBinaryOperator("^",
+ (o1, o2) -> o1.toExponent(o2.getMultiplier()), 1)
+ .build();
+
+ /**
+ * A parser that can parse unit dimension expressions.
+ *
+ * @since 2019-04-13
+ * @since v0.2.0
+ */
+ private final ExpressionParser> unitDimensionParser = new ExpressionParser.Builder<>(
+ this::getDimension).addBinaryOperator("*", (o1, o2) -> o1.times(o2), 0)
+ .addSpaceFunction("*")
+ .addBinaryOperator("/", (o1, o2) -> o1.dividedBy(o2), 0).build();
+
+ /**
+ * Creates the {@code UnitsDatabase}.
+ *
+ * @since 2019-01-10
+ * @since v0.1.0
+ */
+ public UnitDatabase() {
+ this(prefixes -> true);
+ }
+
+ /**
+ * Creates the {@code UnitsDatabase}
+ *
+ * @param prefixRepetitionRule the rule that determines when prefix
+ * repetition is allowed
+ * @since 2020-08-26
+ */
+ public UnitDatabase(Predicate> prefixRepetitionRule) {
+ this.prefixlessUnits = new HashMap<>();
+ this.prefixes = new HashMap<>();
+ this.dimensions = new HashMap<>();
+ this.prefixRepetitionRule = prefixRepetitionRule;
+ this.units = ConditionalExistenceCollections.conditionalExistenceMap(
+ new PrefixedUnitMap(this.prefixlessUnits, this.prefixes),
+ entry -> this.prefixRepetitionRule
+ .test(this.getPrefixesFromName(entry.getKey())));
+ }
+
+ /**
+ * 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
+ * @since v0.2.0
+ */
+ public void addDimension(final String name,
+ final ObjectProduct dimension) {
+ this.dimensions.put(
+ Objects.requireNonNull(name, "name must not be null."),
+ Objects.requireNonNull(dimension, "dimension must not be null."));
+ }
+
+ /**
+ * Adds to the list from a line in a unit dimension file.
+ *
+ * @param line line to look at
+ * @param lineCounter number of line, for error messages
+ * @since 2019-04-10
+ * @since v0.2.0
+ */
+ private void addDimensionFromLine(final String line,
+ final long lineCounter) {
+ // ignore lines that start with a # sign - they're comments
+ if (line.isEmpty())
+ return;
+ if (line.contains("#")) {
+ this.addDimensionFromLine(line.substring(0, line.indexOf("#")),
+ lineCounter);
+ return;
+ }
+
+ // divide line into name and expression
+ final Matcher lineMatcher = NAME_EXPRESSION.matcher(line);
+ if (!lineMatcher.matches())
+ throw new IllegalArgumentException(String.format(
+ "Error at line %d: Lines of a dimension file must consist of a dimension name, then spaces or tabs, then a dimension expression.",
+ 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("!")) {
+ if (!this.containsDimensionName(name))
+ throw new IllegalArgumentException(String.format(
+ "! used but no dimension found (line %d).", lineCounter));
+ } else {
+ // it's a unit, get the unit
+ final ObjectProduct dimension;
+ try {
+ dimension = this.getDimensionFromExpression(expression);
+ } catch (final IllegalArgumentException e) {
+ System.err.printf("Parsing error on line %d:%n", lineCounter);
+ throw e;
+ }
+
+ this.addDimension(name, dimension);
+ }
+ }
+
+ /**
+ * 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.prefixlessUnits.put(
+ Objects.requireNonNull(name, "name must not be null."),
+ Objects.requireNonNull(unit, "unit must not be null."));
+ }
+
+ /**
+ * Adds to the list from a line in a unit file.
+ *
+ * @param line line to look at
+ * @param lineCounter number of line, for error messages
+ * @since 2019-04-10
+ * @since v0.2.0
+ */
+ private void addUnitOrPrefixFromLine(final String line,
+ final long lineCounter) {
+ // ignore lines that start with a # sign - they're comments
+ if (line.isEmpty())
+ return;
+ if (line.contains("#")) {
+ this.addUnitOrPrefixFromLine(line.substring(0, line.indexOf("#")),
+ lineCounter);
+ return;
+ }
+
+ // divide line into name and expression
+ final Matcher lineMatcher = NAME_EXPRESSION.matcher(line);
+ if (!lineMatcher.matches())
+ throw new IllegalArgumentException(String.format(
+ "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);
+
+ 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;
+ }
+
+ this.addUnit(name, unit);
+ }
+ }
+ }
+
+ /**
+ * 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
+ * @since v0.2.0
+ */
+ public boolean containsDimensionName(final String name) {
+ return this.dimensions.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) {
+ return this.units.containsKey(name);
+ }
+
+ /**
+ * @return a map mapping dimension names to dimensions
+ * @since 2019-04-13
+ * @since v0.2.0
+ */
+ public Map> dimensionMap() {
+ return Collections.unmodifiableMap(this.dimensions);
+ }
+
+ /**
+ * Evaluates a unit expression, following the same rules as
+ * {@link #getUnitFromExpression}.
+ *
+ * @param expression expression to parse
+ * @return {@code LinearUnitValue} representing value of expression
+ * @since 2020-08-04
+ */
+ 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 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 operaton 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);
+ }
+
+ /**
+ * Gets a unit dimension from the database using its name.
+ *
+ *
+ * This method accepts exponents, like "L^3"
+ *
+ *
+ * @param name dimension's name
+ * @return dimension
+ * @since 2019-03-14
+ * @since v0.2.0
+ */
+ public ObjectProduct getDimension(final String name) {
+ Objects.requireNonNull(name, "name must not be null.");
+ if (name.contains("^")) {
+ final String[] baseAndExponent = name.split("\\^");
+
+ final ObjectProduct base = this
+ .getDimension(baseAndExponent[0]);
+
+ final int exponent;
+ try {
+ exponent = Integer
+ .parseInt(baseAndExponent[baseAndExponent.length - 1]);
+ } catch (final NumberFormatException e2) {
+ throw new IllegalArgumentException("Exponent must be an integer.");
+ }
+
+ return base.toExponent(exponent);
+ }
+ return this.dimensions.get(name);
+ }
+
+ /**
+ * Uses the database's data to parse an expression into a unit dimension
+ *
+ * The expression is a series of any of the following:
+ *
+ * - The name of a unit dimension, which multiplies or divides the result
+ * based on preceding operators
+ * - The operators '*' and '/', which multiply and divide (note that just
+ * putting two unit dimensions next to each other is equivalent to
+ * multiplication)
+ * - The operator '^' which exponentiates. Exponents must be integers.
+ *
+ *
+ * @param expression expression to parse
+ * @throws IllegalArgumentException if the expression cannot be parsed
+ * @throws NullPointerException if expression is null
+ * @since 2019-04-13
+ * @since v0.2.0
+ */
+ public ObjectProduct 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 replacement : EXPRESSION_REPLACEMENTS
+ .entrySet()) {
+ modifiedExpression = replacement.getKey().matcher(modifiedExpression)
+ .replaceAll(replacement.getValue());
+ }
+ modifiedExpression = modifiedExpression.replaceAll(" *\\^ *", "\\^");
+
+ return this.unitDimensionParser.parseExpression(modifiedExpression);
+ }
+
+ /**
+ * Gets a unit. If it is linear, cast it to a LinearUnit and return it.
+ * Otherwise, throw an {@code IllegalArgumentException}.
+ *
+ * @param name unit's name
+ * @return unit
+ * @since 2019-03-22
+ * @since v0.2.0
+ */
+ private LinearUnit getLinearUnit(final String name) {
+ // see if I am using a function-unit like tempC(100)
+ Objects.requireNonNull(name, "name may not be null");
+ if (name.contains("(") && name.contains(")")) {
+ // break it into function name and value
+ final List parts = Arrays.asList(name.split("\\("));
+ 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(
+ parts.get(1).substring(0, parts.get(1).length() - 1));
+ return LinearUnit.fromUnitValue(unit, value);
+ } else {
+ // get a linear unit
+ final Unit unit = this.getUnit(name);
+
+ if (unit instanceof LinearUnit)
+ return (LinearUnit) unit;
+ else
+ throw new IllegalArgumentException(
+ 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.
+ *
+ * @param name name of unit
+ * @return {@code LinearUnitValue} instance
+ * @since 2020-08-04
+ */
+ private LinearUnitValue getLinearUnitValue(final String name) {
+ try {
+ // try to parse it as a number - otherwise it is not a number!
+ final BigDecimal number = new BigDecimal(name);
+
+ final double uncertainty = Math.pow(10, -number.scale());
+ return LinearUnitValue.of(SI.ONE,
+ UncertainDouble.of(number.doubleValue(), uncertainty));
+ } catch (final NumberFormatException e) {
+ return LinearUnitValue.getExact(this.getLinearUnit(name), 1);
+ }
+ }
+
+ /**
+ * 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) {
+ try {
+ return UnitPrefix.valueOf(Double.parseDouble(name));
+ } catch (final NumberFormatException e) {
+ return this.prefixes.get(name);
+ }
+ }
+
+ /**
+ * Gets all of the prefixes that are on a unit name, in application order.
+ *
+ * @param unitName name of unit
+ * @return prefixes
+ * @since 2020-08-26
+ */
+ List getPrefixesFromName(final String unitName) {
+ final List 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)
+ throw new AssertionError(
+ "No prefix found in " + name + ", but it is not a unit!");
+ if (this.prefixes.containsKey(name.substring(0, longestLength))) {
+ longestPrefixName = name.substring(0, longestLength);
+ }
+ }
+
+ // longest prefix found!
+ final UnitPrefix prefix = this.getPrefix(longestPrefixName);
+ prefixes.add(0, prefix);
+ name = name.substring(longestLength);
+ }
+ return prefixes;
+ }
+
+ /**
+ * 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.");
+
+ // 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 replacement : EXPRESSION_REPLACEMENTS
+ .entrySet()) {
+ modifiedExpression = replacement.getKey().matcher(modifiedExpression)
+ .replaceAll(replacement.getValue());
+ }
+
+ return this.prefixExpressionParser.parseExpression(modifiedExpression);
+ }
+
+ /**
+ * @return the prefixRepetitionRule
+ * @since 2020-08-26
+ */
+ public final Predicate> getPrefixRepetitionRule() {
+ return this.prefixRepetitionRule;
+ }
+
+ /**
+ * 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) {
+ try {
+ final double value = Double.parseDouble(name);
+ return SI.ONE.times(value);
+ } catch (final NumberFormatException e) {
+ final Unit unit = this.units.get(name);
+ if (unit == null)
+ throw new NoSuchElementException("No unit " + name);
+ else if (unit.getPrimaryName().isEmpty())
+ return unit.withName(NameSymbol.ofName(name));
+ else if (!unit.getPrimaryName().get().equals(name)) {
+ final Set otherNames = new HashSet<>(unit.getOtherNames());
+ otherNames.add(unit.getPrimaryName().get());
+ return unit.withName(NameSymbol.ofNullable(name,
+ unit.getSymbol().orElse(null), otherNames));
+ } else if (!unit.getOtherNames().contains(name)) {
+ final Set otherNames = new HashSet<>(unit.getOtherNames());
+ otherNames.add(name);
+ return unit.withName(
+ NameSymbol.ofNullable(unit.getPrimaryName().orElse(null),
+ unit.getSymbol().orElse(null), otherNames));
+ } else
+ return unit;
+ }
+
+ }
+
+ /**
+ * 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 expression is null
+ * @since 2019-01-07
+ * @since v0.1.0
+ */
+ 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 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);
+ }
+
+ /**
+ * Adds all dimensions 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 getDimensionFromExpression) separated by any number of tab characters.
+ *
+ *
+ * Allowed exceptions:
+ *
+ * - Anything after a '#' 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 loadDimensionFile(final Path file) {
+ Objects.requireNonNull(file, "file must not be null.");
+ try {
+ long lineCounter = 0;
+ for (final String line : Files.readAllLines(file)) {
+ this.addDimensionFromLine(line, ++lineCounter);
+ }
+ } 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 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:
+ *
+ * - Anything after a '#' 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 loadUnitsFile(final Path file) {
+ Objects.requireNonNull(file, "file must not be null.");
+ try {
+ long lineCounter = 0;
+ for (final String line : Files.readAllLines(file)) {
+ this.addUnitOrPrefixFromLine(line, ++lineCounter);
+ }
+ } 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);
+ }
+ }
+
+ /**
+ * @return a map mapping prefix names to prefixes
+ * @since 2019-04-13
+ * @since v0.2.0
+ */
+ public Map prefixMap() {
+ return Collections.unmodifiableMap(this.prefixes);
+ }
+
+ /**
+ * @param prefixRepetitionRule the prefixRepetitionRule to set
+ * @since 2020-08-26
+ */
+ public final void setPrefixRepetitionRule(
+ Predicate> prefixRepetitionRule) {
+ this.prefixRepetitionRule = prefixRepetitionRule;
+ }
+
+ /**
+ * @return a string stating the number of units, prefixes and dimensions in
+ * the database
+ */
+ @Override
+ public String toString() {
+ return String.format(
+ "Unit Database with %d units, %d unit prefixes and %d dimensions",
+ this.prefixlessUnits.size(), this.prefixes.size(),
+ this.dimensions.size());
+ }
+
+ /**
+ * Returns a map mapping unit names to units, including units with prefixes.
+ *
+ * The returned map is infinite in size if there is at least one unit and at
+ * least one prefix. If it is infinite, some operations that only work with
+ * finite collections, like converting name/entry sets to arrays, will throw
+ * an {@code IllegalStateException}.
+ *
+ *
+ * Specifically, the operations that will throw an IllegalStateException if
+ * the map is infinite in size are:
+ *
+ * - {@code unitMap.entrySet().toArray()} (either overloading)
+ * - {@code unitMap.keySet().toArray()} (either overloading)
+ *
+ *
+ *
+ * Because of ambiguities between prefixes (i.e. kilokilo = mega), the map's
+ * {@link PrefixedUnitMap#containsValue containsValue} and
+ * {@link PrefixedUnitMap#values() values()} methods currently ignore
+ * prefixes.
+ *
+ *
+ * @return a map mapping unit names to units, including prefixed names
+ * @since 2019-04-13
+ * @since v0.2.0
+ */
+ public Map unitMap() {
+ return this.units; // PrefixedUnitMap is immutable so I don't need to make
+ // an unmodifiable map.
+ }
+
+ /**
+ * @return a map mapping unit names to units, ignoring prefixes
+ * @since 2019-04-13
+ * @since v0.2.0
+ */
+ public Map unitMapPrefixless() {
+ return Collections.unmodifiableMap(this.prefixlessUnits);
+ }
+}
--
cgit v1.2.3