summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.classpath6
-rw-r--r--.gitignore3
-rw-r--r--.settings/org.eclipse.jdt.core.prefs12
-rw-r--r--metric_exceptions.txt19
-rw-r--r--src/about.txt12
-rw-r--r--src/org/unitConverter/converterGUI/DefaultPrefixRepetitionRule.java95
-rw-r--r--src/org/unitConverter/converterGUI/SearchBoxList.java122
-rw-r--r--src/org/unitConverter/converterGUI/UnitConverterGUI.java1062
-rw-r--r--src/org/unitConverter/math/ConditionalExistenceCollections.java295
-rw-r--r--src/org/unitConverter/math/DecimalComparison.java207
-rw-r--r--src/org/unitConverter/math/UncertainDouble.java419
-rw-r--r--src/org/unitConverter/unit/BaseUnit.java86
-rw-r--r--src/org/unitConverter/unit/FunctionalUnitlike.java72
-rw-r--r--src/org/unitConverter/unit/LinearUnit.java328
-rw-r--r--src/org/unitConverter/unit/LinearUnitValue.java341
-rw-r--r--src/org/unitConverter/unit/MultiUnit.java160
-rw-r--r--src/org/unitConverter/unit/MultiUnitTest.java106
-rw-r--r--src/org/unitConverter/unit/NameSymbol.java317
-rw-r--r--src/org/unitConverter/unit/Nameable.java59
-rw-r--r--src/org/unitConverter/unit/SI.java490
-rw-r--r--src/org/unitConverter/unit/Unit.java342
-rw-r--r--src/org/unitConverter/unit/UnitDatabase.java1269
-rw-r--r--src/org/unitConverter/unit/UnitDatabaseTest.java184
-rw-r--r--src/org/unitConverter/unit/UnitTest.java90
-rw-r--r--src/org/unitConverter/unit/UnitValue.java172
-rw-r--r--src/org/unitConverter/unit/Unitlike.java260
-rw-r--r--src/org/unitConverter/unit/UnitlikeValue.java174
-rw-r--r--unitsfile.txt5
28 files changed, 4966 insertions, 1741 deletions
diff --git a/.classpath b/.classpath
index 35e2ca9..a6325e3 100644
--- a/.classpath
+++ b/.classpath
@@ -1,10 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
- <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8">
- <attributes>
- <attribute name="maven.pomderived" value="true"/>
- </attributes>
- </classpathentry>
+ <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11"/>
<classpathentry kind="con" path="org.eclipse.jdt.junit.JUNIT_CONTAINER/5"/>
<classpathentry kind="src" output="target/classes" path="src">
<attributes>
diff --git a/.gitignore b/.gitignore
index 52a523a..1d7e13f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
bin/
target/
*.class
-*~ \ No newline at end of file
+*~
+settings.txt \ No newline at end of file
diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs
index ea7a397..01cf56c 100644
--- a/.settings/org.eclipse.jdt.core.prefs
+++ b/.settings/org.eclipse.jdt.core.prefs
@@ -1,16 +1,16 @@
eclipse.preferences.version=1
org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
-org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8
+org.eclipse.jdt.core.compiler.codegen.methodParameters=do not generate
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=11
org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
-org.eclipse.jdt.core.compiler.compliance=1.8
+org.eclipse.jdt.core.compiler.compliance=11
org.eclipse.jdt.core.compiler.debug.lineNumber=generate
org.eclipse.jdt.core.compiler.debug.localVariable=generate
org.eclipse.jdt.core.compiler.debug.sourceFile=generate
org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled
org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
-org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning
-org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=ignore
+org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=warning
org.eclipse.jdt.core.compiler.processAnnotations=disabled
-org.eclipse.jdt.core.compiler.release=disabled
-org.eclipse.jdt.core.compiler.source=1.8
+org.eclipse.jdt.core.compiler.release=enabled
+org.eclipse.jdt.core.compiler.source=11
diff --git a/metric_exceptions.txt b/metric_exceptions.txt
new file mode 100644
index 0000000..73748c0
--- /dev/null
+++ b/metric_exceptions.txt
@@ -0,0 +1,19 @@
+# This is a list of exceptions for the one-way conversion mode
+# Units in this list will be included in both From: and To:
+# regardless of whether or not one-way conversion is enabled.
+
+tempC
+tempCelsius
+s
+second
+min
+minute
+h
+hour
+d
+day
+wk
+week
+gregorianmonth
+gregorianyear
+km/h \ No newline at end of file
diff --git a/src/about.txt b/src/about.txt
new file mode 100644
index 0000000..1bad9e8
--- /dev/null
+++ b/src/about.txt
@@ -0,0 +1,12 @@
+About Unit Converter v0.2.0
+
+Copyright Notice:
+
+Unit Converter Copyright (C) 2018-2021 Adrien Hopkins
+This program comes with ABSOLUTELY NO WARRANTY;
+for details read the LICENSE file, section 15
+
+This is free software, and you are welcome to redistribute
+it under certain conditions; for details go to
+<https://www.gnu.org/licenses/quick-guide-gplv3.html>
+or read the LICENSE file. \ No newline at end of file
diff --git a/src/org/unitConverter/converterGUI/DefaultPrefixRepetitionRule.java b/src/org/unitConverter/converterGUI/DefaultPrefixRepetitionRule.java
new file mode 100644
index 0000000..bdc3a2e
--- /dev/null
+++ b/src/org/unitConverter/converterGUI/DefaultPrefixRepetitionRule.java
@@ -0,0 +1,95 @@
+/**
+ * @since 2020-08-26
+ */
+package org.unitConverter.converterGUI;
+
+import java.util.List;
+import java.util.function.Predicate;
+
+import org.unitConverter.unit.SI;
+import org.unitConverter.unit.UnitPrefix;
+
+/**
+ * A rule that specifies whether prefix repetition is allowed
+ *
+ * @since 2020-08-26
+ */
+enum DefaultPrefixRepetitionRule implements Predicate<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 (!SI.DECIMAL_PREFIXES.contains(prefixes.get(0)))
+ return NO_REPETITION.test(prefixes);
+
+ int part = 0; // 0=yotta/yoctos, 1=kilo-zetta/milli-zepto,
+ // 2=deka,hecto,deci,centi
+
+ for (final UnitPrefix prefix : prefixes) {
+ // check that the current prefix is metric and appropriately
+ // magnifying/reducing
+ if (!SI.DECIMAL_PREFIXES.contains(prefix))
+ return false;
+ if (magnifying != prefix.getMultiplier() > 1)
+ return false;
+
+ // check if the current prefix is correct
+ // since part is set *after* this check, part designates the state
+ // of the *previous* prefix
+ switch (part) {
+ case 0:
+ // do nothing, any prefix is valid after a yotta
+ break;
+ case 1:
+ // after a kilo-zetta, only deka/hecto are valid
+ if (SI.THOUSAND_PREFIXES.contains(prefix))
+ return false;
+ break;
+ case 2:
+ // deka/hecto must be the last prefix, so this is always invalid
+ return false;
+ }
+
+ // set part
+ if (SI.YOTTA.equals(prefix) || SI.YOCTO.equals(prefix)) {
+ part = 0;
+ } else if (SI.THOUSAND_PREFIXES.contains(prefix)) {
+ part = 1;
+ } else {
+ part = 2;
+ }
+ }
+ return true;
+ }
+ };
+}
diff --git a/src/org/unitConverter/converterGUI/SearchBoxList.java b/src/org/unitConverter/converterGUI/SearchBoxList.java
index 1995466..10ef589 100644
--- a/src/org/unitConverter/converterGUI/SearchBoxList.java
+++ b/src/org/unitConverter/converterGUI/SearchBoxList.java
@@ -36,13 +36,13 @@ import javax.swing.JTextField;
* @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.
*
@@ -50,7 +50,7 @@ final class SearchBoxList extends JPanel {
* @since v0.2.0
*/
private static final String EMPTY_TEXT = "Search...";
-
+
/**
* The color to use for an empty foreground.
*
@@ -58,94 +58,92 @@ final class SearchBoxList extends JPanel {
* @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
+
+ // 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
+ * @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
+ * @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,
+ public SearchBoxList(final Collection<String> itemsToFilter,
+ final Comparator<String> defaultOrdering,
final boolean caseSensitive) {
super(new BorderLayout(), true);
this.itemsToFilter = itemsToFilter;
this.defaultOrdering = defaultOrdering;
this.caseSensitive = caseSensitive;
-
+
// create the components
this.listModel = new DelegateListModel<>(new ArrayList<>(itemsToFilter));
this.searchItems = new JList<>(this.listModel);
-
+
this.searchBox = new JTextField(EMPTY_TEXT);
this.searchBox.setForeground(EMPTY_FOREGROUND);
-
+
// add them to the panel
this.add(this.searchBox, BorderLayout.PAGE_START);
this.add(new JScrollPane(this.searchItems), BorderLayout.CENTER);
-
+
// set up the search box
this.searchBox.addFocusListener(new FocusListener() {
@Override
public void focusGained(final FocusEvent e) {
SearchBoxList.this.searchBoxFocusGained(e);
}
-
+
@Override
public void focusLost(final FocusEvent e) {
SearchBoxList.this.searchBoxFocusLost(e);
}
});
-
+
this.searchBox.addCaretListener(e -> this.searchBoxTextChanged());
this.searchBoxEmpty = true;
}
-
+
/**
* Adds an additional filter for searching.
*
- * @param filter
- * filter to add.
+ * @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.
*
@@ -155,7 +153,7 @@ final class SearchBoxList extends JPanel {
public void clearSearchFilters() {
this.customSearchFilter = o -> true;
}
-
+
/**
* @return this component's search box component
* @since 2019-04-14
@@ -164,11 +162,11 @@ final class SearchBoxList extends JPanel {
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
+ * @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
*/
@@ -176,9 +174,10 @@ final class SearchBoxList extends JPanel {
if (this.caseSensitive)
return string -> string.contains(searchText);
else
- return string -> string.toLowerCase().contains(searchText.toLowerCase());
+ return string -> string.toLowerCase()
+ .contains(searchText.toLowerCase());
}
-
+
/**
* @return this component's list component
* @since 2019-04-14
@@ -187,7 +186,7 @@ final class SearchBoxList extends JPanel {
public final JList<String> getSearchList() {
return this.searchItems;
}
-
+
/**
* @return index selected in item list
* @since 2019-04-14
@@ -196,7 +195,7 @@ final class SearchBoxList extends JPanel {
public int getSelectedIndex() {
return this.searchItems.getSelectedIndex();
}
-
+
/**
* @return value selected in item list
* @since 2019-04-13
@@ -205,7 +204,7 @@ final class SearchBoxList extends JPanel {
public String getSelectedValue() {
return this.searchItems.getSelectedValue();
}
-
+
/**
* Re-applies the filters.
*
@@ -213,29 +212,30 @@ final class SearchBoxList extends JPanel {
* @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 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
+ * @param e focus event
* @since 2019-04-13
* @since v0.2.0
*/
@@ -246,12 +246,11 @@ final class SearchBoxList extends JPanel {
this.searchBox.setForeground(Color.BLACK);
}
}
-
+
/**
* Runs whenever the search box loses focus.
*
- * @param e
- * focus event
+ * @param e focus event
* @since 2019-04-13
* @since v0.2.0
*/
@@ -262,7 +261,7 @@ final class SearchBoxList extends JPanel {
this.searchBox.setForeground(EMPTY_FOREGROUND);
}
}
-
+
/**
* Runs whenever the text in the search box is changed.
* <p>
@@ -276,10 +275,12 @@ final class SearchBoxList extends JPanel {
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 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 -> {
@@ -287,11 +288,20 @@ final class SearchBoxList extends JPanel {
this.listModel.add(string);
}
});
-
+
// applies the custom filters
this.listModel.removeIf(this.customSearchFilter.negate());
-
+
// sorts the remaining items
this.listModel.sort(comparator);
}
+
+ /**
+ * Manually updates the search box's item list.
+ *
+ * @since 2020-08-27
+ */
+ public void updateList() {
+ this.searchBoxTextChanged();
+ }
}
diff --git a/src/org/unitConverter/converterGUI/UnitConverterGUI.java b/src/org/unitConverter/converterGUI/UnitConverterGUI.java
index 0230728..6ddc4a0 100644
--- a/src/org/unitConverter/converterGUI/UnitConverterGUI.java
+++ b/src/org/unitConverter/converterGUI/UnitConverterGUI.java
@@ -1,5 +1,5 @@
/**
- * Copyright (C) 2018 Adrien Hopkins
+ * 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
@@ -17,41 +17,63 @@
package org.unitConverter.converterGUI;
import java.awt.BorderLayout;
+import java.awt.GridBagConstraints;
+import java.awt.GridBagLayout;
import java.awt.GridLayout;
import java.awt.event.KeyEvent;
-import java.io.File;
+import java.io.BufferedWriter;
+import java.io.IOException;
import java.math.BigDecimal;
import java.math.MathContext;
+import java.math.RoundingMode;
+import java.nio.file.Files;
+import java.nio.file.Path;
import java.text.DecimalFormat;
+import java.text.NumberFormat;
+import java.text.ParseException;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
+import java.util.NoSuchElementException;
import java.util.Set;
import java.util.function.Predicate;
+import java.util.stream.Collectors;
import javax.swing.BorderFactory;
+import javax.swing.BoxLayout;
+import javax.swing.ButtonGroup;
import javax.swing.JButton;
+import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JFormattedTextField;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
+import javax.swing.JRadioButton;
+import javax.swing.JScrollPane;
import javax.swing.JSlider;
import javax.swing.JTabbedPane;
import javax.swing.JTextArea;
import javax.swing.JTextField;
+import javax.swing.UIManager;
+import javax.swing.UnsupportedLookAndFeelException;
import javax.swing.WindowConstants;
+import javax.swing.border.TitledBorder;
+import org.unitConverter.math.ConditionalExistenceCollections;
import org.unitConverter.math.ObjectProduct;
import org.unitConverter.unit.BaseDimension;
import org.unitConverter.unit.BritishImperial;
import org.unitConverter.unit.LinearUnit;
+import org.unitConverter.unit.LinearUnitValue;
+import org.unitConverter.unit.NameSymbol;
import org.unitConverter.unit.SI;
import org.unitConverter.unit.Unit;
import org.unitConverter.unit.UnitDatabase;
import org.unitConverter.unit.UnitPrefix;
+import org.unitConverter.unit.UnitValue;
/**
* @author Adrien Hopkins
@@ -63,10 +85,22 @@ final class UnitConverterGUI {
* A tab in the View.
*/
private enum Pane {
- UNIT_CONVERTER, EXPRESSION_CONVERTER, UNIT_VIEWER, PREFIX_VIEWER;
+ UNIT_CONVERTER, EXPRESSION_CONVERTER, UNIT_VIEWER, PREFIX_VIEWER, ABOUT,
+ SETTINGS;
}
-
+
private static class Presenter {
+ /** The default place where settings are stored. */
+ private static final String DEFAULT_SETTINGS_FILEPATH = "settings.txt";
+ /** The default place where units are stored. */
+ private static final Path DEFAULT_UNITS_FILE = Path.of("unitsfile.txt");
+ /** The default place where dimensions are stored. */
+ private static final Path DEFAULT_DIMENSION_FILE = Path
+ .of("dimensionfile.txt");
+ /** The default place where exceptions are stored. */
+ private static final Path DEFAULT_EXCEPTIONS_FILE = Path
+ .of("metric_exceptions.txt");
+
/**
* Adds default units and dimensions to a database.
*
@@ -88,33 +122,65 @@ final class UnitConverterGUI {
// nonlinear units - must be loaded manually
database.addUnit("tempCelsius", SI.CELSIUS);
database.addUnit("tempFahrenheit", BritishImperial.FAHRENHEIT);
-
+
// load initial dimensions
database.addDimension("LENGTH", SI.Dimensions.LENGTH);
database.addDimension("MASS", SI.Dimensions.MASS);
database.addDimension("TIME", SI.Dimensions.TIME);
database.addDimension("TEMPERATURE", SI.Dimensions.TEMPERATURE);
}
-
+
+ /**
+ * @return {@code line} with any comments removed.
+ * @since 2021-03-13
+ */
+ private static final String withoutComments(String line) {
+ final int index = line.indexOf('#');
+ return index == -1 ? line : line.substring(index);
+ }
+
/** The presenter's associated view. */
private final View view;
-
+
/** The units known by the program. */
private final UnitDatabase database;
-
+
/** The names of all of the units */
private final List<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;
-
- private int significantFigures = 6;
-
+
+ /** 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;
+
/**
* Creates the presenter.
*
@@ -124,14 +190,28 @@ final class UnitConverterGUI {
*/
Presenter(final View view) {
this.view = view;
-
+
// load initial units
- this.database = new UnitDatabase();
+ this.database = new UnitDatabase(
+ DefaultPrefixRepetitionRule.NO_RESTRICTION);
Presenter.addDefaults(this.database);
-
- this.database.loadUnitsFile(new File("unitsfile.txt"));
- this.database.loadDimensionFile(new File("dimensionfile.txt"));
-
+
+ this.database.loadUnitsFile(DEFAULT_UNITS_FILE);
+ this.database.loadDimensionFile(DEFAULT_DIMENSION_FILE);
+
+ // load metric exceptions
+ try {
+ this.metricExceptions = Files.readAllLines(DEFAULT_EXCEPTIONS_FILE)
+ .stream().map(Presenter::withoutComments)
+ .filter(s -> !s.isBlank()).collect(Collectors.toSet());
+ } catch (final IOException e) {
+ throw new AssertionError("Loading of metric_exceptions.txt failed.",
+ e);
+ }
+
+ // load settings - requires database to exist
+ this.loadSettings();
+
// a comparator that can be used to compare prefix names
// any name that does not exist is less than a name that does.
// otherwise, they are compared by value
@@ -140,37 +220,43 @@ final class UnitConverterGUI {
return -1;
else if (!Presenter.this.database.containsPrefixName(o2))
return 1;
-
+
final UnitPrefix p1 = Presenter.this.database.getPrefix(o1);
final UnitPrefix p2 = Presenter.this.database.getPrefix(o2);
-
+
if (p1.getMultiplier() < p2.getMultiplier())
return -1;
else if (p1.getMultiplier() > p2.getMultiplier())
return 1;
-
+
return o1.compareTo(o2);
};
-
- this.unitNames = new ArrayList<>(this.database.unitMapPrefixless().keySet());
+
+ this.unitNames = new ArrayList<>(
+ this.database.unitMapPrefixless().keySet());
this.unitNames.sort(null); // sorts it using Comparable
-
+
this.prefixNames = new ArrayList<>(this.database.prefixMap().keySet());
- this.prefixNames.sort(this.prefixNameComparator); // sorts it using my comparator
-
- this.dimensionNames = new DelegateListModel<>(new ArrayList<>(this.database.dimensionMap().keySet()));
+ this.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();
-
+ 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",
+ System.out.printf(
+ "Successfully loaded %d units with %d unit names (%d base units).%n",
new HashSet<>(this.database.unitMapPrefixless().values()).size(),
this.database.unitMapPrefixless().size(),
- new HashSet<>(this.database.unitMapPrefixless().values()).stream().filter(isFullBase).count());
+ new HashSet<>(this.database.unitMapPrefixless().values())
+ .stream().filter(isFullBase).count());
}
-
+
/**
* Converts in the dimension-based converter
*
@@ -180,7 +266,8 @@ final class UnitConverterGUI {
public final void convertDimensionBased() {
final String fromSelection = this.view.getFromSelection();
if (fromSelection == null) {
- this.view.showErrorDialog("Error", "No unit selected in From field");
+ this.view.showErrorDialog("Error",
+ "No unit selected in From field");
return;
}
final String toSelection = this.view.getToSelection();
@@ -188,30 +275,35 @@ final class UnitConverterGUI {
this.view.showErrorDialog("Error", "No unit selected in To field");
return;
}
-
+
final Unit from = this.database.getUnit(fromSelection);
- final Unit to = this.database.getUnit(toSelection);
-
- final String input = this.view.getDimensionConverterInput();
- if (input.equals("")) {
- this.view.showErrorDialog("Error", "No value to convert entered.");
+ 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 double beforeValue = Double.parseDouble(input);
- final double value = from.convertTo(to, beforeValue);
-
+ final UnitValue value = beforeValue.convertTo(to);
+
final String output = this.getRoundedString(value);
-
+
this.view.setDimensionConverterOutputText(
- String.format("%s %s = %s %s", input, fromSelection, output, toSelection));
+ 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.
+ * 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
@@ -220,64 +312,79 @@ final class UnitConverterGUI {
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.");
+ 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.");
+ this.view.showErrorDialog("Parse Error",
+ "Please enter a unit expression in the To: box.");
return;
}
-
- // try to parse from
- final Unit from;
+
+ final LinearUnitValue from;
+ final Unit to;
try {
- from = this.database.getUnitFromExpression(fromUnitString);
- } catch (final IllegalArgumentException e) {
- this.view.showErrorDialog("Parse Error", "Could not recognize text in From entry: " + e.getMessage());
+ 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;
}
-
- final double value;
- // try to parse to
- final Unit to;
try {
- if (this.database.containsUnitName(toUnitString)) {
- // if it's a unit, convert to that
- to = this.database.getUnit(toUnitString);
+ 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 {
- to = this.database.getUnitFromExpression(toUnitString);
+ // if I can't convert, leave
+ this.view.showErrorDialog("Conversion Error",
+ String.format("Cannot convert between %s and %s",
+ fromUnitString, toUnitString));
+ return;
}
- } catch (final IllegalArgumentException e) {
- this.view.showErrorDialog("Parse Error", "Could not recognize text in To entry: " + e.getMessage());
+
+ final LinearUnitValue converted = from2.convertTo(to2);
+ this.view.setExpressionConverterOutputText((useSlash ? "1 / " : "")
+ + String.format("%s = %s", fromUnitString,
+ this.getRoundedString(converted, false)));
return;
- }
-
- if (from.canConvertTo(to)) {
- value = from.convertTo(to, 1);
-
- // round value
- final String output = this.getRoundedString(value);
-
- this.view.setExpressionConverterOutputText(
- String.format("%s = %s %s", fromUnitString, output, toUnitString));
- } else if (from instanceof LinearUnit && SI.ONE.dividedBy((LinearUnit) from).canConvertTo(to)) {
- // reciprocal conversion (like seconds to hertz)
- value = SI.ONE.dividedBy((LinearUnit) from).convertTo(to, 1);
-
- // round value
- final String output = this.getRoundedString(value);
-
- this.view.setExpressionConverterOutputText(
- String.format("1 / %s = %s %s", fromUnitString, output, toUnitString));
} else {
- // if I can't convert, leave
- this.view.showErrorDialog("Conversion Error",
- String.format("Cannot convert between %s and %s", fromUnitString, toUnitString));
+ // 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
@@ -286,7 +393,17 @@ final class UnitConverterGUI {
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
@@ -295,19 +412,54 @@ final class UnitConverterGUI {
public final Comparator<String> getPrefixNameComparator() {
return this.prefixNameComparator;
}
-
+
/**
- * @param value value to round
- * @return string of that value rounded to {@code significantDigits} significant
- * digits.
- * @since 2019-04-14
- * @since v0.2.0
+ * Like {@link LinearUnitValue#toString(boolean)}, but obeys this unit
+ * converter's rounding settings.
+ *
+ * @since 2020-08-04
*/
- private final String getRoundedString(final double value) {
- // round value
- final BigDecimal bigValue = new BigDecimal(value).round(new MathContext(this.significantFigures));
- String output = bigValue.toString();
-
+ 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")) {
@@ -317,10 +469,74 @@ final class UnitConverterGUI {
output = output.substring(0, output.length() - 1);
}
}
-
- return output;
+
+ return output + " " + value.getUnit().getPrimaryName().get();
}
-
+
+ /**
+ * @return The file where settings are stored;
+ * @since 2020-12-11
+ */
+ private final Path getSettingsFile() {
+ return Path.of(DEFAULT_SETTINGS_FILEPATH);
+ }
+
+ /**
+ * Loads settings from the settings file.
+ *
+ * @since 2021-02-17
+ */
+ public final void loadSettings() {
+ try {
+ // read file line by line
+ final int lineNum = 0;
+ for (final String line : Files
+ .readAllLines(this.getSettingsFile())) {
+ final int equalsIndex = line.indexOf('=');
+ if (equalsIndex == -1)
+ throw new IllegalStateException(
+ "Settings file is malformed at line " + lineNum);
+
+ final String param = line.substring(0, equalsIndex);
+ final String value = line.substring(equalsIndex + 1);
+
+ switch (param) {
+ // set manually to avoid the unnecessary saving of the non-manual
+ // methods
+ case "precision":
+ this.precision = Integer.valueOf(value);
+ break;
+ case "rounding_type":
+ this.roundingType = RoundingType.valueOf(value);
+ break;
+ case "prefix_rule":
+ this.prefixRule = DefaultPrefixRepetitionRule.valueOf(value);
+ this.database.setPrefixRepetitionRule(this.prefixRule);
+ break;
+ case "one_way":
+ this.oneWay = Boolean.valueOf(value);
+ if (this.oneWay) {
+ this.fromExistenceCondition.setPredicate(
+ unitName -> this.metricExceptions.contains(unitName)
+ || !this.database.getUnit(unitName)
+ .isMetric());
+ this.toExistenceCondition.setPredicate(
+ unitName -> this.metricExceptions.contains(unitName)
+ || this.database.getUnit(unitName).isMetric());
+ } else {
+ this.fromExistenceCondition.setPredicate(unitName -> true);
+ this.toExistenceCondition.setPredicate(unitName -> true);
+ }
+ break;
+ default:
+ System.err.printf("Warning: unrecognized setting \"%s\".",
+ param);
+ break;
+ }
+ }
+ } catch (final IOException e) {}
+ }
+
/**
* @return a set of all prefix names in the database
* @since 2019-04-14
@@ -329,7 +545,7 @@ final class UnitConverterGUI {
public final Set<String> prefixNameSet() {
return this.database.prefixMap().keySet();
}
-
+
/**
* Runs whenever a prefix is selected in the viewer.
* <p>
@@ -345,23 +561,107 @@ final class UnitConverterGUI {
return;
else {
final UnitPrefix prefix = this.database.getPrefix(prefixName);
-
- this.view.setPrefixTextBoxText(String.format("%s%nMultiplier: %s", prefixName, prefix.getMultiplier()));
+
+ this.view.setPrefixTextBoxText(String.format("%s%nMultiplier: %s",
+ prefixName, prefix.getMultiplier()));
}
}
-
+
+ /**
+ * Saves the settings to the settings file.
+ *
+ * @since 2021-02-17
+ */
+ public final void saveSettings() {
+ try (BufferedWriter writer = Files
+ .newBufferedWriter(this.getSettingsFile())) {
+ writer.write(String.format("precision=%d\n", this.precision));
+ writer.write(
+ String.format("rounding_type=%s\n", this.roundingType));
+ writer.write(String.format("prefix_rule=%s\n", this.prefixRule));
+ writer.write(String.format("one_way=%s\n", this.oneWay));
+ } catch (final IOException e) {
+ e.printStackTrace();
+ this.view.showErrorDialog("I/O Error",
+ "Error occurred while saving settings: "
+ + e.getLocalizedMessage());
+ }
+ }
+
+ /**
+ * Enables or disables one-way conversion.
+ *
+ * @param oneWay whether one-way conversion should be on (true) or off
+ * (false)
+ * @since 2020-08-27
+ */
+ public final void setOneWay(boolean oneWay) {
+ this.oneWay = oneWay;
+ if (oneWay) {
+ this.fromExistenceCondition.setPredicate(
+ unitName -> this.metricExceptions.contains(unitName)
+ || !this.database.getUnit(unitName).isMetric());
+ this.toExistenceCondition.setPredicate(
+ unitName -> this.metricExceptions.contains(unitName)
+ || this.database.getUnit(unitName).isMetric());
+ } else {
+ this.fromExistenceCondition.setPredicate(unitName -> true);
+ this.toExistenceCondition.setPredicate(unitName -> true);
+ }
+
+ this.saveSettings();
+ }
+
/**
- * @param significantFigures new value of significantFigures
+ * @param precision new value of precision
* @since 2019-01-15
* @since v0.1.0
*/
- public final void setSignificantFigures(final int significantFigures) {
- this.significantFigures = significantFigures;
+ public final void setPrecision(final int precision) {
+ this.precision = precision;
+
+ this.saveSettings();
}
-
+
/**
- * Returns true if and only if the unit represented by {@code unitName} has the
- * dimension represented by {@code dimensionName}.
+ * @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
@@ -369,12 +669,14 @@ final class UnitConverterGUI {
* @since 2019-04-13
* @since v0.2.0
*/
- public final boolean unitMatchesDimension(final String unitName, final String dimensionName) {
+ 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);
+ final ObjectProduct<BaseDimension> dimension = this.database
+ .getDimension(dimensionName);
return unit.getDimension().equals(dimension);
}
-
+
/**
* Runs whenever a unit is selected in the viewer.
* <p>
@@ -390,29 +692,44 @@ final class UnitConverterGUI {
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() {
+ private final Set<String> unitNameSet() {
return this.database.unitMapPrefixless().keySet();
}
}
-
+
+ /**
+ * Different types of rounding.
+ *
+ * Significant digits: Rounds to a number of digits. i.e. with precision 5,
+ * 12345.6789 rounds to 12346. Decimal places: Rounds to a number of digits
+ * after the decimal point, i.e. with precision 5, 12345.6789 rounds to
+ * 12345.67890. Scientific: Rounds based on the number of digits and
+ * operations, following standard scientific rounding.
+ */
+ private static enum RoundingType {
+ SIGNIFICANT_DIGITS, DECIMAL_PLACES, SCIENTIFIC;
+ }
+
private static class View {
+ private static final NumberFormat NUMBER_FORMATTER = new DecimalFormat();
+
/** The view's frame. */
private final JFrame frame;
/** The view's associated presenter. */
private final Presenter presenter;
/** The master pane containing all of the tabs. */
private final JTabbedPane masterPane;
-
+
// DIMENSION-BASED CONVERTER
/** The panel for inputting values in the dimension-based converter */
private final JTextField valueInput;
@@ -422,7 +739,7 @@ final class UnitConverterGUI {
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;
@@ -430,7 +747,7 @@ final class UnitConverterGUI {
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;
@@ -440,7 +757,7 @@ final class UnitConverterGUI {
private final JTextArea unitTextBox;
/** The text box for prefix data in the prefix viewer */
private final JTextArea prefixTextBox;
-
+
/**
* Creates the {@code View}.
*
@@ -451,28 +768,38 @@ final class UnitConverterGUI {
this.presenter = new Presenter(this);
this.frame = new JFrame("Unit Converter");
this.frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
- this.masterPane = new JTabbedPane();
-
+
+ // 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.unitNameSet());
- this.toSearch = new SearchBoxList(this.presenter.unitNameSet());
- this.valueInput = new JFormattedTextField(new DecimalFormat("###############0.################"));
+ 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
@@ -487,20 +814,30 @@ final class UnitConverterGUI {
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
- * @since 2019-04-13
- * @since v0.2.0
+ * @throws ParseException
+ * @since 2020-07-07
*/
- public String getDimensionConverterInput() {
- return this.valueInput.getText();
+ 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
@@ -509,7 +846,7 @@ final class UnitConverterGUI {
public String getFromSelection() {
return this.fromSearch.getSelectedValue();
}
-
+
/**
* @return text in "From" box in converter panel
* @since 2019-01-15
@@ -518,7 +855,7 @@ final class UnitConverterGUI {
public String getFromText() {
return this.fromEntry.getText();
}
-
+
/**
* @return index of selected prefix in prefix viewer
* @since 2019-01-15
@@ -527,7 +864,7 @@ final class UnitConverterGUI {
public String getPrefixViewerSelection() {
return this.prefixNameList.getSelectedValue();
}
-
+
/**
* @return selection in "To" selector in dimension-based converter
* @since 2019-04-13
@@ -536,7 +873,7 @@ final class UnitConverterGUI {
public String getToSelection() {
return this.toSearch.getSelectedValue();
}
-
+
/**
* @return text in "To" box in converter panel
* @since 2019-01-26
@@ -545,7 +882,7 @@ final class UnitConverterGUI {
public String getToText() {
return this.toEntry.getText();
}
-
+
/**
* @return index of selected unit in unit viewer
* @since 2019-01-15
@@ -554,7 +891,7 @@ final class UnitConverterGUI {
public String getUnitViewerSelection() {
return this.unitNameList.getSelectedValue();
}
-
+
/**
* Starts up the application.
*
@@ -564,7 +901,7 @@ final class UnitConverterGUI {
public final void init() {
this.frame.setVisible(true);
}
-
+
/**
* Initializes the view's components.
*
@@ -574,229 +911,443 @@ final class UnitConverterGUI {
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]));
+ this.presenter.dimensionNameList()
+ .toArray(new String[0]));
dimensionSelector.setSelectedItem("LENGTH");
-
+
// handle dimension filter
- final MutablePredicate<String> dimensionFilter = new MutablePredicate<>(s -> true);
-
+ 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
+
+ { // 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);
+ 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()));
+ 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()));
+
+ // 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);
+ final JLabel valuePrompt = new JLabel(
+ "Value to convert: ");
+ valueInputPanel.add(valuePrompt,
+ BorderLayout.LINE_START);
}
-
+
{ // value to convert
- valueInputPanel.add(this.valueInput, BorderLayout.CENTER);
+ 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.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.addTab("Convert Unit Expressions",
+ convertExpressionPanel);
this.masterPane.setMnemonicAt(1, KeyEvent.VK_E);
-
- convertExpressionPanel.setLayout(new GridLayout(5, 1));
-
+
+ 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.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
+ .setBorder(BorderFactory.createTitledBorder("Output"));
outputPanel.setLayout(new GridLayout(1, 1));
-
+
{ // output
outputPanel.add(this.output);
this.output.setEditable(false);
}
}
-
- { // panel for specifying precision
- final JPanel sigDigPanel = new JPanel();
- convertExpressionPanel.add(sigDigPanel);
-
- sigDigPanel.setBorder(BorderFactory.createTitledBorder("Significant Digits"));
-
- { // slider
- final JSlider sigDigSlider = new JSlider(0, 12);
- sigDigPanel.add(sigDigSlider);
-
- sigDigSlider.setMajorTickSpacing(4);
- sigDigSlider.setMinorTickSpacing(1);
- sigDigSlider.setSnapToTicks(true);
- sigDigSlider.setPaintTicks(true);
- sigDigSlider.setPaintLabels(true);
-
- sigDigSlider.addChangeListener(
- e -> this.presenter.setSignificantFigures(sigDigSlider.getValue()));
- }
- }
}
-
+
{ // panel to look up units
final JPanel unitLookupPanel = new JPanel();
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());
+
+ 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());
+
+ this.prefixNameList.getSearchList().addListSelectionListener(
+ e -> this.presenter.prefixSelected());
}
-
+
{ // the text box for prefix's toString
prefixLookupPanel.add(this.prefixTextBox);
this.prefixTextBox.setEditable(false);
this.prefixTextBox.setLineWrap(true);
}
}
+
+ { // Info panel
+ final JPanel infoPanel = new JPanel();
+ this.masterPane.addTab("\uD83D\uDEC8", // info (i) character
+ new JScrollPane(infoPanel));
+
+ final JTextArea infoTextArea = new JTextArea();
+ infoTextArea.setEditable(false);
+ infoTextArea.setOpaque(false);
+ infoPanel.add(infoTextArea);
+
+ // get info text
+ final String infoText;
+ try {
+ final Path aboutFile = Path.of("src", "about.txt");
+ infoText = Files.readAllLines(aboutFile).stream()
+ .map(Presenter::withoutComments)
+ .collect(Collectors.joining("\n"));
+ } catch (final IOException e) {
+ throw new AssertionError("I/O exception loading about.txt");
+ }
+ infoTextArea.setText(infoText);
+ }
+
+ { // Settings panel
+ final JPanel settingsPanel = new JPanel();
+ this.masterPane.addTab("\u2699", new JScrollPane(settingsPanel));
+ this.masterPane.setMnemonicAt(5, KeyEvent.VK_S);
+
+ settingsPanel.setLayout(
+ new BoxLayout(settingsPanel, BoxLayout.PAGE_AXIS));
+
+ { // rounding settings
+ final JPanel roundingPanel = new JPanel();
+ settingsPanel.add(roundingPanel);
+ roundingPanel
+ .setBorder(new TitledBorder("Rounding Settings"));
+ roundingPanel.setLayout(new GridBagLayout());
+
+ // rounding rule selection
+ final ButtonGroup roundingRuleButtons = new ButtonGroup();
+
+ final JLabel roundingRuleLabel = new JLabel("Rounding Rule:");
+ roundingPanel.add(roundingRuleLabel, new GridBagBuilder(0, 0)
+ .setAnchor(GridBagConstraints.LINE_START).build());
+
+ final JRadioButton fixedPrecision = new JRadioButton(
+ "Fixed Precision");
+ if (this.presenter.roundingType == RoundingType.SIGNIFICANT_DIGITS) {
+ fixedPrecision.setSelected(true);
+ }
+ fixedPrecision.addActionListener(e -> this.presenter
+ .setRoundingType(RoundingType.SIGNIFICANT_DIGITS));
+ roundingRuleButtons.add(fixedPrecision);
+ roundingPanel.add(fixedPrecision, new GridBagBuilder(0, 1)
+ .setAnchor(GridBagConstraints.LINE_START).build());
+
+ final JRadioButton fixedDecimals = new JRadioButton(
+ "Fixed Decimal Places");
+ if (this.presenter.roundingType == RoundingType.DECIMAL_PLACES) {
+ fixedDecimals.setSelected(true);
+ }
+ fixedDecimals.addActionListener(e -> this.presenter
+ .setRoundingType(RoundingType.DECIMAL_PLACES));
+ roundingRuleButtons.add(fixedDecimals);
+ roundingPanel.add(fixedDecimals, new GridBagBuilder(0, 2)
+ .setAnchor(GridBagConstraints.LINE_START).build());
+
+ final JRadioButton relativePrecision = new JRadioButton(
+ "Scientific Precision");
+ if (this.presenter.roundingType == RoundingType.SCIENTIFIC) {
+ relativePrecision.setSelected(true);
+ }
+ relativePrecision.addActionListener(e -> this.presenter
+ .setRoundingType(RoundingType.SCIENTIFIC));
+ roundingRuleButtons.add(relativePrecision);
+ roundingPanel.add(relativePrecision, new GridBagBuilder(0, 3)
+ .setAnchor(GridBagConstraints.LINE_START).build());
+
+ final JLabel sliderLabel = new JLabel("Precision:");
+ roundingPanel.add(sliderLabel, new GridBagBuilder(0, 4)
+ .setAnchor(GridBagConstraints.LINE_START).build());
+
+ final JSlider sigDigSlider = new JSlider(0, 12);
+ roundingPanel.add(sigDigSlider, new GridBagBuilder(0, 5)
+ .setAnchor(GridBagConstraints.LINE_START).build());
+
+ sigDigSlider.setMajorTickSpacing(4);
+ sigDigSlider.setMinorTickSpacing(1);
+ sigDigSlider.setSnapToTicks(true);
+ sigDigSlider.setPaintTicks(true);
+ sigDigSlider.setPaintLabels(true);
+ sigDigSlider.setValue(this.presenter.precision);
+
+ sigDigSlider.addChangeListener(e -> this.presenter
+ .setPrecision(sigDigSlider.getValue()));
+ }
+
+ { // prefix repetition settings
+ final JPanel prefixRepetitionPanel = new JPanel();
+ settingsPanel.add(prefixRepetitionPanel);
+ prefixRepetitionPanel.setBorder(
+ new TitledBorder("Prefix Repetition Settings"));
+ prefixRepetitionPanel.setLayout(new GridBagLayout());
+
+ // prefix rules
+ final ButtonGroup prefixRuleButtons = new ButtonGroup();
+
+ final JRadioButton noRepetition = new JRadioButton(
+ "No Repetition");
+ if (this.presenter.prefixRule == DefaultPrefixRepetitionRule.NO_REPETITION) {
+ noRepetition.setSelected(true);
+ }
+ noRepetition.addActionListener(
+ e -> this.presenter.setPrefixRepetitionRule(
+ DefaultPrefixRepetitionRule.NO_REPETITION));
+ prefixRuleButtons.add(noRepetition);
+ prefixRepetitionPanel.add(noRepetition,
+ new GridBagBuilder(0, 0)
+ .setAnchor(GridBagConstraints.LINE_START)
+ .build());
+
+ final JRadioButton noRestriction = new JRadioButton(
+ "No Restriction");
+ if (this.presenter.prefixRule == DefaultPrefixRepetitionRule.NO_RESTRICTION) {
+ noRestriction.setSelected(true);
+ }
+ noRestriction.addActionListener(
+ e -> this.presenter.setPrefixRepetitionRule(
+ DefaultPrefixRepetitionRule.NO_RESTRICTION));
+ prefixRuleButtons.add(noRestriction);
+ prefixRepetitionPanel.add(noRestriction,
+ new GridBagBuilder(0, 1)
+ .setAnchor(GridBagConstraints.LINE_START)
+ .build());
+
+ final JRadioButton customRepetition = new JRadioButton(
+ "Complex Repetition");
+ if (this.presenter.prefixRule == DefaultPrefixRepetitionRule.COMPLEX_REPETITION) {
+ customRepetition.setSelected(true);
+ }
+ customRepetition.addActionListener(
+ e -> this.presenter.setPrefixRepetitionRule(
+ DefaultPrefixRepetitionRule.COMPLEX_REPETITION));
+ prefixRuleButtons.add(customRepetition);
+ prefixRepetitionPanel.add(customRepetition,
+ new GridBagBuilder(0, 2)
+ .setAnchor(GridBagConstraints.LINE_START)
+ .build());
+ }
+
+ { // search settings
+ final JPanel searchingPanel = new JPanel();
+ settingsPanel.add(searchingPanel);
+ searchingPanel.setBorder(new TitledBorder("Search Settings"));
+ searchingPanel.setLayout(new GridBagLayout());
+
+ // searching rules
+ final ButtonGroup searchRuleButtons = new ButtonGroup();
+
+ final JRadioButton noPrefixes = new JRadioButton(
+ "Never Include Prefixed Units");
+ noPrefixes.setEnabled(false);
+ searchRuleButtons.add(noPrefixes);
+ searchingPanel.add(noPrefixes, new GridBagBuilder(0, 0)
+ .setAnchor(GridBagConstraints.LINE_START).build());
+
+ final JRadioButton fixedPrefixes = new JRadioButton(
+ "Include Some Prefixes");
+ fixedPrefixes.setEnabled(false);
+ searchRuleButtons.add(fixedPrefixes);
+ searchingPanel.add(fixedPrefixes, new GridBagBuilder(0, 1)
+ .setAnchor(GridBagConstraints.LINE_START).build());
+
+ final JRadioButton explicitPrefixes = new JRadioButton(
+ "Include Explicit Prefixes");
+ explicitPrefixes.setEnabled(false);
+ searchRuleButtons.add(explicitPrefixes);
+ searchingPanel.add(explicitPrefixes, new GridBagBuilder(0, 2)
+ .setAnchor(GridBagConstraints.LINE_START).build());
+
+ final JRadioButton alwaysInclude = new JRadioButton(
+ "Include All Single Prefixes");
+ alwaysInclude.setEnabled(false);
+ searchRuleButtons.add(alwaysInclude);
+ searchingPanel.add(alwaysInclude, new GridBagBuilder(0, 3)
+ .setAnchor(GridBagConstraints.LINE_START).build());
+ }
+
+ { // miscellaneous settings
+ final JPanel miscPanel = new JPanel();
+ settingsPanel.add(miscPanel);
+ miscPanel
+ .setBorder(new TitledBorder("Miscellaneous Settings"));
+ miscPanel.setLayout(new GridBagLayout());
+
+ final JCheckBox oneWay = new JCheckBox(
+ "Convert One Way Only");
+ oneWay.setSelected(this.presenter.oneWay);
+ oneWay.addItemListener(
+ e -> this.presenter.setOneWay(e.getStateChange() == 1));
+ miscPanel.add(oneWay, new GridBagBuilder(0, 0)
+ .setAnchor(GridBagConstraints.LINE_START).build());
+
+ final JCheckBox showAllVariations = new JCheckBox(
+ "Show Symbols in \"Convert Units\"");
+ showAllVariations.setSelected(true);
+ showAllVariations.setEnabled(false);
+ miscPanel.add(showAllVariations, new GridBagBuilder(0, 1)
+ .setAnchor(GridBagConstraints.LINE_START).build());
+
+ final JButton unitFileButton = new JButton(
+ "Manage Unit Data Files");
+ unitFileButton.setEnabled(false);
+ miscPanel.add(unitFileButton, new GridBagBuilder(0, 2)
+ .setAnchor(GridBagConstraints.LINE_START).build());
+ }
+ }
}
}
-
+
/**
* Sets the text in the output of the dimension-based converter.
*
@@ -807,7 +1358,7 @@ final class UnitConverterGUI {
public void setDimensionConverterOutputText(final String text) {
this.dimensionBasedOutput.setText(text);
}
-
+
/**
* Sets the text in the output of the conversion panel.
*
@@ -818,7 +1369,7 @@ final class UnitConverterGUI {
public void setExpressionConverterOutputText(final String text) {
this.output.setText(text);
}
-
+
/**
* Sets the text of the prefix text box in the prefix viewer.
*
@@ -829,7 +1380,7 @@ final class UnitConverterGUI {
public void setPrefixTextBoxText(final String text) {
this.prefixTextBox.setText(text);
}
-
+
/**
* Sets the text of the unit text box in the unit viewer.
*
@@ -840,7 +1391,7 @@ final class UnitConverterGUI {
public void setUnitTextBoxText(final String text) {
this.unitTextBox.setText(text);
}
-
+
/**
* Shows an error dialog.
*
@@ -850,10 +1401,23 @@ final class UnitConverterGUI {
* @since v0.1.0
*/
public void showErrorDialog(final String title, final String message) {
- JOptionPane.showMessageDialog(this.frame, message, title, JOptionPane.ERROR_MESSAGE);
+ JOptionPane.showMessageDialog(this.frame, message, title,
+ JOptionPane.ERROR_MESSAGE);
+ }
+
+ public void update() {
+ switch (this.getActivePane()) {
+ case UNIT_CONVERTER:
+ this.fromSearch.updateList();
+ this.toSearch.updateList();
+ break;
+ default:
+ // do nothing, for now
+ break;
+ }
}
}
-
+
public static void main(final String[] args) {
new View().init();
}
diff --git a/src/org/unitConverter/math/ConditionalExistenceCollections.java b/src/org/unitConverter/math/ConditionalExistenceCollections.java
index 9522885..ac1c0cf 100644
--- a/src/org/unitConverter/math/ConditionalExistenceCollections.java
+++ b/src/org/unitConverter/math/ConditionalExistenceCollections.java
@@ -30,20 +30,25 @@ import java.util.function.Predicate;
/**
* Elements in these wrapper collections only exist if they pass a condition.
* <p>
- * All of the collections in this class are "views" of the provided collections. They are mutable if the provided
- * collections are mutable, they allow null if the provided collections allow null, they will reflect changes in the
+ * All of the collections in this class are "views" of the provided collections.
+ * They are mutable if the provided collections are mutable, they allow null if
+ * the provided collections allow null, they will reflect changes in the
* provided collection, etc.
* <p>
- * The modification operations will always run the corresponding operations, even if the conditional existence
- * collection doesn't change. For example, if you have a set that ignores even numbers, add(2) will still add a 2 to the
+ * The modification operations will always run the corresponding operations,
+ * even if the conditional existence collection doesn't change. For example, if
+ * you have a set that ignores even numbers, add(2) will still add a 2 to the
* backing set (but the conditional existence set will say it doesn't exist).
* <p>
- * The returned collections do <i>not</i> pass the hashCode and equals operations through to the backing collections,
- * but rely on {@code Object}'s {@code equals} and {@code hashCode} methods. This is necessary to preserve the contracts
- * of these operations in the case that the backing collections are sets or lists.
+ * The returned collections do <i>not</i> pass the hashCode and equals
+ * operations through to the backing collections, but rely on {@code Object}'s
+ * {@code equals} and {@code hashCode} methods. This is necessary to preserve
+ * the contracts of these operations in the case that the backing collections
+ * are sets or lists.
* <p>
- * Other than that, <i>the only difference between the provided collections and the returned collections are that
- * elements don't exist if they don't pass the provided condition</i>.
+ * Other than that, <i>the only difference between the provided collections and
+ * the returned collections are that elements don't exist if they don't pass the
+ * provided condition</i>.
*
*
* @author Adrien Hopkins
@@ -56,13 +61,13 @@ public final class ConditionalExistenceCollections {
*
* @author Adrien Hopkins
* @since 2019-10-17
- * @param <E>
- * type of element in collection
+ * @param <E> type of element in collection
*/
- static final class ConditionalExistenceCollection<E> extends AbstractCollection<E> {
+ static final class ConditionalExistenceCollection<E>
+ extends AbstractCollection<E> {
final Collection<E> collection;
final Predicate<E> existenceCondition;
-
+
/**
* Creates the {@code ConditionalExistenceCollection}.
*
@@ -70,67 +75,89 @@ public final class ConditionalExistenceCollections {
* @param existenceCondition
* @since 2019-10-17
*/
- private ConditionalExistenceCollection(final Collection<E> collection, final Predicate<E> existenceCondition) {
+ private ConditionalExistenceCollection(final Collection<E> collection,
+ final Predicate<E> existenceCondition) {
this.collection = collection;
this.existenceCondition = existenceCondition;
}
-
+
@Override
public boolean add(final E e) {
return this.collection.add(e) && this.existenceCondition.test(e);
}
-
+
@Override
public void clear() {
this.collection.clear();
}
-
+
@Override
public boolean contains(final Object o) {
if (!this.collection.contains(o))
return false;
-
+
// this collection can only contain instances of E
- // since the object is in the collection, we know that it must be an instance of E
+ // since the object is in the collection, we know that it must be an
+ // instance of E
// therefore this cast will always work
@SuppressWarnings("unchecked")
final E e = (E) o;
-
+
return this.existenceCondition.test(e);
}
-
+
@Override
public Iterator<E> iterator() {
- return conditionalExistenceIterator(this.collection.iterator(), this.existenceCondition);
+ return conditionalExistenceIterator(this.collection.iterator(),
+ this.existenceCondition);
}
-
+
@Override
public boolean remove(final Object o) {
- // remove() must be first in the && statement, otherwise it may not execute
+ // remove() must be first in the && statement, otherwise it may not
+ // execute
final boolean containedObject = this.contains(o);
return this.collection.remove(o) && containedObject;
}
-
+
@Override
public int size() {
- return (int) this.collection.stream().filter(this.existenceCondition).count();
+ return (int) this.collection.stream().filter(this.existenceCondition)
+ .count();
+ }
+
+ @Override
+ public Object[] toArray() {
+ // ensure the toArray operation is supported
+ this.collection.toArray();
+
+ // if it works, do it for real
+ return super.toArray();
+ }
+
+ @Override
+ public <T> T[] toArray(T[] a) {
+ // ensure the toArray operation is supported
+ this.collection.toArray();
+
+ // if it works, do it for real
+ return super.toArray(a);
}
}
-
+
/**
* Elements in this wrapper iterator only exist if they pass a condition.
*
* @author Adrien Hopkins
* @since 2019-10-17
- * @param <E>
- * type of elements in iterator
+ * @param <E> type of elements in iterator
*/
static final class ConditionalExistenceIterator<E> implements Iterator<E> {
final Iterator<E> iterator;
final Predicate<E> existenceCondition;
E nextElement;
boolean hasNext;
-
+
/**
* Creates the {@code ConditionalExistenceIterator}.
*
@@ -138,12 +165,13 @@ public final class ConditionalExistenceCollections {
* @param condition
* @since 2019-10-17
*/
- private ConditionalExistenceIterator(final Iterator<E> iterator, final Predicate<E> condition) {
+ private ConditionalExistenceIterator(final Iterator<E> iterator,
+ final Predicate<E> condition) {
this.iterator = iterator;
this.existenceCondition = condition;
this.getAndSetNextElement();
}
-
+
/**
* Gets the next element, and sets nextElement and hasNext accordingly.
*
@@ -160,12 +188,12 @@ public final class ConditionalExistenceCollections {
} while (!this.existenceCondition.test(this.nextElement));
this.hasNext = true;
}
-
+
@Override
public boolean hasNext() {
return this.hasNext;
}
-
+
@Override
public E next() {
if (this.hasNext()) {
@@ -175,27 +203,25 @@ public final class ConditionalExistenceCollections {
} else
throw new NoSuchElementException();
}
-
+
@Override
public void remove() {
this.iterator.remove();
}
}
-
+
/**
* Mappings in this map only exist if the entry passes some condition.
*
* @author Adrien Hopkins
* @since 2019-10-17
- * @param <K>
- * key type
- * @param <V>
- * value type
+ * @param <K> key type
+ * @param <V> value type
*/
static final class ConditionalExistenceMap<K, V> extends AbstractMap<K, V> {
Map<K, V> map;
Predicate<Entry<K, V>> entryExistenceCondition;
-
+
/**
* Creates the {@code ConditionalExistenceMap}.
*
@@ -203,205 +229,240 @@ public final class ConditionalExistenceCollections {
* @param entryExistenceCondition
* @since 2019-10-17
*/
- private ConditionalExistenceMap(final Map<K, V> map, final Predicate<Entry<K, V>> entryExistenceCondition) {
+ private ConditionalExistenceMap(final Map<K, V> map,
+ final Predicate<Entry<K, V>> entryExistenceCondition) {
this.map = map;
this.entryExistenceCondition = entryExistenceCondition;
}
-
+
@Override
public boolean containsKey(final Object key) {
if (!this.map.containsKey(key))
return false;
-
+
// only instances of K have mappings in the backing map
// since we know that key is a valid key, it must be an instance of K
@SuppressWarnings("unchecked")
final K keyAsK = (K) key;
-
+
// get and test entry
final V value = this.map.get(key);
final Entry<K, V> entry = new SimpleEntry<>(keyAsK, value);
return this.entryExistenceCondition.test(entry);
}
-
+
@Override
public Set<Entry<K, V>> entrySet() {
- return conditionalExistenceSet(this.map.entrySet(), this.entryExistenceCondition);
+ return conditionalExistenceSet(this.map.entrySet(),
+ this.entryExistenceCondition);
}
-
+
@Override
public V get(final Object key) {
return this.containsKey(key) ? this.map.get(key) : null;
}
-
+
+ private final Entry<K, V> getEntry(K key) {
+ return new Entry<K, V>() {
+ @Override
+ public K getKey() {
+ return key;
+ }
+
+ @Override
+ public V getValue() {
+ return ConditionalExistenceMap.this.map.get(key);
+ }
+
+ @Override
+ public V setValue(V value) {
+ return ConditionalExistenceMap.this.map.put(key, value);
+ }
+ };
+ }
+
@Override
public Set<K> keySet() {
- // maybe change this to use ConditionalExistenceSet
- return super.keySet();
+ return conditionalExistenceSet(super.keySet(),
+ k -> this.entryExistenceCondition.test(this.getEntry(k)));
}
-
+
@Override
public V put(final K key, final V value) {
final V oldValue = this.map.put(key, value);
-
+
// get and test entry
final Entry<K, V> entry = new SimpleEntry<>(key, oldValue);
return this.entryExistenceCondition.test(entry) ? oldValue : null;
}
-
+
@Override
public V remove(final Object key) {
final V oldValue = this.map.remove(key);
return this.containsKey(key) ? oldValue : null;
}
-
+
@Override
public Collection<V> values() {
// maybe change this to use ConditionalExistenceCollection
return super.values();
}
-
}
-
+
/**
* Elements in this set only exist if a certain condition is true.
*
* @author Adrien Hopkins
* @since 2019-10-17
- * @param <E>
- * type of element in set
+ * @param <E> type of element in set
*/
static final class ConditionalExistenceSet<E> extends AbstractSet<E> {
private final Set<E> set;
private final Predicate<E> existenceCondition;
-
+
/**
* Creates the {@code ConditionalNonexistenceSet}.
*
- * @param set
- * set to use
- * @param existenceCondition
- * condition where element exists
+ * @param set set to use
+ * @param existenceCondition condition where element exists
* @since 2019-10-17
*/
- private ConditionalExistenceSet(final Set<E> set, final Predicate<E> existenceCondition) {
+ private ConditionalExistenceSet(final Set<E> set,
+ final Predicate<E> existenceCondition) {
this.set = set;
this.existenceCondition = existenceCondition;
}
-
+
/**
* {@inheritDoc}
* <p>
- * Note that this method returns {@code false} if {@code e} does not pass the existence condition.
+ * Note that this method returns {@code false} if {@code e} does not pass
+ * the existence condition.
*/
@Override
public boolean add(final E e) {
return this.set.add(e) && this.existenceCondition.test(e);
}
-
+
@Override
public void clear() {
this.set.clear();
}
-
+
@Override
public boolean contains(final Object o) {
if (!this.set.contains(o))
return false;
-
+
// this set can only contain instances of E
- // since the object is in the set, we know that it must be an instance of E
+ // since the object is in the set, we know that it must be an instance
+ // of E
// therefore this cast will always work
@SuppressWarnings("unchecked")
final E e = (E) o;
-
+
return this.existenceCondition.test(e);
}
-
+
@Override
public Iterator<E> iterator() {
- return conditionalExistenceIterator(this.set.iterator(), this.existenceCondition);
+ return conditionalExistenceIterator(this.set.iterator(),
+ this.existenceCondition);
}
-
+
@Override
public boolean remove(final Object o) {
- // remove() must be first in the && statement, otherwise it may not execute
+ // remove() must be first in the && statement, otherwise it may not
+ // execute
final boolean containedObject = this.contains(o);
return this.set.remove(o) && containedObject;
}
-
+
@Override
public int size() {
return (int) this.set.stream().filter(this.existenceCondition).count();
}
+
+ @Override
+ public Object[] toArray() {
+ // ensure the toArray operation is supported
+ this.set.toArray();
+
+ // if it works, do it for real
+ return super.toArray();
+ }
+
+ @Override
+ public <T> T[] toArray(T[] a) {
+ // ensure the toArray operation is supported
+ this.set.toArray();
+
+ // if it works, do it for real
+ return super.toArray(a);
+ }
}
-
+
/**
- * Elements in the returned wrapper collection are ignored if they don't pass a condition.
+ * Elements in the returned wrapper collection are ignored if they don't pass
+ * a condition.
*
- * @param <E>
- * type of elements in collection
- * @param collection
- * collection to wrap
- * @param existenceCondition
- * elements only exist if this returns true
+ * @param <E> type of elements in collection
+ * @param collection collection to wrap
+ * @param existenceCondition elements only exist if this returns true
* @return wrapper collection
* @since 2019-10-17
*/
- public static final <E> Collection<E> conditionalExistenceCollection(final Collection<E> collection,
+ public static final <E> Collection<E> conditionalExistenceCollection(
+ final Collection<E> collection,
final Predicate<E> existenceCondition) {
- return new ConditionalExistenceCollection<>(collection, existenceCondition);
+ return new ConditionalExistenceCollection<>(collection,
+ existenceCondition);
}
-
+
/**
- * Elements in the returned wrapper iterator are ignored if they don't pass a condition.
+ * Elements in the returned wrapper iterator are ignored if they don't pass a
+ * condition.
*
- * @param <E>
- * type of elements in iterator
- * @param iterator
- * iterator to wrap
- * @param existenceCondition
- * elements only exist if this returns true
+ * @param <E> type of elements in iterator
+ * @param iterator iterator to wrap
+ * @param existenceCondition elements only exist if this returns true
* @return wrapper iterator
* @since 2019-10-17
*/
- public static final <E> Iterator<E> conditionalExistenceIterator(final Iterator<E> iterator,
- final Predicate<E> existenceCondition) {
+ public static final <E> Iterator<E> conditionalExistenceIterator(
+ final Iterator<E> iterator, final Predicate<E> existenceCondition) {
return new ConditionalExistenceIterator<>(iterator, existenceCondition);
}
-
+
/**
- * Mappings in the returned wrapper map are ignored if the corresponding entry doesn't pass a condition
+ * Mappings in the returned wrapper map are ignored if the corresponding
+ * entry doesn't pass a condition
*
- * @param <K>
- * type of key in map
- * @param <V>
- * type of value in map
- * @param map
- * map to wrap
- * @param entryExistenceCondition
- * mappings only exist if this returns true
+ * @param <K> type of key in map
+ * @param <V> type of value in map
+ * @param map map to wrap
+ * @param entryExistenceCondition mappings only exist if this returns true
* @return wrapper map
* @since 2019-10-17
*/
- public static final <K, V> Map<K, V> conditionalExistenceMap(final Map<K, V> map,
+ public static final <K, V> Map<K, V> conditionalExistenceMap(
+ final Map<K, V> map,
final Predicate<Entry<K, V>> entryExistenceCondition) {
return new ConditionalExistenceMap<>(map, entryExistenceCondition);
}
-
+
/**
- * Elements in the returned wrapper set are ignored if they don't pass a condition.
+ * Elements in the returned wrapper set are ignored if they don't pass a
+ * condition.
*
- * @param <E>
- * type of elements in set
- * @param set
- * set to wrap
- * @param existenceCondition
- * elements only exist if this returns true
+ * @param <E> type of elements in set
+ * @param set set to wrap
+ * @param existenceCondition elements only exist if this returns true
* @return wrapper set
* @since 2019-10-17
*/
- public static final <E> Set<E> conditionalExistenceSet(final Set<E> set, final Predicate<E> existenceCondition) {
+ public static final <E> Set<E> conditionalExistenceSet(final Set<E> set,
+ final Predicate<E> existenceCondition) {
return new ConditionalExistenceSet<>(set, existenceCondition);
}
}
diff --git a/src/org/unitConverter/math/DecimalComparison.java b/src/org/unitConverter/math/DecimalComparison.java
index 859e8da..0f5b91e 100644
--- a/src/org/unitConverter/math/DecimalComparison.java
+++ b/src/org/unitConverter/math/DecimalComparison.java
@@ -27,42 +27,45 @@ import java.math.BigDecimal;
*/
public final class DecimalComparison {
/**
- * The value used for double comparison. If two double values are within this value multiplied by the larger value,
- * they are considered equal.
+ * The value used for double comparison. If two double values are within this
+ * value multiplied by the larger value, they are considered equal.
*
* @since 2019-03-18
* @since v0.2.0
*/
public static final double DOUBLE_EPSILON = 1.0e-15;
-
+
/**
- * The value used for float comparison. If two float values are within this value multiplied by the larger value,
- * they are considered equal.
+ * The value used for float comparison. If two float values are within this
+ * value multiplied by the larger value, they are considered equal.
*
* @since 2019-03-18
* @since v0.2.0
*/
public static final float FLOAT_EPSILON = 1.0e-6f;
-
+
/**
* Tests for equality of double values using {@link #DOUBLE_EPSILON}.
* <p>
- * <strong>WARNING: </strong>this method is not technically transitive. If a and b are off by slightly less than
- * {@code epsilon * max(abs(a), abs(b))}, and b and c are off by slightly less than
- * {@code epsilon * max(abs(b), abs(c))}, then equals(a, b) and equals(b, c) will both return true, but equals(a, c)
- * will return false. However, this situation is very unlikely to ever happen in a real programming situation.
+ * <strong>WARNING: </strong>this method is not technically transitive. If a
+ * and b are off by slightly less than {@code epsilon * max(abs(a), abs(b))},
+ * and b and c are off by slightly less than
+ * {@code epsilon * max(abs(b), abs(c))}, then equals(a, b) and equals(b, c)
+ * will both return true, but equals(a, c) will return false. However, this
+ * situation is very unlikely to ever happen in a real programming situation.
* <p>
* If this does become a concern, some ways to solve this problem:
* <ol>
- * <li>Raise the value of epsilon using {@link #equals(double, double, double)} (this does not make a violation of
- * transitivity impossible, it just significantly reduces the chances of it happening)
- * <li>Use {@link BigDecimal} instead of {@code double} (this will make a violation of transitivity 100% impossible)
+ * <li>Raise the value of epsilon using
+ * {@link #equals(double, double, double)} (this does not make a violation of
+ * transitivity impossible, it just significantly reduces the chances of it
+ * happening)
+ * <li>Use {@link BigDecimal} instead of {@code double} (this will make a
+ * violation of transitivity 100% impossible)
* </ol>
*
- * @param a
- * first value to test
- * @param b
- * second value to test
+ * @param a first value to test
+ * @param b second value to test
* @return whether they are equal
* @since 2019-03-18
* @since v0.2.0
@@ -71,57 +74,61 @@ public final class DecimalComparison {
public static final boolean equals(final double a, final double b) {
return DecimalComparison.equals(a, b, DOUBLE_EPSILON);
}
-
+
/**
* Tests for double equality using a custom epsilon value.
*
* <p>
- * <strong>WARNING: </strong>this method is not technically transitive. If a and b are off by slightly less than
- * {@code epsilon * max(abs(a), abs(b))}, and b and c are off by slightly less than
- * {@code epsilon * max(abs(b), abs(c))}, then equals(a, b) and equals(b, c) will both return true, but equals(a, c)
- * will return false. However, this situation is very unlikely to ever happen in a real programming situation.
+ * <strong>WARNING: </strong>this method is not technically transitive. If a
+ * and b are off by slightly less than {@code epsilon * max(abs(a), abs(b))},
+ * and b and c are off by slightly less than
+ * {@code epsilon * max(abs(b), abs(c))}, then equals(a, b) and equals(b, c)
+ * will both return true, but equals(a, c) will return false. However, this
+ * situation is very unlikely to ever happen in a real programming situation.
* <p>
* If this does become a concern, some ways to solve this problem:
* <ol>
- * <li>Raise the value of epsilon (this does not make a violation of transitivity impossible, it just significantly
- * reduces the chances of it happening)
- * <li>Use {@link BigDecimal} instead of {@code double} (this will make a violation of transitivity 100% impossible)
+ * <li>Raise the value of epsilon (this does not make a violation of
+ * transitivity impossible, it just significantly reduces the chances of it
+ * happening)
+ * <li>Use {@link BigDecimal} instead of {@code double} (this will make a
+ * violation of transitivity 100% impossible)
* </ol>
*
- * @param a
- * first value to test
- * @param b
- * second value to test
- * @param epsilon
- * allowed difference
+ * @param a first value to test
+ * @param b second value to test
+ * @param epsilon allowed difference
* @return whether they are equal
* @since 2019-03-18
* @since v0.2.0
*/
- public static final boolean equals(final double a, final double b, final double epsilon) {
+ public static final boolean equals(final double a, final double b,
+ final double epsilon) {
return Math.abs(a - b) <= epsilon * Math.max(Math.abs(a), Math.abs(b));
}
-
+
/**
* Tests for equality of float values using {@link #FLOAT_EPSILON}.
*
* <p>
- * <strong>WARNING: </strong>this method is not technically transitive. If a and b are off by slightly less than
- * {@code epsilon * max(abs(a), abs(b))}, and b and c are off by slightly less than
- * {@code epsilon * max(abs(b), abs(c))}, then equals(a, b) and equals(b, c) will both return true, but equals(a, c)
- * will return false. However, this situation is very unlikely to ever happen in a real programming situation.
+ * <strong>WARNING: </strong>this method is not technically transitive. If a
+ * and b are off by slightly less than {@code epsilon * max(abs(a), abs(b))},
+ * and b and c are off by slightly less than
+ * {@code epsilon * max(abs(b), abs(c))}, then equals(a, b) and equals(b, c)
+ * will both return true, but equals(a, c) will return false. However, this
+ * situation is very unlikely to ever happen in a real programming situation.
* <p>
* If this does become a concern, some ways to solve this problem:
* <ol>
- * <li>Raise the value of epsilon using {@link #equals(float, float, float)} (this does not make a violation of
- * transitivity impossible, it just significantly reduces the chances of it happening)
- * <li>Use {@link BigDecimal} instead of {@code float} (this will make a violation of transitivity 100% impossible)
+ * <li>Raise the value of epsilon using {@link #equals(float, float, float)}
+ * (this does not make a violation of transitivity impossible, it just
+ * significantly reduces the chances of it happening)
+ * <li>Use {@link BigDecimal} instead of {@code float} (this will make a
+ * violation of transitivity 100% impossible)
* </ol>
*
- * @param a
- * first value to test
- * @param b
- * second value to test
+ * @param a first value to test
+ * @param b second value to test
* @return whether they are equal
* @since 2019-03-18
* @since v0.2.0
@@ -129,53 +136,121 @@ public final class DecimalComparison {
public static final boolean equals(final float a, final float b) {
return DecimalComparison.equals(a, b, FLOAT_EPSILON);
}
-
+
/**
* Tests for float equality using a custom epsilon value.
*
* <p>
- * <strong>WARNING: </strong>this method is not technically transitive. If a and b are off by slightly less than
- * {@code epsilon * max(abs(a), abs(b))}, and b and c are off by slightly less than
- * {@code epsilon * max(abs(b), abs(c))}, then equals(a, b) and equals(b, c) will both return true, but equals(a, c)
- * will return false. However, this situation is very unlikely to ever happen in a real programming situation.
+ * <strong>WARNING: </strong>this method is not technically transitive. If a
+ * and b are off by slightly less than {@code epsilon * max(abs(a), abs(b))},
+ * and b and c are off by slightly less than
+ * {@code epsilon * max(abs(b), abs(c))}, then equals(a, b) and equals(b, c)
+ * will both return true, but equals(a, c) will return false. However, this
+ * situation is very unlikely to ever happen in a real programming situation.
* <p>
* If this does become a concern, some ways to solve this problem:
* <ol>
- * <li>Raise the value of epsilon (this does not make a violation of transitivity impossible, it just significantly
- * reduces the chances of it happening)
- * <li>Use {@link BigDecimal} instead of {@code float} (this will make a violation of transitivity 100% impossible)
+ * <li>Raise the value of epsilon (this does not make a violation of
+ * transitivity impossible, it just significantly reduces the chances of it
+ * happening)
+ * <li>Use {@link BigDecimal} instead of {@code float} (this will make a
+ * violation of transitivity 100% impossible)
* </ol>
*
- * @param a
- * first value to test
- * @param b
- * second value to test
- * @param epsilon
- * allowed difference
+ * @param a first value to test
+ * @param b second value to test
+ * @param epsilon allowed difference
* @return whether they are equal
* @since 2019-03-18
* @since v0.2.0
*/
- public static final boolean equals(final float a, final float b, final float epsilon) {
+ public static final boolean equals(final float a, final float b,
+ final float epsilon) {
return Math.abs(a - b) <= epsilon * Math.max(Math.abs(a), Math.abs(b));
}
-
+
+ /**
+ * Tests for equality of {@code UncertainDouble} values using
+ * {@link #DOUBLE_EPSILON}.
+ * <p>
+ * <strong>WARNING: </strong>this method is not technically transitive. If a
+ * and b are off by slightly less than {@code epsilon * max(abs(a), abs(b))},
+ * and b and c are off by slightly less than
+ * {@code epsilon * max(abs(b), abs(c))}, then equals(a, b) and equals(b, c)
+ * will both return true, but equals(a, c) will return false. However, this
+ * situation is very unlikely to ever happen in a real programming situation.
+ * <p>
+ * If this does become a concern, some ways to solve this problem:
+ * <ol>
+ * <li>Raise the value of epsilon using
+ * {@link #equals(UncertainDouble, UncertainDouble, double)} (this does not
+ * make a violation of transitivity impossible, it just significantly reduces
+ * the chances of it happening)
+ * <li>Use {@link BigDecimal} instead of {@code double} (this will make a
+ * violation of transitivity 100% impossible)
+ * </ol>
+ *
+ * @param a first value to test
+ * @param b second value to test
+ * @return whether they are equal
+ * @since 2020-09-07
+ * @see #hashCode(double)
+ */
+ public static final boolean equals(final UncertainDouble a,
+ final UncertainDouble b) {
+ return DecimalComparison.equals(a.value(), b.value())
+ && DecimalComparison.equals(a.uncertainty(), b.uncertainty());
+ }
+
+ /**
+ * Tests for {@code UncertainDouble} equality using a custom epsilon value.
+ *
+ * <p>
+ * <strong>WARNING: </strong>this method is not technically transitive. If a
+ * and b are off by slightly less than {@code epsilon * max(abs(a), abs(b))},
+ * and b and c are off by slightly less than
+ * {@code epsilon * max(abs(b), abs(c))}, then equals(a, b) and equals(b, c)
+ * will both return true, but equals(a, c) will return false. However, this
+ * situation is very unlikely to ever happen in a real programming situation.
+ * <p>
+ * If this does become a concern, some ways to solve this problem:
+ * <ol>
+ * <li>Raise the value of epsilon (this does not make a violation of
+ * transitivity impossible, it just significantly reduces the chances of it
+ * happening)
+ * <li>Use {@link BigDecimal} instead of {@code double} (this will make a
+ * violation of transitivity 100% impossible)
+ * </ol>
+ *
+ * @param a first value to test
+ * @param b second value to test
+ * @param epsilon allowed difference
+ * @return whether they are equal
+ * @since 2019-03-18
+ * @since v0.2.0
+ */
+ public static final boolean equals(final UncertainDouble a,
+ final UncertainDouble b, final double epsilon) {
+ return DecimalComparison.equals(a.value(), b.value(), epsilon)
+ && DecimalComparison.equals(a.uncertainty(), b.uncertainty(),
+ epsilon);
+ }
+
/**
- * Takes the hash code of doubles. Values that are equal according to {@link #equals(double, double)} will have the
- * same hash code.
+ * Takes the hash code of doubles. Values that are equal according to
+ * {@link #equals(double, double)} will have the same hash code.
*
- * @param d
- * double to hash
+ * @param d double to hash
* @return hash code of double
* @since 2019-10-16
*/
public static final int hash(final double d) {
return Float.hashCode((float) d);
}
-
+
// You may NOT get any DecimalComparison instances
private DecimalComparison() {
throw new AssertionError();
}
-
+
}
diff --git a/src/org/unitConverter/math/UncertainDouble.java b/src/org/unitConverter/math/UncertainDouble.java
new file mode 100644
index 0000000..3651bd5
--- /dev/null
+++ b/src/org/unitConverter/math/UncertainDouble.java
@@ -0,0 +1,419 @@
+/**
+ * Copyright (C) 2020 Adrien Hopkins
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package org.unitConverter.math;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.util.Objects;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * A double with an associated uncertainty value. For example, 3.2 ± 0.2.
+ * <p>
+ * All methods in this class throw a NullPointerException if any of their
+ * arguments is null.
+ *
+ * @since 2020-09-07
+ */
+public final class UncertainDouble implements Comparable<UncertainDouble> {
+ /**
+ * The exact value 0
+ */
+ public static final UncertainDouble ZERO = UncertainDouble.of(0, 0);
+
+ /**
+ * A regular expression that can recognize toString forms
+ */
+ private static final Pattern TO_STRING = Pattern
+ .compile("([a-zA-Z_0-9\\.\\,]+)" // a number
+ // optional "± [number]"
+ + "(?:\\s*(?:±|\\+-)\\s*([a-zA-Z_0-9\\.\\,]+))?");
+
+ /**
+ * Parses a string in the form of {@link UncertainDouble#toString(boolean)}
+ * and returns the corresponding {@code UncertainDouble} instance.
+ * <p>
+ * This method allows some alternative forms of the string representation,
+ * such as using "+-" instead of "±".
+ *
+ * @param s string to parse
+ * @return {@code UncertainDouble} instance
+ * @throws IllegalArgumentException if the string is invalid
+ * @since 2020-09-07
+ */
+ public static final UncertainDouble fromString(String s) {
+ Objects.requireNonNull(s, "s may not be null");
+ final Matcher matcher = TO_STRING.matcher(s);
+
+ double value, uncertainty;
+ try {
+ value = Double.parseDouble(matcher.group(1));
+ } catch (IllegalStateException | NumberFormatException e) {
+ throw new IllegalArgumentException(
+ "String " + s + " not in correct format.");
+ }
+
+ final String uncertaintyString = matcher.group(2);
+ if (uncertaintyString == null) {
+ uncertainty = 0;
+ } else {
+ try {
+ uncertainty = Double.parseDouble(uncertaintyString);
+ } catch (final NumberFormatException e) {
+ throw new IllegalArgumentException(
+ "String " + s + " not in correct format.");
+ }
+ }
+
+ return UncertainDouble.of(value, uncertainty);
+ }
+
+ /**
+ * Gets an {@code UncertainDouble} from its value and <b>absolute</b>
+ * uncertainty.
+ *
+ * @since 2020-09-07
+ */
+ public static final UncertainDouble of(double value, double uncertainty) {
+ return new UncertainDouble(value, uncertainty);
+ }
+
+ /**
+ * Gets an {@code UncertainDouble} from its value and <b>relative</b>
+ * uncertainty.
+ *
+ * @since 2020-09-07
+ */
+ public static final UncertainDouble ofRelative(double value,
+ double relativeUncertainty) {
+ return new UncertainDouble(value, value * relativeUncertainty);
+ }
+
+ private final double value;
+
+ private final double uncertainty;
+
+ /**
+ * @param value
+ * @param uncertainty
+ * @since 2020-09-07
+ */
+ private UncertainDouble(double value, double uncertainty) {
+ this.value = value;
+ // uncertainty should only ever be positive
+ this.uncertainty = Math.abs(uncertainty);
+ }
+
+ /**
+ * Compares this {@code UncertainDouble} with another
+ * {@code UncertainDouble}.
+ * <p>
+ * This method only compares the values, not the uncertainties. So 3.1 ± 0.5
+ * is considered less than 3.2 ± 0.5, even though they are equivalent.
+ * <p>
+ * <b>Note:</b> The natural ordering of this class is inconsistent with
+ * equals. Specifically, if two {@code UncertainDouble} instances {@code a}
+ * and {@code b} have the same value but different uncertainties,
+ * {@code a.compareTo(b)} will return 0 but {@code a.equals(b)} will return
+ * {@code false}.
+ */
+ @Override
+ public final int compareTo(UncertainDouble o) {
+ return Double.compare(this.value, o.value);
+ }
+
+ /**
+ * Returns the quotient of {@code this} and {@code other}.
+ *
+ * @since 2020-09-07
+ */
+ public final UncertainDouble dividedBy(UncertainDouble other) {
+ Objects.requireNonNull(other, "other may not be null");
+ return UncertainDouble.ofRelative(this.value / other.value, Math
+ .hypot(this.relativeUncertainty(), other.relativeUncertainty()));
+ }
+
+ /**
+ * Returns the quotient of {@code this} and the exact value {@code other}.
+ *
+ * @since 2020-09-07
+ */
+ public final UncertainDouble dividedByExact(double other) {
+ return UncertainDouble.of(this.value / other, this.uncertainty / other);
+ }
+
+ @Override
+ public final boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (!(obj instanceof UncertainDouble))
+ return false;
+ final UncertainDouble other = (UncertainDouble) obj;
+ if (Double.compare(this.value, other.value) != 0)
+ return false;
+ if (Double.compare(this.uncertainty, other.uncertainty) != 0)
+ return false;
+ return true;
+ }
+
+ /**
+ * @param other another {@code UncertainDouble}
+ * @return true iff this and {@code other} are within each other's
+ * uncertainty range.
+ * @since 2020-09-07
+ */
+ public final boolean equivalent(UncertainDouble other) {
+ Objects.requireNonNull(other, "other may not be null");
+ return Math.abs(this.value - other.value) <= Math.min(this.uncertainty,
+ other.uncertainty);
+ }
+
+ /**
+ * Gets the preferred scale for rounding a value for toString.
+ *
+ * @since 2020-09-07
+ */
+ private final int getDisplayScale() {
+ // round based on uncertainty
+ // if uncertainty starts with 1 (ignoring zeroes and the decimal
+ // point), rounds
+ // so that uncertainty has 2 significant digits.
+ // otherwise, rounds so that uncertainty has 1 significant digits.
+ // the value is rounded to the same number of decimal places as the
+ // uncertainty.
+ final BigDecimal bigUncertainty = BigDecimal.valueOf(this.uncertainty);
+
+ // the scale that will give the uncertainty two decimal places
+ final int twoDecimalPlacesScale = bigUncertainty.scale()
+ - bigUncertainty.precision() + 2;
+ final BigDecimal roundedUncertainty = bigUncertainty
+ .setScale(twoDecimalPlacesScale, RoundingMode.HALF_EVEN);
+
+ if (roundedUncertainty.unscaledValue().intValue() >= 20)
+ return twoDecimalPlacesScale - 1; // one decimal place
+ else
+ return twoDecimalPlacesScale;
+ }
+
+ @Override
+ public final int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + Double.hashCode(this.value);
+ result = prime * result + Double.hashCode(this.uncertainty);
+ return result;
+ }
+
+ /**
+ * @return true iff the value has no uncertainty
+ *
+ * @since 2020-09-07
+ */
+ public final boolean isExact() {
+ return this.uncertainty == 0;
+ }
+
+ /**
+ * Returns the difference of {@code this} and {@code other}.
+ *
+ * @since 2020-09-07
+ */
+ public final UncertainDouble minus(UncertainDouble other) {
+ Objects.requireNonNull(other, "other may not be null");
+ return UncertainDouble.of(this.value - other.value,
+ Math.hypot(this.uncertainty, other.uncertainty));
+ }
+
+ /**
+ * Returns the difference of {@code this} and the exact value {@code other}.
+ *
+ * @since 2020-09-07
+ */
+ public final UncertainDouble minusExact(double other) {
+ return UncertainDouble.of(this.value - other, this.uncertainty);
+ }
+
+ /**
+ * Returns the sum of {@code this} and {@code other}.
+ *
+ * @since 2020-09-07
+ */
+ public final UncertainDouble plus(UncertainDouble other) {
+ Objects.requireNonNull(other, "other may not be null");
+ return UncertainDouble.of(this.value + other.value,
+ Math.hypot(this.uncertainty, other.uncertainty));
+ }
+
+ /**
+ * Returns the sum of {@code this} and the exact value {@code other}.
+ *
+ * @since 2020-09-07
+ */
+ public final UncertainDouble plusExact(double other) {
+ return UncertainDouble.of(this.value + other, this.uncertainty);
+ }
+
+ /**
+ * @return relative uncertainty
+ * @since 2020-09-07
+ */
+ public final double relativeUncertainty() {
+ return this.uncertainty / this.value;
+ }
+
+ /**
+ * Returns the product of {@code this} and {@code other}.
+ *
+ * @since 2020-09-07
+ */
+ public final UncertainDouble times(UncertainDouble other) {
+ Objects.requireNonNull(other, "other may not be null");
+ return UncertainDouble.ofRelative(this.value * other.value, Math
+ .hypot(this.relativeUncertainty(), other.relativeUncertainty()));
+ }
+
+ /**
+ * Returns the product of {@code this} and the exact value {@code other}.
+ *
+ * @since 2020-09-07
+ */
+ public final UncertainDouble timesExact(double other) {
+ return UncertainDouble.of(this.value * other, this.uncertainty * other);
+ }
+
+ /**
+ * Returns the result of {@code this} raised to the exponent {@code other}.
+ *
+ * @since 2020-09-07
+ */
+ public final UncertainDouble toExponent(UncertainDouble other) {
+ Objects.requireNonNull(other, "other may not be null");
+
+ final double result = Math.pow(this.value, other.value);
+ final double relativeUncertainty = Math.hypot(
+ other.value * this.relativeUncertainty(),
+ Math.log(this.value) * other.uncertainty);
+
+ return UncertainDouble.ofRelative(result, relativeUncertainty);
+ }
+
+ /**
+ * Returns the result of {@code this} raised the exact exponent
+ * {@code other}.
+ *
+ * @since 2020-09-07
+ */
+ public final UncertainDouble toExponentExact(double other) {
+ return UncertainDouble.ofRelative(Math.pow(this.value, other),
+ this.relativeUncertainty() * other);
+ }
+
+ /**
+ * Returns a string representation of this {@code UncertainDouble}.
+ * <p>
+ * This method returns the same value as {@link #toString(boolean)}, but
+ * {@code showUncertainty} is true if and only if the uncertainty is
+ * non-zero.
+ *
+ * <p>
+ * Examples:
+ *
+ * <pre>
+ * UncertainDouble.of(3.27, 0.22).toString() = "3.3 ± 0.2"
+ * UncertainDouble.of(3.27, 0.13).toString() = "3.27 ± 0.13"
+ * UncertainDouble.of(-5.01, 0).toString() = "-5.01"
+ * </pre>
+ *
+ * @since 2020-09-07
+ */
+ @Override
+ public final String toString() {
+ return this.toString(!this.isExact());
+ }
+
+ /**
+ * Returns a string representation of this {@code UncertainDouble}.
+ * <p>
+ * If {@code showUncertainty} is true, the string will be of the form "VALUE
+ * ± UNCERTAINTY", and if it is false the string will be of the form "VALUE"
+ * <p>
+ * VALUE represents a string representation of this {@code UncertainDouble}'s
+ * value. If the uncertainty is non-zero, the string will be rounded to the
+ * same precision as the uncertainty, otherwise it will not be rounded. The
+ * string is still rounded if {@code showUncertainty} is false.<br>
+ * UNCERTAINTY represents a string representation of this
+ * {@code UncertainDouble}'s uncertainty. If the uncertainty ends in 1X
+ * (where X represents any digit) it will be rounded to two significant
+ * digits otherwise it will be rounded to one significant digit.
+ * <p>
+ * Examples:
+ *
+ * <pre>
+ * UncertainDouble.of(3.27, 0.22).toString(false) = "3.3"
+ * UncertainDouble.of(3.27, 0.22).toString(true) = "3.3 ± 0.2"
+ * UncertainDouble.of(3.27, 0.13).toString(false) = "3.27"
+ * UncertainDouble.of(3.27, 0.13).toString(true) = "3.27 ± 0.13"
+ * UncertainDouble.of(-5.01, 0).toString(false) = "-5.01"
+ * UncertainDouble.of(-5.01, 0).toString(true) = "-5.01 ± 0.0"
+ * </pre>
+ *
+ * @since 2020-09-07
+ */
+ public final String toString(boolean showUncertainty) {
+ String valueString, uncertaintyString;
+
+ // generate the string representation of value and uncertainty
+ if (this.isExact()) {
+ uncertaintyString = "0.0";
+ valueString = Double.toString(this.value);
+
+ } else {
+ // round the value and uncertainty according to getDisplayScale()
+ final BigDecimal bigValue = BigDecimal.valueOf(this.value);
+ final BigDecimal bigUncertainty = BigDecimal.valueOf(this.uncertainty);
+
+ final int displayScale = this.getDisplayScale();
+ final BigDecimal roundedUncertainty = bigUncertainty
+ .setScale(displayScale, RoundingMode.HALF_EVEN);
+ final BigDecimal roundedValue = bigValue.setScale(displayScale,
+ RoundingMode.HALF_EVEN);
+
+ valueString = roundedValue.toString();
+ uncertaintyString = roundedUncertainty.toString();
+ }
+
+ // return "value" or "value ± uncertainty" depending on showUncertainty
+ return valueString + (showUncertainty ? " ± " + uncertaintyString : "");
+ }
+
+ /**
+ * @return absolute uncertainty
+ * @since 2020-09-07
+ */
+ public final double uncertainty() {
+ return this.uncertainty;
+ }
+
+ /**
+ * @return value without uncertainty
+ * @since 2020-09-07
+ */
+ public final double value() {
+ return this.value;
+ }
+}
diff --git a/src/org/unitConverter/unit/BaseUnit.java b/src/org/unitConverter/unit/BaseUnit.java
index d9f7965..6757bd0 100644
--- a/src/org/unitConverter/unit/BaseUnit.java
+++ b/src/org/unitConverter/unit/BaseUnit.java
@@ -23,8 +23,9 @@ import java.util.Set;
/**
* A unit that other units are defined by.
* <p>
- * Note that BaseUnits <b>must</b> have names and symbols. This is because they are used for toString code. Therefore,
- * the Optionals provided by {@link #getPrimaryName} and {@link #getSymbol} will always contain a value.
+ * Note that BaseUnits <b>must</b> have names and symbols. This is because they
+ * are used for toString code. Therefore, the Optionals provided by
+ * {@link #getPrimaryName} and {@link #getSymbol} will always contain a value.
*
* @author Adrien Hopkins
* @since 2019-10-16
@@ -33,63 +34,56 @@ public final class BaseUnit extends Unit {
/**
* Gets a base unit from the dimension it measures, its name and its symbol.
*
- * @param dimension
- * dimension measured by this unit
- * @param name
- * name of unit
- * @param symbol
- * symbol of unit
+ * @param dimension dimension measured by this unit
+ * @param name name of unit
+ * @param symbol symbol of unit
* @return base unit
* @since 2019-10-16
*/
- public static BaseUnit valueOf(final BaseDimension dimension, final String name, final String symbol) {
+ public static BaseUnit valueOf(final BaseDimension dimension,
+ final String name, final String symbol) {
return new BaseUnit(dimension, name, symbol, new HashSet<>());
}
-
+
/**
* Gets a base unit from the dimension it measures, its name and its symbol.
*
- * @param dimension
- * dimension measured by this unit
- * @param name
- * name of unit
- * @param symbol
- * symbol of unit
+ * @param dimension dimension measured by this unit
+ * @param name name of unit
+ * @param symbol symbol of unit
* @return base unit
* @since 2019-10-21
*/
- public static BaseUnit valueOf(final BaseDimension dimension, final String name, final String symbol,
- final Set<String> otherNames) {
+ public static BaseUnit valueOf(final BaseDimension dimension,
+ final String name, final String symbol, final Set<String> otherNames) {
return new BaseUnit(dimension, name, symbol, otherNames);
}
-
+
/**
* The dimension measured by this base unit.
*/
private final BaseDimension dimension;
-
+
/**
* Creates the {@code BaseUnit}.
*
- * @param dimension
- * dimension of unit
- * @param primaryName
- * name of unit
- * @param symbol
- * symbol of unit
- * @throws NullPointerException
- * if any argument is null
+ * @param dimension dimension of unit
+ * @param primaryName name of unit
+ * @param symbol symbol of unit
+ * @throws NullPointerException if any argument is null
* @since 2019-10-16
*/
- private BaseUnit(final BaseDimension dimension, final String primaryName, final String symbol,
- final Set<String> otherNames) {
+ private BaseUnit(final BaseDimension dimension, final String primaryName,
+ final String symbol, final Set<String> otherNames) {
super(primaryName, symbol, otherNames);
- this.dimension = Objects.requireNonNull(dimension, "dimension must not be null.");
+ this.dimension = Objects.requireNonNull(dimension,
+ "dimension must not be null.");
}
-
+
/**
- * Returns a {@code LinearUnit} with this unit as a base and a conversion factor of 1. This operation must be done
- * in order to allow units to be created with operations.
+ * Returns a {@code LinearUnit} with this unit as a base and a conversion
+ * factor of 1. This operation must be done in order to allow units to be
+ * created with operations.
*
* @return this unit as a {@code LinearUnit}
* @since 2019-10-16
@@ -97,17 +91,17 @@ public final class BaseUnit extends Unit {
public LinearUnit asLinearUnit() {
return LinearUnit.valueOf(this.getBase(), 1);
}
-
+
@Override
- public double convertFromBase(final double value) {
+ protected double convertFromBase(final double value) {
return value;
}
-
+
@Override
- public double convertToBase(final double value) {
+ protected double convertToBase(final double value) {
return value;
}
-
+
/**
* @return dimension
* @since 2019-10-16
@@ -115,21 +109,25 @@ public final class BaseUnit extends Unit {
public final BaseDimension getBaseDimension() {
return this.dimension;
}
-
+
@Override
public String toString() {
return this.getPrimaryName().orElse("Unnamed unit")
- + (this.getSymbol().isPresent() ? String.format(" (%s)", this.getSymbol().get()) : "");
+ + (this.getSymbol().isPresent()
+ ? String.format(" (%s)", this.getSymbol().get())
+ : "");
}
-
+
@Override
public BaseUnit withName(final NameSymbol ns) {
Objects.requireNonNull(ns, "ns must not be null.");
if (!ns.getPrimaryName().isPresent())
- throw new IllegalArgumentException("BaseUnits must have primary names.");
+ throw new IllegalArgumentException(
+ "BaseUnits must have primary names.");
if (!ns.getSymbol().isPresent())
throw new IllegalArgumentException("BaseUnits must have symbols.");
- return BaseUnit.valueOf(this.getBaseDimension(), ns.getPrimaryName().get(), ns.getSymbol().get(),
+ return BaseUnit.valueOf(this.getBaseDimension(),
+ ns.getPrimaryName().get(), ns.getSymbol().get(),
ns.getOtherNames());
}
}
diff --git a/src/org/unitConverter/unit/FunctionalUnitlike.java b/src/org/unitConverter/unit/FunctionalUnitlike.java
new file mode 100644
index 0000000..21c1fca
--- /dev/null
+++ b/src/org/unitConverter/unit/FunctionalUnitlike.java
@@ -0,0 +1,72 @@
+/**
+ * Copyright (C) 2020 Adrien Hopkins
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package org.unitConverter.unit;
+
+import java.util.function.DoubleFunction;
+import java.util.function.ToDoubleFunction;
+
+import org.unitConverter.math.ObjectProduct;
+
+/**
+ * A unitlike form that converts using two conversion functions.
+ *
+ * @since 2020-09-07
+ */
+final class FunctionalUnitlike<V> extends Unitlike<V> {
+ /**
+ * A function that accepts a value in the unitlike form's base and returns a
+ * value in the unitlike form.
+ *
+ * @since 2020-09-07
+ */
+ private final DoubleFunction<V> converterFrom;
+
+ /**
+ * A function that accepts a value in the unitlike form and returns a value
+ * in the unitlike form's base.
+ */
+ private final ToDoubleFunction<V> converterTo;
+
+ /**
+ * Creates the {@code FunctionalUnitlike}.
+ *
+ * @param base unitlike form's base
+ * @param converterFrom function that accepts a value in the unitlike form's
+ * base and returns a value in the unitlike form.
+ * @param converterTo function that accepts a value in the unitlike form
+ * and returns a value in the unitlike form's base.
+ * @throws NullPointerException if any argument is null
+ * @since 2019-05-22
+ */
+ protected FunctionalUnitlike(ObjectProduct<BaseUnit> unitBase, NameSymbol ns,
+ DoubleFunction<V> converterFrom, ToDoubleFunction<V> converterTo) {
+ super(unitBase, ns);
+ this.converterFrom = converterFrom;
+ this.converterTo = converterTo;
+ }
+
+ @Override
+ protected V convertFromBase(double value) {
+ return this.converterFrom.apply(value);
+ }
+
+ @Override
+ protected double convertToBase(V value) {
+ return this.converterTo.applyAsDouble(value);
+ }
+
+}
diff --git a/src/org/unitConverter/unit/LinearUnit.java b/src/org/unitConverter/unit/LinearUnit.java
index 1e5ae53..b7f33d5 100644
--- a/src/org/unitConverter/unit/LinearUnit.java
+++ b/src/org/unitConverter/unit/LinearUnit.java
@@ -20,89 +20,101 @@ import java.util.Objects;
import org.unitConverter.math.DecimalComparison;
import org.unitConverter.math.ObjectProduct;
+import org.unitConverter.math.UncertainDouble;
/**
- * A unit that can be expressed as a product of its base and a number. For example, kilometres, inches and pounds.
+ * A unit that can be expressed as a product of its base and a number. For
+ * example, kilometres, inches and pounds.
*
* @author Adrien Hopkins
* @since 2019-10-16
*/
public final class LinearUnit extends Unit {
/**
- * Gets a {@code LinearUnit} from a unit and a value. For example, converts '59 °F' to a linear unit with the value
- * of '288.15 K'
+ * Gets a {@code LinearUnit} from a unit and a value. For example, converts
+ * '59 °F' to a linear unit with the value of '288.15 K'
*
- * @param unit
- * unit to convert
- * @param value
- * value to convert
+ * @param unit unit to convert
+ * @param value value to convert
* @return value expressed as a {@code LinearUnit}
* @since 2019-10-16
- * @throws NullPointerException
- * if unit is null
+ * @throws NullPointerException if unit is null
*/
public static LinearUnit fromUnitValue(final Unit unit, final double value) {
- return new LinearUnit(Objects.requireNonNull(unit, "unit must not be null.").getBase(),
+ return new LinearUnit(
+ Objects.requireNonNull(unit, "unit must not be null.").getBase(),
unit.convertToBase(value), NameSymbol.EMPTY);
}
-
+
/**
- * Gets a {@code LinearUnit} from a unit and a value. For example, converts '59 °F' to a linear unit with the value
- * of '288.15 K'
+ * Gets a {@code LinearUnit} from a unit and a value. For example, converts
+ * '59 °F' to a linear unit with the value of '288.15 K'
*
- * @param unit
- * unit to convert
- * @param value
- * value to convert
- * @param ns
- * name(s) and symbol of unit
+ * @param unit unit to convert
+ * @param value value to convert
+ * @param ns name(s) and symbol of unit
* @return value expressed as a {@code LinearUnit}
* @since 2019-10-21
- * @throws NullPointerException
- * if unit or ns is null
+ * @throws NullPointerException if unit or ns is null
*/
- public static LinearUnit fromUnitValue(final Unit unit, final double value, final NameSymbol ns) {
- return new LinearUnit(Objects.requireNonNull(unit, "unit must not be null.").getBase(),
+ public static LinearUnit fromUnitValue(final Unit unit, final double value,
+ final NameSymbol ns) {
+ return new LinearUnit(
+ Objects.requireNonNull(unit, "unit must not be null.").getBase(),
unit.convertToBase(value), ns);
}
-
+
/**
- * Gets a {@code LinearUnit} from a unit base and a conversion factor. In other words, gets the product of
- * {@code unitBase} and {@code conversionFactor}, expressed as a {@code LinearUnit}.
+ * @return the base unit associated with {@code unit}, as a
+ * {@code LinearUnit}.
+ * @since 2020-10-02
+ */
+ public static LinearUnit getBase(final Unit unit) {
+ return new LinearUnit(unit.getBase(), 1, NameSymbol.EMPTY);
+ }
+
+ /**
+ * @return the base unit associated with {@code unitlike}, as a
+ * {@code LinearUnit}.
+ * @since 2020-10-02
+ */
+ public static LinearUnit getBase(final Unitlike<?> unit) {
+ return new LinearUnit(unit.getBase(), 1, NameSymbol.EMPTY);
+ }
+
+ /**
+ * Gets a {@code LinearUnit} from a unit base and a conversion factor. In
+ * other words, gets the product of {@code unitBase} and
+ * {@code conversionFactor}, expressed as a {@code LinearUnit}.
*
- * @param unitBase
- * unit base to multiply by
- * @param conversionFactor
- * number to multiply base by
+ * @param unitBase unit base to multiply by
+ * @param conversionFactor number to multiply base by
* @return product of base and conversion factor
* @since 2019-10-16
- * @throws NullPointerException
- * if unitBase is null
+ * @throws NullPointerException if unitBase is null
*/
- public static LinearUnit valueOf(final ObjectProduct<BaseUnit> unitBase, final double conversionFactor) {
+ public static LinearUnit valueOf(final ObjectProduct<BaseUnit> unitBase,
+ final double conversionFactor) {
return new LinearUnit(unitBase, conversionFactor, NameSymbol.EMPTY);
}
-
+
/**
- * Gets a {@code LinearUnit} from a unit base and a conversion factor. In other words, gets the product of
- * {@code unitBase} and {@code conversionFactor}, expressed as a {@code LinearUnit}.
+ * Gets a {@code LinearUnit} from a unit base and a conversion factor. In
+ * other words, gets the product of {@code unitBase} and
+ * {@code conversionFactor}, expressed as a {@code LinearUnit}.
*
- * @param unitBase
- * unit base to multiply by
- * @param conversionFactor
- * number to multiply base by
- * @param ns
- * name(s) and symbol of unit
+ * @param unitBase unit base to multiply by
+ * @param conversionFactor number to multiply base by
+ * @param ns name(s) and symbol of unit
* @return product of base and conversion factor
* @since 2019-10-21
- * @throws NullPointerException
- * if unitBase is null
+ * @throws NullPointerException if unitBase is null
*/
- public static LinearUnit valueOf(final ObjectProduct<BaseUnit> unitBase, final double conversionFactor,
- final NameSymbol ns) {
+ public static LinearUnit valueOf(final ObjectProduct<BaseUnit> unitBase,
+ final double conversionFactor, final NameSymbol ns) {
return new LinearUnit(unitBase, conversionFactor, ns);
}
-
+
/**
* The value of this unit as represented in its base form. Mathematically,
*
@@ -113,21 +125,20 @@ public final class LinearUnit extends Unit {
* @since 2019-10-16
*/
private final double conversionFactor;
-
+
/**
* Creates the {@code LinearUnit}.
*
- * @param unitBase
- * base of linear unit
- * @param conversionFactor
- * conversion factor between base and unit
+ * @param unitBase base of linear unit
+ * @param conversionFactor conversion factor between base and unit
* @since 2019-10-16
*/
- private LinearUnit(final ObjectProduct<BaseUnit> unitBase, final double conversionFactor, final NameSymbol ns) {
+ private LinearUnit(final ObjectProduct<BaseUnit> unitBase,
+ final double conversionFactor, final NameSymbol ns) {
super(unitBase, ns);
this.conversionFactor = conversionFactor;
}
-
+
/**
* {@inheritDoc}
*
@@ -137,7 +148,32 @@ public final class LinearUnit extends Unit {
protected double convertFromBase(final double value) {
return value / this.getConversionFactor();
}
-
+
+ /**
+ * Converts an {@code UncertainDouble} value expressed in this unit to an
+ * {@code UncertainValue} value expressed in {@code other}.
+ *
+ * @param other unit to convert to
+ * @param value value to convert
+ * @return converted value
+ * @since 2019-09-07
+ * @throws IllegalArgumentException if {@code other} is incompatible for
+ * conversion with this unit (as tested by
+ * {@link Unit#canConvertTo}).
+ * @throws NullPointerException if value or other is null
+ */
+ public UncertainDouble convertTo(LinearUnit other, UncertainDouble value) {
+ Objects.requireNonNull(other, "other must not be null.");
+ Objects.requireNonNull(value, "value may not be null.");
+ if (this.canConvertTo(other))
+ return value.timesExact(
+ this.getConversionFactor() / other.getConversionFactor());
+ else
+ throw new IllegalArgumentException(
+ String.format("Cannot convert from %s to %s.", this, other));
+
+ }
+
/**
* {@inheritDoc}
*
@@ -147,12 +183,20 @@ public final class LinearUnit extends Unit {
protected double convertToBase(final double value) {
return value * this.getConversionFactor();
}
-
+
+ /**
+ * Converts an {@code UncertainDouble} to the base unit.
+ *
+ * @since 2020-09-07
+ */
+ UncertainDouble convertToBase(final UncertainDouble value) {
+ return value.timesExact(this.getConversionFactor());
+ }
+
/**
* Divides this unit by a scalar.
*
- * @param divisor
- * scalar to divide by
+ * @param divisor scalar to divide by
* @return quotient
* @since 2018-12-23
* @since v0.1.0
@@ -160,26 +204,26 @@ public final class LinearUnit extends Unit {
public LinearUnit dividedBy(final double divisor) {
return valueOf(this.getBase(), this.getConversionFactor() / divisor);
}
-
+
/**
* Returns the quotient of this unit and another.
*
- * @param divisor
- * unit to divide by
+ * @param divisor unit to divide by
* @return quotient of two units
- * @throws NullPointerException
- * if {@code divisor} is null
+ * @throws NullPointerException if {@code divisor} is null
* @since 2018-12-22
* @since v0.1.0
*/
public LinearUnit dividedBy(final LinearUnit divisor) {
Objects.requireNonNull(divisor, "other must not be null");
-
+
// divide the units
- final ObjectProduct<BaseUnit> base = this.getBase().dividedBy(divisor.getBase());
- return valueOf(base, this.getConversionFactor() / divisor.getConversionFactor());
+ final ObjectProduct<BaseUnit> base = this.getBase()
+ .dividedBy(divisor.getBase());
+ return valueOf(base,
+ this.getConversionFactor() / divisor.getConversionFactor());
}
-
+
/**
* {@inheritDoc}
*
@@ -191,9 +235,10 @@ public final class LinearUnit extends Unit {
return false;
final LinearUnit other = (LinearUnit) obj;
return Objects.equals(this.getBase(), other.getBase())
- && DecimalComparison.equals(this.getConversionFactor(), other.getConversionFactor());
+ && DecimalComparison.equals(this.getConversionFactor(),
+ other.getConversionFactor());
}
-
+
/**
* @return conversion factor
* @since 2019-10-16
@@ -201,7 +246,7 @@ public final class LinearUnit extends Unit {
public double getConversionFactor() {
return this.conversionFactor;
}
-
+
/**
* {@inheritDoc}
*
@@ -209,18 +254,20 @@ public final class LinearUnit extends Unit {
*/
@Override
public int hashCode() {
- return 31 * this.getBase().hashCode() + DecimalComparison.hash(this.getConversionFactor());
+ return 31 * this.getBase().hashCode()
+ + DecimalComparison.hash(this.getConversionFactor());
}
-
+
/**
- * @return whether this unit is equivalent to a {@code BaseUnit} (i.e. there is a {@code BaseUnit b} where
+ * @return whether this unit is equivalent to a {@code BaseUnit} (i.e. there
+ * is a {@code BaseUnit b} where
* {@code b.asLinearUnit().equals(this)} returns {@code true}.)
* @since 2019-10-16
*/
public boolean isBase() {
return this.isCoherent() && this.getBase().isSingleObject();
}
-
+
/**
* @return whether this unit is coherent (i.e. has conversion factor 1)
* @since 2019-10-16
@@ -228,70 +275,73 @@ public final class LinearUnit extends Unit {
public boolean isCoherent() {
return this.getConversionFactor() == 1;
}
-
+
/**
* Returns the difference of this unit and another.
* <p>
- * Two units can be subtracted if they have the same base. Note that {@link #canConvertTo} can be used to determine
- * this. If {@code subtrahend} does not meet this condition, an {@code IllegalArgumentException} will be thrown.
+ * Two units can be subtracted if they have the same base. Note that
+ * {@link #canConvertTo} can be used to determine this. If {@code subtrahend}
+ * does not meet this condition, an {@code IllegalArgumentException} will be
+ * thrown.
* </p>
*
- * @param subtrahend
- * unit to subtract
+ * @param subtrahend unit to subtract
* @return difference of units
- * @throws IllegalArgumentException
- * if {@code subtrahend} is not compatible for subtraction as described above
- * @throws NullPointerException
- * if {@code subtrahend} is null
+ * @throws IllegalArgumentException if {@code subtrahend} is not compatible
+ * for subtraction as described above
+ * @throws NullPointerException if {@code subtrahend} is null
* @since 2019-03-17
* @since v0.2.0
*/
- public LinearUnit minus(final LinearUnit subtrahendend) {
- Objects.requireNonNull(subtrahendend, "addend must not be null.");
-
+ public LinearUnit minus(final LinearUnit subtrahend) {
+ Objects.requireNonNull(subtrahend, "addend must not be null.");
+
// reject subtrahends that cannot be added to this unit
- if (!this.getBase().equals(subtrahendend.getBase()))
- throw new IllegalArgumentException(
- String.format("Incompatible units for subtraction \"%s\" and \"%s\".", this, subtrahendend));
-
+ if (!this.getBase().equals(subtrahend.getBase()))
+ throw new IllegalArgumentException(String.format(
+ "Incompatible units for subtraction \"%s\" and \"%s\".", this,
+ subtrahend));
+
// subtract the units
- return valueOf(this.getBase(), this.getConversionFactor() - subtrahendend.getConversionFactor());
+ return valueOf(this.getBase(),
+ this.getConversionFactor() - subtrahend.getConversionFactor());
}
-
+
/**
* Returns the sum of this unit and another.
* <p>
- * Two units can be added if they have the same base. Note that {@link #canConvertTo} can be used to determine this.
- * If {@code addend} does not meet this condition, an {@code IllegalArgumentException} will be thrown.
+ * Two units can be added if they have the same base. Note that
+ * {@link #canConvertTo} can be used to determine this. If {@code addend}
+ * does not meet this condition, an {@code IllegalArgumentException} will be
+ * thrown.
* </p>
*
- * @param addend
- * unit to add
+ * @param addend unit to add
* @return sum of units
- * @throws IllegalArgumentException
- * if {@code addend} is not compatible for addition as described above
- * @throws NullPointerException
- * if {@code addend} is null
+ * @throws IllegalArgumentException if {@code addend} is not compatible for
+ * addition as described above
+ * @throws NullPointerException if {@code addend} is null
* @since 2019-03-17
* @since v0.2.0
*/
public LinearUnit plus(final LinearUnit addend) {
Objects.requireNonNull(addend, "addend must not be null.");
-
+
// reject addends that cannot be added to this unit
if (!this.getBase().equals(addend.getBase()))
- throw new IllegalArgumentException(
- String.format("Incompatible units for addition \"%s\" and \"%s\".", this, addend));
-
+ throw new IllegalArgumentException(String.format(
+ "Incompatible units for addition \"%s\" and \"%s\".", this,
+ addend));
+
// add the units
- return valueOf(this.getBase(), this.getConversionFactor() + addend.getConversionFactor());
+ return valueOf(this.getBase(),
+ this.getConversionFactor() + addend.getConversionFactor());
}
-
+
/**
* Multiplies this unit by a scalar.
*
- * @param multiplier
- * scalar to multiply by
+ * @param multiplier scalar to multiply by
* @return product
* @since 2018-12-23
* @since v0.1.0
@@ -299,39 +349,39 @@ public final class LinearUnit extends Unit {
public LinearUnit times(final double multiplier) {
return valueOf(this.getBase(), this.getConversionFactor() * multiplier);
}
-
+
/**
* Returns the product of this unit and another.
*
- * @param multiplier
- * unit to multiply by
+ * @param multiplier unit to multiply by
* @return product of two units
- * @throws NullPointerException
- * if {@code multiplier} is null
+ * @throws NullPointerException if {@code multiplier} is null
* @since 2018-12-22
* @since v0.1.0
*/
public LinearUnit times(final LinearUnit multiplier) {
Objects.requireNonNull(multiplier, "other must not be null");
-
+
// multiply the units
- final ObjectProduct<BaseUnit> base = this.getBase().times(multiplier.getBase());
- return valueOf(base, this.getConversionFactor() * multiplier.getConversionFactor());
+ final ObjectProduct<BaseUnit> base = this.getBase()
+ .times(multiplier.getBase());
+ return valueOf(base,
+ this.getConversionFactor() * multiplier.getConversionFactor());
}
-
+
/**
* Returns this unit but to an exponent.
*
- * @param exponent
- * exponent to exponentiate unit to
+ * @param exponent exponent to exponentiate unit to
* @return exponentiated unit
* @since 2019-01-15
* @since v0.1.0
*/
public LinearUnit toExponent(final int exponent) {
- return valueOf(this.getBase().toExponent(exponent), Math.pow(this.conversionFactor, exponent));
+ return valueOf(this.getBase().toExponent(exponent),
+ Math.pow(this.conversionFactor, exponent));
}
-
+
/**
* @return a string providing a definition of this unit
* @since 2019-10-21
@@ -339,49 +389,53 @@ public final class LinearUnit extends Unit {
@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());
+ + (this.getSymbol().isPresent()
+ ? String.format(" (%s)", this.getSymbol().get())
+ : "")
+ + ", " + Double.toString(this.conversionFactor) + " * "
+ + this.getBase().toString(u -> u.getSymbol().get());
}
-
+
@Override
public LinearUnit withName(final NameSymbol ns) {
return valueOf(this.getBase(), this.getConversionFactor(), ns);
}
-
+
/**
* Returns the result of applying {@code prefix} to this unit.
* <p>
- * If this unit and the provided prefix have a primary name, the returned unit will have a primary name (prefix's
- * name + unit's name). <br>
- * If this unit and the provided prefix have a symbol, the returned unit will have a symbol. <br>
- * This method ignores alternate names of both this unit and the provided prefix.
+ * If this unit and the provided prefix have a primary name, the returned
+ * unit will have a primary name (prefix's name + unit's name). <br>
+ * If this unit and the provided prefix have a symbol, the returned unit will
+ * have a symbol. <br>
+ * This method ignores alternate names of both this unit and the provided
+ * prefix.
*
- * @param prefix
- * prefix to apply
+ * @param prefix prefix to apply
* @return unit with prefix
* @since 2019-03-18
* @since v0.2.0
- * @throws NullPointerException
- * if prefix is null
+ * @throws NullPointerException if prefix is null
*/
public LinearUnit withPrefix(final UnitPrefix prefix) {
final LinearUnit unit = this.times(prefix.getMultiplier());
-
+
// create new name and symbol, if possible
final String name;
- if (this.getPrimaryName().isPresent() && prefix.getPrimaryName().isPresent()) {
+ if (this.getPrimaryName().isPresent()
+ && prefix.getPrimaryName().isPresent()) {
name = prefix.getPrimaryName().get() + this.getPrimaryName().get();
} else {
name = null;
}
-
+
final String symbol;
if (this.getSymbol().isPresent() && prefix.getSymbol().isPresent()) {
symbol = prefix.getSymbol().get() + this.getSymbol().get();
} else {
symbol = null;
}
-
+
return unit.withName(NameSymbol.ofNullable(name, symbol));
}
}
diff --git a/src/org/unitConverter/unit/LinearUnitValue.java b/src/org/unitConverter/unit/LinearUnitValue.java
new file mode 100644
index 0000000..8de734e
--- /dev/null
+++ b/src/org/unitConverter/unit/LinearUnitValue.java
@@ -0,0 +1,341 @@
+/**
+ * Copyright (C) 2019 Adrien Hopkins
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package org.unitConverter.unit;
+
+import java.util.Objects;
+import java.util.Optional;
+
+import org.unitConverter.math.DecimalComparison;
+import org.unitConverter.math.UncertainDouble;
+
+/**
+ * A possibly uncertain value expressed in a linear unit.
+ *
+ * Unless otherwise indicated, all methods in this class throw a
+ * {@code NullPointerException} when an argument is null.
+ *
+ * @author Adrien Hopkins
+ * @since 2020-07-26
+ */
+public final class LinearUnitValue {
+ public static final LinearUnitValue ONE = getExact(SI.ONE, 1);
+
+ /**
+ * Gets an exact {@code LinearUnitValue}
+ *
+ * @param unit unit to express with
+ * @param value value to express
+ * @return exact {@code LinearUnitValue} instance
+ * @since 2020-07-26
+ */
+ public static final LinearUnitValue getExact(final LinearUnit unit,
+ final double value) {
+ return new LinearUnitValue(
+ Objects.requireNonNull(unit, "unit must not be null"),
+ UncertainDouble.of(value, 0));
+ }
+
+ /**
+ * Gets an uncertain {@code LinearUnitValue}
+ *
+ * @param unit unit to express with
+ * @param value value to express
+ * @param uncertainty absolute uncertainty of value
+ * @return uncertain {@code LinearUnitValue} instance
+ * @since 2020-07-26
+ */
+ public static final LinearUnitValue of(final LinearUnit unit,
+ final UncertainDouble value) {
+ return new LinearUnitValue(
+ Objects.requireNonNull(unit, "unit must not be null"),
+ Objects.requireNonNull(value, "value may not be null"));
+ }
+
+ private final LinearUnit unit;
+
+ private final UncertainDouble value;
+
+ /**
+ * @param unit unit to express as
+ * @param value value to express
+ * @since 2020-07-26
+ */
+ private LinearUnitValue(final LinearUnit unit, final UncertainDouble value) {
+ this.unit = unit;
+ this.value = value;
+ }
+
+ /**
+ * @return this value as a {@code UnitValue}. All uncertainty information is
+ * removed from the returned value.
+ * @since 2020-08-04
+ */
+ public final UnitValue asUnitValue() {
+ return UnitValue.of(this.unit, this.value.value());
+ }
+
+ /**
+ * @param other a {@code LinearUnit}
+ * @return true iff this value can be represented with {@code other}.
+ * @since 2020-07-26
+ */
+ public final boolean canConvertTo(final LinearUnit other) {
+ return this.unit.canConvertTo(other);
+ }
+
+ /**
+ * Returns a LinearUnitValue that represents the same value expressed in a
+ * different unit
+ *
+ * @param other new unit to express value in
+ * @return value expressed in {@code other}
+ * @since 2020-07-26
+ */
+ public final LinearUnitValue convertTo(final LinearUnit other) {
+ return LinearUnitValue.of(other, this.unit.convertTo(other, this.value));
+ }
+
+ /**
+ * Divides this value by a scalar
+ *
+ * @param divisor value to divide by
+ * @return multiplied value
+ * @since 2020-07-28
+ */
+ public LinearUnitValue dividedBy(final double divisor) {
+ return LinearUnitValue.of(this.unit, this.value.dividedByExact(divisor));
+ }
+
+ /**
+ * Divides this value by another value
+ *
+ * @param divisor value to multiply by
+ * @return quotient
+ * @since 2020-07-28
+ */
+ public LinearUnitValue dividedBy(final LinearUnitValue divisor) {
+ return LinearUnitValue.of(this.unit.dividedBy(divisor.unit),
+ this.value.dividedBy(divisor.value));
+ }
+
+ /**
+ * Returns true if this and obj represent the same value, regardless of
+ * whether or not they are expressed in the same unit. So (1000 m).equals(1
+ * km) returns true.
+ *
+ * @since 2020-07-26
+ * @see #equals(Object, boolean)
+ */
+ @Override
+ public boolean equals(final Object obj) {
+ if (!(obj instanceof LinearUnitValue))
+ return false;
+ final LinearUnitValue other = (LinearUnitValue) obj;
+ return Objects.equals(this.unit.getBase(), other.unit.getBase())
+ && this.unit.convertToBase(this.value)
+ .equals(other.unit.convertToBase(other.value));
+ }
+
+ /**
+ * Returns true if this and obj represent the same value, regardless of
+ * whether or not they are expressed in the same unit. So (1000 m).equals(1
+ * km) returns true.
+ * <p>
+ * If avoidFPErrors is true, this method will attempt to avoid floating-point
+ * errors, at the cost of not always being transitive.
+ *
+ * @since 2020-07-28
+ */
+ public boolean equals(final Object obj, final boolean avoidFPErrors) {
+ if (!avoidFPErrors)
+ return this.equals(obj);
+ if (!(obj instanceof LinearUnitValue))
+ return false;
+ final LinearUnitValue other = (LinearUnitValue) obj;
+ return Objects.equals(this.unit.getBase(), other.unit.getBase())
+ && DecimalComparison.equals(this.unit.convertToBase(this.value),
+ other.unit.convertToBase(other.value));
+ }
+
+ /**
+ * @param other another {@code LinearUnitValue}
+ * @return true iff this and other are within each other's uncertainty range
+ *
+ * @since 2020-07-26
+ */
+ public boolean equivalent(final LinearUnitValue other) {
+ if (other == null
+ || !Objects.equals(this.unit.getBase(), other.unit.getBase()))
+ return false;
+ final LinearUnit base = LinearUnit.valueOf(this.unit.getBase(), 1);
+ final LinearUnitValue thisBase = this.convertTo(base);
+ final LinearUnitValue otherBase = other.convertTo(base);
+
+ return thisBase.value.equivalent(otherBase.value);
+ }
+
+ /**
+ * @return the unit
+ * @since 2020-09-29
+ */
+ public final LinearUnit getUnit() {
+ return this.unit;
+ }
+
+ /**
+ * @return the value
+ * @since 2020-09-29
+ */
+ public final UncertainDouble getValue() {
+ return this.value;
+ }
+
+ /**
+ * @return the exact value
+ * @since 2020-09-07
+ */
+ public final double getValueExact() {
+ return this.value.value();
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(this.unit.getBase(),
+ this.unit.convertToBase(this.getValue()));
+ }
+
+ /**
+ * Returns the difference of this value and another, expressed in this
+ * value's unit
+ *
+ * @param subtrahend value to subtract
+ * @return difference of values
+ * @throws IllegalArgumentException if {@code subtrahend} has a unit that is
+ * not compatible for addition
+ * @since 2020-07-26
+ */
+ public LinearUnitValue minus(final LinearUnitValue subtrahend) {
+ Objects.requireNonNull(subtrahend, "subtrahend may not be null");
+
+ if (!this.canConvertTo(subtrahend.unit))
+ throw new IllegalArgumentException(String.format(
+ "Incompatible units for subtraction \"%s\" and \"%s\".",
+ this.unit, subtrahend.unit));
+
+ final LinearUnitValue otherConverted = subtrahend.convertTo(this.unit);
+ return LinearUnitValue.of(this.unit,
+ this.value.minus(otherConverted.value));
+ }
+
+ /**
+ * Returns the sum of this value and another, expressed in this value's unit
+ *
+ * @param addend value to add
+ * @return sum of values
+ * @throws IllegalArgumentException if {@code addend} has a unit that is not
+ * compatible for addition
+ * @since 2020-07-26
+ */
+ public LinearUnitValue plus(final LinearUnitValue addend) {
+ Objects.requireNonNull(addend, "addend may not be null");
+
+ if (!this.canConvertTo(addend.unit))
+ throw new IllegalArgumentException(String.format(
+ "Incompatible units for addition \"%s\" and \"%s\".", this.unit,
+ addend.unit));
+
+ final LinearUnitValue otherConverted = addend.convertTo(this.unit);
+ return LinearUnitValue.of(this.unit,
+ this.value.plus(otherConverted.value));
+ }
+
+ /**
+ * Multiplies this value by a scalar
+ *
+ * @param multiplier value to multiply by
+ * @return multiplied value
+ * @since 2020-07-28
+ */
+ public LinearUnitValue times(final double multiplier) {
+ return LinearUnitValue.of(this.unit, this.value.timesExact(multiplier));
+ }
+
+ /**
+ * Multiplies this value by another value
+ *
+ * @param multiplier value to multiply by
+ * @return product
+ * @since 2020-07-28
+ */
+ public LinearUnitValue times(final LinearUnitValue multiplier) {
+ return LinearUnitValue.of(this.unit.times(multiplier.unit),
+ this.value.times(multiplier.value));
+ }
+
+ /**
+ * Raises a value to an exponent
+ *
+ * @param exponent exponent to raise to
+ * @return result of exponentiation
+ * @since 2020-07-28
+ */
+ public LinearUnitValue toExponent(final int exponent) {
+ return LinearUnitValue.of(this.unit.toExponent(exponent),
+ this.value.toExponentExact(exponent));
+ }
+
+ @Override
+ public String toString() {
+ return this.toString(!this.value.isExact());
+ }
+
+ /**
+ * Returns a string representing the object. <br>
+ * If the attached unit has a name or symbol, the string looks like "12 km".
+ * Otherwise, it looks like "13 unnamed unit (= 2 m/s)".
+ * <p>
+ * If showUncertainty is true, strings like "35 ± 8" are shown instead of
+ * single numbers.
+ * <p>
+ * Non-exact values are rounded intelligently based on their uncertainty.
+ *
+ * @since 2020-07-26
+ */
+ public String toString(final boolean showUncertainty) {
+ final Optional<String> primaryName = this.unit.getPrimaryName();
+ final Optional<String> symbol = this.unit.getSymbol();
+ final String chosenName = symbol.orElse(primaryName.orElse(null));
+
+ final UncertainDouble baseValue = this.unit.convertToBase(this.value);
+
+ // get rounded strings
+ // if showUncertainty is true, add brackets around the string
+ final String valueString = showUncertainty ? "("
+ : "" + this.value.toString(showUncertainty)
+ + (showUncertainty ? ")" : "");
+ final String baseValueString = showUncertainty ? "("
+ : "" + baseValue.toString(showUncertainty)
+ + (showUncertainty ? ")" : "");
+
+ // create string
+ if (primaryName.isEmpty() && symbol.isEmpty())
+ return String.format("%s unnamed unit (= %s %s)", valueString,
+ baseValueString, this.unit.getBase());
+ else
+ return String.format("%s %s", valueString, chosenName);
+ }
+}
diff --git a/src/org/unitConverter/unit/MultiUnit.java b/src/org/unitConverter/unit/MultiUnit.java
new file mode 100644
index 0000000..a1623f8
--- /dev/null
+++ b/src/org/unitConverter/unit/MultiUnit.java
@@ -0,0 +1,160 @@
+/**
+ * Copyright (C) 2020 Adrien Hopkins
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package org.unitConverter.unit;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import org.unitConverter.math.ObjectProduct;
+
+/**
+ * A combination of units, like "5 foot + 7 inch". All but the last units should
+ * have a whole number value associated with them.
+ *
+ * @since 2020-10-02
+ */
+public final class MultiUnit extends Unitlike<List<Double>> {
+ /**
+ * Creates a {@code MultiUnit} from its units. It will not have a name or
+ * symbol.
+ *
+ * @since 2020-10-03
+ */
+ public static final MultiUnit of(LinearUnit... units) {
+ return of(Arrays.asList(units));
+ }
+
+ /**
+ * Creates a {@code MultiUnit} from its units. It will not have a name or
+ * symbol.
+ *
+ * @since 2020-10-03
+ */
+ public static final MultiUnit of(List<LinearUnit> units) {
+ if (units.size() < 1)
+ throw new IllegalArgumentException("Must have at least one unit");
+ final ObjectProduct<BaseUnit> unitBase = units.get(0).getBase();
+ for (final LinearUnit unit : units) {
+ if (!unitBase.equals(unit.getBase()))
+ throw new IllegalArgumentException(
+ "All units must have the same base.");
+ }
+ return new MultiUnit(new ArrayList<>(units), unitBase, NameSymbol.EMPTY);
+ }
+
+ /**
+ * The units that make up this value.
+ */
+ private final List<LinearUnit> units;
+
+ /**
+ * Creates a {@code MultiUnit}.
+ *
+ * @since 2020-10-03
+ */
+ private MultiUnit(List<LinearUnit> units, ObjectProduct<BaseUnit> unitBase,
+ NameSymbol ns) {
+ super(unitBase, ns);
+ this.units = units;
+ }
+
+ @Override
+ protected List<Double> convertFromBase(double value) {
+ final List<Double> values = new ArrayList<>(this.units.size());
+ double temp = value;
+
+ for (final LinearUnit unit : this.units.subList(0,
+ this.units.size() - 1)) {
+ values.add(Math.floor(temp / unit.getConversionFactor()));
+ temp %= unit.getConversionFactor();
+ }
+
+ values.add(this.units.size() - 1,
+ this.units.get(this.units.size() - 1).convertFromBase(temp));
+
+ return values;
+ }
+
+ /**
+ * Converts a value expressed in this unitlike form to a value expressed in
+ * {@code other}.
+ *
+ * @implSpec If conversion is possible, this implementation returns
+ * {@code other.convertFromBase(this.convertToBase(value))}.
+ * Therefore, overriding either of those methods will change the
+ * output of this method.
+ *
+ * @param other unit to convert to
+ * @param value value to convert
+ * @return converted value
+ * @since 2020-10-03
+ * @throws IllegalArgumentException if {@code other} is incompatible for
+ * conversion with this unitlike form (as
+ * tested by {@link Unit#canConvertTo}).
+ * @throws NullPointerException if other is null
+ */
+ public final <U extends Unitlike<V>, V> V convertTo(U other,
+ double... values) {
+ final List<Double> valueList = new ArrayList<>(values.length);
+ for (final double d : values) {
+ valueList.add(d);
+ }
+
+ return this.convertTo(other, valueList);
+ }
+
+ /**
+ * Converts a value expressed in this unitlike form to a value expressed in
+ * {@code other}.
+ *
+ * @implSpec If conversion is possible, this implementation returns
+ * {@code other.convertFromBase(this.convertToBase(value))}.
+ * Therefore, overriding either of those methods will change the
+ * output of this method.
+ *
+ * @param other unit to convert to
+ * @param value value to convert
+ * @return converted value
+ * @since 2020-10-03
+ * @throws IllegalArgumentException if {@code other} is incompatible for
+ * conversion with this unitlike form (as
+ * tested by {@link Unit#canConvertTo}).
+ * @throws NullPointerException if other is null
+ */
+ public final double convertTo(Unit other, double... values) {
+ final List<Double> valueList = new ArrayList<>(values.length);
+ for (final double d : values) {
+ valueList.add(d);
+ }
+
+ return this.convertTo(other, valueList);
+ }
+
+ @Override
+ protected double convertToBase(List<Double> value) {
+ if (value.size() != this.units.size())
+ throw new IllegalArgumentException("Wrong number of values for "
+ + this.units.size() + "-unit MultiUnit.");
+
+ double baseValue = 0;
+ for (int i = 0; i < this.units.size(); i++) {
+ baseValue += value.get(i) * this.units.get(i).getConversionFactor();
+ }
+ return baseValue;
+ }
+}
diff --git a/src/org/unitConverter/unit/MultiUnitTest.java b/src/org/unitConverter/unit/MultiUnitTest.java
new file mode 100644
index 0000000..5ea9d07
--- /dev/null
+++ b/src/org/unitConverter/unit/MultiUnitTest.java
@@ -0,0 +1,106 @@
+/**
+ * Copyright (C) 2020 Adrien Hopkins
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package org.unitConverter.unit;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Random;
+import java.util.concurrent.ThreadLocalRandom;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests related to the {@code MultiUnit}.
+ *
+ * @since 2020-10-03
+ */
+class MultiUnitTest {
+
+ @Test
+ final void testConvert() {
+ final Random rng = ThreadLocalRandom.current();
+ final MultiUnit footInch = MultiUnit.of(BritishImperial.Length.FOOT,
+ BritishImperial.Length.INCH);
+
+ assertEquals(1702.0, footInch.convertTo(SI.METRE.withPrefix(SI.MILLI),
+ Arrays.asList(5.0, 7.0)), 1.0);
+
+ for (int i = 0; i < 1000; i++) {
+ final double feet = rng.nextInt(1000);
+ final double inches = rng.nextDouble() * 12;
+ final double millimetres = feet * 304.8 + inches * 25.4;
+
+ final List<Double> feetAndInches = SI.METRE.withPrefix(SI.MILLI)
+ .convertTo(footInch, millimetres);
+ assertEquals(feet, feetAndInches.get(0), 1e-10);
+ assertEquals(inches, feetAndInches.get(1), 1e-10);
+ }
+ }
+
+ /**
+ * Test method for
+ * {@link org.unitConverter.unit.MultiUnit#convertFromBase(double)}.
+ */
+ @Test
+ final void testConvertFromBase() {
+ final Random rng = ThreadLocalRandom.current();
+ final MultiUnit footInch = MultiUnit.of(BritishImperial.Length.FOOT,
+ BritishImperial.Length.INCH);
+
+ // 1.7 m =~ 5' + 7"
+ final List<Double> values = footInch.convertFromBase(1.7018);
+
+ assertEquals(5, values.get(0));
+ assertEquals(7, values.get(1), 1e-12);
+
+ for (int i = 0; i < 1000; i++) {
+ final double feet = rng.nextInt(1000);
+ final double inches = rng.nextDouble() * 12;
+ final double metres = feet * 0.3048 + inches * 0.0254;
+
+ final List<Double> feetAndInches = footInch.convertFromBase(metres);
+ assertEquals(feet, feetAndInches.get(0), 1e-10);
+ assertEquals(inches, feetAndInches.get(1), 1e-10);
+ }
+ }
+
+ /**
+ * Test method for
+ * {@link org.unitConverter.unit.MultiUnit#convertToBase(java.util.List)}.
+ */
+ @Test
+ final void testConvertToBase() {
+ final Random rng = ThreadLocalRandom.current();
+ final MultiUnit footInch = MultiUnit.of(BritishImperial.Length.FOOT,
+ BritishImperial.Length.INCH);
+
+ // 1.7 m =~ 5' + 7"
+ assertEquals(1.7018, footInch.convertToBase(Arrays.asList(5.0, 7.0)),
+ 1e-12);
+
+ for (int i = 0; i < 1000; i++) {
+ final double feet = rng.nextInt(1000);
+ final double inches = rng.nextDouble() * 12;
+ final double metres = feet * 0.3048 + inches * 0.0254;
+
+ assertEquals(metres,
+ footInch.convertToBase(Arrays.asList(feet, inches)), 1e-12);
+ }
+ }
+}
diff --git a/src/org/unitConverter/unit/NameSymbol.java b/src/org/unitConverter/unit/NameSymbol.java
index 96fab45..8d8302a 100644
--- a/src/org/unitConverter/unit/NameSymbol.java
+++ b/src/org/unitConverter/unit/NameSymbol.java
@@ -19,6 +19,7 @@ package org.unitConverter.unit;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
+import java.util.Iterator;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
@@ -30,227 +31,208 @@ import java.util.Set;
* @since 2019-10-21
*/
public final class NameSymbol {
- public static final NameSymbol EMPTY = new NameSymbol(Optional.empty(), Optional.empty(), new HashSet<>());
-
+ public static final NameSymbol EMPTY = new NameSymbol(Optional.empty(),
+ Optional.empty(), new HashSet<>());
+
/**
- * Gets a {@code NameSymbol} with a primary name, a symbol and no other names.
+ * Creates a {@code NameSymbol}, ensuring that if primaryName is null and
+ * otherNames is not empty, one name is moved from otherNames to primaryName
*
- * @param name
- * name to use
- * @param symbol
- * symbol to use
- * @return NameSymbol instance
- * @since 2019-10-21
- * @throws NullPointerException
- * if name or symbol is null
+ * Ensure that otherNames is a copy of the inputted argument.
*/
- public static final NameSymbol of(final String name, final String symbol) {
- return new NameSymbol(Optional.of(name), Optional.of(symbol), new HashSet<>());
+ private static final NameSymbol create(final String name,
+ final String symbol, final Set<String> otherNames) {
+ final Optional<String> primaryName;
+
+ if (name == null && !otherNames.isEmpty()) {
+ // get primary name and remove it from savedNames
+ final Iterator<String> it = otherNames.iterator();
+ assert it.hasNext();
+ primaryName = Optional.of(it.next());
+ otherNames.remove(primaryName.get());
+ } else {
+ primaryName = Optional.ofNullable(name);
+ }
+
+ return new NameSymbol(primaryName, Optional.ofNullable(symbol),
+ otherNames);
}
-
+
/**
- * Gets a {@code NameSymbol} with a primary name, a symbol and additional names.
+ * Gets a {@code NameSymbol} with a primary name, a symbol and no other
+ * names.
*
- * @param name
- * name to use
- * @param symbol
- * symbol to use
- * @param otherNames
- * other names to use
+ * @param name name to use
+ * @param symbol symbol to use
* @return NameSymbol instance
* @since 2019-10-21
- * @throws NullPointerException
- * if any argument is null
+ * @throws NullPointerException if name or symbol is null
*/
- public static final NameSymbol of(final String name, final String symbol, final Set<String> otherNames) {
+ public static final NameSymbol of(final String name, final String symbol) {
return new NameSymbol(Optional.of(name), Optional.of(symbol),
- new HashSet<>(Objects.requireNonNull(otherNames, "otherNames must not be null.")));
+ new HashSet<>());
}
-
+
/**
- * h * Gets a {@code NameSymbol} with a primary name, a symbol and additional names.
+ * Gets a {@code NameSymbol} with a primary name, a symbol and additional
+ * names.
*
- * @param name
- * name to use
- * @param symbol
- * symbol to use
- * @param otherNames
- * other names to use
+ * @param name name to use
+ * @param symbol symbol to use
+ * @param otherNames other names to use
* @return NameSymbol instance
* @since 2019-10-21
- * @throws NullPointerException
- * if any argument is null
+ * @throws NullPointerException if any argument is null
*/
- public static final NameSymbol of(final String name, final String symbol, final String... otherNames) {
+ public static final NameSymbol of(final String name, final String symbol,
+ final Set<String> otherNames) {
return new NameSymbol(Optional.of(name), Optional.of(symbol),
- new HashSet<>(Arrays.asList(Objects.requireNonNull(otherNames, "otherNames must not be null."))));
- }
-
- /**
- * Gets a {@code NameSymbol} with a primary name, a symbol and an additional name.
- *
- * @param name
- * name to use
- * @param symbol
- * symbol to use
- * @param otherNames
- * other names to use
- * @param name2
- * alternate name
- * @return NameSymbol instance
- * @since 2019-10-21
- * @throws NullPointerException
- * if any argument is null
- */
- public static final NameSymbol of(final String name, final String symbol, final String name2) {
- final Set<String> otherNames = new HashSet<>();
- otherNames.add(Objects.requireNonNull(name2, "name2 must not be null."));
- return new NameSymbol(Optional.of(name), Optional.of(symbol), otherNames);
- }
-
- /**
- * Gets a {@code NameSymbol} with a primary name, a symbol and additional names.
- *
- * @param name
- * name to use
- * @param symbol
- * symbol to use
- * @param otherNames
- * other names to use
- * @param name2
- * alternate name
- * @param name3
- * alternate name
- * @return NameSymbol instance
- * @since 2019-10-21
- * @throws NullPointerException
- * if any argument is null
- */
- public static final NameSymbol of(final String name, final String symbol, final String name2, final String name3) {
- final Set<String> otherNames = new HashSet<>();
- otherNames.add(Objects.requireNonNull(name2, "name2 must not be null."));
- otherNames.add(Objects.requireNonNull(name3, "name3 must not be null."));
- return new NameSymbol(Optional.of(name), Optional.of(symbol), otherNames);
+ new HashSet<>(Objects.requireNonNull(otherNames,
+ "otherNames must not be null.")));
}
-
+
/**
- * Gets a {@code NameSymbol} with a primary name, a symbol and additional names.
+ * h * Gets a {@code NameSymbol} with a primary name, a symbol and additional
+ * names.
*
- * @param name
- * name to use
- * @param symbol
- * symbol to use
- * @param otherNames
- * other names to use
- * @param name2
- * alternate name
- * @param name3
- * alternate name
- * @param name4
- * alternate name
+ * @param name name to use
+ * @param symbol symbol to use
+ * @param otherNames other names to use
* @return NameSymbol instance
* @since 2019-10-21
- * @throws NullPointerException
- * if any argument is null
+ * @throws NullPointerException if any argument is null
*/
- public static final NameSymbol of(final String name, final String symbol, final String name2, final String name3,
- final String name4) {
- final Set<String> otherNames = new HashSet<>();
- otherNames.add(Objects.requireNonNull(name2, "name2 must not be null."));
- otherNames.add(Objects.requireNonNull(name3, "name3 must not be null."));
- otherNames.add(Objects.requireNonNull(name4, "name4 must not be null."));
- return new NameSymbol(Optional.of(name), Optional.of(symbol), otherNames);
+ public static final NameSymbol of(final String name, final String symbol,
+ final String... otherNames) {
+ return new NameSymbol(Optional.of(name), Optional.of(symbol),
+ new HashSet<>(Arrays.asList(Objects.requireNonNull(otherNames,
+ "otherNames must not be null."))));
}
-
+
/**
- * Gets a {@code NameSymbol} with a primary name, no symbol, and no other names.
+ * Gets a {@code NameSymbol} with a primary name, no symbol, and no other
+ * names.
*
- * @param name
- * name to use
+ * @param name name to use
* @return NameSymbol instance
* @since 2019-10-21
- * @throws NullPointerException
- * if name is null
+ * @throws NullPointerException if name is null
*/
public static final NameSymbol ofName(final String name) {
- return new NameSymbol(Optional.of(name), Optional.empty(), new HashSet<>());
+ return new NameSymbol(Optional.of(name), Optional.empty(),
+ new HashSet<>());
}
-
+
/**
- * Gets a {@code NameSymbol} with a primary name, a symbol and additional names.
+ * Gets a {@code NameSymbol} with a primary name, a symbol and additional
+ * names.
+ * <p>
+ * If any argument is null, this static factory replaces it with an empty
+ * Optional or empty Set.
* <p>
- * If any argument is null, this static factory replaces it with an empty Optional or empty Set.
+ * If {@code name} is null and {@code otherNames} is not empty, a primary
+ * name will be picked from {@code otherNames}. This name will not appear in
+ * getOtherNames().
*
- * @param name
- * name to use
- * @param symbol
- * symbol to use
- * @param otherNames
- * other names to use
+ * @param name name to use
+ * @param symbol symbol to use
+ * @param otherNames other names to use
* @return NameSymbol instance
* @since 2019-11-26
*/
- public static final NameSymbol ofNullable(final String name, final String symbol, final Set<String> otherNames) {
- return new NameSymbol(Optional.ofNullable(name), Optional.ofNullable(symbol),
+ public static final NameSymbol ofNullable(final String name,
+ final String symbol, final Set<String> otherNames) {
+ return NameSymbol.create(name, symbol,
otherNames == null ? new HashSet<>() : new HashSet<>(otherNames));
}
-
+
/**
- * h * Gets a {@code NameSymbol} with a primary name, a symbol and additional names.
+ * h * Gets a {@code NameSymbol} with a primary name, a symbol and additional
+ * names.
+ * <p>
+ * If any argument is null, this static factory replaces it with an empty
+ * Optional or empty Set.
* <p>
- * If any argument is null, this static factory replaces it with an empty Optional or empty Set.
+ * If {@code name} is null and {@code otherNames} is not empty, a primary
+ * name will be picked from {@code otherNames}. This name will not appear in
+ * getOtherNames().
*
- * @param name
- * name to use
- * @param symbol
- * symbol to use
- * @param otherNames
- * other names to use
+ * @param name name to use
+ * @param symbol symbol to use
+ * @param otherNames other names to use
* @return NameSymbol instance
* @since 2019-11-26
*/
- public static final NameSymbol ofNullable(final String name, final String symbol, final String... otherNames) {
- return new NameSymbol(Optional.ofNullable(name), Optional.ofNullable(symbol),
- otherNames == null ? new HashSet<>() : new HashSet<>(Arrays.asList(otherNames)));
+ public static final NameSymbol ofNullable(final String name,
+ final String symbol, final String... otherNames) {
+ return create(name, symbol, otherNames == null ? new HashSet<>()
+ : new HashSet<>(Arrays.asList(otherNames)));
}
-
+
/**
* Gets a {@code NameSymbol} with a symbol and no names.
*
- * @param symbol
- * symbol to use
+ * @param symbol symbol to use
* @return NameSymbol instance
* @since 2019-10-21
- * @throws NullPointerException
- * if symbol is null
+ * @throws NullPointerException if symbol is null
*/
public static final NameSymbol ofSymbol(final String symbol) {
- return new NameSymbol(Optional.empty(), Optional.of(symbol), new HashSet<>());
+ return new NameSymbol(Optional.empty(), Optional.of(symbol),
+ new HashSet<>());
}
-
+
private final Optional<String> primaryName;
private final Optional<String> symbol;
-
+
private final Set<String> otherNames;
-
+
/**
* Creates the {@code NameSymbol}.
*
- * @param primaryName
- * primary name of unit
- * @param symbol
- * symbol used to represent unit
- * @param otherNames
- * other names and/or spellings
+ * @param primaryName primary name of unit
+ * @param symbol symbol used to represent unit
+ * @param otherNames other names and/or spellings, should be a mutable copy
+ * of the argument
* @since 2019-10-21
*/
- private NameSymbol(final Optional<String> primaryName, final Optional<String> symbol,
- final Set<String> otherNames) {
+ private NameSymbol(final Optional<String> primaryName,
+ final Optional<String> symbol, final Set<String> otherNames) {
this.primaryName = primaryName;
this.symbol = symbol;
+ otherNames.remove(null);
this.otherNames = Collections.unmodifiableSet(otherNames);
+
+ if (this.primaryName.isEmpty()) {
+ assert this.otherNames.isEmpty();
+ }
}
-
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (!(obj instanceof NameSymbol))
+ return false;
+ final NameSymbol other = (NameSymbol) obj;
+ if (this.otherNames == null) {
+ if (other.otherNames != null)
+ return false;
+ } else if (!this.otherNames.equals(other.otherNames))
+ return false;
+ if (this.primaryName == null) {
+ if (other.primaryName != null)
+ return false;
+ } else if (!this.primaryName.equals(other.primaryName))
+ return false;
+ if (this.symbol == null) {
+ if (other.symbol != null)
+ return false;
+ } else if (!this.symbol.equals(other.symbol))
+ return false;
+ return true;
+ }
+
/**
* @return otherNames
* @since 2019-10-21
@@ -258,7 +240,7 @@ public final class NameSymbol {
public final Set<String> getOtherNames() {
return this.otherNames;
}
-
+
/**
* @return primaryName
* @since 2019-10-21
@@ -266,7 +248,7 @@ public final class NameSymbol {
public final Optional<String> getPrimaryName() {
return this.primaryName;
}
-
+
/**
* @return symbol
* @since 2019-10-21
@@ -274,4 +256,25 @@ public final class NameSymbol {
public final Optional<String> getSymbol() {
return this.symbol;
}
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result
+ + (this.otherNames == null ? 0 : this.otherNames.hashCode());
+ result = prime * result
+ + (this.primaryName == null ? 0 : this.primaryName.hashCode());
+ result = prime * result
+ + (this.symbol == null ? 0 : this.symbol.hashCode());
+ return result;
+ }
+
+ /**
+ * @return true iff this {@code NameSymbol} contains no names or symbols.
+ */
+ public final boolean isEmpty() {
+ // if primaryName is empty, otherNames must also be empty
+ return this.primaryName.isEmpty() && this.symbol.isEmpty();
+ }
} \ No newline at end of file
diff --git a/src/org/unitConverter/unit/Nameable.java b/src/org/unitConverter/unit/Nameable.java
new file mode 100644
index 0000000..36740ab
--- /dev/null
+++ b/src/org/unitConverter/unit/Nameable.java
@@ -0,0 +1,59 @@
+/**
+ * Copyright (C) 2020 Adrien Hopkins
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package org.unitConverter.unit;
+
+import java.util.Optional;
+import java.util.Set;
+
+/**
+ * An object that can hold one or more names, and possibly a symbol. The name
+ * and symbol data should be immutable.
+ *
+ * @since 2020-09-07
+ */
+public interface Nameable {
+ /**
+ * @return a {@code NameSymbol} that contains this object's primary name,
+ * symbol and other names
+ * @since 2020-09-07
+ */
+ NameSymbol getNameSymbol();
+
+ /**
+ * @return set of alternate names
+ * @since 2020-09-07
+ */
+ default Set<String> getOtherNames() {
+ return this.getNameSymbol().getOtherNames();
+ }
+
+ /**
+ * @return preferred name of object
+ * @since 2020-09-07
+ */
+ default Optional<String> getPrimaryName() {
+ return this.getNameSymbol().getPrimaryName();
+ }
+
+ /**
+ * @return short symbol representing object
+ * @since 2020-09-07
+ */
+ default Optional<String> getSymbol() {
+ return this.getNameSymbol().getSymbol();
+ }
+}
diff --git a/src/org/unitConverter/unit/SI.java b/src/org/unitConverter/unit/SI.java
index 0a1bb9b..c88d2bc 100644
--- a/src/org/unitConverter/unit/SI.java
+++ b/src/org/unitConverter/unit/SI.java
@@ -16,13 +16,17 @@
*/
package org.unitConverter.unit;
+import java.util.Set;
+
import org.unitConverter.math.ObjectProduct;
/**
- * All of the units, prefixes and dimensions that are used by the SI, as well as some outside the SI.
+ * All of the units, prefixes and dimensions that are used by the SI, as well as
+ * some outside the SI.
*
* <p>
- * This class does not include prefixed units. To obtain prefixed units, use {@link LinearUnit#withPrefix}:
+ * This class does not include prefixed units. To obtain prefixed units, use
+ * {@link LinearUnit#withPrefix}:
*
* <pre>
* LinearUnit KILOMETRE = SI.METRE.withPrefix(SI.KILO);
@@ -36,42 +40,64 @@ public final class SI {
/// dimensions used by SI units
// base dimensions, as BaseDimensions
public static final class BaseDimensions {
- public static final BaseDimension LENGTH = BaseDimension.valueOf("Length", "L");
- public static final BaseDimension MASS = BaseDimension.valueOf("Mass", "M");
- public static final BaseDimension TIME = BaseDimension.valueOf("Time", "T");
- public static final BaseDimension ELECTRIC_CURRENT = BaseDimension.valueOf("Electric Current", "I");
- public static final BaseDimension TEMPERATURE = BaseDimension.valueOf("Temperature", "\u0398"); // theta symbol
- public static final BaseDimension QUANTITY = BaseDimension.valueOf("Quantity", "N");
- public static final BaseDimension LUMINOUS_INTENSITY = BaseDimension.valueOf("Luminous Intensity", "J");
- public static final BaseDimension INFORMATION = BaseDimension.valueOf("Information", "Info"); // non-SI
- public static final BaseDimension CURRENCY = BaseDimension.valueOf("Currency", "$$"); // non-SI
-
+ public static final BaseDimension LENGTH = BaseDimension.valueOf("Length",
+ "L");
+ public static final BaseDimension MASS = BaseDimension.valueOf("Mass",
+ "M");
+ public static final BaseDimension TIME = BaseDimension.valueOf("Time",
+ "T");
+ public static final BaseDimension ELECTRIC_CURRENT = BaseDimension
+ .valueOf("Electric Current", "I");
+ public static final BaseDimension TEMPERATURE = BaseDimension
+ .valueOf("Temperature", "\u0398"); // theta symbol
+ public static final BaseDimension QUANTITY = BaseDimension
+ .valueOf("Quantity", "N");
+ public static final BaseDimension LUMINOUS_INTENSITY = BaseDimension
+ .valueOf("Luminous Intensity", "J");
+ public static final BaseDimension INFORMATION = BaseDimension
+ .valueOf("Information", "Info"); // non-SI
+ public static final BaseDimension CURRENCY = BaseDimension
+ .valueOf("Currency", "$$"); // non-SI
+
// You may NOT get SI.BaseDimensions instances!
private BaseDimensions() {
throw new AssertionError();
}
}
-
+
/// base units of the SI
- // suppressing warnings since these are the same object, but in a different form (class)
+ // suppressing warnings since these are the same object, but in a different
+ /// form (class)
@SuppressWarnings("hiding")
public static final class BaseUnits {
- public static final BaseUnit METRE = BaseUnit.valueOf(BaseDimensions.LENGTH, "metre", "m");
- public static final BaseUnit KILOGRAM = BaseUnit.valueOf(BaseDimensions.MASS, "kilogram", "kg");
- public static final BaseUnit SECOND = BaseUnit.valueOf(BaseDimensions.TIME, "second", "s");
- public static final BaseUnit AMPERE = BaseUnit.valueOf(BaseDimensions.ELECTRIC_CURRENT, "ampere", "A");
- public static final BaseUnit KELVIN = BaseUnit.valueOf(BaseDimensions.TEMPERATURE, "kelvin", "K");
- public static final BaseUnit MOLE = BaseUnit.valueOf(BaseDimensions.QUANTITY, "mole", "mol");
- public static final BaseUnit CANDELA = BaseUnit.valueOf(BaseDimensions.LUMINOUS_INTENSITY, "candela", "cd");
- public static final BaseUnit BIT = BaseUnit.valueOf(BaseDimensions.INFORMATION, "bit", "b");
- public static final BaseUnit DOLLAR = BaseUnit.valueOf(BaseDimensions.CURRENCY, "dollar", "$");
-
+ public static final BaseUnit METRE = BaseUnit
+ .valueOf(BaseDimensions.LENGTH, "metre", "m");
+ public static final BaseUnit KILOGRAM = BaseUnit
+ .valueOf(BaseDimensions.MASS, "kilogram", "kg");
+ public static final BaseUnit SECOND = BaseUnit
+ .valueOf(BaseDimensions.TIME, "second", "s");
+ public static final BaseUnit AMPERE = BaseUnit
+ .valueOf(BaseDimensions.ELECTRIC_CURRENT, "ampere", "A");
+ public static final BaseUnit KELVIN = BaseUnit
+ .valueOf(BaseDimensions.TEMPERATURE, "kelvin", "K");
+ public static final BaseUnit MOLE = BaseUnit
+ .valueOf(BaseDimensions.QUANTITY, "mole", "mol");
+ public static final BaseUnit CANDELA = BaseUnit
+ .valueOf(BaseDimensions.LUMINOUS_INTENSITY, "candela", "cd");
+ public static final BaseUnit BIT = BaseUnit
+ .valueOf(BaseDimensions.INFORMATION, "bit", "b");
+ public static final BaseUnit DOLLAR = BaseUnit
+ .valueOf(BaseDimensions.CURRENCY, "dollar", "$");
+
+ public static final Set<BaseUnit> BASE_UNITS = Set.of(METRE, KILOGRAM,
+ SECOND, AMPERE, KELVIN, MOLE, CANDELA, BIT);
+
// You may NOT get SI.BaseUnits instances!
private BaseUnits() {
throw new AssertionError();
}
}
-
+
/**
* Constants that relate to the SI or other systems.
*
@@ -79,195 +105,308 @@ public final class SI {
* @since 2019-11-08
*/
public static final class Constants {
- public static final LinearUnit EARTH_GRAVITY = METRE.dividedBy(SECOND).dividedBy(SECOND).times(9.80665);
+ public static final LinearUnit EARTH_GRAVITY = METRE.dividedBy(SECOND)
+ .dividedBy(SECOND).times(9.80665);
}
-
+
// dimensions used in the SI, as ObjectProducts
public static final class Dimensions {
- public static final ObjectProduct<BaseDimension> EMPTY = ObjectProduct.empty();
- public static final ObjectProduct<BaseDimension> LENGTH = ObjectProduct.oneOf(BaseDimensions.LENGTH);
- public static final ObjectProduct<BaseDimension> MASS = ObjectProduct.oneOf(BaseDimensions.MASS);
- public static final ObjectProduct<BaseDimension> TIME = ObjectProduct.oneOf(BaseDimensions.TIME);
+ public static final ObjectProduct<BaseDimension> EMPTY = ObjectProduct
+ .empty();
+ public static final ObjectProduct<BaseDimension> LENGTH = ObjectProduct
+ .oneOf(BaseDimensions.LENGTH);
+ public static final ObjectProduct<BaseDimension> MASS = ObjectProduct
+ .oneOf(BaseDimensions.MASS);
+ public static final ObjectProduct<BaseDimension> TIME = ObjectProduct
+ .oneOf(BaseDimensions.TIME);
public static final ObjectProduct<BaseDimension> ELECTRIC_CURRENT = ObjectProduct
.oneOf(BaseDimensions.ELECTRIC_CURRENT);
- public static final ObjectProduct<BaseDimension> TEMPERATURE = ObjectProduct.oneOf(BaseDimensions.TEMPERATURE);
- public static final ObjectProduct<BaseDimension> QUANTITY = ObjectProduct.oneOf(BaseDimensions.QUANTITY);
+ public static final ObjectProduct<BaseDimension> TEMPERATURE = ObjectProduct
+ .oneOf(BaseDimensions.TEMPERATURE);
+ public static final ObjectProduct<BaseDimension> QUANTITY = ObjectProduct
+ .oneOf(BaseDimensions.QUANTITY);
public static final ObjectProduct<BaseDimension> LUMINOUS_INTENSITY = ObjectProduct
.oneOf(BaseDimensions.LUMINOUS_INTENSITY);
- public static final ObjectProduct<BaseDimension> INFORMATION = ObjectProduct.oneOf(BaseDimensions.INFORMATION);
- public static final ObjectProduct<BaseDimension> CURRENCY = ObjectProduct.oneOf(BaseDimensions.CURRENCY);
+ public static final ObjectProduct<BaseDimension> INFORMATION = ObjectProduct
+ .oneOf(BaseDimensions.INFORMATION);
+ public static final ObjectProduct<BaseDimension> CURRENCY = ObjectProduct
+ .oneOf(BaseDimensions.CURRENCY);
+
// derived dimensions without named SI units
- public static final ObjectProduct<BaseDimension> AREA = LENGTH.times(LENGTH);
-
- public static final ObjectProduct<BaseDimension> VOLUME = AREA.times(LENGTH);
- public static final ObjectProduct<BaseDimension> VELOCITY = LENGTH.dividedBy(TIME);
- public static final ObjectProduct<BaseDimension> ACCELERATION = VELOCITY.dividedBy(TIME);
- public static final ObjectProduct<BaseDimension> WAVENUMBER = EMPTY.dividedBy(LENGTH);
- public static final ObjectProduct<BaseDimension> MASS_DENSITY = MASS.dividedBy(VOLUME);
- public static final ObjectProduct<BaseDimension> SURFACE_DENSITY = MASS.dividedBy(AREA);
- public static final ObjectProduct<BaseDimension> SPECIFIC_VOLUME = VOLUME.dividedBy(MASS);
- public static final ObjectProduct<BaseDimension> CURRENT_DENSITY = ELECTRIC_CURRENT.dividedBy(AREA);
- public static final ObjectProduct<BaseDimension> MAGNETIC_FIELD_STRENGTH = ELECTRIC_CURRENT.dividedBy(LENGTH);
- public static final ObjectProduct<BaseDimension> CONCENTRATION = QUANTITY.dividedBy(VOLUME);
- public static final ObjectProduct<BaseDimension> MASS_CONCENTRATION = CONCENTRATION.times(MASS);
- public static final ObjectProduct<BaseDimension> LUMINANCE = LUMINOUS_INTENSITY.dividedBy(AREA);
- public static final ObjectProduct<BaseDimension> REFRACTIVE_INDEX = VELOCITY.dividedBy(VELOCITY);
- public static final ObjectProduct<BaseDimension> REFLACTIVE_PERMEABILITY = EMPTY.times(EMPTY);
- public static final ObjectProduct<BaseDimension> ANGLE = LENGTH.dividedBy(LENGTH);
- public static final ObjectProduct<BaseDimension> SOLID_ANGLE = AREA.dividedBy(AREA);
-
+ public static final ObjectProduct<BaseDimension> AREA = LENGTH
+ .times(LENGTH);
+ public static final ObjectProduct<BaseDimension> VOLUME = AREA
+ .times(LENGTH);
+ public static final ObjectProduct<BaseDimension> VELOCITY = LENGTH
+ .dividedBy(TIME);
+ public static final ObjectProduct<BaseDimension> ACCELERATION = VELOCITY
+ .dividedBy(TIME);
+ public static final ObjectProduct<BaseDimension> WAVENUMBER = EMPTY
+ .dividedBy(LENGTH);
+ public static final ObjectProduct<BaseDimension> MASS_DENSITY = MASS
+ .dividedBy(VOLUME);
+ public static final ObjectProduct<BaseDimension> SURFACE_DENSITY = MASS
+ .dividedBy(AREA);
+ public static final ObjectProduct<BaseDimension> SPECIFIC_VOLUME = VOLUME
+ .dividedBy(MASS);
+ public static final ObjectProduct<BaseDimension> CURRENT_DENSITY = ELECTRIC_CURRENT
+ .dividedBy(AREA);
+ public static final ObjectProduct<BaseDimension> MAGNETIC_FIELD_STRENGTH = ELECTRIC_CURRENT
+ .dividedBy(LENGTH);
+ public static final ObjectProduct<BaseDimension> CONCENTRATION = QUANTITY
+ .dividedBy(VOLUME);
+ public static final ObjectProduct<BaseDimension> MASS_CONCENTRATION = CONCENTRATION
+ .times(MASS);
+ public static final ObjectProduct<BaseDimension> LUMINANCE = LUMINOUS_INTENSITY
+ .dividedBy(AREA);
+ public static final ObjectProduct<BaseDimension> REFRACTIVE_INDEX = VELOCITY
+ .dividedBy(VELOCITY);
+ public static final ObjectProduct<BaseDimension> REFRACTIVE_PERMEABILITY = EMPTY
+ .times(EMPTY);
+ public static final ObjectProduct<BaseDimension> ANGLE = LENGTH
+ .dividedBy(LENGTH);
+ public static final ObjectProduct<BaseDimension> SOLID_ANGLE = AREA
+ .dividedBy(AREA);
+
// derived dimensions with named SI units
- public static final ObjectProduct<BaseDimension> FREQUENCY = EMPTY.dividedBy(TIME);
- public static final ObjectProduct<BaseDimension> FORCE = MASS.times(ACCELERATION);
- public static final ObjectProduct<BaseDimension> ENERGY = FORCE.times(LENGTH);
- public static final ObjectProduct<BaseDimension> POWER = ENERGY.dividedBy(TIME);
- public static final ObjectProduct<BaseDimension> ELECTRIC_CHARGE = ELECTRIC_CURRENT.times(TIME);
- public static final ObjectProduct<BaseDimension> VOLTAGE = ENERGY.dividedBy(ELECTRIC_CHARGE);
- public static final ObjectProduct<BaseDimension> CAPACITANCE = ELECTRIC_CHARGE.dividedBy(VOLTAGE);
- public static final ObjectProduct<BaseDimension> ELECTRIC_RESISTANCE = VOLTAGE.dividedBy(ELECTRIC_CURRENT);
- public static final ObjectProduct<BaseDimension> ELECTRIC_CONDUCTANCE = ELECTRIC_CURRENT.dividedBy(VOLTAGE);
- public static final ObjectProduct<BaseDimension> MAGNETIC_FLUX = VOLTAGE.times(TIME);
- public static final ObjectProduct<BaseDimension> MAGNETIC_FLUX_DENSITY = MAGNETIC_FLUX.dividedBy(AREA);
- public static final ObjectProduct<BaseDimension> INDUCTANCE = MAGNETIC_FLUX.dividedBy(ELECTRIC_CURRENT);
- public static final ObjectProduct<BaseDimension> LUMINOUS_FLUX = LUMINOUS_INTENSITY.times(SOLID_ANGLE);
- public static final ObjectProduct<BaseDimension> ILLUMINANCE = LUMINOUS_FLUX.dividedBy(AREA);
- public static final ObjectProduct<BaseDimension> SPECIFIC_ENERGY = ENERGY.dividedBy(MASS);
- public static final ObjectProduct<BaseDimension> CATALYTIC_ACTIVITY = QUANTITY.dividedBy(TIME);
-
+ public static final ObjectProduct<BaseDimension> FREQUENCY = EMPTY
+ .dividedBy(TIME);
+ public static final ObjectProduct<BaseDimension> FORCE = MASS
+ .times(ACCELERATION);
+ public static final ObjectProduct<BaseDimension> ENERGY = FORCE
+ .times(LENGTH);
+ public static final ObjectProduct<BaseDimension> POWER = ENERGY
+ .dividedBy(TIME);
+ public static final ObjectProduct<BaseDimension> ELECTRIC_CHARGE = ELECTRIC_CURRENT
+ .times(TIME);
+ public static final ObjectProduct<BaseDimension> VOLTAGE = ENERGY
+ .dividedBy(ELECTRIC_CHARGE);
+ public static final ObjectProduct<BaseDimension> CAPACITANCE = ELECTRIC_CHARGE
+ .dividedBy(VOLTAGE);
+ public static final ObjectProduct<BaseDimension> ELECTRIC_RESISTANCE = VOLTAGE
+ .dividedBy(ELECTRIC_CURRENT);
+ public static final ObjectProduct<BaseDimension> ELECTRIC_CONDUCTANCE = ELECTRIC_CURRENT
+ .dividedBy(VOLTAGE);
+ public static final ObjectProduct<BaseDimension> MAGNETIC_FLUX = VOLTAGE
+ .times(TIME);
+ public static final ObjectProduct<BaseDimension> MAGNETIC_FLUX_DENSITY = MAGNETIC_FLUX
+ .dividedBy(AREA);
+ public static final ObjectProduct<BaseDimension> INDUCTANCE = MAGNETIC_FLUX
+ .dividedBy(ELECTRIC_CURRENT);
+ public static final ObjectProduct<BaseDimension> LUMINOUS_FLUX = LUMINOUS_INTENSITY
+ .times(SOLID_ANGLE);
+ public static final ObjectProduct<BaseDimension> ILLUMINANCE = LUMINOUS_FLUX
+ .dividedBy(AREA);
+ public static final ObjectProduct<BaseDimension> SPECIFIC_ENERGY = ENERGY
+ .dividedBy(MASS);
+ public static final ObjectProduct<BaseDimension> CATALYTIC_ACTIVITY = QUANTITY
+ .dividedBy(TIME);
+
// You may NOT get SI.Dimension instances!
private Dimensions() {
throw new AssertionError();
}
}
-
+
/// The units of the SI
- public static final LinearUnit ONE = LinearUnit.valueOf(ObjectProduct.empty(), 1);
+ public static final LinearUnit ONE = LinearUnit
+ .valueOf(ObjectProduct.empty(), 1);
+
public static final LinearUnit METRE = BaseUnits.METRE.asLinearUnit()
.withName(NameSymbol.of("metre", "m", "meter"));
public static final LinearUnit KILOGRAM = BaseUnits.KILOGRAM.asLinearUnit()
.withName(NameSymbol.of("kilogram", "kg"));
public static final LinearUnit SECOND = BaseUnits.SECOND.asLinearUnit()
.withName(NameSymbol.of("second", "s", "sec"));
- public static final LinearUnit AMPERE = BaseUnits.AMPERE.asLinearUnit().withName(NameSymbol.of("ampere", "A"));
- public static final LinearUnit KELVIN = BaseUnits.KELVIN.asLinearUnit().withName(NameSymbol.of("kelvin", "K"));
- public static final LinearUnit MOLE = BaseUnits.MOLE.asLinearUnit().withName(NameSymbol.of("mole", "mol"));
- public static final LinearUnit CANDELA = BaseUnits.CANDELA.asLinearUnit().withName(NameSymbol.of("candela", "cd"));
- public static final LinearUnit BIT = BaseUnits.BIT.asLinearUnit().withName(NameSymbol.of("bit", "b"));
- public static final LinearUnit DOLLAR = BaseUnits.DOLLAR.asLinearUnit().withName(NameSymbol.of("dollar", "$"));
-
+ public static final LinearUnit AMPERE = BaseUnits.AMPERE.asLinearUnit()
+ .withName(NameSymbol.of("ampere", "A"));
+ public static final LinearUnit KELVIN = BaseUnits.KELVIN.asLinearUnit()
+ .withName(NameSymbol.of("kelvin", "K"));
+ public static final LinearUnit MOLE = BaseUnits.MOLE.asLinearUnit()
+ .withName(NameSymbol.of("mole", "mol"));
+ public static final LinearUnit CANDELA = BaseUnits.CANDELA.asLinearUnit()
+ .withName(NameSymbol.of("candela", "cd"));
+ public static final LinearUnit BIT = BaseUnits.BIT.asLinearUnit()
+ .withName(NameSymbol.of("bit", "b"));
+ public static final LinearUnit DOLLAR = BaseUnits.DOLLAR.asLinearUnit()
+ .withName(NameSymbol.of("dollar", "$"));
// Non-base units
- public static final LinearUnit RADIAN = METRE.dividedBy(METRE).withName(NameSymbol.of("radian", "rad"));
- public static final LinearUnit STERADIAN = RADIAN.times(RADIAN).withName(NameSymbol.of("steradian", "sr"));
- public static final LinearUnit HERTZ = ONE.dividedBy(SECOND).withName(NameSymbol.of("hertz", "Hz"));
+ public static final LinearUnit RADIAN = METRE.dividedBy(METRE)
+ .withName(NameSymbol.of("radian", "rad"));
+
+ public static final LinearUnit STERADIAN = RADIAN.times(RADIAN)
+ .withName(NameSymbol.of("steradian", "sr"));
+ public static final LinearUnit HERTZ = ONE.dividedBy(SECOND)
+ .withName(NameSymbol.of("hertz", "Hz"));
// for periodic phenomena
- public static final LinearUnit NEWTON = KILOGRAM.times(METRE).dividedBy(SECOND.times(SECOND))
+ public static final LinearUnit NEWTON = KILOGRAM.times(METRE)
+ .dividedBy(SECOND.times(SECOND))
.withName(NameSymbol.of("newton", "N"));
public static final LinearUnit PASCAL = NEWTON.dividedBy(METRE.times(METRE))
.withName(NameSymbol.of("pascal", "Pa"));
- public static final LinearUnit JOULE = NEWTON.times(METRE).withName(NameSymbol.of("joule", "J"));
- public static final LinearUnit WATT = JOULE.dividedBy(SECOND).withName(NameSymbol.of("watt", "W"));
- public static final LinearUnit COULOMB = AMPERE.times(SECOND).withName(NameSymbol.of("coulomb", "C"));
- public static final LinearUnit VOLT = JOULE.dividedBy(COULOMB).withName(NameSymbol.of("volt", "V"));
- public static final LinearUnit FARAD = COULOMB.dividedBy(VOLT).withName(NameSymbol.of("farad", "F"));
- public static final LinearUnit OHM = VOLT.dividedBy(AMPERE).withName(NameSymbol.of("ohm", "\u03A9")); // omega
- public static final LinearUnit SIEMENS = ONE.dividedBy(OHM).withName(NameSymbol.of("siemens", "S"));
- public static final LinearUnit WEBER = VOLT.times(SECOND).withName(NameSymbol.of("weber", "Wb"));
- public static final LinearUnit TESLA = WEBER.dividedBy(METRE.times(METRE)).withName(NameSymbol.of("tesla", "T"));
- public static final LinearUnit HENRY = WEBER.dividedBy(AMPERE).withName(NameSymbol.of("henry", "H"));
- public static final LinearUnit LUMEN = CANDELA.times(STERADIAN).withName(NameSymbol.of("lumen", "lm"));
- public static final LinearUnit LUX = LUMEN.dividedBy(METRE.times(METRE)).withName(NameSymbol.of("lux", "lx"));
- public static final LinearUnit BEQUEREL = ONE.dividedBy(SECOND).withName(NameSymbol.of("bequerel", "Bq"));
+ public static final LinearUnit JOULE = NEWTON.times(METRE)
+ .withName(NameSymbol.of("joule", "J"));
+ public static final LinearUnit WATT = JOULE.dividedBy(SECOND)
+ .withName(NameSymbol.of("watt", "W"));
+ public static final LinearUnit COULOMB = AMPERE.times(SECOND)
+ .withName(NameSymbol.of("coulomb", "C"));
+ public static final LinearUnit VOLT = JOULE.dividedBy(COULOMB)
+ .withName(NameSymbol.of("volt", "V"));
+ public static final LinearUnit FARAD = COULOMB.dividedBy(VOLT)
+ .withName(NameSymbol.of("farad", "F"));
+ public static final LinearUnit OHM = VOLT.dividedBy(AMPERE)
+ .withName(NameSymbol.of("ohm", "\u03A9")); // omega
+ public static final LinearUnit SIEMENS = ONE.dividedBy(OHM)
+ .withName(NameSymbol.of("siemens", "S"));
+ public static final LinearUnit WEBER = VOLT.times(SECOND)
+ .withName(NameSymbol.of("weber", "Wb"));
+ public static final LinearUnit TESLA = WEBER.dividedBy(METRE.times(METRE))
+ .withName(NameSymbol.of("tesla", "T"));
+ public static final LinearUnit HENRY = WEBER.dividedBy(AMPERE)
+ .withName(NameSymbol.of("henry", "H"));
+ public static final LinearUnit LUMEN = CANDELA.times(STERADIAN)
+ .withName(NameSymbol.of("lumen", "lm"));
+ public static final LinearUnit LUX = LUMEN.dividedBy(METRE.times(METRE))
+ .withName(NameSymbol.of("lux", "lx"));
+ public static final LinearUnit BEQUEREL = ONE.dividedBy(SECOND)
+ .withName(NameSymbol.of("bequerel", "Bq"));
// for activity referred to a nucleotide
- public static final LinearUnit GRAY = JOULE.dividedBy(KILOGRAM).withName(NameSymbol.of("grey", "Gy"));
+ public static final LinearUnit GRAY = JOULE.dividedBy(KILOGRAM)
+ .withName(NameSymbol.of("grey", "Gy"));
// for absorbed dose
- public static final LinearUnit SIEVERT = JOULE.dividedBy(KILOGRAM).withName(NameSymbol.of("sievert", "Sv"));
+ public static final LinearUnit SIEVERT = JOULE.dividedBy(KILOGRAM)
+ .withName(NameSymbol.of("sievert", "Sv"));
// for dose equivalent
- public static final LinearUnit KATAL = MOLE.dividedBy(SECOND).withName(NameSymbol.of("katal", "kat"));
-
+ public static final LinearUnit KATAL = MOLE.dividedBy(SECOND)
+ .withName(NameSymbol.of("katal", "kat"));
// common derived units included for convenience
- public static final LinearUnit GRAM = KILOGRAM.dividedBy(1000).withName(NameSymbol.of("gram", "g"));
+ public static final LinearUnit GRAM = KILOGRAM.dividedBy(1000)
+ .withName(NameSymbol.of("gram", "g"));
+
public static final LinearUnit SQUARE_METRE = METRE.toExponent(2)
- .withName(NameSymbol.of("square metre", "m^2", "square meter", "metre squared", "meter squared"));
+ .withName(NameSymbol.of("square metre", "m^2", "square meter",
+ "metre squared", "meter squared"));
public static final LinearUnit CUBIC_METRE = METRE.toExponent(3)
- .withName(NameSymbol.of("cubic metre", "m^3", "cubic meter", "metre cubed", "meter cubed"));
+ .withName(NameSymbol.of("cubic metre", "m^3", "cubic meter",
+ "metre cubed", "meter cubed"));
public static final LinearUnit METRE_PER_SECOND = METRE.dividedBy(SECOND)
- .withName(NameSymbol.of("metre per second", "m/s", "meter per second"));
-
+ .withName(
+ NameSymbol.of("metre per second", "m/s", "meter per second"));
// Non-SI units included for convenience
public static final Unit CELSIUS = Unit
- .fromConversionFunctions(KELVIN.getBase(), tempK -> tempK - 273.15, tempC -> tempC + 273.15)
+ .fromConversionFunctions(KELVIN.getBase(), tempK -> tempK - 273.15,
+ tempC -> tempC + 273.15)
.withName(NameSymbol.of("degree Celsius", "\u00B0C"));
- public static final LinearUnit MINUTE = SECOND.times(60).withName(NameSymbol.of("minute", "min"));
- public static final LinearUnit HOUR = MINUTE.times(60).withName(NameSymbol.of("hour", "h", "hr"));
- public static final LinearUnit DAY = HOUR.times(60).withName(NameSymbol.of("day", "d"));
- public static final LinearUnit KILOMETRE_PER_HOUR = METRE.times(1000).dividedBy(HOUR)
- .withName(NameSymbol.of("kilometre per hour", "km/h", "kilometer per hour"));
+
+ public static final LinearUnit MINUTE = SECOND.times(60)
+ .withName(NameSymbol.of("minute", "min"));
+ public static final LinearUnit HOUR = MINUTE.times(60)
+ .withName(NameSymbol.of("hour", "h", "hr"));
+ public static final LinearUnit DAY = HOUR.times(60)
+ .withName(NameSymbol.of("day", "d"));
+ public static final LinearUnit KILOMETRE_PER_HOUR = METRE.times(1000)
+ .dividedBy(HOUR).withName(NameSymbol.of("kilometre per hour", "km/h",
+ "kilometer per hour"));
public static final LinearUnit DEGREE = RADIAN.times(360 / (2 * Math.PI))
.withName(NameSymbol.of("degree", "\u00B0", "deg"));
- public static final LinearUnit ARCMINUTE = DEGREE.dividedBy(60).withName(NameSymbol.of("arcminute", "arcmin"));
- public static final LinearUnit ARCSECOND = ARCMINUTE.dividedBy(60).withName(NameSymbol.of("arcsecond", "arcsec"));
- public static final LinearUnit ASTRONOMICAL_UNIT = METRE.times(149597870700.0)
+ public static final LinearUnit ARCMINUTE = DEGREE.dividedBy(60)
+ .withName(NameSymbol.of("arcminute", "arcmin"));
+ public static final LinearUnit ARCSECOND = ARCMINUTE.dividedBy(60)
+ .withName(NameSymbol.of("arcsecond", "arcsec"));
+ public static final LinearUnit ASTRONOMICAL_UNIT = METRE
+ .times(149597870700.0)
.withName(NameSymbol.of("astronomical unit", "au"));
- public static final LinearUnit PARSEC = ASTRONOMICAL_UNIT.dividedBy(ARCSECOND)
- .withName(NameSymbol.of("parsec", "pc"));
- public static final LinearUnit HECTARE = METRE.times(METRE).times(10000.0).withName(NameSymbol.of("hectare", "ha"));
- public static final LinearUnit LITRE = METRE.times(METRE).times(METRE).dividedBy(1000.0)
- .withName(NameSymbol.of("litre", "L", "l", "liter"));
- public static final LinearUnit TONNE = KILOGRAM.times(1000.0).withName(NameSymbol.of("tonne", "t", "metric ton"));
+ public static final LinearUnit PARSEC = ASTRONOMICAL_UNIT
+ .dividedBy(ARCSECOND).withName(NameSymbol.of("parsec", "pc"));
+ public static final LinearUnit HECTARE = METRE.times(METRE).times(10000.0)
+ .withName(NameSymbol.of("hectare", "ha"));
+ public static final LinearUnit LITRE = METRE.times(METRE).times(METRE)
+ .dividedBy(1000.0).withName(NameSymbol.of("litre", "L", "l", "liter"));
+ public static final LinearUnit TONNE = KILOGRAM.times(1000.0)
+ .withName(NameSymbol.of("tonne", "t", "metric ton"));
public static final LinearUnit DALTON = KILOGRAM.times(1.660539040e-27)
- .withName(NameSymbol.of("dalton", "Da", "atomic unit", "u")); // approximate value
+ .withName(NameSymbol.of("dalton", "Da", "atomic unit", "u")); // approximate
+ // value
public static final LinearUnit ELECTRONVOLT = JOULE.times(1.602176634e-19)
.withName(NameSymbol.of("electron volt", "eV"));
- public static final LinearUnit BYTE = BIT.times(8).withName(NameSymbol.of("byte", "B"));
- public static final Unit NEPER = Unit
- .fromConversionFunctions(ONE.getBase(), pr -> 0.5 * Math.log(pr), Np -> Math.exp(2 * Np))
+ public static final LinearUnit BYTE = BIT.times(8)
+ .withName(NameSymbol.of("byte", "B"));
+ public static final Unit NEPER = Unit.fromConversionFunctions(ONE.getBase(),
+ pr -> 0.5 * Math.log(pr), Np -> Math.exp(2 * Np))
.withName(NameSymbol.of("neper", "Np"));
- public static final Unit BEL = Unit
- .fromConversionFunctions(ONE.getBase(), pr -> Math.log10(pr), dB -> Math.pow(10, dB))
+ public static final Unit BEL = Unit.fromConversionFunctions(ONE.getBase(),
+ pr -> Math.log10(pr), dB -> Math.pow(10, dB))
.withName(NameSymbol.of("bel", "B"));
public static final Unit DECIBEL = Unit
- .fromConversionFunctions(ONE.getBase(), pr -> 10 * Math.log10(pr), dB -> Math.pow(10, dB / 10))
+ .fromConversionFunctions(ONE.getBase(), pr -> 10 * Math.log10(pr),
+ dB -> Math.pow(10, dB / 10))
.withName(NameSymbol.of("decibel", "dB"));
-
+
/// The prefixes of the SI
// expanding decimal prefixes
- public static final UnitPrefix KILO = UnitPrefix.valueOf(1e3).withName(NameSymbol.of("kilo", "k", "K"));
- public static final UnitPrefix MEGA = UnitPrefix.valueOf(1e6).withName(NameSymbol.of("mega", "M"));
- public static final UnitPrefix GIGA = UnitPrefix.valueOf(1e9).withName(NameSymbol.of("giga", "G"));
- public static final UnitPrefix TERA = UnitPrefix.valueOf(1e12).withName(NameSymbol.of("tera", "T"));
- public static final UnitPrefix PETA = UnitPrefix.valueOf(1e15).withName(NameSymbol.of("peta", "P"));
- public static final UnitPrefix EXA = UnitPrefix.valueOf(1e18).withName(NameSymbol.of("exa", "E"));
- public static final UnitPrefix ZETTA = UnitPrefix.valueOf(1e21).withName(NameSymbol.of("zetta", "Z"));
- public static final UnitPrefix YOTTA = UnitPrefix.valueOf(1e24).withName(NameSymbol.of("yotta", "Y"));
-
+ public static final UnitPrefix KILO = UnitPrefix.valueOf(1e3)
+ .withName(NameSymbol.of("kilo", "k", "K"));
+ public static final UnitPrefix MEGA = UnitPrefix.valueOf(1e6)
+ .withName(NameSymbol.of("mega", "M"));
+ public static final UnitPrefix GIGA = UnitPrefix.valueOf(1e9)
+ .withName(NameSymbol.of("giga", "G"));
+ public static final UnitPrefix TERA = UnitPrefix.valueOf(1e12)
+ .withName(NameSymbol.of("tera", "T"));
+ public static final UnitPrefix PETA = UnitPrefix.valueOf(1e15)
+ .withName(NameSymbol.of("peta", "P"));
+ public static final UnitPrefix EXA = UnitPrefix.valueOf(1e18)
+ .withName(NameSymbol.of("exa", "E"));
+ public static final UnitPrefix ZETTA = UnitPrefix.valueOf(1e21)
+ .withName(NameSymbol.of("zetta", "Z"));
+ public static final UnitPrefix YOTTA = UnitPrefix.valueOf(1e24)
+ .withName(NameSymbol.of("yotta", "Y"));
+
// contracting decimal prefixes
- public static final UnitPrefix MILLI = UnitPrefix.valueOf(1e-3).withName(NameSymbol.of("milli", "m"));
- public static final UnitPrefix MICRO = UnitPrefix.valueOf(1e-6).withName(NameSymbol.of("micro", "\u03BC", "u")); // mu
- public static final UnitPrefix NANO = UnitPrefix.valueOf(1e-9).withName(NameSymbol.of("nano", "n"));
- public static final UnitPrefix PICO = UnitPrefix.valueOf(1e-12).withName(NameSymbol.of("pico", "p"));
- public static final UnitPrefix FEMTO = UnitPrefix.valueOf(1e-15).withName(NameSymbol.of("femto", "f"));
- public static final UnitPrefix ATTO = UnitPrefix.valueOf(1e-18).withName(NameSymbol.of("atto", "a"));
- public static final UnitPrefix ZEPTO = UnitPrefix.valueOf(1e-21).withName(NameSymbol.of("zepto", "z"));
- public static final UnitPrefix YOCTO = UnitPrefix.valueOf(1e-24).withName(NameSymbol.of("yocto", "y"));
-
+ public static final UnitPrefix MILLI = UnitPrefix.valueOf(1e-3)
+ .withName(NameSymbol.of("milli", "m"));
+ public static final UnitPrefix MICRO = UnitPrefix.valueOf(1e-6)
+ .withName(NameSymbol.of("micro", "\u03BC", "u")); // mu
+ public static final UnitPrefix NANO = UnitPrefix.valueOf(1e-9)
+ .withName(NameSymbol.of("nano", "n"));
+ public static final UnitPrefix PICO = UnitPrefix.valueOf(1e-12)
+ .withName(NameSymbol.of("pico", "p"));
+ public static final UnitPrefix FEMTO = UnitPrefix.valueOf(1e-15)
+ .withName(NameSymbol.of("femto", "f"));
+ public static final UnitPrefix ATTO = UnitPrefix.valueOf(1e-18)
+ .withName(NameSymbol.of("atto", "a"));
+ public static final UnitPrefix ZEPTO = UnitPrefix.valueOf(1e-21)
+ .withName(NameSymbol.of("zepto", "z"));
+ public static final UnitPrefix YOCTO = UnitPrefix.valueOf(1e-24)
+ .withName(NameSymbol.of("yocto", "y"));
+
// prefixes that don't match the pattern of thousands
- public static final UnitPrefix DEKA = UnitPrefix.valueOf(1e1).withName(NameSymbol.of("deka", "da", "deca", "D"));
- public static final UnitPrefix HECTO = UnitPrefix.valueOf(1e2).withName(NameSymbol.of("hecto", "h", "H", "hekto"));
- public static final UnitPrefix DECI = UnitPrefix.valueOf(1e-1).withName(NameSymbol.of("deci", "d"));
- public static final UnitPrefix CENTI = UnitPrefix.valueOf(1e-2).withName(NameSymbol.of("centi", "c"));
- public static final UnitPrefix KIBI = UnitPrefix.valueOf(1024).withName(NameSymbol.of("kibi", "Ki"));
- public static final UnitPrefix MEBI = KIBI.times(1024).withName(NameSymbol.of("mebi", "Mi"));
- public static final UnitPrefix GIBI = MEBI.times(1024).withName(NameSymbol.of("gibi", "Gi"));
- public static final UnitPrefix TEBI = GIBI.times(1024).withName(NameSymbol.of("tebi", "Ti"));
- public static final UnitPrefix PEBI = TEBI.times(1024).withName(NameSymbol.of("pebi", "Pi"));
- public static final UnitPrefix EXBI = PEBI.times(1024).withName(NameSymbol.of("exbi", "Ei"));
-
+ public static final UnitPrefix DEKA = UnitPrefix.valueOf(1e1)
+ .withName(NameSymbol.of("deka", "da", "deca", "D"));
+ public static final UnitPrefix HECTO = UnitPrefix.valueOf(1e2)
+ .withName(NameSymbol.of("hecto", "h", "H", "hekto"));
+ public static final UnitPrefix DECI = UnitPrefix.valueOf(1e-1)
+ .withName(NameSymbol.of("deci", "d"));
+ public static final UnitPrefix CENTI = UnitPrefix.valueOf(1e-2)
+ .withName(NameSymbol.of("centi", "c"));
+ public static final UnitPrefix KIBI = UnitPrefix.valueOf(1024)
+ .withName(NameSymbol.of("kibi", "Ki"));
+ public static final UnitPrefix MEBI = KIBI.times(1024)
+ .withName(NameSymbol.of("mebi", "Mi"));
+ public static final UnitPrefix GIBI = MEBI.times(1024)
+ .withName(NameSymbol.of("gibi", "Gi"));
+ public static final UnitPrefix TEBI = GIBI.times(1024)
+ .withName(NameSymbol.of("tebi", "Ti"));
+ public static final UnitPrefix PEBI = TEBI.times(1024)
+ .withName(NameSymbol.of("pebi", "Pi"));
+ public static final UnitPrefix EXBI = PEBI.times(1024)
+ .withName(NameSymbol.of("exbi", "Ei"));
+
// a few prefixed units
public static final LinearUnit MICROMETRE = SI.METRE.withPrefix(SI.MICRO);
public static final LinearUnit MILLIMETRE = SI.METRE.withPrefix(SI.MILLI);
public static final LinearUnit KILOMETRE = SI.METRE.withPrefix(SI.KILO);
public static final LinearUnit MEGAMETRE = SI.METRE.withPrefix(SI.MEGA);
-
+
public static final LinearUnit MICROLITRE = SI.LITRE.withPrefix(SI.MICRO);
public static final LinearUnit MILLILITRE = SI.LITRE.withPrefix(SI.MILLI);
public static final LinearUnit KILOLITRE = SI.LITRE.withPrefix(SI.KILO);
@@ -291,27 +430,48 @@ public final class SI {
public static final LinearUnit MILLIJOULE = SI.JOULE.withPrefix(SI.MILLI);
public static final LinearUnit KILOJOULE = SI.JOULE.withPrefix(SI.KILO);
public static final LinearUnit MEGAJOULE = SI.JOULE.withPrefix(SI.MEGA);
-
+
public static final LinearUnit MICROWATT = SI.WATT.withPrefix(SI.MICRO);
public static final LinearUnit MILLIWATT = SI.WATT.withPrefix(SI.MILLI);
public static final LinearUnit KILOWATT = SI.WATT.withPrefix(SI.KILO);
public static final LinearUnit MEGAWATT = SI.WATT.withPrefix(SI.MEGA);
- public static final LinearUnit MICROCOULOMB = SI.COULOMB.withPrefix(SI.MICRO);
- public static final LinearUnit MILLICOULOMB = SI.COULOMB.withPrefix(SI.MILLI);
+ public static final LinearUnit MICROCOULOMB = SI.COULOMB
+ .withPrefix(SI.MICRO);
+ public static final LinearUnit MILLICOULOMB = SI.COULOMB
+ .withPrefix(SI.MILLI);
public static final LinearUnit KILOCOULOMB = SI.COULOMB.withPrefix(SI.KILO);
public static final LinearUnit MEGACOULOMB = SI.COULOMB.withPrefix(SI.MEGA);
-
+
public static final LinearUnit MICROAMPERE = SI.AMPERE.withPrefix(SI.MICRO);
public static final LinearUnit MILLIAMPERE = SI.AMPERE.withPrefix(SI.MILLI);
-
+
public static final LinearUnit MICROVOLT = SI.VOLT.withPrefix(SI.MICRO);
public static final LinearUnit MILLIVOLT = SI.VOLT.withPrefix(SI.MILLI);
public static final LinearUnit KILOVOLT = SI.VOLT.withPrefix(SI.KILO);
public static final LinearUnit MEGAVOLT = SI.VOLT.withPrefix(SI.MEGA);
-
+
public static final LinearUnit KILOOHM = SI.OHM.withPrefix(SI.KILO);
public static final LinearUnit MEGAOHM = SI.OHM.withPrefix(SI.MEGA);
+
+ // sets of prefixes
+ public static final Set<UnitPrefix> ALL_PREFIXES = Set.of(DEKA, HECTO, KILO,
+ MEGA, GIGA, TERA, PETA, EXA, ZETTA, YOTTA, DECI, CENTI, MILLI, MICRO,
+ NANO, PICO, FEMTO, ATTO, ZEPTO, YOCTO, KIBI, MEBI, GIBI, TEBI, PEBI,
+ EXBI);
+
+ public static final Set<UnitPrefix> DECIMAL_PREFIXES = Set.of(DEKA, HECTO,
+ KILO, MEGA, GIGA, TERA, PETA, EXA, ZETTA, YOTTA, DECI, CENTI, MILLI,
+ MICRO, NANO, PICO, FEMTO, ATTO, ZEPTO, YOCTO);
+ public static final Set<UnitPrefix> THOUSAND_PREFIXES = Set.of(KILO, MEGA,
+ GIGA, TERA, PETA, EXA, ZETTA, YOTTA, MILLI, MICRO, NANO, PICO, FEMTO,
+ ATTO, ZEPTO, YOCTO);
+ public static final Set<UnitPrefix> MAGNIFYING_PREFIXES = Set.of(DEKA, HECTO,
+ KILO, MEGA, GIGA, TERA, PETA, EXA, ZETTA, YOTTA, KIBI, MEBI, GIBI,
+ TEBI, PEBI, EXBI);
+ public static final Set<UnitPrefix> REDUCING_PREFIXES = Set.of(DECI, CENTI,
+ MILLI, MICRO, NANO, PICO, FEMTO, ATTO, ZEPTO, YOCTO);
+
// You may NOT get SI instances!
private SI() {
throw new AssertionError();
diff --git a/src/org/unitConverter/unit/Unit.java b/src/org/unitConverter/unit/Unit.java
index 35b32fc..0a3298f 100644
--- a/src/org/unitConverter/unit/Unit.java
+++ b/src/org/unitConverter/unit/Unit.java
@@ -16,15 +16,14 @@
*/
package org.unitConverter.unit;
-import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
-import java.util.Optional;
import java.util.Set;
import java.util.function.DoubleUnaryOperator;
+import org.unitConverter.math.DecimalComparison;
import org.unitConverter.math.ObjectProduct;
/**
@@ -33,211 +32,252 @@ import org.unitConverter.math.ObjectProduct;
* @author Adrien Hopkins
* @since 2019-10-16
*/
-public abstract class Unit {
+public abstract class Unit implements Nameable {
/**
- * Returns a unit from its base and the functions it uses to convert to and from its base.
+ * Returns a unit from its base and the functions it uses to convert to and
+ * from its base.
*
* <p>
- * For example, to get a unit representing the degree Celsius, the following code can be used:
+ * For example, to get a unit representing the degree Celsius, the following
+ * code can be used:
*
* {@code Unit.fromConversionFunctions(SI.KELVIN, tempK -> tempK - 273.15, tempC -> tempC + 273.15);}
* </p>
*
- * @param base
- * unit's base
- * @param converterFrom
- * function that accepts a value expressed in the unit's base and returns that value expressed in this
- * unit.
- * @param converterTo
- * function that accepts a value expressed in the unit and returns that value expressed in the unit's
- * base.
+ * @param base unit's base
+ * @param converterFrom function that accepts a value expressed in the unit's
+ * base and returns that value expressed in this unit.
+ * @param converterTo function that accepts a value expressed in the unit
+ * and returns that value expressed in the unit's base.
* @return a unit that uses the provided functions to convert.
* @since 2019-05-22
- * @throws NullPointerException
- * if any argument is null
+ * @throws NullPointerException if any argument is null
*/
- public static final Unit fromConversionFunctions(final ObjectProduct<BaseUnit> base,
- final DoubleUnaryOperator converterFrom, final DoubleUnaryOperator converterTo) {
+ public static final Unit fromConversionFunctions(
+ final ObjectProduct<BaseUnit> base,
+ final DoubleUnaryOperator converterFrom,
+ final DoubleUnaryOperator converterTo) {
return new FunctionalUnit(base, converterFrom, converterTo);
}
-
+
/**
- * Returns a unit from its base and the functions it uses to convert to and from its base.
+ * Returns a unit from its base and the functions it uses to convert to and
+ * from its base.
*
* <p>
- * For example, to get a unit representing the degree Celsius, the following code can be used:
+ * For example, to get a unit representing the degree Celsius, the following
+ * code can be used:
*
* {@code Unit.fromConversionFunctions(SI.KELVIN, tempK -> tempK - 273.15, tempC -> tempC + 273.15);}
* </p>
*
- * @param base
- * unit's base
- * @param converterFrom
- * function that accepts a value expressed in the unit's base and returns that value expressed in this
- * unit.
- * @param converterTo
- * function that accepts a value expressed in the unit and returns that value expressed in the unit's
- * base.
- * @param ns
- * names and symbol of unit
+ * @param base unit's base
+ * @param converterFrom function that accepts a value expressed in the unit's
+ * base and returns that value expressed in this unit.
+ * @param converterTo function that accepts a value expressed in the unit
+ * and returns that value expressed in the unit's base.
+ * @param ns names and symbol of unit
* @return a unit that uses the provided functions to convert.
* @since 2019-05-22
- * @throws NullPointerException
- * if any argument is null
+ * @throws NullPointerException if any argument is null
*/
- public static final Unit fromConversionFunctions(final ObjectProduct<BaseUnit> base,
- final DoubleUnaryOperator converterFrom, final DoubleUnaryOperator converterTo, final NameSymbol ns) {
+ public static final Unit fromConversionFunctions(
+ final ObjectProduct<BaseUnit> base,
+ final DoubleUnaryOperator converterFrom,
+ final DoubleUnaryOperator converterTo, final NameSymbol ns) {
return new FunctionalUnit(base, converterFrom, converterTo, ns);
}
-
+
/**
* The combination of units that this unit is based on.
*
* @since 2019-10-16
*/
private final ObjectProduct<BaseUnit> unitBase;
-
- /**
- * The primary name used by this unit.
- */
- private final Optional<String> primaryName;
-
+
/**
- * A short symbol used to represent this unit.
- */
- private final Optional<String> symbol;
-
- /**
- * A set of any additional names and/or spellings that the unit uses.
+ * This unit's name(s) and symbol
+ *
+ * @since 2020-09-07
*/
- private final Set<String> otherNames;
-
+ private final NameSymbol nameSymbol;
+
/**
* Cache storing the result of getDimension()
*
* @since 2019-10-16
*/
private transient ObjectProduct<BaseDimension> dimension = null;
-
+
/**
- * Creates the {@code AbstractUnit}.
+ * Creates the {@code Unit}.
*
- * @param unitBase
- * base of unit
- * @param ns
- * names and symbol of unit
+ * @param unitBase base of unit
+ * @param ns names and symbol of unit
* @since 2019-10-16
- * @throws NullPointerException
- * if unitBase or ns is null
+ * @throws NullPointerException if unitBase or ns is null
*/
- protected Unit(final ObjectProduct<BaseUnit> unitBase, final NameSymbol ns) {
- this.unitBase = Objects.requireNonNull(unitBase, "unitBase must not be null.");
- this.primaryName = Objects.requireNonNull(ns, "ns must not be null.").getPrimaryName();
- this.symbol = ns.getSymbol();
- this.otherNames = ns.getOtherNames();
+ Unit(ObjectProduct<BaseUnit> unitBase, NameSymbol ns) {
+ this.unitBase = Objects.requireNonNull(unitBase,
+ "unitBase may not be null");
+ this.nameSymbol = Objects.requireNonNull(ns, "ns may not be null");
}
-
+
/**
* A constructor that constructs {@code BaseUnit} instances.
*
* @since 2019-10-16
*/
- Unit(final String primaryName, final String symbol, final Set<String> otherNames) {
+ Unit(final String primaryName, final String symbol,
+ final Set<String> otherNames) {
if (this instanceof BaseUnit) {
this.unitBase = ObjectProduct.oneOf((BaseUnit) this);
} else
throw new AssertionError();
- this.primaryName = Optional.of(primaryName);
- this.symbol = Optional.of(symbol);
- this.otherNames = Collections.unmodifiableSet(
- new HashSet<>(Objects.requireNonNull(otherNames, "additionalNames must not be null.")));
+ this.nameSymbol = NameSymbol.of(primaryName, symbol,
+ new HashSet<>(otherNames));
}
-
+
/**
- * Checks if a value expressed in this unit can be converted to a value expressed in {@code other}
+ * @return this unit as a {@link Unitlike}
+ * @since 2020-09-07
+ */
+ public final Unitlike<Double> asUnitlike() {
+ return Unitlike.fromConversionFunctions(this.getBase(),
+ this::convertFromBase, this::convertToBase, this.getNameSymbol());
+ }
+
+ /**
+ * Checks if a value expressed in this unit can be converted to a value
+ * expressed in {@code other}
*
- * @param other
- * unit to test with
- * @return true if the units are compatible
+ * @param other unit or unitlike form to test with
+ * @return true if they are compatible
* @since 2019-01-13
* @since v0.1.0
- * @throws NullPointerException
- * if other is null
+ * @throws NullPointerException if other is null
*/
public final boolean canConvertTo(final Unit other) {
Objects.requireNonNull(other, "other must not be null.");
return Objects.equals(this.getBase(), other.getBase());
}
-
+
+ /**
+ * Checks if a value expressed in this unit can be converted to a value
+ * expressed in {@code other}
+ *
+ * @param other unit or unitlike form to test with
+ * @return true if they are compatible
+ * @since 2019-01-13
+ * @since v0.1.0
+ * @throws NullPointerException if other is null
+ */
+ public final <W> boolean canConvertTo(final Unitlike<W> other) {
+ Objects.requireNonNull(other, "other must not be null.");
+ return Objects.equals(this.getBase(), other.getBase());
+ }
+
/**
- * Converts from a value expressed in this unit's base unit to a value expressed in this unit.
+ * Converts from a value expressed in this unit's base unit to a value
+ * expressed in this unit.
* <p>
- * This must be the inverse of {@code convertToBase}, so {@code convertFromBase(convertToBase(value))} must be equal
- * to {@code value} for any value, ignoring precision loss by roundoff error.
+ * This must be the inverse of {@code convertToBase}, so
+ * {@code convertFromBase(convertToBase(value))} must be equal to
+ * {@code value} for any value, ignoring precision loss by roundoff error.
* </p>
* <p>
- * If this unit <i>is</i> a base unit, this method should return {@code value}.
+ * If this unit <i>is</i> a base unit, this method should return
+ * {@code value}.
* </p>
*
- * @implSpec This method is used by {@link #convertTo}, and its behaviour affects the behaviour of
- * {@code convertTo}.
+ * @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
+ * @param value value expressed in <b>base</b> unit
* @return value expressed in <b>this</b> unit
* @since 2018-12-22
* @since v0.1.0
*/
protected abstract double convertFromBase(double value);
-
+
/**
- * Converts a value expressed in this unit to a value expressed in {@code other}.
+ * Converts a value expressed in this unit to a value expressed in
+ * {@code other}.
*
* @implSpec If unit conversion is possible, this implementation returns
- * {@code other.convertFromBase(this.convertToBase(value))}. Therefore, overriding either of those methods
- * will change the output of this method.
+ * {@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
+ * @param other unit to convert to
+ * @param value value to convert
* @return converted value
* @since 2019-05-22
- * @throws IllegalArgumentException
- * if {@code other} is incompatible for conversion with this unit (as tested by
- * {@link Unit#canConvertTo}).
- * @throws NullPointerException
- * if other is null
+ * @throws IllegalArgumentException if {@code other} is incompatible for
+ * conversion with this unit (as tested by
+ * {@link Unit#canConvertTo}).
+ * @throws NullPointerException if other is null
*/
public final double convertTo(final Unit other, final double value) {
Objects.requireNonNull(other, "other must not be null.");
if (this.canConvertTo(other))
return other.convertFromBase(this.convertToBase(value));
else
- throw new IllegalArgumentException(String.format("Cannot convert from %s to %s.", this, other));
+ throw new IllegalArgumentException(
+ String.format("Cannot convert from %s to %s.", this, other));
}
-
+
+ /**
+ * Converts a value expressed in this unit to a value expressed in
+ * {@code other}.
+ *
+ * @implSpec If conversion is possible, this implementation returns
+ * {@code other.convertFromBase(this.convertToBase(value))}.
+ * Therefore, overriding either of those methods will change the
+ * output of this method.
+ *
+ * @param other unitlike form to convert to
+ * @param value value to convert
+ * @param <W> type of value to convert to
+ * @return converted value
+ * @since 2020-09-07
+ * @throws IllegalArgumentException if {@code other} is incompatible for
+ * conversion with this unit (as tested by
+ * {@link Unit#canConvertTo}).
+ * @throws NullPointerException if other is null
+ */
+ public final <W> W convertTo(final Unitlike<W> other, final double value) {
+ Objects.requireNonNull(other, "other must not be null.");
+ if (this.canConvertTo(other))
+ return other.convertFromBase(this.convertToBase(value));
+ else
+ throw new IllegalArgumentException(
+ String.format("Cannot convert from %s to %s.", this, other));
+ }
+
/**
- * Converts from a value expressed in this unit to a value expressed in this unit's base unit.
+ * Converts from a value expressed in this unit to a value expressed in this
+ * unit's base unit.
* <p>
- * This must be the inverse of {@code convertFromBase}, so {@code convertToBase(convertFromBase(value))} must be
- * equal to {@code value} for any value, ignoring precision loss by roundoff error.
+ * This must be the inverse of {@code convertFromBase}, so
+ * {@code convertToBase(convertFromBase(value))} must be equal to
+ * {@code value} for any value, ignoring precision loss by roundoff error.
* </p>
* <p>
- * If this unit <i>is</i> a base unit, this method should return {@code value}.
+ * If this unit <i>is</i> a base unit, this method should return
+ * {@code value}.
* </p>
*
- * @implSpec This method is used by {@link #convertTo}, and its behaviour affects the behaviour of
- * {@code convertTo}.
+ * @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
+ * @param value value expressed in <b>this</b> unit
* @return value expressed in <b>base</b> unit
* @since 2018-12-22
* @since v0.1.0
*/
protected abstract double convertToBase(double value);
-
+
/**
* @return combination of units that this unit is based on
* @since 2018-12-22
@@ -246,7 +286,7 @@ public abstract class Unit {
public final ObjectProduct<BaseUnit> getBase() {
return this.unitBase;
}
-
+
/**
* @return dimension measured by this unit
* @since 2018-12-22
@@ -256,58 +296,82 @@ public abstract class Unit {
if (this.dimension == null) {
final Map<BaseUnit, Integer> mapping = this.unitBase.exponentMap();
final Map<BaseDimension, Integer> dimensionMap = new HashMap<>();
-
+
for (final BaseUnit key : mapping.keySet()) {
dimensionMap.put(key.getBaseDimension(), mapping.get(key));
}
-
+
this.dimension = ObjectProduct.fromExponentMapping(dimensionMap);
}
return this.dimension;
}
-
+
/**
- * @return additionalNames
- * @since 2019-10-21
+ * @return the nameSymbol
+ * @since 2020-09-07
*/
- public final Set<String> getOtherNames() {
- return this.otherNames;
- }
-
- /**
- * @return primaryName
- * @since 2019-10-21
- */
- public final Optional<String> getPrimaryName() {
- return this.primaryName;
+ @Override
+ public final NameSymbol getNameSymbol() {
+ return this.nameSymbol;
}
-
+
/**
- * @return symbol
- * @since 2019-10-21
+ * Returns true iff this unit is metric.
+ * <p>
+ * "Metric" is defined by three conditions:
+ * <ul>
+ * <li>Must be an instance of {@link LinearUnit}.</li>
+ * <li>Must be based on the SI base units (as determined by getBase())</li>
+ * <li>The conversion factor must be a power of 10.</li>
+ * </ul>
+ * <p>
+ * Note that this definition excludes some units that many would consider
+ * "metric", such as the degree Celsius (fails the first condition),
+ * calories, minutes and hours (fail the third condition).
+ * <p>
+ * All SI units (as designated by the BIPM) except the degree Celsius are
+ * considered "metric" by this definition.
+ *
+ * @since 2020-08-27
*/
- public final Optional<String> getSymbol() {
- return this.symbol;
+ public final boolean isMetric() {
+ // first condition - check that it is a linear unit
+ if (!(this instanceof LinearUnit))
+ return false;
+ final LinearUnit linear = (LinearUnit) this;
+
+ // second condition - check that
+ for (final BaseUnit b : linear.getBase().getBaseSet()) {
+ if (!SI.BaseUnits.BASE_UNITS.contains(b))
+ return false;
+ }
+
+ // third condition - check that conversion factor is a power of 10
+ return DecimalComparison
+ .equals(Math.log10(linear.getConversionFactor()) % 1.0, 0);
}
-
+
@Override
public String toString() {
return this.getPrimaryName().orElse("Unnamed unit")
- + (this.getSymbol().isPresent() ? String.format(" (%s)", this.getSymbol().get()) : "")
- + ", derived from " + this.getBase().toString(u -> u.getSymbol().get())
- + (this.getOtherNames().isEmpty() ? "" : ", also called " + String.join(", ", this.getOtherNames()));
+ + (this.getSymbol().isPresent()
+ ? String.format(" (%s)", this.getSymbol().get())
+ : "")
+ + ", derived from "
+ + this.getBase().toString(u -> u.getSymbol().get())
+ + (this.getOtherNames().isEmpty() ? ""
+ : ", also called " + String.join(", ", this.getOtherNames()));
}
-
+
/**
- * @param ns
- * name(s) and symbol to use
+ * @param ns name(s) and symbol to use
* @return a copy of this unit with provided name(s) and symbol
* @since 2019-10-21
- * @throws NullPointerException
- * if ns is null
+ * @throws NullPointerException if ns is null
*/
public Unit withName(final NameSymbol ns) {
- return fromConversionFunctions(this.getBase(), this::convertFromBase, this::convertToBase,
+ return fromConversionFunctions(this.getBase(), this::convertFromBase,
+ this::convertToBase,
Objects.requireNonNull(ns, "ns must not be null."));
}
}
diff --git a/src/org/unitConverter/unit/UnitDatabase.java b/src/org/unitConverter/unit/UnitDatabase.java
index 507266d..000acf5 100644
--- a/src/org/unitConverter/unit/UnitDatabase.java
+++ b/src/org/unitConverter/unit/UnitDatabase.java
@@ -16,17 +16,18 @@
*/
package org.unitConverter.unit;
-import java.io.BufferedReader;
-import java.io.File;
import java.io.FileNotFoundException;
-import java.io.FileReader;
import java.io.IOException;
+import java.math.BigDecimal;
+import java.nio.file.Files;
+import java.nio.file.Path;
import java.util.AbstractSet;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
@@ -40,9 +41,11 @@ import java.util.function.Predicate;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
+import org.unitConverter.math.ConditionalExistenceCollections;
import org.unitConverter.math.DecimalComparison;
import org.unitConverter.math.ExpressionParser;
import org.unitConverter.math.ObjectProduct;
+import org.unitConverter.math.UncertainDouble;
/**
* A database of units, prefixes and dimensions, and their names.
@@ -55,29 +58,33 @@ public final class UnitDatabase {
/**
* A map for units that allows the use of prefixes.
* <p>
- * As this map implementation is intended to be used as a sort of "augmented view" of a unit and prefix map, it is
- * unmodifiable but instead reflects the changes to the maps passed into it. Do not edit this map, instead edit the
- * maps that were passed in during construction.
+ * As this map implementation is intended to be used as a sort of "augmented
+ * view" of a unit and prefix map, it is unmodifiable but instead reflects
+ * the changes to the maps passed into it. Do not edit this map, instead edit
+ * the maps that were passed in during construction.
* </p>
* <p>
* The rules for applying prefixes onto units are the following:
* <ul>
* <li>Prefixes can only be applied to linear units.</li>
- * <li>Before attempting to search for prefixes in a unit name, this map will first search for a unit name. So, if
- * there are two units, "B" and "AB", and a prefix "A", this map will favour the unit "AB" over the unit "B" with
- * the prefix "A", even though they have the same string.</li>
- * <li>Longer prefixes are preferred to shorter prefixes. So, if you have units "BC" and "C", and prefixes "AB" and
- * "A", inputting "ABC" will return the unit "C" with the prefix "AB", not "BC" with the prefix "A".</li>
+ * <li>Before attempting to search for prefixes in a unit name, this map will
+ * first search for a unit name. So, if there are two units, "B" and "AB",
+ * and a prefix "A", this map will favour the unit "AB" over the unit "B"
+ * with the prefix "A", even though they have the same string.</li>
+ * <li>Longer prefixes are preferred to shorter prefixes. So, if you have
+ * units "BC" and "C", and prefixes "AB" and "A", inputting "ABC" will return
+ * the unit "C" with the prefix "AB", not "BC" with the prefix "A".</li>
* </ul>
* </p>
* <p>
- * This map is infinite in size if there is at least one unit and at least one prefix. If it is infinite, some
- * operations that only work with finite collections, like converting name/entry sets to arrays, will throw an
+ * This map is infinite in size if there is at least one unit and at least
+ * one prefix. If it is infinite, some operations that only work with finite
+ * collections, like converting name/entry sets to arrays, will throw an
* {@code IllegalStateException}.
* </p>
* <p>
- * Because of ambiguities between prefixes (i.e. kilokilo = mega), {@link #containsValue} and {@link #values()}
- * currently ignore prefixes.
+ * Because of ambiguities between prefixes (i.e. kilokilo = mega),
+ * {@link #containsValue} and {@link #values()} currently ignore prefixes.
* </p>
*
* @author Adrien Hopkins
@@ -89,16 +96,19 @@ public final class UnitDatabase {
* The class used for entry sets.
*
* <p>
- * If the map that created this set is infinite in size (has at least one unit and at least one prefix), this
- * set is infinite as well. If this set is infinite in size, {@link #toArray} will fail with a
- * {@code IllegalStateException} instead of creating an infinite-sized array.
+ * If the map that created this set is infinite in size (has at least one
+ * unit and at least one prefix), this set is infinite as well. If this
+ * set is infinite in size, {@link #toArray} will fail with a
+ * {@code IllegalStateException} instead of creating an infinite-sized
+ * array.
* </p>
*
* @author Adrien Hopkins
* @since 2019-04-13
* @since v0.2.0
*/
- private static final class PrefixedUnitEntrySet extends AbstractSet<Map.Entry<String, Unit>> {
+ private static final class PrefixedUnitEntrySet
+ extends AbstractSet<Map.Entry<String, Unit>> {
/**
* The entry for this set.
*
@@ -106,17 +116,16 @@ public final class UnitDatabase {
* @since 2019-04-14
* @since v0.2.0
*/
- private static final class PrefixedUnitEntry implements Entry<String, Unit> {
+ private static final class PrefixedUnitEntry
+ implements Entry<String, Unit> {
private final String key;
private final Unit value;
-
+
/**
* Creates the {@code PrefixedUnitEntry}.
*
- * @param key
- * key
- * @param value
- * value
+ * @param key key
+ * @param value value
* @since 2019-04-14
* @since v0.2.0
*/
@@ -124,7 +133,7 @@ public final class UnitDatabase {
this.key = key;
this.value = value;
}
-
+
/**
* @since 2019-05-03
*/
@@ -136,34 +145,38 @@ public final class UnitDatabase {
return Objects.equals(this.getKey(), other.getKey())
&& Objects.equals(this.getValue(), other.getValue());
}
-
+
@Override
public String getKey() {
return this.key;
}
-
+
@Override
public Unit getValue() {
return this.value;
}
-
+
/**
* @since 2019-05-03
*/
@Override
public int hashCode() {
return (this.getKey() == null ? 0 : this.getKey().hashCode())
- ^ (this.getValue() == null ? 0 : this.getValue().hashCode());
+ ^ (this.getValue() == null ? 0
+ : this.getValue().hashCode());
}
-
+
@Override
public Unit setValue(final Unit value) {
- throw new UnsupportedOperationException("Cannot set value in an immutable entry");
+ throw new UnsupportedOperationException(
+ "Cannot set value in an immutable entry");
}
-
+
/**
- * Returns a string representation of the entry. The format of the string is the string representation
- * of the key, then the equals ({@code =}) character, then the string representation of the value.
+ * Returns a string representation of the entry. The format of the
+ * string is the string representation of the key, then the equals
+ * ({@code =}) character, then the string representation of the
+ * value.
*
* @since 2019-05-03
*/
@@ -172,27 +185,30 @@ public final class UnitDatabase {
return this.getKey() + "=" + this.getValue();
}
}
-
+
/**
- * An iterator that iterates over the units of a {@code PrefixedUnitNameSet}.
+ * An iterator that iterates over the units of a
+ * {@code PrefixedUnitNameSet}.
*
* @author Adrien Hopkins
* @since 2019-04-14
* @since v0.2.0
*/
- private static final class PrefixedUnitEntryIterator implements Iterator<Entry<String, Unit>> {
+ private static final class PrefixedUnitEntryIterator
+ implements Iterator<Entry<String, Unit>> {
// position in the unit list
private int unitNamePosition = 0;
// the indices of the prefixes attached to the current unit
private final List<Integer> prefixCoordinates = new ArrayList<>();
-
+
// values from the unit entry set
private final Map<String, Unit> map;
private transient final List<String> unitNames;
private transient final List<String> prefixNames;
-
+
/**
- * Creates the {@code UnitsDatabase.PrefixedUnitMap.PrefixedUnitNameSet.PrefixedUnitNameIterator}.
+ * Creates the
+ * {@code UnitsDatabase.PrefixedUnitMap.PrefixedUnitNameSet.PrefixedUnitNameIterator}.
*
* @since 2019-04-14
* @since v0.2.0
@@ -202,7 +218,7 @@ public final class UnitDatabase {
this.unitNames = new ArrayList<>(map.units.keySet());
this.prefixNames = new ArrayList<>(map.prefixes.keySet());
}
-
+
/**
* @return current unit name
* @since 2019-04-14
@@ -214,10 +230,10 @@ public final class UnitDatabase {
unitName.append(this.prefixNames.get(i));
}
unitName.append(this.unitNames.get(this.unitNamePosition));
-
+
return unitName.toString();
}
-
+
@Override
public boolean hasNext() {
if (this.unitNames.isEmpty())
@@ -229,7 +245,7 @@ public final class UnitDatabase {
return true;
}
}
-
+
/**
* Changes this iterator's position to the next available one.
*
@@ -238,127 +254,142 @@ public final class UnitDatabase {
*/
private void incrementPosition() {
this.unitNamePosition++;
-
+
if (this.unitNamePosition >= this.unitNames.size()) {
// we have used all of our units, go to a different prefix
this.unitNamePosition = 0;
-
+
// if the prefix coordinates are empty, then set it to [0]
if (this.prefixCoordinates.isEmpty()) {
this.prefixCoordinates.add(0, 0);
} else {
// get the prefix coordinate to increment, then increment
int i = this.prefixCoordinates.size() - 1;
- this.prefixCoordinates.set(i, this.prefixCoordinates.get(i) + 1);
-
+ this.prefixCoordinates.set(i,
+ this.prefixCoordinates.get(i) + 1);
+
// fix any carrying errors
- while (i >= 0 && this.prefixCoordinates.get(i) >= this.prefixNames.size()) {
+ while (i >= 0 && this.prefixCoordinates
+ .get(i) >= this.prefixNames.size()) {
// carry over
- this.prefixCoordinates.set(i--, 0); // null and decrement at the same time
-
+ this.prefixCoordinates.set(i--, 0); // null and
+ // decrement at the
+ // same time
+
if (i < 0) { // we need to add a new coordinate
this.prefixCoordinates.add(0, 0);
} else { // increment an existing one
- this.prefixCoordinates.set(i, this.prefixCoordinates.get(i) + 1);
+ this.prefixCoordinates.set(i,
+ this.prefixCoordinates.get(i) + 1);
}
}
}
}
}
-
+
@Override
public Entry<String, Unit> next() {
// get next element
final Entry<String, Unit> nextEntry = this.peek();
-
+
// iterate to next position
this.incrementPosition();
-
+
return nextEntry;
}
-
+
/**
- * @return the next element in the iterator, without iterating over it
+ * @return the next element in the iterator, without iterating over
+ * it
* @since 2019-05-03
*/
private Entry<String, Unit> peek() {
if (!this.hasNext())
throw new NoSuchElementException("No units left!");
-
+
// if I have prefixes, ensure I'm not using a nonlinear unit
- // since all of the unprefixed stuff is done, just remove nonlinear units
+ // since all of the unprefixed stuff is done, just remove
+ // nonlinear units
if (!this.prefixCoordinates.isEmpty()) {
while (this.unitNamePosition < this.unitNames.size()
- && !(this.map.get(this.unitNames.get(this.unitNamePosition)) instanceof LinearUnit)) {
+ && !(this.map.get(this.unitNames.get(
+ this.unitNamePosition)) instanceof LinearUnit)) {
this.unitNames.remove(this.unitNamePosition);
}
}
-
+
final String nextName = this.getCurrentUnitName();
-
+
return new PrefixedUnitEntry(nextName, this.map.get(nextName));
}
-
+
/**
- * Returns a string representation of the object. The exact details of the representation are
- * unspecified and subject to change.
+ * Returns a string representation of the object. The exact details
+ * of the representation are unspecified and subject to change.
*
* @since 2019-05-03
*/
@Override
public String toString() {
- return String.format("Iterator iterating over name-unit entries; next value is \"%s\"",
+ return String.format(
+ "Iterator iterating over name-unit entries; next value is \"%s\"",
this.peek());
}
}
-
+
// the map that created this set
private final PrefixedUnitMap map;
-
+
/**
* Creates the {@code PrefixedUnitNameSet}.
*
- * @param map
- * map that created this set
+ * @param map map that created this set
* @since 2019-04-13
* @since v0.2.0
*/
public PrefixedUnitEntrySet(final PrefixedUnitMap map) {
this.map = map;
}
-
+
@Override
public boolean add(final Map.Entry<String, Unit> e) {
- throw new UnsupportedOperationException("Cannot add to an immutable set");
+ throw new UnsupportedOperationException(
+ "Cannot add to an immutable set");
}
-
+
@Override
- public boolean addAll(final Collection<? extends Map.Entry<String, Unit>> c) {
- throw new UnsupportedOperationException("Cannot add to an immutable set");
+ public boolean addAll(
+ final Collection<? extends Map.Entry<String, Unit>> c) {
+ throw new UnsupportedOperationException(
+ "Cannot add to an immutable set");
}
-
+
@Override
public void clear() {
- throw new UnsupportedOperationException("Cannot clear an immutable set");
+ throw new UnsupportedOperationException(
+ "Cannot clear an immutable set");
}
-
+
@Override
public boolean contains(final Object o) {
// get the entry
final Entry<String, Unit> entry;
-
+
try {
- // This is OK because I'm in a try-catch block, catching the exact exception that would be thrown.
+ // This is OK because I'm in a try-catch block, catching the
+ // exact exception that would be thrown.
@SuppressWarnings("unchecked")
final Entry<String, Unit> tempEntry = (Entry<String, Unit>) o;
entry = tempEntry;
} catch (final ClassCastException e) {
- throw new IllegalArgumentException("Attempted to test for an entry using a non-entry.");
+ throw new IllegalArgumentException(
+ "Attempted to test for an entry using a non-entry.");
}
-
- return this.map.containsKey(entry.getKey()) && this.map.get(entry.getKey()).equals(entry.getValue());
+
+ return this.map.containsKey(entry.getKey())
+ && this.map.get(entry.getKey()).equals(entry.getValue());
}
-
+
@Override
public boolean containsAll(final Collection<?> c) {
for (final Object o : c)
@@ -366,37 +397,42 @@ public final class UnitDatabase {
return false;
return true;
}
-
+
@Override
public boolean isEmpty() {
return this.map.isEmpty();
}
-
+
@Override
public Iterator<Entry<String, Unit>> iterator() {
return new PrefixedUnitEntryIterator(this.map);
}
-
+
@Override
public boolean remove(final Object o) {
- throw new UnsupportedOperationException("Cannot remove from an immutable set");
+ throw new UnsupportedOperationException(
+ "Cannot remove from an immutable set");
}
-
+
@Override
public boolean removeAll(final Collection<?> c) {
- throw new UnsupportedOperationException("Cannot remove from an immutable set");
+ throw new UnsupportedOperationException(
+ "Cannot remove from an immutable set");
}
-
+
@Override
- public boolean removeIf(final Predicate<? super Entry<String, Unit>> filter) {
- throw new UnsupportedOperationException("Cannot remove from an immutable set");
+ public boolean removeIf(
+ final Predicate<? super Entry<String, Unit>> filter) {
+ throw new UnsupportedOperationException(
+ "Cannot remove from an immutable set");
}
-
+
@Override
public boolean retainAll(final Collection<?> c) {
- throw new UnsupportedOperationException("Cannot remove from an immutable set");
+ throw new UnsupportedOperationException(
+ "Cannot remove from an immutable set");
}
-
+
@Override
public int size() {
if (this.map.units.isEmpty())
@@ -409,10 +445,9 @@ public final class UnitDatabase {
return Integer.MAX_VALUE;
}
}
-
+
/**
- * @throws IllegalStateException
- * if the set is infinite in size
+ * @throws IllegalStateException if the set is infinite in size
*/
@Override
public Object[] toArray() {
@@ -420,12 +455,12 @@ public final class UnitDatabase {
return super.toArray();
else
// infinite set
- throw new IllegalStateException("Cannot make an infinite set into an array.");
+ throw new IllegalStateException(
+ "Cannot make an infinite set into an array.");
}
-
+
/**
- * @throws IllegalStateException
- * if the set is infinite in size
+ * @throws IllegalStateException if the set is infinite in size
*/
@Override
public <T> T[] toArray(final T[] a) {
@@ -433,53 +468,61 @@ public final class UnitDatabase {
return super.toArray(a);
else
// infinite set
- throw new IllegalStateException("Cannot make an infinite set into an array.");
+ throw new IllegalStateException(
+ "Cannot make an infinite set into an array.");
}
-
+
@Override
public String toString() {
if (this.map.units.isEmpty() || this.map.prefixes.isEmpty())
return super.toString();
else
- return String.format("Infinite set of name-unit entries created from units %s and prefixes %s",
+ return String.format(
+ "Infinite set of name-unit entries created from units %s and prefixes %s",
this.map.units, this.map.prefixes);
}
}
-
+
/**
* The class used for unit name sets.
*
* <p>
- * If the map that created this set is infinite in size (has at least one unit and at least one prefix), this
- * set is infinite as well. If this set is infinite in size, {@link #toArray} will fail with a
- * {@code IllegalStateException} instead of creating an infinite-sized array.
+ * If the map that created this set is infinite in size (has at least one
+ * unit and at least one prefix), this set is infinite as well. If this
+ * set is infinite in size, {@link #toArray} will fail with a
+ * {@code IllegalStateException} instead of creating an infinite-sized
+ * array.
* </p>
*
* @author Adrien Hopkins
* @since 2019-04-13
* @since v0.2.0
*/
- private static final class PrefixedUnitNameSet extends AbstractSet<String> {
+ private static final class PrefixedUnitNameSet
+ extends AbstractSet<String> {
/**
- * An iterator that iterates over the units of a {@code PrefixedUnitNameSet}.
+ * An iterator that iterates over the units of a
+ * {@code PrefixedUnitNameSet}.
*
* @author Adrien Hopkins
* @since 2019-04-14
* @since v0.2.0
*/
- private static final class PrefixedUnitNameIterator implements Iterator<String> {
+ private static final class PrefixedUnitNameIterator
+ implements Iterator<String> {
// position in the unit list
private int unitNamePosition = 0;
// the indices of the prefixes attached to the current unit
private final List<Integer> prefixCoordinates = new ArrayList<>();
-
+
// values from the unit name set
private final Map<String, Unit> map;
private transient final List<String> unitNames;
private transient final List<String> prefixNames;
-
+
/**
- * Creates the {@code UnitsDatabase.PrefixedUnitMap.PrefixedUnitNameSet.PrefixedUnitNameIterator}.
+ * Creates the
+ * {@code UnitsDatabase.PrefixedUnitMap.PrefixedUnitNameSet.PrefixedUnitNameIterator}.
*
* @since 2019-04-14
* @since v0.2.0
@@ -489,7 +532,7 @@ public final class UnitDatabase {
this.unitNames = new ArrayList<>(map.units.keySet());
this.prefixNames = new ArrayList<>(map.prefixes.keySet());
}
-
+
/**
* @return current unit name
* @since 2019-04-14
@@ -501,10 +544,10 @@ public final class UnitDatabase {
unitName.append(this.prefixNames.get(i));
}
unitName.append(this.unitNames.get(this.unitNamePosition));
-
+
return unitName.toString();
}
-
+
@Override
public boolean hasNext() {
if (this.unitNames.isEmpty())
@@ -516,7 +559,7 @@ public final class UnitDatabase {
return true;
}
}
-
+
/**
* Changes this iterator's position to the next available one.
*
@@ -525,109 +568,121 @@ public final class UnitDatabase {
*/
private void incrementPosition() {
this.unitNamePosition++;
-
+
if (this.unitNamePosition >= this.unitNames.size()) {
// we have used all of our units, go to a different prefix
this.unitNamePosition = 0;
-
+
// if the prefix coordinates are empty, then set it to [0]
if (this.prefixCoordinates.isEmpty()) {
this.prefixCoordinates.add(0, 0);
} else {
// get the prefix coordinate to increment, then increment
int i = this.prefixCoordinates.size() - 1;
- this.prefixCoordinates.set(i, this.prefixCoordinates.get(i) + 1);
-
+ this.prefixCoordinates.set(i,
+ this.prefixCoordinates.get(i) + 1);
+
// fix any carrying errors
- while (i >= 0 && this.prefixCoordinates.get(i) >= this.prefixNames.size()) {
+ while (i >= 0 && this.prefixCoordinates
+ .get(i) >= this.prefixNames.size()) {
// carry over
- this.prefixCoordinates.set(i--, 0); // null and decrement at the same time
-
+ this.prefixCoordinates.set(i--, 0); // null and
+ // decrement at the
+ // same time
+
if (i < 0) { // we need to add a new coordinate
this.prefixCoordinates.add(0, 0);
} else { // increment an existing one
- this.prefixCoordinates.set(i, this.prefixCoordinates.get(i) + 1);
+ this.prefixCoordinates.set(i,
+ this.prefixCoordinates.get(i) + 1);
}
}
}
}
}
-
+
@Override
public String next() {
final String nextName = this.peek();
-
+
this.incrementPosition();
-
+
return nextName;
}
-
+
/**
- * @return the next element in the iterator, without iterating over it
+ * @return the next element in the iterator, without iterating over
+ * it
* @since 2019-05-03
*/
private String peek() {
if (!this.hasNext())
throw new NoSuchElementException("No units left!");
// if I have prefixes, ensure I'm not using a nonlinear unit
- // since all of the unprefixed stuff is done, just remove nonlinear units
+ // since all of the unprefixed stuff is done, just remove
+ // nonlinear units
if (!this.prefixCoordinates.isEmpty()) {
while (this.unitNamePosition < this.unitNames.size()
- && !(this.map.get(this.unitNames.get(this.unitNamePosition)) instanceof LinearUnit)) {
+ && !(this.map.get(this.unitNames.get(
+ this.unitNamePosition)) instanceof LinearUnit)) {
this.unitNames.remove(this.unitNamePosition);
}
}
-
+
return this.getCurrentUnitName();
}
-
+
/**
- * Returns a string representation of the object. The exact details of the representation are
- * unspecified and subject to change.
+ * Returns a string representation of the object. The exact details
+ * of the representation are unspecified and subject to change.
*
* @since 2019-05-03
*/
@Override
public String toString() {
- return String.format("Iterator iterating over unit names; next value is \"%s\"", this.peek());
+ return String.format(
+ "Iterator iterating over unit names; next value is \"%s\"",
+ this.peek());
}
}
-
+
// the map that created this set
private final PrefixedUnitMap map;
-
+
/**
* Creates the {@code PrefixedUnitNameSet}.
*
- * @param map
- * map that created this set
+ * @param map map that created this set
* @since 2019-04-13
* @since v0.2.0
*/
public PrefixedUnitNameSet(final PrefixedUnitMap map) {
this.map = map;
}
-
+
@Override
public boolean add(final String e) {
- throw new UnsupportedOperationException("Cannot add to an immutable set");
+ throw new UnsupportedOperationException(
+ "Cannot add to an immutable set");
}
-
+
@Override
public boolean addAll(final Collection<? extends String> c) {
- throw new UnsupportedOperationException("Cannot add to an immutable set");
+ throw new UnsupportedOperationException(
+ "Cannot add to an immutable set");
}
-
+
@Override
public void clear() {
- throw new UnsupportedOperationException("Cannot clear an immutable set");
+ throw new UnsupportedOperationException(
+ "Cannot clear an immutable set");
}
-
+
@Override
public boolean contains(final Object o) {
return this.map.containsKey(o);
}
-
+
@Override
public boolean containsAll(final Collection<?> c) {
for (final Object o : c)
@@ -635,37 +690,41 @@ public final class UnitDatabase {
return false;
return true;
}
-
+
@Override
public boolean isEmpty() {
return this.map.isEmpty();
}
-
+
@Override
public Iterator<String> iterator() {
return new PrefixedUnitNameIterator(this.map);
}
-
+
@Override
public boolean remove(final Object o) {
- throw new UnsupportedOperationException("Cannot remove from an immutable set");
+ throw new UnsupportedOperationException(
+ "Cannot remove from an immutable set");
}
-
+
@Override
public boolean removeAll(final Collection<?> c) {
- throw new UnsupportedOperationException("Cannot remove from an immutable set");
+ throw new UnsupportedOperationException(
+ "Cannot remove from an immutable set");
}
-
+
@Override
public boolean removeIf(final Predicate<? super String> filter) {
- throw new UnsupportedOperationException("Cannot remove from an immutable set");
+ throw new UnsupportedOperationException(
+ "Cannot remove from an immutable set");
}
-
+
@Override
public boolean retainAll(final Collection<?> c) {
- throw new UnsupportedOperationException("Cannot remove from an immutable set");
+ throw new UnsupportedOperationException(
+ "Cannot remove from an immutable set");
}
-
+
@Override
public int size() {
if (this.map.units.isEmpty())
@@ -678,10 +737,9 @@ public final class UnitDatabase {
return Integer.MAX_VALUE;
}
}
-
+
/**
- * @throws IllegalStateException
- * if the set is infinite in size
+ * @throws IllegalStateException if the set is infinite in size
*/
@Override
public Object[] toArray() {
@@ -689,13 +747,13 @@ public final class UnitDatabase {
return super.toArray();
else
// infinite set
- throw new IllegalStateException("Cannot make an infinite set into an array.");
-
+ throw new IllegalStateException(
+ "Cannot make an infinite set into an array.");
+
}
-
+
/**
- * @throws IllegalStateException
- * if the set is infinite in size
+ * @throws IllegalStateException if the set is infinite in size
*/
@Override
public <T> T[] toArray(final T[] a) {
@@ -703,19 +761,21 @@ public final class UnitDatabase {
return super.toArray(a);
else
// infinite set
- throw new IllegalStateException("Cannot make an infinite set into an array.");
+ throw new IllegalStateException(
+ "Cannot make an infinite set into an array.");
}
-
+
@Override
public String toString() {
if (this.map.units.isEmpty() || this.map.prefixes.isEmpty())
return super.toString();
else
- return String.format("Infinite set of name-unit entries created from units %s and prefixes %s",
+ return String.format(
+ "Infinite set of name-unit entries created from units %s and prefixes %s",
this.map.units, this.map.prefixes);
}
}
-
+
/**
* The units stored in this collection, without prefixes.
*
@@ -723,7 +783,7 @@ public final class UnitDatabase {
* @since v0.2.0
*/
private final Map<String, Unit> units;
-
+
/**
* The available prefixes for use.
*
@@ -731,95 +791,106 @@ public final class UnitDatabase {
* @since v0.2.0
*/
private final Map<String, UnitPrefix> prefixes;
-
+
// caches
private transient Collection<Unit> values = null;
private transient Set<String> keySet = null;
private transient Set<Entry<String, Unit>> entrySet = null;
-
+
/**
* Creates the {@code PrefixedUnitMap}.
*
- * @param units
- * map mapping unit names to units
- * @param prefixes
- * map mapping prefix names to prefixes
+ * @param units map mapping unit names to units
+ * @param prefixes map mapping prefix names to prefixes
* @since 2019-04-13
* @since v0.2.0
*/
- public PrefixedUnitMap(final Map<String, Unit> units, final Map<String, UnitPrefix> prefixes) {
- // I am making unmodifiable maps to ensure I don't accidentally make changes.
+ public PrefixedUnitMap(final Map<String, Unit> units,
+ final Map<String, UnitPrefix> prefixes) {
+ // I am making unmodifiable maps to ensure I don't accidentally make
+ // changes.
this.units = Collections.unmodifiableMap(units);
this.prefixes = Collections.unmodifiableMap(prefixes);
}
-
+
@Override
public void clear() {
- throw new UnsupportedOperationException("Cannot clear an immutable map");
+ throw new UnsupportedOperationException(
+ "Cannot clear an immutable map");
}
-
+
@Override
public Unit compute(final String key,
final BiFunction<? super String, ? super Unit, ? extends Unit> remappingFunction) {
- throw new UnsupportedOperationException("Cannot edit an immutable map");
+ throw new UnsupportedOperationException(
+ "Cannot edit an immutable map");
}
-
+
@Override
- public Unit computeIfAbsent(final String key, final Function<? super String, ? extends Unit> mappingFunction) {
- throw new UnsupportedOperationException("Cannot edit an immutable map");
+ public Unit computeIfAbsent(final String key,
+ final Function<? super String, ? extends Unit> mappingFunction) {
+ throw new UnsupportedOperationException(
+ "Cannot edit an immutable map");
}
-
+
@Override
public Unit computeIfPresent(final String key,
final BiFunction<? super String, ? super Unit, ? extends Unit> remappingFunction) {
- throw new UnsupportedOperationException("Cannot edit an immutable map");
+ throw new UnsupportedOperationException(
+ "Cannot edit an immutable map");
}
-
+
@Override
public boolean containsKey(final Object key) {
// First, test if there is a unit with the key
if (this.units.containsKey(key))
return true;
-
+
// Next, try to cast it to String
if (!(key instanceof String))
- throw new IllegalArgumentException("Attempted to test for a unit using a non-string name.");
+ throw new IllegalArgumentException(
+ "Attempted to test for a unit using a non-string name.");
final String unitName = (String) key;
-
+
// Then, look for the longest prefix that is attached to a valid unit
String longestPrefix = null;
int longestLength = 0;
-
+
for (final String prefixName : this.prefixes.keySet()) {
// a prefix name is valid if:
// - it is prefixed (i.e. the unit name starts with it)
- // - it is longer than the existing largest prefix (since I am looking for the longest valid prefix)
+ // - it is longer than the existing largest prefix (since I am
+ // looking for the longest valid prefix)
// - the part after the prefix is a valid unit name
- // - the unit described that name is a linear unit (since only linear units can have prefixes)
- if (unitName.startsWith(prefixName) && prefixName.length() > longestLength) {
+ // - the unit described that name is a linear unit (since only
+ // linear units can have prefixes)
+ if (unitName.startsWith(prefixName)
+ && prefixName.length() > longestLength) {
final String rest = unitName.substring(prefixName.length());
- if (this.containsKey(rest) && this.get(rest) instanceof LinearUnit) {
+ if (this.containsKey(rest)
+ && this.get(rest) instanceof LinearUnit) {
longestPrefix = prefixName;
longestLength = prefixName.length();
}
}
}
-
+
return longestPrefix != null;
}
-
+
/**
* {@inheritDoc}
*
* <p>
- * Because of ambiguities between prefixes (i.e. kilokilo = mega), this method only tests for prefixless units.
+ * Because of ambiguities between prefixes (i.e. kilokilo = mega), this
+ * method only tests for prefixless units.
* </p>
*/
@Override
public boolean containsValue(final Object value) {
return this.units.containsValue(value);
}
-
+
@Override
public Set<Entry<String, Unit>> entrySet() {
if (this.entrySet == null) {
@@ -827,56 +898,62 @@ public final class UnitDatabase {
}
return this.entrySet;
}
-
+
@Override
public Unit get(final Object key) {
// First, test if there is a unit with the key
if (this.units.containsKey(key))
return this.units.get(key);
-
+
// Next, try to cast it to String
if (!(key instanceof String))
- throw new IllegalArgumentException("Attempted to obtain a unit using a non-string name.");
+ throw new IllegalArgumentException(
+ "Attempted to obtain a unit using a non-string name.");
final String unitName = (String) key;
-
+
// Then, look for the longest prefix that is attached to a valid unit
String longestPrefix = null;
int longestLength = 0;
-
+
for (final String prefixName : this.prefixes.keySet()) {
// a prefix name is valid if:
// - it is prefixed (i.e. the unit name starts with it)
- // - it is longer than the existing largest prefix (since I am looking for the longest valid prefix)
+ // - it is longer than the existing largest prefix (since I am
+ // looking for the longest valid prefix)
// - the part after the prefix is a valid unit name
- // - the unit described that name is a linear unit (since only linear units can have prefixes)
- if (unitName.startsWith(prefixName) && prefixName.length() > longestLength) {
+ // - the unit described that name is a linear unit (since only
+ // linear units can have prefixes)
+ if (unitName.startsWith(prefixName)
+ && prefixName.length() > longestLength) {
final String rest = unitName.substring(prefixName.length());
- if (this.containsKey(rest) && this.get(rest) instanceof LinearUnit) {
+ if (this.containsKey(rest)
+ && this.get(rest) instanceof LinearUnit) {
longestPrefix = prefixName;
longestLength = prefixName.length();
}
}
}
-
+
// if none found, returns null
if (longestPrefix == null)
return null;
else {
// get necessary data
final String rest = unitName.substring(longestLength);
- // this cast will not fail because I verified that it would work before selecting this prefix
+ // this cast will not fail because I verified that it would work
+ // before selecting this prefix
final LinearUnit unit = (LinearUnit) this.get(rest);
final UnitPrefix prefix = this.prefixes.get(longestPrefix);
-
+
return unit.withPrefix(prefix);
}
}
-
+
@Override
public boolean isEmpty() {
return this.units.isEmpty();
}
-
+
@Override
public Set<String> keySet() {
if (this.keySet == null) {
@@ -884,53 +961,64 @@ public final class UnitDatabase {
}
return this.keySet;
}
-
+
@Override
public Unit merge(final String key, final Unit value,
final BiFunction<? super Unit, ? super Unit, ? extends Unit> remappingFunction) {
- throw new UnsupportedOperationException("Cannot merge into an immutable map");
+ throw new UnsupportedOperationException(
+ "Cannot merge into an immutable map");
}
-
+
@Override
public Unit put(final String key, final Unit value) {
- throw new UnsupportedOperationException("Cannot add entries to an immutable map");
+ throw new UnsupportedOperationException(
+ "Cannot add entries to an immutable map");
}
-
+
@Override
public void putAll(final Map<? extends String, ? extends Unit> m) {
- throw new UnsupportedOperationException("Cannot add entries to an immutable map");
+ throw new UnsupportedOperationException(
+ "Cannot add entries to an immutable map");
}
-
+
@Override
public Unit putIfAbsent(final String key, final Unit value) {
- throw new UnsupportedOperationException("Cannot add entries to an immutable map");
+ throw new UnsupportedOperationException(
+ "Cannot add entries to an immutable map");
}
-
+
@Override
public Unit remove(final Object key) {
- throw new UnsupportedOperationException("Cannot remove entries from an immutable map");
+ throw new UnsupportedOperationException(
+ "Cannot remove entries from an immutable map");
}
-
+
@Override
public boolean remove(final Object key, final Object value) {
- throw new UnsupportedOperationException("Cannot remove entries from an immutable map");
+ throw new UnsupportedOperationException(
+ "Cannot remove entries from an immutable map");
}
-
+
@Override
public Unit replace(final String key, final Unit value) {
- throw new UnsupportedOperationException("Cannot replace entries in an immutable map");
+ throw new UnsupportedOperationException(
+ "Cannot replace entries in an immutable map");
}
-
+
@Override
- public boolean replace(final String key, final Unit oldValue, final Unit newValue) {
- throw new UnsupportedOperationException("Cannot replace entries in an immutable map");
+ public boolean replace(final String key, final Unit oldValue,
+ final Unit newValue) {
+ throw new UnsupportedOperationException(
+ "Cannot replace entries in an immutable map");
}
-
+
@Override
- public void replaceAll(final BiFunction<? super String, ? super Unit, ? extends Unit> function) {
- throw new UnsupportedOperationException("Cannot replace entries in an immutable map");
+ public void replaceAll(
+ final BiFunction<? super String, ? super Unit, ? extends Unit> function) {
+ throw new UnsupportedOperationException(
+ "Cannot replace entries in an immutable map");
}
-
+
@Override
public int size() {
if (this.units.isEmpty())
@@ -943,66 +1031,80 @@ public final class UnitDatabase {
return Integer.MAX_VALUE;
}
}
-
+
@Override
public String toString() {
if (this.units.isEmpty() || this.prefixes.isEmpty())
return super.toString();
else
- return String.format("Infinite map of name-unit entries created from units %s and prefixes %s",
+ return String.format(
+ "Infinite map of name-unit entries created from units %s and prefixes %s",
this.units, this.prefixes);
}
-
+
/**
* {@inheritDoc}
*
* <p>
- * Because of ambiguities between prefixes (i.e. kilokilo = mega), this method ignores prefixes.
+ * Because of ambiguities between prefixes (i.e. kilokilo = mega), this
+ * method ignores prefixes.
* </p>
*/
@Override
public Collection<Unit> values() {
if (this.values == null) {
- this.values = Collections.unmodifiableCollection(this.units.values());
+ this.values = Collections
+ .unmodifiableCollection(this.units.values());
}
return this.values;
}
}
-
+
/**
* Replacements done to *all* expression types
*/
private static final Map<Pattern, String> EXPRESSION_REPLACEMENTS = new HashMap<>();
-
+
// add data to expression replacements
static {
- // place brackets around any expression of the form "number unit", with or without the space
+ // add spaces around operators
+ for (final String operator : Arrays.asList("\\*", "/", "\\^")) {
+ EXPRESSION_REPLACEMENTS.put(Pattern.compile(operator),
+ " " + operator + " ");
+ }
+
+ // replace multiple spaces with a single space
+ EXPRESSION_REPLACEMENTS.put(Pattern.compile(" +"), " ");
+ // place brackets around any expression of the form "number unit", with or
+ // without the space
EXPRESSION_REPLACEMENTS.put(Pattern.compile("((?:-?[1-9]\\d*|0)" // integer
- + "(?:\\.\\d+(?:[eE]\\d+))?)" // optional decimal point with numbers after it
+ + "(?:\\.\\d+(?:[eE]\\d+))?)" // optional decimal point with numbers
+ // after it
+ "\\s*" // optional space(s)
+ "([a-zA-Z]+(?:\\^\\d+)?" // any string of letters
+ "(?:\\s+[a-zA-Z]+(?:\\^\\d+)?))" // optional other letters
- + "(?!-?\\d)" // no number directly afterwards (avoids matching "1e3")
+ + "(?!-?\\d)" // no number directly afterwards (avoids matching
+ // "1e3")
), "\\($1 $2\\)");
}
-
+
/**
* A regular expression that separates names and expressions in unit files.
*/
- private static final Pattern NAME_EXPRESSION = Pattern.compile("(\\S+)\\s+(\\S.*)");
-
+ private static final Pattern NAME_EXPRESSION = Pattern
+ .compile("(\\S+)\\s+(\\S.*)");
+
/**
* The exponent operator
*
- * @param base
- * base of exponentiation
- * @param exponentUnit
- * exponent
+ * @param base base of exponentiation
+ * @param exponentUnit exponent
* @return result
* @since 2019-04-10
* @since v0.2.0
*/
- private static final LinearUnit exponentiateUnits(final LinearUnit base, final LinearUnit exponentUnit) {
+ private static final LinearUnit exponentiateUnits(final LinearUnit base,
+ final LinearUnit exponentUnit) {
// exponent function - first check if o2 is a number,
if (exponentUnit.getBase().equals(SI.ONE.getBase())) {
// then check if it is an integer,
@@ -1012,12 +1114,39 @@ public final class UnitDatabase {
return base.toExponent((int) (exponent + 0.5));
else
// not an integer
- throw new UnsupportedOperationException("Decimal exponents are currently not supported.");
+ throw new UnsupportedOperationException(
+ "Decimal exponents are currently not supported.");
} else
// not a number
throw new IllegalArgumentException("Exponents must be numbers.");
}
-
+
+ /**
+ * The exponent operator
+ *
+ * @param base base of exponentiation
+ * @param exponentUnit exponent
+ * @return result
+ * @since 2020-08-04
+ */
+ private static final LinearUnitValue exponentiateUnitValues(
+ final LinearUnitValue base, final LinearUnitValue exponentValue) {
+ // exponent function - first check if o2 is a number,
+ if (exponentValue.canConvertTo(SI.ONE)) {
+ // then check if it is an integer,
+ final double exponent = exponentValue.getValueExact();
+ if (DecimalComparison.equals(exponent % 1, 0))
+ // then exponentiate
+ return base.toExponent((int) (exponent + 0.5));
+ else
+ // not an integer
+ throw new UnsupportedOperationException(
+ "Decimal exponents are currently not supported.");
+ } else
+ // not a number
+ throw new IllegalArgumentException("Exponents must be numbers.");
+ }
+
/**
* The units in this system, excluding prefixes.
*
@@ -1025,7 +1154,7 @@ public final class UnitDatabase {
* @since v0.1.0
*/
private final Map<String, Unit> prefixlessUnits;
-
+
/**
* The unit prefixes in this system.
*
@@ -1033,7 +1162,7 @@ public final class UnitDatabase {
* @since v0.1.0
*/
private final Map<String, UnitPrefix> prefixes;
-
+
/**
* The dimensions in this system.
*
@@ -1041,7 +1170,7 @@ public final class UnitDatabase {
* @since v0.2.0
*/
private final Map<String, ObjectProduct<BaseDimension>> dimensions;
-
+
/**
* A map mapping strings to units (including prefixes)
*
@@ -1049,7 +1178,19 @@ public final class UnitDatabase {
* @since v0.2.0
*/
private final Map<String, Unit> units;
-
+
+ /**
+ * The rule that specifies when prefix repetition is allowed. It takes in one
+ * argument: a list of the prefixes being applied to the unit
+ * <p>
+ * The prefixes are inputted in <em>application order</em>. This means that
+ * testing whether "kilomegagigametre" is a valid unit is equivalent to
+ * running the following code (assuming all variables are defined correctly):
+ * <br>
+ * {@code prefixRepetitionRule.test(Arrays.asList(giga, mega, kilo))}
+ */
+ private Predicate<List<UnitPrefix>> prefixRepetitionRule;
+
/**
* A parser that can parse unit expressions.
*
@@ -1059,21 +1200,41 @@ public final class UnitDatabase {
private final ExpressionParser<LinearUnit> unitExpressionParser = new ExpressionParser.Builder<>(
this::getLinearUnit).addBinaryOperator("+", (o1, o2) -> o1.plus(o2), 0)
.addBinaryOperator("-", (o1, o2) -> o1.minus(o2), 0)
- .addBinaryOperator("*", (o1, o2) -> o1.times(o2), 1).addSpaceFunction("*")
+ .addBinaryOperator("*", (o1, o2) -> o1.times(o2), 1)
+ .addSpaceFunction("*")
.addBinaryOperator("/", (o1, o2) -> o1.dividedBy(o2), 1)
- .addBinaryOperator("^", UnitDatabase::exponentiateUnits, 2).build();
-
+ .addBinaryOperator("^", UnitDatabase::exponentiateUnits, 2)
+ .build();
+
+ /**
+ * A parser that can parse unit value expressions.
+ *
+ * @since 2020-08-04
+ */
+ private final ExpressionParser<LinearUnitValue> unitValueExpressionParser = new ExpressionParser.Builder<>(
+ this::getLinearUnitValue)
+ .addBinaryOperator("+", (o1, o2) -> o1.plus(o2), 0)
+ .addBinaryOperator("-", (o1, o2) -> o1.minus(o2), 0)
+ .addBinaryOperator("*", (o1, o2) -> o1.times(o2), 1)
+ .addSpaceFunction("*")
+ .addBinaryOperator("/", (o1, o2) -> o1.dividedBy(o2), 1)
+ .addBinaryOperator("^", UnitDatabase::exponentiateUnitValues, 2)
+ .build();
+
/**
* A parser that can parse unit prefix expressions
*
* @since 2019-04-13
* @since v0.2.0
*/
- private final ExpressionParser<UnitPrefix> prefixExpressionParser = new ExpressionParser.Builder<>(this::getPrefix)
- .addBinaryOperator("*", (o1, o2) -> o1.times(o2), 0).addSpaceFunction("*")
- .addBinaryOperator("/", (o1, o2) -> o1.dividedBy(o2), 0)
- .addBinaryOperator("^", (o1, o2) -> o1.toExponent(o2.getMultiplier()), 1).build();
-
+ private final ExpressionParser<UnitPrefix> prefixExpressionParser = new ExpressionParser.Builder<>(
+ this::getPrefix).addBinaryOperator("*", (o1, o2) -> o1.times(o2), 0)
+ .addSpaceFunction("*")
+ .addBinaryOperator("/", (o1, o2) -> o1.dividedBy(o2), 0)
+ .addBinaryOperator("^",
+ (o1, o2) -> o1.toExponent(o2.getMultiplier()), 1)
+ .build();
+
/**
* A parser that can parse unit dimension expressions.
*
@@ -1081,9 +1242,10 @@ public final class UnitDatabase {
* @since v0.2.0
*/
private final ExpressionParser<ObjectProduct<BaseDimension>> unitDimensionParser = new ExpressionParser.Builder<>(
- this::getDimension).addBinaryOperator("*", (o1, o2) -> o1.times(o2), 0).addSpaceFunction("*")
+ this::getDimension).addBinaryOperator("*", (o1, o2) -> o1.times(o2), 0)
+ .addSpaceFunction("*")
.addBinaryOperator("/", (o1, o2) -> o1.dividedBy(o2), 0).build();
-
+
/**
* Creates the {@code UnitsDatabase}.
*
@@ -1091,48 +1253,62 @@ public final class UnitDatabase {
* @since v0.1.0
*/
public UnitDatabase() {
+ this(prefixes -> true);
+ }
+
+ /**
+ * Creates the {@code UnitsDatabase}
+ *
+ * @param prefixRepetitionRule the rule that determines when prefix
+ * repetition is allowed
+ * @since 2020-08-26
+ */
+ public UnitDatabase(Predicate<List<UnitPrefix>> prefixRepetitionRule) {
this.prefixlessUnits = new HashMap<>();
this.prefixes = new HashMap<>();
this.dimensions = new HashMap<>();
- this.units = new PrefixedUnitMap(this.prefixlessUnits, this.prefixes);
+ this.prefixRepetitionRule = prefixRepetitionRule;
+ this.units = ConditionalExistenceCollections.conditionalExistenceMap(
+ new PrefixedUnitMap(this.prefixlessUnits, this.prefixes),
+ entry -> this.prefixRepetitionRule
+ .test(this.getPrefixesFromName(entry.getKey())));
}
-
+
/**
* Adds a unit dimension to the database.
*
- * @param name
- * dimension's name
- * @param dimension
- * dimension to add
- * @throws NullPointerException
- * if name or dimension is null
+ * @param name dimension's name
+ * @param dimension dimension to add
+ * @throws NullPointerException if name or dimension is null
* @since 2019-03-14
* @since v0.2.0
*/
- public void addDimension(final String name, final ObjectProduct<BaseDimension> dimension) {
- this.dimensions.put(Objects.requireNonNull(name, "name must not be null."),
+ 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."));
}
-
+
/**
* Adds to the list from a line in a unit dimension file.
*
- * @param line
- * line to look at
- * @param lineCounter
- * number of line, for error messages
+ * @param line line to look at
+ * @param lineCounter number of line, for error messages
* @since 2019-04-10
* @since v0.2.0
*/
- private void addDimensionFromLine(final String line, final long lineCounter) {
+ private void addDimensionFromLine(final String line,
+ final long lineCounter) {
// ignore lines that start with a # sign - they're comments
if (line.isEmpty())
return;
if (line.contains("#")) {
- this.addDimensionFromLine(line.substring(0, line.indexOf("#")), lineCounter);
+ this.addDimensionFromLine(line.substring(0, line.indexOf("#")),
+ lineCounter);
return;
}
-
+
// divide line into name and expression
final Matcher lineMatcher = NAME_EXPRESSION.matcher(line);
if (!lineMatcher.matches())
@@ -1141,17 +1317,18 @@ public final class UnitDatabase {
lineCounter));
final String name = lineMatcher.group(1);
final String expression = lineMatcher.group(2);
-
+
if (name.endsWith(" ")) {
- System.err.printf("Warning - line %d's dimension name ends in a space", lineCounter);
+ System.err.printf("Warning - line %d's dimension name ends in a space",
+ lineCounter);
}
-
+
// if expression is "!", search for an existing dimension
// if no unit found, throw an error
if (expression.equals("!")) {
if (!this.containsDimensionName(name))
- throw new IllegalArgumentException(
- String.format("! used but no dimension found (line %d).", lineCounter));
+ throw new IllegalArgumentException(String.format(
+ "! used but no dimension found (line %d).", lineCounter));
} else {
// it's a unit, get the unit
final ObjectProduct<BaseDimension> dimension;
@@ -1161,20 +1338,17 @@ public final class UnitDatabase {
System.err.printf("Parsing error on line %d:%n", lineCounter);
throw e;
}
-
+
this.addDimension(name, dimension);
}
}
-
+
/**
* Adds a unit prefix to the database.
*
- * @param name
- * prefix's name
- * @param prefix
- * prefix to add
- * @throws NullPointerException
- * if name or prefix is null
+ * @param name prefix's name
+ * @param prefix prefix to add
+ * @throws NullPointerException if name or prefix is null
* @since 2019-01-14
* @since v0.1.0
*/
@@ -1182,43 +1356,41 @@ public final class UnitDatabase {
this.prefixes.put(Objects.requireNonNull(name, "name must not be null."),
Objects.requireNonNull(prefix, "prefix must not be null."));
}
-
+
/**
* Adds a unit to the database.
*
- * @param name
- * unit's name
- * @param unit
- * unit to add
- * @throws NullPointerException
- * if unit is null
+ * @param name unit's name
+ * @param unit unit to add
+ * @throws NullPointerException if unit is null
* @since 2019-01-10
* @since v0.1.0
*/
public void addUnit(final String name, final Unit unit) {
- this.prefixlessUnits.put(Objects.requireNonNull(name, "name must not be null."),
+ this.prefixlessUnits.put(
+ Objects.requireNonNull(name, "name must not be null."),
Objects.requireNonNull(unit, "unit must not be null."));
}
-
+
/**
* Adds to the list from a line in a unit file.
*
- * @param line
- * line to look at
- * @param lineCounter
- * number of line, for error messages
+ * @param line line to look at
+ * @param lineCounter number of line, for error messages
* @since 2019-04-10
* @since v0.2.0
*/
- private void addUnitOrPrefixFromLine(final String line, final long lineCounter) {
+ private void addUnitOrPrefixFromLine(final String line,
+ final long lineCounter) {
// ignore lines that start with a # sign - they're comments
if (line.isEmpty())
return;
if (line.contains("#")) {
- this.addUnitOrPrefixFromLine(line.substring(0, line.indexOf("#")), lineCounter);
+ this.addUnitOrPrefixFromLine(line.substring(0, line.indexOf("#")),
+ lineCounter);
return;
}
-
+
// divide line into name and expression
final Matcher lineMatcher = NAME_EXPRESSION.matcher(line);
if (!lineMatcher.matches())
@@ -1226,18 +1398,20 @@ public final class UnitDatabase {
"Error at line %d: Lines of a unit file must consist of a unit name, then spaces or tabs, then a unit expression.",
lineCounter));
final String name = lineMatcher.group(1);
-
+
final String expression = lineMatcher.group(2);
-
+
if (name.endsWith(" ")) {
- System.err.printf("Warning - line %d's unit name ends in a space", lineCounter);
+ System.err.printf("Warning - line %d's unit name ends in a space",
+ lineCounter);
}
-
+
// if expression is "!", search for an existing unit
// if no unit found, throw an error
if (expression.equals("!")) {
if (!this.containsUnitName(name))
- throw new IllegalArgumentException(String.format("! used but no unit found (line %d).", lineCounter));
+ throw new IllegalArgumentException(String
+ .format("! used but no unit found (line %d).", lineCounter));
} else {
if (name.endsWith("-")) {
final UnitPrefix prefix;
@@ -1257,17 +1431,16 @@ public final class UnitDatabase {
System.err.printf("Parsing error on line %d:%n", lineCounter);
throw e;
}
-
+
this.addUnit(name, unit);
}
}
}
-
+
/**
* Tests if the database has a unit dimension with this name.
*
- * @param name
- * name to test
+ * @param name name to test
* @return if database contains name
* @since 2019-03-14
* @since v0.2.0
@@ -1275,12 +1448,11 @@ public final class UnitDatabase {
public boolean containsDimensionName(final String name) {
return this.dimensions.containsKey(name);
}
-
+
/**
* Tests if the database has a unit prefix with this name.
*
- * @param name
- * name to test
+ * @param name name to test
* @return if database contains name
* @since 2019-01-13
* @since v0.1.0
@@ -1288,12 +1460,12 @@ public final class UnitDatabase {
public boolean containsPrefixName(final String name) {
return this.prefixes.containsKey(name);
}
-
+
/**
- * Tests if the database has a unit with this name, taking prefixes into consideration
+ * Tests if the database has a unit with this name, taking prefixes into
+ * consideration
*
- * @param name
- * name to test
+ * @param name name to test
* @return if database contains name
* @since 2019-01-13
* @since v0.1.0
@@ -1301,7 +1473,7 @@ public final class UnitDatabase {
public boolean containsUnitName(final String name) {
return this.units.containsKey(name);
}
-
+
/**
* @return a map mapping dimension names to dimensions
* @since 2019-04-13
@@ -1310,7 +1482,50 @@ public final class UnitDatabase {
public Map<String, ObjectProduct<BaseDimension>> dimensionMap() {
return Collections.unmodifiableMap(this.dimensions);
}
-
+
+ /**
+ * Evaluates a unit expression, following the same rules as
+ * {@link #getUnitFromExpression}.
+ *
+ * @param expression expression to parse
+ * @return {@code LinearUnitValue} representing value of expression
+ * @since 2020-08-04
+ */
+ public LinearUnitValue evaluateUnitExpression(final String expression) {
+ Objects.requireNonNull(expression, "expression must not be null.");
+
+ // attempt to get a unit as an alias, or a number with precision first
+ if (this.containsUnitName(expression))
+ return this.getLinearUnitValue(expression);
+
+ // force operators to have spaces
+ String modifiedExpression = expression;
+ modifiedExpression = modifiedExpression.replaceAll("\\+", " \\+ ");
+ modifiedExpression = modifiedExpression.replaceAll("-", " - ");
+
+ // format expression
+ for (final Entry<Pattern, String> replacement : EXPRESSION_REPLACEMENTS
+ .entrySet()) {
+ modifiedExpression = replacement.getKey().matcher(modifiedExpression)
+ .replaceAll(replacement.getValue());
+ }
+
+ // the previous operation breaks negative numbers, fix them!
+ // (i.e. -2 becomes - 2)
+ // FIXME the previous operaton also breaks stuff like "1e-5"
+ for (int i = 0; i < modifiedExpression.length(); i++) {
+ if (modifiedExpression.charAt(i) == '-'
+ && (i < 2 || Arrays.asList('+', '-', '*', '/', '^')
+ .contains(modifiedExpression.charAt(i - 2)))) {
+ // found a broken negative number
+ modifiedExpression = modifiedExpression.substring(0, i + 1)
+ + modifiedExpression.substring(i + 2);
+ }
+ }
+
+ return this.unitValueExpressionParser.parseExpression(modifiedExpression);
+ }
+
/**
* Gets a unit dimension from the database using its name.
*
@@ -1318,8 +1533,7 @@ public final class UnitDatabase {
* This method accepts exponents, like "L^3"
* </p>
*
- * @param name
- * dimension's name
+ * @param name dimension's name
* @return dimension
* @since 2019-03-14
* @since v0.2.0
@@ -1328,102 +1542,125 @@ public final class UnitDatabase {
Objects.requireNonNull(name, "name must not be null.");
if (name.contains("^")) {
final String[] baseAndExponent = name.split("\\^");
-
- final ObjectProduct<BaseDimension> base = this.getDimension(baseAndExponent[0]);
-
+
+ final ObjectProduct<BaseDimension> base = this
+ .getDimension(baseAndExponent[0]);
+
final int exponent;
try {
- exponent = Integer.parseInt(baseAndExponent[baseAndExponent.length - 1]);
+ exponent = Integer
+ .parseInt(baseAndExponent[baseAndExponent.length - 1]);
} catch (final NumberFormatException e2) {
throw new IllegalArgumentException("Exponent must be an integer.");
}
-
+
return base.toExponent(exponent);
}
return this.dimensions.get(name);
}
-
+
/**
* Uses the database's data to parse an expression into a unit dimension
* <p>
* The expression is a series of any of the following:
* <ul>
- * <li>The name of a unit dimension, which multiplies or divides the result based on preceding operators</li>
- * <li>The operators '*' and '/', which multiply and divide (note that just putting two unit dimensions next to each
- * other is equivalent to multiplication)</li>
+ * <li>The name of a unit dimension, which multiplies or divides the result
+ * based on preceding operators</li>
+ * <li>The operators '*' and '/', which multiply and divide (note that just
+ * putting two unit dimensions next to each other is equivalent to
+ * multiplication)</li>
* <li>The operator '^' which exponentiates. Exponents must be integers.</li>
* </ul>
*
- * @param expression
- * expression to parse
- * @throws IllegalArgumentException
- * if the expression cannot be parsed
- * @throws NullPointerException
- * if expression is null
+ * @param expression expression to parse
+ * @throws IllegalArgumentException if the expression cannot be parsed
+ * @throws NullPointerException if expression is null
* @since 2019-04-13
* @since v0.2.0
*/
- public ObjectProduct<BaseDimension> getDimensionFromExpression(final String expression) {
+ public ObjectProduct<BaseDimension> getDimensionFromExpression(
+ final String expression) {
Objects.requireNonNull(expression, "expression must not be null.");
-
+
// attempt to get a dimension as an alias first
if (this.containsDimensionName(expression))
return this.getDimension(expression);
-
+
// force operators to have spaces
String modifiedExpression = expression;
- modifiedExpression = modifiedExpression.replaceAll("\\*", " \\* ");
- modifiedExpression = modifiedExpression.replaceAll("/", " / ");
- modifiedExpression = modifiedExpression.replaceAll(" *\\^ *", "\\^");
-
- // fix broken spaces
- modifiedExpression = modifiedExpression.replaceAll(" +", " ");
-
+
// format expression
- for (final Entry<Pattern, String> replacement : EXPRESSION_REPLACEMENTS.entrySet()) {
- modifiedExpression = replacement.getKey().matcher(modifiedExpression).replaceAll(replacement.getValue());
+ for (final Entry<Pattern, String> replacement : EXPRESSION_REPLACEMENTS
+ .entrySet()) {
+ modifiedExpression = replacement.getKey().matcher(modifiedExpression)
+ .replaceAll(replacement.getValue());
}
-
+ modifiedExpression = modifiedExpression.replaceAll(" *\\^ *", "\\^");
+
return this.unitDimensionParser.parseExpression(modifiedExpression);
}
-
+
/**
- * Gets a unit. If it is linear, cast it to a LinearUnit and return it. Otherwise, throw an
- * {@code IllegalArgumentException}.
+ * Gets a unit. If it is linear, cast it to a LinearUnit and return it.
+ * Otherwise, throw an {@code IllegalArgumentException}.
*
- * @param name
- * unit's name
+ * @param name unit's name
* @return unit
* @since 2019-03-22
* @since v0.2.0
*/
private LinearUnit getLinearUnit(final String name) {
// see if I am using a function-unit like tempC(100)
+ Objects.requireNonNull(name, "name may not be null");
if (name.contains("(") && name.contains(")")) {
// break it into function name and value
final List<String> parts = Arrays.asList(name.split("\\("));
if (parts.size() != 2)
- throw new IllegalArgumentException("Format nonlinear units like: unit(value).");
-
+ throw new IllegalArgumentException(
+ "Format nonlinear units like: unit(value).");
+
// solve the function
final Unit unit = this.getUnit(parts.get(0));
- final double value = Double.parseDouble(parts.get(1).substring(0, parts.get(1).length() - 1));
+ final double value = Double.parseDouble(
+ parts.get(1).substring(0, parts.get(1).length() - 1));
return LinearUnit.fromUnitValue(unit, value);
} else {
// get a linear unit
final Unit unit = this.getUnit(name);
+
if (unit instanceof LinearUnit)
return (LinearUnit) unit;
else
- throw new IllegalArgumentException(String.format("%s is not a linear unit.", name));
+ throw new IllegalArgumentException(
+ String.format("%s is not a linear unit.", name));
}
}
-
+
+ /**
+ * Gets a {@code LinearUnitValue} from a unit name. Nonlinear units will be
+ * converted to their base units.
+ *
+ * @param name name of unit
+ * @return {@code LinearUnitValue} instance
+ * @since 2020-08-04
+ */
+ private LinearUnitValue getLinearUnitValue(final String name) {
+ try {
+ // try to parse it as a number - otherwise it is not a number!
+ final BigDecimal number = new BigDecimal(name);
+
+ final double uncertainty = Math.pow(10, -number.scale());
+ return LinearUnitValue.of(SI.ONE,
+ UncertainDouble.of(number.doubleValue(), uncertainty));
+ } catch (final NumberFormatException e) {
+ return LinearUnitValue.getExact(this.getLinearUnit(name), 1);
+ }
+ }
+
/**
* Gets a unit prefix from the database from its name
*
- * @param name
- * prefix's name
+ * @param name prefix's name
* @return prefix
* @since 2019-01-10
* @since v0.1.0
@@ -1435,53 +1672,87 @@ public final class UnitDatabase {
return this.prefixes.get(name);
}
}
-
+
+ /**
+ * Gets all of the prefixes that are on a unit name, in application order.
+ *
+ * @param unitName name of unit
+ * @return prefixes
+ * @since 2020-08-26
+ */
+ List<UnitPrefix> getPrefixesFromName(final String unitName) {
+ final List<UnitPrefix> prefixes = new ArrayList<>();
+ String name = unitName;
+
+ while (!this.prefixlessUnits.containsKey(name)) {
+ // find the longest prefix
+ String longestPrefixName = null;
+ int longestLength = name.length();
+
+ while (longestPrefixName == null) {
+ longestLength--;
+ if (longestLength <= 0)
+ throw new AssertionError(
+ "No prefix found in " + name + ", but it is not a unit!");
+ if (this.prefixes.containsKey(name.substring(0, longestLength))) {
+ longestPrefixName = name.substring(0, longestLength);
+ }
+ }
+
+ // longest prefix found!
+ final UnitPrefix prefix = this.getPrefix(longestPrefixName);
+ prefixes.add(0, prefix);
+ name = name.substring(longestLength);
+ }
+ return prefixes;
+ }
+
/**
* Gets a unit prefix from a prefix expression
* <p>
- * Currently, prefix expressions are much simpler than unit expressions: They are either a number or the name of
- * another prefix
+ * Currently, prefix expressions are much simpler than unit expressions: They
+ * are either a number or the name of another prefix
* </p>
*
- * @param expression
- * expression to input
+ * @param expression expression to input
* @return prefix
- * @throws IllegalArgumentException
- * if expression cannot be parsed
- * @throws NullPointerException
- * if any argument is null
+ * @throws IllegalArgumentException if expression cannot be parsed
+ * @throws NullPointerException if any argument is null
* @since 2019-01-14
* @since v0.1.0
*/
public UnitPrefix getPrefixFromExpression(final String expression) {
Objects.requireNonNull(expression, "expression must not be null.");
-
+
// attempt to get a unit as an alias first
if (this.containsUnitName(expression))
return this.getPrefix(expression);
-
+
// force operators to have spaces
String modifiedExpression = expression;
- modifiedExpression = modifiedExpression.replaceAll("\\*", " \\* ");
- modifiedExpression = modifiedExpression.replaceAll("/", " / ");
- modifiedExpression = modifiedExpression.replaceAll("\\^", " \\^ ");
-
- // fix broken spaces
- modifiedExpression = modifiedExpression.replaceAll(" +", " ");
-
+
// format expression
- for (final Entry<Pattern, String> replacement : EXPRESSION_REPLACEMENTS.entrySet()) {
- modifiedExpression = replacement.getKey().matcher(modifiedExpression).replaceAll(replacement.getValue());
+ for (final Entry<Pattern, String> replacement : EXPRESSION_REPLACEMENTS
+ .entrySet()) {
+ modifiedExpression = replacement.getKey().matcher(modifiedExpression)
+ .replaceAll(replacement.getValue());
}
-
+
return this.prefixExpressionParser.parseExpression(modifiedExpression);
}
-
+
+ /**
+ * @return the prefixRepetitionRule
+ * @since 2020-08-26
+ */
+ public final Predicate<List<UnitPrefix>> getPrefixRepetitionRule() {
+ return this.prefixRepetitionRule;
+ }
+
/**
* Gets a unit from the database from its name, looking for prefixes.
*
- * @param name
- * unit's name
+ * @param name unit's name
* @return unit
* @since 2019-01-10
* @since v0.1.0
@@ -1491,101 +1762,115 @@ public final class UnitDatabase {
final double value = Double.parseDouble(name);
return SI.ONE.times(value);
} catch (final NumberFormatException e) {
- return this.units.get(name);
+ final Unit unit = this.units.get(name);
+ if (unit == null)
+ throw new NoSuchElementException("No unit " + name);
+ else if (unit.getPrimaryName().isEmpty())
+ return unit.withName(NameSymbol.ofName(name));
+ else if (!unit.getPrimaryName().get().equals(name)) {
+ final Set<String> otherNames = new HashSet<>(unit.getOtherNames());
+ otherNames.add(unit.getPrimaryName().get());
+ return unit.withName(NameSymbol.ofNullable(name,
+ unit.getSymbol().orElse(null), otherNames));
+ } else if (!unit.getOtherNames().contains(name)) {
+ final Set<String> otherNames = new HashSet<>(unit.getOtherNames());
+ otherNames.add(name);
+ return unit.withName(
+ NameSymbol.ofNullable(unit.getPrimaryName().orElse(null),
+ unit.getSymbol().orElse(null), otherNames));
+ } else
+ return unit;
}
-
+
}
-
+
/**
* Uses the database's unit data to parse an expression into a unit
* <p>
* The expression is a series of any of the following:
* <ul>
- * <li>The name of a unit, which multiplies or divides the result based on preceding operators</li>
- * <li>The operators '*' and '/', which multiply and divide (note that just putting two units or values next to each
- * other is equivalent to multiplication)</li>
+ * <li>The name of a unit, which multiplies or divides the result based on
+ * preceding operators</li>
+ * <li>The operators '*' and '/', which multiply and divide (note that just
+ * putting two units or values next to each other is equivalent to
+ * multiplication)</li>
* <li>The operator '^' which exponentiates. Exponents must be integers.</li>
* <li>A number which is multiplied or divided</li>
* </ul>
* This method only works with linear units.
*
- * @param expression
- * expression to parse
- * @throws IllegalArgumentException
- * if the expression cannot be parsed
- * @throws NullPointerException
- * if expression is null
+ * @param expression expression to parse
+ * @throws IllegalArgumentException if the expression cannot be parsed
+ * @throws NullPointerException if expression is null
* @since 2019-01-07
* @since v0.1.0
*/
public Unit getUnitFromExpression(final String expression) {
Objects.requireNonNull(expression, "expression must not be null.");
-
+
// attempt to get a unit as an alias first
if (this.containsUnitName(expression))
return this.getUnit(expression);
-
+
// force operators to have spaces
String modifiedExpression = expression;
modifiedExpression = modifiedExpression.replaceAll("\\+", " \\+ ");
modifiedExpression = modifiedExpression.replaceAll("-", " - ");
- modifiedExpression = modifiedExpression.replaceAll("\\*", " \\* ");
- modifiedExpression = modifiedExpression.replaceAll("/", " / ");
- modifiedExpression = modifiedExpression.replaceAll("\\^", " \\^ ");
-
- // fix broken spaces
- modifiedExpression = modifiedExpression.replaceAll(" +", " ");
// format expression
- for (final Entry<Pattern, String> replacement : EXPRESSION_REPLACEMENTS.entrySet()) {
- modifiedExpression = replacement.getKey().matcher(modifiedExpression).replaceAll(replacement.getValue());
+ for (final Entry<Pattern, String> replacement : EXPRESSION_REPLACEMENTS
+ .entrySet()) {
+ modifiedExpression = replacement.getKey().matcher(modifiedExpression)
+ .replaceAll(replacement.getValue());
}
-
+
// the previous operation breaks negative numbers, fix them!
// (i.e. -2 becomes - 2)
for (int i = 0; i < modifiedExpression.length(); i++) {
if (modifiedExpression.charAt(i) == '-'
- && (i < 2 || Arrays.asList('+', '-', '*', '/', '^').contains(modifiedExpression.charAt(i - 2)))) {
+ && (i < 2 || Arrays.asList('+', '-', '*', '/', '^')
+ .contains(modifiedExpression.charAt(i - 2)))) {
// found a broken negative number
- modifiedExpression = modifiedExpression.substring(0, i + 1) + modifiedExpression.substring(i + 2);
+ modifiedExpression = modifiedExpression.substring(0, i + 1)
+ + modifiedExpression.substring(i + 2);
}
}
-
+
return this.unitExpressionParser.parseExpression(modifiedExpression);
}
-
+
/**
- * Adds all dimensions from a file, using data from the database to parse them.
+ * Adds all dimensions from a file, using data from the database to parse
+ * them.
* <p>
- * Each line in the file should consist of a name and an expression (parsed by getDimensionFromExpression) separated
- * by any number of tab characters.
+ * Each line in the file should consist of a name and an expression (parsed
+ * by getDimensionFromExpression) separated by any number of tab characters.
* <p>
* <p>
* Allowed exceptions:
* <ul>
- * <li>Anything after a '#' character is considered a comment and ignored.</li>
+ * <li>Anything after a '#' character is considered a comment and
+ * ignored.</li>
* <li>Blank lines are also ignored</li>
- * <li>If an expression consists of a single exclamation point, instead of parsing it, this method will search the
- * database for an existing unit. If no unit is found, an IllegalArgumentException is thrown. This is used to define
- * initial units and ensure that the database contains them.</li>
+ * <li>If an expression consists of a single exclamation point, instead of
+ * parsing it, this method will search the database for an existing unit. If
+ * no unit is found, an IllegalArgumentException is thrown. This is used to
+ * define initial units and ensure that the database contains them.</li>
* </ul>
*
- * @param file
- * file to read
- * @throws IllegalArgumentException
- * if the file cannot be parsed, found or read
- * @throws NullPointerException
- * if file is null
+ * @param file file to read
+ * @throws IllegalArgumentException if the file cannot be parsed, found or
+ * read
+ * @throws NullPointerException if file is null
* @since 2019-01-13
* @since v0.1.0
*/
- public void loadDimensionFile(final File file) {
+ public void loadDimensionFile(final Path file) {
Objects.requireNonNull(file, "file must not be null.");
- try (FileReader fileReader = new FileReader(file); BufferedReader reader = new BufferedReader(fileReader)) {
- // while the reader has lines to read, read a line, then parse it, then add it
+ try {
long lineCounter = 0;
- while (reader.ready()) {
- this.addDimensionFromLine(reader.readLine(), ++lineCounter);
+ for (final String line : Files.readAllLines(file)) {
+ this.addDimensionFromLine(line, ++lineCounter);
}
} catch (final FileNotFoundException e) {
throw new IllegalArgumentException("Could not find file " + file, e);
@@ -1593,39 +1878,38 @@ public final class UnitDatabase {
throw new IllegalArgumentException("Could not read file " + file, e);
}
}
-
+
/**
* Adds all units from a file, using data from the database to parse them.
* <p>
- * Each line in the file should consist of a name and an expression (parsed by getUnitFromExpression) separated by
- * any number of tab characters.
+ * Each line in the file should consist of a name and an expression (parsed
+ * by getUnitFromExpression) separated by any number of tab characters.
* <p>
* <p>
* Allowed exceptions:
* <ul>
- * <li>Anything after a '#' character is considered a comment and ignored.</li>
+ * <li>Anything after a '#' character is considered a comment and
+ * ignored.</li>
* <li>Blank lines are also ignored</li>
- * <li>If an expression consists of a single exclamation point, instead of parsing it, this method will search the
- * database for an existing unit. If no unit is found, an IllegalArgumentException is thrown. This is used to define
- * initial units and ensure that the database contains them.</li>
+ * <li>If an expression consists of a single exclamation point, instead of
+ * parsing it, this method will search the database for an existing unit. If
+ * no unit is found, an IllegalArgumentException is thrown. This is used to
+ * define initial units and ensure that the database contains them.</li>
* </ul>
*
- * @param file
- * file to read
- * @throws IllegalArgumentException
- * if the file cannot be parsed, found or read
- * @throws NullPointerException
- * if file is null
+ * @param file file to read
+ * @throws IllegalArgumentException if the file cannot be parsed, found or
+ * read
+ * @throws NullPointerException if file is null
* @since 2019-01-13
* @since v0.1.0
*/
- public void loadUnitsFile(final File file) {
+ public void loadUnitsFile(final Path file) {
Objects.requireNonNull(file, "file must not be null.");
- try (FileReader fileReader = new FileReader(file); BufferedReader reader = new BufferedReader(fileReader)) {
- // while the reader has lines to read, read a line, then parse it, then add it
+ try {
long lineCounter = 0;
- while (reader.ready()) {
- this.addUnitOrPrefixFromLine(reader.readLine(), ++lineCounter);
+ for (final String line : Files.readAllLines(file)) {
+ this.addUnitOrPrefixFromLine(line, ++lineCounter);
}
} catch (final FileNotFoundException e) {
throw new IllegalArgumentException("Could not find file " + file, e);
@@ -1633,7 +1917,7 @@ public final class UnitDatabase {
throw new IllegalArgumentException("Could not read file " + file, e);
}
}
-
+
/**
* @return a map mapping prefix names to prefixes
* @since 2019-04-13
@@ -1642,33 +1926,49 @@ public final class UnitDatabase {
public Map<String, UnitPrefix> prefixMap() {
return Collections.unmodifiableMap(this.prefixes);
}
-
+
/**
- * @return a string stating the number of units, prefixes and dimensions in the database
+ * @param prefixRepetitionRule the prefixRepetitionRule to set
+ * @since 2020-08-26
+ */
+ public final void setPrefixRepetitionRule(
+ Predicate<List<UnitPrefix>> prefixRepetitionRule) {
+ this.prefixRepetitionRule = prefixRepetitionRule;
+ }
+
+ /**
+ * @return a string stating the number of units, prefixes and dimensions in
+ * the database
*/
@Override
public String toString() {
- return String.format("Unit Database with %d units, %d unit prefixes and %d dimensions",
- this.prefixlessUnits.size(), this.prefixes.size(), this.dimensions.size());
+ return String.format(
+ "Unit Database with %d units, %d unit prefixes and %d dimensions",
+ this.prefixlessUnits.size(), this.prefixes.size(),
+ this.dimensions.size());
}
-
+
/**
* Returns a map mapping unit names to units, including units with prefixes.
* <p>
- * The returned map is infinite in size if there is at least one unit and at least one prefix. If it is infinite,
- * some operations that only work with finite collections, like converting name/entry sets to arrays, will throw an
- * {@code IllegalStateException}.
+ * The returned map is infinite in size if there is at least one unit and at
+ * least one prefix. If it is infinite, some operations that only work with
+ * finite collections, like converting name/entry sets to arrays, will throw
+ * an {@code IllegalStateException}.
* </p>
* <p>
- * Specifically, the operations that will throw an IllegalStateException if the map is infinite in size are:
+ * Specifically, the operations that will throw an IllegalStateException if
+ * the map is infinite in size are:
* <ul>
* <li>{@code unitMap.entrySet().toArray()} (either overloading)</li>
* <li>{@code unitMap.keySet().toArray()} (either overloading)</li>
* </ul>
* </p>
* <p>
- * Because of ambiguities between prefixes (i.e. kilokilo = mega), the map's {@link PrefixedUnitMap#containsValue
- * containsValue} and {@link PrefixedUnitMap#values() values()} methods currently ignore prefixes.
+ * Because of ambiguities between prefixes (i.e. kilokilo = mega), the map's
+ * {@link PrefixedUnitMap#containsValue containsValue} and
+ * {@link PrefixedUnitMap#values() values()} methods currently ignore
+ * prefixes.
* </p>
*
* @return a map mapping unit names to units, including prefixed names
@@ -1676,9 +1976,10 @@ public final class UnitDatabase {
* @since v0.2.0
*/
public Map<String, Unit> unitMap() {
- return this.units; // PrefixedUnitMap is immutable so I don't need to make an unmodifiable map.
+ return this.units; // PrefixedUnitMap is immutable so I don't need to make
+ // an unmodifiable map.
}
-
+
/**
* @return a map mapping unit names to units, ignoring prefixes
* @since 2019-04-13
diff --git a/src/org/unitConverter/unit/UnitDatabaseTest.java b/src/org/unitConverter/unit/UnitDatabaseTest.java
index 164172b..2b981b6 100644
--- a/src/org/unitConverter/unit/UnitDatabaseTest.java
+++ b/src/org/unitConverter/unit/UnitDatabaseTest.java
@@ -18,17 +18,22 @@ package org.unitConverter.unit;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
-import static org.junit.jupiter.api.Assertions.fail;
+import java.util.Arrays;
import java.util.Iterator;
+import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
+import java.util.NoSuchElementException;
+import java.util.Set;
import org.junit.jupiter.api.Test;
/**
- * A test for the {@link UnitDatabase} class. This is NOT part of this program's public API.
+ * A test for the {@link UnitDatabase} class. This is NOT part of this program's
+ * public API.
*
* @author Adrien Hopkins
* @since 2019-04-14
@@ -39,67 +44,52 @@ class UnitDatabaseTest {
private static final Unit U = SI.METRE;
private static final Unit V = SI.KILOGRAM;
private static final Unit W = SI.SECOND;
-
+
// used for testing expressions
// J = U^2 * V / W^2
- private static final LinearUnit J = SI.KILOGRAM.times(SI.METRE.toExponent(2)).dividedBy(SI.SECOND.toExponent(2));
+ private static final LinearUnit J = SI.KILOGRAM.times(SI.METRE.toExponent(2))
+ .dividedBy(SI.SECOND.toExponent(2));
private static final LinearUnit K = SI.KELVIN;
-
- private static final Unit NONLINEAR = Unit.fromConversionFunctions(SI.METRE.getBase(), o -> o + 1, o -> o - 1);
-
+
+ private static final Unit NONLINEAR = Unit
+ .fromConversionFunctions(SI.METRE.getBase(), o -> o + 1, o -> o - 1);
+
// make the prefix values prime so I can tell which multiplications were made
- private static final UnitPrefix A = UnitPrefix.valueOf(2);
- private static final UnitPrefix B = UnitPrefix.valueOf(3);
- private static final UnitPrefix C = UnitPrefix.valueOf(5);
+ private static final UnitPrefix A = UnitPrefix.valueOf(2)
+ .withName(NameSymbol.ofName("A"));
+ private static final UnitPrefix B = UnitPrefix.valueOf(3)
+ .withName(NameSymbol.ofName("B"));
+ private static final UnitPrefix C = UnitPrefix.valueOf(5)
+ .withName(NameSymbol.ofName("C"));
private static final UnitPrefix AB = UnitPrefix.valueOf(7);
private static final UnitPrefix BC = UnitPrefix.valueOf(11);
-
+
/**
- * Confirms that operations that shouldn't function for infinite databases throw an {@code IllegalStateException}.
+ * Confirms that operations that shouldn't function for infinite databases
+ * throw an {@code IllegalStateException}.
*
* @since 2019-05-03
*/
@Test
+ // @Timeout(value = 5, unit = TimeUnit.SECONDS)
public void testInfiniteSetExceptions() {
// load units
final UnitDatabase infiniteDatabase = new UnitDatabase();
-
+
infiniteDatabase.addUnit("J", J);
infiniteDatabase.addUnit("K", K);
-
+
infiniteDatabase.addPrefix("A", A);
infiniteDatabase.addPrefix("B", B);
infiniteDatabase.addPrefix("C", C);
-
- {
- boolean exceptionThrown = false;
- try {
- infiniteDatabase.unitMap().entrySet().toArray();
- } catch (final IllegalStateException e) {
- exceptionThrown = true;
- // pass!
- } finally {
- if (!exceptionThrown) {
- fail("No IllegalStateException thrown");
- }
- }
- }
-
- {
- boolean exceptionThrown = false;
- try {
- infiniteDatabase.unitMap().keySet().toArray();
- } catch (final IllegalStateException e) {
- exceptionThrown = true;
- // pass!
- } finally {
- if (!exceptionThrown) {
- fail("No IllegalStateException thrown");
- }
- }
- }
+
+ final Set<Entry<String, Unit>> entrySet = infiniteDatabase.unitMap()
+ .entrySet();
+ final Set<String> keySet = infiniteDatabase.unitMap().keySet();
+ assertThrows(IllegalStateException.class, () -> entrySet.toArray());
+ assertThrows(IllegalStateException.class, () -> keySet.toArray());
}
-
+
/**
* Test that prefixes correctly apply to units.
*
@@ -109,23 +99,28 @@ class UnitDatabaseTest {
@Test
public void testPrefixes() {
final UnitDatabase database = new UnitDatabase();
-
+
database.addUnit("U", U);
database.addUnit("V", V);
database.addUnit("W", W);
-
+
database.addPrefix("A", A);
database.addPrefix("B", B);
database.addPrefix("C", C);
-
+
+ // test the getPrefixesFromName method
+ final List<UnitPrefix> expected = Arrays.asList(C, B, A);
+ assertEquals(expected, database.getPrefixesFromName("ABCU"));
+
// get the product
final Unit abcuNonlinear = database.getUnit("ABCU");
assert abcuNonlinear instanceof LinearUnit;
-
+
final LinearUnit abcu = (LinearUnit) abcuNonlinear;
- assertEquals(A.getMultiplier() * B.getMultiplier() * C.getMultiplier(), abcu.getConversionFactor(), 1e-15);
+ assertEquals(A.getMultiplier() * B.getMultiplier() * C.getMultiplier(),
+ abcu.getConversionFactor(), 1e-15);
}
-
+
/**
* Tests the functionnalites of the prefixless unit map.
*
@@ -140,21 +135,22 @@ class UnitDatabaseTest {
public void testPrefixlessUnitMap() {
final UnitDatabase database = new UnitDatabase();
final Map<String, Unit> prefixlessUnits = database.unitMapPrefixless();
-
+
database.addUnit("U", U);
database.addUnit("V", V);
database.addUnit("W", W);
-
+
// this should work because the map should be an auto-updating view
assertTrue(prefixlessUnits.containsKey("U"));
assertFalse(prefixlessUnits.containsKey("Z"));
-
+
assertTrue(prefixlessUnits.containsValue(U));
assertFalse(prefixlessUnits.containsValue(NONLINEAR));
}
-
+
/**
- * Tests that the database correctly stores and retrieves units, ignoring prefixes.
+ * Tests that the database correctly stores and retrieves units, ignoring
+ * prefixes.
*
* @since 2019-04-14
* @since v0.2.0
@@ -162,18 +158,18 @@ class UnitDatabaseTest {
@Test
public void testPrefixlessUnits() {
final UnitDatabase database = new UnitDatabase();
-
+
database.addUnit("U", U);
database.addUnit("V", V);
database.addUnit("W", W);
-
+
assertTrue(database.containsUnitName("U"));
assertFalse(database.containsUnitName("Z"));
-
+
assertEquals(U, database.getUnit("U"));
- assertEquals(null, database.getUnit("Z"));
+ assertThrows(NoSuchElementException.class, () -> database.getUnit("Z"));
}
-
+
/**
* Test that unit expressions return the correct value.
*
@@ -184,30 +180,31 @@ class UnitDatabaseTest {
public void testUnitExpressions() {
// load units
final UnitDatabase database = new UnitDatabase();
-
+
database.addUnit("U", U);
database.addUnit("V", V);
database.addUnit("W", W);
database.addUnit("fj", J.times(5));
database.addUnit("ej", J.times(8));
-
+
database.addPrefix("A", A);
database.addPrefix("B", B);
database.addPrefix("C", C);
-
+
// first test - test prefixes and operations
- final Unit expected1 = J.withPrefix(A).withPrefix(B).withPrefix(C).withPrefix(C);
+ final Unit expected1 = J.withPrefix(A).withPrefix(B).withPrefix(C)
+ .withPrefix(C);
final Unit actual1 = database.getUnitFromExpression("ABV * CU^2 / W / W");
-
+
assertEquals(expected1, actual1);
-
+
// second test - test addition and subtraction
final Unit expected2 = J.times(58);
final Unit actual2 = database.getUnitFromExpression("2 fj + 6 ej");
-
+
assertEquals(expected2, actual2);
}
-
+
/**
* Tests both the unit name iterator and the name-unit entry iterator
*
@@ -218,59 +215,64 @@ class UnitDatabaseTest {
public void testUnitIterator() {
// load units
final UnitDatabase database = new UnitDatabase();
-
+
database.addUnit("J", J);
database.addUnit("K", K);
-
+
database.addPrefix("A", A);
database.addPrefix("B", B);
database.addPrefix("C", C);
-
+
final int NUM_UNITS = database.unitMapPrefixless().size();
final int NUM_PREFIXES = database.prefixMap().size();
-
- final Iterator<String> nameIterator = database.unitMap().keySet().iterator();
- final Iterator<Entry<String, Unit>> entryIterator = database.unitMap().entrySet().iterator();
-
+
+ final Iterator<String> nameIterator = database.unitMap().keySet()
+ .iterator();
+ final Iterator<Entry<String, Unit>> entryIterator = database.unitMap()
+ .entrySet().iterator();
+
int expectedLength = 1;
int unitsWithThisLengthSoFar = 0;
-
+
// loop 1000 times
for (int i = 0; i < 1000; i++) {
// expected length of next
- if (unitsWithThisLengthSoFar >= NUM_UNITS * (int) Math.pow(NUM_PREFIXES, expectedLength - 1)) {
+ if (unitsWithThisLengthSoFar >= NUM_UNITS
+ * (int) Math.pow(NUM_PREFIXES, expectedLength - 1)) {
expectedLength++;
unitsWithThisLengthSoFar = 0;
}
-
+
// test that stuff is valid
final String nextName = nameIterator.next();
final Unit nextUnit = database.getUnit(nextName);
final Entry<String, Unit> nextEntry = entryIterator.next();
-
+
assertEquals(expectedLength, nextName.length());
assertEquals(nextName, nextEntry.getKey());
assertEquals(nextUnit, nextEntry.getValue());
-
+
unitsWithThisLengthSoFar++;
}
-
+
// test toString for consistency
final String entryIteratorString = entryIterator.toString();
for (int i = 0; i < 3; i++) {
assertEquals(entryIteratorString, entryIterator.toString());
}
-
+
final String nameIteratorString = nameIterator.toString();
for (int i = 0; i < 3; i++) {
assertEquals(nameIteratorString, nameIterator.toString());
}
}
-
+
/**
- * Determine, given a unit name that could mean multiple things, which meaning is chosen.
+ * Determine, given a unit name that could mean multiple things, which
+ * meaning is chosen.
* <p>
- * For example, "ABCU" could mean "A-B-C-U", "AB-C-U", or "A-BC-U". In this case, "AB-C-U" is the correct choice.
+ * For example, "ABCU" could mean "A-B-C-U", "AB-C-U", or "A-BC-U". In this
+ * case, "AB-C-U" is the correct choice.
* </p>
*
* @since 2019-04-14
@@ -280,28 +282,28 @@ class UnitDatabaseTest {
public void testUnitPrefixCombinations() {
// load units
final UnitDatabase database = new UnitDatabase();
-
+
database.addUnit("J", J);
-
+
database.addPrefix("A", A);
database.addPrefix("B", B);
database.addPrefix("C", C);
database.addPrefix("AB", AB);
database.addPrefix("BC", BC);
-
+
// test 1 - AB-C-J vs A-BC-J vs A-B-C-J
final Unit expected1 = J.withPrefix(AB).withPrefix(C);
final Unit actual1 = database.getUnit("ABCJ");
-
+
assertEquals(expected1, actual1);
-
+
// test 2 - ABC-J vs AB-CJ vs AB-C-J
database.addUnit("CJ", J.times(13));
database.addPrefix("ABC", UnitPrefix.valueOf(17));
-
+
final Unit expected2 = J.times(17);
final Unit actual2 = database.getUnit("ABCJ");
-
+
assertEquals(expected2, actual2);
}
}
diff --git a/src/org/unitConverter/unit/UnitTest.java b/src/org/unitConverter/unit/UnitTest.java
index c078cfc..c0711dc 100644
--- a/src/org/unitConverter/unit/UnitTest.java
+++ b/src/org/unitConverter/unit/UnitTest.java
@@ -16,7 +16,10 @@
*/
package org.unitConverter.unit;
+import static org.junit.Assert.assertFalse;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.Random;
import java.util.concurrent.ThreadLocalRandom;
@@ -25,7 +28,8 @@ import org.junit.jupiter.api.Test;
import org.unitConverter.math.DecimalComparison;
/**
- * Testing the various Unit classes. This is NOT part of this program's public API.
+ * Testing the various Unit classes. This is NOT part of this program's public
+ * API.
*
* @author Adrien Hopkins
* @since 2018-12-22
@@ -34,69 +38,109 @@ import org.unitConverter.math.DecimalComparison;
class UnitTest {
/** A random number generator */
private static final Random rng = ThreadLocalRandom.current();
-
+
@Test
public void testAdditionAndSubtraction() {
- final LinearUnit inch = SI.METRE.times(0.0254);
- final LinearUnit foot = SI.METRE.times(0.3048);
-
+ final LinearUnit inch = SI.METRE.times(0.0254)
+ .withName(NameSymbol.of("inch", "in"));
+ final LinearUnit foot = SI.METRE.times(0.3048)
+ .withName(NameSymbol.of("foot", "ft"));
+
assertEquals(inch.plus(foot), SI.METRE.times(0.3302));
assertEquals(foot.minus(inch), SI.METRE.times(0.2794));
+
+ // test with LinearUnitValue
+ final LinearUnitValue value1 = LinearUnitValue.getExact(SI.METRE, 15);
+ final LinearUnitValue value2 = LinearUnitValue.getExact(foot, 120);
+ final LinearUnitValue value3 = LinearUnitValue.getExact(SI.METRE, 0.5);
+ final LinearUnitValue value4 = LinearUnitValue.getExact(SI.KILOGRAM, 60);
+
+ // make sure addition is done correctly
+ assertEquals(51.576, value1.plus(value2).getValueExact(), 0.001);
+ assertEquals(15.5, value1.plus(value3).getValueExact());
+ assertEquals(52.076, value1.plus(value2).plus(value3).getValueExact(),
+ 0.001);
+
+ // make sure addition uses the correct unit, and is still associative
+ // (ignoring floating-point rounding errors)
+ assertEquals(SI.METRE, value1.plus(value2).getUnit());
+ assertEquals(SI.METRE, value1.plus(value2).plus(value3).getUnit());
+ assertEquals(foot, value2.plus(value1).getUnit());
+ assertTrue(value1.plus(value2).equals(value2.plus(value1), true));
+
+ // make sure errors happen when they should
+ assertThrows(IllegalArgumentException.class, () -> value1.plus(value4));
}
-
+
@Test
public void testConversion() {
final LinearUnit metre = SI.METRE;
final Unit inch = metre.times(0.0254);
-
+
+ final UnitValue value = UnitValue.of(inch, 75);
+
assertEquals(1.9, inch.convertTo(metre, 75), 0.01);
-
+ assertEquals(1.9, value.convertTo(metre).getValue(), 0.01);
+
// try random stuff
for (int i = 0; i < 1000; i++) {
// initiate random values
- final double conversionFactor = rng.nextDouble() * 1000000;
- final double testValue = rng.nextDouble() * 1000000;
+ final double conversionFactor = UnitTest.rng.nextDouble() * 1000000;
+ final double testValue = UnitTest.rng.nextDouble() * 1000000;
final double expected = testValue * conversionFactor;
-
+
// test
final Unit unit = SI.METRE.times(conversionFactor);
final double actual = unit.convertToBase(testValue);
-
- assertEquals(actual, expected, expected * DecimalComparison.DOUBLE_EPSILON);
+
+ assertEquals(actual, expected,
+ expected * DecimalComparison.DOUBLE_EPSILON);
}
}
-
+
@Test
public void testEquals() {
final LinearUnit metre = SI.METRE;
final Unit meter = SI.BaseUnits.METRE.asLinearUnit();
-
+
assertEquals(metre, meter);
}
-
+
+ @Test
+ public void testIsMetric() {
+ final Unit metre = SI.METRE;
+ final Unit megasecond = SI.SECOND.withPrefix(SI.MEGA);
+ final Unit hour = SI.HOUR;
+
+ assertTrue(metre.isMetric());
+ assertTrue(megasecond.isMetric());
+ assertFalse(hour.isMetric());
+ }
+
@Test
public void testMultiplicationAndDivision() {
// test unit-times-unit multiplication
- final LinearUnit generatedJoule = SI.KILOGRAM.times(SI.METRE.toExponent(2)).dividedBy(SI.SECOND.toExponent(2));
+ final LinearUnit generatedJoule = SI.KILOGRAM
+ .times(SI.METRE.toExponent(2)).dividedBy(SI.SECOND.toExponent(2));
final LinearUnit actualJoule = SI.JOULE;
-
+
assertEquals(generatedJoule, actualJoule);
-
+
// test multiplication by conversion factors
final LinearUnit kilometre = SI.METRE.times(1000);
final LinearUnit hour = SI.SECOND.times(3600);
final LinearUnit generatedKPH = kilometre.dividedBy(hour);
-
+
final LinearUnit actualKPH = SI.METRE.dividedBy(SI.SECOND).dividedBy(3.6);
-
+
assertEquals(generatedKPH, actualKPH);
}
-
+
@Test
public void testPrefixes() {
final LinearUnit generatedKilometre = SI.METRE.withPrefix(SI.KILO);
final LinearUnit actualKilometre = SI.METRE.times(1000);
-
+
assertEquals(generatedKilometre, actualKilometre);
}
}
diff --git a/src/org/unitConverter/unit/UnitValue.java b/src/org/unitConverter/unit/UnitValue.java
new file mode 100644
index 0000000..c138332
--- /dev/null
+++ b/src/org/unitConverter/unit/UnitValue.java
@@ -0,0 +1,172 @@
+/**
+ * Copyright (C) 2019 Adrien Hopkins
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package org.unitConverter.unit;
+
+import java.util.Objects;
+import java.util.Optional;
+
+/**
+ * A value expressed in a unit.
+ *
+ * Unless otherwise indicated, all methods in this class throw a
+ * {@code NullPointerException} when an argument is null.
+ *
+ * @author Adrien Hopkins
+ * @since 2020-07-26
+ */
+public final class UnitValue {
+ /**
+ * Creates a {@code UnitValue} from a unit and the associated value.
+ *
+ * @param unit unit to use
+ * @param value value to use
+ * @return {@code UnitValue} instance
+ */
+ public static UnitValue of(Unit unit, double value) {
+ return new UnitValue(
+ Objects.requireNonNull(unit, "unit must not be null"), value);
+ }
+
+ private final Unit unit;
+ private final double value;
+
+ /**
+ * @param unit the unit being used
+ * @param value the value being represented
+ */
+ private UnitValue(Unit unit, Double value) {
+ this.unit = unit;
+ this.value = value;
+ }
+
+ /**
+ * @return true if this value can be converted to {@code other}.
+ * @since 2020-10-01
+ */
+ public final boolean canConvertTo(Unit other) {
+ return this.unit.canConvertTo(other);
+ }
+
+ /**
+ * @return true if this value can be converted to {@code other}.
+ * @since 2020-10-01
+ */
+ public final <W> boolean canConvertTo(Unitlike<W> other) {
+ return this.unit.canConvertTo(other);
+ }
+
+ /**
+ * Returns a UnitlikeValue that represents the same value expressed in a
+ * different unitlike form.
+ *
+ * @param other new unit to express value in
+ * @return value expressed in {@code other}
+ */
+ public final <U extends Unitlike<W>, W> UnitlikeValue<U, W> convertTo(
+ U other) {
+ return UnitlikeValue.of(other,
+ this.unit.convertTo(other, this.getValue()));
+ }
+
+ /**
+ * Returns a UnitValue that represents the same value expressed in a
+ * different unit
+ *
+ * @param other new unit to express value in
+ * @return value expressed in {@code other}
+ */
+ public final UnitValue convertTo(Unit other) {
+ return UnitValue.of(other,
+ this.getUnit().convertTo(other, this.getValue()));
+ }
+
+ /**
+ * Returns this unit value represented as a {@code LinearUnitValue} with this
+ * unit's base unit as the base.
+ *
+ * @param ns name and symbol for the base unit, use NameSymbol.EMPTY if not
+ * needed.
+ * @since 2020-09-29
+ */
+ public final LinearUnitValue convertToBase(NameSymbol ns) {
+ final LinearUnit base = LinearUnit.getBase(this.unit).withName(ns);
+ return this.convertToLinear(base);
+ }
+
+ /**
+ * @return a {@code LinearUnitValue} that is equivalent to this value. It
+ * will have zero uncertainty.
+ * @since 2020-09-29
+ */
+ public final LinearUnitValue convertToLinear(LinearUnit other) {
+ return LinearUnitValue.getExact(other,
+ this.getUnit().convertTo(other, this.getValue()));
+ }
+
+ /**
+ * Returns true if this and obj represent the same value, regardless of
+ * whether or not they are expressed in the same unit. So (1000 m).equals(1
+ * km) returns true.
+ */
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof UnitValue))
+ return false;
+ final UnitValue other = (UnitValue) obj;
+ return Objects.equals(this.getUnit().getBase(), other.getUnit().getBase())
+ && Double.doubleToLongBits(
+ this.getUnit().convertToBase(this.getValue())) == Double
+ .doubleToLongBits(
+ other.getUnit().convertToBase(other.getValue()));
+ }
+
+ /**
+ * @return the unit
+ * @since 2020-09-29
+ */
+ public final Unit getUnit() {
+ return this.unit;
+ }
+
+ /**
+ * @return the value
+ * @since 2020-09-29
+ */
+ public final double getValue() {
+ return this.value;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(this.getUnit().getBase(),
+ this.getUnit().convertFromBase(this.getValue()));
+ }
+
+ @Override
+ public String toString() {
+ final Optional<String> primaryName = this.getUnit().getPrimaryName();
+ final Optional<String> symbol = this.getUnit().getSymbol();
+ if (primaryName.isEmpty() && symbol.isEmpty()) {
+ final double baseValue = this.getUnit().convertToBase(this.getValue());
+ return String.format("%s unnamed unit (= %s %s)", this.getValue(),
+ baseValue, this.getUnit().getBase());
+ } else {
+ final String unitName = symbol.orElse(primaryName.get());
+ return this.getValue() + " " + unitName;
+ }
+ }
+}
diff --git a/src/org/unitConverter/unit/Unitlike.java b/src/org/unitConverter/unit/Unitlike.java
new file mode 100644
index 0000000..8077771
--- /dev/null
+++ b/src/org/unitConverter/unit/Unitlike.java
@@ -0,0 +1,260 @@
+/**
+ * Copyright (C) 2020 Adrien Hopkins
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package org.unitConverter.unit;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.function.DoubleFunction;
+import java.util.function.ToDoubleFunction;
+
+import org.unitConverter.math.ObjectProduct;
+
+/**
+ * An object that can convert a value between multiple forms (instances of the
+ * object); like a unit but the "converted value" can be any type.
+ *
+ * @since 2020-09-07
+ */
+public abstract class Unitlike<V> implements Nameable {
+ /**
+ * Returns a unitlike form from its base and the functions it uses to convert
+ * to and from its base.
+ *
+ * @param base unitlike form's base
+ * @param converterFrom function that accepts a value expressed in the
+ * unitlike form's base and returns that value expressed
+ * in this unitlike form.
+ * @param converterTo function that accepts a value expressed in the
+ * unitlike form and returns that value expressed in the
+ * unit's base.
+ * @return a unitlike form that uses the provided functions to convert.
+ * @since 2020-09-07
+ * @throws NullPointerException if any argument is null
+ */
+ public static final <W> Unitlike<W> fromConversionFunctions(
+ final ObjectProduct<BaseUnit> base,
+ final DoubleFunction<W> converterFrom,
+ final ToDoubleFunction<W> converterTo) {
+ return new FunctionalUnitlike<>(base, NameSymbol.EMPTY, converterFrom,
+ converterTo);
+ }
+
+ /**
+ * Returns a unitlike form from its base and the functions it uses to convert
+ * to and from its base.
+ *
+ * @param base unitlike form's base
+ * @param converterFrom function that accepts a value expressed in the
+ * unitlike form's base and returns that value expressed
+ * in this unitlike form.
+ * @param converterTo function that accepts a value expressed in the
+ * unitlike form and returns that value expressed in the
+ * unit's base.
+ * @param ns names and symbol of unit
+ * @return a unitlike form that uses the provided functions to convert.
+ * @since 2020-09-07
+ * @throws NullPointerException if any argument is null
+ */
+ public static final <W> Unitlike<W> fromConversionFunctions(
+ final ObjectProduct<BaseUnit> base,
+ final DoubleFunction<W> converterFrom,
+ final ToDoubleFunction<W> converterTo, final NameSymbol ns) {
+ return new FunctionalUnitlike<>(base, ns, converterFrom, converterTo);
+ }
+
+ /**
+ * The combination of units that this unit is based on.
+ *
+ * @since 2019-10-16
+ */
+ private final ObjectProduct<BaseUnit> unitBase;
+
+ /**
+ * This unit's name(s) and symbol
+ *
+ * @since 2020-09-07
+ */
+ private final NameSymbol nameSymbol;
+
+ /**
+ * Cache storing the result of getDimension()
+ *
+ * @since 2019-10-16
+ */
+ private transient ObjectProduct<BaseDimension> dimension = null;
+
+ /**
+ * @param unitBase
+ * @since 2020-09-07
+ */
+ protected Unitlike(ObjectProduct<BaseUnit> unitBase, NameSymbol ns) {
+ this.unitBase = Objects.requireNonNull(unitBase,
+ "unitBase may not be null");
+ this.nameSymbol = Objects.requireNonNull(ns, "ns may not be null");
+ }
+
+ /**
+ * Checks if a value expressed in this unitlike form can be converted to a
+ * value expressed in {@code other}
+ *
+ * @param other unit or unitlike form to test with
+ * @return true if they are compatible
+ * @since 2019-01-13
+ * @since v0.1.0
+ * @throws NullPointerException if other is null
+ */
+ public final boolean canConvertTo(final Unit other) {
+ Objects.requireNonNull(other, "other must not be null.");
+ return Objects.equals(this.getBase(), other.getBase());
+ }
+
+ /**
+ * Checks if a value expressed in this unitlike form can be converted to a
+ * value expressed in {@code other}
+ *
+ * @param other unit or unitlike form to test with
+ * @return true if they are compatible
+ * @since 2019-01-13
+ * @since v0.1.0
+ * @throws NullPointerException if other is null
+ */
+ public final <W> boolean canConvertTo(final Unitlike<W> other) {
+ Objects.requireNonNull(other, "other must not be null.");
+ return Objects.equals(this.getBase(), other.getBase());
+ }
+
+ protected abstract V convertFromBase(double value);
+
+ /**
+ * Converts a value expressed in this unitlike form to a value expressed in
+ * {@code other}.
+ *
+ * @implSpec If conversion is possible, this implementation returns
+ * {@code other.convertFromBase(this.convertToBase(value))}.
+ * Therefore, overriding either of those methods will change the
+ * output of this method.
+ *
+ * @param other unit to convert to
+ * @param value value to convert
+ * @return converted value
+ * @since 2019-05-22
+ * @throws IllegalArgumentException if {@code other} is incompatible for
+ * conversion with this unitlike form (as
+ * tested by {@link Unit#canConvertTo}).
+ * @throws NullPointerException if other is null
+ */
+ public final double convertTo(final Unit other, final V value) {
+ Objects.requireNonNull(other, "other must not be null.");
+ if (this.canConvertTo(other))
+ return other.convertFromBase(this.convertToBase(value));
+ else
+ throw new IllegalArgumentException(
+ String.format("Cannot convert from %s to %s.", this, other));
+ }
+
+ /**
+ * Converts a value expressed in this unitlike form to a value expressed in
+ * {@code other}.
+ *
+ * @implSpec If conversion is possible, this implementation returns
+ * {@code other.convertFromBase(this.convertToBase(value))}.
+ * Therefore, overriding either of those methods will change the
+ * output of this method.
+ *
+ * @param other unitlike form to convert to
+ * @param value value to convert
+ * @param <W> type of value to convert to
+ * @return converted value
+ * @since 2020-09-07
+ * @throws IllegalArgumentException if {@code other} is incompatible for
+ * conversion with this unitlike form (as
+ * tested by {@link Unit#canConvertTo}).
+ * @throws NullPointerException if other is null
+ */
+ public final <W> W convertTo(final Unitlike<W> other, final V value) {
+ Objects.requireNonNull(other, "other must not be null.");
+ if (this.canConvertTo(other))
+ return other.convertFromBase(this.convertToBase(value));
+ else
+ throw new IllegalArgumentException(
+ String.format("Cannot convert from %s to %s.", this, other));
+ }
+
+ protected abstract double convertToBase(V value);
+
+ /**
+ * @return combination of units that this unit is based on
+ * @since 2018-12-22
+ * @since v0.1.0
+ */
+ public final ObjectProduct<BaseUnit> getBase() {
+ return this.unitBase;
+ }
+
+ /**
+ * @return dimension measured by this unit
+ * @since 2018-12-22
+ * @since v0.1.0
+ */
+ public final ObjectProduct<BaseDimension> getDimension() {
+ if (this.dimension == null) {
+ final Map<BaseUnit, Integer> mapping = this.unitBase.exponentMap();
+ final Map<BaseDimension, Integer> dimensionMap = new HashMap<>();
+
+ for (final BaseUnit key : mapping.keySet()) {
+ dimensionMap.put(key.getBaseDimension(), mapping.get(key));
+ }
+
+ this.dimension = ObjectProduct.fromExponentMapping(dimensionMap);
+ }
+ return this.dimension;
+ }
+
+ /**
+ * @return the nameSymbol
+ * @since 2020-09-07
+ */
+ @Override
+ public final NameSymbol getNameSymbol() {
+ return this.nameSymbol;
+ }
+
+ @Override
+ public String toString() {
+ return this.getPrimaryName().orElse("Unnamed unitlike form")
+ + (this.getSymbol().isPresent()
+ ? String.format(" (%s)", this.getSymbol().get())
+ : "")
+ + ", derived from "
+ + this.getBase().toString(u -> u.getSymbol().get())
+ + (this.getOtherNames().isEmpty() ? ""
+ : ", also called " + String.join(", ", this.getOtherNames()));
+ }
+
+ /**
+ * @param ns name(s) and symbol to use
+ * @return a copy of this unitlike form with provided name(s) and symbol
+ * @since 2020-09-07
+ * @throws NullPointerException if ns is null
+ */
+ public Unitlike<V> withName(final NameSymbol ns) {
+ return fromConversionFunctions(this.getBase(), this::convertFromBase,
+ this::convertToBase,
+ Objects.requireNonNull(ns, "ns must not be null."));
+ }
+}
diff --git a/src/org/unitConverter/unit/UnitlikeValue.java b/src/org/unitConverter/unit/UnitlikeValue.java
new file mode 100644
index 0000000..79201c4
--- /dev/null
+++ b/src/org/unitConverter/unit/UnitlikeValue.java
@@ -0,0 +1,174 @@
+/**
+ * Copyright (C) 2020 Adrien Hopkins
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package org.unitConverter.unit;
+
+import java.util.Optional;
+
+/**
+ *
+ * @since 2020-09-07
+ */
+final class UnitlikeValue<T extends Unitlike<V>, V> {
+ /**
+ * Gets a {@code UnitlikeValue<V>}.
+ *
+ * @since 2020-10-02
+ */
+ public static <T extends Unitlike<V>, V> UnitlikeValue<T, V> of(T unitlike,
+ V value) {
+ return new UnitlikeValue<>(unitlike, value);
+ }
+
+ private final T unitlike;
+ private final V value;
+
+ /**
+ * @param unitlike
+ * @param value
+ * @since 2020-09-07
+ */
+ private UnitlikeValue(T unitlike, V value) {
+ this.unitlike = unitlike;
+ this.value = value;
+ }
+
+ /**
+ * @return true if this value can be converted to {@code other}.
+ * @since 2020-10-01
+ */
+ public final boolean canConvertTo(Unit other) {
+ return this.unitlike.canConvertTo(other);
+ }
+
+ /**
+ * @return true if this value can be converted to {@code other}.
+ * @since 2020-10-01
+ */
+ public final <W> boolean canConvertTo(Unitlike<W> other) {
+ return this.unitlike.canConvertTo(other);
+ }
+
+ /**
+ * Returns a UnitlikeValue that represents the same value expressed in a
+ * different unitlike form.
+ *
+ * @param other new unit to express value in
+ * @return value expressed in {@code other}
+ */
+ public final <U extends Unitlike<W>, W> UnitlikeValue<U, W> convertTo(
+ U other) {
+ return UnitlikeValue.of(other,
+ this.unitlike.convertTo(other, this.getValue()));
+ }
+
+ /**
+ * Returns a UnitValue that represents the same value expressed in a
+ * different unit
+ *
+ * @param other new unit to express value in
+ * @return value expressed in {@code other}
+ */
+ public final UnitValue convertTo(Unit other) {
+ return UnitValue.of(other,
+ this.unitlike.convertTo(other, this.getValue()));
+ }
+
+ /**
+ * Returns this unit value represented as a {@code LinearUnitValue} with this
+ * unit's base unit as the base.
+ *
+ * @param ns name and symbol for the base unit, use NameSymbol.EMPTY if not
+ * needed.
+ * @since 2020-09-29
+ */
+ public final LinearUnitValue convertToBase(NameSymbol ns) {
+ final LinearUnit base = LinearUnit.getBase(this.unitlike).withName(ns);
+ return this.convertToLinear(base);
+ }
+
+ /**
+ * @return a {@code LinearUnitValue} that is equivalent to this value. It
+ * will have zero uncertainty.
+ * @since 2020-09-29
+ */
+ public final LinearUnitValue convertToLinear(LinearUnit other) {
+ return LinearUnitValue.getExact(other,
+ this.getUnitlike().convertTo(other, this.getValue()));
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (!(obj instanceof UnitlikeValue))
+ return false;
+ final UnitlikeValue<?, ?> other = (UnitlikeValue<?, ?>) obj;
+ if (this.getUnitlike() == null) {
+ if (other.getUnitlike() != null)
+ return false;
+ } else if (!this.getUnitlike().equals(other.getUnitlike()))
+ return false;
+ if (this.getValue() == null) {
+ if (other.getValue() != null)
+ return false;
+ } else if (!this.getValue().equals(other.getValue()))
+ return false;
+ return true;
+ }
+
+ /**
+ * @return the unitlike
+ * @since 2020-09-29
+ */
+ public final Unitlike<V> getUnitlike() {
+ return this.unitlike;
+ }
+
+ /**
+ * @return the value
+ * @since 2020-09-29
+ */
+ public final V getValue() {
+ return this.value;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result
+ + (this.getUnitlike() == null ? 0 : this.getUnitlike().hashCode());
+ result = prime * result
+ + (this.getValue() == null ? 0 : this.getValue().hashCode());
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ final Optional<String> primaryName = this.getUnitlike().getPrimaryName();
+ final Optional<String> symbol = this.getUnitlike().getSymbol();
+ if (primaryName.isEmpty() && symbol.isEmpty()) {
+ final double baseValue = this.getUnitlike()
+ .convertToBase(this.getValue());
+ return String.format("%s unnamed unit (= %s %s)", this.getValue(),
+ baseValue, this.getUnitlike().getBase());
+ } else {
+ final String unitName = symbol.orElse(primaryName.get());
+ return this.getValue() + " " + unitName;
+ }
+ }
+}
diff --git a/unitsfile.txt b/unitsfile.txt
index a067d14..eafe885 100644
--- a/unitsfile.txt
+++ b/unitsfile.txt
@@ -157,8 +157,11 @@ hour 3600 second
h hour
day 86400 second
d day
+week 7 day
+wk week
julianyear 365.25 day
gregorianyear 365.2425 day
+gregorianmonth gregorianyear / 12
# Other non-SI "metric" units
litre 0.001 m^3
@@ -180,7 +183,7 @@ waterdensity kilogram / litre
# Imperial length units
foot 0.3048 m
ft foot
-inch 1 / 12 foot
+inch foot / 12
in inch
yard 3 foot
yd yard