/** * 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 sevenUnits.unit; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.math.BigDecimal; import java.nio.file.Files; import java.nio.file.Path; import java.util.AbstractSet; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.NoSuchElementException; import java.util.Objects; import java.util.Scanner; import java.util.Set; import java.util.function.BiFunction; import java.util.function.Function; import java.util.function.Predicate; import java.util.regex.Matcher; import java.util.regex.Pattern; import sevenUnits.utils.ConditionalExistenceCollections; import sevenUnits.utils.DecimalComparison; import sevenUnits.utils.ExpressionParser; import sevenUnits.utils.ObjectProduct; import sevenUnits.utils.UncertainDouble; /** * A database of units, prefixes and dimensions, and their names. * * @author Adrien Hopkins * @since 2019-01-07 * @since v0.1.0 */ public final class UnitDatabase { /** * A map for units that allows the use of prefixes. *

* As this map implementation is intended to be used as a sort of "augmented * view" of a unit and prefix map, it is unmodifiable but instead reflects * the changes to the maps passed into it. Do not edit this map, instead edit * the maps that were passed in during construction. *

*

* The rules for applying prefixes onto units are the following: *

*

*

* This 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}. *

*

* Because of ambiguities between prefixes (i.e. kilokilo = mega), * {@link #containsValue} and {@link #values()} currently ignore prefixes. *

* * @author Adrien Hopkins * @since 2019-04-13 * @since v0.2.0 */ private static final class PrefixedUnitMap implements Map { /** * The class used for entry sets. * *

* If the map that created this set is infinite in size (has at least one * unit and at least one prefix), this set is infinite as well. If this * set is infinite in size, {@link #toArray} will fail with a * {@code IllegalStateException} instead of creating an infinite-sized * array. *

* * @author Adrien Hopkins * @since 2019-04-13 * @since v0.2.0 */ private static final class PrefixedUnitEntrySet extends AbstractSet> { /** * The entry for this set. * * @author Adrien Hopkins * @since 2019-04-14 * @since v0.2.0 */ private static final class PrefixedUnitEntry implements Entry { private final String key; private final Unit value; /** * Creates the {@code PrefixedUnitEntry}. * * @param key key * @param value value * @since 2019-04-14 * @since v0.2.0 */ public PrefixedUnitEntry(final String key, final Unit value) { this.key = key; this.value = value; } /** * @since 2019-05-03 */ @Override public boolean equals(final Object o) { if (!(o instanceof Map.Entry)) return false; final Map.Entry other = (Map.Entry) o; 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 */ @Override public int hashCode() { return (this.getKey() == null ? 0 : this.getKey().hashCode()) ^ (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 * ({@code =}) character, then the string representation of the * value. * * @since 2019-05-03 */ @Override public String toString() { return this.getKey() + "=" + this.getValue(); } } /** * An iterator that iterates over the units of a * {@code PrefixedUnitNameSet}. * * @author Adrien Hopkins * @since 2019-04-14 * @since v0.2.0 */ private static final class PrefixedUnitEntryIterator implements Iterator> { // position in the unit list private int unitNamePosition = 0; // the indices of the prefixes attached to the current unit private final List prefixCoordinates = new ArrayList<>(); // values from the unit entry set private final Map map; private transient final List unitNames; private transient final List prefixNames; /** * Creates the * {@code UnitsDatabase.PrefixedUnitMap.PrefixedUnitNameSet.PrefixedUnitNameIterator}. * * @since 2019-04-14 * @since v0.2.0 */ public PrefixedUnitEntryIterator(final PrefixedUnitMap map) { this.map = map; this.unitNames = new ArrayList<>(map.units.keySet()); this.prefixNames = new ArrayList<>(map.prefixes.keySet()); } /** * @return current unit name * @since 2019-04-14 * @since v0.2.0 */ private String getCurrentUnitName() { final StringBuilder unitName = new StringBuilder(); for (final int i : this.prefixCoordinates) { unitName.append(this.prefixNames.get(i)); } unitName.append(this.unitNames.get(this.unitNamePosition)); return unitName.toString(); } @Override public boolean hasNext() { if (this.unitNames.isEmpty()) return false; else { if (this.prefixNames.isEmpty()) return this.unitNamePosition >= this.unitNames.size() - 1; else return true; } } /** * Changes this iterator's position to the next available one. * * @since 2019-04-14 * @since v0.2.0 */ 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); } else { // get the prefix coordinate to increment, then increment 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()) { // carry over 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 this.prefixCoordinates.set(i, this.prefixCoordinates.get(i) + 1); } } } } } @Override public Entry next() { // get next element final Entry nextEntry = this.peek(); // iterate to next position this.incrementPosition(); return nextEntry; } /** * @return the next element in the iterator, without iterating over * it * @since 2019-05-03 */ private Entry 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 if (!this.prefixCoordinates.isEmpty()) { while (this.unitNamePosition < this.unitNames.size() && !(this.map.get(this.unitNames.get( this.unitNamePosition)) instanceof LinearUnit)) { 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. * * @since 2019-05-03 */ @Override public String toString() { return String.format( "Iterator iterating over name-unit entries; next value is \"%s\"", this.peek()); } } // the map that created this set private final PrefixedUnitMap map; /** * Creates the {@code PrefixedUnitNameSet}. * * @param map map that created this set * @since 2019-04-13 * @since v0.2.0 */ public PrefixedUnitEntrySet(final PrefixedUnitMap map) { this.map = map; } @Override public boolean add(final Map.Entry e) { throw new UnsupportedOperationException( "Cannot add to an immutable set"); } @Override public boolean addAll( final Collection> 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 entry; try { // This is OK because I'm in a try-catch block, catching the // exact exception that would be thrown. @SuppressWarnings("unchecked") final Entry tempEntry = (Entry) o; entry = tempEntry; } catch (final ClassCastException e) { 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) if (!this.contains(o)) return false; return true; } @Override public boolean isEmpty() { return this.map.isEmpty(); } @Override public Iterator> 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> 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()) return 0; else { if (this.map.prefixes.isEmpty()) return this.map.units.size(); else // infinite set return Integer.MAX_VALUE; } } /** * @throws IllegalStateException if the set is infinite in size */ @Override public Object[] toArray() { if (this.map.units.isEmpty() || this.map.prefixes.isEmpty()) return super.toArray(); else // infinite set throw new IllegalStateException( "Cannot make an infinite set into an array."); } /** * @throws IllegalStateException if the set is infinite in size */ @Override public T[] toArray(final T[] a) { if (this.map.units.isEmpty() || this.map.prefixes.isEmpty()) return super.toArray(a); else // infinite set throw new IllegalStateException( "Cannot make an infinite set into an array."); } @Override public String toString() { if (this.map.units.isEmpty() || this.map.prefixes.isEmpty()) return super.toString(); else return String.format( "Infinite set of name-unit entries created from units %s and prefixes %s", this.map.units, this.map.prefixes); } } /** * The class used for unit name sets. * *

* If the map that created this set is infinite in size (has at least one * unit and at least one prefix), this set is infinite as well. If this * set is infinite in size, {@link #toArray} will fail with a * {@code IllegalStateException} instead of creating an infinite-sized * array. *

* * @author Adrien Hopkins * @since 2019-04-13 * @since v0.2.0 */ private static final class PrefixedUnitNameSet extends AbstractSet { /** * An iterator that iterates over the units of a * {@code PrefixedUnitNameSet}. * * @author Adrien Hopkins * @since 2019-04-14 * @since v0.2.0 */ private static final class PrefixedUnitNameIterator implements Iterator { // position in the unit list private int unitNamePosition = 0; // the indices of the prefixes attached to the current unit private final List prefixCoordinates = new ArrayList<>(); // values from the unit name set private final Map map; private transient final List unitNames; private transient final List prefixNames; /** * Creates the * {@code UnitsDatabase.PrefixedUnitMap.PrefixedUnitNameSet.PrefixedUnitNameIterator}. * * @since 2019-04-14 * @since v0.2.0 */ public PrefixedUnitNameIterator(final PrefixedUnitMap map) { this.map = map; this.unitNames = new ArrayList<>(map.units.keySet()); this.prefixNames = new ArrayList<>(map.prefixes.keySet()); } /** * @return current unit name * @since 2019-04-14 * @since v0.2.0 */ private String getCurrentUnitName() { final StringBuilder unitName = new StringBuilder(); for (final int i : this.prefixCoordinates) { unitName.append(this.prefixNames.get(i)); } unitName.append(this.unitNames.get(this.unitNamePosition)); return unitName.toString(); } @Override public boolean hasNext() { if (this.unitNames.isEmpty()) return false; else { if (this.prefixNames.isEmpty()) return this.unitNamePosition >= this.unitNames.size() - 1; else return true; } } /** * Changes this iterator's position to the next available one. * * @since 2019-04-14 * @since v0.2.0 */ 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); } else { // get the prefix coordinate to increment, then increment 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()) { // carry over 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 this.prefixCoordinates.set(i, this.prefixCoordinates.get(i) + 1); } } } } } @Override public String next() { final String nextName = this.peek(); this.incrementPosition(); return nextName; } /** * @return the next element in the iterator, without iterating over * it * @since 2019-05-03 */ private String 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 if (!this.prefixCoordinates.isEmpty()) { while (this.unitNamePosition < this.unitNames.size() && !(this.map.get(this.unitNames.get( this.unitNamePosition)) instanceof LinearUnit)) { 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. * * @since 2019-05-03 */ @Override public String toString() { return String.format( "Iterator iterating over unit names; next value is \"%s\"", this.peek()); } } // the map that created this set private final PrefixedUnitMap map; /** * Creates the {@code PrefixedUnitNameSet}. * * @param map map that created this set * @since 2019-04-13 * @since v0.2.0 */ 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 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) if (!this.contains(o)) return false; return true; } @Override public boolean isEmpty() { return this.map.isEmpty(); } @Override public Iterator 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 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()) return 0; else { if (this.map.prefixes.isEmpty()) return this.map.units.size(); else // infinite set return Integer.MAX_VALUE; } } /** * @throws IllegalStateException if the set is infinite in size */ @Override public Object[] toArray() { if (this.map.units.isEmpty() || this.map.prefixes.isEmpty()) return super.toArray(); else // infinite set throw new IllegalStateException( "Cannot make an infinite set into an array."); } /** * @throws IllegalStateException if the set is infinite in size */ @Override public T[] toArray(final T[] a) { if (this.map.units.isEmpty() || this.map.prefixes.isEmpty()) return super.toArray(a); else // infinite set throw new IllegalStateException( "Cannot make an infinite set into an array."); } @Override public String toString() { if (this.map.units.isEmpty() || this.map.prefixes.isEmpty()) return super.toString(); else return String.format( "Infinite set of name-unit entries created from units %s and prefixes %s", this.map.units, this.map.prefixes); } } /** * The units stored in this collection, without prefixes. * * @since 2019-04-13 * @since v0.2.0 */ private final Map units; /** * The available prefixes for use. * * @since 2019-04-13 * @since v0.2.0 */ private final Map prefixes; // caches private transient Collection values = null; private transient Set keySet = null; private transient Set> entrySet = null; /** * Creates the {@code PrefixedUnitMap}. * * @param units map mapping unit names to units * @param prefixes map mapping prefix names to prefixes * @since 2019-04-13 * @since v0.2.0 */ public PrefixedUnitMap(final Map units, final Map prefixes) { // I am making unmodifiable maps to ensure I don't accidentally make // changes. 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 remappingFunction) { throw new UnsupportedOperationException( "Cannot edit an immutable map"); } @Override public Unit computeIfAbsent(final String key, final Function mappingFunction) { throw new UnsupportedOperationException( "Cannot edit an immutable map"); } @Override public Unit computeIfPresent(final String key, final BiFunction 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) // - it is longer than the existing largest prefix (since I am // looking for the longest valid prefix) // - the part after the prefix is a valid unit name // - the unit described that name is a linear unit (since only // linear units can have prefixes) if (unitName.startsWith(prefixName) && prefixName.length() > longestLength) { final String rest = unitName.substring(prefixName.length()); if (this.containsKey(rest) && this.get(rest) instanceof LinearUnit) { longestPrefix = prefixName; longestLength = prefixName.length(); } } } return longestPrefix != null; } /** * {@inheritDoc} * *

* Because of ambiguities between prefixes (i.e. kilokilo = mega), this * method only tests for prefixless units. *

*/ @Override public boolean containsValue(final Object value) { return this.units.containsValue(value); } @Override public Set> entrySet() { if (this.entrySet == null) { this.entrySet = new PrefixedUnitEntrySet(this); } 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) // - it is longer than the existing largest prefix (since I am // looking for the longest valid prefix) // - the part after the prefix is a valid unit name // - the unit described that name is a linear unit (since only // linear units can have prefixes) if (unitName.startsWith(prefixName) && prefixName.length() > longestLength) { final String rest = unitName.substring(prefixName.length()); if (this.containsKey(rest) && this.get(rest) instanceof LinearUnit) { longestPrefix = prefixName; longestLength = prefixName.length(); } } } // if none found, returns null if (longestPrefix == null) return null; else { // get necessary data final String rest = unitName.substring(longestLength); // this cast will not fail because I verified that it would work // 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 keySet() { if (this.keySet == null) { this.keySet = new PrefixedUnitNameSet(this); } return this.keySet; } @Override public Unit merge(final String key, final Unit value, final BiFunction 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 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 function) { throw new UnsupportedOperationException( "Cannot replace entries in an immutable map"); } @Override public int size() { if (this.units.isEmpty()) return 0; else { if (this.prefixes.isEmpty()) return this.units.size(); else // infinite set return Integer.MAX_VALUE; } } @Override public String toString() { if (this.units.isEmpty() || this.prefixes.isEmpty()) return super.toString(); else return String.format( "Infinite map of name-unit entries created from units %s and prefixes %s", this.units, this.prefixes); } /** * {@inheritDoc} * *

* Because of ambiguities between prefixes (i.e. kilokilo = mega), this * method ignores prefixes. *

*/ @Override public Collection values() { if (this.values == null) { this.values = Collections .unmodifiableCollection(this.units.values()); } return this.values; } } /** * Replacements done to *all* expression types */ private static final Map 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 lengthFirstComparator = Comparator .comparingInt(String::length).thenComparing(Comparator.naturalOrder()); /** * The exponent operator * * @param base base of exponentiation * @param exponentUnit exponent * @return result * @since 2019-04-10 * @since v0.2.0 */ private static final LinearUnit exponentiateUnits(final LinearUnit base, final LinearUnit exponentUnit) { // exponent function - first check if o2 is a number, if (exponentUnit.getBase().equals(Metric.ONE.getBase())) { // then check if it is an integer, final double exponent = exponentUnit.getConversionFactor(); if (DecimalComparison.equals(exponent % 1, 0)) // then exponentiate return base.toExponent((int) (exponent + 0.5)); else // not an integer throw new UnsupportedOperationException( "Decimal exponents are currently not supported."); } else // not a number throw new IllegalArgumentException("Exponents must be numbers."); } /** * The exponent operator * * @param base base of exponentiation * @param exponentUnit exponent * @return result * @since 2020-08-04 */ private static final LinearUnitValue exponentiateUnitValues( final LinearUnitValue base, final LinearUnitValue exponentValue) { // exponent function - first check if o2 is a number, if (exponentValue.canConvertTo(Metric.ONE)) { // then check if it is an integer, final double exponent = exponentValue.getValueExact(); if (DecimalComparison.equals(exponent % 1, 0)) // then exponentiate return base.toExponent((int) (exponent + 0.5)); else // not an integer throw new UnsupportedOperationException( "Decimal exponents are currently not supported."); } else // not a number throw new IllegalArgumentException("Exponents must be numbers."); } /** * @return true if entry represents a removable duplicate entry of unitMap. * @since 2021-05-22 */ private static boolean isRemovableDuplicate(Map unitMap, Entry entry) { for (final Entry e : unitMap.entrySet()) { final String name = e.getKey(); final Unit value = e.getValue(); if (lengthFirstComparator.compare(entry.getKey(), name) < 0 && Objects.equals(unitMap.get(entry.getKey()), value)) return true; } return false; } /** * The units in this system, excluding prefixes. * * @since 2019-01-07 * @since v0.1.0 */ private final Map prefixlessUnits; /** * 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 * @since v0.2.0 */ private final Map> dimensions; /** * A map mapping strings to units (including prefixes) * * @since 2019-04-13 * @since v0.2.0 */ private final Map 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); // 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("!")) { 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(Metric.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 Metric.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 dimensions from a {@code InputStream}. Otherwise, works like * {@link #loadDimensionFile}. * * @param stream stream to load from * @since 2021-03-27 */ public void loadDimensionsFromStream(final InputStream stream) { try (final Scanner scanner = new Scanner(stream)) { long lineCounter = 0; while (scanner.hasNextLine()) { this.addDimensionFromLine(scanner.nextLine(), ++lineCounter); } } } /** * 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); } } /** * Adds all units from a {@code InputStream}. Otherwise, works like * {@link #loadUnitsFile}. * * @param stream stream to load from * @since 2021-03-27 */ public void loadUnitsFromStream(InputStream stream) { try (final Scanner scanner = new Scanner(stream)) { long lineCounter = 0; while (scanner.hasNextLine()) { this.addUnitOrPrefixFromLine(scanner.nextLine(), ++lineCounter); } } } /** * @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. } /** * @param includeDuplicates if true, duplicate units will all exist in the * map; if false, only one of each unit will exist, * even if the names are different * @return a map mapping unit names to units, ignoring prefixes * @since 2019-04-13 * @since v0.2.0 */ public Map unitMapPrefixless(boolean includeDuplicates) { if (includeDuplicates) return Collections.unmodifiableMap(this.prefixlessUnits); else return Collections.unmodifiableMap(ConditionalExistenceCollections .conditionalExistenceMap(this.prefixlessUnits, entry -> !isRemovableDuplicate(this.prefixlessUnits, entry))); } }