summaryrefslogtreecommitdiff
path: root/src/main/java/org/unitConverter/unit/UnitDatabase.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/org/unitConverter/unit/UnitDatabase.java')
-rw-r--r--src/main/java/org/unitConverter/unit/UnitDatabase.java2025
1 files changed, 2025 insertions, 0 deletions
diff --git a/src/main/java/org/unitConverter/unit/UnitDatabase.java b/src/main/java/org/unitConverter/unit/UnitDatabase.java
new file mode 100644
index 0000000..6322fef
--- /dev/null
+++ b/src/main/java/org/unitConverter/unit/UnitDatabase.java
@@ -0,0 +1,2025 @@
+/**
+ * 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.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.*)");
+
+ /**
+ * 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.");
+ }
+
+ /**
+ * 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.
+ }
+
+ /**
+ * @return a map mapping unit names to units, ignoring prefixes
+ * @since 2019-04-13
+ * @since v0.2.0
+ */
+ public Map<String, Unit> unitMapPrefixless() {
+ return Collections.unmodifiableMap(this.prefixlessUnits);
+ }
+}