diff options
author | Adrien Hopkins <adrien.p.hopkins@gmail.com> | 2019-02-01 09:14:45 -0500 |
---|---|---|
committer | Adrien Hopkins <adrien.p.hopkins@gmail.com> | 2019-02-01 09:14:45 -0500 |
commit | 70273e127b061c69ce4b3d9d6c3881c6b0c2b829 (patch) | |
tree | b45241f4c56a055148f4a15a47d2d6ff04fd382a | |
parent | 1def718782e0a7c32a42b5ceb24cd6c16ce3d6e0 (diff) | |
parent | a3dfff2c1a3a906fa2c3cb3f4cec1a150c5bf795 (diff) |
Release v0.1.0v0.1.0
26 files changed, 3307 insertions, 4 deletions
diff --git a/CHANGELOG.org b/CHANGELOG.org new file mode 100644 index 0000000..1dbe268 --- /dev/null +++ b/CHANGELOG.org @@ -0,0 +1,11 @@ +* Changelog +All notable changes in this project will be shown in this file. + +** v0.1.0 +NOTE: At this stage, the API is subject to significant change. +*** Added + - Unit interface, implemented and supporting classes + - UnitPrefix interface, implemented and supporting classes + - UnitDimension and supporting classes + - UnitDatabase to store and parse units + - A GUI for unit conversion diff --git a/README.org b/README.org new file mode 100644 index 0000000..175db07 --- /dev/null +++ b/README.org @@ -0,0 +1,62 @@ +* What is it? + This is a unit converter, which allows you to convert between different units, and includes a GUI which can read unit data from a file (using some unit math) and convert between units that you type in, and has a unit and prefix viewer to check the units that have been loaded in. +* Features + - Convert between units and expressions of units + - linear or base unit can use unit prefixes (including non-metric units!) + - and prefixes are defined in an editable data file, in a simple and intuitive format. + - Viewer and Prefix Viewer which allow you to search through all of the available units and prefixes and learn details about them. + - All of SI included in default text file + - Choose your precision +* How to Convert +To convert units, simply enter the first unit in the From box and the second unit in the To box. Press Convert and a result will appear in the output box. Both boxes accept unit expressions, so you can input things like ‘1.7 m’, ‘85 km/h’ and ‘35 A * 14 s’ into either box. + +*Warning*: The space between the number and the unit name is required, or else the whole expression will be interpreted as a unit name. Spaces are not required for operators. + +Use the slider at the bottom to choose the maximum precision of the result, in significant digits, not decimal places. +* Units Files and Expressions +As mentioned previously, all units are loaded from a units file. The format for lines consists of a name for the unit, some tabs (not spaces), and an expression. The following operations are supported in expressions: + - Multiplication using spaces ( ) or asterisks (*) + - Division using the forward slash (/) + - Exponentiation using the caret (^) +Every argument can be either a linear unit, a base unit or a number, except exponents which must be integers. There is no way to use brackets to manipulate order of operations. + +Example (Explanation provided after # sign, this will *not* work in a real units file): + +inch 25.4 mm # Define ‘inch’ as equal to the product of 25.4 and ‘mm’ + +mph mile / hour # Define ‘mph’ as equal to the quotient of ‘mile’ and ‘hour’ + +litre 0.001 m^3 # Define ‘litre’ as equal to the product of 0.001 and the unit ‘m’ raised to the exponent 3 + +Lines that start with the number sign (#) and blank lines will be ignored. + +Unit prefixes are defined differently. When a unit name ends with the dash (-) character, it is interpreted as a prefix. Prefix expressions are not as powerful as unit expressions, they can only be: + - a number (1000) + - an exponent (10^3) + - the name of another prefix without the dash (kilo) +* Unit Prefixes +In SI, you can have a unit prefix, which you attach to the start of a unit and it multiplies the unit by a certain value. For example, milli-metre = 0.001 * metre. This unit converter supports unit prefixes, and you can put any number of prefixes before a linear or base unit to transform it (this includes non-SI units). +You can use more than one prefix at once (yottayottametre = really really long distance), but you may not convert prefixes alone. If you want to do that, use ‘unit’ or ‘u’ (ku = 1000000 mu). You can also use u for converting from numbers (pi = 3.141593 u). + +*Warning*: While the standard symbol for ‘deca’ is ‘da’, this program uses ‘D’ instead since ‘da’ can be confused with ‘deciatto’. Also, you may use capital letters for the symbols of ‘hecto’ and ‘kilo’. + +*Warning*: The standard prefixes will never use 1024 instead of 1000, even when operating on bits and bytes. Use ‘Ki’, ‘Mi’, ‘Gi’, ‘Ti’ and so on instead. +* Nonlinear Units +Sometimes, units cannot be converted from and to by simply multiplying and dividing. A common example of this is the Celsius and Fahrenheit temperature scales, which require multiplication and addition to convert to each other (and to their base, Kelvin). + +To use nonlinear units, use the following syntax: +FROM: unit1(value) +TO: unit2 + +Nonlinear units cannot: + - multiply, divide or exponentiate + - use prefixes + - be defined by unit files + +To define a nonlinear unit, make an anonymous inner type (or any other subclass) of AbstractUnit, and define the conversion methods. +* Unit and Prefix Viewers +The unit and prefix viewers can be used to see the available units (without prefixes) and prefixes. Upon opening them, you will see a list of units or prefixes on your left. Using the text box above, the list can be filtered. When a unit is clicked on, details about will be displayed on the right. +* Copyright and Licences +The Unit Converter program is Copyright (C) 2018, 2019 Adrien Hopkins. It is released under the terms of the AGPL v3 licence. + +This document is Copyright (C) 2019 Adrien Hopkins. It is released under the terms of the CC BY-SA (Creative Commons Attribution-ShareAlike) licence. diff --git a/src/unitConverter/UnitsDatabase.java b/src/unitConverter/UnitsDatabase.java new file mode 100755 index 0000000..4816db1 --- /dev/null +++ b/src/unitConverter/UnitsDatabase.java @@ -0,0 +1,584 @@ +/** + * 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 + * @since v0.1.0 + */ +public final class UnitsDatabase { + /** + * The units in this system. + * + * @since 2019-01-07 + * @since v0.1.0 + */ + private final Map<String, Unit> units; + + /** + * The unit prefixes in this system. + * + * @since 2019-01-14 + * @since v0.1.0 + */ + private final Map<String, UnitPrefix> prefixes; + + /** + * Creates the {@code UnitsDatabase}. + * + * @since 2019-01-10 + * @since v0.1.0 + */ + 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 + * @since v0.1.0 + */ + 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); + } 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 + * @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 using a custom name + * + * @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.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 + * @since v0.1.0 + */ + 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 + * @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) { + // 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 + * @since v0.1.0 + */ + public UnitPrefix getPrefix(final String name) { + 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."); + + 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 + * @since v0.1.0 + */ + 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 + * @since v0.1.0 + */ + 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>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 any argument is null + * @since 2019-01-07 + * @since v0.1.0 + */ + 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 + * @since v0.1.0 + */ + 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 + * @since v0.1.0 + */ + public Set<String> prefixNameSet() { + return Collections.unmodifiableSet(this.prefixes.keySet()); + } +} diff --git a/src/unitConverter/converterGUI/DelegateListModel.java b/src/unitConverter/converterGUI/DelegateListModel.java new file mode 100755 index 0000000..0e9b342 --- /dev/null +++ b/src/unitConverter/converterGUI/DelegateListModel.java @@ -0,0 +1,232 @@ +/** + * 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 + * @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 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/unitConverter/converterGUI/FilterComparator.java b/src/unitConverter/converterGUI/FilterComparator.java new file mode 100755 index 0000000..27ec3ab --- /dev/null +++ b/src/unitConverter/converterGUI/FilterComparator.java @@ -0,0 +1,93 @@ +/**
+ * 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;
+import java.util.Objects;
+
+/**
+ * A comparator that compares strings using a filter.
+ *
+ * @author Adrien Hopkins
+ * @since 2019-01-15
+ * @since v0.1.0
+ */
+public 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;
+
+ /**
+ * 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.
+ * @since 2019-01-15
+ * @since v0.1.0
+ * @throws NullPointerException
+ * if filter is null
+ */
+ public FilterComparator(final String filter, final Comparator<String> comparator) {
+ this.filter = Objects.requireNonNull(filter, "filter must not be null.");
+ 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..e036677 --- /dev/null +++ b/src/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 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/unitConverter/converterGUI/UnitConverterGUI.java b/src/unitConverter/converterGUI/UnitConverterGUI.java new file mode 100755 index 0000000..b54e0da --- /dev/null +++ b/src/unitConverter/converterGUI/UnitConverterGUI.java @@ -0,0 +1,671 @@ +/** + * 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 + * @since v0.1.0 + */ +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 + * @since v0.1.0 + */ + 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()); + } + + /** + * 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 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 + * @since v0.1.0 + */ + 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 + * @since v0.1.0 + */ + 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 + * @since v0.1.0 + */ + public final ListModel<String> keyListModel() { + return this.unitNamesFiltered; + } + + /** + * Runs whenever the prefix filter is changed. + * <p> + * Filters the prefix list then sorts it using a {@code FilterComparator}. + * </p> + * + * @since 2019-01-15 + * @since v0.1.0 + */ + 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 of the prefix names + * @since 2019-01-15 + */ + public final ListModel<String> prefixNameListModel() { + return this.prefixNamesFiltered; + } + + /** + * 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 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; + } + + /** + * Runs whenever the unit filter is changed. + * <p> + * Filters the unit list then sorts it using a {@code FilterComparator}. + * </p> + * + * @since 2019-01-15 + * @since v0.1.0 + */ + 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)); + } + + /** + * 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 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; + + /** The list of unit names in the unit viewer */ + private final JList<String> unitNameList; + /** The list of prefix names in the prefix viewer */ + private final JList<String> prefixNameList; + /** The unit search box in the unit viewer */ + private final JTextField unitFilterEntry; + /** The text box for unit data in the unit viewer */ + private final JTextArea unitTextBox; + /** The prefix search box in the prefix viewer */ + private final JTextField prefixFilterEntry; + /** The text box for prefix data in the prefix viewer */ + private final JTextArea prefixTextBox; + /** 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; + + /** + * 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 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); + + // create more components + this.initComponents(); + + this.frame.pack(); + } + + /** + * @return text in "From" box in converter panel + * @since 2019-01-15 + * @since v0.1.0 + */ + public String getFromText() { + return this.fromEntry.getText(); + } + + /** + * @return text in prefix filter + * @since 2019-01-15 + * @since v0.1.0 + */ + public String getPrefixFilterText() { + return this.prefixFilterEntry.getText(); + } + + /** + * @return index of selected prefix + * @since 2019-01-15 + * @since v0.1.0 + */ + public int getPrefixListSelection() { + return this.prefixNameList.getSelectedIndex(); + } + + /** + * @return text in "To" box in converter panel + * @since 2019-01-26 + * @since v0.1.0 + */ + 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 + * @since v0.1.0 + */ + public int getUnitListSelection() { + return this.unitNameList.getSelectedIndex(); + } + + /** + * 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); + + { // 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); + } + } + + { // panel for specifying precision + 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()); + + { // unit search box + 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)); + + { // panel for listing and seaching + final JPanel prefixListPanel = new JPanel(); + prefixLookupPanel.add(prefixListPanel); + + prefixListPanel.setLayout(new BorderLayout()); + + { // prefix search box + 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); + } + } + } + } + + /** + * 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 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 + * @since v0.1.0 + */ + 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 + * @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/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/dimension/BaseDimension.java b/src/unitConverter/dimension/BaseDimension.java index 6a727a9..0c09dce 100755 --- a/src/unitConverter/dimension/BaseDimension.java +++ b/src/unitConverter/dimension/BaseDimension.java @@ -21,17 +21,20 @@ package unitConverter.dimension; * * @author Adrien Hopkins * @since 2018-12-22 + * @since v0.1.0 */ public interface BaseDimension { /** * @return the dimension's name - * @since 2019-01-25 + * @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/unitConverter/dimension/OtherBaseDimension.java b/src/unitConverter/dimension/OtherBaseDimension.java index 2ab19b3..8c6d25d 100755 --- a/src/unitConverter/dimension/OtherBaseDimension.java +++ b/src/unitConverter/dimension/OtherBaseDimension.java @@ -23,6 +23,7 @@ import java.util.Objects; * * @author Adrien Hopkins * @since 2019-01-14 + * @since v0.1.0 */ public enum OtherBaseDimension implements BaseDimension { INFORMATION("Info"), CURRENCY("$$"); @@ -36,6 +37,7 @@ public enum OtherBaseDimension implements BaseDimension { * @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."); diff --git a/src/unitConverter/dimension/SIBaseDimension.java b/src/unitConverter/dimension/SIBaseDimension.java index b731b00..928d8d6 100755 --- a/src/unitConverter/dimension/SIBaseDimension.java +++ b/src/unitConverter/dimension/SIBaseDimension.java @@ -23,6 +23,7 @@ import java.util.Objects; *
* @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
@@ -37,6 +38,7 @@ public enum SIBaseDimension implements BaseDimension { * @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.");
diff --git a/src/unitConverter/dimension/StandardDimensions.java b/src/unitConverter/dimension/StandardDimensions.java index c830f00..b3edb7d 100755 --- a/src/unitConverter/dimension/StandardDimensions.java +++ b/src/unitConverter/dimension/StandardDimensions.java @@ -21,6 +21,7 @@ package unitConverter.dimension; *
* @author Adrien Hopkins
* @since 2018-12-11
+ * @since v0.1.0
*/
public final class StandardDimensions {
// base dimensions
diff --git a/src/unitConverter/dimension/UnitDimension.java b/src/unitConverter/dimension/UnitDimension.java index ba2a750..40e5bbc 100755 --- a/src/unitConverter/dimension/UnitDimension.java +++ b/src/unitConverter/dimension/UnitDimension.java @@ -29,12 +29,14 @@ import java.util.Set; *
* @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<>());
@@ -45,6 +47,7 @@ public final class UnitDimension { * 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<>();
@@ -56,6 +59,7 @@ public final class UnitDimension { * The base dimensions that make up this dimension.
*
* @since 2018-12-11
+ * @since v0.1.0
*/
final Map<BaseDimension, Integer> exponents;
@@ -65,6 +69,7 @@ public final class 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);
@@ -77,6 +82,7 @@ public final class UnitDimension { * 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);
@@ -119,6 +125,7 @@ public final class UnitDimension { /**
* @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<>();
@@ -140,6 +147,7 @@ public final class UnitDimension { * 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);
@@ -153,6 +161,7 @@ public final class UnitDimension { /**
* @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;
@@ -174,6 +183,7 @@ public final class UnitDimension { * 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);
@@ -196,6 +206,7 @@ public final class UnitDimension { * 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);
diff --git a/src/unitConverter/dimension/UnitDimensionTest.java b/src/unitConverter/dimension/UnitDimensionTest.java index 603320b..86db1b8 100755 --- a/src/unitConverter/dimension/UnitDimensionTest.java +++ b/src/unitConverter/dimension/UnitDimensionTest.java @@ -30,22 +30,43 @@ import static unitConverter.dimension.StandardDimensions.VOLUME; import org.junit.jupiter.api.Test; /** + * Tests for {@link UnitDimension}. + * * @author Adrien Hopkins * @since 2018-12-12 + * @since v0.1.0 */ class UnitDimensionTest { + /** + * Tests {@link UnitDimension#equals} + * + * @since 2018-12-12 + * @since v0.1.0 + */ @Test void testEquals() { assertEquals(LENGTH, LENGTH); assertFalse(LENGTH.equals(QUANTITY)); } + /** + * Tests {@code UnitDimension}'s exponentiation + * + * @since 2019-01-15 + * @since v0.1.0 + */ @Test void testExponents() { assertEquals(1, LENGTH.getExponent(SIBaseDimension.LENGTH)); assertEquals(3, VOLUME.getExponent(SIBaseDimension.LENGTH)); } + /** + * Tests {@code UnitDimension}'s multiplication and division. + * + * @since 2018-12-12 + * @since v0.1.0 + */ @Test void testMultiplicationAndDivision() { assertEquals(AREA, LENGTH.times(LENGTH)); diff --git a/src/unitConverter/unit/AbstractUnit.java b/src/unitConverter/unit/AbstractUnit.java new file mode 100644 index 0000000..24814e8 --- /dev/null +++ b/src/unitConverter/unit/AbstractUnit.java @@ -0,0 +1,172 @@ +/** + * 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 unitConverter.unit; + +import java.util.Objects; + +import 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 number of units created, including base units. + * + * @since 2019-01-02 + * @since v0.1.0 + */ + private static long unitCount = 0; + + /** + * The number of base units created. + * + * @since 2019-01-02 + * @since v0.1.0 + */ + private static long baseUnitCount = 0; + + /** + * @return number of base units created + * @since 2019-01-02 + * @since v0.1.0 + */ + public static final long getBaseUnitCount() { + return baseUnitCount; + } + + /** + * @return number of units created + * @since 2019-01-02 + * @since v0.1.0 + */ + public static final long getUnitCount() { + return unitCount; + } + + /** + * Increments the number of base units. + * + * @since 2019-01-15 + * @since v0.1.0 + */ + public static final void incrementBaseUnitCounter() { + baseUnitCount++; + } + + /** + * Increments the number of units. + * + * @since 2019-01-15 + * @since v0.1.0 + */ + public static final void incrementUnitCounter() { + unitCount++; + } + + /** + * 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; + } + + // TODO document and revise units' toString methods + @Override + public String toString() { + return String.format("%s-derived unit of dimension %s", this.getSystem(), this.getDimension()); + } +} diff --git a/src/unitConverter/unit/BaseUnit.java b/src/unitConverter/unit/BaseUnit.java new file mode 100755 index 0000000..fe36c45 --- /dev/null +++ b/src/unitConverter/unit/BaseUnit.java @@ -0,0 +1,180 @@ +/** + * 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; + +import 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. + * + * @author Adrien Hopkins + * @since 2018-12-23 + * @since v0.1.0 + */ +public final class BaseUnit extends AbstractUnit { + /** + * 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); + this.isFullBase = dimension.isBase(); + } + + /** + * @return this unit as a {@code LinearUnit} + * @since 2019-01-25 + * @since v0.1.0 + */ + public LinearUnit asLinearUnit() { + return this.times(1); + } + + @Override + public double convertFromBase(final double value) { + return value; + } + + @Override + public double convertToBase(final double value) { + return value; + } + + /** + * Divides this unit by another unit. + * + * @param other + * unit to divide by + * @return quotient of two units + * @throws IllegalArgumentException + * if this unit's system is not other's system + * @throws NullPointerException + * if other is null + * @since 2018-12-22 + * @since v0.1.0 + */ + public BaseUnit dividedBy(final BaseUnit other) { + Objects.requireNonNull(other, "other must not be null."); + if (!this.getSystem().equals(other.getSystem())) + throw new IllegalArgumentException("Incompatible base units for division."); + return new BaseUnit(this.getDimension().dividedBy(other.getDimension()), this.getSystem()); + } + + /** + * Divides this unit by a divisor + * + * @param divisor + * amount to divide by + * @return quotient + * @since 2018-12-23 + * @since v0.1.0 + */ + public LinearUnit dividedBy(final double divisor) { + return new LinearUnit(this, 1 / divisor); + } + + @Override + public boolean equals(final Object obj) { + if (!(obj instanceof BaseUnit)) + return false; + final BaseUnit other = (BaseUnit) obj; + return Objects.equals(this.getSystem(), other.getSystem()) + && Objects.equals(this.getDimension(), other.getDimension()); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = result * prime + this.getSystem().hashCode(); + result = result * prime + this.getDimension().hashCode(); + return result; + } + + /** + * Multiplies this unit by another unit. + * + * @param other + * unit to multiply by + * @return product of two units + * @throws IllegalArgumentException + * if this unit's system is not other's system + * @throws NullPointerException + * if other is null + * @since 2018-12-22 + * @since v0.1.0 + */ + public BaseUnit times(final BaseUnit other) { + Objects.requireNonNull(other, "other must not be null."); + if (!this.getSystem().equals(other.getSystem())) + throw new IllegalArgumentException("Incompatible base units for multiplication."); + return new BaseUnit(this.getDimension().times(other.getDimension()), this.getSystem()); + } + + /** + * Multiplies this unit by a multiplier. + * + * @param multiplier + * amount to multiply by + * @return product + * @since 2018-12-23 + * @since v0.1.0 + */ + public LinearUnit times(final double multiplier) { + return new LinearUnit(this, multiplier); + } + + /** + * Returns this unit, but to an exponent. + * + * @param exponent + * exponent + * @return result of exponentiation + * @since 2019-01-15 + * @since v0.1.0 + */ + 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/unitConverter/unit/DefaultUnitPrefix.java b/src/unitConverter/unit/DefaultUnitPrefix.java new file mode 100755 index 0000000..d19161b --- /dev/null +++ b/src/unitConverter/unit/DefaultUnitPrefix.java @@ -0,0 +1,67 @@ +/** + * 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; + +/** + * 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 + */ + 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/LinearUnit.java b/src/unitConverter/unit/LinearUnit.java new file mode 100644 index 0000000..b786b3b --- /dev/null +++ b/src/unitConverter/unit/LinearUnit.java @@ -0,0 +1,184 @@ +/** + * 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 unitConverter.unit; + +import java.util.Objects; + +import unitConverter.dimension.UnitDimension; + +/** + * A unit that is equal to a certain number multiplied by its base. + * + * @author Adrien Hopkins + * @since 2018-12-22 + * @since v0.1.0 + */ +public final 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); + } + + /** + * Divides this unit by another unit. + * + * @param other + * unit to divide by + * @return quotient of two units + * @throws NullPointerException + * if other is null + * @since 2018-12-22 + * @since v0.1.0 + */ + public LinearUnit dividedBy(final LinearUnit other) { + Objects.requireNonNull(other, "other must not be null"); + final BaseUnit base = this.getBase().dividedBy(other.getBase()); + return new LinearUnit(base, this.getConversionFactor() / other.getConversionFactor()); + } + + @Override + public boolean equals(final Object obj) { + if (!(obj instanceof LinearUnit)) + return false; + final LinearUnit other = (LinearUnit) obj; + return Objects.equals(this.getBase(), other.getBase()) + && Objects.equals(this.getConversionFactor(), other.getConversionFactor()); + } + + /** + * @return conversionFactor + * @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.getBase().hashCode(); + result = result * prime + Double.hashCode(this.getConversionFactor()); + return result; + } + + /** + * 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); + } + + /** + * Multiplies this unit by another unit. + * + * @param other + * unit to multiply by= + * @return product of two units + * @throws NullPointerException + * if other is null + * @since 2018-12-22 + * @since v0.1.0 + */ + public LinearUnit times(final LinearUnit other) { + Objects.requireNonNull(other, "other must not be null"); + final BaseUnit base = this.getBase().times(other.getBase()); + return new LinearUnit(base, this.getConversionFactor() * other.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()); + } +} diff --git a/src/unitConverter/unit/NonlinearUnits.java b/src/unitConverter/unit/NonlinearUnits.java new file mode 100755 index 0000000..ec1874c --- /dev/null +++ b/src/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 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/unitConverter/unit/SI.java b/src/unitConverter/unit/SI.java new file mode 100644 index 0000000..54eb4c5 --- /dev/null +++ b/src/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 unitConverter.unit; + +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; + +import unitConverter.dimension.StandardDimensions; +import 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/unitConverter/unit/SIPrefix.java b/src/unitConverter/unit/SIPrefix.java new file mode 100755 index 0000000..54625fb --- /dev/null +++ b/src/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 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/unitConverter/unit/Unit.java b/src/unitConverter/unit/Unit.java index 3e7f9da..54f1423 100755 --- a/src/unitConverter/unit/Unit.java +++ b/src/unitConverter/unit/Unit.java @@ -25,6 +25,7 @@ import unitConverter.dimension.UnitDimension; * * @author Adrien Hopkins * @since 2018-12-22 + * @since v0.1.0 */ public interface Unit { /** @@ -34,6 +35,7 @@ public interface Unit { * 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()); @@ -53,6 +55,7 @@ public interface Unit { * 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); @@ -70,6 +73,7 @@ public interface Unit { * 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); @@ -86,18 +90,21 @@ public interface Unit { * * @return base unit associated with this unit * @since 2018-12-22 + * @since v0.1.0 */ - Unit getBase(); + 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/unitConverter/unit/UnitPrefix.java b/src/unitConverter/unit/UnitPrefix.java new file mode 100755 index 0000000..a0c1b7c --- /dev/null +++ b/src/unitConverter/unit/UnitPrefix.java @@ -0,0 +1,33 @@ +/** + * 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; + +/** + * 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 { + /** + * @return this prefix's multiplier + * @since 2019-01-14 + * @since v0.1.0 + */ + double getMultiplier(); +} diff --git a/src/unitConverter/unit/UnitSystem.java b/src/unitConverter/unit/UnitSystem.java index 2e3a5d8..ce8c249 100755 --- a/src/unitConverter/unit/UnitSystem.java +++ b/src/unitConverter/unit/UnitSystem.java @@ -16,16 +16,38 @@ */ package unitConverter.unit; +import java.util.Objects; + +import 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 { /** - * @return name of system + * 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/unitConverter/unit/UnitTest.java b/src/unitConverter/unit/UnitTest.java new file mode 100755 index 0000000..c3237eb --- /dev/null +++ b/src/unitConverter/unit/UnitTest.java @@ -0,0 +1,47 @@ +/** + * 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 static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +import unitConverter.dimension.StandardDimensions; + +/** + * Testing the various Unit classes + * + * @author Adrien Hopkins + * @since 2018-12-22 + */ +class UnitTest { + @Test + void testConversion() { + final BaseUnit metre = SI.METRE; + final Unit inch = metre.times(0.0254); + + assertEquals(1.9, inch.convertToBase(75), 0.01); + } + + @Test + void testEquals() { + final BaseUnit metre = SI.METRE; + final Unit meter = SI.SI.getBaseUnit(StandardDimensions.LENGTH); + + assertEquals(metre, meter); + } +} 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 |