summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-xsrc/unitConverter/UnitsDatabase.java573
-rwxr-xr-xsrc/unitConverter/UnitsFileTest.java31
-rwxr-xr-xsrc/unitConverter/converterGUI/DelegateListModel.java228
-rwxr-xr-xsrc/unitConverter/converterGUI/FilterComparator.java72
-rwxr-xr-xsrc/unitConverter/converterGUI/GridBagBuilder.java432
-rwxr-xr-xsrc/unitConverter/converterGUI/UnitConverterGUI.java586
-rw-r--r--src/unitConverter/converterGUI/package-info.java (renamed from src/unitConverter/converter/package-info.java)2
-rw-r--r--src/unitConverter/unit/AbstractUnit.java50
-rwxr-xr-xsrc/unitConverter/unit/BaseUnit.java10
-rwxr-xr-xsrc/unitConverter/unit/DefaultUnitPrefix.java64
-rwxr-xr-xsrc/unitConverter/unit/NonlinearUnits.java56
-rwxr-xr-xsrc/unitConverter/unit/SIPrefix.java14
-rwxr-xr-xsrc/unitConverter/unit/Unit.java2
-rwxr-xr-xsrc/unitConverter/unit/UnitPrefix.java4
-rwxr-xr-xunitsfile.txt234
15 files changed, 2345 insertions, 13 deletions
diff --git a/src/unitConverter/UnitsDatabase.java b/src/unitConverter/UnitsDatabase.java
new file mode 100755
index 0000000..d479917
--- /dev/null
+++ b/src/unitConverter/UnitsDatabase.java
@@ -0,0 +1,573 @@
+/**
+ * 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 unitConverter;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileReader;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+import unitConverter.dimension.UnitDimension;
+import unitConverter.unit.AbstractUnit;
+import unitConverter.unit.BaseUnit;
+import unitConverter.unit.DefaultUnitPrefix;
+import unitConverter.unit.LinearUnit;
+import unitConverter.unit.SI;
+import unitConverter.unit.Unit;
+import unitConverter.unit.UnitPrefix;
+
+/**
+ * A database of units.
+ *
+ * @author Adrien Hopkins
+ * @since 2019-01-07
+ */
+public final class UnitsDatabase {
+ /**
+ * The units in this system.
+ *
+ * @since 2019-01-07
+ */
+ private final Map<String, Unit> units;
+
+ /**
+ * The unit prefixes in this system.
+ *
+ * @since 2019-01-14
+ */
+ private final Map<String, UnitPrefix> prefixes;
+
+ /**
+ * Creates the {@code UnitsDatabase}.
+ *
+ * @since 2019-01-10
+ */
+ public UnitsDatabase() {
+ this.units = new HashMap<>();
+ this.prefixes = new HashMap<>();
+ }
+
+ /**
+ * 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>Any line that begins with the '#' 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
+ */
+ public void addAllFromFile(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()) {
+ final String line = reader.readLine();
+ lineCounter++;
+
+ // ignore lines that start with a # sign - they're comments
+ if (line.startsWith("#") || line.isEmpty()) {
+ continue;
+ }
+
+ // 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, name.substring(0, name.length() - 1));
+ } 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;
+ }
+ AbstractUnit.incrementUnitCounter();
+ if (unit instanceof BaseUnit) {
+ AbstractUnit.incrementBaseUnitCounter();
+ }
+ this.addUnit(name, unit);
+ }
+ }
+ }
+ } 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 a unit prefix to the database using a custom name
+ *
+ * @param name
+ * prefix's name
+ * @param prefix
+ * prefix to add
+ * @throws NullPointerException
+ * if name or unit is null
+ * @since 2019-01-14
+ */
+ 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 using a custom name
+ *
+ * @param name
+ * unit's name
+ * @param unit
+ * unit to add
+ * @throws NullPointerException
+ * if unit is null
+ * @since 2019-01-10
+ */
+ public void addUnit(final String name, final Unit unit) {
+ this.units.put(name, Objects.requireNonNull(unit, "unit must not be null."));
+ }
+
+ /**
+ * Tests if the database has a unit with this name, ignoring prefixes
+ *
+ * @param name
+ * name to test
+ * @return if database contains name
+ * @since 2019-01-13
+ */
+ public boolean containsPrefixlessUnitName(final String name) {
+ return this.units.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
+ */
+ 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
+ */
+ public boolean containsUnitName(final String name) {
+ // check for prefixes
+ for (final String prefixName : this.prefixNameSet()) {
+ if (name.startsWith(prefixName))
+ if (this.containsUnitName(name.substring(prefixName.length())))
+ return true;
+ }
+ return this.units.containsKey(name);
+ }
+
+ /**
+ * Gets a unit prefix from the database from its name
+ *
+ * @param name
+ * prefix's name
+ * @return prefix
+ * @since 2019-01-10
+ */
+ public UnitPrefix getPrefix(final String name) {
+ return this.prefixes.get(name);
+ }
+
+ /**
+ * Gets a unit prefix from a prefix expression and a name
+ * <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
+ * @param name
+ * name of prefix if a new prefix is created
+ * @return prefix
+ * @throws IllegalArgumentException
+ * if expression cannot be parsed
+ * @throws NullPointerException
+ * if any argument is null
+ * @since 2019-01-14
+ */
+ public UnitPrefix getPrefixFromExpression(final String expression, final String name) {
+ Objects.requireNonNull(expression, "expression must not be null.");
+ Objects.requireNonNull(name, "name must not be null.");
+
+ try {
+ return new DefaultUnitPrefix(Double.parseDouble(expression));
+ } catch (final NumberFormatException e) {
+ if (expression.contains("^")) {
+ final String[] baseAndExponent = expression.split("\\^");
+
+ final double base;
+ try {
+ base = Double.parseDouble(baseAndExponent[0]);
+ } catch (final NumberFormatException e2) {
+ throw new IllegalArgumentException("Base of exponientation must be a number.");
+ }
+
+ final int exponent;
+ try {
+ exponent = Integer.parseInt(baseAndExponent[baseAndExponent.length - 1]);
+ } catch (final NumberFormatException e2) {
+ throw new IllegalArgumentException("Exponent must be an integer.");
+ }
+
+ return new DefaultUnitPrefix(Math.pow(base, exponent));
+ } else {
+ if (!this.containsPrefixName(expression))
+ throw new IllegalArgumentException("Unrecognized prefix name \"" + expression + "\".");
+ return this.getPrefix(expression);
+ }
+ }
+ }
+
+ /**
+ * Gets a unit from the database from its name, ignoring prefixes.
+ *
+ * @param name
+ * unit's name
+ * @return unit
+ * @since 2019-01-10
+ */
+ public Unit getPrefixlessUnit(final String name) {
+ return this.units.get(name);
+ }
+
+ /**
+ * Gets a unit from the database from its name, looking for prefixes.
+ *
+ * @param name
+ * unit's name
+ * @return unit
+ * @since 2019-01-10
+ */
+ public Unit getUnit(final String name) {
+ if (name.contains("^")) {
+ final String[] baseAndExponent = name.split("\\^");
+
+ LinearUnit base;
+ try {
+ base = SI.SI.getBaseUnit(UnitDimension.EMPTY).times(Double.parseDouble(baseAndExponent[0]));
+ } catch (final NumberFormatException e2) {
+ final Unit unit = this.getUnit(baseAndExponent[0]);
+ if (unit instanceof LinearUnit) {
+ base = (LinearUnit) unit;
+ } else if (unit instanceof BaseUnit) {
+ base = ((BaseUnit) unit).asLinearUnit();
+ } else
+ throw new IllegalArgumentException("Base of exponientation must be a linear or base unit.");
+ }
+
+ final int exponent;
+ try {
+ exponent = Integer.parseInt(baseAndExponent[baseAndExponent.length - 1]);
+ } catch (final NumberFormatException e2) {
+ throw new IllegalArgumentException("Exponent must be an integer.");
+ }
+
+ final LinearUnit exponentiated = base.toExponent(exponent);
+ if (exponentiated.getConversionFactor() == 1)
+ return exponentiated.getSystem().getBaseUnit(exponentiated.getDimension());
+ else
+ return exponentiated;
+ } else {
+ for (final String prefixName : this.prefixNameSet()) {
+ // check for a prefix
+ if (name.startsWith(prefixName)) {
+ // prefix found! Make sure what comes after it is actually a unit!
+ final String prefixless = name.substring(prefixName.length());
+ if (this.containsUnitName(prefixless)) {
+ // yep, it's a proper prefix! Get the unit!
+ final Unit unit = this.getUnit(prefixless);
+ final UnitPrefix prefix = this.getPrefix(prefixName);
+
+ // Prefixes only work with linear and base units, so make sure it's one of those
+ if (unit instanceof LinearUnit) {
+ final LinearUnit linearUnit = (LinearUnit) unit;
+ return linearUnit.times(prefix.getMultiplier());
+ } else if (unit instanceof BaseUnit) {
+ final BaseUnit baseUnit = (BaseUnit) unit;
+ return baseUnit.times(prefix.getMultiplier());
+ }
+ }
+ }
+ }
+ 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>A number which is multiplied or divided</li>
+ * </ul>
+ * This method only works with linear units.
+ * <p>
+ * If the expression contains just the name of a unit, returns that unit without changing name or symbol. This
+ * exists for the creation of aliases.
+ * </p>
+ *
+ * @param line
+ * line to parse
+ * @throws IllegalArgumentException
+ * if the expression cannot be parsed
+ * @throws NullPointerException
+ * if any argument is null
+ * @since 2019-01-07
+ */
+ public Unit getUnitFromExpression(final String expression) {
+ Objects.requireNonNull(expression, "expression must not be null.");
+
+ // parse the expression
+ // start with an "empty" unit then apply operations on it
+ LinearUnit unit = SI.SI.getBaseUnit(UnitDimension.EMPTY).asLinearUnit();
+ boolean dividing = false;
+
+ // if I'm just creating an alias, just create one instead of going through the parsing process
+ if (!expression.contains(" ") && !expression.contains("*") && !expression.contains("/")
+ && !expression.contains("(") && !expression.contains(")") && !expression.contains("^")) {
+ try {
+ return SI.SI.getBaseUnit(UnitDimension.EMPTY).times(Double.parseDouble(expression));
+ } catch (final NumberFormatException e) {
+ if (!this.containsUnitName(expression))
+ throw new IllegalArgumentException("Unrecognized unit name \"" + expression + "\".");
+ return this.getUnit(expression);
+ }
+ }
+
+ // \\* means "asterisk", * is reserved
+ for (final String part : expression.replaceAll("\\*", " \\* ").replaceAll("/", " / ").replaceAll(" \\^", "\\^")
+ .replaceAll("\\^ ", "\\^").split(" ")) {
+ if ("".equals(part)) {
+ continue;
+ }
+ // "unit1 unit2" is the same as "unit1 * unit2", so multiplication signs do nothing
+ if ("*".equals(part)) {
+ continue;
+ }
+ // When I reach a division sign, don't parse a unit, instead tell myself I'm going to divide the next
+ // thing
+ if ("/".equals(part) || "per".equals(part)) {
+ dividing = true;
+ continue;
+ }
+
+ try {
+ final double partAsNumber = Double.parseDouble(part); // if this works, it's a number
+ // this code should not throw any exceptions, so I'm going to throw AssertionErrors if it does
+ try {
+ if (dividing) {
+ unit = unit.dividedBy(partAsNumber);
+ dividing = false;
+ } else {
+ unit = unit.times(partAsNumber);
+ }
+ } catch (final Exception e) {
+ throw new AssertionError(e);
+ }
+ } catch (final NumberFormatException e) {
+ // it's a unit, try that
+
+ if (part.contains("(") && part.endsWith(")")) {
+ // the unitsfile is looking for a nonlinear unit
+ final String[] unitAndValue = part.split("\\(");
+
+ // this will work because I've checked that it contains a (
+ final String unitName = unitAndValue[0];
+ final String valueStringWithBracket = unitAndValue[unitAndValue.length - 1];
+ final String valueString = valueStringWithBracket.substring(0, valueStringWithBracket.length() - 1);
+ final double value;
+
+ // try to get the value - else throw an error
+ try {
+ value = Double.parseDouble(valueString);
+ } catch (final NumberFormatException e2) {
+ throw new IllegalArgumentException("Unparseable value " + valueString);
+ }
+
+ // get this unit in a linear form
+ if (!this.containsPrefixlessUnitName(unitName))
+ throw new IllegalArgumentException("Unrecognized unit name \"" + part + "\".");
+ final Unit partUnit = this.getPrefixlessUnit(unitName);
+ final LinearUnit multiplier = partUnit.getBase().times(partUnit.convertToBase(value));
+
+ // finally, add it to the expression
+ if (dividing) {
+ unit = unit.dividedBy(multiplier);
+ dividing = false;
+ } else {
+ unit = unit.times(multiplier);
+ }
+ } else {
+ // check for exponientation
+ if (part.contains("^")) {
+ final String[] valueAndExponent = part.split("\\^");
+ // this will always work because of the contains check
+ final String valueString = valueAndExponent[0];
+ final String exponentString = valueAndExponent[valueAndExponent.length - 1];
+
+ LinearUnit value;
+
+ // first, try to get the value
+ try {
+ final double valueAsNumber = Double.parseDouble(valueString);
+
+ value = SI.SI.getBaseUnit(UnitDimension.EMPTY).times(valueAsNumber);
+ } catch (final NumberFormatException e2) {
+
+ // look for a unit
+ if (!this.containsUnitName(valueString))
+ throw new IllegalArgumentException("Unrecognized unit name \"" + valueString + "\".");
+ final Unit valueUnit = this.getUnit(valueString);
+
+ // try to turn the value into a linear unit
+ if (valueUnit instanceof LinearUnit) {
+ value = (LinearUnit) valueUnit;
+ } else if (valueUnit instanceof BaseUnit) {
+ value = ((BaseUnit) valueUnit).asLinearUnit();
+ } else
+ throw new IllegalArgumentException("Only linear and base units can be exponientated.");
+ }
+
+ // now, try to get the exponent
+ final int exponent;
+ try {
+ exponent = Integer.parseInt(exponentString);
+ } catch (final NumberFormatException e2) {
+ throw new IllegalArgumentException("Exponents must be integers.");
+ }
+
+ final LinearUnit exponientated = value.toExponent(exponent);
+
+ if (dividing) {
+ unit = unit.dividedBy(exponientated);
+ dividing = false;
+ } else {
+ unit = unit.times(exponientated);
+ }
+ } else {
+ // no exponent - look for a unit
+ // the unitsfile is looking for a linear unit
+ if (!this.containsUnitName(part))
+ throw new IllegalArgumentException("Unrecognized unit name \"" + part + "\".");
+ Unit other = this.getUnit(part);
+ if (other instanceof BaseUnit) {
+ other = ((BaseUnit) other).asLinearUnit();
+ }
+ if (other instanceof LinearUnit) {
+ if (dividing) {
+ unit = unit.dividedBy((LinearUnit) other);
+ dividing = false;
+ } else {
+ unit = unit.times((LinearUnit) other);
+ }
+ } else
+ throw new IllegalArgumentException(
+ "Only linear or base units can be multiplied and divided. If you want to use a non-linear unit, try the format UNITNAME(VALUE).");
+ }
+ }
+ }
+ }
+
+ // replace conversion-factor-1 linear units with base units
+ // this improves the autogenerated names, allowing them to use derived SI names
+ if (unit != null && unit.getConversionFactor() == 1)
+ return unit.getSystem().getBaseUnit(unit.getDimension());
+ else
+ return unit;
+ }
+
+ /**
+ * @return an immutable set of all of the unit names in this database, ignoring prefixes
+ * @since 2019-01-14
+ */
+ public Set<String> prefixlessUnitNameSet() {
+ return Collections.unmodifiableSet(this.units.keySet());
+ }
+
+ /**
+ * @return an immutable set of all of the prefix names in this database
+ * @since 2019-01-14
+ */
+ public Set<String> prefixNameSet() {
+ return Collections.unmodifiableSet(this.prefixes.keySet());
+ }
+}
diff --git a/src/unitConverter/UnitsFileTest.java b/src/unitConverter/UnitsFileTest.java
new file mode 100755
index 0000000..8edeab0
--- /dev/null
+++ b/src/unitConverter/UnitsFileTest.java
@@ -0,0 +1,31 @@
+/**
+ * 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 unitConverter;
+
+import static org.junit.jupiter.api.Assertions.fail;
+
+/**
+ * @author Adrien Hopkins
+ * @since 2019-01-02
+ */
+class UnitsFileTest {
+
+ // @Test
+ void testReading() {
+ fail("Not yet implemented.");
+ }
+}
diff --git a/src/unitConverter/converterGUI/DelegateListModel.java b/src/unitConverter/converterGUI/DelegateListModel.java
new file mode 100755
index 0000000..42bc0dc
--- /dev/null
+++ b/src/unitConverter/converterGUI/DelegateListModel.java
@@ -0,0 +1,228 @@
+/**
+ * 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 unitConverter.converterGUI;
+
+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
+ */
+final class DelegateListModel<E> extends AbstractListModel<E> implements List<E> {
+ /**
+ * @since 2019-01-14
+ */
+ private static final long serialVersionUID = 8985494428224810045L;
+
+ /**
+ * The list that this model is a delegate to.
+ *
+ * @since 2019-01-14
+ */
+ private final List<E> delegate;
+
+ /**
+ * Creates the {@code DelegateListModel}.
+ *
+ * @param delegate
+ * list to delegate
+ * @since 2019-01-14
+ */
+ 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/unitConverter/converterGUI/FilterComparator.java b/src/unitConverter/converterGUI/FilterComparator.java
new file mode 100755
index 0000000..ab25f2f
--- /dev/null
+++ b/src/unitConverter/converterGUI/FilterComparator.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 unitConverter.converterGUI;
+
+import java.util.Comparator;
+
+/**
+ * @author Adrien Hopkins
+ * @since 2019-01-15
+ */
+public final class FilterComparator implements Comparator<String> {
+
+ private final String filter;
+ private final Comparator<String> comparator;
+
+ /**
+ * Creates the {@code FilterComparator}.
+ *
+ * @param filter
+ * @since 2019-01-15
+ */
+ public FilterComparator(final String filter) {
+ this(filter, null);
+ }
+
+ /**
+ * Creates the {@code FilterComparator}.
+ *
+ * @param filter
+ * @param comparator
+ * @since 2019-01-15
+ */
+ public FilterComparator(final String filter, final Comparator<String> comparator) {
+ this.filter = filter;
+ this.comparator = comparator;
+ }
+
+ @Override
+ public int compare(final String arg0, final String arg1) {
+ // elements that start with the filter always go first
+ if (arg0.startsWith(this.filter) && !arg1.startsWith(this.filter))
+ return -1;
+ else if (!arg0.startsWith(this.filter) && arg1.startsWith(this.filter))
+ return 1;
+
+ // elements that contain the filter but don't start with them go next
+ if (arg0.contains(this.filter) && !arg1.contains(this.filter))
+ return -1;
+ else if (!arg0.contains(this.filter) && !arg1.contains(this.filter))
+ return 1;
+
+ // other elements go last
+ if (this.comparator == null)
+ return arg0.compareTo(arg1);
+ else
+ return this.comparator.compare(arg0, arg1);
+ }
+}
diff --git a/src/unitConverter/converterGUI/GridBagBuilder.java b/src/unitConverter/converterGUI/GridBagBuilder.java
new file mode 100755
index 0000000..7a0615a
--- /dev/null
+++ b/src/unitConverter/converterGUI/GridBagBuilder.java
@@ -0,0 +1,432 @@
+/**
+ * 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 unitConverter.converterGUI;
+
+import java.awt.GridBagConstraints;
+import java.awt.Insets;
+
+/**
+ * @author Adrien Hopkins
+ * @since 2018-11-30
+ */
+final class GridBagBuilder {
+ /**
+ * 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;
+
+ /**
+ * 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;
+
+ /**
+ * 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;
+
+ /**
+ * 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;
+
+ /**
+ * 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;
+
+ /**
+ * 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;
+
+ /**
+ * 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;
+
+ /**
+ * 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;
+
+ /**
+ * 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;
+
+ /**
+ * 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;
+
+ /**
+ * 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
+ */
+ 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
+ */
+ 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
+ */
+ 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
+ */
+ 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
+ */
+ public int getAnchor() {
+ return this.anchor;
+ }
+
+ /**
+ * @return fill
+ * @since 2018-11-30
+ */
+ public int getFill() {
+ return this.fill;
+ }
+
+ /**
+ * @return gridheight
+ * @since 2018-11-30
+ */
+ public int getGridheight() {
+ return this.gridheight;
+ }
+
+ /**
+ * @return gridwidth
+ * @since 2018-11-30
+ */
+ public int getGridwidth() {
+ return this.gridwidth;
+ }
+
+ /**
+ * @return gridx
+ * @since 2018-11-30
+ */
+ public int getGridx() {
+ return this.gridx;
+ }
+
+ /**
+ * @return gridy
+ * @since 2018-11-30
+ */
+ public int getGridy() {
+ return this.gridy;
+ }
+
+ /**
+ * @return insets
+ * @since 2018-11-30
+ */
+ public Insets getInsets() {
+ return this.insets;
+ }
+
+ /**
+ * @return ipadx
+ * @since 2018-11-30
+ */
+ public int getIpadx() {
+ return this.ipadx;
+ }
+
+ /**
+ * @return ipady
+ * @since 2018-11-30
+ */
+ public int getIpady() {
+ return this.ipady;
+ }
+
+ /**
+ * @return weightx
+ * @since 2018-11-30
+ */
+ public double getWeightx() {
+ return this.weightx;
+ }
+
+ /**
+ * @return weighty
+ * @since 2018-11-30
+ */
+ public double getWeighty() {
+ return this.weighty;
+ }
+
+ /**
+ * @param anchor
+ * anchor to set
+ * @since 2018-11-30
+ */
+ public GridBagBuilder setAnchor(final int anchor) {
+ this.anchor = anchor;
+ return this;
+ }
+
+ /**
+ * @param fill
+ * fill to set
+ * @since 2018-11-30
+ */
+ public GridBagBuilder setFill(final int fill) {
+ this.fill = fill;
+ return this;
+ }
+
+ /**
+ * @param insets
+ * insets to set
+ * @since 2018-11-30
+ */
+ public GridBagBuilder setInsets(final Insets insets) {
+ this.insets = insets;
+ return this;
+ }
+
+ /**
+ * @param ipadx
+ * ipadx to set
+ * @since 2018-11-30
+ */
+ public GridBagBuilder setIpadx(final int ipadx) {
+ this.ipadx = ipadx;
+ return this;
+ }
+
+ /**
+ * @param ipady
+ * ipady to set
+ * @since 2018-11-30
+ */
+ public GridBagBuilder setIpady(final int ipady) {
+ this.ipady = ipady;
+ return this;
+ }
+
+ /**
+ * @param weightx
+ * weightx to set
+ * @since 2018-11-30
+ */
+ public GridBagBuilder setWeightx(final double weightx) {
+ this.weightx = weightx;
+ return this;
+ }
+
+ /**
+ * @param weighty
+ * weighty to set
+ * @since 2018-11-30
+ */
+ public GridBagBuilder setWeighty(final double weighty) {
+ this.weighty = weighty;
+ return this;
+ }
+}
diff --git a/src/unitConverter/converterGUI/UnitConverterGUI.java b/src/unitConverter/converterGUI/UnitConverterGUI.java
new file mode 100755
index 0000000..0068312
--- /dev/null
+++ b/src/unitConverter/converterGUI/UnitConverterGUI.java
@@ -0,0 +1,586 @@
+/**
+ * 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 unitConverter.converterGUI;
+
+import java.awt.BorderLayout;
+import java.awt.GridLayout;
+import java.io.File;
+import java.math.BigDecimal;
+import java.math.MathContext;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.function.Predicate;
+
+import javax.swing.BorderFactory;
+import javax.swing.JButton;
+import javax.swing.JFrame;
+import javax.swing.JList;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.JSlider;
+import javax.swing.JTabbedPane;
+import javax.swing.JTextArea;
+import javax.swing.JTextField;
+import javax.swing.ListModel;
+import javax.swing.ListSelectionModel;
+
+import unitConverter.UnitsDatabase;
+import unitConverter.dimension.StandardDimensions;
+import unitConverter.dimension.UnitDimension;
+import unitConverter.unit.AbstractUnit;
+import unitConverter.unit.NonlinearUnits;
+import unitConverter.unit.SI;
+import unitConverter.unit.Unit;
+import unitConverter.unit.UnitPrefix;
+
+/**
+ * @author Adrien Hopkins
+ * @since 2018-12-27
+ */
+final class UnitConverterGUI {
+ private static class Presenter {
+ /** The presenter's associated view. */
+ private final View view;
+
+ /** The units known by the program. */
+ private final UnitsDatabase units;
+
+ /** The names of all of the units */
+ private final List<String> unitNames;
+
+ /** The names of all of the units, but filtered */
+ private final DelegateListModel<String> unitNamesFiltered;
+
+ /** The names of all of the prefixes */
+ private final List<String> prefixNames;
+
+ /** The names of all of the prefixes */
+ private final DelegateListModel<String> prefixNamesFiltered;
+
+ private final Comparator<String> prefixNameComparator;
+
+ private int significantFigures = 6;
+
+ /**
+ * Creates the presenter.
+ *
+ * @param view
+ * presenter's associated view
+ * @since 2018-12-27
+ */
+ Presenter(final View view) {
+ this.view = view;
+
+ // load initial units
+ this.units = new UnitsDatabase();
+ this.units.addUnit("metre", SI.METRE);
+ this.units.addUnit("kilogram", SI.KILOGRAM);
+ this.units.addUnit("gram", SI.KILOGRAM.dividedBy(1000));
+ this.units.addUnit("second", SI.SECOND);
+ this.units.addUnit("ampere", SI.AMPERE);
+ this.units.addUnit("kelvin", SI.KELVIN);
+ this.units.addUnit("mole", SI.MOLE);
+ this.units.addUnit("candela", SI.CANDELA);
+ this.units.addUnit("bit", SI.SI.getBaseUnit(StandardDimensions.INFORMATION));
+ this.units.addUnit("unit", SI.SI.getBaseUnit(UnitDimension.EMPTY));
+ // nonlinear units - must be loaded manually
+ this.units.addUnit("tempCelsius", NonlinearUnits.CELSIUS);
+ this.units.addUnit("tempFahrenheit", NonlinearUnits.FAHRENHEIT);
+
+ this.units.addAllFromFile(new File("unitsfile.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.units.containsPrefixName(o1))
+ return -1;
+ else if (!Presenter.this.units.containsPrefixName(o2))
+ return 1;
+
+ final UnitPrefix p1 = Presenter.this.units.getPrefix(o1);
+ final UnitPrefix p2 = Presenter.this.units.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.units.prefixlessUnitNameSet());
+ this.unitNames.sort(null); // sorts it using Comparable
+
+ this.unitNamesFiltered = new DelegateListModel<>(new ArrayList<>(this.units.prefixlessUnitNameSet()));
+ this.unitNamesFiltered.sort(null); // sorts it using Comparable
+
+ this.prefixNames = new ArrayList<>(this.units.prefixNameSet());
+ this.prefixNames.sort(this.prefixNameComparator); // sorts it using my comparator
+
+ this.prefixNamesFiltered = new DelegateListModel<>(new ArrayList<>(this.units.prefixNameSet()));
+ this.prefixNamesFiltered.sort(this.prefixNameComparator); // sorts it using my comparator
+
+ System.out.printf("Successfully loaded %d units (%d base units)", AbstractUnit.getUnitCount(),
+ AbstractUnit.getBaseUnitCount());
+ }
+
+ public final void convert() {
+ final String fromUnitString = this.view.getFromText();
+ final String toUnitString = this.view.getToText();
+
+ // try to parse from
+ final Unit from;
+ try {
+ from = this.units.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 {
+ to = this.units.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 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);
+ }
+ }
+
+ this.view.setOutputText(String.format("%s = %s %s", fromUnitString, output, toUnitString));
+ }
+
+ /**
+ * Filters the filtered model for units
+ *
+ * @param filter
+ * filter to use
+ * @since 2019-01-15
+ */
+ private final void filterFilteredPrefixModel(final Predicate<String> filter) {
+ this.prefixNamesFiltered.clear();
+ for (final String prefixName : this.prefixNames) {
+ if (filter.test(prefixName)) {
+ this.prefixNamesFiltered.add(prefixName);
+ }
+ }
+ }
+
+ /**
+ * Filters the filtered model for units
+ *
+ * @param filter
+ * filter to use
+ * @since 2019-01-15
+ */
+ private final void filterFilteredUnitModel(final Predicate<String> filter) {
+ this.unitNamesFiltered.clear();
+ for (final String unitName : this.unitNames) {
+ if (filter.test(unitName)) {
+ this.unitNamesFiltered.add(unitName);
+ }
+ }
+ }
+
+ /**
+ * @return a list model of all of the unit keys
+ * @since 2019-01-14
+ */
+ public final ListModel<String> keyListModel() {
+ return this.unitNamesFiltered;
+ }
+
+ public final void prefixFilterUpdated() {
+ final String filter = this.view.getPrefixFilterText();
+ if (filter.equals("")) {
+ this.filterFilteredPrefixModel(t -> true);
+ } else {
+ this.filterFilteredPrefixModel(t -> t.contains(filter));
+ }
+ this.prefixNamesFiltered.sort(new FilterComparator(filter));
+ }
+
+ /**
+ * @return a list model of all fo the prefix names
+ * @since 2019-01-15
+ */
+ public final ListModel<String> prefixNameListModel() {
+ return this.prefixNamesFiltered;
+ }
+
+ public final void prefixSelected() {
+ final int index = this.view.getPrefixListSelection();
+ if (index == -1)
+ return;
+ else {
+ final String prefixName = this.prefixNamesFiltered.get(index);
+ final UnitPrefix prefix = this.units.getPrefix(prefixName);
+
+ this.view.setPrefixTextBoxText(String.format("%s%nMultiplier: %s", prefixName, prefix.getMultiplier()));
+ }
+ }
+
+ /**
+ * @param significantFigures
+ * new value of significantFigures
+ * @since 2019-01-15
+ */
+ public final void setSignificantFigures(final int significantFigures) {
+ this.significantFigures = significantFigures;
+ }
+
+ public final void unitFilterUpdated() {
+ final String filter = this.view.getUnitFilterText();
+ if (filter.equals("")) {
+ this.filterFilteredUnitModel(t -> true);
+ } else {
+ this.filterFilteredUnitModel(t -> t.contains(filter));
+ }
+ this.unitNamesFiltered.sort(new FilterComparator(filter));
+ }
+
+ /**
+ *
+ * @since 2019-01-15
+ */
+ public void unitNameSelected() {
+ final int index = this.view.getUnitListSelection();
+ if (index == -1)
+ return;
+ else {
+ final String unitName = this.unitNamesFiltered.get(index);
+ final Unit unit = this.units.getUnit(unitName);
+
+ this.view.setUnitTextBoxText(unit.toString());
+ }
+ }
+ }
+
+ private static class View {
+ /** The view's frame. */
+ private final JFrame frame;
+ /** The view's associated presenter. */
+ private final Presenter presenter;
+
+ private final JList<String> unitNameList;
+ private final JList<String> prefixNameList;
+ private final JTextField unitFilterEntry;
+ private final JTextArea unitTextBox;
+ private final JTextField prefixFilterEntry;
+ private final JTextArea prefixTextBox;
+ private final JTextField fromEntry;
+ private final JTextField toEntry;
+ private final JTextArea output;
+
+ /**
+ * Creates the {@code View}.
+ *
+ * @since 2019-01-14
+ */
+ public View() {
+ this.presenter = new Presenter(this);
+ this.frame = new JFrame("Unit Converter");
+ this.frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
+
+ this.unitNameList = new JList<>(this.presenter.keyListModel());
+ this.prefixNameList = new JList<>(this.presenter.prefixNameListModel());
+ this.unitFilterEntry = new JTextField();
+ this.unitTextBox = new JTextArea();
+ this.prefixFilterEntry = new JTextField();
+ this.prefixTextBox = new JTextArea();
+ this.fromEntry = new JTextField();
+ this.toEntry = new JTextField();
+ this.output = new JTextArea(2, 32);
+
+ this.initComponents();
+
+ this.frame.pack();
+ }
+
+ public String getFromText() {
+ return this.fromEntry.getText();
+ }
+
+ /**
+ * @return text in prefix filter
+ * @since 2019-01-15
+ */
+ public String getPrefixFilterText() {
+ return this.prefixFilterEntry.getText();
+ }
+
+ /**
+ * @return index of selected prefix
+ * @since 2019-01-15
+ */
+ public int getPrefixListSelection() {
+ return this.prefixNameList.getSelectedIndex();
+ }
+
+ public String getToText() {
+ return this.toEntry.getText();
+ }
+
+ /**
+ * @return text in unit filter
+ * @see javax.swing.text.JTextComponent#getText()
+ */
+ public String getUnitFilterText() {
+ return this.unitFilterEntry.getText();
+ }
+
+ /**
+ * @return index of selected unit
+ * @since 2019-01-15
+ */
+ public int getUnitListSelection() {
+ return this.unitNameList.getSelectedIndex();
+ }
+
+ /**
+ * Starts up the application.
+ *
+ * @since 2018-12-27
+ */
+ public final void init() {
+ this.frame.setVisible(true);
+ }
+
+ /**
+ * Initializes the view's components.
+ *
+ * @since 2018-12-27
+ */
+ 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);
+
+ { // panel for unit conversion
+ final JPanel convertPanel = new JPanel();
+ masterPane.addTab("Convert Units", convertPanel);
+
+ convertPanel.setLayout(new GridLayout(5, 1));
+
+ { // panel for units to convert from
+ final JPanel fromPanel = new JPanel();
+ convertPanel.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();
+ convertPanel.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!");
+ convertPanel.add(convertButton);
+
+ convertButton.addActionListener(e -> this.presenter.convert());
+ }
+
+ { // output of conversion
+ final JPanel outputPanel = new JPanel();
+ convertPanel.add(outputPanel);
+
+ outputPanel.setBorder(BorderFactory.createTitledBorder("Output"));
+ outputPanel.setLayout(new GridLayout(1, 1));
+
+ { // output
+ outputPanel.add(this.output);
+ this.output.setEditable(false);
+ }
+ }
+
+ {
+ final JPanel sigDigPanel = new JPanel();
+ convertPanel.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());
+
+ { // panel for listing and searching
+ final JPanel listPanel = new JPanel();
+ unitLookupPanel.add(listPanel);
+
+ listPanel.setLayout(new BorderLayout());
+
+ {
+ listPanel.add(this.unitFilterEntry, BorderLayout.PAGE_START);
+ this.unitFilterEntry.addCaretListener(e -> this.presenter.unitFilterUpdated());
+ }
+
+ { // a list of units
+ listPanel.add(new JScrollPane(this.unitNameList), BorderLayout.CENTER);
+ this.unitNameList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); // temp
+ this.unitNameList.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));
+
+ {
+ final JPanel prefixListPanel = new JPanel();
+ prefixLookupPanel.add(prefixListPanel);
+
+ prefixListPanel.setLayout(new BorderLayout());
+
+ {
+ prefixListPanel.add(this.prefixFilterEntry, BorderLayout.PAGE_START);
+ this.prefixFilterEntry.addCaretListener(e -> this.presenter.prefixFilterUpdated());
+ }
+
+ { // a list of prefixes
+ prefixListPanel.add(new JScrollPane(this.prefixNameList), BorderLayout.CENTER);
+ this.prefixNameList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); // temp
+ this.prefixNameList.addListSelectionListener(e -> {
+ this.presenter.prefixSelected();
+ });
+ }
+ }
+
+ { // the text box for prefix's toString
+ prefixLookupPanel.add(this.prefixTextBox);
+ this.unitTextBox.setEditable(false);
+ }
+ }
+ }
+ }
+
+ public void setOutputText(final String text) {
+ this.output.setText(text);
+ }
+
+ /**
+ * Sets the text of the prefix text box.
+ *
+ * @param text
+ * text to set
+ * @since 2019-01-15
+ */
+ public void setPrefixTextBoxText(final String text) {
+ this.prefixTextBox.setText(text);
+ }
+
+ /**
+ * Sets the text of the unit text box.
+ *
+ * @param t
+ * text to set
+ * @see javax.swing.text.JTextComponent#setText(java.lang.String)
+ */
+ public void setUnitTextBoxText(final String t) {
+ this.unitTextBox.setText(t);
+ }
+
+ /**
+ * Shows an error dialog.
+ *
+ * @param title
+ * title of dialog
+ * @param message
+ * message in dialog
+ * @since 2019-01-14
+ */
+ 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/unitConverter/converter/package-info.java b/src/unitConverter/converterGUI/package-info.java
index 6148784..9f7fa57 100644
--- a/src/unitConverter/converter/package-info.java
+++ b/src/unitConverter/converterGUI/package-info.java
@@ -20,4 +20,4 @@
* @author Adrien Hopkins
* @since 2019-01-25
*/
-package unitConverter.converter; \ No newline at end of file
+package unitConverter.converterGUI; \ No newline at end of file
diff --git a/src/unitConverter/unit/AbstractUnit.java b/src/unitConverter/unit/AbstractUnit.java
index 62c07a2..d3d6dbd 100644
--- a/src/unitConverter/unit/AbstractUnit.java
+++ b/src/unitConverter/unit/AbstractUnit.java
@@ -26,7 +26,55 @@ import unitConverter.dimension.UnitDimension;
* @author Adrien Hopkins
* @since 2019-01-25
*/
-abstract class AbstractUnit implements Unit {
+public abstract class AbstractUnit implements Unit {
+ /**
+ * The number of units created, including base units.
+ *
+ * @since 2019-01-02
+ */
+ private static long unitCount = 0;
+
+ /**
+ * The number of base units created.
+ *
+ * @since 2019-01-02
+ */
+ private static long baseUnitCount = 0;
+
+ /**
+ * @return number of base units created
+ * @since 2019-01-02
+ */
+ public static final long getBaseUnitCount() {
+ return baseUnitCount;
+ }
+
+ /**
+ * @return number of units created
+ * @since 2019-01-02
+ */
+ public static final long getUnitCount() {
+ return unitCount;
+ }
+
+ /**
+ * Increments the number of base units.
+ *
+ * @since 2019-01-15
+ */
+ public static final void incrementBaseUnitCounter() {
+ baseUnitCount++;
+ }
+
+ /**
+ * Increments the number of units.
+ *
+ * @since 2019-01-15
+ */
+ public static final void incrementUnitCounter() {
+ unitCount++;
+ }
+
/**
* The dimension, or what the unit measures.
*
diff --git a/src/unitConverter/unit/BaseUnit.java b/src/unitConverter/unit/BaseUnit.java
index 46316bf..204b1cd 100755
--- a/src/unitConverter/unit/BaseUnit.java
+++ b/src/unitConverter/unit/BaseUnit.java
@@ -53,6 +53,14 @@ public final class BaseUnit extends AbstractUnit {
this.isFullBase = dimension.isBase();
}
+ /**
+ * @return this unit as a {@code LinearUnit}
+ * @since 2019-01-25
+ */
+ public LinearUnit asLinearUnit() {
+ return this.times(1);
+ }
+
@Override
public double convertFromBase(final double value) {
return value;
@@ -134,7 +142,7 @@ public final class BaseUnit extends AbstractUnit {
* @since 2019-01-15
*/
public BaseUnit toExponent(final int exponent) {
- return this.toExponent(exponent);
+ return this.getSystem().getBaseUnit(this.getDimension().toExponent(exponent));
}
@Override
diff --git a/src/unitConverter/unit/DefaultUnitPrefix.java b/src/unitConverter/unit/DefaultUnitPrefix.java
new file mode 100755
index 0000000..1cce413
--- /dev/null
+++ b/src/unitConverter/unit/DefaultUnitPrefix.java
@@ -0,0 +1,64 @@
+/**
+ * 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 unitConverter.unit;
+
+import java.util.Objects;
+
+/**
+ * @author Adrien Hopkins
+ * @since 2019-01-14
+ */
+public final class DefaultUnitPrefix implements UnitPrefix {
+ private final double multiplier;
+
+ /**
+ * Creates the {@code DefaultUnitPrefix}.
+ *
+ * @param multiplier
+ * @since 2019-01-14
+ */
+ 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/unitConverter/unit/NonlinearUnits.java b/src/unitConverter/unit/NonlinearUnits.java
new file mode 100755
index 0000000..f7e257c
--- /dev/null
+++ b/src/unitConverter/unit/NonlinearUnits.java
@@ -0,0 +1,56 @@
+/**
+ * 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 unitConverter.unit;
+
+/**
+ * Some major nonlinear units.
+ *
+ * @author Adrien Hopkins
+ * @since 2019-01-14
+ */
+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/unitConverter/unit/SIPrefix.java b/src/unitConverter/unit/SIPrefix.java
index 452d46f..39f1a8c 100755
--- a/src/unitConverter/unit/SIPrefix.java
+++ b/src/unitConverter/unit/SIPrefix.java
@@ -25,17 +25,17 @@ public enum SIPrefix implements UnitPrefix {
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 value;
+ private final double multiplier;
/**
* Creates the {@code SIPrefix}.
*
- * @param value
- * value of prefix
+ * @param multiplier
+ * prefix's multiplier
* @since 2019-01-14
*/
- private SIPrefix(final double value) {
- this.value = value;
+ private SIPrefix(final double multiplier) {
+ this.multiplier = multiplier;
}
/**
@@ -43,7 +43,7 @@ public enum SIPrefix implements UnitPrefix {
* @since 2019-01-14
*/
@Override
- public final double getValue() {
- return this.value;
+ public final double getMultiplier() {
+ return this.multiplier;
}
}
diff --git a/src/unitConverter/unit/Unit.java b/src/unitConverter/unit/Unit.java
index 3e7f9da..9e1375a 100755
--- a/src/unitConverter/unit/Unit.java
+++ b/src/unitConverter/unit/Unit.java
@@ -87,7 +87,7 @@ public interface Unit {
* @return base unit associated with this unit
* @since 2018-12-22
*/
- Unit getBase();
+ BaseUnit getBase();
/**
* @return dimension measured by this unit
diff --git a/src/unitConverter/unit/UnitPrefix.java b/src/unitConverter/unit/UnitPrefix.java
index 0dbdc00..cb50fd9 100755
--- a/src/unitConverter/unit/UnitPrefix.java
+++ b/src/unitConverter/unit/UnitPrefix.java
@@ -24,8 +24,8 @@ package unitConverter.unit;
*/
public interface UnitPrefix {
/**
- * @return value of this prefix
+ * @return this prefix's multiplier
* @since 2019-01-14
*/
- double getValue();
+ double getMultiplier();
}
diff --git a/unitsfile.txt b/unitsfile.txt
new file mode 100755
index 0000000..78e51f7
--- /dev/null
+++ b/unitsfile.txt
@@ -0,0 +1,234 @@
+# A file for the units in my unit converter program
+
+# SI Base Units
+# ! means "look for an existing unit which I will load at the start"
+# This is necessary because every unit must be defined by others, and I need somewhere to start.
+
+metre !
+kilogram !
+second !
+ampere !
+kelvin !
+mole !
+candela !
+
+# Symbols and aliases for base units
+
+meter metre
+m metre
+kg kilogram
+s second
+A ampere
+K kelvin
+mol mole
+cd candela
+
+# the bit and byte, units of information
+
+bit !
+b bit
+byte 8 bit
+B byte
+
+# SI prefixes
+
+deca- 10
+deka- deca
+hecto- 100
+kilo- 1e3
+mega- 1e6
+giga- 1e9
+tera- 1e12
+peta- 1e15
+exa- 1e18
+zetta- 1e21
+yotta- 1e24
+
+deci- 1e-1
+centi- 1e-2
+milli- 1e-3
+micro- 1e-6
+nano- 1e-9
+pico- 1e-12
+femto- 1e-15
+atto- 1e-18
+zepto- 1e-21
+yocto- 1e-24
+
+D- deca
+h- hecto
+H- hecto
+k- kilo
+K- kilo
+M- mega
+G- giga
+T- tera
+P- peta
+E- exa
+Z- zetta
+Y- yotta
+
+d- deci
+c- centi
+m- milli
+u- micro
+n- nano
+p- pico
+f- femto
+a- atto
+z- zepto
+y- yocto
+
+# Binary prefixes (i.e. metric but 1024 replaces 1000)
+
+kibi- 1024^1
+mebi- 1024^2
+gibi- 1024^3
+tebi- 1024^4
+pebi- 1024^5
+exbi- 1024^6
+Ki- kibi
+Mi- mebi
+Gi- gibi
+Ti- tebi
+Pi- pebi
+Ei- exbi
+
+# Derived SI units
+# Note: it is best to have these before any non-SI units
+
+newton kg m / s^2
+N newton
+pascal N / m^2
+Pa pascal
+joule N m
+J joule
+watt J/s
+W watt
+coulomb A s
+C coulomb
+volt W/A
+V volt
+ohm V/A
+siemens A/V
+S siemens
+farad C/V
+F farad
+weber V s
+Wb weber
+henry V s / A
+H henry
+tesla Wb / m^2
+T tesla
+hertz s^-1
+Hz hertz
+
+# Angle units and constants
+
+# Tau is the circle constant, equal to a circle's diameter divided by its radius
+tau 6.28318530717958
+# Another common circle constant
+pi tau / 2
+
+radian m / m
+rad radian
+steradian m^2 / m^2
+sr steradian
+degree 360 / tau * radian
+deg degree
+° degree
+
+# Nonlinear units, which are not supported by the file reader and must be defined manually
+# Use tempC(100) for 100 degrees Celsius
+
+tempCelsius !
+tempFahrenheit !
+tempC tempCelsius
+tempF tempFahrenheit
+
+# Common time units
+minute 60 second
+min minute
+hour 3600 second
+h hour
+day 86400 second
+d day
+julianyear 365.25 day
+gregorianyear 365.2425 day
+
+# Other non-SI "metric" units
+litre 0.001 m^3
+liter litre
+l litre
+L litre
+tonne 1000 kg
+t tonne
+are 100 m^2
+hectare hectoare
+arcminute 1 / 60 degree
+arcmin arcminute
+arcsecond 1 / 60 arcminute
+arcsec arcsecond
+
+# constants
+waterdensity 1 kilogram / litre
+
+# Imperial length units
+foot 0.3048 m
+ft foot
+inch 1 / 12 foot
+in inch
+yard 3 foot
+yd yard
+mile 1760 yard
+mi mile
+
+# Imperial weight units
+pound 0.45359237 kg
+lb pound
+ounce pound / 16
+oz ounce
+stone 14 lb
+UShundredweight 100 lb
+UKhundredweight 8 stone
+USimperialton 20 UShundredweight
+UKimperialton 10 UKhundredweight
+
+# Imperial volume units
+UKfluidounce ounce / waterdensity
+UKfloz UKfluidounce
+UKcup 10 UKfloz
+UKpint 2 UKcup
+UKquart 2 UKpint
+UKgallon 4 UKquart
+UKgal UKgallon
+
+USgallon 231 inch^3
+USgal USgallon
+USquart USgallon / 4
+USpint USquart / 2
+UScup USpint / 2
+USfluidounce UScup / 8
+USfloz USfluidounce
+UStablespoon USfluidounce / 2
+UStbsp UStablespoon
+USteaspoon UStablespoon / 3
+UStsp USteaspoon
+
+# Metric versions!
+# tsp = 5 mL, tbsp = 15 mL, floz = 30 mL, cup = 240 mL, pint = 480 mL, quart = 960 mL, gallon = 3840 mL
+# only metrictsp, metrictbsp and metriccup are common, the rest are derived from the US formulae with 240 mL cup
+metricteaspoon 5 mL
+teaspoon metricteaspoon
+tsp metricteaspoon
+metrictablespoon 3 metricteaspoon
+tablespoon metrictablespoon
+tbsp metrictablespoon
+metricfluidounce 2 metrictablespoon
+metriccup 8 metricfluidounce
+cup metriccup
+metricpint 2 metriccup
+pint metricpint
+metricquart 2 metricpint
+quart metricquart
+metricgallon 4 metricquart \ No newline at end of file