diff options
author | Adrien Hopkins <adrien.p.hopkins@gmail.com> | 2019-04-14 17:46:43 -0400 |
---|---|---|
committer | Adrien Hopkins <adrien.p.hopkins@gmail.com> | 2019-04-14 17:46:43 -0400 |
commit | 910b2f1b448ec56e6a66f4aa4f72e71c39de40a1 (patch) | |
tree | 231fcb8f72a467a77efc1e6050de12bd70f7b152 /src/org/unitConverter | |
parent | 70273e127b061c69ce4b3d9d6c3881c6b0c2b829 (diff) | |
parent | 2ce65fa76908d77a5e3b045a8eb40c798939b8be (diff) |
Release v0.2.0v0.2.0
Diffstat (limited to 'src/org/unitConverter')
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 |