diff options
Diffstat (limited to 'src/org/unitConverter/unit/UnitDatabase.java')
-rw-r--r-- | src/org/unitConverter/unit/UnitDatabase.java | 1269 |
1 files changed, 785 insertions, 484 deletions
diff --git a/src/org/unitConverter/unit/UnitDatabase.java b/src/org/unitConverter/unit/UnitDatabase.java index 507266d..000acf5 100644 --- a/src/org/unitConverter/unit/UnitDatabase.java +++ b/src/org/unitConverter/unit/UnitDatabase.java @@ -16,17 +16,18 @@ */ package org.unitConverter.unit; -import java.io.BufferedReader; -import java.io.File; import java.io.FileNotFoundException; -import java.io.FileReader; import java.io.IOException; +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; @@ -40,9 +41,11 @@ 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. @@ -55,29 +58,33 @@ 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. + * 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> + * <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 + * 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. + * Because of ambiguities between prefixes (i.e. kilokilo = mega), + * {@link #containsValue} and {@link #values()} currently ignore prefixes. * </p> * * @author Adrien Hopkins @@ -89,16 +96,19 @@ public final class UnitDatabase { * 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. + * 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>> { + private static final class PrefixedUnitEntrySet + extends AbstractSet<Map.Entry<String, Unit>> { /** * The entry for this set. * @@ -106,17 +116,16 @@ public final class UnitDatabase { * @since 2019-04-14 * @since v0.2.0 */ - private static final class PrefixedUnitEntry implements Entry<String, Unit> { + 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 + * @param key key + * @param value value * @since 2019-04-14 * @since v0.2.0 */ @@ -124,7 +133,7 @@ public final class UnitDatabase { this.key = key; this.value = value; } - + /** * @since 2019-05-03 */ @@ -136,34 +145,38 @@ public final class UnitDatabase { 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()); + ^ (this.getValue() == null ? 0 + : this.getValue().hashCode()); } - + @Override public Unit setValue(final Unit value) { - throw new UnsupportedOperationException("Cannot set value in an immutable entry"); + 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. + * 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 */ @@ -172,27 +185,30 @@ public final class UnitDatabase { return this.getKey() + "=" + this.getValue(); } } - + /** - * An iterator that iterates over the units of a {@code PrefixedUnitNameSet}. + * 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>> { + 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}. + * Creates the + * {@code UnitsDatabase.PrefixedUnitMap.PrefixedUnitNameSet.PrefixedUnitNameIterator}. * * @since 2019-04-14 * @since v0.2.0 @@ -202,7 +218,7 @@ public final class UnitDatabase { this.unitNames = new ArrayList<>(map.units.keySet()); this.prefixNames = new ArrayList<>(map.prefixes.keySet()); } - + /** * @return current unit name * @since 2019-04-14 @@ -214,10 +230,10 @@ public final class UnitDatabase { unitName.append(this.prefixNames.get(i)); } unitName.append(this.unitNames.get(this.unitNamePosition)); - + return unitName.toString(); } - + @Override public boolean hasNext() { if (this.unitNames.isEmpty()) @@ -229,7 +245,7 @@ public final class UnitDatabase { return true; } } - + /** * Changes this iterator's position to the next available one. * @@ -238,127 +254,142 @@ public final class UnitDatabase { */ 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); - + this.prefixCoordinates.set(i, + this.prefixCoordinates.get(i) + 1); + // fix any carrying errors - while (i >= 0 && this.prefixCoordinates.get(i) >= this.prefixNames.size()) { + while (i >= 0 && this.prefixCoordinates + .get(i) >= this.prefixNames.size()) { // carry over - this.prefixCoordinates.set(i--, 0); // null and decrement at the same time - + 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); + 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 + * @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 + // 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.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. + * 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\"", + 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 + * @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"); + 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"); + 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"); + 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. + // 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."); + 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()); + + 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) @@ -366,37 +397,42 @@ public final class UnitDatabase { 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"); + 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"); + 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"); + 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"); + throw new UnsupportedOperationException( + "Cannot remove from an immutable set"); } - + @Override public int size() { if (this.map.units.isEmpty()) @@ -409,10 +445,9 @@ public final class UnitDatabase { return Integer.MAX_VALUE; } } - + /** - * @throws IllegalStateException - * if the set is infinite in size + * @throws IllegalStateException if the set is infinite in size */ @Override public Object[] toArray() { @@ -420,12 +455,12 @@ public final class UnitDatabase { return super.toArray(); else // infinite set - throw new IllegalStateException("Cannot make an infinite set into an array."); + throw new IllegalStateException( + "Cannot make an infinite set into an array."); } - + /** - * @throws IllegalStateException - * if the set is infinite in size + * @throws IllegalStateException if the set is infinite in size */ @Override public <T> T[] toArray(final T[] a) { @@ -433,53 +468,61 @@ public final class UnitDatabase { return super.toArray(a); else // infinite set - throw new IllegalStateException("Cannot make an infinite set into an array."); + 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", + 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. + * 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> { + private static final class PrefixedUnitNameSet + extends AbstractSet<String> { /** - * An iterator that iterates over the units of a {@code PrefixedUnitNameSet}. + * 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> { + 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}. + * Creates the + * {@code UnitsDatabase.PrefixedUnitMap.PrefixedUnitNameSet.PrefixedUnitNameIterator}. * * @since 2019-04-14 * @since v0.2.0 @@ -489,7 +532,7 @@ public final class UnitDatabase { this.unitNames = new ArrayList<>(map.units.keySet()); this.prefixNames = new ArrayList<>(map.prefixes.keySet()); } - + /** * @return current unit name * @since 2019-04-14 @@ -501,10 +544,10 @@ public final class UnitDatabase { unitName.append(this.prefixNames.get(i)); } unitName.append(this.unitNames.get(this.unitNamePosition)); - + return unitName.toString(); } - + @Override public boolean hasNext() { if (this.unitNames.isEmpty()) @@ -516,7 +559,7 @@ public final class UnitDatabase { return true; } } - + /** * Changes this iterator's position to the next available one. * @@ -525,109 +568,121 @@ public final class UnitDatabase { */ 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); - + this.prefixCoordinates.set(i, + this.prefixCoordinates.get(i) + 1); + // fix any carrying errors - while (i >= 0 && this.prefixCoordinates.get(i) >= this.prefixNames.size()) { + while (i >= 0 && this.prefixCoordinates + .get(i) >= this.prefixNames.size()) { // carry over - this.prefixCoordinates.set(i--, 0); // null and decrement at the same time - + 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); + 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 + * @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 + // 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.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. + * 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()); + 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 + * @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"); + 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"); + throw new UnsupportedOperationException( + "Cannot add to an immutable set"); } - + @Override public void clear() { - throw new UnsupportedOperationException("Cannot clear an immutable set"); + 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) @@ -635,37 +690,41 @@ public final class UnitDatabase { 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"); + 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"); + 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"); + 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"); + throw new UnsupportedOperationException( + "Cannot remove from an immutable set"); } - + @Override public int size() { if (this.map.units.isEmpty()) @@ -678,10 +737,9 @@ public final class UnitDatabase { return Integer.MAX_VALUE; } } - + /** - * @throws IllegalStateException - * if the set is infinite in size + * @throws IllegalStateException if the set is infinite in size */ @Override public Object[] toArray() { @@ -689,13 +747,13 @@ public final class UnitDatabase { return super.toArray(); else // infinite set - throw new IllegalStateException("Cannot make an infinite set into an array."); - + throw new IllegalStateException( + "Cannot make an infinite set into an array."); + } - + /** - * @throws IllegalStateException - * if the set is infinite in size + * @throws IllegalStateException if the set is infinite in size */ @Override public <T> T[] toArray(final T[] a) { @@ -703,19 +761,21 @@ public final class UnitDatabase { return super.toArray(a); else // infinite set - throw new IllegalStateException("Cannot make an infinite set into an array."); + 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", + 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. * @@ -723,7 +783,7 @@ public final class UnitDatabase { * @since v0.2.0 */ private final Map<String, Unit> units; - + /** * The available prefixes for use. * @@ -731,95 +791,106 @@ public final class UnitDatabase { * @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 + * @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. + 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"); + 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"); + 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"); + 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"); + 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."); + 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) + // - 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) { + // - 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) { + 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. + * 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) { @@ -827,56 +898,62 @@ public final class UnitDatabase { } 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."); + 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) + // - 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) { + // - 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) { + 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 + // 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) { @@ -884,53 +961,64 @@ public final class UnitDatabase { } 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"); + 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"); + 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"); + 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"); + 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"); + 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"); + 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"); + 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"); + 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"); + 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()) @@ -943,66 +1031,80 @@ public final class UnitDatabase { 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", + 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. + * 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()); + 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 { - // place brackets around any expression of the form "number unit", with or without the space + // 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 + + "(?:\\.\\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") + + "(?!-?\\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.*)"); - + private static final Pattern NAME_EXPRESSION = Pattern + .compile("(\\S+)\\s+(\\S.*)"); + /** * The exponent operator * - * @param base - * base of exponentiation - * @param exponentUnit - * exponent + * @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) { + 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, @@ -1012,12 +1114,39 @@ public final class UnitDatabase { return base.toExponent((int) (exponent + 0.5)); else // not an integer - throw new UnsupportedOperationException("Decimal exponents are currently not supported."); + 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. * @@ -1025,7 +1154,7 @@ public final class UnitDatabase { * @since v0.1.0 */ private final Map<String, Unit> prefixlessUnits; - + /** * The unit prefixes in this system. * @@ -1033,7 +1162,7 @@ public final class UnitDatabase { * @since v0.1.0 */ private final Map<String, UnitPrefix> prefixes; - + /** * The dimensions in this system. * @@ -1041,7 +1170,7 @@ public final class UnitDatabase { * @since v0.2.0 */ private final Map<String, ObjectProduct<BaseDimension>> dimensions; - + /** * A map mapping strings to units (including prefixes) * @@ -1049,7 +1178,19 @@ public final class UnitDatabase { * @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. * @@ -1059,21 +1200,41 @@ public final class UnitDatabase { 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.times(o2), 1) + .addSpaceFunction("*") .addBinaryOperator("/", (o1, o2) -> o1.dividedBy(o2), 1) - .addBinaryOperator("^", UnitDatabase::exponentiateUnits, 2).build(); - + .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(); - + 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. * @@ -1081,9 +1242,10 @@ public final class UnitDatabase { * @since v0.2.0 */ private final ExpressionParser<ObjectProduct<BaseDimension>> unitDimensionParser = new ExpressionParser.Builder<>( - this::getDimension).addBinaryOperator("*", (o1, o2) -> o1.times(o2), 0).addSpaceFunction("*") + this::getDimension).addBinaryOperator("*", (o1, o2) -> o1.times(o2), 0) + .addSpaceFunction("*") .addBinaryOperator("/", (o1, o2) -> o1.dividedBy(o2), 0).build(); - + /** * Creates the {@code UnitsDatabase}. * @@ -1091,48 +1253,62 @@ public final class UnitDatabase { * @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.units = new PrefixedUnitMap(this.prefixlessUnits, this.prefixes); + 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 + * @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."), + 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 + * @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) { + 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); + 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()) @@ -1141,17 +1317,18 @@ public final class UnitDatabase { 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); + 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)); + 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; @@ -1161,20 +1338,17 @@ public final class UnitDatabase { 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 + * @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 */ @@ -1182,43 +1356,41 @@ public final class UnitDatabase { 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 + * @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."), + 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 + * @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) { + 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); + 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()) @@ -1226,18 +1398,20 @@ public final class UnitDatabase { "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); + 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)); + throw new IllegalArgumentException(String + .format("! used but no unit found (line %d).", lineCounter)); } else { if (name.endsWith("-")) { final UnitPrefix prefix; @@ -1257,17 +1431,16 @@ public final class UnitDatabase { 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 + * @param name name to test * @return if database contains name * @since 2019-03-14 * @since v0.2.0 @@ -1275,12 +1448,11 @@ public final class UnitDatabase { 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 + * @param name name to test * @return if database contains name * @since 2019-01-13 * @since v0.1.0 @@ -1288,12 +1460,12 @@ public final class UnitDatabase { 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 + * Tests if the database has a unit with this name, taking prefixes into + * consideration * - * @param name - * name to test + * @param name name to test * @return if database contains name * @since 2019-01-13 * @since v0.1.0 @@ -1301,7 +1473,7 @@ public final class UnitDatabase { public boolean containsUnitName(final String name) { return this.units.containsKey(name); } - + /** * @return a map mapping dimension names to dimensions * @since 2019-04-13 @@ -1310,7 +1482,50 @@ public final class UnitDatabase { 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. * @@ -1318,8 +1533,7 @@ public final class UnitDatabase { * This method accepts exponents, like "L^3" * </p> * - * @param name - * dimension's name + * @param name dimension's name * @return dimension * @since 2019-03-14 * @since v0.2.0 @@ -1328,102 +1542,125 @@ public final class UnitDatabase { 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 ObjectProduct<BaseDimension> base = this + .getDimension(baseAndExponent[0]); + final int exponent; try { - exponent = Integer.parseInt(baseAndExponent[baseAndExponent.length - 1]); + 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 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 + * @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) { + 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; - modifiedExpression = modifiedExpression.replaceAll("\\*", " \\* "); - modifiedExpression = modifiedExpression.replaceAll("/", " / "); - modifiedExpression = modifiedExpression.replaceAll(" *\\^ *", "\\^"); - - // fix broken spaces - modifiedExpression = modifiedExpression.replaceAll(" +", " "); - + // format expression - for (final Entry<Pattern, String> replacement : EXPRESSION_REPLACEMENTS.entrySet()) { - modifiedExpression = replacement.getKey().matcher(modifiedExpression).replaceAll(replacement.getValue()); + 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}. + * 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 + * @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)."); - + 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)); + 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)); + 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 + * @param name prefix's name * @return prefix * @since 2019-01-10 * @since v0.1.0 @@ -1435,53 +1672,87 @@ public final class UnitDatabase { 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 + * 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 + * @param expression expression to input * @return prefix - * @throws IllegalArgumentException - * if expression cannot be parsed - * @throws NullPointerException - * if any argument is null + * @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; - modifiedExpression = modifiedExpression.replaceAll("\\*", " \\* "); - modifiedExpression = modifiedExpression.replaceAll("/", " / "); - modifiedExpression = modifiedExpression.replaceAll("\\^", " \\^ "); - - // fix broken spaces - modifiedExpression = modifiedExpression.replaceAll(" +", " "); - + // format expression - for (final Entry<Pattern, String> replacement : EXPRESSION_REPLACEMENTS.entrySet()) { - modifiedExpression = replacement.getKey().matcher(modifiedExpression).replaceAll(replacement.getValue()); + 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 + * @param name unit's name * @return unit * @since 2019-01-10 * @since v0.1.0 @@ -1491,101 +1762,115 @@ public final class UnitDatabase { final double value = Double.parseDouble(name); return SI.ONE.times(value); } catch (final NumberFormatException e) { - return this.units.get(name); + 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 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 + * @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("-", " - "); - modifiedExpression = modifiedExpression.replaceAll("\\*", " \\* "); - modifiedExpression = modifiedExpression.replaceAll("/", " / "); - modifiedExpression = modifiedExpression.replaceAll("\\^", " \\^ "); - - // fix broken spaces - modifiedExpression = modifiedExpression.replaceAll(" +", " "); // format expression - for (final Entry<Pattern, String> replacement : EXPRESSION_REPLACEMENTS.entrySet()) { - modifiedExpression = replacement.getKey().matcher(modifiedExpression).replaceAll(replacement.getValue()); + 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)))) { + && (i < 2 || Arrays.asList('+', '-', '*', '/', '^') + .contains(modifiedExpression.charAt(i - 2)))) { // found a broken negative number - modifiedExpression = modifiedExpression.substring(0, i + 1) + modifiedExpression.substring(i + 2); + 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. + * 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. + * 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>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> + * <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 + * @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 File file) { + public void loadDimensionFile(final Path file) { Objects.requireNonNull(file, "file must not be null."); - try (FileReader fileReader = new FileReader(file); BufferedReader reader = new BufferedReader(fileReader)) { - // while the reader has lines to read, read a line, then parse it, then add it + try { long lineCounter = 0; - while (reader.ready()) { - this.addDimensionFromLine(reader.readLine(), ++lineCounter); + for (final String line : Files.readAllLines(file)) { + this.addDimensionFromLine(line, ++lineCounter); } } catch (final FileNotFoundException e) { throw new IllegalArgumentException("Could not find file " + file, e); @@ -1593,39 +1878,38 @@ public final class UnitDatabase { throw new IllegalArgumentException("Could not read file " + file, e); } } - + /** * 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. + * 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>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> + * <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 + * @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 File file) { + public void loadUnitsFile(final Path file) { Objects.requireNonNull(file, "file must not be null."); - try (FileReader fileReader = new FileReader(file); BufferedReader reader = new BufferedReader(fileReader)) { - // while the reader has lines to read, read a line, then parse it, then add it + try { long lineCounter = 0; - while (reader.ready()) { - this.addUnitOrPrefixFromLine(reader.readLine(), ++lineCounter); + for (final String line : Files.readAllLines(file)) { + this.addUnitOrPrefixFromLine(line, ++lineCounter); } } catch (final FileNotFoundException e) { throw new IllegalArgumentException("Could not find file " + file, e); @@ -1633,7 +1917,7 @@ public final class UnitDatabase { throw new IllegalArgumentException("Could not read file " + file, e); } } - + /** * @return a map mapping prefix names to prefixes * @since 2019-04-13 @@ -1642,33 +1926,49 @@ public final class UnitDatabase { public Map<String, UnitPrefix> prefixMap() { return Collections.unmodifiableMap(this.prefixes); } - + /** - * @return a string stating the number of units, prefixes and dimensions in the database + * @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()); + 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}. + * 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: + * 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. + * 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 @@ -1676,9 +1976,10 @@ public final class UnitDatabase { * @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 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 |