diff options
Diffstat (limited to 'src/main/java/org/unitConverter/unit/UnitDatabase.java')
-rw-r--r-- | src/main/java/org/unitConverter/unit/UnitDatabase.java | 2058 |
1 files changed, 0 insertions, 2058 deletions
diff --git a/src/main/java/org/unitConverter/unit/UnitDatabase.java b/src/main/java/org/unitConverter/unit/UnitDatabase.java deleted file mode 100644 index 673f119..0000000 --- a/src/main/java/org/unitConverter/unit/UnitDatabase.java +++ /dev/null @@ -1,2058 +0,0 @@ -/** - * 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 <https://www.gnu.org/licenses/>. - */ -package org.unitConverter.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 org.unitConverter.math.ConditionalExistenceCollections; -import org.unitConverter.math.DecimalComparison; -import org.unitConverter.math.ExpressionParser; -import org.unitConverter.math.ObjectProduct; -import org.unitConverter.math.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. - * <p> - * 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. - * </p> - * <p> - * The rules for applying prefixes onto units are the following: - * <ul> - * <li>Prefixes can only be applied to linear units.</li> - * <li>Before attempting to search for prefixes in a unit name, this map will - * first search for a unit name. So, if there are two units, "B" and "AB", - * and a prefix "A", this map will favour the unit "AB" over the unit "B" - * with the prefix "A", even though they have the same string.</li> - * <li>Longer prefixes are preferred to shorter prefixes. So, if you have - * units "BC" and "C", and prefixes "AB" and "A", inputting "ABC" will return - * the unit "C" with the prefix "AB", not "BC" with the prefix "A".</li> - * </ul> - * </p> - * <p> - * 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}. - * </p> - * <p> - * Because of ambiguities between prefixes (i.e. kilokilo = mega), - * {@link #containsValue} and {@link #values()} currently ignore prefixes. - * </p> - * - * @author Adrien Hopkins - * @since 2019-04-13 - * @since v0.2.0 - */ - private static final class PrefixedUnitMap implements Map<String, Unit> { - /** - * The class used for entry sets. - * - * <p> - * 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. - * </p> - * - * @author Adrien Hopkins - * @since 2019-04-13 - * @since v0.2.0 - */ - private static final class PrefixedUnitEntrySet - extends AbstractSet<Map.Entry<String, Unit>> { - /** - * The entry for this set. - * - * @author Adrien Hopkins - * @since 2019-04-14 - * @since v0.2.0 - */ - private static final class PrefixedUnitEntry - implements Entry<String, Unit> { - 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<Entry<String, Unit>> { - // position in the unit list - private int unitNamePosition = 0; - // the indices of the prefixes attached to the current unit - private final List<Integer> prefixCoordinates = new ArrayList<>(); - - // values from the unit entry set - private final Map<String, Unit> map; - private transient final List<String> unitNames; - private transient final List<String> prefixNames; - - /** - * Creates the - * {@code UnitsDatabase.PrefixedUnitMap.PrefixedUnitNameSet.PrefixedUnitNameIterator}. - * - * @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<String, Unit> next() { - // get next element - final Entry<String, Unit> nextEntry = this.peek(); - - // iterate to next position - this.incrementPosition(); - - return nextEntry; - } - - /** - * @return the next element in the iterator, without iterating over - * it - * @since 2019-05-03 - */ - private Entry<String, Unit> peek() { - if (!this.hasNext()) - throw new NoSuchElementException("No units left!"); - - // if I have prefixes, ensure I'm not using a nonlinear unit - // since all of the unprefixed stuff is done, just remove - // nonlinear units - 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<String, Unit> e) { - throw new UnsupportedOperationException( - "Cannot add to an immutable set"); - } - - @Override - public boolean addAll( - final Collection<? extends Map.Entry<String, Unit>> c) { - throw new UnsupportedOperationException( - "Cannot add to an immutable set"); - } - - @Override - public void clear() { - throw new UnsupportedOperationException( - "Cannot clear an immutable set"); - } - - @Override - public boolean contains(final Object o) { - // get the entry - final Entry<String, Unit> entry; - - try { - // This is OK because I'm in a try-catch block, catching the - // exact exception that would be thrown. - @SuppressWarnings("unchecked") - final Entry<String, Unit> tempEntry = (Entry<String, Unit>) 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<Entry<String, Unit>> iterator() { - return new PrefixedUnitEntryIterator(this.map); - } - - @Override - public boolean remove(final Object o) { - throw new UnsupportedOperationException( - "Cannot remove from an immutable set"); - } - - @Override - public boolean removeAll(final Collection<?> c) { - throw new UnsupportedOperationException( - "Cannot remove from an immutable set"); - } - - @Override - public boolean removeIf( - final Predicate<? super Entry<String, Unit>> filter) { - throw new UnsupportedOperationException( - "Cannot remove from an immutable set"); - } - - @Override - public boolean retainAll(final Collection<?> c) { - throw new UnsupportedOperationException( - "Cannot remove from an immutable set"); - } - - @Override - public int size() { - if (this.map.units.isEmpty()) - 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> 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. - * - * <p> - * 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. - * </p> - * - * @author Adrien Hopkins - * @since 2019-04-13 - * @since v0.2.0 - */ - private static final class PrefixedUnitNameSet - extends AbstractSet<String> { - /** - * 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<String> { - // position in the unit list - private int unitNamePosition = 0; - // the indices of the prefixes attached to the current unit - private final List<Integer> prefixCoordinates = new ArrayList<>(); - - // values from the unit name set - private final Map<String, Unit> map; - private transient final List<String> unitNames; - private transient final List<String> prefixNames; - - /** - * Creates the - * {@code UnitsDatabase.PrefixedUnitMap.PrefixedUnitNameSet.PrefixedUnitNameIterator}. - * - * @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<? extends String> c) { - throw new UnsupportedOperationException( - "Cannot add to an immutable set"); - } - - @Override - public void clear() { - throw new UnsupportedOperationException( - "Cannot clear an immutable set"); - } - - @Override - public boolean contains(final Object o) { - return this.map.containsKey(o); - } - - @Override - public boolean containsAll(final Collection<?> c) { - for (final Object o : c) - if (!this.contains(o)) - return false; - return true; - } - - @Override - public boolean isEmpty() { - return this.map.isEmpty(); - } - - @Override - public Iterator<String> iterator() { - return new PrefixedUnitNameIterator(this.map); - } - - @Override - public boolean remove(final Object o) { - throw new UnsupportedOperationException( - "Cannot remove from an immutable set"); - } - - @Override - public boolean removeAll(final Collection<?> c) { - throw new UnsupportedOperationException( - "Cannot remove from an immutable set"); - } - - @Override - public boolean removeIf(final Predicate<? super String> filter) { - throw new UnsupportedOperationException( - "Cannot remove from an immutable set"); - } - - @Override - public boolean retainAll(final Collection<?> c) { - throw new UnsupportedOperationException( - "Cannot remove from an immutable set"); - } - - @Override - public int size() { - if (this.map.units.isEmpty()) - 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> 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<String, Unit> units; - - /** - * The available prefixes for use. - * - * @since 2019-04-13 - * @since v0.2.0 - */ - private final Map<String, UnitPrefix> prefixes; - - // caches - private transient Collection<Unit> values = null; - private transient Set<String> keySet = null; - private transient Set<Entry<String, Unit>> entrySet = null; - - /** - * Creates the {@code PrefixedUnitMap}. - * - * @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<String, Unit> units, - final Map<String, UnitPrefix> 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<? super String, ? super Unit, ? extends Unit> remappingFunction) { - throw new UnsupportedOperationException( - "Cannot edit an immutable map"); - } - - @Override - public Unit computeIfAbsent(final String key, - final Function<? super String, ? extends Unit> mappingFunction) { - throw new UnsupportedOperationException( - "Cannot edit an immutable map"); - } - - @Override - public Unit computeIfPresent(final String key, - final BiFunction<? super String, ? super Unit, ? extends Unit> remappingFunction) { - throw new UnsupportedOperationException( - "Cannot edit an immutable map"); - } - - @Override - public boolean containsKey(final Object key) { - // First, test if there is a unit with the key - if (this.units.containsKey(key)) - return true; - - // Next, try to cast it to String - if (!(key instanceof String)) - throw new IllegalArgumentException( - "Attempted to test for a unit using a non-string name."); - final String unitName = (String) key; - - // Then, look for the longest prefix that is attached to a valid unit - String longestPrefix = null; - int longestLength = 0; - - for (final String prefixName : this.prefixes.keySet()) { - // a prefix name is valid if: - // - it is prefixed (i.e. the unit name starts with it) - // - 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} - * - * <p> - * Because of ambiguities between prefixes (i.e. kilokilo = mega), this - * method only tests for prefixless units. - * </p> - */ - @Override - public boolean containsValue(final Object value) { - return this.units.containsValue(value); - } - - @Override - public Set<Entry<String, Unit>> 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<String> 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<? super Unit, ? super Unit, ? extends Unit> remappingFunction) { - throw new UnsupportedOperationException( - "Cannot merge into an immutable map"); - } - - @Override - public Unit put(final String key, final Unit value) { - throw new UnsupportedOperationException( - "Cannot add entries to an immutable map"); - } - - @Override - public void putAll(final Map<? extends String, ? extends Unit> m) { - throw new UnsupportedOperationException( - "Cannot add entries to an immutable map"); - } - - @Override - public Unit putIfAbsent(final String key, final Unit value) { - throw new UnsupportedOperationException( - "Cannot add entries to an immutable map"); - } - - @Override - public Unit remove(final Object key) { - throw new UnsupportedOperationException( - "Cannot remove entries from an immutable map"); - } - - @Override - public boolean remove(final Object key, final Object value) { - throw new UnsupportedOperationException( - "Cannot remove entries from an immutable map"); - } - - @Override - public Unit replace(final String key, final Unit value) { - throw new UnsupportedOperationException( - "Cannot replace entries in an immutable map"); - } - - @Override - public boolean replace(final String key, final Unit oldValue, - final Unit newValue) { - throw new UnsupportedOperationException( - "Cannot replace entries in an immutable map"); - } - - @Override - public void replaceAll( - final BiFunction<? super String, ? super Unit, ? extends Unit> function) { - throw new UnsupportedOperationException( - "Cannot replace entries in an immutable map"); - } - - @Override - public int size() { - if (this.units.isEmpty()) - 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} - * - * <p> - * Because of ambiguities between prefixes (i.e. kilokilo = mega), this - * method ignores prefixes. - * </p> - */ - @Override - public Collection<Unit> 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<Pattern, String> EXPRESSION_REPLACEMENTS = new HashMap<>(); - - // add data to expression replacements - static { - // add spaces around operators - for (final String operator : Arrays.asList("\\*", "/", "\\^")) { - EXPRESSION_REPLACEMENTS.put(Pattern.compile(operator), - " " + operator + " "); - } - - // replace multiple spaces with a single space - EXPRESSION_REPLACEMENTS.put(Pattern.compile(" +"), " "); - // place brackets around any expression of the form "number unit", with or - // without the space - EXPRESSION_REPLACEMENTS.put(Pattern.compile("((?:-?[1-9]\\d*|0)" // integer - + "(?:\\.\\d+(?:[eE]\\d+))?)" // optional decimal point with numbers - // after it - + "\\s*" // optional space(s) - + "([a-zA-Z]+(?:\\^\\d+)?" // any string of letters - + "(?:\\s+[a-zA-Z]+(?:\\^\\d+)?))" // optional other letters - + "(?!-?\\d)" // no number directly afterwards (avoids matching - // "1e3") - ), "\\($1 $2\\)"); - } - - /** - * A regular expression that separates names and expressions in unit files. - */ - private static final Pattern NAME_EXPRESSION = Pattern - .compile("(\\S+)\\s+(\\S.*)"); - - /** - * Like normal string comparisons, but shorter strings are always less than - * longer strings. - */ - private static final Comparator<String> lengthFirstComparator = Comparator - .comparingInt(String::length).thenComparing(Comparator.naturalOrder()); - - /** - * The exponent operator - * - * @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(SI.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(SI.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<String, Unit> unitMap, - Entry<String, Unit> entry) { - for (final Entry<String, Unit> 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<String, Unit> prefixlessUnits; - - /** - * The unit prefixes in this system. - * - * @since 2019-01-14 - * @since v0.1.0 - */ - private final Map<String, UnitPrefix> prefixes; - - /** - * The dimensions in this system. - * - * @since 2019-03-14 - * @since v0.2.0 - */ - private final Map<String, ObjectProduct<BaseDimension>> dimensions; - - /** - * A map mapping strings to units (including prefixes) - * - * @since 2019-04-13 - * @since v0.2.0 - */ - private final Map<String, Unit> 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 - * <p> - * The prefixes are inputted in <em>application order</em>. This means that - * testing whether "kilomegagigametre" is a valid unit is equivalent to - * running the following code (assuming all variables are defined correctly): - * <br> - * {@code prefixRepetitionRule.test(Arrays.asList(giga, mega, kilo))} - */ - private Predicate<List<UnitPrefix>> prefixRepetitionRule; - - /** - * A parser that can parse unit expressions. - * - * @since 2019-03-22 - * @since v0.2.0 - */ - private final ExpressionParser<LinearUnit> unitExpressionParser = new ExpressionParser.Builder<>( - this::getLinearUnit).addBinaryOperator("+", (o1, o2) -> o1.plus(o2), 0) - .addBinaryOperator("-", (o1, o2) -> o1.minus(o2), 0) - .addBinaryOperator("*", (o1, o2) -> o1.times(o2), 1) - .addSpaceFunction("*") - .addBinaryOperator("/", (o1, o2) -> o1.dividedBy(o2), 1) - .addBinaryOperator("^", UnitDatabase::exponentiateUnits, 2) - .build(); - - /** - * A parser that can parse unit value expressions. - * - * @since 2020-08-04 - */ - private final ExpressionParser<LinearUnitValue> unitValueExpressionParser = new ExpressionParser.Builder<>( - this::getLinearUnitValue) - .addBinaryOperator("+", (o1, o2) -> o1.plus(o2), 0) - .addBinaryOperator("-", (o1, o2) -> o1.minus(o2), 0) - .addBinaryOperator("*", (o1, o2) -> o1.times(o2), 1) - .addSpaceFunction("*") - .addBinaryOperator("/", (o1, o2) -> o1.dividedBy(o2), 1) - .addBinaryOperator("^", UnitDatabase::exponentiateUnitValues, 2) - .build(); - - /** - * A parser that can parse unit prefix expressions - * - * @since 2019-04-13 - * @since v0.2.0 - */ - private final ExpressionParser<UnitPrefix> 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<ObjectProduct<BaseDimension>> 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<List<UnitPrefix>> 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<BaseDimension> 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<BaseDimension> 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<String, ObjectProduct<BaseDimension>> 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<Pattern, String> replacement : EXPRESSION_REPLACEMENTS - .entrySet()) { - modifiedExpression = replacement.getKey().matcher(modifiedExpression) - .replaceAll(replacement.getValue()); - } - - // the previous operation breaks negative numbers, fix them! - // (i.e. -2 becomes - 2) - // FIXME the previous 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. - * - * <p> - * This method accepts exponents, like "L^3" - * </p> - * - * @param name dimension's name - * @return dimension - * @since 2019-03-14 - * @since v0.2.0 - */ - public ObjectProduct<BaseDimension> getDimension(final String name) { - Objects.requireNonNull(name, "name must not be null."); - if (name.contains("^")) { - final String[] baseAndExponent = name.split("\\^"); - - final ObjectProduct<BaseDimension> 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 - * <p> - * The expression is a series of any of the following: - * <ul> - * <li>The name of a unit dimension, which multiplies or divides the result - * based on preceding operators</li> - * <li>The operators '*' and '/', which multiply and divide (note that just - * putting two unit dimensions next to each other is equivalent to - * multiplication)</li> - * <li>The operator '^' which exponentiates. Exponents must be integers.</li> - * </ul> - * - * @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<BaseDimension> getDimensionFromExpression( - final String expression) { - Objects.requireNonNull(expression, "expression must not be null."); - - // attempt to get a dimension as an alias first - if (this.containsDimensionName(expression)) - return this.getDimension(expression); - - // force operators to have spaces - String modifiedExpression = expression; - - // format expression - for (final Entry<Pattern, String> replacement : EXPRESSION_REPLACEMENTS - .entrySet()) { - modifiedExpression = replacement.getKey().matcher(modifiedExpression) - .replaceAll(replacement.getValue()); - } - 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<String> 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<UnitPrefix> getPrefixesFromName(final String unitName) { - final List<UnitPrefix> prefixes = new ArrayList<>(); - String name = unitName; - - while (!this.prefixlessUnits.containsKey(name)) { - // find the longest prefix - String longestPrefixName = null; - int longestLength = name.length(); - - while (longestPrefixName == null) { - longestLength--; - if (longestLength <= 0) - 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 - * <p> - * Currently, prefix expressions are much simpler than unit expressions: They - * are either a number or the name of another prefix - * </p> - * - * @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<Pattern, String> 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<List<UnitPrefix>> 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<String> 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<String> 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 - * <p> - * The expression is a series of any of the following: - * <ul> - * <li>The name of a unit, which multiplies or divides the result based on - * preceding operators</li> - * <li>The operators '*' and '/', which multiply and divide (note that just - * putting two units or values next to each other is equivalent to - * multiplication)</li> - * <li>The operator '^' which exponentiates. Exponents must be integers.</li> - * <li>A number which is multiplied or divided</li> - * </ul> - * 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<Pattern, String> replacement : EXPRESSION_REPLACEMENTS - .entrySet()) { - modifiedExpression = replacement.getKey().matcher(modifiedExpression) - .replaceAll(replacement.getValue()); - } - - // the previous operation breaks negative numbers, fix them! - // (i.e. -2 becomes - 2) - for (int i = 0; i < modifiedExpression.length(); i++) { - if (modifiedExpression.charAt(i) == '-' - && (i < 2 || Arrays.asList('+', '-', '*', '/', '^') - .contains(modifiedExpression.charAt(i - 2)))) { - // found a broken negative number - modifiedExpression = modifiedExpression.substring(0, i + 1) - + modifiedExpression.substring(i + 2); - } - } - - return this.unitExpressionParser.parseExpression(modifiedExpression); - } - - /** - * Adds all dimensions from a file, using data from the database to parse - * them. - * <p> - * Each line in the file should consist of a name and an expression (parsed - * by getDimensionFromExpression) separated by any number of tab characters. - * <p> - * <p> - * Allowed exceptions: - * <ul> - * <li>Anything after a '#' character is considered a comment and - * ignored.</li> - * <li>Blank lines are also ignored</li> - * <li>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.</li> - * </ul> - * - * @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. - * <p> - * Each line in the file should consist of a name and an expression (parsed - * by getUnitFromExpression) separated by any number of tab characters. - * <p> - * <p> - * Allowed exceptions: - * <ul> - * <li>Anything after a '#' character is considered a comment and - * ignored.</li> - * <li>Blank lines are also ignored</li> - * <li>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.</li> - * </ul> - * - * @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<String, UnitPrefix> prefixMap() { - return Collections.unmodifiableMap(this.prefixes); - } - - /** - * @param prefixRepetitionRule the prefixRepetitionRule to set - * @since 2020-08-26 - */ - public final void setPrefixRepetitionRule( - Predicate<List<UnitPrefix>> 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. - * <p> - * 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}. - * </p> - * <p> - * Specifically, the operations that will throw an IllegalStateException if - * the map is infinite in size are: - * <ul> - * <li>{@code unitMap.entrySet().toArray()} (either overloading)</li> - * <li>{@code unitMap.keySet().toArray()} (either overloading)</li> - * </ul> - * </p> - * <p> - * 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. - * </p> - * - * @return a map mapping unit names to units, including prefixed names - * @since 2019-04-13 - * @since v0.2.0 - */ - public Map<String, Unit> 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<String, Unit> unitMapPrefixless(boolean includeDuplicates) { - if (includeDuplicates) - return Collections.unmodifiableMap(this.prefixlessUnits); - else - return Collections.unmodifiableMap(ConditionalExistenceCollections - .conditionalExistenceMap(this.prefixlessUnits, - entry -> !isRemovableDuplicate(this.prefixlessUnits, - entry))); - } -} |