diff options
Diffstat (limited to 'src/main/java/sevenUnits')
30 files changed, 1133 insertions, 3095 deletions
diff --git a/src/main/java/sevenUnits/ProgramInfo.java b/src/main/java/sevenUnits/ProgramInfo.java index 31e43c7..f32d2c7 100644 --- a/src/main/java/sevenUnits/ProgramInfo.java +++ b/src/main/java/sevenUnits/ProgramInfo.java @@ -16,6 +16,8 @@ */ package sevenUnits; +import sevenUnits.utils.SemanticVersionNumber; + /** * Information about 7Units * @@ -24,8 +26,14 @@ package sevenUnits; */ public final class ProgramInfo { - public static final String VERSION = "0.3.2"; + /** The version number (0.4.0-alpha+dev) */ + public static final SemanticVersionNumber VERSION = SemanticVersionNumber + .preRelease(0, 4, 0, "alpha", 1); - private ProgramInfo() {} + private ProgramInfo() { + // this class is only for static variables, you shouldn't be able to + // construct an instance + throw new AssertionError(); + } } diff --git a/src/main/java/sevenUnits/converterGUI/DefaultPrefixRepetitionRule.java b/src/main/java/sevenUnits/converterGUI/DefaultPrefixRepetitionRule.java deleted file mode 100644 index 6b6abf0..0000000 --- a/src/main/java/sevenUnits/converterGUI/DefaultPrefixRepetitionRule.java +++ /dev/null @@ -1,95 +0,0 @@ -/** - * @since 2020-08-26 - */ -package sevenUnits.converterGUI; - -import java.util.List; -import java.util.function.Predicate; - -import sevenUnits.unit.Metric; -import sevenUnits.unit.UnitPrefix; - -/** - * A rule that specifies whether prefix repetition is allowed - * - * @since 2020-08-26 - */ -enum DefaultPrefixRepetitionRule implements Predicate<List<UnitPrefix>> { - NO_REPETITION { - @Override - public boolean test(List<UnitPrefix> prefixes) { - return prefixes.size() <= 1; - } - }, - NO_RESTRICTION { - @Override - public boolean test(List<UnitPrefix> 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<UnitPrefix> 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 (!Metric.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 (!Metric.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 (Metric.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 (Metric.YOTTA.equals(prefix) || Metric.YOCTO.equals(prefix)) { - part = 0; - } else if (Metric.THOUSAND_PREFIXES.contains(prefix)) { - part = 1; - } else { - part = 2; - } - } - return true; - } - }; -} diff --git a/src/main/java/sevenUnits/converterGUI/DelegateListModel.java b/src/main/java/sevenUnits/converterGUI/DelegateListModel.java deleted file mode 100644 index dd8cc97..0000000 --- a/src/main/java/sevenUnits/converterGUI/DelegateListModel.java +++ /dev/null @@ -1,242 +0,0 @@ -/** - * Copyright (C) 2018 Adrien Hopkins - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ -package sevenUnits.converterGUI; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.Iterator; -import java.util.List; -import java.util.ListIterator; - -import javax.swing.AbstractListModel; - -/** - * A list model that delegates to a list. - * <p> - * It is recommended to use the delegate methods in DelegateListModel instead of the delegated list's methods because - * the delegate methods handle updating the list. - * </p> - * - * @author Adrien Hopkins - * @since 2019-01-14 - * @since v0.1.0 - */ -final class DelegateListModel<E> extends AbstractListModel<E> implements List<E> { - /** - * @since 2019-01-14 - * @since v0.1.0 - */ - private static final long serialVersionUID = 8985494428224810045L; - - /** - * The list that this model is a delegate to. - * - * @since 2019-01-14 - * @since v0.1.0 - */ - private final List<E> delegate; - - /** - * Creates an empty {@code DelegateListModel}. - * - * @since 2019-04-13 - */ - public DelegateListModel() { - this(new ArrayList<>()); - } - - /** - * Creates the {@code DelegateListModel}. - * - * @param delegate - * list to delegate - * @since 2019-01-14 - * @since v0.1.0 - */ - public DelegateListModel(final List<E> delegate) { - this.delegate = delegate; - } - - @Override - public boolean add(final E element) { - final int index = this.delegate.size(); - final boolean success = this.delegate.add(element); - this.fireIntervalAdded(this, index, index); - return success; - } - - @Override - public void add(final int index, final E element) { - this.delegate.add(index, element); - this.fireIntervalAdded(this, index, index); - } - - @Override - public boolean addAll(final Collection<? extends E> c) { - boolean changed = false; - for (final E e : c) { - if (this.add(e)) { - changed = true; - } - } - return changed; - } - - @Override - public boolean addAll(final int index, final Collection<? extends E> c) { - for (final E e : c) { - this.add(index, e); - } - return !c.isEmpty(); // Since this is a list, it will always change if c has elements. - } - - @Override - public void clear() { - final int oldSize = this.delegate.size(); - this.delegate.clear(); - if (oldSize >= 1) { - this.fireIntervalRemoved(this, 0, oldSize - 1); - } - } - - @Override - public boolean contains(final Object elem) { - return this.delegate.contains(elem); - } - - @Override - public boolean containsAll(final Collection<?> c) { - for (final Object e : c) { - if (!c.contains(e)) - return false; - } - return true; - } - - @Override - public E get(final int index) { - return this.delegate.get(index); - } - - @Override - public E getElementAt(final int index) { - return this.delegate.get(index); - } - - @Override - public int getSize() { - return this.delegate.size(); - } - - @Override - public int indexOf(final Object elem) { - return this.delegate.indexOf(elem); - } - - @Override - public boolean isEmpty() { - return this.delegate.isEmpty(); - } - - @Override - public Iterator<E> iterator() { - return this.delegate.iterator(); - } - - @Override - public int lastIndexOf(final Object elem) { - return this.delegate.lastIndexOf(elem); - } - - @Override - public ListIterator<E> listIterator() { - return this.delegate.listIterator(); - } - - @Override - public ListIterator<E> listIterator(final int index) { - return this.delegate.listIterator(index); - } - - @Override - public E remove(final int index) { - final E returnValue = this.delegate.get(index); - this.delegate.remove(index); - this.fireIntervalRemoved(this, index, index); - return returnValue; - } - - @Override - public boolean remove(final Object o) { - final int index = this.delegate.indexOf(o); - final boolean returnValue = this.delegate.remove(o); - this.fireIntervalRemoved(this, index, index); - return returnValue; - } - - @Override - public boolean removeAll(final Collection<?> c) { - boolean changed = false; - for (final Object e : c) { - if (this.remove(e)) { - changed = true; - } - } - return changed; - } - - @Override - public boolean retainAll(final Collection<?> c) { - final int oldSize = this.size(); - final boolean returnValue = this.delegate.retainAll(c); - this.fireIntervalRemoved(this, this.size(), oldSize - 1); - return returnValue; - } - - @Override - public E set(final int index, final E element) { - final E returnValue = this.delegate.get(index); - this.delegate.set(index, element); - this.fireContentsChanged(this, index, index); - return returnValue; - } - - @Override - public int size() { - return this.delegate.size(); - } - - @Override - public List<E> subList(final int fromIndex, final int toIndex) { - return this.delegate.subList(fromIndex, toIndex); - } - - @Override - public Object[] toArray() { - return this.delegate.toArray(); - } - - @Override - public <T> T[] toArray(final T[] a) { - return this.delegate.toArray(a); - } - - @Override - public String toString() { - return this.delegate.toString(); - } -} diff --git a/src/main/java/sevenUnits/converterGUI/FilterComparator.java b/src/main/java/sevenUnits/converterGUI/FilterComparator.java deleted file mode 100644 index edd00e2..0000000 --- a/src/main/java/sevenUnits/converterGUI/FilterComparator.java +++ /dev/null @@ -1,129 +0,0 @@ -/** - * Copyright (C) 2018 Adrien Hopkins - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ -package sevenUnits.converterGUI; - -import java.util.Comparator; -import java.util.Objects; - -/** - * A comparator that compares strings using a filter. - * - * @author Adrien Hopkins - * @since 2019-01-15 - * @since v0.1.0 - */ -final class FilterComparator implements Comparator<String> { - /** - * The filter that the comparator is filtered by. - * - * @since 2019-01-15 - * @since v0.1.0 - */ - private final String filter; - /** - * The comparator to use if the arguments are otherwise equal. - * - * @since 2019-01-15 - * @since v0.1.0 - */ - private final Comparator<String> comparator; - /** - * Whether or not the comparison is case-sensitive. - * - * @since 2019-04-14 - * @since v0.2.0 - */ - private final boolean caseSensitive; - - /** - * Creates the {@code FilterComparator}. - * - * @param filter - * @since 2019-01-15 - * @since v0.1.0 - */ - public FilterComparator(final String filter) { - this(filter, null); - } - - /** - * Creates the {@code FilterComparator}. - * - * @param filter - * string to filter by - * @param comparator - * comparator to fall back to if all else fails, null is compareTo. - * @throws NullPointerException - * if filter is null - * @since 2019-01-15 - * @since v0.1.0 - */ - public FilterComparator(final String filter, final Comparator<String> comparator) { - this(filter, comparator, false); - } - - /** - * Creates the {@code FilterComparator}. - * - * @param filter - * string to filter by - * @param comparator - * comparator to fall back to if all else fails, null is compareTo. - * @param caseSensitive - * whether or not the comparator is case-sensitive - * @throws NullPointerException - * if filter is null - * @since 2019-04-14 - * @since v0.2.0 - */ - public FilterComparator(final String filter, final Comparator<String> comparator, final boolean caseSensitive) { - this.filter = Objects.requireNonNull(filter, "filter must not be null."); - this.comparator = comparator; - this.caseSensitive = caseSensitive; - } - - @Override - public int compare(final String arg0, final String arg1) { - // if this is case insensitive, make them lowercase - final String str0, str1; - if (this.caseSensitive) { - str0 = arg0; - str1 = arg1; - } else { - str0 = arg0.toLowerCase(); - str1 = arg1.toLowerCase(); - } - - // elements that start with the filter always go first - if (str0.startsWith(this.filter) && !str1.startsWith(this.filter)) - return -1; - else if (!str0.startsWith(this.filter) && str1.startsWith(this.filter)) - return 1; - - // elements that contain the filter but don't start with them go next - if (str0.contains(this.filter) && !str1.contains(this.filter)) - return -1; - else if (!str0.contains(this.filter) && !str1.contains(this.filter)) - return 1; - - // other elements go last - if (this.comparator == null) - return str0.compareTo(str1); - else - return this.comparator.compare(str0, str1); - } -} diff --git a/src/main/java/sevenUnits/converterGUI/GridBagBuilder.java b/src/main/java/sevenUnits/converterGUI/GridBagBuilder.java deleted file mode 100644 index 0b71d78..0000000 --- a/src/main/java/sevenUnits/converterGUI/GridBagBuilder.java +++ /dev/null @@ -1,479 +0,0 @@ -/** - * Copyright (C) 2018 Adrien Hopkins - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ -package sevenUnits.converterGUI; - -import java.awt.GridBagConstraints; -import java.awt.Insets; - -/** - * A builder for Java's {@link java.awt.GridBagConstraints} class. - * - * @author Adrien Hopkins - * @since 2018-11-30 - * @since v0.1.0 - */ -final class GridBagBuilder { - /** - * The built {@code GridBagConstraints}'s {@code gridx} property. - * <p> - * Specifies the cell containing the leading edge of the component's display area, where the first cell in a row has - * <code>gridx=0</code>. The leading edge of a component's display area is its left edge for a horizontal, - * left-to-right container and its right edge for a horizontal, right-to-left container. The value - * <code>RELATIVE</code> specifies that the component be placed immediately following the component that was added - * to the container just before this component was added. - * <p> - * The default value is <code>RELATIVE</code>. <code>gridx</code> should be a non-negative value. - * - * @serial - * @see #clone() - * @see java.awt.GridBagConstraints#gridy - * @see java.awt.ComponentOrientation - */ - private final int gridx; - - /** - * The built {@code GridBagConstraints}'s {@code gridy} property. - * <p> - * Specifies the cell at the top of the component's display area, where the topmost cell has <code>gridy=0</code>. - * The value <code>RELATIVE</code> specifies that the component be placed just below the component that was added to - * the container just before this component was added. - * <p> - * The default value is <code>RELATIVE</code>. <code>gridy</code> should be a non-negative value. - * - * @serial - * @see #clone() - * @see java.awt.GridBagConstraints#gridx - */ - private final int gridy; - - /** - * The built {@code GridBagConstraints}'s {@code gridwidth} property. - * <p> - * Specifies the number of cells in a row for the component's display area. - * <p> - * Use <code>REMAINDER</code> to specify that the component's display area will be from <code>gridx</code> to the - * last cell in the row. Use <code>RELATIVE</code> to specify that the component's display area will be from - * <code>gridx</code> to the next to the last one in its row. - * <p> - * <code>gridwidth</code> should be non-negative and the default value is 1. - * - * @serial - * @see #clone() - * @see java.awt.GridBagConstraints#gridheight - */ - private final int gridwidth; - - /** - * The built {@code GridBagConstraints}'s {@code gridheight} property. - * <p> - * Specifies the number of cells in a column for the component's display area. - * <p> - * Use <code>REMAINDER</code> to specify that the component's display area will be from <code>gridy</code> to the - * last cell in the column. Use <code>RELATIVE</code> to specify that the component's display area will be from - * <code>gridy</code> to the next to the last one in its column. - * <p> - * <code>gridheight</code> should be a non-negative value and the default value is 1. - * - * @serial - * @see #clone() - * @see java.awt.GridBagConstraints#gridwidth - */ - private final int gridheight; - - /** - * The built {@code GridBagConstraints}'s {@code weightx} property. - * <p> - * Specifies how to distribute extra horizontal space. - * <p> - * The grid bag layout manager calculates the weight of a column to be the maximum <code>weightx</code> of all the - * components in a column. If the resulting layout is smaller horizontally than the area it needs to fill, the extra - * space is distributed to each column in proportion to its weight. A column that has a weight of zero receives no - * extra space. - * <p> - * If all the weights are zero, all the extra space appears between the grids of the cell and the left and right - * edges. - * <p> - * The default value of this field is <code>0</code>. <code>weightx</code> should be a non-negative value. - * - * @serial - * @see #clone() - * @see java.awt.GridBagConstraints#weighty - */ - private double weightx; - - /** - * The built {@code GridBagConstraints}'s {@code weighty} property. - * <p> - * Specifies how to distribute extra vertical space. - * <p> - * The grid bag layout manager calculates the weight of a row to be the maximum <code>weighty</code> of all the - * components in a row. If the resulting layout is smaller vertically than the area it needs to fill, the extra - * space is distributed to each row in proportion to its weight. A row that has a weight of zero receives no extra - * space. - * <p> - * If all the weights are zero, all the extra space appears between the grids of the cell and the top and bottom - * edges. - * <p> - * The default value of this field is <code>0</code>. <code>weighty</code> should be a non-negative value. - * - * @serial - * @see #clone() - * @see java.awt.GridBagConstraints#weightx - */ - private double weighty; - - /** - * The built {@code GridBagConstraints}'s {@code anchor} property. - * <p> - * This field is used when the component is smaller than its display area. It determines where, within the display - * area, to place the component. - * <p> - * There are three kinds of possible values: orientation relative, baseline relative and absolute. Orientation - * relative values are interpreted relative to the container's component orientation property, baseline relative - * values are interpreted relative to the baseline and absolute values are not. The absolute values are: - * <code>CENTER</code>, <code>NORTH</code>, <code>NORTHEAST</code>, <code>EAST</code>, <code>SOUTHEAST</code>, - * <code>SOUTH</code>, <code>SOUTHWEST</code>, <code>WEST</code>, and <code>NORTHWEST</code>. The orientation - * relative values are: <code>PAGE_START</code>, <code>PAGE_END</code>, <code>LINE_START</code>, - * <code>LINE_END</code>, <code>FIRST_LINE_START</code>, <code>FIRST_LINE_END</code>, <code>LAST_LINE_START</code> - * and <code>LAST_LINE_END</code>. The baseline relative values are: <code>BASELINE</code>, - * <code>BASELINE_LEADING</code>, <code>BASELINE_TRAILING</code>, <code>ABOVE_BASELINE</code>, - * <code>ABOVE_BASELINE_LEADING</code>, <code>ABOVE_BASELINE_TRAILING</code>, <code>BELOW_BASELINE</code>, - * <code>BELOW_BASELINE_LEADING</code>, and <code>BELOW_BASELINE_TRAILING</code>. The default value is - * <code>CENTER</code>. - * - * @serial - * @see #clone() - * @see java.awt.ComponentOrientation - */ - private int anchor; - - /** - * The built {@code GridBagConstraints}'s {@code fill} property. - * <p> - * This field is used when the component's display area is larger than the component's requested size. It determines - * whether to resize the component, and if so, how. - * <p> - * The following values are valid for <code>fill</code>: - * - * <ul> - * <li><code>NONE</code>: Do not resize the component. - * <li><code>HORIZONTAL</code>: Make the component wide enough to fill its display area horizontally, but do not - * change its height. - * <li><code>VERTICAL</code>: Make the component tall enough to fill its display area vertically, but do not change - * its width. - * <li><code>BOTH</code>: Make the component fill its display area entirely. - * </ul> - * <p> - * The default value is <code>NONE</code>. - * - * @serial - * @see #clone() - */ - private int fill; - - /** - * The built {@code GridBagConstraints}'s {@code insets} property. - * <p> - * This field specifies the external padding of the component, the minimum amount of space between the component and - * the edges of its display area. - * <p> - * The default value is <code>new Insets(0, 0, 0, 0)</code>. - * - * @serial - * @see #clone() - */ - private Insets insets; - - /** - * The built {@code GridBagConstraints}'s {@code ipadx} property. - * <p> - * This field specifies the internal padding of the component, how much space to add to the minimum width of the - * component. The width of the component is at least its minimum width plus <code>ipadx</code> pixels. - * <p> - * The default value is <code>0</code>. - * - * @serial - * @see #clone() - * @see java.awt.GridBagConstraints#ipady - */ - private int ipadx; - - /** - * The built {@code GridBagConstraints}'s {@code ipady} property. - * <p> - * This field specifies the internal padding, that is, how much space to add to the minimum height of the component. - * The height of the component is at least its minimum height plus <code>ipady</code> pixels. - * <p> - * The default value is 0. - * - * @serial - * @see #clone() - * @see java.awt.GridBagConstraints#ipadx - */ - private int ipady; - - /** - * @param gridx - * x position - * @param gridy - * y position - * @since 2018-11-30 - * @since v0.1.0 - */ - public GridBagBuilder(final int gridx, final int gridy) { - this(gridx, gridy, 1, 1); - } - - /** - * @param gridx - * x position - * @param gridy - * y position - * @param gridwidth - * number of cells occupied horizontally - * @param gridheight - * number of cells occupied vertically - * @since 2018-11-30 - * @since v0.1.0 - */ - public GridBagBuilder(final int gridx, final int gridy, final int gridwidth, final int gridheight) { - this(gridx, gridy, gridwidth, gridheight, 0.0, 0.0, GridBagConstraints.CENTER, GridBagConstraints.NONE, - new Insets(0, 0, 0, 0), 0, 0); - } - - /** - * @param gridx - * x position - * @param gridy - * y position - * @param gridwidth - * number of cells occupied horizontally - * @param gridheight - * number of cells occupied vertically - * @param weightx - * @param weighty - * @param anchor - * @param fill - * @param insets - * @param ipadx - * @param ipady - * @since 2018-11-30 - * @since v0.1.0 - */ - private GridBagBuilder(final int gridx, final int gridy, final int gridwidth, final int gridheight, - final double weightx, final double weighty, final int anchor, final int fill, final Insets insets, - final int ipadx, final int ipady) { - super(); - this.gridx = gridx; - this.gridy = gridy; - this.gridwidth = gridwidth; - this.gridheight = gridheight; - this.weightx = weightx; - this.weighty = weighty; - this.anchor = anchor; - this.fill = fill; - this.insets = (Insets) insets.clone(); - this.ipadx = ipadx; - this.ipady = ipady; - } - - /** - * @return {@code GridBagConstraints} created by this builder - * @since 2018-11-30 - * @since v0.1.0 - */ - public GridBagConstraints build() { - return new GridBagConstraints(this.gridx, this.gridy, this.gridwidth, this.gridheight, this.weightx, - this.weighty, this.anchor, this.fill, this.insets, this.ipadx, this.ipady); - } - - /** - * @return anchor - * @since 2018-11-30 - * @since v0.1.0 - */ - public int getAnchor() { - return this.anchor; - } - - /** - * @return fill - * @since 2018-11-30 - * @since v0.1.0 - */ - public int getFill() { - return this.fill; - } - - /** - * @return gridheight - * @since 2018-11-30 - * @since v0.1.0 - */ - public int getGridheight() { - return this.gridheight; - } - - /** - * @return gridwidth - * @since 2018-11-30 - * @since v0.1.0 - */ - public int getGridwidth() { - return this.gridwidth; - } - - /** - * @return gridx - * @since 2018-11-30 - * @since v0.1.0 - */ - public int getGridx() { - return this.gridx; - } - - /** - * @return gridy - * @since 2018-11-30 - * @since v0.1.0 - */ - public int getGridy() { - return this.gridy; - } - - /** - * @return insets - * @since 2018-11-30 - * @since v0.1.0 - */ - public Insets getInsets() { - return this.insets; - } - - /** - * @return ipadx - * @since 2018-11-30 - * @since v0.1.0 - */ - public int getIpadx() { - return this.ipadx; - } - - /** - * @return ipady - * @since 2018-11-30 - * @since v0.1.0 - */ - public int getIpady() { - return this.ipady; - } - - /** - * @return weightx - * @since 2018-11-30 - * @since v0.1.0 - */ - public double getWeightx() { - return this.weightx; - } - - /** - * @return weighty - * @since 2018-11-30 - * @since v0.1.0 - */ - public double getWeighty() { - return this.weighty; - } - - /** - * @param anchor - * anchor to set - * @since 2018-11-30 - * @since v0.1.0 - */ - public GridBagBuilder setAnchor(final int anchor) { - this.anchor = anchor; - return this; - } - - /** - * @param fill - * fill to set - * @since 2018-11-30 - * @since v0.1.0 - */ - public GridBagBuilder setFill(final int fill) { - this.fill = fill; - return this; - } - - /** - * @param insets - * insets to set - * @since 2018-11-30 - * @since v0.1.0 - */ - public GridBagBuilder setInsets(final Insets insets) { - this.insets = insets; - return this; - } - - /** - * @param ipadx - * ipadx to set - * @since 2018-11-30 - * @since v0.1.0 - */ - public GridBagBuilder setIpadx(final int ipadx) { - this.ipadx = ipadx; - return this; - } - - /** - * @param ipady - * ipady to set - * @since 2018-11-30 - * @since v0.1.0 - */ - public GridBagBuilder setIpady(final int ipady) { - this.ipady = ipady; - return this; - } - - /** - * @param weightx - * weightx to set - * @since 2018-11-30 - * @since v0.1.0 - */ - public GridBagBuilder setWeightx(final double weightx) { - this.weightx = weightx; - return this; - } - - /** - * @param weighty - * weighty to set - * @since 2018-11-30 - * @since v0.1.0 - */ - public GridBagBuilder setWeighty(final double weighty) { - this.weighty = weighty; - return this; - } -} diff --git a/src/main/java/sevenUnits/converterGUI/MutablePredicate.java b/src/main/java/sevenUnits/converterGUI/MutablePredicate.java deleted file mode 100644 index ae6b7a1..0000000 --- a/src/main/java/sevenUnits/converterGUI/MutablePredicate.java +++ /dev/null @@ -1,70 +0,0 @@ -/** - * Copyright (C) 2019 Adrien Hopkins - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ -package sevenUnits.converterGUI; - -import java.util.function.Predicate; - -/** - * A container for a predicate, which can be changed later. - * - * @author Adrien Hopkins - * @since 2019-04-13 - * @since v0.2.0 - */ -final class MutablePredicate<T> implements Predicate<T> { - /** - * The predicate stored in this {@code MutablePredicate} - * - * @since 2019-04-13 - * @since v0.2.0 - */ - private Predicate<T> predicate; - - /** - * Creates the {@code MutablePredicate}. - * - * @since 2019-04-13 - * @since v0.2.0 - */ - public MutablePredicate(final Predicate<T> predicate) { - this.predicate = predicate; - } - - /** - * @return predicate - * @since 2019-04-13 - * @since v0.2.0 - */ - public final Predicate<T> getPredicate() { - return this.predicate; - } - - /** - * @param predicate - * new value of predicate - * @since 2019-04-13 - * @since v0.2.0 - */ - public final void setPredicate(final Predicate<T> predicate) { - this.predicate = predicate; - } - - @Override - public boolean test(final T t) { - return this.predicate.test(t); - } -} diff --git a/src/main/java/sevenUnits/converterGUI/SearchBoxList.java b/src/main/java/sevenUnits/converterGUI/SearchBoxList.java deleted file mode 100644 index 2aa9fce..0000000 --- a/src/main/java/sevenUnits/converterGUI/SearchBoxList.java +++ /dev/null @@ -1,320 +0,0 @@ -/** - * Copyright (C) 2019 Adrien Hopkins - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ -package sevenUnits.converterGUI; - -import java.awt.BorderLayout; -import java.awt.Color; -import java.awt.event.FocusEvent; -import java.awt.event.FocusListener; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Comparator; -import java.util.function.Predicate; - -import javax.swing.JList; -import javax.swing.JPanel; -import javax.swing.JScrollPane; -import javax.swing.JTextField; - -/** - * @author Adrien Hopkins - * @since 2019-04-13 - * @since v0.2.0 - */ -final class SearchBoxList extends JPanel { - - /** - * @since 2019-04-13 - * @since v0.2.0 - */ - private static final long serialVersionUID = 6226930279415983433L; - - /** - * The text to place in an empty search box. - * - * @since 2019-04-13 - * @since v0.2.0 - */ - private static final String EMPTY_TEXT = "Search..."; - - /** - * The color to use for an empty foreground. - * - * @since 2019-04-13 - * @since v0.2.0 - */ - private static final Color EMPTY_FOREGROUND = new Color(192, 192, 192); - - // the components - private final Collection<String> itemsToFilter; - private final DelegateListModel<String> listModel; - private final JTextField searchBox; - private final JList<String> searchItems; - - private boolean searchBoxEmpty = true; - - // I need to do this because, for some reason, Swing is auto-focusing my - // search box without triggering a focus - // event. - private boolean searchBoxFocused = false; - - private Predicate<String> customSearchFilter = o -> true; - private final Comparator<String> defaultOrdering; - private final boolean caseSensitive; - - /** - * Creates the {@code SearchBoxList}. - * - * @param itemsToFilter items to put in the list - * @since 2019-04-14 - */ - public SearchBoxList(final Collection<String> itemsToFilter) { - this(itemsToFilter, null, false); - } - - /** - * Creates the {@code SearchBoxList}. - * - * @param itemsToFilter items to put in the list - * @param defaultOrdering default ordering of items after filtration - * (null=Comparable) - * @param caseSensitive whether or not the filtration is case-sensitive - * - * @since 2019-04-13 - * @since v0.2.0 - */ - public SearchBoxList(final Collection<String> itemsToFilter, - final Comparator<String> defaultOrdering, - final boolean caseSensitive) { - super(new BorderLayout(), true); - this.itemsToFilter = new ArrayList<>(itemsToFilter); - this.defaultOrdering = defaultOrdering; - this.caseSensitive = caseSensitive; - - // create the components - this.listModel = new DelegateListModel<>(new ArrayList<>(itemsToFilter)); - this.searchItems = new JList<>(this.listModel); - - this.searchBox = new JTextField(EMPTY_TEXT); - this.searchBox.setForeground(EMPTY_FOREGROUND); - - // add them to the panel - this.add(this.searchBox, BorderLayout.PAGE_START); - this.add(new JScrollPane(this.searchItems), BorderLayout.CENTER); - - // set up the search box - this.searchBox.addFocusListener(new FocusListener() { - @Override - public void focusGained(final FocusEvent e) { - SearchBoxList.this.searchBoxFocusGained(e); - } - - @Override - public void focusLost(final FocusEvent e) { - SearchBoxList.this.searchBoxFocusLost(e); - } - }); - - this.searchBox.addCaretListener(e -> this.searchBoxTextChanged()); - this.searchBoxEmpty = true; - } - - /** - * Adds an additional filter for searching. - * - * @param filter filter to add. - * @since 2019-04-13 - * @since v0.2.0 - */ - public void addSearchFilter(final Predicate<String> filter) { - this.customSearchFilter = this.customSearchFilter.and(filter); - } - - /** - * Resets the search filter. - * - * @since 2019-04-13 - * @since v0.2.0 - */ - public void clearSearchFilters() { - this.customSearchFilter = o -> true; - } - - /** - * @return this component's search box component - * @since 2019-04-14 - * @since v0.2.0 - */ - public final JTextField getSearchBox() { - return this.searchBox; - } - - /** - * @param searchText text to search for - * @return a filter that filters out that text, based on this list's case - * sensitive setting - * @since 2019-04-14 - * @since v0.2.0 - */ - private Predicate<String> getSearchFilter(final String searchText) { - if (this.caseSensitive) - return string -> string.contains(searchText); - else - return string -> string.toLowerCase() - .contains(searchText.toLowerCase()); - } - - /** - * @return this component's list component - * @since 2019-04-14 - * @since v0.2.0 - */ - public final JList<String> getSearchList() { - return this.searchItems; - } - - /** - * @return index selected in item list - * @since 2019-04-14 - * @since v0.2.0 - */ - public int getSelectedIndex() { - return this.searchItems.getSelectedIndex(); - } - - /** - * @return value selected in item list - * @since 2019-04-13 - * @since v0.2.0 - */ - public String getSelectedValue() { - return this.searchItems.getSelectedValue(); - } - - /** - * Re-applies the filters. - * - * @since 2019-04-13 - * @since v0.2.0 - */ - public void reapplyFilter() { - final String searchText = this.searchBoxEmpty ? "" - : this.searchBox.getText(); - final FilterComparator comparator = new FilterComparator(searchText, - this.defaultOrdering, this.caseSensitive); - final Predicate<String> searchFilter = this.getSearchFilter(searchText); - - this.listModel.clear(); - this.itemsToFilter.forEach(string -> { - if (searchFilter.test(string)) { - this.listModel.add(string); - } - }); - - // applies the custom filters - this.listModel.removeIf(this.customSearchFilter.negate()); - - // sorts the remaining items - this.listModel.sort(comparator); - } - - /** - * Runs whenever the search box gains focus. - * - * @param e focus event - * @since 2019-04-13 - * @since v0.2.0 - */ - private void searchBoxFocusGained(final FocusEvent e) { - this.searchBoxFocused = true; - if (this.searchBoxEmpty) { - this.searchBox.setText(""); - this.searchBox.setForeground(Color.BLACK); - } - } - - /** - * Runs whenever the search box loses focus. - * - * @param e focus event - * @since 2019-04-13 - * @since v0.2.0 - */ - private void searchBoxFocusLost(final FocusEvent e) { - this.searchBoxFocused = false; - if (this.searchBoxEmpty) { - this.searchBox.setText(EMPTY_TEXT); - this.searchBox.setForeground(EMPTY_FOREGROUND); - } - } - - /** - * Runs whenever the text in the search box is changed. - * <p> - * Reapplies the search filter, and custom filters. - * </p> - * - * @since 2019-04-14 - * @since v0.2.0 - */ - private void searchBoxTextChanged() { - if (this.searchBoxFocused) { - this.searchBoxEmpty = this.searchBox.getText().equals(""); - } - final String searchText = this.searchBoxEmpty ? "" - : this.searchBox.getText(); - final FilterComparator comparator = new FilterComparator(searchText, - this.defaultOrdering, this.caseSensitive); - final Predicate<String> searchFilter = this.getSearchFilter(searchText); - - // initialize list with items that match the filter then sort - this.listModel.clear(); - this.itemsToFilter.forEach(string -> { - if (searchFilter.test(string)) { - this.listModel.add(string); - } - }); - - // applies the custom filters - this.listModel.removeIf(this.customSearchFilter.negate()); - - // sorts the remaining items - this.listModel.sort(comparator); - } - - /** - * Resets the search box list's contents to the provided items, removing any - * old items - * - * @param newItems new items to put in list - * @since 2021-05-22 - */ - public void setItems(Collection<String> newItems) { - this.itemsToFilter.clear(); - this.itemsToFilter.addAll(newItems); - this.reapplyFilter(); - } - - /** - * Manually updates the search box's item list. - * - * @since 2020-08-27 - */ - public void updateList() { - this.searchBoxTextChanged(); - } -} diff --git a/src/main/java/sevenUnits/converterGUI/SevenUnitsGUI.java b/src/main/java/sevenUnits/converterGUI/SevenUnitsGUI.java deleted file mode 100644 index bfd5974..0000000 --- a/src/main/java/sevenUnits/converterGUI/SevenUnitsGUI.java +++ /dev/null @@ -1,1505 +0,0 @@ -/** - * 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 <https://www.gnu.org/licenses/>. - */ -package sevenUnits.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.io.InputStream; -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.Scanner; -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 sevenUnits.ProgramInfo; -import sevenUnits.unit.BaseDimension; -import sevenUnits.unit.BritishImperial; -import sevenUnits.unit.LinearUnit; -import sevenUnits.unit.LinearUnitValue; -import sevenUnits.unit.NameSymbol; -import sevenUnits.unit.Metric; -import sevenUnits.unit.Unit; -import sevenUnits.unit.UnitDatabase; -import sevenUnits.unit.UnitPrefix; -import sevenUnits.unit.UnitValue; -import sevenUnits.utils.ConditionalExistenceCollections; -import sevenUnits.utils.ObjectProduct; - -/** - * @author Adrien Hopkins - * @since 2018-12-27 - * @since v0.1.0 - */ -final class SevenUnitsGUI { - /** - * 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 String DEFAULT_UNITS_FILEPATH = "/unitsfile.txt"; - /** The default place where dimensions are stored. */ - private static final String DEFAULT_DIMENSIONS_FILEPATH = "/dimensionfile.txt"; - /** The default place where exceptions are stored. */ - private static final String DEFAULT_EXCEPTIONS_FILEPATH = "/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", Metric.METRE); - database.addUnit("kilogram", Metric.KILOGRAM); - database.addUnit("gram", Metric.KILOGRAM.dividedBy(1000)); - database.addUnit("second", Metric.SECOND); - database.addUnit("ampere", Metric.AMPERE); - database.addUnit("kelvin", Metric.KELVIN); - database.addUnit("mole", Metric.MOLE); - database.addUnit("candela", Metric.CANDELA); - database.addUnit("bit", Metric.BIT); - database.addUnit("unit", Metric.ONE); - // nonlinear units - must be loaded manually - database.addUnit("tempCelsius", Metric.CELSIUS); - database.addUnit("tempFahrenheit", BritishImperial.FAHRENHEIT); - - // load initial dimensions - database.addDimension("LENGTH", Metric.Dimensions.LENGTH); - database.addDimension("MASS", Metric.Dimensions.MASS); - database.addDimension("TIME", Metric.Dimensions.TIME); - database.addDimension("TEMPERATURE", Metric.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<String> getLinesFromResource(String filename) { - final List<String> 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 SevenUnitsGUI.class.getResourceAsStream(filepath); - } - - /** - * @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<String> unitNames; - - /** The names of all of the prefixes */ - private final List<String> prefixNames; - - /** The names of all of the dimensions */ - private final List<String> dimensionNames; - - /** Unit names that are ignored by the metric-only/imperial-only filter */ - private final Set<String> metricExceptions; - - private final Comparator<String> 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<String> fromExistenceCondition = new MutablePredicate<>( - s -> true); - - private final MutablePredicate<String> 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; - - // The "include duplicate units" setting - private boolean includeDuplicateUnits = true; - - /** - * 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); - - // 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 = 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 - 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. - // 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(true).keySet()); - this.unitNames.sort(null); // sorts it using Comparable - - this.prefixNames = new ArrayList<>(this.database.prefixMap().keySet()); - this.prefixNames.sort(this.prefixNameComparator); // sorts it using my - // comparator - - this.dimensionNames = new DelegateListModel<>( - new ArrayList<>(this.database.dimensionMap().keySet())); - this.dimensionNames.sort(null); // sorts it using Comparable - - // a Predicate that returns true iff the argument is a full base unit - final Predicate<Unit> isFullBase = unit -> unit instanceof LinearUnit - && ((LinearUnit) unit).isBase(); - - // print out unit counts - System.out.printf( - "Successfully loaded %d units with %d unit names (%d base units).%n", - this.database.unitMapPrefixless(false).size(), - this.database.unitMapPrefixless(true).size(), - this.database.unitMapPrefixless(false).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. - * - * <p> - * Reads and parses a unit expression from the from and to boxes, then - * converts {@code from} to {@code to}. Any errors are shown in - * JOptionPanes. - * </p> - * - * @since 2019-01-26 - * @since v0.1.0 - */ - public final void convertExpressions() { - final String fromUnitString = this.view.getFromText(); - final String toUnitString = this.view.getToText(); - - if (fromUnitString.isEmpty()) { - this.view.showErrorDialog("Parse Error", - "Please enter a unit expression in the From: box."); - return; - } - if (toUnitString.isEmpty()) { - this.view.showErrorDialog("Parse Error", - "Please enter a unit expression in the To: box."); - return; - } - - 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<String> 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<String> 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<String> 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; - case "include_duplicates": - this.includeDuplicateUnits = Boolean.valueOf(value); - if (this.view.presenter != null) { - this.view.update(); - } - 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<String> prefixNameSet() { - return this.database.prefixMap().keySet(); - } - - /** - * Runs whenever a prefix is selected in the viewer. - * <p> - * Shows its information in the text box to the right. - * </p> - * - * @since 2019-01-15 - * @since v0.1.0 - */ - public final void prefixSelected() { - final String prefixName = this.view.getPrefixViewerSelection(); - if (prefixName == null) - return; - else { - final UnitPrefix prefix = this.database.getPrefix(prefixName); - - this.view.setPrefixTextBoxText(String.format("%s%nMultiplier: %s", - prefixName, prefix.getMultiplier())); - } - } - - /** - * 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)); - writer.write(String.format("include_duplicates=%s\n", - this.includeDuplicateUnits)); - } catch (final IOException e) { - e.printStackTrace(); - this.view.showErrorDialog("I/O Error", - "Error occurred while saving settings: " - + e.getLocalizedMessage()); - } - } - - public final void setIncludeDuplicateUnits( - boolean includeDuplicateUnits) { - this.includeDuplicateUnits = includeDuplicateUnits; - - this.view.update(); - this.saveSettings(); - } - - /** - * 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<List<UnitPrefix>> 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<String> 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<BaseDimension> dimension = this.database - .getDimension(dimensionName); - return unit.getDimension().equals(dimension); - } - - /** - * Runs whenever a unit is selected in the viewer. - * <p> - * Shows its information in the text box to the right. - * </p> - * - * @since 2019-01-15 - * @since v0.1.0 - */ - public final void unitNameSelected() { - final String unitName = this.view.getUnitViewerSelection(); - if (unitName == null) - return; - else { - final Unit unit = this.database.getUnit(unitName); - - this.view.setUnitTextBoxText(unit.toString()); - } - } - - /** - * @return a set of all of the unit names - * @since 2019-04-14 - * @since v0.2.0 - */ - public final Set<String> unitNameSet() { - return this.database.unitMapPrefixless(this.includeDuplicateUnits) - .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("7Units"); - 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<String> dimensionSelector = new JComboBox<>( - this.presenter.dimensionNameList() - .toArray(new String[0])); - dimensionSelector.setSelectedItem("LENGTH"); - - // handle dimension filter - final MutablePredicate<String> dimensionFilter = new MutablePredicate<>( - s -> true); - - // panel for From things - inputPanel.add(this.fromSearch); - - this.fromSearch.addSearchFilter(dimensionFilter); - - { // for dimension selector and arrow that represents - // conversion - final JPanel inBetweenPanel = new JPanel(); - inputPanel.add(inBetweenPanel); - - inBetweenPanel.setLayout(new BorderLayout()); - - { // dimension selector - inBetweenPanel.add(dimensionSelector, - BorderLayout.PAGE_START); - } - - { // the arrow in the middle - final JLabel arrowLabel = new JLabel("->"); - inBetweenPanel.add(arrowLabel, BorderLayout.CENTER); - } - } - - // panel for To things - - inputPanel.add(this.toSearch); - - this.toSearch.addSearchFilter(dimensionFilter); - - // code for dimension filter - dimensionSelector.addItemListener(e -> { - dimensionFilter.setPredicate(string -> View.this.presenter - .unitMatchesDimension(string, - (String) dimensionSelector.getSelectedItem())); - this.fromSearch.reapplyFilter(); - this.toSearch.reapplyFilter(); - }); - - // apply the item listener once because I have a default - // selection - dimensionFilter.setPredicate(string -> View.this.presenter - .unitMatchesDimension(string, - (String) dimensionSelector.getSelectedItem())); - this.fromSearch.reapplyFilter(); - this.toSearch.reapplyFilter(); - } - - { // panel for submit and output, and also value entry - final JPanel outputPanel = new JPanel(); - convertUnitPanel.add(outputPanel, BorderLayout.PAGE_END); - - outputPanel.setLayout(new GridLayout(3, 1)); - - { // unit input - final JPanel valueInputPanel = new JPanel(); - outputPanel.add(valueInputPanel); - - valueInputPanel.setLayout(new BorderLayout()); - - { // prompt - final JLabel valuePrompt = new JLabel( - "Value to convert: "); - valueInputPanel.add(valuePrompt, - BorderLayout.LINE_START); - } - - { // value to convert - valueInputPanel.add(this.valueInput, - BorderLayout.CENTER); - } - } - - { // button to convert - final JButton convertButton = new JButton("Convert"); - outputPanel.add(convertButton); - - convertButton.addActionListener( - e -> this.presenter.convertDimensionBased()); - 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 = Presenter - .getLinesFromResource("/about.txt").stream() - .map(Presenter::withoutComments) - .collect(Collectors.joining("\n")) - .replaceAll("\\[VERSION\\]", ProgramInfo.VERSION); - 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 Duplicates in \"Convert Units\""); - showAllVariations - .setSelected(this.presenter.includeDuplicateUnits); - showAllVariations.addItemListener(e -> this.presenter - .setIncludeDuplicateUnits(e.getStateChange() == 1)); - 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() { - this.unitNameList.setItems(this.presenter.unitNameSet()); - this.fromSearch.setItems(this.presenter.fromEntries()); - this.toSearch.setItems(this.presenter.toEntries()); - - 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/sevenUnits/converterGUI/package-info.java b/src/main/java/sevenUnits/converterGUI/package-info.java deleted file mode 100644 index 784664f..0000000 --- a/src/main/java/sevenUnits/converterGUI/package-info.java +++ /dev/null @@ -1,24 +0,0 @@ -/** - * Copyright (C) 2019 Adrien Hopkins - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ -/** - * The GUI interface of the Unit Converter. - * - * @author Adrien Hopkins - * @since 2019-01-25 - * @since v0.2.0 - */ -package sevenUnits.converterGUI;
\ No newline at end of file diff --git a/src/main/java/sevenUnits/unit/BaseDimension.java b/src/main/java/sevenUnits/unit/BaseDimension.java index d5e98ca..bcd57d9 100644 --- a/src/main/java/sevenUnits/unit/BaseDimension.java +++ b/src/main/java/sevenUnits/unit/BaseDimension.java @@ -18,70 +18,58 @@ package sevenUnits.unit; import java.util.Objects; +import sevenUnits.utils.NameSymbol; +import sevenUnits.utils.Nameable; + /** * A dimension that defines a {@code BaseUnit} * * @author Adrien Hopkins * @since 2019-10-16 */ -public final class BaseDimension { +public final class BaseDimension implements Nameable { /** * Gets a {@code BaseDimension} with the provided name and symbol. * - * @param name - * name of dimension - * @param symbol - * symbol used for dimension + * @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. + * 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 + * @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 NameSymbol getNameSymbol() { + return NameSymbol.of(this.name, this.symbol); } - + @Override public String toString() { - return String.format("%s (%s)", this.getName(), this.getSymbol()); + return String.format("%s (%s)", this.name, this.symbol); } } diff --git a/src/main/java/sevenUnits/unit/BaseUnit.java b/src/main/java/sevenUnits/unit/BaseUnit.java index ee2c277..dba7f52 100644 --- a/src/main/java/sevenUnits/unit/BaseUnit.java +++ b/src/main/java/sevenUnits/unit/BaseUnit.java @@ -20,6 +20,8 @@ import java.util.HashSet; import java.util.Objects; import java.util.Set; +import sevenUnits.utils.NameSymbol; + /** * A unit that other units are defined by. * <p> diff --git a/src/main/java/sevenUnits/unit/BritishImperial.java b/src/main/java/sevenUnits/unit/BritishImperial.java index 743beeb..0ecba6d 100644 --- a/src/main/java/sevenUnits/unit/BritishImperial.java +++ b/src/main/java/sevenUnits/unit/BritishImperial.java @@ -16,6 +16,8 @@ */ package sevenUnits.unit; +import sevenUnits.utils.NameSymbol; + /** * A static utility class that contains units in the British Imperial system. * @@ -119,5 +121,5 @@ public final class BritishImperial { public static final Unit FAHRENHEIT = Unit .fromConversionFunctions(Metric.KELVIN.getBase(), tempK -> tempK * 1.8 - 459.67, tempF -> (tempF + 459.67) / 1.8) - .withName(NameSymbol.of("degrees Fahrenheit", "\u00B0F")); + .withName(NameSymbol.of("degree Fahrenheit", "\u00B0F")); } diff --git a/src/main/java/sevenUnits/unit/FunctionalUnit.java b/src/main/java/sevenUnits/unit/FunctionalUnit.java index df457e4..720b0af 100644 --- a/src/main/java/sevenUnits/unit/FunctionalUnit.java +++ b/src/main/java/sevenUnits/unit/FunctionalUnit.java @@ -19,6 +19,7 @@ package sevenUnits.unit; import java.util.Objects; import java.util.function.DoubleUnaryOperator; +import sevenUnits.utils.NameSymbol; import sevenUnits.utils.ObjectProduct; /** diff --git a/src/main/java/sevenUnits/unit/FunctionalUnitlike.java b/src/main/java/sevenUnits/unit/FunctionalUnitlike.java index 2ee9e19..d6046c0 100644 --- a/src/main/java/sevenUnits/unit/FunctionalUnitlike.java +++ b/src/main/java/sevenUnits/unit/FunctionalUnitlike.java @@ -19,6 +19,7 @@ package sevenUnits.unit; import java.util.function.DoubleFunction; import java.util.function.ToDoubleFunction; +import sevenUnits.utils.NameSymbol; import sevenUnits.utils.ObjectProduct; /** diff --git a/src/main/java/sevenUnits/unit/LinearUnit.java b/src/main/java/sevenUnits/unit/LinearUnit.java index 25c2e2e..103b7f6 100644 --- a/src/main/java/sevenUnits/unit/LinearUnit.java +++ b/src/main/java/sevenUnits/unit/LinearUnit.java @@ -19,6 +19,7 @@ package sevenUnits.unit; import java.util.Objects; import sevenUnits.utils.DecimalComparison; +import sevenUnits.utils.NameSymbol; import sevenUnits.utils.ObjectProduct; import sevenUnits.utils.UncertainDouble; @@ -369,6 +370,13 @@ public final class LinearUnit extends Unit { this.getConversionFactor() * multiplier.getConversionFactor()); } + @Override + public String toDefinitionString() { + return Double.toString(this.conversionFactor) + + (this.getBase().equals(ObjectProduct.empty()) ? "" + : " " + this.getBase().toString(BaseUnit::getShortName)); + } + /** * Returns this unit but to an exponent. * @@ -382,20 +390,6 @@ public final class LinearUnit extends Unit { 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); diff --git a/src/main/java/sevenUnits/unit/LinearUnitValue.java b/src/main/java/sevenUnits/unit/LinearUnitValue.java index a50e1f5..f91d30b 100644 --- a/src/main/java/sevenUnits/unit/LinearUnitValue.java +++ b/src/main/java/sevenUnits/unit/LinearUnitValue.java @@ -16,6 +16,7 @@ */ package sevenUnits.unit; +import java.math.RoundingMode; import java.util.Objects; import java.util.Optional; @@ -300,7 +301,7 @@ public final class LinearUnitValue { @Override public String toString() { - return this.toString(!this.value.isExact()); + return this.toString(!this.value.isExact(), RoundingMode.HALF_EVEN); } /** @@ -315,7 +316,8 @@ public final class LinearUnitValue { * * @since 2020-07-26 */ - public String toString(final boolean showUncertainty) { + public String toString(final boolean showUncertainty, + RoundingMode roundingMode) { final Optional<String> primaryName = this.unit.getPrimaryName(); final Optional<String> symbol = this.unit.getSymbol(); final String chosenName = symbol.orElse(primaryName.orElse(null)); @@ -325,10 +327,10 @@ public final class LinearUnitValue { // get rounded strings // if showUncertainty is true, add brackets around the string final String valueString = (showUncertainty ? "(" : "") - + this.value.toString(showUncertainty) + + this.value.toString(showUncertainty, roundingMode) + (showUncertainty ? ")" : ""); final String baseValueString = (showUncertainty ? "(" : "") - + baseValue.toString(showUncertainty) + + baseValue.toString(showUncertainty, roundingMode) + (showUncertainty ? ")" : ""); // create string diff --git a/src/main/java/sevenUnits/unit/Metric.java b/src/main/java/sevenUnits/unit/Metric.java index 3c4d291..05e82ba 100644 --- a/src/main/java/sevenUnits/unit/Metric.java +++ b/src/main/java/sevenUnits/unit/Metric.java @@ -18,6 +18,7 @@ package sevenUnits.unit; import java.util.Set; +import sevenUnits.utils.NameSymbol; import sevenUnits.utils.ObjectProduct; /** @@ -114,23 +115,30 @@ public final class Metric { public static final ObjectProduct<BaseDimension> EMPTY = ObjectProduct .empty(); public static final ObjectProduct<BaseDimension> LENGTH = ObjectProduct - .oneOf(BaseDimensions.LENGTH); + .oneOf(BaseDimensions.LENGTH) + .withName(NameSymbol.of("Length", "L")); public static final ObjectProduct<BaseDimension> MASS = ObjectProduct - .oneOf(BaseDimensions.MASS); + .oneOf(BaseDimensions.MASS).withName(NameSymbol.of("Mass", "M")); public static final ObjectProduct<BaseDimension> TIME = ObjectProduct - .oneOf(BaseDimensions.TIME); + .oneOf(BaseDimensions.TIME).withName(NameSymbol.of("Time", "T")); public static final ObjectProduct<BaseDimension> ELECTRIC_CURRENT = ObjectProduct - .oneOf(BaseDimensions.ELECTRIC_CURRENT); + .oneOf(BaseDimensions.ELECTRIC_CURRENT) + .withName(NameSymbol.of("Current", "I")); public static final ObjectProduct<BaseDimension> TEMPERATURE = ObjectProduct - .oneOf(BaseDimensions.TEMPERATURE); + .oneOf(BaseDimensions.TEMPERATURE) + .withName(NameSymbol.of("Temperature", "\u0398")); public static final ObjectProduct<BaseDimension> QUANTITY = ObjectProduct - .oneOf(BaseDimensions.QUANTITY); + .oneOf(BaseDimensions.QUANTITY) + .withName(NameSymbol.of("Quantity", "N")); public static final ObjectProduct<BaseDimension> LUMINOUS_INTENSITY = ObjectProduct - .oneOf(BaseDimensions.LUMINOUS_INTENSITY); + .oneOf(BaseDimensions.LUMINOUS_INTENSITY) + .withName(NameSymbol.of("Luminous Intensity", "J")); public static final ObjectProduct<BaseDimension> INFORMATION = ObjectProduct - .oneOf(BaseDimensions.INFORMATION); + .oneOf(BaseDimensions.INFORMATION) + .withName(NameSymbol.ofName("Information")); public static final ObjectProduct<BaseDimension> CURRENCY = ObjectProduct - .oneOf(BaseDimensions.CURRENCY); + .oneOf(BaseDimensions.CURRENCY) + .withName(NameSymbol.ofName("Currency")); // derived dimensions without named SI units public static final ObjectProduct<BaseDimension> AREA = LENGTH @@ -138,7 +146,7 @@ public final class Metric { public static final ObjectProduct<BaseDimension> VOLUME = AREA .times(LENGTH); public static final ObjectProduct<BaseDimension> VELOCITY = LENGTH - .dividedBy(TIME); + .dividedBy(TIME).withName(NameSymbol.ofName("Velocity")); public static final ObjectProduct<BaseDimension> ACCELERATION = VELOCITY .dividedBy(TIME); public static final ObjectProduct<BaseDimension> WAVENUMBER = EMPTY @@ -402,54 +410,89 @@ public final class Metric { .withName(NameSymbol.of("exbi", "Ei")); // a few prefixed units - public static final LinearUnit MICROMETRE = Metric.METRE.withPrefix(Metric.MICRO); - public static final LinearUnit MILLIMETRE = Metric.METRE.withPrefix(Metric.MILLI); - public static final LinearUnit KILOMETRE = Metric.METRE.withPrefix(Metric.KILO); - public static final LinearUnit MEGAMETRE = Metric.METRE.withPrefix(Metric.MEGA); + public static final LinearUnit MICROMETRE = Metric.METRE + .withPrefix(Metric.MICRO); + public static final LinearUnit MILLIMETRE = Metric.METRE + .withPrefix(Metric.MILLI); + public static final LinearUnit KILOMETRE = Metric.METRE + .withPrefix(Metric.KILO); + public static final LinearUnit MEGAMETRE = Metric.METRE + .withPrefix(Metric.MEGA); - public static final LinearUnit MICROLITRE = Metric.LITRE.withPrefix(Metric.MICRO); - public static final LinearUnit MILLILITRE = Metric.LITRE.withPrefix(Metric.MILLI); - public static final LinearUnit KILOLITRE = Metric.LITRE.withPrefix(Metric.KILO); - public static final LinearUnit MEGALITRE = Metric.LITRE.withPrefix(Metric.MEGA); + public static final LinearUnit MICROLITRE = Metric.LITRE + .withPrefix(Metric.MICRO); + public static final LinearUnit MILLILITRE = Metric.LITRE + .withPrefix(Metric.MILLI); + public static final LinearUnit KILOLITRE = Metric.LITRE + .withPrefix(Metric.KILO); + public static final LinearUnit MEGALITRE = Metric.LITRE + .withPrefix(Metric.MEGA); - public static final LinearUnit MICROSECOND = Metric.SECOND.withPrefix(Metric.MICRO); - public static final LinearUnit MILLISECOND = Metric.SECOND.withPrefix(Metric.MILLI); - public static final LinearUnit KILOSECOND = Metric.SECOND.withPrefix(Metric.KILO); - public static final LinearUnit MEGASECOND = Metric.SECOND.withPrefix(Metric.MEGA); + public static final LinearUnit MICROSECOND = Metric.SECOND + .withPrefix(Metric.MICRO); + public static final LinearUnit MILLISECOND = Metric.SECOND + .withPrefix(Metric.MILLI); + public static final LinearUnit KILOSECOND = Metric.SECOND + .withPrefix(Metric.KILO); + public static final LinearUnit MEGASECOND = Metric.SECOND + .withPrefix(Metric.MEGA); - public static final LinearUnit MICROGRAM = Metric.GRAM.withPrefix(Metric.MICRO); - public static final LinearUnit MILLIGRAM = Metric.GRAM.withPrefix(Metric.MILLI); - public static final LinearUnit MEGAGRAM = Metric.GRAM.withPrefix(Metric.MEGA); + public static final LinearUnit MICROGRAM = Metric.GRAM + .withPrefix(Metric.MICRO); + public static final LinearUnit MILLIGRAM = Metric.GRAM + .withPrefix(Metric.MILLI); + public static final LinearUnit MEGAGRAM = Metric.GRAM + .withPrefix(Metric.MEGA); - public static final LinearUnit MICRONEWTON = Metric.NEWTON.withPrefix(Metric.MICRO); - public static final LinearUnit MILLINEWTON = Metric.NEWTON.withPrefix(Metric.MILLI); - public static final LinearUnit KILONEWTON = Metric.NEWTON.withPrefix(Metric.KILO); - public static final LinearUnit MEGANEWTON = Metric.NEWTON.withPrefix(Metric.MEGA); + public static final LinearUnit MICRONEWTON = Metric.NEWTON + .withPrefix(Metric.MICRO); + public static final LinearUnit MILLINEWTON = Metric.NEWTON + .withPrefix(Metric.MILLI); + public static final LinearUnit KILONEWTON = Metric.NEWTON + .withPrefix(Metric.KILO); + public static final LinearUnit MEGANEWTON = Metric.NEWTON + .withPrefix(Metric.MEGA); - public static final LinearUnit MICROJOULE = Metric.JOULE.withPrefix(Metric.MICRO); - public static final LinearUnit MILLIJOULE = Metric.JOULE.withPrefix(Metric.MILLI); - public static final LinearUnit KILOJOULE = Metric.JOULE.withPrefix(Metric.KILO); - public static final LinearUnit MEGAJOULE = Metric.JOULE.withPrefix(Metric.MEGA); + public static final LinearUnit MICROJOULE = Metric.JOULE + .withPrefix(Metric.MICRO); + public static final LinearUnit MILLIJOULE = Metric.JOULE + .withPrefix(Metric.MILLI); + public static final LinearUnit KILOJOULE = Metric.JOULE + .withPrefix(Metric.KILO); + public static final LinearUnit MEGAJOULE = Metric.JOULE + .withPrefix(Metric.MEGA); - public static final LinearUnit MICROWATT = Metric.WATT.withPrefix(Metric.MICRO); - public static final LinearUnit MILLIWATT = Metric.WATT.withPrefix(Metric.MILLI); - public static final LinearUnit KILOWATT = Metric.WATT.withPrefix(Metric.KILO); - public static final LinearUnit MEGAWATT = Metric.WATT.withPrefix(Metric.MEGA); + public static final LinearUnit MICROWATT = Metric.WATT + .withPrefix(Metric.MICRO); + public static final LinearUnit MILLIWATT = Metric.WATT + .withPrefix(Metric.MILLI); + public static final LinearUnit KILOWATT = Metric.WATT + .withPrefix(Metric.KILO); + public static final LinearUnit MEGAWATT = Metric.WATT + .withPrefix(Metric.MEGA); public static final LinearUnit MICROCOULOMB = Metric.COULOMB .withPrefix(Metric.MICRO); public static final LinearUnit MILLICOULOMB = Metric.COULOMB .withPrefix(Metric.MILLI); - public static final LinearUnit KILOCOULOMB = Metric.COULOMB.withPrefix(Metric.KILO); - public static final LinearUnit MEGACOULOMB = Metric.COULOMB.withPrefix(Metric.MEGA); + public static final LinearUnit KILOCOULOMB = Metric.COULOMB + .withPrefix(Metric.KILO); + public static final LinearUnit MEGACOULOMB = Metric.COULOMB + .withPrefix(Metric.MEGA); - public static final LinearUnit MICROAMPERE = Metric.AMPERE.withPrefix(Metric.MICRO); - public static final LinearUnit MILLIAMPERE = Metric.AMPERE.withPrefix(Metric.MILLI); + public static final LinearUnit MICROAMPERE = Metric.AMPERE + .withPrefix(Metric.MICRO); + public static final LinearUnit MILLIAMPERE = Metric.AMPERE + .withPrefix(Metric.MILLI); - public static final LinearUnit MICROVOLT = Metric.VOLT.withPrefix(Metric.MICRO); - public static final LinearUnit MILLIVOLT = Metric.VOLT.withPrefix(Metric.MILLI); - public static final LinearUnit KILOVOLT = Metric.VOLT.withPrefix(Metric.KILO); - public static final LinearUnit MEGAVOLT = Metric.VOLT.withPrefix(Metric.MEGA); + public static final LinearUnit MICROVOLT = Metric.VOLT + .withPrefix(Metric.MICRO); + public static final LinearUnit MILLIVOLT = Metric.VOLT + .withPrefix(Metric.MILLI); + public static final LinearUnit KILOVOLT = Metric.VOLT + .withPrefix(Metric.KILO); + public static final LinearUnit MEGAVOLT = Metric.VOLT + .withPrefix(Metric.MEGA); public static final LinearUnit KILOOHM = Metric.OHM.withPrefix(Metric.KILO); public static final LinearUnit MEGAOHM = Metric.OHM.withPrefix(Metric.MEGA); diff --git a/src/main/java/sevenUnits/unit/MultiUnit.java b/src/main/java/sevenUnits/unit/MultiUnit.java index 83cdb03..bc240e3 100644 --- a/src/main/java/sevenUnits/unit/MultiUnit.java +++ b/src/main/java/sevenUnits/unit/MultiUnit.java @@ -20,6 +20,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import sevenUnits.utils.NameSymbol; import sevenUnits.utils.ObjectProduct; /** diff --git a/src/main/java/sevenUnits/unit/Unit.java b/src/main/java/sevenUnits/unit/Unit.java index 005b6f7..14478ba 100644 --- a/src/main/java/sevenUnits/unit/Unit.java +++ b/src/main/java/sevenUnits/unit/Unit.java @@ -22,6 +22,8 @@ import java.util.Objects; import java.util.function.DoubleUnaryOperator; import sevenUnits.utils.DecimalComparison; +import sevenUnits.utils.NameSymbol; +import sevenUnits.utils.Nameable; import sevenUnits.utils.ObjectProduct; /** @@ -188,7 +190,7 @@ public abstract class Unit implements Nameable { * * @implSpec This method is used by {@link #convertTo}, and its behaviour * affects the behaviour of {@code convertTo}. - * + * * @param value value expressed in <b>base</b> unit * @return value expressed in <b>this</b> unit * @since 2018-12-22 @@ -204,7 +206,7 @@ public abstract class Unit implements Nameable { * {@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 @@ -231,7 +233,7 @@ public abstract class Unit implements Nameable { * {@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 <W> type of value to convert to @@ -266,7 +268,7 @@ public abstract class Unit implements Nameable { * * @implSpec This method is used by {@link #convertTo}, and its behaviour * affects the behaviour of {@code convertTo}. - * + * * @param value value expressed in <b>this</b> unit * @return value expressed in <b>base</b> unit * @since 2018-12-22 @@ -347,16 +349,34 @@ public abstract class Unit implements Nameable { .equals(Math.log10(linear.getConversionFactor()) % 1.0, 0); } + /** + * @return a string representing this unit's definition + * @since 2022-03-10 + */ + public String toDefinitionString() { + if (!this.unitBase.getNameSymbol().isEmpty()) + return "derived from " + this.unitBase.getName(); + else + return "derived from " + + this.getBase().toString(BaseUnit::getShortName); + } + + /** + * @return a string containing both this unit's name and its definition + * @since 2022-03-10 + */ + public final String toFullString() { + return this.toString() + " (" + this.toDefinitionString() + ")"; + } + @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())); + if (this.nameSymbol.getPrimaryName().isPresent() + && this.nameSymbol.getSymbol().isPresent()) + return this.nameSymbol.getPrimaryName().orElseThrow() + " (" + + this.nameSymbol.getSymbol().orElseThrow() + ")"; + else + return this.getName(); } /** diff --git a/src/main/java/sevenUnits/unit/UnitDatabase.java b/src/main/java/sevenUnits/unit/UnitDatabase.java index 18ac619..12b78a7 100644 --- a/src/main/java/sevenUnits/unit/UnitDatabase.java +++ b/src/main/java/sevenUnits/unit/UnitDatabase.java @@ -19,7 +19,6 @@ package sevenUnits.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; import java.util.AbstractSet; @@ -47,6 +46,7 @@ import java.util.regex.Pattern; import sevenUnits.utils.ConditionalExistenceCollections; import sevenUnits.utils.DecimalComparison; import sevenUnits.utils.ExpressionParser; +import sevenUnits.utils.NameSymbol; import sevenUnits.utils.ObjectProduct; import sevenUnits.utils.UncertainDouble; @@ -1160,16 +1160,16 @@ public final class UnitDatabase { } /** - * @return true if entry represents a removable duplicate entry of unitMap. + * @return true if entry represents a removable duplicate entry of map. * @since 2021-05-22 */ - static boolean isRemovableDuplicate(Map<String, Unit> unitMap, - Entry<String, Unit> entry) { - for (final Entry<String, Unit> e : unitMap.entrySet()) { + static <T> boolean isRemovableDuplicate(Map<String, T> map, + Entry<String, T> entry) { + for (final Entry<String, T> e : map.entrySet()) { final String name = e.getKey(); - final Unit value = e.getValue(); + final T value = e.getValue(); if (lengthFirstComparator.compare(entry.getKey(), name) < 0 - && Objects.equals(unitMap.get(entry.getKey()), value)) + && Objects.equals(map.get(entry.getKey()), value)) return true; } return false; @@ -1313,9 +1313,11 @@ public final class UnitDatabase { */ public void addDimension(final String name, final ObjectProduct<BaseDimension> dimension) { - this.dimensions.put( - Objects.requireNonNull(name, "name must not be null."), - Objects.requireNonNull(dimension, "dimension must not be null.")); + Objects.requireNonNull(name, "name may not be null"); + Objects.requireNonNull(dimension, "dimension may not be null"); + final ObjectProduct<BaseDimension> namedDimension = dimension + .withName(dimension.getNameSymbol().withExtraName(name)); + this.dimensions.put(name, namedDimension); } /** @@ -1381,8 +1383,11 @@ public final class UnitDatabase { * @since v0.1.0 */ public void addPrefix(final String name, final UnitPrefix prefix) { + Objects.requireNonNull(prefix, "prefix may not be null"); + final var namedPrefix = prefix + .withName(prefix.getNameSymbol().withExtraName(name)); this.prefixes.put(Objects.requireNonNull(name, "name must not be null."), - Objects.requireNonNull(prefix, "prefix must not be null.")); + namedPrefix); } /** @@ -1395,9 +1400,11 @@ public final class UnitDatabase { * @since v0.1.0 */ public void addUnit(final String name, final Unit unit) { + Objects.requireNonNull(unit, "unit may not be null"); + final var namedUnit = unit + .withName(unit.getNameSymbol().withExtraName(name)); this.prefixlessUnits.put( - Objects.requireNonNull(name, "name must not be null."), - Objects.requireNonNull(unit, "unit must not be null.")); + Objects.requireNonNull(name, "name must not be null."), namedUnit); } /** @@ -1451,7 +1458,8 @@ public final class UnitDatabase { System.err.printf("Parsing error on line %d:%n", lineCounter); throw e; } - this.addPrefix(name.substring(0, name.length() - 1), prefix); + final String prefixName = name.substring(0, name.length() - 1); + this.addPrefix(prefixName, prefix); } else { // it's a unit, get the unit final Unit unit; @@ -1462,13 +1470,23 @@ public final class UnitDatabase { System.err.printf("Parsing error on line %d:%n", lineCounter); throw e; } - this.addUnit(name, unit); } } } /** + * Removes all units, prefixes and dimensions from this database. + * + * @since 2022-02-26 + */ + public void clear() { + this.dimensions.clear(); + this.prefixes.clear(); + this.prefixlessUnits.clear(); + } + + /** * Tests if the database has a unit dimension with this name. * * @param name name to test @@ -1685,11 +1703,8 @@ public final class UnitDatabase { 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(Metric.ONE, - UncertainDouble.of(number.doubleValue(), uncertainty)); + UncertainDouble.fromRoundedString(name)); } catch (final NumberFormatException e) { return LinearUnitValue.getExact(this.getLinearUnit(name), 1); } @@ -1994,12 +2009,18 @@ public final class UnitDatabase { } /** + * @param includeDuplicates if false, duplicates are removed from the map * @return a map mapping prefix names to prefixes - * @since 2019-04-13 - * @since v0.2.0 + * @since 2022-04-18 + * @since v0.4.0 */ - public Map<String, UnitPrefix> prefixMap() { - return Collections.unmodifiableMap(this.prefixes); + public Map<String, UnitPrefix> prefixMap(boolean includeDuplicates) { + if (includeDuplicates) + return Collections.unmodifiableMap(this.prefixes); + else + return Collections.unmodifiableMap(ConditionalExistenceCollections + .conditionalExistenceMap(this.prefixes, + entry -> !isRemovableDuplicate(this.prefixes, entry))); } /** diff --git a/src/main/java/sevenUnits/unit/UnitPrefix.java b/src/main/java/sevenUnits/unit/UnitPrefix.java index 308f4b0..e1f7788 100644 --- a/src/main/java/sevenUnits/unit/UnitPrefix.java +++ b/src/main/java/sevenUnits/unit/UnitPrefix.java @@ -17,68 +17,59 @@ package sevenUnits.unit; import java.util.Objects; -import java.util.Optional; -import java.util.Set; import sevenUnits.utils.DecimalComparison; +import sevenUnits.utils.NameSymbol; +import sevenUnits.utils.Nameable; /** - * A prefix that can be applied to a {@code LinearUnit} to multiply it by some value + * 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 { +public final class UnitPrefix implements Nameable { /** * Gets a {@code UnitPrefix} from a multiplier * - * @param multiplier - * multiplier of prefix + * @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 + * @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 + * @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.")); + 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<String> primaryName; - - /** - * This prefix's symbol - */ - private final Optional<String> symbol; - + /** - * Other names and symbols used by this prefix + * This prefix's name(s) and symbol. + * + * @since 2022-04-16 */ - private final Set<String> otherNames; - + private final NameSymbol nameSymbol; + /** * The number that this prefix multiplies units by * * @since 2019-10-16 */ private final double multiplier; - + /** * Creates the {@code DefaultUnitPrefix}. * @@ -88,28 +79,24 @@ public final class UnitPrefix { */ private UnitPrefix(final double multiplier, final NameSymbol ns) { this.multiplier = multiplier; - this.primaryName = ns.getPrimaryName(); - this.symbol = ns.getSymbol(); - this.otherNames = ns.getOtherNames(); + this.nameSymbol = ns; } - + /** * Divides this prefix by a scalar * - * @param divisor - * number to divide by + * @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 + * @param other prefix to divide by * @return quotient of prefixes * @since 2019-04-13 * @since v0.2.0 @@ -117,7 +104,7 @@ public final class UnitPrefix { public UnitPrefix dividedBy(final UnitPrefix other) { return valueOf(this.getMultiplier() / other.getMultiplier()); } - + /** * {@inheritDoc} * @@ -132,9 +119,10 @@ public final class UnitPrefix { if (!(obj instanceof UnitPrefix)) return false; final UnitPrefix other = (UnitPrefix) obj; - return DecimalComparison.equals(this.getMultiplier(), other.getMultiplier()); + return DecimalComparison.equals(this.getMultiplier(), + other.getMultiplier()); } - + /** * @return prefix's multiplier * @since 2019-11-26 @@ -142,31 +130,12 @@ public final class UnitPrefix { public double getMultiplier() { return this.multiplier; } - - /** - * @return other names - * @since 2019-11-26 - */ - public final Set<String> getOtherNames() { - return this.otherNames; - } - - /** - * @return primary name - * @since 2019-11-26 - */ - public final Optional<String> getPrimaryName() { - return this.primaryName; - } - - /** - * @return symbol - * @since 2019-11-26 - */ - public final Optional<String> getSymbol() { - return this.symbol; + + @Override + public NameSymbol getNameSymbol() { + return this.nameSymbol; } - + /** * {@inheritDoc} * @@ -176,24 +145,22 @@ public final class UnitPrefix { public int hashCode() { return DecimalComparison.hash(this.getMultiplier()); } - + /** * Multiplies this prefix by a scalar * - * @param multiplicand - * number to multiply by + * @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 + * @param other prefix to multiply by * @return product of prefixes * @since 2019-04-13 * @since v0.2.0 @@ -201,12 +168,11 @@ public final class UnitPrefix { public UnitPrefix times(final UnitPrefix other) { return valueOf(this.getMultiplier() * other.getMultiplier()); } - + /** * Raises this prefix to an exponent. * - * @param exponent - * exponent to raise to + * @param exponent exponent to raise to * @return result of exponentiation. * @since 2019-04-13 * @since v0.2.0 @@ -214,27 +180,27 @@ public final class UnitPrefix { 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); + if (this.getPrimaryName().isPresent()) + return String.format("%s (\u00D7 %s)", this.getPrimaryName().get(), + this.multiplier); + else if (this.getSymbol().isPresent()) + return String.format("%s (\u00D7 %s)", this.getSymbol().get(), + this.multiplier); else return String.format("Unit Prefix (\u00D7 %s)", this.multiplier); } - + /** - * @param ns - * name(s) and symbol to use + * @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 + * @throws NullPointerException if ns is null */ public UnitPrefix withName(final NameSymbol ns) { return valueOf(this.multiplier, ns); diff --git a/src/main/java/sevenUnits/unit/UnitType.java b/src/main/java/sevenUnits/unit/UnitType.java new file mode 100644 index 0000000..7cebf2d --- /dev/null +++ b/src/main/java/sevenUnits/unit/UnitType.java @@ -0,0 +1,58 @@ +/** + * Copyright (C) 2022 Adrien Hopkins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ +package sevenUnits.unit; + +import java.util.function.Predicate; + +/** + * A type of unit, as chosen by the type of system it is in. + * <ul> + * <li>{@code METRIC} refers to metric/SI units that pass {@link Unit#isMetric} + * <li>{@code SEMI_METRIC} refers to the degree Celsius (which is an official SI + * unit but does not pass {@link Unit#isMetric}) and non-metric units intended + * for use with the SI. + * <li>{@code NON_METRIC} refers to units that are neither metric nor intended + * for use with the metric system (e.g. imperial and customary units) + * </ul> + * + * @since 2022-04-10 + */ +public enum UnitType { + METRIC, SEMI_METRIC, NON_METRIC; + + /** + * Determines which type a unit is. The type will be: + * <ul> + * <li>{@code SEMI_METRIC} if the unit passes the provided predicate + * <li>{@code METRIC} if it fails the predicate but is metric + * <li>{@code NON_METRIC} if it fails the predicate and is not metric + * </ul> + * + * @param u unit to test + * @param isSemiMetric predicate to determine if a unit is semi-metric + * @return type of unit + * @since 2022-04-18 + */ + public static final UnitType getType(Unit u, Predicate<Unit> isSemiMetric) { + if (isSemiMetric.test(u)) + return SEMI_METRIC; + else if (u.isMetric()) + return METRIC; + else + return NON_METRIC; + } +} diff --git a/src/main/java/sevenUnits/unit/UnitValue.java b/src/main/java/sevenUnits/unit/UnitValue.java index f6d18f8..339263d 100644 --- a/src/main/java/sevenUnits/unit/UnitValue.java +++ b/src/main/java/sevenUnits/unit/UnitValue.java @@ -19,6 +19,8 @@ package sevenUnits.unit; import java.util.Objects; import java.util.Optional; +import sevenUnits.utils.NameSymbol; + /** * A value expressed in a unit. * diff --git a/src/main/java/sevenUnits/unit/Unitlike.java b/src/main/java/sevenUnits/unit/Unitlike.java index d2dcbbb..68de2c2 100644 --- a/src/main/java/sevenUnits/unit/Unitlike.java +++ b/src/main/java/sevenUnits/unit/Unitlike.java @@ -22,6 +22,8 @@ import java.util.Objects; import java.util.function.DoubleFunction; import java.util.function.ToDoubleFunction; +import sevenUnits.utils.NameSymbol; +import sevenUnits.utils.Nameable; import sevenUnits.utils.ObjectProduct; /** diff --git a/src/main/java/sevenUnits/unit/UnitlikeValue.java b/src/main/java/sevenUnits/unit/UnitlikeValue.java index edc13ca..26354b1 100644 --- a/src/main/java/sevenUnits/unit/UnitlikeValue.java +++ b/src/main/java/sevenUnits/unit/UnitlikeValue.java @@ -18,6 +18,8 @@ package sevenUnits.unit; import java.util.Optional; +import sevenUnits.utils.NameSymbol; + /** * * @since 2020-09-07 diff --git a/src/main/java/sevenUnits/unit/NameSymbol.java b/src/main/java/sevenUnits/utils/NameSymbol.java index 3e26138..7ef2967 100644 --- a/src/main/java/sevenUnits/unit/NameSymbol.java +++ b/src/main/java/sevenUnits/utils/NameSymbol.java @@ -14,7 +14,7 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. */ -package sevenUnits.unit; +package sevenUnits.utils; import java.util.Arrays; import java.util.Collections; @@ -38,7 +38,7 @@ public final class NameSymbol { * 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. + * Ensure that otherNames is not a copy of the inputted argument. */ private static final NameSymbol create(final String name, final String symbol, final Set<String> otherNames) { @@ -277,4 +277,32 @@ public final class NameSymbol { // if primaryName is empty, otherNames must also be empty return this.primaryName.isEmpty() && this.symbol.isEmpty(); } + + @Override + public String toString() { + if (this.isEmpty()) + return "NameSymbol.EMPTY"; + else if (this.primaryName.isPresent() && this.symbol.isPresent()) + return this.primaryName.orElseThrow() + " (" + + this.symbol.orElseThrow() + ")"; + else + return this.primaryName.orElseGet(this.symbol::orElseThrow); + } + + /** + * Creates and returns a copy of this {@code NameSymbol} with the provided + * extra name. If this {@code NameSymbol} has a primary name, the provided + * name will become an other name, otherwise it will become the primary name. + * + * @since 2022-04-19 + */ + public final NameSymbol withExtraName(String name) { + if (this.primaryName.isPresent()) { + final var otherNames = new HashSet<>(this.otherNames); + otherNames.add(name); + return NameSymbol.ofNullable(this.primaryName.orElse(null), + this.symbol.orElse(null), otherNames); + } else + return NameSymbol.ofNullable(name, this.symbol.orElse(null)); + } }
\ No newline at end of file diff --git a/src/main/java/sevenUnits/unit/Nameable.java b/src/main/java/sevenUnits/utils/Nameable.java index ed23687..e469d04 100644 --- a/src/main/java/sevenUnits/unit/Nameable.java +++ b/src/main/java/sevenUnits/utils/Nameable.java @@ -14,7 +14,7 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. */ -package sevenUnits.unit; +package sevenUnits.utils; import java.util.Optional; import java.util.Set; @@ -27,6 +27,16 @@ import java.util.Set; */ public interface Nameable { /** + * @return a name for the object - if there's a primary name, it's that, + * otherwise the symbol, otherwise "Unnamed" + * @since 2022-02-26 + */ + default String getName() { + final NameSymbol ns = this.getNameSymbol(); + return ns.getPrimaryName().or(ns::getSymbol).orElse("Unnamed"); + } + + /** * @return a {@code NameSymbol} that contains this object's primary name, * symbol and other names * @since 2020-09-07 @@ -50,6 +60,16 @@ public interface Nameable { } /** + * @return a short name for the object - if there's a symbol, it's that, + * otherwise the symbol, otherwise "Unnamed" + * @since 2022-02-26 + */ + default String getShortName() { + final NameSymbol ns = this.getNameSymbol(); + return ns.getSymbol().or(ns::getPrimaryName).orElse("Unnamed"); + } + + /** * @return short symbol representing object * @since 2020-09-07 */ diff --git a/src/main/java/sevenUnits/utils/ObjectProduct.java b/src/main/java/sevenUnits/utils/ObjectProduct.java index 5b1b739..66bb773 100644 --- a/src/main/java/sevenUnits/utils/ObjectProduct.java +++ b/src/main/java/sevenUnits/utils/ObjectProduct.java @@ -33,7 +33,7 @@ import java.util.function.Function; * @author Adrien Hopkins * @since 2019-10-16 */ -public final class ObjectProduct<T> { +public class ObjectProduct<T> implements Nameable { /** * Returns an empty ObjectProduct of a certain type * @@ -83,15 +83,32 @@ public final class ObjectProduct<T> { final Map<T, Integer> exponents; /** - * Creates the {@code ObjectProduct}. + * The object's name and symbol + */ + private final NameSymbol nameSymbol; + + /** + * Creates a {@code ObjectProduct} without a name/symbol. * * @param exponents objects that make up this product * @since 2019-10-16 */ - private ObjectProduct(final Map<T, Integer> exponents) { + ObjectProduct(final Map<T, Integer> exponents) { + this(exponents, NameSymbol.EMPTY); + } + + /** + * Creates the {@code ObjectProduct}. + * + * @param exponents objects that make up this product + * @param nameSymbol name and symbol of object product + * @since 2019-10-16 + */ + ObjectProduct(final Map<T, Integer> exponents, NameSymbol nameSymbol) { this.exponents = Collections.unmodifiableMap( ConditionalExistenceCollections.conditionalExistenceMap(exponents, e -> !Integer.valueOf(0).equals(e.getValue()))); + this.nameSymbol = nameSymbol; } /** @@ -171,6 +188,11 @@ public final class ObjectProduct<T> { } @Override + public NameSymbol getNameSymbol() { + return this.nameSymbol; + } + + @Override public int hashCode() { return Objects.hash(this.exponents); } @@ -235,16 +257,19 @@ public final class ObjectProduct<T> { /** * 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. + * {@link Object#toString()} method (or {@link Nameable#getShortName} if + * available). If objects have a long toString representation, it is + * recommended to use {@link #toString(Function)} instead to shorten the + * returned string. * * <p> * {@inheritDoc} */ @Override public String toString() { - return this.toString(Object::toString); + return this + .toString(o -> o instanceof Nameable ? ((Nameable) o).getShortName() + : o.toString()); } /** @@ -280,4 +305,13 @@ public final class ObjectProduct<T> { return positiveString + negativeString; } + + /** + * @return named version of this {@code ObjectProduct}, using data from + * {@code nameSymbol} + * @since 2021-12-15 + */ + public ObjectProduct<T> withName(NameSymbol nameSymbol) { + return new ObjectProduct<>(this.exponents, nameSymbol); + } } diff --git a/src/main/java/sevenUnits/utils/SemanticVersionNumber.java b/src/main/java/sevenUnits/utils/SemanticVersionNumber.java new file mode 100644 index 0000000..06417c5 --- /dev/null +++ b/src/main/java/sevenUnits/utils/SemanticVersionNumber.java @@ -0,0 +1,691 @@ +/** + * Copyright (C) 2022 Adrien Hopkins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ +package sevenUnits.utils; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * A version number in the <a href="https://semver.org">Semantic Versioning</a> + * scheme + * <p> + * Each version number has three main parts: + * <ol> + * <li>The major version, which increments when backwards incompatible changes + * are made + * <li>The minor version, which increments when backwards compatible feature + * changes are made + * <li>The patch version, which increments when backwards compatible bug fixes + * are made + * </ol> + * + * @since 2022-02-19 + */ +public final class SemanticVersionNumber + implements Comparable<SemanticVersionNumber> { + /** + * A builder that can be used to create complex version numbers. + * <p> + * Note: None of this builder's methods tolerate null arguments, arrays + * containing nulls, negative numbers, or non-alphanumeric identifiers. Nulls + * throw NullPointerExceptions, everything else throws + * IllegalArgumentException. + * + * @since 2022-02-19 + */ + public static final class Builder { + private final int major; + private final int minor; + private final int patch; + private final List<String> preReleaseIdentifiers; + private final List<String> buildMetadata; + + /** + * Creates a builder which can be used to create a + * {@code SemanticVersionNumber} + * + * @param major major version number of final version + * @param minor minor version number of final version + * @param patch patch version number of final version + * @since 2022-02-19 + */ + private Builder(int major, int minor, int patch) { + this.major = major; + this.minor = minor; + this.patch = patch; + this.preReleaseIdentifiers = new ArrayList<>(); + this.buildMetadata = new ArrayList<>(); + } + + /** + * @return version number created by this builder + * @since 2022-02-19 + */ + public SemanticVersionNumber build() { + return new SemanticVersionNumber(this.major, this.minor, this.patch, + this.preReleaseIdentifiers, this.buildMetadata); + } + + /** + * Adds one or more build metadata identifiers + * + * @param identifiers build metadata + * @return this builder + * @since 2022-02-19 + */ + public Builder buildMetadata(List<String> identifiers) { + Objects.requireNonNull(identifiers, "identifiers may not be null"); + for (final String identifier : identifiers) { + Objects.requireNonNull(identifier, "identifier may not be null"); + if (!VALID_IDENTIFIER.matcher(identifier).matches()) + throw new IllegalArgumentException( + String.format("Invalid identifier \"%s\"", identifier)); + this.buildMetadata.add(identifier); + } + return this; + } + + /** + * Adds one or more build metadata identifiers + * + * @param identifiers build metadata + * @return this builder + * @since 2022-02-19 + */ + public Builder buildMetadata(String... identifiers) { + Objects.requireNonNull(identifiers, "identifiers may not be null"); + for (final String identifier : identifiers) { + Objects.requireNonNull(identifier, "identifier may not be null"); + if (!VALID_IDENTIFIER.matcher(identifier).matches()) + throw new IllegalArgumentException( + String.format("Invalid identifier \"%s\"", identifier)); + this.buildMetadata.add(identifier); + } + return this; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (!(obj instanceof Builder)) + return false; + final Builder other = (Builder) obj; + return Objects.equals(this.buildMetadata, other.buildMetadata) + && this.major == other.major && this.minor == other.minor + && this.patch == other.patch && Objects.equals( + this.preReleaseIdentifiers, other.preReleaseIdentifiers); + } + + @Override + public int hashCode() { + return Objects.hash(this.buildMetadata, this.major, this.minor, + this.patch, this.preReleaseIdentifiers); + } + + /** + * Adds one or more numeric identifiers to the version number + * + * @param identifiers pre-release identifier(s) to add + * @return this builder + * @since 2022-02-19 + */ + public Builder preRelease(int... identifiers) { + Objects.requireNonNull(identifiers, "identifiers may not be null"); + for (final int identifier : identifiers) { + if (identifier < 0) + throw new IllegalArgumentException( + "Numeric identifiers may not be negative"); + this.preReleaseIdentifiers.add(Integer.toString(identifier)); + } + return this; + } + + /** + * Adds one or more pre-release identifier(s) to the version number + * + * @param identifiers pre-release identifier(s) to add + * @return this builder + * @since 2022-02-19 + */ + public Builder preRelease(List<String> identifiers) { + Objects.requireNonNull(identifiers, "identifiers may not be null"); + for (final String identifier : identifiers) { + Objects.requireNonNull(identifier, "identifier may not be null"); + if (!VALID_IDENTIFIER.matcher(identifier).matches()) + throw new IllegalArgumentException( + String.format("Invalid identifier \"%s\"", identifier)); + this.preReleaseIdentifiers.add(identifier); + } + return this; + } + + /** + * Adds one or more pre-release identifier(s) to the version number + * + * @param identifiers pre-release identifier(s) to add + * @return this builder + * @since 2022-02-19 + */ + public Builder preRelease(String... identifiers) { + Objects.requireNonNull(identifiers, "identifiers may not be null"); + for (final String identifier : identifiers) { + Objects.requireNonNull(identifier, "identifier may not be null"); + if (!VALID_IDENTIFIER.matcher(identifier).matches()) + throw new IllegalArgumentException( + String.format("Invalid identifier \"%s\"", identifier)); + this.preReleaseIdentifiers.add(identifier); + } + return this; + } + + /** + * Adds a string identifier and an integer identifer to pre-release data + * + * @param identifier1 first identifier + * @param identifier2 second identifier + * @return this builder + * @since 2022-02-19 + */ + public Builder preRelease(String identifier1, int identifier2) { + Objects.requireNonNull(identifier1, "identifier1 may not be null"); + if (!VALID_IDENTIFIER.matcher(identifier1).matches()) + throw new IllegalArgumentException( + String.format("Invalid identifier \"%s\"", identifier1)); + if (identifier2 < 0) + throw new IllegalArgumentException( + "Integer identifier cannot be negative"); + this.preReleaseIdentifiers.add(identifier1); + this.preReleaseIdentifiers.add(Integer.toString(identifier2)); + return this; + } + + @Override + public String toString() { + return "Semantic Version Builder: " + this.build().toString(); + } + } + + /** + * An alternative comparison method for version numbers. This uses the + * version's natural order, but the build metadata will be compared (using + * the same rules as pre-release identifiers) if everything else is equal. + * <p> + * This ordering is consistent with equals, unlike + * {@code SemanticVersionNumber}'s natural ordering. + */ + public static final Comparator<SemanticVersionNumber> BUILD_METADATA_COMPARATOR = new Comparator<>() { + @Override + public int compare(SemanticVersionNumber o1, SemanticVersionNumber o2) { + Objects.requireNonNull(o1, "o1 may not be null"); + Objects.requireNonNull(o2, "o2 may not be null"); + final int naturalComparison = o1.compareTo(o2); + if (naturalComparison == 0) + return SemanticVersionNumber.compareIdentifiers(o1.buildMetadata, + o2.buildMetadata); + else + return naturalComparison; + }; + }; + + /** The alphanumeric pattern all identifiers must follow */ + private static final Pattern VALID_IDENTIFIER = Pattern + .compile("[0-9A-Za-z-]+"); + + /** The numeric pattern which causes special behaviour */ + private static final Pattern NUMERIC_IDENTIFER = Pattern.compile("[0-9]+"); + + /** The pattern for a version number */ + private static final Pattern VERSION_NUMBER = Pattern + .compile("(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)" // main + // version + + "(?:-([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?" // pre-release + + "(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?"); // build data + + /** + * Creates a builder that can be used to create a version number + * + * @param major major version number of final version + * @param minor minor version number of final version + * @param patch patch version number of final version + * @return version number builder + * @throws IllegalArgumentException if any argument is negative + * @since 2022-02-19 + */ + public static final SemanticVersionNumber.Builder builder(int major, + int minor, int patch) { + if (major < 0) + throw new IllegalArgumentException( + "Major version must be non-negative."); + if (minor < 0) + throw new IllegalArgumentException( + "Minor version must be non-negative."); + if (patch < 0) + throw new IllegalArgumentException( + "Patch version must be non-negative."); + return new SemanticVersionNumber.Builder(major, minor, patch); + } + + /** + * Compares two lists of strings based on SemVer's precedence rules + * + * @param a first list + * @param b second list + * @return result of comparison as in a comparator + * @see Comparator + * @since 2022-02-20 + */ + private static final int compareIdentifiers(List<String> a, List<String> b) { + // test pre-release size + final int aSize = a.size(); + final int bSize = b.size(); + + // no identifiers is greater than any identifiers + if (aSize != 0 && bSize == 0) + return -1; + else if (aSize == 0 && bSize != 0) + return 1; + + // test identifiers one by one + for (int i = 0; i < Math.min(aSize, bSize); i++) { + final String aElement = a.get(i); + final String bElement = b.get(i); + + if (NUMERIC_IDENTIFER.matcher(aElement).matches()) { + if (NUMERIC_IDENTIFER.matcher(bElement).matches()) { + // both are numbers, compare them + final int aNumber = Integer.parseInt(aElement); + final int bNumber = Integer.parseInt(bElement); + + if (aNumber < bNumber) + return -1; + else if (aNumber > bNumber) + return 1; + } else + // aElement is a number and bElement is not a number + // by the rules, a goes before b + return -1; + } else { + if (NUMERIC_IDENTIFER.matcher(bElement).matches()) + // aElement is not a number but bElement is + // by the rules, a goes after b + return 1; + else { + // both are not numbers, compare them + final int comparison = aElement.compareTo(bElement); + if (comparison != 0) + return comparison; + } + } + } + + // we just tested the stuff that's in common, maybe someone has more + if (aSize < bSize) + return -1; + else if (aSize > bSize) + return 1; + else + return 0; + } + + /** + * Gets a version number from a string in the official format + * + * @param versionString string to parse + * @return {@code SemanticVersionNumber} instance + * @since 2022-02-19 + * @see {@link #toString} + */ + public static final SemanticVersionNumber fromString(String versionString) { + // parse & validate version string + Objects.requireNonNull(versionString, "versionString may not be null"); + final Matcher m = VERSION_NUMBER.matcher(versionString); + if (!m.matches()) + throw new IllegalArgumentException( + String.format("Provided string \"%s\" is not a version number", + versionString)); + + // main parts + final int major = Integer.parseInt(m.group(1)); + final int minor = Integer.parseInt(m.group(2)); + final int patch = Integer.parseInt(m.group(3)); + + // pre release + final List<String> preRelease; + if (m.group(4) == null) { + preRelease = List.of(); + } else { + preRelease = Arrays.asList(m.group(4).split("\\.")); + } + + // build metadata + final List<String> buildMetadata; + if (m.group(5) == null) { + buildMetadata = List.of(); + } else { + buildMetadata = Arrays.asList(m.group(5).split("\\.")); + } + + // return number + return new SemanticVersionNumber(major, minor, patch, preRelease, + buildMetadata); + } + + /** + * Tests whether a string is a valid Semantic Version string + * + * @param versionString string to test + * @return true iff string is valid + * @since 2022-02-19 + */ + public static final boolean isValidVersionString(String versionString) { + return VERSION_NUMBER.matcher(versionString).matches(); + } + + /** + * Creates a simple pre-release version number of the form + * MAJOR.MINOR.PATH-TYPE.NUMBER (e.g. 1.2.3-alpha.4). + * + * @param major major version number + * @param minor minor version number + * @param patch patch version number + * @param preReleaseType first pre-release element + * @param preReleaseNumber second pre-release element + * @return {@code SemanticVersionNumber} instance + * @throws IllegalArgumentException if any argument is negative or if the + * preReleaseType is null, empty or not + * alphanumeric (0-9, A-Z, a-z, - only) + * @since 2022-02-19 + */ + public static final SemanticVersionNumber preRelease(int major, int minor, + int patch, String preReleaseType, int preReleaseNumber) { + if (major < 0) + throw new IllegalArgumentException( + "Major version must be non-negative."); + if (minor < 0) + throw new IllegalArgumentException( + "Minor version must be non-negative."); + if (patch < 0) + throw new IllegalArgumentException( + "Patch version must be non-negative."); + Objects.requireNonNull(preReleaseType, "preReleaseType may not be null"); + if (!VALID_IDENTIFIER.matcher(preReleaseType).matches()) + throw new IllegalArgumentException( + String.format("Invalid identifier \"%s\".", preReleaseType)); + if (preReleaseNumber < 0) + throw new IllegalArgumentException( + "Pre-release number must be non-negative."); + return new SemanticVersionNumber(major, minor, patch, + List.of(preReleaseType, Integer.toString(preReleaseNumber)), + List.of()); + } + + /** + * Creates a {@code SemanticVersionNumber} instance without pre-release + * identifiers or build metadata. + * <p> + * Note: this method allows you to create versions with major version number + * 0, even though these versions would not be considered stable. + * + * @param major major version number + * @param minor minor version number + * @param patch patch version number + * @return {@code SemanticVersionNumber} instance + * @throws IllegalArgumentException if any argument is negative + * @since 2022-02-19 + */ + public static final SemanticVersionNumber stableVersion(int major, int minor, + int patch) { + if (major < 0) + throw new IllegalArgumentException( + "Major version must be non-negative."); + if (minor < 0) + throw new IllegalArgumentException( + "Minor version must be non-negative."); + if (patch < 0) + throw new IllegalArgumentException( + "Patch version must be non-negative."); + return new SemanticVersionNumber(major, minor, patch, List.of(), + List.of()); + } + + // parts of the version number + private final int major; + private final int minor; + private final int patch; + private final List<String> preReleaseIdentifiers; + private final List<String> buildMetadata; + + /** + * Creates a version number + * + * @param major major version number + * @param minor minor version number + * @param patch patch version number + * @param preReleaseIdentifiers pre-release version data + * @param buildMetadata build metadata + * @since 2022-02-19 + */ + private SemanticVersionNumber(int major, int minor, int patch, + List<String> preReleaseIdentifiers, List<String> buildMetadata) { + this.major = major; + this.minor = minor; + this.patch = patch; + this.preReleaseIdentifiers = preReleaseIdentifiers; + this.buildMetadata = buildMetadata; + } + + /** + * @return build metadata (empty if there is none) + * @since 2022-02-19 + */ + public List<String> buildMetadata() { + return Collections.unmodifiableList(this.buildMetadata); + } + + /** + * Compares two version numbers according to the official Semantic Versioning + * order. + * <p> + * Note: this ordering is not consistent with equals. Specifically, two + * versions that are identical except for their build metadata will be + * considered different by equals but the same by this method. This is + * required to follow the official Semantic Versioning specification. + * <p> + */ + @Override + public int compareTo(SemanticVersionNumber o) { + // test the three big numbers in order first + if (this.major < o.major) + return -1; + else if (this.major > o.major) + return 1; + + if (this.minor < o.minor) + return -1; + else if (this.minor > o.minor) + return 1; + + if (this.patch < o.patch) + return -1; + else if (this.patch > o.patch) + return 1; + + // now we just compare pre-release identifiers + // (remember: build metadata is ignored) + return SemanticVersionNumber.compareIdentifiers(this.preReleaseIdentifiers, + o.preReleaseIdentifiers); + } + + /** + * Determines the compatibility of code written for this version to + * {@code other}. More specifically: + * <p> + * If this function returns <b>true</b>, then there should be no problems + * upgrading code written for this version to version {@code other} as long + * as: + * <ul> + * <li>Semantic Versioning is being used properly + * <li>Your code doesn't depend on unintended features (if it does, it isn't + * necessarily compatible with any other version) + * </ul> + * If this function returns <b>false</b>, you may have to change your code to + * upgrade it to {@code other} + * + * <p> + * Two version numbers that are identical (ignoring build metadata) are + * always compatible. Different version numbers are compatible as long as: + * <ul> + * <li>The major version number is not 0 (if it is, the API is considered + * unstable and any upgrade can be backwards compatible) + * <li>The major version number is the same (changing the major version + * number implies bacwards incompatible changes) + * <li>This version comes before the other one in the official precedence + * order (downgrading can remove features you depend on) + * </ul> + * + * @param other version to compare with + * @return true if you can definitely upgrade to {@code other} without + * changing code + * @since 2022-02-20 + */ + public boolean compatibleWith(SemanticVersionNumber other) { + Objects.requireNonNull(other, "other may not be null"); + + return this.compareTo(other) == 0 || this.major != 0 + && this.major == other.major && this.compareTo(other) < 0; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (!(obj instanceof SemanticVersionNumber)) + return false; + final SemanticVersionNumber other = (SemanticVersionNumber) obj; + if (this.buildMetadata == null) { + if (other.buildMetadata != null) + return false; + } else if (!this.buildMetadata.equals(other.buildMetadata)) + return false; + if (this.major != other.major) + return false; + if (this.minor != other.minor) + return false; + if (this.patch != other.patch) + return false; + if (this.preReleaseIdentifiers == null) { + if (other.preReleaseIdentifiers != null) + return false; + } else if (!this.preReleaseIdentifiers + .equals(other.preReleaseIdentifiers)) + return false; + return true; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + + (this.buildMetadata == null ? 0 : this.buildMetadata.hashCode()); + result = prime * result + this.major; + result = prime * result + this.minor; + result = prime * result + this.patch; + result = prime * result + (this.preReleaseIdentifiers == null ? 0 + : this.preReleaseIdentifiers.hashCode()); + return result; + } + + /** + * @return true iff this version is stable (major version > 0 and not a + * pre-release) + * @since 2022-02-19 + */ + public boolean isStable() { + return this.major > 0 && this.preReleaseIdentifiers.isEmpty(); + } + + /** + * @return the MAJOR version number, incremented when you make backwards + * incompatible API changes + * @since 2022-02-19 + */ + public int majorVersion() { + return this.major; + } + + /** + * @return the MINOR version number, incremented when you add backwards + * compatible functionality + * @since 2022-02-19 + */ + public int minorVersion() { + return this.minor; + } + + /** + * @return the PATCH version number, incremented when you make backwards + * compatible bug fixes + * @since 2022-02-19 + */ + public int patchVersion() { + return this.patch; + } + + /** + * @return identifiers describing this pre-release (empty if not a + * pre-release) + * @since 2022-02-19 + */ + public List<String> preReleaseIdentifiers() { + return Collections.unmodifiableList(this.preReleaseIdentifiers); + } + + /** + * Converts a version number to a string using the official SemVer format. + * The core of a version is MAJOR.MINOR.PATCH, without zero-padding. If + * pre-release identifiers are present, they are separated by periods and + * added after a '-'. If build metadata is present, it is separated by + * periods and added after a '+'. Pre-release identifiers go before version + * metadata. + * <p> + * For example, the version with major number 3, minor number 2, patch number + * 1, pre-release identifiers "alpha" and "1" and build metadata "2022-02-19" + * has a string representation "3.2.1-alpha.1+2022-02-19". + * + * @see <a href="https://semver.org">The official SemVer specification</a> + */ + @Override + public String toString() { + String versionString = String.format("%d.%d.%d", this.major, this.minor, + this.patch); + if (!this.preReleaseIdentifiers.isEmpty()) { + versionString += "-" + String.join(".", this.preReleaseIdentifiers); + } + if (!this.buildMetadata.isEmpty()) { + versionString += "+" + String.join(".", this.buildMetadata); + } + return versionString; + } +} diff --git a/src/main/java/sevenUnits/utils/UncertainDouble.java b/src/main/java/sevenUnits/utils/UncertainDouble.java index fe41104..ac523b3 100644 --- a/src/main/java/sevenUnits/utils/UncertainDouble.java +++ b/src/main/java/sevenUnits/utils/UncertainDouble.java @@ -46,6 +46,21 @@ public final class UncertainDouble implements Comparable<UncertainDouble> { + "(?:\\s*(?:±|\\+-)\\s*" + NUMBER_REGEX + ")?"); /** + * Gets an UncertainDouble from a double string. The uncertainty of the + * double will be one of the lowest decimal place of the number. For example, + * "12345.678" will become 12345.678 ± 0.001. + * + * @throws NumberFormatException if the argument is not a number + * + * @since 2022-04-18 + */ + public static final UncertainDouble fromRoundedString(String s) { + final BigDecimal value = new BigDecimal(s); + final double uncertainty = Math.pow(10, -value.scale()); + return UncertainDouble.of(value.doubleValue(), uncertainty); + } + + /** * Parses a string in the form of {@link UncertainDouble#toString(boolean)} * and returns the corresponding {@code UncertainDouble} instance. * <p> @@ -348,7 +363,7 @@ public final class UncertainDouble implements Comparable<UncertainDouble> { */ @Override public final String toString() { - return this.toString(!this.isExact()); + return this.toString(!this.isExact(), RoundingMode.HALF_EVEN); } /** @@ -379,7 +394,8 @@ public final class UncertainDouble implements Comparable<UncertainDouble> { * * @since 2020-09-07 */ - public final String toString(boolean showUncertainty) { + public final String toString(boolean showUncertainty, + RoundingMode roundingMode) { String valueString, uncertaintyString; // generate the string representation of value and uncertainty @@ -394,9 +410,9 @@ public final class UncertainDouble implements Comparable<UncertainDouble> { final int displayScale = this.getDisplayScale(); final BigDecimal roundedUncertainty = bigUncertainty - .setScale(displayScale, RoundingMode.HALF_EVEN); + .setScale(displayScale, roundingMode); final BigDecimal roundedValue = bigValue.setScale(displayScale, - RoundingMode.HALF_EVEN); + roundingMode); valueString = roundedValue.toString(); uncertaintyString = roundedUncertainty.toString(); |