From 77c7c962a6ed810b12685aa9ace4bd8a62761cea Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Tue, 4 Aug 2020 16:08:22 -0500 Subject: Made UnitConverterGUI use UnitValue and LinearUnitValue --- src/org/unitConverter/unit/UnitDatabase.java | 1163 +++++++++++++++----------- 1 file changed, 689 insertions(+), 474 deletions(-) (limited to 'src/org/unitConverter/unit/UnitDatabase.java') diff --git a/src/org/unitConverter/unit/UnitDatabase.java b/src/org/unitConverter/unit/UnitDatabase.java index 507266d..0246630 100644 --- a/src/org/unitConverter/unit/UnitDatabase.java +++ b/src/org/unitConverter/unit/UnitDatabase.java @@ -27,6 +27,7 @@ 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; @@ -55,29 +56,33 @@ public final class UnitDatabase { /** * A map for units that allows the use of prefixes. *

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

*

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

*

*

- * This map is infinite in size if there is at least one unit and at least one prefix. If it is infinite, some - * operations that only work with finite collections, like converting name/entry sets to arrays, will throw an + * This map is infinite in size if there is at least one unit and at least + * one prefix. If it is infinite, some operations that only work with finite + * collections, like converting name/entry sets to arrays, will throw an * {@code IllegalStateException}. *

*

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

* * @author Adrien Hopkins @@ -89,16 +94,19 @@ public final class UnitDatabase { * The class used for entry sets. * *

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

* * @author Adrien Hopkins * @since 2019-04-13 * @since v0.2.0 */ - private static final class PrefixedUnitEntrySet extends AbstractSet> { + private static final class PrefixedUnitEntrySet + extends AbstractSet> { /** * The entry for this set. * @@ -106,17 +114,16 @@ public final class UnitDatabase { * @since 2019-04-14 * @since v0.2.0 */ - private static final class PrefixedUnitEntry implements Entry { + private static final class PrefixedUnitEntry + implements Entry { private final String key; private final Unit value; - + /** * Creates the {@code PrefixedUnitEntry}. * - * @param key - * key - * @param value - * value + * @param key key + * @param value value * @since 2019-04-14 * @since v0.2.0 */ @@ -124,7 +131,7 @@ public final class UnitDatabase { this.key = key; this.value = value; } - + /** * @since 2019-05-03 */ @@ -136,34 +143,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 +183,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> { + private static final class PrefixedUnitEntryIterator + implements Iterator> { // position in the unit list private int unitNamePosition = 0; // the indices of the prefixes attached to the current unit private final List prefixCoordinates = new ArrayList<>(); - + // values from the unit entry set private final Map map; private transient final List unitNames; private transient final List prefixNames; - + /** - * Creates the {@code UnitsDatabase.PrefixedUnitMap.PrefixedUnitNameSet.PrefixedUnitNameIterator}. + * Creates the + * {@code UnitsDatabase.PrefixedUnitMap.PrefixedUnitNameSet.PrefixedUnitNameIterator}. * * @since 2019-04-14 * @since v0.2.0 @@ -202,7 +216,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 +228,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 +243,7 @@ public final class UnitDatabase { return true; } } - + /** * Changes this iterator's position to the next available one. * @@ -238,127 +252,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 next() { // get next element final Entry nextEntry = this.peek(); - + // iterate to next position this.incrementPosition(); - + return nextEntry; } - + /** - * @return the next element in the iterator, without iterating over it + * @return the next element in the iterator, without iterating over + * it * @since 2019-05-03 */ private Entry peek() { if (!this.hasNext()) throw new NoSuchElementException("No units left!"); - + // if I have prefixes, ensure I'm not using a nonlinear unit - // since all of the unprefixed stuff is done, just remove nonlinear units + // 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 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> c) { - throw new UnsupportedOperationException("Cannot add to an immutable set"); + public boolean addAll( + final Collection> c) { + throw new UnsupportedOperationException( + "Cannot add to an immutable set"); } - + @Override public void clear() { - throw new UnsupportedOperationException("Cannot clear an immutable set"); + throw new UnsupportedOperationException( + "Cannot clear an immutable set"); } - + @Override public boolean contains(final Object o) { // get the entry final Entry entry; - + try { - // This is OK because I'm in a try-catch block, catching the exact exception that would be thrown. + // This is OK because I'm in a try-catch block, catching the + // exact exception that would be thrown. @SuppressWarnings("unchecked") final Entry tempEntry = (Entry) o; entry = tempEntry; } catch (final ClassCastException e) { - throw new IllegalArgumentException("Attempted to test for an entry using a non-entry."); + 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 +395,42 @@ public final class UnitDatabase { return false; return true; } - + @Override public boolean isEmpty() { return this.map.isEmpty(); } - + @Override public Iterator> iterator() { return new PrefixedUnitEntryIterator(this.map); } - + @Override public boolean remove(final Object o) { - throw new UnsupportedOperationException("Cannot remove from an immutable set"); + 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> filter) { - throw new UnsupportedOperationException("Cannot remove from an immutable set"); + public boolean removeIf( + final Predicate> filter) { + throw new UnsupportedOperationException( + "Cannot remove from an immutable set"); } - + @Override public boolean retainAll(final Collection c) { - throw new UnsupportedOperationException("Cannot remove from an immutable set"); + throw new UnsupportedOperationException( + "Cannot remove from an immutable set"); } - + @Override public int size() { if (this.map.units.isEmpty()) @@ -409,10 +443,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 +453,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[] toArray(final T[] a) { @@ -433,53 +466,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. * *

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

* * @author Adrien Hopkins * @since 2019-04-13 * @since v0.2.0 */ - private static final class PrefixedUnitNameSet extends AbstractSet { + private static final class PrefixedUnitNameSet + extends AbstractSet { /** - * 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 { + private static final class PrefixedUnitNameIterator + implements Iterator { // position in the unit list private int unitNamePosition = 0; // the indices of the prefixes attached to the current unit private final List prefixCoordinates = new ArrayList<>(); - + // values from the unit name set private final Map map; private transient final List unitNames; private transient final List prefixNames; - + /** - * Creates the {@code UnitsDatabase.PrefixedUnitMap.PrefixedUnitNameSet.PrefixedUnitNameIterator}. + * Creates the + * {@code UnitsDatabase.PrefixedUnitMap.PrefixedUnitNameSet.PrefixedUnitNameIterator}. * * @since 2019-04-14 * @since v0.2.0 @@ -489,7 +530,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 +542,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 +557,7 @@ public final class UnitDatabase { return true; } } - + /** * Changes this iterator's position to the next available one. * @@ -525,109 +566,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 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 +688,41 @@ public final class UnitDatabase { return false; return true; } - + @Override public boolean isEmpty() { return this.map.isEmpty(); } - + @Override public Iterator iterator() { return new PrefixedUnitNameIterator(this.map); } - + @Override public boolean remove(final Object o) { - throw new UnsupportedOperationException("Cannot remove from an immutable set"); + 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 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 +735,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 +745,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[] toArray(final T[] a) { @@ -703,19 +759,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 +781,7 @@ public final class UnitDatabase { * @since v0.2.0 */ private final Map units; - + /** * The available prefixes for use. * @@ -731,95 +789,106 @@ public final class UnitDatabase { * @since v0.2.0 */ private final Map prefixes; - + // caches private transient Collection values = null; private transient Set keySet = null; private transient Set> entrySet = null; - + /** * Creates the {@code PrefixedUnitMap}. * - * @param units - * map mapping unit names to units - * @param prefixes - * map mapping prefix names to prefixes + * @param units map mapping unit names to units + * @param prefixes map mapping prefix names to prefixes * @since 2019-04-13 * @since v0.2.0 */ - public PrefixedUnitMap(final Map units, final Map prefixes) { - // I am making unmodifiable maps to ensure I don't accidentally make changes. + public PrefixedUnitMap(final Map units, + final Map prefixes) { + // I am making unmodifiable maps to ensure I don't accidentally make + // changes. this.units = Collections.unmodifiableMap(units); this.prefixes = Collections.unmodifiableMap(prefixes); } - + @Override public void clear() { - throw new UnsupportedOperationException("Cannot clear an immutable map"); + throw new UnsupportedOperationException( + "Cannot clear an immutable map"); } - + @Override public Unit compute(final String key, final BiFunction remappingFunction) { - throw new UnsupportedOperationException("Cannot edit an immutable map"); + throw new UnsupportedOperationException( + "Cannot edit an immutable map"); } - + @Override - public Unit computeIfAbsent(final String key, final Function mappingFunction) { - throw new UnsupportedOperationException("Cannot edit an immutable map"); + public Unit computeIfAbsent(final String key, + final Function mappingFunction) { + throw new UnsupportedOperationException( + "Cannot edit an immutable map"); } - + @Override public Unit computeIfPresent(final String key, final BiFunction remappingFunction) { - throw new UnsupportedOperationException("Cannot edit an immutable map"); + 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} * *

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

*/ @Override public boolean containsValue(final Object value) { return this.units.containsValue(value); } - + @Override public Set> entrySet() { if (this.entrySet == null) { @@ -827,56 +896,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 keySet() { if (this.keySet == null) { @@ -884,53 +959,64 @@ public final class UnitDatabase { } return this.keySet; } - + @Override public Unit merge(final String key, final Unit value, final BiFunction remappingFunction) { - throw new UnsupportedOperationException("Cannot merge into an immutable map"); + 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 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 function) { - throw new UnsupportedOperationException("Cannot replace entries in an immutable map"); + public void replaceAll( + final BiFunction function) { + throw new UnsupportedOperationException( + "Cannot replace entries in an immutable map"); } - + @Override public int size() { if (this.units.isEmpty()) @@ -943,66 +1029,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} * *

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

*/ @Override public Collection 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 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 +1112,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.getValue(); + 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 +1152,7 @@ public final class UnitDatabase { * @since v0.1.0 */ private final Map prefixlessUnits; - + /** * The unit prefixes in this system. * @@ -1033,7 +1160,7 @@ public final class UnitDatabase { * @since v0.1.0 */ private final Map prefixes; - + /** * The dimensions in this system. * @@ -1041,7 +1168,7 @@ public final class UnitDatabase { * @since v0.2.0 */ private final Map> dimensions; - + /** * A map mapping strings to units (including prefixes) * @@ -1049,7 +1176,7 @@ public final class UnitDatabase { * @since v0.2.0 */ private final Map units; - + /** * A parser that can parse unit expressions. * @@ -1059,21 +1186,41 @@ public final class UnitDatabase { private final ExpressionParser unitExpressionParser = new ExpressionParser.Builder<>( this::getLinearUnit).addBinaryOperator("+", (o1, o2) -> o1.plus(o2), 0) .addBinaryOperator("-", (o1, o2) -> o1.minus(o2), 0) - .addBinaryOperator("*", (o1, o2) -> o1.times(o2), 1).addSpaceFunction("*") + .addBinaryOperator("*", (o1, o2) -> o1.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 unitValueExpressionParser = new ExpressionParser.Builder<>( + this::getLinearUnitValue) + .addBinaryOperator("+", (o1, o2) -> o1.plus(o2), 0) + .addBinaryOperator("-", (o1, o2) -> o1.minus(o2), 0) + .addBinaryOperator("*", (o1, o2) -> o1.times(o2), 1) + .addSpaceFunction("*") + .addBinaryOperator("/", (o1, o2) -> o1.dividedBy(o2), 1) + .addBinaryOperator("^", UnitDatabase::exponentiateUnitValues, 2) + .build(); + /** * A parser that can parse unit prefix expressions * * @since 2019-04-13 * @since v0.2.0 */ - private final ExpressionParser prefixExpressionParser = new ExpressionParser.Builder<>(this::getPrefix) - .addBinaryOperator("*", (o1, o2) -> o1.times(o2), 0).addSpaceFunction("*") - .addBinaryOperator("/", (o1, o2) -> o1.dividedBy(o2), 0) - .addBinaryOperator("^", (o1, o2) -> o1.toExponent(o2.getMultiplier()), 1).build(); - + private final ExpressionParser prefixExpressionParser = new ExpressionParser.Builder<>( + this::getPrefix).addBinaryOperator("*", (o1, o2) -> o1.times(o2), 0) + .addSpaceFunction("*") + .addBinaryOperator("/", (o1, o2) -> o1.dividedBy(o2), 0) + .addBinaryOperator("^", + (o1, o2) -> o1.toExponent(o2.getMultiplier()), 1) + .build(); + /** * A parser that can parse unit dimension expressions. * @@ -1081,9 +1228,10 @@ public final class UnitDatabase { * @since v0.2.0 */ private final ExpressionParser> 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}. * @@ -1096,43 +1244,42 @@ public final class UnitDatabase { this.dimensions = new HashMap<>(); this.units = new PrefixedUnitMap(this.prefixlessUnits, this.prefixes); } - + /** * 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 dimension) { - this.dimensions.put(Objects.requireNonNull(name, "name must not be null."), + public void addDimension(final String name, + final ObjectProduct dimension) { + this.dimensions.put( + Objects.requireNonNull(name, "name must not be null."), Objects.requireNonNull(dimension, "dimension must not be null.")); } - + /** * Adds to the list from a line in a unit dimension file. * - * @param line - * line to look at - * @param lineCounter - * number of line, for error messages + * @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 +1288,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 dimension; @@ -1161,20 +1309,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 +1327,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 +1369,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 +1402,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 +1419,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 +1431,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 +1444,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 +1453,50 @@ public final class UnitDatabase { public Map> dimensionMap() { return Collections.unmodifiableMap(this.dimensions); } - + + /** + * Evaluates a unit expression, following the same rules as + * {@link #getUnitFromExpression}. + * + * @param expression expression to parse + * @return {@code LinearUnitValue} representing value of expression + * @since 2020-08-04 + */ + public LinearUnitValue evaluateUnitExpression(final String expression) { + Objects.requireNonNull(expression, "expression must not be null."); + + // attempt to get a unit as an alias first + if (this.containsUnitName(expression)) + return this.getLinearUnitValue(expression); + + // force operators to have spaces + String modifiedExpression = expression; + modifiedExpression = modifiedExpression.replaceAll("\\+", " \\+ "); + modifiedExpression = modifiedExpression.replaceAll("-", " - "); + + // format expression + for (final Entry replacement : EXPRESSION_REPLACEMENTS + .entrySet()) { + modifiedExpression = replacement.getKey().matcher(modifiedExpression) + .replaceAll(replacement.getValue()); + } + + // the previous operation breaks negative numbers, fix them! + // (i.e. -2 becomes - 2) + // FIXME the previous operaton also breaks stuff like "1e-5" + for (int i = 0; i < modifiedExpression.length(); i++) { + if (modifiedExpression.charAt(i) == '-' + && (i < 2 || Arrays.asList('+', '-', '*', '/', '^') + .contains(modifiedExpression.charAt(i - 2)))) { + // found a broken negative number + modifiedExpression = modifiedExpression.substring(0, i + 1) + + modifiedExpression.substring(i + 2); + } + } + + return this.unitValueExpressionParser.parseExpression(modifiedExpression); + } + /** * Gets a unit dimension from the database using its name. * @@ -1318,8 +1504,7 @@ public final class UnitDatabase { * This method accepts exponents, like "L^3" *

* - * @param name - * dimension's name + * @param name dimension's name * @return dimension * @since 2019-03-14 * @since v0.2.0 @@ -1328,86 +1513,87 @@ public final class UnitDatabase { Objects.requireNonNull(name, "name must not be null."); if (name.contains("^")) { final String[] baseAndExponent = name.split("\\^"); - - final ObjectProduct base = this.getDimension(baseAndExponent[0]); - + + final ObjectProduct 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 *

* The expression is a series of any of the following: *

    - *
  • The name of a unit dimension, which multiplies or divides the result based on preceding operators
  • - *
  • The operators '*' and '/', which multiply and divide (note that just putting two unit dimensions next to each - * other is equivalent to multiplication)
  • + *
  • The name of a unit dimension, which multiplies or divides the result + * based on preceding operators
  • + *
  • The operators '*' and '/', which multiply and divide (note that just + * putting two unit dimensions next to each other is equivalent to + * multiplication)
  • *
  • The operator '^' which exponentiates. Exponents must be integers.
  • *
* - * @param expression - * expression to parse - * @throws IllegalArgumentException - * if the expression cannot be parsed - * @throws NullPointerException - * if expression is null + * @param expression expression to parse + * @throws IllegalArgumentException if the expression cannot be parsed + * @throws NullPointerException if expression is null * @since 2019-04-13 * @since v0.2.0 */ - public ObjectProduct getDimensionFromExpression(final String expression) { + public ObjectProduct getDimensionFromExpression( + final String expression) { Objects.requireNonNull(expression, "expression must not be null."); - + // attempt to get a dimension as an alias first if (this.containsDimensionName(expression)) return this.getDimension(expression); - + // force operators to have spaces String modifiedExpression = expression; - modifiedExpression = modifiedExpression.replaceAll("\\*", " \\* "); - modifiedExpression = modifiedExpression.replaceAll("/", " / "); - modifiedExpression = modifiedExpression.replaceAll(" *\\^ *", "\\^"); - - // fix broken spaces - modifiedExpression = modifiedExpression.replaceAll(" +", " "); - + // format expression - for (final Entry replacement : EXPRESSION_REPLACEMENTS.entrySet()) { - modifiedExpression = replacement.getKey().matcher(modifiedExpression).replaceAll(replacement.getValue()); + for (final Entry replacement : EXPRESSION_REPLACEMENTS + .entrySet()) { + modifiedExpression = replacement.getKey().matcher(modifiedExpression) + .replaceAll(replacement.getValue()); } - + modifiedExpression = modifiedExpression.replaceAll(" *\\^ *", "\\^"); + return this.unitDimensionParser.parseExpression(modifiedExpression); } - + /** - * Gets a unit. If it is linear, cast it to a LinearUnit and return it. Otherwise, throw an - * {@code IllegalArgumentException}. + * 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 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 @@ -1415,15 +1601,27 @@ public final class UnitDatabase { 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) { + 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 +1633,45 @@ public final class UnitDatabase { return this.prefixes.get(name); } } - + /** * Gets a unit prefix from a prefix expression *

- * Currently, prefix expressions are much simpler than unit expressions: They are either a number or the name of - * another prefix + * Currently, prefix expressions are much simpler than unit expressions: They + * are either a number or the name of another prefix *

* - * @param expression - * expression to input + * @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 replacement : EXPRESSION_REPLACEMENTS.entrySet()) { - modifiedExpression = replacement.getKey().matcher(modifiedExpression).replaceAll(replacement.getValue()); + for (final Entry replacement : EXPRESSION_REPLACEMENTS + .entrySet()) { + modifiedExpression = replacement.getKey().matcher(modifiedExpression) + .replaceAll(replacement.getValue()); } - + return this.prefixExpressionParser.parseExpression(modifiedExpression); } - + /** * 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,98 +1681,113 @@ 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.getPrimaryName().isEmpty()) + return unit.withName(NameSymbol.ofName(name)); + else if (!unit.getPrimaryName().get().equals(name)) { + final Set otherNames = new HashSet<>(unit.getOtherNames()); + otherNames.add(unit.getPrimaryName().get()); + return unit.withName(NameSymbol.ofNullable(name, + unit.getSymbol().orElse(null), otherNames)); + } else if (!unit.getOtherNames().contains(name)) { + final Set otherNames = new HashSet<>(unit.getOtherNames()); + otherNames.add(name); + return unit.withName( + NameSymbol.ofNullable(unit.getPrimaryName().orElse(null), + unit.getSymbol().orElse(null), otherNames)); + } else + return unit; } - + } - + /** * Uses the database's unit data to parse an expression into a unit *

* The expression is a series of any of the following: *

    - *
  • The name of a unit, which multiplies or divides the result based on preceding operators
  • - *
  • The operators '*' and '/', which multiply and divide (note that just putting two units or values next to each - * other is equivalent to multiplication)
  • + *
  • The name of a unit, which multiplies or divides the result based on + * preceding operators
  • + *
  • The operators '*' and '/', which multiply and divide (note that just + * putting two units or values next to each other is equivalent to + * multiplication)
  • *
  • The operator '^' which exponentiates. Exponents must be integers.
  • *
  • A number which is multiplied or divided
  • *
* This method only works with linear units. * - * @param expression - * expression to parse - * @throws IllegalArgumentException - * if the expression cannot be parsed - * @throws NullPointerException - * if expression is null + * @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 replacement : EXPRESSION_REPLACEMENTS.entrySet()) { - modifiedExpression = replacement.getKey().matcher(modifiedExpression).replaceAll(replacement.getValue()); + for (final Entry replacement : EXPRESSION_REPLACEMENTS + .entrySet()) { + modifiedExpression = replacement.getKey().matcher(modifiedExpression) + .replaceAll(replacement.getValue()); } - + // the previous operation breaks negative numbers, fix them! // (i.e. -2 becomes - 2) for (int i = 0; i < modifiedExpression.length(); i++) { if (modifiedExpression.charAt(i) == '-' - && (i < 2 || Arrays.asList('+', '-', '*', '/', '^').contains(modifiedExpression.charAt(i - 2)))) { + && (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. *

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

*

* Allowed exceptions: *

    - *
  • Anything after a '#' character is considered a comment and ignored.
  • + *
  • Anything after a '#' character is considered a comment and + * ignored.
  • *
  • Blank lines are also ignored
  • - *
  • If an expression consists of a single exclamation point, instead of parsing it, this method will search the - * database for an existing unit. If no unit is found, an IllegalArgumentException is thrown. This is used to define - * initial units and ensure that the database contains them.
  • + *
  • If an expression consists of a single exclamation point, instead of + * parsing it, this method will search the database for an existing unit. If + * no unit is found, an IllegalArgumentException is thrown. This is used to + * define initial units and ensure that the database contains them.
  • *
* - * @param file - * file to read - * @throws IllegalArgumentException - * if the file cannot be parsed, found or read - * @throws NullPointerException - * if file is null + * @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) { 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 (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 long lineCounter = 0; while (reader.ready()) { this.addDimensionFromLine(reader.readLine(), ++lineCounter); @@ -1593,36 +1798,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. *

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

*

* Allowed exceptions: *

    - *
  • Anything after a '#' character is considered a comment and ignored.
  • + *
  • Anything after a '#' character is considered a comment and + * ignored.
  • *
  • Blank lines are also ignored
  • - *
  • If an expression consists of a single exclamation point, instead of parsing it, this method will search the - * database for an existing unit. If no unit is found, an IllegalArgumentException is thrown. This is used to define - * initial units and ensure that the database contains them.
  • + *
  • If an expression consists of a single exclamation point, instead of + * parsing it, this method will search the database for an existing unit. If + * no unit is found, an IllegalArgumentException is thrown. This is used to + * define initial units and ensure that the database contains them.
  • *
* - * @param file - * file to read - * @throws IllegalArgumentException - * if the file cannot be parsed, found or read - * @throws NullPointerException - * if file is null + * @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) { 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 (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 long lineCounter = 0; while (reader.ready()) { this.addUnitOrPrefixFromLine(reader.readLine(), ++lineCounter); @@ -1633,7 +1840,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 +1849,40 @@ public final class UnitDatabase { public Map prefixMap() { return Collections.unmodifiableMap(this.prefixes); } - + /** - * @return a string stating the number of units, prefixes and dimensions in the database + * @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. *

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

*

- * 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: *

    *
  • {@code unitMap.entrySet().toArray()} (either overloading)
  • *
  • {@code unitMap.keySet().toArray()} (either overloading)
  • *
*

*

- * Because of ambiguities between prefixes (i.e. kilokilo = mega), the map's {@link PrefixedUnitMap#containsValue - * containsValue} and {@link PrefixedUnitMap#values() values()} methods currently ignore prefixes. + * Because of ambiguities between prefixes (i.e. kilokilo = mega), the map's + * {@link PrefixedUnitMap#containsValue containsValue} and + * {@link PrefixedUnitMap#values() values()} methods currently ignore + * prefixes. *

* * @return a map mapping unit names to units, including prefixed names @@ -1676,9 +1890,10 @@ public final class UnitDatabase { * @since v0.2.0 */ public Map 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 -- cgit v1.2.3 From 618b81da627b55dcb051d889c7faffd91804497a Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Tue, 4 Aug 2020 16:42:55 -0500 Subject: Added scientific rounding. --- .../converterGUI/UnitConverterGUI.java | 22 +++++++++++---------- src/org/unitConverter/unit/LinearUnitValue.java | 23 +++++++++++++++++++++- src/org/unitConverter/unit/UnitDatabase.java | 18 ++++++++++++++--- 3 files changed, 49 insertions(+), 14 deletions(-) (limited to 'src/org/unitConverter/unit/UnitDatabase.java') diff --git a/src/org/unitConverter/converterGUI/UnitConverterGUI.java b/src/org/unitConverter/converterGUI/UnitConverterGUI.java index 8c70df4..f7c3479 100644 --- a/src/org/unitConverter/converterGUI/UnitConverterGUI.java +++ b/src/org/unitConverter/converterGUI/UnitConverterGUI.java @@ -32,6 +32,7 @@ import java.util.ArrayList; import java.util.Comparator; import java.util.HashSet; import java.util.List; +import java.util.NoSuchElementException; import java.util.Set; import java.util.function.Predicate; @@ -270,14 +271,14 @@ final class UnitConverterGUI { final Unit to; try { from = this.database.evaluateUnitExpression(fromUnitString); - } catch (final IllegalArgumentException e) { + } catch (final IllegalArgumentException | NoSuchElementException e) { this.view.showErrorDialog("Parse Error", "Could not recognize text in From entry: " + e.getMessage()); return; } try { to = this.database.getUnitFromExpression(toUnitString); - } catch (final IllegalArgumentException e) { + } catch (final IllegalArgumentException | NoSuchElementException e) { this.view.showErrorDialog("Parse Error", "Could not recognize text in To entry: " + e.getMessage()); return; @@ -304,9 +305,10 @@ final class UnitConverterGUI { } final LinearUnitValue converted = from2.convertTo(to2); - this.view.setExpressionConverterOutputText( - (useSlash ? "1 / " : "") + String.format("%s = %s", - fromUnitString, this.getRoundedString(converted))); + this.view.setExpressionConverterOutputText((useSlash ? "1 / " : "") + + String.format("%s = %s", fromUnitString, + this.getRoundedString(converted, false))); + final String toString = this.getRoundedString(converted, false); return; } else { // convert to UnitValue @@ -384,18 +386,19 @@ final class UnitConverterGUI { } /** - * Like {@link LinearUnitValue#toString(boolean)} with parameter - * {@code false}, but obeys this unit converter's rounding settings. + * Like {@link LinearUnitValue#toString(boolean)}, but obeys this unit + * converter's rounding settings. * * @since 2020-08-04 */ - private final String getRoundedString(final LinearUnitValue value) { + private final String getRoundedString(final LinearUnitValue value, + boolean showUncertainty) { switch (this.roundingType) { case DECIMAL_PLACES: case SIGNIFICANT_DIGITS: return this.getRoundedString(value.asUnitValue()); case SCIENTIFIC: - return value.toString(false); + return value.toString(showUncertainty); default: throw new AssertionError("Invalid switch condition."); } @@ -992,7 +995,6 @@ final class UnitConverterGUI { final JRadioButton relativePrecision = new JRadioButton( "Scientific Precision"); - relativePrecision.setEnabled(false); relativePrecision.addActionListener(e -> this.presenter .setRoundingType(RoundingType.SCIENTIFIC)); roundingRuleButtons.add(relativePrecision); diff --git a/src/org/unitConverter/unit/LinearUnitValue.java b/src/org/unitConverter/unit/LinearUnitValue.java index 7096738..5685a6d 100644 --- a/src/org/unitConverter/unit/LinearUnitValue.java +++ b/src/org/unitConverter/unit/LinearUnitValue.java @@ -367,7 +367,7 @@ public final class LinearUnitValue { final double baseUncertainty = this.unit.convertToBase(this.uncertainty); // get rounded strings - final String valueString, baseValueString, uncertaintyString, + String valueString, baseValueString, uncertaintyString, baseUncertaintyString; if (this.isExact()) { valueString = Double.toString(this.value); @@ -438,6 +438,27 @@ public final class LinearUnitValue { return String.format("(%s ± %s) %s", valueString, uncertaintyString, chosenName); } else { + // truncate excess zeroes + if (valueString.contains(".")) { + while (valueString.endsWith("0")) { + valueString = valueString.substring(0, valueString.length() - 1); + } + if (valueString.endsWith(".")) { + valueString = valueString.substring(0, valueString.length() - 1); + } + } + + if (baseValueString.contains(".")) { + while (baseValueString.endsWith("0")) { + baseValueString = baseValueString.substring(0, + baseValueString.length() - 1); + } + if (baseValueString.endsWith(".")) { + baseValueString = baseValueString.substring(0, + baseValueString.length() - 1); + } + } + if (primaryName.isEmpty() && symbol.isEmpty()) return String.format("%s unnamed unit (= %s %s)", valueString, baseValueString, this.unit.getBase()); diff --git a/src/org/unitConverter/unit/UnitDatabase.java b/src/org/unitConverter/unit/UnitDatabase.java index 0246630..c5432f7 100644 --- a/src/org/unitConverter/unit/UnitDatabase.java +++ b/src/org/unitConverter/unit/UnitDatabase.java @@ -21,6 +21,7 @@ import java.io.File; import java.io.FileNotFoundException; import java.io.FileReader; import java.io.IOException; +import java.math.BigDecimal; import java.util.AbstractSet; import java.util.ArrayList; import java.util.Arrays; @@ -1465,7 +1466,7 @@ public final class UnitDatabase { public LinearUnitValue evaluateUnitExpression(final String expression) { Objects.requireNonNull(expression, "expression must not be null."); - // attempt to get a unit as an alias first + // attempt to get a unit as an alias, or a number with precision first if (this.containsUnitName(expression)) return this.getLinearUnitValue(expression); @@ -1598,6 +1599,7 @@ public final class UnitDatabase { } else { // get a linear unit final Unit unit = this.getUnit(name); + if (unit instanceof LinearUnit) return (LinearUnit) unit; else @@ -1615,7 +1617,15 @@ public final class UnitDatabase { * @since 2020-08-04 */ private LinearUnitValue getLinearUnitValue(final String name) { - return LinearUnitValue.getExact(this.getLinearUnit(name), 1); + 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, number.doubleValue(), uncertainty); + } catch (final NumberFormatException e) { + return LinearUnitValue.getExact(this.getLinearUnit(name), 1); + } } /** @@ -1682,7 +1692,9 @@ public final class UnitDatabase { return SI.ONE.times(value); } catch (final NumberFormatException e) { final Unit unit = this.units.get(name); - if (unit.getPrimaryName().isEmpty()) + if (unit == null) + throw new NoSuchElementException("No unit " + name); + else if (unit.getPrimaryName().isEmpty()) return unit.withName(NameSymbol.ofName(name)); else if (!unit.getPrimaryName().get().equals(name)) { final Set otherNames = new HashSet<>(unit.getOtherNames()); -- cgit v1.2.3 From 6d7d172e2e706da44c2b30177a04648671aad69e Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Wed, 26 Aug 2020 12:37:12 -0500 Subject: Added the prefix repetition rule, and the two basic rules. --- .../converterGUI/DefaultPrefixRepetitionRule.java | 42 ++ .../converterGUI/UnitConverterGUI.java | 61 +-- src/org/unitConverter/unit/SI.java | 472 ++++++++++++++------- src/org/unitConverter/unit/UnitDatabase.java | 88 +++- src/org/unitConverter/unit/UnitDatabaseTest.java | 142 ++++--- 5 files changed, 542 insertions(+), 263 deletions(-) create mode 100644 src/org/unitConverter/converterGUI/DefaultPrefixRepetitionRule.java (limited to 'src/org/unitConverter/unit/UnitDatabase.java') diff --git a/src/org/unitConverter/converterGUI/DefaultPrefixRepetitionRule.java b/src/org/unitConverter/converterGUI/DefaultPrefixRepetitionRule.java new file mode 100644 index 0000000..34d8467 --- /dev/null +++ b/src/org/unitConverter/converterGUI/DefaultPrefixRepetitionRule.java @@ -0,0 +1,42 @@ +/** + * @since 2020-08-26 + */ +package org.unitConverter.converterGUI; + +import java.util.List; +import java.util.function.Predicate; + +import org.unitConverter.unit.UnitPrefix; + +/** + * A rule that specifies whether prefix repetition is allowed + * + * @since 2020-08-26 + */ +enum DefaultPrefixRepetitionRule implements Predicate> { + NO_REPETITION { + @Override + public boolean test(List prefixes) { + return prefixes.size() <= 1; + } + }, + NO_RESTRICTION { + @Override + public boolean test(List prefixes) { + return true; + } + }, + /** + * You are allowed to have any number of Yotta/Yocto followed by possibly one + * Kilo-Zetta/Milli-Zepto followed by possibly one Deca/Hecto. Same for + * reducing prefixes, don't mix magnifying and reducing. Non-metric + * (including binary) prefixes can't be repeated. + */ + COMPLEX_REPETITION { + @Override + public boolean test(List prefixes) { + // TODO method stub + return false; + } + }; +} diff --git a/src/org/unitConverter/converterGUI/UnitConverterGUI.java b/src/org/unitConverter/converterGUI/UnitConverterGUI.java index f7c3479..eff0c47 100644 --- a/src/org/unitConverter/converterGUI/UnitConverterGUI.java +++ b/src/org/unitConverter/converterGUI/UnitConverterGUI.java @@ -149,7 +149,8 @@ final class UnitConverterGUI { this.view = view; // load initial units - this.database = new UnitDatabase(); + this.database = new UnitDatabase( + DefaultPrefixRepetitionRule.NO_RESTRICTION); Presenter.addDefaults(this.database); this.database.loadUnitsFile(new File("unitsfile.txt")); @@ -308,7 +309,6 @@ final class UnitConverterGUI { this.view.setExpressionConverterOutputText((useSlash ? "1 / " : "") + String.format("%s = %s", fromUnitString, this.getRoundedString(converted, false))); - final String toString = this.getRoundedString(converted, false); return; } else { // convert to UnitValue @@ -346,45 +346,6 @@ final class UnitConverterGUI { return this.prefixNameComparator; } - /** - * @param value value to round - * @return string of that value rounded to {@code significantDigits} - * significant digits. - * @since 2019-04-14 - * @since v0.2.0 - */ - private final String getRoundedString(final BigDecimal value) { - // round value based on rounding type - final BigDecimal roundedValue; - switch (this.roundingType) { - case DECIMAL_PLACES: - roundedValue = value.setScale(this.precision, - RoundingMode.HALF_EVEN); - break; - case SCIENTIFIC: - throw new UnsupportedOperationException("Not yet implemented."); - case SIGNIFICANT_DIGITS: - roundedValue = value.round(new MathContext(this.precision)); - break; - default: - throw new AssertionError("Invalid switch condition."); - } - - String output = roundedValue.toString(); - - // remove trailing zeroes - if (output.contains(".")) { - while (output.endsWith("0")) { - output = output.substring(0, output.length() - 1); - } - if (output.endsWith(".")) { - output = output.substring(0, output.length() - 1); - } - } - - return output; - } - /** * Like {@link LinearUnitValue#toString(boolean)}, but obeys this unit * converter's rounding settings. @@ -414,6 +375,7 @@ final class UnitConverterGUI { final BigDecimal unrounded = new BigDecimal(value.getValue()); final BigDecimal rounded; int precision = this.precision; + switch (this.roundingType) { case DECIMAL_PLACES: rounded = unrounded.setScale(precision, RoundingMode.HALF_EVEN); @@ -483,6 +445,15 @@ final class UnitConverterGUI { this.precision = precision; } + /** + * @param prefixRepetitionRule the prefixRepetitionRule to set + * @since 2020-08-26 + */ + public void setPrefixRepetitionRule( + Predicate> prefixRepetitionRule) { + this.database.setPrefixRepetitionRule(prefixRepetitionRule); + } + /** * @param roundingType the roundingType to set * @since 2020-07-16 @@ -1031,7 +1002,9 @@ final class UnitConverterGUI { final JRadioButton noRepetition = new JRadioButton( "No Repetition"); - noRepetition.setEnabled(false); + noRepetition.addActionListener( + e -> this.presenter.setPrefixRepetitionRule( + DefaultPrefixRepetitionRule.NO_REPETITION)); prefixRuleButtons.add(noRepetition); prefixRepetitionPanel.add(noRepetition, new GridBagBuilder(0, 0) @@ -1041,7 +1014,9 @@ final class UnitConverterGUI { final JRadioButton noRestriction = new JRadioButton( "No Restriction"); noRestriction.setSelected(true); - noRestriction.setEnabled(false); + noRestriction.addActionListener( + e -> this.presenter.setPrefixRepetitionRule( + DefaultPrefixRepetitionRule.NO_RESTRICTION)); prefixRuleButtons.add(noRestriction); prefixRepetitionPanel.add(noRestriction, new GridBagBuilder(0, 1) diff --git a/src/org/unitConverter/unit/SI.java b/src/org/unitConverter/unit/SI.java index 1a9eaf1..a4fbd5f 100644 --- a/src/org/unitConverter/unit/SI.java +++ b/src/org/unitConverter/unit/SI.java @@ -16,13 +16,19 @@ */ package org.unitConverter.unit; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + import org.unitConverter.math.ObjectProduct; /** - * All of the units, prefixes and dimensions that are used by the SI, as well as some outside the SI. + * All of the units, prefixes and dimensions that are used by the SI, as well as + * some outside the SI. * *

- * This class does not include prefixed units. To obtain prefixed units, use {@link LinearUnit#withPrefix}: + * This class does not include prefixed units. To obtain prefixed units, use + * {@link LinearUnit#withPrefix}: * *

  * LinearUnit KILOMETRE = SI.METRE.withPrefix(SI.KILO);
@@ -36,42 +42,61 @@ public final class SI {
 	/// dimensions used by SI units
 	// base dimensions, as BaseDimensions
 	public static final class BaseDimensions {
-		public static final BaseDimension LENGTH = BaseDimension.valueOf("Length", "L");
-		public static final BaseDimension MASS = BaseDimension.valueOf("Mass", "M");
-		public static final BaseDimension TIME = BaseDimension.valueOf("Time", "T");
-		public static final BaseDimension ELECTRIC_CURRENT = BaseDimension.valueOf("Electric Current", "I");
-		public static final BaseDimension TEMPERATURE = BaseDimension.valueOf("Temperature", "\u0398"); // theta symbol
-		public static final BaseDimension QUANTITY = BaseDimension.valueOf("Quantity", "N");
-		public static final BaseDimension LUMINOUS_INTENSITY = BaseDimension.valueOf("Luminous Intensity", "J");
-		public static final BaseDimension INFORMATION = BaseDimension.valueOf("Information", "Info"); // non-SI
-		public static final BaseDimension CURRENCY = BaseDimension.valueOf("Currency", "$$"); // non-SI
-
+		public static final BaseDimension LENGTH = BaseDimension.valueOf("Length",
+				"L");
+		public static final BaseDimension MASS = BaseDimension.valueOf("Mass",
+				"M");
+		public static final BaseDimension TIME = BaseDimension.valueOf("Time",
+				"T");
+		public static final BaseDimension ELECTRIC_CURRENT = BaseDimension
+				.valueOf("Electric Current", "I");
+		public static final BaseDimension TEMPERATURE = BaseDimension
+				.valueOf("Temperature", "\u0398"); // theta symbol
+		public static final BaseDimension QUANTITY = BaseDimension
+				.valueOf("Quantity", "N");
+		public static final BaseDimension LUMINOUS_INTENSITY = BaseDimension
+				.valueOf("Luminous Intensity", "J");
+		public static final BaseDimension INFORMATION = BaseDimension
+				.valueOf("Information", "Info"); // non-SI
+		public static final BaseDimension CURRENCY = BaseDimension
+				.valueOf("Currency", "$$"); // non-SI
+		
 		// You may NOT get SI.BaseDimensions instances!
 		private BaseDimensions() {
 			throw new AssertionError();
 		}
 	}
-
+	
 	/// base units of the SI
-	// suppressing warnings since these are the same object, but in a different form (class)
+	// suppressing warnings since these are the same object, but in a different
+	/// form (class)
 	@SuppressWarnings("hiding")
 	public static final class BaseUnits {
-		public static final BaseUnit METRE = BaseUnit.valueOf(BaseDimensions.LENGTH, "metre", "m");
-		public static final BaseUnit KILOGRAM = BaseUnit.valueOf(BaseDimensions.MASS, "kilogram", "kg");
-		public static final BaseUnit SECOND = BaseUnit.valueOf(BaseDimensions.TIME, "second", "s");
-		public static final BaseUnit AMPERE = BaseUnit.valueOf(BaseDimensions.ELECTRIC_CURRENT, "ampere", "A");
-		public static final BaseUnit KELVIN = BaseUnit.valueOf(BaseDimensions.TEMPERATURE, "kelvin", "K");
-		public static final BaseUnit MOLE = BaseUnit.valueOf(BaseDimensions.QUANTITY, "mole", "mol");
-		public static final BaseUnit CANDELA = BaseUnit.valueOf(BaseDimensions.LUMINOUS_INTENSITY, "candela", "cd");
-		public static final BaseUnit BIT = BaseUnit.valueOf(BaseDimensions.INFORMATION, "bit", "b");
-		public static final BaseUnit DOLLAR = BaseUnit.valueOf(BaseDimensions.CURRENCY, "dollar", "$");
-
+		public static final BaseUnit METRE = BaseUnit
+				.valueOf(BaseDimensions.LENGTH, "metre", "m");
+		public static final BaseUnit KILOGRAM = BaseUnit
+				.valueOf(BaseDimensions.MASS, "kilogram", "kg");
+		public static final BaseUnit SECOND = BaseUnit
+				.valueOf(BaseDimensions.TIME, "second", "s");
+		public static final BaseUnit AMPERE = BaseUnit
+				.valueOf(BaseDimensions.ELECTRIC_CURRENT, "ampere", "A");
+		public static final BaseUnit KELVIN = BaseUnit
+				.valueOf(BaseDimensions.TEMPERATURE, "kelvin", "K");
+		public static final BaseUnit MOLE = BaseUnit
+				.valueOf(BaseDimensions.QUANTITY, "mole", "mol");
+		public static final BaseUnit CANDELA = BaseUnit
+				.valueOf(BaseDimensions.LUMINOUS_INTENSITY, "candela", "cd");
+		public static final BaseUnit BIT = BaseUnit
+				.valueOf(BaseDimensions.INFORMATION, "bit", "b");
+		public static final BaseUnit DOLLAR = BaseUnit
+				.valueOf(BaseDimensions.CURRENCY, "dollar", "$");
+		
 		// You may NOT get SI.BaseUnits instances!
 		private BaseUnits() {
 			throw new AssertionError();
 		}
 	}
-
+	
 	/**
 	 * Constants that relate to the SI or other systems.
 	 * 
@@ -79,189 +104,320 @@ public final class SI {
 	 * @since 2019-11-08
 	 */
 	public static final class Constants {
-		public static final LinearUnit EARTH_GRAVITY = METRE.dividedBy(SECOND).dividedBy(SECOND).times(9.80665);
+		public static final LinearUnit EARTH_GRAVITY = METRE.dividedBy(SECOND)
+				.dividedBy(SECOND).times(9.80665);
 	}
-
+	
 	// dimensions used in the SI, as ObjectProducts
 	public static final class Dimensions {
-		public static final ObjectProduct EMPTY = ObjectProduct.empty();
-		public static final ObjectProduct LENGTH = ObjectProduct.oneOf(BaseDimensions.LENGTH);
-		public static final ObjectProduct MASS = ObjectProduct.oneOf(BaseDimensions.MASS);
-		public static final ObjectProduct TIME = ObjectProduct.oneOf(BaseDimensions.TIME);
+		public static final ObjectProduct EMPTY = ObjectProduct
+				.empty();
+		public static final ObjectProduct LENGTH = ObjectProduct
+				.oneOf(BaseDimensions.LENGTH);
+		public static final ObjectProduct MASS = ObjectProduct
+				.oneOf(BaseDimensions.MASS);
+		public static final ObjectProduct TIME = ObjectProduct
+				.oneOf(BaseDimensions.TIME);
 		public static final ObjectProduct ELECTRIC_CURRENT = ObjectProduct
 				.oneOf(BaseDimensions.ELECTRIC_CURRENT);
-		public static final ObjectProduct TEMPERATURE = ObjectProduct.oneOf(BaseDimensions.TEMPERATURE);
-		public static final ObjectProduct QUANTITY = ObjectProduct.oneOf(BaseDimensions.QUANTITY);
+		public static final ObjectProduct TEMPERATURE = ObjectProduct
+				.oneOf(BaseDimensions.TEMPERATURE);
+		public static final ObjectProduct QUANTITY = ObjectProduct
+				.oneOf(BaseDimensions.QUANTITY);
 		public static final ObjectProduct LUMINOUS_INTENSITY = ObjectProduct
 				.oneOf(BaseDimensions.LUMINOUS_INTENSITY);
-		public static final ObjectProduct INFORMATION = ObjectProduct.oneOf(BaseDimensions.INFORMATION);
-		public static final ObjectProduct CURRENCY = ObjectProduct.oneOf(BaseDimensions.CURRENCY);
+		public static final ObjectProduct INFORMATION = ObjectProduct
+				.oneOf(BaseDimensions.INFORMATION);
+		public static final ObjectProduct CURRENCY = ObjectProduct
+				.oneOf(BaseDimensions.CURRENCY);
 		// derived dimensions without named SI units
-		public static final ObjectProduct AREA = LENGTH.times(LENGTH);
-
-		public static final ObjectProduct VOLUME = AREA.times(LENGTH);
-		public static final ObjectProduct VELOCITY = LENGTH.dividedBy(TIME);
-		public static final ObjectProduct ACCELERATION = VELOCITY.dividedBy(TIME);
-		public static final ObjectProduct WAVENUMBER = EMPTY.dividedBy(LENGTH);
-		public static final ObjectProduct MASS_DENSITY = MASS.dividedBy(VOLUME);
-		public static final ObjectProduct SURFACE_DENSITY = MASS.dividedBy(AREA);
-		public static final ObjectProduct SPECIFIC_VOLUME = VOLUME.dividedBy(MASS);
-		public static final ObjectProduct CURRENT_DENSITY = ELECTRIC_CURRENT.dividedBy(AREA);
-		public static final ObjectProduct MAGNETIC_FIELD_STRENGTH = ELECTRIC_CURRENT.dividedBy(LENGTH);
-		public static final ObjectProduct CONCENTRATION = QUANTITY.dividedBy(VOLUME);
-		public static final ObjectProduct MASS_CONCENTRATION = CONCENTRATION.times(MASS);
-		public static final ObjectProduct LUMINANCE = LUMINOUS_INTENSITY.dividedBy(AREA);
-		public static final ObjectProduct REFRACTIVE_INDEX = VELOCITY.dividedBy(VELOCITY);
-		public static final ObjectProduct REFLACTIVE_PERMEABILITY = EMPTY.times(EMPTY);
-		public static final ObjectProduct ANGLE = LENGTH.dividedBy(LENGTH);
-		public static final ObjectProduct SOLID_ANGLE = AREA.dividedBy(AREA);
-
+		public static final ObjectProduct AREA = LENGTH
+				.times(LENGTH);
+		
+		public static final ObjectProduct VOLUME = AREA
+				.times(LENGTH);
+		public static final ObjectProduct VELOCITY = LENGTH
+				.dividedBy(TIME);
+		public static final ObjectProduct ACCELERATION = VELOCITY
+				.dividedBy(TIME);
+		public static final ObjectProduct WAVENUMBER = EMPTY
+				.dividedBy(LENGTH);
+		public static final ObjectProduct MASS_DENSITY = MASS
+				.dividedBy(VOLUME);
+		public static final ObjectProduct SURFACE_DENSITY = MASS
+				.dividedBy(AREA);
+		public static final ObjectProduct SPECIFIC_VOLUME = VOLUME
+				.dividedBy(MASS);
+		public static final ObjectProduct CURRENT_DENSITY = ELECTRIC_CURRENT
+				.dividedBy(AREA);
+		public static final ObjectProduct MAGNETIC_FIELD_STRENGTH = ELECTRIC_CURRENT
+				.dividedBy(LENGTH);
+		public static final ObjectProduct CONCENTRATION = QUANTITY
+				.dividedBy(VOLUME);
+		public static final ObjectProduct MASS_CONCENTRATION = CONCENTRATION
+				.times(MASS);
+		public static final ObjectProduct LUMINANCE = LUMINOUS_INTENSITY
+				.dividedBy(AREA);
+		public static final ObjectProduct REFRACTIVE_INDEX = VELOCITY
+				.dividedBy(VELOCITY);
+		public static final ObjectProduct REFLACTIVE_PERMEABILITY = EMPTY
+				.times(EMPTY);
+		public static final ObjectProduct ANGLE = LENGTH
+				.dividedBy(LENGTH);
+		public static final ObjectProduct SOLID_ANGLE = AREA
+				.dividedBy(AREA);
+		
 		// derived dimensions with named SI units
-		public static final ObjectProduct FREQUENCY = EMPTY.dividedBy(TIME);
-		public static final ObjectProduct FORCE = MASS.times(ACCELERATION);
-		public static final ObjectProduct ENERGY = FORCE.times(LENGTH);
-		public static final ObjectProduct POWER = ENERGY.dividedBy(TIME);
-		public static final ObjectProduct ELECTRIC_CHARGE = ELECTRIC_CURRENT.times(TIME);
-		public static final ObjectProduct VOLTAGE = ENERGY.dividedBy(ELECTRIC_CHARGE);
-		public static final ObjectProduct CAPACITANCE = ELECTRIC_CHARGE.dividedBy(VOLTAGE);
-		public static final ObjectProduct ELECTRIC_RESISTANCE = VOLTAGE.dividedBy(ELECTRIC_CURRENT);
-		public static final ObjectProduct ELECTRIC_CONDUCTANCE = ELECTRIC_CURRENT.dividedBy(VOLTAGE);
-		public static final ObjectProduct MAGNETIC_FLUX = VOLTAGE.times(TIME);
-		public static final ObjectProduct MAGNETIC_FLUX_DENSITY = MAGNETIC_FLUX.dividedBy(AREA);
-		public static final ObjectProduct INDUCTANCE = MAGNETIC_FLUX.dividedBy(ELECTRIC_CURRENT);
-		public static final ObjectProduct LUMINOUS_FLUX = LUMINOUS_INTENSITY.times(SOLID_ANGLE);
-		public static final ObjectProduct ILLUMINANCE = LUMINOUS_FLUX.dividedBy(AREA);
-		public static final ObjectProduct SPECIFIC_ENERGY = ENERGY.dividedBy(MASS);
-		public static final ObjectProduct CATALYTIC_ACTIVITY = QUANTITY.dividedBy(TIME);
-
+		public static final ObjectProduct FREQUENCY = EMPTY
+				.dividedBy(TIME);
+		public static final ObjectProduct FORCE = MASS
+				.times(ACCELERATION);
+		public static final ObjectProduct ENERGY = FORCE
+				.times(LENGTH);
+		public static final ObjectProduct POWER = ENERGY
+				.dividedBy(TIME);
+		public static final ObjectProduct ELECTRIC_CHARGE = ELECTRIC_CURRENT
+				.times(TIME);
+		public static final ObjectProduct VOLTAGE = ENERGY
+				.dividedBy(ELECTRIC_CHARGE);
+		public static final ObjectProduct CAPACITANCE = ELECTRIC_CHARGE
+				.dividedBy(VOLTAGE);
+		public static final ObjectProduct ELECTRIC_RESISTANCE = VOLTAGE
+				.dividedBy(ELECTRIC_CURRENT);
+		public static final ObjectProduct ELECTRIC_CONDUCTANCE = ELECTRIC_CURRENT
+				.dividedBy(VOLTAGE);
+		public static final ObjectProduct MAGNETIC_FLUX = VOLTAGE
+				.times(TIME);
+		public static final ObjectProduct MAGNETIC_FLUX_DENSITY = MAGNETIC_FLUX
+				.dividedBy(AREA);
+		public static final ObjectProduct INDUCTANCE = MAGNETIC_FLUX
+				.dividedBy(ELECTRIC_CURRENT);
+		public static final ObjectProduct LUMINOUS_FLUX = LUMINOUS_INTENSITY
+				.times(SOLID_ANGLE);
+		public static final ObjectProduct ILLUMINANCE = LUMINOUS_FLUX
+				.dividedBy(AREA);
+		public static final ObjectProduct SPECIFIC_ENERGY = ENERGY
+				.dividedBy(MASS);
+		public static final ObjectProduct CATALYTIC_ACTIVITY = QUANTITY
+				.dividedBy(TIME);
+		
 		// You may NOT get SI.Dimension instances!
 		private Dimensions() {
 			throw new AssertionError();
 		}
 	}
-
+	
 	/// The units of the SI
-	public static final LinearUnit ONE = LinearUnit.valueOf(ObjectProduct.empty(), 1);
+	public static final LinearUnit ONE = LinearUnit
+			.valueOf(ObjectProduct.empty(), 1);
 	public static final LinearUnit METRE = BaseUnits.METRE.asLinearUnit()
 			.withName(NameSymbol.of("metre", "m", "meter"));
 	public static final LinearUnit KILOGRAM = BaseUnits.KILOGRAM.asLinearUnit()
 			.withName(NameSymbol.of("kilogram", "kg"));
 	public static final LinearUnit SECOND = BaseUnits.SECOND.asLinearUnit()
 			.withName(NameSymbol.of("second", "s", "sec"));
-	public static final LinearUnit AMPERE = BaseUnits.AMPERE.asLinearUnit().withName(NameSymbol.of("ampere", "A"));
-	public static final LinearUnit KELVIN = BaseUnits.KELVIN.asLinearUnit().withName(NameSymbol.of("kelvin", "K"));
-	public static final LinearUnit MOLE = BaseUnits.MOLE.asLinearUnit().withName(NameSymbol.of("mole", "mol"));
-	public static final LinearUnit CANDELA = BaseUnits.CANDELA.asLinearUnit().withName(NameSymbol.of("candela", "cd"));
-	public static final LinearUnit BIT = BaseUnits.BIT.asLinearUnit().withName(NameSymbol.of("bit", "b"));
-	public static final LinearUnit DOLLAR = BaseUnits.DOLLAR.asLinearUnit().withName(NameSymbol.of("dollar", "$"));
-
+	public static final LinearUnit AMPERE = BaseUnits.AMPERE.asLinearUnit()
+			.withName(NameSymbol.of("ampere", "A"));
+	public static final LinearUnit KELVIN = BaseUnits.KELVIN.asLinearUnit()
+			.withName(NameSymbol.of("kelvin", "K"));
+	public static final LinearUnit MOLE = BaseUnits.MOLE.asLinearUnit()
+			.withName(NameSymbol.of("mole", "mol"));
+	public static final LinearUnit CANDELA = BaseUnits.CANDELA.asLinearUnit()
+			.withName(NameSymbol.of("candela", "cd"));
+	public static final LinearUnit BIT = BaseUnits.BIT.asLinearUnit()
+			.withName(NameSymbol.of("bit", "b"));
+	public static final LinearUnit DOLLAR = BaseUnits.DOLLAR.asLinearUnit()
+			.withName(NameSymbol.of("dollar", "$"));
+	
 	// Non-base units
-	public static final LinearUnit RADIAN = METRE.dividedBy(METRE).withName(NameSymbol.of("radian", "rad"));
-	public static final LinearUnit STERADIAN = RADIAN.times(RADIAN).withName(NameSymbol.of("steradian", "sr"));
-	public static final LinearUnit HERTZ = ONE.dividedBy(SECOND).withName(NameSymbol.of("hertz", "Hz"));
+	public static final LinearUnit RADIAN = METRE.dividedBy(METRE)
+			.withName(NameSymbol.of("radian", "rad"));
+	public static final LinearUnit STERADIAN = RADIAN.times(RADIAN)
+			.withName(NameSymbol.of("steradian", "sr"));
+	public static final LinearUnit HERTZ = ONE.dividedBy(SECOND)
+			.withName(NameSymbol.of("hertz", "Hz"));
 	// for periodic phenomena
-	public static final LinearUnit NEWTON = KILOGRAM.times(METRE).dividedBy(SECOND.times(SECOND))
+	public static final LinearUnit NEWTON = KILOGRAM.times(METRE)
+			.dividedBy(SECOND.times(SECOND))
 			.withName(NameSymbol.of("newton", "N"));
 	public static final LinearUnit PASCAL = NEWTON.dividedBy(METRE.times(METRE))
 			.withName(NameSymbol.of("pascal", "Pa"));
-	public static final LinearUnit JOULE = NEWTON.times(METRE).withName(NameSymbol.of("joule", "J"));
-	public static final LinearUnit WATT = JOULE.dividedBy(SECOND).withName(NameSymbol.of("watt", "W"));
-	public static final LinearUnit COULOMB = AMPERE.times(SECOND).withName(NameSymbol.of("coulomb", "C"));
-	public static final LinearUnit VOLT = JOULE.dividedBy(COULOMB).withName(NameSymbol.of("volt", "V"));
-	public static final LinearUnit FARAD = COULOMB.dividedBy(VOLT).withName(NameSymbol.of("farad", "F"));
-	public static final LinearUnit OHM = VOLT.dividedBy(AMPERE).withName(NameSymbol.of("ohm", "\u03A9")); // omega
-	public static final LinearUnit SIEMENS = ONE.dividedBy(OHM).withName(NameSymbol.of("siemens", "S"));
-	public static final LinearUnit WEBER = VOLT.times(SECOND).withName(NameSymbol.of("weber", "Wb"));
-	public static final LinearUnit TESLA = WEBER.dividedBy(METRE.times(METRE)).withName(NameSymbol.of("tesla", "T"));
-	public static final LinearUnit HENRY = WEBER.dividedBy(AMPERE).withName(NameSymbol.of("henry", "H"));
-	public static final LinearUnit LUMEN = CANDELA.times(STERADIAN).withName(NameSymbol.of("lumen", "lm"));
-	public static final LinearUnit LUX = LUMEN.dividedBy(METRE.times(METRE)).withName(NameSymbol.of("lux", "lx"));
-	public static final LinearUnit BEQUEREL = ONE.dividedBy(SECOND).withName(NameSymbol.of("bequerel", "Bq"));
+	public static final LinearUnit JOULE = NEWTON.times(METRE)
+			.withName(NameSymbol.of("joule", "J"));
+	public static final LinearUnit WATT = JOULE.dividedBy(SECOND)
+			.withName(NameSymbol.of("watt", "W"));
+	public static final LinearUnit COULOMB = AMPERE.times(SECOND)
+			.withName(NameSymbol.of("coulomb", "C"));
+	public static final LinearUnit VOLT = JOULE.dividedBy(COULOMB)
+			.withName(NameSymbol.of("volt", "V"));
+	public static final LinearUnit FARAD = COULOMB.dividedBy(VOLT)
+			.withName(NameSymbol.of("farad", "F"));
+	public static final LinearUnit OHM = VOLT.dividedBy(AMPERE)
+			.withName(NameSymbol.of("ohm", "\u03A9")); // omega
+	public static final LinearUnit SIEMENS = ONE.dividedBy(OHM)
+			.withName(NameSymbol.of("siemens", "S"));
+	public static final LinearUnit WEBER = VOLT.times(SECOND)
+			.withName(NameSymbol.of("weber", "Wb"));
+	public static final LinearUnit TESLA = WEBER.dividedBy(METRE.times(METRE))
+			.withName(NameSymbol.of("tesla", "T"));
+	public static final LinearUnit HENRY = WEBER.dividedBy(AMPERE)
+			.withName(NameSymbol.of("henry", "H"));
+	public static final LinearUnit LUMEN = CANDELA.times(STERADIAN)
+			.withName(NameSymbol.of("lumen", "lm"));
+	public static final LinearUnit LUX = LUMEN.dividedBy(METRE.times(METRE))
+			.withName(NameSymbol.of("lux", "lx"));
+	public static final LinearUnit BEQUEREL = ONE.dividedBy(SECOND)
+			.withName(NameSymbol.of("bequerel", "Bq"));
 	// for activity referred to a nucleotide
-	public static final LinearUnit GRAY = JOULE.dividedBy(KILOGRAM).withName(NameSymbol.of("grey", "Gy"));
+	public static final LinearUnit GRAY = JOULE.dividedBy(KILOGRAM)
+			.withName(NameSymbol.of("grey", "Gy"));
 	// for absorbed dose
-	public static final LinearUnit SIEVERT = JOULE.dividedBy(KILOGRAM).withName(NameSymbol.of("sievert", "Sv"));
+	public static final LinearUnit SIEVERT = JOULE.dividedBy(KILOGRAM)
+			.withName(NameSymbol.of("sievert", "Sv"));
 	// for dose equivalent
-	public static final LinearUnit KATAL = MOLE.dividedBy(SECOND).withName(NameSymbol.of("katal", "kat"));
-
+	public static final LinearUnit KATAL = MOLE.dividedBy(SECOND)
+			.withName(NameSymbol.of("katal", "kat"));
+	
 	// common derived units included for convenience
-	public static final LinearUnit GRAM = KILOGRAM.dividedBy(1000).withName(NameSymbol.of("gram", "g"));
+	public static final LinearUnit GRAM = KILOGRAM.dividedBy(1000)
+			.withName(NameSymbol.of("gram", "g"));
 	public static final LinearUnit SQUARE_METRE = METRE.toExponent(2)
-			.withName(NameSymbol.of("square metre", "m^2", "square meter", "metre squared", "meter squared"));
+			.withName(NameSymbol.of("square metre", "m^2", "square meter",
+					"metre squared", "meter squared"));
 	public static final LinearUnit CUBIC_METRE = METRE.toExponent(3)
-			.withName(NameSymbol.of("cubic metre", "m^3", "cubic meter", "metre cubed", "meter cubed"));
+			.withName(NameSymbol.of("cubic metre", "m^3", "cubic meter",
+					"metre cubed", "meter cubed"));
 	public static final LinearUnit METRE_PER_SECOND = METRE.dividedBy(SECOND)
-			.withName(NameSymbol.of("metre per second", "m/s", "meter per second"));
-
+			.withName(
+					NameSymbol.of("metre per second", "m/s", "meter per second"));
+	
 	// Non-SI units included for convenience
 	public static final Unit CELSIUS = Unit
-			.fromConversionFunctions(KELVIN.getBase(), tempK -> tempK - 273.15, tempC -> tempC + 273.15)
+			.fromConversionFunctions(KELVIN.getBase(), tempK -> tempK - 273.15,
+					tempC -> tempC + 273.15)
 			.withName(NameSymbol.of("degree Celsius", "\u00B0C"));
-	public static final LinearUnit MINUTE = SECOND.times(60).withName(NameSymbol.of("minute", "min"));
-	public static final LinearUnit HOUR = MINUTE.times(60).withName(NameSymbol.of("hour", "h", "hr"));
-	public static final LinearUnit DAY = HOUR.times(60).withName(NameSymbol.of("day", "d"));
-	public static final LinearUnit KILOMETRE_PER_HOUR = METRE.times(1000).dividedBy(HOUR)
-			.withName(NameSymbol.of("kilometre per hour", "km/h", "kilometer per hour"));
+	public static final LinearUnit MINUTE = SECOND.times(60)
+			.withName(NameSymbol.of("minute", "min"));
+	public static final LinearUnit HOUR = MINUTE.times(60)
+			.withName(NameSymbol.of("hour", "h", "hr"));
+	public static final LinearUnit DAY = HOUR.times(60)
+			.withName(NameSymbol.of("day", "d"));
+	public static final LinearUnit KILOMETRE_PER_HOUR = METRE.times(1000)
+			.dividedBy(HOUR).withName(NameSymbol.of("kilometre per hour", "km/h",
+					"kilometer per hour"));
 	public static final LinearUnit DEGREE = RADIAN.times(360 / (2 * Math.PI))
 			.withName(NameSymbol.of("degree", "\u00B0", "deg"));
-	public static final LinearUnit ARCMINUTE = DEGREE.dividedBy(60).withName(NameSymbol.of("arcminute", "arcmin"));
-	public static final LinearUnit ARCSECOND = ARCMINUTE.dividedBy(60).withName(NameSymbol.of("arcsecond", "arcsec"));
-	public static final LinearUnit ASTRONOMICAL_UNIT = METRE.times(149597870700.0)
+	public static final LinearUnit ARCMINUTE = DEGREE.dividedBy(60)
+			.withName(NameSymbol.of("arcminute", "arcmin"));
+	public static final LinearUnit ARCSECOND = ARCMINUTE.dividedBy(60)
+			.withName(NameSymbol.of("arcsecond", "arcsec"));
+	public static final LinearUnit ASTRONOMICAL_UNIT = METRE
+			.times(149597870700.0)
 			.withName(NameSymbol.of("astronomical unit", "au"));
-	public static final LinearUnit PARSEC = ASTRONOMICAL_UNIT.dividedBy(ARCSECOND)
-			.withName(NameSymbol.of("parsec", "pc"));
-	public static final LinearUnit HECTARE = METRE.times(METRE).times(10000.0).withName(NameSymbol.of("hectare", "ha"));
-	public static final LinearUnit LITRE = METRE.times(METRE).times(METRE).dividedBy(1000.0)
-			.withName(NameSymbol.of("litre", "L", "l", "liter"));
-	public static final LinearUnit TONNE = KILOGRAM.times(1000.0).withName(NameSymbol.of("tonne", "t", "metric ton"));
+	public static final LinearUnit PARSEC = ASTRONOMICAL_UNIT
+			.dividedBy(ARCSECOND).withName(NameSymbol.of("parsec", "pc"));
+	public static final LinearUnit HECTARE = METRE.times(METRE).times(10000.0)
+			.withName(NameSymbol.of("hectare", "ha"));
+	public static final LinearUnit LITRE = METRE.times(METRE).times(METRE)
+			.dividedBy(1000.0).withName(NameSymbol.of("litre", "L", "l", "liter"));
+	public static final LinearUnit TONNE = KILOGRAM.times(1000.0)
+			.withName(NameSymbol.of("tonne", "t", "metric ton"));
 	public static final LinearUnit DALTON = KILOGRAM.times(1.660539040e-27)
-			.withName(NameSymbol.of("dalton", "Da", "atomic unit", "u")); // approximate value
+			.withName(NameSymbol.of("dalton", "Da", "atomic unit", "u")); // approximate
+																								// value
 	public static final LinearUnit ELECTRONVOLT = JOULE.times(1.602176634e-19)
 			.withName(NameSymbol.of("electron volt", "eV"));
-	public static final LinearUnit BYTE = BIT.times(8).withName(NameSymbol.of("byte", "B"));
-	public static final Unit NEPER = Unit
-			.fromConversionFunctions(ONE.getBase(), pr -> 0.5 * Math.log(pr), Np -> Math.exp(2 * Np))
+	public static final LinearUnit BYTE = BIT.times(8)
+			.withName(NameSymbol.of("byte", "B"));
+	public static final Unit NEPER = Unit.fromConversionFunctions(ONE.getBase(),
+			pr -> 0.5 * Math.log(pr), Np -> Math.exp(2 * Np))
 			.withName(NameSymbol.of("neper", "Np"));
-	public static final Unit BEL = Unit
-			.fromConversionFunctions(ONE.getBase(), pr -> Math.log10(pr), dB -> Math.pow(10, dB))
+	public static final Unit BEL = Unit.fromConversionFunctions(ONE.getBase(),
+			pr -> Math.log10(pr), dB -> Math.pow(10, dB))
 			.withName(NameSymbol.of("bel", "B"));
 	public static final Unit DECIBEL = Unit
-			.fromConversionFunctions(ONE.getBase(), pr -> 10 * Math.log10(pr), dB -> Math.pow(10, dB / 10))
+			.fromConversionFunctions(ONE.getBase(), pr -> 10 * Math.log10(pr),
+					dB -> Math.pow(10, dB / 10))
 			.withName(NameSymbol.of("decibel", "dB"));
-
+	
 	/// The prefixes of the SI
 	// expanding decimal prefixes
-	public static final UnitPrefix KILO = UnitPrefix.valueOf(1e3).withName(NameSymbol.of("kilo", "k", "K"));
-	public static final UnitPrefix MEGA = UnitPrefix.valueOf(1e6).withName(NameSymbol.of("mega", "M"));
-	public static final UnitPrefix GIGA = UnitPrefix.valueOf(1e9).withName(NameSymbol.of("giga", "G"));
-	public static final UnitPrefix TERA = UnitPrefix.valueOf(1e12).withName(NameSymbol.of("tera", "T"));
-	public static final UnitPrefix PETA = UnitPrefix.valueOf(1e15).withName(NameSymbol.of("peta", "P"));
-	public static final UnitPrefix EXA = UnitPrefix.valueOf(1e18).withName(NameSymbol.of("exa", "E"));
-	public static final UnitPrefix ZETTA = UnitPrefix.valueOf(1e21).withName(NameSymbol.of("zetta", "Z"));
-	public static final UnitPrefix YOTTA = UnitPrefix.valueOf(1e24).withName(NameSymbol.of("yotta", "Y"));
-
+	public static final UnitPrefix KILO = UnitPrefix.valueOf(1e3)
+			.withName(NameSymbol.of("kilo", "k", "K"));
+	public static final UnitPrefix MEGA = UnitPrefix.valueOf(1e6)
+			.withName(NameSymbol.of("mega", "M"));
+	public static final UnitPrefix GIGA = UnitPrefix.valueOf(1e9)
+			.withName(NameSymbol.of("giga", "G"));
+	public static final UnitPrefix TERA = UnitPrefix.valueOf(1e12)
+			.withName(NameSymbol.of("tera", "T"));
+	public static final UnitPrefix PETA = UnitPrefix.valueOf(1e15)
+			.withName(NameSymbol.of("peta", "P"));
+	public static final UnitPrefix EXA = UnitPrefix.valueOf(1e18)
+			.withName(NameSymbol.of("exa", "E"));
+	public static final UnitPrefix ZETTA = UnitPrefix.valueOf(1e21)
+			.withName(NameSymbol.of("zetta", "Z"));
+	public static final UnitPrefix YOTTA = UnitPrefix.valueOf(1e24)
+			.withName(NameSymbol.of("yotta", "Y"));
+	
 	// contracting decimal prefixes
-	public static final UnitPrefix MILLI = UnitPrefix.valueOf(1e-3).withName(NameSymbol.of("milli", "m"));
-	public static final UnitPrefix MICRO = UnitPrefix.valueOf(1e-6).withName(NameSymbol.of("micro", "\u03BC", "u")); // mu
-	public static final UnitPrefix NANO = UnitPrefix.valueOf(1e-9).withName(NameSymbol.of("nano", "n"));
-	public static final UnitPrefix PICO = UnitPrefix.valueOf(1e-12).withName(NameSymbol.of("pico", "p"));
-	public static final UnitPrefix FEMTO = UnitPrefix.valueOf(1e-15).withName(NameSymbol.of("femto", "f"));
-	public static final UnitPrefix ATTO = UnitPrefix.valueOf(1e-18).withName(NameSymbol.of("atto", "a"));
-	public static final UnitPrefix ZEPTO = UnitPrefix.valueOf(1e-21).withName(NameSymbol.of("zepto", "z"));
-	public static final UnitPrefix YOCTO = UnitPrefix.valueOf(1e-24).withName(NameSymbol.of("yocto", "y"));
-
+	public static final UnitPrefix MILLI = UnitPrefix.valueOf(1e-3)
+			.withName(NameSymbol.of("milli", "m"));
+	public static final UnitPrefix MICRO = UnitPrefix.valueOf(1e-6)
+			.withName(NameSymbol.of("micro", "\u03BC", "u")); // mu
+	public static final UnitPrefix NANO = UnitPrefix.valueOf(1e-9)
+			.withName(NameSymbol.of("nano", "n"));
+	public static final UnitPrefix PICO = UnitPrefix.valueOf(1e-12)
+			.withName(NameSymbol.of("pico", "p"));
+	public static final UnitPrefix FEMTO = UnitPrefix.valueOf(1e-15)
+			.withName(NameSymbol.of("femto", "f"));
+	public static final UnitPrefix ATTO = UnitPrefix.valueOf(1e-18)
+			.withName(NameSymbol.of("atto", "a"));
+	public static final UnitPrefix ZEPTO = UnitPrefix.valueOf(1e-21)
+			.withName(NameSymbol.of("zepto", "z"));
+	public static final UnitPrefix YOCTO = UnitPrefix.valueOf(1e-24)
+			.withName(NameSymbol.of("yocto", "y"));
+	
 	// prefixes that don't match the pattern of thousands
-	public static final UnitPrefix DEKA = UnitPrefix.valueOf(1e1).withName(NameSymbol.of("deka", "da", "deca", "D"));
-	public static final UnitPrefix HECTO = UnitPrefix.valueOf(1e2).withName(NameSymbol.of("hecto", "h", "H", "hekto"));
-	public static final UnitPrefix DECI = UnitPrefix.valueOf(1e-1).withName(NameSymbol.of("deci", "d"));
-	public static final UnitPrefix CENTI = UnitPrefix.valueOf(1e-2).withName(NameSymbol.of("centi", "c"));
-	public static final UnitPrefix KIBI = UnitPrefix.valueOf(1024).withName(NameSymbol.of("kibi", "Ki"));
-	public static final UnitPrefix MEBI = KIBI.times(1024).withName(NameSymbol.of("mebi", "Mi"));
-	public static final UnitPrefix GIBI = MEBI.times(1024).withName(NameSymbol.of("gibi", "Gi"));
-	public static final UnitPrefix TEBI = GIBI.times(1024).withName(NameSymbol.of("tebi", "Ti"));
-	public static final UnitPrefix PEBI = TEBI.times(1024).withName(NameSymbol.of("pebi", "Pi"));
-	public static final UnitPrefix EXBI = PEBI.times(1024).withName(NameSymbol.of("exbi", "Ei"));
-
+	public static final UnitPrefix DEKA = UnitPrefix.valueOf(1e1)
+			.withName(NameSymbol.of("deka", "da", "deca", "D"));
+	public static final UnitPrefix HECTO = UnitPrefix.valueOf(1e2)
+			.withName(NameSymbol.of("hecto", "h", "H", "hekto"));
+	public static final UnitPrefix DECI = UnitPrefix.valueOf(1e-1)
+			.withName(NameSymbol.of("deci", "d"));
+	public static final UnitPrefix CENTI = UnitPrefix.valueOf(1e-2)
+			.withName(NameSymbol.of("centi", "c"));
+	public static final UnitPrefix KIBI = UnitPrefix.valueOf(1024)
+			.withName(NameSymbol.of("kibi", "Ki"));
+	public static final UnitPrefix MEBI = KIBI.times(1024)
+			.withName(NameSymbol.of("mebi", "Mi"));
+	public static final UnitPrefix GIBI = MEBI.times(1024)
+			.withName(NameSymbol.of("gibi", "Gi"));
+	public static final UnitPrefix TEBI = GIBI.times(1024)
+			.withName(NameSymbol.of("tebi", "Ti"));
+	public static final UnitPrefix PEBI = TEBI.times(1024)
+			.withName(NameSymbol.of("pebi", "Pi"));
+	public static final UnitPrefix EXBI = PEBI.times(1024)
+			.withName(NameSymbol.of("exbi", "Ei"));
+	
+	// sets of prefixes
+	public static final Set ALL_PREFIXES = new HashSet<>(
+			Arrays.asList(DEKA, HECTO, KILO, MEGA, GIGA, TERA, PETA, EXA, ZETTA,
+					YOTTA, DECI, CENTI, MILLI, MICRO, NANO, PICO, FEMTO, ATTO, ZEPTO,
+					YOCTO, KIBI, MEBI, GIBI, TEBI, PEBI, EXBI));
+	public static final Set DECIMAL_PREFIXES = new HashSet<>(
+			Arrays.asList(DEKA, HECTO, KILO, MEGA, GIGA, TERA, PETA, EXA, ZETTA,
+					YOTTA, DECI, CENTI, MILLI, MICRO, NANO, PICO, FEMTO, ATTO, ZEPTO,
+					YOCTO));
+	public static final Set THOUSAND_PREFIXES = new HashSet<>(
+			Arrays.asList(KILO, MEGA, GIGA, TERA, PETA, EXA, ZETTA, YOTTA, MILLI,
+					MICRO, NANO, PICO, FEMTO, ATTO, ZEPTO, YOCTO));
+	public static final Set MAGNIFYING_PREFIXES = new HashSet<>(
+			Arrays.asList(DEKA, HECTO, KILO, MEGA, GIGA, TERA, PETA, EXA, ZETTA,
+					YOTTA, KIBI, MEBI, GIBI, TEBI, PEBI, EXBI));
+	public static final Set REDUCING_PREFIXES = new HashSet<>(
+			Arrays.asList(DECI, CENTI, MILLI, MICRO, NANO, PICO, FEMTO, ATTO,
+					ZEPTO, YOCTO));
+	
 	// You may NOT get SI instances!
 	private SI() {
 		throw new AssertionError();
diff --git a/src/org/unitConverter/unit/UnitDatabase.java b/src/org/unitConverter/unit/UnitDatabase.java
index c5432f7..56846a1 100644
--- a/src/org/unitConverter/unit/UnitDatabase.java
+++ b/src/org/unitConverter/unit/UnitDatabase.java
@@ -42,6 +42,7 @@ 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;
@@ -1178,6 +1179,18 @@ public final class UnitDatabase {
 	 */
 	private final Map units;
 	
+	/**
+	 * The rule that specifies when prefix repetition is allowed. It takes in one
+	 * argument: a list of the prefixes being applied to the unit
+	 * 

+ * The prefixes are inputted in application order. This means that + * testing whether "kilomegagigametre" is a valid unit is equivalent to + * running the following code (assuming all variables are defined correctly): + *
+ * {@code prefixRepetitionRule.test(Arrays.asList(giga, mega, kilo))} + */ + private Predicate> prefixRepetitionRule; + /** * A parser that can parse unit expressions. * @@ -1240,10 +1253,25 @@ 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> 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()))); } /** @@ -1644,6 +1672,47 @@ public final class UnitDatabase { } } + /** + * Gets all of the prefixes that are on a unit name, in application order. + * + * @param unitName name of unit + * @return prefixes + * @since 2020-08-26 + */ + List getPrefixesFromName(final String unitName) { + if (this.prefixlessUnits.containsKey(unitName)) + return new ArrayList<>(); + else { + // find the longest prefix + 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.containsUnitName(rest) + && this.getUnit(rest) instanceof LinearUnit) { + longestPrefix = prefixName; + longestLength = prefixName.length(); + } + } + } + + final List prefixes = this + .getPrefixesFromName(unitName.substring(longestLength)); + prefixes.add(this.getPrefix(longestPrefix)); + return prefixes; + } + } + /** * Gets a unit prefix from a prefix expression *

@@ -1678,6 +1747,14 @@ public final class UnitDatabase { return this.prefixExpressionParser.parseExpression(modifiedExpression); } + /** + * @return the prefixRepetitionRule + * @since 2020-08-26 + */ + public final Predicate> getPrefixRepetitionRule() { + return this.prefixRepetitionRule; + } + /** * Gets a unit from the database from its name, looking for prefixes. * @@ -1862,6 +1939,15 @@ public final class UnitDatabase { return Collections.unmodifiableMap(this.prefixes); } + /** + * @param prefixRepetitionRule the prefixRepetitionRule to set + * @since 2020-08-26 + */ + public final void setPrefixRepetitionRule( + Predicate> prefixRepetitionRule) { + this.prefixRepetitionRule = prefixRepetitionRule; + } + /** * @return a string stating the number of units, prefixes and dimensions in * the database diff --git a/src/org/unitConverter/unit/UnitDatabaseTest.java b/src/org/unitConverter/unit/UnitDatabaseTest.java index 164172b..96a0b83 100644 --- a/src/org/unitConverter/unit/UnitDatabaseTest.java +++ b/src/org/unitConverter/unit/UnitDatabaseTest.java @@ -18,17 +18,22 @@ package org.unitConverter.unit; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; +import java.util.Arrays; import java.util.Iterator; +import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.NoSuchElementException; import org.junit.jupiter.api.Test; /** - * A test for the {@link UnitDatabase} class. This is NOT part of this program's public API. + * A test for the {@link UnitDatabase} class. This is NOT part of this program's + * public API. * * @author Adrien Hopkins * @since 2019-04-14 @@ -39,23 +44,26 @@ class UnitDatabaseTest { private static final Unit U = SI.METRE; private static final Unit V = SI.KILOGRAM; private static final Unit W = SI.SECOND; - + // used for testing expressions // J = U^2 * V / W^2 - private static final LinearUnit J = SI.KILOGRAM.times(SI.METRE.toExponent(2)).dividedBy(SI.SECOND.toExponent(2)); + private static final LinearUnit J = SI.KILOGRAM.times(SI.METRE.toExponent(2)) + .dividedBy(SI.SECOND.toExponent(2)); private static final LinearUnit K = SI.KELVIN; - - private static final Unit NONLINEAR = Unit.fromConversionFunctions(SI.METRE.getBase(), o -> o + 1, o -> o - 1); - + + private static final Unit NONLINEAR = Unit + .fromConversionFunctions(SI.METRE.getBase(), o -> o + 1, o -> o - 1); + // make the prefix values prime so I can tell which multiplications were made private static final UnitPrefix A = UnitPrefix.valueOf(2); private static final UnitPrefix B = UnitPrefix.valueOf(3); private static final UnitPrefix C = UnitPrefix.valueOf(5); private static final UnitPrefix AB = UnitPrefix.valueOf(7); private static final UnitPrefix BC = UnitPrefix.valueOf(11); - + /** - * Confirms that operations that shouldn't function for infinite databases throw an {@code IllegalStateException}. + * Confirms that operations that shouldn't function for infinite databases + * throw an {@code IllegalStateException}. * * @since 2019-05-03 */ @@ -63,14 +71,14 @@ class UnitDatabaseTest { public void testInfiniteSetExceptions() { // load units final UnitDatabase infiniteDatabase = new UnitDatabase(); - + infiniteDatabase.addUnit("J", J); infiniteDatabase.addUnit("K", K); - + infiniteDatabase.addPrefix("A", A); infiniteDatabase.addPrefix("B", B); infiniteDatabase.addPrefix("C", C); - + { boolean exceptionThrown = false; try { @@ -84,7 +92,7 @@ class UnitDatabaseTest { } } } - + { boolean exceptionThrown = false; try { @@ -99,7 +107,7 @@ class UnitDatabaseTest { } } } - + /** * Test that prefixes correctly apply to units. * @@ -109,23 +117,28 @@ class UnitDatabaseTest { @Test public void testPrefixes() { final UnitDatabase database = new UnitDatabase(); - + database.addUnit("U", U); database.addUnit("V", V); database.addUnit("W", W); - + database.addPrefix("A", A); database.addPrefix("B", B); database.addPrefix("C", C); - + + // test the getPrefixesFromName method + final List expected = Arrays.asList(C, B, A); + assertEquals(expected, database.getPrefixesFromName("ABCU")); + // get the product final Unit abcuNonlinear = database.getUnit("ABCU"); assert abcuNonlinear instanceof LinearUnit; - + final LinearUnit abcu = (LinearUnit) abcuNonlinear; - assertEquals(A.getMultiplier() * B.getMultiplier() * C.getMultiplier(), abcu.getConversionFactor(), 1e-15); + assertEquals(A.getMultiplier() * B.getMultiplier() * C.getMultiplier(), + abcu.getConversionFactor(), 1e-15); } - + /** * Tests the functionnalites of the prefixless unit map. * @@ -140,21 +153,22 @@ class UnitDatabaseTest { public void testPrefixlessUnitMap() { final UnitDatabase database = new UnitDatabase(); final Map prefixlessUnits = database.unitMapPrefixless(); - + database.addUnit("U", U); database.addUnit("V", V); database.addUnit("W", W); - + // this should work because the map should be an auto-updating view assertTrue(prefixlessUnits.containsKey("U")); assertFalse(prefixlessUnits.containsKey("Z")); - + assertTrue(prefixlessUnits.containsValue(U)); assertFalse(prefixlessUnits.containsValue(NONLINEAR)); } - + /** - * Tests that the database correctly stores and retrieves units, ignoring prefixes. + * Tests that the database correctly stores and retrieves units, ignoring + * prefixes. * * @since 2019-04-14 * @since v0.2.0 @@ -162,18 +176,18 @@ class UnitDatabaseTest { @Test public void testPrefixlessUnits() { final UnitDatabase database = new UnitDatabase(); - + database.addUnit("U", U); database.addUnit("V", V); database.addUnit("W", W); - + assertTrue(database.containsUnitName("U")); assertFalse(database.containsUnitName("Z")); - + assertEquals(U, database.getUnit("U")); - assertEquals(null, database.getUnit("Z")); + assertThrows(NoSuchElementException.class, () -> database.getUnit("Z")); } - + /** * Test that unit expressions return the correct value. * @@ -184,30 +198,31 @@ class UnitDatabaseTest { public void testUnitExpressions() { // load units final UnitDatabase database = new UnitDatabase(); - + database.addUnit("U", U); database.addUnit("V", V); database.addUnit("W", W); database.addUnit("fj", J.times(5)); database.addUnit("ej", J.times(8)); - + database.addPrefix("A", A); database.addPrefix("B", B); database.addPrefix("C", C); - + // first test - test prefixes and operations - final Unit expected1 = J.withPrefix(A).withPrefix(B).withPrefix(C).withPrefix(C); + final Unit expected1 = J.withPrefix(A).withPrefix(B).withPrefix(C) + .withPrefix(C); final Unit actual1 = database.getUnitFromExpression("ABV * CU^2 / W / W"); - + assertEquals(expected1, actual1); - + // second test - test addition and subtraction final Unit expected2 = J.times(58); final Unit actual2 = database.getUnitFromExpression("2 fj + 6 ej"); - + assertEquals(expected2, actual2); } - + /** * Tests both the unit name iterator and the name-unit entry iterator * @@ -218,59 +233,64 @@ class UnitDatabaseTest { public void testUnitIterator() { // load units final UnitDatabase database = new UnitDatabase(); - + database.addUnit("J", J); database.addUnit("K", K); - + database.addPrefix("A", A); database.addPrefix("B", B); database.addPrefix("C", C); - + final int NUM_UNITS = database.unitMapPrefixless().size(); final int NUM_PREFIXES = database.prefixMap().size(); - - final Iterator nameIterator = database.unitMap().keySet().iterator(); - final Iterator> entryIterator = database.unitMap().entrySet().iterator(); - + + final Iterator nameIterator = database.unitMap().keySet() + .iterator(); + final Iterator> entryIterator = database.unitMap() + .entrySet().iterator(); + int expectedLength = 1; int unitsWithThisLengthSoFar = 0; - + // loop 1000 times for (int i = 0; i < 1000; i++) { // expected length of next - if (unitsWithThisLengthSoFar >= NUM_UNITS * (int) Math.pow(NUM_PREFIXES, expectedLength - 1)) { + if (unitsWithThisLengthSoFar >= NUM_UNITS + * (int) Math.pow(NUM_PREFIXES, expectedLength - 1)) { expectedLength++; unitsWithThisLengthSoFar = 0; } - + // test that stuff is valid final String nextName = nameIterator.next(); final Unit nextUnit = database.getUnit(nextName); final Entry nextEntry = entryIterator.next(); - + assertEquals(expectedLength, nextName.length()); assertEquals(nextName, nextEntry.getKey()); assertEquals(nextUnit, nextEntry.getValue()); - + unitsWithThisLengthSoFar++; } - + // test toString for consistency final String entryIteratorString = entryIterator.toString(); for (int i = 0; i < 3; i++) { assertEquals(entryIteratorString, entryIterator.toString()); } - + final String nameIteratorString = nameIterator.toString(); for (int i = 0; i < 3; i++) { assertEquals(nameIteratorString, nameIterator.toString()); } } - + /** - * Determine, given a unit name that could mean multiple things, which meaning is chosen. + * Determine, given a unit name that could mean multiple things, which + * meaning is chosen. *

- * For example, "ABCU" could mean "A-B-C-U", "AB-C-U", or "A-BC-U". In this case, "AB-C-U" is the correct choice. + * For example, "ABCU" could mean "A-B-C-U", "AB-C-U", or "A-BC-U". In this + * case, "AB-C-U" is the correct choice. *

* * @since 2019-04-14 @@ -280,28 +300,28 @@ class UnitDatabaseTest { public void testUnitPrefixCombinations() { // load units final UnitDatabase database = new UnitDatabase(); - + database.addUnit("J", J); - + database.addPrefix("A", A); database.addPrefix("B", B); database.addPrefix("C", C); database.addPrefix("AB", AB); database.addPrefix("BC", BC); - + // test 1 - AB-C-J vs A-BC-J vs A-B-C-J final Unit expected1 = J.withPrefix(AB).withPrefix(C); final Unit actual1 = database.getUnit("ABCJ"); - + assertEquals(expected1, actual1); - + // test 2 - ABC-J vs AB-CJ vs AB-C-J database.addUnit("CJ", J.times(13)); database.addPrefix("ABC", UnitPrefix.valueOf(17)); - + final Unit expected2 = J.times(17); final Unit actual2 = database.getUnit("ABCJ"); - + assertEquals(expected2, actual2); } } -- cgit v1.2.3 From 0245594222bfa0bd9a47d8326ed323c7356ac27c Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Thu, 27 Aug 2020 07:06:35 -0500 Subject: Added Complex Repetition. --- .../converterGUI/DefaultPrefixRepetitionRule.java | 57 +++- .../converterGUI/UnitConverterGUI.java | 6 +- .../math/ConditionalExistenceCollections.java | 295 +++++++++++++-------- src/org/unitConverter/unit/UnitDatabase.java | 43 ++- src/org/unitConverter/unit/UnitDatabaseTest.java | 44 +-- 5 files changed, 268 insertions(+), 177 deletions(-) (limited to 'src/org/unitConverter/unit/UnitDatabase.java') diff --git a/src/org/unitConverter/converterGUI/DefaultPrefixRepetitionRule.java b/src/org/unitConverter/converterGUI/DefaultPrefixRepetitionRule.java index 34d8467..bdc3a2e 100644 --- a/src/org/unitConverter/converterGUI/DefaultPrefixRepetitionRule.java +++ b/src/org/unitConverter/converterGUI/DefaultPrefixRepetitionRule.java @@ -6,6 +6,7 @@ package org.unitConverter.converterGUI; import java.util.List; import java.util.function.Predicate; +import org.unitConverter.unit.SI; import org.unitConverter.unit.UnitPrefix; /** @@ -35,8 +36,60 @@ enum DefaultPrefixRepetitionRule implements Predicate> { COMPLEX_REPETITION { @Override public boolean test(List prefixes) { - // TODO method stub - return false; + // determine whether we are magnifying or reducing + final boolean magnifying; + if (prefixes.isEmpty()) + return true; + else if (prefixes.get(0).getMultiplier() > 1) { + magnifying = true; + } else { + magnifying = false; + } + + // if the first prefix is non-metric (including binary prefixes), + // assume we are using non-metric prefixes + // non-metric prefixes are allowed, but can't be repeated. + if (!SI.DECIMAL_PREFIXES.contains(prefixes.get(0))) + return NO_REPETITION.test(prefixes); + + int part = 0; // 0=yotta/yoctos, 1=kilo-zetta/milli-zepto, + // 2=deka,hecto,deci,centi + + for (final UnitPrefix prefix : prefixes) { + // check that the current prefix is metric and appropriately + // magnifying/reducing + if (!SI.DECIMAL_PREFIXES.contains(prefix)) + return false; + if (magnifying != prefix.getMultiplier() > 1) + return false; + + // check if the current prefix is correct + // since part is set *after* this check, part designates the state + // of the *previous* prefix + switch (part) { + case 0: + // do nothing, any prefix is valid after a yotta + break; + case 1: + // after a kilo-zetta, only deka/hecto are valid + if (SI.THOUSAND_PREFIXES.contains(prefix)) + return false; + break; + case 2: + // deka/hecto must be the last prefix, so this is always invalid + return false; + } + + // set part + if (SI.YOTTA.equals(prefix) || SI.YOCTO.equals(prefix)) { + part = 0; + } else if (SI.THOUSAND_PREFIXES.contains(prefix)) { + part = 1; + } else { + part = 2; + } + } + return true; } }; } diff --git a/src/org/unitConverter/converterGUI/UnitConverterGUI.java b/src/org/unitConverter/converterGUI/UnitConverterGUI.java index eff0c47..5fe4ee5 100644 --- a/src/org/unitConverter/converterGUI/UnitConverterGUI.java +++ b/src/org/unitConverter/converterGUI/UnitConverterGUI.java @@ -1024,8 +1024,10 @@ final class UnitConverterGUI { .build()); final JRadioButton customRepetition = new JRadioButton( - "Custom Repetition Rule"); - customRepetition.setEnabled(false); + "Complex Repetition"); + customRepetition.addActionListener( + e -> this.presenter.setPrefixRepetitionRule( + DefaultPrefixRepetitionRule.COMPLEX_REPETITION)); prefixRuleButtons.add(customRepetition); prefixRepetitionPanel.add(customRepetition, new GridBagBuilder(0, 2) diff --git a/src/org/unitConverter/math/ConditionalExistenceCollections.java b/src/org/unitConverter/math/ConditionalExistenceCollections.java index 9522885..ac1c0cf 100644 --- a/src/org/unitConverter/math/ConditionalExistenceCollections.java +++ b/src/org/unitConverter/math/ConditionalExistenceCollections.java @@ -30,20 +30,25 @@ import java.util.function.Predicate; /** * Elements in these wrapper collections only exist if they pass a condition. *

- * All of the collections in this class are "views" of the provided collections. They are mutable if the provided - * collections are mutable, they allow null if the provided collections allow null, they will reflect changes in the + * All of the collections in this class are "views" of the provided collections. + * They are mutable if the provided collections are mutable, they allow null if + * the provided collections allow null, they will reflect changes in the * provided collection, etc. *

- * The modification operations will always run the corresponding operations, even if the conditional existence - * collection doesn't change. For example, if you have a set that ignores even numbers, add(2) will still add a 2 to the + * The modification operations will always run the corresponding operations, + * even if the conditional existence collection doesn't change. For example, if + * you have a set that ignores even numbers, add(2) will still add a 2 to the * backing set (but the conditional existence set will say it doesn't exist). *

- * The returned collections do not pass the hashCode and equals operations through to the backing collections, - * but rely on {@code Object}'s {@code equals} and {@code hashCode} methods. This is necessary to preserve the contracts - * of these operations in the case that the backing collections are sets or lists. + * The returned collections do not pass the hashCode and equals + * operations through to the backing collections, but rely on {@code Object}'s + * {@code equals} and {@code hashCode} methods. This is necessary to preserve + * the contracts of these operations in the case that the backing collections + * are sets or lists. *

- * Other than that, the only difference between the provided collections and the returned collections are that - * elements don't exist if they don't pass the provided condition. + * Other than that, the only difference between the provided collections and + * the returned collections are that elements don't exist if they don't pass the + * provided condition. * * * @author Adrien Hopkins @@ -56,13 +61,13 @@ public final class ConditionalExistenceCollections { * * @author Adrien Hopkins * @since 2019-10-17 - * @param - * type of element in collection + * @param type of element in collection */ - static final class ConditionalExistenceCollection extends AbstractCollection { + static final class ConditionalExistenceCollection + extends AbstractCollection { final Collection collection; final Predicate existenceCondition; - + /** * Creates the {@code ConditionalExistenceCollection}. * @@ -70,67 +75,89 @@ public final class ConditionalExistenceCollections { * @param existenceCondition * @since 2019-10-17 */ - private ConditionalExistenceCollection(final Collection collection, final Predicate existenceCondition) { + private ConditionalExistenceCollection(final Collection collection, + final Predicate existenceCondition) { this.collection = collection; this.existenceCondition = existenceCondition; } - + @Override public boolean add(final E e) { return this.collection.add(e) && this.existenceCondition.test(e); } - + @Override public void clear() { this.collection.clear(); } - + @Override public boolean contains(final Object o) { if (!this.collection.contains(o)) return false; - + // this collection can only contain instances of E - // since the object is in the collection, we know that it must be an instance of E + // since the object is in the collection, we know that it must be an + // instance of E // therefore this cast will always work @SuppressWarnings("unchecked") final E e = (E) o; - + return this.existenceCondition.test(e); } - + @Override public Iterator iterator() { - return conditionalExistenceIterator(this.collection.iterator(), this.existenceCondition); + return conditionalExistenceIterator(this.collection.iterator(), + this.existenceCondition); } - + @Override public boolean remove(final Object o) { - // remove() must be first in the && statement, otherwise it may not execute + // remove() must be first in the && statement, otherwise it may not + // execute final boolean containedObject = this.contains(o); return this.collection.remove(o) && containedObject; } - + @Override public int size() { - return (int) this.collection.stream().filter(this.existenceCondition).count(); + return (int) this.collection.stream().filter(this.existenceCondition) + .count(); + } + + @Override + public Object[] toArray() { + // ensure the toArray operation is supported + this.collection.toArray(); + + // if it works, do it for real + return super.toArray(); + } + + @Override + public T[] toArray(T[] a) { + // ensure the toArray operation is supported + this.collection.toArray(); + + // if it works, do it for real + return super.toArray(a); } } - + /** * Elements in this wrapper iterator only exist if they pass a condition. * * @author Adrien Hopkins * @since 2019-10-17 - * @param - * type of elements in iterator + * @param type of elements in iterator */ static final class ConditionalExistenceIterator implements Iterator { final Iterator iterator; final Predicate existenceCondition; E nextElement; boolean hasNext; - + /** * Creates the {@code ConditionalExistenceIterator}. * @@ -138,12 +165,13 @@ public final class ConditionalExistenceCollections { * @param condition * @since 2019-10-17 */ - private ConditionalExistenceIterator(final Iterator iterator, final Predicate condition) { + private ConditionalExistenceIterator(final Iterator iterator, + final Predicate condition) { this.iterator = iterator; this.existenceCondition = condition; this.getAndSetNextElement(); } - + /** * Gets the next element, and sets nextElement and hasNext accordingly. * @@ -160,12 +188,12 @@ public final class ConditionalExistenceCollections { } while (!this.existenceCondition.test(this.nextElement)); this.hasNext = true; } - + @Override public boolean hasNext() { return this.hasNext; } - + @Override public E next() { if (this.hasNext()) { @@ -175,27 +203,25 @@ public final class ConditionalExistenceCollections { } else throw new NoSuchElementException(); } - + @Override public void remove() { this.iterator.remove(); } } - + /** * Mappings in this map only exist if the entry passes some condition. * * @author Adrien Hopkins * @since 2019-10-17 - * @param - * key type - * @param - * value type + * @param key type + * @param value type */ static final class ConditionalExistenceMap extends AbstractMap { Map map; Predicate> entryExistenceCondition; - + /** * Creates the {@code ConditionalExistenceMap}. * @@ -203,205 +229,240 @@ public final class ConditionalExistenceCollections { * @param entryExistenceCondition * @since 2019-10-17 */ - private ConditionalExistenceMap(final Map map, final Predicate> entryExistenceCondition) { + private ConditionalExistenceMap(final Map map, + final Predicate> entryExistenceCondition) { this.map = map; this.entryExistenceCondition = entryExistenceCondition; } - + @Override public boolean containsKey(final Object key) { if (!this.map.containsKey(key)) return false; - + // only instances of K have mappings in the backing map // since we know that key is a valid key, it must be an instance of K @SuppressWarnings("unchecked") final K keyAsK = (K) key; - + // get and test entry final V value = this.map.get(key); final Entry entry = new SimpleEntry<>(keyAsK, value); return this.entryExistenceCondition.test(entry); } - + @Override public Set> entrySet() { - return conditionalExistenceSet(this.map.entrySet(), this.entryExistenceCondition); + return conditionalExistenceSet(this.map.entrySet(), + this.entryExistenceCondition); } - + @Override public V get(final Object key) { return this.containsKey(key) ? this.map.get(key) : null; } - + + private final Entry getEntry(K key) { + return new Entry() { + @Override + public K getKey() { + return key; + } + + @Override + public V getValue() { + return ConditionalExistenceMap.this.map.get(key); + } + + @Override + public V setValue(V value) { + return ConditionalExistenceMap.this.map.put(key, value); + } + }; + } + @Override public Set keySet() { - // maybe change this to use ConditionalExistenceSet - return super.keySet(); + return conditionalExistenceSet(super.keySet(), + k -> this.entryExistenceCondition.test(this.getEntry(k))); } - + @Override public V put(final K key, final V value) { final V oldValue = this.map.put(key, value); - + // get and test entry final Entry entry = new SimpleEntry<>(key, oldValue); return this.entryExistenceCondition.test(entry) ? oldValue : null; } - + @Override public V remove(final Object key) { final V oldValue = this.map.remove(key); return this.containsKey(key) ? oldValue : null; } - + @Override public Collection values() { // maybe change this to use ConditionalExistenceCollection return super.values(); } - } - + /** * Elements in this set only exist if a certain condition is true. * * @author Adrien Hopkins * @since 2019-10-17 - * @param - * type of element in set + * @param type of element in set */ static final class ConditionalExistenceSet extends AbstractSet { private final Set set; private final Predicate existenceCondition; - + /** * Creates the {@code ConditionalNonexistenceSet}. * - * @param set - * set to use - * @param existenceCondition - * condition where element exists + * @param set set to use + * @param existenceCondition condition where element exists * @since 2019-10-17 */ - private ConditionalExistenceSet(final Set set, final Predicate existenceCondition) { + private ConditionalExistenceSet(final Set set, + final Predicate existenceCondition) { this.set = set; this.existenceCondition = existenceCondition; } - + /** * {@inheritDoc} *

- * Note that this method returns {@code false} if {@code e} does not pass the existence condition. + * Note that this method returns {@code false} if {@code e} does not pass + * the existence condition. */ @Override public boolean add(final E e) { return this.set.add(e) && this.existenceCondition.test(e); } - + @Override public void clear() { this.set.clear(); } - + @Override public boolean contains(final Object o) { if (!this.set.contains(o)) return false; - + // this set can only contain instances of E - // since the object is in the set, we know that it must be an instance of E + // since the object is in the set, we know that it must be an instance + // of E // therefore this cast will always work @SuppressWarnings("unchecked") final E e = (E) o; - + return this.existenceCondition.test(e); } - + @Override public Iterator iterator() { - return conditionalExistenceIterator(this.set.iterator(), this.existenceCondition); + return conditionalExistenceIterator(this.set.iterator(), + this.existenceCondition); } - + @Override public boolean remove(final Object o) { - // remove() must be first in the && statement, otherwise it may not execute + // remove() must be first in the && statement, otherwise it may not + // execute final boolean containedObject = this.contains(o); return this.set.remove(o) && containedObject; } - + @Override public int size() { return (int) this.set.stream().filter(this.existenceCondition).count(); } + + @Override + public Object[] toArray() { + // ensure the toArray operation is supported + this.set.toArray(); + + // if it works, do it for real + return super.toArray(); + } + + @Override + public T[] toArray(T[] a) { + // ensure the toArray operation is supported + this.set.toArray(); + + // if it works, do it for real + return super.toArray(a); + } } - + /** - * Elements in the returned wrapper collection are ignored if they don't pass a condition. + * Elements in the returned wrapper collection are ignored if they don't pass + * a condition. * - * @param - * type of elements in collection - * @param collection - * collection to wrap - * @param existenceCondition - * elements only exist if this returns true + * @param type of elements in collection + * @param collection collection to wrap + * @param existenceCondition elements only exist if this returns true * @return wrapper collection * @since 2019-10-17 */ - public static final Collection conditionalExistenceCollection(final Collection collection, + public static final Collection conditionalExistenceCollection( + final Collection collection, final Predicate existenceCondition) { - return new ConditionalExistenceCollection<>(collection, existenceCondition); + return new ConditionalExistenceCollection<>(collection, + existenceCondition); } - + /** - * Elements in the returned wrapper iterator are ignored if they don't pass a condition. + * Elements in the returned wrapper iterator are ignored if they don't pass a + * condition. * - * @param - * type of elements in iterator - * @param iterator - * iterator to wrap - * @param existenceCondition - * elements only exist if this returns true + * @param type of elements in iterator + * @param iterator iterator to wrap + * @param existenceCondition elements only exist if this returns true * @return wrapper iterator * @since 2019-10-17 */ - public static final Iterator conditionalExistenceIterator(final Iterator iterator, - final Predicate existenceCondition) { + public static final Iterator conditionalExistenceIterator( + final Iterator iterator, final Predicate existenceCondition) { return new ConditionalExistenceIterator<>(iterator, existenceCondition); } - + /** - * Mappings in the returned wrapper map are ignored if the corresponding entry doesn't pass a condition + * Mappings in the returned wrapper map are ignored if the corresponding + * entry doesn't pass a condition * - * @param - * type of key in map - * @param - * type of value in map - * @param map - * map to wrap - * @param entryExistenceCondition - * mappings only exist if this returns true + * @param type of key in map + * @param type of value in map + * @param map map to wrap + * @param entryExistenceCondition mappings only exist if this returns true * @return wrapper map * @since 2019-10-17 */ - public static final Map conditionalExistenceMap(final Map map, + public static final Map conditionalExistenceMap( + final Map map, final Predicate> entryExistenceCondition) { return new ConditionalExistenceMap<>(map, entryExistenceCondition); } - + /** - * Elements in the returned wrapper set are ignored if they don't pass a condition. + * Elements in the returned wrapper set are ignored if they don't pass a + * condition. * - * @param - * type of elements in set - * @param set - * set to wrap - * @param existenceCondition - * elements only exist if this returns true + * @param type of elements in set + * @param set set to wrap + * @param existenceCondition elements only exist if this returns true * @return wrapper set * @since 2019-10-17 */ - public static final Set conditionalExistenceSet(final Set set, final Predicate existenceCondition) { + public static final Set conditionalExistenceSet(final Set set, + final Predicate existenceCondition) { return new ConditionalExistenceSet<>(set, existenceCondition); } } diff --git a/src/org/unitConverter/unit/UnitDatabase.java b/src/org/unitConverter/unit/UnitDatabase.java index 56846a1..9812bd0 100644 --- a/src/org/unitConverter/unit/UnitDatabase.java +++ b/src/org/unitConverter/unit/UnitDatabase.java @@ -1680,37 +1680,30 @@ public final class UnitDatabase { * @since 2020-08-26 */ List getPrefixesFromName(final String unitName) { - if (this.prefixlessUnits.containsKey(unitName)) - return new ArrayList<>(); - else { + final List prefixes = new ArrayList<>(); + String name = unitName; + + while (!this.prefixlessUnits.containsKey(name)) { // find the longest prefix - String longestPrefix = null; - int longestLength = 0; + String longestPrefixName = null; + int longestLength = name.length(); - 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.containsUnitName(rest) - && this.getUnit(rest) instanceof LinearUnit) { - longestPrefix = prefixName; - longestLength = prefixName.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); } } - final List prefixes = this - .getPrefixesFromName(unitName.substring(longestLength)); - prefixes.add(this.getPrefix(longestPrefix)); - return prefixes; + // longest prefix found! + final UnitPrefix prefix = this.getPrefix(longestPrefixName); + prefixes.add(0, prefix); + name = name.substring(longestLength); } + return prefixes; } /** diff --git a/src/org/unitConverter/unit/UnitDatabaseTest.java b/src/org/unitConverter/unit/UnitDatabaseTest.java index 96a0b83..2b981b6 100644 --- a/src/org/unitConverter/unit/UnitDatabaseTest.java +++ b/src/org/unitConverter/unit/UnitDatabaseTest.java @@ -20,7 +20,6 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assertions.fail; import java.util.Arrays; import java.util.Iterator; @@ -28,6 +27,7 @@ import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.NoSuchElementException; +import java.util.Set; import org.junit.jupiter.api.Test; @@ -55,9 +55,12 @@ class UnitDatabaseTest { .fromConversionFunctions(SI.METRE.getBase(), o -> o + 1, o -> o - 1); // make the prefix values prime so I can tell which multiplications were made - private static final UnitPrefix A = UnitPrefix.valueOf(2); - private static final UnitPrefix B = UnitPrefix.valueOf(3); - private static final UnitPrefix C = UnitPrefix.valueOf(5); + private static final UnitPrefix A = UnitPrefix.valueOf(2) + .withName(NameSymbol.ofName("A")); + private static final UnitPrefix B = UnitPrefix.valueOf(3) + .withName(NameSymbol.ofName("B")); + private static final UnitPrefix C = UnitPrefix.valueOf(5) + .withName(NameSymbol.ofName("C")); private static final UnitPrefix AB = UnitPrefix.valueOf(7); private static final UnitPrefix BC = UnitPrefix.valueOf(11); @@ -68,6 +71,7 @@ class UnitDatabaseTest { * @since 2019-05-03 */ @Test + // @Timeout(value = 5, unit = TimeUnit.SECONDS) public void testInfiniteSetExceptions() { // load units final UnitDatabase infiniteDatabase = new UnitDatabase(); @@ -79,33 +83,11 @@ class UnitDatabaseTest { infiniteDatabase.addPrefix("B", B); infiniteDatabase.addPrefix("C", C); - { - boolean exceptionThrown = false; - try { - infiniteDatabase.unitMap().entrySet().toArray(); - } catch (final IllegalStateException e) { - exceptionThrown = true; - // pass! - } finally { - if (!exceptionThrown) { - fail("No IllegalStateException thrown"); - } - } - } - - { - boolean exceptionThrown = false; - try { - infiniteDatabase.unitMap().keySet().toArray(); - } catch (final IllegalStateException e) { - exceptionThrown = true; - // pass! - } finally { - if (!exceptionThrown) { - fail("No IllegalStateException thrown"); - } - } - } + final Set> entrySet = infiniteDatabase.unitMap() + .entrySet(); + final Set keySet = infiniteDatabase.unitMap().keySet(); + assertThrows(IllegalStateException.class, () -> entrySet.toArray()); + assertThrows(IllegalStateException.class, () -> keySet.toArray()); } /** -- cgit v1.2.3 From 5a7e8f6fcb175b238eb1d5481513b35039107a3e Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Mon, 7 Sep 2020 15:15:20 -0500 Subject: Created an UncertainDouble class for uncertainty operations. --- .settings/org.eclipse.jdt.core.prefs | 100 ++++++ src/org/unitConverter/math/DecimalComparison.java | 207 +++++++---- src/org/unitConverter/math/UncertainDouble.java | 411 ++++++++++++++++++++++ src/org/unitConverter/unit/LinearUnit.java | 302 +++++++++------- src/org/unitConverter/unit/LinearUnitValue.java | 252 +++---------- src/org/unitConverter/unit/UnitDatabase.java | 6 +- src/org/unitConverter/unit/UnitTest.java | 7 +- 7 files changed, 885 insertions(+), 400 deletions(-) create mode 100644 src/org/unitConverter/math/UncertainDouble.java (limited to 'src/org/unitConverter/unit/UnitDatabase.java') diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs index ea7a397..f77f6a1 100644 --- a/.settings/org.eclipse.jdt.core.prefs +++ b/.settings/org.eclipse.jdt.core.prefs @@ -1,4 +1,13 @@ eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.annotation.inheritNullAnnotations=disabled +org.eclipse.jdt.core.compiler.annotation.missingNonNullByDefaultAnnotation=ignore +org.eclipse.jdt.core.compiler.annotation.nonnull=org.eclipse.jdt.annotation.NonNull +org.eclipse.jdt.core.compiler.annotation.nonnull.secondary= +org.eclipse.jdt.core.compiler.annotation.nonnullbydefault=org.eclipse.jdt.annotation.NonNullByDefault +org.eclipse.jdt.core.compiler.annotation.nonnullbydefault.secondary= +org.eclipse.jdt.core.compiler.annotation.nullable=org.eclipse.jdt.annotation.Nullable +org.eclipse.jdt.core.compiler.annotation.nullable.secondary= +org.eclipse.jdt.core.compiler.annotation.nullanalysis=disabled org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve @@ -6,11 +15,102 @@ org.eclipse.jdt.core.compiler.compliance=1.8 org.eclipse.jdt.core.compiler.debug.lineNumber=generate org.eclipse.jdt.core.compiler.debug.localVariable=generate org.eclipse.jdt.core.compiler.debug.sourceFile=generate +org.eclipse.jdt.core.compiler.problem.APILeak=warning +org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=warning org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.autoboxing=ignore +org.eclipse.jdt.core.compiler.problem.comparingIdentical=warning +org.eclipse.jdt.core.compiler.problem.deadCode=warning +org.eclipse.jdt.core.compiler.problem.deprecation=warning +org.eclipse.jdt.core.compiler.problem.deprecationInDeprecatedCode=disabled +org.eclipse.jdt.core.compiler.problem.deprecationWhenOverridingDeprecatedMethod=enabled +org.eclipse.jdt.core.compiler.problem.discouragedReference=warning +org.eclipse.jdt.core.compiler.problem.emptyStatement=ignore org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.problem.explicitlyClosedAutoCloseable=warning +org.eclipse.jdt.core.compiler.problem.fallthroughCase=error +org.eclipse.jdt.core.compiler.problem.fatalOptionalError=enabled +org.eclipse.jdt.core.compiler.problem.fieldHiding=ignore +org.eclipse.jdt.core.compiler.problem.finalParameterBound=warning +org.eclipse.jdt.core.compiler.problem.finallyBlockNotCompletingNormally=warning org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning +org.eclipse.jdt.core.compiler.problem.hiddenCatchBlock=warning +org.eclipse.jdt.core.compiler.problem.includeNullInfoFromAsserts=disabled +org.eclipse.jdt.core.compiler.problem.incompatibleNonInheritedInterfaceMethod=warning +org.eclipse.jdt.core.compiler.problem.incompleteEnumSwitch=warning +org.eclipse.jdt.core.compiler.problem.indirectStaticAccess=ignore +org.eclipse.jdt.core.compiler.problem.localVariableHiding=ignore +org.eclipse.jdt.core.compiler.problem.methodWithConstructorName=warning +org.eclipse.jdt.core.compiler.problem.missingDefaultCase=ignore +org.eclipse.jdt.core.compiler.problem.missingDeprecatedAnnotation=warning +org.eclipse.jdt.core.compiler.problem.missingEnumCaseDespiteDefault=disabled +org.eclipse.jdt.core.compiler.problem.missingHashCodeMethod=error +org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotation=warning +org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotationForInterfaceMethodImplementation=enabled +org.eclipse.jdt.core.compiler.problem.missingSerialVersion=warning +org.eclipse.jdt.core.compiler.problem.missingSynchronizedOnInheritedMethod=warning +org.eclipse.jdt.core.compiler.problem.noEffectAssignment=warning +org.eclipse.jdt.core.compiler.problem.noImplicitStringConversion=warning +org.eclipse.jdt.core.compiler.problem.nonExternalizedStringLiteral=ignore +org.eclipse.jdt.core.compiler.problem.nonnullParameterAnnotationDropped=warning +org.eclipse.jdt.core.compiler.problem.nonnullTypeVariableFromLegacyInvocation=warning +org.eclipse.jdt.core.compiler.problem.nullAnnotationInferenceConflict=error +org.eclipse.jdt.core.compiler.problem.nullReference=error +org.eclipse.jdt.core.compiler.problem.nullSpecViolation=error +org.eclipse.jdt.core.compiler.problem.nullUncheckedConversion=warning +org.eclipse.jdt.core.compiler.problem.overridingPackageDefaultMethod=warning +org.eclipse.jdt.core.compiler.problem.parameterAssignment=info +org.eclipse.jdt.core.compiler.problem.pessimisticNullAnalysisForFreeTypeVariables=warning +org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=warning +org.eclipse.jdt.core.compiler.problem.potentialNullReference=warning +org.eclipse.jdt.core.compiler.problem.potentiallyUnclosedCloseable=info +org.eclipse.jdt.core.compiler.problem.rawTypeReference=error +org.eclipse.jdt.core.compiler.problem.redundantNullAnnotation=warning +org.eclipse.jdt.core.compiler.problem.redundantNullCheck=info +org.eclipse.jdt.core.compiler.problem.redundantSpecificationOfTypeArguments=info +org.eclipse.jdt.core.compiler.problem.redundantSuperinterface=info +org.eclipse.jdt.core.compiler.problem.reportMethodCanBePotentiallyStatic=ignore +org.eclipse.jdt.core.compiler.problem.reportMethodCanBeStatic=info org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=ignore +org.eclipse.jdt.core.compiler.problem.specialParameterHidingField=disabled +org.eclipse.jdt.core.compiler.problem.staticAccessReceiver=info +org.eclipse.jdt.core.compiler.problem.suppressOptionalErrors=disabled +org.eclipse.jdt.core.compiler.problem.suppressWarnings=enabled +org.eclipse.jdt.core.compiler.problem.suppressWarningsNotFullyAnalysed=ignore +org.eclipse.jdt.core.compiler.problem.syntacticNullAnalysisForFields=disabled +org.eclipse.jdt.core.compiler.problem.syntheticAccessEmulation=ignore +org.eclipse.jdt.core.compiler.problem.terminalDeprecation=error +org.eclipse.jdt.core.compiler.problem.typeParameterHiding=warning +org.eclipse.jdt.core.compiler.problem.unavoidableGenericTypeProblems=disabled +org.eclipse.jdt.core.compiler.problem.uncheckedTypeOperation=warning +org.eclipse.jdt.core.compiler.problem.unclosedCloseable=warning +org.eclipse.jdt.core.compiler.problem.undocumentedEmptyBlock=ignore +org.eclipse.jdt.core.compiler.problem.unhandledWarningToken=warning +org.eclipse.jdt.core.compiler.problem.unlikelyCollectionMethodArgumentType=warning +org.eclipse.jdt.core.compiler.problem.unlikelyCollectionMethodArgumentTypeStrict=disabled +org.eclipse.jdt.core.compiler.problem.unlikelyEqualsArgumentType=warning +org.eclipse.jdt.core.compiler.problem.unnecessaryElse=ignore +org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=info +org.eclipse.jdt.core.compiler.problem.unqualifiedFieldAccess=ignore +org.eclipse.jdt.core.compiler.problem.unstableAutoModuleName=warning +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownException=info +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionExemptExceptionAndThrowable=disabled +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionIncludeDocCommentReference=enabled +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionWhenOverriding=disabled +org.eclipse.jdt.core.compiler.problem.unusedExceptionParameter=ignore +org.eclipse.jdt.core.compiler.problem.unusedImport=info +org.eclipse.jdt.core.compiler.problem.unusedLabel=warning +org.eclipse.jdt.core.compiler.problem.unusedLocal=warning +org.eclipse.jdt.core.compiler.problem.unusedObjectAllocation=info +org.eclipse.jdt.core.compiler.problem.unusedParameter=info +org.eclipse.jdt.core.compiler.problem.unusedParameterIncludeDocCommentReference=enabled +org.eclipse.jdt.core.compiler.problem.unusedParameterWhenImplementingAbstract=disabled +org.eclipse.jdt.core.compiler.problem.unusedParameterWhenOverridingConcrete=disabled +org.eclipse.jdt.core.compiler.problem.unusedPrivateMember=info +org.eclipse.jdt.core.compiler.problem.unusedTypeParameter=info +org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning +org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning org.eclipse.jdt.core.compiler.processAnnotations=disabled org.eclipse.jdt.core.compiler.release=disabled org.eclipse.jdt.core.compiler.source=1.8 diff --git a/src/org/unitConverter/math/DecimalComparison.java b/src/org/unitConverter/math/DecimalComparison.java index 859e8da..0f5b91e 100644 --- a/src/org/unitConverter/math/DecimalComparison.java +++ b/src/org/unitConverter/math/DecimalComparison.java @@ -27,42 +27,45 @@ import java.math.BigDecimal; */ public final class DecimalComparison { /** - * The value used for double comparison. If two double values are within this value multiplied by the larger value, - * they are considered equal. + * The value used for double comparison. If two double values are within this + * value multiplied by the larger value, they are considered equal. * * @since 2019-03-18 * @since v0.2.0 */ public static final double DOUBLE_EPSILON = 1.0e-15; - + /** - * The value used for float comparison. If two float values are within this value multiplied by the larger value, - * they are considered equal. + * The value used for float comparison. If two float values are within this + * value multiplied by the larger value, they are considered equal. * * @since 2019-03-18 * @since v0.2.0 */ public static final float FLOAT_EPSILON = 1.0e-6f; - + /** * Tests for equality of double values using {@link #DOUBLE_EPSILON}. *

- * WARNING: this method is not technically transitive. If a and b are off by slightly less than - * {@code epsilon * max(abs(a), abs(b))}, and b and c are off by slightly less than - * {@code epsilon * max(abs(b), abs(c))}, then equals(a, b) and equals(b, c) will both return true, but equals(a, c) - * will return false. However, this situation is very unlikely to ever happen in a real programming situation. + * WARNING: this method is not technically transitive. If a + * and b are off by slightly less than {@code epsilon * max(abs(a), abs(b))}, + * and b and c are off by slightly less than + * {@code epsilon * max(abs(b), abs(c))}, then equals(a, b) and equals(b, c) + * will both return true, but equals(a, c) will return false. However, this + * situation is very unlikely to ever happen in a real programming situation. *

* If this does become a concern, some ways to solve this problem: *

    - *
  1. Raise the value of epsilon using {@link #equals(double, double, double)} (this does not make a violation of - * transitivity impossible, it just significantly reduces the chances of it happening) - *
  2. Use {@link BigDecimal} instead of {@code double} (this will make a violation of transitivity 100% impossible) + *
  3. Raise the value of epsilon using + * {@link #equals(double, double, double)} (this does not make a violation of + * transitivity impossible, it just significantly reduces the chances of it + * happening) + *
  4. Use {@link BigDecimal} instead of {@code double} (this will make a + * violation of transitivity 100% impossible) *
* - * @param a - * first value to test - * @param b - * second value to test + * @param a first value to test + * @param b second value to test * @return whether they are equal * @since 2019-03-18 * @since v0.2.0 @@ -71,57 +74,61 @@ public final class DecimalComparison { public static final boolean equals(final double a, final double b) { return DecimalComparison.equals(a, b, DOUBLE_EPSILON); } - + /** * Tests for double equality using a custom epsilon value. * *

- * WARNING: this method is not technically transitive. If a and b are off by slightly less than - * {@code epsilon * max(abs(a), abs(b))}, and b and c are off by slightly less than - * {@code epsilon * max(abs(b), abs(c))}, then equals(a, b) and equals(b, c) will both return true, but equals(a, c) - * will return false. However, this situation is very unlikely to ever happen in a real programming situation. + * WARNING: this method is not technically transitive. If a + * and b are off by slightly less than {@code epsilon * max(abs(a), abs(b))}, + * and b and c are off by slightly less than + * {@code epsilon * max(abs(b), abs(c))}, then equals(a, b) and equals(b, c) + * will both return true, but equals(a, c) will return false. However, this + * situation is very unlikely to ever happen in a real programming situation. *

* If this does become a concern, some ways to solve this problem: *

    - *
  1. Raise the value of epsilon (this does not make a violation of transitivity impossible, it just significantly - * reduces the chances of it happening) - *
  2. Use {@link BigDecimal} instead of {@code double} (this will make a violation of transitivity 100% impossible) + *
  3. Raise the value of epsilon (this does not make a violation of + * transitivity impossible, it just significantly reduces the chances of it + * happening) + *
  4. Use {@link BigDecimal} instead of {@code double} (this will make a + * violation of transitivity 100% impossible) *
* - * @param a - * first value to test - * @param b - * second value to test - * @param epsilon - * allowed difference + * @param a first value to test + * @param b second value to test + * @param epsilon allowed difference * @return whether they are equal * @since 2019-03-18 * @since v0.2.0 */ - public static final boolean equals(final double a, final double b, final double epsilon) { + public static final boolean equals(final double a, final double b, + final double epsilon) { return Math.abs(a - b) <= epsilon * Math.max(Math.abs(a), Math.abs(b)); } - + /** * Tests for equality of float values using {@link #FLOAT_EPSILON}. * *

- * WARNING: this method is not technically transitive. If a and b are off by slightly less than - * {@code epsilon * max(abs(a), abs(b))}, and b and c are off by slightly less than - * {@code epsilon * max(abs(b), abs(c))}, then equals(a, b) and equals(b, c) will both return true, but equals(a, c) - * will return false. However, this situation is very unlikely to ever happen in a real programming situation. + * WARNING: this method is not technically transitive. If a + * and b are off by slightly less than {@code epsilon * max(abs(a), abs(b))}, + * and b and c are off by slightly less than + * {@code epsilon * max(abs(b), abs(c))}, then equals(a, b) and equals(b, c) + * will both return true, but equals(a, c) will return false. However, this + * situation is very unlikely to ever happen in a real programming situation. *

* If this does become a concern, some ways to solve this problem: *

    - *
  1. Raise the value of epsilon using {@link #equals(float, float, float)} (this does not make a violation of - * transitivity impossible, it just significantly reduces the chances of it happening) - *
  2. Use {@link BigDecimal} instead of {@code float} (this will make a violation of transitivity 100% impossible) + *
  3. Raise the value of epsilon using {@link #equals(float, float, float)} + * (this does not make a violation of transitivity impossible, it just + * significantly reduces the chances of it happening) + *
  4. Use {@link BigDecimal} instead of {@code float} (this will make a + * violation of transitivity 100% impossible) *
* - * @param a - * first value to test - * @param b - * second value to test + * @param a first value to test + * @param b second value to test * @return whether they are equal * @since 2019-03-18 * @since v0.2.0 @@ -129,53 +136,121 @@ public final class DecimalComparison { public static final boolean equals(final float a, final float b) { return DecimalComparison.equals(a, b, FLOAT_EPSILON); } - + /** * Tests for float equality using a custom epsilon value. * *

- * WARNING: this method is not technically transitive. If a and b are off by slightly less than - * {@code epsilon * max(abs(a), abs(b))}, and b and c are off by slightly less than - * {@code epsilon * max(abs(b), abs(c))}, then equals(a, b) and equals(b, c) will both return true, but equals(a, c) - * will return false. However, this situation is very unlikely to ever happen in a real programming situation. + * WARNING: this method is not technically transitive. If a + * and b are off by slightly less than {@code epsilon * max(abs(a), abs(b))}, + * and b and c are off by slightly less than + * {@code epsilon * max(abs(b), abs(c))}, then equals(a, b) and equals(b, c) + * will both return true, but equals(a, c) will return false. However, this + * situation is very unlikely to ever happen in a real programming situation. *

* If this does become a concern, some ways to solve this problem: *

    - *
  1. Raise the value of epsilon (this does not make a violation of transitivity impossible, it just significantly - * reduces the chances of it happening) - *
  2. Use {@link BigDecimal} instead of {@code float} (this will make a violation of transitivity 100% impossible) + *
  3. Raise the value of epsilon (this does not make a violation of + * transitivity impossible, it just significantly reduces the chances of it + * happening) + *
  4. Use {@link BigDecimal} instead of {@code float} (this will make a + * violation of transitivity 100% impossible) *
* - * @param a - * first value to test - * @param b - * second value to test - * @param epsilon - * allowed difference + * @param a first value to test + * @param b second value to test + * @param epsilon allowed difference * @return whether they are equal * @since 2019-03-18 * @since v0.2.0 */ - public static final boolean equals(final float a, final float b, final float epsilon) { + public static final boolean equals(final float a, final float b, + final float epsilon) { return Math.abs(a - b) <= epsilon * Math.max(Math.abs(a), Math.abs(b)); } - + + /** + * Tests for equality of {@code UncertainDouble} values using + * {@link #DOUBLE_EPSILON}. + *

+ * WARNING: this method is not technically transitive. If a + * and b are off by slightly less than {@code epsilon * max(abs(a), abs(b))}, + * and b and c are off by slightly less than + * {@code epsilon * max(abs(b), abs(c))}, then equals(a, b) and equals(b, c) + * will both return true, but equals(a, c) will return false. However, this + * situation is very unlikely to ever happen in a real programming situation. + *

+ * If this does become a concern, some ways to solve this problem: + *

    + *
  1. Raise the value of epsilon using + * {@link #equals(UncertainDouble, UncertainDouble, double)} (this does not + * make a violation of transitivity impossible, it just significantly reduces + * the chances of it happening) + *
  2. Use {@link BigDecimal} instead of {@code double} (this will make a + * violation of transitivity 100% impossible) + *
+ * + * @param a first value to test + * @param b second value to test + * @return whether they are equal + * @since 2020-09-07 + * @see #hashCode(double) + */ + public static final boolean equals(final UncertainDouble a, + final UncertainDouble b) { + return DecimalComparison.equals(a.value(), b.value()) + && DecimalComparison.equals(a.uncertainty(), b.uncertainty()); + } + + /** + * Tests for {@code UncertainDouble} equality using a custom epsilon value. + * + *

+ * WARNING: this method is not technically transitive. If a + * and b are off by slightly less than {@code epsilon * max(abs(a), abs(b))}, + * and b and c are off by slightly less than + * {@code epsilon * max(abs(b), abs(c))}, then equals(a, b) and equals(b, c) + * will both return true, but equals(a, c) will return false. However, this + * situation is very unlikely to ever happen in a real programming situation. + *

+ * If this does become a concern, some ways to solve this problem: + *

    + *
  1. Raise the value of epsilon (this does not make a violation of + * transitivity impossible, it just significantly reduces the chances of it + * happening) + *
  2. Use {@link BigDecimal} instead of {@code double} (this will make a + * violation of transitivity 100% impossible) + *
+ * + * @param a first value to test + * @param b second value to test + * @param epsilon allowed difference + * @return whether they are equal + * @since 2019-03-18 + * @since v0.2.0 + */ + public static final boolean equals(final UncertainDouble a, + final UncertainDouble b, final double epsilon) { + return DecimalComparison.equals(a.value(), b.value(), epsilon) + && DecimalComparison.equals(a.uncertainty(), b.uncertainty(), + epsilon); + } + /** - * Takes the hash code of doubles. Values that are equal according to {@link #equals(double, double)} will have the - * same hash code. + * Takes the hash code of doubles. Values that are equal according to + * {@link #equals(double, double)} will have the same hash code. * - * @param d - * double to hash + * @param d double to hash * @return hash code of double * @since 2019-10-16 */ public static final int hash(final double d) { return Float.hashCode((float) d); } - + // You may NOT get any DecimalComparison instances private DecimalComparison() { throw new AssertionError(); } - + } diff --git a/src/org/unitConverter/math/UncertainDouble.java b/src/org/unitConverter/math/UncertainDouble.java new file mode 100644 index 0000000..e948df9 --- /dev/null +++ b/src/org/unitConverter/math/UncertainDouble.java @@ -0,0 +1,411 @@ +/** + * @since 2020-09-07 + */ +package org.unitConverter.math; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.Objects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * A double with an associated uncertainty value. For example, 3.2 ± 0.2. + *

+ * All methods in this class throw a NullPointerException if any of their + * arguments is null. + * + * @since 2020-09-07 + */ +public final class UncertainDouble implements Comparable { + /** + * The exact value 0 + */ + public static final UncertainDouble ZERO = UncertainDouble.of(0, 0); + + /** + * A regular expression that can recognize toString forms + */ + private static final Pattern TO_STRING = Pattern + .compile("([a-zA-Z_0-9\\.\\,]+)" // a number + // optional "± [number]" + + "(?:\\s*(?:±|\\+-)\\s*([a-zA-Z_0-9\\.\\,]+))?"); + + /** + * Parses a string in the form of {@link UncertainDouble#toString(boolean)} + * and returns the corresponding {@code UncertainDouble} instance. + *

+ * This method allows some alternative forms of the string representation, + * such as using "+-" instead of "±". + * + * @param s string to parse + * @return {@code UncertainDouble} instance + * @throws IllegalArgumentException if the string is invalid + * @since 2020-09-07 + */ + public static final UncertainDouble fromString(String s) { + Objects.requireNonNull(s, "s may not be null"); + final Matcher matcher = TO_STRING.matcher(s); + + double value, uncertainty; + try { + value = Double.parseDouble(matcher.group(1)); + } catch (IllegalStateException | NumberFormatException e) { + throw new IllegalArgumentException( + "String " + s + " not in correct format."); + } + + final String uncertaintyString = matcher.group(2); + if (uncertaintyString == null) { + uncertainty = 0; + } else { + try { + uncertainty = Double.parseDouble(uncertaintyString); + } catch (final NumberFormatException e) { + throw new IllegalArgumentException( + "String " + s + " not in correct format."); + } + } + + return UncertainDouble.of(value, uncertainty); + } + + /** + * Gets an {@code UncertainDouble} from its value and absolute + * uncertainty. + * + * @since 2020-09-07 + */ + public static final UncertainDouble of(double value, double uncertainty) { + return new UncertainDouble(value, uncertainty); + } + + /** + * Gets an {@code UncertainDouble} from its value and relative + * uncertainty. + * + * @since 2020-09-07 + */ + public static final UncertainDouble ofRelative(double value, + double relativeUncertainty) { + return new UncertainDouble(value, value * relativeUncertainty); + } + + private final double value; + + private final double uncertainty; + + /** + * @param value + * @param uncertainty + * @since 2020-09-07 + */ + private UncertainDouble(double value, double uncertainty) { + this.value = value; + // uncertainty should only ever be positive + this.uncertainty = Math.abs(uncertainty); + } + + /** + * Compares this {@code UncertainDouble} with another + * {@code UncertainDouble}. + *

+ * This method only compares the values, not the uncertainties. So 3.1 ± 0.5 + * is considered less than 3.2 ± 0.5, even though they are equivalent. + *

+ * Note: The natural ordering of this class is inconsistent with + * equals. Specifically, if two {@code UncertainDouble} instances {@code a} + * and {@code b} have the same value but different uncertainties, + * {@code a.compareTo(b)} will return 0 but {@code a.equals(b)} will return + * {@code false}. + */ + @Override + public final int compareTo(UncertainDouble o) { + return Double.compare(this.value, o.value); + } + + /** + * Returns the quotient of {@code this} and {@code other}. + * + * @since 2020-09-07 + */ + public final UncertainDouble dividedBy(UncertainDouble other) { + Objects.requireNonNull(other, "other may not be null"); + return UncertainDouble.ofRelative(this.value / other.value, Math + .hypot(this.relativeUncertainty(), other.relativeUncertainty())); + } + + /** + * Returns the quotient of {@code this} and the exact value {@code other}. + * + * @since 2020-09-07 + */ + public final UncertainDouble dividedByExact(double other) { + return UncertainDouble.of(this.value / other, this.uncertainty / other); + } + + @Override + public final boolean equals(Object obj) { + if (this == obj) + return true; + if (!(obj instanceof UncertainDouble)) + return false; + final UncertainDouble other = (UncertainDouble) obj; + if (Double.doubleToLongBits(this.uncertainty) != Double + .doubleToLongBits(other.uncertainty)) + return false; + if (Double.doubleToLongBits(this.value) != Double + .doubleToLongBits(other.value)) + return false; + return true; + } + + /** + * @param other another {@code UncertainDouble} + * @return true iff this and {@code other} are within each other's + * uncertainty range. + * @since 2020-09-07 + */ + public final boolean equivalent(UncertainDouble other) { + Objects.requireNonNull(other, "other may not be null"); + return Math.abs(this.value - other.value) <= Math.min(this.uncertainty, + other.uncertainty); + } + + /** + * Gets the preferred scale for rounding a value for toString. + * + * @since 2020-09-07 + */ + private final int getDisplayScale() { + // round based on uncertainty + // if uncertainty starts with 1 (ignoring zeroes and the decimal + // point), rounds + // so that uncertainty has 2 significant digits. + // otherwise, rounds so that uncertainty has 1 significant digits. + // the value is rounded to the same number of decimal places as the + // uncertainty. + final BigDecimal bigUncertainty = BigDecimal.valueOf(this.uncertainty); + + // the scale that will give the uncertainty two decimal places + final int twoDecimalPlacesScale = bigUncertainty.scale() + - bigUncertainty.precision() + 2; + final BigDecimal roundedUncertainty = bigUncertainty + .setScale(twoDecimalPlacesScale, RoundingMode.HALF_EVEN); + + if (roundedUncertainty.unscaledValue().intValue() >= 20) + return twoDecimalPlacesScale - 1; // one decimal place + else + return twoDecimalPlacesScale; + } + + @Override + public final int hashCode() { + final int prime = 31; + int result = 1; + long temp; + temp = Double.doubleToLongBits(this.uncertainty); + result = prime * result + (int) (temp ^ temp >>> 32); + temp = Double.doubleToLongBits(this.value); + result = prime * result + (int) (temp ^ temp >>> 32); + return result; + } + + /** + * @return true iff the value has no uncertainty + * + * @since 2020-09-07 + */ + public final boolean isExact() { + return this.uncertainty == 0; + } + + /** + * Returns the difference of {@code this} and {@code other}. + * + * @since 2020-09-07 + */ + public final UncertainDouble minus(UncertainDouble other) { + Objects.requireNonNull(other, "other may not be null"); + return UncertainDouble.of(this.value - other.value, + Math.hypot(this.uncertainty, other.uncertainty)); + } + + /** + * Returns the difference of {@code this} and the exact value {@code other}. + * + * @since 2020-09-07 + */ + public final UncertainDouble minusExact(double other) { + return UncertainDouble.of(this.value - other, this.uncertainty); + } + + /** + * Returns the sum of {@code this} and {@code other}. + * + * @since 2020-09-07 + */ + public final UncertainDouble plus(UncertainDouble other) { + Objects.requireNonNull(other, "other may not be null"); + return UncertainDouble.of(this.value + other.value, + Math.hypot(this.uncertainty, other.uncertainty)); + } + + /** + * Returns the sum of {@code this} and the exact value {@code other}. + * + * @since 2020-09-07 + */ + public final UncertainDouble plusExact(double other) { + return UncertainDouble.of(this.value + other, this.uncertainty); + } + + /** + * @return relative uncertainty + * @since 2020-09-07 + */ + public final double relativeUncertainty() { + return this.uncertainty / this.value; + } + + /** + * Returns the product of {@code this} and {@code other}. + * + * @since 2020-09-07 + */ + public final UncertainDouble times(UncertainDouble other) { + Objects.requireNonNull(other, "other may not be null"); + return UncertainDouble.ofRelative(this.value * other.value, Math + .hypot(this.relativeUncertainty(), other.relativeUncertainty())); + } + + /** + * Returns the product of {@code this} and the exact value {@code other}. + * + * @since 2020-09-07 + */ + public final UncertainDouble timesExact(double other) { + return UncertainDouble.of(this.value * other, this.uncertainty * other); + } + + /** + * Returns the result of {@code this} raised to the exponent {@code other}. + * + * @since 2020-09-07 + */ + public final UncertainDouble toExponent(UncertainDouble other) { + Objects.requireNonNull(other, "other may not be null"); + + final double result = Math.pow(this.value, other.value); + final double relativeUncertainty = Math.hypot( + other.value * this.relativeUncertainty(), + Math.log(this.value) * other.uncertainty); + + return UncertainDouble.ofRelative(result, relativeUncertainty); + } + + /** + * Returns the result of {@code this} raised the exact exponent + * {@code other}. + * + * @since 2020-09-07 + */ + public final UncertainDouble toExponentExact(double other) { + return UncertainDouble.ofRelative(Math.pow(this.value, other), + this.relativeUncertainty() * other); + } + + /** + * Returns a string representation of this {@code UncertainDouble}. + *

+ * This method returns the same value as {@link #toString(boolean)}, but + * {@code showUncertainty} is true if and only if the uncertainty is + * non-zero. + * + *

+ * Examples: + * + *

+	 * UncertainDouble.of(3.27, 0.22).toString() = "3.3 ± 0.2"
+	 * UncertainDouble.of(3.27, 0.13).toString() = "3.27 ± 0.13"
+	 * UncertainDouble.of(-5.01, 0).toString() = "-5.01"
+	 * 
+ * + * @since 2020-09-07 + */ + @Override + public final String toString() { + return this.toString(!this.isExact()); + } + + /** + * Returns a string representation of this {@code UncertainDouble}. + *

+ * If {@code showUncertainty} is true, the string will be of the form "VALUE + * ± UNCERTAINTY", and if it is false the string will be of the form "VALUE" + *

+ * VALUE represents a string representation of this {@code UncertainDouble}'s + * value. If the uncertainty is non-zero, the string will be rounded to the + * same precision as the uncertainty, otherwise it will not be rounded. The + * string is still rounded if {@code showUncertainty} is false.
+ * UNCERTAINTY represents a string representation of this + * {@code UncertainDouble}'s uncertainty. If the uncertainty ends in 1X + * (where X represents any digit) it will be rounded to two significant + * digits otherwise it will be rounded to one significant digit. + *

+ * Examples: + * + *

+	 * UncertainDouble.of(3.27, 0.22).toString(false) = "3.3"
+	 * UncertainDouble.of(3.27, 0.22).toString(true) = "3.3 ± 0.2"
+	 * UncertainDouble.of(3.27, 0.13).toString(false) = "3.27"
+	 * UncertainDouble.of(3.27, 0.13).toString(true) = "3.27 ± 0.13"
+	 * UncertainDouble.of(-5.01, 0).toString(false) = "-5.01"
+	 * UncertainDouble.of(-5.01, 0).toString(true) = "-5.01 ± 0.0"
+	 * 
+ * + * @since 2020-09-07 + */ + public final String toString(boolean showUncertainty) { + String valueString, uncertaintyString; + + // generate the string representation of value and uncertainty + if (this.isExact()) { + uncertaintyString = "0.0"; + valueString = Double.toString(this.value); + + } else { + // round the value and uncertainty according to getDisplayScale() + final BigDecimal bigValue = BigDecimal.valueOf(this.value); + final BigDecimal bigUncertainty = BigDecimal.valueOf(this.uncertainty); + + final int displayScale = this.getDisplayScale(); + final BigDecimal roundedUncertainty = bigUncertainty + .setScale(displayScale, RoundingMode.HALF_EVEN); + final BigDecimal roundedValue = bigValue.setScale(displayScale, + RoundingMode.HALF_EVEN); + + valueString = roundedValue.toString(); + uncertaintyString = roundedUncertainty.toString(); + } + + // return "value" or "value ± uncertainty" depending on showUncertainty + return valueString + (showUncertainty ? " ± " + uncertaintyString : ""); + } + + /** + * @return absolute uncertainty + * @since 2020-09-07 + */ + public final double uncertainty() { + return this.uncertainty; + } + + /** + * @return value without uncertainty + * @since 2020-09-07 + */ + public final double value() { + return this.value; + } +} diff --git a/src/org/unitConverter/unit/LinearUnit.java b/src/org/unitConverter/unit/LinearUnit.java index 762572a..2d63ca7 100644 --- a/src/org/unitConverter/unit/LinearUnit.java +++ b/src/org/unitConverter/unit/LinearUnit.java @@ -20,89 +20,83 @@ import java.util.Objects; import org.unitConverter.math.DecimalComparison; import org.unitConverter.math.ObjectProduct; +import org.unitConverter.math.UncertainDouble; /** - * A unit that can be expressed as a product of its base and a number. For example, kilometres, inches and pounds. + * A unit that can be expressed as a product of its base and a number. For + * example, kilometres, inches and pounds. * * @author Adrien Hopkins * @since 2019-10-16 */ public final class LinearUnit extends Unit { /** - * Gets a {@code LinearUnit} from a unit and a value. For example, converts '59 °F' to a linear unit with the value - * of '288.15 K' + * Gets a {@code LinearUnit} from a unit and a value. For example, converts + * '59 °F' to a linear unit with the value of '288.15 K' * - * @param unit - * unit to convert - * @param value - * value to convert + * @param unit unit to convert + * @param value value to convert * @return value expressed as a {@code LinearUnit} * @since 2019-10-16 - * @throws NullPointerException - * if unit is null + * @throws NullPointerException if unit is null */ public static LinearUnit fromUnitValue(final Unit unit, final double value) { - return new LinearUnit(Objects.requireNonNull(unit, "unit must not be null.").getBase(), + return new LinearUnit( + Objects.requireNonNull(unit, "unit must not be null.").getBase(), unit.convertToBase(value), NameSymbol.EMPTY); } - + /** - * Gets a {@code LinearUnit} from a unit and a value. For example, converts '59 °F' to a linear unit with the value - * of '288.15 K' + * Gets a {@code LinearUnit} from a unit and a value. For example, converts + * '59 °F' to a linear unit with the value of '288.15 K' * - * @param unit - * unit to convert - * @param value - * value to convert - * @param ns - * name(s) and symbol of unit + * @param unit unit to convert + * @param value value to convert + * @param ns name(s) and symbol of unit * @return value expressed as a {@code LinearUnit} * @since 2019-10-21 - * @throws NullPointerException - * if unit or ns is null + * @throws NullPointerException if unit or ns is null */ - public static LinearUnit fromUnitValue(final Unit unit, final double value, final NameSymbol ns) { - return new LinearUnit(Objects.requireNonNull(unit, "unit must not be null.").getBase(), + public static LinearUnit fromUnitValue(final Unit unit, final double value, + final NameSymbol ns) { + return new LinearUnit( + Objects.requireNonNull(unit, "unit must not be null.").getBase(), unit.convertToBase(value), ns); } - + /** - * Gets a {@code LinearUnit} from a unit base and a conversion factor. In other words, gets the product of - * {@code unitBase} and {@code conversionFactor}, expressed as a {@code LinearUnit}. + * Gets a {@code LinearUnit} from a unit base and a conversion factor. In + * other words, gets the product of {@code unitBase} and + * {@code conversionFactor}, expressed as a {@code LinearUnit}. * - * @param unitBase - * unit base to multiply by - * @param conversionFactor - * number to multiply base by + * @param unitBase unit base to multiply by + * @param conversionFactor number to multiply base by * @return product of base and conversion factor * @since 2019-10-16 - * @throws NullPointerException - * if unitBase is null + * @throws NullPointerException if unitBase is null */ - public static LinearUnit valueOf(final ObjectProduct unitBase, final double conversionFactor) { + public static LinearUnit valueOf(final ObjectProduct unitBase, + final double conversionFactor) { return new LinearUnit(unitBase, conversionFactor, NameSymbol.EMPTY); } - + /** - * Gets a {@code LinearUnit} from a unit base and a conversion factor. In other words, gets the product of - * {@code unitBase} and {@code conversionFactor}, expressed as a {@code LinearUnit}. + * Gets a {@code LinearUnit} from a unit base and a conversion factor. In + * other words, gets the product of {@code unitBase} and + * {@code conversionFactor}, expressed as a {@code LinearUnit}. * - * @param unitBase - * unit base to multiply by - * @param conversionFactor - * number to multiply base by - * @param ns - * name(s) and symbol of unit + * @param unitBase unit base to multiply by + * @param conversionFactor number to multiply base by + * @param ns name(s) and symbol of unit * @return product of base and conversion factor * @since 2019-10-21 - * @throws NullPointerException - * if unitBase is null + * @throws NullPointerException if unitBase is null */ - public static LinearUnit valueOf(final ObjectProduct unitBase, final double conversionFactor, - final NameSymbol ns) { + public static LinearUnit valueOf(final ObjectProduct unitBase, + final double conversionFactor, final NameSymbol ns) { return new LinearUnit(unitBase, conversionFactor, ns); } - + /** * The value of this unit as represented in its base form. Mathematically, * @@ -113,21 +107,20 @@ public final class LinearUnit extends Unit { * @since 2019-10-16 */ private final double conversionFactor; - + /** * Creates the {@code LinearUnit}. * - * @param unitBase - * base of linear unit - * @param conversionFactor - * conversion factor between base and unit + * @param unitBase base of linear unit + * @param conversionFactor conversion factor between base and unit * @since 2019-10-16 */ - private LinearUnit(final ObjectProduct unitBase, final double conversionFactor, final NameSymbol ns) { + private LinearUnit(final ObjectProduct unitBase, + final double conversionFactor, final NameSymbol ns) { super(unitBase, ns); this.conversionFactor = conversionFactor; } - + /** * {@inheritDoc} * @@ -137,7 +130,32 @@ public final class LinearUnit extends Unit { protected double convertFromBase(final double value) { return value / this.getConversionFactor(); } - + + /** + * Converts an {@code UncertainDouble} value expressed in this unit to an + * {@code UncertainValue} value expressed in {@code other}. + * + * @param other unit to convert to + * @param value value to convert + * @return converted value + * @since 2019-09-07 + * @throws IllegalArgumentException if {@code other} is incompatible for + * conversion with this unit (as tested by + * {@link Unit#canConvertTo}). + * @throws NullPointerException if value or other is null + */ + public UncertainDouble convertTo(LinearUnit other, UncertainDouble value) { + Objects.requireNonNull(other, "other must not be null."); + Objects.requireNonNull(value, "value may not be null."); + if (this.canConvertTo(other)) + return value.timesExact( + this.getConversionFactor() / other.getConversionFactor()); + else + throw new IllegalArgumentException( + String.format("Cannot convert from %s to %s.", this, other)); + + } + /** * {@inheritDoc} * @@ -147,12 +165,20 @@ public final class LinearUnit extends Unit { protected double convertToBase(final double value) { return value * this.getConversionFactor(); } - + + /** + * Converts an {@code UncertainDouble} to the base unit. + * + * @since 2020-09-07 + */ + UncertainDouble convertToBase(final UncertainDouble value) { + return value.timesExact(this.getConversionFactor()); + } + /** * Divides this unit by a scalar. * - * @param divisor - * scalar to divide by + * @param divisor scalar to divide by * @return quotient * @since 2018-12-23 * @since v0.1.0 @@ -160,26 +186,26 @@ public final class LinearUnit extends Unit { public LinearUnit dividedBy(final double divisor) { return valueOf(this.getBase(), this.getConversionFactor() / divisor); } - + /** * Returns the quotient of this unit and another. * - * @param divisor - * unit to divide by + * @param divisor unit to divide by * @return quotient of two units - * @throws NullPointerException - * if {@code divisor} is null + * @throws NullPointerException if {@code divisor} is null * @since 2018-12-22 * @since v0.1.0 */ public LinearUnit dividedBy(final LinearUnit divisor) { Objects.requireNonNull(divisor, "other must not be null"); - + // divide the units - final ObjectProduct base = this.getBase().dividedBy(divisor.getBase()); - return valueOf(base, this.getConversionFactor() / divisor.getConversionFactor()); + final ObjectProduct base = this.getBase() + .dividedBy(divisor.getBase()); + return valueOf(base, + this.getConversionFactor() / divisor.getConversionFactor()); } - + /** * {@inheritDoc} * @@ -191,9 +217,10 @@ public final class LinearUnit extends Unit { return false; final LinearUnit other = (LinearUnit) obj; return Objects.equals(this.getBase(), other.getBase()) - && DecimalComparison.equals(this.getConversionFactor(), other.getConversionFactor()); + && DecimalComparison.equals(this.getConversionFactor(), + other.getConversionFactor()); } - + /** * @return conversion factor * @since 2019-10-16 @@ -201,7 +228,7 @@ public final class LinearUnit extends Unit { public double getConversionFactor() { return this.conversionFactor; } - + /** * {@inheritDoc} * @@ -209,18 +236,20 @@ public final class LinearUnit extends Unit { */ @Override public int hashCode() { - return 31 * this.getBase().hashCode() + DecimalComparison.hash(this.getConversionFactor()); + return 31 * this.getBase().hashCode() + + DecimalComparison.hash(this.getConversionFactor()); } - + /** - * @return whether this unit is equivalent to a {@code BaseUnit} (i.e. there is a {@code BaseUnit b} where + * @return whether this unit is equivalent to a {@code BaseUnit} (i.e. there + * is a {@code BaseUnit b} where * {@code b.asLinearUnit().equals(this)} returns {@code true}.) * @since 2019-10-16 */ public boolean isBase() { return this.isCoherent() && this.getBase().isSingleObject(); } - + /** * @return whether this unit is coherent (i.e. has conversion factor 1) * @since 2019-10-16 @@ -228,70 +257,73 @@ public final class LinearUnit extends Unit { public boolean isCoherent() { return this.getConversionFactor() == 1; } - + /** * Returns the difference of this unit and another. *

- * Two units can be subtracted if they have the same base. Note that {@link #canConvertTo} can be used to determine - * this. If {@code subtrahend} does not meet this condition, an {@code IllegalArgumentException} will be thrown. + * Two units can be subtracted if they have the same base. Note that + * {@link #canConvertTo} can be used to determine this. If {@code subtrahend} + * does not meet this condition, an {@code IllegalArgumentException} will be + * thrown. *

* - * @param subtrahend - * unit to subtract + * @param subtrahend unit to subtract * @return difference of units - * @throws IllegalArgumentException - * if {@code subtrahend} is not compatible for subtraction as described above - * @throws NullPointerException - * if {@code subtrahend} is null + * @throws IllegalArgumentException if {@code subtrahend} is not compatible + * for subtraction as described above + * @throws NullPointerException if {@code subtrahend} is null * @since 2019-03-17 * @since v0.2.0 */ public LinearUnit minus(final LinearUnit subtrahend) { Objects.requireNonNull(subtrahend, "addend must not be null."); - + // reject subtrahends that cannot be added to this unit if (!this.getBase().equals(subtrahend.getBase())) - throw new IllegalArgumentException( - String.format("Incompatible units for subtraction \"%s\" and \"%s\".", this, subtrahend)); - + throw new IllegalArgumentException(String.format( + "Incompatible units for subtraction \"%s\" and \"%s\".", this, + subtrahend)); + // subtract the units - return valueOf(this.getBase(), this.getConversionFactor() - subtrahend.getConversionFactor()); + return valueOf(this.getBase(), + this.getConversionFactor() - subtrahend.getConversionFactor()); } - + /** * Returns the sum of this unit and another. *

- * Two units can be added if they have the same base. Note that {@link #canConvertTo} can be used to determine this. - * If {@code addend} does not meet this condition, an {@code IllegalArgumentException} will be thrown. + * Two units can be added if they have the same base. Note that + * {@link #canConvertTo} can be used to determine this. If {@code addend} + * does not meet this condition, an {@code IllegalArgumentException} will be + * thrown. *

* - * @param addend - * unit to add + * @param addend unit to add * @return sum of units - * @throws IllegalArgumentException - * if {@code addend} is not compatible for addition as described above - * @throws NullPointerException - * if {@code addend} is null + * @throws IllegalArgumentException if {@code addend} is not compatible for + * addition as described above + * @throws NullPointerException if {@code addend} is null * @since 2019-03-17 * @since v0.2.0 */ public LinearUnit plus(final LinearUnit addend) { Objects.requireNonNull(addend, "addend must not be null."); - + // reject addends that cannot be added to this unit if (!this.getBase().equals(addend.getBase())) - throw new IllegalArgumentException( - String.format("Incompatible units for addition \"%s\" and \"%s\".", this, addend)); - + throw new IllegalArgumentException(String.format( + "Incompatible units for addition \"%s\" and \"%s\".", this, + addend)); + // add the units - return valueOf(this.getBase(), this.getConversionFactor() + addend.getConversionFactor()); + return valueOf(this.getBase(), + this.getConversionFactor() + addend.getConversionFactor()); } - + /** * Multiplies this unit by a scalar. * - * @param multiplier - * scalar to multiply by + * @param multiplier scalar to multiply by * @return product * @since 2018-12-23 * @since v0.1.0 @@ -299,39 +331,39 @@ public final class LinearUnit extends Unit { public LinearUnit times(final double multiplier) { return valueOf(this.getBase(), this.getConversionFactor() * multiplier); } - + /** * Returns the product of this unit and another. * - * @param multiplier - * unit to multiply by + * @param multiplier unit to multiply by * @return product of two units - * @throws NullPointerException - * if {@code multiplier} is null + * @throws NullPointerException if {@code multiplier} is null * @since 2018-12-22 * @since v0.1.0 */ public LinearUnit times(final LinearUnit multiplier) { Objects.requireNonNull(multiplier, "other must not be null"); - + // multiply the units - final ObjectProduct base = this.getBase().times(multiplier.getBase()); - return valueOf(base, this.getConversionFactor() * multiplier.getConversionFactor()); + final ObjectProduct base = this.getBase() + .times(multiplier.getBase()); + return valueOf(base, + this.getConversionFactor() * multiplier.getConversionFactor()); } - + /** * Returns this unit but to an exponent. * - * @param exponent - * exponent to exponentiate unit to + * @param exponent exponent to exponentiate unit to * @return exponentiated unit * @since 2019-01-15 * @since v0.1.0 */ public LinearUnit toExponent(final int exponent) { - return valueOf(this.getBase().toExponent(exponent), Math.pow(this.conversionFactor, exponent)); + return valueOf(this.getBase().toExponent(exponent), + Math.pow(this.conversionFactor, exponent)); } - + /** * @return a string providing a definition of this unit * @since 2019-10-21 @@ -339,10 +371,13 @@ public final class LinearUnit extends Unit { @Override public String toString() { return this.getPrimaryName().orElse("Unnamed unit") - + (this.getSymbol().isPresent() ? String.format(" (%s)", this.getSymbol().get()) : "") + ", " - + Double.toString(this.conversionFactor) + " * " + this.getBase().toString(u -> u.getSymbol().get()); + + (this.getSymbol().isPresent() + ? String.format(" (%s)", this.getSymbol().get()) + : "") + + ", " + Double.toString(this.conversionFactor) + " * " + + this.getBase().toString(u -> u.getSymbol().get()); } - + @Override public LinearUnit withName(final NameSymbol ns) { return valueOf(this.getBase(), this.getConversionFactor(), ns); @@ -351,37 +386,38 @@ public final class LinearUnit extends Unit { /** * Returns the result of applying {@code prefix} to this unit. *

- * If this unit and the provided prefix have a primary name, the returned unit will have a primary name (prefix's - * name + unit's name).
- * If this unit and the provided prefix have a symbol, the returned unit will have a symbol.
- * This method ignores alternate names of both this unit and the provided prefix. + * If this unit and the provided prefix have a primary name, the returned + * unit will have a primary name (prefix's name + unit's name).
+ * If this unit and the provided prefix have a symbol, the returned unit will + * have a symbol.
+ * This method ignores alternate names of both this unit and the provided + * prefix. * - * @param prefix - * prefix to apply + * @param prefix prefix to apply * @return unit with prefix * @since 2019-03-18 * @since v0.2.0 - * @throws NullPointerException - * if prefix is null + * @throws NullPointerException if prefix is null */ public LinearUnit withPrefix(final UnitPrefix prefix) { final LinearUnit unit = this.times(prefix.getMultiplier()); - + // create new name and symbol, if possible final String name; - if (this.getPrimaryName().isPresent() && prefix.getPrimaryName().isPresent()) { + if (this.getPrimaryName().isPresent() + && prefix.getPrimaryName().isPresent()) { name = prefix.getPrimaryName().get() + this.getPrimaryName().get(); } else { name = null; } - + final String symbol; if (this.getSymbol().isPresent() && prefix.getSymbol().isPresent()) { symbol = prefix.getSymbol().get() + this.getSymbol().get(); } else { symbol = null; } - + return unit.withName(NameSymbol.ofNullable(name, symbol)); } } diff --git a/src/org/unitConverter/unit/LinearUnitValue.java b/src/org/unitConverter/unit/LinearUnitValue.java index 5685a6d..d86d344 100644 --- a/src/org/unitConverter/unit/LinearUnitValue.java +++ b/src/org/unitConverter/unit/LinearUnitValue.java @@ -3,12 +3,11 @@ */ package org.unitConverter.unit; -import java.math.BigDecimal; -import java.math.RoundingMode; import java.util.Objects; import java.util.Optional; import org.unitConverter.math.DecimalComparison; +import org.unitConverter.math.UncertainDouble; /** * A possibly uncertain value expressed in a linear unit. @@ -33,7 +32,8 @@ public final class LinearUnitValue { public static final LinearUnitValue getExact(final LinearUnit unit, final double value) { return new LinearUnitValue( - Objects.requireNonNull(unit, "unit must not be null"), value, 0); + Objects.requireNonNull(unit, "unit must not be null"), + UncertainDouble.of(value, 0)); } /** @@ -46,41 +46,23 @@ public final class LinearUnitValue { * @since 2020-07-26 */ public static final LinearUnitValue of(final LinearUnit unit, - final double value, final double uncertainty) { + final UncertainDouble value) { return new LinearUnitValue( - Objects.requireNonNull(unit, "unit must not be null"), value, - uncertainty); - } - - /** - * Gets an uncertain {@code LinearUnitValue} - * - * @param unit unit to express with - * @param value value to express - * @param relativeUncertainty relative uncertainty of value - * @return uncertain {@code LinearUnitValue} instance - * @since 2020-07-28 - */ - public static final LinearUnitValue ofRelative(final LinearUnit unit, - final double value, final double relativeUncertainty) { - return LinearUnitValue.of(unit, value, relativeUncertainty * value); + Objects.requireNonNull(unit, "unit must not be null"), + Objects.requireNonNull(value, "value may not be null")); } private final LinearUnit unit; - private final double value; - private final double uncertainty; + private final UncertainDouble value; /** - * @param unit unit to express as - * @param value value to express - * @param uncertainty absolute uncertainty of value + * @param unit unit to express as + * @param value value to express * @since 2020-07-26 */ - private LinearUnitValue(final LinearUnit unit, final double value, - final double uncertainty) { + private LinearUnitValue(final LinearUnit unit, final UncertainDouble value) { this.unit = unit; this.value = value; - this.uncertainty = uncertainty; } /** @@ -89,7 +71,7 @@ public final class LinearUnitValue { * @since 2020-08-04 */ public final UnitValue asUnitValue() { - return UnitValue.of(this.unit, this.value); + return UnitValue.of(this.unit, this.value.value()); } /** @@ -110,8 +92,7 @@ public final class LinearUnitValue { * @since 2020-07-26 */ public final LinearUnitValue convertTo(final LinearUnit other) { - return LinearUnitValue.of(other, this.unit.convertTo(other, this.value), - this.unit.convertTo(other, this.uncertainty)); + return LinearUnitValue.of(other, this.unit.convertTo(other, this.value)); } /** @@ -122,8 +103,7 @@ public final class LinearUnitValue { * @since 2020-07-28 */ public LinearUnitValue dividedBy(final double divisor) { - return LinearUnitValue.of(this.unit, this.value / divisor, - this.uncertainty / divisor); + return LinearUnitValue.of(this.unit, this.value.dividedByExact(divisor)); } /** @@ -134,10 +114,8 @@ public final class LinearUnitValue { * @since 2020-07-28 */ public LinearUnitValue dividedBy(final LinearUnitValue divisor) { - return LinearUnitValue.ofRelative(this.unit.dividedBy(divisor.unit), - this.value / divisor.value, - Math.hypot(this.getRelativeUncertainty(), - divisor.getRelativeUncertainty())); + return LinearUnitValue.of(this.unit.dividedBy(divisor.unit), + this.value.dividedBy(divisor.value)); } /** @@ -154,12 +132,8 @@ public final class LinearUnitValue { return false; final LinearUnitValue other = (LinearUnitValue) obj; return Objects.equals(this.unit.getBase(), other.unit.getBase()) - && Double.doubleToLongBits( - this.unit.convertToBase(this.getValue())) == Double - .doubleToLongBits( - other.unit.convertToBase(other.getValue())) - && Double.doubleToLongBits(this.getRelativeUncertainty()) == Double - .doubleToLongBits(other.getRelativeUncertainty()); + && this.unit.convertToBase(this.value) + .equals(other.unit.convertToBase(other.value)); } /** @@ -180,9 +154,7 @@ public final class LinearUnitValue { final LinearUnitValue other = (LinearUnitValue) obj; return Objects.equals(this.unit.getBase(), other.unit.getBase()) && DecimalComparison.equals(this.unit.convertToBase(this.value), - other.unit.convertToBase(other.value)) - && DecimalComparison.equals(this.getRelativeUncertainty(), - other.getRelativeUncertainty()); + other.unit.convertToBase(other.value)); } /** @@ -195,32 +167,11 @@ public final class LinearUnitValue { if (other == null || !Objects.equals(this.unit.getBase(), other.unit.getBase())) return false; - final double thisBaseValue = this.unit.convertToBase(this.value); - final double otherBaseValue = other.unit.convertToBase(other.value); - final double thisBaseUncertainty = this.unit - .convertToBase(this.uncertainty); - final double otherBaseUncertainty = other.unit - .convertToBase(other.uncertainty); - return Math.abs(thisBaseValue - otherBaseValue) <= Math - .min(thisBaseUncertainty, otherBaseUncertainty); - } - - /** - * @return relative uncertainty of value - * - * @since 2020-07-26 - */ - public final double getRelativeUncertainty() { - return this.uncertainty / this.value; - } - - /** - * @return absolute uncertainty of value - * - * @since 2020-07-26 - */ - public final double getUncertainty() { - return this.uncertainty; + final LinearUnit base = LinearUnit.valueOf(this.unit.getBase(), 1); + final LinearUnitValue thisBase = this.convertTo(base); + final LinearUnitValue otherBase = other.convertTo(base); + + return thisBase.value.equivalent(otherBase.value); } /** @@ -237,24 +188,22 @@ public final class LinearUnitValue { * * @since 2020-07-26 */ - public final double getValue() { + public final UncertainDouble getValue() { return this.value; } + /** + * @return the exact value + * @since 2020-09-07 + */ + public final double getValueExact() { + return this.value.value(); + } + @Override public int hashCode() { return Objects.hash(this.unit.getBase(), - this.unit.convertToBase(this.getValue()), - this.getRelativeUncertainty()); - } - - /** - * @return true iff the value has no uncertainty - * - * @since 2020-07-26 - */ - public final boolean isExact() { - return this.uncertainty == 0; + this.unit.convertToBase(this.getValue())); } /** @@ -276,8 +225,8 @@ public final class LinearUnitValue { this.unit, subtrahend.unit)); final LinearUnitValue otherConverted = subtrahend.convertTo(this.unit); - return LinearUnitValue.of(this.unit, this.value - otherConverted.value, - Math.hypot(this.uncertainty, otherConverted.uncertainty)); + return LinearUnitValue.of(this.unit, + this.value.minus(otherConverted.value)); } /** @@ -298,8 +247,8 @@ public final class LinearUnitValue { addend.unit)); final LinearUnitValue otherConverted = addend.convertTo(this.unit); - return LinearUnitValue.of(this.unit, this.value + otherConverted.value, - Math.hypot(this.uncertainty, otherConverted.uncertainty)); + return LinearUnitValue.of(this.unit, + this.value.plus(otherConverted.value)); } /** @@ -310,8 +259,7 @@ public final class LinearUnitValue { * @since 2020-07-28 */ public LinearUnitValue times(final double multiplier) { - return LinearUnitValue.of(this.unit, this.value * multiplier, - this.uncertainty * multiplier); + return LinearUnitValue.of(this.unit, this.value.timesExact(multiplier)); } /** @@ -322,10 +270,8 @@ public final class LinearUnitValue { * @since 2020-07-28 */ public LinearUnitValue times(final LinearUnitValue multiplier) { - return LinearUnitValue.ofRelative(this.unit.times(multiplier.unit), - this.value * multiplier.value, - Math.hypot(this.getRelativeUncertainty(), - multiplier.getRelativeUncertainty())); + return LinearUnitValue.of(this.unit.times(multiplier.unit), + this.value.times(multiplier.value)); } /** @@ -336,14 +282,13 @@ public final class LinearUnitValue { * @since 2020-07-28 */ public LinearUnitValue toExponent(final int exponent) { - return LinearUnitValue.ofRelative(this.unit.toExponent(exponent), - Math.pow(this.value, exponent), - this.getRelativeUncertainty() * Math.sqrt(exponent)); + return LinearUnitValue.of(this.unit.toExponent(exponent), + this.value.toExponentExact(exponent)); } @Override public String toString() { - return this.toString(!this.isExact()); + return this.toString(!this.value.isExact()); } /** @@ -363,107 +308,22 @@ public final class LinearUnitValue { final Optional symbol = this.unit.getSymbol(); final String chosenName = symbol.orElse(primaryName.orElse(null)); - final double baseValue = this.unit.convertToBase(this.value); - final double baseUncertainty = this.unit.convertToBase(this.uncertainty); + final UncertainDouble baseValue = this.unit.convertToBase(this.value); // get rounded strings - String valueString, baseValueString, uncertaintyString, - baseUncertaintyString; - if (this.isExact()) { - valueString = Double.toString(this.value); - baseValueString = Double.toString(baseValue); - uncertaintyString = "0"; - baseUncertaintyString = "0"; - } else { - final BigDecimal bigValue = BigDecimal.valueOf(this.value); - final BigDecimal bigUncertainty = BigDecimal.valueOf(this.uncertainty); - - // round based on uncertainty - // if uncertainty starts with 1 (ignoring zeroes and the decimal - // point), rounds - // so that uncertainty has 2 significant digits. - // otherwise, rounds so that uncertainty has 1 significant digits. - // the value is rounded to the same number of decimal places as the - // uncertainty. - BigDecimal roundedUncertainty = bigUncertainty.setScale( - bigUncertainty.scale() - bigUncertainty.precision() + 2, - RoundingMode.HALF_EVEN); - if (roundedUncertainty.unscaledValue().intValue() >= 20) { - roundedUncertainty = bigUncertainty.setScale( - bigUncertainty.scale() - bigUncertainty.precision() + 1, - RoundingMode.HALF_EVEN); - } - final BigDecimal roundedValue = bigValue - .setScale(roundedUncertainty.scale(), RoundingMode.HALF_EVEN); - - valueString = roundedValue.toString(); - uncertaintyString = roundedUncertainty.toString(); - - if (primaryName.isEmpty() && symbol.isEmpty()) { - final BigDecimal bigBaseValue = BigDecimal.valueOf(baseValue); - final BigDecimal bigBaseUncertainty = BigDecimal - .valueOf(baseUncertainty); - - BigDecimal roundedBaseUncertainty = bigBaseUncertainty - .setScale( - bigBaseUncertainty.scale() - - bigBaseUncertainty.precision() + 2, - RoundingMode.HALF_EVEN); - if (roundedBaseUncertainty.unscaledValue().intValue() >= 20) { - roundedBaseUncertainty = bigBaseUncertainty - .setScale( - bigBaseUncertainty.scale() - - bigBaseUncertainty.precision() + 1, - RoundingMode.HALF_EVEN); - } - final BigDecimal roundedBaseValue = bigBaseValue.setScale( - roundedBaseUncertainty.scale(), RoundingMode.HALF_EVEN); - - baseValueString = roundedBaseValue.toString(); - baseUncertaintyString = roundedBaseUncertainty.toString(); - } else { - // unused - baseValueString = ""; - baseUncertaintyString = ""; - } - } + // if showUncertainty is true, add brackets around the string + final String valueString = showUncertainty ? "(" + : "" + this.value.toString(showUncertainty) + + (showUncertainty ? ")" : ""); + final String baseValueString = showUncertainty ? "(" + : "" + baseValue.toString(showUncertainty) + + (showUncertainty ? ")" : ""); // create string - if (showUncertainty) { - if (primaryName.isEmpty() && symbol.isEmpty()) - return String.format("(%s ± %s) unnamed unit (= %s ± %s %s)", - valueString, uncertaintyString, baseValueString, - baseUncertaintyString, this.unit.getBase()); - else - return String.format("(%s ± %s) %s", valueString, uncertaintyString, - chosenName); - } else { - // truncate excess zeroes - if (valueString.contains(".")) { - while (valueString.endsWith("0")) { - valueString = valueString.substring(0, valueString.length() - 1); - } - if (valueString.endsWith(".")) { - valueString = valueString.substring(0, valueString.length() - 1); - } - } - - if (baseValueString.contains(".")) { - while (baseValueString.endsWith("0")) { - baseValueString = baseValueString.substring(0, - baseValueString.length() - 1); - } - if (baseValueString.endsWith(".")) { - baseValueString = baseValueString.substring(0, - baseValueString.length() - 1); - } - } - - if (primaryName.isEmpty() && symbol.isEmpty()) - return String.format("%s unnamed unit (= %s %s)", valueString, - baseValueString, this.unit.getBase()); - else - return String.format("%s %s", valueString, chosenName); - } + if (primaryName.isEmpty() && symbol.isEmpty()) + return String.format("%s unnamed unit (= %s %s)", valueString, + baseValueString, this.unit.getBase()); + else + return String.format("%s %s", valueString, chosenName); } } diff --git a/src/org/unitConverter/unit/UnitDatabase.java b/src/org/unitConverter/unit/UnitDatabase.java index 9812bd0..9ca9617 100644 --- a/src/org/unitConverter/unit/UnitDatabase.java +++ b/src/org/unitConverter/unit/UnitDatabase.java @@ -46,6 +46,7 @@ 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. @@ -1134,7 +1135,7 @@ public final class UnitDatabase { // 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.getValue(); + final double exponent = exponentValue.getValueExact(); if (DecimalComparison.equals(exponent % 1, 0)) // then exponentiate return base.toExponent((int) (exponent + 0.5)); @@ -1650,7 +1651,8 @@ public final class UnitDatabase { final BigDecimal number = new BigDecimal(name); final double uncertainty = Math.pow(10, -number.scale()); - return LinearUnitValue.of(SI.ONE, number.doubleValue(), uncertainty); + return LinearUnitValue.of(SI.ONE, + UncertainDouble.of(number.doubleValue(), uncertainty)); } catch (final NumberFormatException e) { return LinearUnitValue.getExact(this.getLinearUnit(name), 1); } diff --git a/src/org/unitConverter/unit/UnitTest.java b/src/org/unitConverter/unit/UnitTest.java index ff83805..c0711dc 100644 --- a/src/org/unitConverter/unit/UnitTest.java +++ b/src/org/unitConverter/unit/UnitTest.java @@ -56,9 +56,10 @@ class UnitTest { final LinearUnitValue value4 = LinearUnitValue.getExact(SI.KILOGRAM, 60); // make sure addition is done correctly - assertEquals(51.576, value1.plus(value2).getValue(), 0.001); - assertEquals(15.5, value1.plus(value3).getValue()); - assertEquals(52.076, value1.plus(value2).plus(value3).getValue(), 0.001); + assertEquals(51.576, value1.plus(value2).getValueExact(), 0.001); + assertEquals(15.5, value1.plus(value3).getValueExact()); + assertEquals(52.076, value1.plus(value2).plus(value3).getValueExact(), + 0.001); // make sure addition uses the correct unit, and is still associative // (ignoring floating-point rounding errors) -- cgit v1.2.3 From 184b7cc697ffc2dcbd49cfb3d0fd7b14bdac8803 Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Sat, 13 Mar 2021 15:55:35 -0500 Subject: Upgraded to Java 11, and upgraded file code to the new I/O --- .classpath | 6 +-- .settings/org.eclipse.jdt.core.prefs | 11 +++-- .../converterGUI/UnitConverterGUI.java | 57 +++++++++++----------- src/org/unitConverter/unit/UnitDatabase.java | 27 ++++------ 4 files changed, 45 insertions(+), 56 deletions(-) (limited to 'src/org/unitConverter/unit/UnitDatabase.java') diff --git a/.classpath b/.classpath index 35e2ca9..a6325e3 100644 --- a/.classpath +++ b/.classpath @@ -1,10 +1,6 @@ - - - - - + diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs index 2dd1838..01cf56c 100644 --- a/.settings/org.eclipse.jdt.core.prefs +++ b/.settings/org.eclipse.jdt.core.prefs @@ -1,15 +1,16 @@ eclipse.preferences.version=1 org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled -org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 +org.eclipse.jdt.core.compiler.codegen.methodParameters=do not generate +org.eclipse.jdt.core.compiler.codegen.targetPlatform=11 org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve -org.eclipse.jdt.core.compiler.compliance=1.8 +org.eclipse.jdt.core.compiler.compliance=11 org.eclipse.jdt.core.compiler.debug.lineNumber=generate org.eclipse.jdt.core.compiler.debug.localVariable=generate org.eclipse.jdt.core.compiler.debug.sourceFile=generate org.eclipse.jdt.core.compiler.problem.assertIdentifier=error org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled org.eclipse.jdt.core.compiler.problem.enumIdentifier=error -org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=ignore +org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=warning org.eclipse.jdt.core.compiler.processAnnotations=disabled -org.eclipse.jdt.core.compiler.release=disabled -org.eclipse.jdt.core.compiler.source=1.8 +org.eclipse.jdt.core.compiler.release=enabled +org.eclipse.jdt.core.compiler.source=11 diff --git a/src/org/unitConverter/converterGUI/UnitConverterGUI.java b/src/org/unitConverter/converterGUI/UnitConverterGUI.java index 69f188b..6ddc4a0 100644 --- a/src/org/unitConverter/converterGUI/UnitConverterGUI.java +++ b/src/org/unitConverter/converterGUI/UnitConverterGUI.java @@ -21,10 +21,7 @@ import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.GridLayout; import java.awt.event.KeyEvent; -import java.io.BufferedReader; import java.io.BufferedWriter; -import java.io.File; -import java.io.FileReader; import java.io.IOException; import java.math.BigDecimal; import java.math.MathContext; @@ -95,6 +92,14 @@ final class UnitConverterGUI { private static class Presenter { /** The default place where settings are stored. */ private static final String DEFAULT_SETTINGS_FILEPATH = "settings.txt"; + /** The default place where units are stored. */ + private static final Path DEFAULT_UNITS_FILE = Path.of("unitsfile.txt"); + /** The default place where dimensions are stored. */ + private static final Path DEFAULT_DIMENSION_FILE = Path + .of("dimensionfile.txt"); + /** The default place where exceptions are stored. */ + private static final Path DEFAULT_EXCEPTIONS_FILE = Path + .of("metric_exceptions.txt"); /** * Adds default units and dimensions to a database. @@ -125,6 +130,15 @@ final class UnitConverterGUI { database.addDimension("TEMPERATURE", SI.Dimensions.TEMPERATURE); } + /** + * @return {@code line} with any comments removed. + * @since 2021-03-13 + */ + private static final String withoutComments(String line) { + final int index = line.indexOf('#'); + return index == -1 ? line : line.substring(index); + } + /** The presenter's associated view. */ private final View view; @@ -150,11 +164,11 @@ final class UnitConverterGUI { /** The prefix rule */ private DefaultPrefixRepetitionRule prefixRule = null; - // conditions for existence of From and To entries // used for one-way conversion private final MutablePredicate fromExistenceCondition = new MutablePredicate<>( s -> true); + private final MutablePredicate toExistenceCondition = new MutablePredicate<>( s -> true); @@ -182,27 +196,14 @@ final class UnitConverterGUI { DefaultPrefixRepetitionRule.NO_RESTRICTION); Presenter.addDefaults(this.database); - this.database.loadUnitsFile(new File("unitsfile.txt")); - this.database.loadDimensionFile(new File("dimensionfile.txt")); + this.database.loadUnitsFile(DEFAULT_UNITS_FILE); + this.database.loadDimensionFile(DEFAULT_DIMENSION_FILE); // load metric exceptions - final File exceptions = new File("metric_exceptions.txt"); - this.metricExceptions = new HashSet<>(); - try (FileReader fileReader = new FileReader(exceptions); - BufferedReader reader = new BufferedReader(fileReader)) { - while (reader.ready()) { - String line = reader.readLine(); - - // # can be used for comments - if (line.contains("#")) { - line = line.substring(line.indexOf("#")); - } - - // don't read black lines - if (!line.isBlank()) { - this.metricExceptions.add(line.strip()); - } - } + try { + this.metricExceptions = Files.readAllLines(DEFAULT_EXCEPTIONS_FILE) + .stream().map(Presenter::withoutComments) + .filter(s -> !s.isBlank()).collect(Collectors.toSet()); } catch (final IOException e) { throw new AssertionError("Loading of metric_exceptions.txt failed.", e); @@ -486,13 +487,11 @@ final class UnitConverterGUI { * @since 2021-02-17 */ public final void loadSettings() { - try (BufferedReader reader = Files - .newBufferedReader(this.getSettingsFile())) { - + try { // read file line by line final int lineNum = 0; - String line; - while ((line = reader.readLine()) != null) { + for (final String line : Files + .readAllLines(this.getSettingsFile())) { final int equalsIndex = line.indexOf('='); if (equalsIndex == -1) throw new IllegalStateException( @@ -1144,7 +1143,7 @@ final class UnitConverterGUI { try { final Path aboutFile = Path.of("src", "about.txt"); infoText = Files.readAllLines(aboutFile).stream() - .filter(s -> !s.trim().startsWith("#")) + .map(Presenter::withoutComments) .collect(Collectors.joining("\n")); } catch (final IOException e) { throw new AssertionError("I/O exception loading about.txt"); diff --git a/src/org/unitConverter/unit/UnitDatabase.java b/src/org/unitConverter/unit/UnitDatabase.java index 9ca9617..000acf5 100644 --- a/src/org/unitConverter/unit/UnitDatabase.java +++ b/src/org/unitConverter/unit/UnitDatabase.java @@ -16,12 +16,11 @@ */ 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; @@ -1866,15 +1865,12 @@ public final class UnitDatabase { * @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); @@ -1908,15 +1904,12 @@ public final class UnitDatabase { * @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); -- cgit v1.2.3