From 2eee97c9e64dca79fc6b1614b304b398d25a7f4b Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Sat, 27 Mar 2021 16:36:39 -0500 Subject: Added automatic building with Gradle --- .../converterGUI/DefaultPrefixRepetitionRule.java | 95 + .../converterGUI/DelegateListModel.java | 242 +++ .../converterGUI/FilterComparator.java | 129 ++ .../unitConverter/converterGUI/GridBagBuilder.java | 479 +++++ .../converterGUI/MutablePredicate.java | 70 + .../unitConverter/converterGUI/SearchBoxList.java | 307 +++ .../converterGUI/UnitConverterGUI.java | 1426 ++++++++++++++ .../unitConverter/converterGUI/package-info.java | 24 + .../math/ConditionalExistenceCollections.java | 468 +++++ .../org/unitConverter/math/DecimalComparison.java | 256 +++ .../org/unitConverter/math/ExpressionParser.java | 708 +++++++ .../java/org/unitConverter/math/ObjectProduct.java | 284 +++ .../org/unitConverter/math/UncertainDouble.java | 419 ++++ .../java/org/unitConverter/math/package-info.java | 24 + src/main/java/org/unitConverter/package-info.java | 24 + .../java/org/unitConverter/unit/BaseDimension.java | 87 + src/main/java/org/unitConverter/unit/BaseUnit.java | 133 ++ .../org/unitConverter/unit/BritishImperial.java | 116 ++ .../org/unitConverter/unit/FunctionalUnit.java | 109 ++ .../org/unitConverter/unit/FunctionalUnitlike.java | 72 + .../java/org/unitConverter/unit/LinearUnit.java | 441 +++++ .../org/unitConverter/unit/LinearUnitValue.java | 341 ++++ .../java/org/unitConverter/unit/MultiUnit.java | 160 ++ .../java/org/unitConverter/unit/NameSymbol.java | 280 +++ src/main/java/org/unitConverter/unit/Nameable.java | 59 + src/main/java/org/unitConverter/unit/SI.java | 479 +++++ .../java/org/unitConverter/unit/USCustomary.java | 135 ++ src/main/java/org/unitConverter/unit/Unit.java | 377 ++++ .../java/org/unitConverter/unit/UnitDatabase.java | 1991 ++++++++++++++++++++ .../java/org/unitConverter/unit/UnitPrefix.java | 242 +++ .../java/org/unitConverter/unit/UnitValue.java | 172 ++ src/main/java/org/unitConverter/unit/Unitlike.java | 260 +++ .../java/org/unitConverter/unit/UnitlikeValue.java | 174 ++ .../java/org/unitConverter/unit/package-info.java | 24 + src/main/resources/about.txt | 12 + 35 files changed, 10619 insertions(+) create mode 100644 src/main/java/org/unitConverter/converterGUI/DefaultPrefixRepetitionRule.java create mode 100644 src/main/java/org/unitConverter/converterGUI/DelegateListModel.java create mode 100644 src/main/java/org/unitConverter/converterGUI/FilterComparator.java create mode 100644 src/main/java/org/unitConverter/converterGUI/GridBagBuilder.java create mode 100644 src/main/java/org/unitConverter/converterGUI/MutablePredicate.java create mode 100644 src/main/java/org/unitConverter/converterGUI/SearchBoxList.java create mode 100644 src/main/java/org/unitConverter/converterGUI/UnitConverterGUI.java create mode 100644 src/main/java/org/unitConverter/converterGUI/package-info.java create mode 100644 src/main/java/org/unitConverter/math/ConditionalExistenceCollections.java create mode 100644 src/main/java/org/unitConverter/math/DecimalComparison.java create mode 100644 src/main/java/org/unitConverter/math/ExpressionParser.java create mode 100644 src/main/java/org/unitConverter/math/ObjectProduct.java create mode 100644 src/main/java/org/unitConverter/math/UncertainDouble.java create mode 100644 src/main/java/org/unitConverter/math/package-info.java create mode 100644 src/main/java/org/unitConverter/package-info.java create mode 100644 src/main/java/org/unitConverter/unit/BaseDimension.java create mode 100644 src/main/java/org/unitConverter/unit/BaseUnit.java create mode 100644 src/main/java/org/unitConverter/unit/BritishImperial.java create mode 100644 src/main/java/org/unitConverter/unit/FunctionalUnit.java create mode 100644 src/main/java/org/unitConverter/unit/FunctionalUnitlike.java create mode 100644 src/main/java/org/unitConverter/unit/LinearUnit.java create mode 100644 src/main/java/org/unitConverter/unit/LinearUnitValue.java create mode 100644 src/main/java/org/unitConverter/unit/MultiUnit.java create mode 100644 src/main/java/org/unitConverter/unit/NameSymbol.java create mode 100644 src/main/java/org/unitConverter/unit/Nameable.java create mode 100644 src/main/java/org/unitConverter/unit/SI.java create mode 100644 src/main/java/org/unitConverter/unit/USCustomary.java create mode 100644 src/main/java/org/unitConverter/unit/Unit.java create mode 100644 src/main/java/org/unitConverter/unit/UnitDatabase.java create mode 100644 src/main/java/org/unitConverter/unit/UnitPrefix.java create mode 100644 src/main/java/org/unitConverter/unit/UnitValue.java create mode 100644 src/main/java/org/unitConverter/unit/Unitlike.java create mode 100644 src/main/java/org/unitConverter/unit/UnitlikeValue.java create mode 100644 src/main/java/org/unitConverter/unit/package-info.java create mode 100644 src/main/resources/about.txt (limited to 'src/main') diff --git a/src/main/java/org/unitConverter/converterGUI/DefaultPrefixRepetitionRule.java b/src/main/java/org/unitConverter/converterGUI/DefaultPrefixRepetitionRule.java new file mode 100644 index 0000000..bdc3a2e --- /dev/null +++ b/src/main/java/org/unitConverter/converterGUI/DefaultPrefixRepetitionRule.java @@ -0,0 +1,95 @@ +/** + * @since 2020-08-26 + */ +package org.unitConverter.converterGUI; + +import java.util.List; +import java.util.function.Predicate; + +import org.unitConverter.unit.SI; +import org.unitConverter.unit.UnitPrefix; + +/** + * A rule that specifies whether prefix repetition is allowed + * + * @since 2020-08-26 + */ +enum DefaultPrefixRepetitionRule implements Predicate> { + NO_REPETITION { + @Override + public boolean test(List prefixes) { + return prefixes.size() <= 1; + } + }, + NO_RESTRICTION { + @Override + public boolean test(List prefixes) { + return true; + } + }, + /** + * You are allowed to have any number of Yotta/Yocto followed by possibly one + * Kilo-Zetta/Milli-Zepto followed by possibly one Deca/Hecto. Same for + * reducing prefixes, don't mix magnifying and reducing. Non-metric + * (including binary) prefixes can't be repeated. + */ + COMPLEX_REPETITION { + @Override + public boolean test(List prefixes) { + // determine whether we are magnifying or reducing + final boolean magnifying; + if (prefixes.isEmpty()) + return true; + else if (prefixes.get(0).getMultiplier() > 1) { + magnifying = true; + } else { + magnifying = false; + } + + // if the first prefix is non-metric (including binary prefixes), + // assume we are using non-metric prefixes + // non-metric prefixes are allowed, but can't be repeated. + if (!SI.DECIMAL_PREFIXES.contains(prefixes.get(0))) + return NO_REPETITION.test(prefixes); + + int part = 0; // 0=yotta/yoctos, 1=kilo-zetta/milli-zepto, + // 2=deka,hecto,deci,centi + + for (final UnitPrefix prefix : prefixes) { + // check that the current prefix is metric and appropriately + // magnifying/reducing + if (!SI.DECIMAL_PREFIXES.contains(prefix)) + return false; + if (magnifying != prefix.getMultiplier() > 1) + return false; + + // check if the current prefix is correct + // since part is set *after* this check, part designates the state + // of the *previous* prefix + switch (part) { + case 0: + // do nothing, any prefix is valid after a yotta + break; + case 1: + // after a kilo-zetta, only deka/hecto are valid + if (SI.THOUSAND_PREFIXES.contains(prefix)) + return false; + break; + case 2: + // deka/hecto must be the last prefix, so this is always invalid + return false; + } + + // set part + if (SI.YOTTA.equals(prefix) || SI.YOCTO.equals(prefix)) { + part = 0; + } else if (SI.THOUSAND_PREFIXES.contains(prefix)) { + part = 1; + } else { + part = 2; + } + } + return true; + } + }; +} diff --git a/src/main/java/org/unitConverter/converterGUI/DelegateListModel.java b/src/main/java/org/unitConverter/converterGUI/DelegateListModel.java new file mode 100644 index 0000000..b80f63d --- /dev/null +++ b/src/main/java/org/unitConverter/converterGUI/DelegateListModel.java @@ -0,0 +1,242 @@ +/** + * Copyright (C) 2018 Adrien Hopkins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.unitConverter.converterGUI; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; + +import javax.swing.AbstractListModel; + +/** + * A list model that delegates to a list. + *

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

+ * + * @author Adrien Hopkins + * @since 2019-01-14 + * @since v0.1.0 + */ +final class DelegateListModel extends AbstractListModel implements List { + /** + * @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 delegate; + + /** + * Creates an empty {@code DelegateListModel}. + * + * @since 2019-04-13 + */ + public DelegateListModel() { + this(new ArrayList<>()); + } + + /** + * Creates the {@code DelegateListModel}. + * + * @param delegate + * list to delegate + * @since 2019-01-14 + * @since v0.1.0 + */ + public DelegateListModel(final List 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 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 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 iterator() { + return this.delegate.iterator(); + } + + @Override + public int lastIndexOf(final Object elem) { + return this.delegate.lastIndexOf(elem); + } + + @Override + public ListIterator listIterator() { + return this.delegate.listIterator(); + } + + @Override + public ListIterator 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 subList(final int fromIndex, final int toIndex) { + return this.delegate.subList(fromIndex, toIndex); + } + + @Override + public Object[] toArray() { + return this.delegate.toArray(); + } + + @Override + public T[] toArray(final T[] a) { + return this.delegate.toArray(a); + } + + @Override + public String toString() { + return this.delegate.toString(); + } +} diff --git a/src/main/java/org/unitConverter/converterGUI/FilterComparator.java b/src/main/java/org/unitConverter/converterGUI/FilterComparator.java new file mode 100644 index 0000000..9b77f21 --- /dev/null +++ b/src/main/java/org/unitConverter/converterGUI/FilterComparator.java @@ -0,0 +1,129 @@ +/** + * Copyright (C) 2018 Adrien Hopkins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.unitConverter.converterGUI; + +import java.util.Comparator; +import java.util.Objects; + +/** + * A comparator that compares strings using a filter. + * + * @author Adrien Hopkins + * @since 2019-01-15 + * @since v0.1.0 + */ +final class FilterComparator implements Comparator { + /** + * 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 comparator; + /** + * Whether or not the comparison is case-sensitive. + * + * @since 2019-04-14 + * @since v0.2.0 + */ + private final boolean caseSensitive; + + /** + * Creates the {@code FilterComparator}. + * + * @param filter + * @since 2019-01-15 + * @since v0.1.0 + */ + public FilterComparator(final String filter) { + this(filter, null); + } + + /** + * Creates the {@code FilterComparator}. + * + * @param filter + * string to filter by + * @param comparator + * comparator to fall back to if all else fails, null is compareTo. + * @throws NullPointerException + * if filter is null + * @since 2019-01-15 + * @since v0.1.0 + */ + public FilterComparator(final String filter, final Comparator comparator) { + this(filter, comparator, false); + } + + /** + * Creates the {@code FilterComparator}. + * + * @param filter + * string to filter by + * @param comparator + * comparator to fall back to if all else fails, null is compareTo. + * @param caseSensitive + * whether or not the comparator is case-sensitive + * @throws NullPointerException + * if filter is null + * @since 2019-04-14 + * @since v0.2.0 + */ + public FilterComparator(final String filter, final Comparator comparator, final boolean caseSensitive) { + this.filter = Objects.requireNonNull(filter, "filter must not be null."); + this.comparator = comparator; + this.caseSensitive = caseSensitive; + } + + @Override + public int compare(final String arg0, final String arg1) { + // if this is case insensitive, make them lowercase + final String str0, str1; + if (this.caseSensitive) { + str0 = arg0; + str1 = arg1; + } else { + str0 = arg0.toLowerCase(); + str1 = arg1.toLowerCase(); + } + + // elements that start with the filter always go first + if (str0.startsWith(this.filter) && !str1.startsWith(this.filter)) + return -1; + else if (!str0.startsWith(this.filter) && str1.startsWith(this.filter)) + return 1; + + // elements that contain the filter but don't start with them go next + if (str0.contains(this.filter) && !str1.contains(this.filter)) + return -1; + else if (!str0.contains(this.filter) && !str1.contains(this.filter)) + return 1; + + // other elements go last + if (this.comparator == null) + return str0.compareTo(str1); + else + return this.comparator.compare(str0, str1); + } +} diff --git a/src/main/java/org/unitConverter/converterGUI/GridBagBuilder.java b/src/main/java/org/unitConverter/converterGUI/GridBagBuilder.java new file mode 100644 index 0000000..f1229b2 --- /dev/null +++ b/src/main/java/org/unitConverter/converterGUI/GridBagBuilder.java @@ -0,0 +1,479 @@ +/** + * Copyright (C) 2018 Adrien Hopkins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.unitConverter.converterGUI; + +import java.awt.GridBagConstraints; +import java.awt.Insets; + +/** + * A builder for Java's {@link java.awt.GridBagConstraints} class. + * + * @author Adrien Hopkins + * @since 2018-11-30 + * @since v0.1.0 + */ +final class GridBagBuilder { + /** + * The built {@code GridBagConstraints}'s {@code gridx} property. + *

+ * Specifies the cell containing the leading edge of the component's display area, where the first cell in a row has + * gridx=0. 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 + * RELATIVE specifies that the component be placed immediately following the component that was added + * to the container just before this component was added. + *

+ * The default value is RELATIVE. gridx 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. + *

+ * Specifies the cell at the top of the component's display area, where the topmost cell has gridy=0. + * The value RELATIVE specifies that the component be placed just below the component that was added to + * the container just before this component was added. + *

+ * The default value is RELATIVE. gridy 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. + *

+ * Specifies the number of cells in a row for the component's display area. + *

+ * Use REMAINDER to specify that the component's display area will be from gridx to the + * last cell in the row. Use RELATIVE to specify that the component's display area will be from + * gridx to the next to the last one in its row. + *

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

+ * Specifies the number of cells in a column for the component's display area. + *

+ * Use REMAINDER to specify that the component's display area will be from gridy to the + * last cell in the column. Use RELATIVE to specify that the component's display area will be from + * gridy to the next to the last one in its column. + *

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

+ * Specifies how to distribute extra horizontal space. + *

+ * The grid bag layout manager calculates the weight of a column to be the maximum weightx 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. + *

+ * If all the weights are zero, all the extra space appears between the grids of the cell and the left and right + * edges. + *

+ * The default value of this field is 0. weightx 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. + *

+ * Specifies how to distribute extra vertical space. + *

+ * The grid bag layout manager calculates the weight of a row to be the maximum weighty 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. + *

+ * If all the weights are zero, all the extra space appears between the grids of the cell and the top and bottom + * edges. + *

+ * The default value of this field is 0. weighty 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. + *

+ * This field is used when the component is smaller than its display area. It determines where, within the display + * area, to place the component. + *

+ * 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: + * CENTER, NORTH, NORTHEAST, EAST, SOUTHEAST, + * SOUTH, SOUTHWEST, WEST, and NORTHWEST. The orientation + * relative values are: PAGE_START, PAGE_END, LINE_START, + * LINE_END, FIRST_LINE_START, FIRST_LINE_END, LAST_LINE_START + * and LAST_LINE_END. The baseline relative values are: BASELINE, + * BASELINE_LEADING, BASELINE_TRAILING, ABOVE_BASELINE, + * ABOVE_BASELINE_LEADING, ABOVE_BASELINE_TRAILING, BELOW_BASELINE, + * BELOW_BASELINE_LEADING, and BELOW_BASELINE_TRAILING. The default value is + * CENTER. + * + * @serial + * @see #clone() + * @see java.awt.ComponentOrientation + */ + private int anchor; + + /** + * The built {@code GridBagConstraints}'s {@code fill} property. + *

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

+ * The following values are valid for fill: + * + *

    + *
  • NONE: Do not resize the component. + *
  • HORIZONTAL: Make the component wide enough to fill its display area horizontally, but do not + * change its height. + *
  • VERTICAL: Make the component tall enough to fill its display area vertically, but do not change + * its width. + *
  • BOTH: Make the component fill its display area entirely. + *
+ *

+ * The default value is NONE. + * + * @serial + * @see #clone() + */ + private int fill; + + /** + * The built {@code GridBagConstraints}'s {@code insets} property. + *

+ * This field specifies the external padding of the component, the minimum amount of space between the component and + * the edges of its display area. + *

+ * The default value is new Insets(0, 0, 0, 0). + * + * @serial + * @see #clone() + */ + private Insets insets; + + /** + * The built {@code GridBagConstraints}'s {@code ipadx} property. + *

+ * 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 ipadx pixels. + *

+ * The default value is 0. + * + * @serial + * @see #clone() + * @see java.awt.GridBagConstraints#ipady + */ + private int ipadx; + + /** + * The built {@code GridBagConstraints}'s {@code ipady} property. + *

+ * 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 ipady pixels. + *

+ * 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/main/java/org/unitConverter/converterGUI/MutablePredicate.java b/src/main/java/org/unitConverter/converterGUI/MutablePredicate.java new file mode 100644 index 0000000..e15b3cd --- /dev/null +++ b/src/main/java/org/unitConverter/converterGUI/MutablePredicate.java @@ -0,0 +1,70 @@ +/** + * Copyright (C) 2019 Adrien Hopkins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.unitConverter.converterGUI; + +import java.util.function.Predicate; + +/** + * A container for a predicate, which can be changed later. + * + * @author Adrien Hopkins + * @since 2019-04-13 + * @since v0.2.0 + */ +final class MutablePredicate implements Predicate { + /** + * The predicate stored in this {@code MutablePredicate} + * + * @since 2019-04-13 + * @since v0.2.0 + */ + private Predicate predicate; + + /** + * Creates the {@code MutablePredicate}. + * + * @since 2019-04-13 + * @since v0.2.0 + */ + public MutablePredicate(final Predicate predicate) { + this.predicate = predicate; + } + + /** + * @return predicate + * @since 2019-04-13 + * @since v0.2.0 + */ + public final Predicate getPredicate() { + return this.predicate; + } + + /** + * @param predicate + * new value of predicate + * @since 2019-04-13 + * @since v0.2.0 + */ + public final void setPredicate(final Predicate predicate) { + this.predicate = predicate; + } + + @Override + public boolean test(final T t) { + return this.predicate.test(t); + } +} diff --git a/src/main/java/org/unitConverter/converterGUI/SearchBoxList.java b/src/main/java/org/unitConverter/converterGUI/SearchBoxList.java new file mode 100644 index 0000000..10ef589 --- /dev/null +++ b/src/main/java/org/unitConverter/converterGUI/SearchBoxList.java @@ -0,0 +1,307 @@ +/** + * 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 . + */ +package org.unitConverter.converterGUI; + +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.event.FocusEvent; +import java.awt.event.FocusListener; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.function.Predicate; + +import javax.swing.JList; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTextField; + +/** + * @author Adrien Hopkins + * @since 2019-04-13 + * @since v0.2.0 + */ +final class SearchBoxList extends JPanel { + + /** + * @since 2019-04-13 + * @since v0.2.0 + */ + private static final long serialVersionUID = 6226930279415983433L; + + /** + * The text to place in an empty search box. + * + * @since 2019-04-13 + * @since v0.2.0 + */ + private static final String EMPTY_TEXT = "Search..."; + + /** + * The color to use for an empty foreground. + * + * @since 2019-04-13 + * @since v0.2.0 + */ + private static final Color EMPTY_FOREGROUND = new Color(192, 192, 192); + + // the components + private final Collection itemsToFilter; + private final DelegateListModel listModel; + private final JTextField searchBox; + private final JList searchItems; + + private boolean searchBoxEmpty = true; + + // I need to do this because, for some reason, Swing is auto-focusing my + // search box without triggering a focus + // event. + private boolean searchBoxFocused = false; + + private Predicate customSearchFilter = o -> true; + private final Comparator defaultOrdering; + private final boolean caseSensitive; + + /** + * Creates the {@code SearchBoxList}. + * + * @param itemsToFilter items to put in the list + * @since 2019-04-14 + */ + public SearchBoxList(final Collection itemsToFilter) { + this(itemsToFilter, null, false); + } + + /** + * Creates the {@code SearchBoxList}. + * + * @param itemsToFilter items to put in the list + * @param defaultOrdering default ordering of items after filtration + * (null=Comparable) + * @param caseSensitive whether or not the filtration is case-sensitive + * + * @since 2019-04-13 + * @since v0.2.0 + */ + public SearchBoxList(final Collection itemsToFilter, + final Comparator defaultOrdering, + final boolean caseSensitive) { + super(new BorderLayout(), true); + this.itemsToFilter = itemsToFilter; + this.defaultOrdering = defaultOrdering; + this.caseSensitive = caseSensitive; + + // create the components + this.listModel = new DelegateListModel<>(new ArrayList<>(itemsToFilter)); + this.searchItems = new JList<>(this.listModel); + + this.searchBox = new JTextField(EMPTY_TEXT); + this.searchBox.setForeground(EMPTY_FOREGROUND); + + // add them to the panel + this.add(this.searchBox, BorderLayout.PAGE_START); + this.add(new JScrollPane(this.searchItems), BorderLayout.CENTER); + + // set up the search box + this.searchBox.addFocusListener(new FocusListener() { + @Override + public void focusGained(final FocusEvent e) { + SearchBoxList.this.searchBoxFocusGained(e); + } + + @Override + public void focusLost(final FocusEvent e) { + SearchBoxList.this.searchBoxFocusLost(e); + } + }); + + this.searchBox.addCaretListener(e -> this.searchBoxTextChanged()); + this.searchBoxEmpty = true; + } + + /** + * Adds an additional filter for searching. + * + * @param filter filter to add. + * @since 2019-04-13 + * @since v0.2.0 + */ + public void addSearchFilter(final Predicate filter) { + this.customSearchFilter = this.customSearchFilter.and(filter); + } + + /** + * Resets the search filter. + * + * @since 2019-04-13 + * @since v0.2.0 + */ + public void clearSearchFilters() { + this.customSearchFilter = o -> true; + } + + /** + * @return this component's search box component + * @since 2019-04-14 + * @since v0.2.0 + */ + public final JTextField getSearchBox() { + return this.searchBox; + } + + /** + * @param searchText text to search for + * @return a filter that filters out that text, based on this list's case + * sensitive setting + * @since 2019-04-14 + * @since v0.2.0 + */ + private Predicate getSearchFilter(final String searchText) { + if (this.caseSensitive) + return string -> string.contains(searchText); + else + return string -> string.toLowerCase() + .contains(searchText.toLowerCase()); + } + + /** + * @return this component's list component + * @since 2019-04-14 + * @since v0.2.0 + */ + public final JList getSearchList() { + return this.searchItems; + } + + /** + * @return index selected in item list + * @since 2019-04-14 + * @since v0.2.0 + */ + public int getSelectedIndex() { + return this.searchItems.getSelectedIndex(); + } + + /** + * @return value selected in item list + * @since 2019-04-13 + * @since v0.2.0 + */ + public String getSelectedValue() { + return this.searchItems.getSelectedValue(); + } + + /** + * Re-applies the filters. + * + * @since 2019-04-13 + * @since v0.2.0 + */ + public void reapplyFilter() { + final String searchText = this.searchBoxEmpty ? "" + : this.searchBox.getText(); + final FilterComparator comparator = new FilterComparator(searchText, + this.defaultOrdering, this.caseSensitive); + final Predicate searchFilter = this.getSearchFilter(searchText); + + this.listModel.clear(); + this.itemsToFilter.forEach(string -> { + if (searchFilter.test(string)) { + this.listModel.add(string); + } + }); + + // applies the custom filters + this.listModel.removeIf(this.customSearchFilter.negate()); + + // sorts the remaining items + this.listModel.sort(comparator); + } + + /** + * Runs whenever the search box gains focus. + * + * @param e focus event + * @since 2019-04-13 + * @since v0.2.0 + */ + private void searchBoxFocusGained(final FocusEvent e) { + this.searchBoxFocused = true; + if (this.searchBoxEmpty) { + this.searchBox.setText(""); + this.searchBox.setForeground(Color.BLACK); + } + } + + /** + * Runs whenever the search box loses focus. + * + * @param e focus event + * @since 2019-04-13 + * @since v0.2.0 + */ + private void searchBoxFocusLost(final FocusEvent e) { + this.searchBoxFocused = false; + if (this.searchBoxEmpty) { + this.searchBox.setText(EMPTY_TEXT); + this.searchBox.setForeground(EMPTY_FOREGROUND); + } + } + + /** + * Runs whenever the text in the search box is changed. + *

+ * Reapplies the search filter, and custom filters. + *

+ * + * @since 2019-04-14 + * @since v0.2.0 + */ + private void searchBoxTextChanged() { + if (this.searchBoxFocused) { + this.searchBoxEmpty = this.searchBox.getText().equals(""); + } + final String searchText = this.searchBoxEmpty ? "" + : this.searchBox.getText(); + final FilterComparator comparator = new FilterComparator(searchText, + this.defaultOrdering, this.caseSensitive); + final Predicate searchFilter = this.getSearchFilter(searchText); + + // initialize list with items that match the filter then sort + this.listModel.clear(); + this.itemsToFilter.forEach(string -> { + if (searchFilter.test(string)) { + this.listModel.add(string); + } + }); + + // applies the custom filters + this.listModel.removeIf(this.customSearchFilter.negate()); + + // sorts the remaining items + this.listModel.sort(comparator); + } + + /** + * Manually updates the search box's item list. + * + * @since 2020-08-27 + */ + public void updateList() { + this.searchBoxTextChanged(); + } +} diff --git a/src/main/java/org/unitConverter/converterGUI/UnitConverterGUI.java b/src/main/java/org/unitConverter/converterGUI/UnitConverterGUI.java new file mode 100644 index 0000000..c046846 --- /dev/null +++ b/src/main/java/org/unitConverter/converterGUI/UnitConverterGUI.java @@ -0,0 +1,1426 @@ +/** + * Copyright (C) 2018-2021 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 . + */ +package org.unitConverter.converterGUI; + +import java.awt.BorderLayout; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.GridLayout; +import java.awt.event.KeyEvent; +import java.io.BufferedWriter; +import java.io.IOException; +import java.math.BigDecimal; +import java.math.MathContext; +import java.math.RoundingMode; +import java.nio.file.Files; +import java.nio.file.Path; +import java.text.DecimalFormat; +import java.text.NumberFormat; +import java.text.ParseException; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Set; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import javax.swing.BorderFactory; +import javax.swing.BoxLayout; +import javax.swing.ButtonGroup; +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JComboBox; +import javax.swing.JFormattedTextField; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JRadioButton; +import javax.swing.JScrollPane; +import javax.swing.JSlider; +import javax.swing.JTabbedPane; +import javax.swing.JTextArea; +import javax.swing.JTextField; +import javax.swing.UIManager; +import javax.swing.UnsupportedLookAndFeelException; +import javax.swing.WindowConstants; +import javax.swing.border.TitledBorder; + +import org.unitConverter.math.ConditionalExistenceCollections; +import org.unitConverter.math.ObjectProduct; +import org.unitConverter.unit.BaseDimension; +import org.unitConverter.unit.BritishImperial; +import org.unitConverter.unit.LinearUnit; +import org.unitConverter.unit.LinearUnitValue; +import org.unitConverter.unit.NameSymbol; +import org.unitConverter.unit.SI; +import org.unitConverter.unit.Unit; +import org.unitConverter.unit.UnitDatabase; +import org.unitConverter.unit.UnitPrefix; +import org.unitConverter.unit.UnitValue; + +/** + * @author Adrien Hopkins + * @since 2018-12-27 + * @since v0.1.0 + */ +final class UnitConverterGUI { + /** + * A tab in the View. + */ + private enum Pane { + UNIT_CONVERTER, EXPRESSION_CONVERTER, UNIT_VIEWER, PREFIX_VIEWER, ABOUT, + SETTINGS; + } + + private static class Presenter { + /** The default place where settings are stored. */ + private static final String DEFAULT_SETTINGS_FILEPATH = "settings.txt"; + /** The default place where units are stored. */ + private static final Path DEFAULT_UNITS_FILE = Path.of("unitsfile.txt"); + /** The default place where dimensions are stored. */ + private static final Path DEFAULT_DIMENSION_FILE = Path + .of("dimensionfile.txt"); + /** The default place where exceptions are stored. */ + private static final Path DEFAULT_EXCEPTIONS_FILE = Path + .of("metric_exceptions.txt"); + + /** + * Adds default units and dimensions to a database. + * + * @param database database to add to + * @since 2019-04-14 + * @since v0.2.0 + */ + private static void addDefaults(final UnitDatabase database) { + database.addUnit("metre", SI.METRE); + database.addUnit("kilogram", SI.KILOGRAM); + database.addUnit("gram", SI.KILOGRAM.dividedBy(1000)); + database.addUnit("second", SI.SECOND); + database.addUnit("ampere", SI.AMPERE); + database.addUnit("kelvin", SI.KELVIN); + database.addUnit("mole", SI.MOLE); + database.addUnit("candela", SI.CANDELA); + database.addUnit("bit", SI.BIT); + database.addUnit("unit", SI.ONE); + // nonlinear units - must be loaded manually + database.addUnit("tempCelsius", SI.CELSIUS); + database.addUnit("tempFahrenheit", BritishImperial.FAHRENHEIT); + + // load initial dimensions + database.addDimension("LENGTH", SI.Dimensions.LENGTH); + database.addDimension("MASS", SI.Dimensions.MASS); + database.addDimension("TIME", SI.Dimensions.TIME); + database.addDimension("TEMPERATURE", SI.Dimensions.TEMPERATURE); + } + + /** + * @return {@code line} with any comments removed. + * @since 2021-03-13 + */ + private static final String withoutComments(String line) { + final int index = line.indexOf('#'); + return index == -1 ? line : line.substring(index); + } + + /** The presenter's associated view. */ + private final View view; + + /** The units known by the program. */ + private final UnitDatabase database; + + /** The names of all of the units */ + private final List unitNames; + + /** The names of all of the prefixes */ + private final List prefixNames; + + /** The names of all of the dimensions */ + private final List dimensionNames; + + /** Unit names that are ignored by the metric-only/imperial-only filter */ + private final Set metricExceptions; + + private final Comparator prefixNameComparator; + + /** A boolean remembering whether or not one-way conversion is on */ + private boolean oneWay = true; + + /** The prefix rule */ + private DefaultPrefixRepetitionRule prefixRule = null; + // conditions for existence of From and To entries + // used for one-way conversion + private final MutablePredicate fromExistenceCondition = new MutablePredicate<>( + s -> true); + + private final MutablePredicate toExistenceCondition = new MutablePredicate<>( + s -> true); + + /* + * Rounding-related settings. I am using my own system, and not + * MathContext, because MathContext does not support decimal place based + * or scientific rounding, only significant digit based rounding. + */ + private int precision = 6; + + private RoundingType roundingType = RoundingType.SIGNIFICANT_DIGITS; + + /** + * Creates the presenter. + * + * @param view presenter's associated view + * @since 2018-12-27 + * @since v0.1.0 + */ + Presenter(final View view) { + this.view = view; + + // load initial units + this.database = new UnitDatabase( + DefaultPrefixRepetitionRule.NO_RESTRICTION); + Presenter.addDefaults(this.database); + + this.database.loadUnitsFile(DEFAULT_UNITS_FILE); + this.database.loadDimensionFile(DEFAULT_DIMENSION_FILE); + + // load metric exceptions + try { + this.metricExceptions = Files.readAllLines(DEFAULT_EXCEPTIONS_FILE) + .stream().map(Presenter::withoutComments) + .filter(s -> !s.isBlank()).collect(Collectors.toSet()); + } catch (final IOException e) { + throw new AssertionError("Loading of metric_exceptions.txt failed.", + e); + } + + // load settings - requires database to exist + this.loadSettings(); + + // a comparator that can be used to compare prefix names + // any name that does not exist is less than a name that does. + // otherwise, they are compared by value + this.prefixNameComparator = (o1, o2) -> { + if (!Presenter.this.database.containsPrefixName(o1)) + return -1; + else if (!Presenter.this.database.containsPrefixName(o2)) + return 1; + + final UnitPrefix p1 = Presenter.this.database.getPrefix(o1); + final UnitPrefix p2 = Presenter.this.database.getPrefix(o2); + + if (p1.getMultiplier() < p2.getMultiplier()) + return -1; + else if (p1.getMultiplier() > p2.getMultiplier()) + return 1; + + return o1.compareTo(o2); + }; + + this.unitNames = new ArrayList<>( + this.database.unitMapPrefixless().keySet()); + this.unitNames.sort(null); // sorts it using Comparable + + this.prefixNames = new ArrayList<>(this.database.prefixMap().keySet()); + this.prefixNames.sort(this.prefixNameComparator); // sorts it using my + // comparator + + this.dimensionNames = new DelegateListModel<>( + new ArrayList<>(this.database.dimensionMap().keySet())); + this.dimensionNames.sort(null); // sorts it using Comparable + + // a Predicate that returns true iff the argument is a full base unit + final Predicate isFullBase = unit -> unit instanceof LinearUnit + && ((LinearUnit) unit).isBase(); + + // print out unit counts + System.out.printf( + "Successfully loaded %d units with %d unit names (%d base units).%n", + new HashSet<>(this.database.unitMapPrefixless().values()).size(), + this.database.unitMapPrefixless().size(), + new HashSet<>(this.database.unitMapPrefixless().values()) + .stream().filter(isFullBase).count()); + } + + /** + * Converts in the dimension-based converter + * + * @since 2019-04-13 + * @since v0.2.0 + */ + public final void convertDimensionBased() { + final String fromSelection = this.view.getFromSelection(); + if (fromSelection == null) { + this.view.showErrorDialog("Error", + "No unit selected in From field"); + return; + } + final String toSelection = this.view.getToSelection(); + if (toSelection == null) { + this.view.showErrorDialog("Error", "No unit selected in To field"); + return; + } + + final Unit from = this.database.getUnit(fromSelection); + final Unit to = this.database.getUnit(toSelection) + .withName(NameSymbol.ofName(toSelection)); + + final UnitValue beforeValue; + try { + beforeValue = UnitValue.of(from, + this.view.getDimensionConverterInput()); + } catch (final ParseException e) { + this.view.showErrorDialog("Error", + "Error in parsing: " + e.getMessage()); + return; + } + final UnitValue value = beforeValue.convertTo(to); + + final String output = this.getRoundedString(value); + + this.view.setDimensionConverterOutputText( + String.format("%s = %s", beforeValue, output)); + } + + /** + * Runs whenever the convert button is pressed. + * + *

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

+ * + * @since 2019-01-26 + * @since v0.1.0 + */ + public final void convertExpressions() { + final String fromUnitString = this.view.getFromText(); + final String toUnitString = this.view.getToText(); + + if (fromUnitString.isEmpty()) { + this.view.showErrorDialog("Parse Error", + "Please enter a unit expression in the From: box."); + return; + } + if (toUnitString.isEmpty()) { + this.view.showErrorDialog("Parse Error", + "Please enter a unit expression in the To: box."); + return; + } + + final LinearUnitValue from; + final Unit to; + try { + from = this.database.evaluateUnitExpression(fromUnitString); + } catch (final IllegalArgumentException | NoSuchElementException e) { + this.view.showErrorDialog("Parse Error", + "Could not recognize text in From entry: " + e.getMessage()); + return; + } + try { + to = this.database.getUnitFromExpression(toUnitString); + } catch (final IllegalArgumentException | NoSuchElementException e) { + this.view.showErrorDialog("Parse Error", + "Could not recognize text in To entry: " + e.getMessage()); + return; + } + + if (to instanceof LinearUnit) { + // convert to LinearUnitValue + final LinearUnitValue from2; + final LinearUnit to2 = ((LinearUnit) to) + .withName(NameSymbol.ofName(toUnitString)); + final boolean useSlash; + + if (from.canConvertTo(to2)) { + from2 = from; + useSlash = false; + } else if (LinearUnitValue.ONE.dividedBy(from).canConvertTo(to2)) { + from2 = LinearUnitValue.ONE.dividedBy(from); + useSlash = true; + } else { + // if I can't convert, leave + this.view.showErrorDialog("Conversion Error", + String.format("Cannot convert between %s and %s", + fromUnitString, toUnitString)); + return; + } + + final LinearUnitValue converted = from2.convertTo(to2); + this.view.setExpressionConverterOutputText((useSlash ? "1 / " : "") + + String.format("%s = %s", fromUnitString, + this.getRoundedString(converted, false))); + return; + } else { + // convert to UnitValue + final UnitValue from2 = from.asUnitValue(); + if (from2.canConvertTo(to)) { + final UnitValue converted = from2.convertTo(to); + + this.view + .setExpressionConverterOutputText(String.format("%s = %s", + fromUnitString, this.getRoundedString(converted))); + } else { + // if I can't convert, leave + this.view.showErrorDialog("Conversion Error", + String.format("Cannot convert between %s and %s", + fromUnitString, toUnitString)); + } + } + } + + /** + * @return a list of all of the unit dimensions + * @since 2019-04-13 + * @since v0.2.0 + */ + public final List dimensionNameList() { + return this.dimensionNames; + } + + /** + * @return a list of all the entries in the dimension-based converter's + * From box + * @since 2020-08-27 + */ + public final Set fromEntries() { + return ConditionalExistenceCollections.conditionalExistenceSet( + this.unitNameSet(), this.fromExistenceCondition); + } + + /** + * @return a comparator to compare prefix names + * @since 2019-04-14 + * @since v0.2.0 + */ + public final Comparator getPrefixNameComparator() { + return this.prefixNameComparator; + } + + /** + * Like {@link LinearUnitValue#toString(boolean)}, but obeys this unit + * converter's rounding settings. + * + * @since 2020-08-04 + */ + private final String getRoundedString(final LinearUnitValue value, + boolean showUncertainty) { + switch (this.roundingType) { + case DECIMAL_PLACES: + case SIGNIFICANT_DIGITS: + return this.getRoundedString(value.asUnitValue()); + case SCIENTIFIC: + return value.toString(showUncertainty); + default: + throw new AssertionError("Invalid switch condition."); + } + } + + /** + * Like {@link UnitValue#toString()}, but obeys this unit converter's + * rounding settings. + * + * @since 2020-08-04 + */ + private final String getRoundedString(final UnitValue value) { + final BigDecimal unrounded = new BigDecimal(value.getValue()); + final BigDecimal rounded; + int precision = this.precision; + + switch (this.roundingType) { + case DECIMAL_PLACES: + rounded = unrounded.setScale(precision, RoundingMode.HALF_EVEN); + break; + case SCIENTIFIC: + precision = 12; + //$FALL-THROUGH$ + case SIGNIFICANT_DIGITS: + rounded = unrounded + .round(new MathContext(precision, RoundingMode.HALF_EVEN)); + break; + default: + throw new AssertionError("Invalid switch condition."); + } + + String output = rounded.toString(); + + // remove trailing zeroes + if (output.contains(".")) { + while (output.endsWith("0")) { + output = output.substring(0, output.length() - 1); + } + if (output.endsWith(".")) { + output = output.substring(0, output.length() - 1); + } + } + + return output + " " + value.getUnit().getPrimaryName().get(); + } + + /** + * @return The file where settings are stored; + * @since 2020-12-11 + */ + private final Path getSettingsFile() { + return Path.of(DEFAULT_SETTINGS_FILEPATH); + } + + /** + * Loads settings from the settings file. + * + * @since 2021-02-17 + */ + public final void loadSettings() { + try { + // read file line by line + final int lineNum = 0; + for (final String line : Files + .readAllLines(this.getSettingsFile())) { + final int equalsIndex = line.indexOf('='); + if (equalsIndex == -1) + throw new IllegalStateException( + "Settings file is malformed at line " + lineNum); + + final String param = line.substring(0, equalsIndex); + final String value = line.substring(equalsIndex + 1); + + switch (param) { + // set manually to avoid the unnecessary saving of the non-manual + // methods + case "precision": + this.precision = Integer.valueOf(value); + break; + case "rounding_type": + this.roundingType = RoundingType.valueOf(value); + break; + case "prefix_rule": + this.prefixRule = DefaultPrefixRepetitionRule.valueOf(value); + this.database.setPrefixRepetitionRule(this.prefixRule); + break; + case "one_way": + this.oneWay = Boolean.valueOf(value); + if (this.oneWay) { + this.fromExistenceCondition.setPredicate( + unitName -> this.metricExceptions.contains(unitName) + || !this.database.getUnit(unitName) + .isMetric()); + this.toExistenceCondition.setPredicate( + unitName -> this.metricExceptions.contains(unitName) + || this.database.getUnit(unitName).isMetric()); + } else { + this.fromExistenceCondition.setPredicate(unitName -> true); + this.toExistenceCondition.setPredicate(unitName -> true); + } + break; + default: + System.err.printf("Warning: unrecognized setting \"%s\".", + param); + break; + } + } + } catch (final IOException e) {} + } + + /** + * @return a set of all prefix names in the database + * @since 2019-04-14 + * @since v0.2.0 + */ + public final Set prefixNameSet() { + return this.database.prefixMap().keySet(); + } + + /** + * Runs whenever a prefix is selected in the viewer. + *

+ * Shows its information in the text box to the right. + *

+ * + * @since 2019-01-15 + * @since v0.1.0 + */ + public final void prefixSelected() { + final String prefixName = this.view.getPrefixViewerSelection(); + if (prefixName == null) + return; + else { + final UnitPrefix prefix = this.database.getPrefix(prefixName); + + this.view.setPrefixTextBoxText(String.format("%s%nMultiplier: %s", + prefixName, prefix.getMultiplier())); + } + } + + /** + * Saves the settings to the settings file. + * + * @since 2021-02-17 + */ + public final void saveSettings() { + try (BufferedWriter writer = Files + .newBufferedWriter(this.getSettingsFile())) { + writer.write(String.format("precision=%d\n", this.precision)); + writer.write( + String.format("rounding_type=%s\n", this.roundingType)); + writer.write(String.format("prefix_rule=%s\n", this.prefixRule)); + writer.write(String.format("one_way=%s\n", this.oneWay)); + } catch (final IOException e) { + e.printStackTrace(); + this.view.showErrorDialog("I/O Error", + "Error occurred while saving settings: " + + e.getLocalizedMessage()); + } + } + + /** + * Enables or disables one-way conversion. + * + * @param oneWay whether one-way conversion should be on (true) or off + * (false) + * @since 2020-08-27 + */ + public final void setOneWay(boolean oneWay) { + this.oneWay = oneWay; + if (oneWay) { + this.fromExistenceCondition.setPredicate( + unitName -> this.metricExceptions.contains(unitName) + || !this.database.getUnit(unitName).isMetric()); + this.toExistenceCondition.setPredicate( + unitName -> this.metricExceptions.contains(unitName) + || this.database.getUnit(unitName).isMetric()); + } else { + this.fromExistenceCondition.setPredicate(unitName -> true); + this.toExistenceCondition.setPredicate(unitName -> true); + } + + this.saveSettings(); + } + + /** + * @param precision new value of precision + * @since 2019-01-15 + * @since v0.1.0 + */ + public final void setPrecision(final int precision) { + this.precision = precision; + + this.saveSettings(); + } + + /** + * @param prefixRepetitionRule the prefixRepetitionRule to set + * @since 2020-08-26 + */ + public void setPrefixRepetitionRule( + Predicate> prefixRepetitionRule) { + if (prefixRepetitionRule instanceof DefaultPrefixRepetitionRule) { + this.prefixRule = (DefaultPrefixRepetitionRule) prefixRepetitionRule; + } else { + this.prefixRule = null; + } + this.database.setPrefixRepetitionRule(prefixRepetitionRule); + + this.saveSettings(); + } + + /** + * @param roundingType the roundingType to set + * @since 2020-07-16 + */ + public final void setRoundingType(RoundingType roundingType) { + this.roundingType = roundingType; + + this.saveSettings(); + } + + /** + * @return a list of all the entries in the dimension-based converter's To + * box + * @since 2020-08-27 + */ + public final Set toEntries() { + return ConditionalExistenceCollections.conditionalExistenceSet( + this.unitNameSet(), this.toExistenceCondition); + } + + /** + * Returns true if and only if the unit represented by {@code unitName} + * has the dimension represented by {@code dimensionName}. + * + * @param unitName name of unit to test + * @param dimensionName name of dimension to test + * @return whether unit has dimenision + * @since 2019-04-13 + * @since v0.2.0 + */ + public final boolean unitMatchesDimension(final String unitName, + final String dimensionName) { + final Unit unit = this.database.getUnit(unitName); + final ObjectProduct dimension = this.database + .getDimension(dimensionName); + return unit.getDimension().equals(dimension); + } + + /** + * Runs whenever a unit is selected in the viewer. + *

+ * Shows its information in the text box to the right. + *

+ * + * @since 2019-01-15 + * @since v0.1.0 + */ + public final void unitNameSelected() { + final String unitName = this.view.getUnitViewerSelection(); + if (unitName == null) + return; + else { + final Unit unit = this.database.getUnit(unitName); + + this.view.setUnitTextBoxText(unit.toString()); + } + } + + /** + * @return a set of all of the unit names + * @since 2019-04-14 + * @since v0.2.0 + */ + private final Set unitNameSet() { + return this.database.unitMapPrefixless().keySet(); + } + } + + /** + * Different types of rounding. + * + * Significant digits: Rounds to a number of digits. i.e. with precision 5, + * 12345.6789 rounds to 12346. Decimal places: Rounds to a number of digits + * after the decimal point, i.e. with precision 5, 12345.6789 rounds to + * 12345.67890. Scientific: Rounds based on the number of digits and + * operations, following standard scientific rounding. + */ + private static enum RoundingType { + SIGNIFICANT_DIGITS, DECIMAL_PLACES, SCIENTIFIC; + } + + private static class View { + private static final NumberFormat NUMBER_FORMATTER = new DecimalFormat(); + + /** The view's frame. */ + private final JFrame frame; + /** The view's associated presenter. */ + private final Presenter presenter; + /** The master pane containing all of the tabs. */ + private final JTabbedPane masterPane; + + // DIMENSION-BASED CONVERTER + /** The panel for inputting values in the dimension-based converter */ + private final JTextField valueInput; + /** The panel for "From" in the dimension-based converter */ + private final SearchBoxList fromSearch; + /** The panel for "To" in the dimension-based converter */ + private final SearchBoxList toSearch; + /** The output area in the dimension-based converter */ + private final JTextArea dimensionBasedOutput; + + // EXPRESSION-BASED CONVERTER + /** The "From" entry in the conversion panel */ + private final JTextField fromEntry; + /** The "To" entry in the conversion panel */ + private final JTextField toEntry; + /** The output area in the conversion panel */ + private final JTextArea output; + + // UNIT AND PREFIX VIEWERS + /** The searchable list of unit names in the unit viewer */ + private final SearchBoxList unitNameList; + /** The searchable list of prefix names in the prefix viewer */ + private final SearchBoxList prefixNameList; + /** The text box for unit data in the unit viewer */ + private final JTextArea unitTextBox; + /** The text box for prefix data in the prefix viewer */ + private final JTextArea prefixTextBox; + + /** + * Creates the {@code View}. + * + * @since 2019-01-14 + * @since v0.1.0 + */ + public View() { + this.presenter = new Presenter(this); + this.frame = new JFrame("Unit Converter"); + this.frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); + + // enable system look and feel + try { + UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + } catch (ClassNotFoundException | InstantiationException + | IllegalAccessException | UnsupportedLookAndFeelException e) { + // oh well, just use default theme + System.err.println("Failed to enable system look-and-feel."); + e.printStackTrace(); + } + + // create the components + this.masterPane = new JTabbedPane(); + this.unitNameList = new SearchBoxList(this.presenter.unitNameSet()); + this.prefixNameList = new SearchBoxList(this.presenter.prefixNameSet(), + this.presenter.getPrefixNameComparator(), true); + this.unitTextBox = new JTextArea(); + this.prefixTextBox = new JTextArea(); + this.fromSearch = new SearchBoxList(this.presenter.fromEntries()); + this.toSearch = new SearchBoxList(this.presenter.toEntries()); + this.valueInput = new JFormattedTextField(NUMBER_FORMATTER); + this.dimensionBasedOutput = new JTextArea(2, 32); + this.fromEntry = new JTextField(); + this.toEntry = new JTextField(); + this.output = new JTextArea(2, 32); + + // create more components + this.initComponents(); + + this.frame.pack(); + } + + /** + * @return the currently selected pane. + * @throws AssertionError if no pane (or an invalid pane) is selected + */ + public Pane getActivePane() { + switch (this.masterPane.getSelectedIndex()) { + case 0: + return Pane.UNIT_CONVERTER; + case 1: + return Pane.EXPRESSION_CONVERTER; + case 2: + return Pane.UNIT_VIEWER; + case 3: + return Pane.PREFIX_VIEWER; + case 4: + return Pane.ABOUT; + case 5: + return Pane.SETTINGS; + default: + throw new AssertionError("No selected pane, or invalid pane."); + } + } + + /** + * @return value in dimension-based converter + * @throws ParseException + * @since 2020-07-07 + */ + public double getDimensionConverterInput() throws ParseException { + final Number value = NUMBER_FORMATTER.parse(this.valueInput.getText()); + if (value instanceof Double) + return (double) value; + else if (value instanceof Long) + return ((Long) value).longValue(); + else + throw new AssertionError(); + } + + /** + * @return selection in "From" selector in dimension-based converter + * @since 2019-04-13 + * @since v0.2.0 + */ + public String getFromSelection() { + return this.fromSearch.getSelectedValue(); + } + + /** + * @return text in "From" box in converter panel + * @since 2019-01-15 + * @since v0.1.0 + */ + public String getFromText() { + return this.fromEntry.getText(); + } + + /** + * @return index of selected prefix in prefix viewer + * @since 2019-01-15 + * @since v0.1.0 + */ + public String getPrefixViewerSelection() { + return this.prefixNameList.getSelectedValue(); + } + + /** + * @return selection in "To" selector in dimension-based converter + * @since 2019-04-13 + * @since v0.2.0 + */ + public String getToSelection() { + return this.toSearch.getSelectedValue(); + } + + /** + * @return text in "To" box in converter panel + * @since 2019-01-26 + * @since v0.1.0 + */ + public String getToText() { + return this.toEntry.getText(); + } + + /** + * @return index of selected unit in unit viewer + * @since 2019-01-15 + * @since v0.1.0 + */ + public String getUnitViewerSelection() { + return this.unitNameList.getSelectedValue(); + } + + /** + * Starts up the application. + * + * @since 2018-12-27 + * @since v0.1.0 + */ + public final void init() { + this.frame.setVisible(true); + } + + /** + * Initializes the view's components. + * + * @since 2018-12-27 + * @since v0.1.0 + */ + private final void initComponents() { + final JPanel masterPanel = new JPanel(); + this.frame.add(masterPanel); + + masterPanel.setLayout(new BorderLayout()); + + { // pane with all of the tabs + masterPanel.add(this.masterPane, BorderLayout.CENTER); + + // update stuff + this.masterPane.addChangeListener(e -> this.update()); + + { // a panel for unit conversion using a selector + final JPanel convertUnitPanel = new JPanel(); + this.masterPane.addTab("Convert Units", convertUnitPanel); + this.masterPane.setMnemonicAt(0, KeyEvent.VK_U); + + convertUnitPanel.setLayout(new BorderLayout()); + + { // panel for input part + final JPanel inputPanel = new JPanel(); + convertUnitPanel.add(inputPanel, BorderLayout.CENTER); + + inputPanel.setLayout(new GridLayout(1, 3)); + + final JComboBox dimensionSelector = new JComboBox<>( + this.presenter.dimensionNameList() + .toArray(new String[0])); + dimensionSelector.setSelectedItem("LENGTH"); + + // handle dimension filter + final MutablePredicate dimensionFilter = new MutablePredicate<>( + s -> true); + + // panel for From things + inputPanel.add(this.fromSearch); + + this.fromSearch.addSearchFilter(dimensionFilter); + + { // for dimension selector and arrow that represents + // conversion + final JPanel inBetweenPanel = new JPanel(); + inputPanel.add(inBetweenPanel); + + inBetweenPanel.setLayout(new BorderLayout()); + + { // dimension selector + inBetweenPanel.add(dimensionSelector, + BorderLayout.PAGE_START); + } + + { // the arrow in the middle + final JLabel arrowLabel = new JLabel("->"); + inBetweenPanel.add(arrowLabel, BorderLayout.CENTER); + } + } + + // panel for To things + + inputPanel.add(this.toSearch); + + this.toSearch.addSearchFilter(dimensionFilter); + + // code for dimension filter + dimensionSelector.addItemListener(e -> { + dimensionFilter.setPredicate(string -> View.this.presenter + .unitMatchesDimension(string, + (String) dimensionSelector.getSelectedItem())); + this.fromSearch.reapplyFilter(); + this.toSearch.reapplyFilter(); + }); + + // apply the item listener once because I have a default + // selection + dimensionFilter.setPredicate(string -> View.this.presenter + .unitMatchesDimension(string, + (String) dimensionSelector.getSelectedItem())); + this.fromSearch.reapplyFilter(); + this.toSearch.reapplyFilter(); + } + + { // panel for submit and output, and also value entry + final JPanel outputPanel = new JPanel(); + convertUnitPanel.add(outputPanel, BorderLayout.PAGE_END); + + outputPanel.setLayout(new GridLayout(3, 1)); + + { // unit input + final JPanel valueInputPanel = new JPanel(); + outputPanel.add(valueInputPanel); + + valueInputPanel.setLayout(new BorderLayout()); + + { // prompt + final JLabel valuePrompt = new JLabel( + "Value to convert: "); + valueInputPanel.add(valuePrompt, + BorderLayout.LINE_START); + } + + { // value to convert + valueInputPanel.add(this.valueInput, + BorderLayout.CENTER); + } + } + + { // button to convert + final JButton convertButton = new JButton("Convert"); + outputPanel.add(convertButton); + + convertButton.addActionListener( + e -> this.presenter.convertDimensionBased()); + convertButton.setMnemonic(KeyEvent.VK_ENTER); + } + + { // output of conversion + outputPanel.add(this.dimensionBasedOutput); + this.dimensionBasedOutput.setEditable(false); + } + } + } + + { // panel for unit conversion using expressions + final JPanel convertExpressionPanel = new JPanel(); + this.masterPane.addTab("Convert Unit Expressions", + convertExpressionPanel); + this.masterPane.setMnemonicAt(1, KeyEvent.VK_E); + + convertExpressionPanel.setLayout(new GridLayout(4, 1)); + + { // panel for units to convert from + final JPanel fromPanel = new JPanel(); + convertExpressionPanel.add(fromPanel); + + fromPanel.setBorder(BorderFactory.createTitledBorder("From")); + fromPanel.setLayout(new GridLayout(1, 1)); + + { // entry for units + fromPanel.add(this.fromEntry); + } + } + + { // panel for units to convert to + final JPanel toPanel = new JPanel(); + convertExpressionPanel.add(toPanel); + + toPanel.setBorder(BorderFactory.createTitledBorder("To")); + toPanel.setLayout(new GridLayout(1, 1)); + + { // entry for units + toPanel.add(this.toEntry); + } + } + + { // button to convert + final JButton convertButton = new JButton("Convert"); + convertExpressionPanel.add(convertButton); + + convertButton.addActionListener( + e -> this.presenter.convertExpressions()); + convertButton.setMnemonic(KeyEvent.VK_ENTER); + } + + { // output of conversion + final JPanel outputPanel = new JPanel(); + convertExpressionPanel.add(outputPanel); + + outputPanel + .setBorder(BorderFactory.createTitledBorder("Output")); + outputPanel.setLayout(new GridLayout(1, 1)); + + { // output + outputPanel.add(this.output); + this.output.setEditable(false); + } + } + } + + { // panel to look up units + final JPanel unitLookupPanel = new JPanel(); + this.masterPane.addTab("Unit Viewer", unitLookupPanel); + this.masterPane.setMnemonicAt(2, KeyEvent.VK_V); + + unitLookupPanel.setLayout(new GridLayout()); + + { // search panel + unitLookupPanel.add(this.unitNameList); + + this.unitNameList.getSearchList().addListSelectionListener( + e -> this.presenter.unitNameSelected()); + } + + { // the text box for unit's toString + unitLookupPanel.add(this.unitTextBox); + this.unitTextBox.setEditable(false); + this.unitTextBox.setLineWrap(true); + } + } + + { // panel to look up prefixes + final JPanel prefixLookupPanel = new JPanel(); + this.masterPane.addTab("Prefix Viewer", prefixLookupPanel); + this.masterPane.setMnemonicAt(3, KeyEvent.VK_P); + + prefixLookupPanel.setLayout(new GridLayout(1, 2)); + + { // panel for listing and seaching + prefixLookupPanel.add(this.prefixNameList); + + this.prefixNameList.getSearchList().addListSelectionListener( + e -> this.presenter.prefixSelected()); + } + + { // the text box for prefix's toString + prefixLookupPanel.add(this.prefixTextBox); + this.prefixTextBox.setEditable(false); + this.prefixTextBox.setLineWrap(true); + } + } + + { // Info panel + final JPanel infoPanel = new JPanel(); + this.masterPane.addTab("\uD83D\uDEC8", // info (i) character + new JScrollPane(infoPanel)); + + final JTextArea infoTextArea = new JTextArea(); + infoTextArea.setEditable(false); + infoTextArea.setOpaque(false); + infoPanel.add(infoTextArea); + + // get info text + final String infoText; + try { + final Path aboutFile = Path.of("src", "main", "resources", + "about.txt"); + infoText = Files.readAllLines(aboutFile).stream() + .map(Presenter::withoutComments) + .collect(Collectors.joining("\n")); + } catch (final IOException e) { + throw new AssertionError("I/O exception loading about.txt", + e); + } + infoTextArea.setText(infoText); + } + + { // Settings panel + final JPanel settingsPanel = new JPanel(); + this.masterPane.addTab("\u2699", new JScrollPane(settingsPanel)); + this.masterPane.setMnemonicAt(5, KeyEvent.VK_S); + + settingsPanel.setLayout( + new BoxLayout(settingsPanel, BoxLayout.PAGE_AXIS)); + + { // rounding settings + final JPanel roundingPanel = new JPanel(); + settingsPanel.add(roundingPanel); + roundingPanel + .setBorder(new TitledBorder("Rounding Settings")); + roundingPanel.setLayout(new GridBagLayout()); + + // rounding rule selection + final ButtonGroup roundingRuleButtons = new ButtonGroup(); + + final JLabel roundingRuleLabel = new JLabel("Rounding Rule:"); + roundingPanel.add(roundingRuleLabel, new GridBagBuilder(0, 0) + .setAnchor(GridBagConstraints.LINE_START).build()); + + final JRadioButton fixedPrecision = new JRadioButton( + "Fixed Precision"); + if (this.presenter.roundingType == RoundingType.SIGNIFICANT_DIGITS) { + fixedPrecision.setSelected(true); + } + fixedPrecision.addActionListener(e -> this.presenter + .setRoundingType(RoundingType.SIGNIFICANT_DIGITS)); + roundingRuleButtons.add(fixedPrecision); + roundingPanel.add(fixedPrecision, new GridBagBuilder(0, 1) + .setAnchor(GridBagConstraints.LINE_START).build()); + + final JRadioButton fixedDecimals = new JRadioButton( + "Fixed Decimal Places"); + if (this.presenter.roundingType == RoundingType.DECIMAL_PLACES) { + fixedDecimals.setSelected(true); + } + fixedDecimals.addActionListener(e -> this.presenter + .setRoundingType(RoundingType.DECIMAL_PLACES)); + roundingRuleButtons.add(fixedDecimals); + roundingPanel.add(fixedDecimals, new GridBagBuilder(0, 2) + .setAnchor(GridBagConstraints.LINE_START).build()); + + final JRadioButton relativePrecision = new JRadioButton( + "Scientific Precision"); + if (this.presenter.roundingType == RoundingType.SCIENTIFIC) { + relativePrecision.setSelected(true); + } + relativePrecision.addActionListener(e -> this.presenter + .setRoundingType(RoundingType.SCIENTIFIC)); + roundingRuleButtons.add(relativePrecision); + roundingPanel.add(relativePrecision, new GridBagBuilder(0, 3) + .setAnchor(GridBagConstraints.LINE_START).build()); + + final JLabel sliderLabel = new JLabel("Precision:"); + roundingPanel.add(sliderLabel, new GridBagBuilder(0, 4) + .setAnchor(GridBagConstraints.LINE_START).build()); + + final JSlider sigDigSlider = new JSlider(0, 12); + roundingPanel.add(sigDigSlider, new GridBagBuilder(0, 5) + .setAnchor(GridBagConstraints.LINE_START).build()); + + sigDigSlider.setMajorTickSpacing(4); + sigDigSlider.setMinorTickSpacing(1); + sigDigSlider.setSnapToTicks(true); + sigDigSlider.setPaintTicks(true); + sigDigSlider.setPaintLabels(true); + sigDigSlider.setValue(this.presenter.precision); + + sigDigSlider.addChangeListener(e -> this.presenter + .setPrecision(sigDigSlider.getValue())); + } + + { // prefix repetition settings + final JPanel prefixRepetitionPanel = new JPanel(); + settingsPanel.add(prefixRepetitionPanel); + prefixRepetitionPanel.setBorder( + new TitledBorder("Prefix Repetition Settings")); + prefixRepetitionPanel.setLayout(new GridBagLayout()); + + // prefix rules + final ButtonGroup prefixRuleButtons = new ButtonGroup(); + + final JRadioButton noRepetition = new JRadioButton( + "No Repetition"); + if (this.presenter.prefixRule == DefaultPrefixRepetitionRule.NO_REPETITION) { + noRepetition.setSelected(true); + } + noRepetition.addActionListener( + e -> this.presenter.setPrefixRepetitionRule( + DefaultPrefixRepetitionRule.NO_REPETITION)); + prefixRuleButtons.add(noRepetition); + prefixRepetitionPanel.add(noRepetition, + new GridBagBuilder(0, 0) + .setAnchor(GridBagConstraints.LINE_START) + .build()); + + final JRadioButton noRestriction = new JRadioButton( + "No Restriction"); + if (this.presenter.prefixRule == DefaultPrefixRepetitionRule.NO_RESTRICTION) { + noRestriction.setSelected(true); + } + noRestriction.addActionListener( + e -> this.presenter.setPrefixRepetitionRule( + DefaultPrefixRepetitionRule.NO_RESTRICTION)); + prefixRuleButtons.add(noRestriction); + prefixRepetitionPanel.add(noRestriction, + new GridBagBuilder(0, 1) + .setAnchor(GridBagConstraints.LINE_START) + .build()); + + final JRadioButton customRepetition = new JRadioButton( + "Complex Repetition"); + if (this.presenter.prefixRule == DefaultPrefixRepetitionRule.COMPLEX_REPETITION) { + customRepetition.setSelected(true); + } + customRepetition.addActionListener( + e -> this.presenter.setPrefixRepetitionRule( + DefaultPrefixRepetitionRule.COMPLEX_REPETITION)); + prefixRuleButtons.add(customRepetition); + prefixRepetitionPanel.add(customRepetition, + new GridBagBuilder(0, 2) + .setAnchor(GridBagConstraints.LINE_START) + .build()); + } + + { // search settings + final JPanel searchingPanel = new JPanel(); + settingsPanel.add(searchingPanel); + searchingPanel.setBorder(new TitledBorder("Search Settings")); + searchingPanel.setLayout(new GridBagLayout()); + + // searching rules + final ButtonGroup searchRuleButtons = new ButtonGroup(); + + final JRadioButton noPrefixes = new JRadioButton( + "Never Include Prefixed Units"); + noPrefixes.setEnabled(false); + searchRuleButtons.add(noPrefixes); + searchingPanel.add(noPrefixes, new GridBagBuilder(0, 0) + .setAnchor(GridBagConstraints.LINE_START).build()); + + final JRadioButton fixedPrefixes = new JRadioButton( + "Include Some Prefixes"); + fixedPrefixes.setEnabled(false); + searchRuleButtons.add(fixedPrefixes); + searchingPanel.add(fixedPrefixes, new GridBagBuilder(0, 1) + .setAnchor(GridBagConstraints.LINE_START).build()); + + final JRadioButton explicitPrefixes = new JRadioButton( + "Include Explicit Prefixes"); + explicitPrefixes.setEnabled(false); + searchRuleButtons.add(explicitPrefixes); + searchingPanel.add(explicitPrefixes, new GridBagBuilder(0, 2) + .setAnchor(GridBagConstraints.LINE_START).build()); + + final JRadioButton alwaysInclude = new JRadioButton( + "Include All Single Prefixes"); + alwaysInclude.setEnabled(false); + searchRuleButtons.add(alwaysInclude); + searchingPanel.add(alwaysInclude, new GridBagBuilder(0, 3) + .setAnchor(GridBagConstraints.LINE_START).build()); + } + + { // miscellaneous settings + final JPanel miscPanel = new JPanel(); + settingsPanel.add(miscPanel); + miscPanel + .setBorder(new TitledBorder("Miscellaneous Settings")); + miscPanel.setLayout(new GridBagLayout()); + + final JCheckBox oneWay = new JCheckBox( + "Convert One Way Only"); + oneWay.setSelected(this.presenter.oneWay); + oneWay.addItemListener( + e -> this.presenter.setOneWay(e.getStateChange() == 1)); + miscPanel.add(oneWay, new GridBagBuilder(0, 0) + .setAnchor(GridBagConstraints.LINE_START).build()); + + final JCheckBox showAllVariations = new JCheckBox( + "Show Symbols in \"Convert Units\""); + showAllVariations.setSelected(true); + showAllVariations.setEnabled(false); + miscPanel.add(showAllVariations, new GridBagBuilder(0, 1) + .setAnchor(GridBagConstraints.LINE_START).build()); + + final JButton unitFileButton = new JButton( + "Manage Unit Data Files"); + unitFileButton.setEnabled(false); + miscPanel.add(unitFileButton, new GridBagBuilder(0, 2) + .setAnchor(GridBagConstraints.LINE_START).build()); + } + } + } + } + + /** + * Sets the text in the output of the dimension-based converter. + * + * @param text text to set + * @since 2019-04-13 + * @since v0.2.0 + */ + public void setDimensionConverterOutputText(final String text) { + this.dimensionBasedOutput.setText(text); + } + + /** + * Sets the text in the output of the conversion panel. + * + * @param text text to set + * @since 2019-01-15 + * @since v0.1.0 + */ + public void setExpressionConverterOutputText(final String text) { + this.output.setText(text); + } + + /** + * Sets the text of the prefix text box in the prefix viewer. + * + * @param text text to set + * @since 2019-01-15 + * @since v0.1.0 + */ + public void setPrefixTextBoxText(final String text) { + this.prefixTextBox.setText(text); + } + + /** + * Sets the text of the unit text box in the unit viewer. + * + * @param text text to set + * @since 2019-01-15 + * @since v0.1.0 + */ + public void setUnitTextBoxText(final String text) { + this.unitTextBox.setText(text); + } + + /** + * Shows an error dialog. + * + * @param title title of dialog + * @param message message in dialog + * @since 2019-01-14 + * @since v0.1.0 + */ + public void showErrorDialog(final String title, final String message) { + JOptionPane.showMessageDialog(this.frame, message, title, + JOptionPane.ERROR_MESSAGE); + } + + public void update() { + switch (this.getActivePane()) { + case UNIT_CONVERTER: + this.fromSearch.updateList(); + this.toSearch.updateList(); + break; + default: + // do nothing, for now + break; + } + } + } + + public static void main(final String[] args) { + new View().init(); + } +} diff --git a/src/main/java/org/unitConverter/converterGUI/package-info.java b/src/main/java/org/unitConverter/converterGUI/package-info.java new file mode 100644 index 0000000..d85ecab --- /dev/null +++ b/src/main/java/org/unitConverter/converterGUI/package-info.java @@ -0,0 +1,24 @@ +/** + * Copyright (C) 2019 Adrien Hopkins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +/** + * The GUI interface of the Unit Converter. + * + * @author Adrien Hopkins + * @since 2019-01-25 + * @since v0.2.0 + */ +package org.unitConverter.converterGUI; \ No newline at end of file diff --git a/src/main/java/org/unitConverter/math/ConditionalExistenceCollections.java b/src/main/java/org/unitConverter/math/ConditionalExistenceCollections.java new file mode 100644 index 0000000..ac1c0cf --- /dev/null +++ b/src/main/java/org/unitConverter/math/ConditionalExistenceCollections.java @@ -0,0 +1,468 @@ +/** + * 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 . + */ +package org.unitConverter.math; + +import java.util.AbstractCollection; +import java.util.AbstractMap; +import java.util.AbstractSet; +import java.util.Collection; +import java.util.Iterator; +import java.util.Map; +import java.util.Map.Entry; +import java.util.NoSuchElementException; +import java.util.Set; +import java.util.function.Predicate; + +/** + * Elements in these wrapper collections only exist if they pass a condition. + *

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

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

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

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

+ * Note that this method returns {@code false} if {@code e} does not pass + * the existence condition. + */ + @Override + public boolean add(final E e) { + return this.set.add(e) && this.existenceCondition.test(e); + } + + @Override + public void clear() { + this.set.clear(); + } + + @Override + public boolean contains(final Object o) { + if (!this.set.contains(o)) + return false; + + // this set can only contain instances of E + // since the object is in the set, we know that it must be an instance + // of E + // therefore this cast will always work + @SuppressWarnings("unchecked") + final E e = (E) o; + + return this.existenceCondition.test(e); + } + + @Override + public Iterator iterator() { + return conditionalExistenceIterator(this.set.iterator(), + this.existenceCondition); + } + + @Override + public boolean remove(final Object o) { + // remove() must be first in the && statement, otherwise it may not + // execute + final boolean containedObject = this.contains(o); + return this.set.remove(o) && containedObject; + } + + @Override + public int size() { + return (int) this.set.stream().filter(this.existenceCondition).count(); + } + + @Override + public Object[] toArray() { + // ensure the toArray operation is supported + this.set.toArray(); + + // if it works, do it for real + return super.toArray(); + } + + @Override + public T[] toArray(T[] a) { + // ensure the toArray operation is supported + this.set.toArray(); + + // if it works, do it for real + return super.toArray(a); + } + } + + /** + * Elements in the returned wrapper collection are ignored if they don't pass + * a condition. + * + * @param type of elements in collection + * @param collection collection to wrap + * @param existenceCondition elements only exist if this returns true + * @return wrapper collection + * @since 2019-10-17 + */ + public static final Collection conditionalExistenceCollection( + final Collection collection, + final Predicate existenceCondition) { + return new ConditionalExistenceCollection<>(collection, + existenceCondition); + } + + /** + * Elements in the returned wrapper iterator are ignored if they don't pass a + * condition. + * + * @param type of elements in iterator + * @param iterator iterator to wrap + * @param existenceCondition elements only exist if this returns true + * @return wrapper iterator + * @since 2019-10-17 + */ + public static final Iterator conditionalExistenceIterator( + final Iterator iterator, final Predicate existenceCondition) { + return new ConditionalExistenceIterator<>(iterator, existenceCondition); + } + + /** + * Mappings in the returned wrapper map are ignored if the corresponding + * entry doesn't pass a condition + * + * @param type of key in map + * @param type of value in map + * @param map map to wrap + * @param entryExistenceCondition mappings only exist if this returns true + * @return wrapper map + * @since 2019-10-17 + */ + public static final Map conditionalExistenceMap( + final Map map, + final Predicate> entryExistenceCondition) { + return new ConditionalExistenceMap<>(map, entryExistenceCondition); + } + + /** + * Elements in the returned wrapper set are ignored if they don't pass a + * condition. + * + * @param type of elements in set + * @param set set to wrap + * @param existenceCondition elements only exist if this returns true + * @return wrapper set + * @since 2019-10-17 + */ + public static final Set conditionalExistenceSet(final Set set, + final Predicate existenceCondition) { + return new ConditionalExistenceSet<>(set, existenceCondition); + } +} diff --git a/src/main/java/org/unitConverter/math/DecimalComparison.java b/src/main/java/org/unitConverter/math/DecimalComparison.java new file mode 100644 index 0000000..0f5b91e --- /dev/null +++ b/src/main/java/org/unitConverter/math/DecimalComparison.java @@ -0,0 +1,256 @@ +/** + * 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 . + */ +package org.unitConverter.math; + +import java.math.BigDecimal; + +/** + * A class that contains methods to compare float and double values. + * + * @author Adrien Hopkins + * @since 2019-03-18 + * @since v0.2.0 + */ +public final class DecimalComparison { + /** + * The value used for double comparison. If two double values are within this + * value multiplied by the larger value, they are considered equal. + * + * @since 2019-03-18 + * @since v0.2.0 + */ + public static final double DOUBLE_EPSILON = 1.0e-15; + + /** + * The value used for float comparison. If two float values are within this + * value multiplied by the larger value, they are considered equal. + * + * @since 2019-03-18 + * @since v0.2.0 + */ + public static final float FLOAT_EPSILON = 1.0e-6f; + + /** + * Tests for equality of double values using {@link #DOUBLE_EPSILON}. + *

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    + *
  1. Raise the value of epsilon (this does not make a violation of + * transitivity impossible, it just significantly reduces the chances of it + * happening) + *
  2. Use {@link BigDecimal} instead of {@code double} (this will make a + * violation of transitivity 100% impossible) + *
+ * + * @param a first value to test + * @param b second value to test + * @param epsilon allowed difference + * @return whether they are equal + * @since 2019-03-18 + * @since v0.2.0 + */ + public static final boolean equals(final UncertainDouble a, + final UncertainDouble b, final double epsilon) { + return DecimalComparison.equals(a.value(), b.value(), epsilon) + && DecimalComparison.equals(a.uncertainty(), b.uncertainty(), + epsilon); + } + + /** + * Takes the hash code of doubles. Values that are equal according to + * {@link #equals(double, double)} will have the same hash code. + * + * @param d double to hash + * @return hash code of double + * @since 2019-10-16 + */ + public static final int hash(final double d) { + return Float.hashCode((float) d); + } + + // You may NOT get any DecimalComparison instances + private DecimalComparison() { + throw new AssertionError(); + } + +} diff --git a/src/main/java/org/unitConverter/math/ExpressionParser.java b/src/main/java/org/unitConverter/math/ExpressionParser.java new file mode 100644 index 0000000..8a0e97d --- /dev/null +++ b/src/main/java/org/unitConverter/math/ExpressionParser.java @@ -0,0 +1,708 @@ +/** + * Copyright (C) 2019 Adrien Hopkins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.unitConverter.math; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Deque; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.BinaryOperator; +import java.util.function.Function; +import java.util.function.UnaryOperator; + +/** + * An object that can parse expressions with unary or binary operators. + * + * @author Adrien Hopkins + * @param + * type of object that exists in parsed expressions + * @since 2019-03-14 + * @since v0.2.0 + */ +public final class ExpressionParser { + /** + * A builder that can create {@code ExpressionParser} instances. + * + * @author Adrien Hopkins + * @param + * type of object that exists in parsed expressions + * @since 2019-03-17 + * @since v0.2.0 + */ + public static final class Builder { + /** + * A function that obtains a parseable object from a string. For example, an integer {@code ExpressionParser} + * would use {@code Integer::parseInt}. + * + * @since 2019-03-14 + * @since v0.2.0 + */ + private final Function objectObtainer; + + /** + * The function of the space as an operator (like 3 x y) + * + * @since 2019-03-22 + * @since v0.2.0 + */ + private String spaceFunction = null; + + /** + * A map mapping operator strings to operator functions, for unary operators. + * + * @since 2019-03-14 + * @since v0.2.0 + */ + private final Map> unaryOperators; + + /** + * A map mapping operator strings to operator functions, for binary operators. + * + * @since 2019-03-14 + * @since v0.2.0 + */ + private final Map> binaryOperators; + + /** + * Creates the {@code Builder}. + * + * @param objectObtainer + * a function that can turn strings into objects of the type handled by the parser. + * @throws NullPointerException + * if {@code objectObtainer} is null + * @since 2019-03-17 + * @since v0.2.0 + */ + public Builder(final Function objectObtainer) { + this.objectObtainer = Objects.requireNonNull(objectObtainer, "objectObtainer must not be null."); + this.unaryOperators = new HashMap<>(); + this.binaryOperators = new HashMap<>(); + } + + /** + * Adds a binary operator to the builder. + * + * @param text + * text used to reference the operator, like '+' + * @param operator + * operator to add + * @param priority + * operator's priority, which determines which operators are applied first + * @return this builder + * @throws NullPointerException + * if {@code text} or {@code operator} is null + * @since 2019-03-17 + * @since v0.2.0 + */ + public Builder addBinaryOperator(final String text, final BinaryOperator operator, final int priority) { + Objects.requireNonNull(text, "text must not be null."); + Objects.requireNonNull(operator, "operator must not be null."); + + // Unfortunately, I cannot use a lambda because the PriorityBinaryOperator requires arguments. + final PriorityBinaryOperator priorityOperator = new PriorityBinaryOperator(priority) { + @Override + public T apply(final T t, final T u) { + return operator.apply(t, u); + } + + }; + this.binaryOperators.put(text, priorityOperator); + return this; + } + + /** + * Adds a function for spaces. You must use the text of an existing binary operator. + * + * @param operator + * text of operator to use + * @return this builder + * @since 2019-03-22 + * @since v0.2.0 + */ + public Builder addSpaceFunction(final String operator) { + Objects.requireNonNull(operator, "operator must not be null."); + + if (!this.binaryOperators.containsKey(operator)) + throw new IllegalArgumentException(String.format("Could not find binary operator '%s'", operator)); + + this.spaceFunction = operator; + return this; + } + + /** + * Adds a unary operator to the builder. + * + * @param text + * text used to reference the operator, like '-' + * @param operator + * operator to add + * @param priority + * operator's priority, which determines which operators are applied first + * @return this builder + * @throws NullPointerException + * if {@code text} or {@code operator} is null + * @since 2019-03-17 + * @since v0.2.0 + */ + public Builder addUnaryOperator(final String text, final UnaryOperator operator, final int priority) { + Objects.requireNonNull(text, "text must not be null."); + Objects.requireNonNull(operator, "operator must not be null."); + + // Unfortunately, I cannot use a lambda because the PriorityUnaryOperator requires arguments. + final PriorityUnaryOperator priorityOperator = new PriorityUnaryOperator(priority) { + @Override + public T apply(final T t) { + return operator.apply(t); + } + }; + this.unaryOperators.put(text, priorityOperator); + return this; + } + + /** + * @return an {@code ExpressionParser} instance with the properties given to this builder + * @since 2019-03-17 + * @since v0.2.0 + */ + public ExpressionParser build() { + return new ExpressionParser<>(this.objectObtainer, this.unaryOperators, this.binaryOperators, + this.spaceFunction); + } + } + + /** + * A binary operator with a priority field that determines which operators apply first. + * + * @author Adrien Hopkins + * @param + * type of operand and result + * @since 2019-03-17 + * @since v0.2.0 + */ + private static abstract class PriorityBinaryOperator + implements BinaryOperator, Comparable> { + /** + * The operator's priority. Higher-priority operators are applied before lower-priority operators + * + * @since 2019-03-17 + * @since v0.2.0 + */ + private final int priority; + + /** + * Creates the {@code PriorityBinaryOperator}. + * + * @param priority + * operator's priority + * @since 2019-03-17 + * @since v0.2.0 + */ + public PriorityBinaryOperator(final int priority) { + this.priority = priority; + } + + /** + * Compares this object to another by priority. + * + *

+ * {@inheritDoc} + *

+ * + * @since 2019-03-17 + * @since v0.2.0 + */ + @Override + public int compareTo(final PriorityBinaryOperator o) { + if (this.priority < o.priority) + return -1; + else if (this.priority > o.priority) + return 1; + else + return 0; + } + + /** + * @return priority + * @since 2019-03-22 + * @since v0.2.0 + */ + public final int getPriority() { + return this.priority; + } + } + + /** + * A unary operator with a priority field that determines which operators apply first. + * + * @author Adrien Hopkins + * @param + * type of operand and result + * @since 2019-03-17 + * @since v0.2.0 + */ + private static abstract class PriorityUnaryOperator + implements UnaryOperator, Comparable> { + /** + * The operator's priority. Higher-priority operators are applied before lower-priority operators + * + * @since 2019-03-17 + * @since v0.2.0 + */ + private final int priority; + + /** + * Creates the {@code PriorityUnaryOperator}. + * + * @param priority + * operator's priority + * @since 2019-03-17 + * @since v0.2.0 + */ + public PriorityUnaryOperator(final int priority) { + this.priority = priority; + } + + /** + * Compares this object to another by priority. + * + *

+ * {@inheritDoc} + *

+ * + * @since 2019-03-17 + * @since v0.2.0 + */ + @Override + public int compareTo(final PriorityUnaryOperator o) { + if (this.priority < o.priority) + return -1; + else if (this.priority > o.priority) + return 1; + else + return 0; + } + + /** + * @return priority + * @since 2019-03-22 + * @since v0.2.0 + */ + public final int getPriority() { + return this.priority; + } + } + + /** + * The types of tokens that are available. + * + * @author Adrien Hopkins + * @since 2019-03-14 + * @since v0.2.0 + */ + private static enum TokenType { + OBJECT, UNARY_OPERATOR, BINARY_OPERATOR; + } + + /** + * The opening bracket. + * + * @since 2019-03-22 + * @since v0.2.0 + */ + public static final char OPENING_BRACKET = '('; + + /** + * The closing bracket. + * + * @since 2019-03-22 + * @since v0.2.0 + */ + public static final char CLOSING_BRACKET = ')'; + + /** + * Finds the other bracket in a pair of brackets, given the position of one. + * + * @param string + * string that contains brackets + * @param bracketPosition + * position of first bracket + * @return position of matching bracket + * @throws NullPointerException + * if string is null + * @since 2019-03-22 + * @since v0.2.0 + */ + private static int findBracketPair(final String string, final int bracketPosition) { + Objects.requireNonNull(string, "string must not be null."); + + final char openingBracket = string.charAt(bracketPosition); + + // figure out what closing bracket to look for + final char closingBracket; + switch (openingBracket) { + case '(': + closingBracket = ')'; + break; + case '[': + closingBracket = ']'; + break; + case '{': + closingBracket = '}'; + break; + default: + throw new IllegalArgumentException(String.format("Invalid bracket '%s'", openingBracket)); + } + + // level of brackets. every opening bracket increments this; every closing bracket decrements it + int bracketLevel = 0; + + // iterate over the string to find the closing bracket + for (int currentPosition = bracketPosition; currentPosition < string.length(); currentPosition++) { + final char currentCharacter = string.charAt(currentPosition); + + if (currentCharacter == openingBracket) { + bracketLevel++; + } else if (currentCharacter == closingBracket) { + bracketLevel--; + if (bracketLevel == 0) + return currentPosition; + } + } + + throw new IllegalArgumentException("No matching bracket found."); + } + + /** + * A function that obtains a parseable object from a string. For example, an integer {@code ExpressionParser} would + * use {@code Integer::parseInt}. + * + * @since 2019-03-14 + * @since v0.2.0 + */ + private final Function objectObtainer; + + /** + * A map mapping operator strings to operator functions, for unary operators. + * + * @since 2019-03-14 + * @since v0.2.0 + */ + private final Map> unaryOperators; + + /** + * A map mapping operator strings to operator functions, for binary operators. + * + * @since 2019-03-14 + * @since v0.2.0 + */ + private final Map> binaryOperators; + + /** + * The operator for space, or null if spaces have no function. + * + * @since 2019-03-22 + * @since v0.2.0 + */ + private final String spaceOperator; + + /** + * Creates the {@code ExpressionParser}. + * + * @param objectObtainer + * function to get objects from strings + * @param unaryOperators + * unary operators available to the parser + * @param binaryOperators + * binary operators available to the parser + * @param spaceOperator + * operator used by spaces + * @since 2019-03-14 + * @since v0.2.0 + */ + private ExpressionParser(final Function objectObtainer, + final Map> unaryOperators, + final Map> binaryOperators, final String spaceOperator) { + this.objectObtainer = objectObtainer; + this.unaryOperators = unaryOperators; + this.binaryOperators = binaryOperators; + this.spaceOperator = spaceOperator; + } + + /** + * Converts a given mathematical expression to reverse Polish notation (operators after operands). + *

+ * For example,
+ * {@code 2 * (3 + 4)}
+ * becomes
+ * {@code 2 3 4 + *}. + * + * @param expression + * expression + * @return expression in RPN + * @since 2019-03-17 + * @since v0.2.0 + */ + private String convertExpressionToReversePolish(final String expression) { + Objects.requireNonNull(expression, "expression must not be null."); + + final List components = new ArrayList<>(); + + // the part of the expression remaining to parse + String partialExpression = expression; + + // find and deal with brackets + while (partialExpression.indexOf(OPENING_BRACKET) != -1) { + final int openingBracketPosition = partialExpression.indexOf(OPENING_BRACKET); + final int closingBracketPosition = findBracketPair(partialExpression, openingBracketPosition); + + // check for function + if (openingBracketPosition > 0 && partialExpression.charAt(openingBracketPosition - 1) != ' ') { + // function like sin(2) or tempF(32) + // find the position of the last space + int spacePosition = openingBracketPosition; + while (spacePosition >= 0 && partialExpression.charAt(spacePosition) != ' ') { + spacePosition--; + } + // then split the function into pre-function and function, using the space position + components.addAll(Arrays.asList(partialExpression.substring(0, spacePosition + 1).split(" "))); + components.add(partialExpression.substring(spacePosition + 1, closingBracketPosition + 1)); + partialExpression = partialExpression.substring(closingBracketPosition + 1); + } else { + // normal brackets like (1 + 2) * (3 / 5) + components.addAll(Arrays.asList(partialExpression.substring(0, openingBracketPosition).split(" "))); + components.add(this.convertExpressionToReversePolish( + partialExpression.substring(openingBracketPosition + 1, closingBracketPosition))); + partialExpression = partialExpression.substring(closingBracketPosition + 1); + } + } + + // add everything else + components.addAll(Arrays.asList(partialExpression.split(" "))); + + // remove empty entries + while (components.contains("")) { + components.remove(""); + } + + // deal with space multiplication (x y) + if (this.spaceOperator != null) { + for (int i = 0; i < components.size() - 1; i++) { + if (this.getTokenType(components.get(i)) == TokenType.OBJECT + && this.getTokenType(components.get(i + 1)) == TokenType.OBJECT) { + components.add(++i, this.spaceOperator); + } + } + } + + // turn the expression into reverse Polish + while (true) { + final int highestPriorityOperatorPosition = this.findHighestPriorityOperatorPosition(components); + if (highestPriorityOperatorPosition == -1) { + break; + } + + // swap components based on what kind of operator there is + // 1 + 2 becomes 2 1 + + // - 1 becomes 1 - + switch (this.getTokenType(components.get(highestPriorityOperatorPosition))) { + case UNARY_OPERATOR: + final String unaryOperator = components.remove(highestPriorityOperatorPosition); + final String operand = components.remove(highestPriorityOperatorPosition); + components.add(highestPriorityOperatorPosition, operand + " " + unaryOperator); + break; + case BINARY_OPERATOR: + final String binaryOperator = components.remove(highestPriorityOperatorPosition); + final String operand1 = components.remove(highestPriorityOperatorPosition - 1); + final String operand2 = components.remove(highestPriorityOperatorPosition - 1); + components.add(highestPriorityOperatorPosition - 1, + operand2 + " " + operand1 + " " + binaryOperator); + break; + default: + throw new AssertionError("Expected operator, found non-operator."); + } + } + + // join all of the components together, then ensure there is only one space in a row + String expressionRPN = String.join(" ", components).replaceAll(" +", " "); + + while (expressionRPN.charAt(0) == ' ') { + expressionRPN = expressionRPN.substring(1); + } + while (expressionRPN.charAt(expressionRPN.length() - 1) == ' ') { + expressionRPN = expressionRPN.substring(0, expressionRPN.length() - 1); + } + return expressionRPN; + } + + /** + * Finds the position of the highest-priority operator in a list + * + * @param components + * components to test + * @param blacklist + * positions of operators that should be ignored + * @return position of highest priority, or -1 if the list contains no operators + * @throws NullPointerException + * if components is null + * @since 2019-03-22 + * @since v0.2.0 + */ + private int findHighestPriorityOperatorPosition(final List components) { + Objects.requireNonNull(components, "components must not be null."); + // find highest priority + int maxPriority = Integer.MIN_VALUE; + int maxPriorityPosition = -1; + + // go over components one by one + // if it is an operator, test its priority to see if it's max + // if it is, update maxPriority and maxPriorityPosition + for (int i = 0; i < components.size(); i++) { + + switch (this.getTokenType(components.get(i))) { + case UNARY_OPERATOR: + final PriorityUnaryOperator unaryOperator = this.unaryOperators.get(components.get(i)); + final int unaryPriority = unaryOperator.getPriority(); + + if (unaryPriority > maxPriority) { + maxPriority = unaryPriority; + maxPriorityPosition = i; + } + break; + case BINARY_OPERATOR: + final PriorityBinaryOperator binaryOperator = this.binaryOperators.get(components.get(i)); + final int binaryPriority = binaryOperator.getPriority(); + + if (binaryPriority > maxPriority) { + maxPriority = binaryPriority; + maxPriorityPosition = i; + } + break; + default: + break; + } + } + + // max priority position found + return maxPriorityPosition; + } + + /** + * Determines whether an inputted string is an object or an operator + * + * @param token + * string to input + * @return type of token it is + * @throws NullPointerException + * if {@code expression} is null + * @since 2019-03-14 + * @since v0.2.0 + */ + private TokenType getTokenType(final String token) { + Objects.requireNonNull(token, "token must not be null."); + + if (this.unaryOperators.containsKey(token)) + return TokenType.UNARY_OPERATOR; + else if (this.binaryOperators.containsKey(token)) + return TokenType.BINARY_OPERATOR; + else + return TokenType.OBJECT; + } + + /** + * Parses an expression. + * + * @param expression + * expression to parse + * @return result + * @throws NullPointerException + * if {@code expression} is null + * @since 2019-03-14 + * @since v0.2.0 + */ + public T parseExpression(final String expression) { + return this.parseReversePolishExpression(this.convertExpressionToReversePolish(expression)); + } + + /** + * Parses an expression expressed in reverse Polish notation. + * + * @param expression + * expression to parse + * @return result + * @throws NullPointerException + * if {@code expression} is null + * @since 2019-03-14 + * @since v0.2.0 + */ + private T parseReversePolishExpression(final String expression) { + Objects.requireNonNull(expression, "expression must not be null."); + + final Deque stack = new ArrayDeque<>(); + + // iterate over every item in the expression, then + for (final String item : expression.split(" ")) { + // choose a path based on what kind of thing was just read + switch (this.getTokenType(item)) { + + case BINARY_OPERATOR: + if (stack.size() < 2) + throw new IllegalStateException(String.format( + "Attempted to call binary operator %s with only %d arguments.", item, stack.size())); + + // get two arguments and operator, then apply! + final T o1 = stack.pop(); + final T o2 = stack.pop(); + final BinaryOperator binaryOperator = this.binaryOperators.get(item); + + stack.push(binaryOperator.apply(o1, o2)); + break; + + case OBJECT: + // just add it to the stack + stack.push(this.objectObtainer.apply(item)); + break; + + case UNARY_OPERATOR: + if (stack.size() < 1) + throw new IllegalStateException(String.format( + "Attempted to call unary operator %s with only %d arguments.", item, stack.size())); + + // get one argument and operator, then apply! + final T o = stack.pop(); + final UnaryOperator unaryOperator = this.unaryOperators.get(item); + + stack.push(unaryOperator.apply(o)); + break; + default: + throw new AssertionError( + String.format("Internal error: Invalid token type %s.", this.getTokenType(item))); + + } + } + + // return answer, or throw an exception if I can't + if (stack.size() > 1) + throw new IllegalStateException("Computation ended up with more than one answer."); + else if (stack.size() == 0) + throw new IllegalStateException("Computation ended up without an answer."); + return stack.pop(); + } +} diff --git a/src/main/java/org/unitConverter/math/ObjectProduct.java b/src/main/java/org/unitConverter/math/ObjectProduct.java new file mode 100644 index 0000000..5217d93 --- /dev/null +++ b/src/main/java/org/unitConverter/math/ObjectProduct.java @@ -0,0 +1,284 @@ +/** + * 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 . + */ +package org.unitConverter.math; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.Function; + +/** + * An immutable product of multiple objects of a type, such as base units. The objects can be multiplied and + * exponentiated. + * + * @author Adrien Hopkins + * @since 2019-10-16 + */ +public final class ObjectProduct { + /** + * Returns an empty ObjectProduct of a certain type + * + * @param + * type of objects that can be multiplied + * @return empty product + * @since 2019-10-16 + */ + public static final ObjectProduct empty() { + return new ObjectProduct<>(new HashMap<>()); + } + + /** + * Gets an {@code ObjectProduct} from an object-to-integer mapping + * + * @param + * type of object in product + * @param map + * map mapping objects to exponents + * @return object product + * @since 2019-10-16 + */ + public static final ObjectProduct fromExponentMapping(final Map map) { + return new ObjectProduct<>(new HashMap<>(map)); + } + + /** + * Gets an ObjectProduct that has one of the inputted argument, and nothing else. + * + * @param object + * object that will be in the product + * @return product + * @since 2019-10-16 + * @throws NullPointerException + * if object is null + */ + public static final ObjectProduct oneOf(final T object) { + Objects.requireNonNull(object, "object must not be null."); + final Map map = new HashMap<>(); + map.put(object, 1); + return new ObjectProduct<>(map); + } + + /** + * The objects that make up the product, mapped to their exponents. This map treats zero as null, and is immutable. + * + * @since 2019-10-16 + */ + final Map exponents; + + /** + * Creates the {@code ObjectProduct}. + * + * @param exponents + * objects that make up this product + * @since 2019-10-16 + */ + private ObjectProduct(final Map exponents) { + this.exponents = Collections.unmodifiableMap(ConditionalExistenceCollections.conditionalExistenceMap(exponents, + e -> !Integer.valueOf(0).equals(e.getValue()))); + } + + /** + * Calculates the quotient of two products + * + * @param other + * other product + * @return quotient of two products + * @since 2019-10-16 + * @throws NullPointerException + * if other is null + */ + public ObjectProduct dividedBy(final ObjectProduct other) { + Objects.requireNonNull(other, "other must not be null."); + // get a list of all objects in both sets + final Set objects = new HashSet<>(); + objects.addAll(this.getBaseSet()); + objects.addAll(other.getBaseSet()); + + // get a list of all exponents + final Map map = new HashMap<>(objects.size()); + for (final T key : objects) { + map.put(key, this.getExponent(key) - other.getExponent(key)); + } + + // create the product + return new ObjectProduct<>(map); + } + + // this method relies on the use of ZeroIsNullMap + @Override + public boolean equals(final Object obj) { + if (this == obj) + return true; + if (!(obj instanceof ObjectProduct)) + return false; + final ObjectProduct other = (ObjectProduct) obj; + return Objects.equals(this.exponents, other.exponents); + } + + /** + * @return immutable map mapping objects to exponents + * @since 2019-10-16 + */ + public Map exponentMap() { + return this.exponents; + } + + /** + * @return a set of all of the base objects with non-zero exponents that make up this dimension. + * @since 2018-12-12 + * @since v0.1.0 + */ + public final Set getBaseSet() { + final Set dimensions = new HashSet<>(); + + // add all dimensions with a nonzero exponent - zero exponents shouldn't be there in the first place + for (final T dimension : this.exponents.keySet()) { + if (!this.exponents.get(dimension).equals(0)) { + dimensions.add(dimension); + } + } + + return dimensions; + } + + /** + * Gets the exponent for a specific dimension. + * + * @param dimension + * dimension to check + * @return exponent for that dimension + * @since 2018-12-12 + * @since v0.1.0 + */ + public int getExponent(final T dimension) { + return this.exponents.getOrDefault(dimension, 0); + } + + @Override + public int hashCode() { + return Objects.hash(this.exponents); + } + + /** + * @return true if this product is a single object, i.e. it has one exponent of one and no other nonzero exponents + * @since 2019-10-16 + */ + public boolean isSingleObject() { + int oneCount = 0; + boolean twoOrMore = false; // has exponents of 2 or more + for (final T b : this.getBaseSet()) { + if (this.getExponent(b) == 1) { + oneCount++; + } else if (this.getExponent(b) != 0) { + twoOrMore = true; + } + } + return oneCount == 1 && !twoOrMore; + } + + /** + * Multiplies this product by another + * + * @param other + * other product + * @return product of two products + * @since 2019-10-16 + * @throws NullPointerException + * if other is null + */ + public ObjectProduct times(final ObjectProduct other) { + Objects.requireNonNull(other, "other must not be null."); + // get a list of all objects in both sets + final Set objects = new HashSet<>(); + objects.addAll(this.getBaseSet()); + objects.addAll(other.getBaseSet()); + + // get a list of all exponents + final Map map = new HashMap<>(objects.size()); + for (final T key : objects) { + map.put(key, this.getExponent(key) + other.getExponent(key)); + } + + // create the product + return new ObjectProduct<>(map); + } + + /** + * Returns this product, but to an exponent + * + * @param exponent + * exponent + * @return result of exponentiation + * @since 2019-10-16 + */ + public ObjectProduct toExponent(final int exponent) { + final Map map = new HashMap<>(this.exponents); + for (final T key : this.exponents.keySet()) { + map.put(key, this.getExponent(key) * exponent); + } + return new ObjectProduct<>(map); + } + + /** + * Converts this product to a string using the objects' {@link Object#toString()} method. If objects have a long + * toString representation, it is recommended to use {@link #toString(Function)} instead to shorten the returned + * string. + * + *

+ * {@inheritDoc} + */ + @Override + public String toString() { + return this.toString(Object::toString); + } + + /** + * Converts this product to a string. The objects that make up this product are represented by + * {@code objectToString} + * + * @param objectToString + * function to convert objects to strings + * @return string representation of product + * @since 2019-10-16 + */ + public String toString(final Function objectToString) { + final List positiveStringComponents = new ArrayList<>(); + final List negativeStringComponents = new ArrayList<>(); + + // for each base object that makes up this object, add it and its exponent + for (final T object : this.getBaseSet()) { + final int exponent = this.exponents.get(object); + if (exponent > 0) { + positiveStringComponents.add(String.format("%s^%d", objectToString.apply(object), exponent)); + } else if (exponent < 0) { + negativeStringComponents.add(String.format("%s^%d", objectToString.apply(object), -exponent)); + } + } + + final String positiveString = positiveStringComponents.isEmpty() ? "1" + : String.join(" * ", positiveStringComponents); + final String negativeString = negativeStringComponents.isEmpty() ? "" + : " / " + String.join(" * ", negativeStringComponents); + + return positiveString + negativeString; + } +} diff --git a/src/main/java/org/unitConverter/math/UncertainDouble.java b/src/main/java/org/unitConverter/math/UncertainDouble.java new file mode 100644 index 0000000..3651bd5 --- /dev/null +++ b/src/main/java/org/unitConverter/math/UncertainDouble.java @@ -0,0 +1,419 @@ +/** + * Copyright (C) 2020 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 . + */ +package org.unitConverter.math; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.Objects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * A double with an associated uncertainty value. For example, 3.2 ± 0.2. + *

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

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

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

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

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

+ * Examples: + * + *

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

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

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

+ * Examples: + * + *

+	 * UncertainDouble.of(3.27, 0.22).toString(false) = "3.3"
+	 * UncertainDouble.of(3.27, 0.22).toString(true) = "3.3 ± 0.2"
+	 * UncertainDouble.of(3.27, 0.13).toString(false) = "3.27"
+	 * UncertainDouble.of(3.27, 0.13).toString(true) = "3.27 ± 0.13"
+	 * UncertainDouble.of(-5.01, 0).toString(false) = "-5.01"
+	 * UncertainDouble.of(-5.01, 0).toString(true) = "-5.01 ± 0.0"
+	 * 
+ * + * @since 2020-09-07 + */ + public final String toString(boolean showUncertainty) { + String valueString, uncertaintyString; + + // generate the string representation of value and uncertainty + if (this.isExact()) { + uncertaintyString = "0.0"; + valueString = Double.toString(this.value); + + } else { + // round the value and uncertainty according to getDisplayScale() + final BigDecimal bigValue = BigDecimal.valueOf(this.value); + final BigDecimal bigUncertainty = BigDecimal.valueOf(this.uncertainty); + + final int displayScale = this.getDisplayScale(); + final BigDecimal roundedUncertainty = bigUncertainty + .setScale(displayScale, RoundingMode.HALF_EVEN); + final BigDecimal roundedValue = bigValue.setScale(displayScale, + RoundingMode.HALF_EVEN); + + valueString = roundedValue.toString(); + uncertaintyString = roundedUncertainty.toString(); + } + + // return "value" or "value ± uncertainty" depending on showUncertainty + return valueString + (showUncertainty ? " ± " + uncertaintyString : ""); + } + + /** + * @return absolute uncertainty + * @since 2020-09-07 + */ + public final double uncertainty() { + return this.uncertainty; + } + + /** + * @return value without uncertainty + * @since 2020-09-07 + */ + public final double value() { + return this.value; + } +} diff --git a/src/main/java/org/unitConverter/math/package-info.java b/src/main/java/org/unitConverter/math/package-info.java new file mode 100644 index 0000000..65727e4 --- /dev/null +++ b/src/main/java/org/unitConverter/math/package-info.java @@ -0,0 +1,24 @@ +/** + * Copyright (C) 2019 Adrien Hopkins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +/** + * Supplementary classes that are not related to units, but are necessary for their function. + * + * @author Adrien Hopkins + * @since 2019-03-14 + * @since v0.2.0 + */ +package org.unitConverter.math; \ No newline at end of file diff --git a/src/main/java/org/unitConverter/package-info.java b/src/main/java/org/unitConverter/package-info.java new file mode 100644 index 0000000..23dd165 --- /dev/null +++ b/src/main/java/org/unitConverter/package-info.java @@ -0,0 +1,24 @@ +/** + * Copyright (C) 2019 Adrien Hopkins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +/** + * A program that converts units. + * + * @author Adrien Hopkins + * @version v0.2.0 + * @since 2019-01-25 + */ +package org.unitConverter; \ No newline at end of file diff --git a/src/main/java/org/unitConverter/unit/BaseDimension.java b/src/main/java/org/unitConverter/unit/BaseDimension.java new file mode 100644 index 0000000..8e63a17 --- /dev/null +++ b/src/main/java/org/unitConverter/unit/BaseDimension.java @@ -0,0 +1,87 @@ +/** + * 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 . + */ +package org.unitConverter.unit; + +import java.util.Objects; + +/** + * A dimension that defines a {@code BaseUnit} + * + * @author Adrien Hopkins + * @since 2019-10-16 + */ +public final class BaseDimension { + /** + * Gets a {@code BaseDimension} with the provided name and symbol. + * + * @param name + * name of dimension + * @param symbol + * symbol used for dimension + * @return dimension + * @since 2019-10-16 + */ + public static BaseDimension valueOf(final String name, final String symbol) { + return new BaseDimension(name, symbol); + } + + /** + * The name of the dimension. + */ + private final String name; + /** + * The symbol used by the dimension. Symbols should be short, generally one or two characters. + */ + private final String symbol; + + /** + * Creates the {@code BaseDimension}. + * + * @param name + * name of unit + * @param symbol + * symbol of unit + * @throws NullPointerException + * if any argument is null + * @since 2019-10-16 + */ + private BaseDimension(final String name, final String symbol) { + this.name = Objects.requireNonNull(name, "name must not be null."); + this.symbol = Objects.requireNonNull(symbol, "symbol must not be null."); + } + + /** + * @return name + * @since 2019-10-16 + */ + public final String getName() { + return this.name; + } + + /** + * @return symbol + * @since 2019-10-16 + */ + public final String getSymbol() { + return this.symbol; + } + + @Override + public String toString() { + return String.format("%s (%s)", this.getName(), this.getSymbol()); + } +} diff --git a/src/main/java/org/unitConverter/unit/BaseUnit.java b/src/main/java/org/unitConverter/unit/BaseUnit.java new file mode 100644 index 0000000..6757bd0 --- /dev/null +++ b/src/main/java/org/unitConverter/unit/BaseUnit.java @@ -0,0 +1,133 @@ +/** + * 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 . + */ +package org.unitConverter.unit; + +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; + +/** + * A unit that other units are defined by. + *

+ * Note that BaseUnits must have names and symbols. This is because they + * are used for toString code. Therefore, the Optionals provided by + * {@link #getPrimaryName} and {@link #getSymbol} will always contain a value. + * + * @author Adrien Hopkins + * @since 2019-10-16 + */ +public final class BaseUnit extends Unit { + /** + * Gets a base unit from the dimension it measures, its name and its symbol. + * + * @param dimension dimension measured by this unit + * @param name name of unit + * @param symbol symbol of unit + * @return base unit + * @since 2019-10-16 + */ + public static BaseUnit valueOf(final BaseDimension dimension, + final String name, final String symbol) { + return new BaseUnit(dimension, name, symbol, new HashSet<>()); + } + + /** + * Gets a base unit from the dimension it measures, its name and its symbol. + * + * @param dimension dimension measured by this unit + * @param name name of unit + * @param symbol symbol of unit + * @return base unit + * @since 2019-10-21 + */ + public static BaseUnit valueOf(final BaseDimension dimension, + final String name, final String symbol, final Set otherNames) { + return new BaseUnit(dimension, name, symbol, otherNames); + } + + /** + * The dimension measured by this base unit. + */ + private final BaseDimension dimension; + + /** + * Creates the {@code BaseUnit}. + * + * @param dimension dimension of unit + * @param primaryName name of unit + * @param symbol symbol of unit + * @throws NullPointerException if any argument is null + * @since 2019-10-16 + */ + private BaseUnit(final BaseDimension dimension, final String primaryName, + final String symbol, final Set otherNames) { + super(primaryName, symbol, otherNames); + this.dimension = Objects.requireNonNull(dimension, + "dimension must not be null."); + } + + /** + * Returns a {@code LinearUnit} with this unit as a base and a conversion + * factor of 1. This operation must be done in order to allow units to be + * created with operations. + * + * @return this unit as a {@code LinearUnit} + * @since 2019-10-16 + */ + public LinearUnit asLinearUnit() { + return LinearUnit.valueOf(this.getBase(), 1); + } + + @Override + protected double convertFromBase(final double value) { + return value; + } + + @Override + protected double convertToBase(final double value) { + return value; + } + + /** + * @return dimension + * @since 2019-10-16 + */ + public final BaseDimension getBaseDimension() { + return this.dimension; + } + + @Override + public String toString() { + return this.getPrimaryName().orElse("Unnamed unit") + + (this.getSymbol().isPresent() + ? String.format(" (%s)", this.getSymbol().get()) + : ""); + } + + @Override + public BaseUnit withName(final NameSymbol ns) { + Objects.requireNonNull(ns, "ns must not be null."); + if (!ns.getPrimaryName().isPresent()) + throw new IllegalArgumentException( + "BaseUnits must have primary names."); + if (!ns.getSymbol().isPresent()) + throw new IllegalArgumentException("BaseUnits must have symbols."); + return BaseUnit.valueOf(this.getBaseDimension(), + ns.getPrimaryName().get(), ns.getSymbol().get(), + ns.getOtherNames()); + } +} diff --git a/src/main/java/org/unitConverter/unit/BritishImperial.java b/src/main/java/org/unitConverter/unit/BritishImperial.java new file mode 100644 index 0000000..ea23cd1 --- /dev/null +++ b/src/main/java/org/unitConverter/unit/BritishImperial.java @@ -0,0 +1,116 @@ +/** + * 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 . + */ +package org.unitConverter.unit; + +/** + * A static utility class that contains units in the British Imperial system. + * + * @author Adrien Hopkins + * @since 2019-10-21 + */ +public final class BritishImperial { + /** + * Imperial units that measure area + * + * @author Adrien Hopkins + * @since 2019-11-08 + */ + public static final class Area { + public static final LinearUnit SQUARE_FOOT = Length.FOOT.toExponent(2); + public static final LinearUnit SQUARE_YARD = Length.YARD.toExponent(2); + public static final LinearUnit SQUARE_MILE = Length.MILE.toExponent(2); + public static final LinearUnit PERCH = Length.ROD.times(Length.ROD); + public static final LinearUnit ROOD = Length.ROD.times(Length.FURLONG); + public static final LinearUnit ACRE = Length.FURLONG.times(Length.CHAIN); + } + + /** + * Imperial units that measure length + * + * @author Adrien Hopkins + * @since 2019-10-28 + */ + public static final class Length { + /** + * According to the International Yard and Pound of 1959, a yard is defined as exactly 0.9144 metres. + */ + public static final LinearUnit YARD = SI.METRE.times(0.9144); + public static final LinearUnit FOOT = YARD.dividedBy(3); + public static final LinearUnit INCH = FOOT.dividedBy(12); + public static final LinearUnit THOU = INCH.dividedBy(1000); + public static final LinearUnit CHAIN = YARD.times(22); + public static final LinearUnit FURLONG = CHAIN.times(10); + public static final LinearUnit MILE = FURLONG.times(8); + public static final LinearUnit LEAGUE = MILE.times(3); + + public static final LinearUnit NAUTICAL_MILE = SI.METRE.times(1852); + public static final LinearUnit CABLE = NAUTICAL_MILE.dividedBy(10); + public static final LinearUnit FATHOM = CABLE.dividedBy(100); + + public static final LinearUnit ROD = YARD.times(5.5); + public static final LinearUnit LINK = ROD.dividedBy(25); + } + + /** + * British Imperial units that measure mass. + * + * @author Adrien Hopkins + * @since 2019-11-08 + */ + public static final class Mass { + public static final LinearUnit POUND = SI.GRAM.times(453.59237); + public static final LinearUnit OUNCE = POUND.dividedBy(16); + public static final LinearUnit DRACHM = POUND.dividedBy(256); + public static final LinearUnit GRAIN = POUND.dividedBy(7000); + public static final LinearUnit STONE = POUND.times(14); + public static final LinearUnit QUARTER = STONE.times(2); + public static final LinearUnit HUNDREDWEIGHT = QUARTER.times(4); + public static final LinearUnit LONG_TON = HUNDREDWEIGHT.times(20); + public static final LinearUnit SLUG = SI.KILOGRAM.times(14.59390294); + } + + /** + * British Imperial units that measure volume + * + * @author Adrien Hopkins + * @since 2019-11-08 + */ + public static final class Volume { + public static final LinearUnit FLUID_OUNCE = SI.LITRE.withPrefix(SI.MILLI).times(28.4130625); + public static final LinearUnit GILL = FLUID_OUNCE.times(5); + public static final LinearUnit PINT = FLUID_OUNCE.times(20); + public static final LinearUnit QUART = PINT.times(2); + public static final LinearUnit GALLON = QUART.times(4); + public static final LinearUnit PECK = GALLON.times(2); + public static final LinearUnit BUSHEL = PECK.times(4); + + public static final LinearUnit CUBIC_INCH = Length.INCH.toExponent(3); + public static final LinearUnit CUBIC_FOOT = Length.FOOT.toExponent(3); + public static final LinearUnit CUBIC_YARD = Length.YARD.toExponent(3); + public static final LinearUnit ACRE_FOOT = Area.ACRE.times(Length.FOOT); + } + + public static final LinearUnit OUNCE_FORCE = Mass.OUNCE.times(SI.Constants.EARTH_GRAVITY); + public static final LinearUnit POUND_FORCE = Mass.POUND.times(SI.Constants.EARTH_GRAVITY); + + public static final LinearUnit BRITISH_THERMAL_UNIT = SI.JOULE.times(1055.06); + public static final LinearUnit CALORIE = SI.JOULE.times(4.184); + public static final LinearUnit KILOCALORIE = SI.JOULE.times(4184); + + public static final Unit FAHRENHEIT = Unit.fromConversionFunctions(SI.KELVIN.getBase(), + tempK -> tempK * 1.8 - 459.67, tempF -> (tempF + 459.67) / 1.8); +} diff --git a/src/main/java/org/unitConverter/unit/FunctionalUnit.java b/src/main/java/org/unitConverter/unit/FunctionalUnit.java new file mode 100644 index 0000000..586e0d7 --- /dev/null +++ b/src/main/java/org/unitConverter/unit/FunctionalUnit.java @@ -0,0 +1,109 @@ +/** + * 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 . + */ +package org.unitConverter.unit; + +import java.util.Objects; +import java.util.function.DoubleUnaryOperator; + +import org.unitConverter.math.ObjectProduct; + +/** + * A unit that uses functional objects to convert to and from its base. + * + * @author Adrien Hopkins + * @since 2019-05-22 + */ +final class FunctionalUnit extends Unit { + /** + * A function that accepts a value expressed in the unit's base and returns that value expressed in this unit. + * + * @since 2019-05-22 + */ + private final DoubleUnaryOperator converterFrom; + + /** + * A function that accepts a value expressed in the unit and returns that value expressed in the unit's base. + * + * @since 2019-05-22 + */ + private final DoubleUnaryOperator converterTo; + + /** + * Creates the {@code FunctionalUnit}. + * + * @param base + * unit's base + * @param converterFrom + * function that accepts a value expressed in the unit's base and returns that value expressed in this + * unit. + * @param converterTo + * function that accepts a value expressed in the unit and returns that value expressed in the unit's + * base. + * @throws NullPointerException + * if any argument is null + * @since 2019-05-22 + */ + public FunctionalUnit(final ObjectProduct base, final DoubleUnaryOperator converterFrom, + final DoubleUnaryOperator converterTo) { + super(base, NameSymbol.EMPTY); + this.converterFrom = Objects.requireNonNull(converterFrom, "converterFrom must not be null."); + this.converterTo = Objects.requireNonNull(converterTo, "converterTo must not be null."); + } + + /** + * Creates the {@code FunctionalUnit}. + * + * @param base + * unit's base + * @param converterFrom + * function that accepts a value expressed in the unit's base and returns that value expressed in this + * unit. + * @param converterTo + * function that accepts a value expressed in the unit and returns that value expressed in the unit's + * base. + * @throws NullPointerException + * if any argument is null + * @since 2019-05-22 + */ + public FunctionalUnit(final ObjectProduct base, final DoubleUnaryOperator converterFrom, + final DoubleUnaryOperator converterTo, final NameSymbol ns) { + super(base, ns); + this.converterFrom = Objects.requireNonNull(converterFrom, "converterFrom must not be null."); + this.converterTo = Objects.requireNonNull(converterTo, "converterTo must not be null."); + } + + /** + * {@inheritDoc} + * + * Uses {@code converterFrom} to convert. + */ + @Override + public double convertFromBase(final double value) { + return this.converterFrom.applyAsDouble(value); + } + + /** + * {@inheritDoc} + * + * Uses {@code converterTo} to convert. + */ + @Override + public double convertToBase(final double value) { + return this.converterTo.applyAsDouble(value); + } + +} diff --git a/src/main/java/org/unitConverter/unit/FunctionalUnitlike.java b/src/main/java/org/unitConverter/unit/FunctionalUnitlike.java new file mode 100644 index 0000000..21c1fca --- /dev/null +++ b/src/main/java/org/unitConverter/unit/FunctionalUnitlike.java @@ -0,0 +1,72 @@ +/** + * Copyright (C) 2020 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 . + */ +package org.unitConverter.unit; + +import java.util.function.DoubleFunction; +import java.util.function.ToDoubleFunction; + +import org.unitConverter.math.ObjectProduct; + +/** + * A unitlike form that converts using two conversion functions. + * + * @since 2020-09-07 + */ +final class FunctionalUnitlike extends Unitlike { + /** + * A function that accepts a value in the unitlike form's base and returns a + * value in the unitlike form. + * + * @since 2020-09-07 + */ + private final DoubleFunction converterFrom; + + /** + * A function that accepts a value in the unitlike form and returns a value + * in the unitlike form's base. + */ + private final ToDoubleFunction converterTo; + + /** + * Creates the {@code FunctionalUnitlike}. + * + * @param base unitlike form's base + * @param converterFrom function that accepts a value in the unitlike form's + * base and returns a value in the unitlike form. + * @param converterTo function that accepts a value in the unitlike form + * and returns a value in the unitlike form's base. + * @throws NullPointerException if any argument is null + * @since 2019-05-22 + */ + protected FunctionalUnitlike(ObjectProduct unitBase, NameSymbol ns, + DoubleFunction converterFrom, ToDoubleFunction converterTo) { + super(unitBase, ns); + this.converterFrom = converterFrom; + this.converterTo = converterTo; + } + + @Override + protected V convertFromBase(double value) { + return this.converterFrom.apply(value); + } + + @Override + protected double convertToBase(V value) { + return this.converterTo.applyAsDouble(value); + } + +} diff --git a/src/main/java/org/unitConverter/unit/LinearUnit.java b/src/main/java/org/unitConverter/unit/LinearUnit.java new file mode 100644 index 0000000..b7f33d5 --- /dev/null +++ b/src/main/java/org/unitConverter/unit/LinearUnit.java @@ -0,0 +1,441 @@ +/** + * 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 . + */ +package org.unitConverter.unit; + +import java.util.Objects; + +import org.unitConverter.math.DecimalComparison; +import org.unitConverter.math.ObjectProduct; +import org.unitConverter.math.UncertainDouble; + +/** + * A unit that can be expressed as a product of its base and a number. For + * example, kilometres, inches and pounds. + * + * @author Adrien Hopkins + * @since 2019-10-16 + */ +public final class LinearUnit extends Unit { + /** + * Gets a {@code LinearUnit} from a unit and a value. For example, converts + * '59 °F' to a linear unit with the value of '288.15 K' + * + * @param unit unit to convert + * @param value value to convert + * @return value expressed as a {@code LinearUnit} + * @since 2019-10-16 + * @throws NullPointerException if unit is null + */ + public static LinearUnit fromUnitValue(final Unit unit, final double value) { + return new LinearUnit( + Objects.requireNonNull(unit, "unit must not be null.").getBase(), + unit.convertToBase(value), NameSymbol.EMPTY); + } + + /** + * Gets a {@code LinearUnit} from a unit and a value. For example, converts + * '59 °F' to a linear unit with the value of '288.15 K' + * + * @param unit unit to convert + * @param value value to convert + * @param ns name(s) and symbol of unit + * @return value expressed as a {@code LinearUnit} + * @since 2019-10-21 + * @throws NullPointerException if unit or ns is null + */ + public static LinearUnit fromUnitValue(final Unit unit, final double value, + final NameSymbol ns) { + return new LinearUnit( + Objects.requireNonNull(unit, "unit must not be null.").getBase(), + unit.convertToBase(value), ns); + } + + /** + * @return the base unit associated with {@code unit}, as a + * {@code LinearUnit}. + * @since 2020-10-02 + */ + public static LinearUnit getBase(final Unit unit) { + return new LinearUnit(unit.getBase(), 1, NameSymbol.EMPTY); + } + + /** + * @return the base unit associated with {@code unitlike}, as a + * {@code LinearUnit}. + * @since 2020-10-02 + */ + public static LinearUnit getBase(final Unitlike unit) { + return new LinearUnit(unit.getBase(), 1, NameSymbol.EMPTY); + } + + /** + * Gets a {@code LinearUnit} from a unit base and a conversion factor. In + * other words, gets the product of {@code unitBase} and + * {@code conversionFactor}, expressed as a {@code LinearUnit}. + * + * @param unitBase unit base to multiply by + * @param conversionFactor number to multiply base by + * @return product of base and conversion factor + * @since 2019-10-16 + * @throws NullPointerException if unitBase is null + */ + public static LinearUnit valueOf(final ObjectProduct unitBase, + final double conversionFactor) { + return new LinearUnit(unitBase, conversionFactor, NameSymbol.EMPTY); + } + + /** + * Gets a {@code LinearUnit} from a unit base and a conversion factor. In + * other words, gets the product of {@code unitBase} and + * {@code conversionFactor}, expressed as a {@code LinearUnit}. + * + * @param unitBase unit base to multiply by + * @param conversionFactor number to multiply base by + * @param ns name(s) and symbol of unit + * @return product of base and conversion factor + * @since 2019-10-21 + * @throws NullPointerException if unitBase is null + */ + public static LinearUnit valueOf(final ObjectProduct unitBase, + final double conversionFactor, final NameSymbol ns) { + return new LinearUnit(unitBase, conversionFactor, ns); + } + + /** + * The value of this unit as represented in its base form. Mathematically, + * + *

+	 * this = conversionFactor * getBase()
+	 * 
+ * + * @since 2019-10-16 + */ + private final double conversionFactor; + + /** + * Creates the {@code LinearUnit}. + * + * @param unitBase base of linear unit + * @param conversionFactor conversion factor between base and unit + * @since 2019-10-16 + */ + private LinearUnit(final ObjectProduct unitBase, + final double conversionFactor, final NameSymbol ns) { + super(unitBase, ns); + this.conversionFactor = conversionFactor; + } + + /** + * {@inheritDoc} + * + * Converts by dividing by {@code conversionFactor} + */ + @Override + protected double convertFromBase(final double value) { + return value / this.getConversionFactor(); + } + + /** + * Converts an {@code UncertainDouble} value expressed in this unit to an + * {@code UncertainValue} value expressed in {@code other}. + * + * @param other unit to convert to + * @param value value to convert + * @return converted value + * @since 2019-09-07 + * @throws IllegalArgumentException if {@code other} is incompatible for + * conversion with this unit (as tested by + * {@link Unit#canConvertTo}). + * @throws NullPointerException if value or other is null + */ + public UncertainDouble convertTo(LinearUnit other, UncertainDouble value) { + Objects.requireNonNull(other, "other must not be null."); + Objects.requireNonNull(value, "value may not be null."); + if (this.canConvertTo(other)) + return value.timesExact( + this.getConversionFactor() / other.getConversionFactor()); + else + throw new IllegalArgumentException( + String.format("Cannot convert from %s to %s.", this, other)); + + } + + /** + * {@inheritDoc} + * + * Converts by multiplying by {@code conversionFactor} + */ + @Override + protected double convertToBase(final double value) { + return value * this.getConversionFactor(); + } + + /** + * Converts an {@code UncertainDouble} to the base unit. + * + * @since 2020-09-07 + */ + UncertainDouble convertToBase(final UncertainDouble value) { + return value.timesExact(this.getConversionFactor()); + } + + /** + * Divides this unit by a scalar. + * + * @param divisor scalar to divide by + * @return quotient + * @since 2018-12-23 + * @since v0.1.0 + */ + public LinearUnit dividedBy(final double divisor) { + return valueOf(this.getBase(), this.getConversionFactor() / divisor); + } + + /** + * Returns the quotient of this unit and another. + * + * @param divisor unit to divide by + * @return quotient of two units + * @throws NullPointerException if {@code divisor} is null + * @since 2018-12-22 + * @since v0.1.0 + */ + public LinearUnit dividedBy(final LinearUnit divisor) { + Objects.requireNonNull(divisor, "other must not be null"); + + // divide the units + final ObjectProduct base = this.getBase() + .dividedBy(divisor.getBase()); + return valueOf(base, + this.getConversionFactor() / divisor.getConversionFactor()); + } + + /** + * {@inheritDoc} + * + * Uses the base and conversion factor of units to test for equality. + */ + @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()) + && DecimalComparison.equals(this.getConversionFactor(), + other.getConversionFactor()); + } + + /** + * @return conversion factor + * @since 2019-10-16 + */ + public double getConversionFactor() { + return this.conversionFactor; + } + + /** + * {@inheritDoc} + * + * Uses the base and conversion factor to compute a hash code. + */ + @Override + public int hashCode() { + return 31 * this.getBase().hashCode() + + DecimalComparison.hash(this.getConversionFactor()); + } + + /** + * @return whether this unit is equivalent to a {@code BaseUnit} (i.e. there + * is a {@code BaseUnit b} where + * {@code b.asLinearUnit().equals(this)} returns {@code true}.) + * @since 2019-10-16 + */ + public boolean isBase() { + return this.isCoherent() && this.getBase().isSingleObject(); + } + + /** + * @return whether this unit is coherent (i.e. has conversion factor 1) + * @since 2019-10-16 + */ + public boolean isCoherent() { + return this.getConversionFactor() == 1; + } + + /** + * Returns the difference of this unit and another. + *

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

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

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

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

+ * If this unit and the provided prefix have a primary name, the returned + * unit will have a primary name (prefix's name + unit's name).
+ * If this unit and the provided prefix have a symbol, the returned unit will + * have a symbol.
+ * This method ignores alternate names of both this unit and the provided + * prefix. + * + * @param prefix prefix to apply + * @return unit with prefix + * @since 2019-03-18 + * @since v0.2.0 + * @throws NullPointerException if prefix is null + */ + public LinearUnit withPrefix(final UnitPrefix prefix) { + final LinearUnit unit = this.times(prefix.getMultiplier()); + + // create new name and symbol, if possible + final String name; + if (this.getPrimaryName().isPresent() + && prefix.getPrimaryName().isPresent()) { + name = prefix.getPrimaryName().get() + this.getPrimaryName().get(); + } else { + name = null; + } + + final String symbol; + if (this.getSymbol().isPresent() && prefix.getSymbol().isPresent()) { + symbol = prefix.getSymbol().get() + this.getSymbol().get(); + } else { + symbol = null; + } + + return unit.withName(NameSymbol.ofNullable(name, symbol)); + } +} diff --git a/src/main/java/org/unitConverter/unit/LinearUnitValue.java b/src/main/java/org/unitConverter/unit/LinearUnitValue.java new file mode 100644 index 0000000..8de734e --- /dev/null +++ b/src/main/java/org/unitConverter/unit/LinearUnitValue.java @@ -0,0 +1,341 @@ +/** + * 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 . + */ +package org.unitConverter.unit; + +import java.util.Objects; +import java.util.Optional; + +import org.unitConverter.math.DecimalComparison; +import org.unitConverter.math.UncertainDouble; + +/** + * A possibly uncertain value expressed in a linear unit. + * + * Unless otherwise indicated, all methods in this class throw a + * {@code NullPointerException} when an argument is null. + * + * @author Adrien Hopkins + * @since 2020-07-26 + */ +public final class LinearUnitValue { + public static final LinearUnitValue ONE = getExact(SI.ONE, 1); + + /** + * Gets an exact {@code LinearUnitValue} + * + * @param unit unit to express with + * @param value value to express + * @return exact {@code LinearUnitValue} instance + * @since 2020-07-26 + */ + public static final LinearUnitValue getExact(final LinearUnit unit, + final double value) { + return new LinearUnitValue( + Objects.requireNonNull(unit, "unit must not be null"), + UncertainDouble.of(value, 0)); + } + + /** + * Gets an uncertain {@code LinearUnitValue} + * + * @param unit unit to express with + * @param value value to express + * @param uncertainty absolute uncertainty of value + * @return uncertain {@code LinearUnitValue} instance + * @since 2020-07-26 + */ + public static final LinearUnitValue of(final LinearUnit unit, + final UncertainDouble value) { + return new LinearUnitValue( + Objects.requireNonNull(unit, "unit must not be null"), + Objects.requireNonNull(value, "value may not be null")); + } + + private final LinearUnit unit; + + private final UncertainDouble value; + + /** + * @param unit unit to express as + * @param value value to express + * @since 2020-07-26 + */ + private LinearUnitValue(final LinearUnit unit, final UncertainDouble value) { + this.unit = unit; + this.value = value; + } + + /** + * @return this value as a {@code UnitValue}. All uncertainty information is + * removed from the returned value. + * @since 2020-08-04 + */ + public final UnitValue asUnitValue() { + return UnitValue.of(this.unit, this.value.value()); + } + + /** + * @param other a {@code LinearUnit} + * @return true iff this value can be represented with {@code other}. + * @since 2020-07-26 + */ + public final boolean canConvertTo(final LinearUnit other) { + return this.unit.canConvertTo(other); + } + + /** + * Returns a LinearUnitValue that represents the same value expressed in a + * different unit + * + * @param other new unit to express value in + * @return value expressed in {@code other} + * @since 2020-07-26 + */ + public final LinearUnitValue convertTo(final LinearUnit other) { + return LinearUnitValue.of(other, this.unit.convertTo(other, this.value)); + } + + /** + * Divides this value by a scalar + * + * @param divisor value to divide by + * @return multiplied value + * @since 2020-07-28 + */ + public LinearUnitValue dividedBy(final double divisor) { + return LinearUnitValue.of(this.unit, this.value.dividedByExact(divisor)); + } + + /** + * Divides this value by another value + * + * @param divisor value to multiply by + * @return quotient + * @since 2020-07-28 + */ + public LinearUnitValue dividedBy(final LinearUnitValue divisor) { + return LinearUnitValue.of(this.unit.dividedBy(divisor.unit), + this.value.dividedBy(divisor.value)); + } + + /** + * Returns true if this and obj represent the same value, regardless of + * whether or not they are expressed in the same unit. So (1000 m).equals(1 + * km) returns true. + * + * @since 2020-07-26 + * @see #equals(Object, boolean) + */ + @Override + public boolean equals(final Object obj) { + if (!(obj instanceof LinearUnitValue)) + return false; + final LinearUnitValue other = (LinearUnitValue) obj; + return Objects.equals(this.unit.getBase(), other.unit.getBase()) + && this.unit.convertToBase(this.value) + .equals(other.unit.convertToBase(other.value)); + } + + /** + * Returns true if this and obj represent the same value, regardless of + * whether or not they are expressed in the same unit. So (1000 m).equals(1 + * km) returns true. + *

+ * If avoidFPErrors is true, this method will attempt to avoid floating-point + * errors, at the cost of not always being transitive. + * + * @since 2020-07-28 + */ + public boolean equals(final Object obj, final boolean avoidFPErrors) { + if (!avoidFPErrors) + return this.equals(obj); + if (!(obj instanceof LinearUnitValue)) + return false; + final LinearUnitValue other = (LinearUnitValue) obj; + return Objects.equals(this.unit.getBase(), other.unit.getBase()) + && DecimalComparison.equals(this.unit.convertToBase(this.value), + other.unit.convertToBase(other.value)); + } + + /** + * @param other another {@code LinearUnitValue} + * @return true iff this and other are within each other's uncertainty range + * + * @since 2020-07-26 + */ + public boolean equivalent(final LinearUnitValue other) { + if (other == null + || !Objects.equals(this.unit.getBase(), other.unit.getBase())) + return false; + final LinearUnit base = LinearUnit.valueOf(this.unit.getBase(), 1); + final LinearUnitValue thisBase = this.convertTo(base); + final LinearUnitValue otherBase = other.convertTo(base); + + return thisBase.value.equivalent(otherBase.value); + } + + /** + * @return the unit + * @since 2020-09-29 + */ + public final LinearUnit getUnit() { + return this.unit; + } + + /** + * @return the value + * @since 2020-09-29 + */ + public final UncertainDouble getValue() { + return this.value; + } + + /** + * @return the exact value + * @since 2020-09-07 + */ + public final double getValueExact() { + return this.value.value(); + } + + @Override + public int hashCode() { + return Objects.hash(this.unit.getBase(), + this.unit.convertToBase(this.getValue())); + } + + /** + * Returns the difference of this value and another, expressed in this + * value's unit + * + * @param subtrahend value to subtract + * @return difference of values + * @throws IllegalArgumentException if {@code subtrahend} has a unit that is + * not compatible for addition + * @since 2020-07-26 + */ + public LinearUnitValue minus(final LinearUnitValue subtrahend) { + Objects.requireNonNull(subtrahend, "subtrahend may not be null"); + + if (!this.canConvertTo(subtrahend.unit)) + throw new IllegalArgumentException(String.format( + "Incompatible units for subtraction \"%s\" and \"%s\".", + this.unit, subtrahend.unit)); + + final LinearUnitValue otherConverted = subtrahend.convertTo(this.unit); + return LinearUnitValue.of(this.unit, + this.value.minus(otherConverted.value)); + } + + /** + * Returns the sum of this value and another, expressed in this value's unit + * + * @param addend value to add + * @return sum of values + * @throws IllegalArgumentException if {@code addend} has a unit that is not + * compatible for addition + * @since 2020-07-26 + */ + public LinearUnitValue plus(final LinearUnitValue addend) { + Objects.requireNonNull(addend, "addend may not be null"); + + if (!this.canConvertTo(addend.unit)) + throw new IllegalArgumentException(String.format( + "Incompatible units for addition \"%s\" and \"%s\".", this.unit, + addend.unit)); + + final LinearUnitValue otherConverted = addend.convertTo(this.unit); + return LinearUnitValue.of(this.unit, + this.value.plus(otherConverted.value)); + } + + /** + * Multiplies this value by a scalar + * + * @param multiplier value to multiply by + * @return multiplied value + * @since 2020-07-28 + */ + public LinearUnitValue times(final double multiplier) { + return LinearUnitValue.of(this.unit, this.value.timesExact(multiplier)); + } + + /** + * Multiplies this value by another value + * + * @param multiplier value to multiply by + * @return product + * @since 2020-07-28 + */ + public LinearUnitValue times(final LinearUnitValue multiplier) { + return LinearUnitValue.of(this.unit.times(multiplier.unit), + this.value.times(multiplier.value)); + } + + /** + * Raises a value to an exponent + * + * @param exponent exponent to raise to + * @return result of exponentiation + * @since 2020-07-28 + */ + public LinearUnitValue toExponent(final int exponent) { + return LinearUnitValue.of(this.unit.toExponent(exponent), + this.value.toExponentExact(exponent)); + } + + @Override + public String toString() { + return this.toString(!this.value.isExact()); + } + + /** + * Returns a string representing the object.
+ * If the attached unit has a name or symbol, the string looks like "12 km". + * Otherwise, it looks like "13 unnamed unit (= 2 m/s)". + *

+ * If showUncertainty is true, strings like "35 ± 8" are shown instead of + * single numbers. + *

+ * Non-exact values are rounded intelligently based on their uncertainty. + * + * @since 2020-07-26 + */ + public String toString(final boolean showUncertainty) { + final Optional primaryName = this.unit.getPrimaryName(); + final Optional symbol = this.unit.getSymbol(); + final String chosenName = symbol.orElse(primaryName.orElse(null)); + + final UncertainDouble baseValue = this.unit.convertToBase(this.value); + + // get rounded strings + // if showUncertainty is true, add brackets around the string + final String valueString = showUncertainty ? "(" + : "" + this.value.toString(showUncertainty) + + (showUncertainty ? ")" : ""); + final String baseValueString = showUncertainty ? "(" + : "" + baseValue.toString(showUncertainty) + + (showUncertainty ? ")" : ""); + + // create string + if (primaryName.isEmpty() && symbol.isEmpty()) + return String.format("%s unnamed unit (= %s %s)", valueString, + baseValueString, this.unit.getBase()); + else + return String.format("%s %s", valueString, chosenName); + } +} diff --git a/src/main/java/org/unitConverter/unit/MultiUnit.java b/src/main/java/org/unitConverter/unit/MultiUnit.java new file mode 100644 index 0000000..a1623f8 --- /dev/null +++ b/src/main/java/org/unitConverter/unit/MultiUnit.java @@ -0,0 +1,160 @@ +/** + * Copyright (C) 2020 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 . + */ +package org.unitConverter.unit; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.unitConverter.math.ObjectProduct; + +/** + * A combination of units, like "5 foot + 7 inch". All but the last units should + * have a whole number value associated with them. + * + * @since 2020-10-02 + */ +public final class MultiUnit extends Unitlike> { + /** + * Creates a {@code MultiUnit} from its units. It will not have a name or + * symbol. + * + * @since 2020-10-03 + */ + public static final MultiUnit of(LinearUnit... units) { + return of(Arrays.asList(units)); + } + + /** + * Creates a {@code MultiUnit} from its units. It will not have a name or + * symbol. + * + * @since 2020-10-03 + */ + public static final MultiUnit of(List units) { + if (units.size() < 1) + throw new IllegalArgumentException("Must have at least one unit"); + final ObjectProduct unitBase = units.get(0).getBase(); + for (final LinearUnit unit : units) { + if (!unitBase.equals(unit.getBase())) + throw new IllegalArgumentException( + "All units must have the same base."); + } + return new MultiUnit(new ArrayList<>(units), unitBase, NameSymbol.EMPTY); + } + + /** + * The units that make up this value. + */ + private final List units; + + /** + * Creates a {@code MultiUnit}. + * + * @since 2020-10-03 + */ + private MultiUnit(List units, ObjectProduct unitBase, + NameSymbol ns) { + super(unitBase, ns); + this.units = units; + } + + @Override + protected List convertFromBase(double value) { + final List values = new ArrayList<>(this.units.size()); + double temp = value; + + for (final LinearUnit unit : this.units.subList(0, + this.units.size() - 1)) { + values.add(Math.floor(temp / unit.getConversionFactor())); + temp %= unit.getConversionFactor(); + } + + values.add(this.units.size() - 1, + this.units.get(this.units.size() - 1).convertFromBase(temp)); + + return values; + } + + /** + * Converts a value expressed in this unitlike form to a value expressed in + * {@code other}. + * + * @implSpec If conversion is possible, this implementation returns + * {@code other.convertFromBase(this.convertToBase(value))}. + * Therefore, overriding either of those methods will change the + * output of this method. + * + * @param other unit to convert to + * @param value value to convert + * @return converted value + * @since 2020-10-03 + * @throws IllegalArgumentException if {@code other} is incompatible for + * conversion with this unitlike form (as + * tested by {@link Unit#canConvertTo}). + * @throws NullPointerException if other is null + */ + public final , V> V convertTo(U other, + double... values) { + final List valueList = new ArrayList<>(values.length); + for (final double d : values) { + valueList.add(d); + } + + return this.convertTo(other, valueList); + } + + /** + * Converts a value expressed in this unitlike form to a value expressed in + * {@code other}. + * + * @implSpec If conversion is possible, this implementation returns + * {@code other.convertFromBase(this.convertToBase(value))}. + * Therefore, overriding either of those methods will change the + * output of this method. + * + * @param other unit to convert to + * @param value value to convert + * @return converted value + * @since 2020-10-03 + * @throws IllegalArgumentException if {@code other} is incompatible for + * conversion with this unitlike form (as + * tested by {@link Unit#canConvertTo}). + * @throws NullPointerException if other is null + */ + public final double convertTo(Unit other, double... values) { + final List valueList = new ArrayList<>(values.length); + for (final double d : values) { + valueList.add(d); + } + + return this.convertTo(other, valueList); + } + + @Override + protected double convertToBase(List value) { + if (value.size() != this.units.size()) + throw new IllegalArgumentException("Wrong number of values for " + + this.units.size() + "-unit MultiUnit."); + + double baseValue = 0; + for (int i = 0; i < this.units.size(); i++) { + baseValue += value.get(i) * this.units.get(i).getConversionFactor(); + } + return baseValue; + } +} diff --git a/src/main/java/org/unitConverter/unit/NameSymbol.java b/src/main/java/org/unitConverter/unit/NameSymbol.java new file mode 100644 index 0000000..8d8302a --- /dev/null +++ b/src/main/java/org/unitConverter/unit/NameSymbol.java @@ -0,0 +1,280 @@ +/** + * 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 . + */ +package org.unitConverter.unit; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; + +/** + * A class that can be used to specify names and a symbol for a unit. + * + * @author Adrien Hopkins + * @since 2019-10-21 + */ +public final class NameSymbol { + public static final NameSymbol EMPTY = new NameSymbol(Optional.empty(), + Optional.empty(), new HashSet<>()); + + /** + * Creates a {@code NameSymbol}, ensuring that if primaryName is null and + * otherNames is not empty, one name is moved from otherNames to primaryName + * + * Ensure that otherNames is a copy of the inputted argument. + */ + private static final NameSymbol create(final String name, + final String symbol, final Set otherNames) { + final Optional primaryName; + + if (name == null && !otherNames.isEmpty()) { + // get primary name and remove it from savedNames + final Iterator it = otherNames.iterator(); + assert it.hasNext(); + primaryName = Optional.of(it.next()); + otherNames.remove(primaryName.get()); + } else { + primaryName = Optional.ofNullable(name); + } + + return new NameSymbol(primaryName, Optional.ofNullable(symbol), + otherNames); + } + + /** + * Gets a {@code NameSymbol} with a primary name, a symbol and no other + * names. + * + * @param name name to use + * @param symbol symbol to use + * @return NameSymbol instance + * @since 2019-10-21 + * @throws NullPointerException if name or symbol is null + */ + public static final NameSymbol of(final String name, final String symbol) { + return new NameSymbol(Optional.of(name), Optional.of(symbol), + new HashSet<>()); + } + + /** + * Gets a {@code NameSymbol} with a primary name, a symbol and additional + * names. + * + * @param name name to use + * @param symbol symbol to use + * @param otherNames other names to use + * @return NameSymbol instance + * @since 2019-10-21 + * @throws NullPointerException if any argument is null + */ + public static final NameSymbol of(final String name, final String symbol, + final Set otherNames) { + return new NameSymbol(Optional.of(name), Optional.of(symbol), + new HashSet<>(Objects.requireNonNull(otherNames, + "otherNames must not be null."))); + } + + /** + * h * Gets a {@code NameSymbol} with a primary name, a symbol and additional + * names. + * + * @param name name to use + * @param symbol symbol to use + * @param otherNames other names to use + * @return NameSymbol instance + * @since 2019-10-21 + * @throws NullPointerException if any argument is null + */ + public static final NameSymbol of(final String name, final String symbol, + final String... otherNames) { + return new NameSymbol(Optional.of(name), Optional.of(symbol), + new HashSet<>(Arrays.asList(Objects.requireNonNull(otherNames, + "otherNames must not be null.")))); + } + + /** + * Gets a {@code NameSymbol} with a primary name, no symbol, and no other + * names. + * + * @param name name to use + * @return NameSymbol instance + * @since 2019-10-21 + * @throws NullPointerException if name is null + */ + public static final NameSymbol ofName(final String name) { + return new NameSymbol(Optional.of(name), Optional.empty(), + new HashSet<>()); + } + + /** + * Gets a {@code NameSymbol} with a primary name, a symbol and additional + * names. + *

+ * If any argument is null, this static factory replaces it with an empty + * Optional or empty Set. + *

+ * If {@code name} is null and {@code otherNames} is not empty, a primary + * name will be picked from {@code otherNames}. This name will not appear in + * getOtherNames(). + * + * @param name name to use + * @param symbol symbol to use + * @param otherNames other names to use + * @return NameSymbol instance + * @since 2019-11-26 + */ + public static final NameSymbol ofNullable(final String name, + final String symbol, final Set otherNames) { + return NameSymbol.create(name, symbol, + otherNames == null ? new HashSet<>() : new HashSet<>(otherNames)); + } + + /** + * h * Gets a {@code NameSymbol} with a primary name, a symbol and additional + * names. + *

+ * If any argument is null, this static factory replaces it with an empty + * Optional or empty Set. + *

+ * If {@code name} is null and {@code otherNames} is not empty, a primary + * name will be picked from {@code otherNames}. This name will not appear in + * getOtherNames(). + * + * @param name name to use + * @param symbol symbol to use + * @param otherNames other names to use + * @return NameSymbol instance + * @since 2019-11-26 + */ + public static final NameSymbol ofNullable(final String name, + final String symbol, final String... otherNames) { + return create(name, symbol, otherNames == null ? new HashSet<>() + : new HashSet<>(Arrays.asList(otherNames))); + } + + /** + * Gets a {@code NameSymbol} with a symbol and no names. + * + * @param symbol symbol to use + * @return NameSymbol instance + * @since 2019-10-21 + * @throws NullPointerException if symbol is null + */ + public static final NameSymbol ofSymbol(final String symbol) { + return new NameSymbol(Optional.empty(), Optional.of(symbol), + new HashSet<>()); + } + + private final Optional primaryName; + private final Optional symbol; + + private final Set otherNames; + + /** + * Creates the {@code NameSymbol}. + * + * @param primaryName primary name of unit + * @param symbol symbol used to represent unit + * @param otherNames other names and/or spellings, should be a mutable copy + * of the argument + * @since 2019-10-21 + */ + private NameSymbol(final Optional primaryName, + final Optional symbol, final Set otherNames) { + this.primaryName = primaryName; + this.symbol = symbol; + otherNames.remove(null); + this.otherNames = Collections.unmodifiableSet(otherNames); + + if (this.primaryName.isEmpty()) { + assert this.otherNames.isEmpty(); + } + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (!(obj instanceof NameSymbol)) + return false; + final NameSymbol other = (NameSymbol) obj; + if (this.otherNames == null) { + if (other.otherNames != null) + return false; + } else if (!this.otherNames.equals(other.otherNames)) + return false; + if (this.primaryName == null) { + if (other.primaryName != null) + return false; + } else if (!this.primaryName.equals(other.primaryName)) + return false; + if (this.symbol == null) { + if (other.symbol != null) + return false; + } else if (!this.symbol.equals(other.symbol)) + return false; + return true; + } + + /** + * @return otherNames + * @since 2019-10-21 + */ + public final Set getOtherNames() { + return this.otherNames; + } + + /** + * @return primaryName + * @since 2019-10-21 + */ + public final Optional getPrimaryName() { + return this.primaryName; + } + + /** + * @return symbol + * @since 2019-10-21 + */ + public final Optional getSymbol() { + return this.symbol; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + + (this.otherNames == null ? 0 : this.otherNames.hashCode()); + result = prime * result + + (this.primaryName == null ? 0 : this.primaryName.hashCode()); + result = prime * result + + (this.symbol == null ? 0 : this.symbol.hashCode()); + return result; + } + + /** + * @return true iff this {@code NameSymbol} contains no names or symbols. + */ + public final boolean isEmpty() { + // if primaryName is empty, otherNames must also be empty + return this.primaryName.isEmpty() && this.symbol.isEmpty(); + } +} \ No newline at end of file diff --git a/src/main/java/org/unitConverter/unit/Nameable.java b/src/main/java/org/unitConverter/unit/Nameable.java new file mode 100644 index 0000000..36740ab --- /dev/null +++ b/src/main/java/org/unitConverter/unit/Nameable.java @@ -0,0 +1,59 @@ +/** + * Copyright (C) 2020 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 . + */ +package org.unitConverter.unit; + +import java.util.Optional; +import java.util.Set; + +/** + * An object that can hold one or more names, and possibly a symbol. The name + * and symbol data should be immutable. + * + * @since 2020-09-07 + */ +public interface Nameable { + /** + * @return a {@code NameSymbol} that contains this object's primary name, + * symbol and other names + * @since 2020-09-07 + */ + NameSymbol getNameSymbol(); + + /** + * @return set of alternate names + * @since 2020-09-07 + */ + default Set getOtherNames() { + return this.getNameSymbol().getOtherNames(); + } + + /** + * @return preferred name of object + * @since 2020-09-07 + */ + default Optional getPrimaryName() { + return this.getNameSymbol().getPrimaryName(); + } + + /** + * @return short symbol representing object + * @since 2020-09-07 + */ + default Optional getSymbol() { + return this.getNameSymbol().getSymbol(); + } +} diff --git a/src/main/java/org/unitConverter/unit/SI.java b/src/main/java/org/unitConverter/unit/SI.java new file mode 100644 index 0000000..81736f3 --- /dev/null +++ b/src/main/java/org/unitConverter/unit/SI.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 . + */ +package org.unitConverter.unit; + +import java.util.Set; + +import org.unitConverter.math.ObjectProduct; + +/** + * All of the units, prefixes and dimensions that are used by the SI, as well as + * some outside the SI. + * + *

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

+ * LinearUnit KILOMETRE = SI.METRE.withPrefix(SI.KILO);
+ * 
+ * + * + * @author Adrien Hopkins + * @since 2019-10-16 + */ +public final class SI { + /// dimensions used by SI units + // base dimensions, as BaseDimensions + public static final class BaseDimensions { + public static final BaseDimension LENGTH = BaseDimension.valueOf("Length", + "L"); + public static final BaseDimension MASS = BaseDimension.valueOf("Mass", + "M"); + public static final BaseDimension TIME = BaseDimension.valueOf("Time", + "T"); + public static final BaseDimension ELECTRIC_CURRENT = BaseDimension + .valueOf("Electric Current", "I"); + public static final BaseDimension TEMPERATURE = BaseDimension + .valueOf("Temperature", "\u0398"); // theta symbol + public static final BaseDimension QUANTITY = BaseDimension + .valueOf("Quantity", "N"); + public static final BaseDimension LUMINOUS_INTENSITY = BaseDimension + .valueOf("Luminous Intensity", "J"); + public static final BaseDimension INFORMATION = BaseDimension + .valueOf("Information", "Info"); // non-SI + public static final BaseDimension CURRENCY = BaseDimension + .valueOf("Currency", "$$"); // non-SI + + // You may NOT get SI.BaseDimensions instances! + private BaseDimensions() { + throw new AssertionError(); + } + } + + /// base units of the SI + // suppressing warnings since these are the same object, but in a different + /// form (class) + @SuppressWarnings("hiding") + public static final class BaseUnits { + public static final BaseUnit METRE = BaseUnit + .valueOf(BaseDimensions.LENGTH, "metre", "m"); + public static final BaseUnit KILOGRAM = BaseUnit + .valueOf(BaseDimensions.MASS, "kilogram", "kg"); + public static final BaseUnit SECOND = BaseUnit + .valueOf(BaseDimensions.TIME, "second", "s"); + public static final BaseUnit AMPERE = BaseUnit + .valueOf(BaseDimensions.ELECTRIC_CURRENT, "ampere", "A"); + public static final BaseUnit KELVIN = BaseUnit + .valueOf(BaseDimensions.TEMPERATURE, "kelvin", "K"); + public static final BaseUnit MOLE = BaseUnit + .valueOf(BaseDimensions.QUANTITY, "mole", "mol"); + public static final BaseUnit CANDELA = BaseUnit + .valueOf(BaseDimensions.LUMINOUS_INTENSITY, "candela", "cd"); + public static final BaseUnit BIT = BaseUnit + .valueOf(BaseDimensions.INFORMATION, "bit", "b"); + public static final BaseUnit DOLLAR = BaseUnit + .valueOf(BaseDimensions.CURRENCY, "dollar", "$"); + + public static final Set BASE_UNITS = Set.of(METRE, KILOGRAM, + SECOND, AMPERE, KELVIN, MOLE, CANDELA, BIT); + + // You may NOT get SI.BaseUnits instances! + private BaseUnits() { + throw new AssertionError(); + } + } + + /** + * Constants that relate to the SI or other systems. + * + * @author Adrien Hopkins + * @since 2019-11-08 + */ + public static final class Constants { + public static final LinearUnit EARTH_GRAVITY = METRE.dividedBy(SECOND) + .dividedBy(SECOND).times(9.80665); + } + + // dimensions used in the SI, as ObjectProducts + public static final class Dimensions { + public static final ObjectProduct EMPTY = ObjectProduct + .empty(); + public static final ObjectProduct LENGTH = ObjectProduct + .oneOf(BaseDimensions.LENGTH); + public static final ObjectProduct MASS = ObjectProduct + .oneOf(BaseDimensions.MASS); + public static final ObjectProduct TIME = ObjectProduct + .oneOf(BaseDimensions.TIME); + public static final ObjectProduct ELECTRIC_CURRENT = ObjectProduct + .oneOf(BaseDimensions.ELECTRIC_CURRENT); + public static final ObjectProduct TEMPERATURE = ObjectProduct + .oneOf(BaseDimensions.TEMPERATURE); + public static final ObjectProduct QUANTITY = ObjectProduct + .oneOf(BaseDimensions.QUANTITY); + public static final ObjectProduct LUMINOUS_INTENSITY = ObjectProduct + .oneOf(BaseDimensions.LUMINOUS_INTENSITY); + public static final ObjectProduct INFORMATION = ObjectProduct + .oneOf(BaseDimensions.INFORMATION); + public static final ObjectProduct CURRENCY = ObjectProduct + .oneOf(BaseDimensions.CURRENCY); + + // derived dimensions without named SI units + public static final ObjectProduct AREA = LENGTH + .times(LENGTH); + public static final ObjectProduct VOLUME = AREA + .times(LENGTH); + public static final ObjectProduct VELOCITY = LENGTH + .dividedBy(TIME); + public static final ObjectProduct ACCELERATION = VELOCITY + .dividedBy(TIME); + public static final ObjectProduct WAVENUMBER = EMPTY + .dividedBy(LENGTH); + public static final ObjectProduct MASS_DENSITY = MASS + .dividedBy(VOLUME); + public static final ObjectProduct SURFACE_DENSITY = MASS + .dividedBy(AREA); + public static final ObjectProduct SPECIFIC_VOLUME = VOLUME + .dividedBy(MASS); + public static final ObjectProduct CURRENT_DENSITY = ELECTRIC_CURRENT + .dividedBy(AREA); + public static final ObjectProduct MAGNETIC_FIELD_STRENGTH = ELECTRIC_CURRENT + .dividedBy(LENGTH); + public static final ObjectProduct CONCENTRATION = QUANTITY + .dividedBy(VOLUME); + public static final ObjectProduct MASS_CONCENTRATION = CONCENTRATION + .times(MASS); + public static final ObjectProduct LUMINANCE = LUMINOUS_INTENSITY + .dividedBy(AREA); + public static final ObjectProduct REFRACTIVE_INDEX = VELOCITY + .dividedBy(VELOCITY); + public static final ObjectProduct REFRACTIVE_PERMEABILITY = EMPTY + .times(EMPTY); + public static final ObjectProduct ANGLE = LENGTH + .dividedBy(LENGTH); + public static final ObjectProduct SOLID_ANGLE = AREA + .dividedBy(AREA); + + // derived dimensions with named SI units + public static final ObjectProduct FREQUENCY = EMPTY + .dividedBy(TIME); + public static final ObjectProduct FORCE = MASS + .times(ACCELERATION); + public static final ObjectProduct ENERGY = FORCE + .times(LENGTH); + public static final ObjectProduct POWER = ENERGY + .dividedBy(TIME); + public static final ObjectProduct ELECTRIC_CHARGE = ELECTRIC_CURRENT + .times(TIME); + public static final ObjectProduct VOLTAGE = ENERGY + .dividedBy(ELECTRIC_CHARGE); + public static final ObjectProduct CAPACITANCE = ELECTRIC_CHARGE + .dividedBy(VOLTAGE); + public static final ObjectProduct ELECTRIC_RESISTANCE = VOLTAGE + .dividedBy(ELECTRIC_CURRENT); + public static final ObjectProduct ELECTRIC_CONDUCTANCE = ELECTRIC_CURRENT + .dividedBy(VOLTAGE); + public static final ObjectProduct MAGNETIC_FLUX = VOLTAGE + .times(TIME); + public static final ObjectProduct MAGNETIC_FLUX_DENSITY = MAGNETIC_FLUX + .dividedBy(AREA); + public static final ObjectProduct INDUCTANCE = MAGNETIC_FLUX + .dividedBy(ELECTRIC_CURRENT); + public static final ObjectProduct LUMINOUS_FLUX = LUMINOUS_INTENSITY + .times(SOLID_ANGLE); + public static final ObjectProduct ILLUMINANCE = LUMINOUS_FLUX + .dividedBy(AREA); + public static final ObjectProduct SPECIFIC_ENERGY = ENERGY + .dividedBy(MASS); + public static final ObjectProduct CATALYTIC_ACTIVITY = QUANTITY + .dividedBy(TIME); + + // You may NOT get SI.Dimension instances! + private Dimensions() { + throw new AssertionError(); + } + } + + /// The units of the SI + public static final LinearUnit ONE = LinearUnit + .valueOf(ObjectProduct.empty(), 1); + + public static final LinearUnit METRE = BaseUnits.METRE.asLinearUnit() + .withName(NameSymbol.of("metre", "m", "meter")); + public static final LinearUnit KILOGRAM = BaseUnits.KILOGRAM.asLinearUnit() + .withName(NameSymbol.of("kilogram", "kg")); + public static final LinearUnit SECOND = BaseUnits.SECOND.asLinearUnit() + .withName(NameSymbol.of("second", "s", "sec")); + public static final LinearUnit AMPERE = BaseUnits.AMPERE.asLinearUnit() + .withName(NameSymbol.of("ampere", "A")); + public static final LinearUnit KELVIN = BaseUnits.KELVIN.asLinearUnit() + .withName(NameSymbol.of("kelvin", "K")); + public static final LinearUnit MOLE = BaseUnits.MOLE.asLinearUnit() + .withName(NameSymbol.of("mole", "mol")); + public static final LinearUnit CANDELA = BaseUnits.CANDELA.asLinearUnit() + .withName(NameSymbol.of("candela", "cd")); + public static final LinearUnit BIT = BaseUnits.BIT.asLinearUnit() + .withName(NameSymbol.of("bit", "b")); + public static final LinearUnit DOLLAR = BaseUnits.DOLLAR.asLinearUnit() + .withName(NameSymbol.of("dollar", "$")); + // Non-base units + public static final LinearUnit RADIAN = METRE.dividedBy(METRE) + .withName(NameSymbol.of("radian", "rad")); + + public static final LinearUnit STERADIAN = RADIAN.times(RADIAN) + .withName(NameSymbol.of("steradian", "sr")); + public static final LinearUnit HERTZ = ONE.dividedBy(SECOND) + .withName(NameSymbol.of("hertz", "Hz")); + // for periodic phenomena + public static final LinearUnit NEWTON = KILOGRAM.times(METRE) + .dividedBy(SECOND.times(SECOND)) + .withName(NameSymbol.of("newton", "N")); + public static final LinearUnit PASCAL = NEWTON.dividedBy(METRE.times(METRE)) + .withName(NameSymbol.of("pascal", "Pa")); + public static final LinearUnit JOULE = NEWTON.times(METRE) + .withName(NameSymbol.of("joule", "J")); + public static final LinearUnit WATT = JOULE.dividedBy(SECOND) + .withName(NameSymbol.of("watt", "W")); + public static final LinearUnit COULOMB = AMPERE.times(SECOND) + .withName(NameSymbol.of("coulomb", "C")); + public static final LinearUnit VOLT = JOULE.dividedBy(COULOMB) + .withName(NameSymbol.of("volt", "V")); + public static final LinearUnit FARAD = COULOMB.dividedBy(VOLT) + .withName(NameSymbol.of("farad", "F")); + public static final LinearUnit OHM = VOLT.dividedBy(AMPERE) + .withName(NameSymbol.of("ohm", "\u03A9")); // omega + public static final LinearUnit SIEMENS = ONE.dividedBy(OHM) + .withName(NameSymbol.of("siemens", "S")); + public static final LinearUnit WEBER = VOLT.times(SECOND) + .withName(NameSymbol.of("weber", "Wb")); + public static final LinearUnit TESLA = WEBER.dividedBy(METRE.times(METRE)) + .withName(NameSymbol.of("tesla", "T")); + public static final LinearUnit HENRY = WEBER.dividedBy(AMPERE) + .withName(NameSymbol.of("henry", "H")); + public static final LinearUnit LUMEN = CANDELA.times(STERADIAN) + .withName(NameSymbol.of("lumen", "lm")); + public static final LinearUnit LUX = LUMEN.dividedBy(METRE.times(METRE)) + .withName(NameSymbol.of("lux", "lx")); + public static final LinearUnit BEQUEREL = ONE.dividedBy(SECOND) + .withName(NameSymbol.of("bequerel", "Bq")); + // for activity referred to a nucleotide + public static final LinearUnit GRAY = JOULE.dividedBy(KILOGRAM) + .withName(NameSymbol.of("grey", "Gy")); + // for absorbed dose + public static final LinearUnit SIEVERT = JOULE.dividedBy(KILOGRAM) + .withName(NameSymbol.of("sievert", "Sv")); + // for dose equivalent + public static final LinearUnit KATAL = MOLE.dividedBy(SECOND) + .withName(NameSymbol.of("katal", "kat")); + // common derived units included for convenience + public static final LinearUnit GRAM = KILOGRAM.dividedBy(1000) + .withName(NameSymbol.of("gram", "g")); + + public static final LinearUnit SQUARE_METRE = METRE.toExponent(2) + .withName(NameSymbol.of("square metre", "m^2", "square meter", + "metre squared", "meter squared")); + public static final LinearUnit CUBIC_METRE = METRE.toExponent(3) + .withName(NameSymbol.of("cubic metre", "m^3", "cubic meter", + "metre cubed", "meter cubed")); + public static final LinearUnit METRE_PER_SECOND = METRE.dividedBy(SECOND) + .withName( + NameSymbol.of("metre per second", "m/s", "meter per second")); + // Non-SI units included for convenience + public static final Unit CELSIUS = Unit + .fromConversionFunctions(KELVIN.getBase(), tempK -> tempK - 273.15, + tempC -> tempC + 273.15) + .withName(NameSymbol.of("degree Celsius", "\u00B0C")); + + public static final LinearUnit MINUTE = SECOND.times(60) + .withName(NameSymbol.of("minute", "min")); + public static final LinearUnit HOUR = MINUTE.times(60) + .withName(NameSymbol.of("hour", "h", "hr")); + public static final LinearUnit DAY = HOUR.times(60) + .withName(NameSymbol.of("day", "d")); + public static final LinearUnit KILOMETRE_PER_HOUR = METRE.times(1000) + .dividedBy(HOUR).withName(NameSymbol.of("kilometre per hour", "km/h", + "kilometer per hour")); + public static final LinearUnit DEGREE = RADIAN.times(360 / (2 * Math.PI)) + .withName(NameSymbol.of("degree", "\u00B0", "deg")); + public static final LinearUnit ARCMINUTE = DEGREE.dividedBy(60) + .withName(NameSymbol.of("arcminute", "arcmin")); + public static final LinearUnit ARCSECOND = ARCMINUTE.dividedBy(60) + .withName(NameSymbol.of("arcsecond", "arcsec")); + public static final LinearUnit ASTRONOMICAL_UNIT = METRE + .times(149597870700.0) + .withName(NameSymbol.of("astronomical unit", "au")); + public static final LinearUnit PARSEC = ASTRONOMICAL_UNIT + .dividedBy(ARCSECOND).withName(NameSymbol.of("parsec", "pc")); + public static final LinearUnit HECTARE = METRE.times(METRE).times(10000.0) + .withName(NameSymbol.of("hectare", "ha")); + public static final LinearUnit LITRE = METRE.times(METRE).times(METRE) + .dividedBy(1000.0).withName(NameSymbol.of("litre", "L", "l", "liter")); + public static final LinearUnit TONNE = KILOGRAM.times(1000.0) + .withName(NameSymbol.of("tonne", "t", "metric ton")); + public static final LinearUnit DALTON = KILOGRAM.times(1.660539040e-27) + .withName(NameSymbol.of("dalton", "Da", "atomic unit", "u")); // approximate + // value + public static final LinearUnit ELECTRONVOLT = JOULE.times(1.602176634e-19) + .withName(NameSymbol.of("electron volt", "eV")); + public static final LinearUnit BYTE = BIT.times(8) + .withName(NameSymbol.of("byte", "B")); + public static final Unit NEPER = Unit.fromConversionFunctions(ONE.getBase(), + pr -> 0.5 * Math.log(pr), Np -> Math.exp(2 * Np)) + .withName(NameSymbol.of("neper", "Np")); + public static final Unit BEL = Unit.fromConversionFunctions(ONE.getBase(), + pr -> Math.log10(pr), dB -> Math.pow(10, dB)) + .withName(NameSymbol.of("bel", "B")); + public static final Unit DECIBEL = Unit + .fromConversionFunctions(ONE.getBase(), pr -> 10 * Math.log10(pr), + dB -> Math.pow(10, dB / 10)) + .withName(NameSymbol.of("decibel", "dB")); + + /// The prefixes of the SI + // expanding decimal prefixes + public static final UnitPrefix KILO = UnitPrefix.valueOf(1e3) + .withName(NameSymbol.of("kilo", "k", "K")); + public static final UnitPrefix MEGA = UnitPrefix.valueOf(1e6) + .withName(NameSymbol.of("mega", "M")); + public static final UnitPrefix GIGA = UnitPrefix.valueOf(1e9) + .withName(NameSymbol.of("giga", "G")); + public static final UnitPrefix TERA = UnitPrefix.valueOf(1e12) + .withName(NameSymbol.of("tera", "T")); + public static final UnitPrefix PETA = UnitPrefix.valueOf(1e15) + .withName(NameSymbol.of("peta", "P")); + public static final UnitPrefix EXA = UnitPrefix.valueOf(1e18) + .withName(NameSymbol.of("exa", "E")); + public static final UnitPrefix ZETTA = UnitPrefix.valueOf(1e21) + .withName(NameSymbol.of("zetta", "Z")); + public static final UnitPrefix YOTTA = UnitPrefix.valueOf(1e24) + .withName(NameSymbol.of("yotta", "Y")); + + // contracting decimal prefixes + public static final UnitPrefix MILLI = UnitPrefix.valueOf(1e-3) + .withName(NameSymbol.of("milli", "m")); + public static final UnitPrefix MICRO = UnitPrefix.valueOf(1e-6) + .withName(NameSymbol.of("micro", "\u03BC", "u")); // mu + public static final UnitPrefix NANO = UnitPrefix.valueOf(1e-9) + .withName(NameSymbol.of("nano", "n")); + public static final UnitPrefix PICO = UnitPrefix.valueOf(1e-12) + .withName(NameSymbol.of("pico", "p")); + public static final UnitPrefix FEMTO = UnitPrefix.valueOf(1e-15) + .withName(NameSymbol.of("femto", "f")); + public static final UnitPrefix ATTO = UnitPrefix.valueOf(1e-18) + .withName(NameSymbol.of("atto", "a")); + public static final UnitPrefix ZEPTO = UnitPrefix.valueOf(1e-21) + .withName(NameSymbol.of("zepto", "z")); + public static final UnitPrefix YOCTO = UnitPrefix.valueOf(1e-24) + .withName(NameSymbol.of("yocto", "y")); + + // prefixes that don't match the pattern of thousands + public static final UnitPrefix DEKA = UnitPrefix.valueOf(1e1) + .withName(NameSymbol.of("deka", "da", "deca", "D")); + public static final UnitPrefix HECTO = UnitPrefix.valueOf(1e2) + .withName(NameSymbol.of("hecto", "h", "H", "hekto")); + public static final UnitPrefix DECI = UnitPrefix.valueOf(1e-1) + .withName(NameSymbol.of("deci", "d")); + public static final UnitPrefix CENTI = UnitPrefix.valueOf(1e-2) + .withName(NameSymbol.of("centi", "c")); + public static final UnitPrefix KIBI = UnitPrefix.valueOf(1024) + .withName(NameSymbol.of("kibi", "Ki")); + public static final UnitPrefix MEBI = KIBI.times(1024) + .withName(NameSymbol.of("mebi", "Mi")); + public static final UnitPrefix GIBI = MEBI.times(1024) + .withName(NameSymbol.of("gibi", "Gi")); + public static final UnitPrefix TEBI = GIBI.times(1024) + .withName(NameSymbol.of("tebi", "Ti")); + public static final UnitPrefix PEBI = TEBI.times(1024) + .withName(NameSymbol.of("pebi", "Pi")); + public static final UnitPrefix EXBI = PEBI.times(1024) + .withName(NameSymbol.of("exbi", "Ei")); + + // a few prefixed units + public static final LinearUnit MICROMETRE = SI.METRE.withPrefix(SI.MICRO); + public static final LinearUnit MILLIMETRE = SI.METRE.withPrefix(SI.MILLI); + public static final LinearUnit KILOMETRE = SI.METRE.withPrefix(SI.KILO); + public static final LinearUnit MEGAMETRE = SI.METRE.withPrefix(SI.MEGA); + + public static final LinearUnit MICROLITRE = SI.LITRE.withPrefix(SI.MICRO); + public static final LinearUnit MILLILITRE = SI.LITRE.withPrefix(SI.MILLI); + public static final LinearUnit KILOLITRE = SI.LITRE.withPrefix(SI.KILO); + public static final LinearUnit MEGALITRE = SI.LITRE.withPrefix(SI.MEGA); + + public static final LinearUnit MICROSECOND = SI.SECOND.withPrefix(SI.MICRO); + public static final LinearUnit MILLISECOND = SI.SECOND.withPrefix(SI.MILLI); + public static final LinearUnit KILOSECOND = SI.SECOND.withPrefix(SI.KILO); + public static final LinearUnit MEGASECOND = SI.SECOND.withPrefix(SI.MEGA); + + public static final LinearUnit MICROGRAM = SI.GRAM.withPrefix(SI.MICRO); + public static final LinearUnit MILLIGRAM = SI.GRAM.withPrefix(SI.MILLI); + public static final LinearUnit MEGAGRAM = SI.GRAM.withPrefix(SI.MEGA); + + public static final LinearUnit MICRONEWTON = SI.NEWTON.withPrefix(SI.MICRO); + public static final LinearUnit MILLINEWTON = SI.NEWTON.withPrefix(SI.MILLI); + public static final LinearUnit KILONEWTON = SI.NEWTON.withPrefix(SI.KILO); + public static final LinearUnit MEGANEWTON = SI.NEWTON.withPrefix(SI.MEGA); + + public static final LinearUnit MICROJOULE = SI.JOULE.withPrefix(SI.MICRO); + public static final LinearUnit MILLIJOULE = SI.JOULE.withPrefix(SI.MILLI); + public static final LinearUnit KILOJOULE = SI.JOULE.withPrefix(SI.KILO); + public static final LinearUnit MEGAJOULE = SI.JOULE.withPrefix(SI.MEGA); + + public static final LinearUnit MICROWATT = SI.WATT.withPrefix(SI.MICRO); + public static final LinearUnit MILLIWATT = SI.WATT.withPrefix(SI.MILLI); + public static final LinearUnit KILOWATT = SI.WATT.withPrefix(SI.KILO); + public static final LinearUnit MEGAWATT = SI.WATT.withPrefix(SI.MEGA); + + public static final LinearUnit MICROCOULOMB = SI.COULOMB + .withPrefix(SI.MICRO); + public static final LinearUnit MILLICOULOMB = SI.COULOMB + .withPrefix(SI.MILLI); + public static final LinearUnit KILOCOULOMB = SI.COULOMB.withPrefix(SI.KILO); + public static final LinearUnit MEGACOULOMB = SI.COULOMB.withPrefix(SI.MEGA); + + public static final LinearUnit MICROAMPERE = SI.AMPERE.withPrefix(SI.MICRO); + public static final LinearUnit MILLIAMPERE = SI.AMPERE.withPrefix(SI.MILLI); + + public static final LinearUnit MICROVOLT = SI.VOLT.withPrefix(SI.MICRO); + public static final LinearUnit MILLIVOLT = SI.VOLT.withPrefix(SI.MILLI); + public static final LinearUnit KILOVOLT = SI.VOLT.withPrefix(SI.KILO); + public static final LinearUnit MEGAVOLT = SI.VOLT.withPrefix(SI.MEGA); + + public static final LinearUnit KILOOHM = SI.OHM.withPrefix(SI.KILO); + public static final LinearUnit MEGAOHM = SI.OHM.withPrefix(SI.MEGA); + + // sets of prefixes + public static final Set ALL_PREFIXES = Set.of(DEKA, HECTO, KILO, + MEGA, GIGA, TERA, PETA, EXA, ZETTA, YOTTA, DECI, CENTI, MILLI, MICRO, + NANO, PICO, FEMTO, ATTO, ZEPTO, YOCTO, KIBI, MEBI, GIBI, TEBI, PEBI, + EXBI); + + public static final Set DECIMAL_PREFIXES = Set.of(DEKA, HECTO, + KILO, MEGA, GIGA, TERA, PETA, EXA, ZETTA, YOTTA, DECI, CENTI, MILLI, + MICRO, NANO, PICO, FEMTO, ATTO, ZEPTO, YOCTO); + public static final Set THOUSAND_PREFIXES = Set.of(KILO, MEGA, + GIGA, TERA, PETA, EXA, ZETTA, YOTTA, MILLI, MICRO, NANO, PICO, FEMTO, + ATTO, ZEPTO, YOCTO); + public static final Set MAGNIFYING_PREFIXES = Set.of(DEKA, HECTO, + KILO, MEGA, GIGA, TERA, PETA, EXA, ZETTA, YOTTA, KIBI, MEBI, GIBI, + TEBI, PEBI, EXBI); + public static final Set REDUCING_PREFIXES = Set.of(DECI, CENTI, + MILLI, MICRO, NANO, PICO, FEMTO, ATTO, ZEPTO, YOCTO); + + // You may NOT get SI instances! + private SI() { + throw new AssertionError(); + } +} diff --git a/src/main/java/org/unitConverter/unit/USCustomary.java b/src/main/java/org/unitConverter/unit/USCustomary.java new file mode 100644 index 0000000..1c4bcfe --- /dev/null +++ b/src/main/java/org/unitConverter/unit/USCustomary.java @@ -0,0 +1,135 @@ +/** + * 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 . + */ +package org.unitConverter.unit; + +/** + * A static utility class that contains units in the US Customary system. + * + * @author Adrien Hopkins + * @since 2019-10-21 + */ +public final class USCustomary { + /** + * US Customary units that measure area + * + * @author Adrien Hopkins + * @since 2019-11-08 + */ + public static final class Area { + public static final LinearUnit SQUARE_SURVEY_FOOT = Length.SURVEY_FOOT.times(Length.SURVEY_FOOT); + public static final LinearUnit SQUARE_CHAIN = Length.SURVEY_CHAIN.times(Length.SURVEY_CHAIN); + public static final LinearUnit ACRE = Length.SURVEY_CHAIN.times(Length.SURVEY_FURLONG); + public static final LinearUnit SECTION = Length.SURVEY_MILE.times(Length.SURVEY_MILE); + public static final LinearUnit SURVEY_TOWNSHIP = SECTION.times(36); + } + + /** + * US Customary units that measure length + * + * @author Adrien Hopkins + * @since 2019-10-28 + */ + public static final class Length { + public static final LinearUnit FOOT = BritishImperial.Length.FOOT; + public static final LinearUnit INCH = BritishImperial.Length.INCH; + public static final LinearUnit HAND = INCH.times(4); + public static final LinearUnit PICA = INCH.dividedBy(6); + public static final LinearUnit POINT = PICA.dividedBy(12); + public static final LinearUnit YARD = BritishImperial.Length.YARD; + public static final LinearUnit MILE = BritishImperial.Length.MILE; + + public static final LinearUnit SURVEY_FOOT = SI.METRE.times(1200.0 / 3937.0); + public static final LinearUnit SURVEY_LINK = SURVEY_FOOT.times(33.0 / 50.0); + public static final LinearUnit SURVEY_ROD = SURVEY_FOOT.times(16.5); + public static final LinearUnit SURVEY_CHAIN = SURVEY_ROD.times(4); + public static final LinearUnit SURVEY_FURLONG = SURVEY_CHAIN.times(10); + public static final LinearUnit SURVEY_MILE = SURVEY_FURLONG.times(8); + public static final LinearUnit SURVEY_LEAGUE = SURVEY_MILE.times(3); + + public static final LinearUnit NAUTICAL_MILE = BritishImperial.Length.NAUTICAL_MILE; + public static final LinearUnit FATHOM = YARD.times(2); + public static final LinearUnit CABLE = FATHOM.times(120); + } + + /** + * mass units + * + * @author Adrien Hopkins + * @since 2019-11-08 + */ + public static final class Mass { + public static final LinearUnit GRAIN = BritishImperial.Mass.GRAIN; + public static final LinearUnit DRAM = BritishImperial.Mass.DRACHM; + public static final LinearUnit OUNCE = BritishImperial.Mass.OUNCE; + public static final LinearUnit POUND = BritishImperial.Mass.POUND; + public static final LinearUnit HUNDREDWEIGHT = POUND.times(100); + public static final LinearUnit SHORT_TON = HUNDREDWEIGHT.times(20); + + // troy system for precious metals + public static final LinearUnit PENNYWEIGHT = GRAIN.times(24); + public static final LinearUnit TROY_OUNCE = PENNYWEIGHT.times(20); + public static final LinearUnit TROY_POUND = TROY_OUNCE.times(12); + } + + /** + * Volume units + * + * @author Adrien Hopkins + * @since 2019-11-08 + */ + public static final class Volume { + public static final LinearUnit CUBIC_INCH = Length.INCH.toExponent(3); + public static final LinearUnit CUBIC_FOOT = Length.FOOT.toExponent(3); + public static final LinearUnit CUBIC_YARD = Length.YARD.toExponent(3); + public static final LinearUnit ACRE_FOOT = Area.ACRE.times(Length.FOOT); + + public static final LinearUnit MINIM = SI.LITRE.withPrefix(SI.MICRO).times(61.611519921875); + public static final LinearUnit FLUID_DRAM = MINIM.times(60); + public static final LinearUnit TEASPOON = MINIM.times(80); + public static final LinearUnit TABLESPOON = TEASPOON.times(3); + public static final LinearUnit FLUID_OUNCE = TABLESPOON.times(2); + public static final LinearUnit SHOT = TABLESPOON.times(3); + public static final LinearUnit GILL = FLUID_OUNCE.times(4); + public static final LinearUnit CUP = GILL.times(2); + public static final LinearUnit PINT = CUP.times(2); + public static final LinearUnit QUART = PINT.times(2); + public static final LinearUnit GALLON = QUART.times(4); + public static final LinearUnit BARREL = GALLON.times(31.5); + public static final LinearUnit OIL_BARREL = GALLON.times(42); + public static final LinearUnit HOGSHEAD = GALLON.times(63); + + public static final LinearUnit DRY_PINT = SI.LITRE.times(0.5506104713575); + public static final LinearUnit DRY_QUART = DRY_PINT.times(2); + public static final LinearUnit DRY_GALLON = DRY_QUART.times(4); + public static final LinearUnit PECK = DRY_GALLON.times(2); + public static final LinearUnit BUSHEL = PECK.times(4); + public static final LinearUnit DRY_BARREL = CUBIC_INCH.times(7056); + } + + public static final LinearUnit OUNCE_FORCE = BritishImperial.OUNCE_FORCE; + public static final LinearUnit POUND_FORCE = BritishImperial.POUND_FORCE; + + public static final LinearUnit BRITISH_THERMAL_UNIT = BritishImperial.BRITISH_THERMAL_UNIT; + public static final LinearUnit CALORIE = BritishImperial.CALORIE; + public static final LinearUnit KILOCALORIE = BritishImperial.KILOCALORIE; + public static final LinearUnit FOOT_POUND = POUND_FORCE.times(Length.FOOT); + + public static final LinearUnit HORSEPOWER = Length.FOOT.times(POUND_FORCE).dividedBy(SI.MINUTE).times(33000); + public static final LinearUnit POUND_PER_SQUARE_INCH = POUND_FORCE.dividedBy(Length.INCH.toExponent(2)); + + public static final Unit FAHRENHEIT = BritishImperial.FAHRENHEIT; +} diff --git a/src/main/java/org/unitConverter/unit/Unit.java b/src/main/java/org/unitConverter/unit/Unit.java new file mode 100644 index 0000000..0a3298f --- /dev/null +++ b/src/main/java/org/unitConverter/unit/Unit.java @@ -0,0 +1,377 @@ +/** + * 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 . + */ +package org.unitConverter.unit; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.DoubleUnaryOperator; + +import org.unitConverter.math.DecimalComparison; +import org.unitConverter.math.ObjectProduct; + +/** + * A unit that is composed of base units. + * + * @author Adrien Hopkins + * @since 2019-10-16 + */ +public abstract class Unit implements Nameable { + /** + * Returns a unit from its base and the functions it uses to convert to and + * from its base. + * + *

+ * For example, to get a unit representing the degree Celsius, the following + * code can be used: + * + * {@code Unit.fromConversionFunctions(SI.KELVIN, tempK -> tempK - 273.15, tempC -> tempC + 273.15);} + *

+ * + * @param base unit's base + * @param converterFrom function that accepts a value expressed in the unit's + * base and returns that value expressed in this unit. + * @param converterTo function that accepts a value expressed in the unit + * and returns that value expressed in the unit's base. + * @return a unit that uses the provided functions to convert. + * @since 2019-05-22 + * @throws NullPointerException if any argument is null + */ + public static final Unit fromConversionFunctions( + final ObjectProduct base, + final DoubleUnaryOperator converterFrom, + final DoubleUnaryOperator converterTo) { + return new FunctionalUnit(base, converterFrom, converterTo); + } + + /** + * Returns a unit from its base and the functions it uses to convert to and + * from its base. + * + *

+ * For example, to get a unit representing the degree Celsius, the following + * code can be used: + * + * {@code Unit.fromConversionFunctions(SI.KELVIN, tempK -> tempK - 273.15, tempC -> tempC + 273.15);} + *

+ * + * @param base unit's base + * @param converterFrom function that accepts a value expressed in the unit's + * base and returns that value expressed in this unit. + * @param converterTo function that accepts a value expressed in the unit + * and returns that value expressed in the unit's base. + * @param ns names and symbol of unit + * @return a unit that uses the provided functions to convert. + * @since 2019-05-22 + * @throws NullPointerException if any argument is null + */ + public static final Unit fromConversionFunctions( + final ObjectProduct base, + final DoubleUnaryOperator converterFrom, + final DoubleUnaryOperator converterTo, final NameSymbol ns) { + return new FunctionalUnit(base, converterFrom, converterTo, ns); + } + + /** + * The combination of units that this unit is based on. + * + * @since 2019-10-16 + */ + private final ObjectProduct unitBase; + + /** + * This unit's name(s) and symbol + * + * @since 2020-09-07 + */ + private final NameSymbol nameSymbol; + + /** + * Cache storing the result of getDimension() + * + * @since 2019-10-16 + */ + private transient ObjectProduct dimension = null; + + /** + * Creates the {@code Unit}. + * + * @param unitBase base of unit + * @param ns names and symbol of unit + * @since 2019-10-16 + * @throws NullPointerException if unitBase or ns is null + */ + Unit(ObjectProduct unitBase, NameSymbol ns) { + this.unitBase = Objects.requireNonNull(unitBase, + "unitBase may not be null"); + this.nameSymbol = Objects.requireNonNull(ns, "ns may not be null"); + } + + /** + * A constructor that constructs {@code BaseUnit} instances. + * + * @since 2019-10-16 + */ + Unit(final String primaryName, final String symbol, + final Set otherNames) { + if (this instanceof BaseUnit) { + this.unitBase = ObjectProduct.oneOf((BaseUnit) this); + } else + throw new AssertionError(); + this.nameSymbol = NameSymbol.of(primaryName, symbol, + new HashSet<>(otherNames)); + } + + /** + * @return this unit as a {@link Unitlike} + * @since 2020-09-07 + */ + public final Unitlike asUnitlike() { + return Unitlike.fromConversionFunctions(this.getBase(), + this::convertFromBase, this::convertToBase, this.getNameSymbol()); + } + + /** + * Checks if a value expressed in this unit can be converted to a value + * expressed in {@code other} + * + * @param other unit or unitlike form to test with + * @return true if they are compatible + * @since 2019-01-13 + * @since v0.1.0 + * @throws NullPointerException if other is null + */ + public final boolean canConvertTo(final Unit other) { + Objects.requireNonNull(other, "other must not be null."); + return Objects.equals(this.getBase(), other.getBase()); + } + + /** + * Checks if a value expressed in this unit can be converted to a value + * expressed in {@code other} + * + * @param other unit or unitlike form to test with + * @return true if they are compatible + * @since 2019-01-13 + * @since v0.1.0 + * @throws NullPointerException if other is null + */ + public final boolean canConvertTo(final Unitlike other) { + Objects.requireNonNull(other, "other must not be null."); + return Objects.equals(this.getBase(), other.getBase()); + } + + /** + * Converts from a value expressed in this unit's base unit to a value + * expressed in this unit. + *

+ * This must be the inverse of {@code convertToBase}, so + * {@code convertFromBase(convertToBase(value))} must be equal to + * {@code value} for any value, ignoring precision loss by roundoff error. + *

+ *

+ * If this unit is a base unit, this method should return + * {@code value}. + *

+ * + * @implSpec This method is used by {@link #convertTo}, and its behaviour + * affects the behaviour of {@code convertTo}. + * + * @param value value expressed in base unit + * @return value expressed in this unit + * @since 2018-12-22 + * @since v0.1.0 + */ + protected abstract double convertFromBase(double value); + + /** + * Converts a value expressed in this unit to a value expressed in + * {@code other}. + * + * @implSpec If unit conversion is possible, this implementation returns + * {@code other.convertFromBase(this.convertToBase(value))}. + * Therefore, overriding either of those methods will change the + * output of this method. + * + * @param other unit to convert to + * @param value value to convert + * @return converted value + * @since 2019-05-22 + * @throws IllegalArgumentException if {@code other} is incompatible for + * conversion with this unit (as tested by + * {@link Unit#canConvertTo}). + * @throws NullPointerException if other is null + */ + public final double convertTo(final Unit other, final double value) { + Objects.requireNonNull(other, "other must not be null."); + if (this.canConvertTo(other)) + return other.convertFromBase(this.convertToBase(value)); + else + throw new IllegalArgumentException( + String.format("Cannot convert from %s to %s.", this, other)); + } + + /** + * Converts a value expressed in this unit to a value expressed in + * {@code other}. + * + * @implSpec If conversion is possible, this implementation returns + * {@code other.convertFromBase(this.convertToBase(value))}. + * Therefore, overriding either of those methods will change the + * output of this method. + * + * @param other unitlike form to convert to + * @param value value to convert + * @param type of value to convert to + * @return converted value + * @since 2020-09-07 + * @throws IllegalArgumentException if {@code other} is incompatible for + * conversion with this unit (as tested by + * {@link Unit#canConvertTo}). + * @throws NullPointerException if other is null + */ + public final W convertTo(final Unitlike other, final double value) { + Objects.requireNonNull(other, "other must not be null."); + if (this.canConvertTo(other)) + return other.convertFromBase(this.convertToBase(value)); + else + throw new IllegalArgumentException( + String.format("Cannot convert from %s to %s.", this, other)); + } + + /** + * Converts from a value expressed in this unit to a value expressed in this + * unit's base unit. + *

+ * This must be the inverse of {@code convertFromBase}, so + * {@code convertToBase(convertFromBase(value))} must be equal to + * {@code value} for any value, ignoring precision loss by roundoff error. + *

+ *

+ * If this unit is a base unit, this method should return + * {@code value}. + *

+ * + * @implSpec This method is used by {@link #convertTo}, and its behaviour + * affects the behaviour of {@code convertTo}. + * + * @param value value expressed in this unit + * @return value expressed in base unit + * @since 2018-12-22 + * @since v0.1.0 + */ + protected abstract double convertToBase(double value); + + /** + * @return combination of units that this unit is based on + * @since 2018-12-22 + * @since v0.1.0 + */ + public final ObjectProduct getBase() { + return this.unitBase; + } + + /** + * @return dimension measured by this unit + * @since 2018-12-22 + * @since v0.1.0 + */ + public final ObjectProduct getDimension() { + if (this.dimension == null) { + final Map mapping = this.unitBase.exponentMap(); + final Map dimensionMap = new HashMap<>(); + + for (final BaseUnit key : mapping.keySet()) { + dimensionMap.put(key.getBaseDimension(), mapping.get(key)); + } + + this.dimension = ObjectProduct.fromExponentMapping(dimensionMap); + } + return this.dimension; + } + + /** + * @return the nameSymbol + * @since 2020-09-07 + */ + @Override + public final NameSymbol getNameSymbol() { + return this.nameSymbol; + } + + /** + * Returns true iff this unit is metric. + *

+ * "Metric" is defined by three conditions: + *

    + *
  • Must be an instance of {@link LinearUnit}.
  • + *
  • Must be based on the SI base units (as determined by getBase())
  • + *
  • The conversion factor must be a power of 10.
  • + *
+ *

+ * Note that this definition excludes some units that many would consider + * "metric", such as the degree Celsius (fails the first condition), + * calories, minutes and hours (fail the third condition). + *

+ * All SI units (as designated by the BIPM) except the degree Celsius are + * considered "metric" by this definition. + * + * @since 2020-08-27 + */ + public final boolean isMetric() { + // first condition - check that it is a linear unit + if (!(this instanceof LinearUnit)) + return false; + final LinearUnit linear = (LinearUnit) this; + + // second condition - check that + for (final BaseUnit b : linear.getBase().getBaseSet()) { + if (!SI.BaseUnits.BASE_UNITS.contains(b)) + return false; + } + + // third condition - check that conversion factor is a power of 10 + return DecimalComparison + .equals(Math.log10(linear.getConversionFactor()) % 1.0, 0); + } + + @Override + public String toString() { + return this.getPrimaryName().orElse("Unnamed unit") + + (this.getSymbol().isPresent() + ? String.format(" (%s)", this.getSymbol().get()) + : "") + + ", derived from " + + this.getBase().toString(u -> u.getSymbol().get()) + + (this.getOtherNames().isEmpty() ? "" + : ", also called " + String.join(", ", this.getOtherNames())); + } + + /** + * @param ns name(s) and symbol to use + * @return a copy of this unit with provided name(s) and symbol + * @since 2019-10-21 + * @throws NullPointerException if ns is null + */ + public Unit withName(final NameSymbol ns) { + return fromConversionFunctions(this.getBase(), this::convertFromBase, + this::convertToBase, + Objects.requireNonNull(ns, "ns must not be null.")); + } +} diff --git a/src/main/java/org/unitConverter/unit/UnitDatabase.java b/src/main/java/org/unitConverter/unit/UnitDatabase.java new file mode 100644 index 0000000..000acf5 --- /dev/null +++ b/src/main/java/org/unitConverter/unit/UnitDatabase.java @@ -0,0 +1,1991 @@ +/** + * 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 . + */ +package org.unitConverter.unit; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.math.BigDecimal; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.AbstractSet; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.NoSuchElementException; +import java.util.Objects; +import java.util.Set; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.unitConverter.math.ConditionalExistenceCollections; +import org.unitConverter.math.DecimalComparison; +import org.unitConverter.math.ExpressionParser; +import org.unitConverter.math.ObjectProduct; +import org.unitConverter.math.UncertainDouble; + +/** + * A database of units, prefixes and dimensions, and their names. + * + * @author Adrien Hopkins + * @since 2019-01-07 + * @since v0.1.0 + */ +public final class UnitDatabase { + /** + * A map for units that allows the use of prefixes. + *

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

+ *

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

    + *
  • Prefixes can only be applied to linear units.
  • + *
  • Before attempting to search for prefixes in a unit name, this map will + * first search for a unit name. So, if there are two units, "B" and "AB", + * and a prefix "A", this map will favour the unit "AB" over the unit "B" + * with the prefix "A", even though they have the same string.
  • + *
  • Longer prefixes are preferred to shorter prefixes. So, if you have + * units "BC" and "C", and prefixes "AB" and "A", inputting "ABC" will return + * the unit "C" with the prefix "AB", not "BC" with the prefix "A".
  • + *
+ *

+ *

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

+ *

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

+ * + * @author Adrien Hopkins + * @since 2019-04-13 + * @since v0.2.0 + */ + private static final class PrefixedUnitMap implements Map { + /** + * The class used for entry sets. + * + *

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

+ * + * @author Adrien Hopkins + * @since 2019-04-13 + * @since v0.2.0 + */ + private static final class PrefixedUnitEntrySet + extends AbstractSet> { + /** + * The entry for this set. + * + * @author Adrien Hopkins + * @since 2019-04-14 + * @since v0.2.0 + */ + private static final class PrefixedUnitEntry + implements Entry { + private final String key; + private final Unit value; + + /** + * Creates the {@code PrefixedUnitEntry}. + * + * @param key key + * @param value value + * @since 2019-04-14 + * @since v0.2.0 + */ + public PrefixedUnitEntry(final String key, final Unit value) { + this.key = key; + this.value = value; + } + + /** + * @since 2019-05-03 + */ + @Override + public boolean equals(final Object o) { + if (!(o instanceof Map.Entry)) + return false; + final Map.Entry other = (Map.Entry) o; + return Objects.equals(this.getKey(), other.getKey()) + && Objects.equals(this.getValue(), other.getValue()); + } + + @Override + public String getKey() { + return this.key; + } + + @Override + public Unit getValue() { + return this.value; + } + + /** + * @since 2019-05-03 + */ + @Override + public int hashCode() { + return (this.getKey() == null ? 0 : this.getKey().hashCode()) + ^ (this.getValue() == null ? 0 + : this.getValue().hashCode()); + } + + @Override + public Unit setValue(final Unit value) { + throw new UnsupportedOperationException( + "Cannot set value in an immutable entry"); + } + + /** + * Returns a string representation of the entry. The format of the + * string is the string representation of the key, then the equals + * ({@code =}) character, then the string representation of the + * value. + * + * @since 2019-05-03 + */ + @Override + public String toString() { + return this.getKey() + "=" + this.getValue(); + } + } + + /** + * An iterator that iterates over the units of a + * {@code PrefixedUnitNameSet}. + * + * @author Adrien Hopkins + * @since 2019-04-14 + * @since v0.2.0 + */ + private static final class PrefixedUnitEntryIterator + implements Iterator> { + // position in the unit list + private int unitNamePosition = 0; + // the indices of the prefixes attached to the current unit + private final List prefixCoordinates = new ArrayList<>(); + + // values from the unit entry set + private final Map map; + private transient final List unitNames; + private transient final List prefixNames; + + /** + * Creates the + * {@code UnitsDatabase.PrefixedUnitMap.PrefixedUnitNameSet.PrefixedUnitNameIterator}. + * + * @since 2019-04-14 + * @since v0.2.0 + */ + public PrefixedUnitEntryIterator(final PrefixedUnitMap map) { + this.map = map; + this.unitNames = new ArrayList<>(map.units.keySet()); + this.prefixNames = new ArrayList<>(map.prefixes.keySet()); + } + + /** + * @return current unit name + * @since 2019-04-14 + * @since v0.2.0 + */ + private String getCurrentUnitName() { + final StringBuilder unitName = new StringBuilder(); + for (final int i : this.prefixCoordinates) { + unitName.append(this.prefixNames.get(i)); + } + unitName.append(this.unitNames.get(this.unitNamePosition)); + + return unitName.toString(); + } + + @Override + public boolean hasNext() { + if (this.unitNames.isEmpty()) + return false; + else { + if (this.prefixNames.isEmpty()) + return this.unitNamePosition >= this.unitNames.size() - 1; + else + return true; + } + } + + /** + * Changes this iterator's position to the next available one. + * + * @since 2019-04-14 + * @since v0.2.0 + */ + private void incrementPosition() { + this.unitNamePosition++; + + if (this.unitNamePosition >= this.unitNames.size()) { + // we have used all of our units, go to a different prefix + this.unitNamePosition = 0; + + // if the prefix coordinates are empty, then set it to [0] + if (this.prefixCoordinates.isEmpty()) { + this.prefixCoordinates.add(0, 0); + } else { + // get the prefix coordinate to increment, then increment + int i = this.prefixCoordinates.size() - 1; + this.prefixCoordinates.set(i, + this.prefixCoordinates.get(i) + 1); + + // fix any carrying errors + while (i >= 0 && this.prefixCoordinates + .get(i) >= this.prefixNames.size()) { + // carry over + this.prefixCoordinates.set(i--, 0); // null and + // decrement at the + // same time + + if (i < 0) { // we need to add a new coordinate + this.prefixCoordinates.add(0, 0); + } else { // increment an existing one + this.prefixCoordinates.set(i, + this.prefixCoordinates.get(i) + 1); + } + } + } + } + } + + @Override + public Entry next() { + // get next element + final Entry nextEntry = this.peek(); + + // iterate to next position + this.incrementPosition(); + + return nextEntry; + } + + /** + * @return the next element in the iterator, without iterating over + * it + * @since 2019-05-03 + */ + private Entry peek() { + if (!this.hasNext()) + throw new NoSuchElementException("No units left!"); + + // if I have prefixes, ensure I'm not using a nonlinear unit + // since all of the unprefixed stuff is done, just remove + // nonlinear units + if (!this.prefixCoordinates.isEmpty()) { + while (this.unitNamePosition < this.unitNames.size() + && !(this.map.get(this.unitNames.get( + this.unitNamePosition)) instanceof LinearUnit)) { + this.unitNames.remove(this.unitNamePosition); + } + } + + final String nextName = this.getCurrentUnitName(); + + return new PrefixedUnitEntry(nextName, this.map.get(nextName)); + } + + /** + * Returns a string representation of the object. The exact details + * of the representation are unspecified and subject to change. + * + * @since 2019-05-03 + */ + @Override + public String toString() { + return String.format( + "Iterator iterating over name-unit entries; next value is \"%s\"", + this.peek()); + } + } + + // the map that created this set + private final PrefixedUnitMap map; + + /** + * Creates the {@code PrefixedUnitNameSet}. + * + * @param map map that created this set + * @since 2019-04-13 + * @since v0.2.0 + */ + public PrefixedUnitEntrySet(final PrefixedUnitMap map) { + this.map = map; + } + + @Override + public boolean add(final Map.Entry e) { + throw new UnsupportedOperationException( + "Cannot add to an immutable set"); + } + + @Override + public boolean addAll( + final Collection> c) { + throw new UnsupportedOperationException( + "Cannot add to an immutable set"); + } + + @Override + public void clear() { + throw new UnsupportedOperationException( + "Cannot clear an immutable set"); + } + + @Override + public boolean contains(final Object o) { + // get the entry + final Entry entry; + + try { + // This is OK because I'm in a try-catch block, catching the + // exact exception that would be thrown. + @SuppressWarnings("unchecked") + final Entry tempEntry = (Entry) o; + entry = tempEntry; + } catch (final ClassCastException e) { + throw new IllegalArgumentException( + "Attempted to test for an entry using a non-entry."); + } + + return this.map.containsKey(entry.getKey()) + && this.map.get(entry.getKey()).equals(entry.getValue()); + } + + @Override + public boolean containsAll(final Collection c) { + for (final Object o : c) + if (!this.contains(o)) + return false; + return true; + } + + @Override + public boolean isEmpty() { + return this.map.isEmpty(); + } + + @Override + public Iterator> iterator() { + return new PrefixedUnitEntryIterator(this.map); + } + + @Override + public boolean remove(final Object o) { + throw new UnsupportedOperationException( + "Cannot remove from an immutable set"); + } + + @Override + public boolean removeAll(final Collection c) { + throw new UnsupportedOperationException( + "Cannot remove from an immutable set"); + } + + @Override + public boolean removeIf( + final Predicate> filter) { + throw new UnsupportedOperationException( + "Cannot remove from an immutable set"); + } + + @Override + public boolean retainAll(final Collection c) { + throw new UnsupportedOperationException( + "Cannot remove from an immutable set"); + } + + @Override + public int size() { + if (this.map.units.isEmpty()) + return 0; + else { + if (this.map.prefixes.isEmpty()) + return this.map.units.size(); + else + // infinite set + return Integer.MAX_VALUE; + } + } + + /** + * @throws IllegalStateException if the set is infinite in size + */ + @Override + public Object[] toArray() { + if (this.map.units.isEmpty() || this.map.prefixes.isEmpty()) + return super.toArray(); + else + // infinite set + throw new IllegalStateException( + "Cannot make an infinite set into an array."); + } + + /** + * @throws IllegalStateException if the set is infinite in size + */ + @Override + public T[] toArray(final T[] a) { + if (this.map.units.isEmpty() || this.map.prefixes.isEmpty()) + return super.toArray(a); + else + // infinite set + throw new IllegalStateException( + "Cannot make an infinite set into an array."); + } + + @Override + public String toString() { + if (this.map.units.isEmpty() || this.map.prefixes.isEmpty()) + return super.toString(); + else + return String.format( + "Infinite set of name-unit entries created from units %s and prefixes %s", + this.map.units, this.map.prefixes); + } + } + + /** + * The class used for unit name sets. + * + *

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

+ * + * @author Adrien Hopkins + * @since 2019-04-13 + * @since v0.2.0 + */ + private static final class PrefixedUnitNameSet + extends AbstractSet { + /** + * An iterator that iterates over the units of a + * {@code PrefixedUnitNameSet}. + * + * @author Adrien Hopkins + * @since 2019-04-14 + * @since v0.2.0 + */ + private static final class PrefixedUnitNameIterator + implements Iterator { + // position in the unit list + private int unitNamePosition = 0; + // the indices of the prefixes attached to the current unit + private final List prefixCoordinates = new ArrayList<>(); + + // values from the unit name set + private final Map map; + private transient final List unitNames; + private transient final List prefixNames; + + /** + * Creates the + * {@code UnitsDatabase.PrefixedUnitMap.PrefixedUnitNameSet.PrefixedUnitNameIterator}. + * + * @since 2019-04-14 + * @since v0.2.0 + */ + public PrefixedUnitNameIterator(final PrefixedUnitMap map) { + this.map = map; + this.unitNames = new ArrayList<>(map.units.keySet()); + this.prefixNames = new ArrayList<>(map.prefixes.keySet()); + } + + /** + * @return current unit name + * @since 2019-04-14 + * @since v0.2.0 + */ + private String getCurrentUnitName() { + final StringBuilder unitName = new StringBuilder(); + for (final int i : this.prefixCoordinates) { + unitName.append(this.prefixNames.get(i)); + } + unitName.append(this.unitNames.get(this.unitNamePosition)); + + return unitName.toString(); + } + + @Override + public boolean hasNext() { + if (this.unitNames.isEmpty()) + return false; + else { + if (this.prefixNames.isEmpty()) + return this.unitNamePosition >= this.unitNames.size() - 1; + else + return true; + } + } + + /** + * Changes this iterator's position to the next available one. + * + * @since 2019-04-14 + * @since v0.2.0 + */ + private void incrementPosition() { + this.unitNamePosition++; + + if (this.unitNamePosition >= this.unitNames.size()) { + // we have used all of our units, go to a different prefix + this.unitNamePosition = 0; + + // if the prefix coordinates are empty, then set it to [0] + if (this.prefixCoordinates.isEmpty()) { + this.prefixCoordinates.add(0, 0); + } else { + // get the prefix coordinate to increment, then increment + int i = this.prefixCoordinates.size() - 1; + this.prefixCoordinates.set(i, + this.prefixCoordinates.get(i) + 1); + + // fix any carrying errors + while (i >= 0 && this.prefixCoordinates + .get(i) >= this.prefixNames.size()) { + // carry over + this.prefixCoordinates.set(i--, 0); // null and + // decrement at the + // same time + + if (i < 0) { // we need to add a new coordinate + this.prefixCoordinates.add(0, 0); + } else { // increment an existing one + this.prefixCoordinates.set(i, + this.prefixCoordinates.get(i) + 1); + } + } + } + } + } + + @Override + public String next() { + final String nextName = this.peek(); + + this.incrementPosition(); + + return nextName; + } + + /** + * @return the next element in the iterator, without iterating over + * it + * @since 2019-05-03 + */ + private String peek() { + if (!this.hasNext()) + throw new NoSuchElementException("No units left!"); + // if I have prefixes, ensure I'm not using a nonlinear unit + // since all of the unprefixed stuff is done, just remove + // nonlinear units + if (!this.prefixCoordinates.isEmpty()) { + while (this.unitNamePosition < this.unitNames.size() + && !(this.map.get(this.unitNames.get( + this.unitNamePosition)) instanceof LinearUnit)) { + this.unitNames.remove(this.unitNamePosition); + } + } + + return this.getCurrentUnitName(); + } + + /** + * Returns a string representation of the object. The exact details + * of the representation are unspecified and subject to change. + * + * @since 2019-05-03 + */ + @Override + public String toString() { + return String.format( + "Iterator iterating over unit names; next value is \"%s\"", + this.peek()); + } + } + + // the map that created this set + private final PrefixedUnitMap map; + + /** + * Creates the {@code PrefixedUnitNameSet}. + * + * @param map map that created this set + * @since 2019-04-13 + * @since v0.2.0 + */ + public PrefixedUnitNameSet(final PrefixedUnitMap map) { + this.map = map; + } + + @Override + public boolean add(final String e) { + throw new UnsupportedOperationException( + "Cannot add to an immutable set"); + } + + @Override + public boolean addAll(final Collection c) { + throw new UnsupportedOperationException( + "Cannot add to an immutable set"); + } + + @Override + public void clear() { + throw new UnsupportedOperationException( + "Cannot clear an immutable set"); + } + + @Override + public boolean contains(final Object o) { + return this.map.containsKey(o); + } + + @Override + public boolean containsAll(final Collection c) { + for (final Object o : c) + if (!this.contains(o)) + return false; + return true; + } + + @Override + public boolean isEmpty() { + return this.map.isEmpty(); + } + + @Override + public Iterator iterator() { + return new PrefixedUnitNameIterator(this.map); + } + + @Override + public boolean remove(final Object o) { + throw new UnsupportedOperationException( + "Cannot remove from an immutable set"); + } + + @Override + public boolean removeAll(final Collection c) { + throw new UnsupportedOperationException( + "Cannot remove from an immutable set"); + } + + @Override + public boolean removeIf(final Predicate filter) { + throw new UnsupportedOperationException( + "Cannot remove from an immutable set"); + } + + @Override + public boolean retainAll(final Collection c) { + throw new UnsupportedOperationException( + "Cannot remove from an immutable set"); + } + + @Override + public int size() { + if (this.map.units.isEmpty()) + return 0; + else { + if (this.map.prefixes.isEmpty()) + return this.map.units.size(); + else + // infinite set + return Integer.MAX_VALUE; + } + } + + /** + * @throws IllegalStateException if the set is infinite in size + */ + @Override + public Object[] toArray() { + if (this.map.units.isEmpty() || this.map.prefixes.isEmpty()) + return super.toArray(); + else + // infinite set + throw new IllegalStateException( + "Cannot make an infinite set into an array."); + + } + + /** + * @throws IllegalStateException if the set is infinite in size + */ + @Override + public T[] toArray(final T[] a) { + if (this.map.units.isEmpty() || this.map.prefixes.isEmpty()) + return super.toArray(a); + else + // infinite set + throw new IllegalStateException( + "Cannot make an infinite set into an array."); + } + + @Override + public String toString() { + if (this.map.units.isEmpty() || this.map.prefixes.isEmpty()) + return super.toString(); + else + return String.format( + "Infinite set of name-unit entries created from units %s and prefixes %s", + this.map.units, this.map.prefixes); + } + } + + /** + * The units stored in this collection, without prefixes. + * + * @since 2019-04-13 + * @since v0.2.0 + */ + private final Map units; + + /** + * The available prefixes for use. + * + * @since 2019-04-13 + * @since v0.2.0 + */ + private final Map prefixes; + + // caches + private transient Collection values = null; + private transient Set keySet = null; + private transient Set> entrySet = null; + + /** + * Creates the {@code PrefixedUnitMap}. + * + * @param units map mapping unit names to units + * @param prefixes map mapping prefix names to prefixes + * @since 2019-04-13 + * @since v0.2.0 + */ + public PrefixedUnitMap(final Map units, + final Map prefixes) { + // I am making unmodifiable maps to ensure I don't accidentally make + // changes. + this.units = Collections.unmodifiableMap(units); + this.prefixes = Collections.unmodifiableMap(prefixes); + } + + @Override + public void clear() { + throw new UnsupportedOperationException( + "Cannot clear an immutable map"); + } + + @Override + public Unit compute(final String key, + final BiFunction remappingFunction) { + throw new UnsupportedOperationException( + "Cannot edit an immutable map"); + } + + @Override + public Unit computeIfAbsent(final String key, + final Function mappingFunction) { + throw new UnsupportedOperationException( + "Cannot edit an immutable map"); + } + + @Override + public Unit computeIfPresent(final String key, + final BiFunction remappingFunction) { + throw new UnsupportedOperationException( + "Cannot edit an immutable map"); + } + + @Override + public boolean containsKey(final Object key) { + // First, test if there is a unit with the key + if (this.units.containsKey(key)) + return true; + + // Next, try to cast it to String + if (!(key instanceof String)) + throw new IllegalArgumentException( + "Attempted to test for a unit using a non-string name."); + final String unitName = (String) key; + + // Then, look for the longest prefix that is attached to a valid unit + String longestPrefix = null; + int longestLength = 0; + + for (final String prefixName : this.prefixes.keySet()) { + // a prefix name is valid if: + // - it is prefixed (i.e. the unit name starts with it) + // - it is longer than the existing largest prefix (since I am + // looking for the longest valid prefix) + // - the part after the prefix is a valid unit name + // - the unit described that name is a linear unit (since only + // linear units can have prefixes) + if (unitName.startsWith(prefixName) + && prefixName.length() > longestLength) { + final String rest = unitName.substring(prefixName.length()); + if (this.containsKey(rest) + && this.get(rest) instanceof LinearUnit) { + longestPrefix = prefixName; + longestLength = prefixName.length(); + } + } + } + + return longestPrefix != null; + } + + /** + * {@inheritDoc} + * + *

+ * Because of ambiguities between prefixes (i.e. kilokilo = mega), this + * method only tests for prefixless units. + *

+ */ + @Override + public boolean containsValue(final Object value) { + return this.units.containsValue(value); + } + + @Override + public Set> entrySet() { + if (this.entrySet == null) { + this.entrySet = new PrefixedUnitEntrySet(this); + } + return this.entrySet; + } + + @Override + public Unit get(final Object key) { + // First, test if there is a unit with the key + if (this.units.containsKey(key)) + return this.units.get(key); + + // Next, try to cast it to String + if (!(key instanceof String)) + throw new IllegalArgumentException( + "Attempted to obtain a unit using a non-string name."); + final String unitName = (String) key; + + // Then, look for the longest prefix that is attached to a valid unit + String longestPrefix = null; + int longestLength = 0; + + for (final String prefixName : this.prefixes.keySet()) { + // a prefix name is valid if: + // - it is prefixed (i.e. the unit name starts with it) + // - it is longer than the existing largest prefix (since I am + // looking for the longest valid prefix) + // - the part after the prefix is a valid unit name + // - the unit described that name is a linear unit (since only + // linear units can have prefixes) + if (unitName.startsWith(prefixName) + && prefixName.length() > longestLength) { + final String rest = unitName.substring(prefixName.length()); + if (this.containsKey(rest) + && this.get(rest) instanceof LinearUnit) { + longestPrefix = prefixName; + longestLength = prefixName.length(); + } + } + } + + // if none found, returns null + if (longestPrefix == null) + return null; + else { + // get necessary data + final String rest = unitName.substring(longestLength); + // this cast will not fail because I verified that it would work + // before selecting this prefix + final LinearUnit unit = (LinearUnit) this.get(rest); + final UnitPrefix prefix = this.prefixes.get(longestPrefix); + + return unit.withPrefix(prefix); + } + } + + @Override + public boolean isEmpty() { + return this.units.isEmpty(); + } + + @Override + public Set keySet() { + if (this.keySet == null) { + this.keySet = new PrefixedUnitNameSet(this); + } + return this.keySet; + } + + @Override + public Unit merge(final String key, final Unit value, + final BiFunction remappingFunction) { + throw new UnsupportedOperationException( + "Cannot merge into an immutable map"); + } + + @Override + public Unit put(final String key, final Unit value) { + throw new UnsupportedOperationException( + "Cannot add entries to an immutable map"); + } + + @Override + public void putAll(final Map m) { + throw new UnsupportedOperationException( + "Cannot add entries to an immutable map"); + } + + @Override + public Unit putIfAbsent(final String key, final Unit value) { + throw new UnsupportedOperationException( + "Cannot add entries to an immutable map"); + } + + @Override + public Unit remove(final Object key) { + throw new UnsupportedOperationException( + "Cannot remove entries from an immutable map"); + } + + @Override + public boolean remove(final Object key, final Object value) { + throw new UnsupportedOperationException( + "Cannot remove entries from an immutable map"); + } + + @Override + public Unit replace(final String key, final Unit value) { + throw new UnsupportedOperationException( + "Cannot replace entries in an immutable map"); + } + + @Override + public boolean replace(final String key, final Unit oldValue, + final Unit newValue) { + throw new UnsupportedOperationException( + "Cannot replace entries in an immutable map"); + } + + @Override + public void replaceAll( + final BiFunction function) { + throw new UnsupportedOperationException( + "Cannot replace entries in an immutable map"); + } + + @Override + public int size() { + if (this.units.isEmpty()) + return 0; + else { + if (this.prefixes.isEmpty()) + return this.units.size(); + else + // infinite set + return Integer.MAX_VALUE; + } + } + + @Override + public String toString() { + if (this.units.isEmpty() || this.prefixes.isEmpty()) + return super.toString(); + else + return String.format( + "Infinite map of name-unit entries created from units %s and prefixes %s", + this.units, this.prefixes); + } + + /** + * {@inheritDoc} + * + *

+ * Because of ambiguities between prefixes (i.e. kilokilo = mega), this + * method ignores prefixes. + *

+ */ + @Override + public Collection values() { + if (this.values == null) { + this.values = Collections + .unmodifiableCollection(this.units.values()); + } + return this.values; + } + } + + /** + * Replacements done to *all* expression types + */ + private static final Map EXPRESSION_REPLACEMENTS = new HashMap<>(); + + // add data to expression replacements + static { + // add spaces around operators + for (final String operator : Arrays.asList("\\*", "/", "\\^")) { + EXPRESSION_REPLACEMENTS.put(Pattern.compile(operator), + " " + operator + " "); + } + + // replace multiple spaces with a single space + EXPRESSION_REPLACEMENTS.put(Pattern.compile(" +"), " "); + // place brackets around any expression of the form "number unit", with or + // without the space + EXPRESSION_REPLACEMENTS.put(Pattern.compile("((?:-?[1-9]\\d*|0)" // integer + + "(?:\\.\\d+(?:[eE]\\d+))?)" // optional decimal point with numbers + // after it + + "\\s*" // optional space(s) + + "([a-zA-Z]+(?:\\^\\d+)?" // any string of letters + + "(?:\\s+[a-zA-Z]+(?:\\^\\d+)?))" // optional other letters + + "(?!-?\\d)" // no number directly afterwards (avoids matching + // "1e3") + ), "\\($1 $2\\)"); + } + + /** + * A regular expression that separates names and expressions in unit files. + */ + private static final Pattern NAME_EXPRESSION = Pattern + .compile("(\\S+)\\s+(\\S.*)"); + + /** + * The exponent operator + * + * @param base base of exponentiation + * @param exponentUnit exponent + * @return result + * @since 2019-04-10 + * @since v0.2.0 + */ + private static final LinearUnit exponentiateUnits(final LinearUnit base, + final LinearUnit exponentUnit) { + // exponent function - first check if o2 is a number, + if (exponentUnit.getBase().equals(SI.ONE.getBase())) { + // then check if it is an integer, + final double exponent = exponentUnit.getConversionFactor(); + if (DecimalComparison.equals(exponent % 1, 0)) + // then exponentiate + return base.toExponent((int) (exponent + 0.5)); + else + // not an integer + throw new UnsupportedOperationException( + "Decimal exponents are currently not supported."); + } else + // not a number + throw new IllegalArgumentException("Exponents must be numbers."); + } + + /** + * The exponent operator + * + * @param base base of exponentiation + * @param exponentUnit exponent + * @return result + * @since 2020-08-04 + */ + private static final LinearUnitValue exponentiateUnitValues( + final LinearUnitValue base, final LinearUnitValue exponentValue) { + // exponent function - first check if o2 is a number, + if (exponentValue.canConvertTo(SI.ONE)) { + // then check if it is an integer, + final double exponent = exponentValue.getValueExact(); + if (DecimalComparison.equals(exponent % 1, 0)) + // then exponentiate + return base.toExponent((int) (exponent + 0.5)); + else + // not an integer + throw new UnsupportedOperationException( + "Decimal exponents are currently not supported."); + } else + // not a number + throw new IllegalArgumentException("Exponents must be numbers."); + } + + /** + * The units in this system, excluding prefixes. + * + * @since 2019-01-07 + * @since v0.1.0 + */ + private final Map prefixlessUnits; + + /** + * The unit prefixes in this system. + * + * @since 2019-01-14 + * @since v0.1.0 + */ + private final Map prefixes; + + /** + * The dimensions in this system. + * + * @since 2019-03-14 + * @since v0.2.0 + */ + private final Map> dimensions; + + /** + * A map mapping strings to units (including prefixes) + * + * @since 2019-04-13 + * @since v0.2.0 + */ + private final Map units; + + /** + * The rule that specifies when prefix repetition is allowed. It takes in one + * argument: a list of the prefixes being applied to the unit + *

+ * The prefixes are inputted in application order. This means that + * testing whether "kilomegagigametre" is a valid unit is equivalent to + * running the following code (assuming all variables are defined correctly): + *
+ * {@code prefixRepetitionRule.test(Arrays.asList(giga, mega, kilo))} + */ + private Predicate> prefixRepetitionRule; + + /** + * A parser that can parse unit expressions. + * + * @since 2019-03-22 + * @since v0.2.0 + */ + private final ExpressionParser unitExpressionParser = new ExpressionParser.Builder<>( + this::getLinearUnit).addBinaryOperator("+", (o1, o2) -> o1.plus(o2), 0) + .addBinaryOperator("-", (o1, o2) -> o1.minus(o2), 0) + .addBinaryOperator("*", (o1, o2) -> o1.times(o2), 1) + .addSpaceFunction("*") + .addBinaryOperator("/", (o1, o2) -> o1.dividedBy(o2), 1) + .addBinaryOperator("^", UnitDatabase::exponentiateUnits, 2) + .build(); + + /** + * A parser that can parse unit value expressions. + * + * @since 2020-08-04 + */ + private final ExpressionParser unitValueExpressionParser = new ExpressionParser.Builder<>( + this::getLinearUnitValue) + .addBinaryOperator("+", (o1, o2) -> o1.plus(o2), 0) + .addBinaryOperator("-", (o1, o2) -> o1.minus(o2), 0) + .addBinaryOperator("*", (o1, o2) -> o1.times(o2), 1) + .addSpaceFunction("*") + .addBinaryOperator("/", (o1, o2) -> o1.dividedBy(o2), 1) + .addBinaryOperator("^", UnitDatabase::exponentiateUnitValues, 2) + .build(); + + /** + * A parser that can parse unit prefix expressions + * + * @since 2019-04-13 + * @since v0.2.0 + */ + private final ExpressionParser prefixExpressionParser = new ExpressionParser.Builder<>( + this::getPrefix).addBinaryOperator("*", (o1, o2) -> o1.times(o2), 0) + .addSpaceFunction("*") + .addBinaryOperator("/", (o1, o2) -> o1.dividedBy(o2), 0) + .addBinaryOperator("^", + (o1, o2) -> o1.toExponent(o2.getMultiplier()), 1) + .build(); + + /** + * A parser that can parse unit dimension expressions. + * + * @since 2019-04-13 + * @since v0.2.0 + */ + private final ExpressionParser> unitDimensionParser = new ExpressionParser.Builder<>( + this::getDimension).addBinaryOperator("*", (o1, o2) -> o1.times(o2), 0) + .addSpaceFunction("*") + .addBinaryOperator("/", (o1, o2) -> o1.dividedBy(o2), 0).build(); + + /** + * Creates the {@code UnitsDatabase}. + * + * @since 2019-01-10 + * @since v0.1.0 + */ + public UnitDatabase() { + this(prefixes -> true); + } + + /** + * Creates the {@code UnitsDatabase} + * + * @param prefixRepetitionRule the rule that determines when prefix + * repetition is allowed + * @since 2020-08-26 + */ + public UnitDatabase(Predicate> prefixRepetitionRule) { + this.prefixlessUnits = new HashMap<>(); + this.prefixes = new HashMap<>(); + this.dimensions = new HashMap<>(); + this.prefixRepetitionRule = prefixRepetitionRule; + this.units = ConditionalExistenceCollections.conditionalExistenceMap( + new PrefixedUnitMap(this.prefixlessUnits, this.prefixes), + entry -> this.prefixRepetitionRule + .test(this.getPrefixesFromName(entry.getKey()))); + } + + /** + * Adds a unit dimension to the database. + * + * @param name dimension's name + * @param dimension dimension to add + * @throws NullPointerException if name or dimension is null + * @since 2019-03-14 + * @since v0.2.0 + */ + public void addDimension(final String name, + final ObjectProduct dimension) { + this.dimensions.put( + Objects.requireNonNull(name, "name must not be null."), + Objects.requireNonNull(dimension, "dimension must not be null.")); + } + + /** + * Adds to the list from a line in a unit dimension file. + * + * @param line line to look at + * @param lineCounter number of line, for error messages + * @since 2019-04-10 + * @since v0.2.0 + */ + private void addDimensionFromLine(final String line, + final long lineCounter) { + // ignore lines that start with a # sign - they're comments + if (line.isEmpty()) + return; + if (line.contains("#")) { + this.addDimensionFromLine(line.substring(0, line.indexOf("#")), + lineCounter); + return; + } + + // divide line into name and expression + final Matcher lineMatcher = NAME_EXPRESSION.matcher(line); + if (!lineMatcher.matches()) + throw new IllegalArgumentException(String.format( + "Error at line %d: Lines of a dimension file must consist of a dimension name, then spaces or tabs, then a dimension expression.", + lineCounter)); + final String name = lineMatcher.group(1); + final String expression = lineMatcher.group(2); + + if (name.endsWith(" ")) { + System.err.printf("Warning - line %d's dimension name ends in a space", + lineCounter); + } + + // if expression is "!", search for an existing dimension + // if no unit found, throw an error + if (expression.equals("!")) { + if (!this.containsDimensionName(name)) + throw new IllegalArgumentException(String.format( + "! used but no dimension found (line %d).", lineCounter)); + } else { + // it's a unit, get the unit + final ObjectProduct dimension; + try { + dimension = this.getDimensionFromExpression(expression); + } catch (final IllegalArgumentException e) { + System.err.printf("Parsing error on line %d:%n", lineCounter); + throw e; + } + + this.addDimension(name, dimension); + } + } + + /** + * Adds a unit prefix to the database. + * + * @param name prefix's name + * @param prefix prefix to add + * @throws NullPointerException if name or prefix is null + * @since 2019-01-14 + * @since v0.1.0 + */ + public void addPrefix(final String name, final UnitPrefix prefix) { + this.prefixes.put(Objects.requireNonNull(name, "name must not be null."), + Objects.requireNonNull(prefix, "prefix must not be null.")); + } + + /** + * Adds a unit to the database. + * + * @param name unit's name + * @param unit unit to add + * @throws NullPointerException if unit is null + * @since 2019-01-10 + * @since v0.1.0 + */ + public void addUnit(final String name, final Unit unit) { + this.prefixlessUnits.put( + Objects.requireNonNull(name, "name must not be null."), + Objects.requireNonNull(unit, "unit must not be null.")); + } + + /** + * Adds to the list from a line in a unit file. + * + * @param line line to look at + * @param lineCounter number of line, for error messages + * @since 2019-04-10 + * @since v0.2.0 + */ + private void addUnitOrPrefixFromLine(final String line, + final long lineCounter) { + // ignore lines that start with a # sign - they're comments + if (line.isEmpty()) + return; + if (line.contains("#")) { + this.addUnitOrPrefixFromLine(line.substring(0, line.indexOf("#")), + lineCounter); + return; + } + + // divide line into name and expression + final Matcher lineMatcher = NAME_EXPRESSION.matcher(line); + if (!lineMatcher.matches()) + throw new IllegalArgumentException(String.format( + "Error at line %d: Lines of a unit file must consist of a unit name, then spaces or tabs, then a unit expression.", + lineCounter)); + final String name = lineMatcher.group(1); + + final String expression = lineMatcher.group(2); + + if (name.endsWith(" ")) { + System.err.printf("Warning - line %d's unit name ends in a space", + lineCounter); + } + + // if expression is "!", search for an existing unit + // if no unit found, throw an error + if (expression.equals("!")) { + if (!this.containsUnitName(name)) + throw new IllegalArgumentException(String + .format("! used but no unit found (line %d).", lineCounter)); + } else { + if (name.endsWith("-")) { + final UnitPrefix prefix; + try { + prefix = this.getPrefixFromExpression(expression); + } catch (final IllegalArgumentException e) { + System.err.printf("Parsing error on line %d:%n", lineCounter); + throw e; + } + this.addPrefix(name.substring(0, name.length() - 1), prefix); + } else { + // it's a unit, get the unit + final Unit unit; + try { + unit = this.getUnitFromExpression(expression); + } catch (final IllegalArgumentException e) { + System.err.printf("Parsing error on line %d:%n", lineCounter); + throw e; + } + + this.addUnit(name, unit); + } + } + } + + /** + * Tests if the database has a unit dimension with this name. + * + * @param name name to test + * @return if database contains name + * @since 2019-03-14 + * @since v0.2.0 + */ + public boolean containsDimensionName(final String name) { + return this.dimensions.containsKey(name); + } + + /** + * Tests if the database has a unit prefix with this name. + * + * @param name name to test + * @return if database contains name + * @since 2019-01-13 + * @since v0.1.0 + */ + public boolean containsPrefixName(final String name) { + return this.prefixes.containsKey(name); + } + + /** + * Tests if the database has a unit with this name, taking prefixes into + * consideration + * + * @param name name to test + * @return if database contains name + * @since 2019-01-13 + * @since v0.1.0 + */ + public boolean containsUnitName(final String name) { + return this.units.containsKey(name); + } + + /** + * @return a map mapping dimension names to dimensions + * @since 2019-04-13 + * @since v0.2.0 + */ + public Map> dimensionMap() { + return Collections.unmodifiableMap(this.dimensions); + } + + /** + * Evaluates a unit expression, following the same rules as + * {@link #getUnitFromExpression}. + * + * @param expression expression to parse + * @return {@code LinearUnitValue} representing value of expression + * @since 2020-08-04 + */ + public LinearUnitValue evaluateUnitExpression(final String expression) { + Objects.requireNonNull(expression, "expression must not be null."); + + // attempt to get a unit as an alias, or a number with precision first + if (this.containsUnitName(expression)) + return this.getLinearUnitValue(expression); + + // force operators to have spaces + String modifiedExpression = expression; + modifiedExpression = modifiedExpression.replaceAll("\\+", " \\+ "); + modifiedExpression = modifiedExpression.replaceAll("-", " - "); + + // format expression + for (final Entry replacement : EXPRESSION_REPLACEMENTS + .entrySet()) { + modifiedExpression = replacement.getKey().matcher(modifiedExpression) + .replaceAll(replacement.getValue()); + } + + // the previous operation breaks negative numbers, fix them! + // (i.e. -2 becomes - 2) + // FIXME the previous operaton also breaks stuff like "1e-5" + for (int i = 0; i < modifiedExpression.length(); i++) { + if (modifiedExpression.charAt(i) == '-' + && (i < 2 || Arrays.asList('+', '-', '*', '/', '^') + .contains(modifiedExpression.charAt(i - 2)))) { + // found a broken negative number + modifiedExpression = modifiedExpression.substring(0, i + 1) + + modifiedExpression.substring(i + 2); + } + } + + return this.unitValueExpressionParser.parseExpression(modifiedExpression); + } + + /** + * Gets a unit dimension from the database using its name. + * + *

+ * This method accepts exponents, like "L^3" + *

+ * + * @param name dimension's name + * @return dimension + * @since 2019-03-14 + * @since v0.2.0 + */ + public ObjectProduct getDimension(final String name) { + Objects.requireNonNull(name, "name must not be null."); + if (name.contains("^")) { + final String[] baseAndExponent = name.split("\\^"); + + final ObjectProduct base = this + .getDimension(baseAndExponent[0]); + + final int exponent; + try { + exponent = Integer + .parseInt(baseAndExponent[baseAndExponent.length - 1]); + } catch (final NumberFormatException e2) { + throw new IllegalArgumentException("Exponent must be an integer."); + } + + return base.toExponent(exponent); + } + return this.dimensions.get(name); + } + + /** + * Uses the database's data to parse an expression into a unit dimension + *

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

    + *
  • The name of a unit dimension, which multiplies or divides the result + * based on preceding operators
  • + *
  • The operators '*' and '/', which multiply and divide (note that just + * putting two unit dimensions next to each other is equivalent to + * multiplication)
  • + *
  • The operator '^' which exponentiates. Exponents must be integers.
  • + *
+ * + * @param expression expression to parse + * @throws IllegalArgumentException if the expression cannot be parsed + * @throws NullPointerException if expression is null + * @since 2019-04-13 + * @since v0.2.0 + */ + public ObjectProduct getDimensionFromExpression( + final String expression) { + Objects.requireNonNull(expression, "expression must not be null."); + + // attempt to get a dimension as an alias first + if (this.containsDimensionName(expression)) + return this.getDimension(expression); + + // force operators to have spaces + String modifiedExpression = expression; + + // format expression + for (final Entry replacement : EXPRESSION_REPLACEMENTS + .entrySet()) { + modifiedExpression = replacement.getKey().matcher(modifiedExpression) + .replaceAll(replacement.getValue()); + } + modifiedExpression = modifiedExpression.replaceAll(" *\\^ *", "\\^"); + + return this.unitDimensionParser.parseExpression(modifiedExpression); + } + + /** + * Gets a unit. If it is linear, cast it to a LinearUnit and return it. + * Otherwise, throw an {@code IllegalArgumentException}. + * + * @param name unit's name + * @return unit + * @since 2019-03-22 + * @since v0.2.0 + */ + private LinearUnit getLinearUnit(final String name) { + // see if I am using a function-unit like tempC(100) + Objects.requireNonNull(name, "name may not be null"); + if (name.contains("(") && name.contains(")")) { + // break it into function name and value + final List parts = Arrays.asList(name.split("\\(")); + if (parts.size() != 2) + throw new IllegalArgumentException( + "Format nonlinear units like: unit(value)."); + + // solve the function + final Unit unit = this.getUnit(parts.get(0)); + final double value = Double.parseDouble( + parts.get(1).substring(0, parts.get(1).length() - 1)); + return LinearUnit.fromUnitValue(unit, value); + } else { + // get a linear unit + final Unit unit = this.getUnit(name); + + if (unit instanceof LinearUnit) + return (LinearUnit) unit; + else + throw new IllegalArgumentException( + String.format("%s is not a linear unit.", name)); + } + } + + /** + * Gets a {@code LinearUnitValue} from a unit name. Nonlinear units will be + * converted to their base units. + * + * @param name name of unit + * @return {@code LinearUnitValue} instance + * @since 2020-08-04 + */ + private LinearUnitValue getLinearUnitValue(final String name) { + try { + // try to parse it as a number - otherwise it is not a number! + final BigDecimal number = new BigDecimal(name); + + final double uncertainty = Math.pow(10, -number.scale()); + return LinearUnitValue.of(SI.ONE, + UncertainDouble.of(number.doubleValue(), uncertainty)); + } catch (final NumberFormatException e) { + return LinearUnitValue.getExact(this.getLinearUnit(name), 1); + } + } + + /** + * Gets a unit prefix from the database from its name + * + * @param name prefix's name + * @return prefix + * @since 2019-01-10 + * @since v0.1.0 + */ + public UnitPrefix getPrefix(final String name) { + try { + return UnitPrefix.valueOf(Double.parseDouble(name)); + } catch (final NumberFormatException e) { + return this.prefixes.get(name); + } + } + + /** + * Gets all of the prefixes that are on a unit name, in application order. + * + * @param unitName name of unit + * @return prefixes + * @since 2020-08-26 + */ + List getPrefixesFromName(final String unitName) { + final List prefixes = new ArrayList<>(); + String name = unitName; + + while (!this.prefixlessUnits.containsKey(name)) { + // find the longest prefix + String longestPrefixName = null; + int longestLength = name.length(); + + while (longestPrefixName == null) { + longestLength--; + if (longestLength <= 0) + throw new AssertionError( + "No prefix found in " + name + ", but it is not a unit!"); + if (this.prefixes.containsKey(name.substring(0, longestLength))) { + longestPrefixName = name.substring(0, longestLength); + } + } + + // longest prefix found! + final UnitPrefix prefix = this.getPrefix(longestPrefixName); + prefixes.add(0, prefix); + name = name.substring(longestLength); + } + return prefixes; + } + + /** + * Gets a unit prefix from a prefix expression + *

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

+ * + * @param expression expression to input + * @return prefix + * @throws IllegalArgumentException if expression cannot be parsed + * @throws NullPointerException if any argument is null + * @since 2019-01-14 + * @since v0.1.0 + */ + public UnitPrefix getPrefixFromExpression(final String expression) { + Objects.requireNonNull(expression, "expression must not be null."); + + // attempt to get a unit as an alias first + if (this.containsUnitName(expression)) + return this.getPrefix(expression); + + // force operators to have spaces + String modifiedExpression = expression; + + // format expression + for (final Entry replacement : EXPRESSION_REPLACEMENTS + .entrySet()) { + modifiedExpression = replacement.getKey().matcher(modifiedExpression) + .replaceAll(replacement.getValue()); + } + + return this.prefixExpressionParser.parseExpression(modifiedExpression); + } + + /** + * @return the prefixRepetitionRule + * @since 2020-08-26 + */ + public final Predicate> getPrefixRepetitionRule() { + return this.prefixRepetitionRule; + } + + /** + * Gets a unit from the database from its name, looking for prefixes. + * + * @param name unit's name + * @return unit + * @since 2019-01-10 + * @since v0.1.0 + */ + public Unit getUnit(final String name) { + try { + final double value = Double.parseDouble(name); + return SI.ONE.times(value); + } catch (final NumberFormatException e) { + final Unit unit = this.units.get(name); + if (unit == null) + throw new NoSuchElementException("No unit " + name); + else if (unit.getPrimaryName().isEmpty()) + return unit.withName(NameSymbol.ofName(name)); + else if (!unit.getPrimaryName().get().equals(name)) { + final Set otherNames = new HashSet<>(unit.getOtherNames()); + otherNames.add(unit.getPrimaryName().get()); + return unit.withName(NameSymbol.ofNullable(name, + unit.getSymbol().orElse(null), otherNames)); + } else if (!unit.getOtherNames().contains(name)) { + final Set otherNames = new HashSet<>(unit.getOtherNames()); + otherNames.add(name); + return unit.withName( + NameSymbol.ofNullable(unit.getPrimaryName().orElse(null), + unit.getSymbol().orElse(null), otherNames)); + } else + return unit; + } + + } + + /** + * Uses the database's unit data to parse an expression into a unit + *

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

    + *
  • The name of a unit, which multiplies or divides the result based on + * preceding operators
  • + *
  • The operators '*' and '/', which multiply and divide (note that just + * putting two units or values next to each other is equivalent to + * multiplication)
  • + *
  • The operator '^' which exponentiates. Exponents must be integers.
  • + *
  • A number which is multiplied or divided
  • + *
+ * This method only works with linear units. + * + * @param expression expression to parse + * @throws IllegalArgumentException if the expression cannot be parsed + * @throws NullPointerException if expression is null + * @since 2019-01-07 + * @since v0.1.0 + */ + public Unit getUnitFromExpression(final String expression) { + Objects.requireNonNull(expression, "expression must not be null."); + + // attempt to get a unit as an alias first + if (this.containsUnitName(expression)) + return this.getUnit(expression); + + // force operators to have spaces + String modifiedExpression = expression; + modifiedExpression = modifiedExpression.replaceAll("\\+", " \\+ "); + modifiedExpression = modifiedExpression.replaceAll("-", " - "); + + // format expression + for (final Entry replacement : EXPRESSION_REPLACEMENTS + .entrySet()) { + modifiedExpression = replacement.getKey().matcher(modifiedExpression) + .replaceAll(replacement.getValue()); + } + + // the previous operation breaks negative numbers, fix them! + // (i.e. -2 becomes - 2) + for (int i = 0; i < modifiedExpression.length(); i++) { + if (modifiedExpression.charAt(i) == '-' + && (i < 2 || Arrays.asList('+', '-', '*', '/', '^') + .contains(modifiedExpression.charAt(i - 2)))) { + // found a broken negative number + modifiedExpression = modifiedExpression.substring(0, i + 1) + + modifiedExpression.substring(i + 2); + } + } + + return this.unitExpressionParser.parseExpression(modifiedExpression); + } + + /** + * Adds all dimensions from a file, using data from the database to parse + * them. + *

+ * Each line in the file should consist of a name and an expression (parsed + * by getDimensionFromExpression) separated by any number of tab characters. + *

+ *

+ * Allowed exceptions: + *

    + *
  • Anything after a '#' character is considered a comment and + * ignored.
  • + *
  • Blank lines are also ignored
  • + *
  • If an expression consists of a single exclamation point, instead of + * parsing it, this method will search the database for an existing unit. If + * no unit is found, an IllegalArgumentException is thrown. This is used to + * define initial units and ensure that the database contains them.
  • + *
+ * + * @param file file to read + * @throws IllegalArgumentException if the file cannot be parsed, found or + * read + * @throws NullPointerException if file is null + * @since 2019-01-13 + * @since v0.1.0 + */ + public void loadDimensionFile(final Path file) { + Objects.requireNonNull(file, "file must not be null."); + try { + long lineCounter = 0; + for (final String line : Files.readAllLines(file)) { + this.addDimensionFromLine(line, ++lineCounter); + } + } catch (final FileNotFoundException e) { + throw new IllegalArgumentException("Could not find file " + file, e); + } catch (final IOException e) { + throw new IllegalArgumentException("Could not read file " + file, e); + } + } + + /** + * Adds all units from a file, using data from the database to parse them. + *

+ * Each line in the file should consist of a name and an expression (parsed + * by getUnitFromExpression) separated by any number of tab characters. + *

+ *

+ * Allowed exceptions: + *

    + *
  • Anything after a '#' character is considered a comment and + * ignored.
  • + *
  • Blank lines are also ignored
  • + *
  • If an expression consists of a single exclamation point, instead of + * parsing it, this method will search the database for an existing unit. If + * no unit is found, an IllegalArgumentException is thrown. This is used to + * define initial units and ensure that the database contains them.
  • + *
+ * + * @param file file to read + * @throws IllegalArgumentException if the file cannot be parsed, found or + * read + * @throws NullPointerException if file is null + * @since 2019-01-13 + * @since v0.1.0 + */ + public void loadUnitsFile(final Path file) { + Objects.requireNonNull(file, "file must not be null."); + try { + long lineCounter = 0; + for (final String line : Files.readAllLines(file)) { + this.addUnitOrPrefixFromLine(line, ++lineCounter); + } + } catch (final FileNotFoundException e) { + throw new IllegalArgumentException("Could not find file " + file, e); + } catch (final IOException e) { + throw new IllegalArgumentException("Could not read file " + file, e); + } + } + + /** + * @return a map mapping prefix names to prefixes + * @since 2019-04-13 + * @since v0.2.0 + */ + public Map prefixMap() { + return Collections.unmodifiableMap(this.prefixes); + } + + /** + * @param prefixRepetitionRule the prefixRepetitionRule to set + * @since 2020-08-26 + */ + public final void setPrefixRepetitionRule( + Predicate> prefixRepetitionRule) { + this.prefixRepetitionRule = prefixRepetitionRule; + } + + /** + * @return a string stating the number of units, prefixes and dimensions in + * the database + */ + @Override + public String toString() { + return String.format( + "Unit Database with %d units, %d unit prefixes and %d dimensions", + this.prefixlessUnits.size(), this.prefixes.size(), + this.dimensions.size()); + } + + /** + * Returns a map mapping unit names to units, including units with prefixes. + *

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

+ *

+ * Specifically, the operations that will throw an IllegalStateException if + * the map is infinite in size are: + *

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

+ *

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

+ * + * @return a map mapping unit names to units, including prefixed names + * @since 2019-04-13 + * @since v0.2.0 + */ + public Map unitMap() { + return this.units; // PrefixedUnitMap is immutable so I don't need to make + // an unmodifiable map. + } + + /** + * @return a map mapping unit names to units, ignoring prefixes + * @since 2019-04-13 + * @since v0.2.0 + */ + public Map unitMapPrefixless() { + return Collections.unmodifiableMap(this.prefixlessUnits); + } +} diff --git a/src/main/java/org/unitConverter/unit/UnitPrefix.java b/src/main/java/org/unitConverter/unit/UnitPrefix.java new file mode 100644 index 0000000..31cc0b3 --- /dev/null +++ b/src/main/java/org/unitConverter/unit/UnitPrefix.java @@ -0,0 +1,242 @@ +/** + * 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 . + */ +package org.unitConverter.unit; + +import java.util.Objects; +import java.util.Optional; +import java.util.Set; + +import org.unitConverter.math.DecimalComparison; + +/** + * A prefix that can be applied to a {@code LinearUnit} to multiply it by some value + * + * @author Adrien Hopkins + * @since 2019-10-16 + */ +public final class UnitPrefix { + /** + * Gets a {@code UnitPrefix} from a multiplier + * + * @param multiplier + * multiplier of prefix + * @return prefix + * @since 2019-10-16 + */ + public static UnitPrefix valueOf(final double multiplier) { + return new UnitPrefix(multiplier, NameSymbol.EMPTY); + } + + /** + * Gets a {@code UnitPrefix} from a multiplier and a name + * + * @param multiplier + * multiplier of prefix + * @param ns + * name(s) and symbol of prefix + * @return prefix + * @since 2019-10-16 + * @throws NullPointerException + * if ns is null + */ + public static UnitPrefix valueOf(final double multiplier, final NameSymbol ns) { + return new UnitPrefix(multiplier, Objects.requireNonNull(ns, "ns must not be null.")); + } + + /** + * This prefix's primary name + */ + private final Optional primaryName; + + /** + * This prefix's symbol + */ + private final Optional symbol; + + /** + * Other names and symbols used by this prefix + */ + private final Set otherNames; + + /** + * The number that this prefix multiplies units by + * + * @since 2019-10-16 + */ + private final double multiplier; + + /** + * Creates the {@code DefaultUnitPrefix}. + * + * @param multiplier + * @since 2019-01-14 + * @since v0.2.0 + */ + private UnitPrefix(final double multiplier, final NameSymbol ns) { + this.multiplier = multiplier; + this.primaryName = ns.getPrimaryName(); + this.symbol = ns.getSymbol(); + this.otherNames = ns.getOtherNames(); + } + + /** + * Divides this prefix by a scalar + * + * @param divisor + * number to divide by + * @return quotient of prefix and scalar + * @since 2019-10-16 + */ + public UnitPrefix dividedBy(final double divisor) { + return valueOf(this.getMultiplier() / divisor); + } + + /** + * Divides this prefix by {@code other}. + * + * @param other + * prefix to divide by + * @return quotient of prefixes + * @since 2019-04-13 + * @since v0.2.0 + */ + public UnitPrefix dividedBy(final UnitPrefix other) { + return valueOf(this.getMultiplier() / other.getMultiplier()); + } + + /** + * {@inheritDoc} + * + * Uses the prefix's multiplier to determine equality. + */ + @Override + public boolean equals(final Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (!(obj instanceof UnitPrefix)) + return false; + final UnitPrefix other = (UnitPrefix) obj; + return DecimalComparison.equals(this.getMultiplier(), other.getMultiplier()); + } + + /** + * @return prefix's multiplier + * @since 2019-11-26 + */ + public double getMultiplier() { + return this.multiplier; + } + + /** + * @return other names + * @since 2019-11-26 + */ + public final Set getOtherNames() { + return this.otherNames; + } + + /** + * @return primary name + * @since 2019-11-26 + */ + public final Optional getPrimaryName() { + return this.primaryName; + } + + /** + * @return symbol + * @since 2019-11-26 + */ + public final Optional getSymbol() { + return this.symbol; + } + + /** + * {@inheritDoc} + * + * Uses the prefix's multiplier to determine a hash code. + */ + @Override + public int hashCode() { + return DecimalComparison.hash(this.getMultiplier()); + } + + /** + * Multiplies this prefix by a scalar + * + * @param multiplicand + * number to multiply by + * @return product of prefix and scalar + * @since 2019-10-16 + */ + public UnitPrefix times(final double multiplicand) { + return valueOf(this.getMultiplier() * multiplicand); + } + + /** + * Multiplies this prefix by {@code other}. + * + * @param other + * prefix to multiply by + * @return product of prefixes + * @since 2019-04-13 + * @since v0.2.0 + */ + public UnitPrefix times(final UnitPrefix other) { + return valueOf(this.getMultiplier() * other.getMultiplier()); + } + + /** + * Raises this prefix to an exponent. + * + * @param exponent + * exponent to raise to + * @return result of exponentiation. + * @since 2019-04-13 + * @since v0.2.0 + */ + public UnitPrefix toExponent(final double exponent) { + return valueOf(Math.pow(this.getMultiplier(), exponent)); + } + + /** + * @return a string describing the prefix and its multiplier + */ + @Override + public String toString() { + if (this.primaryName.isPresent()) + return String.format("%s (\u00D7 %s)", this.primaryName.get(), this.multiplier); + else if (this.symbol.isPresent()) + return String.format("%s (\u00D7 %s)", this.symbol.get(), this.multiplier); + else + return String.format("Unit Prefix (\u00D7 %s)", this.multiplier); + } + + /** + * @param ns + * name(s) and symbol to use + * @return copy of this prefix with provided name(s) and symbol + * @since 2019-11-26 + * @throws NullPointerException + * if ns is null + */ + public UnitPrefix withName(final NameSymbol ns) { + return valueOf(this.multiplier, ns); + } +} diff --git a/src/main/java/org/unitConverter/unit/UnitValue.java b/src/main/java/org/unitConverter/unit/UnitValue.java new file mode 100644 index 0000000..c138332 --- /dev/null +++ b/src/main/java/org/unitConverter/unit/UnitValue.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 . + */ +package org.unitConverter.unit; + +import java.util.Objects; +import java.util.Optional; + +/** + * A value expressed in a unit. + * + * Unless otherwise indicated, all methods in this class throw a + * {@code NullPointerException} when an argument is null. + * + * @author Adrien Hopkins + * @since 2020-07-26 + */ +public final class UnitValue { + /** + * Creates a {@code UnitValue} from a unit and the associated value. + * + * @param unit unit to use + * @param value value to use + * @return {@code UnitValue} instance + */ + public static UnitValue of(Unit unit, double value) { + return new UnitValue( + Objects.requireNonNull(unit, "unit must not be null"), value); + } + + private final Unit unit; + private final double value; + + /** + * @param unit the unit being used + * @param value the value being represented + */ + private UnitValue(Unit unit, Double value) { + this.unit = unit; + this.value = value; + } + + /** + * @return true if this value can be converted to {@code other}. + * @since 2020-10-01 + */ + public final boolean canConvertTo(Unit other) { + return this.unit.canConvertTo(other); + } + + /** + * @return true if this value can be converted to {@code other}. + * @since 2020-10-01 + */ + public final boolean canConvertTo(Unitlike other) { + return this.unit.canConvertTo(other); + } + + /** + * Returns a UnitlikeValue that represents the same value expressed in a + * different unitlike form. + * + * @param other new unit to express value in + * @return value expressed in {@code other} + */ + public final , W> UnitlikeValue convertTo( + U other) { + return UnitlikeValue.of(other, + this.unit.convertTo(other, this.getValue())); + } + + /** + * Returns a UnitValue that represents the same value expressed in a + * different unit + * + * @param other new unit to express value in + * @return value expressed in {@code other} + */ + public final UnitValue convertTo(Unit other) { + return UnitValue.of(other, + this.getUnit().convertTo(other, this.getValue())); + } + + /** + * Returns this unit value represented as a {@code LinearUnitValue} with this + * unit's base unit as the base. + * + * @param ns name and symbol for the base unit, use NameSymbol.EMPTY if not + * needed. + * @since 2020-09-29 + */ + public final LinearUnitValue convertToBase(NameSymbol ns) { + final LinearUnit base = LinearUnit.getBase(this.unit).withName(ns); + return this.convertToLinear(base); + } + + /** + * @return a {@code LinearUnitValue} that is equivalent to this value. It + * will have zero uncertainty. + * @since 2020-09-29 + */ + public final LinearUnitValue convertToLinear(LinearUnit other) { + return LinearUnitValue.getExact(other, + this.getUnit().convertTo(other, this.getValue())); + } + + /** + * Returns true if this and obj represent the same value, regardless of + * whether or not they are expressed in the same unit. So (1000 m).equals(1 + * km) returns true. + */ + @Override + public boolean equals(Object obj) { + if (!(obj instanceof UnitValue)) + return false; + final UnitValue other = (UnitValue) obj; + return Objects.equals(this.getUnit().getBase(), other.getUnit().getBase()) + && Double.doubleToLongBits( + this.getUnit().convertToBase(this.getValue())) == Double + .doubleToLongBits( + other.getUnit().convertToBase(other.getValue())); + } + + /** + * @return the unit + * @since 2020-09-29 + */ + public final Unit getUnit() { + return this.unit; + } + + /** + * @return the value + * @since 2020-09-29 + */ + public final double getValue() { + return this.value; + } + + @Override + public int hashCode() { + return Objects.hash(this.getUnit().getBase(), + this.getUnit().convertFromBase(this.getValue())); + } + + @Override + public String toString() { + final Optional primaryName = this.getUnit().getPrimaryName(); + final Optional symbol = this.getUnit().getSymbol(); + if (primaryName.isEmpty() && symbol.isEmpty()) { + final double baseValue = this.getUnit().convertToBase(this.getValue()); + return String.format("%s unnamed unit (= %s %s)", this.getValue(), + baseValue, this.getUnit().getBase()); + } else { + final String unitName = symbol.orElse(primaryName.get()); + return this.getValue() + " " + unitName; + } + } +} diff --git a/src/main/java/org/unitConverter/unit/Unitlike.java b/src/main/java/org/unitConverter/unit/Unitlike.java new file mode 100644 index 0000000..8077771 --- /dev/null +++ b/src/main/java/org/unitConverter/unit/Unitlike.java @@ -0,0 +1,260 @@ +/** + * Copyright (C) 2020 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 . + */ +package org.unitConverter.unit; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.function.DoubleFunction; +import java.util.function.ToDoubleFunction; + +import org.unitConverter.math.ObjectProduct; + +/** + * An object that can convert a value between multiple forms (instances of the + * object); like a unit but the "converted value" can be any type. + * + * @since 2020-09-07 + */ +public abstract class Unitlike implements Nameable { + /** + * Returns a unitlike form from its base and the functions it uses to convert + * to and from its base. + * + * @param base unitlike form's base + * @param converterFrom function that accepts a value expressed in the + * unitlike form's base and returns that value expressed + * in this unitlike form. + * @param converterTo function that accepts a value expressed in the + * unitlike form and returns that value expressed in the + * unit's base. + * @return a unitlike form that uses the provided functions to convert. + * @since 2020-09-07 + * @throws NullPointerException if any argument is null + */ + public static final Unitlike fromConversionFunctions( + final ObjectProduct base, + final DoubleFunction converterFrom, + final ToDoubleFunction converterTo) { + return new FunctionalUnitlike<>(base, NameSymbol.EMPTY, converterFrom, + converterTo); + } + + /** + * Returns a unitlike form from its base and the functions it uses to convert + * to and from its base. + * + * @param base unitlike form's base + * @param converterFrom function that accepts a value expressed in the + * unitlike form's base and returns that value expressed + * in this unitlike form. + * @param converterTo function that accepts a value expressed in the + * unitlike form and returns that value expressed in the + * unit's base. + * @param ns names and symbol of unit + * @return a unitlike form that uses the provided functions to convert. + * @since 2020-09-07 + * @throws NullPointerException if any argument is null + */ + public static final Unitlike fromConversionFunctions( + final ObjectProduct base, + final DoubleFunction converterFrom, + final ToDoubleFunction converterTo, final NameSymbol ns) { + return new FunctionalUnitlike<>(base, ns, converterFrom, converterTo); + } + + /** + * The combination of units that this unit is based on. + * + * @since 2019-10-16 + */ + private final ObjectProduct unitBase; + + /** + * This unit's name(s) and symbol + * + * @since 2020-09-07 + */ + private final NameSymbol nameSymbol; + + /** + * Cache storing the result of getDimension() + * + * @since 2019-10-16 + */ + private transient ObjectProduct dimension = null; + + /** + * @param unitBase + * @since 2020-09-07 + */ + protected Unitlike(ObjectProduct unitBase, NameSymbol ns) { + this.unitBase = Objects.requireNonNull(unitBase, + "unitBase may not be null"); + this.nameSymbol = Objects.requireNonNull(ns, "ns may not be null"); + } + + /** + * Checks if a value expressed in this unitlike form can be converted to a + * value expressed in {@code other} + * + * @param other unit or unitlike form to test with + * @return true if they are compatible + * @since 2019-01-13 + * @since v0.1.0 + * @throws NullPointerException if other is null + */ + public final boolean canConvertTo(final Unit other) { + Objects.requireNonNull(other, "other must not be null."); + return Objects.equals(this.getBase(), other.getBase()); + } + + /** + * Checks if a value expressed in this unitlike form can be converted to a + * value expressed in {@code other} + * + * @param other unit or unitlike form to test with + * @return true if they are compatible + * @since 2019-01-13 + * @since v0.1.0 + * @throws NullPointerException if other is null + */ + public final boolean canConvertTo(final Unitlike other) { + Objects.requireNonNull(other, "other must not be null."); + return Objects.equals(this.getBase(), other.getBase()); + } + + protected abstract V convertFromBase(double value); + + /** + * Converts a value expressed in this unitlike form to a value expressed in + * {@code other}. + * + * @implSpec If conversion is possible, this implementation returns + * {@code other.convertFromBase(this.convertToBase(value))}. + * Therefore, overriding either of those methods will change the + * output of this method. + * + * @param other unit to convert to + * @param value value to convert + * @return converted value + * @since 2019-05-22 + * @throws IllegalArgumentException if {@code other} is incompatible for + * conversion with this unitlike form (as + * tested by {@link Unit#canConvertTo}). + * @throws NullPointerException if other is null + */ + public final double convertTo(final Unit other, final V value) { + Objects.requireNonNull(other, "other must not be null."); + if (this.canConvertTo(other)) + return other.convertFromBase(this.convertToBase(value)); + else + throw new IllegalArgumentException( + String.format("Cannot convert from %s to %s.", this, other)); + } + + /** + * Converts a value expressed in this unitlike form to a value expressed in + * {@code other}. + * + * @implSpec If conversion is possible, this implementation returns + * {@code other.convertFromBase(this.convertToBase(value))}. + * Therefore, overriding either of those methods will change the + * output of this method. + * + * @param other unitlike form to convert to + * @param value value to convert + * @param type of value to convert to + * @return converted value + * @since 2020-09-07 + * @throws IllegalArgumentException if {@code other} is incompatible for + * conversion with this unitlike form (as + * tested by {@link Unit#canConvertTo}). + * @throws NullPointerException if other is null + */ + public final W convertTo(final Unitlike other, final V value) { + Objects.requireNonNull(other, "other must not be null."); + if (this.canConvertTo(other)) + return other.convertFromBase(this.convertToBase(value)); + else + throw new IllegalArgumentException( + String.format("Cannot convert from %s to %s.", this, other)); + } + + protected abstract double convertToBase(V value); + + /** + * @return combination of units that this unit is based on + * @since 2018-12-22 + * @since v0.1.0 + */ + public final ObjectProduct getBase() { + return this.unitBase; + } + + /** + * @return dimension measured by this unit + * @since 2018-12-22 + * @since v0.1.0 + */ + public final ObjectProduct getDimension() { + if (this.dimension == null) { + final Map mapping = this.unitBase.exponentMap(); + final Map dimensionMap = new HashMap<>(); + + for (final BaseUnit key : mapping.keySet()) { + dimensionMap.put(key.getBaseDimension(), mapping.get(key)); + } + + this.dimension = ObjectProduct.fromExponentMapping(dimensionMap); + } + return this.dimension; + } + + /** + * @return the nameSymbol + * @since 2020-09-07 + */ + @Override + public final NameSymbol getNameSymbol() { + return this.nameSymbol; + } + + @Override + public String toString() { + return this.getPrimaryName().orElse("Unnamed unitlike form") + + (this.getSymbol().isPresent() + ? String.format(" (%s)", this.getSymbol().get()) + : "") + + ", derived from " + + this.getBase().toString(u -> u.getSymbol().get()) + + (this.getOtherNames().isEmpty() ? "" + : ", also called " + String.join(", ", this.getOtherNames())); + } + + /** + * @param ns name(s) and symbol to use + * @return a copy of this unitlike form with provided name(s) and symbol + * @since 2020-09-07 + * @throws NullPointerException if ns is null + */ + public Unitlike withName(final NameSymbol ns) { + return fromConversionFunctions(this.getBase(), this::convertFromBase, + this::convertToBase, + Objects.requireNonNull(ns, "ns must not be null.")); + } +} diff --git a/src/main/java/org/unitConverter/unit/UnitlikeValue.java b/src/main/java/org/unitConverter/unit/UnitlikeValue.java new file mode 100644 index 0000000..79201c4 --- /dev/null +++ b/src/main/java/org/unitConverter/unit/UnitlikeValue.java @@ -0,0 +1,174 @@ +/** + * Copyright (C) 2020 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 . + */ +package org.unitConverter.unit; + +import java.util.Optional; + +/** + * + * @since 2020-09-07 + */ +final class UnitlikeValue, V> { + /** + * Gets a {@code UnitlikeValue}. + * + * @since 2020-10-02 + */ + public static , V> UnitlikeValue of(T unitlike, + V value) { + return new UnitlikeValue<>(unitlike, value); + } + + private final T unitlike; + private final V value; + + /** + * @param unitlike + * @param value + * @since 2020-09-07 + */ + private UnitlikeValue(T unitlike, V value) { + this.unitlike = unitlike; + this.value = value; + } + + /** + * @return true if this value can be converted to {@code other}. + * @since 2020-10-01 + */ + public final boolean canConvertTo(Unit other) { + return this.unitlike.canConvertTo(other); + } + + /** + * @return true if this value can be converted to {@code other}. + * @since 2020-10-01 + */ + public final boolean canConvertTo(Unitlike other) { + return this.unitlike.canConvertTo(other); + } + + /** + * Returns a UnitlikeValue that represents the same value expressed in a + * different unitlike form. + * + * @param other new unit to express value in + * @return value expressed in {@code other} + */ + public final , W> UnitlikeValue convertTo( + U other) { + return UnitlikeValue.of(other, + this.unitlike.convertTo(other, this.getValue())); + } + + /** + * Returns a UnitValue that represents the same value expressed in a + * different unit + * + * @param other new unit to express value in + * @return value expressed in {@code other} + */ + public final UnitValue convertTo(Unit other) { + return UnitValue.of(other, + this.unitlike.convertTo(other, this.getValue())); + } + + /** + * Returns this unit value represented as a {@code LinearUnitValue} with this + * unit's base unit as the base. + * + * @param ns name and symbol for the base unit, use NameSymbol.EMPTY if not + * needed. + * @since 2020-09-29 + */ + public final LinearUnitValue convertToBase(NameSymbol ns) { + final LinearUnit base = LinearUnit.getBase(this.unitlike).withName(ns); + return this.convertToLinear(base); + } + + /** + * @return a {@code LinearUnitValue} that is equivalent to this value. It + * will have zero uncertainty. + * @since 2020-09-29 + */ + public final LinearUnitValue convertToLinear(LinearUnit other) { + return LinearUnitValue.getExact(other, + this.getUnitlike().convertTo(other, this.getValue())); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (!(obj instanceof UnitlikeValue)) + return false; + final UnitlikeValue other = (UnitlikeValue) obj; + if (this.getUnitlike() == null) { + if (other.getUnitlike() != null) + return false; + } else if (!this.getUnitlike().equals(other.getUnitlike())) + return false; + if (this.getValue() == null) { + if (other.getValue() != null) + return false; + } else if (!this.getValue().equals(other.getValue())) + return false; + return true; + } + + /** + * @return the unitlike + * @since 2020-09-29 + */ + public final Unitlike getUnitlike() { + return this.unitlike; + } + + /** + * @return the value + * @since 2020-09-29 + */ + public final V getValue() { + return this.value; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + + (this.getUnitlike() == null ? 0 : this.getUnitlike().hashCode()); + result = prime * result + + (this.getValue() == null ? 0 : this.getValue().hashCode()); + return result; + } + + @Override + public String toString() { + final Optional primaryName = this.getUnitlike().getPrimaryName(); + final Optional symbol = this.getUnitlike().getSymbol(); + if (primaryName.isEmpty() && symbol.isEmpty()) { + final double baseValue = this.getUnitlike() + .convertToBase(this.getValue()); + return String.format("%s unnamed unit (= %s %s)", this.getValue(), + baseValue, this.getUnitlike().getBase()); + } else { + final String unitName = symbol.orElse(primaryName.get()); + return this.getValue() + " " + unitName; + } + } +} diff --git a/src/main/java/org/unitConverter/unit/package-info.java b/src/main/java/org/unitConverter/unit/package-info.java new file mode 100644 index 0000000..2f0e097 --- /dev/null +++ b/src/main/java/org/unitConverter/unit/package-info.java @@ -0,0 +1,24 @@ +/** + * Copyright (C) 2019 Adrien Hopkins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +/** + * Everything to do with the units that make up Unit Converter. + * + * @author Adrien Hopkins + * @since 2019-10-16 + * @since v0.1.0 + */ +package org.unitConverter.unit; \ No newline at end of file diff --git a/src/main/resources/about.txt b/src/main/resources/about.txt new file mode 100644 index 0000000..1bad9e8 --- /dev/null +++ b/src/main/resources/about.txt @@ -0,0 +1,12 @@ +About Unit Converter v0.2.0 + +Copyright Notice: + +Unit Converter Copyright (C) 2018-2021 Adrien Hopkins +This program comes with ABSOLUTELY NO WARRANTY; +for details read the LICENSE file, section 15 + +This is free software, and you are welcome to redistribute +it under certain conditions; for details go to + +or read the LICENSE file. \ No newline at end of file -- cgit v1.2.3 From 32f390a86ac9b868b7eb9c1d47875c6a990b6d11 Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Sat, 27 Mar 2021 16:38:21 -0500 Subject: Finalized the upgrade to Java 11 --- .../math/ConditionalExistenceCollections.java | 2 +- .../org/unitConverter/math/ExpressionParser.java | 537 +++++++++++---------- 2 files changed, 283 insertions(+), 256 deletions(-) (limited to 'src/main') diff --git a/src/main/java/org/unitConverter/math/ConditionalExistenceCollections.java b/src/main/java/org/unitConverter/math/ConditionalExistenceCollections.java index ac1c0cf..000658b 100644 --- a/src/main/java/org/unitConverter/math/ConditionalExistenceCollections.java +++ b/src/main/java/org/unitConverter/math/ConditionalExistenceCollections.java @@ -263,7 +263,7 @@ public final class ConditionalExistenceCollections { } private final Entry getEntry(K key) { - return new Entry() { + return new Entry<>() { @Override public K getKey() { return key; diff --git a/src/main/java/org/unitConverter/math/ExpressionParser.java b/src/main/java/org/unitConverter/math/ExpressionParser.java index 8a0e97d..deee51d 100644 --- a/src/main/java/org/unitConverter/math/ExpressionParser.java +++ b/src/main/java/org/unitConverter/math/ExpressionParser.java @@ -32,8 +32,7 @@ import java.util.function.UnaryOperator; * An object that can parse expressions with unary or binary operators. * * @author Adrien Hopkins - * @param - * type of object that exists in parsed expressions + * @param type of object that exists in parsed expressions * @since 2019-03-14 * @since v0.2.0 */ @@ -42,21 +41,21 @@ public final class ExpressionParser { * A builder that can create {@code ExpressionParser} instances. * * @author Adrien Hopkins - * @param - * type of object that exists in parsed expressions + * @param type of object that exists in parsed expressions * @since 2019-03-17 * @since v0.2.0 */ public static final class Builder { /** - * A function that obtains a parseable object from a string. For example, an integer {@code ExpressionParser} - * would use {@code Integer::parseInt}. + * A function that obtains a parseable object from a string. For example, + * an integer {@code ExpressionParser} would use + * {@code Integer::parseInt}. * * @since 2019-03-14 * @since v0.2.0 */ private final Function objectObtainer; - + /** * The function of the space as an operator (like 3 x y) * @@ -64,110 +63,115 @@ public final class ExpressionParser { * @since v0.2.0 */ private String spaceFunction = null; - + /** - * A map mapping operator strings to operator functions, for unary operators. + * A map mapping operator strings to operator functions, for unary + * operators. * * @since 2019-03-14 * @since v0.2.0 */ private final Map> unaryOperators; - + /** - * A map mapping operator strings to operator functions, for binary operators. + * A map mapping operator strings to operator functions, for binary + * operators. * * @since 2019-03-14 * @since v0.2.0 */ private final Map> binaryOperators; - + /** * Creates the {@code Builder}. * - * @param objectObtainer - * a function that can turn strings into objects of the type handled by the parser. - * @throws NullPointerException - * if {@code objectObtainer} is null + * @param objectObtainer a function that can turn strings into objects of + * the type handled by the parser. + * @throws NullPointerException if {@code objectObtainer} is null * @since 2019-03-17 * @since v0.2.0 */ public Builder(final Function objectObtainer) { - this.objectObtainer = Objects.requireNonNull(objectObtainer, "objectObtainer must not be null."); + this.objectObtainer = Objects.requireNonNull(objectObtainer, + "objectObtainer must not be null."); this.unaryOperators = new HashMap<>(); this.binaryOperators = new HashMap<>(); } - + /** * Adds a binary operator to the builder. * - * @param text - * text used to reference the operator, like '+' - * @param operator - * operator to add - * @param priority - * operator's priority, which determines which operators are applied first + * @param text text used to reference the operator, like '+' + * @param operator operator to add + * @param priority operator's priority, which determines which operators + * are applied first * @return this builder - * @throws NullPointerException - * if {@code text} or {@code operator} is null + * @throws NullPointerException if {@code text} or {@code operator} is + * null * @since 2019-03-17 * @since v0.2.0 */ - public Builder addBinaryOperator(final String text, final BinaryOperator operator, final int priority) { + public Builder addBinaryOperator(final String text, + final BinaryOperator operator, final int priority) { Objects.requireNonNull(text, "text must not be null."); Objects.requireNonNull(operator, "operator must not be null."); - - // Unfortunately, I cannot use a lambda because the PriorityBinaryOperator requires arguments. - final PriorityBinaryOperator priorityOperator = new PriorityBinaryOperator(priority) { + + // Unfortunately, I cannot use a lambda because the + // PriorityBinaryOperator requires arguments. + final PriorityBinaryOperator priorityOperator = new PriorityBinaryOperator<>( + priority) { @Override public T apply(final T t, final T u) { return operator.apply(t, u); } - + }; this.binaryOperators.put(text, priorityOperator); return this; } - + /** - * Adds a function for spaces. You must use the text of an existing binary operator. + * Adds a function for spaces. You must use the text of an existing binary + * operator. * - * @param operator - * text of operator to use + * @param operator text of operator to use * @return this builder * @since 2019-03-22 * @since v0.2.0 */ public Builder addSpaceFunction(final String operator) { Objects.requireNonNull(operator, "operator must not be null."); - + if (!this.binaryOperators.containsKey(operator)) - throw new IllegalArgumentException(String.format("Could not find binary operator '%s'", operator)); - + throw new IllegalArgumentException(String + .format("Could not find binary operator '%s'", operator)); + this.spaceFunction = operator; return this; } - + /** * Adds a unary operator to the builder. * - * @param text - * text used to reference the operator, like '-' - * @param operator - * operator to add - * @param priority - * operator's priority, which determines which operators are applied first + * @param text text used to reference the operator, like '-' + * @param operator operator to add + * @param priority operator's priority, which determines which operators + * are applied first * @return this builder - * @throws NullPointerException - * if {@code text} or {@code operator} is null + * @throws NullPointerException if {@code text} or {@code operator} is + * null * @since 2019-03-17 * @since v0.2.0 */ - public Builder addUnaryOperator(final String text, final UnaryOperator operator, final int priority) { + public Builder addUnaryOperator(final String text, + final UnaryOperator operator, final int priority) { Objects.requireNonNull(text, "text must not be null."); Objects.requireNonNull(operator, "operator must not be null."); - - // Unfortunately, I cannot use a lambda because the PriorityUnaryOperator requires arguments. - final PriorityUnaryOperator priorityOperator = new PriorityUnaryOperator(priority) { + + // Unfortunately, I cannot use a lambda because the + // PriorityUnaryOperator requires arguments. + final PriorityUnaryOperator priorityOperator = new PriorityUnaryOperator<>( + priority) { @Override public T apply(final T t) { return operator.apply(t); @@ -176,49 +180,50 @@ public final class ExpressionParser { this.unaryOperators.put(text, priorityOperator); return this; } - + /** - * @return an {@code ExpressionParser} instance with the properties given to this builder + * @return an {@code ExpressionParser} instance with the properties + * given to this builder * @since 2019-03-17 * @since v0.2.0 */ public ExpressionParser build() { - return new ExpressionParser<>(this.objectObtainer, this.unaryOperators, this.binaryOperators, - this.spaceFunction); + return new ExpressionParser<>(this.objectObtainer, this.unaryOperators, + this.binaryOperators, this.spaceFunction); } } - + /** - * A binary operator with a priority field that determines which operators apply first. + * A binary operator with a priority field that determines which operators + * apply first. * * @author Adrien Hopkins - * @param - * type of operand and result + * @param type of operand and result * @since 2019-03-17 * @since v0.2.0 */ private static abstract class PriorityBinaryOperator implements BinaryOperator, Comparable> { /** - * The operator's priority. Higher-priority operators are applied before lower-priority operators + * The operator's priority. Higher-priority operators are applied before + * lower-priority operators * * @since 2019-03-17 * @since v0.2.0 */ private final int priority; - + /** * Creates the {@code PriorityBinaryOperator}. * - * @param priority - * operator's priority + * @param priority operator's priority * @since 2019-03-17 * @since v0.2.0 */ public PriorityBinaryOperator(final int priority) { this.priority = priority; } - + /** * Compares this object to another by priority. * @@ -238,7 +243,7 @@ public final class ExpressionParser { else return 0; } - + /** * @return priority * @since 2019-03-22 @@ -248,38 +253,38 @@ public final class ExpressionParser { return this.priority; } } - + /** - * A unary operator with a priority field that determines which operators apply first. + * A unary operator with a priority field that determines which operators + * apply first. * * @author Adrien Hopkins - * @param - * type of operand and result + * @param type of operand and result * @since 2019-03-17 * @since v0.2.0 */ private static abstract class PriorityUnaryOperator implements UnaryOperator, Comparable> { /** - * The operator's priority. Higher-priority operators are applied before lower-priority operators + * The operator's priority. Higher-priority operators are applied before + * lower-priority operators * * @since 2019-03-17 * @since v0.2.0 */ private final int priority; - + /** * Creates the {@code PriorityUnaryOperator}. * - * @param priority - * operator's priority + * @param priority operator's priority * @since 2019-03-17 * @since v0.2.0 */ public PriorityUnaryOperator(final int priority) { this.priority = priority; } - + /** * Compares this object to another by priority. * @@ -299,7 +304,7 @@ public final class ExpressionParser { else return 0; } - + /** * @return priority * @since 2019-03-22 @@ -309,7 +314,7 @@ public final class ExpressionParser { return this.priority; } } - + /** * The types of tokens that are available. * @@ -320,7 +325,7 @@ public final class ExpressionParser { private static enum TokenType { OBJECT, UNARY_OPERATOR, BINARY_OPERATOR; } - + /** * The opening bracket. * @@ -328,7 +333,7 @@ public final class ExpressionParser { * @since v0.2.0 */ public static final char OPENING_BRACKET = '('; - + /** * The closing bracket. * @@ -336,48 +341,49 @@ public final class ExpressionParser { * @since v0.2.0 */ public static final char CLOSING_BRACKET = ')'; - + /** * Finds the other bracket in a pair of brackets, given the position of one. * - * @param string - * string that contains brackets - * @param bracketPosition - * position of first bracket + * @param string string that contains brackets + * @param bracketPosition position of first bracket * @return position of matching bracket - * @throws NullPointerException - * if string is null + * @throws NullPointerException if string is null * @since 2019-03-22 * @since v0.2.0 */ - private static int findBracketPair(final String string, final int bracketPosition) { + private static int findBracketPair(final String string, + final int bracketPosition) { Objects.requireNonNull(string, "string must not be null."); - + final char openingBracket = string.charAt(bracketPosition); - + // figure out what closing bracket to look for final char closingBracket; switch (openingBracket) { - case '(': - closingBracket = ')'; - break; - case '[': - closingBracket = ']'; - break; - case '{': - closingBracket = '}'; - break; - default: - throw new IllegalArgumentException(String.format("Invalid bracket '%s'", openingBracket)); + case '(': + closingBracket = ')'; + break; + case '[': + closingBracket = ']'; + break; + case '{': + closingBracket = '}'; + break; + default: + throw new IllegalArgumentException( + String.format("Invalid bracket '%s'", openingBracket)); } - - // level of brackets. every opening bracket increments this; every closing bracket decrements it + + // level of brackets. every opening bracket increments this; every closing + // bracket decrements it int bracketLevel = 0; - + // iterate over the string to find the closing bracket - for (int currentPosition = bracketPosition; currentPosition < string.length(); currentPosition++) { + for (int currentPosition = bracketPosition; currentPosition < string + .length(); currentPosition++) { final char currentCharacter = string.charAt(currentPosition); - + if (currentCharacter == openingBracket) { bracketLevel++; } else if (currentCharacter == closingBracket) { @@ -386,19 +392,19 @@ public final class ExpressionParser { return currentPosition; } } - + throw new IllegalArgumentException("No matching bracket found."); } - + /** - * A function that obtains a parseable object from a string. For example, an integer {@code ExpressionParser} would - * use {@code Integer::parseInt}. + * A function that obtains a parseable object from a string. For example, an + * integer {@code ExpressionParser} would use {@code Integer::parseInt}. * * @since 2019-03-14 * @since v0.2.0 */ private final Function objectObtainer; - + /** * A map mapping operator strings to operator functions, for unary operators. * @@ -406,15 +412,16 @@ public final class ExpressionParser { * @since v0.2.0 */ private final Map> unaryOperators; - + /** - * A map mapping operator strings to operator functions, for binary operators. + * A map mapping operator strings to operator functions, for binary + * operators. * * @since 2019-03-14 * @since v0.2.0 */ private final Map> binaryOperators; - + /** * The operator for space, or null if spaces have no function. * @@ -422,127 +429,144 @@ public final class ExpressionParser { * @since v0.2.0 */ private final String spaceOperator; - + /** * Creates the {@code ExpressionParser}. * - * @param objectObtainer - * function to get objects from strings - * @param unaryOperators - * unary operators available to the parser - * @param binaryOperators - * binary operators available to the parser - * @param spaceOperator - * operator used by spaces + * @param objectObtainer function to get objects from strings + * @param unaryOperators unary operators available to the parser + * @param binaryOperators binary operators available to the parser + * @param spaceOperator operator used by spaces * @since 2019-03-14 * @since v0.2.0 */ private ExpressionParser(final Function objectObtainer, final Map> unaryOperators, - final Map> binaryOperators, final String spaceOperator) { + final Map> binaryOperators, + final String spaceOperator) { this.objectObtainer = objectObtainer; this.unaryOperators = unaryOperators; this.binaryOperators = binaryOperators; this.spaceOperator = spaceOperator; } - + /** - * Converts a given mathematical expression to reverse Polish notation (operators after operands). + * Converts a given mathematical expression to reverse Polish notation + * (operators after operands). *

* For example,
* {@code 2 * (3 + 4)}
* becomes
* {@code 2 3 4 + *}. * - * @param expression - * expression + * @param expression expression * @return expression in RPN * @since 2019-03-17 * @since v0.2.0 */ private String convertExpressionToReversePolish(final String expression) { Objects.requireNonNull(expression, "expression must not be null."); - + final List components = new ArrayList<>(); - + // the part of the expression remaining to parse String partialExpression = expression; - + // find and deal with brackets while (partialExpression.indexOf(OPENING_BRACKET) != -1) { - final int openingBracketPosition = partialExpression.indexOf(OPENING_BRACKET); - final int closingBracketPosition = findBracketPair(partialExpression, openingBracketPosition); - + final int openingBracketPosition = partialExpression + .indexOf(OPENING_BRACKET); + final int closingBracketPosition = findBracketPair(partialExpression, + openingBracketPosition); + // check for function - if (openingBracketPosition > 0 && partialExpression.charAt(openingBracketPosition - 1) != ' ') { + if (openingBracketPosition > 0 + && partialExpression.charAt(openingBracketPosition - 1) != ' ') { // function like sin(2) or tempF(32) // find the position of the last space int spacePosition = openingBracketPosition; - while (spacePosition >= 0 && partialExpression.charAt(spacePosition) != ' ') { + while (spacePosition >= 0 + && partialExpression.charAt(spacePosition) != ' ') { spacePosition--; } - // then split the function into pre-function and function, using the space position - components.addAll(Arrays.asList(partialExpression.substring(0, spacePosition + 1).split(" "))); - components.add(partialExpression.substring(spacePosition + 1, closingBracketPosition + 1)); - partialExpression = partialExpression.substring(closingBracketPosition + 1); + // then split the function into pre-function and function, using the + // space position + components.addAll(Arrays.asList(partialExpression + .substring(0, spacePosition + 1).split(" "))); + components.add(partialExpression.substring(spacePosition + 1, + closingBracketPosition + 1)); + partialExpression = partialExpression + .substring(closingBracketPosition + 1); } else { // normal brackets like (1 + 2) * (3 / 5) - components.addAll(Arrays.asList(partialExpression.substring(0, openingBracketPosition).split(" "))); + components.addAll(Arrays.asList(partialExpression + .substring(0, openingBracketPosition).split(" "))); components.add(this.convertExpressionToReversePolish( - partialExpression.substring(openingBracketPosition + 1, closingBracketPosition))); - partialExpression = partialExpression.substring(closingBracketPosition + 1); + partialExpression.substring(openingBracketPosition + 1, + closingBracketPosition))); + partialExpression = partialExpression + .substring(closingBracketPosition + 1); } } - + // add everything else components.addAll(Arrays.asList(partialExpression.split(" "))); - + // remove empty entries while (components.contains("")) { components.remove(""); } - + // deal with space multiplication (x y) if (this.spaceOperator != null) { for (int i = 0; i < components.size() - 1; i++) { - if (this.getTokenType(components.get(i)) == TokenType.OBJECT - && this.getTokenType(components.get(i + 1)) == TokenType.OBJECT) { + if (this.getTokenType(components.get(i)) == TokenType.OBJECT && this + .getTokenType(components.get(i + 1)) == TokenType.OBJECT) { components.add(++i, this.spaceOperator); } } } - + // turn the expression into reverse Polish while (true) { - final int highestPriorityOperatorPosition = this.findHighestPriorityOperatorPosition(components); + final int highestPriorityOperatorPosition = this + .findHighestPriorityOperatorPosition(components); if (highestPriorityOperatorPosition == -1) { break; } - + // swap components based on what kind of operator there is // 1 + 2 becomes 2 1 + // - 1 becomes 1 - - switch (this.getTokenType(components.get(highestPriorityOperatorPosition))) { - case UNARY_OPERATOR: - final String unaryOperator = components.remove(highestPriorityOperatorPosition); - final String operand = components.remove(highestPriorityOperatorPosition); - components.add(highestPriorityOperatorPosition, operand + " " + unaryOperator); - break; - case BINARY_OPERATOR: - final String binaryOperator = components.remove(highestPriorityOperatorPosition); - final String operand1 = components.remove(highestPriorityOperatorPosition - 1); - final String operand2 = components.remove(highestPriorityOperatorPosition - 1); - components.add(highestPriorityOperatorPosition - 1, - operand2 + " " + operand1 + " " + binaryOperator); - break; - default: - throw new AssertionError("Expected operator, found non-operator."); + switch (this + .getTokenType(components.get(highestPriorityOperatorPosition))) { + case UNARY_OPERATOR: + final String unaryOperator = components + .remove(highestPriorityOperatorPosition); + final String operand = components + .remove(highestPriorityOperatorPosition); + components.add(highestPriorityOperatorPosition, + operand + " " + unaryOperator); + break; + case BINARY_OPERATOR: + final String binaryOperator = components + .remove(highestPriorityOperatorPosition); + final String operand1 = components + .remove(highestPriorityOperatorPosition - 1); + final String operand2 = components + .remove(highestPriorityOperatorPosition - 1); + components.add(highestPriorityOperatorPosition - 1, + operand2 + " " + operand1 + " " + binaryOperator); + break; + default: + throw new AssertionError("Expected operator, found non-operator."); } } - - // join all of the components together, then ensure there is only one space in a row + + // join all of the components together, then ensure there is only one + // space in a row String expressionRPN = String.join(" ", components).replaceAll(" +", " "); - + while (expressionRPN.charAt(0) == ' ') { expressionRPN = expressionRPN.substring(1); } @@ -551,73 +575,72 @@ public final class ExpressionParser { } return expressionRPN; } - + /** * Finds the position of the highest-priority operator in a list * - * @param components - * components to test - * @param blacklist - * positions of operators that should be ignored - * @return position of highest priority, or -1 if the list contains no operators - * @throws NullPointerException - * if components is null + * @param components components to test + * @param blacklist positions of operators that should be ignored + * @return position of highest priority, or -1 if the list contains no + * operators + * @throws NullPointerException if components is null * @since 2019-03-22 * @since v0.2.0 */ - private int findHighestPriorityOperatorPosition(final List components) { + private int findHighestPriorityOperatorPosition( + final List components) { Objects.requireNonNull(components, "components must not be null."); // find highest priority int maxPriority = Integer.MIN_VALUE; int maxPriorityPosition = -1; - + // go over components one by one // if it is an operator, test its priority to see if it's max // if it is, update maxPriority and maxPriorityPosition for (int i = 0; i < components.size(); i++) { - + switch (this.getTokenType(components.get(i))) { - case UNARY_OPERATOR: - final PriorityUnaryOperator unaryOperator = this.unaryOperators.get(components.get(i)); - final int unaryPriority = unaryOperator.getPriority(); - - if (unaryPriority > maxPriority) { - maxPriority = unaryPriority; - maxPriorityPosition = i; - } - break; - case BINARY_OPERATOR: - final PriorityBinaryOperator binaryOperator = this.binaryOperators.get(components.get(i)); - final int binaryPriority = binaryOperator.getPriority(); - - if (binaryPriority > maxPriority) { - maxPriority = binaryPriority; - maxPriorityPosition = i; - } - break; - default: - break; + case UNARY_OPERATOR: + final PriorityUnaryOperator unaryOperator = this.unaryOperators + .get(components.get(i)); + final int unaryPriority = unaryOperator.getPriority(); + + if (unaryPriority > maxPriority) { + maxPriority = unaryPriority; + maxPriorityPosition = i; + } + break; + case BINARY_OPERATOR: + final PriorityBinaryOperator binaryOperator = this.binaryOperators + .get(components.get(i)); + final int binaryPriority = binaryOperator.getPriority(); + + if (binaryPriority > maxPriority) { + maxPriority = binaryPriority; + maxPriorityPosition = i; + } + break; + default: + break; } } - + // max priority position found return maxPriorityPosition; } - + /** * Determines whether an inputted string is an object or an operator * - * @param token - * string to input + * @param token string to input * @return type of token it is - * @throws NullPointerException - * if {@code expression} is null + * @throws NullPointerException if {@code expression} is null * @since 2019-03-14 * @since v0.2.0 */ private TokenType getTokenType(final String token) { Objects.requireNonNull(token, "token must not be null."); - + if (this.unaryOperators.containsKey(token)) return TokenType.UNARY_OPERATOR; else if (this.binaryOperators.containsKey(token)) @@ -625,84 +648,88 @@ public final class ExpressionParser { else return TokenType.OBJECT; } - + /** * Parses an expression. * - * @param expression - * expression to parse + * @param expression expression to parse * @return result - * @throws NullPointerException - * if {@code expression} is null + * @throws NullPointerException if {@code expression} is null * @since 2019-03-14 * @since v0.2.0 */ public T parseExpression(final String expression) { - return this.parseReversePolishExpression(this.convertExpressionToReversePolish(expression)); + return this.parseReversePolishExpression( + this.convertExpressionToReversePolish(expression)); } - + /** * Parses an expression expressed in reverse Polish notation. * - * @param expression - * expression to parse + * @param expression expression to parse * @return result - * @throws NullPointerException - * if {@code expression} is null + * @throws NullPointerException if {@code expression} is null * @since 2019-03-14 * @since v0.2.0 */ private T parseReversePolishExpression(final String expression) { Objects.requireNonNull(expression, "expression must not be null."); - + final Deque stack = new ArrayDeque<>(); - + // iterate over every item in the expression, then for (final String item : expression.split(" ")) { // choose a path based on what kind of thing was just read switch (this.getTokenType(item)) { - - case BINARY_OPERATOR: - if (stack.size() < 2) - throw new IllegalStateException(String.format( - "Attempted to call binary operator %s with only %d arguments.", item, stack.size())); - - // get two arguments and operator, then apply! - final T o1 = stack.pop(); - final T o2 = stack.pop(); - final BinaryOperator binaryOperator = this.binaryOperators.get(item); - - stack.push(binaryOperator.apply(o1, o2)); - break; - - case OBJECT: - // just add it to the stack - stack.push(this.objectObtainer.apply(item)); - break; - - case UNARY_OPERATOR: - if (stack.size() < 1) - throw new IllegalStateException(String.format( - "Attempted to call unary operator %s with only %d arguments.", item, stack.size())); - - // get one argument and operator, then apply! - final T o = stack.pop(); - final UnaryOperator unaryOperator = this.unaryOperators.get(item); - - stack.push(unaryOperator.apply(o)); - break; - default: - throw new AssertionError( - String.format("Internal error: Invalid token type %s.", this.getTokenType(item))); - + + case BINARY_OPERATOR: + if (stack.size() < 2) + throw new IllegalStateException(String.format( + "Attempted to call binary operator %s with only %d arguments.", + item, stack.size())); + + // get two arguments and operator, then apply! + final T o1 = stack.pop(); + final T o2 = stack.pop(); + final BinaryOperator binaryOperator = this.binaryOperators + .get(item); + + stack.push(binaryOperator.apply(o1, o2)); + break; + + case OBJECT: + // just add it to the stack + stack.push(this.objectObtainer.apply(item)); + break; + + case UNARY_OPERATOR: + if (stack.size() < 1) + throw new IllegalStateException(String.format( + "Attempted to call unary operator %s with only %d arguments.", + item, stack.size())); + + // get one argument and operator, then apply! + final T o = stack.pop(); + final UnaryOperator unaryOperator = this.unaryOperators + .get(item); + + stack.push(unaryOperator.apply(o)); + break; + default: + throw new AssertionError( + String.format("Internal error: Invalid token type %s.", + this.getTokenType(item))); + } } - + // return answer, or throw an exception if I can't if (stack.size() > 1) - throw new IllegalStateException("Computation ended up with more than one answer."); + throw new IllegalStateException( + "Computation ended up with more than one answer."); else if (stack.size() == 0) - throw new IllegalStateException("Computation ended up without an answer."); + throw new IllegalStateException( + "Computation ended up without an answer."); return stack.pop(); } } -- cgit v1.2.3 From 3e6fee9561a00e5d9958c5685336c9d68c8629e1 Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Sat, 27 Mar 2021 18:00:14 -0500 Subject: Used resources instead of Paths to make the generated jar work --- bin/main/.gitignore | 3 + build.gradle | 16 +- dimensionfile.txt | 18 -- metric_exceptions.txt | 19 -- .../converterGUI/UnitConverterGUI.java | 97 ++++++-- .../java/org/unitConverter/unit/UnitDatabase.java | 34 +++ src/main/resources/dimensionfile.txt | 18 ++ src/main/resources/metric_exceptions.txt | 19 ++ src/main/resources/unitsfile.txt | 267 +++++++++++++++++++++ unitsfile.txt | 267 --------------------- 10 files changed, 426 insertions(+), 332 deletions(-) delete mode 100644 dimensionfile.txt delete mode 100644 metric_exceptions.txt create mode 100644 src/main/resources/dimensionfile.txt create mode 100644 src/main/resources/metric_exceptions.txt create mode 100644 src/main/resources/unitsfile.txt delete mode 100644 unitsfile.txt (limited to 'src/main') diff --git a/bin/main/.gitignore b/bin/main/.gitignore index b2d0b77..e5fcaae 100644 --- a/bin/main/.gitignore +++ b/bin/main/.gitignore @@ -1,2 +1,5 @@ /about.txt /org/ +/dimensionfile.txt +/metric_exceptions.txt +/unitsfile.txt diff --git a/build.gradle b/build.gradle index 11bf07b..1a2ef44 100644 --- a/build.gradle +++ b/build.gradle @@ -20,12 +20,18 @@ dependencies { testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.7.0' } +jar { + manifest { + attributes 'Main-Class': "org.unitConverter.converterGUI.UnitConverterGUI" + } +} + test { - useJUnitPlatform() - testLogging { - events 'passed', 'skipped', 'failed' - } - finalizedBy jacocoTestReport + useJUnitPlatform() + testLogging { + events 'passed', 'skipped', 'failed' + } + finalizedBy jacocoTestReport } jacoco { diff --git a/dimensionfile.txt b/dimensionfile.txt deleted file mode 100644 index 3485de5..0000000 --- a/dimensionfile.txt +++ /dev/null @@ -1,18 +0,0 @@ -# A file for the unit dimensions in my unit converter program - -# SI Base Dimensions -# ! means "look for an existing dimension which I will load at the start" -# This is necessary because every dimension must be defined by others, and I need somewhere to start. - -# I have excluded electric current, quantity and luminous intensity since their units are exclusively SI. - -LENGTH ! -MASS ! -TIME ! -TEMPERATURE ! - -# Derived Dimensions -AREA LENGTH^2 -VOLUME LENGTH^3 -VELOCITY LENGTH / TIME -ENERGY MASS * VELOCITY^2 \ No newline at end of file diff --git a/metric_exceptions.txt b/metric_exceptions.txt deleted file mode 100644 index 73748c0..0000000 --- a/metric_exceptions.txt +++ /dev/null @@ -1,19 +0,0 @@ -# This is a list of exceptions for the one-way conversion mode -# Units in this list will be included in both From: and To: -# regardless of whether or not one-way conversion is enabled. - -tempC -tempCelsius -s -second -min -minute -h -hour -d -day -wk -week -gregorianmonth -gregorianyear -km/h \ No newline at end of file diff --git a/src/main/java/org/unitConverter/converterGUI/UnitConverterGUI.java b/src/main/java/org/unitConverter/converterGUI/UnitConverterGUI.java index c046846..ee1bcc3 100644 --- a/src/main/java/org/unitConverter/converterGUI/UnitConverterGUI.java +++ b/src/main/java/org/unitConverter/converterGUI/UnitConverterGUI.java @@ -23,6 +23,7 @@ import java.awt.GridLayout; import java.awt.event.KeyEvent; import java.io.BufferedWriter; import java.io.IOException; +import java.io.InputStream; import java.math.BigDecimal; import java.math.MathContext; import java.math.RoundingMode; @@ -36,6 +37,7 @@ import java.util.Comparator; import java.util.HashSet; import java.util.List; import java.util.NoSuchElementException; +import java.util.Scanner; import java.util.Set; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -93,13 +95,11 @@ final class UnitConverterGUI { /** The default place where settings are stored. */ private static final String DEFAULT_SETTINGS_FILEPATH = "settings.txt"; /** The default place where units are stored. */ - private static final Path DEFAULT_UNITS_FILE = Path.of("unitsfile.txt"); + private static final String DEFAULT_UNITS_FILEPATH = "/unitsfile.txt"; /** The default place where dimensions are stored. */ - private static final Path DEFAULT_DIMENSION_FILE = Path - .of("dimensionfile.txt"); + private static final String DEFAULT_DIMENSIONS_FILEPATH = "/dimensionfile.txt"; /** The default place where exceptions are stored. */ - private static final Path DEFAULT_EXCEPTIONS_FILE = Path - .of("metric_exceptions.txt"); + private static final String DEFAULT_EXCEPTIONS_FILEPATH = "/metric_exceptions.txt"; /** * Adds default units and dimensions to a database. @@ -130,6 +130,41 @@ final class UnitConverterGUI { database.addDimension("TEMPERATURE", SI.Dimensions.TEMPERATURE); } + /** + * Gets the text of a resource file as a set of strings (each one is one + * line of the text). + * + * @param filename filename to get resource from + * @return contents of file + * @since 2021-03-27 + */ + public static final List getLinesFromResource(String filename) { + final List lines = new ArrayList<>(); + + try (InputStream stream = inputStream(filename); + Scanner scanner = new Scanner(stream)) { + while (scanner.hasNextLine()) { + lines.add(scanner.nextLine()); + } + } catch (final IOException e) { + throw new AssertionError( + "Error occurred while loading file " + filename, e); + } + + return lines; + } + + /** + * Gets an input stream for a resource file. + * + * @param filepath file to use as resource + * @return obtained Path + * @since 2021-03-27 + */ + private static final InputStream inputStream(String filepath) { + return UnitConverterGUI.class.getResourceAsStream(filepath); + } + /** * @return {@code line} with any comments removed. * @since 2021-03-13 @@ -161,9 +196,9 @@ final class UnitConverterGUI { /** A boolean remembering whether or not one-way conversion is on */ private boolean oneWay = true; - /** The prefix rule */ private DefaultPrefixRepetitionRule prefixRule = null; + // conditions for existence of From and To entries // used for one-way conversion private final MutablePredicate fromExistenceCondition = new MutablePredicate<>( @@ -196,21 +231,44 @@ final class UnitConverterGUI { DefaultPrefixRepetitionRule.NO_RESTRICTION); Presenter.addDefaults(this.database); - this.database.loadUnitsFile(DEFAULT_UNITS_FILE); - this.database.loadDimensionFile(DEFAULT_DIMENSION_FILE); + // load units and prefixes + try (final InputStream units = inputStream(DEFAULT_UNITS_FILEPATH)) { + this.database.loadUnitsFromStream(units); + } catch (final IOException e) { + throw new AssertionError("Loading of unitsfile.txt failed.", e); + } + + // load dimensions + try (final InputStream dimensions = inputStream( + DEFAULT_DIMENSIONS_FILEPATH)) { + this.database.loadDimensionsFromStream(dimensions); + } catch (final IOException e) { + throw new AssertionError("Loading of dimensionfile.txt failed.", e); + } // load metric exceptions try { - this.metricExceptions = Files.readAllLines(DEFAULT_EXCEPTIONS_FILE) - .stream().map(Presenter::withoutComments) - .filter(s -> !s.isBlank()).collect(Collectors.toSet()); + this.metricExceptions = new HashSet<>(); + try (InputStream exceptions = inputStream( + DEFAULT_EXCEPTIONS_FILEPATH); + Scanner scanner = new Scanner(exceptions)) { + while (scanner.hasNextLine()) { + final String line = Presenter + .withoutComments(scanner.nextLine()); + if (!line.isBlank()) { + this.metricExceptions.add(line); + } + } + } } catch (final IOException e) { throw new AssertionError("Loading of metric_exceptions.txt failed.", e); } // load settings - requires database to exist - this.loadSettings(); + if (Files.exists(this.getSettingsFile())) { + this.loadSettings(); + } // a comparator that can be used to compare prefix names // any name that does not exist is less than a name that does. @@ -1139,17 +1197,10 @@ final class UnitConverterGUI { infoPanel.add(infoTextArea); // get info text - final String infoText; - try { - final Path aboutFile = Path.of("src", "main", "resources", - "about.txt"); - infoText = Files.readAllLines(aboutFile).stream() - .map(Presenter::withoutComments) - .collect(Collectors.joining("\n")); - } catch (final IOException e) { - throw new AssertionError("I/O exception loading about.txt", - e); - } + final String infoText = Presenter + .getLinesFromResource("/about.txt").stream() + .map(Presenter::withoutComments) + .collect(Collectors.joining("\n")); infoTextArea.setText(infoText); } diff --git a/src/main/java/org/unitConverter/unit/UnitDatabase.java b/src/main/java/org/unitConverter/unit/UnitDatabase.java index 000acf5..6322fef 100644 --- a/src/main/java/org/unitConverter/unit/UnitDatabase.java +++ b/src/main/java/org/unitConverter/unit/UnitDatabase.java @@ -18,6 +18,7 @@ package org.unitConverter.unit; import java.io.FileNotFoundException; import java.io.IOException; +import java.io.InputStream; import java.math.BigDecimal; import java.nio.file.Files; import java.nio.file.Path; @@ -34,6 +35,7 @@ import java.util.Map; import java.util.Map.Entry; import java.util.NoSuchElementException; import java.util.Objects; +import java.util.Scanner; import java.util.Set; import java.util.function.BiFunction; import java.util.function.Function; @@ -1879,6 +1881,22 @@ public final class UnitDatabase { } } + /** + * Adds all dimensions from a {@code InputStream}. Otherwise, works like + * {@link #loadDimensionFile}. + * + * @param stream stream to load from + * @since 2021-03-27 + */ + public void loadDimensionsFromStream(final InputStream stream) { + try (final Scanner scanner = new Scanner(stream)) { + long lineCounter = 0; + while (scanner.hasNextLine()) { + this.addDimensionFromLine(scanner.nextLine(), ++lineCounter); + } + } + } + /** * Adds all units from a file, using data from the database to parse them. *

@@ -1918,6 +1936,22 @@ public final class UnitDatabase { } } + /** + * Adds all units from a {@code InputStream}. Otherwise, works like + * {@link #loadUnitsFile}. + * + * @param stream stream to load from + * @since 2021-03-27 + */ + public void loadUnitsFromStream(InputStream stream) { + try (final Scanner scanner = new Scanner(stream)) { + long lineCounter = 0; + while (scanner.hasNextLine()) { + this.addUnitOrPrefixFromLine(scanner.nextLine(), ++lineCounter); + } + } + } + /** * @return a map mapping prefix names to prefixes * @since 2019-04-13 diff --git a/src/main/resources/dimensionfile.txt b/src/main/resources/dimensionfile.txt new file mode 100644 index 0000000..3485de5 --- /dev/null +++ b/src/main/resources/dimensionfile.txt @@ -0,0 +1,18 @@ +# A file for the unit dimensions in my unit converter program + +# SI Base Dimensions +# ! means "look for an existing dimension which I will load at the start" +# This is necessary because every dimension must be defined by others, and I need somewhere to start. + +# I have excluded electric current, quantity and luminous intensity since their units are exclusively SI. + +LENGTH ! +MASS ! +TIME ! +TEMPERATURE ! + +# Derived Dimensions +AREA LENGTH^2 +VOLUME LENGTH^3 +VELOCITY LENGTH / TIME +ENERGY MASS * VELOCITY^2 \ No newline at end of file diff --git a/src/main/resources/metric_exceptions.txt b/src/main/resources/metric_exceptions.txt new file mode 100644 index 0000000..73748c0 --- /dev/null +++ b/src/main/resources/metric_exceptions.txt @@ -0,0 +1,19 @@ +# This is a list of exceptions for the one-way conversion mode +# Units in this list will be included in both From: and To: +# regardless of whether or not one-way conversion is enabled. + +tempC +tempCelsius +s +second +min +minute +h +hour +d +day +wk +week +gregorianmonth +gregorianyear +km/h \ No newline at end of file diff --git a/src/main/resources/unitsfile.txt b/src/main/resources/unitsfile.txt new file mode 100644 index 0000000..eafe885 --- /dev/null +++ b/src/main/resources/unitsfile.txt @@ -0,0 +1,267 @@ +# 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 + +da- deca +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 1 / s +Hz hertz + +gram millikg +g gram + +# 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 tau / 360 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 +week 7 day +wk week +julianyear 365.25 day +gregorianyear 365.2425 day +gregorianmonth gregorianyear / 12 + +# 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 kilogram / litre + +# Imperial length units +foot 0.3048 m +ft foot +inch foot / 12 +in inch +yard 3 foot +yd yard +mile 1760 yard +mi mile + +# Compressed notation +kph km / hour +mph mile / hour + +# 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 + +# Energy units +calorie 4.18 J +cal calorie +Calorie kilocalorie +Cal Calorie +Wh W h + +# Extra units to only include in the dimension-based converter +km km +cm cm +mm mm +mg mg +mL mL +ml ml +kJ kJ +MJ MJ +kWh kWh +m/s m / s +km/h km / h +ft/s foot / s +mi/h mile / hour \ No newline at end of file diff --git a/unitsfile.txt b/unitsfile.txt deleted file mode 100644 index eafe885..0000000 --- a/unitsfile.txt +++ /dev/null @@ -1,267 +0,0 @@ -# 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 - -da- deca -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 1 / s -Hz hertz - -gram millikg -g gram - -# 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 tau / 360 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 -week 7 day -wk week -julianyear 365.25 day -gregorianyear 365.2425 day -gregorianmonth gregorianyear / 12 - -# 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 kilogram / litre - -# Imperial length units -foot 0.3048 m -ft foot -inch foot / 12 -in inch -yard 3 foot -yd yard -mile 1760 yard -mi mile - -# Compressed notation -kph km / hour -mph mile / hour - -# 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 - -# Energy units -calorie 4.18 J -cal calorie -Calorie kilocalorie -Cal Calorie -Wh W h - -# Extra units to only include in the dimension-based converter -km km -cm cm -mm mm -mg mg -mL mL -ml ml -kJ kJ -MJ MJ -kWh kWh -m/s m / s -km/h km / h -ft/s foot / s -mi/h mile / hour \ No newline at end of file -- cgit v1.2.3