summaryrefslogtreecommitdiff
path: root/src/org/unitConverter
diff options
context:
space:
mode:
Diffstat (limited to 'src/org/unitConverter')
-rwxr-xr-xsrc/org/unitConverter/UnitsDatabase.java1479
-rwxr-xr-xsrc/org/unitConverter/converterGUI/DelegateListModel.java242
-rwxr-xr-xsrc/org/unitConverter/converterGUI/FilterComparator.java129
-rwxr-xr-xsrc/org/unitConverter/converterGUI/GridBagBuilder.java479
-rw-r--r--src/org/unitConverter/converterGUI/MutablePredicate.java70
-rw-r--r--src/org/unitConverter/converterGUI/SearchBoxList.java297
-rwxr-xr-xsrc/org/unitConverter/converterGUI/UnitConverterGUI.java827
-rw-r--r--src/org/unitConverter/converterGUI/package-info.java24
-rwxr-xr-xsrc/org/unitConverter/dimension/BaseDimension.java40
-rwxr-xr-xsrc/org/unitConverter/dimension/OtherBaseDimension.java55
-rwxr-xr-xsrc/org/unitConverter/dimension/SIBaseDimension.java57
-rwxr-xr-xsrc/org/unitConverter/dimension/StandardDimensions.java80
-rwxr-xr-xsrc/org/unitConverter/dimension/UnitDimension.java241
-rwxr-xr-xsrc/org/unitConverter/dimension/package-info.java24
-rw-r--r--src/org/unitConverter/math/DecimalComparison.java114
-rw-r--r--src/org/unitConverter/math/ExpressionParser.java708
-rw-r--r--src/org/unitConverter/math/package-info.java23
-rw-r--r--src/org/unitConverter/package-info.java24
-rw-r--r--src/org/unitConverter/unit/AbstractUnit.java117
-rwxr-xr-xsrc/org/unitConverter/unit/BaseUnit.java168
-rwxr-xr-xsrc/org/unitConverter/unit/DefaultUnitPrefix.java68
-rw-r--r--src/org/unitConverter/unit/LinearUnit.java294
-rwxr-xr-xsrc/org/unitConverter/unit/NonlinearUnits.java57
-rw-r--r--src/org/unitConverter/unit/SI.java74
-rwxr-xr-xsrc/org/unitConverter/unit/SIPrefix.java54
-rwxr-xr-xsrc/org/unitConverter/unit/Unit.java110
-rwxr-xr-xsrc/org/unitConverter/unit/UnitPrefix.java72
-rwxr-xr-xsrc/org/unitConverter/unit/UnitSystem.java53
-rw-r--r--src/org/unitConverter/unit/package-info.java24
29 files changed, 6004 insertions, 0 deletions
diff --git a/src/org/unitConverter/UnitsDatabase.java b/src/org/unitConverter/UnitsDatabase.java
new file mode 100755
index 0000000..e5d2f67
--- /dev/null
+++ b/src/org/unitConverter/UnitsDatabase.java
@@ -0,0 +1,1479 @@
+/**
+ * Copyright (C) 2018 Adrien Hopkins
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package org.unitConverter;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileReader;
+import java.io.IOException;
+import java.util.AbstractSet;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.Objects;
+import java.util.Set;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+import org.unitConverter.dimension.UnitDimension;
+import org.unitConverter.math.DecimalComparison;
+import org.unitConverter.math.ExpressionParser;
+import org.unitConverter.unit.DefaultUnitPrefix;
+import org.unitConverter.unit.LinearUnit;
+import org.unitConverter.unit.SI;
+import org.unitConverter.unit.Unit;
+import org.unitConverter.unit.UnitPrefix;
+
+/**
+ * A database of units, prefixes and dimensions, and their names.
+ *
+ * @author Adrien Hopkins
+ * @since 2019-01-07
+ * @since v0.1.0
+ */
+public final class UnitsDatabase {
+ /**
+ * A map for units that allows the use of prefixes.
+ * <p>
+ * As this map implementation is intended to be used as a sort of "augmented view" of a unit and prefix map, it is
+ * unmodifiable but instead reflects the changes to the maps passed into it. Do not edit this map, instead edit the
+ * maps that were passed in during construction.
+ * </p>
+ * <p>
+ * The rules for applying prefixes onto units are the following:
+ * <ul>
+ * <li>Prefixes can only be applied to linear units.</li>
+ * <li>Before attempting to search for prefixes in a unit name, this map will first search for a unit name. So, if
+ * there are two units, "B" and "AB", and a prefix "A", this map will favour the unit "AB" over the unit "B" with
+ * the prefix "A", even though they have the same string.</li>
+ * <li>Longer prefixes are preferred to shorter prefixes. So, if you have units "BC" and "C", and prefixes "AB" and
+ * "A", inputting "ABC" will return the unit "C" with the prefix "AB", not "BC" with the prefix "A".</li>
+ * </ul>
+ * </p>
+ * <p>
+ * This map is infinite in size if there is at least one unit and at least one prefix. If it is infinite, some
+ * operations that only work with finite collections, like converting name/entry sets to arrays, will throw an
+ * {@code UnsupportedOperationException}.
+ * </p>
+ *
+ * @author Adrien Hopkins
+ * @since 2019-04-13
+ * @since v0.2.0
+ */
+ private static final class PrefixedUnitMap implements Map<String, Unit> {
+ /**
+ * The class used for entry sets.
+ *
+ * @author Adrien Hopkins
+ * @since 2019-04-13
+ * @since v0.2.0
+ */
+ private static final class PrefixedUnitEntrySet extends AbstractSet<Map.Entry<String, Unit>> {
+ /**
+ * The entry for this set.
+ *
+ * @author Adrien Hopkins
+ * @since 2019-04-14
+ * @since v0.2.0
+ */
+ private static final class PrefixedUnitEntry implements Entry<String, Unit> {
+ private final String key;
+ private final Unit value;
+
+ /**
+ * Creates the {@code PrefixedUnitEntry}.
+ *
+ * @param key
+ * key
+ * @param value
+ * value
+ * @since 2019-04-14
+ * @since v0.2.0
+ */
+ public PrefixedUnitEntry(final String key, final Unit value) {
+ this.key = key;
+ this.value = value;
+ }
+
+ @Override
+ public String getKey() {
+ return this.key;
+ }
+
+ @Override
+ public Unit getValue() {
+ return this.value;
+ }
+
+ @Override
+ public Unit setValue(final Unit value) {
+ throw new UnsupportedOperationException();
+ }
+ }
+
+ /**
+ * An iterator that iterates over the units of a {@code PrefixedUnitNameSet}.
+ *
+ * @author Adrien Hopkins
+ * @since 2019-04-14
+ * @since v0.2.0
+ */
+ private static final class PrefixedUnitEntryIterator implements Iterator<Entry<String, Unit>> {
+ // position in the unit list
+ private int unitNamePosition = 0;
+ // the indices of the prefixes attached to the current unit
+ private final List<Integer> prefixCoordinates = new ArrayList<>();
+
+ // values from the unit entry set
+ private final Map<String, Unit> map;
+ private final List<String> unitNames;
+ private final List<String> prefixNames;
+
+ /**
+ * Creates the {@code UnitsDatabase.PrefixedUnitMap.PrefixedUnitNameSet.PrefixedUnitNameIterator}.
+ *
+ * @since 2019-04-14
+ * @since v0.2.0
+ */
+ public PrefixedUnitEntryIterator(final PrefixedUnitEntrySet set) {
+ this.map = set.map;
+ this.unitNames = new ArrayList<>(set.map.units.keySet());
+ this.prefixNames = new ArrayList<>(set.map.prefixes.keySet());
+ }
+
+ /**
+ * @return current unit name
+ * @since 2019-04-14
+ * @since v0.2.0
+ */
+ private String getCurrentUnitName() {
+ final StringBuilder unitName = new StringBuilder();
+ for (final int i : this.prefixCoordinates) {
+ unitName.append(this.prefixNames.get(i));
+ }
+ unitName.append(this.unitNames.get(this.unitNamePosition));
+
+ return unitName.toString();
+ }
+
+ @Override
+ public boolean hasNext() {
+ if (this.unitNames.isEmpty())
+ return false;
+ else {
+ if (this.prefixNames.isEmpty())
+ return this.unitNamePosition >= this.unitNames.size() - 1;
+ else
+ return true;
+ }
+ }
+
+ /**
+ * Changes this iterator's position to the next available one.
+ *
+ * @since 2019-04-14
+ * @since v0.2.0
+ */
+ private void incrementPosition() {
+ this.unitNamePosition++;
+
+ if (this.unitNamePosition >= this.unitNames.size()) {
+ // we have used all of our units, go to a different prefix
+ this.unitNamePosition = 0;
+
+ // if the prefix coordinates are empty, then set it to [0]
+ if (this.prefixCoordinates.isEmpty()) {
+ this.prefixCoordinates.add(0, 0);
+ } else {
+ // get the prefix coordinate to increment, then increment
+ int i = this.prefixCoordinates.size() - 1;
+ this.prefixCoordinates.set(i, this.prefixCoordinates.get(i) + 1);
+
+ // fix any carrying errors
+ while (i >= 0 && this.prefixCoordinates.get(i) >= this.prefixNames.size()) {
+ // carry over
+ this.prefixCoordinates.set(i--, 0); // null and decrement at the same time
+
+ if (i < 0) { // we need to add a new coordinate
+ this.prefixCoordinates.add(0, 0);
+ } else { // increment an existing one
+ this.prefixCoordinates.set(i, this.prefixCoordinates.get(i) + 1);
+ }
+ }
+ }
+ }
+ }
+
+ @Override
+ public Entry<String, Unit> next() {
+ if (!this.hasNext())
+ throw new NoSuchElementException("No units left!");
+ // if I have prefixes, ensure I'm not using a nonlinear unit
+ // since all of the unprefixed stuff is done, just remove nonlinear units
+ if (!this.prefixCoordinates.isEmpty()) {
+ while (this.unitNamePosition < this.unitNames.size()
+ && !(this.map.get(this.unitNames.get(this.unitNamePosition)) instanceof LinearUnit)) {
+ this.unitNames.remove(this.unitNamePosition);
+ }
+ }
+
+ final String nextName = this.getCurrentUnitName();
+
+ this.incrementPosition();
+
+ return new PrefixedUnitEntry(nextName, this.map.get(nextName));
+ }
+ }
+
+ // the map that created this set
+ private final PrefixedUnitMap map;
+
+ /**
+ * Creates the {@code PrefixedUnitNameSet}.
+ *
+ * @param map
+ * map that created this set
+ * @since 2019-04-13
+ * @since v0.2.0
+ */
+ public PrefixedUnitEntrySet(final PrefixedUnitMap map) {
+ this.map = map;
+ }
+
+ @Override
+ public boolean add(final Map.Entry<String, Unit> e) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean addAll(final Collection<? extends Map.Entry<String, Unit>> c) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void clear() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean contains(final Object o) {
+ // get the entry
+ final Entry<String, Unit> entry;
+
+ try {
+ // This is OK because I'm in a try-catch block.
+ @SuppressWarnings("unchecked")
+ final Entry<String, Unit> tempEntry = (Entry<String, Unit>) o;
+ entry = tempEntry;
+ } catch (final ClassCastException e) {
+ throw new IllegalArgumentException("Attempted to test for an entry using a non-entry.");
+ }
+
+ return this.map.containsKey(entry.getKey()) && this.map.get(entry.getKey()).equals(entry.getValue());
+ }
+
+ @Override
+ public boolean containsAll(final Collection<?> c) {
+ for (final Object o : c)
+ if (!this.contains(o))
+ return false;
+ return true;
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return this.map.isEmpty();
+ }
+
+ @Override
+ public Iterator<Entry<String, Unit>> iterator() {
+ return new PrefixedUnitEntryIterator(this);
+ }
+
+ @Override
+ public boolean remove(final Object o) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean removeAll(final Collection<?> c) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean removeIf(final Predicate<? super Entry<String, Unit>> filter) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean retainAll(final Collection<?> c) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public int size() {
+ if (this.map.units.isEmpty())
+ return 0;
+ else {
+ if (this.map.prefixes.isEmpty())
+ return this.map.units.size();
+ else
+ // infinite set
+ return Integer.MAX_VALUE;
+ }
+ }
+
+ @Override
+ public Object[] toArray() {
+ if (this.map.units.isEmpty() || this.map.prefixes.isEmpty())
+ return super.toArray();
+ else
+ // infinite set
+ throw new UnsupportedOperationException("Cannot make an infinite set into an array.");
+ }
+
+ @Override
+ public <T> T[] toArray(final T[] a) {
+ if (this.map.units.isEmpty() || this.map.prefixes.isEmpty())
+ return super.toArray(a);
+ else
+ // infinite set
+ throw new UnsupportedOperationException("Cannot make an infinite set into an array.");
+ }
+
+ }
+
+ /**
+ * The class used for unit name sets.
+ *
+ * @author Adrien Hopkins
+ * @since 2019-04-13
+ * @since v0.2.0
+ */
+ private static final class PrefixedUnitNameSet extends AbstractSet<String> {
+ /**
+ * An iterator that iterates over the units of a {@code PrefixedUnitNameSet}.
+ *
+ * @author Adrien Hopkins
+ * @since 2019-04-14
+ * @since v0.2.0
+ */
+ private static final class PrefixedUnitNameIterator implements Iterator<String> {
+ // position in the unit list
+ private int unitNamePosition = 0;
+ // the indices of the prefixes attached to the current unit
+ private final List<Integer> prefixCoordinates = new ArrayList<>();
+
+ // values from the unit name set
+ private final Map<String, Unit> map;
+ private final List<String> unitNames;
+ private final List<String> prefixNames;
+
+ /**
+ * Creates the {@code UnitsDatabase.PrefixedUnitMap.PrefixedUnitNameSet.PrefixedUnitNameIterator}.
+ *
+ * @since 2019-04-14
+ * @since v0.2.0
+ */
+ public PrefixedUnitNameIterator(final PrefixedUnitNameSet set) {
+ this.map = set.map;
+ this.unitNames = new ArrayList<>(set.map.units.keySet());
+ this.prefixNames = new ArrayList<>(set.map.prefixes.keySet());
+ }
+
+ /**
+ * @return current unit name
+ * @since 2019-04-14
+ * @since v0.2.0
+ */
+ private String getCurrentUnitName() {
+ final StringBuilder unitName = new StringBuilder();
+ for (final int i : this.prefixCoordinates) {
+ unitName.append(this.prefixNames.get(i));
+ }
+ unitName.append(this.unitNames.get(this.unitNamePosition));
+
+ return unitName.toString();
+ }
+
+ @Override
+ public boolean hasNext() {
+ if (this.unitNames.isEmpty())
+ return false;
+ else {
+ if (this.prefixNames.isEmpty())
+ return this.unitNamePosition >= this.unitNames.size() - 1;
+ else
+ return true;
+ }
+ }
+
+ /**
+ * Changes this iterator's position to the next available one.
+ *
+ * @since 2019-04-14
+ * @since v0.2.0
+ */
+ private void incrementPosition() {
+ this.unitNamePosition++;
+
+ if (this.unitNamePosition >= this.unitNames.size()) {
+ // we have used all of our units, go to a different prefix
+ this.unitNamePosition = 0;
+
+ // if the prefix coordinates are empty, then set it to [0]
+ if (this.prefixCoordinates.isEmpty()) {
+ this.prefixCoordinates.add(0, 0);
+ } else {
+ // get the prefix coordinate to increment, then increment
+ int i = this.prefixCoordinates.size() - 1;
+ this.prefixCoordinates.set(i, this.prefixCoordinates.get(i) + 1);
+
+ // fix any carrying errors
+ while (i >= 0 && this.prefixCoordinates.get(i) >= this.prefixNames.size()) {
+ // carry over
+ this.prefixCoordinates.set(i--, 0); // null and decrement at the same time
+
+ if (i < 0) { // we need to add a new coordinate
+ this.prefixCoordinates.add(0, 0);
+ } else { // increment an existing one
+ this.prefixCoordinates.set(i, this.prefixCoordinates.get(i) + 1);
+ }
+ }
+ }
+ }
+ }
+
+ @Override
+ public String next() {
+ if (!this.hasNext())
+ throw new NoSuchElementException("No units left!");
+ // if I have prefixes, ensure I'm not using a nonlinear unit
+ // since all of the unprefixed stuff is done, just remove nonlinear units
+ if (!this.prefixCoordinates.isEmpty()) {
+ while (this.unitNamePosition < this.unitNames.size()
+ && !(this.map.get(this.unitNames.get(this.unitNamePosition)) instanceof LinearUnit)) {
+ this.unitNames.remove(this.unitNamePosition);
+ }
+ }
+
+ final String nextName = this.getCurrentUnitName();
+
+ this.incrementPosition();
+
+ return nextName;
+ }
+ }
+
+ // the map that created this set
+ private final PrefixedUnitMap map;
+
+ /**
+ * Creates the {@code PrefixedUnitNameSet}.
+ *
+ * @param map
+ * map that created this set
+ * @since 2019-04-13
+ * @since v0.2.0
+ */
+ public PrefixedUnitNameSet(final PrefixedUnitMap map) {
+ this.map = map;
+ }
+
+ @Override
+ public boolean add(final String e) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean addAll(final Collection<? extends String> c) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void clear() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean contains(final Object o) {
+ return this.map.containsKey(o);
+ }
+
+ @Override
+ public boolean containsAll(final Collection<?> c) {
+ for (final Object o : c)
+ if (!this.contains(o))
+ return false;
+ return true;
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return this.map.isEmpty();
+ }
+
+ @Override
+ public Iterator<String> iterator() {
+ return new PrefixedUnitNameIterator(this);
+ }
+
+ @Override
+ public boolean remove(final Object o) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean removeAll(final Collection<?> c) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean removeIf(final Predicate<? super String> filter) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean retainAll(final Collection<?> c) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public int size() {
+ if (this.map.units.isEmpty())
+ return 0;
+ else {
+ if (this.map.prefixes.isEmpty())
+ return this.map.units.size();
+ else
+ // infinite set
+ return Integer.MAX_VALUE;
+ }
+ }
+
+ @Override
+ public Object[] toArray() {
+ if (this.map.units.isEmpty() || this.map.prefixes.isEmpty())
+ return super.toArray();
+ else
+ // infinite set
+ throw new UnsupportedOperationException("Cannot make an infinite set into an array.");
+
+ }
+
+ @Override
+ public <T> T[] toArray(final T[] a) {
+ if (this.map.units.isEmpty() || this.map.prefixes.isEmpty())
+ return super.toArray(a);
+ else
+ // infinite set
+ throw new UnsupportedOperationException("Cannot make an infinite set into an array.");
+ }
+ }
+
+ /**
+ * The units stored in this collection, without prefixes.
+ *
+ * @since 2019-04-13
+ * @since v0.2.0
+ */
+ private final Map<String, Unit> units;
+
+ /**
+ * The available prefixes for use.
+ *
+ * @since 2019-04-13
+ * @since v0.2.0
+ */
+ private final Map<String, UnitPrefix> prefixes;
+
+ // caches
+ private Collection<Unit> values = null;
+ private Set<String> keySet = null;
+ private Set<Entry<String, Unit>> entrySet = null;
+
+ /**
+ * Creates the {@code PrefixedUnitMap}.
+ *
+ * @param units
+ * map mapping unit names to units
+ * @param prefixes
+ * map mapping prefix names to prefixes
+ * @since 2019-04-13
+ * @since v0.2.0
+ */
+ public PrefixedUnitMap(final Map<String, Unit> units, final Map<String, UnitPrefix> prefixes) {
+ // I am making unmodifiable maps to ensure I don't accidentally make changes.
+ this.units = Collections.unmodifiableMap(units);
+ this.prefixes = Collections.unmodifiableMap(prefixes);
+ }
+
+ @Override
+ public void clear() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Unit compute(final String key,
+ final BiFunction<? super String, ? super Unit, ? extends Unit> remappingFunction) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Unit computeIfAbsent(final String key, final Function<? super String, ? extends Unit> mappingFunction) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Unit computeIfPresent(final String key,
+ final BiFunction<? super String, ? super Unit, ? extends Unit> remappingFunction) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean containsKey(final Object key) {
+ // First, test if there is a unit with the key
+ if (this.units.containsKey(key))
+ return true;
+
+ // Next, try to cast it to String
+ if (!(key instanceof String))
+ throw new IllegalArgumentException("Attempted to test for a unit using a non-string name.");
+ final String unitName = (String) key;
+
+ // Then, look for the longest prefix that is attached to a valid unit
+ String longestPrefix = null;
+ int longestLength = 0;
+
+ for (final String prefixName : this.prefixes.keySet()) {
+ // a prefix name is valid if:
+ // - it is prefixed (i.e. the unit name starts with it)
+ // - it is longer than the existing largest prefix (since I am looking for the longest valid prefix)
+ // - the part after the prefix is a valid unit name
+ // - the unit described that name is a linear unit (since only linear units can have prefixes)
+ if (unitName.startsWith(prefixName) && prefixName.length() > longestLength) {
+ final String rest = unitName.substring(prefixName.length());
+ if (this.containsKey(rest) && this.get(rest) instanceof LinearUnit) {
+ longestPrefix = prefixName;
+ longestLength = prefixName.length();
+ }
+ }
+ }
+
+ return longestPrefix != null;
+ }
+
+ @Override
+ public boolean containsValue(final Object value) {
+ return this.units.containsValue(value);
+ }
+
+ @Override
+ public Set<Entry<String, Unit>> entrySet() {
+ if (this.entrySet == null) {
+ this.entrySet = new PrefixedUnitEntrySet(this);
+ }
+ return this.entrySet;
+ }
+
+ @Override
+ public Unit get(final Object key) {
+ // First, test if there is a unit with the key
+ if (this.units.containsKey(key))
+ return this.units.get(key);
+
+ // Next, try to cast it to String
+ if (!(key instanceof String))
+ throw new IllegalArgumentException("Attempted to obtain a unit using a non-string name.");
+ final String unitName = (String) key;
+
+ // Then, look for the longest prefix that is attached to a valid unit
+ String longestPrefix = null;
+ int longestLength = 0;
+
+ for (final String prefixName : this.prefixes.keySet()) {
+ // a prefix name is valid if:
+ // - it is prefixed (i.e. the unit name starts with it)
+ // - it is longer than the existing largest prefix (since I am looking for the longest valid prefix)
+ // - the part after the prefix is a valid unit name
+ // - the unit described that name is a linear unit (since only linear units can have prefixes)
+ if (unitName.startsWith(prefixName) && prefixName.length() > longestLength) {
+ final String rest = unitName.substring(prefixName.length());
+ if (this.containsKey(rest) && this.get(rest) instanceof LinearUnit) {
+ longestPrefix = prefixName;
+ longestLength = prefixName.length();
+ }
+ }
+ }
+
+ // if none found, returns null
+ if (longestPrefix == null)
+ return null;
+ else {
+ // get necessary data
+ final String rest = unitName.substring(longestLength);
+ // this cast will not fail because I verified that it would work before selecting this prefix
+ final LinearUnit unit = (LinearUnit) this.get(rest);
+ final UnitPrefix prefix = this.prefixes.get(longestPrefix);
+
+ return unit.withPrefix(prefix);
+ }
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return this.units.isEmpty();
+ }
+
+ @Override
+ public Set<String> keySet() {
+ if (this.keySet == null) {
+ this.keySet = new PrefixedUnitNameSet(this);
+ }
+ return this.keySet;
+ }
+
+ @Override
+ public Unit merge(final String key, final Unit value,
+ final BiFunction<? super Unit, ? super Unit, ? extends Unit> remappingFunction) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Unit put(final String key, final Unit value) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void putAll(final Map<? extends String, ? extends Unit> m) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Unit putIfAbsent(final String key, final Unit value) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Unit remove(final Object key) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean remove(final Object key, final Object value) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Unit replace(final String key, final Unit value) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean replace(final String key, final Unit oldValue, final Unit newValue) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void replaceAll(final BiFunction<? super String, ? super Unit, ? extends Unit> function) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public int size() {
+ if (this.units.isEmpty())
+ return 0;
+ else {
+ if (this.prefixes.isEmpty())
+ return this.units.size();
+ else
+ // infinite set
+ return Integer.MAX_VALUE;
+ }
+ }
+
+ @Override
+ public Collection<Unit> values() {
+ if (this.values == null) {
+ this.values = Collections.unmodifiableCollection(this.units.values());
+ }
+ return this.values;
+ }
+ }
+
+ /**
+ * The exponent operator
+ *
+ * @param base
+ * base of exponentiation
+ * @param exponentUnit
+ * exponent
+ * @return result
+ * @since 2019-04-10
+ * @since v0.2.0
+ */
+ private static final LinearUnit exponentiateUnits(final LinearUnit base, final LinearUnit exponentUnit) {
+ // exponent function - first check if o2 is a number,
+ if (exponentUnit.getBase().equals(SI.SI.getBaseUnit(UnitDimension.EMPTY))) {
+ // then check if it is an integer,
+ final double exponent = exponentUnit.getConversionFactor();
+ if (DecimalComparison.equals(exponent % 1, 0))
+ // then exponentiate
+ return base.toExponent((int) (exponent + 0.5));
+ else
+ // not an integer
+ throw new UnsupportedOperationException("Decimal exponents are currently not supported.");
+ } else
+ // not a number
+ throw new IllegalArgumentException("Exponents must be numbers.");
+ }
+
+ /**
+ * The units in this system, excluding prefixes.
+ *
+ * @since 2019-01-07
+ * @since v0.1.0
+ */
+ private final Map<String, Unit> prefixlessUnits;
+
+ /**
+ * The unit prefixes in this system.
+ *
+ * @since 2019-01-14
+ * @since v0.1.0
+ */
+ private final Map<String, UnitPrefix> prefixes;
+
+ /**
+ * The dimensions in this system.
+ *
+ * @since 2019-03-14
+ * @since v0.2.0
+ */
+ private final Map<String, UnitDimension> dimensions;
+
+ /**
+ * A map mapping strings to units (including prefixes)
+ *
+ * @since 2019-04-13
+ * @since v0.2.0
+ */
+ private final Map<String, Unit> units;
+
+ /**
+ * A parser that can parse unit expressions.
+ *
+ * @since 2019-03-22
+ * @since v0.2.0
+ */
+ private final ExpressionParser<LinearUnit> unitExpressionParser = new ExpressionParser.Builder<>(
+ this::getLinearUnit).addBinaryOperator("+", (o1, o2) -> o1.plus(o2), 0)
+ .addBinaryOperator("-", (o1, o2) -> o1.minus(o2), 0)
+ .addBinaryOperator("*", (o1, o2) -> o1.times(o2), 1).addSpaceFunction("*")
+ .addBinaryOperator("/", (o1, o2) -> o1.dividedBy(o2), 1)
+ .addBinaryOperator("^", UnitsDatabase::exponentiateUnits, 2).build();
+
+ /**
+ * A parser that can parse unit prefix expressions
+ *
+ * @since 2019-04-13
+ * @since v0.2.0
+ */
+ private final ExpressionParser<UnitPrefix> prefixExpressionParser = new ExpressionParser.Builder<>(this::getPrefix)
+ .addBinaryOperator("*", (o1, o2) -> o1.times(o2), 0).addSpaceFunction("*")
+ .addBinaryOperator("/", (o1, o2) -> o1.dividedBy(o2), 0)
+ .addBinaryOperator("^", (o1, o2) -> o1.toExponent(o2.getMultiplier()), 1).build();
+
+ /**
+ * A parser that can parse unit dimension expressions.
+ *
+ * @since 2019-04-13
+ * @since v0.2.0
+ */
+ private final ExpressionParser<UnitDimension> unitDimensionParser = new ExpressionParser.Builder<>(
+ this::getDimension).addBinaryOperator("*", (o1, o2) -> o1.times(o2), 0).addSpaceFunction("*")
+ .addBinaryOperator("/", (o1, o2) -> o1.dividedBy(o2), 0).build();
+
+ /**
+ * Creates the {@code UnitsDatabase}.
+ *
+ * @since 2019-01-10
+ * @since v0.1.0
+ */
+ public UnitsDatabase() {
+ this.prefixlessUnits = new HashMap<>();
+ this.prefixes = new HashMap<>();
+ 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
+ * @since 2019-03-14
+ * @since v0.2.0
+ */
+ public void addDimension(final String name, final UnitDimension dimension) {
+ this.dimensions.put(Objects.requireNonNull(name, "name must not be null."),
+ Objects.requireNonNull(dimension, "dimension must not be null."));
+ }
+
+ /**
+ * Adds to the list from a line in a unit dimension file.
+ *
+ * @param line
+ * line to look at
+ * @param lineCounter
+ * number of line, for error messages
+ * @since 2019-04-10
+ * @since v0.2.0
+ */
+ private void addDimensionFromLine(final String line, final long lineCounter) {
+ // ignore lines that start with a # sign - they're comments
+ if (line.isEmpty())
+ return;
+ if (line.contains("#")) {
+ this.addDimensionFromLine(line.substring(0, line.indexOf("#")), lineCounter);
+ return;
+ }
+
+ // divide line into name and expression
+ final String[] parts = line.split("\t");
+ if (parts.length < 2)
+ throw new IllegalArgumentException(String.format(
+ "Lines must consist of a dimension name and its definition, separated by tab(s) (line %d).",
+ lineCounter));
+ final String name = parts[0];
+ final String expression = parts[parts.length - 1];
+
+ if (name.endsWith(" ")) {
+ System.err.printf("Warning - line %d's dimension name ends in a space", lineCounter);
+ }
+
+ // if expression is "!", search for an existing dimension
+ // if no unit found, throw an error
+ if (expression.equals("!")) {
+ if (!this.containsDimensionName(name))
+ throw new IllegalArgumentException(
+ String.format("! used but no dimension found (line %d).", lineCounter));
+ } else {
+ // it's a unit, get the unit
+ final UnitDimension dimension;
+ try {
+ dimension = this.getDimensionFromExpression(expression);
+ } catch (final IllegalArgumentException e) {
+ System.err.printf("Parsing error on line %d:%n", lineCounter);
+ throw e;
+ }
+
+ this.addDimension(name, dimension);
+ }
+ }
+
+ /**
+ * Adds a unit prefix to the database.
+ *
+ * @param name
+ * prefix's name
+ * @param prefix
+ * prefix to add
+ * @throws NullPointerException
+ * if name or prefix is null
+ * @since 2019-01-14
+ * @since v0.1.0
+ */
+ public void addPrefix(final String name, final UnitPrefix prefix) {
+ this.prefixes.put(Objects.requireNonNull(name, "name must not be null."),
+ Objects.requireNonNull(prefix, "prefix must not be null."));
+ }
+
+ /**
+ * Adds a unit to the database.
+ *
+ * @param name
+ * unit's name
+ * @param unit
+ * unit to add
+ * @throws NullPointerException
+ * if unit is null
+ * @since 2019-01-10
+ * @since v0.1.0
+ */
+ public void addUnit(final String name, final Unit unit) {
+ this.prefixlessUnits.put(Objects.requireNonNull(name, "name must not be null."),
+ Objects.requireNonNull(unit, "unit must not be null."));
+ }
+
+ /**
+ * Adds to the list from a line in a unit file.
+ *
+ * @param line
+ * line to look at
+ * @param lineCounter
+ * number of line, for error messages
+ * @since 2019-04-10
+ * @since v0.2.0
+ */
+ private void addUnitOrPrefixFromLine(final String line, final long lineCounter) {
+ // ignore lines that start with a # sign - they're comments
+ if (line.isEmpty())
+ return;
+ if (line.contains("#")) {
+ this.addUnitOrPrefixFromLine(line.substring(0, line.indexOf("#")), lineCounter);
+ return;
+ }
+
+ // divide line into name and expression
+ final String[] parts = line.split("\t");
+ if (parts.length < 2)
+ throw new IllegalArgumentException(String.format(
+ "Lines must consist of a unit name and its definition, separated by tab(s) (line %d).",
+ lineCounter));
+ final String name = parts[0];
+ final String expression = parts[parts.length - 1];
+
+ if (name.endsWith(" ")) {
+ System.err.printf("Warning - line %d's unit name ends in a space", lineCounter);
+ }
+
+ // if expression is "!", search for an existing unit
+ // if no unit found, throw an error
+ if (expression.equals("!")) {
+ if (!this.containsUnitName(name))
+ throw new IllegalArgumentException(String.format("! used but no unit found (line %d).", lineCounter));
+ } else {
+ if (name.endsWith("-")) {
+ final UnitPrefix prefix;
+ try {
+ prefix = this.getPrefixFromExpression(expression);
+ } catch (final IllegalArgumentException e) {
+ System.err.printf("Parsing error on line %d:%n", lineCounter);
+ throw e;
+ }
+ this.addPrefix(name.substring(0, name.length() - 1), prefix);
+ } else {
+ // it's a unit, get the unit
+ final Unit unit;
+ try {
+ unit = this.getUnitFromExpression(expression);
+ } catch (final IllegalArgumentException e) {
+ System.err.printf("Parsing error on line %d:%n", lineCounter);
+ throw e;
+ }
+
+ this.addUnit(name, unit);
+ }
+ }
+ }
+
+ /**
+ * Tests if the database has a unit dimension with this name.
+ *
+ * @param name
+ * name to test
+ * @return if database contains name
+ * @since 2019-03-14
+ * @since v0.2.0
+ */
+ public boolean containsDimensionName(final String name) {
+ return this.dimensions.containsKey(name);
+ }
+
+ /**
+ * Tests if the database has a unit prefix with this name.
+ *
+ * @param name
+ * name to test
+ * @return if database contains name
+ * @since 2019-01-13
+ * @since v0.1.0
+ */
+ public boolean containsPrefixName(final String name) {
+ return this.prefixes.containsKey(name);
+ }
+
+ /**
+ * Tests if the database has a unit with this name, taking prefixes into consideration
+ *
+ * @param name
+ * name to test
+ * @return if database contains name
+ * @since 2019-01-13
+ * @since v0.1.0
+ */
+ public boolean containsUnitName(final String name) {
+ return this.units.containsKey(name);
+ }
+
+ /**
+ * @return a map mapping dimension names to dimensions
+ * @since 2019-04-13
+ * @since v0.2.0
+ */
+ public Map<String, UnitDimension> dimensionMap() {
+ return Collections.unmodifiableMap(this.dimensions);
+ }
+
+ /**
+ * Gets a unit dimension from the database using its name.
+ *
+ * <p>
+ * This method accepts exponents, like "L^3"
+ * </p>
+ *
+ * @param name
+ * dimension's name
+ * @return dimension
+ * @since 2019-03-14
+ * @since v0.2.0
+ */
+ public UnitDimension getDimension(final String name) {
+ Objects.requireNonNull(name, "name must not be null.");
+ if (name.contains("^")) {
+ final String[] baseAndExponent = name.split("\\^");
+
+ final UnitDimension base = this.getDimension(baseAndExponent[0]);
+
+ final int exponent;
+ try {
+ exponent = Integer.parseInt(baseAndExponent[baseAndExponent.length - 1]);
+ } catch (final NumberFormatException e2) {
+ throw new IllegalArgumentException("Exponent must be an integer.");
+ }
+
+ return base.toExponent(exponent);
+ }
+ return this.dimensions.get(name);
+ }
+
+ /**
+ * Uses the database's data to parse an expression into a unit dimension
+ * <p>
+ * The expression is a series of any of the following:
+ * <ul>
+ * <li>The name of a unit dimension, which multiplies or divides the result based on preceding operators</li>
+ * <li>The operators '*' and '/', which multiply and divide (note that just putting two unit dimensions next to each
+ * other is equivalent to multiplication)</li>
+ * <li>The operator '^' which exponentiates. Exponents must be integers.</li>
+ * </ul>
+ *
+ * @param expression
+ * expression to parse
+ * @throws IllegalArgumentException
+ * if the expression cannot be parsed
+ * @throws NullPointerException
+ * if expression is null
+ * @since 2019-04-13
+ * @since v0.2.0
+ */
+ public UnitDimension 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(" +", " ");
+
+ return this.unitDimensionParser.parseExpression(modifiedExpression);
+ }
+
+ /**
+ * Gets a unit. If it is linear, cast it to a LinearUnit and return it. Otherwise, throw an
+ * {@code IllegalArgumentException}.
+ *
+ * @param name
+ * unit's name
+ * @return unit
+ * @since 2019-03-22
+ * @since v0.2.0
+ */
+ private LinearUnit getLinearUnit(final String name) {
+ // see if I am using a function-unit like tempC(100)
+ if (name.contains("(") && name.contains(")")) {
+ // break it into function name and value
+ final List<String> parts = Arrays.asList(name.split("\\("));
+ if (parts.size() != 2)
+ throw new IllegalArgumentException("Format nonlinear units like: unit(value).");
+
+ // solve the function
+ final Unit unit = this.getUnit(parts.get(0));
+ final double value = Double.parseDouble(parts.get(1).substring(0, parts.get(1).length() - 1));
+ return unit.getBase().times(unit.convertToBase(value));
+ } else {
+ // get a linear unit
+ final Unit unit = this.getUnit(name);
+ if (unit instanceof LinearUnit)
+ return (LinearUnit) unit;
+ else
+ throw new IllegalArgumentException(String.format("%s is not a linear unit.", name));
+ }
+ }
+
+ /**
+ * Gets a unit prefix from the database from its name
+ *
+ * @param name
+ * prefix's name
+ * @return prefix
+ * @since 2019-01-10
+ * @since v0.1.0
+ */
+ public UnitPrefix getPrefix(final String name) {
+ try {
+ return new DefaultUnitPrefix(Double.parseDouble(name));
+ } catch (final NumberFormatException e) {
+ return this.prefixes.get(name);
+ }
+ }
+
+ /**
+ * Gets a unit prefix from a prefix expression
+ * <p>
+ * Currently, prefix expressions are much simpler than unit expressions: They are either a number or the name of
+ * another prefix
+ * </p>
+ *
+ * @param expression
+ * expression to input
+ * @return prefix
+ * @throws IllegalArgumentException
+ * if expression cannot be parsed
+ * @throws NullPointerException
+ * if any argument is null
+ * @since 2019-01-14
+ * @since v0.1.0
+ */
+ public UnitPrefix getPrefixFromExpression(final String expression) {
+ Objects.requireNonNull(expression, "expression must not be null.");
+
+ // attempt to get a unit as an alias first
+ if (this.containsUnitName(expression))
+ return this.getPrefix(expression);
+
+ // force operators to have spaces
+ String modifiedExpression = expression;
+ modifiedExpression = modifiedExpression.replaceAll("\\*", " \\* ");
+ modifiedExpression = modifiedExpression.replaceAll("/", " / ");
+ modifiedExpression = modifiedExpression.replaceAll("\\^", " \\^ ");
+
+ // fix broken spaces
+ modifiedExpression = modifiedExpression.replaceAll(" +", " ");
+
+ return this.prefixExpressionParser.parseExpression(modifiedExpression);
+ }
+
+ /**
+ * Gets a unit from the database from its name, looking for prefixes.
+ *
+ * @param name
+ * unit's name
+ * @return unit
+ * @since 2019-01-10
+ * @since v0.1.0
+ */
+ public Unit getUnit(final String name) {
+ try {
+ final double value = Double.parseDouble(name);
+ return SI.SI.getBaseUnit(UnitDimension.EMPTY).times(value);
+ } catch (final NumberFormatException e) {
+ return this.units.get(name);
+ }
+
+ }
+
+ /**
+ * Uses the database's unit data to parse an expression into a unit
+ * <p>
+ * The expression is a series of any of the following:
+ * <ul>
+ * <li>The name of a unit, which multiplies or divides the result based on preceding operators</li>
+ * <li>The operators '*' and '/', which multiply and divide (note that just putting two units or values next to each
+ * other is equivalent to multiplication)</li>
+ * <li>The operator '^' which exponentiates. Exponents must be integers.</li>
+ * <li>A number which is multiplied or divided</li>
+ * </ul>
+ * This method only works with linear units.
+ *
+ * @param expression
+ * expression to parse
+ * @throws IllegalArgumentException
+ * if the expression cannot be parsed
+ * @throws NullPointerException
+ * if expression is null
+ * @since 2019-01-07
+ * @since v0.1.0
+ */
+ public Unit getUnitFromExpression(final String expression) {
+ Objects.requireNonNull(expression, "expression must not be null.");
+
+ // attempt to get a unit as an alias first
+ if (this.containsUnitName(expression))
+ return this.getUnit(expression);
+
+ // force operators to have spaces
+ String modifiedExpression = expression;
+ modifiedExpression = modifiedExpression.replaceAll("\\+", " \\+ ");
+ modifiedExpression = modifiedExpression.replaceAll("-", " - ");
+ modifiedExpression = modifiedExpression.replaceAll("\\*", " \\* ");
+ modifiedExpression = modifiedExpression.replaceAll("/", " / ");
+ modifiedExpression = modifiedExpression.replaceAll("\\^", " \\^ ");
+
+ // fix broken spaces
+ modifiedExpression = modifiedExpression.replaceAll(" +", " ");
+
+ // the previous operation breaks negative numbers, fix them!
+ // (i.e. -2 becomes - 2)
+ for (int i = 2; i < modifiedExpression.length(); i++) {
+ if (modifiedExpression.charAt(i) == '-'
+ && Arrays.asList('+', '-', '*', '/', '^').contains(modifiedExpression.charAt(i - 2))) {
+ // found a broken negative number
+ modifiedExpression = modifiedExpression.substring(0, i + 1) + modifiedExpression.substring(i + 2);
+ }
+ }
+
+ return this.unitExpressionParser.parseExpression(modifiedExpression);
+ }
+
+ /**
+ * Adds all dimensions from a file, using data from the database to parse them.
+ * <p>
+ * Each line in the file should consist of a name and an expression (parsed by getDimensionFromExpression) separated
+ * by any number of tab characters.
+ * <p>
+ * <p>
+ * Allowed exceptions:
+ * <ul>
+ * <li>Anything after a '#' character is considered a comment and ignored.</li>
+ * <li>Blank lines are also ignored</li>
+ * <li>If an expression consists of a single exclamation point, instead of parsing it, this method will search the
+ * database for an existing unit. If no unit is found, an IllegalArgumentException is thrown. This is used to define
+ * initial units and ensure that the database contains them.</li>
+ * </ul>
+ *
+ * @param file
+ * file to read
+ * @throws IllegalArgumentException
+ * if the file cannot be parsed, found or read
+ * @throws NullPointerException
+ * if file is null
+ * @since 2019-01-13
+ * @since v0.1.0
+ */
+ public void loadDimensionFile(final 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
+ long lineCounter = 0;
+ while (reader.ready()) {
+ this.addDimensionFromLine(reader.readLine(), ++lineCounter);
+ }
+ } catch (final FileNotFoundException e) {
+ throw new IllegalArgumentException("Could not find file " + file, e);
+ } catch (final IOException e) {
+ throw new IllegalArgumentException("Could not read file " + file, e);
+ }
+ }
+
+ /**
+ * Adds all units from a file, using data from the database to parse them.
+ * <p>
+ * Each line in the file should consist of a name and an expression (parsed by getUnitFromExpression) separated by
+ * any number of tab characters.
+ * <p>
+ * <p>
+ * Allowed exceptions:
+ * <ul>
+ * <li>Anything after a '#' character is considered a comment and ignored.</li>
+ * <li>Blank lines are also ignored</li>
+ * <li>If an expression consists of a single exclamation point, instead of parsing it, this method will search the
+ * database for an existing unit. If no unit is found, an IllegalArgumentException is thrown. This is used to define
+ * initial units and ensure that the database contains them.</li>
+ * </ul>
+ *
+ * @param file
+ * file to read
+ * @throws IllegalArgumentException
+ * if the file cannot be parsed, found or read
+ * @throws NullPointerException
+ * if file is null
+ * @since 2019-01-13
+ * @since v0.1.0
+ */
+ public void loadUnitsFile(final 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
+ long lineCounter = 0;
+ while (reader.ready()) {
+ this.addUnitOrPrefixFromLine(reader.readLine(), ++lineCounter);
+ }
+ } catch (final FileNotFoundException e) {
+ throw new IllegalArgumentException("Could not find file " + file, e);
+ } catch (final IOException e) {
+ throw new IllegalArgumentException("Could not read file " + file, e);
+ }
+ }
+
+ /**
+ * @return a map mapping prefix names to prefixes
+ * @since 2019-04-13
+ * @since v0.2.0
+ */
+ public Map<String, UnitPrefix> prefixMap() {
+ return Collections.unmodifiableMap(this.prefixes);
+ }
+
+ /**
+ * @return a map mapping unit names to units, including prefixed names
+ * @since 2019-04-13
+ * @since v0.2.0
+ */
+ public Map<String, Unit> unitMap() {
+ return this.units; // PrefixedUnitMap is immutable so I don't need to make an unmodifiable map.
+ }
+
+ /**
+ * @return a map mapping unit names to units, ignoring prefixes
+ * @since 2019-04-13
+ * @since v0.2.0
+ */
+ public Map<String, Unit> unitMapPrefixless() {
+ return Collections.unmodifiableMap(this.prefixlessUnits);
+ }
+}
diff --git a/src/org/unitConverter/converterGUI/DelegateListModel.java b/src/org/unitConverter/converterGUI/DelegateListModel.java
new file mode 100755
index 0000000..b80f63d
--- /dev/null
+++ b/src/org/unitConverter/converterGUI/DelegateListModel.java
@@ -0,0 +1,242 @@
+/**
+ * Copyright (C) 2018 Adrien Hopkins
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package org.unitConverter.converterGUI;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+import java.util.ListIterator;
+
+import javax.swing.AbstractListModel;
+
+/**
+ * A list model that delegates to a list.
+ * <p>
+ * It is recommended to use the delegate methods in DelegateListModel instead of the delegated list's methods because
+ * the delegate methods handle updating the list.
+ * </p>
+ *
+ * @author Adrien Hopkins
+ * @since 2019-01-14
+ * @since v0.1.0
+ */
+final class DelegateListModel<E> extends AbstractListModel<E> implements List<E> {
+ /**
+ * @since 2019-01-14
+ * @since v0.1.0
+ */
+ private static final long serialVersionUID = 8985494428224810045L;
+
+ /**
+ * The list that this model is a delegate to.
+ *
+ * @since 2019-01-14
+ * @since v0.1.0
+ */
+ private final List<E> delegate;
+
+ /**
+ * Creates an empty {@code DelegateListModel}.
+ *
+ * @since 2019-04-13
+ */
+ public DelegateListModel() {
+ this(new ArrayList<>());
+ }
+
+ /**
+ * Creates the {@code DelegateListModel}.
+ *
+ * @param delegate
+ * list to delegate
+ * @since 2019-01-14
+ * @since v0.1.0
+ */
+ public DelegateListModel(final List<E> delegate) {
+ this.delegate = delegate;
+ }
+
+ @Override
+ public boolean add(final E element) {
+ final int index = this.delegate.size();
+ final boolean success = this.delegate.add(element);
+ this.fireIntervalAdded(this, index, index);
+ return success;
+ }
+
+ @Override
+ public void add(final int index, final E element) {
+ this.delegate.add(index, element);
+ this.fireIntervalAdded(this, index, index);
+ }
+
+ @Override
+ public boolean addAll(final Collection<? extends E> c) {
+ boolean changed = false;
+ for (final E e : c) {
+ if (this.add(e)) {
+ changed = true;
+ }
+ }
+ return changed;
+ }
+
+ @Override
+ public boolean addAll(final int index, final Collection<? extends E> c) {
+ for (final E e : c) {
+ this.add(index, e);
+ }
+ return !c.isEmpty(); // Since this is a list, it will always change if c has elements.
+ }
+
+ @Override
+ public void clear() {
+ final int oldSize = this.delegate.size();
+ this.delegate.clear();
+ if (oldSize >= 1) {
+ this.fireIntervalRemoved(this, 0, oldSize - 1);
+ }
+ }
+
+ @Override
+ public boolean contains(final Object elem) {
+ return this.delegate.contains(elem);
+ }
+
+ @Override
+ public boolean containsAll(final Collection<?> c) {
+ for (final Object e : c) {
+ if (!c.contains(e))
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public E get(final int index) {
+ return this.delegate.get(index);
+ }
+
+ @Override
+ public E getElementAt(final int index) {
+ return this.delegate.get(index);
+ }
+
+ @Override
+ public int getSize() {
+ return this.delegate.size();
+ }
+
+ @Override
+ public int indexOf(final Object elem) {
+ return this.delegate.indexOf(elem);
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return this.delegate.isEmpty();
+ }
+
+ @Override
+ public Iterator<E> iterator() {
+ return this.delegate.iterator();
+ }
+
+ @Override
+ public int lastIndexOf(final Object elem) {
+ return this.delegate.lastIndexOf(elem);
+ }
+
+ @Override
+ public ListIterator<E> listIterator() {
+ return this.delegate.listIterator();
+ }
+
+ @Override
+ public ListIterator<E> listIterator(final int index) {
+ return this.delegate.listIterator(index);
+ }
+
+ @Override
+ public E remove(final int index) {
+ final E returnValue = this.delegate.get(index);
+ this.delegate.remove(index);
+ this.fireIntervalRemoved(this, index, index);
+ return returnValue;
+ }
+
+ @Override
+ public boolean remove(final Object o) {
+ final int index = this.delegate.indexOf(o);
+ final boolean returnValue = this.delegate.remove(o);
+ this.fireIntervalRemoved(this, index, index);
+ return returnValue;
+ }
+
+ @Override
+ public boolean removeAll(final Collection<?> c) {
+ boolean changed = false;
+ for (final Object e : c) {
+ if (this.remove(e)) {
+ changed = true;
+ }
+ }
+ return changed;
+ }
+
+ @Override
+ public boolean retainAll(final Collection<?> c) {
+ final int oldSize = this.size();
+ final boolean returnValue = this.delegate.retainAll(c);
+ this.fireIntervalRemoved(this, this.size(), oldSize - 1);
+ return returnValue;
+ }
+
+ @Override
+ public E set(final int index, final E element) {
+ final E returnValue = this.delegate.get(index);
+ this.delegate.set(index, element);
+ this.fireContentsChanged(this, index, index);
+ return returnValue;
+ }
+
+ @Override
+ public int size() {
+ return this.delegate.size();
+ }
+
+ @Override
+ public List<E> subList(final int fromIndex, final int toIndex) {
+ return this.delegate.subList(fromIndex, toIndex);
+ }
+
+ @Override
+ public Object[] toArray() {
+ return this.delegate.toArray();
+ }
+
+ @Override
+ public <T> T[] toArray(final T[] a) {
+ return this.delegate.toArray(a);
+ }
+
+ @Override
+ public String toString() {
+ return this.delegate.toString();
+ }
+}
diff --git a/src/org/unitConverter/converterGUI/FilterComparator.java b/src/org/unitConverter/converterGUI/FilterComparator.java
new file mode 100755
index 0000000..7b17bfc
--- /dev/null
+++ b/src/org/unitConverter/converterGUI/FilterComparator.java
@@ -0,0 +1,129 @@
+/**
+ * Copyright (C) 2018 Adrien Hopkins
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package org.unitConverter.converterGUI;
+
+import java.util.Comparator;
+import java.util.Objects;
+
+/**
+ * A comparator that compares strings using a filter.
+ *
+ * @author Adrien Hopkins
+ * @since 2019-01-15
+ * @since v0.1.0
+ */
+final class FilterComparator implements Comparator<String> {
+ /**
+ * The filter that the comparator is filtered by.
+ *
+ * @since 2019-01-15
+ * @since v0.1.0
+ */
+ private final String filter;
+ /**
+ * The comparator to use if the arguments are otherwise equal.
+ *
+ * @since 2019-01-15
+ * @since v0.1.0
+ */
+ private final Comparator<String> comparator;
+ /**
+ * Whether or not the comparison is case-sensitive.
+ *
+ * @since 2019-04-14
+ * @since v0.2.0
+ */
+ private final boolean caseSensitive;
+
+ /**
+ * Creates the {@code FilterComparator}.
+ *
+ * @param filter
+ * @since 2019-01-15
+ * @since v0.1.0
+ */
+ public FilterComparator(final String filter) {
+ this(filter, null);
+ }
+
+ /**
+ * Creates the {@code FilterComparator}.
+ *
+ * @param filter
+ * string to filter by
+ * @param comparator
+ * comparator to fall back to if all else fails, null is compareTo.
+ * @throws NullPointerException
+ * if filter is null
+ * @since 2019-01-15
+ * @since v0.1.0
+ */
+ public FilterComparator(final String filter, final Comparator<String> comparator) {
+ this(filter, comparator, false);
+ }
+
+ /**
+ * Creates the {@code FilterComparator}.
+ *
+ * @param filter
+ * string to filter by
+ * @param comparator
+ * comparator to fall back to if all else fails, null is compareTo.
+ * @param caseSensitive
+ * whether or not the comparator is case-sensitive
+ * @throws NullPointerException
+ * if filter is null
+ * @since 2019-04-14
+ * @since v0.2.0
+ */
+ public FilterComparator(final String filter, final Comparator<String> comparator, final boolean caseSensitive) {
+ this.filter = Objects.requireNonNull(filter, "filter must not be null.");
+ this.comparator = comparator;
+ this.caseSensitive = caseSensitive;
+ }
+
+ @Override
+ public int compare(final String arg0, final String arg1) {
+ // if this is case insensitive, make them lowercase
+ final String str0, str1;
+ if (this.caseSensitive) {
+ str0 = arg0;
+ str1 = arg1;
+ } else {
+ str0 = arg0.toLowerCase();
+ str1 = arg1.toLowerCase();
+ }
+
+ // elements that start with the filter always go first
+ if (str0.startsWith(this.filter) && !str1.startsWith(this.filter))
+ return -1;
+ else if (!str0.startsWith(this.filter) && str1.startsWith(this.filter))
+ return 1;
+
+ // elements that contain the filter but don't start with them go next
+ if (str0.contains(this.filter) && !str1.contains(this.filter))
+ return -1;
+ else if (!str0.contains(this.filter) && !str1.contains(this.filter))
+ return 1;
+
+ // other elements go last
+ if (this.comparator == null)
+ return str0.compareTo(str1);
+ else
+ return this.comparator.compare(str0, str1);
+ }
+}
diff --git a/src/org/unitConverter/converterGUI/GridBagBuilder.java b/src/org/unitConverter/converterGUI/GridBagBuilder.java
new file mode 100755
index 0000000..f1229b2
--- /dev/null
+++ b/src/org/unitConverter/converterGUI/GridBagBuilder.java
@@ -0,0 +1,479 @@
+/**
+ * Copyright (C) 2018 Adrien Hopkins
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package org.unitConverter.converterGUI;
+
+import java.awt.GridBagConstraints;
+import java.awt.Insets;
+
+/**
+ * A builder for Java's {@link java.awt.GridBagConstraints} class.
+ *
+ * @author Adrien Hopkins
+ * @since 2018-11-30
+ * @since v0.1.0
+ */
+final class GridBagBuilder {
+ /**
+ * The built {@code GridBagConstraints}'s {@code gridx} property.
+ * <p>
+ * Specifies the cell containing the leading edge of the component's display area, where the first cell in a row has
+ * <code>gridx=0</code>. The leading edge of a component's display area is its left edge for a horizontal,
+ * left-to-right container and its right edge for a horizontal, right-to-left container. The value
+ * <code>RELATIVE</code> specifies that the component be placed immediately following the component that was added
+ * to the container just before this component was added.
+ * <p>
+ * The default value is <code>RELATIVE</code>. <code>gridx</code> should be a non-negative value.
+ *
+ * @serial
+ * @see #clone()
+ * @see java.awt.GridBagConstraints#gridy
+ * @see java.awt.ComponentOrientation
+ */
+ private final int gridx;
+
+ /**
+ * The built {@code GridBagConstraints}'s {@code gridy} property.
+ * <p>
+ * Specifies the cell at the top of the component's display area, where the topmost cell has <code>gridy=0</code>.
+ * The value <code>RELATIVE</code> specifies that the component be placed just below the component that was added to
+ * the container just before this component was added.
+ * <p>
+ * The default value is <code>RELATIVE</code>. <code>gridy</code> should be a non-negative value.
+ *
+ * @serial
+ * @see #clone()
+ * @see java.awt.GridBagConstraints#gridx
+ */
+ private final int gridy;
+
+ /**
+ * The built {@code GridBagConstraints}'s {@code gridwidth} property.
+ * <p>
+ * Specifies the number of cells in a row for the component's display area.
+ * <p>
+ * Use <code>REMAINDER</code> to specify that the component's display area will be from <code>gridx</code> to the
+ * last cell in the row. Use <code>RELATIVE</code> to specify that the component's display area will be from
+ * <code>gridx</code> to the next to the last one in its row.
+ * <p>
+ * <code>gridwidth</code> should be non-negative and the default value is 1.
+ *
+ * @serial
+ * @see #clone()
+ * @see java.awt.GridBagConstraints#gridheight
+ */
+ private final int gridwidth;
+
+ /**
+ * The built {@code GridBagConstraints}'s {@code gridheight} property.
+ * <p>
+ * Specifies the number of cells in a column for the component's display area.
+ * <p>
+ * Use <code>REMAINDER</code> to specify that the component's display area will be from <code>gridy</code> to the
+ * last cell in the column. Use <code>RELATIVE</code> to specify that the component's display area will be from
+ * <code>gridy</code> to the next to the last one in its column.
+ * <p>
+ * <code>gridheight</code> should be a non-negative value and the default value is 1.
+ *
+ * @serial
+ * @see #clone()
+ * @see java.awt.GridBagConstraints#gridwidth
+ */
+ private final int gridheight;
+
+ /**
+ * The built {@code GridBagConstraints}'s {@code weightx} property.
+ * <p>
+ * Specifies how to distribute extra horizontal space.
+ * <p>
+ * The grid bag layout manager calculates the weight of a column to be the maximum <code>weightx</code> of all the
+ * components in a column. If the resulting layout is smaller horizontally than the area it needs to fill, the extra
+ * space is distributed to each column in proportion to its weight. A column that has a weight of zero receives no
+ * extra space.
+ * <p>
+ * If all the weights are zero, all the extra space appears between the grids of the cell and the left and right
+ * edges.
+ * <p>
+ * The default value of this field is <code>0</code>. <code>weightx</code> should be a non-negative value.
+ *
+ * @serial
+ * @see #clone()
+ * @see java.awt.GridBagConstraints#weighty
+ */
+ private double weightx;
+
+ /**
+ * The built {@code GridBagConstraints}'s {@code weighty} property.
+ * <p>
+ * Specifies how to distribute extra vertical space.
+ * <p>
+ * The grid bag layout manager calculates the weight of a row to be the maximum <code>weighty</code> of all the
+ * components in a row. If the resulting layout is smaller vertically than the area it needs to fill, the extra
+ * space is distributed to each row in proportion to its weight. A row that has a weight of zero receives no extra
+ * space.
+ * <p>
+ * If all the weights are zero, all the extra space appears between the grids of the cell and the top and bottom
+ * edges.
+ * <p>
+ * The default value of this field is <code>0</code>. <code>weighty</code> should be a non-negative value.
+ *
+ * @serial
+ * @see #clone()
+ * @see java.awt.GridBagConstraints#weightx
+ */
+ private double weighty;
+
+ /**
+ * The built {@code GridBagConstraints}'s {@code anchor} property.
+ * <p>
+ * This field is used when the component is smaller than its display area. It determines where, within the display
+ * area, to place the component.
+ * <p>
+ * There are three kinds of possible values: orientation relative, baseline relative and absolute. Orientation
+ * relative values are interpreted relative to the container's component orientation property, baseline relative
+ * values are interpreted relative to the baseline and absolute values are not. The absolute values are:
+ * <code>CENTER</code>, <code>NORTH</code>, <code>NORTHEAST</code>, <code>EAST</code>, <code>SOUTHEAST</code>,
+ * <code>SOUTH</code>, <code>SOUTHWEST</code>, <code>WEST</code>, and <code>NORTHWEST</code>. The orientation
+ * relative values are: <code>PAGE_START</code>, <code>PAGE_END</code>, <code>LINE_START</code>,
+ * <code>LINE_END</code>, <code>FIRST_LINE_START</code>, <code>FIRST_LINE_END</code>, <code>LAST_LINE_START</code>
+ * and <code>LAST_LINE_END</code>. The baseline relative values are: <code>BASELINE</code>,
+ * <code>BASELINE_LEADING</code>, <code>BASELINE_TRAILING</code>, <code>ABOVE_BASELINE</code>,
+ * <code>ABOVE_BASELINE_LEADING</code>, <code>ABOVE_BASELINE_TRAILING</code>, <code>BELOW_BASELINE</code>,
+ * <code>BELOW_BASELINE_LEADING</code>, and <code>BELOW_BASELINE_TRAILING</code>. The default value is
+ * <code>CENTER</code>.
+ *
+ * @serial
+ * @see #clone()
+ * @see java.awt.ComponentOrientation
+ */
+ private int anchor;
+
+ /**
+ * The built {@code GridBagConstraints}'s {@code fill} property.
+ * <p>
+ * This field is used when the component's display area is larger than the component's requested size. It determines
+ * whether to resize the component, and if so, how.
+ * <p>
+ * The following values are valid for <code>fill</code>:
+ *
+ * <ul>
+ * <li><code>NONE</code>: Do not resize the component.
+ * <li><code>HORIZONTAL</code>: Make the component wide enough to fill its display area horizontally, but do not
+ * change its height.
+ * <li><code>VERTICAL</code>: Make the component tall enough to fill its display area vertically, but do not change
+ * its width.
+ * <li><code>BOTH</code>: Make the component fill its display area entirely.
+ * </ul>
+ * <p>
+ * The default value is <code>NONE</code>.
+ *
+ * @serial
+ * @see #clone()
+ */
+ private int fill;
+
+ /**
+ * The built {@code GridBagConstraints}'s {@code insets} property.
+ * <p>
+ * This field specifies the external padding of the component, the minimum amount of space between the component and
+ * the edges of its display area.
+ * <p>
+ * The default value is <code>new Insets(0, 0, 0, 0)</code>.
+ *
+ * @serial
+ * @see #clone()
+ */
+ private Insets insets;
+
+ /**
+ * The built {@code GridBagConstraints}'s {@code ipadx} property.
+ * <p>
+ * This field specifies the internal padding of the component, how much space to add to the minimum width of the
+ * component. The width of the component is at least its minimum width plus <code>ipadx</code> pixels.
+ * <p>
+ * The default value is <code>0</code>.
+ *
+ * @serial
+ * @see #clone()
+ * @see java.awt.GridBagConstraints#ipady
+ */
+ private int ipadx;
+
+ /**
+ * The built {@code GridBagConstraints}'s {@code ipady} property.
+ * <p>
+ * This field specifies the internal padding, that is, how much space to add to the minimum height of the component.
+ * The height of the component is at least its minimum height plus <code>ipady</code> pixels.
+ * <p>
+ * The default value is 0.
+ *
+ * @serial
+ * @see #clone()
+ * @see java.awt.GridBagConstraints#ipadx
+ */
+ private int ipady;
+
+ /**
+ * @param gridx
+ * x position
+ * @param gridy
+ * y position
+ * @since 2018-11-30
+ * @since v0.1.0
+ */
+ public GridBagBuilder(final int gridx, final int gridy) {
+ this(gridx, gridy, 1, 1);
+ }
+
+ /**
+ * @param gridx
+ * x position
+ * @param gridy
+ * y position
+ * @param gridwidth
+ * number of cells occupied horizontally
+ * @param gridheight
+ * number of cells occupied vertically
+ * @since 2018-11-30
+ * @since v0.1.0
+ */
+ public GridBagBuilder(final int gridx, final int gridy, final int gridwidth, final int gridheight) {
+ this(gridx, gridy, gridwidth, gridheight, 0.0, 0.0, GridBagConstraints.CENTER, GridBagConstraints.NONE,
+ new Insets(0, 0, 0, 0), 0, 0);
+ }
+
+ /**
+ * @param gridx
+ * x position
+ * @param gridy
+ * y position
+ * @param gridwidth
+ * number of cells occupied horizontally
+ * @param gridheight
+ * number of cells occupied vertically
+ * @param weightx
+ * @param weighty
+ * @param anchor
+ * @param fill
+ * @param insets
+ * @param ipadx
+ * @param ipady
+ * @since 2018-11-30
+ * @since v0.1.0
+ */
+ private GridBagBuilder(final int gridx, final int gridy, final int gridwidth, final int gridheight,
+ final double weightx, final double weighty, final int anchor, final int fill, final Insets insets,
+ final int ipadx, final int ipady) {
+ super();
+ this.gridx = gridx;
+ this.gridy = gridy;
+ this.gridwidth = gridwidth;
+ this.gridheight = gridheight;
+ this.weightx = weightx;
+ this.weighty = weighty;
+ this.anchor = anchor;
+ this.fill = fill;
+ this.insets = (Insets) insets.clone();
+ this.ipadx = ipadx;
+ this.ipady = ipady;
+ }
+
+ /**
+ * @return {@code GridBagConstraints} created by this builder
+ * @since 2018-11-30
+ * @since v0.1.0
+ */
+ public GridBagConstraints build() {
+ return new GridBagConstraints(this.gridx, this.gridy, this.gridwidth, this.gridheight, this.weightx,
+ this.weighty, this.anchor, this.fill, this.insets, this.ipadx, this.ipady);
+ }
+
+ /**
+ * @return anchor
+ * @since 2018-11-30
+ * @since v0.1.0
+ */
+ public int getAnchor() {
+ return this.anchor;
+ }
+
+ /**
+ * @return fill
+ * @since 2018-11-30
+ * @since v0.1.0
+ */
+ public int getFill() {
+ return this.fill;
+ }
+
+ /**
+ * @return gridheight
+ * @since 2018-11-30
+ * @since v0.1.0
+ */
+ public int getGridheight() {
+ return this.gridheight;
+ }
+
+ /**
+ * @return gridwidth
+ * @since 2018-11-30
+ * @since v0.1.0
+ */
+ public int getGridwidth() {
+ return this.gridwidth;
+ }
+
+ /**
+ * @return gridx
+ * @since 2018-11-30
+ * @since v0.1.0
+ */
+ public int getGridx() {
+ return this.gridx;
+ }
+
+ /**
+ * @return gridy
+ * @since 2018-11-30
+ * @since v0.1.0
+ */
+ public int getGridy() {
+ return this.gridy;
+ }
+
+ /**
+ * @return insets
+ * @since 2018-11-30
+ * @since v0.1.0
+ */
+ public Insets getInsets() {
+ return this.insets;
+ }
+
+ /**
+ * @return ipadx
+ * @since 2018-11-30
+ * @since v0.1.0
+ */
+ public int getIpadx() {
+ return this.ipadx;
+ }
+
+ /**
+ * @return ipady
+ * @since 2018-11-30
+ * @since v0.1.0
+ */
+ public int getIpady() {
+ return this.ipady;
+ }
+
+ /**
+ * @return weightx
+ * @since 2018-11-30
+ * @since v0.1.0
+ */
+ public double getWeightx() {
+ return this.weightx;
+ }
+
+ /**
+ * @return weighty
+ * @since 2018-11-30
+ * @since v0.1.0
+ */
+ public double getWeighty() {
+ return this.weighty;
+ }
+
+ /**
+ * @param anchor
+ * anchor to set
+ * @since 2018-11-30
+ * @since v0.1.0
+ */
+ public GridBagBuilder setAnchor(final int anchor) {
+ this.anchor = anchor;
+ return this;
+ }
+
+ /**
+ * @param fill
+ * fill to set
+ * @since 2018-11-30
+ * @since v0.1.0
+ */
+ public GridBagBuilder setFill(final int fill) {
+ this.fill = fill;
+ return this;
+ }
+
+ /**
+ * @param insets
+ * insets to set
+ * @since 2018-11-30
+ * @since v0.1.0
+ */
+ public GridBagBuilder setInsets(final Insets insets) {
+ this.insets = insets;
+ return this;
+ }
+
+ /**
+ * @param ipadx
+ * ipadx to set
+ * @since 2018-11-30
+ * @since v0.1.0
+ */
+ public GridBagBuilder setIpadx(final int ipadx) {
+ this.ipadx = ipadx;
+ return this;
+ }
+
+ /**
+ * @param ipady
+ * ipady to set
+ * @since 2018-11-30
+ * @since v0.1.0
+ */
+ public GridBagBuilder setIpady(final int ipady) {
+ this.ipady = ipady;
+ return this;
+ }
+
+ /**
+ * @param weightx
+ * weightx to set
+ * @since 2018-11-30
+ * @since v0.1.0
+ */
+ public GridBagBuilder setWeightx(final double weightx) {
+ this.weightx = weightx;
+ return this;
+ }
+
+ /**
+ * @param weighty
+ * weighty to set
+ * @since 2018-11-30
+ * @since v0.1.0
+ */
+ public GridBagBuilder setWeighty(final double weighty) {
+ this.weighty = weighty;
+ return this;
+ }
+}
diff --git a/src/org/unitConverter/converterGUI/MutablePredicate.java b/src/org/unitConverter/converterGUI/MutablePredicate.java
new file mode 100644
index 0000000..e15b3cd
--- /dev/null
+++ b/src/org/unitConverter/converterGUI/MutablePredicate.java
@@ -0,0 +1,70 @@
+/**
+ * Copyright (C) 2019 Adrien Hopkins
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package org.unitConverter.converterGUI;
+
+import java.util.function.Predicate;
+
+/**
+ * A container for a predicate, which can be changed later.
+ *
+ * @author Adrien Hopkins
+ * @since 2019-04-13
+ * @since v0.2.0
+ */
+final class MutablePredicate<T> implements Predicate<T> {
+ /**
+ * The predicate stored in this {@code MutablePredicate}
+ *
+ * @since 2019-04-13
+ * @since v0.2.0
+ */
+ private Predicate<T> predicate;
+
+ /**
+ * Creates the {@code MutablePredicate}.
+ *
+ * @since 2019-04-13
+ * @since v0.2.0
+ */
+ public MutablePredicate(final Predicate<T> predicate) {
+ this.predicate = predicate;
+ }
+
+ /**
+ * @return predicate
+ * @since 2019-04-13
+ * @since v0.2.0
+ */
+ public final Predicate<T> getPredicate() {
+ return this.predicate;
+ }
+
+ /**
+ * @param predicate
+ * new value of predicate
+ * @since 2019-04-13
+ * @since v0.2.0
+ */
+ public final void setPredicate(final Predicate<T> predicate) {
+ this.predicate = predicate;
+ }
+
+ @Override
+ public boolean test(final T t) {
+ return this.predicate.test(t);
+ }
+}
diff --git a/src/org/unitConverter/converterGUI/SearchBoxList.java b/src/org/unitConverter/converterGUI/SearchBoxList.java
new file mode 100644
index 0000000..1995466
--- /dev/null
+++ b/src/org/unitConverter/converterGUI/SearchBoxList.java
@@ -0,0 +1,297 @@
+/**
+ * Copyright (C) 2019 Adrien Hopkins
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package org.unitConverter.converterGUI;
+
+import java.awt.BorderLayout;
+import java.awt.Color;
+import java.awt.event.FocusEvent;
+import java.awt.event.FocusListener;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.function.Predicate;
+
+import javax.swing.JList;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.JTextField;
+
+/**
+ * @author Adrien Hopkins
+ * @since 2019-04-13
+ * @since v0.2.0
+ */
+final class SearchBoxList extends JPanel {
+
+ /**
+ * @since 2019-04-13
+ * @since v0.2.0
+ */
+ private static final long serialVersionUID = 6226930279415983433L;
+
+ /**
+ * The text to place in an empty search box.
+ *
+ * @since 2019-04-13
+ * @since v0.2.0
+ */
+ private static final String EMPTY_TEXT = "Search...";
+
+ /**
+ * The color to use for an empty foreground.
+ *
+ * @since 2019-04-13
+ * @since v0.2.0
+ */
+ private static final Color EMPTY_FOREGROUND = new Color(192, 192, 192);
+
+ // the components
+ private final Collection<String> itemsToFilter;
+ private final DelegateListModel<String> listModel;
+ private final JTextField searchBox;
+ private final JList<String> searchItems;
+
+ private boolean searchBoxEmpty = true;
+
+ // I need to do this because, for some reason, Swing is auto-focusing my search box without triggering a focus
+ // event.
+ private boolean searchBoxFocused = false;
+
+ private Predicate<String> customSearchFilter = o -> true;
+ private final Comparator<String> defaultOrdering;
+ private final boolean caseSensitive;
+
+ /**
+ * Creates the {@code SearchBoxList}.
+ *
+ * @param itemsToFilter
+ * items to put in the list
+ * @since 2019-04-14
+ */
+ public SearchBoxList(final Collection<String> itemsToFilter) {
+ this(itemsToFilter, null, false);
+ }
+
+ /**
+ * Creates the {@code SearchBoxList}.
+ *
+ * @param itemsToFilter
+ * items to put in the list
+ * @param defaultOrdering
+ * default ordering of items after filtration (null=Comparable)
+ * @param caseSensitive
+ * whether or not the filtration is case-sensitive
+ *
+ * @since 2019-04-13
+ * @since v0.2.0
+ */
+ public SearchBoxList(final Collection<String> itemsToFilter, final Comparator<String> defaultOrdering,
+ final boolean caseSensitive) {
+ super(new BorderLayout(), true);
+ this.itemsToFilter = itemsToFilter;
+ this.defaultOrdering = defaultOrdering;
+ this.caseSensitive = caseSensitive;
+
+ // create the components
+ this.listModel = new DelegateListModel<>(new ArrayList<>(itemsToFilter));
+ this.searchItems = new JList<>(this.listModel);
+
+ this.searchBox = new JTextField(EMPTY_TEXT);
+ this.searchBox.setForeground(EMPTY_FOREGROUND);
+
+ // add them to the panel
+ this.add(this.searchBox, BorderLayout.PAGE_START);
+ this.add(new JScrollPane(this.searchItems), BorderLayout.CENTER);
+
+ // set up the search box
+ this.searchBox.addFocusListener(new FocusListener() {
+ @Override
+ public void focusGained(final FocusEvent e) {
+ SearchBoxList.this.searchBoxFocusGained(e);
+ }
+
+ @Override
+ public void focusLost(final FocusEvent e) {
+ SearchBoxList.this.searchBoxFocusLost(e);
+ }
+ });
+
+ this.searchBox.addCaretListener(e -> this.searchBoxTextChanged());
+ this.searchBoxEmpty = true;
+ }
+
+ /**
+ * Adds an additional filter for searching.
+ *
+ * @param filter
+ * filter to add.
+ * @since 2019-04-13
+ * @since v0.2.0
+ */
+ public void addSearchFilter(final Predicate<String> filter) {
+ this.customSearchFilter = this.customSearchFilter.and(filter);
+ }
+
+ /**
+ * Resets the search filter.
+ *
+ * @since 2019-04-13
+ * @since v0.2.0
+ */
+ public void clearSearchFilters() {
+ this.customSearchFilter = o -> true;
+ }
+
+ /**
+ * @return this component's search box component
+ * @since 2019-04-14
+ * @since v0.2.0
+ */
+ public final JTextField getSearchBox() {
+ return this.searchBox;
+ }
+
+ /**
+ * @param searchText
+ * text to search for
+ * @return a filter that filters out that text, based on this list's case sensitive setting
+ * @since 2019-04-14
+ * @since v0.2.0
+ */
+ private Predicate<String> getSearchFilter(final String searchText) {
+ if (this.caseSensitive)
+ return string -> string.contains(searchText);
+ else
+ return string -> string.toLowerCase().contains(searchText.toLowerCase());
+ }
+
+ /**
+ * @return this component's list component
+ * @since 2019-04-14
+ * @since v0.2.0
+ */
+ public final JList<String> getSearchList() {
+ return this.searchItems;
+ }
+
+ /**
+ * @return index selected in item list
+ * @since 2019-04-14
+ * @since v0.2.0
+ */
+ public int getSelectedIndex() {
+ return this.searchItems.getSelectedIndex();
+ }
+
+ /**
+ * @return value selected in item list
+ * @since 2019-04-13
+ * @since v0.2.0
+ */
+ public String getSelectedValue() {
+ return this.searchItems.getSelectedValue();
+ }
+
+ /**
+ * Re-applies the filters.
+ *
+ * @since 2019-04-13
+ * @since v0.2.0
+ */
+ public void reapplyFilter() {
+ final String searchText = this.searchBoxEmpty ? "" : this.searchBox.getText();
+ final FilterComparator comparator = new FilterComparator(searchText, this.defaultOrdering, this.caseSensitive);
+ final Predicate<String> searchFilter = this.getSearchFilter(searchText);
+
+ this.listModel.clear();
+ this.itemsToFilter.forEach(string -> {
+ if (searchFilter.test(string)) {
+ this.listModel.add(string);
+ }
+ });
+
+ // applies the custom filters
+ this.listModel.removeIf(this.customSearchFilter.negate());
+
+ // sorts the remaining items
+ this.listModel.sort(comparator);
+ }
+
+ /**
+ * Runs whenever the search box gains focus.
+ *
+ * @param e
+ * focus event
+ * @since 2019-04-13
+ * @since v0.2.0
+ */
+ private void searchBoxFocusGained(final FocusEvent e) {
+ this.searchBoxFocused = true;
+ if (this.searchBoxEmpty) {
+ this.searchBox.setText("");
+ this.searchBox.setForeground(Color.BLACK);
+ }
+ }
+
+ /**
+ * Runs whenever the search box loses focus.
+ *
+ * @param e
+ * focus event
+ * @since 2019-04-13
+ * @since v0.2.0
+ */
+ private void searchBoxFocusLost(final FocusEvent e) {
+ this.searchBoxFocused = false;
+ if (this.searchBoxEmpty) {
+ this.searchBox.setText(EMPTY_TEXT);
+ this.searchBox.setForeground(EMPTY_FOREGROUND);
+ }
+ }
+
+ /**
+ * Runs whenever the text in the search box is changed.
+ * <p>
+ * Reapplies the search filter, and custom filters.
+ * </p>
+ *
+ * @since 2019-04-14
+ * @since v0.2.0
+ */
+ private void searchBoxTextChanged() {
+ if (this.searchBoxFocused) {
+ this.searchBoxEmpty = this.searchBox.getText().equals("");
+ }
+ final String searchText = this.searchBoxEmpty ? "" : this.searchBox.getText();
+ final FilterComparator comparator = new FilterComparator(searchText, this.defaultOrdering, this.caseSensitive);
+ final Predicate<String> searchFilter = this.getSearchFilter(searchText);
+
+ // initialize list with items that match the filter then sort
+ this.listModel.clear();
+ this.itemsToFilter.forEach(string -> {
+ if (searchFilter.test(string)) {
+ this.listModel.add(string);
+ }
+ });
+
+ // applies the custom filters
+ this.listModel.removeIf(this.customSearchFilter.negate());
+
+ // sorts the remaining items
+ this.listModel.sort(comparator);
+ }
+}
diff --git a/src/org/unitConverter/converterGUI/UnitConverterGUI.java b/src/org/unitConverter/converterGUI/UnitConverterGUI.java
new file mode 100755
index 0000000..e258c6f
--- /dev/null
+++ b/src/org/unitConverter/converterGUI/UnitConverterGUI.java
@@ -0,0 +1,827 @@
+/**
+ * Copyright (C) 2018 Adrien Hopkins
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package org.unitConverter.converterGUI;
+
+import java.awt.BorderLayout;
+import java.awt.GridLayout;
+import java.io.File;
+import java.math.BigDecimal;
+import java.math.MathContext;
+import java.text.DecimalFormat;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.function.Predicate;
+
+import javax.swing.BorderFactory;
+import javax.swing.JButton;
+import javax.swing.JComboBox;
+import javax.swing.JFormattedTextField;
+import javax.swing.JFrame;
+import javax.swing.JLabel;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.JSlider;
+import javax.swing.JTabbedPane;
+import javax.swing.JTextArea;
+import javax.swing.JTextField;
+
+import org.unitConverter.UnitsDatabase;
+import org.unitConverter.dimension.StandardDimensions;
+import org.unitConverter.dimension.UnitDimension;
+import org.unitConverter.unit.BaseUnit;
+import org.unitConverter.unit.NonlinearUnits;
+import org.unitConverter.unit.SI;
+import org.unitConverter.unit.Unit;
+import org.unitConverter.unit.UnitPrefix;
+
+/**
+ * @author Adrien Hopkins
+ * @since 2018-12-27
+ * @since v0.1.0
+ */
+final class UnitConverterGUI {
+ private static class Presenter {
+ /**
+ * Adds default units and dimensions to a database.
+ *
+ * @param database
+ * database to add to
+ * @since 2019-04-14
+ * @since v0.2.0
+ */
+ private static void addDefaults(final UnitsDatabase database) {
+ database.addUnit("metre", SI.METRE);
+ database.addUnit("kilogram", SI.KILOGRAM);
+ database.addUnit("gram", SI.KILOGRAM.dividedBy(1000));
+ database.addUnit("second", SI.SECOND);
+ database.addUnit("ampere", SI.AMPERE);
+ database.addUnit("kelvin", SI.KELVIN);
+ database.addUnit("mole", SI.MOLE);
+ database.addUnit("candela", SI.CANDELA);
+ database.addUnit("bit", SI.SI.getBaseUnit(StandardDimensions.INFORMATION));
+ database.addUnit("unit", SI.SI.getBaseUnit(UnitDimension.EMPTY));
+ // nonlinear units - must be loaded manually
+ database.addUnit("tempCelsius", NonlinearUnits.CELSIUS);
+ database.addUnit("tempFahrenheit", NonlinearUnits.FAHRENHEIT);
+
+ // load initial dimensions
+ database.addDimension("LENGTH", StandardDimensions.LENGTH);
+ database.addDimension("MASS", StandardDimensions.MASS);
+ database.addDimension("TIME", StandardDimensions.TIME);
+ database.addDimension("TEMPERATURE", StandardDimensions.TEMPERATURE);
+ }
+
+ /** The presenter's associated view. */
+ private final View view;
+
+ /** The units known by the program. */
+ private final UnitsDatabase database;
+
+ /** The names of all of the units */
+ private final List<String> unitNames;
+
+ /** The names of all of the prefixes */
+ private final List<String> prefixNames;
+
+ /** The names of all of the dimensions */
+ private final List<String> dimensionNames;
+
+ private final Comparator<String> prefixNameComparator;
+
+ private int significantFigures = 6;
+
+ /**
+ * Creates the presenter.
+ *
+ * @param view
+ * presenter's associated view
+ * @since 2018-12-27
+ * @since v0.1.0
+ */
+ Presenter(final View view) {
+ this.view = view;
+
+ // load initial units
+ this.database = new UnitsDatabase();
+ Presenter.addDefaults(this.database);
+
+ this.database.loadUnitsFile(new File("unitsfile.txt"));
+ this.database.loadDimensionFile(new File("dimensionfile.txt"));
+
+ // a comparator that can be used to compare prefix names
+ // any name that does not exist is less than a name that does.
+ // otherwise, they are compared by value
+ this.prefixNameComparator = (o1, o2) -> {
+ if (!Presenter.this.database.containsPrefixName(o1))
+ return -1;
+ else if (!Presenter.this.database.containsPrefixName(o2))
+ return 1;
+
+ final UnitPrefix p1 = Presenter.this.database.getPrefix(o1);
+ final UnitPrefix p2 = Presenter.this.database.getPrefix(o2);
+
+ if (p1.getMultiplier() < p2.getMultiplier())
+ return -1;
+ else if (p1.getMultiplier() > p2.getMultiplier())
+ return 1;
+
+ return o1.compareTo(o2);
+ };
+
+ this.unitNames = new ArrayList<>(this.database.unitMapPrefixless().keySet());
+ this.unitNames.sort(null); // sorts it using Comparable
+
+ this.prefixNames = new ArrayList<>(this.database.prefixMap().keySet());
+ this.prefixNames.sort(this.prefixNameComparator); // sorts it using my comparator
+
+ this.dimensionNames = new DelegateListModel<>(new ArrayList<>(this.database.dimensionMap().keySet()));
+ this.dimensionNames.sort(null); // sorts it using Comparable
+
+ // a Predicate that returns true iff the argument is a full base unit
+ final Predicate<Unit> isFullBase = unit -> unit instanceof BaseUnit && ((BaseUnit) unit).isFullBase();
+
+ // print out unit counts
+ System.out.printf("Successfully loaded %d units with %d unit names (%d base units).%n",
+ new HashSet<>(this.database.unitMapPrefixless().values()).size(),
+ this.database.unitMapPrefixless().size(),
+ new HashSet<>(this.database.unitMapPrefixless().values()).stream().filter(isFullBase).count());
+ }
+
+ /**
+ * Converts in the dimension-based converter
+ *
+ * @since 2019-04-13
+ * @since v0.2.0
+ */
+ public final void convertDimensionBased() {
+ final String fromSelection = this.view.getFromSelection();
+ if (fromSelection == null) {
+ this.view.showErrorDialog("Error", "No unit selected in From field");
+ return;
+ }
+ final String toSelection = this.view.getToSelection();
+ if (toSelection == null) {
+ this.view.showErrorDialog("Error", "No unit selected in To field");
+ return;
+ }
+
+ final Unit from = this.database.getUnit(fromSelection);
+ final Unit to = this.database.getUnit(toSelection);
+
+ final String input = this.view.getDimensionConverterInput();
+ if (input.equals("")) {
+ this.view.showErrorDialog("Error", "No value to convert entered.");
+ return;
+ }
+ final double beforeValue = Double.parseDouble(input);
+ final double value = to.convertFromBase(from.convertToBase(beforeValue));
+
+ final String output = this.getRoundedString(value);
+
+ this.view.setDimensionConverterOutputText(
+ String.format("%s %s = %s %s", input, fromSelection, output, toSelection));
+ }
+
+ /**
+ * Runs whenever the convert button is pressed.
+ *
+ * <p>
+ * Reads and parses a unit expression from the from and to boxes, then converts {@code from} to {@code to}. Any
+ * errors are shown in JOptionPanes.
+ * </p>
+ *
+ * @since 2019-01-26
+ * @since v0.1.0
+ */
+ public final void convertExpressions() {
+ final String fromUnitString = this.view.getFromText();
+ final String toUnitString = this.view.getToText();
+
+ if (fromUnitString.isEmpty()) {
+ this.view.showErrorDialog("Parse Error", "Please enter a unit expression in the From: box.");
+ return;
+ }
+ if (toUnitString.isEmpty()) {
+ this.view.showErrorDialog("Parse Error", "Please enter a unit expression in the To: box.");
+ return;
+ }
+
+ // try to parse from
+ final Unit from;
+ try {
+ from = this.database.getUnitFromExpression(fromUnitString);
+ } catch (final IllegalArgumentException e) {
+ this.view.showErrorDialog("Parse Error", "Could not recognize text in From entry: " + e.getMessage());
+ return;
+ }
+
+ final double value;
+ // try to parse to
+ final Unit to;
+ try {
+ if (this.database.containsUnitName(toUnitString)) {
+ // if it's a unit, convert to that
+ to = this.database.getUnit(toUnitString);
+ } else {
+ to = this.database.getUnitFromExpression(toUnitString);
+ }
+ } catch (final IllegalArgumentException e) {
+ this.view.showErrorDialog("Parse Error", "Could not recognize text in To entry: " + e.getMessage());
+ return;
+ }
+
+ // if I can't convert, leave
+ if (!from.canConvertTo(to)) {
+ this.view.showErrorDialog("Conversion Error",
+ String.format("Cannot convert between %s and %s", fromUnitString, toUnitString));
+ return;
+ }
+
+ value = to.convertFromBase(from.convertToBase(1));
+
+ // round value
+ final String output = this.getRoundedString(value);
+
+ this.view.setExpressionConverterOutputText(
+ String.format("%s = %s %s", fromUnitString, output, toUnitString));
+ }
+
+ /**
+ * @return a list of all of the unit dimensions
+ * @since 2019-04-13
+ * @since v0.2.0
+ */
+ public final List<String> dimensionNameList() {
+ return this.dimensionNames;
+ }
+
+ /**
+ * @return a comparator to compare prefix names
+ * @since 2019-04-14
+ * @since v0.2.0
+ */
+ public final Comparator<String> getPrefixNameComparator() {
+ 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 double value) {
+ // round value
+ final BigDecimal bigValue = new BigDecimal(value).round(new MathContext(this.significantFigures));
+ String output = bigValue.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;
+ }
+
+ /**
+ * @return a set of all prefix names in the database
+ * @since 2019-04-14
+ * @since v0.2.0
+ */
+ public final Set<String> prefixNameSet() {
+ return this.database.prefixMap().keySet();
+ }
+
+ /**
+ * Runs whenever a prefix is selected in the viewer.
+ * <p>
+ * Shows its information in the text box to the right.
+ * </p>
+ *
+ * @since 2019-01-15
+ * @since v0.1.0
+ */
+ public final void prefixSelected() {
+ final String prefixName = this.view.getPrefixViewerSelection();
+ if (prefixName == null)
+ return;
+ else {
+ final UnitPrefix prefix = this.database.getPrefix(prefixName);
+
+ this.view.setPrefixTextBoxText(String.format("%s%nMultiplier: %s", prefixName, prefix.getMultiplier()));
+ }
+ }
+
+ /**
+ * @param significantFigures
+ * new value of significantFigures
+ * @since 2019-01-15
+ * @since v0.1.0
+ */
+ public final void setSignificantFigures(final int significantFigures) {
+ this.significantFigures = significantFigures;
+ }
+
+ /**
+ * Returns true if and only if the unit represented by {@code unitName} has the dimension represented by
+ * {@code dimensionName}.
+ *
+ * @param unitName
+ * name of unit to test
+ * @param dimensionName
+ * name of dimension to test
+ * @return whether unit has dimenision
+ * @since 2019-04-13
+ * @since v0.2.0
+ */
+ public final boolean unitMatchesDimension(final String unitName, final String dimensionName) {
+ final Unit unit = this.database.getUnit(unitName);
+ final UnitDimension dimension = this.database.getDimension(dimensionName);
+ return unit.getDimension().equals(dimension);
+ }
+
+ /**
+ * Runs whenever a unit is selected in the viewer.
+ * <p>
+ * Shows its information in the text box to the right.
+ * </p>
+ *
+ * @since 2019-01-15
+ * @since v0.1.0
+ */
+ public final void unitNameSelected() {
+ final String unitName = this.view.getUnitViewerSelection();
+ if (unitName == null)
+ return;
+ else {
+ final Unit unit = this.database.getUnit(unitName);
+
+ this.view.setUnitTextBoxText(unit.toString());
+ }
+ }
+
+ /**
+ * @return a set of all of the unit names
+ * @since 2019-04-14
+ * @since v0.2.0
+ */
+ public final Set<String> unitNameSet() {
+ return this.database.unitMapPrefixless().keySet();
+ }
+ }
+
+ private static class View {
+ /** The view's frame. */
+ private final JFrame frame;
+ /** The view's associated presenter. */
+ private final Presenter presenter;
+
+ // DIMENSION-BASED CONVERTER
+ /** The panel for inputting values in the dimension-based converter */
+ private final JTextField valueInput;
+ /** The panel for "From" in the dimension-based converter */
+ private final SearchBoxList fromSearch;
+ /** The panel for "To" in the dimension-based converter */
+ private final SearchBoxList toSearch;
+ /** The output area in the dimension-based converter */
+ private final JTextArea dimensionBasedOutput;
+
+ // EXPRESSION-BASED CONVERTER
+ /** The "From" entry in the conversion panel */
+ private final JTextField fromEntry;
+ /** The "To" entry in the conversion panel */
+ private final JTextField toEntry;
+ /** The output area in the conversion panel */
+ private final JTextArea output;
+
+ // UNIT AND PREFIX VIEWERS
+ /** The searchable list of unit names in the unit viewer */
+ private final SearchBoxList unitNameList;
+ /** The searchable list of prefix names in the prefix viewer */
+ private final SearchBoxList prefixNameList;
+ /** The text box for unit data in the unit viewer */
+ private final JTextArea unitTextBox;
+ /** The text box for prefix data in the prefix viewer */
+ private final JTextArea prefixTextBox;
+
+ /**
+ * Creates the {@code View}.
+ *
+ * @since 2019-01-14
+ * @since v0.1.0
+ */
+ public View() {
+ this.presenter = new Presenter(this);
+ this.frame = new JFrame("Unit Converter");
+ this.frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
+
+ // create the components
+ this.unitNameList = new SearchBoxList(this.presenter.unitNameSet());
+ this.prefixNameList = new SearchBoxList(this.presenter.prefixNameSet(),
+ this.presenter.getPrefixNameComparator(), true);
+ this.unitTextBox = new JTextArea();
+ this.prefixTextBox = new JTextArea();
+ this.fromSearch = new SearchBoxList(this.presenter.unitNameSet());
+ this.toSearch = new SearchBoxList(this.presenter.unitNameSet());
+ this.valueInput = new JFormattedTextField(new DecimalFormat("###############0.################"));
+ this.dimensionBasedOutput = new JTextArea(2, 32);
+ this.fromEntry = new JTextField();
+ this.toEntry = new JTextField();
+ this.output = new JTextArea(2, 32);
+
+ // create more components
+ this.initComponents();
+
+ this.frame.pack();
+ }
+
+ /**
+ * @return value in dimension-based converter
+ * @since 2019-04-13
+ * @since v0.2.0
+ */
+ public String getDimensionConverterInput() {
+ return this.valueInput.getText();
+ }
+
+ /**
+ * @return selection in "From" selector in dimension-based converter
+ * @since 2019-04-13
+ * @since v0.2.0
+ */
+ public String getFromSelection() {
+ return this.fromSearch.getSelectedValue();
+ }
+
+ /**
+ * @return text in "From" box in converter panel
+ * @since 2019-01-15
+ * @since v0.1.0
+ */
+ public String getFromText() {
+ return this.fromEntry.getText();
+ }
+
+ /**
+ * @return index of selected prefix in prefix viewer
+ * @since 2019-01-15
+ * @since v0.1.0
+ */
+ public String getPrefixViewerSelection() {
+ return this.prefixNameList.getSelectedValue();
+ }
+
+ /**
+ * @return selection in "To" selector in dimension-based converter
+ * @since 2019-04-13
+ * @since v0.2.0
+ */
+ public String getToSelection() {
+ return this.toSearch.getSelectedValue();
+ }
+
+ /**
+ * @return text in "To" box in converter panel
+ * @since 2019-01-26
+ * @since v0.1.0
+ */
+ public String getToText() {
+ return this.toEntry.getText();
+ }
+
+ /**
+ * @return index of selected unit in unit viewer
+ * @since 2019-01-15
+ * @since v0.1.0
+ */
+ public String getUnitViewerSelection() {
+ return this.unitNameList.getSelectedValue();
+ }
+
+ /**
+ * Starts up the application.
+ *
+ * @since 2018-12-27
+ * @since v0.1.0
+ */
+ public final void init() {
+ this.frame.setVisible(true);
+ }
+
+ /**
+ * Initializes the view's components.
+ *
+ * @since 2018-12-27
+ * @since v0.1.0
+ */
+ private final void initComponents() {
+ final JPanel masterPanel = new JPanel();
+ this.frame.add(masterPanel);
+
+ masterPanel.setLayout(new BorderLayout());
+
+ { // pane with all of the tabs
+ final JTabbedPane masterPane = new JTabbedPane();
+ masterPanel.add(masterPane, BorderLayout.CENTER);
+
+ { // a panel for unit conversion using a selector
+ final JPanel convertUnitPanel = new JPanel();
+ masterPane.addTab("Convert Units", convertUnitPanel);
+
+ convertUnitPanel.setLayout(new BorderLayout());
+
+ { // panel for input part
+ final JPanel inputPanel = new JPanel();
+ convertUnitPanel.add(inputPanel, BorderLayout.CENTER);
+
+ inputPanel.setLayout(new GridLayout(1, 3));
+
+ final JComboBox<String> dimensionSelector = new JComboBox<>(
+ this.presenter.dimensionNameList().toArray(new String[0]));
+ dimensionSelector.setSelectedItem("LENGTH");
+
+ // handle dimension filter
+ final MutablePredicate<String> dimensionFilter = new MutablePredicate<>(s -> true);
+
+ // panel for From things
+ inputPanel.add(this.fromSearch);
+
+ this.fromSearch.addSearchFilter(dimensionFilter);
+
+ { // for dimension selector and arrow that represents conversion
+ final JPanel inBetweenPanel = new JPanel();
+ inputPanel.add(inBetweenPanel);
+
+ inBetweenPanel.setLayout(new BorderLayout());
+
+ { // dimension selector
+ inBetweenPanel.add(dimensionSelector, BorderLayout.PAGE_START);
+ }
+
+ { // the arrow in the middle
+ final JLabel arrowLabel = new JLabel("->");
+ inBetweenPanel.add(arrowLabel, BorderLayout.CENTER);
+ }
+ }
+
+ // panel for To things
+
+ inputPanel.add(this.toSearch);
+
+ this.toSearch.addSearchFilter(dimensionFilter);
+
+ // code for dimension filter
+ dimensionSelector.addItemListener(e -> {
+ dimensionFilter.setPredicate(string -> View.this.presenter.unitMatchesDimension(string,
+ (String) dimensionSelector.getSelectedItem()));
+ this.fromSearch.reapplyFilter();
+ this.toSearch.reapplyFilter();
+ });
+
+ // apply the item listener once because I have a default selection
+ dimensionFilter.setPredicate(string -> View.this.presenter.unitMatchesDimension(string,
+ (String) dimensionSelector.getSelectedItem()));
+ this.fromSearch.reapplyFilter();
+ this.toSearch.reapplyFilter();
+ }
+
+ { // panel for submit and output, and also value entry
+ final JPanel outputPanel = new JPanel();
+ convertUnitPanel.add(outputPanel, BorderLayout.PAGE_END);
+
+ outputPanel.setLayout(new GridLayout(3, 1));
+
+ { // unit input
+ final JPanel valueInputPanel = new JPanel();
+ outputPanel.add(valueInputPanel);
+
+ valueInputPanel.setLayout(new BorderLayout());
+
+ { // prompt
+ final JLabel valuePrompt = new JLabel("Value to convert: ");
+ valueInputPanel.add(valuePrompt, BorderLayout.LINE_START);
+ }
+
+ { // value to convert
+ valueInputPanel.add(this.valueInput, BorderLayout.CENTER);
+ }
+ }
+
+ { // button to convert
+ final JButton convertButton = new JButton("Convert");
+ outputPanel.add(convertButton);
+
+ convertButton.addActionListener(e -> this.presenter.convertDimensionBased());
+ }
+
+ { // output of conversion
+ outputPanel.add(this.dimensionBasedOutput);
+ this.dimensionBasedOutput.setEditable(false);
+ }
+ }
+ }
+
+ { // panel for unit conversion using expressions
+ final JPanel convertExpressionPanel = new JPanel();
+ masterPane.addTab("Convert Unit Expressions", convertExpressionPanel);
+
+ convertExpressionPanel.setLayout(new GridLayout(5, 1));
+
+ { // panel for units to convert from
+ final JPanel fromPanel = new JPanel();
+ convertExpressionPanel.add(fromPanel);
+
+ fromPanel.setBorder(BorderFactory.createTitledBorder("From"));
+ fromPanel.setLayout(new GridLayout(1, 1));
+
+ { // entry for units
+ fromPanel.add(this.fromEntry);
+ }
+ }
+
+ { // panel for units to convert to
+ final JPanel toPanel = new JPanel();
+ convertExpressionPanel.add(toPanel);
+
+ toPanel.setBorder(BorderFactory.createTitledBorder("To"));
+ toPanel.setLayout(new GridLayout(1, 1));
+
+ { // entry for units
+ toPanel.add(this.toEntry);
+ }
+ }
+
+ { // button to convert
+ final JButton convertButton = new JButton("Convert!");
+ convertExpressionPanel.add(convertButton);
+
+ convertButton.addActionListener(e -> this.presenter.convertExpressions());
+ }
+
+ { // output of conversion
+ final JPanel outputPanel = new JPanel();
+ convertExpressionPanel.add(outputPanel);
+
+ outputPanel.setBorder(BorderFactory.createTitledBorder("Output"));
+ outputPanel.setLayout(new GridLayout(1, 1));
+
+ { // output
+ outputPanel.add(this.output);
+ this.output.setEditable(false);
+ }
+ }
+
+ { // panel for specifying precision
+ final JPanel sigDigPanel = new JPanel();
+ convertExpressionPanel.add(sigDigPanel);
+
+ sigDigPanel.setBorder(BorderFactory.createTitledBorder("Significant Digits"));
+
+ { // slider
+ final JSlider sigDigSlider = new JSlider(0, 12);
+ sigDigPanel.add(sigDigSlider);
+
+ sigDigSlider.setMajorTickSpacing(4);
+ sigDigSlider.setMinorTickSpacing(1);
+ sigDigSlider.setSnapToTicks(true);
+ sigDigSlider.setPaintTicks(true);
+ sigDigSlider.setPaintLabels(true);
+
+ sigDigSlider.addChangeListener(
+ e -> this.presenter.setSignificantFigures(sigDigSlider.getValue()));
+ }
+ }
+ }
+
+ { // panel to look up units
+ final JPanel unitLookupPanel = new JPanel();
+ masterPane.addTab("Unit Viewer", unitLookupPanel);
+
+ unitLookupPanel.setLayout(new GridLayout());
+
+ { // search panel
+ unitLookupPanel.add(this.unitNameList);
+
+ this.unitNameList.getSearchList()
+ .addListSelectionListener(e -> this.presenter.unitNameSelected());
+ }
+
+ { // the text box for unit's toString
+ unitLookupPanel.add(this.unitTextBox);
+ this.unitTextBox.setEditable(false);
+ this.unitTextBox.setLineWrap(true);
+ }
+ }
+
+ { // panel to look up prefixes
+ final JPanel prefixLookupPanel = new JPanel();
+ masterPane.addTab("Prefix Viewer", prefixLookupPanel);
+
+ prefixLookupPanel.setLayout(new GridLayout(1, 2));
+
+ { // panel for listing and seaching
+ prefixLookupPanel.add(this.prefixNameList);
+
+ this.prefixNameList.getSearchList()
+ .addListSelectionListener(e -> this.presenter.prefixSelected());
+ }
+
+ { // the text box for prefix's toString
+ prefixLookupPanel.add(this.prefixTextBox);
+ this.prefixTextBox.setEditable(false);
+ this.prefixTextBox.setLineWrap(true);
+ }
+ }
+ }
+ }
+
+ /**
+ * Sets the text in the output of the dimension-based converter.
+ *
+ * @param text
+ * text to set
+ * @since 2019-04-13
+ * @since v0.2.0
+ */
+ public void setDimensionConverterOutputText(final String text) {
+ this.dimensionBasedOutput.setText(text);
+ }
+
+ /**
+ * Sets the text in the output of the conversion panel.
+ *
+ * @param text
+ * text to set
+ * @since 2019-01-15
+ * @since v0.1.0
+ */
+ public void setExpressionConverterOutputText(final String text) {
+ this.output.setText(text);
+ }
+
+ /**
+ * Sets the text of the prefix text box in the prefix viewer.
+ *
+ * @param text
+ * text to set
+ * @since 2019-01-15
+ * @since v0.1.0
+ */
+ public void setPrefixTextBoxText(final String text) {
+ this.prefixTextBox.setText(text);
+ }
+
+ /**
+ * Sets the text of the unit text box in the unit viewer.
+ *
+ * @param text
+ * text to set
+ * @since 2019-01-15
+ * @since v0.1.0
+ */
+ public void setUnitTextBoxText(final String text) {
+ this.unitTextBox.setText(text);
+ }
+
+ /**
+ * Shows an error dialog.
+ *
+ * @param title
+ * title of dialog
+ * @param message
+ * message in dialog
+ * @since 2019-01-14
+ * @since v0.1.0
+ */
+ public void showErrorDialog(final String title, final String message) {
+ JOptionPane.showMessageDialog(this.frame, message, title, JOptionPane.ERROR_MESSAGE);
+ }
+ }
+
+ public static void main(final String[] args) {
+ new View().init();
+ }
+}
diff --git a/src/org/unitConverter/converterGUI/package-info.java b/src/org/unitConverter/converterGUI/package-info.java
new file mode 100644
index 0000000..1555291
--- /dev/null
+++ b/src/org/unitConverter/converterGUI/package-info.java
@@ -0,0 +1,24 @@
+/**
+ * Copyright (C) 2019 Adrien Hopkins
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+/**
+ * All classes that work to convert units.
+ *
+ * @author Adrien Hopkins
+ * @since 2019-01-25
+ * @since v0.2.0
+ */
+package org.unitConverter.converterGUI; \ No newline at end of file
diff --git a/src/org/unitConverter/dimension/BaseDimension.java b/src/org/unitConverter/dimension/BaseDimension.java
new file mode 100755
index 0000000..5e3ddad
--- /dev/null
+++ b/src/org/unitConverter/dimension/BaseDimension.java
@@ -0,0 +1,40 @@
+/**
+ * Copyright (C) 2018 Adrien Hopkins
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package org.unitConverter.dimension;
+
+/**
+ * A base dimension that makes up {@code UnitDimension} objects.
+ *
+ * @author Adrien Hopkins
+ * @since 2018-12-22
+ * @since v0.1.0
+ */
+public interface BaseDimension {
+ /**
+ * @return the dimension's name
+ * @since 2018-12-22
+ * @since v0.1.0
+ */
+ String getName();
+
+ /**
+ * @return a short string (usually one character) that represents this base dimension
+ * @since 2018-12-22
+ * @since v0.1.0
+ */
+ String getSymbol();
+}
diff --git a/src/org/unitConverter/dimension/OtherBaseDimension.java b/src/org/unitConverter/dimension/OtherBaseDimension.java
new file mode 100755
index 0000000..8aea2b9
--- /dev/null
+++ b/src/org/unitConverter/dimension/OtherBaseDimension.java
@@ -0,0 +1,55 @@
+/**
+ * Copyright (C) 2018 Adrien Hopkins
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package org.unitConverter.dimension;
+
+import java.util.Objects;
+
+/**
+ * Non-SI base dimensions.
+ *
+ * @author Adrien Hopkins
+ * @since 2019-01-14
+ * @since v0.1.0
+ */
+public enum OtherBaseDimension implements BaseDimension {
+ INFORMATION("Info"), CURRENCY("$$");
+
+ /** The dimension's symbol */
+ private final String symbol;
+
+ /**
+ * Creates the {@code SIBaseDimension}.
+ *
+ * @param symbol
+ * dimension's symbol
+ * @since 2018-12-11
+ * @since v0.1.0
+ */
+ private OtherBaseDimension(final String symbol) {
+ this.symbol = Objects.requireNonNull(symbol, "symbol must not be null.");
+ }
+
+ @Override
+ public String getName() {
+ return this.toString();
+ }
+
+ @Override
+ public String getSymbol() {
+ return this.symbol;
+ }
+}
diff --git a/src/org/unitConverter/dimension/SIBaseDimension.java b/src/org/unitConverter/dimension/SIBaseDimension.java
new file mode 100755
index 0000000..c459963
--- /dev/null
+++ b/src/org/unitConverter/dimension/SIBaseDimension.java
@@ -0,0 +1,57 @@
+/**
+ * Copyright (C) 2018 Adrien Hopkins
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package org.unitConverter.dimension;
+
+import java.util.Objects;
+
+/**
+ * The seven base dimensions that make up the SI.
+ *
+ * @author Adrien Hopkins
+ * @since 2018-12-11
+ * @since v0.1.0
+ */
+public enum SIBaseDimension implements BaseDimension {
+ LENGTH("L"), MASS("M"), TIME("T"), ELECTRIC_CURRENT("I"), TEMPERATURE("\u0398"), // u0398 is the theta symbol
+ QUANTITY("N"), LUMINOUS_INTENSITY("J");
+
+ /** The dimension's symbol */
+ private final String symbol;
+
+ /**
+ * Creates the {@code SIBaseDimension}.
+ *
+ * @param symbol
+ * dimension's symbol
+ * @since 2018-12-11
+ * @since v0.1.0
+ */
+ private SIBaseDimension(final String symbol) {
+ this.symbol = Objects.requireNonNull(symbol, "symbol must not be null.");
+ }
+
+ @Override
+ public String getName() {
+ return this.toString();
+ }
+
+ @Override
+ public String getSymbol() {
+ return this.symbol;
+ }
+
+}
diff --git a/src/org/unitConverter/dimension/StandardDimensions.java b/src/org/unitConverter/dimension/StandardDimensions.java
new file mode 100755
index 0000000..4b1b814
--- /dev/null
+++ b/src/org/unitConverter/dimension/StandardDimensions.java
@@ -0,0 +1,80 @@
+/**
+ * Copyright (C) 2018 Adrien Hopkins
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package org.unitConverter.dimension;
+
+/**
+ * All of the dimensions that are used by the SI.
+ *
+ * @author Adrien Hopkins
+ * @since 2018-12-11
+ * @since v0.1.0
+ */
+public final class StandardDimensions {
+ // base dimensions
+ public static final UnitDimension EMPTY = UnitDimension.EMPTY;
+ public static final UnitDimension LENGTH = UnitDimension.getBase(SIBaseDimension.LENGTH);
+ public static final UnitDimension MASS = UnitDimension.getBase(SIBaseDimension.MASS);
+ public static final UnitDimension TIME = UnitDimension.getBase(SIBaseDimension.TIME);
+ public static final UnitDimension ELECTRIC_CURRENT = UnitDimension.getBase(SIBaseDimension.ELECTRIC_CURRENT);
+ public static final UnitDimension TEMPERATURE = UnitDimension.getBase(SIBaseDimension.TEMPERATURE);
+ public static final UnitDimension QUANTITY = UnitDimension.getBase(SIBaseDimension.QUANTITY);
+ public static final UnitDimension LUMINOUS_INTENSITY = UnitDimension.getBase(SIBaseDimension.LUMINOUS_INTENSITY);
+ public static final UnitDimension INFORMATION = UnitDimension.getBase(OtherBaseDimension.INFORMATION);
+ public static final UnitDimension CURRENCY = UnitDimension.getBase(OtherBaseDimension.CURRENCY);
+ // derived dimensions without named SI units
+ public static final UnitDimension AREA = LENGTH.times(LENGTH);
+
+ public static final UnitDimension VOLUME = AREA.times(LENGTH);
+ public static final UnitDimension VELOCITY = LENGTH.dividedBy(TIME);
+ public static final UnitDimension ACCELERATION = VELOCITY.dividedBy(TIME);
+ public static final UnitDimension WAVENUMBER = EMPTY.dividedBy(LENGTH);
+ public static final UnitDimension MASS_DENSITY = MASS.dividedBy(VOLUME);
+ public static final UnitDimension SURFACE_DENSITY = MASS.dividedBy(AREA);
+ public static final UnitDimension SPECIFIC_VOLUME = VOLUME.dividedBy(MASS);
+ public static final UnitDimension CURRENT_DENSITY = ELECTRIC_CURRENT.dividedBy(AREA);
+ public static final UnitDimension MAGNETIC_FIELD_STRENGTH = ELECTRIC_CURRENT.dividedBy(LENGTH);
+ public static final UnitDimension CONCENTRATION = QUANTITY.dividedBy(VOLUME);
+ public static final UnitDimension MASS_CONCENTRATION = CONCENTRATION.times(MASS);
+ public static final UnitDimension LUMINANCE = LUMINOUS_INTENSITY.dividedBy(AREA);
+ public static final UnitDimension REFRACTIVE_INDEX = VELOCITY.dividedBy(VELOCITY);
+ public static final UnitDimension REFLACTIVE_PERMEABILITY = EMPTY.times(EMPTY);
+ public static final UnitDimension ANGLE = LENGTH.dividedBy(LENGTH);
+ public static final UnitDimension SOLID_ANGLE = AREA.dividedBy(AREA);
+ // derived dimensions with named SI units
+ public static final UnitDimension FREQUENCY = EMPTY.dividedBy(TIME);
+
+ public static final UnitDimension FORCE = MASS.times(ACCELERATION);
+ public static final UnitDimension ENERGY = FORCE.times(LENGTH);
+ public static final UnitDimension POWER = ENERGY.dividedBy(TIME);
+ public static final UnitDimension ELECTRIC_CHARGE = ELECTRIC_CURRENT.times(TIME);
+ public static final UnitDimension VOLTAGE = ENERGY.dividedBy(ELECTRIC_CHARGE);
+ public static final UnitDimension CAPACITANCE = ELECTRIC_CHARGE.dividedBy(VOLTAGE);
+ public static final UnitDimension ELECTRIC_RESISTANCE = VOLTAGE.dividedBy(ELECTRIC_CURRENT);
+ public static final UnitDimension ELECTRIC_CONDUCTANCE = ELECTRIC_CURRENT.dividedBy(VOLTAGE);
+ public static final UnitDimension MAGNETIC_FLUX = VOLTAGE.times(TIME);
+ public static final UnitDimension MAGNETIC_FLUX_DENSITY = MAGNETIC_FLUX.dividedBy(AREA);
+ public static final UnitDimension INDUCTANCE = MAGNETIC_FLUX.dividedBy(ELECTRIC_CURRENT);
+ public static final UnitDimension LUMINOUS_FLUX = LUMINOUS_INTENSITY.times(SOLID_ANGLE);
+ public static final UnitDimension ILLUMINANCE = LUMINOUS_FLUX.dividedBy(AREA);
+ public static final UnitDimension SPECIFIC_ENERGY = ENERGY.dividedBy(MASS);
+ public static final UnitDimension CATALYTIC_ACTIVITY = QUANTITY.dividedBy(TIME);
+
+ // You may NOT get StandardDimensions instances!
+ private StandardDimensions() {
+ throw new AssertionError();
+ }
+}
diff --git a/src/org/unitConverter/dimension/UnitDimension.java b/src/org/unitConverter/dimension/UnitDimension.java
new file mode 100755
index 0000000..dbeaeff
--- /dev/null
+++ b/src/org/unitConverter/dimension/UnitDimension.java
@@ -0,0 +1,241 @@
+/**
+ * Copyright (C) 2018 Adrien Hopkins
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package org.unitConverter.dimension;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * An object that represents what a unit measures, like length, mass, area, energy, etc.
+ *
+ * @author Adrien Hopkins
+ * @since 2018-12-11
+ * @since v0.1.0
+ */
+public final class UnitDimension {
+ /**
+ * The unit dimension where every exponent is zero
+ *
+ * @since 2018-12-12
+ * @since v0.1.0
+ */
+ public static final UnitDimension EMPTY = new UnitDimension(new HashMap<>());
+
+ /**
+ * Gets an UnitDimension that has 1 of a certain dimension and nothing else
+ *
+ * @param dimension
+ * dimension to get
+ * @return unit dimension
+ * @since 2018-12-11
+ * @since v0.1.0
+ */
+ public static final UnitDimension getBase(final BaseDimension dimension) {
+ final Map<BaseDimension, Integer> map = new HashMap<>();
+ map.put(dimension, 1);
+ return new UnitDimension(map);
+ }
+
+ /**
+ * The base dimensions that make up this dimension.
+ *
+ * @since 2018-12-11
+ * @since v0.1.0
+ */
+ final Map<BaseDimension, Integer> exponents;
+
+ /**
+ * Creates the {@code UnitDimension}.
+ *
+ * @param exponents
+ * base dimensions that make up this dimension
+ * @since 2018-12-11
+ * @since v0.1.0
+ */
+ private UnitDimension(final Map<BaseDimension, Integer> exponents) {
+ this.exponents = new HashMap<>(exponents);
+ }
+
+ /**
+ * Divides this dimension by another
+ *
+ * @param other
+ * other dimension
+ * @return quotient of two dimensions
+ * @since 2018-12-11
+ * @since v0.1.0
+ */
+ public UnitDimension dividedBy(final UnitDimension other) {
+ final Map<BaseDimension, Integer> map = new HashMap<>(this.exponents);
+
+ for (final BaseDimension key : other.exponents.keySet()) {
+ if (map.containsKey(key)) {
+ // add the dimensions
+ map.put(key, map.get(key) - other.exponents.get(key));
+ } else {
+ map.put(key, -other.exponents.get(key));
+ }
+ }
+ return new UnitDimension(map);
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (!(obj instanceof UnitDimension))
+ return false;
+ final UnitDimension other = (UnitDimension) obj;
+
+ // anything with a value of 0 is equal to a nonexistent value
+ for (final BaseDimension b : this.getBaseSet()) {
+ if (this.exponents.get(b) != other.exponents.get(b))
+ if (!(this.exponents.get(b) == 0 && !other.exponents.containsKey(b)))
+ return false;
+ }
+ for (final BaseDimension b : other.getBaseSet()) {
+ if (this.exponents.get(b) != other.exponents.get(b))
+ if (!(other.exponents.get(b) == 0 && !this.exponents.containsKey(b)))
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * @return a set of all of the base dimensions with non-zero exponents that make up this dimension.
+ * @since 2018-12-12
+ * @since v0.1.0
+ */
+ public final Set<BaseDimension> getBaseSet() {
+ final Set<BaseDimension> dimensions = new HashSet<>();
+
+ // add all dimensions with a nonzero exponent - they shouldn't be there in the first place
+ for (final BaseDimension dimension : this.exponents.keySet()) {
+ if (!this.exponents.get(dimension).equals(0)) {
+ dimensions.add(dimension);
+ }
+ }
+
+ return dimensions;
+ }
+
+ /**
+ * Gets the exponent for a specific dimension.
+ *
+ * @param dimension
+ * dimension to check
+ * @return exponent for that dimension
+ * @since 2018-12-12
+ * @since v0.1.0
+ */
+ public int getExponent(final BaseDimension dimension) {
+ return this.exponents.getOrDefault(dimension, 0);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(this.exponents);
+ }
+
+ /**
+ * @return true if this dimension is a base, i.e. it has one exponent of one and no other nonzero exponents
+ * @since 2019-01-15
+ * @since v0.1.0
+ */
+ public boolean isBase() {
+ int oneCount = 0;
+ boolean twoOrMore = false; // has exponents of 2 or more
+ for (final BaseDimension b : this.getBaseSet()) {
+ if (this.exponents.get(b) == 1) {
+ oneCount++;
+ } else if (this.exponents.get(b) != 0) {
+ twoOrMore = true;
+ }
+ }
+ return (oneCount == 0 || oneCount == 1) && !twoOrMore;
+ }
+
+ /**
+ * Multiplies this dimension by another
+ *
+ * @param other
+ * other dimension
+ * @return product of two dimensions
+ * @since 2018-12-11
+ * @since v0.1.0
+ */
+ public UnitDimension times(final UnitDimension other) {
+ final Map<BaseDimension, Integer> map = new HashMap<>(this.exponents);
+
+ for (final BaseDimension key : other.exponents.keySet()) {
+ if (map.containsKey(key)) {
+ // add the dimensions
+ map.put(key, map.get(key) + other.exponents.get(key));
+ } else {
+ map.put(key, other.exponents.get(key));
+ }
+ }
+ return new UnitDimension(map);
+ }
+
+ /**
+ * Returns this dimension, but to an exponent
+ *
+ * @param exp
+ * exponent
+ * @return result of exponientation
+ * @since 2019-01-15
+ * @since v0.1.0
+ */
+ public UnitDimension toExponent(final int exp) {
+ final Map<BaseDimension, Integer> map = new HashMap<>(this.exponents);
+ for (final BaseDimension key : this.exponents.keySet()) {
+ map.put(key, this.getExponent(key) * exp);
+ }
+ return new UnitDimension(map);
+ }
+
+ @Override
+ public String toString() {
+ final List<String> positiveStringComponents = new ArrayList<>();
+ final List<String> negativeStringComponents = new ArrayList<>();
+
+ // for each base dimension that makes up this dimension, add it and its exponent
+ for (final BaseDimension dimension : this.getBaseSet()) {
+ final int exponent = this.exponents.get(dimension);
+ if (exponent > 0) {
+ positiveStringComponents.add(String.format("%s^%d", dimension.getSymbol(), exponent));
+ } else if (exponent < 0) {
+ negativeStringComponents.add(String.format("%s^%d", dimension.getSymbol(), -exponent));
+ }
+ }
+
+ final String positiveString = positiveStringComponents.isEmpty() ? "1"
+ : String.join(" ", positiveStringComponents);
+ final String negativeString = negativeStringComponents.isEmpty() ? ""
+ : " / " + String.join(" ", negativeStringComponents);
+
+ return positiveString + negativeString;
+ }
+}
diff --git a/src/org/unitConverter/dimension/package-info.java b/src/org/unitConverter/dimension/package-info.java
new file mode 100755
index 0000000..8cb26b1
--- /dev/null
+++ b/src/org/unitConverter/dimension/package-info.java
@@ -0,0 +1,24 @@
+/**
+ * Copyright (C) 2018 Adrien Hopkins
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+/**
+ * Everything to do with what a unit measures, or its dimension.
+ *
+ * @author Adrien Hopkins
+ * @since 2018-12-22
+ * @since v0.1.0
+ */
+package org.unitConverter.dimension; \ No newline at end of file
diff --git a/src/org/unitConverter/math/DecimalComparison.java b/src/org/unitConverter/math/DecimalComparison.java
new file mode 100644
index 0000000..7cdbe5b
--- /dev/null
+++ b/src/org/unitConverter/math/DecimalComparison.java
@@ -0,0 +1,114 @@
+/**
+ * Copyright (C) 2019 Adrien Hopkins
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package org.unitConverter.math;
+
+/**
+ * A class that contains methods to compare float and double values.
+ *
+ * @author Adrien Hopkins
+ * @since 2019-03-18
+ * @since v0.2.0
+ */
+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.
+ *
+ * @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.
+ *
+ * @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}.
+ *
+ * @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
+ */
+ 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.
+ *
+ * @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) {
+ return Math.abs(a - b) <= epsilon * Math.max(Math.abs(a), Math.abs(b));
+ }
+
+ /**
+ * Tests for equality of float values using {@link #FLOAT_EPSILON}.
+ *
+ * @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
+ */
+ 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.
+ *
+ * @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) {
+ return Math.abs(a - b) <= epsilon * Math.max(Math.abs(a), Math.abs(b));
+ }
+
+ // You may NOT get any DecimalComparison instances
+ private DecimalComparison() {
+ throw new AssertionError();
+ }
+
+}
diff --git a/src/org/unitConverter/math/ExpressionParser.java b/src/org/unitConverter/math/ExpressionParser.java
new file mode 100644
index 0000000..b2261ed
--- /dev/null
+++ b/src/org/unitConverter/math/ExpressionParser.java
@@ -0,0 +1,708 @@
+/**
+ * Copyright (C) 2019 Adrien Hopkins
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package org.unitConverter.math;
+
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Deque;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.function.BinaryOperator;
+import java.util.function.Function;
+import java.util.function.UnaryOperator;
+
+/**
+ * An object that can parse expressions with unary or binary operators.
+ *
+ * @author Adrien Hopkins
+ * @param <T>
+ * type of object that exists in parsed expressions
+ * @since 2019-03-14
+ * @since v0.2.0
+ */
+public final class ExpressionParser<T> {
+ /**
+ * A builder that can create {@code ExpressionParser<T>} instances.
+ *
+ * @author Adrien Hopkins
+ * @param <T>
+ * type of object that exists in parsed expressions
+ * @since 2019-03-17
+ * @since v0.2.0
+ */
+ public static final class Builder<T> {
+ /**
+ * A function that obtains a parseable object from a string. For example, an integer {@code ExpressionParser}
+ * would use {@code Integer::parseInt}.
+ *
+ * @since 2019-03-14
+ * @since v0.2.0
+ */
+ private final Function<String, T> objectObtainer;
+
+ /**
+ * The function of the space as an operator (like 3 x y)
+ *
+ * @since 2019-03-22
+ * @since v0.2.0
+ */
+ private String spaceFunction = null;
+
+ /**
+ * A map mapping operator strings to operator functions, for unary operators.
+ *
+ * @since 2019-03-14
+ * @since v0.2.0
+ */
+ private final Map<String, PriorityUnaryOperator<T>> unaryOperators;
+
+ /**
+ * A map mapping operator strings to operator functions, for binary operators.
+ *
+ * @since 2019-03-14
+ * @since v0.2.0
+ */
+ private final Map<String, PriorityBinaryOperator<T>> binaryOperators;
+
+ /**
+ * Creates the {@code Builder}.
+ *
+ * @param objectObtainer
+ * a function that can turn strings into objects of the type handled by the parser.
+ * @throws NullPointerException
+ * if {@code objectObtainer} is null
+ * @since 2019-03-17
+ * @since v0.2.0
+ */
+ public Builder(final Function<String, T> objectObtainer) {
+ this.objectObtainer = Objects.requireNonNull(objectObtainer, "objectObtainer must not be null.");
+ this.unaryOperators = new HashMap<>();
+ this.binaryOperators = new HashMap<>();
+ }
+
+ /**
+ * Adds a binary operator to the builder.
+ *
+ * @param text
+ * text used to reference the operator, like '+'
+ * @param operator
+ * operator to add
+ * @param priority
+ * operator's priority, which determines which operators are applied first
+ * @return this builder
+ * @throws NullPointerException
+ * if {@code text} or {@code operator} is null
+ * @since 2019-03-17
+ * @since v0.2.0
+ */
+ public Builder<T> addBinaryOperator(final String text, final BinaryOperator<T> operator, final int priority) {
+ Objects.requireNonNull(text, "text must not be null.");
+ Objects.requireNonNull(operator, "operator must not be null.");
+
+ // Unfortunately, I cannot use a lambda because the PriorityBinaryOperator requires arguments.
+ final PriorityBinaryOperator<T> priorityOperator = new PriorityBinaryOperator<T>(priority) {
+ @Override
+ public T apply(final T t, final T u) {
+ return operator.apply(t, u);
+ }
+
+ };
+ this.binaryOperators.put(text, priorityOperator);
+ return this;
+ }
+
+ /**
+ * Adds a function for spaces. You must use the text of an existing binary operator.
+ *
+ * @param operator
+ * text of operator to use
+ * @return this builder
+ * @since 2019-03-22
+ * @since v0.2.0
+ */
+ public Builder<T> addSpaceFunction(final String operator) {
+ Objects.requireNonNull(operator, "operator must not be null.");
+
+ if (!this.binaryOperators.containsKey(operator))
+ throw new IllegalArgumentException(String.format("Could not find binary operator '%s'", operator));
+
+ this.spaceFunction = operator;
+ return this;
+ }
+
+ /**
+ * Adds a unary operator to the builder.
+ *
+ * @param text
+ * text used to reference the operator, like '-'
+ * @param operator
+ * operator to add
+ * @param priority
+ * operator's priority, which determines which operators are applied first
+ * @return this builder
+ * @throws NullPointerException
+ * if {@code text} or {@code operator} is null
+ * @since 2019-03-17
+ * @since v0.2.0
+ */
+ public Builder<T> addUnaryOperator(final String text, final UnaryOperator<T> operator, final int priority) {
+ Objects.requireNonNull(text, "text must not be null.");
+ Objects.requireNonNull(operator, "operator must not be null.");
+
+ // Unfortunately, I cannot use a lambda because the PriorityUnaryOperator requires arguments.
+ final PriorityUnaryOperator<T> priorityOperator = new PriorityUnaryOperator<T>(priority) {
+ @Override
+ public T apply(final T t) {
+ return operator.apply(t);
+ }
+ };
+ this.unaryOperators.put(text, priorityOperator);
+ return this;
+ }
+
+ /**
+ * @return an {@code ExpressionParser<T>} instance with the properties given to this builder
+ * @since 2019-03-17
+ * @since v0.2.0
+ */
+ public ExpressionParser<T> build() {
+ return new ExpressionParser<>(this.objectObtainer, this.unaryOperators, this.binaryOperators,
+ this.spaceFunction);
+ }
+ }
+
+ /**
+ * A binary operator with a priority field that determines which operators apply first.
+ *
+ * @author Adrien Hopkins
+ * @param <T>
+ * type of operand and result
+ * @since 2019-03-17
+ * @since v0.2.0
+ */
+ private static abstract class PriorityBinaryOperator<T>
+ implements BinaryOperator<T>, Comparable<PriorityBinaryOperator<T>> {
+ /**
+ * The operator's priority. Higher-priority operators are applied before lower-priority operators
+ *
+ * @since 2019-03-17
+ * @since v0.2.0
+ */
+ private final int priority;
+
+ /**
+ * Creates the {@code PriorityBinaryOperator}.
+ *
+ * @param priority
+ * operator's priority
+ * @since 2019-03-17
+ * @since v0.2.0
+ */
+ public PriorityBinaryOperator(final int priority) {
+ this.priority = priority;
+ }
+
+ /**
+ * Compares this object to another by priority.
+ *
+ * <p>
+ * {@inheritDoc}
+ * </p>
+ *
+ * @since 2019-03-17
+ * @since v0.2.0
+ */
+ @Override
+ public int compareTo(final PriorityBinaryOperator<T> o) {
+ if (this.priority < o.priority)
+ return -1;
+ else if (this.priority > o.priority)
+ return 1;
+ else
+ return 0;
+ }
+
+ /**
+ * @return priority
+ * @since 2019-03-22
+ * @since v0.2.0
+ */
+ public final int getPriority() {
+ return this.priority;
+ }
+ }
+
+ /**
+ * A unary operator with a priority field that determines which operators apply first.
+ *
+ * @author Adrien Hopkins
+ * @param <T>
+ * type of operand and result
+ * @since 2019-03-17
+ * @since v0.2.0
+ */
+ private static abstract class PriorityUnaryOperator<T>
+ implements UnaryOperator<T>, Comparable<PriorityUnaryOperator<T>> {
+ /**
+ * The operator's priority. Higher-priority operators are applied before lower-priority operators
+ *
+ * @since 2019-03-17
+ * @since v0.2.0
+ */
+ private final int priority;
+
+ /**
+ * Creates the {@code PriorityUnaryOperator}.
+ *
+ * @param priority
+ * operator's priority
+ * @since 2019-03-17
+ * @since v0.2.0
+ */
+ public PriorityUnaryOperator(final int priority) {
+ this.priority = priority;
+ }
+
+ /**
+ * Compares this object to another by priority.
+ *
+ * <p>
+ * {@inheritDoc}
+ * </p>
+ *
+ * @since 2019-03-17
+ * @since v0.2.0
+ */
+ @Override
+ public int compareTo(final PriorityUnaryOperator<T> o) {
+ if (this.priority < o.priority)
+ return -1;
+ else if (this.priority > o.priority)
+ return 1;
+ else
+ return 0;
+ }
+
+ /**
+ * @return priority
+ * @since 2019-03-22
+ * @since v0.2.0
+ */
+ public final int getPriority() {
+ return this.priority;
+ }
+ }
+
+ /**
+ * The types of tokens that are available.
+ *
+ * @author Adrien Hopkins
+ * @since 2019-03-14
+ * @since v0.2.0
+ */
+ private static enum TokenType {
+ OBJECT, UNARY_OPERATOR, BINARY_OPERATOR;
+ }
+
+ /**
+ * The opening bracket.
+ *
+ * @since 2019-03-22
+ * @since v0.2.0
+ */
+ public static final char OPENING_BRACKET = '(';
+
+ /**
+ * The closing bracket.
+ *
+ * @since 2019-03-22
+ * @since v0.2.0
+ */
+ public static final char CLOSING_BRACKET = ')';
+
+ /**
+ * Finds the other bracket in a pair of brackets, given the position of one.
+ *
+ * @param string
+ * string that contains brackets
+ * @param bracketPosition
+ * position of first bracket
+ * @return position of matching bracket
+ * @throws NullPointerException
+ * if string is null
+ * @since 2019-03-22
+ * @since v0.2.0
+ */
+ private static int findBracketPair(final String string, final int bracketPosition) {
+ Objects.requireNonNull(string, "string must not be null.");
+
+ final char openingBracket = string.charAt(bracketPosition);
+
+ // figure out what closing bracket to look for
+ final char closingBracket;
+ switch (openingBracket) {
+ case '(':
+ closingBracket = ')';
+ break;
+ case '[':
+ closingBracket = ']';
+ break;
+ case '{':
+ closingBracket = '}';
+ break;
+ default:
+ throw new IllegalArgumentException(String.format("Invalid bracket '%s'", openingBracket));
+ }
+
+ // level of brackets. every opening bracket increments this; every closing bracket decrements it
+ int bracketLevel = 0;
+
+ // iterate over the string to find the closing bracket
+ for (int currentPosition = bracketPosition; currentPosition < string.length(); currentPosition++) {
+ final char currentCharacter = string.charAt(currentPosition);
+
+ if (currentCharacter == openingBracket) {
+ bracketLevel++;
+ } else if (currentCharacter == closingBracket) {
+ bracketLevel--;
+ if (bracketLevel == 0)
+ return currentPosition;
+ }
+ }
+
+ throw new IllegalArgumentException("No matching bracket found.");
+ }
+
+ /**
+ * A function that obtains a parseable object from a string. For example, an integer {@code ExpressionParser} would
+ * use {@code Integer::parseInt}.
+ *
+ * @since 2019-03-14
+ * @since v0.2.0
+ */
+ private final Function<String, T> objectObtainer;
+
+ /**
+ * A map mapping operator strings to operator functions, for unary operators.
+ *
+ * @since 2019-03-14
+ * @since v0.2.0
+ */
+ private final Map<String, PriorityUnaryOperator<T>> unaryOperators;
+
+ /**
+ * A map mapping operator strings to operator functions, for binary operators.
+ *
+ * @since 2019-03-14
+ * @since v0.2.0
+ */
+ private final Map<String, PriorityBinaryOperator<T>> binaryOperators;
+
+ /**
+ * The operator for space, or null if spaces have no function.
+ *
+ * @since 2019-03-22
+ * @since v0.2.0
+ */
+ private final String spaceOperator;
+
+ /**
+ * Creates the {@code ExpressionParser}.
+ *
+ * @param objectObtainer
+ * function to get objects from strings
+ * @param unaryOperators
+ * unary operators available to the parser
+ * @param binaryOperators
+ * binary operators available to the parser
+ * @param spaceOperator
+ * operator used by spaces
+ * @since 2019-03-14
+ * @since v0.2.0
+ */
+ private ExpressionParser(final Function<String, T> objectObtainer,
+ final Map<String, PriorityUnaryOperator<T>> unaryOperators,
+ final Map<String, PriorityBinaryOperator<T>> binaryOperators, final String spaceOperator) {
+ this.objectObtainer = objectObtainer;
+ this.unaryOperators = unaryOperators;
+ this.binaryOperators = binaryOperators;
+ this.spaceOperator = spaceOperator;
+ }
+
+ /**
+ * Converts a given mathematical expression to reverse Polish notation (operators after operands).
+ * <p>
+ * For example,<br>
+ * {@code 2 * (3 + 4)}<br>
+ * becomes<br>
+ * {@code 2 3 4 + *}.
+ *
+ * @param expression
+ * expression
+ * @return expression in RPN
+ * @since 2019-03-17
+ * @since v0.2.0
+ */
+ private String convertExpressionToReversePolish(final String expression) {
+ Objects.requireNonNull(expression, "expression must not be null.");
+
+ final List<String> components = new ArrayList<>();
+
+ // the part of the expression remaining to parse
+ String partialExpression = expression;
+
+ // find and deal with brackets
+ while (partialExpression.indexOf(OPENING_BRACKET) != -1) {
+ final int openingBracketPosition = partialExpression.indexOf(OPENING_BRACKET);
+ final int closingBracketPosition = findBracketPair(partialExpression, openingBracketPosition);
+
+ // check for function
+ if (openingBracketPosition > 0 && partialExpression.charAt(openingBracketPosition - 1) != ' ') {
+ // function like sin(2) or tempF(32)
+ // find the position of the last space
+ int spacePosition = openingBracketPosition;
+ while (spacePosition >= 0 && partialExpression.charAt(spacePosition) != ' ') {
+ spacePosition--;
+ }
+ // then split the function into pre-function and function, using the space position
+ components.addAll(Arrays.asList(partialExpression.substring(0, spacePosition + 1).split(" ")));
+ components.add(partialExpression.substring(spacePosition + 1, closingBracketPosition + 1));
+ partialExpression = partialExpression.substring(closingBracketPosition + 1);
+ } else {
+ // normal brackets like (1 + 2) * (3 / 5)
+ components.addAll(Arrays.asList(partialExpression.substring(0, openingBracketPosition).split(" ")));
+ components.add(this.convertExpressionToReversePolish(
+ partialExpression.substring(openingBracketPosition + 1, closingBracketPosition)));
+ partialExpression = partialExpression.substring(closingBracketPosition + 1);
+ }
+ }
+
+ // add everything else
+ components.addAll(Arrays.asList(partialExpression.split(" ")));
+
+ // remove empty entries
+ while (components.contains("")) {
+ components.remove("");
+ }
+
+ // deal with space multiplication (x y)
+ if (this.spaceOperator != null) {
+ for (int i = 0; i < components.size() - 1; i++) {
+ if (this.getTokenType(components.get(i)) == TokenType.OBJECT
+ && this.getTokenType(components.get(i + 1)) == TokenType.OBJECT) {
+ components.add(++i, this.spaceOperator);
+ }
+ }
+ }
+
+ // turn the expression into reverse Polish
+ while (true) {
+ final int highestPriorityOperatorPosition = this.findHighestPriorityOperatorPosition(components);
+ if (highestPriorityOperatorPosition == -1) {
+ break;
+ }
+
+ // swap components based on what kind of operator there is
+ // 1 + 2 becomes 2 1 +
+ // - 1 becomes 1 -
+ switch (this.getTokenType(components.get(highestPriorityOperatorPosition))) {
+ case UNARY_OPERATOR:
+ final String unaryOperator = components.remove(highestPriorityOperatorPosition);
+ final String operand = components.remove(highestPriorityOperatorPosition);
+ components.add(highestPriorityOperatorPosition, operand + " " + unaryOperator);
+ break;
+ case BINARY_OPERATOR:
+ final String binaryOperator = components.remove(highestPriorityOperatorPosition);
+ final String operand1 = components.remove(highestPriorityOperatorPosition - 1);
+ final String operand2 = components.remove(highestPriorityOperatorPosition - 1);
+ components.add(highestPriorityOperatorPosition - 1,
+ operand2 + " " + operand1 + " " + binaryOperator);
+ break;
+ default:
+ throw new AssertionError("Expected operator, found non-operator.");
+ }
+ }
+
+ // join all of the components together, then ensure there is only one space in a row
+ String expressionRPN = String.join(" ", components).replaceAll(" +", " ");
+
+ while (expressionRPN.charAt(0) == ' ') {
+ expressionRPN = expressionRPN.substring(1);
+ }
+ while (expressionRPN.charAt(expressionRPN.length() - 1) == ' ') {
+ expressionRPN = expressionRPN.substring(0, expressionRPN.length() - 1);
+ }
+ return expressionRPN;
+ }
+
+ /**
+ * Finds the position of the highest-priority operator in a list
+ *
+ * @param components
+ * components to test
+ * @param blacklist
+ * positions of operators that should be ignored
+ * @return position of highest priority, or -1 if the list contains no operators
+ * @throws NullPointerException
+ * if components is null
+ * @since 2019-03-22
+ * @since v0.2.0
+ */
+ private int findHighestPriorityOperatorPosition(final List<String> components) {
+ Objects.requireNonNull(components, "components must not be null.");
+ // find highest priority
+ int maxPriority = Integer.MIN_VALUE;
+ int maxPriorityPosition = -1;
+
+ // go over components one by one
+ // if it is an operator, test its priority to see if it's max
+ // if it is, update maxPriority and maxPriorityPosition
+ for (int i = 0; i < components.size(); i++) {
+
+ switch (this.getTokenType(components.get(i))) {
+ case UNARY_OPERATOR:
+ final PriorityUnaryOperator<T> unaryOperator = this.unaryOperators.get(components.get(i));
+ final int unaryPriority = unaryOperator.getPriority();
+
+ if (unaryPriority > maxPriority) {
+ maxPriority = unaryPriority;
+ maxPriorityPosition = i;
+ }
+ break;
+ case BINARY_OPERATOR:
+ final PriorityBinaryOperator<T> binaryOperator = this.binaryOperators.get(components.get(i));
+ final int binaryPriority = binaryOperator.getPriority();
+
+ if (binaryPriority > maxPriority) {
+ maxPriority = binaryPriority;
+ maxPriorityPosition = i;
+ }
+ break;
+ default:
+ break;
+ }
+ }
+
+ // max priority position found
+ return maxPriorityPosition;
+ }
+
+ /**
+ * Determines whether an inputted string is an object or an operator
+ *
+ * @param token
+ * string to input
+ * @return type of token it is
+ * @throws NullPointerException
+ * if {@code expression} is null
+ * @since 2019-03-14
+ * @since v0.2.0
+ */
+ private TokenType getTokenType(final String token) {
+ Objects.requireNonNull(token, "token must not be null.");
+
+ if (this.unaryOperators.containsKey(token))
+ return TokenType.UNARY_OPERATOR;
+ else if (this.binaryOperators.containsKey(token))
+ return TokenType.BINARY_OPERATOR;
+ else
+ return TokenType.OBJECT;
+ }
+
+ /**
+ * Parses an expression.
+ *
+ * @param expression
+ * expression to parse
+ * @return result
+ * @throws NullPointerException
+ * if {@code expression} is null
+ * @since 2019-03-14
+ * @since v0.2.0
+ */
+ public T parseExpression(final String expression) {
+ return this.parseReversePolishExpression(this.convertExpressionToReversePolish(expression));
+ }
+
+ /**
+ * Parses an expression expressed in reverse Polish notation.
+ *
+ * @param expression
+ * expression to parse
+ * @return result
+ * @throws NullPointerException
+ * if {@code expression} is null
+ * @since 2019-03-14
+ * @since v0.2.0
+ */
+ private T parseReversePolishExpression(final String expression) {
+ Objects.requireNonNull(expression, "expression must not be null.");
+
+ final Deque<T> stack = new ArrayDeque<>();
+
+ // iterate over every item in the expression, then
+ for (final String item : expression.split(" ")) {
+ // choose a path based on what kind of thing was just read
+ switch (this.getTokenType(item)) {
+
+ case BINARY_OPERATOR:
+ if (stack.size() < 2)
+ throw new IllegalStateException(String.format(
+ "Attempted to call binary operator %s with only %d arguments.", item, stack.size()));
+
+ // get two arguments and operator, then apply!
+ final T o1 = stack.pop();
+ final T o2 = stack.pop();
+ final BinaryOperator<T> binaryOperator = this.binaryOperators.get(item);
+
+ stack.push(binaryOperator.apply(o1, o2));
+ break;
+
+ case OBJECT:
+ // just add it to the stack
+ stack.push(this.objectObtainer.apply(item));
+ break;
+
+ case UNARY_OPERATOR:
+ if (stack.size() < 1)
+ throw new IllegalStateException(String.format(
+ "Attempted to call unary operator %s with only %d arguments.", item, stack.size()));
+
+ // get one argument and operator, then apply!
+ final T o = stack.pop();
+ final UnaryOperator<T> unaryOperator = this.unaryOperators.get(item);
+
+ stack.push(unaryOperator.apply(o));
+ break;
+ default:
+ throw new AssertionError(
+ String.format("Internal error: Invalid token type %s.", this.getTokenType(item)));
+
+ }
+ }
+
+ // return answer, or throw an exception if I can't
+ if (stack.size() > 1)
+ throw new IllegalStateException("Computation ended up with more than one answer.");
+ else if (stack.size() == 0)
+ throw new IllegalStateException("Computation ended up without an answer.");
+ return stack.pop();
+ }
+}
diff --git a/src/org/unitConverter/math/package-info.java b/src/org/unitConverter/math/package-info.java
new file mode 100644
index 0000000..65d6b23
--- /dev/null
+++ b/src/org/unitConverter/math/package-info.java
@@ -0,0 +1,23 @@
+/**
+ * Copyright (C) 2019 Adrien Hopkins
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+/**
+ * A module that is capable of parsing expressions of things, like mathematical expressions or unit expressions.
+ *
+ * @author Adrien Hopkins
+ * @since 2019-03-14
+ */
+package org.unitConverter.math; \ No newline at end of file
diff --git a/src/org/unitConverter/package-info.java b/src/org/unitConverter/package-info.java
new file mode 100644
index 0000000..23dd165
--- /dev/null
+++ b/src/org/unitConverter/package-info.java
@@ -0,0 +1,24 @@
+/**
+ * Copyright (C) 2019 Adrien Hopkins
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+/**
+ * A program that converts units.
+ *
+ * @author Adrien Hopkins
+ * @version v0.2.0
+ * @since 2019-01-25
+ */
+package org.unitConverter; \ No newline at end of file
diff --git a/src/org/unitConverter/unit/AbstractUnit.java b/src/org/unitConverter/unit/AbstractUnit.java
new file mode 100644
index 0000000..05a6c17
--- /dev/null
+++ b/src/org/unitConverter/unit/AbstractUnit.java
@@ -0,0 +1,117 @@
+/**
+ * Copyright (C) 2019 Adrien Hopkins
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package org.unitConverter.unit;
+
+import java.util.Objects;
+
+import org.unitConverter.dimension.UnitDimension;
+
+/**
+ * The default abstract implementation of the {@code Unit} interface.
+ *
+ * @author Adrien Hopkins
+ * @since 2018-12-22
+ * @since v0.1.0
+ */
+public abstract class AbstractUnit implements Unit {
+ /**
+ * The dimension, or what the unit measures.
+ *
+ * @since 2018-12-22
+ * @since v0.1.0
+ */
+ private final UnitDimension dimension;
+
+ /**
+ * The unit's base unit. Values converted by {@code convertFromBase} and {@code convertToBase} are expressed in this
+ * unit.
+ *
+ * @since 2018-12-22
+ * @since v0.1.0
+ */
+ private final BaseUnit base;
+
+ /**
+ * The system that this unit is a part of.
+ *
+ * @since 2018-12-23
+ * @since v0.1.0
+ */
+ private final UnitSystem system;
+
+ /**
+ * Creates the {@code AbstractUnit}.
+ *
+ * @param base
+ * unit's base
+ * @throws NullPointerException
+ * if name, symbol or base is null
+ * @since 2018-12-22
+ * @since v0.1.0
+ */
+ public AbstractUnit(final BaseUnit base) {
+ this.base = Objects.requireNonNull(base, "base must not be null.");
+ this.dimension = this.base.getDimension();
+ this.system = this.base.getSystem();
+ }
+
+ /**
+ * Creates the {@code AbstractUnit} using a unique dimension. This constructor is for making base units and should
+ * only be used by {@code BaseUnit}.
+ *
+ * @param dimension
+ * dimension measured by unit
+ * @param system
+ * system that unit is a part of
+ * @throws AssertionError
+ * if this constructor is not run by {@code BaseUnit} or a subclass
+ * @throws NullPointerException
+ * if name, symbol or dimension is null
+ * @since 2018-12-23
+ * @since v0.1.0
+ */
+ AbstractUnit(final UnitDimension dimension, final UnitSystem system) {
+ // try to set this as a base unit
+ if (this instanceof BaseUnit) {
+ this.base = (BaseUnit) this;
+ } else
+ throw new AssertionError();
+
+ this.dimension = Objects.requireNonNull(dimension, "dimension must not be null.");
+ this.system = Objects.requireNonNull(system, "system must not be null.");
+ }
+
+ @Override
+ public final BaseUnit getBase() {
+ return this.base;
+ }
+
+ @Override
+ public final UnitDimension getDimension() {
+ return this.dimension;
+ }
+
+ @Override
+ public final UnitSystem getSystem() {
+ return this.system;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("%s-derived unit of dimension %s", this.getSystem(), this.getDimension());
+ }
+}
diff --git a/src/org/unitConverter/unit/BaseUnit.java b/src/org/unitConverter/unit/BaseUnit.java
new file mode 100755
index 0000000..67309cf
--- /dev/null
+++ b/src/org/unitConverter/unit/BaseUnit.java
@@ -0,0 +1,168 @@
+/**
+ * Copyright (C) 2018 Adrien Hopkins
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package org.unitConverter.unit;
+
+import java.util.Objects;
+
+import org.unitConverter.dimension.StandardDimensions;
+import org.unitConverter.dimension.UnitDimension;
+
+/**
+ * A unit that is the base for its dimension. It does not have to be for a base dimension, so units like the Newton and
+ * Joule are still base units.
+ * <p>
+ * {@code BaseUnit} does not have any public constructors or static factories. There are two ways to obtain
+ * {@code BaseUnit} instances.
+ * <ol>
+ * <li>The class {@link SI} in this package has constants for all of the SI base units. You can use these constants and
+ * multiply or divide them to get other units. For example:
+ *
+ * <pre>
+ * BaseUnit JOULE = SI.KILOGRAM.times(SI.METRE.toExponent(2)).dividedBy(SI.SECOND.toExponent(2));
+ * </pre>
+ *
+ * </li>
+ * <li>You can also query a unit system for a base unit using a unit dimension. The previously mentioned {@link SI}
+ * class can do this for SI and SI-derived units (including imperial and USC), but if you want to use another system,
+ * this is the way to do it. {@link StandardDimensions} contains common unit dimensions that you can use for this. Here
+ * is an example:
+ *
+ * <pre>
+ * BaseUnit JOULE = SI.SI.getBaseUnit(StandardDimensions.ENERGY);
+ * </pre>
+ *
+ * </li>
+ * </ol>
+ *
+ * @author Adrien Hopkins
+ * @since 2018-12-23
+ * @since v0.1.0
+ */
+public final class BaseUnit extends LinearUnit {
+ /**
+ * Is this unit a full base (i.e. m, s, ... but not N, J, ...)
+ *
+ * @since 2019-01-15
+ * @since v0.1.0
+ */
+ private final boolean isFullBase;
+
+ /**
+ * Creates the {@code BaseUnit}.
+ *
+ * @param dimension
+ * dimension measured by unit
+ * @param system
+ * system that unit is a part of
+ * @param name
+ * name of unit
+ * @param symbol
+ * symbol of unit
+ * @since 2018-12-23
+ * @since v0.1.0
+ */
+ BaseUnit(final UnitDimension dimension, final UnitSystem system) {
+ super(dimension, system, 1);
+ this.isFullBase = dimension.isBase();
+ }
+
+ /**
+ * Returns the quotient of this unit and another.
+ * <p>
+ * Two units can be divided if they are part of the same unit system. If {@code divisor} does not meet this
+ * condition, an {@code IllegalArgumentException} should be thrown.
+ * </p>
+ *
+ * @param divisor
+ * unit to divide by
+ * @return quotient of two units
+ * @throws IllegalArgumentException
+ * if {@code divisor} is not compatible for division as described above
+ * @throws NullPointerException
+ * if {@code divisor} is null
+ * @since 2018-12-22
+ * @since v0.1.0
+ */
+ public BaseUnit dividedBy(final BaseUnit divisor) {
+ Objects.requireNonNull(divisor, "other must not be null.");
+
+ // check that these units can be multiplied
+ if (!this.getSystem().equals(divisor.getSystem()))
+ throw new IllegalArgumentException(
+ String.format("Incompatible units for division \"%s\" and \"%s\".", this, divisor));
+
+ return new BaseUnit(this.getDimension().dividedBy(divisor.getDimension()), this.getSystem());
+ }
+
+ /**
+ * @return true if the unit is a "full base" unit like the metre or second.
+ * @since 2019-04-10
+ * @since v0.2.0
+ */
+ public final boolean isFullBase() {
+ return this.isFullBase;
+ }
+
+ /**
+ * Returns the product of this unit and another.
+ * <p>
+ * Two units can be multiplied if they are part of the same unit system. If {@code multiplier} does not meet this
+ * condition, an {@code IllegalArgumentException} should be thrown.
+ * </p>
+ *
+ * @param multiplier
+ * unit to multiply by
+ * @return product of two units
+ * @throws IllegalArgumentException
+ * if {@code multiplier} is not compatible for multiplication as described above
+ * @throws NullPointerException
+ * if {@code multiplier} is null
+ * @since 2018-12-22
+ * @since v0.1.0
+ */
+ public BaseUnit times(final BaseUnit multiplier) {
+ Objects.requireNonNull(multiplier, "other must not be null");
+
+ // check that these units can be multiplied
+ if (!this.getSystem().equals(multiplier.getSystem()))
+ throw new IllegalArgumentException(
+ String.format("Incompatible units for multiplication \"%s\" and \"%s\".", this, multiplier));
+
+ // multiply the units
+ return new BaseUnit(this.getDimension().times(multiplier.getDimension()), this.getSystem());
+ }
+
+ /**
+ * Returns this unit, but to an exponent.
+ *
+ * @param exponent
+ * exponent
+ * @return result of exponentiation
+ * @since 2019-01-15
+ * @since v0.1.0
+ */
+ @Override
+ public BaseUnit toExponent(final int exponent) {
+ return this.getSystem().getBaseUnit(this.getDimension().toExponent(exponent));
+ }
+
+ @Override
+ public String toString() {
+ return String.format("%s base unit of%s dimension %s", this.getSystem(), this.isFullBase ? " base" : "",
+ this.getDimension());
+ }
+}
diff --git a/src/org/unitConverter/unit/DefaultUnitPrefix.java b/src/org/unitConverter/unit/DefaultUnitPrefix.java
new file mode 100755
index 0000000..4a9e487
--- /dev/null
+++ b/src/org/unitConverter/unit/DefaultUnitPrefix.java
@@ -0,0 +1,68 @@
+/**
+ * Copyright (C) 2018 Adrien Hopkins
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package org.unitConverter.unit;
+
+import java.util.Objects;
+
+/**
+ * The default implementation of {@code UnitPrefix}, which contains a multiplier and nothing else.
+ *
+ * @author Adrien Hopkins
+ * @since 2019-01-14
+ * @since v0.1.0
+ */
+public final class DefaultUnitPrefix implements UnitPrefix {
+ private final double multiplier;
+
+ /**
+ * Creates the {@code DefaultUnitPrefix}.
+ *
+ * @param multiplier
+ * @since 2019-01-14
+ * @since v0.2.0
+ */
+ public DefaultUnitPrefix(final double multiplier) {
+ this.multiplier = multiplier;
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (!(obj instanceof DefaultUnitPrefix))
+ return false;
+ final DefaultUnitPrefix other = (DefaultUnitPrefix) obj;
+ return Double.doubleToLongBits(this.multiplier) == Double.doubleToLongBits(other.multiplier);
+ }
+
+ @Override
+ public double getMultiplier() {
+ return this.multiplier;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(this.multiplier);
+ }
+
+ @Override
+ public String toString() {
+ return String.format("Unit prefix equal to %s", this.multiplier);
+ }
+}
diff --git a/src/org/unitConverter/unit/LinearUnit.java b/src/org/unitConverter/unit/LinearUnit.java
new file mode 100644
index 0000000..1b1ac97
--- /dev/null
+++ b/src/org/unitConverter/unit/LinearUnit.java
@@ -0,0 +1,294 @@
+/**
+ * Copyright (C) 2019 Adrien Hopkins
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package org.unitConverter.unit;
+
+import java.util.Objects;
+
+import org.unitConverter.dimension.UnitDimension;
+import org.unitConverter.math.DecimalComparison;
+
+/**
+ * A unit that is equal to a certain number multiplied by its base.
+ * <p>
+ * {@code LinearUnit} does not have any public constructors or static factories. In order to obtain a {@code LinearUnit}
+ * instance, multiply its base by the conversion factor. Example:
+ *
+ * <pre>
+ * LinearUnit foot = METRE.times(0.3048);
+ * </pre>
+ *
+ * (where {@code METRE} is a {@code BaseUnit} instance)
+ * </p>
+ *
+ * @author Adrien Hopkins
+ * @since 2018-12-22
+ * @since v0.1.0
+ */
+public class LinearUnit extends AbstractUnit {
+ /**
+ * The value of one of this unit in this unit's base unit
+ *
+ * @since 2018-12-22
+ * @since v0.1.0
+ */
+ private final double conversionFactor;
+
+ /**
+ *
+ * Creates the {@code LinearUnit}.
+ *
+ * @param base
+ * unit's base
+ * @param conversionFactor
+ * value of one of this unit in its base
+ * @since 2018-12-23
+ * @since v0.1.0
+ */
+ LinearUnit(final BaseUnit base, final double conversionFactor) {
+ super(base);
+ this.conversionFactor = conversionFactor;
+ }
+
+ /**
+ * Creates the {@code LinearUnit} as a base unit.
+ *
+ * @param dimension
+ * dimension measured by unit
+ * @param system
+ * system unit is part of
+ * @since 2019-01-25
+ * @since v0.1.0
+ */
+ LinearUnit(final UnitDimension dimension, final UnitSystem system, final double conversionFactor) {
+ super(dimension, system);
+ this.conversionFactor = conversionFactor;
+ }
+
+ @Override
+ public double convertFromBase(final double value) {
+ return value / this.getConversionFactor();
+ }
+
+ @Override
+ public double convertToBase(final double value) {
+ return value * this.getConversionFactor();
+ }
+
+ /**
+ * Divides this unit by a scalar.
+ *
+ * @param divisor
+ * scalar to divide by
+ * @return quotient
+ * @since 2018-12-23
+ * @since v0.1.0
+ */
+ public LinearUnit dividedBy(final double divisor) {
+ return new LinearUnit(this.getBase(), this.getConversionFactor() / divisor);
+ }
+
+ /**
+ * Returns the quotient of this unit and another.
+ * <p>
+ * Two units can be divided if they are part of the same unit system. If {@code divisor} does not meet this
+ * condition, an {@code IllegalArgumentException} should be thrown.
+ * </p>
+ *
+ * @param divisor
+ * unit to divide by
+ * @return quotient of two units
+ * @throws IllegalArgumentException
+ * if {@code divisor} is not compatible for division as described above
+ * @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");
+
+ // check that these units can be multiplied
+ if (!this.getSystem().equals(divisor.getSystem()))
+ throw new IllegalArgumentException(
+ String.format("Incompatible units for division \"%s\" and \"%s\".", this, divisor));
+
+ // divide the units
+ final BaseUnit base = this.getBase().dividedBy(divisor.getBase());
+ return new LinearUnit(base, this.getConversionFactor() / divisor.getConversionFactor());
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (!(obj instanceof LinearUnit))
+ return false;
+ final LinearUnit other = (LinearUnit) obj;
+ return Objects.equals(this.getSystem(), other.getSystem())
+ && Objects.equals(this.getDimension(), other.getDimension())
+ && DecimalComparison.equals(this.getConversionFactor(), other.getConversionFactor());
+ }
+
+ /**
+ * @return conversion factor between this unit and its base
+ * @since 2018-12-22
+ * @since v0.1.0
+ */
+ public final double getConversionFactor() {
+ return this.conversionFactor;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = result * prime + this.getSystem().hashCode();
+ result = result * prime + this.getDimension().hashCode();
+ result = result * prime + Double.hashCode(this.getConversionFactor());
+ return result;
+ }
+
+ /**
+ * Returns the difference of this unit and another.
+ * <p>
+ * Two units can be subtracted if they have the same base. If {@code subtrahend} does not meet this condition, an
+ * {@code IllegalArgumentException} will be thrown.
+ * </p>
+ *
+ * @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
+ * @since 2019-03-17
+ * @since v0.2.0
+ */
+ public LinearUnit minus(final LinearUnit subtrahendend) {
+ Objects.requireNonNull(subtrahendend, "addend must not be null.");
+
+ // reject subtrahends that cannot be added to this unit
+ if (!this.getBase().equals(subtrahendend.getBase()))
+ throw new IllegalArgumentException(
+ String.format("Incompatible units for subtraction \"%s\" and \"%s\".", this, subtrahendend));
+
+ // add the units
+ return new LinearUnit(this.getBase(), this.getConversionFactor() - subtrahendend.getConversionFactor());
+ }
+
+ /**
+ * Returns the sum of this unit and another.
+ * <p>
+ * Two units can be added if they have the same base. If {@code addend} does not meet this condition, an
+ * {@code IllegalArgumentException} will be thrown.
+ * </p>
+ *
+ * @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
+ * @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));
+
+ // add the units
+ return new LinearUnit(this.getBase(), this.getConversionFactor() + addend.getConversionFactor());
+ }
+
+ /**
+ * Multiplies this unit by a scalar.
+ *
+ * @param multiplier
+ * scalar to multiply by
+ * @return product
+ * @since 2018-12-23
+ * @since v0.1.0
+ */
+ public LinearUnit times(final double multiplier) {
+ return new LinearUnit(this.getBase(), this.getConversionFactor() * multiplier);
+ }
+
+ /**
+ * Returns the product of this unit and another.
+ * <p>
+ * Two units can be multiplied if they are part of the same unit system. If {@code multiplier} does not meet this
+ * condition, an {@code IllegalArgumentException} should be thrown.
+ * </p>
+ *
+ * @param multiplier
+ * unit to multiply by
+ * @return product of two units
+ * @throws IllegalArgumentException
+ * if {@code multiplier} is not compatible for multiplication as described above
+ * @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");
+
+ // check that these units can be multiplied
+ if (!this.getSystem().equals(multiplier.getSystem()))
+ throw new IllegalArgumentException(
+ String.format("Incompatible units for multiplication \"%s\" and \"%s\".", this, multiplier));
+
+ // multiply the units
+ final BaseUnit base = this.getBase().times(multiplier.getBase());
+ return new LinearUnit(base, this.getConversionFactor() * multiplier.getConversionFactor());
+ }
+
+ /**
+ * Returns this unit but to an exponent.
+ *
+ * @param exponent
+ * exponent to exponientate unit to
+ * @return exponientated unit
+ * @since 2019-01-15
+ * @since v0.1.0
+ */
+ public LinearUnit toExponent(final int exponent) {
+ return new LinearUnit(this.getBase().toExponent(exponent), Math.pow(this.conversionFactor, exponent));
+ }
+
+ @Override
+ public String toString() {
+ return super.toString() + String.format(" (equal to %s * base)", this.getConversionFactor());
+ }
+
+ /**
+ * Returns the result of applying {@code prefix} to this unit.
+ *
+ * @param prefix
+ * prefix to apply
+ * @return unit with prefix
+ * @since 2019-03-18
+ * @since v0.2.0
+ */
+ public LinearUnit withPrefix(final UnitPrefix prefix) {
+ return this.times(prefix.getMultiplier());
+ }
+}
diff --git a/src/org/unitConverter/unit/NonlinearUnits.java b/src/org/unitConverter/unit/NonlinearUnits.java
new file mode 100755
index 0000000..e47c28f
--- /dev/null
+++ b/src/org/unitConverter/unit/NonlinearUnits.java
@@ -0,0 +1,57 @@
+/**
+ * Copyright (C) 2018 Adrien Hopkins
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package org.unitConverter.unit;
+
+/**
+ * Some major nonlinear units.
+ *
+ * @author Adrien Hopkins
+ * @since 2019-01-14
+ * @since v0.1.0
+ */
+public final class NonlinearUnits {
+ public static final Unit CELSIUS = new AbstractUnit(SI.KELVIN) {
+
+ @Override
+ public double convertFromBase(final double value) {
+ return value - 273.15;
+ }
+
+ @Override
+ public double convertToBase(final double value) {
+ return value + 273.15;
+ }
+ };
+
+ public static final Unit FAHRENHEIT = new AbstractUnit(SI.KELVIN) {
+
+ @Override
+ public double convertFromBase(final double value) {
+ return 1.8 * value - 459.67;
+ }
+
+ @Override
+ public double convertToBase(final double value) {
+ return (value + 459.67) / 1.8;
+ }
+ };
+
+ // You may NOT get a NonlinearUnits instance.
+ private NonlinearUnits() {
+ throw new AssertionError();
+ }
+}
diff --git a/src/org/unitConverter/unit/SI.java b/src/org/unitConverter/unit/SI.java
new file mode 100644
index 0000000..46e6ff1
--- /dev/null
+++ b/src/org/unitConverter/unit/SI.java
@@ -0,0 +1,74 @@
+/**
+ * Copyright (C) 2018 Adrien Hopkins
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package org.unitConverter.unit;
+
+import java.util.HashSet;
+import java.util.Objects;
+import java.util.Set;
+
+import org.unitConverter.dimension.StandardDimensions;
+import org.unitConverter.dimension.UnitDimension;
+
+/**
+ * The SI, which holds all SI units
+ *
+ * @author Adrien Hopkins
+ * @since 2018-12-23
+ * @since v0.1.0
+ */
+public enum SI implements UnitSystem {
+ SI;
+
+ /**
+ * This system's base units.
+ *
+ * @since 2019-01-25
+ * @since v0.1.0
+ */
+ private static final Set<BaseUnit> baseUnits = new HashSet<>();
+
+ // base units
+ public static final BaseUnit METRE = SI.getBaseUnit(StandardDimensions.LENGTH);
+ public static final BaseUnit KILOGRAM = SI.getBaseUnit(StandardDimensions.MASS);
+ public static final BaseUnit SECOND = SI.getBaseUnit(StandardDimensions.TIME);
+ public static final BaseUnit AMPERE = SI.getBaseUnit(StandardDimensions.ELECTRIC_CURRENT);
+ public static final BaseUnit KELVIN = SI.getBaseUnit(StandardDimensions.TEMPERATURE);
+ public static final BaseUnit MOLE = SI.getBaseUnit(StandardDimensions.QUANTITY);
+ public static final BaseUnit CANDELA = SI.getBaseUnit(StandardDimensions.LUMINOUS_INTENSITY);
+
+ @Override
+ public BaseUnit getBaseUnit(final UnitDimension dimension) {
+ // try to find an existing unit before creating a new one
+
+ Objects.requireNonNull(dimension, "dimension must not be null.");
+ for (final BaseUnit unit : baseUnits) {
+ // it will be equal since the conditions for equality are dimension and system,
+ // and system is always SI.
+ if (unit.getDimension().equals(dimension))
+ return unit;
+ }
+ // could not find an existing base unit
+ final BaseUnit unit = new BaseUnit(dimension, this);
+ baseUnits.add(unit);
+ return unit;
+ }
+
+ @Override
+ public String getName() {
+ return "SI";
+ }
+}
diff --git a/src/org/unitConverter/unit/SIPrefix.java b/src/org/unitConverter/unit/SIPrefix.java
new file mode 100755
index 0000000..31d7ff2
--- /dev/null
+++ b/src/org/unitConverter/unit/SIPrefix.java
@@ -0,0 +1,54 @@
+/**
+ * Copyright (C) 2018 Adrien Hopkins
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package org.unitConverter.unit;
+
+/**
+ * The SI prefixes.
+ *
+ * @author Adrien Hopkins
+ * @since 2019-01-14
+ * @since v0.1.0
+ */
+public enum SIPrefix implements UnitPrefix {
+ DECA(10), HECTO(100), KILO(1e3), MEGA(1e6), GIGA(1e9), TERA(1e12), PETA(1e15), EXA(1e18), ZETTA(1e21), YOTTA(
+ 1e24), DECI(0.1), CENTI(0.01), MILLI(
+ 1e-3), MICRO(1e-6), NANO(1e-9), PICO(1e-12), FEMTO(1e-15), ATTO(1e-18), ZEPTO(1e-21), YOCTO(1e-24);
+
+ private final double multiplier;
+
+ /**
+ * Creates the {@code SIPrefix}.
+ *
+ * @param multiplier
+ * prefix's multiplier
+ * @since 2019-01-14
+ * @since v0.1.0
+ */
+ private SIPrefix(final double multiplier) {
+ this.multiplier = multiplier;
+ }
+
+ /**
+ * @return value
+ * @since 2019-01-14
+ * @since v0.1.0
+ */
+ @Override
+ public final double getMultiplier() {
+ return this.multiplier;
+ }
+}
diff --git a/src/org/unitConverter/unit/Unit.java b/src/org/unitConverter/unit/Unit.java
new file mode 100755
index 0000000..86fc5a2
--- /dev/null
+++ b/src/org/unitConverter/unit/Unit.java
@@ -0,0 +1,110 @@
+/**
+ * Copyright (C) 2018 Adrien Hopkins
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package org.unitConverter.unit;
+
+import java.util.Objects;
+
+import org.unitConverter.dimension.UnitDimension;
+
+/**
+ * A unit that has an associated base unit, and can convert a value expressed in it to and from that base.
+ *
+ * @author Adrien Hopkins
+ * @since 2018-12-22
+ * @since v0.1.0
+ */
+public interface Unit {
+ /**
+ * Checks if a value expressed in this unit can be converted to a value expressed in {@code other}
+ *
+ * @param other
+ * unit to test with
+ * @return true if the units are compatible
+ * @since 2019-01-13
+ * @since v0.1.0
+ */
+ default boolean canConvertTo(final Unit other) {
+ return Objects.equals(this.getBase(), other.getBase());
+ }
+
+ /**
+ * Converts from a value expressed in this unit's base unit to a value expressed in this unit.
+ * <p>
+ * This must be the inverse of {@code convertToBase}, so {@code convertFromBase(convertToBase(value))} must be equal
+ * to {@code value} for any value, ignoring precision loss by roundoff error.
+ * </p>
+ * <p>
+ * If this unit <i>is</i> a base unit, this method should return {@code value}.
+ * </p>
+ *
+ * @param value
+ * value expressed in <b>base</b> unit
+ * @return value expressed in <b>this</b> unit
+ * @since 2018-12-22
+ * @since v0.1.0
+ */
+ double convertFromBase(double value);
+
+ /**
+ * Converts from a value expressed in this unit to a value expressed in this unit's base unit.
+ * <p>
+ * This must be the inverse of {@code convertFromBase}, so {@code convertToBase(convertFromBase(value))} must be
+ * equal to {@code value} for any value, ignoring precision loss by roundoff error.
+ * </p>
+ * <p>
+ * If this unit <i>is</i> a base unit, this method should return {@code value}.
+ * </p>
+ *
+ * @param value
+ * value expressed in <b>this</b> unit
+ * @return value expressed in <b>base</b> unit
+ * @since 2018-12-22
+ * @since v0.1.0
+ */
+ double convertToBase(double value);
+
+ /**
+ * <p>
+ * Returns the base unit associated with this unit.
+ * </p>
+ * <p>
+ * The dimension of this unit must be equal to the dimension of the returned unit.
+ * </p>
+ * <p>
+ * If this unit <i>is</i> a base unit, this method should return this unit.\
+ * </p>
+ *
+ * @return base unit associated with this unit
+ * @since 2018-12-22
+ * @since v0.1.0
+ */
+ BaseUnit getBase();
+
+ /**
+ * @return dimension measured by this unit
+ * @since 2018-12-22
+ * @since v0.1.0
+ */
+ UnitDimension getDimension();
+
+ /**
+ * @return system that this unit is a part of
+ * @since 2018-12-23
+ * @since v0.1.0
+ */
+ UnitSystem getSystem();
+}
diff --git a/src/org/unitConverter/unit/UnitPrefix.java b/src/org/unitConverter/unit/UnitPrefix.java
new file mode 100755
index 0000000..9f9645d
--- /dev/null
+++ b/src/org/unitConverter/unit/UnitPrefix.java
@@ -0,0 +1,72 @@
+/**
+ * Copyright (C) 2018 Adrien Hopkins
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package org.unitConverter.unit;
+
+/**
+ * A prefix that can be attached onto the front of any unit, which multiplies it by a certain value
+ *
+ * @author Adrien Hopkins
+ * @since 2019-01-14
+ * @since v0.1.0
+ */
+public interface UnitPrefix {
+ /**
+ * Divides this prefix by {@code other}.
+ *
+ * @param other
+ * prefix to divide by
+ * @return quotient of prefixes
+ * @since 2019-04-13
+ * @since v0.2.0
+ */
+ default UnitPrefix dividedBy(final UnitPrefix other) {
+ return new DefaultUnitPrefix(this.getMultiplier() / other.getMultiplier());
+ }
+
+ /**
+ * @return this prefix's multiplier
+ * @since 2019-01-14
+ * @since v0.1.0
+ */
+ double getMultiplier();
+
+ /**
+ * Multiplies this prefix by {@code other}.
+ *
+ * @param other
+ * prefix to multiply by
+ * @return product of prefixes
+ * @since 2019-04-13
+ * @since v0.2.0
+ */
+ default UnitPrefix times(final UnitPrefix other) {
+ return new DefaultUnitPrefix(this.getMultiplier() * other.getMultiplier());
+ }
+
+ /**
+ * Raises this prefix to an exponent.
+ *
+ * @param exponent
+ * exponent to raise to
+ * @return result of exponentiation.
+ * @since 2019-04-13
+ * @since v0.2.0
+ */
+ default UnitPrefix toExponent(final double exponent) {
+ return new DefaultUnitPrefix(Math.pow(getMultiplier(), exponent));
+ }
+}
diff --git a/src/org/unitConverter/unit/UnitSystem.java b/src/org/unitConverter/unit/UnitSystem.java
new file mode 100755
index 0000000..550eff6
--- /dev/null
+++ b/src/org/unitConverter/unit/UnitSystem.java
@@ -0,0 +1,53 @@
+/**
+ * Copyright (C) 2018 Adrien Hopkins
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package org.unitConverter.unit;
+
+import java.util.Objects;
+
+import org.unitConverter.dimension.UnitDimension;
+
+/**
+ * A system of units. Each unit should be aware of its system.
+ *
+ * @author Adrien Hopkins
+ * @since 2018-12-23
+ * @since v0.1.0
+ */
+public interface UnitSystem {
+ /**
+ * Gets a base unit for this system and the provided dimension.
+ *
+ * @param dimension
+ * dimension used by base unit
+ * @return base unit
+ * @throws NullPointerException
+ * if dimension is null
+ * @since 2019-01-25
+ * @since v0.1.0
+ */
+ default BaseUnit getBaseUnit(final UnitDimension dimension) {
+ Objects.requireNonNull(dimension, "dimension must not be null.");
+ return new BaseUnit(dimension, this);
+ }
+
+ /**
+ * @return name of system
+ * @since 2018-12-23
+ * @since v0.1.0
+ */
+ String getName();
+}
diff --git a/src/org/unitConverter/unit/package-info.java b/src/org/unitConverter/unit/package-info.java
new file mode 100644
index 0000000..dd5a939
--- /dev/null
+++ b/src/org/unitConverter/unit/package-info.java
@@ -0,0 +1,24 @@
+/**
+ * Copyright (C) 2019 Adrien Hopkins
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+/**
+ * All of the classes that correspond to the units being converted.
+ *
+ * @author Adrien Hopkins
+ * @since 2019-01-25
+ * @since v0.1.0
+ */
+package org.unitConverter.unit; \ No newline at end of file