summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.org14
-rw-r--r--README.org2
-rw-r--r--build.gradle2
-rw-r--r--src/main/java/sevenUnits/ProgramInfo.java12
-rw-r--r--src/main/java/sevenUnits/converterGUI/SevenUnitsGUI.java1505
-rw-r--r--src/main/java/sevenUnits/unit/BaseDimension.java50
-rw-r--r--src/main/java/sevenUnits/unit/BaseUnit.java2
-rw-r--r--src/main/java/sevenUnits/unit/BritishImperial.java4
-rw-r--r--src/main/java/sevenUnits/unit/FunctionalUnit.java1
-rw-r--r--src/main/java/sevenUnits/unit/FunctionalUnitlike.java1
-rw-r--r--src/main/java/sevenUnits/unit/LinearUnit.java22
-rw-r--r--src/main/java/sevenUnits/unit/LinearUnitValue.java10
-rw-r--r--src/main/java/sevenUnits/unit/Metric.java133
-rw-r--r--src/main/java/sevenUnits/unit/MultiUnit.java1
-rw-r--r--src/main/java/sevenUnits/unit/Unit.java44
-rw-r--r--src/main/java/sevenUnits/unit/UnitDatabase.java67
-rw-r--r--src/main/java/sevenUnits/unit/UnitPrefix.java136
-rw-r--r--src/main/java/sevenUnits/unit/UnitType.java58
-rw-r--r--src/main/java/sevenUnits/unit/UnitValue.java2
-rw-r--r--src/main/java/sevenUnits/unit/Unitlike.java2
-rw-r--r--src/main/java/sevenUnits/unit/UnitlikeValue.java2
-rw-r--r--src/main/java/sevenUnits/utils/NameSymbol.java (renamed from src/main/java/sevenUnits/unit/NameSymbol.java)32
-rw-r--r--src/main/java/sevenUnits/utils/Nameable.java (renamed from src/main/java/sevenUnits/unit/Nameable.java)22
-rw-r--r--src/main/java/sevenUnits/utils/ObjectProduct.java48
-rw-r--r--src/main/java/sevenUnits/utils/SemanticVersionNumber.java691
-rw-r--r--src/main/java/sevenUnits/utils/UncertainDouble.java24
-rw-r--r--src/main/java/sevenUnitsGUI/DefaultPrefixRepetitionRule.java (renamed from src/main/java/sevenUnits/converterGUI/DefaultPrefixRepetitionRule.java)4
-rw-r--r--src/main/java/sevenUnitsGUI/DelegateListModel.java (renamed from src/main/java/sevenUnits/converterGUI/DelegateListModel.java)2
-rw-r--r--src/main/java/sevenUnitsGUI/ExpressionConversionView.java45
-rw-r--r--src/main/java/sevenUnitsGUI/FilterComparator.java (renamed from src/main/java/sevenUnits/converterGUI/FilterComparator.java)63
-rw-r--r--src/main/java/sevenUnitsGUI/GridBagBuilder.java (renamed from src/main/java/sevenUnits/converterGUI/GridBagBuilder.java)2
-rw-r--r--src/main/java/sevenUnitsGUI/Main.java34
-rw-r--r--src/main/java/sevenUnitsGUI/MutablePredicate.java (renamed from src/main/java/sevenUnits/converterGUI/MutablePredicate.java)2
-rw-r--r--src/main/java/sevenUnitsGUI/Presenter.java782
-rw-r--r--src/main/java/sevenUnitsGUI/SearchBoxList.java (renamed from src/main/java/sevenUnits/converterGUI/SearchBoxList.java)75
-rw-r--r--src/main/java/sevenUnitsGUI/StandardDisplayRules.java246
-rw-r--r--src/main/java/sevenUnitsGUI/TabbedView.java797
-rw-r--r--src/main/java/sevenUnitsGUI/UnitConversionRecord.java199
-rw-r--r--src/main/java/sevenUnitsGUI/UnitConversionView.java108
-rw-r--r--src/main/java/sevenUnitsGUI/View.java105
-rw-r--r--src/main/java/sevenUnitsGUI/ViewBot.java507
-rw-r--r--src/main/java/sevenUnitsGUI/package-info.java (renamed from src/main/java/sevenUnits/converterGUI/package-info.java)9
-rw-r--r--src/main/resources/about.txt2
-rw-r--r--src/main/resources/dimensionfile.txt8
-rw-r--r--src/test/java/sevenUnits/unit/UnitDatabaseTest.java3
-rw-r--r--src/test/java/sevenUnits/unit/UnitTest.java15
-rw-r--r--src/test/java/sevenUnits/utils/SemanticVersionTest.java399
-rw-r--r--src/test/java/sevenUnits/utils/UncertainDoubleTest.java11
-rw-r--r--src/test/java/sevenUnitsGUI/PresenterTest.java356
-rw-r--r--src/test/resources/test-settings.txt4
50 files changed, 4847 insertions, 1818 deletions
diff --git a/CHANGELOG.org b/CHANGELOG.org
index 5630737..509553b 100644
--- a/CHANGELOG.org
+++ b/CHANGELOG.org
@@ -1,5 +1,19 @@
* Changelog
All notable changes in this project will be shown in this file.
+** Unreleased
+*** Added
+ - Added tests for the GUI
+ - Added an object for the version numbers (SemanticVersionNumber)
+ - Added some toString methods to NameSymbol and Nameable
+*** Changed
+ - Rewrote the GUI code internally using an MVP model to make it easier to maintain and improve
+ - BaseDimension is now Nameable. As a consequence, its name and symbol return Optional<String> instead of String, even though they will always succeed.
+ - The UnitDatabase's units, prefixes and dimensions are now always named
+ - The toString method of the common unit classes is now simpler. Alternate toString functions that describe the full unit are provided.
+ - UncertainDouble and LinearUnitValue accept a RoundingMode in their complicated toString functions.
+ - Rounding rules are now in their own classes
+ - The "Show Duplicates" setting now affects the prefix viewer in addition to units
+ - Tweaked the look of the unit and expression conversion sections of the view
** v0.3.2 - [2021-12-02 Thu]
*** Added
- Added lots more tests for the backend and utilities
diff --git a/README.org b/README.org
index 2b4ffa0..f891bdc 100644
--- a/README.org
+++ b/README.org
@@ -1,4 +1,4 @@
-* 7Units v0.3.2
+* 7Units v0.4.0a1
(this project uses Semantic Versioning)
** What is it?
This is a unit converter, which allows you to convert between different units, and includes a GUI which can read unit data from a file (using some unit math) and convert between units that you type in, and has a unit and prefix viewer to check the units that have been loaded in.
diff --git a/build.gradle b/build.gradle
index 4484e9d..8b90060 100644
--- a/build.gradle
+++ b/build.gradle
@@ -8,7 +8,7 @@ java {
sourceCompatibility = JavaVersion.VERSION_11
}
-mainClassName = "sevenUnits.converterGUI.SevenUnitsGUI"
+mainClassName = "sevenUnitsGUI.Main"
repositories {
mavenCentral()
diff --git a/src/main/java/sevenUnits/ProgramInfo.java b/src/main/java/sevenUnits/ProgramInfo.java
index 31e43c7..f32d2c7 100644
--- a/src/main/java/sevenUnits/ProgramInfo.java
+++ b/src/main/java/sevenUnits/ProgramInfo.java
@@ -16,6 +16,8 @@
*/
package sevenUnits;
+import sevenUnits.utils.SemanticVersionNumber;
+
/**
* Information about 7Units
*
@@ -24,8 +26,14 @@ package sevenUnits;
*/
public final class ProgramInfo {
- public static final String VERSION = "0.3.2";
+ /** The version number (0.4.0-alpha+dev) */
+ public static final SemanticVersionNumber VERSION = SemanticVersionNumber
+ .preRelease(0, 4, 0, "alpha", 1);
- private ProgramInfo() {}
+ private ProgramInfo() {
+ // this class is only for static variables, you shouldn't be able to
+ // construct an instance
+ throw new AssertionError();
+ }
}
diff --git a/src/main/java/sevenUnits/converterGUI/SevenUnitsGUI.java b/src/main/java/sevenUnits/converterGUI/SevenUnitsGUI.java
deleted file mode 100644
index bfd5974..0000000
--- a/src/main/java/sevenUnits/converterGUI/SevenUnitsGUI.java
+++ /dev/null
@@ -1,1505 +0,0 @@
-/**
- * Copyright (C) 2018-2021 Adrien Hopkins
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <https://www.gnu.org/licenses/>.
- */
-package sevenUnits.converterGUI;
-
-import java.awt.BorderLayout;
-import java.awt.GridBagConstraints;
-import java.awt.GridBagLayout;
-import java.awt.GridLayout;
-import java.awt.event.KeyEvent;
-import java.io.BufferedWriter;
-import java.io.IOException;
-import java.io.InputStream;
-import java.math.BigDecimal;
-import java.math.MathContext;
-import java.math.RoundingMode;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.text.DecimalFormat;
-import java.text.NumberFormat;
-import java.text.ParseException;
-import java.util.ArrayList;
-import java.util.Comparator;
-import java.util.HashSet;
-import java.util.List;
-import java.util.NoSuchElementException;
-import java.util.Scanner;
-import java.util.Set;
-import java.util.function.Predicate;
-import java.util.stream.Collectors;
-
-import javax.swing.BorderFactory;
-import javax.swing.BoxLayout;
-import javax.swing.ButtonGroup;
-import javax.swing.JButton;
-import javax.swing.JCheckBox;
-import javax.swing.JComboBox;
-import javax.swing.JFormattedTextField;
-import javax.swing.JFrame;
-import javax.swing.JLabel;
-import javax.swing.JOptionPane;
-import javax.swing.JPanel;
-import javax.swing.JRadioButton;
-import javax.swing.JScrollPane;
-import javax.swing.JSlider;
-import javax.swing.JTabbedPane;
-import javax.swing.JTextArea;
-import javax.swing.JTextField;
-import javax.swing.UIManager;
-import javax.swing.UnsupportedLookAndFeelException;
-import javax.swing.WindowConstants;
-import javax.swing.border.TitledBorder;
-
-import sevenUnits.ProgramInfo;
-import sevenUnits.unit.BaseDimension;
-import sevenUnits.unit.BritishImperial;
-import sevenUnits.unit.LinearUnit;
-import sevenUnits.unit.LinearUnitValue;
-import sevenUnits.unit.NameSymbol;
-import sevenUnits.unit.Metric;
-import sevenUnits.unit.Unit;
-import sevenUnits.unit.UnitDatabase;
-import sevenUnits.unit.UnitPrefix;
-import sevenUnits.unit.UnitValue;
-import sevenUnits.utils.ConditionalExistenceCollections;
-import sevenUnits.utils.ObjectProduct;
-
-/**
- * @author Adrien Hopkins
- * @since 2018-12-27
- * @since v0.1.0
- */
-final class SevenUnitsGUI {
- /**
- * A tab in the View.
- */
- private enum Pane {
- UNIT_CONVERTER, EXPRESSION_CONVERTER, UNIT_VIEWER, PREFIX_VIEWER, ABOUT,
- SETTINGS;
- }
-
- private static class Presenter {
- /** The default place where settings are stored. */
- private static final String DEFAULT_SETTINGS_FILEPATH = "settings.txt";
- /** The default place where units are stored. */
- private static final String DEFAULT_UNITS_FILEPATH = "/unitsfile.txt";
- /** The default place where dimensions are stored. */
- private static final String DEFAULT_DIMENSIONS_FILEPATH = "/dimensionfile.txt";
- /** The default place where exceptions are stored. */
- private static final String DEFAULT_EXCEPTIONS_FILEPATH = "/metric_exceptions.txt";
-
- /**
- * Adds default units and dimensions to a database.
- *
- * @param database database to add to
- * @since 2019-04-14
- * @since v0.2.0
- */
- private static void addDefaults(final UnitDatabase database) {
- database.addUnit("metre", Metric.METRE);
- database.addUnit("kilogram", Metric.KILOGRAM);
- database.addUnit("gram", Metric.KILOGRAM.dividedBy(1000));
- database.addUnit("second", Metric.SECOND);
- database.addUnit("ampere", Metric.AMPERE);
- database.addUnit("kelvin", Metric.KELVIN);
- database.addUnit("mole", Metric.MOLE);
- database.addUnit("candela", Metric.CANDELA);
- database.addUnit("bit", Metric.BIT);
- database.addUnit("unit", Metric.ONE);
- // nonlinear units - must be loaded manually
- database.addUnit("tempCelsius", Metric.CELSIUS);
- database.addUnit("tempFahrenheit", BritishImperial.FAHRENHEIT);
-
- // load initial dimensions
- database.addDimension("LENGTH", Metric.Dimensions.LENGTH);
- database.addDimension("MASS", Metric.Dimensions.MASS);
- database.addDimension("TIME", Metric.Dimensions.TIME);
- database.addDimension("TEMPERATURE", Metric.Dimensions.TEMPERATURE);
- }
-
- /**
- * Gets the text of a resource file as a set of strings (each one is one
- * line of the text).
- *
- * @param filename filename to get resource from
- * @return contents of file
- * @since 2021-03-27
- */
- public static final List<String> getLinesFromResource(String filename) {
- final List<String> lines = new ArrayList<>();
-
- try (InputStream stream = inputStream(filename);
- Scanner scanner = new Scanner(stream)) {
- while (scanner.hasNextLine()) {
- lines.add(scanner.nextLine());
- }
- } catch (final IOException e) {
- throw new AssertionError(
- "Error occurred while loading file " + filename, e);
- }
-
- return lines;
- }
-
- /**
- * Gets an input stream for a resource file.
- *
- * @param filepath file to use as resource
- * @return obtained Path
- * @since 2021-03-27
- */
- private static final InputStream inputStream(String filepath) {
- return SevenUnitsGUI.class.getResourceAsStream(filepath);
- }
-
- /**
- * @return {@code line} with any comments removed.
- * @since 2021-03-13
- */
- private static final String withoutComments(String line) {
- final int index = line.indexOf('#');
- return index == -1 ? line : line.substring(index);
- }
-
- /** The presenter's associated view. */
- private final View view;
-
- /** The units known by the program. */
- private final UnitDatabase database;
-
- /** The names of all of the units */
- private final List<String> unitNames;
-
- /** The names of all of the prefixes */
- private final List<String> prefixNames;
-
- /** The names of all of the dimensions */
- private final List<String> dimensionNames;
-
- /** Unit names that are ignored by the metric-only/imperial-only filter */
- private final Set<String> metricExceptions;
-
- private final Comparator<String> prefixNameComparator;
-
- /** A boolean remembering whether or not one-way conversion is on */
- private boolean oneWay = true;
- /** The prefix rule */
- private DefaultPrefixRepetitionRule prefixRule = null;
-
- // conditions for existence of From and To entries
- // used for one-way conversion
- private final MutablePredicate<String> fromExistenceCondition = new MutablePredicate<>(
- s -> true);
-
- private final MutablePredicate<String> toExistenceCondition = new MutablePredicate<>(
- s -> true);
-
- /*
- * Rounding-related settings. I am using my own system, and not
- * MathContext, because MathContext does not support decimal place based
- * or scientific rounding, only significant digit based rounding.
- */
- private int precision = 6;
-
- private RoundingType roundingType = RoundingType.SIGNIFICANT_DIGITS;
-
- // The "include duplicate units" setting
- private boolean includeDuplicateUnits = true;
-
- /**
- * Creates the presenter.
- *
- * @param view presenter's associated view
- * @since 2018-12-27
- * @since v0.1.0
- */
- Presenter(final View view) {
- this.view = view;
-
- // load initial units
- this.database = new UnitDatabase(
- DefaultPrefixRepetitionRule.NO_RESTRICTION);
- Presenter.addDefaults(this.database);
-
- // load units and prefixes
- try (final InputStream units = inputStream(DEFAULT_UNITS_FILEPATH)) {
- this.database.loadUnitsFromStream(units);
- } catch (final IOException e) {
- throw new AssertionError("Loading of unitsfile.txt failed.", e);
- }
-
- // load dimensions
- try (final InputStream dimensions = inputStream(
- DEFAULT_DIMENSIONS_FILEPATH)) {
- this.database.loadDimensionsFromStream(dimensions);
- } catch (final IOException e) {
- throw new AssertionError("Loading of dimensionfile.txt failed.", e);
- }
-
- // load metric exceptions
- try {
- this.metricExceptions = new HashSet<>();
- try (InputStream exceptions = inputStream(
- DEFAULT_EXCEPTIONS_FILEPATH);
- Scanner scanner = new Scanner(exceptions)) {
- while (scanner.hasNextLine()) {
- final String line = Presenter
- .withoutComments(scanner.nextLine());
- if (!line.isBlank()) {
- this.metricExceptions.add(line);
- }
- }
- }
- } catch (final IOException e) {
- throw new AssertionError("Loading of metric_exceptions.txt failed.",
- e);
- }
-
- // load settings - requires database to exist
- if (Files.exists(this.getSettingsFile())) {
- this.loadSettings();
- }
-
- // a comparator that can be used to compare prefix names
- // any name that does not exist is less than a name that does.
- // otherwise, they are compared by value
- this.prefixNameComparator = (o1, o2) -> {
- if (!Presenter.this.database.containsPrefixName(o1))
- return -1;
- else if (!Presenter.this.database.containsPrefixName(o2))
- return 1;
-
- final UnitPrefix p1 = Presenter.this.database.getPrefix(o1);
- final UnitPrefix p2 = Presenter.this.database.getPrefix(o2);
-
- if (p1.getMultiplier() < p2.getMultiplier())
- return -1;
- else if (p1.getMultiplier() > p2.getMultiplier())
- return 1;
-
- return o1.compareTo(o2);
- };
-
- this.unitNames = new ArrayList<>(
- this.database.unitMapPrefixless(true).keySet());
- this.unitNames.sort(null); // sorts it using Comparable
-
- this.prefixNames = new ArrayList<>(this.database.prefixMap().keySet());
- this.prefixNames.sort(this.prefixNameComparator); // sorts it using my
- // comparator
-
- this.dimensionNames = new DelegateListModel<>(
- new ArrayList<>(this.database.dimensionMap().keySet()));
- this.dimensionNames.sort(null); // sorts it using Comparable
-
- // a Predicate that returns true iff the argument is a full base unit
- final Predicate<Unit> isFullBase = unit -> unit instanceof LinearUnit
- && ((LinearUnit) unit).isBase();
-
- // print out unit counts
- System.out.printf(
- "Successfully loaded %d units with %d unit names (%d base units).%n",
- this.database.unitMapPrefixless(false).size(),
- this.database.unitMapPrefixless(true).size(),
- this.database.unitMapPrefixless(false).values().stream()
- .filter(isFullBase).count());
- }
-
- /**
- * Converts in the dimension-based converter
- *
- * @since 2019-04-13
- * @since v0.2.0
- */
- public final void convertDimensionBased() {
- final String fromSelection = this.view.getFromSelection();
- if (fromSelection == null) {
- this.view.showErrorDialog("Error",
- "No unit selected in From field");
- return;
- }
- final String toSelection = this.view.getToSelection();
- if (toSelection == null) {
- this.view.showErrorDialog("Error", "No unit selected in To field");
- return;
- }
-
- final Unit from = this.database.getUnit(fromSelection);
- final Unit to = this.database.getUnit(toSelection)
- .withName(NameSymbol.ofName(toSelection));
-
- final UnitValue beforeValue;
- try {
- beforeValue = UnitValue.of(from,
- this.view.getDimensionConverterInput());
- } catch (final ParseException e) {
- this.view.showErrorDialog("Error",
- "Error in parsing: " + e.getMessage());
- return;
- }
- final UnitValue value = beforeValue.convertTo(to);
-
- final String output = this.getRoundedString(value);
-
- this.view.setDimensionConverterOutputText(
- String.format("%s = %s", beforeValue, output));
- }
-
- /**
- * Runs whenever the convert button is pressed.
- *
- * <p>
- * Reads and parses a unit expression from the from and to boxes, then
- * converts {@code from} to {@code to}. Any errors are shown in
- * JOptionPanes.
- * </p>
- *
- * @since 2019-01-26
- * @since v0.1.0
- */
- public final void convertExpressions() {
- final String fromUnitString = this.view.getFromText();
- final String toUnitString = this.view.getToText();
-
- if (fromUnitString.isEmpty()) {
- this.view.showErrorDialog("Parse Error",
- "Please enter a unit expression in the From: box.");
- return;
- }
- if (toUnitString.isEmpty()) {
- this.view.showErrorDialog("Parse Error",
- "Please enter a unit expression in the To: box.");
- return;
- }
-
- final LinearUnitValue from;
- final Unit to;
- try {
- from = this.database.evaluateUnitExpression(fromUnitString);
- } catch (final IllegalArgumentException | NoSuchElementException e) {
- this.view.showErrorDialog("Parse Error",
- "Could not recognize text in From entry: " + e.getMessage());
- return;
- }
- try {
- to = this.database.getUnitFromExpression(toUnitString);
- } catch (final IllegalArgumentException | NoSuchElementException e) {
- this.view.showErrorDialog("Parse Error",
- "Could not recognize text in To entry: " + e.getMessage());
- return;
- }
-
- if (to instanceof LinearUnit) {
- // convert to LinearUnitValue
- final LinearUnitValue from2;
- final LinearUnit to2 = ((LinearUnit) to)
- .withName(NameSymbol.ofName(toUnitString));
- final boolean useSlash;
-
- if (from.canConvertTo(to2)) {
- from2 = from;
- useSlash = false;
- } else if (LinearUnitValue.ONE.dividedBy(from).canConvertTo(to2)) {
- from2 = LinearUnitValue.ONE.dividedBy(from);
- useSlash = true;
- } else {
- // if I can't convert, leave
- this.view.showErrorDialog("Conversion Error",
- String.format("Cannot convert between %s and %s",
- fromUnitString, toUnitString));
- return;
- }
-
- final LinearUnitValue converted = from2.convertTo(to2);
- this.view.setExpressionConverterOutputText((useSlash ? "1 / " : "")
- + String.format("%s = %s", fromUnitString,
- this.getRoundedString(converted, false)));
- return;
- } else {
- // convert to UnitValue
- final UnitValue from2 = from.asUnitValue();
- if (from2.canConvertTo(to)) {
- final UnitValue converted = from2.convertTo(to);
-
- this.view
- .setExpressionConverterOutputText(String.format("%s = %s",
- fromUnitString, this.getRoundedString(converted)));
- } else {
- // if I can't convert, leave
- this.view.showErrorDialog("Conversion Error",
- String.format("Cannot convert between %s and %s",
- fromUnitString, toUnitString));
- }
- }
- }
-
- /**
- * @return a list of all of the unit dimensions
- * @since 2019-04-13
- * @since v0.2.0
- */
- public final List<String> dimensionNameList() {
- return this.dimensionNames;
- }
-
- /**
- * @return a list of all the entries in the dimension-based converter's
- * From box
- * @since 2020-08-27
- */
- public final Set<String> fromEntries() {
- return ConditionalExistenceCollections.conditionalExistenceSet(
- this.unitNameSet(), this.fromExistenceCondition);
- }
-
- /**
- * @return a comparator to compare prefix names
- * @since 2019-04-14
- * @since v0.2.0
- */
- public final Comparator<String> getPrefixNameComparator() {
- return this.prefixNameComparator;
- }
-
- /**
- * Like {@link LinearUnitValue#toString(boolean)}, but obeys this unit
- * converter's rounding settings.
- *
- * @since 2020-08-04
- */
- private final String getRoundedString(final LinearUnitValue value,
- boolean showUncertainty) {
- switch (this.roundingType) {
- case DECIMAL_PLACES:
- case SIGNIFICANT_DIGITS:
- return this.getRoundedString(value.asUnitValue());
- case SCIENTIFIC:
- return value.toString(showUncertainty);
- default:
- throw new AssertionError("Invalid switch condition.");
- }
- }
-
- /**
- * Like {@link UnitValue#toString()}, but obeys this unit converter's
- * rounding settings.
- *
- * @since 2020-08-04
- */
- private final String getRoundedString(final UnitValue value) {
- final BigDecimal unrounded = new BigDecimal(value.getValue());
- final BigDecimal rounded;
- int precision = this.precision;
-
- switch (this.roundingType) {
- case DECIMAL_PLACES:
- rounded = unrounded.setScale(precision, RoundingMode.HALF_EVEN);
- break;
- case SCIENTIFIC:
- precision = 12;
- //$FALL-THROUGH$
- case SIGNIFICANT_DIGITS:
- rounded = unrounded
- .round(new MathContext(precision, RoundingMode.HALF_EVEN));
- break;
- default:
- throw new AssertionError("Invalid switch condition.");
- }
-
- String output = rounded.toString();
-
- // remove trailing zeroes
- if (output.contains(".")) {
- while (output.endsWith("0")) {
- output = output.substring(0, output.length() - 1);
- }
- if (output.endsWith(".")) {
- output = output.substring(0, output.length() - 1);
- }
- }
-
- return output + " " + value.getUnit().getPrimaryName().get();
- }
-
- /**
- * @return The file where settings are stored;
- * @since 2020-12-11
- */
- private final Path getSettingsFile() {
- return Path.of(DEFAULT_SETTINGS_FILEPATH);
- }
-
- /**
- * Loads settings from the settings file.
- *
- * @since 2021-02-17
- */
- public final void loadSettings() {
- try {
- // read file line by line
- final int lineNum = 0;
- for (final String line : Files
- .readAllLines(this.getSettingsFile())) {
- final int equalsIndex = line.indexOf('=');
- if (equalsIndex == -1)
- throw new IllegalStateException(
- "Settings file is malformed at line " + lineNum);
-
- final String param = line.substring(0, equalsIndex);
- final String value = line.substring(equalsIndex + 1);
-
- switch (param) {
- // set manually to avoid the unnecessary saving of the non-manual
- // methods
- case "precision":
- this.precision = Integer.valueOf(value);
- break;
- case "rounding_type":
- this.roundingType = RoundingType.valueOf(value);
- break;
- case "prefix_rule":
- this.prefixRule = DefaultPrefixRepetitionRule.valueOf(value);
- this.database.setPrefixRepetitionRule(this.prefixRule);
- break;
- case "one_way":
- this.oneWay = Boolean.valueOf(value);
- if (this.oneWay) {
- this.fromExistenceCondition.setPredicate(
- unitName -> this.metricExceptions.contains(unitName)
- || !this.database.getUnit(unitName)
- .isMetric());
- this.toExistenceCondition.setPredicate(
- unitName -> this.metricExceptions.contains(unitName)
- || this.database.getUnit(unitName).isMetric());
- } else {
- this.fromExistenceCondition.setPredicate(unitName -> true);
- this.toExistenceCondition.setPredicate(unitName -> true);
- }
- break;
- case "include_duplicates":
- this.includeDuplicateUnits = Boolean.valueOf(value);
- if (this.view.presenter != null) {
- this.view.update();
- }
- break;
- default:
- System.err.printf("Warning: unrecognized setting \"%s\".",
- param);
- break;
- }
- }
- } catch (final IOException e) {}
- }
-
- /**
- * @return a set of all prefix names in the database
- * @since 2019-04-14
- * @since v0.2.0
- */
- public final Set<String> prefixNameSet() {
- return this.database.prefixMap().keySet();
- }
-
- /**
- * Runs whenever a prefix is selected in the viewer.
- * <p>
- * Shows its information in the text box to the right.
- * </p>
- *
- * @since 2019-01-15
- * @since v0.1.0
- */
- public final void prefixSelected() {
- final String prefixName = this.view.getPrefixViewerSelection();
- if (prefixName == null)
- return;
- else {
- final UnitPrefix prefix = this.database.getPrefix(prefixName);
-
- this.view.setPrefixTextBoxText(String.format("%s%nMultiplier: %s",
- prefixName, prefix.getMultiplier()));
- }
- }
-
- /**
- * Saves the settings to the settings file.
- *
- * @since 2021-02-17
- */
- public final void saveSettings() {
- try (BufferedWriter writer = Files
- .newBufferedWriter(this.getSettingsFile())) {
- writer.write(String.format("precision=%d\n", this.precision));
- writer.write(
- String.format("rounding_type=%s\n", this.roundingType));
- writer.write(String.format("prefix_rule=%s\n", this.prefixRule));
- writer.write(String.format("one_way=%s\n", this.oneWay));
- writer.write(String.format("include_duplicates=%s\n",
- this.includeDuplicateUnits));
- } catch (final IOException e) {
- e.printStackTrace();
- this.view.showErrorDialog("I/O Error",
- "Error occurred while saving settings: "
- + e.getLocalizedMessage());
- }
- }
-
- public final void setIncludeDuplicateUnits(
- boolean includeDuplicateUnits) {
- this.includeDuplicateUnits = includeDuplicateUnits;
-
- this.view.update();
- this.saveSettings();
- }
-
- /**
- * Enables or disables one-way conversion.
- *
- * @param oneWay whether one-way conversion should be on (true) or off
- * (false)
- * @since 2020-08-27
- */
- public final void setOneWay(boolean oneWay) {
- this.oneWay = oneWay;
- if (oneWay) {
- this.fromExistenceCondition.setPredicate(
- unitName -> this.metricExceptions.contains(unitName)
- || !this.database.getUnit(unitName).isMetric());
- this.toExistenceCondition.setPredicate(
- unitName -> this.metricExceptions.contains(unitName)
- || this.database.getUnit(unitName).isMetric());
- } else {
- this.fromExistenceCondition.setPredicate(unitName -> true);
- this.toExistenceCondition.setPredicate(unitName -> true);
- }
-
- this.saveSettings();
- }
-
- /**
- * @param precision new value of precision
- * @since 2019-01-15
- * @since v0.1.0
- */
- public final void setPrecision(final int precision) {
- this.precision = precision;
-
- this.saveSettings();
- }
-
- /**
- * @param prefixRepetitionRule the prefixRepetitionRule to set
- * @since 2020-08-26
- */
- public void setPrefixRepetitionRule(
- Predicate<List<UnitPrefix>> prefixRepetitionRule) {
- if (prefixRepetitionRule instanceof DefaultPrefixRepetitionRule) {
- this.prefixRule = (DefaultPrefixRepetitionRule) prefixRepetitionRule;
- } else {
- this.prefixRule = null;
- }
- this.database.setPrefixRepetitionRule(prefixRepetitionRule);
-
- this.saveSettings();
- }
-
- /**
- * @param roundingType the roundingType to set
- * @since 2020-07-16
- */
- public final void setRoundingType(RoundingType roundingType) {
- this.roundingType = roundingType;
-
- this.saveSettings();
- }
-
- /**
- * @return a list of all the entries in the dimension-based converter's To
- * box
- * @since 2020-08-27
- */
- public final Set<String> toEntries() {
- return ConditionalExistenceCollections.conditionalExistenceSet(
- this.unitNameSet(), this.toExistenceCondition);
- }
-
- /**
- * Returns true if and only if the unit represented by {@code unitName}
- * has the dimension represented by {@code dimensionName}.
- *
- * @param unitName name of unit to test
- * @param dimensionName name of dimension to test
- * @return whether unit has dimenision
- * @since 2019-04-13
- * @since v0.2.0
- */
- public final boolean unitMatchesDimension(final String unitName,
- final String dimensionName) {
- final Unit unit = this.database.getUnit(unitName);
- final ObjectProduct<BaseDimension> dimension = this.database
- .getDimension(dimensionName);
- return unit.getDimension().equals(dimension);
- }
-
- /**
- * Runs whenever a unit is selected in the viewer.
- * <p>
- * Shows its information in the text box to the right.
- * </p>
- *
- * @since 2019-01-15
- * @since v0.1.0
- */
- public final void unitNameSelected() {
- final String unitName = this.view.getUnitViewerSelection();
- if (unitName == null)
- return;
- else {
- final Unit unit = this.database.getUnit(unitName);
-
- this.view.setUnitTextBoxText(unit.toString());
- }
- }
-
- /**
- * @return a set of all of the unit names
- * @since 2019-04-14
- * @since v0.2.0
- */
- public final Set<String> unitNameSet() {
- return this.database.unitMapPrefixless(this.includeDuplicateUnits)
- .keySet();
- }
- }
-
- /**
- * Different types of rounding.
- *
- * Significant digits: Rounds to a number of digits. i.e. with precision 5,
- * 12345.6789 rounds to 12346. Decimal places: Rounds to a number of digits
- * after the decimal point, i.e. with precision 5, 12345.6789 rounds to
- * 12345.67890. Scientific: Rounds based on the number of digits and
- * operations, following standard scientific rounding.
- */
- private static enum RoundingType {
- SIGNIFICANT_DIGITS, DECIMAL_PLACES, SCIENTIFIC;
- }
-
- private static class View {
- private static final NumberFormat NUMBER_FORMATTER = new DecimalFormat();
-
- /** The view's frame. */
- private final JFrame frame;
- /** The view's associated presenter. */
- private final Presenter presenter;
- /** The master pane containing all of the tabs. */
- private final JTabbedPane masterPane;
-
- // DIMENSION-BASED CONVERTER
- /** The panel for inputting values in the dimension-based converter */
- private final JTextField valueInput;
- /** The panel for "From" in the dimension-based converter */
- private final SearchBoxList fromSearch;
- /** The panel for "To" in the dimension-based converter */
- private final SearchBoxList toSearch;
- /** The output area in the dimension-based converter */
- private final JTextArea dimensionBasedOutput;
-
- // EXPRESSION-BASED CONVERTER
- /** The "From" entry in the conversion panel */
- private final JTextField fromEntry;
- /** The "To" entry in the conversion panel */
- private final JTextField toEntry;
- /** The output area in the conversion panel */
- private final JTextArea output;
-
- // UNIT AND PREFIX VIEWERS
- /** The searchable list of unit names in the unit viewer */
- private final SearchBoxList unitNameList;
- /** The searchable list of prefix names in the prefix viewer */
- private final SearchBoxList prefixNameList;
- /** The text box for unit data in the unit viewer */
- private final JTextArea unitTextBox;
- /** The text box for prefix data in the prefix viewer */
- private final JTextArea prefixTextBox;
-
- /**
- * Creates the {@code View}.
- *
- * @since 2019-01-14
- * @since v0.1.0
- */
- public View() {
- this.presenter = new Presenter(this);
- this.frame = new JFrame("7Units");
- this.frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
-
- // enable system look and feel
- try {
- UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
- } catch (ClassNotFoundException | InstantiationException
- | IllegalAccessException | UnsupportedLookAndFeelException e) {
- // oh well, just use default theme
- System.err.println("Failed to enable system look-and-feel.");
- e.printStackTrace();
- }
-
- // create the components
- this.masterPane = new JTabbedPane();
- this.unitNameList = new SearchBoxList(this.presenter.unitNameSet());
- this.prefixNameList = new SearchBoxList(this.presenter.prefixNameSet(),
- this.presenter.getPrefixNameComparator(), true);
- this.unitTextBox = new JTextArea();
- this.prefixTextBox = new JTextArea();
- this.fromSearch = new SearchBoxList(this.presenter.fromEntries());
- this.toSearch = new SearchBoxList(this.presenter.toEntries());
- this.valueInput = new JFormattedTextField(NUMBER_FORMATTER);
- this.dimensionBasedOutput = new JTextArea(2, 32);
- this.fromEntry = new JTextField();
- this.toEntry = new JTextField();
- this.output = new JTextArea(2, 32);
-
- // create more components
- this.initComponents();
-
- this.frame.pack();
- }
-
- /**
- * @return the currently selected pane.
- * @throws AssertionError if no pane (or an invalid pane) is selected
- */
- public Pane getActivePane() {
- switch (this.masterPane.getSelectedIndex()) {
- case 0:
- return Pane.UNIT_CONVERTER;
- case 1:
- return Pane.EXPRESSION_CONVERTER;
- case 2:
- return Pane.UNIT_VIEWER;
- case 3:
- return Pane.PREFIX_VIEWER;
- case 4:
- return Pane.ABOUT;
- case 5:
- return Pane.SETTINGS;
- default:
- throw new AssertionError("No selected pane, or invalid pane.");
- }
- }
-
- /**
- * @return value in dimension-based converter
- * @throws ParseException
- * @since 2020-07-07
- */
- public double getDimensionConverterInput() throws ParseException {
- final Number value = NUMBER_FORMATTER.parse(this.valueInput.getText());
- if (value instanceof Double)
- return (double) value;
- else if (value instanceof Long)
- return ((Long) value).longValue();
- else
- throw new AssertionError();
- }
-
- /**
- * @return selection in "From" selector in dimension-based converter
- * @since 2019-04-13
- * @since v0.2.0
- */
- public String getFromSelection() {
- return this.fromSearch.getSelectedValue();
- }
-
- /**
- * @return text in "From" box in converter panel
- * @since 2019-01-15
- * @since v0.1.0
- */
- public String getFromText() {
- return this.fromEntry.getText();
- }
-
- /**
- * @return index of selected prefix in prefix viewer
- * @since 2019-01-15
- * @since v0.1.0
- */
- public String getPrefixViewerSelection() {
- return this.prefixNameList.getSelectedValue();
- }
-
- /**
- * @return selection in "To" selector in dimension-based converter
- * @since 2019-04-13
- * @since v0.2.0
- */
- public String getToSelection() {
- return this.toSearch.getSelectedValue();
- }
-
- /**
- * @return text in "To" box in converter panel
- * @since 2019-01-26
- * @since v0.1.0
- */
- public String getToText() {
- return this.toEntry.getText();
- }
-
- /**
- * @return index of selected unit in unit viewer
- * @since 2019-01-15
- * @since v0.1.0
- */
- public String getUnitViewerSelection() {
- return this.unitNameList.getSelectedValue();
- }
-
- /**
- * Starts up the application.
- *
- * @since 2018-12-27
- * @since v0.1.0
- */
- public final void init() {
- this.frame.setVisible(true);
- }
-
- /**
- * Initializes the view's components.
- *
- * @since 2018-12-27
- * @since v0.1.0
- */
- private final void initComponents() {
- final JPanel masterPanel = new JPanel();
- this.frame.add(masterPanel);
-
- masterPanel.setLayout(new BorderLayout());
-
- { // pane with all of the tabs
- masterPanel.add(this.masterPane, BorderLayout.CENTER);
-
- // update stuff
- this.masterPane.addChangeListener(e -> this.update());
-
- { // a panel for unit conversion using a selector
- final JPanel convertUnitPanel = new JPanel();
- this.masterPane.addTab("Convert Units", convertUnitPanel);
- this.masterPane.setMnemonicAt(0, KeyEvent.VK_U);
-
- convertUnitPanel.setLayout(new BorderLayout());
-
- { // panel for input part
- final JPanel inputPanel = new JPanel();
- convertUnitPanel.add(inputPanel, BorderLayout.CENTER);
-
- inputPanel.setLayout(new GridLayout(1, 3));
-
- final JComboBox<String> dimensionSelector = new JComboBox<>(
- this.presenter.dimensionNameList()
- .toArray(new String[0]));
- dimensionSelector.setSelectedItem("LENGTH");
-
- // handle dimension filter
- final MutablePredicate<String> dimensionFilter = new MutablePredicate<>(
- s -> true);
-
- // panel for From things
- inputPanel.add(this.fromSearch);
-
- this.fromSearch.addSearchFilter(dimensionFilter);
-
- { // for dimension selector and arrow that represents
- // conversion
- final JPanel inBetweenPanel = new JPanel();
- inputPanel.add(inBetweenPanel);
-
- inBetweenPanel.setLayout(new BorderLayout());
-
- { // dimension selector
- inBetweenPanel.add(dimensionSelector,
- BorderLayout.PAGE_START);
- }
-
- { // the arrow in the middle
- final JLabel arrowLabel = new JLabel("->");
- inBetweenPanel.add(arrowLabel, BorderLayout.CENTER);
- }
- }
-
- // panel for To things
-
- inputPanel.add(this.toSearch);
-
- this.toSearch.addSearchFilter(dimensionFilter);
-
- // code for dimension filter
- dimensionSelector.addItemListener(e -> {
- dimensionFilter.setPredicate(string -> View.this.presenter
- .unitMatchesDimension(string,
- (String) dimensionSelector.getSelectedItem()));
- this.fromSearch.reapplyFilter();
- this.toSearch.reapplyFilter();
- });
-
- // apply the item listener once because I have a default
- // selection
- dimensionFilter.setPredicate(string -> View.this.presenter
- .unitMatchesDimension(string,
- (String) dimensionSelector.getSelectedItem()));
- this.fromSearch.reapplyFilter();
- this.toSearch.reapplyFilter();
- }
-
- { // panel for submit and output, and also value entry
- final JPanel outputPanel = new JPanel();
- convertUnitPanel.add(outputPanel, BorderLayout.PAGE_END);
-
- outputPanel.setLayout(new GridLayout(3, 1));
-
- { // unit input
- final JPanel valueInputPanel = new JPanel();
- outputPanel.add(valueInputPanel);
-
- valueInputPanel.setLayout(new BorderLayout());
-
- { // prompt
- final JLabel valuePrompt = new JLabel(
- "Value to convert: ");
- valueInputPanel.add(valuePrompt,
- BorderLayout.LINE_START);
- }
-
- { // value to convert
- valueInputPanel.add(this.valueInput,
- BorderLayout.CENTER);
- }
- }
-
- { // button to convert
- final JButton convertButton = new JButton("Convert");
- outputPanel.add(convertButton);
-
- convertButton.addActionListener(
- e -> this.presenter.convertDimensionBased());
- convertButton.setMnemonic(KeyEvent.VK_ENTER);
- }
-
- { // output of conversion
- outputPanel.add(this.dimensionBasedOutput);
- this.dimensionBasedOutput.setEditable(false);
- }
- }
- }
-
- { // panel for unit conversion using expressions
- final JPanel convertExpressionPanel = new JPanel();
- this.masterPane.addTab("Convert Unit Expressions",
- convertExpressionPanel);
- this.masterPane.setMnemonicAt(1, KeyEvent.VK_E);
-
- convertExpressionPanel.setLayout(new GridLayout(4, 1));
-
- { // panel for units to convert from
- final JPanel fromPanel = new JPanel();
- convertExpressionPanel.add(fromPanel);
-
- fromPanel.setBorder(BorderFactory.createTitledBorder("From"));
- fromPanel.setLayout(new GridLayout(1, 1));
-
- { // entry for units
- fromPanel.add(this.fromEntry);
- }
- }
-
- { // panel for units to convert to
- final JPanel toPanel = new JPanel();
- convertExpressionPanel.add(toPanel);
-
- toPanel.setBorder(BorderFactory.createTitledBorder("To"));
- toPanel.setLayout(new GridLayout(1, 1));
-
- { // entry for units
- toPanel.add(this.toEntry);
- }
- }
-
- { // button to convert
- final JButton convertButton = new JButton("Convert");
- convertExpressionPanel.add(convertButton);
-
- convertButton.addActionListener(
- e -> this.presenter.convertExpressions());
- convertButton.setMnemonic(KeyEvent.VK_ENTER);
- }
-
- { // output of conversion
- final JPanel outputPanel = new JPanel();
- convertExpressionPanel.add(outputPanel);
-
- outputPanel
- .setBorder(BorderFactory.createTitledBorder("Output"));
- outputPanel.setLayout(new GridLayout(1, 1));
-
- { // output
- outputPanel.add(this.output);
- this.output.setEditable(false);
- }
- }
- }
-
- { // panel to look up units
- final JPanel unitLookupPanel = new JPanel();
- this.masterPane.addTab("Unit Viewer", unitLookupPanel);
- this.masterPane.setMnemonicAt(2, KeyEvent.VK_V);
-
- unitLookupPanel.setLayout(new GridLayout());
-
- { // search panel
- unitLookupPanel.add(this.unitNameList);
-
- this.unitNameList.getSearchList().addListSelectionListener(
- e -> this.presenter.unitNameSelected());
- }
-
- { // the text box for unit's toString
- unitLookupPanel.add(this.unitTextBox);
- this.unitTextBox.setEditable(false);
- this.unitTextBox.setLineWrap(true);
- }
- }
-
- { // panel to look up prefixes
- final JPanel prefixLookupPanel = new JPanel();
- this.masterPane.addTab("Prefix Viewer", prefixLookupPanel);
- this.masterPane.setMnemonicAt(3, KeyEvent.VK_P);
-
- prefixLookupPanel.setLayout(new GridLayout(1, 2));
-
- { // panel for listing and seaching
- prefixLookupPanel.add(this.prefixNameList);
-
- this.prefixNameList.getSearchList().addListSelectionListener(
- e -> this.presenter.prefixSelected());
- }
-
- { // the text box for prefix's toString
- prefixLookupPanel.add(this.prefixTextBox);
- this.prefixTextBox.setEditable(false);
- this.prefixTextBox.setLineWrap(true);
- }
- }
-
- { // Info panel
- final JPanel infoPanel = new JPanel();
- this.masterPane.addTab("\uD83D\uDEC8", // info (i) character
- new JScrollPane(infoPanel));
-
- final JTextArea infoTextArea = new JTextArea();
- infoTextArea.setEditable(false);
- infoTextArea.setOpaque(false);
- infoPanel.add(infoTextArea);
-
- // get info text
- final String infoText = Presenter
- .getLinesFromResource("/about.txt").stream()
- .map(Presenter::withoutComments)
- .collect(Collectors.joining("\n"))
- .replaceAll("\\[VERSION\\]", ProgramInfo.VERSION);
- infoTextArea.setText(infoText);
- }
-
- { // Settings panel
- final JPanel settingsPanel = new JPanel();
- this.masterPane.addTab("\u2699", new JScrollPane(settingsPanel));
- this.masterPane.setMnemonicAt(5, KeyEvent.VK_S);
-
- settingsPanel.setLayout(
- new BoxLayout(settingsPanel, BoxLayout.PAGE_AXIS));
-
- { // rounding settings
- final JPanel roundingPanel = new JPanel();
- settingsPanel.add(roundingPanel);
- roundingPanel
- .setBorder(new TitledBorder("Rounding Settings"));
- roundingPanel.setLayout(new GridBagLayout());
-
- // rounding rule selection
- final ButtonGroup roundingRuleButtons = new ButtonGroup();
-
- final JLabel roundingRuleLabel = new JLabel("Rounding Rule:");
- roundingPanel.add(roundingRuleLabel, new GridBagBuilder(0, 0)
- .setAnchor(GridBagConstraints.LINE_START).build());
-
- final JRadioButton fixedPrecision = new JRadioButton(
- "Fixed Precision");
- if (this.presenter.roundingType == RoundingType.SIGNIFICANT_DIGITS) {
- fixedPrecision.setSelected(true);
- }
- fixedPrecision.addActionListener(e -> this.presenter
- .setRoundingType(RoundingType.SIGNIFICANT_DIGITS));
- roundingRuleButtons.add(fixedPrecision);
- roundingPanel.add(fixedPrecision, new GridBagBuilder(0, 1)
- .setAnchor(GridBagConstraints.LINE_START).build());
-
- final JRadioButton fixedDecimals = new JRadioButton(
- "Fixed Decimal Places");
- if (this.presenter.roundingType == RoundingType.DECIMAL_PLACES) {
- fixedDecimals.setSelected(true);
- }
- fixedDecimals.addActionListener(e -> this.presenter
- .setRoundingType(RoundingType.DECIMAL_PLACES));
- roundingRuleButtons.add(fixedDecimals);
- roundingPanel.add(fixedDecimals, new GridBagBuilder(0, 2)
- .setAnchor(GridBagConstraints.LINE_START).build());
-
- final JRadioButton relativePrecision = new JRadioButton(
- "Scientific Precision");
- if (this.presenter.roundingType == RoundingType.SCIENTIFIC) {
- relativePrecision.setSelected(true);
- }
- relativePrecision.addActionListener(e -> this.presenter
- .setRoundingType(RoundingType.SCIENTIFIC));
- roundingRuleButtons.add(relativePrecision);
- roundingPanel.add(relativePrecision, new GridBagBuilder(0, 3)
- .setAnchor(GridBagConstraints.LINE_START).build());
-
- final JLabel sliderLabel = new JLabel("Precision:");
- roundingPanel.add(sliderLabel, new GridBagBuilder(0, 4)
- .setAnchor(GridBagConstraints.LINE_START).build());
-
- final JSlider sigDigSlider = new JSlider(0, 12);
- roundingPanel.add(sigDigSlider, new GridBagBuilder(0, 5)
- .setAnchor(GridBagConstraints.LINE_START).build());
-
- sigDigSlider.setMajorTickSpacing(4);
- sigDigSlider.setMinorTickSpacing(1);
- sigDigSlider.setSnapToTicks(true);
- sigDigSlider.setPaintTicks(true);
- sigDigSlider.setPaintLabels(true);
- sigDigSlider.setValue(this.presenter.precision);
-
- sigDigSlider.addChangeListener(e -> this.presenter
- .setPrecision(sigDigSlider.getValue()));
- }
-
- { // prefix repetition settings
- final JPanel prefixRepetitionPanel = new JPanel();
- settingsPanel.add(prefixRepetitionPanel);
- prefixRepetitionPanel.setBorder(
- new TitledBorder("Prefix Repetition Settings"));
- prefixRepetitionPanel.setLayout(new GridBagLayout());
-
- // prefix rules
- final ButtonGroup prefixRuleButtons = new ButtonGroup();
-
- final JRadioButton noRepetition = new JRadioButton(
- "No Repetition");
- if (this.presenter.prefixRule == DefaultPrefixRepetitionRule.NO_REPETITION) {
- noRepetition.setSelected(true);
- }
- noRepetition.addActionListener(
- e -> this.presenter.setPrefixRepetitionRule(
- DefaultPrefixRepetitionRule.NO_REPETITION));
- prefixRuleButtons.add(noRepetition);
- prefixRepetitionPanel.add(noRepetition,
- new GridBagBuilder(0, 0)
- .setAnchor(GridBagConstraints.LINE_START)
- .build());
-
- final JRadioButton noRestriction = new JRadioButton(
- "No Restriction");
- if (this.presenter.prefixRule == DefaultPrefixRepetitionRule.NO_RESTRICTION) {
- noRestriction.setSelected(true);
- }
- noRestriction.addActionListener(
- e -> this.presenter.setPrefixRepetitionRule(
- DefaultPrefixRepetitionRule.NO_RESTRICTION));
- prefixRuleButtons.add(noRestriction);
- prefixRepetitionPanel.add(noRestriction,
- new GridBagBuilder(0, 1)
- .setAnchor(GridBagConstraints.LINE_START)
- .build());
-
- final JRadioButton customRepetition = new JRadioButton(
- "Complex Repetition");
- if (this.presenter.prefixRule == DefaultPrefixRepetitionRule.COMPLEX_REPETITION) {
- customRepetition.setSelected(true);
- }
- customRepetition.addActionListener(
- e -> this.presenter.setPrefixRepetitionRule(
- DefaultPrefixRepetitionRule.COMPLEX_REPETITION));
- prefixRuleButtons.add(customRepetition);
- prefixRepetitionPanel.add(customRepetition,
- new GridBagBuilder(0, 2)
- .setAnchor(GridBagConstraints.LINE_START)
- .build());
- }
-
- { // search settings
- final JPanel searchingPanel = new JPanel();
- settingsPanel.add(searchingPanel);
- searchingPanel.setBorder(new TitledBorder("Search Settings"));
- searchingPanel.setLayout(new GridBagLayout());
-
- // searching rules
- final ButtonGroup searchRuleButtons = new ButtonGroup();
-
- final JRadioButton noPrefixes = new JRadioButton(
- "Never Include Prefixed Units");
- noPrefixes.setEnabled(false);
- searchRuleButtons.add(noPrefixes);
- searchingPanel.add(noPrefixes, new GridBagBuilder(0, 0)
- .setAnchor(GridBagConstraints.LINE_START).build());
-
- final JRadioButton fixedPrefixes = new JRadioButton(
- "Include Some Prefixes");
- fixedPrefixes.setEnabled(false);
- searchRuleButtons.add(fixedPrefixes);
- searchingPanel.add(fixedPrefixes, new GridBagBuilder(0, 1)
- .setAnchor(GridBagConstraints.LINE_START).build());
-
- final JRadioButton explicitPrefixes = new JRadioButton(
- "Include Explicit Prefixes");
- explicitPrefixes.setEnabled(false);
- searchRuleButtons.add(explicitPrefixes);
- searchingPanel.add(explicitPrefixes, new GridBagBuilder(0, 2)
- .setAnchor(GridBagConstraints.LINE_START).build());
-
- final JRadioButton alwaysInclude = new JRadioButton(
- "Include All Single Prefixes");
- alwaysInclude.setEnabled(false);
- searchRuleButtons.add(alwaysInclude);
- searchingPanel.add(alwaysInclude, new GridBagBuilder(0, 3)
- .setAnchor(GridBagConstraints.LINE_START).build());
- }
-
- { // miscellaneous settings
- final JPanel miscPanel = new JPanel();
- settingsPanel.add(miscPanel);
- miscPanel
- .setBorder(new TitledBorder("Miscellaneous Settings"));
- miscPanel.setLayout(new GridBagLayout());
-
- final JCheckBox oneWay = new JCheckBox(
- "Convert One Way Only");
- oneWay.setSelected(this.presenter.oneWay);
- oneWay.addItemListener(
- e -> this.presenter.setOneWay(e.getStateChange() == 1));
- miscPanel.add(oneWay, new GridBagBuilder(0, 0)
- .setAnchor(GridBagConstraints.LINE_START).build());
-
- final JCheckBox showAllVariations = new JCheckBox(
- "Show Duplicates in \"Convert Units\"");
- showAllVariations
- .setSelected(this.presenter.includeDuplicateUnits);
- showAllVariations.addItemListener(e -> this.presenter
- .setIncludeDuplicateUnits(e.getStateChange() == 1));
- miscPanel.add(showAllVariations, new GridBagBuilder(0, 1)
- .setAnchor(GridBagConstraints.LINE_START).build());
-
- final JButton unitFileButton = new JButton(
- "Manage Unit Data Files");
- unitFileButton.setEnabled(false);
- miscPanel.add(unitFileButton, new GridBagBuilder(0, 2)
- .setAnchor(GridBagConstraints.LINE_START).build());
- }
- }
- }
- }
-
- /**
- * Sets the text in the output of the dimension-based converter.
- *
- * @param text text to set
- * @since 2019-04-13
- * @since v0.2.0
- */
- public void setDimensionConverterOutputText(final String text) {
- this.dimensionBasedOutput.setText(text);
- }
-
- /**
- * Sets the text in the output of the conversion panel.
- *
- * @param text text to set
- * @since 2019-01-15
- * @since v0.1.0
- */
- public void setExpressionConverterOutputText(final String text) {
- this.output.setText(text);
- }
-
- /**
- * Sets the text of the prefix text box in the prefix viewer.
- *
- * @param text text to set
- * @since 2019-01-15
- * @since v0.1.0
- */
- public void setPrefixTextBoxText(final String text) {
- this.prefixTextBox.setText(text);
- }
-
- /**
- * Sets the text of the unit text box in the unit viewer.
- *
- * @param text text to set
- * @since 2019-01-15
- * @since v0.1.0
- */
- public void setUnitTextBoxText(final String text) {
- this.unitTextBox.setText(text);
- }
-
- /**
- * Shows an error dialog.
- *
- * @param title title of dialog
- * @param message message in dialog
- * @since 2019-01-14
- * @since v0.1.0
- */
- public void showErrorDialog(final String title, final String message) {
- JOptionPane.showMessageDialog(this.frame, message, title,
- JOptionPane.ERROR_MESSAGE);
- }
-
- public void update() {
- this.unitNameList.setItems(this.presenter.unitNameSet());
- this.fromSearch.setItems(this.presenter.fromEntries());
- this.toSearch.setItems(this.presenter.toEntries());
-
- switch (this.getActivePane()) {
- case UNIT_CONVERTER:
- this.fromSearch.updateList();
- this.toSearch.updateList();
- break;
- default:
- // do nothing, for now
- break;
- }
- }
- }
-
- public static void main(final String[] args) {
- new View().init();
- }
-}
diff --git a/src/main/java/sevenUnits/unit/BaseDimension.java b/src/main/java/sevenUnits/unit/BaseDimension.java
index d5e98ca..bcd57d9 100644
--- a/src/main/java/sevenUnits/unit/BaseDimension.java
+++ b/src/main/java/sevenUnits/unit/BaseDimension.java
@@ -18,70 +18,58 @@ package sevenUnits.unit;
import java.util.Objects;
+import sevenUnits.utils.NameSymbol;
+import sevenUnits.utils.Nameable;
+
/**
* A dimension that defines a {@code BaseUnit}
*
* @author Adrien Hopkins
* @since 2019-10-16
*/
-public final class BaseDimension {
+public final class BaseDimension implements Nameable {
/**
* Gets a {@code BaseDimension} with the provided name and symbol.
*
- * @param name
- * name of dimension
- * @param symbol
- * symbol used for dimension
+ * @param name name of dimension
+ * @param symbol symbol used for dimension
* @return dimension
* @since 2019-10-16
*/
public static BaseDimension valueOf(final String name, final String symbol) {
return new BaseDimension(name, symbol);
}
-
+
/**
* The name of the dimension.
*/
private final String name;
/**
- * The symbol used by the dimension. Symbols should be short, generally one or two characters.
+ * The symbol used by the dimension. Symbols should be short, generally one
+ * or two characters.
*/
private final String symbol;
-
+
/**
* Creates the {@code BaseDimension}.
*
- * @param name
- * name of unit
- * @param symbol
- * symbol of unit
- * @throws NullPointerException
- * if any argument is null
+ * @param name name of unit
+ * @param symbol symbol of unit
+ * @throws NullPointerException if any argument is null
* @since 2019-10-16
*/
private BaseDimension(final String name, final String symbol) {
this.name = Objects.requireNonNull(name, "name must not be null.");
this.symbol = Objects.requireNonNull(symbol, "symbol must not be null.");
}
-
- /**
- * @return name
- * @since 2019-10-16
- */
- public final String getName() {
- return this.name;
- }
-
- /**
- * @return symbol
- * @since 2019-10-16
- */
- public final String getSymbol() {
- return this.symbol;
+
+ @Override
+ public NameSymbol getNameSymbol() {
+ return NameSymbol.of(this.name, this.symbol);
}
-
+
@Override
public String toString() {
- return String.format("%s (%s)", this.getName(), this.getSymbol());
+ return String.format("%s (%s)", this.name, this.symbol);
}
}
diff --git a/src/main/java/sevenUnits/unit/BaseUnit.java b/src/main/java/sevenUnits/unit/BaseUnit.java
index ee2c277..dba7f52 100644
--- a/src/main/java/sevenUnits/unit/BaseUnit.java
+++ b/src/main/java/sevenUnits/unit/BaseUnit.java
@@ -20,6 +20,8 @@ import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
+import sevenUnits.utils.NameSymbol;
+
/**
* A unit that other units are defined by.
* <p>
diff --git a/src/main/java/sevenUnits/unit/BritishImperial.java b/src/main/java/sevenUnits/unit/BritishImperial.java
index 743beeb..0ecba6d 100644
--- a/src/main/java/sevenUnits/unit/BritishImperial.java
+++ b/src/main/java/sevenUnits/unit/BritishImperial.java
@@ -16,6 +16,8 @@
*/
package sevenUnits.unit;
+import sevenUnits.utils.NameSymbol;
+
/**
* A static utility class that contains units in the British Imperial system.
*
@@ -119,5 +121,5 @@ public final class BritishImperial {
public static final Unit FAHRENHEIT = Unit
.fromConversionFunctions(Metric.KELVIN.getBase(),
tempK -> tempK * 1.8 - 459.67, tempF -> (tempF + 459.67) / 1.8)
- .withName(NameSymbol.of("degrees Fahrenheit", "\u00B0F"));
+ .withName(NameSymbol.of("degree Fahrenheit", "\u00B0F"));
}
diff --git a/src/main/java/sevenUnits/unit/FunctionalUnit.java b/src/main/java/sevenUnits/unit/FunctionalUnit.java
index df457e4..720b0af 100644
--- a/src/main/java/sevenUnits/unit/FunctionalUnit.java
+++ b/src/main/java/sevenUnits/unit/FunctionalUnit.java
@@ -19,6 +19,7 @@ package sevenUnits.unit;
import java.util.Objects;
import java.util.function.DoubleUnaryOperator;
+import sevenUnits.utils.NameSymbol;
import sevenUnits.utils.ObjectProduct;
/**
diff --git a/src/main/java/sevenUnits/unit/FunctionalUnitlike.java b/src/main/java/sevenUnits/unit/FunctionalUnitlike.java
index 2ee9e19..d6046c0 100644
--- a/src/main/java/sevenUnits/unit/FunctionalUnitlike.java
+++ b/src/main/java/sevenUnits/unit/FunctionalUnitlike.java
@@ -19,6 +19,7 @@ package sevenUnits.unit;
import java.util.function.DoubleFunction;
import java.util.function.ToDoubleFunction;
+import sevenUnits.utils.NameSymbol;
import sevenUnits.utils.ObjectProduct;
/**
diff --git a/src/main/java/sevenUnits/unit/LinearUnit.java b/src/main/java/sevenUnits/unit/LinearUnit.java
index 25c2e2e..103b7f6 100644
--- a/src/main/java/sevenUnits/unit/LinearUnit.java
+++ b/src/main/java/sevenUnits/unit/LinearUnit.java
@@ -19,6 +19,7 @@ package sevenUnits.unit;
import java.util.Objects;
import sevenUnits.utils.DecimalComparison;
+import sevenUnits.utils.NameSymbol;
import sevenUnits.utils.ObjectProduct;
import sevenUnits.utils.UncertainDouble;
@@ -369,6 +370,13 @@ public final class LinearUnit extends Unit {
this.getConversionFactor() * multiplier.getConversionFactor());
}
+ @Override
+ public String toDefinitionString() {
+ return Double.toString(this.conversionFactor)
+ + (this.getBase().equals(ObjectProduct.empty()) ? ""
+ : " " + this.getBase().toString(BaseUnit::getShortName));
+ }
+
/**
* Returns this unit but to an exponent.
*
@@ -382,20 +390,6 @@ public final class LinearUnit extends Unit {
Math.pow(this.conversionFactor, exponent));
}
- /**
- * @return a string providing a definition of this unit
- * @since 2019-10-21
- */
- @Override
- public String toString() {
- return this.getPrimaryName().orElse("Unnamed unit")
- + (this.getSymbol().isPresent()
- ? String.format(" (%s)", this.getSymbol().get())
- : "")
- + ", " + Double.toString(this.conversionFactor) + " * "
- + this.getBase().toString(u -> u.getSymbol().get());
- }
-
@Override
public LinearUnit withName(final NameSymbol ns) {
return valueOf(this.getBase(), this.getConversionFactor(), ns);
diff --git a/src/main/java/sevenUnits/unit/LinearUnitValue.java b/src/main/java/sevenUnits/unit/LinearUnitValue.java
index a50e1f5..f91d30b 100644
--- a/src/main/java/sevenUnits/unit/LinearUnitValue.java
+++ b/src/main/java/sevenUnits/unit/LinearUnitValue.java
@@ -16,6 +16,7 @@
*/
package sevenUnits.unit;
+import java.math.RoundingMode;
import java.util.Objects;
import java.util.Optional;
@@ -300,7 +301,7 @@ public final class LinearUnitValue {
@Override
public String toString() {
- return this.toString(!this.value.isExact());
+ return this.toString(!this.value.isExact(), RoundingMode.HALF_EVEN);
}
/**
@@ -315,7 +316,8 @@ public final class LinearUnitValue {
*
* @since 2020-07-26
*/
- public String toString(final boolean showUncertainty) {
+ public String toString(final boolean showUncertainty,
+ RoundingMode roundingMode) {
final Optional<String> primaryName = this.unit.getPrimaryName();
final Optional<String> symbol = this.unit.getSymbol();
final String chosenName = symbol.orElse(primaryName.orElse(null));
@@ -325,10 +327,10 @@ public final class LinearUnitValue {
// get rounded strings
// if showUncertainty is true, add brackets around the string
final String valueString = (showUncertainty ? "(" : "")
- + this.value.toString(showUncertainty)
+ + this.value.toString(showUncertainty, roundingMode)
+ (showUncertainty ? ")" : "");
final String baseValueString = (showUncertainty ? "(" : "")
- + baseValue.toString(showUncertainty)
+ + baseValue.toString(showUncertainty, roundingMode)
+ (showUncertainty ? ")" : "");
// create string
diff --git a/src/main/java/sevenUnits/unit/Metric.java b/src/main/java/sevenUnits/unit/Metric.java
index 3c4d291..05e82ba 100644
--- a/src/main/java/sevenUnits/unit/Metric.java
+++ b/src/main/java/sevenUnits/unit/Metric.java
@@ -18,6 +18,7 @@ package sevenUnits.unit;
import java.util.Set;
+import sevenUnits.utils.NameSymbol;
import sevenUnits.utils.ObjectProduct;
/**
@@ -114,23 +115,30 @@ public final class Metric {
public static final ObjectProduct<BaseDimension> EMPTY = ObjectProduct
.empty();
public static final ObjectProduct<BaseDimension> LENGTH = ObjectProduct
- .oneOf(BaseDimensions.LENGTH);
+ .oneOf(BaseDimensions.LENGTH)
+ .withName(NameSymbol.of("Length", "L"));
public static final ObjectProduct<BaseDimension> MASS = ObjectProduct
- .oneOf(BaseDimensions.MASS);
+ .oneOf(BaseDimensions.MASS).withName(NameSymbol.of("Mass", "M"));
public static final ObjectProduct<BaseDimension> TIME = ObjectProduct
- .oneOf(BaseDimensions.TIME);
+ .oneOf(BaseDimensions.TIME).withName(NameSymbol.of("Time", "T"));
public static final ObjectProduct<BaseDimension> ELECTRIC_CURRENT = ObjectProduct
- .oneOf(BaseDimensions.ELECTRIC_CURRENT);
+ .oneOf(BaseDimensions.ELECTRIC_CURRENT)
+ .withName(NameSymbol.of("Current", "I"));
public static final ObjectProduct<BaseDimension> TEMPERATURE = ObjectProduct
- .oneOf(BaseDimensions.TEMPERATURE);
+ .oneOf(BaseDimensions.TEMPERATURE)
+ .withName(NameSymbol.of("Temperature", "\u0398"));
public static final ObjectProduct<BaseDimension> QUANTITY = ObjectProduct
- .oneOf(BaseDimensions.QUANTITY);
+ .oneOf(BaseDimensions.QUANTITY)
+ .withName(NameSymbol.of("Quantity", "N"));
public static final ObjectProduct<BaseDimension> LUMINOUS_INTENSITY = ObjectProduct
- .oneOf(BaseDimensions.LUMINOUS_INTENSITY);
+ .oneOf(BaseDimensions.LUMINOUS_INTENSITY)
+ .withName(NameSymbol.of("Luminous Intensity", "J"));
public static final ObjectProduct<BaseDimension> INFORMATION = ObjectProduct
- .oneOf(BaseDimensions.INFORMATION);
+ .oneOf(BaseDimensions.INFORMATION)
+ .withName(NameSymbol.ofName("Information"));
public static final ObjectProduct<BaseDimension> CURRENCY = ObjectProduct
- .oneOf(BaseDimensions.CURRENCY);
+ .oneOf(BaseDimensions.CURRENCY)
+ .withName(NameSymbol.ofName("Currency"));
// derived dimensions without named SI units
public static final ObjectProduct<BaseDimension> AREA = LENGTH
@@ -138,7 +146,7 @@ public final class Metric {
public static final ObjectProduct<BaseDimension> VOLUME = AREA
.times(LENGTH);
public static final ObjectProduct<BaseDimension> VELOCITY = LENGTH
- .dividedBy(TIME);
+ .dividedBy(TIME).withName(NameSymbol.ofName("Velocity"));
public static final ObjectProduct<BaseDimension> ACCELERATION = VELOCITY
.dividedBy(TIME);
public static final ObjectProduct<BaseDimension> WAVENUMBER = EMPTY
@@ -402,54 +410,89 @@ public final class Metric {
.withName(NameSymbol.of("exbi", "Ei"));
// a few prefixed units
- public static final LinearUnit MICROMETRE = Metric.METRE.withPrefix(Metric.MICRO);
- public static final LinearUnit MILLIMETRE = Metric.METRE.withPrefix(Metric.MILLI);
- public static final LinearUnit KILOMETRE = Metric.METRE.withPrefix(Metric.KILO);
- public static final LinearUnit MEGAMETRE = Metric.METRE.withPrefix(Metric.MEGA);
+ public static final LinearUnit MICROMETRE = Metric.METRE
+ .withPrefix(Metric.MICRO);
+ public static final LinearUnit MILLIMETRE = Metric.METRE
+ .withPrefix(Metric.MILLI);
+ public static final LinearUnit KILOMETRE = Metric.METRE
+ .withPrefix(Metric.KILO);
+ public static final LinearUnit MEGAMETRE = Metric.METRE
+ .withPrefix(Metric.MEGA);
- public static final LinearUnit MICROLITRE = Metric.LITRE.withPrefix(Metric.MICRO);
- public static final LinearUnit MILLILITRE = Metric.LITRE.withPrefix(Metric.MILLI);
- public static final LinearUnit KILOLITRE = Metric.LITRE.withPrefix(Metric.KILO);
- public static final LinearUnit MEGALITRE = Metric.LITRE.withPrefix(Metric.MEGA);
+ public static final LinearUnit MICROLITRE = Metric.LITRE
+ .withPrefix(Metric.MICRO);
+ public static final LinearUnit MILLILITRE = Metric.LITRE
+ .withPrefix(Metric.MILLI);
+ public static final LinearUnit KILOLITRE = Metric.LITRE
+ .withPrefix(Metric.KILO);
+ public static final LinearUnit MEGALITRE = Metric.LITRE
+ .withPrefix(Metric.MEGA);
- public static final LinearUnit MICROSECOND = Metric.SECOND.withPrefix(Metric.MICRO);
- public static final LinearUnit MILLISECOND = Metric.SECOND.withPrefix(Metric.MILLI);
- public static final LinearUnit KILOSECOND = Metric.SECOND.withPrefix(Metric.KILO);
- public static final LinearUnit MEGASECOND = Metric.SECOND.withPrefix(Metric.MEGA);
+ public static final LinearUnit MICROSECOND = Metric.SECOND
+ .withPrefix(Metric.MICRO);
+ public static final LinearUnit MILLISECOND = Metric.SECOND
+ .withPrefix(Metric.MILLI);
+ public static final LinearUnit KILOSECOND = Metric.SECOND
+ .withPrefix(Metric.KILO);
+ public static final LinearUnit MEGASECOND = Metric.SECOND
+ .withPrefix(Metric.MEGA);
- public static final LinearUnit MICROGRAM = Metric.GRAM.withPrefix(Metric.MICRO);
- public static final LinearUnit MILLIGRAM = Metric.GRAM.withPrefix(Metric.MILLI);
- public static final LinearUnit MEGAGRAM = Metric.GRAM.withPrefix(Metric.MEGA);
+ public static final LinearUnit MICROGRAM = Metric.GRAM
+ .withPrefix(Metric.MICRO);
+ public static final LinearUnit MILLIGRAM = Metric.GRAM
+ .withPrefix(Metric.MILLI);
+ public static final LinearUnit MEGAGRAM = Metric.GRAM
+ .withPrefix(Metric.MEGA);
- public static final LinearUnit MICRONEWTON = Metric.NEWTON.withPrefix(Metric.MICRO);
- public static final LinearUnit MILLINEWTON = Metric.NEWTON.withPrefix(Metric.MILLI);
- public static final LinearUnit KILONEWTON = Metric.NEWTON.withPrefix(Metric.KILO);
- public static final LinearUnit MEGANEWTON = Metric.NEWTON.withPrefix(Metric.MEGA);
+ public static final LinearUnit MICRONEWTON = Metric.NEWTON
+ .withPrefix(Metric.MICRO);
+ public static final LinearUnit MILLINEWTON = Metric.NEWTON
+ .withPrefix(Metric.MILLI);
+ public static final LinearUnit KILONEWTON = Metric.NEWTON
+ .withPrefix(Metric.KILO);
+ public static final LinearUnit MEGANEWTON = Metric.NEWTON
+ .withPrefix(Metric.MEGA);
- public static final LinearUnit MICROJOULE = Metric.JOULE.withPrefix(Metric.MICRO);
- public static final LinearUnit MILLIJOULE = Metric.JOULE.withPrefix(Metric.MILLI);
- public static final LinearUnit KILOJOULE = Metric.JOULE.withPrefix(Metric.KILO);
- public static final LinearUnit MEGAJOULE = Metric.JOULE.withPrefix(Metric.MEGA);
+ public static final LinearUnit MICROJOULE = Metric.JOULE
+ .withPrefix(Metric.MICRO);
+ public static final LinearUnit MILLIJOULE = Metric.JOULE
+ .withPrefix(Metric.MILLI);
+ public static final LinearUnit KILOJOULE = Metric.JOULE
+ .withPrefix(Metric.KILO);
+ public static final LinearUnit MEGAJOULE = Metric.JOULE
+ .withPrefix(Metric.MEGA);
- public static final LinearUnit MICROWATT = Metric.WATT.withPrefix(Metric.MICRO);
- public static final LinearUnit MILLIWATT = Metric.WATT.withPrefix(Metric.MILLI);
- public static final LinearUnit KILOWATT = Metric.WATT.withPrefix(Metric.KILO);
- public static final LinearUnit MEGAWATT = Metric.WATT.withPrefix(Metric.MEGA);
+ public static final LinearUnit MICROWATT = Metric.WATT
+ .withPrefix(Metric.MICRO);
+ public static final LinearUnit MILLIWATT = Metric.WATT
+ .withPrefix(Metric.MILLI);
+ public static final LinearUnit KILOWATT = Metric.WATT
+ .withPrefix(Metric.KILO);
+ public static final LinearUnit MEGAWATT = Metric.WATT
+ .withPrefix(Metric.MEGA);
public static final LinearUnit MICROCOULOMB = Metric.COULOMB
.withPrefix(Metric.MICRO);
public static final LinearUnit MILLICOULOMB = Metric.COULOMB
.withPrefix(Metric.MILLI);
- public static final LinearUnit KILOCOULOMB = Metric.COULOMB.withPrefix(Metric.KILO);
- public static final LinearUnit MEGACOULOMB = Metric.COULOMB.withPrefix(Metric.MEGA);
+ public static final LinearUnit KILOCOULOMB = Metric.COULOMB
+ .withPrefix(Metric.KILO);
+ public static final LinearUnit MEGACOULOMB = Metric.COULOMB
+ .withPrefix(Metric.MEGA);
- public static final LinearUnit MICROAMPERE = Metric.AMPERE.withPrefix(Metric.MICRO);
- public static final LinearUnit MILLIAMPERE = Metric.AMPERE.withPrefix(Metric.MILLI);
+ public static final LinearUnit MICROAMPERE = Metric.AMPERE
+ .withPrefix(Metric.MICRO);
+ public static final LinearUnit MILLIAMPERE = Metric.AMPERE
+ .withPrefix(Metric.MILLI);
- public static final LinearUnit MICROVOLT = Metric.VOLT.withPrefix(Metric.MICRO);
- public static final LinearUnit MILLIVOLT = Metric.VOLT.withPrefix(Metric.MILLI);
- public static final LinearUnit KILOVOLT = Metric.VOLT.withPrefix(Metric.KILO);
- public static final LinearUnit MEGAVOLT = Metric.VOLT.withPrefix(Metric.MEGA);
+ public static final LinearUnit MICROVOLT = Metric.VOLT
+ .withPrefix(Metric.MICRO);
+ public static final LinearUnit MILLIVOLT = Metric.VOLT
+ .withPrefix(Metric.MILLI);
+ public static final LinearUnit KILOVOLT = Metric.VOLT
+ .withPrefix(Metric.KILO);
+ public static final LinearUnit MEGAVOLT = Metric.VOLT
+ .withPrefix(Metric.MEGA);
public static final LinearUnit KILOOHM = Metric.OHM.withPrefix(Metric.KILO);
public static final LinearUnit MEGAOHM = Metric.OHM.withPrefix(Metric.MEGA);
diff --git a/src/main/java/sevenUnits/unit/MultiUnit.java b/src/main/java/sevenUnits/unit/MultiUnit.java
index 83cdb03..bc240e3 100644
--- a/src/main/java/sevenUnits/unit/MultiUnit.java
+++ b/src/main/java/sevenUnits/unit/MultiUnit.java
@@ -20,6 +20,7 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
+import sevenUnits.utils.NameSymbol;
import sevenUnits.utils.ObjectProduct;
/**
diff --git a/src/main/java/sevenUnits/unit/Unit.java b/src/main/java/sevenUnits/unit/Unit.java
index 005b6f7..14478ba 100644
--- a/src/main/java/sevenUnits/unit/Unit.java
+++ b/src/main/java/sevenUnits/unit/Unit.java
@@ -22,6 +22,8 @@ import java.util.Objects;
import java.util.function.DoubleUnaryOperator;
import sevenUnits.utils.DecimalComparison;
+import sevenUnits.utils.NameSymbol;
+import sevenUnits.utils.Nameable;
import sevenUnits.utils.ObjectProduct;
/**
@@ -188,7 +190,7 @@ public abstract class Unit implements Nameable {
*
* @implSpec This method is used by {@link #convertTo}, and its behaviour
* affects the behaviour of {@code convertTo}.
- *
+ *
* @param value value expressed in <b>base</b> unit
* @return value expressed in <b>this</b> unit
* @since 2018-12-22
@@ -204,7 +206,7 @@ public abstract class Unit implements Nameable {
* {@code other.convertFromBase(this.convertToBase(value))}.
* Therefore, overriding either of those methods will change the
* output of this method.
- *
+ *
* @param other unit to convert to
* @param value value to convert
* @return converted value
@@ -231,7 +233,7 @@ public abstract class Unit implements Nameable {
* {@code other.convertFromBase(this.convertToBase(value))}.
* Therefore, overriding either of those methods will change the
* output of this method.
- *
+ *
* @param other unitlike form to convert to
* @param value value to convert
* @param <W> type of value to convert to
@@ -266,7 +268,7 @@ public abstract class Unit implements Nameable {
*
* @implSpec This method is used by {@link #convertTo}, and its behaviour
* affects the behaviour of {@code convertTo}.
- *
+ *
* @param value value expressed in <b>this</b> unit
* @return value expressed in <b>base</b> unit
* @since 2018-12-22
@@ -347,16 +349,34 @@ public abstract class Unit implements Nameable {
.equals(Math.log10(linear.getConversionFactor()) % 1.0, 0);
}
+ /**
+ * @return a string representing this unit's definition
+ * @since 2022-03-10
+ */
+ public String toDefinitionString() {
+ if (!this.unitBase.getNameSymbol().isEmpty())
+ return "derived from " + this.unitBase.getName();
+ else
+ return "derived from "
+ + this.getBase().toString(BaseUnit::getShortName);
+ }
+
+ /**
+ * @return a string containing both this unit's name and its definition
+ * @since 2022-03-10
+ */
+ public final String toFullString() {
+ return this.toString() + " (" + this.toDefinitionString() + ")";
+ }
+
@Override
public String toString() {
- return this.getPrimaryName().orElse("Unnamed unit")
- + (this.getSymbol().isPresent()
- ? String.format(" (%s)", this.getSymbol().get())
- : "")
- + ", derived from "
- + this.getBase().toString(u -> u.getSymbol().get())
- + (this.getOtherNames().isEmpty() ? ""
- : ", also called " + String.join(", ", this.getOtherNames()));
+ if (this.nameSymbol.getPrimaryName().isPresent()
+ && this.nameSymbol.getSymbol().isPresent())
+ return this.nameSymbol.getPrimaryName().orElseThrow() + " ("
+ + this.nameSymbol.getSymbol().orElseThrow() + ")";
+ else
+ return this.getName();
}
/**
diff --git a/src/main/java/sevenUnits/unit/UnitDatabase.java b/src/main/java/sevenUnits/unit/UnitDatabase.java
index 18ac619..12b78a7 100644
--- a/src/main/java/sevenUnits/unit/UnitDatabase.java
+++ b/src/main/java/sevenUnits/unit/UnitDatabase.java
@@ -19,7 +19,6 @@ package sevenUnits.unit;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
-import java.math.BigDecimal;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.AbstractSet;
@@ -47,6 +46,7 @@ import java.util.regex.Pattern;
import sevenUnits.utils.ConditionalExistenceCollections;
import sevenUnits.utils.DecimalComparison;
import sevenUnits.utils.ExpressionParser;
+import sevenUnits.utils.NameSymbol;
import sevenUnits.utils.ObjectProduct;
import sevenUnits.utils.UncertainDouble;
@@ -1160,16 +1160,16 @@ public final class UnitDatabase {
}
/**
- * @return true if entry represents a removable duplicate entry of unitMap.
+ * @return true if entry represents a removable duplicate entry of map.
* @since 2021-05-22
*/
- static boolean isRemovableDuplicate(Map<String, Unit> unitMap,
- Entry<String, Unit> entry) {
- for (final Entry<String, Unit> e : unitMap.entrySet()) {
+ static <T> boolean isRemovableDuplicate(Map<String, T> map,
+ Entry<String, T> entry) {
+ for (final Entry<String, T> e : map.entrySet()) {
final String name = e.getKey();
- final Unit value = e.getValue();
+ final T value = e.getValue();
if (lengthFirstComparator.compare(entry.getKey(), name) < 0
- && Objects.equals(unitMap.get(entry.getKey()), value))
+ && Objects.equals(map.get(entry.getKey()), value))
return true;
}
return false;
@@ -1313,9 +1313,11 @@ public final class UnitDatabase {
*/
public void addDimension(final String name,
final ObjectProduct<BaseDimension> dimension) {
- this.dimensions.put(
- Objects.requireNonNull(name, "name must not be null."),
- Objects.requireNonNull(dimension, "dimension must not be null."));
+ Objects.requireNonNull(name, "name may not be null");
+ Objects.requireNonNull(dimension, "dimension may not be null");
+ final ObjectProduct<BaseDimension> namedDimension = dimension
+ .withName(dimension.getNameSymbol().withExtraName(name));
+ this.dimensions.put(name, namedDimension);
}
/**
@@ -1381,8 +1383,11 @@ public final class UnitDatabase {
* @since v0.1.0
*/
public void addPrefix(final String name, final UnitPrefix prefix) {
+ Objects.requireNonNull(prefix, "prefix may not be null");
+ final var namedPrefix = prefix
+ .withName(prefix.getNameSymbol().withExtraName(name));
this.prefixes.put(Objects.requireNonNull(name, "name must not be null."),
- Objects.requireNonNull(prefix, "prefix must not be null."));
+ namedPrefix);
}
/**
@@ -1395,9 +1400,11 @@ public final class UnitDatabase {
* @since v0.1.0
*/
public void addUnit(final String name, final Unit unit) {
+ Objects.requireNonNull(unit, "unit may not be null");
+ final var namedUnit = unit
+ .withName(unit.getNameSymbol().withExtraName(name));
this.prefixlessUnits.put(
- Objects.requireNonNull(name, "name must not be null."),
- Objects.requireNonNull(unit, "unit must not be null."));
+ Objects.requireNonNull(name, "name must not be null."), namedUnit);
}
/**
@@ -1451,7 +1458,8 @@ public final class UnitDatabase {
System.err.printf("Parsing error on line %d:%n", lineCounter);
throw e;
}
- this.addPrefix(name.substring(0, name.length() - 1), prefix);
+ final String prefixName = name.substring(0, name.length() - 1);
+ this.addPrefix(prefixName, prefix);
} else {
// it's a unit, get the unit
final Unit unit;
@@ -1462,13 +1470,23 @@ public final class UnitDatabase {
System.err.printf("Parsing error on line %d:%n", lineCounter);
throw e;
}
-
this.addUnit(name, unit);
}
}
}
/**
+ * Removes all units, prefixes and dimensions from this database.
+ *
+ * @since 2022-02-26
+ */
+ public void clear() {
+ this.dimensions.clear();
+ this.prefixes.clear();
+ this.prefixlessUnits.clear();
+ }
+
+ /**
* Tests if the database has a unit dimension with this name.
*
* @param name name to test
@@ -1685,11 +1703,8 @@ public final class UnitDatabase {
LinearUnitValue getLinearUnitValue(final String name) {
try {
// try to parse it as a number - otherwise it is not a number!
- final BigDecimal number = new BigDecimal(name);
-
- final double uncertainty = Math.pow(10, -number.scale());
return LinearUnitValue.of(Metric.ONE,
- UncertainDouble.of(number.doubleValue(), uncertainty));
+ UncertainDouble.fromRoundedString(name));
} catch (final NumberFormatException e) {
return LinearUnitValue.getExact(this.getLinearUnit(name), 1);
}
@@ -1994,12 +2009,18 @@ public final class UnitDatabase {
}
/**
+ * @param includeDuplicates if false, duplicates are removed from the map
* @return a map mapping prefix names to prefixes
- * @since 2019-04-13
- * @since v0.2.0
+ * @since 2022-04-18
+ * @since v0.4.0
*/
- public Map<String, UnitPrefix> prefixMap() {
- return Collections.unmodifiableMap(this.prefixes);
+ public Map<String, UnitPrefix> prefixMap(boolean includeDuplicates) {
+ if (includeDuplicates)
+ return Collections.unmodifiableMap(this.prefixes);
+ else
+ return Collections.unmodifiableMap(ConditionalExistenceCollections
+ .conditionalExistenceMap(this.prefixes,
+ entry -> !isRemovableDuplicate(this.prefixes, entry)));
}
/**
diff --git a/src/main/java/sevenUnits/unit/UnitPrefix.java b/src/main/java/sevenUnits/unit/UnitPrefix.java
index 308f4b0..e1f7788 100644
--- a/src/main/java/sevenUnits/unit/UnitPrefix.java
+++ b/src/main/java/sevenUnits/unit/UnitPrefix.java
@@ -17,68 +17,59 @@
package sevenUnits.unit;
import java.util.Objects;
-import java.util.Optional;
-import java.util.Set;
import sevenUnits.utils.DecimalComparison;
+import sevenUnits.utils.NameSymbol;
+import sevenUnits.utils.Nameable;
/**
- * A prefix that can be applied to a {@code LinearUnit} to multiply it by some value
+ * A prefix that can be applied to a {@code LinearUnit} to multiply it by some
+ * value
*
* @author Adrien Hopkins
* @since 2019-10-16
*/
-public final class UnitPrefix {
+public final class UnitPrefix implements Nameable {
/**
* Gets a {@code UnitPrefix} from a multiplier
*
- * @param multiplier
- * multiplier of prefix
+ * @param multiplier multiplier of prefix
* @return prefix
* @since 2019-10-16
*/
public static UnitPrefix valueOf(final double multiplier) {
return new UnitPrefix(multiplier, NameSymbol.EMPTY);
}
-
+
/**
* Gets a {@code UnitPrefix} from a multiplier and a name
*
- * @param multiplier
- * multiplier of prefix
- * @param ns
- * name(s) and symbol of prefix
+ * @param multiplier multiplier of prefix
+ * @param ns name(s) and symbol of prefix
* @return prefix
* @since 2019-10-16
- * @throws NullPointerException
- * if ns is null
+ * @throws NullPointerException if ns is null
*/
- public static UnitPrefix valueOf(final double multiplier, final NameSymbol ns) {
- return new UnitPrefix(multiplier, Objects.requireNonNull(ns, "ns must not be null."));
+ public static UnitPrefix valueOf(final double multiplier,
+ final NameSymbol ns) {
+ return new UnitPrefix(multiplier,
+ Objects.requireNonNull(ns, "ns must not be null."));
}
-
- /**
- * This prefix's primary name
- */
- private final Optional<String> primaryName;
-
- /**
- * This prefix's symbol
- */
- private final Optional<String> symbol;
-
+
/**
- * Other names and symbols used by this prefix
+ * This prefix's name(s) and symbol.
+ *
+ * @since 2022-04-16
*/
- private final Set<String> otherNames;
-
+ private final NameSymbol nameSymbol;
+
/**
* The number that this prefix multiplies units by
*
* @since 2019-10-16
*/
private final double multiplier;
-
+
/**
* Creates the {@code DefaultUnitPrefix}.
*
@@ -88,28 +79,24 @@ public final class UnitPrefix {
*/
private UnitPrefix(final double multiplier, final NameSymbol ns) {
this.multiplier = multiplier;
- this.primaryName = ns.getPrimaryName();
- this.symbol = ns.getSymbol();
- this.otherNames = ns.getOtherNames();
+ this.nameSymbol = ns;
}
-
+
/**
* Divides this prefix by a scalar
*
- * @param divisor
- * number to divide by
+ * @param divisor number to divide by
* @return quotient of prefix and scalar
* @since 2019-10-16
*/
public UnitPrefix dividedBy(final double divisor) {
return valueOf(this.getMultiplier() / divisor);
}
-
+
/**
* Divides this prefix by {@code other}.
*
- * @param other
- * prefix to divide by
+ * @param other prefix to divide by
* @return quotient of prefixes
* @since 2019-04-13
* @since v0.2.0
@@ -117,7 +104,7 @@ public final class UnitPrefix {
public UnitPrefix dividedBy(final UnitPrefix other) {
return valueOf(this.getMultiplier() / other.getMultiplier());
}
-
+
/**
* {@inheritDoc}
*
@@ -132,9 +119,10 @@ public final class UnitPrefix {
if (!(obj instanceof UnitPrefix))
return false;
final UnitPrefix other = (UnitPrefix) obj;
- return DecimalComparison.equals(this.getMultiplier(), other.getMultiplier());
+ return DecimalComparison.equals(this.getMultiplier(),
+ other.getMultiplier());
}
-
+
/**
* @return prefix's multiplier
* @since 2019-11-26
@@ -142,31 +130,12 @@ public final class UnitPrefix {
public double getMultiplier() {
return this.multiplier;
}
-
- /**
- * @return other names
- * @since 2019-11-26
- */
- public final Set<String> getOtherNames() {
- return this.otherNames;
- }
-
- /**
- * @return primary name
- * @since 2019-11-26
- */
- public final Optional<String> getPrimaryName() {
- return this.primaryName;
- }
-
- /**
- * @return symbol
- * @since 2019-11-26
- */
- public final Optional<String> getSymbol() {
- return this.symbol;
+
+ @Override
+ public NameSymbol getNameSymbol() {
+ return this.nameSymbol;
}
-
+
/**
* {@inheritDoc}
*
@@ -176,24 +145,22 @@ public final class UnitPrefix {
public int hashCode() {
return DecimalComparison.hash(this.getMultiplier());
}
-
+
/**
* Multiplies this prefix by a scalar
*
- * @param multiplicand
- * number to multiply by
+ * @param multiplicand number to multiply by
* @return product of prefix and scalar
* @since 2019-10-16
*/
public UnitPrefix times(final double multiplicand) {
return valueOf(this.getMultiplier() * multiplicand);
}
-
+
/**
* Multiplies this prefix by {@code other}.
*
- * @param other
- * prefix to multiply by
+ * @param other prefix to multiply by
* @return product of prefixes
* @since 2019-04-13
* @since v0.2.0
@@ -201,12 +168,11 @@ public final class UnitPrefix {
public UnitPrefix times(final UnitPrefix other) {
return valueOf(this.getMultiplier() * other.getMultiplier());
}
-
+
/**
* Raises this prefix to an exponent.
*
- * @param exponent
- * exponent to raise to
+ * @param exponent exponent to raise to
* @return result of exponentiation.
* @since 2019-04-13
* @since v0.2.0
@@ -214,27 +180,27 @@ public final class UnitPrefix {
public UnitPrefix toExponent(final double exponent) {
return valueOf(Math.pow(this.getMultiplier(), exponent));
}
-
+
/**
* @return a string describing the prefix and its multiplier
*/
@Override
public String toString() {
- if (this.primaryName.isPresent())
- return String.format("%s (\u00D7 %s)", this.primaryName.get(), this.multiplier);
- else if (this.symbol.isPresent())
- return String.format("%s (\u00D7 %s)", this.symbol.get(), this.multiplier);
+ if (this.getPrimaryName().isPresent())
+ return String.format("%s (\u00D7 %s)", this.getPrimaryName().get(),
+ this.multiplier);
+ else if (this.getSymbol().isPresent())
+ return String.format("%s (\u00D7 %s)", this.getSymbol().get(),
+ this.multiplier);
else
return String.format("Unit Prefix (\u00D7 %s)", this.multiplier);
}
-
+
/**
- * @param ns
- * name(s) and symbol to use
+ * @param ns name(s) and symbol to use
* @return copy of this prefix with provided name(s) and symbol
* @since 2019-11-26
- * @throws NullPointerException
- * if ns is null
+ * @throws NullPointerException if ns is null
*/
public UnitPrefix withName(final NameSymbol ns) {
return valueOf(this.multiplier, ns);
diff --git a/src/main/java/sevenUnits/unit/UnitType.java b/src/main/java/sevenUnits/unit/UnitType.java
new file mode 100644
index 0000000..7cebf2d
--- /dev/null
+++ b/src/main/java/sevenUnits/unit/UnitType.java
@@ -0,0 +1,58 @@
+/**
+ * Copyright (C) 2022 Adrien Hopkins
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package sevenUnits.unit;
+
+import java.util.function.Predicate;
+
+/**
+ * A type of unit, as chosen by the type of system it is in.
+ * <ul>
+ * <li>{@code METRIC} refers to metric/SI units that pass {@link Unit#isMetric}
+ * <li>{@code SEMI_METRIC} refers to the degree Celsius (which is an official SI
+ * unit but does not pass {@link Unit#isMetric}) and non-metric units intended
+ * for use with the SI.
+ * <li>{@code NON_METRIC} refers to units that are neither metric nor intended
+ * for use with the metric system (e.g. imperial and customary units)
+ * </ul>
+ *
+ * @since 2022-04-10
+ */
+public enum UnitType {
+ METRIC, SEMI_METRIC, NON_METRIC;
+
+ /**
+ * Determines which type a unit is. The type will be:
+ * <ul>
+ * <li>{@code SEMI_METRIC} if the unit passes the provided predicate
+ * <li>{@code METRIC} if it fails the predicate but is metric
+ * <li>{@code NON_METRIC} if it fails the predicate and is not metric
+ * </ul>
+ *
+ * @param u unit to test
+ * @param isSemiMetric predicate to determine if a unit is semi-metric
+ * @return type of unit
+ * @since 2022-04-18
+ */
+ public static final UnitType getType(Unit u, Predicate<Unit> isSemiMetric) {
+ if (isSemiMetric.test(u))
+ return SEMI_METRIC;
+ else if (u.isMetric())
+ return METRIC;
+ else
+ return NON_METRIC;
+ }
+}
diff --git a/src/main/java/sevenUnits/unit/UnitValue.java b/src/main/java/sevenUnits/unit/UnitValue.java
index f6d18f8..339263d 100644
--- a/src/main/java/sevenUnits/unit/UnitValue.java
+++ b/src/main/java/sevenUnits/unit/UnitValue.java
@@ -19,6 +19,8 @@ package sevenUnits.unit;
import java.util.Objects;
import java.util.Optional;
+import sevenUnits.utils.NameSymbol;
+
/**
* A value expressed in a unit.
*
diff --git a/src/main/java/sevenUnits/unit/Unitlike.java b/src/main/java/sevenUnits/unit/Unitlike.java
index d2dcbbb..68de2c2 100644
--- a/src/main/java/sevenUnits/unit/Unitlike.java
+++ b/src/main/java/sevenUnits/unit/Unitlike.java
@@ -22,6 +22,8 @@ import java.util.Objects;
import java.util.function.DoubleFunction;
import java.util.function.ToDoubleFunction;
+import sevenUnits.utils.NameSymbol;
+import sevenUnits.utils.Nameable;
import sevenUnits.utils.ObjectProduct;
/**
diff --git a/src/main/java/sevenUnits/unit/UnitlikeValue.java b/src/main/java/sevenUnits/unit/UnitlikeValue.java
index edc13ca..26354b1 100644
--- a/src/main/java/sevenUnits/unit/UnitlikeValue.java
+++ b/src/main/java/sevenUnits/unit/UnitlikeValue.java
@@ -18,6 +18,8 @@ package sevenUnits.unit;
import java.util.Optional;
+import sevenUnits.utils.NameSymbol;
+
/**
*
* @since 2020-09-07
diff --git a/src/main/java/sevenUnits/unit/NameSymbol.java b/src/main/java/sevenUnits/utils/NameSymbol.java
index 3e26138..7ef2967 100644
--- a/src/main/java/sevenUnits/unit/NameSymbol.java
+++ b/src/main/java/sevenUnits/utils/NameSymbol.java
@@ -14,7 +14,7 @@
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-package sevenUnits.unit;
+package sevenUnits.utils;
import java.util.Arrays;
import java.util.Collections;
@@ -38,7 +38,7 @@ public final class NameSymbol {
* Creates a {@code NameSymbol}, ensuring that if primaryName is null and
* otherNames is not empty, one name is moved from otherNames to primaryName
*
- * Ensure that otherNames is a copy of the inputted argument.
+ * Ensure that otherNames is not a copy of the inputted argument.
*/
private static final NameSymbol create(final String name,
final String symbol, final Set<String> otherNames) {
@@ -277,4 +277,32 @@ public final class NameSymbol {
// if primaryName is empty, otherNames must also be empty
return this.primaryName.isEmpty() && this.symbol.isEmpty();
}
+
+ @Override
+ public String toString() {
+ if (this.isEmpty())
+ return "NameSymbol.EMPTY";
+ else if (this.primaryName.isPresent() && this.symbol.isPresent())
+ return this.primaryName.orElseThrow() + " ("
+ + this.symbol.orElseThrow() + ")";
+ else
+ return this.primaryName.orElseGet(this.symbol::orElseThrow);
+ }
+
+ /**
+ * Creates and returns a copy of this {@code NameSymbol} with the provided
+ * extra name. If this {@code NameSymbol} has a primary name, the provided
+ * name will become an other name, otherwise it will become the primary name.
+ *
+ * @since 2022-04-19
+ */
+ public final NameSymbol withExtraName(String name) {
+ if (this.primaryName.isPresent()) {
+ final var otherNames = new HashSet<>(this.otherNames);
+ otherNames.add(name);
+ return NameSymbol.ofNullable(this.primaryName.orElse(null),
+ this.symbol.orElse(null), otherNames);
+ } else
+ return NameSymbol.ofNullable(name, this.symbol.orElse(null));
+ }
} \ No newline at end of file
diff --git a/src/main/java/sevenUnits/unit/Nameable.java b/src/main/java/sevenUnits/utils/Nameable.java
index ed23687..e469d04 100644
--- a/src/main/java/sevenUnits/unit/Nameable.java
+++ b/src/main/java/sevenUnits/utils/Nameable.java
@@ -14,7 +14,7 @@
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-package sevenUnits.unit;
+package sevenUnits.utils;
import java.util.Optional;
import java.util.Set;
@@ -27,6 +27,16 @@ import java.util.Set;
*/
public interface Nameable {
/**
+ * @return a name for the object - if there's a primary name, it's that,
+ * otherwise the symbol, otherwise "Unnamed"
+ * @since 2022-02-26
+ */
+ default String getName() {
+ final NameSymbol ns = this.getNameSymbol();
+ return ns.getPrimaryName().or(ns::getSymbol).orElse("Unnamed");
+ }
+
+ /**
* @return a {@code NameSymbol} that contains this object's primary name,
* symbol and other names
* @since 2020-09-07
@@ -50,6 +60,16 @@ public interface Nameable {
}
/**
+ * @return a short name for the object - if there's a symbol, it's that,
+ * otherwise the symbol, otherwise "Unnamed"
+ * @since 2022-02-26
+ */
+ default String getShortName() {
+ final NameSymbol ns = this.getNameSymbol();
+ return ns.getSymbol().or(ns::getPrimaryName).orElse("Unnamed");
+ }
+
+ /**
* @return short symbol representing object
* @since 2020-09-07
*/
diff --git a/src/main/java/sevenUnits/utils/ObjectProduct.java b/src/main/java/sevenUnits/utils/ObjectProduct.java
index 5b1b739..66bb773 100644
--- a/src/main/java/sevenUnits/utils/ObjectProduct.java
+++ b/src/main/java/sevenUnits/utils/ObjectProduct.java
@@ -33,7 +33,7 @@ import java.util.function.Function;
* @author Adrien Hopkins
* @since 2019-10-16
*/
-public final class ObjectProduct<T> {
+public class ObjectProduct<T> implements Nameable {
/**
* Returns an empty ObjectProduct of a certain type
*
@@ -83,15 +83,32 @@ public final class ObjectProduct<T> {
final Map<T, Integer> exponents;
/**
- * Creates the {@code ObjectProduct}.
+ * The object's name and symbol
+ */
+ private final NameSymbol nameSymbol;
+
+ /**
+ * Creates a {@code ObjectProduct} without a name/symbol.
*
* @param exponents objects that make up this product
* @since 2019-10-16
*/
- private ObjectProduct(final Map<T, Integer> exponents) {
+ ObjectProduct(final Map<T, Integer> exponents) {
+ this(exponents, NameSymbol.EMPTY);
+ }
+
+ /**
+ * Creates the {@code ObjectProduct}.
+ *
+ * @param exponents objects that make up this product
+ * @param nameSymbol name and symbol of object product
+ * @since 2019-10-16
+ */
+ ObjectProduct(final Map<T, Integer> exponents, NameSymbol nameSymbol) {
this.exponents = Collections.unmodifiableMap(
ConditionalExistenceCollections.conditionalExistenceMap(exponents,
e -> !Integer.valueOf(0).equals(e.getValue())));
+ this.nameSymbol = nameSymbol;
}
/**
@@ -171,6 +188,11 @@ public final class ObjectProduct<T> {
}
@Override
+ public NameSymbol getNameSymbol() {
+ return this.nameSymbol;
+ }
+
+ @Override
public int hashCode() {
return Objects.hash(this.exponents);
}
@@ -235,16 +257,19 @@ public final class ObjectProduct<T> {
/**
* Converts this product to a string using the objects'
- * {@link Object#toString()} method. If objects have a long toString
- * representation, it is recommended to use {@link #toString(Function)}
- * instead to shorten the returned string.
+ * {@link Object#toString()} method (or {@link Nameable#getShortName} if
+ * available). If objects have a long toString representation, it is
+ * recommended to use {@link #toString(Function)} instead to shorten the
+ * returned string.
*
* <p>
* {@inheritDoc}
*/
@Override
public String toString() {
- return this.toString(Object::toString);
+ return this
+ .toString(o -> o instanceof Nameable ? ((Nameable) o).getShortName()
+ : o.toString());
}
/**
@@ -280,4 +305,13 @@ public final class ObjectProduct<T> {
return positiveString + negativeString;
}
+
+ /**
+ * @return named version of this {@code ObjectProduct}, using data from
+ * {@code nameSymbol}
+ * @since 2021-12-15
+ */
+ public ObjectProduct<T> withName(NameSymbol nameSymbol) {
+ return new ObjectProduct<>(this.exponents, nameSymbol);
+ }
}
diff --git a/src/main/java/sevenUnits/utils/SemanticVersionNumber.java b/src/main/java/sevenUnits/utils/SemanticVersionNumber.java
new file mode 100644
index 0000000..06417c5
--- /dev/null
+++ b/src/main/java/sevenUnits/utils/SemanticVersionNumber.java
@@ -0,0 +1,691 @@
+/**
+ * Copyright (C) 2022 Adrien Hopkins
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package sevenUnits.utils;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Objects;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * A version number in the <a href="https://semver.org">Semantic Versioning</a>
+ * scheme
+ * <p>
+ * Each version number has three main parts:
+ * <ol>
+ * <li>The major version, which increments when backwards incompatible changes
+ * are made
+ * <li>The minor version, which increments when backwards compatible feature
+ * changes are made
+ * <li>The patch version, which increments when backwards compatible bug fixes
+ * are made
+ * </ol>
+ *
+ * @since 2022-02-19
+ */
+public final class SemanticVersionNumber
+ implements Comparable<SemanticVersionNumber> {
+ /**
+ * A builder that can be used to create complex version numbers.
+ * <p>
+ * Note: None of this builder's methods tolerate null arguments, arrays
+ * containing nulls, negative numbers, or non-alphanumeric identifiers. Nulls
+ * throw NullPointerExceptions, everything else throws
+ * IllegalArgumentException.
+ *
+ * @since 2022-02-19
+ */
+ public static final class Builder {
+ private final int major;
+ private final int minor;
+ private final int patch;
+ private final List<String> preReleaseIdentifiers;
+ private final List<String> buildMetadata;
+
+ /**
+ * Creates a builder which can be used to create a
+ * {@code SemanticVersionNumber}
+ *
+ * @param major major version number of final version
+ * @param minor minor version number of final version
+ * @param patch patch version number of final version
+ * @since 2022-02-19
+ */
+ private Builder(int major, int minor, int patch) {
+ this.major = major;
+ this.minor = minor;
+ this.patch = patch;
+ this.preReleaseIdentifiers = new ArrayList<>();
+ this.buildMetadata = new ArrayList<>();
+ }
+
+ /**
+ * @return version number created by this builder
+ * @since 2022-02-19
+ */
+ public SemanticVersionNumber build() {
+ return new SemanticVersionNumber(this.major, this.minor, this.patch,
+ this.preReleaseIdentifiers, this.buildMetadata);
+ }
+
+ /**
+ * Adds one or more build metadata identifiers
+ *
+ * @param identifiers build metadata
+ * @return this builder
+ * @since 2022-02-19
+ */
+ public Builder buildMetadata(List<String> identifiers) {
+ Objects.requireNonNull(identifiers, "identifiers may not be null");
+ for (final String identifier : identifiers) {
+ Objects.requireNonNull(identifier, "identifier may not be null");
+ if (!VALID_IDENTIFIER.matcher(identifier).matches())
+ throw new IllegalArgumentException(
+ String.format("Invalid identifier \"%s\"", identifier));
+ this.buildMetadata.add(identifier);
+ }
+ return this;
+ }
+
+ /**
+ * Adds one or more build metadata identifiers
+ *
+ * @param identifiers build metadata
+ * @return this builder
+ * @since 2022-02-19
+ */
+ public Builder buildMetadata(String... identifiers) {
+ Objects.requireNonNull(identifiers, "identifiers may not be null");
+ for (final String identifier : identifiers) {
+ Objects.requireNonNull(identifier, "identifier may not be null");
+ if (!VALID_IDENTIFIER.matcher(identifier).matches())
+ throw new IllegalArgumentException(
+ String.format("Invalid identifier \"%s\"", identifier));
+ this.buildMetadata.add(identifier);
+ }
+ return this;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (!(obj instanceof Builder))
+ return false;
+ final Builder other = (Builder) obj;
+ return Objects.equals(this.buildMetadata, other.buildMetadata)
+ && this.major == other.major && this.minor == other.minor
+ && this.patch == other.patch && Objects.equals(
+ this.preReleaseIdentifiers, other.preReleaseIdentifiers);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(this.buildMetadata, this.major, this.minor,
+ this.patch, this.preReleaseIdentifiers);
+ }
+
+ /**
+ * Adds one or more numeric identifiers to the version number
+ *
+ * @param identifiers pre-release identifier(s) to add
+ * @return this builder
+ * @since 2022-02-19
+ */
+ public Builder preRelease(int... identifiers) {
+ Objects.requireNonNull(identifiers, "identifiers may not be null");
+ for (final int identifier : identifiers) {
+ if (identifier < 0)
+ throw new IllegalArgumentException(
+ "Numeric identifiers may not be negative");
+ this.preReleaseIdentifiers.add(Integer.toString(identifier));
+ }
+ return this;
+ }
+
+ /**
+ * Adds one or more pre-release identifier(s) to the version number
+ *
+ * @param identifiers pre-release identifier(s) to add
+ * @return this builder
+ * @since 2022-02-19
+ */
+ public Builder preRelease(List<String> identifiers) {
+ Objects.requireNonNull(identifiers, "identifiers may not be null");
+ for (final String identifier : identifiers) {
+ Objects.requireNonNull(identifier, "identifier may not be null");
+ if (!VALID_IDENTIFIER.matcher(identifier).matches())
+ throw new IllegalArgumentException(
+ String.format("Invalid identifier \"%s\"", identifier));
+ this.preReleaseIdentifiers.add(identifier);
+ }
+ return this;
+ }
+
+ /**
+ * Adds one or more pre-release identifier(s) to the version number
+ *
+ * @param identifiers pre-release identifier(s) to add
+ * @return this builder
+ * @since 2022-02-19
+ */
+ public Builder preRelease(String... identifiers) {
+ Objects.requireNonNull(identifiers, "identifiers may not be null");
+ for (final String identifier : identifiers) {
+ Objects.requireNonNull(identifier, "identifier may not be null");
+ if (!VALID_IDENTIFIER.matcher(identifier).matches())
+ throw new IllegalArgumentException(
+ String.format("Invalid identifier \"%s\"", identifier));
+ this.preReleaseIdentifiers.add(identifier);
+ }
+ return this;
+ }
+
+ /**
+ * Adds a string identifier and an integer identifer to pre-release data
+ *
+ * @param identifier1 first identifier
+ * @param identifier2 second identifier
+ * @return this builder
+ * @since 2022-02-19
+ */
+ public Builder preRelease(String identifier1, int identifier2) {
+ Objects.requireNonNull(identifier1, "identifier1 may not be null");
+ if (!VALID_IDENTIFIER.matcher(identifier1).matches())
+ throw new IllegalArgumentException(
+ String.format("Invalid identifier \"%s\"", identifier1));
+ if (identifier2 < 0)
+ throw new IllegalArgumentException(
+ "Integer identifier cannot be negative");
+ this.preReleaseIdentifiers.add(identifier1);
+ this.preReleaseIdentifiers.add(Integer.toString(identifier2));
+ return this;
+ }
+
+ @Override
+ public String toString() {
+ return "Semantic Version Builder: " + this.build().toString();
+ }
+ }
+
+ /**
+ * An alternative comparison method for version numbers. This uses the
+ * version's natural order, but the build metadata will be compared (using
+ * the same rules as pre-release identifiers) if everything else is equal.
+ * <p>
+ * This ordering is consistent with equals, unlike
+ * {@code SemanticVersionNumber}'s natural ordering.
+ */
+ public static final Comparator<SemanticVersionNumber> BUILD_METADATA_COMPARATOR = new Comparator<>() {
+ @Override
+ public int compare(SemanticVersionNumber o1, SemanticVersionNumber o2) {
+ Objects.requireNonNull(o1, "o1 may not be null");
+ Objects.requireNonNull(o2, "o2 may not be null");
+ final int naturalComparison = o1.compareTo(o2);
+ if (naturalComparison == 0)
+ return SemanticVersionNumber.compareIdentifiers(o1.buildMetadata,
+ o2.buildMetadata);
+ else
+ return naturalComparison;
+ };
+ };
+
+ /** The alphanumeric pattern all identifiers must follow */
+ private static final Pattern VALID_IDENTIFIER = Pattern
+ .compile("[0-9A-Za-z-]+");
+
+ /** The numeric pattern which causes special behaviour */
+ private static final Pattern NUMERIC_IDENTIFER = Pattern.compile("[0-9]+");
+
+ /** The pattern for a version number */
+ private static final Pattern VERSION_NUMBER = Pattern
+ .compile("(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)" // main
+ // version
+ + "(?:-([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?" // pre-release
+ + "(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?"); // build data
+
+ /**
+ * Creates a builder that can be used to create a version number
+ *
+ * @param major major version number of final version
+ * @param minor minor version number of final version
+ * @param patch patch version number of final version
+ * @return version number builder
+ * @throws IllegalArgumentException if any argument is negative
+ * @since 2022-02-19
+ */
+ public static final SemanticVersionNumber.Builder builder(int major,
+ int minor, int patch) {
+ if (major < 0)
+ throw new IllegalArgumentException(
+ "Major version must be non-negative.");
+ if (minor < 0)
+ throw new IllegalArgumentException(
+ "Minor version must be non-negative.");
+ if (patch < 0)
+ throw new IllegalArgumentException(
+ "Patch version must be non-negative.");
+ return new SemanticVersionNumber.Builder(major, minor, patch);
+ }
+
+ /**
+ * Compares two lists of strings based on SemVer's precedence rules
+ *
+ * @param a first list
+ * @param b second list
+ * @return result of comparison as in a comparator
+ * @see Comparator
+ * @since 2022-02-20
+ */
+ private static final int compareIdentifiers(List<String> a, List<String> b) {
+ // test pre-release size
+ final int aSize = a.size();
+ final int bSize = b.size();
+
+ // no identifiers is greater than any identifiers
+ if (aSize != 0 && bSize == 0)
+ return -1;
+ else if (aSize == 0 && bSize != 0)
+ return 1;
+
+ // test identifiers one by one
+ for (int i = 0; i < Math.min(aSize, bSize); i++) {
+ final String aElement = a.get(i);
+ final String bElement = b.get(i);
+
+ if (NUMERIC_IDENTIFER.matcher(aElement).matches()) {
+ if (NUMERIC_IDENTIFER.matcher(bElement).matches()) {
+ // both are numbers, compare them
+ final int aNumber = Integer.parseInt(aElement);
+ final int bNumber = Integer.parseInt(bElement);
+
+ if (aNumber < bNumber)
+ return -1;
+ else if (aNumber > bNumber)
+ return 1;
+ } else
+ // aElement is a number and bElement is not a number
+ // by the rules, a goes before b
+ return -1;
+ } else {
+ if (NUMERIC_IDENTIFER.matcher(bElement).matches())
+ // aElement is not a number but bElement is
+ // by the rules, a goes after b
+ return 1;
+ else {
+ // both are not numbers, compare them
+ final int comparison = aElement.compareTo(bElement);
+ if (comparison != 0)
+ return comparison;
+ }
+ }
+ }
+
+ // we just tested the stuff that's in common, maybe someone has more
+ if (aSize < bSize)
+ return -1;
+ else if (aSize > bSize)
+ return 1;
+ else
+ return 0;
+ }
+
+ /**
+ * Gets a version number from a string in the official format
+ *
+ * @param versionString string to parse
+ * @return {@code SemanticVersionNumber} instance
+ * @since 2022-02-19
+ * @see {@link #toString}
+ */
+ public static final SemanticVersionNumber fromString(String versionString) {
+ // parse & validate version string
+ Objects.requireNonNull(versionString, "versionString may not be null");
+ final Matcher m = VERSION_NUMBER.matcher(versionString);
+ if (!m.matches())
+ throw new IllegalArgumentException(
+ String.format("Provided string \"%s\" is not a version number",
+ versionString));
+
+ // main parts
+ final int major = Integer.parseInt(m.group(1));
+ final int minor = Integer.parseInt(m.group(2));
+ final int patch = Integer.parseInt(m.group(3));
+
+ // pre release
+ final List<String> preRelease;
+ if (m.group(4) == null) {
+ preRelease = List.of();
+ } else {
+ preRelease = Arrays.asList(m.group(4).split("\\."));
+ }
+
+ // build metadata
+ final List<String> buildMetadata;
+ if (m.group(5) == null) {
+ buildMetadata = List.of();
+ } else {
+ buildMetadata = Arrays.asList(m.group(5).split("\\."));
+ }
+
+ // return number
+ return new SemanticVersionNumber(major, minor, patch, preRelease,
+ buildMetadata);
+ }
+
+ /**
+ * Tests whether a string is a valid Semantic Version string
+ *
+ * @param versionString string to test
+ * @return true iff string is valid
+ * @since 2022-02-19
+ */
+ public static final boolean isValidVersionString(String versionString) {
+ return VERSION_NUMBER.matcher(versionString).matches();
+ }
+
+ /**
+ * Creates a simple pre-release version number of the form
+ * MAJOR.MINOR.PATH-TYPE.NUMBER (e.g. 1.2.3-alpha.4).
+ *
+ * @param major major version number
+ * @param minor minor version number
+ * @param patch patch version number
+ * @param preReleaseType first pre-release element
+ * @param preReleaseNumber second pre-release element
+ * @return {@code SemanticVersionNumber} instance
+ * @throws IllegalArgumentException if any argument is negative or if the
+ * preReleaseType is null, empty or not
+ * alphanumeric (0-9, A-Z, a-z, - only)
+ * @since 2022-02-19
+ */
+ public static final SemanticVersionNumber preRelease(int major, int minor,
+ int patch, String preReleaseType, int preReleaseNumber) {
+ if (major < 0)
+ throw new IllegalArgumentException(
+ "Major version must be non-negative.");
+ if (minor < 0)
+ throw new IllegalArgumentException(
+ "Minor version must be non-negative.");
+ if (patch < 0)
+ throw new IllegalArgumentException(
+ "Patch version must be non-negative.");
+ Objects.requireNonNull(preReleaseType, "preReleaseType may not be null");
+ if (!VALID_IDENTIFIER.matcher(preReleaseType).matches())
+ throw new IllegalArgumentException(
+ String.format("Invalid identifier \"%s\".", preReleaseType));
+ if (preReleaseNumber < 0)
+ throw new IllegalArgumentException(
+ "Pre-release number must be non-negative.");
+ return new SemanticVersionNumber(major, minor, patch,
+ List.of(preReleaseType, Integer.toString(preReleaseNumber)),
+ List.of());
+ }
+
+ /**
+ * Creates a {@code SemanticVersionNumber} instance without pre-release
+ * identifiers or build metadata.
+ * <p>
+ * Note: this method allows you to create versions with major version number
+ * 0, even though these versions would not be considered stable.
+ *
+ * @param major major version number
+ * @param minor minor version number
+ * @param patch patch version number
+ * @return {@code SemanticVersionNumber} instance
+ * @throws IllegalArgumentException if any argument is negative
+ * @since 2022-02-19
+ */
+ public static final SemanticVersionNumber stableVersion(int major, int minor,
+ int patch) {
+ if (major < 0)
+ throw new IllegalArgumentException(
+ "Major version must be non-negative.");
+ if (minor < 0)
+ throw new IllegalArgumentException(
+ "Minor version must be non-negative.");
+ if (patch < 0)
+ throw new IllegalArgumentException(
+ "Patch version must be non-negative.");
+ return new SemanticVersionNumber(major, minor, patch, List.of(),
+ List.of());
+ }
+
+ // parts of the version number
+ private final int major;
+ private final int minor;
+ private final int patch;
+ private final List<String> preReleaseIdentifiers;
+ private final List<String> buildMetadata;
+
+ /**
+ * Creates a version number
+ *
+ * @param major major version number
+ * @param minor minor version number
+ * @param patch patch version number
+ * @param preReleaseIdentifiers pre-release version data
+ * @param buildMetadata build metadata
+ * @since 2022-02-19
+ */
+ private SemanticVersionNumber(int major, int minor, int patch,
+ List<String> preReleaseIdentifiers, List<String> buildMetadata) {
+ this.major = major;
+ this.minor = minor;
+ this.patch = patch;
+ this.preReleaseIdentifiers = preReleaseIdentifiers;
+ this.buildMetadata = buildMetadata;
+ }
+
+ /**
+ * @return build metadata (empty if there is none)
+ * @since 2022-02-19
+ */
+ public List<String> buildMetadata() {
+ return Collections.unmodifiableList(this.buildMetadata);
+ }
+
+ /**
+ * Compares two version numbers according to the official Semantic Versioning
+ * order.
+ * <p>
+ * Note: this ordering is not consistent with equals. Specifically, two
+ * versions that are identical except for their build metadata will be
+ * considered different by equals but the same by this method. This is
+ * required to follow the official Semantic Versioning specification.
+ * <p>
+ */
+ @Override
+ public int compareTo(SemanticVersionNumber o) {
+ // test the three big numbers in order first
+ if (this.major < o.major)
+ return -1;
+ else if (this.major > o.major)
+ return 1;
+
+ if (this.minor < o.minor)
+ return -1;
+ else if (this.minor > o.minor)
+ return 1;
+
+ if (this.patch < o.patch)
+ return -1;
+ else if (this.patch > o.patch)
+ return 1;
+
+ // now we just compare pre-release identifiers
+ // (remember: build metadata is ignored)
+ return SemanticVersionNumber.compareIdentifiers(this.preReleaseIdentifiers,
+ o.preReleaseIdentifiers);
+ }
+
+ /**
+ * Determines the compatibility of code written for this version to
+ * {@code other}. More specifically:
+ * <p>
+ * If this function returns <b>true</b>, then there should be no problems
+ * upgrading code written for this version to version {@code other} as long
+ * as:
+ * <ul>
+ * <li>Semantic Versioning is being used properly
+ * <li>Your code doesn't depend on unintended features (if it does, it isn't
+ * necessarily compatible with any other version)
+ * </ul>
+ * If this function returns <b>false</b>, you may have to change your code to
+ * upgrade it to {@code other}
+ *
+ * <p>
+ * Two version numbers that are identical (ignoring build metadata) are
+ * always compatible. Different version numbers are compatible as long as:
+ * <ul>
+ * <li>The major version number is not 0 (if it is, the API is considered
+ * unstable and any upgrade can be backwards compatible)
+ * <li>The major version number is the same (changing the major version
+ * number implies bacwards incompatible changes)
+ * <li>This version comes before the other one in the official precedence
+ * order (downgrading can remove features you depend on)
+ * </ul>
+ *
+ * @param other version to compare with
+ * @return true if you can definitely upgrade to {@code other} without
+ * changing code
+ * @since 2022-02-20
+ */
+ public boolean compatibleWith(SemanticVersionNumber other) {
+ Objects.requireNonNull(other, "other may not be null");
+
+ return this.compareTo(other) == 0 || this.major != 0
+ && this.major == other.major && this.compareTo(other) < 0;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (!(obj instanceof SemanticVersionNumber))
+ return false;
+ final SemanticVersionNumber other = (SemanticVersionNumber) obj;
+ if (this.buildMetadata == null) {
+ if (other.buildMetadata != null)
+ return false;
+ } else if (!this.buildMetadata.equals(other.buildMetadata))
+ return false;
+ if (this.major != other.major)
+ return false;
+ if (this.minor != other.minor)
+ return false;
+ if (this.patch != other.patch)
+ return false;
+ if (this.preReleaseIdentifiers == null) {
+ if (other.preReleaseIdentifiers != null)
+ return false;
+ } else if (!this.preReleaseIdentifiers
+ .equals(other.preReleaseIdentifiers))
+ return false;
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result
+ + (this.buildMetadata == null ? 0 : this.buildMetadata.hashCode());
+ result = prime * result + this.major;
+ result = prime * result + this.minor;
+ result = prime * result + this.patch;
+ result = prime * result + (this.preReleaseIdentifiers == null ? 0
+ : this.preReleaseIdentifiers.hashCode());
+ return result;
+ }
+
+ /**
+ * @return true iff this version is stable (major version > 0 and not a
+ * pre-release)
+ * @since 2022-02-19
+ */
+ public boolean isStable() {
+ return this.major > 0 && this.preReleaseIdentifiers.isEmpty();
+ }
+
+ /**
+ * @return the MAJOR version number, incremented when you make backwards
+ * incompatible API changes
+ * @since 2022-02-19
+ */
+ public int majorVersion() {
+ return this.major;
+ }
+
+ /**
+ * @return the MINOR version number, incremented when you add backwards
+ * compatible functionality
+ * @since 2022-02-19
+ */
+ public int minorVersion() {
+ return this.minor;
+ }
+
+ /**
+ * @return the PATCH version number, incremented when you make backwards
+ * compatible bug fixes
+ * @since 2022-02-19
+ */
+ public int patchVersion() {
+ return this.patch;
+ }
+
+ /**
+ * @return identifiers describing this pre-release (empty if not a
+ * pre-release)
+ * @since 2022-02-19
+ */
+ public List<String> preReleaseIdentifiers() {
+ return Collections.unmodifiableList(this.preReleaseIdentifiers);
+ }
+
+ /**
+ * Converts a version number to a string using the official SemVer format.
+ * The core of a version is MAJOR.MINOR.PATCH, without zero-padding. If
+ * pre-release identifiers are present, they are separated by periods and
+ * added after a '-'. If build metadata is present, it is separated by
+ * periods and added after a '+'. Pre-release identifiers go before version
+ * metadata.
+ * <p>
+ * For example, the version with major number 3, minor number 2, patch number
+ * 1, pre-release identifiers "alpha" and "1" and build metadata "2022-02-19"
+ * has a string representation "3.2.1-alpha.1+2022-02-19".
+ *
+ * @see <a href="https://semver.org">The official SemVer specification</a>
+ */
+ @Override
+ public String toString() {
+ String versionString = String.format("%d.%d.%d", this.major, this.minor,
+ this.patch);
+ if (!this.preReleaseIdentifiers.isEmpty()) {
+ versionString += "-" + String.join(".", this.preReleaseIdentifiers);
+ }
+ if (!this.buildMetadata.isEmpty()) {
+ versionString += "+" + String.join(".", this.buildMetadata);
+ }
+ return versionString;
+ }
+}
diff --git a/src/main/java/sevenUnits/utils/UncertainDouble.java b/src/main/java/sevenUnits/utils/UncertainDouble.java
index fe41104..ac523b3 100644
--- a/src/main/java/sevenUnits/utils/UncertainDouble.java
+++ b/src/main/java/sevenUnits/utils/UncertainDouble.java
@@ -46,6 +46,21 @@ public final class UncertainDouble implements Comparable<UncertainDouble> {
+ "(?:\\s*(?:±|\\+-)\\s*" + NUMBER_REGEX + ")?");
/**
+ * Gets an UncertainDouble from a double string. The uncertainty of the
+ * double will be one of the lowest decimal place of the number. For example,
+ * "12345.678" will become 12345.678 ± 0.001.
+ *
+ * @throws NumberFormatException if the argument is not a number
+ *
+ * @since 2022-04-18
+ */
+ public static final UncertainDouble fromRoundedString(String s) {
+ final BigDecimal value = new BigDecimal(s);
+ final double uncertainty = Math.pow(10, -value.scale());
+ return UncertainDouble.of(value.doubleValue(), uncertainty);
+ }
+
+ /**
* Parses a string in the form of {@link UncertainDouble#toString(boolean)}
* and returns the corresponding {@code UncertainDouble} instance.
* <p>
@@ -348,7 +363,7 @@ public final class UncertainDouble implements Comparable<UncertainDouble> {
*/
@Override
public final String toString() {
- return this.toString(!this.isExact());
+ return this.toString(!this.isExact(), RoundingMode.HALF_EVEN);
}
/**
@@ -379,7 +394,8 @@ public final class UncertainDouble implements Comparable<UncertainDouble> {
*
* @since 2020-09-07
*/
- public final String toString(boolean showUncertainty) {
+ public final String toString(boolean showUncertainty,
+ RoundingMode roundingMode) {
String valueString, uncertaintyString;
// generate the string representation of value and uncertainty
@@ -394,9 +410,9 @@ public final class UncertainDouble implements Comparable<UncertainDouble> {
final int displayScale = this.getDisplayScale();
final BigDecimal roundedUncertainty = bigUncertainty
- .setScale(displayScale, RoundingMode.HALF_EVEN);
+ .setScale(displayScale, roundingMode);
final BigDecimal roundedValue = bigValue.setScale(displayScale,
- RoundingMode.HALF_EVEN);
+ roundingMode);
valueString = roundedValue.toString();
uncertaintyString = roundedUncertainty.toString();
diff --git a/src/main/java/sevenUnits/converterGUI/DefaultPrefixRepetitionRule.java b/src/main/java/sevenUnitsGUI/DefaultPrefixRepetitionRule.java
index 6b6abf0..b56356d 100644
--- a/src/main/java/sevenUnits/converterGUI/DefaultPrefixRepetitionRule.java
+++ b/src/main/java/sevenUnitsGUI/DefaultPrefixRepetitionRule.java
@@ -1,7 +1,7 @@
/**
* @since 2020-08-26
*/
-package sevenUnits.converterGUI;
+package sevenUnitsGUI;
import java.util.List;
import java.util.function.Predicate;
@@ -14,7 +14,7 @@ import sevenUnits.unit.UnitPrefix;
*
* @since 2020-08-26
*/
-enum DefaultPrefixRepetitionRule implements Predicate<List<UnitPrefix>> {
+public enum DefaultPrefixRepetitionRule implements Predicate<List<UnitPrefix>> {
NO_REPETITION {
@Override
public boolean test(List<UnitPrefix> prefixes) {
diff --git a/src/main/java/sevenUnits/converterGUI/DelegateListModel.java b/src/main/java/sevenUnitsGUI/DelegateListModel.java
index dd8cc97..5938b59 100644
--- a/src/main/java/sevenUnits/converterGUI/DelegateListModel.java
+++ b/src/main/java/sevenUnitsGUI/DelegateListModel.java
@@ -14,7 +14,7 @@
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-package sevenUnits.converterGUI;
+package sevenUnitsGUI;
import java.util.ArrayList;
import java.util.Collection;
diff --git a/src/main/java/sevenUnitsGUI/ExpressionConversionView.java b/src/main/java/sevenUnitsGUI/ExpressionConversionView.java
new file mode 100644
index 0000000..872ca10
--- /dev/null
+++ b/src/main/java/sevenUnitsGUI/ExpressionConversionView.java
@@ -0,0 +1,45 @@
+/**
+ * Copyright (C) 2021 Adrien Hopkins
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package sevenUnitsGUI;
+
+/**
+ * A View that can convert unit expressions
+ *
+ * @author Adrien Hopkins
+ * @since 2021-12-15
+ */
+public interface ExpressionConversionView extends View {
+ /**
+ * @return unit expression to convert <em>from</em>
+ * @since 2021-12-15
+ */
+ String getFromExpression();
+
+ /**
+ * @return unit expression to convert <em>to</em>
+ * @since 2021-12-15
+ */
+ String getToExpression();
+
+ /**
+ * Shows the output of an expression conversion to the user.
+ *
+ * @param uc unit conversion to show
+ * @since 2021-12-15
+ */
+ void showExpressionConversionOutput(UnitConversionRecord uc);
+}
diff --git a/src/main/java/sevenUnits/converterGUI/FilterComparator.java b/src/main/java/sevenUnitsGUI/FilterComparator.java
index edd00e2..f34d0c0 100644
--- a/src/main/java/sevenUnits/converterGUI/FilterComparator.java
+++ b/src/main/java/sevenUnitsGUI/FilterComparator.java
@@ -14,7 +14,7 @@
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-package sevenUnits.converterGUI;
+package sevenUnitsGUI;
import java.util.Comparator;
import java.util.Objects;
@@ -22,11 +22,13 @@ import java.util.Objects;
/**
* A comparator that compares strings using a filter.
*
+ * @param <T> type of element being compared
+ *
* @author Adrien Hopkins
* @since 2019-01-15
* @since v0.1.0
*/
-final class FilterComparator implements Comparator<String> {
+final class FilterComparator<T> implements Comparator<T> {
/**
* The filter that the comparator is filtered by.
*
@@ -40,7 +42,7 @@ final class FilterComparator implements Comparator<String> {
* @since 2019-01-15
* @since v0.1.0
*/
- private final Comparator<String> comparator;
+ private final Comparator<T> comparator;
/**
* Whether or not the comparison is case-sensitive.
*
@@ -48,7 +50,7 @@ final class FilterComparator implements Comparator<String> {
* @since v0.2.0
*/
private final boolean caseSensitive;
-
+
/**
* Creates the {@code FilterComparator}.
*
@@ -59,71 +61,68 @@ final class FilterComparator implements Comparator<String> {
public FilterComparator(final String filter) {
this(filter, null);
}
-
+
/**
* Creates the {@code FilterComparator}.
*
- * @param filter
- * string to filter by
- * @param comparator
- * comparator to fall back to if all else fails, null is compareTo.
- * @throws NullPointerException
- * if filter is null
+ * @param filter string to filter by
+ * @param comparator comparator to fall back to if all else fails, null is
+ * compareTo.
+ * @throws NullPointerException if filter is null
* @since 2019-01-15
* @since v0.1.0
*/
- public FilterComparator(final String filter, final Comparator<String> comparator) {
+ public FilterComparator(final String filter,
+ final Comparator<T> comparator) {
this(filter, comparator, false);
}
-
+
/**
* Creates the {@code FilterComparator}.
*
- * @param filter
- * string to filter by
- * @param comparator
- * comparator to fall back to if all else fails, null is compareTo.
- * @param caseSensitive
- * whether or not the comparator is case-sensitive
- * @throws NullPointerException
- * if filter is null
+ * @param filter string to filter by
+ * @param comparator comparator to fall back to if all else fails, null is
+ * compareTo.
+ * @param caseSensitive whether or not the comparator is case-sensitive
+ * @throws NullPointerException if filter is null
* @since 2019-04-14
* @since v0.2.0
*/
- public FilterComparator(final String filter, final Comparator<String> comparator, final boolean caseSensitive) {
+ public FilterComparator(final String filter, final Comparator<T> comparator,
+ final boolean caseSensitive) {
this.filter = Objects.requireNonNull(filter, "filter must not be null.");
this.comparator = comparator;
this.caseSensitive = caseSensitive;
}
-
+
@Override
- public int compare(final String arg0, final String arg1) {
+ public int compare(final T arg0, final T arg1) {
// if this is case insensitive, make them lowercase
final String str0, str1;
if (this.caseSensitive) {
- str0 = arg0;
- str1 = arg1;
+ str0 = arg0.toString();
+ str1 = arg1.toString();
} else {
- str0 = arg0.toLowerCase();
- str1 = arg1.toLowerCase();
+ str0 = arg0.toString().toLowerCase();
+ str1 = arg1.toString().toLowerCase();
}
-
+
// elements that start with the filter always go first
if (str0.startsWith(this.filter) && !str1.startsWith(this.filter))
return -1;
else if (!str0.startsWith(this.filter) && str1.startsWith(this.filter))
return 1;
-
+
// elements that contain the filter but don't start with them go next
if (str0.contains(this.filter) && !str1.contains(this.filter))
return -1;
else if (!str0.contains(this.filter) && !str1.contains(this.filter))
return 1;
-
+
// other elements go last
if (this.comparator == null)
return str0.compareTo(str1);
else
- return this.comparator.compare(str0, str1);
+ return this.comparator.compare(arg0, arg1);
}
}
diff --git a/src/main/java/sevenUnits/converterGUI/GridBagBuilder.java b/src/main/java/sevenUnitsGUI/GridBagBuilder.java
index 0b71d78..32e94d7 100644
--- a/src/main/java/sevenUnits/converterGUI/GridBagBuilder.java
+++ b/src/main/java/sevenUnitsGUI/GridBagBuilder.java
@@ -14,7 +14,7 @@
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-package sevenUnits.converterGUI;
+package sevenUnitsGUI;
import java.awt.GridBagConstraints;
import java.awt.Insets;
diff --git a/src/main/java/sevenUnitsGUI/Main.java b/src/main/java/sevenUnitsGUI/Main.java
new file mode 100644
index 0000000..b5a896f
--- /dev/null
+++ b/src/main/java/sevenUnitsGUI/Main.java
@@ -0,0 +1,34 @@
+/**
+ * Copyright (C) 2022 Adrien Hopkins
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package sevenUnitsGUI;
+
+/**
+ * The main code for the 7Units GUI
+ *
+ * @since 2022-04-19
+ */
+public final class Main {
+
+ /**
+ * @param args
+ * @since 2022-04-19
+ */
+ public static void main(String[] args) {
+ View.createTabbedView();
+ }
+
+}
diff --git a/src/main/java/sevenUnits/converterGUI/MutablePredicate.java b/src/main/java/sevenUnitsGUI/MutablePredicate.java
index ae6b7a1..6cb8689 100644
--- a/src/main/java/sevenUnits/converterGUI/MutablePredicate.java
+++ b/src/main/java/sevenUnitsGUI/MutablePredicate.java
@@ -14,7 +14,7 @@
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-package sevenUnits.converterGUI;
+package sevenUnitsGUI;
import java.util.function.Predicate;
diff --git a/src/main/java/sevenUnitsGUI/Presenter.java b/src/main/java/sevenUnitsGUI/Presenter.java
new file mode 100644
index 0000000..4feea44
--- /dev/null
+++ b/src/main/java/sevenUnitsGUI/Presenter.java
@@ -0,0 +1,782 @@
+/**
+ * Copyright (C) 2021-2022 Adrien Hopkins
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package sevenUnitsGUI;
+
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.Optional;
+import java.util.Scanner;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
+import sevenUnits.ProgramInfo;
+import sevenUnits.unit.BaseDimension;
+import sevenUnits.unit.BaseUnit;
+import sevenUnits.unit.BritishImperial;
+import sevenUnits.unit.LinearUnit;
+import sevenUnits.unit.LinearUnitValue;
+import sevenUnits.unit.Metric;
+import sevenUnits.unit.Unit;
+import sevenUnits.unit.UnitDatabase;
+import sevenUnits.unit.UnitPrefix;
+import sevenUnits.unit.UnitType;
+import sevenUnits.unit.UnitValue;
+import sevenUnits.utils.Nameable;
+import sevenUnits.utils.ObjectProduct;
+import sevenUnits.utils.UncertainDouble;
+
+/**
+ * An object that handles interactions between the view and the backend code
+ *
+ * @author Adrien Hopkins
+ * @since 2021-12-15
+ */
+public final class Presenter {
+ /** The default place where settings are stored. */
+ private static final Path DEFAULT_SETTINGS_FILEPATH = Path
+ .of("settings.txt");
+ /** The default place where units are stored. */
+ private static final String DEFAULT_UNITS_FILEPATH = "/unitsfile.txt";
+ /** The default place where dimensions are stored. */
+ private static final String DEFAULT_DIMENSIONS_FILEPATH = "/dimensionfile.txt";
+ /** The default place where exceptions are stored. */
+ private static final String DEFAULT_EXCEPTIONS_FILEPATH = "/metric_exceptions.txt";
+
+ /**
+ * Adds default units and dimensions to a database.
+ *
+ * @param database database to add to
+ * @since 2019-04-14
+ * @since v0.2.0
+ */
+ private static void addDefaults(final UnitDatabase database) {
+ database.addUnit("metre", Metric.METRE);
+ database.addUnit("kilogram", Metric.KILOGRAM);
+ database.addUnit("gram", Metric.KILOGRAM.dividedBy(1000));
+ database.addUnit("second", Metric.SECOND);
+ database.addUnit("ampere", Metric.AMPERE);
+ database.addUnit("kelvin", Metric.KELVIN);
+ database.addUnit("mole", Metric.MOLE);
+ database.addUnit("candela", Metric.CANDELA);
+ database.addUnit("bit", Metric.BIT);
+ database.addUnit("unit", Metric.ONE);
+ // nonlinear units - must be loaded manually
+ database.addUnit("tempCelsius", Metric.CELSIUS);
+ database.addUnit("tempFahrenheit", BritishImperial.FAHRENHEIT);
+
+ // load initial dimensions
+ database.addDimension("LENGTH", Metric.Dimensions.LENGTH);
+ database.addDimension("MASS", Metric.Dimensions.MASS);
+ database.addDimension("TIME", Metric.Dimensions.TIME);
+ database.addDimension("TEMPERATURE", Metric.Dimensions.TEMPERATURE);
+ }
+
+ /**
+ * @return text in About file
+ * @since 2022-02-19
+ */
+ public static final String getAboutText() {
+ return Presenter.getLinesFromResource("/about.txt").stream()
+ .map(Presenter::withoutComments).collect(Collectors.joining("\n"))
+ .replaceAll("\\[VERSION\\]", ProgramInfo.VERSION.toString());
+ }
+
+ /**
+ * Gets the text of a resource file as a set of strings (each one is one line
+ * of the text).
+ *
+ * @param filename filename to get resource from
+ * @return contents of file
+ * @since 2021-03-27
+ */
+ private static final List<String> getLinesFromResource(String filename) {
+ final List<String> lines = new ArrayList<>();
+
+ try (InputStream stream = inputStream(filename);
+ Scanner scanner = new Scanner(stream)) {
+ while (scanner.hasNextLine()) {
+ lines.add(scanner.nextLine());
+ }
+ } catch (final IOException e) {
+ throw new AssertionError(
+ "Error occurred while loading file " + filename, e);
+ }
+
+ return lines;
+ }
+
+ /**
+ * Gets an input stream for a resource file.
+ *
+ * @param filepath file to use as resource
+ * @return obtained Path
+ * @since 2021-03-27
+ */
+ private static final InputStream inputStream(String filepath) {
+ return Presenter.class.getResourceAsStream(filepath);
+ }
+
+ /**
+ * @return true iff a and b have any elements in common
+ * @since 2022-04-19
+ */
+ private static final boolean sharesAnyElements(Set<?> a, Set<?> b) {
+ for (final Object e : a) {
+ if (b.contains(e))
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * @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(0, index);
+ }
+
+ // ====== SETTINGS ======
+
+ /**
+ * The view that this presenter communicates with
+ */
+ private final View view;
+
+ /**
+ * The database that this presenter communicates with (effectively the model)
+ */
+ final UnitDatabase database;
+
+ /**
+ * The rule used for parsing input numbers. Any number-string inputted into
+ * this program will be parsed using this method. <b>Not implemented yet.</b>
+ */
+ private Function<String, UncertainDouble> numberParsingRule;
+
+ /**
+ * The rule used for displaying the results of unit conversions. The result
+ * of unit conversions will be put into this function, and the resulting
+ * string will be used in the output.
+ */
+ private Function<UncertainDouble, String> numberDisplayRule = StandardDisplayRules
+ .uncertaintyBased();
+
+ /**
+ * A predicate that determines whether or not a certain combination of
+ * prefixes is allowed. If it returns false, a combination of prefixes will
+ * not be allowed. Prefixes are put in the list from right to left.
+ */
+ private Predicate<List<UnitPrefix>> prefixRepetitionRule = DefaultPrefixRepetitionRule.NO_RESTRICTION;
+
+ /**
+ * The set of units that is considered neither metric nor nonmetric for the
+ * purposes of the metric-imperial one-way conversion. These units are
+ * included in both From and To, even if One Way Conversion is enabled.
+ */
+ private final Set<String> metricExceptions;
+
+ /**
+ * If this is true, views that show units as a list will have metric units
+ * removed from the From unit list and imperial/USC units removed from the To
+ * unit list.
+ */
+ private boolean oneWayConversionEnabled = false;
+
+ /**
+ * If this is false, duplicate units and prefixes will be removed from the
+ * unit view in views that show units as a list to choose from.
+ */
+ private boolean showDuplicates = false;
+
+ /**
+ * Creates a Presenter
+ *
+ * @param view the view that this presenter communicates with
+ * @since 2021-12-15
+ */
+ public Presenter(View view) {
+ this.view = view;
+ this.database = new UnitDatabase();
+ addDefaults(this.database);
+
+ // load units and prefixes
+ try (final InputStream units = inputStream(DEFAULT_UNITS_FILEPATH)) {
+ this.database.loadUnitsFromStream(units);
+ } catch (final IOException e) {
+ throw new AssertionError("Loading of unitsfile.txt failed.", e);
+ }
+
+ // load dimensions
+ try (final InputStream dimensions = inputStream(
+ DEFAULT_DIMENSIONS_FILEPATH)) {
+ this.database.loadDimensionsFromStream(dimensions);
+ } catch (final IOException e) {
+ throw new AssertionError("Loading of dimensionfile.txt failed.", e);
+ }
+
+ // load metric exceptions
+ try {
+ this.metricExceptions = new HashSet<>();
+ try (InputStream exceptions = inputStream(DEFAULT_EXCEPTIONS_FILEPATH);
+ Scanner scanner = new Scanner(exceptions)) {
+ while (scanner.hasNextLine()) {
+ final String line = Presenter
+ .withoutComments(scanner.nextLine());
+ if (!line.isBlank()) {
+ this.metricExceptions.add(line);
+ }
+ }
+ }
+ } catch (final IOException e) {
+ throw new AssertionError("Loading of metric_exceptions.txt failed.",
+ e);
+ }
+
+ // set default settings temporarily
+ this.loadSettings(DEFAULT_SETTINGS_FILEPATH);
+
+ // a Predicate that returns true iff the argument is a full base unit
+ final Predicate<Unit> isFullBase = unit -> unit instanceof LinearUnit
+ && ((LinearUnit) unit).isBase();
+
+ // print out unit counts
+ System.out.printf(
+ "Successfully loaded %d units with %d unit names (%d base units).%n",
+ this.database.unitMapPrefixless(false).size(),
+ this.database.unitMapPrefixless(true).size(),
+ this.database.unitMapPrefixless(false).values().stream()
+ .filter(isFullBase).count());
+ }
+
+ /**
+ * Converts from the view's input expression to its output expression.
+ * Displays an error message if any of the required fields are invalid.
+ *
+ * @throws UnsupportedOperationException if the view does not support
+ * expression-based conversion (does
+ * not implement
+ * {@link ExpressionConversionView})
+ * @since 2021-12-15
+ */
+ public void convertExpressions() {
+ if (this.view instanceof ExpressionConversionView) {
+ final ExpressionConversionView xcview = (ExpressionConversionView) this.view;
+
+ final String fromExpression = xcview.getFromExpression();
+ final String toExpression = xcview.getToExpression();
+
+ // expressions must not be empty
+ if (fromExpression.isEmpty()) {
+ this.view.showErrorMessage("Parse Error",
+ "Please enter a unit expression in the From: box.");
+ return;
+ }
+ if (toExpression.isEmpty()) {
+ this.view.showErrorMessage("Parse Error",
+ "Please enter a unit expression in the To: box.");
+ return;
+ }
+
+ // evaluate expressions
+ final LinearUnitValue from;
+ final Unit to;
+ try {
+ from = this.database.evaluateUnitExpression(fromExpression);
+ } catch (final IllegalArgumentException | NoSuchElementException e) {
+ this.view.showErrorMessage("Parse Error",
+ "Could not recognize text in From entry: " + e.getMessage());
+ return;
+ }
+ try {
+ to = this.database.getUnitFromExpression(toExpression);
+ } catch (final IllegalArgumentException | NoSuchElementException e) {
+ this.view.showErrorMessage("Parse Error",
+ "Could not recognize text in To entry: " + e.getMessage());
+ return;
+ }
+
+ // convert and show output
+ if (from.getUnit().canConvertTo(to)) {
+ final UncertainDouble uncertainValue;
+
+ // uncertainty is meaningless for non-linear units, so we will have
+ // to erase uncertainty information for them
+ if (to instanceof LinearUnit) {
+ final var toLinear = (LinearUnit) to;
+ uncertainValue = from.convertTo(toLinear).getValue();
+ } else {
+ final double value = from.asUnitValue().convertTo(to).getValue();
+ uncertainValue = UncertainDouble.of(value, 0);
+ }
+
+ final UnitConversionRecord uc = UnitConversionRecord.valueOf(
+ fromExpression, toExpression, "",
+ this.numberDisplayRule.apply(uncertainValue));
+ xcview.showExpressionConversionOutput(uc);
+ } else {
+ this.view.showErrorMessage("Conversion Error",
+ "Cannot convert between \"" + fromExpression + "\" and \""
+ + toExpression + "\".");
+ }
+
+ } else
+ throw new UnsupportedOperationException(
+ "This function can only be called when the view is an ExpressionConversionView");
+ }
+
+ /**
+ * Converts from the view's input unit to its output unit. Displays an error
+ * message if any of the required fields are invalid.
+ *
+ * @throws UnsupportedOperationException if the view does not support
+ * unit-based conversion (does not
+ * implement
+ * {@link UnitConversionView})
+ * @since 2021-12-15
+ */
+ public void convertUnits() {
+ if (this.view instanceof UnitConversionView) {
+ final UnitConversionView ucview = (UnitConversionView) this.view;
+
+ final Optional<String> fromUnitOptional = ucview.getFromSelection();
+ final Optional<String> toUnitOptional = ucview.getToSelection();
+ final String inputValueString = ucview.getInputValue();
+
+ // extract values from optionals
+ final String fromUnitString, toUnitString;
+ if (fromUnitOptional.isPresent()) {
+ fromUnitString = fromUnitOptional.orElseThrow();
+ } else {
+ this.view.showErrorMessage("Unit Selection Error",
+ "Please specify a From unit");
+ return;
+ }
+ if (toUnitOptional.isPresent()) {
+ toUnitString = toUnitOptional.orElseThrow();
+ } else {
+ this.view.showErrorMessage("Unit Selection Error",
+ "Please specify a To unit");
+ return;
+ }
+
+ // convert strings to data, checking if anything is invalid
+ final Unit fromUnit, toUnit;
+ final UncertainDouble uncertainValue;
+
+ if (this.database.containsUnitName(fromUnitString)) {
+ fromUnit = this.database.getUnit(fromUnitString);
+ } else
+ throw this.viewError("Nonexistent From unit: %s", fromUnitString);
+ if (this.database.containsUnitName(toUnitString)) {
+ toUnit = this.database.getUnit(toUnitString);
+ } else
+ throw this.viewError("Nonexistent To unit: %s", toUnitString);
+ try {
+ uncertainValue = UncertainDouble
+ .fromRoundedString(inputValueString);
+ } catch (final NumberFormatException e) {
+ this.view.showErrorMessage("Value Error",
+ "Invalid value " + inputValueString);
+ return;
+ }
+
+ if (!fromUnit.canConvertTo(toUnit))
+ throw this.viewError("Could not convert between %s and %s",
+ fromUnit, toUnit);
+
+ // convert - we will need to erase uncertainty for non-linear units, so
+ // we need to treat linear and non-linear units differently
+ final String outputValueString;
+ if (fromUnit instanceof LinearUnit && toUnit instanceof LinearUnit) {
+ final LinearUnit fromLinear = (LinearUnit) fromUnit;
+ final LinearUnit toLinear = (LinearUnit) toUnit;
+ final LinearUnitValue initialValue = LinearUnitValue.of(fromLinear,
+ uncertainValue);
+ final LinearUnitValue converted = initialValue.convertTo(toLinear);
+
+ outputValueString = this.numberDisplayRule
+ .apply(converted.getValue());
+ } else {
+ final UnitValue initialValue = UnitValue.of(fromUnit,
+ uncertainValue.value());
+ final UnitValue converted = initialValue.convertTo(toUnit);
+
+ outputValueString = this.numberDisplayRule
+ .apply(UncertainDouble.of(converted.getValue(), 0));
+ }
+
+ ucview.showUnitConversionOutput(
+ UnitConversionRecord.valueOf(fromUnitString, toUnitString,
+ inputValueString, outputValueString));
+ } else
+ throw new UnsupportedOperationException(
+ "This function can only be called when the view is a UnitConversionView.");
+ }
+
+ /**
+ * @return true iff duplicate units are shown in unit lists
+ * @since 2022-03-30
+ */
+ public boolean duplicatesShown() {
+ return this.showDuplicates;
+ }
+
+ /**
+ * Gets a name for this dimension using the database
+ *
+ * @param dimension dimension to name
+ * @return name of dimension
+ * @since 2022-04-16
+ */
+ final String getDimensionName(ObjectProduct<BaseDimension> dimension) {
+ // find this dimension in the database and get its name
+ // if it isn't there, use the dimension's toString instead
+ return this.database.dimensionMap().values().stream()
+ .filter(d -> d.equals(dimension)).findAny().map(Nameable::getName)
+ .orElse(dimension.toString(Nameable::getName));
+ }
+
+ /**
+ * @return the rule that is used by this presenter to convert numbers into
+ * strings
+ * @since 2022-04-10
+ */
+ public Function<UncertainDouble, String> getNumberDisplayRule() {
+ return this.numberDisplayRule;
+ }
+
+ /**
+ * @return the rule that is used by this presenter to convert strings into
+ * numbers
+ * @since 2022-04-10
+ */
+ @SuppressWarnings("unused") // not implemented yet
+ private Function<String, UncertainDouble> getNumberParsingRule() {
+ return this.numberParsingRule;
+ }
+
+ /**
+ * @return the rule that determines whether a set of prefixes is valid
+ * @since 2022-04-19
+ */
+ public Predicate<List<UnitPrefix>> getPrefixRepetitionRule() {
+ return this.prefixRepetitionRule;
+ }
+
+ /**
+ * @return the view associated with this presenter
+ * @since 2022-04-19
+ */
+ public View getView() {
+ return this.view;
+ }
+
+ /**
+ * @return whether or not the provided unit is semi-metric (i.e. an
+ * exception)
+ * @since 2022-04-16
+ */
+ final boolean isSemiMetric(Unit u) {
+ // determine if u is an exception
+ final var primaryName = u.getPrimaryName();
+ final var symbol = u.getSymbol();
+ return primaryName.isPresent()
+ && this.metricExceptions.contains(primaryName.orElseThrow())
+ || symbol.isPresent()
+ && this.metricExceptions.contains(symbol.orElseThrow())
+ || sharesAnyElements(this.metricExceptions, u.getOtherNames());
+ }
+
+ /**
+ * Loads settings from the user's settings file and applies them to the
+ * presenter.
+ *
+ * @param settingsFile file settings should be loaded from
+ * @since 2021-12-15
+ */
+ void loadSettings(Path settingsFile) {
+ try {
+ // read file line by line
+ final int lineNum = 0;
+ for (final String line : Files.readAllLines(settingsFile)) {
+ 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 "number_display_rule":
+ this.numberDisplayRule = StandardDisplayRules
+ .getStandardRule(value);
+ break;
+ case "prefix_rule":
+ this.prefixRepetitionRule = DefaultPrefixRepetitionRule
+ .valueOf(value);
+ this.database.setPrefixRepetitionRule(this.prefixRepetitionRule);
+ break;
+ case "one_way":
+ this.oneWayConversionEnabled = Boolean.valueOf(value);
+ break;
+ case "include_duplicates":
+ this.showDuplicates = Boolean.valueOf(value);
+ break;
+ default:
+ System.err.printf("Warning: unrecognized setting \"%s\".%n",
+ param);
+ break;
+ }
+ }
+ if (this.view.getPresenter() != null) {
+ this.updateView();
+ }
+ } catch (final IOException e) {}
+ }
+
+ /**
+ * @return true iff the One-Way Conversion feature is available (views that
+ * show units as a list will have metric units removed from the From
+ * unit list and imperial/USC units removed from the To unit list)
+ *
+ * @since 2022-03-30
+ */
+ public boolean oneWayConversionEnabled() {
+ return this.oneWayConversionEnabled;
+ }
+
+ /**
+ * Completes creation of the presenter. This part of the initialization
+ * depends on the view's functions, so it cannot be run if the components
+ * they depend on are not created yet.
+ *
+ * @since 2022-02-26
+ */
+ public void postViewInitialize() {
+ // unit conversion specific stuff
+ if (this.view instanceof UnitConversionView) {
+ final UnitConversionView ucview = (UnitConversionView) this.view;
+ ucview.setDimensionNames(this.database.dimensionMap().keySet());
+ }
+
+ this.updateView();
+ }
+
+ void prefixSelected() {
+ final Optional<String> selectedPrefixName = this.view
+ .getViewedPrefixName();
+ final Optional<UnitPrefix> selectedPrefix = selectedPrefixName
+ .map(name -> this.database.containsPrefixName(name)
+ ? this.database.getPrefix(name)
+ : null);
+ selectedPrefix
+ .ifPresent(prefix -> this.view.showPrefix(prefix.getNameSymbol(),
+ String.valueOf(prefix.getMultiplier())));
+ }
+
+ /**
+ * Saves the presenter's current settings to its default filepath.
+ *
+ * @since 2022-04-19
+ */
+ public void saveSettings() {
+ this.saveSettings(DEFAULT_SETTINGS_FILEPATH);
+ }
+
+ /**
+ * Saves the presenter's settings to the user settings file.
+ *
+ * @param settingsFile file settings should be saved to
+ * @since 2021-12-15
+ */
+ void saveSettings(Path settingsFile) {
+ try (BufferedWriter writer = Files.newBufferedWriter(settingsFile)) {
+ writer.write(String.format("number_display_rule=%s\n",
+ this.numberDisplayRule));
+ writer.write(
+ String.format("prefix_rule=%s\n", this.prefixRepetitionRule));
+ writer.write(
+ String.format("one_way=%s\n", this.oneWayConversionEnabled));
+ writer.write(
+ String.format("include_duplicates=%s\n", this.showDuplicates));
+ } catch (final IOException e) {
+ e.printStackTrace();
+ this.view.showErrorMessage("I/O Error",
+ "Error occurred while saving settings: "
+ + e.getLocalizedMessage());
+ }
+ }
+
+ /**
+ * @param numberDisplayRule the new rule that will be used by this presenter
+ * to convert numbers into strings
+ * @since 2022-04-10
+ */
+ public void setNumberDisplayRule(
+ Function<UncertainDouble, String> numberDisplayRule) {
+ this.numberDisplayRule = numberDisplayRule;
+ }
+
+ /**
+ * @param numberParsingRule the new rule that will be used by this presenter
+ * to convert strings into numbers
+ * @since 2022-04-10
+ */
+ @SuppressWarnings("unused") // not implemented yet
+ private void setNumberParsingRule(
+ Function<String, UncertainDouble> numberParsingRule) {
+ this.numberParsingRule = numberParsingRule;
+ }
+
+ /**
+ * @param oneWayConversionEnabled whether not one-way conversion should be
+ * enabled
+ * @since 2022-03-30
+ * @see {@link #isOneWayConversionEnabled}
+ */
+ public void setOneWayConversionEnabled(boolean oneWayConversionEnabled) {
+ this.oneWayConversionEnabled = oneWayConversionEnabled;
+ this.updateView();
+ }
+
+ /**
+ * @param prefixRepetitionRule the rule that determines whether a set of
+ * prefixes is valid
+ * @since 2022-04-19
+ */
+ public void setPrefixRepetitionRule(
+ Predicate<List<UnitPrefix>> prefixRepetitionRule) {
+ this.prefixRepetitionRule = prefixRepetitionRule;
+ this.database.setPrefixRepetitionRule(prefixRepetitionRule);
+ }
+
+ /**
+ * @param showDuplicateUnits whether or not duplicate units should be shown
+ * @since 2022-03-30
+ */
+ public void setShowDuplicates(boolean showDuplicateUnits) {
+ this.showDuplicates = showDuplicateUnits;
+ this.updateView();
+ }
+
+ /**
+ * Shows a unit in the unit viewer
+ *
+ * @param u unit to show
+ * @since 2022-04-16
+ */
+ private final void showUnit(Unit u) {
+ final var nameSymbol = u.getNameSymbol();
+ final boolean isBase = u instanceof BaseUnit
+ || u instanceof LinearUnit && ((LinearUnit) u).isBase();
+ final var definition = isBase ? "(Base unit)" : u.toDefinitionString();
+ final var dimensionString = this.getDimensionName(u.getDimension());
+ final var unitType = UnitType.getType(u, this::isSemiMetric);
+ this.view.showUnit(nameSymbol, definition, dimensionString, unitType);
+ }
+
+ /**
+ * Runs whenever a unit name is selected in the unit viewer. Gets the
+ * description of a unit and displays it.
+ *
+ * @since 2022-04-10
+ */
+ void unitNameSelected() {
+ // get selected unit, if it's there and valid
+ final Optional<String> selectedUnitName = this.view.getViewedUnitName();
+ final Optional<Unit> selectedUnit = selectedUnitName
+ .map(unitName -> this.database.containsUnitName(unitName)
+ ? this.database.getUnit(unitName)
+ : null);
+ selectedUnit.ifPresent(this::showUnit);
+ }
+
+ /**
+ * Updates the view's From and To units, if it has some
+ *
+ * @since 2021-12-15
+ */
+ public void updateView() {
+ if (this.view instanceof UnitConversionView) {
+ final UnitConversionView ucview = (UnitConversionView) this.view;
+ final var selectedDimensionName = ucview.getSelectedDimensionName();
+
+ // load units & prefixes into viewers
+ this.view.setViewableUnitNames(
+ this.database.unitMapPrefixless(this.showDuplicates).keySet());
+ this.view.setViewablePrefixNames(
+ this.database.prefixMap(this.showDuplicates).keySet());
+
+ // get From and To units
+ var fromUnits = this.database.unitMapPrefixless(this.showDuplicates)
+ .entrySet().stream();
+ var toUnits = this.database.unitMapPrefixless(this.showDuplicates)
+ .entrySet().stream();
+
+ // filter by dimension, if one is selected
+ if (selectedDimensionName.isPresent()) {
+ final var viewDimension = this.database
+ .getDimension(selectedDimensionName.orElseThrow());
+ fromUnits = fromUnits.filter(
+ u -> viewDimension.equals(u.getValue().getDimension()));
+ toUnits = toUnits.filter(
+ u -> viewDimension.equals(u.getValue().getDimension()));
+ }
+
+ // filter by unit type, if desired
+ if (this.oneWayConversionEnabled) {
+ fromUnits = fromUnits.filter(u -> UnitType.getType(u.getValue(),
+ this::isSemiMetric) != UnitType.METRIC);
+ toUnits = toUnits.filter(u -> UnitType.getType(u.getValue(),
+ this::isSemiMetric) != UnitType.NON_METRIC);
+ }
+
+ // set unit names
+ ucview.setFromUnitNames(
+ fromUnits.map(Map.Entry::getKey).collect(Collectors.toSet()));
+ ucview.setToUnitNames(
+ toUnits.map(Map.Entry::getKey).collect(Collectors.toSet()));
+ }
+ }
+
+ /**
+ * @param message message to add
+ * @param args string formatting arguments for message
+ * @return AssertionError stating that an error has happened in the view's
+ * code
+ * @since 2022-04-09
+ */
+ private AssertionError viewError(String message, Object... args) {
+ return new AssertionError("View Programming Error (from " + this.view
+ + "): " + String.format(message, args));
+ }
+}
diff --git a/src/main/java/sevenUnits/converterGUI/SearchBoxList.java b/src/main/java/sevenUnitsGUI/SearchBoxList.java
index 2aa9fce..9b41601 100644
--- a/src/main/java/sevenUnits/converterGUI/SearchBoxList.java
+++ b/src/main/java/sevenUnitsGUI/SearchBoxList.java
@@ -14,7 +14,7 @@
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-package sevenUnits.converterGUI;
+package sevenUnitsGUI;
import java.awt.BorderLayout;
import java.awt.Color;
@@ -22,7 +22,10 @@ import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.util.ArrayList;
import java.util.Collection;
+import java.util.Collections;
import java.util.Comparator;
+import java.util.List;
+import java.util.Optional;
import java.util.function.Predicate;
import javax.swing.JList;
@@ -31,11 +34,12 @@ import javax.swing.JScrollPane;
import javax.swing.JTextField;
/**
+ * @param <E> type of element in list
* @author Adrien Hopkins
* @since 2019-04-13
* @since v0.2.0
*/
-final class SearchBoxList extends JPanel {
+final class SearchBoxList<E> extends JPanel {
/**
* @since 2019-04-13
@@ -60,10 +64,10 @@ final class SearchBoxList extends JPanel {
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 Collection<E> itemsToFilter;
+ private final DelegateListModel<E> listModel;
private final JTextField searchBox;
- private final JList<String> searchItems;
+ private final JList<E> searchItems;
private boolean searchBoxEmpty = true;
@@ -72,17 +76,26 @@ final class SearchBoxList extends JPanel {
// event.
private boolean searchBoxFocused = false;
- private Predicate<String> customSearchFilter = o -> true;
- private final Comparator<String> defaultOrdering;
+ private Predicate<E> customSearchFilter = o -> true;
+ private final Comparator<E> defaultOrdering;
private final boolean caseSensitive;
/**
+ * Creates an empty SearchBoxList
+ *
+ * @since 2022-02-19
+ */
+ public SearchBoxList() {
+ this(List.of(), null, false);
+ }
+
+ /**
* Creates the {@code SearchBoxList}.
*
* @param itemsToFilter items to put in the list
* @since 2019-04-14
*/
- public SearchBoxList(final Collection<String> itemsToFilter) {
+ public SearchBoxList(final Collection<E> itemsToFilter) {
this(itemsToFilter, null, false);
}
@@ -97,9 +110,8 @@ final class SearchBoxList extends JPanel {
* @since 2019-04-13
* @since v0.2.0
*/
- public SearchBoxList(final Collection<String> itemsToFilter,
- final Comparator<String> defaultOrdering,
- final boolean caseSensitive) {
+ public SearchBoxList(final Collection<E> itemsToFilter,
+ final Comparator<E> defaultOrdering, final boolean caseSensitive) {
super(new BorderLayout(), true);
this.itemsToFilter = new ArrayList<>(itemsToFilter);
this.defaultOrdering = defaultOrdering;
@@ -140,7 +152,7 @@ final class SearchBoxList extends JPanel {
* @since 2019-04-13
* @since v0.2.0
*/
- public void addSearchFilter(final Predicate<String> filter) {
+ public void addSearchFilter(final Predicate<E> filter) {
this.customSearchFilter = this.customSearchFilter.and(filter);
}
@@ -155,6 +167,15 @@ final class SearchBoxList extends JPanel {
}
/**
+ * @return items available in search list, including items that are hidden by
+ * the search filter
+ * @since 2022-03-30
+ */
+ public Collection<E> getItems() {
+ return Collections.unmodifiableCollection(this.itemsToFilter);
+ }
+
+ /**
* @return this component's search box component
* @since 2019-04-14
* @since v0.2.0
@@ -170,11 +191,11 @@ final class SearchBoxList extends JPanel {
* @since 2019-04-14
* @since v0.2.0
*/
- private Predicate<String> getSearchFilter(final String searchText) {
+ private Predicate<E> getSearchFilter(final String searchText) {
if (this.caseSensitive)
- return string -> string.contains(searchText);
+ return item -> item.toString().contains(searchText);
else
- return string -> string.toLowerCase()
+ return item -> item.toString().toLowerCase()
.contains(searchText.toLowerCase());
}
@@ -183,12 +204,12 @@ final class SearchBoxList extends JPanel {
* @since 2019-04-14
* @since v0.2.0
*/
- public final JList<String> getSearchList() {
+ public final JList<E> getSearchList() {
return this.searchItems;
}
/**
- * @return index selected in item list
+ * @return index selected in item list, -1 if no selection
* @since 2019-04-14
* @since v0.2.0
*/
@@ -201,8 +222,8 @@ final class SearchBoxList extends JPanel {
* @since 2019-04-13
* @since v0.2.0
*/
- public String getSelectedValue() {
- return this.searchItems.getSelectedValue();
+ public Optional<E> getSelectedValue() {
+ return Optional.ofNullable(this.searchItems.getSelectedValue());
}
/**
@@ -214,14 +235,14 @@ final class SearchBoxList extends JPanel {
public void reapplyFilter() {
final String searchText = this.searchBoxEmpty ? ""
: this.searchBox.getText();
- final FilterComparator comparator = new FilterComparator(searchText,
+ final FilterComparator<E> comparator = new FilterComparator<>(searchText,
this.defaultOrdering, this.caseSensitive);
- final Predicate<String> searchFilter = this.getSearchFilter(searchText);
+ final Predicate<E> searchFilter = this.getSearchFilter(searchText);
this.listModel.clear();
- this.itemsToFilter.forEach(string -> {
- if (searchFilter.test(string)) {
- this.listModel.add(string);
+ this.itemsToFilter.forEach(item -> {
+ if (searchFilter.test(item)) {
+ this.listModel.add(item);
}
});
@@ -277,9 +298,9 @@ final class SearchBoxList extends JPanel {
}
final String searchText = this.searchBoxEmpty ? ""
: this.searchBox.getText();
- final FilterComparator comparator = new FilterComparator(searchText,
+ final FilterComparator<E> comparator = new FilterComparator<>(searchText,
this.defaultOrdering, this.caseSensitive);
- final Predicate<String> searchFilter = this.getSearchFilter(searchText);
+ final Predicate<E> searchFilter = this.getSearchFilter(searchText);
// initialize list with items that match the filter then sort
this.listModel.clear();
@@ -303,7 +324,7 @@ final class SearchBoxList extends JPanel {
* @param newItems new items to put in list
* @since 2021-05-22
*/
- public void setItems(Collection<String> newItems) {
+ public void setItems(Collection<? extends E> newItems) {
this.itemsToFilter.clear();
this.itemsToFilter.addAll(newItems);
this.reapplyFilter();
diff --git a/src/main/java/sevenUnitsGUI/StandardDisplayRules.java b/src/main/java/sevenUnitsGUI/StandardDisplayRules.java
new file mode 100644
index 0000000..0c0ba8e
--- /dev/null
+++ b/src/main/java/sevenUnitsGUI/StandardDisplayRules.java
@@ -0,0 +1,246 @@
+/**
+ * Copyright (C) 2022 Adrien Hopkins
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package sevenUnitsGUI;
+
+import java.math.BigDecimal;
+import java.math.MathContext;
+import java.math.RoundingMode;
+import java.util.function.Function;
+import java.util.regex.Pattern;
+
+import sevenUnits.utils.UncertainDouble;
+
+/**
+ * A static utility class that can be used to make display rules for the
+ * presenter.
+ *
+ * @since 2022-04-18
+ */
+public final class StandardDisplayRules {
+ /**
+ * A rule that rounds to a fixed number of decimal places.
+ *
+ * @since 2022-04-18
+ */
+ public static final class FixedDecimals
+ implements Function<UncertainDouble, String> {
+ public static final Pattern TO_STRING_PATTERN = Pattern
+ .compile("Round to (\\d+) decimal places");
+ /**
+ * The number of places to round to.
+ */
+ private final int decimalPlaces;
+
+ /**
+ * @param decimalPlaces
+ * @since 2022-04-18
+ */
+ private FixedDecimals(int decimalPlaces) {
+ this.decimalPlaces = decimalPlaces;
+ }
+
+ @Override
+ public String apply(UncertainDouble t) {
+ final var toRound = new BigDecimal(t.value());
+ return toRound.setScale(this.decimalPlaces, RoundingMode.HALF_EVEN)
+ .toPlainString();
+ }
+
+ /**
+ * @return the number of decimal places this rule rounds to
+ * @since 2022-04-18
+ */
+ public int decimalPlaces() {
+ return this.decimalPlaces;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (!(obj instanceof FixedDecimals))
+ return false;
+ final FixedDecimals other = (FixedDecimals) obj;
+ if (this.decimalPlaces != other.decimalPlaces)
+ return false;
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ return 31 + this.decimalPlaces;
+ }
+
+ @Override
+ public String toString() {
+ return "Round to " + this.decimalPlaces + " decimal places";
+ }
+ }
+
+ /**
+ * A rule that rounds to a fixed number of significant digits.
+ *
+ * @since 2022-04-18
+ */
+ public static final class FixedPrecision
+ implements Function<UncertainDouble, String> {
+ public static final Pattern TO_STRING_PATTERN = Pattern
+ .compile("Round to (\\d+) significant figures");
+
+ /**
+ * The number of significant figures to round to.
+ */
+ private final MathContext mathContext;
+
+ /**
+ * @param significantFigures
+ * @since 2022-04-18
+ */
+ private FixedPrecision(int significantFigures) {
+ this.mathContext = new MathContext(significantFigures,
+ RoundingMode.HALF_EVEN);
+ }
+
+ @Override
+ public String apply(UncertainDouble t) {
+ final var toRound = new BigDecimal(t.value());
+ return toRound.round(this.mathContext).toString();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (!(obj instanceof FixedPrecision))
+ return false;
+ final FixedPrecision other = (FixedPrecision) obj;
+ if (this.mathContext == null) {
+ if (other.mathContext != null)
+ return false;
+ } else if (!this.mathContext.equals(other.mathContext))
+ return false;
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ return 127
+ + (this.mathContext == null ? 0 : this.mathContext.hashCode());
+ }
+
+ /**
+ * @return the number of significant figures this rule rounds to
+ * @since 2022-04-18
+ */
+ public int significantFigures() {
+ return this.mathContext.getPrecision();
+ }
+
+ @Override
+ public String toString() {
+ return "Round to " + this.mathContext.getPrecision()
+ + " significant figures";
+ }
+ }
+
+ /**
+ * A rounding rule that rounds based on UncertainDouble's toString method.
+ * This means the output will have around as many significant figures as the
+ * input.
+ *
+ * @since 2022-04-18
+ */
+ public static final class UncertaintyBased
+ implements Function<UncertainDouble, String> {
+ private UncertaintyBased() {}
+
+ @Override
+ public String apply(UncertainDouble t) {
+ return t.toString(false, RoundingMode.HALF_EVEN);
+ }
+
+ @Override
+ public String toString() {
+ return "Uncertainty-Based Rounding";
+ }
+ }
+
+ /**
+ * For now, I want this to be a singleton. I might want to add a parameter
+ * later, so I won't make it an enum.
+ */
+ private static final UncertaintyBased UNCERTAINTY_BASED_ROUNDING_RULE = new UncertaintyBased();
+
+ /**
+ * @param decimalPlaces decimal places to round to
+ * @return a rounding rule that rounds to fixed number of decimal places
+ * @since 2022-04-18
+ */
+ public static final FixedDecimals fixedDecimals(int decimalPlaces) {
+ return new FixedDecimals(decimalPlaces);
+ }
+
+ /**
+ * @param significantFigures significant figures to round to
+ * @return a rounding rule that rounds to a fixed number of significant
+ * figures
+ * @since 2022-04-18
+ */
+ public static final FixedPrecision fixedPrecision(int significantFigures) {
+ return new FixedPrecision(significantFigures);
+ }
+
+ /**
+ * Gets one of the standard rules from its string representation.
+ *
+ * @param ruleToString string representation of the display rule
+ * @return display rule
+ * @throws IllegalArgumentException if the provided string is not that of a
+ * standard rule.
+ * @since 2021-12-24
+ */
+ public static final Function<UncertainDouble, String> getStandardRule(
+ String ruleToString) {
+ if (UNCERTAINTY_BASED_ROUNDING_RULE.toString().equals(ruleToString))
+ return UNCERTAINTY_BASED_ROUNDING_RULE;
+
+ // test if it is a fixed-places rule
+ final var placesMatch = FixedDecimals.TO_STRING_PATTERN
+ .matcher(ruleToString);
+ if (placesMatch.matches())
+ return new FixedDecimals(Integer.valueOf(placesMatch.group(1)));
+
+ // test if it is a fixed-sig-fig rule
+ final var sigFigMatch = FixedPrecision.TO_STRING_PATTERN
+ .matcher(ruleToString);
+ if (sigFigMatch.matches())
+ return new FixedPrecision(Integer.valueOf(sigFigMatch.group(1)));
+
+ throw new IllegalArgumentException(
+ "Provided string does not match any given rules.");
+ }
+
+ /**
+ * @return an UncertainDouble-based rounding rule
+ * @since 2022-04-18
+ */
+ public static final UncertaintyBased uncertaintyBased() {
+ return UNCERTAINTY_BASED_ROUNDING_RULE;
+ }
+
+ private StandardDisplayRules() {}
+}
diff --git a/src/main/java/sevenUnitsGUI/TabbedView.java b/src/main/java/sevenUnitsGUI/TabbedView.java
new file mode 100644
index 0000000..be80ccb
--- /dev/null
+++ b/src/main/java/sevenUnitsGUI/TabbedView.java
@@ -0,0 +1,797 @@
+/**
+ * Copyright (C) 2022 Adrien Hopkins
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package sevenUnitsGUI;
+
+import java.awt.BorderLayout;
+import java.awt.GridBagConstraints;
+import java.awt.GridBagLayout;
+import java.awt.GridLayout;
+import java.awt.event.ItemEvent;
+import java.awt.event.KeyEvent;
+import java.util.AbstractSet;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.OptionalInt;
+import java.util.Set;
+import java.util.function.Function;
+
+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.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.SwingConstants;
+import javax.swing.UIManager;
+import javax.swing.UnsupportedLookAndFeelException;
+import javax.swing.WindowConstants;
+import javax.swing.border.EmptyBorder;
+import javax.swing.border.TitledBorder;
+
+import sevenUnits.ProgramInfo;
+import sevenUnits.unit.UnitType;
+import sevenUnits.utils.NameSymbol;
+import sevenUnits.utils.UncertainDouble;
+
+/**
+ * A View that separates its functions into multiple tabs
+ *
+ * @since 2022-02-19
+ */
+final class TabbedView implements ExpressionConversionView, UnitConversionView {
+ /**
+ * A Set-like view of a JComboBox's items
+ *
+ * @param <E> type of item in list
+ *
+ * @since 2022-02-19
+ */
+ private static final class JComboBoxItemSet<E> extends AbstractSet<E> {
+ private final JComboBox<E> comboBox;
+
+ /**
+ * @param comboBox combo box to get items from
+ * @since 2022-02-19
+ */
+ public JComboBoxItemSet(JComboBox<E> comboBox) {
+ this.comboBox = comboBox;
+ }
+
+ @Override
+ public Iterator<E> iterator() {
+ return new Iterator<>() {
+ private int index = 0;
+
+ @Override
+ public boolean hasNext() {
+ return this.index < JComboBoxItemSet.this.size();
+ }
+
+ @Override
+ public E next() {
+ if (this.hasNext())
+ return JComboBoxItemSet.this.comboBox.getItemAt(this.index++);
+ else
+ throw new NoSuchElementException(
+ "Iterator has finished iteration");
+ }
+ };
+ }
+
+ @Override
+ public int size() {
+ return this.comboBox.getItemCount();
+ }
+
+ }
+
+ /**
+ * The standard types of rounding, corresponding to the options on the
+ * TabbedView's settings panel.
+ *
+ * @since 2022-04-18
+ */
+ private static enum StandardRoundingType {
+ /**
+ * Rounds to a fixed number of significant digits. Precision is used,
+ * representing the number of significant digits to round to.
+ */
+ SIGNIFICANT_DIGITS,
+ /**
+ * Rounds to a fixed number of decimal places. Precision is used,
+ * representing the number of decimal places to round to.
+ */
+ DECIMAL_PLACES,
+ /**
+ * Rounds according to UncertainDouble's toString method. The specified
+ * precision is ignored.
+ */
+ UNCERTAINTY;
+ }
+
+ /**
+ * Creates a TabbedView.
+ *
+ * @param args command line arguments
+ * @since 2022-02-19
+ */
+ public static void main(String[] args) {
+ // This view doesn't need to do anything, the side effects of creating it
+ // are enough to start the program
+ @SuppressWarnings("unused")
+ final View view = new TabbedView();
+ }
+
+ /** The Presenter that handles this View */
+ final Presenter presenter;
+ /** The frame that this view lives on */
+ final JFrame frame;
+ /** The tabbed pane that contains all of the components */
+ final JTabbedPane masterPane;
+
+ // DIMENSION-BASED CONVERTER
+ /** The combo box that selects dimensions */
+ private final JComboBox<String> dimensionSelector;
+ /** The panel for inputting values in the dimension-based converter */
+ private final JTextField valueInput;
+ /** The panel for "From" in the dimension-based converter */
+ private final SearchBoxList<String> fromSearch;
+ /** The panel for "To" in the dimension-based converter */
+ private final SearchBoxList<String> toSearch;
+ /** The output area in the dimension-based converter */
+ private final JTextArea unitOutput;
+
+ // EXPRESSION-BASED CONVERTER
+ /** The "From" entry in the conversion panel */
+ private final JTextField fromEntry;
+ /** The "To" entry in the conversion panel */
+ private final JTextField toEntry;
+ /** The output area in the conversion panel */
+ private final JTextArea expressionOutput;
+
+ // UNIT AND PREFIX VIEWERS
+ /** The searchable list of unit names in the unit viewer */
+ private final SearchBoxList<String> unitNameList;
+ /** The searchable list of prefix names in the prefix viewer */
+ private final SearchBoxList<String> prefixNameList;
+ /** The text box for unit data in the unit viewer */
+ private final JTextArea unitTextBox;
+ /** The text box for prefix data in the prefix viewer */
+ private final JTextArea prefixTextBox;
+
+ // SETTINGS STUFF
+ private StandardRoundingType roundingType;
+ private int precision;
+
+ /**
+ * Creates the view and makes it visible to the user
+ *
+ * @since 2022-02-19
+ */
+ public TabbedView() {
+ // 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();
+ }
+
+ // initialize important components
+ this.presenter = new Presenter(this);
+ this.frame = new JFrame("7Units " + ProgramInfo.VERSION);
+ this.frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
+
+ // master components (those that contain everything else within them)
+ this.masterPane = new JTabbedPane();
+ this.frame.add(this.masterPane);
+
+ // ============ UNIT CONVERSION TAB ============
+ 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));
+ inputPanel.setBorder(new EmptyBorder(6, 6, 3, 6));
+
+ this.fromSearch = new SearchBoxList<>();
+ inputPanel.add(this.fromSearch);
+
+ final JPanel inBetweenPanel = new JPanel();
+ inputPanel.add(inBetweenPanel);
+ inBetweenPanel.setLayout(new BorderLayout());
+
+ this.dimensionSelector = new JComboBox<>();
+ inBetweenPanel.add(this.dimensionSelector, BorderLayout.PAGE_START);
+ this.dimensionSelector
+ .addItemListener(e -> this.presenter.updateView());
+
+ final JLabel arrowLabel = new JLabel("-->");
+ inBetweenPanel.add(arrowLabel, BorderLayout.CENTER);
+ arrowLabel.setHorizontalAlignment(SwingConstants.CENTER);
+
+ this.toSearch = new SearchBoxList<>();
+ inputPanel.add(this.toSearch);
+ }
+
+ { // panel for submit and output, and also value entry
+ final JPanel outputPanel = new JPanel();
+ convertUnitPanel.add(outputPanel, BorderLayout.PAGE_END);
+ outputPanel.setLayout(new BorderLayout());
+ outputPanel.setBorder(new EmptyBorder(3, 6, 6, 6));
+
+ final JLabel valuePrompt = new JLabel("Value to convert: ");
+ outputPanel.add(valuePrompt, BorderLayout.LINE_START);
+
+ this.valueInput = new JTextField();
+ outputPanel.add(this.valueInput, BorderLayout.CENTER);
+
+ // conversion button
+ final JButton convertButton = new JButton("Convert");
+ outputPanel.add(convertButton, BorderLayout.LINE_END);
+ convertButton.addActionListener(e -> this.presenter.convertUnits());
+ convertButton.setMnemonic(KeyEvent.VK_ENTER);
+
+ // conversion output
+ this.unitOutput = new JTextArea(2, 32);
+ outputPanel.add(this.unitOutput, BorderLayout.PAGE_END);
+ this.unitOutput.setEditable(false);
+ }
+
+ // ============ EXPRESSION CONVERSION TAB ============
+ final JPanel convertExpressionPanel = new JPanel();
+ this.masterPane.addTab("Convert Unit Expressions",
+ convertExpressionPanel);
+ this.masterPane.setMnemonicAt(1, KeyEvent.VK_E);
+ convertExpressionPanel.setLayout(new GridLayout(4, 1));
+
+ // from and to expressions
+ this.fromEntry = new JTextField();
+ convertExpressionPanel.add(this.fromEntry);
+ this.fromEntry.setBorder(BorderFactory.createTitledBorder("From"));
+
+ this.toEntry = new JTextField();
+ convertExpressionPanel.add(this.toEntry);
+ this.toEntry.setBorder(BorderFactory.createTitledBorder("To"));
+
+ // button to convert
+ final JButton convertButton = new JButton("Convert");
+ convertExpressionPanel.add(convertButton);
+
+ convertButton.addActionListener(e -> this.presenter.convertExpressions());
+ convertButton.setMnemonic(KeyEvent.VK_ENTER);
+
+ // output of conversion
+ this.expressionOutput = new JTextArea(2, 32);
+ convertExpressionPanel.add(this.expressionOutput);
+ this.expressionOutput
+ .setBorder(BorderFactory.createTitledBorder("Output"));
+ this.expressionOutput.setEditable(false);
+
+ // =========== UNIT VIEWER ===========
+ final JPanel unitLookupPanel = new JPanel();
+ this.masterPane.addTab("Unit Viewer", unitLookupPanel);
+ this.masterPane.setMnemonicAt(2, KeyEvent.VK_V);
+ unitLookupPanel.setLayout(new GridLayout());
+
+ this.unitNameList = new SearchBoxList<>();
+ unitLookupPanel.add(this.unitNameList);
+ this.unitNameList.getSearchList()
+ .addListSelectionListener(e -> this.presenter.unitNameSelected());
+
+ // the text box for unit's toString
+ this.unitTextBox = new JTextArea();
+ unitLookupPanel.add(this.unitTextBox);
+ this.unitTextBox.setEditable(false);
+ this.unitTextBox.setLineWrap(true);
+
+ // ============ PREFIX VIEWER =============
+ final JPanel prefixLookupPanel = new JPanel();
+ this.masterPane.addTab("Prefix Viewer", prefixLookupPanel);
+ this.masterPane.setMnemonicAt(3, KeyEvent.VK_P);
+ prefixLookupPanel.setLayout(new GridLayout(1, 2));
+
+ this.prefixNameList = new SearchBoxList<>();
+ prefixLookupPanel.add(this.prefixNameList);
+ this.prefixNameList.getSearchList()
+ .addListSelectionListener(e -> this.presenter.prefixSelected());
+
+ // the text box for prefix's toString
+ this.prefixTextBox = new JTextArea();
+ 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);
+ infoTextArea.setText(Presenter.getAboutText());
+
+ // ============ SETTINGS PANEL ============
+ this.masterPane.addTab("\u2699",
+ new JScrollPane(this.createSettingsPanel()));
+ this.masterPane.setMnemonicAt(5, KeyEvent.VK_S);
+
+ // ============ FINALIZE CREATION OF VIEW ============
+ this.presenter.postViewInitialize();
+ this.frame.pack();
+ this.frame.setVisible(true);
+ }
+
+ /**
+ * Creates and returns the settings panel (in its own function to make this
+ * code more organized, as this function is massive!)
+ *
+ * @since 2022-02-19
+ */
+ private JPanel createSettingsPanel() {
+ final JPanel settingsPanel = new JPanel();
+
+ 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();
+ this.roundingType = this.getPresenterRoundingType()
+ .orElseThrow(() -> new AssertionError(
+ "Presenter loaded non-standard rounding rule"));
+ this.precision = this.getPresenterPrecision().orElse(6);
+
+ final JLabel roundingRuleLabel = new JLabel("Rounding Rule:");
+ roundingPanel.add(roundingRuleLabel, new GridBagBuilder(0, 0)
+ .setAnchor(GridBagConstraints.LINE_START).build());
+
+ // sigDigSlider needs to be first so that the rounding-type buttons can
+ // show and hide it
+ final JLabel sliderLabel = new JLabel("Precision:");
+ sliderLabel.setVisible(
+ this.roundingType != StandardRoundingType.UNCERTAINTY);
+ 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.setVisible(
+ this.roundingType != StandardRoundingType.UNCERTAINTY);
+ sigDigSlider.setValue(this.precision);
+
+ sigDigSlider.addChangeListener(e -> {
+ this.precision = sigDigSlider.getValue();
+ this.updatePresenterRoundingRule();
+ });
+
+ // significant digit rounding
+ final JRadioButton fixedPrecision = new JRadioButton(
+ "Fixed Precision");
+ if (this.roundingType == StandardRoundingType.SIGNIFICANT_DIGITS) {
+ fixedPrecision.setSelected(true);
+ }
+ fixedPrecision.addActionListener(e -> {
+ this.roundingType = StandardRoundingType.SIGNIFICANT_DIGITS;
+ sliderLabel.setVisible(true);
+ sigDigSlider.setVisible(true);
+ this.updatePresenterRoundingRule();
+ });
+ roundingRuleButtons.add(fixedPrecision);
+ roundingPanel.add(fixedPrecision, new GridBagBuilder(0, 1)
+ .setAnchor(GridBagConstraints.LINE_START).build());
+
+ // decimal place rounding
+ final JRadioButton fixedDecimals = new JRadioButton(
+ "Fixed Decimal Places");
+ if (this.roundingType == StandardRoundingType.DECIMAL_PLACES) {
+ fixedDecimals.setSelected(true);
+ }
+ fixedDecimals.addActionListener(e -> {
+ this.roundingType = StandardRoundingType.DECIMAL_PLACES;
+ sliderLabel.setVisible(true);
+ sigDigSlider.setVisible(true);
+ this.updatePresenterRoundingRule();
+ });
+ roundingRuleButtons.add(fixedDecimals);
+ roundingPanel.add(fixedDecimals, new GridBagBuilder(0, 2)
+ .setAnchor(GridBagConstraints.LINE_START).build());
+
+ // scientific rounding
+ final JRadioButton relativePrecision = new JRadioButton(
+ "Uncertainty-Based Rounding");
+ if (this.roundingType == StandardRoundingType.UNCERTAINTY) {
+ relativePrecision.setSelected(true);
+ }
+ relativePrecision.addActionListener(e -> {
+ this.roundingType = StandardRoundingType.UNCERTAINTY;
+ sliderLabel.setVisible(false);
+ sigDigSlider.setVisible(false);
+ this.updatePresenterRoundingRule();
+ });
+ roundingRuleButtons.add(relativePrecision);
+ roundingPanel.add(relativePrecision, new GridBagBuilder(0, 3)
+ .setAnchor(GridBagConstraints.LINE_START).build());
+ }
+
+ // ============ PREFIX REPETITION SETTINGS ============
+ {
+ final JPanel prefixRepetitionPanel = new JPanel();
+ settingsPanel.add(prefixRepetitionPanel);
+ prefixRepetitionPanel
+ .setBorder(new TitledBorder("Prefix Repetition Settings"));
+ prefixRepetitionPanel.setLayout(new GridBagLayout());
+
+ final var prefixRule = this.getPresenterPrefixRule()
+ .orElseThrow(() -> new AssertionError(
+ "Presenter loaded non-standard prefix rule"));
+
+ // prefix rules
+ final ButtonGroup prefixRuleButtons = new ButtonGroup();
+
+ final JRadioButton noRepetition = new JRadioButton("No Repetition");
+ if (prefixRule == DefaultPrefixRepetitionRule.NO_REPETITION) {
+ noRepetition.setSelected(true);
+ }
+ noRepetition.addActionListener(e -> {
+ this.presenter.setPrefixRepetitionRule(
+ DefaultPrefixRepetitionRule.NO_REPETITION);
+ this.presenter.saveSettings();
+ });
+ prefixRuleButtons.add(noRepetition);
+ prefixRepetitionPanel.add(noRepetition, new GridBagBuilder(0, 0)
+ .setAnchor(GridBagConstraints.LINE_START).build());
+
+ final JRadioButton noRestriction = new JRadioButton("No Restriction");
+ if (prefixRule == DefaultPrefixRepetitionRule.NO_RESTRICTION) {
+ noRestriction.setSelected(true);
+ }
+ noRestriction.addActionListener(e -> {
+ this.presenter.setPrefixRepetitionRule(
+ DefaultPrefixRepetitionRule.NO_RESTRICTION);
+ this.presenter.saveSettings();
+ });
+ prefixRuleButtons.add(noRestriction);
+ prefixRepetitionPanel.add(noRestriction, new GridBagBuilder(0, 1)
+ .setAnchor(GridBagConstraints.LINE_START).build());
+
+ final JRadioButton customRepetition = new JRadioButton(
+ "Complex Repetition");
+ if (prefixRule == DefaultPrefixRepetitionRule.COMPLEX_REPETITION) {
+ customRepetition.setSelected(true);
+ }
+ customRepetition.addActionListener(e -> {
+ this.presenter.setPrefixRepetitionRule(
+ DefaultPrefixRepetitionRule.COMPLEX_REPETITION);
+ this.presenter.saveSettings();
+ });
+ 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());
+ }
+
+ // ============ OTHER SETTINGS ============
+ {
+ final JPanel miscPanel = new JPanel();
+ settingsPanel.add(miscPanel);
+ miscPanel.setLayout(new GridBagLayout());
+
+ final JCheckBox oneWay = new JCheckBox("Convert One Way Only");
+ oneWay.setSelected(this.presenter.oneWayConversionEnabled());
+ oneWay.addItemListener(e -> {
+ this.presenter.setOneWayConversionEnabled(
+ e.getStateChange() == ItemEvent.SELECTED);
+ this.presenter.saveSettings();
+ });
+ miscPanel.add(oneWay, new GridBagBuilder(0, 0)
+ .setAnchor(GridBagConstraints.LINE_START).build());
+
+ final JCheckBox showAllVariations = new JCheckBox(
+ "Show Duplicate Units & Prefixes");
+ showAllVariations.setSelected(this.presenter.duplicatesShown());
+ showAllVariations.addItemListener(e -> {
+ this.presenter
+ .setShowDuplicates(e.getStateChange() == ItemEvent.SELECTED);
+ this.presenter.saveSettings();
+ });
+ 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());
+ }
+
+ return settingsPanel;
+ }
+
+ @Override
+ public Set<String> getDimensionNames() {
+ return Collections
+ .unmodifiableSet(new JComboBoxItemSet<>(this.dimensionSelector));
+ }
+
+ @Override
+ public String getFromExpression() {
+ return this.fromEntry.getText();
+ }
+
+ @Override
+ public Optional<String> getFromSelection() {
+ return this.fromSearch.getSelectedValue();
+ }
+
+ @Override
+ public Set<String> getFromUnitNames() {
+ // this should work because the only way I can mutate the item list is
+ // with setFromUnits which only accepts a Set
+ return new HashSet<>(this.fromSearch.getItems());
+ }
+
+ @Override
+ public String getInputValue() {
+ return this.valueInput.getText();
+ }
+
+ @Override
+ public Presenter getPresenter() {
+ return this.presenter;
+ }
+
+ /**
+ * @return the precision of the presenter's rounding rule, if that is
+ * meaningful
+ * @since 2022-04-18
+ */
+ private OptionalInt getPresenterPrecision() {
+ final var presenterRule = this.presenter.getNumberDisplayRule();
+ if (presenterRule instanceof StandardDisplayRules.FixedDecimals)
+ return OptionalInt
+ .of(((StandardDisplayRules.FixedDecimals) presenterRule)
+ .decimalPlaces());
+ else if (presenterRule instanceof StandardDisplayRules.FixedPrecision)
+ return OptionalInt
+ .of(((StandardDisplayRules.FixedPrecision) presenterRule)
+ .significantFigures());
+ else
+ return OptionalInt.empty();
+ }
+
+ /**
+ * @return presenter's prefix repetition rule
+ * @since 2022-04-19
+ */
+ private Optional<DefaultPrefixRepetitionRule> getPresenterPrefixRule() {
+ final var prefixRule = this.presenter.getPrefixRepetitionRule();
+ return prefixRule instanceof DefaultPrefixRepetitionRule
+ ? Optional.of((DefaultPrefixRepetitionRule) prefixRule)
+ : Optional.empty();
+ }
+
+ /**
+ * Determines which rounding type the presenter is currently using, if any.
+ *
+ * @since 2022-04-18
+ */
+ private Optional<StandardRoundingType> getPresenterRoundingType() {
+ final var presenterRule = this.presenter.getNumberDisplayRule();
+ if (Objects.equals(presenterRule,
+ StandardDisplayRules.uncertaintyBased()))
+ return Optional.of(StandardRoundingType.UNCERTAINTY);
+ else if (presenterRule instanceof StandardDisplayRules.FixedDecimals)
+ return Optional.of(StandardRoundingType.DECIMAL_PLACES);
+ else if (presenterRule instanceof StandardDisplayRules.FixedPrecision)
+ return Optional.of(StandardRoundingType.SIGNIFICANT_DIGITS);
+ else
+ return Optional.empty();
+ }
+
+ @Override
+ public Optional<String> getSelectedDimensionName() {
+ final String selectedItem = (String) this.dimensionSelector
+ .getSelectedItem();
+ return Optional.ofNullable(selectedItem);
+ }
+
+ @Override
+ public String getToExpression() {
+ return this.toEntry.getText();
+ }
+
+ @Override
+ public Optional<String> getToSelection() {
+ return this.toSearch.getSelectedValue();
+ }
+
+ @Override
+ public Set<String> getToUnitNames() {
+ // this should work because the only way I can mutate the item list is
+ // with setToUnits which only accepts a Set
+ return new HashSet<>(this.toSearch.getItems());
+ }
+
+ @Override
+ public Optional<String> getViewedPrefixName() {
+ return this.prefixNameList.getSelectedValue();
+ }
+
+ @Override
+ public Optional<String> getViewedUnitName() {
+ return this.unitNameList.getSelectedValue();
+ }
+
+ @Override
+ public void setDimensionNames(Set<String> dimensionNames) {
+ this.dimensionSelector.removeAllItems();
+ for (final String d : dimensionNames) {
+ this.dimensionSelector.addItem(d);
+ }
+ }
+
+ @Override
+ public void setFromUnitNames(Set<String> units) {
+ this.fromSearch.setItems(units);
+ }
+
+ @Override
+ public void setToUnitNames(Set<String> units) {
+ this.toSearch.setItems(units);
+ }
+
+ @Override
+ public void setViewablePrefixNames(Set<String> prefixNames) {
+ this.prefixNameList.setItems(prefixNames);
+ }
+
+ @Override
+ public void setViewableUnitNames(Set<String> unitNames) {
+ this.unitNameList.setItems(unitNames);
+ }
+
+ @Override
+ public void showErrorMessage(String title, String message) {
+ JOptionPane.showMessageDialog(this.frame, message, title,
+ JOptionPane.ERROR_MESSAGE);
+ }
+
+ @Override
+ public void showExpressionConversionOutput(UnitConversionRecord uc) {
+ this.expressionOutput.setText(String.format("%s = %s %s", uc.fromName(),
+ uc.outputValueString(), uc.toName()));
+ }
+
+ @Override
+ public void showPrefix(NameSymbol name, String multiplierString) {
+ this.prefixTextBox.setText(
+ String.format("%s%nMultiplier: %s", name, multiplierString));
+ }
+
+ @Override
+ public void showUnit(NameSymbol name, String definition,
+ String dimensionName, UnitType type) {
+ this.unitTextBox.setText(
+ String.format("%s%nDefinition: %s%nDimension: %s%nType: %s", name,
+ definition, dimensionName, type));
+ }
+
+ @Override
+ public void showUnitConversionOutput(UnitConversionRecord uc) {
+ this.unitOutput.setText(uc.toString());
+ }
+
+ /**
+ * Sets the presenter's rounding rule to the one specified by the current
+ * settings
+ *
+ * @since 2022-04-18
+ */
+ private void updatePresenterRoundingRule() {
+ final Function<UncertainDouble, String> roundingRule;
+ switch (this.roundingType) {
+ case DECIMAL_PLACES:
+ roundingRule = StandardDisplayRules.fixedDecimals(this.precision);
+ break;
+ case SIGNIFICANT_DIGITS:
+ roundingRule = StandardDisplayRules.fixedPrecision(this.precision);
+ break;
+ case UNCERTAINTY:
+ roundingRule = StandardDisplayRules.uncertaintyBased();
+ break;
+ default:
+ throw new AssertionError();
+ }
+ this.presenter.setNumberDisplayRule(roundingRule);
+ this.presenter.saveSettings();
+ }
+}
diff --git a/src/main/java/sevenUnitsGUI/UnitConversionRecord.java b/src/main/java/sevenUnitsGUI/UnitConversionRecord.java
new file mode 100644
index 0000000..f951f44
--- /dev/null
+++ b/src/main/java/sevenUnitsGUI/UnitConversionRecord.java
@@ -0,0 +1,199 @@
+/**
+ * Copyright (C) 2022 Adrien Hopkins
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package sevenUnitsGUI;
+
+import java.math.RoundingMode;
+
+import sevenUnits.unit.LinearUnitValue;
+import sevenUnits.unit.UnitValue;
+
+/**
+ * A record of a conversion between units or expressions
+ *
+ * @since 2022-04-09
+ */
+public final class UnitConversionRecord {
+ /**
+ * Gets a {@code UnitConversionRecord} from two linear unit values
+ *
+ * @param input input unit & value
+ * @param output output unit & value
+ * @return unit conversion record
+ * @since 2022-04-09
+ */
+ public static UnitConversionRecord fromLinearValues(LinearUnitValue input,
+ LinearUnitValue output) {
+ return UnitConversionRecord.valueOf(input.getUnit().getName(),
+ output.getUnit().getName(),
+ input.getValue().toString(false, RoundingMode.HALF_EVEN),
+ output.getValue().toString(false, RoundingMode.HALF_EVEN));
+ }
+
+ /**
+ * Gets a {@code UnitConversionRecord} from two unit values
+ *
+ * @param input input unit & value
+ * @param output output unit & value
+ * @return unit conversion record
+ * @since 2022-04-09
+ */
+ public static UnitConversionRecord fromValues(UnitValue input,
+ UnitValue output) {
+ return UnitConversionRecord.valueOf(input.getUnit().getName(),
+ output.getUnit().getName(), String.valueOf(input.getValue()),
+ String.valueOf(output.getValue()));
+ }
+
+ /**
+ * Gets a {@code UnitConversionRecord}
+ *
+ * @param fromName name of unit or expression that was converted
+ * from
+ * @param toName name of unit or expression that was converted to
+ * @param inputValueString string representing input value
+ * @param outputValueString string representing output value
+ * @return unit conversion record
+ * @since 2022-04-09
+ */
+ public static UnitConversionRecord valueOf(String fromName, String toName,
+ String inputValueString, String outputValueString) {
+ return new UnitConversionRecord(fromName, toName, inputValueString,
+ outputValueString);
+ }
+
+ /**
+ * The name of the unit or expression that was converted from
+ */
+ private final String fromName;
+ /**
+ * The name of the unit or expression that was converted to
+ */
+ private final String toName;
+
+ /**
+ * A string representing the input value. It doesn't need to be the same as
+ * the input value's string representation; it could be rounded, for example.
+ */
+ private final String inputValueString;
+ /**
+ * A string representing the input value. It doesn't need to be the same as
+ * the input value's string representation; it could be rounded, for example.
+ */
+ private final String outputValueString;
+
+ /**
+ * @param fromName name of unit or expression that was converted
+ * from
+ * @param toName name of unit or expression that was converted to
+ * @param inputValueString string representing input value
+ * @param outputValueString string representing output value
+ * @since 2022-04-09
+ */
+ private UnitConversionRecord(String fromName, String toName,
+ String inputValueString, String outputValueString) {
+ this.fromName = fromName;
+ this.toName = toName;
+ this.inputValueString = inputValueString;
+ this.outputValueString = outputValueString;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (!(obj instanceof UnitConversionRecord))
+ return false;
+ final UnitConversionRecord other = (UnitConversionRecord) obj;
+ if (this.fromName == null) {
+ if (other.fromName != null)
+ return false;
+ } else if (!this.fromName.equals(other.fromName))
+ return false;
+ if (this.inputValueString == null) {
+ if (other.inputValueString != null)
+ return false;
+ } else if (!this.inputValueString.equals(other.inputValueString))
+ return false;
+ if (this.outputValueString == null) {
+ if (other.outputValueString != null)
+ return false;
+ } else if (!this.outputValueString.equals(other.outputValueString))
+ return false;
+ if (this.toName == null) {
+ if (other.toName != null)
+ return false;
+ } else if (!this.toName.equals(other.toName))
+ return false;
+ return true;
+ }
+
+ /**
+ * @return name of unit or expression that was converted from
+ * @since 2022-04-09
+ */
+ public String fromName() {
+ return this.fromName;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result
+ + (this.fromName == null ? 0 : this.fromName.hashCode());
+ result = prime * result + (this.inputValueString == null ? 0
+ : this.inputValueString.hashCode());
+ result = prime * result + (this.outputValueString == null ? 0
+ : this.outputValueString.hashCode());
+ result = prime * result
+ + (this.toName == null ? 0 : this.toName.hashCode());
+ return result;
+ }
+
+ /**
+ * @return string representing input value
+ * @since 2022-04-09
+ */
+ public String inputValueString() {
+ return this.inputValueString;
+ }
+
+ /**
+ * @return string representing output value
+ * @since 2022-04-09
+ */
+ public String outputValueString() {
+ return this.outputValueString;
+ }
+
+ /**
+ * @return name of unit or expression that was converted to
+ * @since 2022-04-09
+ */
+ public String toName() {
+ return this.toName;
+ }
+
+ @Override
+ public String toString() {
+ final String inputString = this.inputValueString.isBlank() ? this.fromName
+ : this.inputValueString + " " + this.fromName;
+ final String outputString = this.outputValueString.isBlank() ? this.toName
+ : this.outputValueString + " " + this.toName;
+ return inputString + " = " + outputString;
+ }
+}
diff --git a/src/main/java/sevenUnitsGUI/UnitConversionView.java b/src/main/java/sevenUnitsGUI/UnitConversionView.java
new file mode 100644
index 0000000..6a95aa5
--- /dev/null
+++ b/src/main/java/sevenUnitsGUI/UnitConversionView.java
@@ -0,0 +1,108 @@
+/**
+ * Copyright (C) 2021-2022 Adrien Hopkins
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package sevenUnitsGUI;
+
+import java.util.Optional;
+import java.util.Set;
+
+/**
+ * A View that supports single unit-based conversion
+ *
+ * @author Adrien Hopkins
+ * @since 2021-12-15
+ */
+public interface UnitConversionView extends View {
+ /**
+ * @return dimensions available for filtering
+ * @since 2022-01-29
+ */
+ Set<String> getDimensionNames();
+
+ /**
+ * @return name of unit to convert <em>from</em>
+ * @since 2021-12-15
+ */
+ Optional<String> getFromSelection();
+
+ /**
+ * @return list of names of units available to convert from
+ * @since 2022-03-30
+ */
+ Set<String> getFromUnitNames();
+
+ /**
+ * @return value to convert between the units (specifically, the numeric
+ * string provided by the user)
+ * @since 2021-12-15
+ */
+ String getInputValue();
+
+ /**
+ * @return selected dimension
+ * @since 2021-12-15
+ */
+ Optional<String> getSelectedDimensionName();
+
+ /**
+ * @return name of unit to convert <em>to</em>
+ * @since 2021-12-15
+ */
+ Optional<String> getToSelection();
+
+ /**
+ * @return list of names of units available to convert to
+ * @since 2022-03-30
+ */
+ Set<String> getToUnitNames();
+
+ /**
+ * Sets the available dimensions for filtering.
+ *
+ * @param dimensionNames names of dimensions to use
+ * @since 2021-12-15
+ */
+ void setDimensionNames(Set<String> dimensionNames);
+
+ /**
+ * Sets the available units to convert from. {@link #getFromSelection} is not
+ * required to use one of these units; this method is to be used for views
+ * that allow the user to select units from a list.
+ *
+ * @param unitNames names of units to convert from
+ * @since 2021-12-15
+ */
+ void setFromUnitNames(Set<String> unitNames);
+
+ /**
+ * Sets the available units to convert to. {@link #getToSelection} is not
+ * required to use one of these units; this method is to be used for views
+ * that allow the user to select units from a list.
+ *
+ * @param unitNames names of units to convert to
+ * @since 2021-12-15
+ */
+ void setToUnitNames(Set<String> unitNames);
+
+ /**
+ * Shows the output of a unit conversion.
+ *
+ * @param input input unit & value (obtained from this view)
+ * @param output output unit & value
+ * @since 2021-12-24
+ */
+ void showUnitConversionOutput(UnitConversionRecord uc);
+}
diff --git a/src/main/java/sevenUnitsGUI/View.java b/src/main/java/sevenUnitsGUI/View.java
new file mode 100644
index 0000000..b2d2b94
--- /dev/null
+++ b/src/main/java/sevenUnitsGUI/View.java
@@ -0,0 +1,105 @@
+/**
+ * Copyright (C) 2021-2022 Adrien Hopkins
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package sevenUnitsGUI;
+
+import java.util.Optional;
+import java.util.Set;
+
+import sevenUnits.unit.UnitType;
+import sevenUnits.utils.NameSymbol;
+
+/**
+ * An object that controls user interaction with 7Units
+ *
+ * @author Adrien Hopkins
+ * @since 2021-12-15
+ */
+public interface View {
+ /**
+ * @return a new tabbed view
+ * @since 2022-04-19
+ */
+ static View createTabbedView() {
+ return new TabbedView();
+ }
+
+ /**
+ * @return the presenter associated with this view
+ * @since 2022-04-19
+ */
+ Presenter getPresenter();
+
+ /**
+ * @return name of prefix currently being viewed
+ * @since 2022-04-10
+ */
+ Optional<String> getViewedPrefixName();
+
+ /**
+ * @return name of unit currently being viewed
+ * @since 2022-04-10
+ */
+ Optional<String> getViewedUnitName();
+
+ /**
+ * Sets the list of prefixes that are available to be viewed in a prefix
+ * viewer
+ *
+ * @param prefixNames prefix names to view
+ * @since 2022-04-10
+ */
+ void setViewablePrefixNames(Set<String> prefixNames);
+
+ /**
+ * Sets the list of units that are available to be viewed in a unit viewer
+ *
+ * @param unitNames unit names to view
+ * @since 2022-04-10
+ */
+ void setViewableUnitNames(Set<String> unitNames);
+
+ /**
+ * Shows an error message.
+ *
+ * @param title title of error message; on any view that uses an error
+ * dialog, this should be the title of the error dialog.
+ * @param message error message
+ * @since 2021-12-15
+ */
+ void showErrorMessage(String title, String message);
+
+ /**
+ * Shows information about a prefix to the user.
+ *
+ * @param name name(s) and symbol of prefix
+ * @param multiplierString string representation of prefix multiplier
+ * @since 2022-04-10
+ */
+ void showPrefix(NameSymbol name, String multiplierString);
+
+ /**
+ * Shows information about a unit to the user.
+ *
+ * @param name name(s) and symbol of unit
+ * @param definition unit's definition string
+ * @param dimensionName name of unit's dimension
+ * @param type type of unit (metric/semi-metric/non-metric)
+ * @since 2022-04-10
+ */
+ void showUnit(NameSymbol name, String definition, String dimensionName,
+ UnitType type);
+}
diff --git a/src/main/java/sevenUnitsGUI/ViewBot.java b/src/main/java/sevenUnitsGUI/ViewBot.java
new file mode 100644
index 0000000..a3ba7a2
--- /dev/null
+++ b/src/main/java/sevenUnitsGUI/ViewBot.java
@@ -0,0 +1,507 @@
+/**
+ * Copyright (C) 2022 Adrien Hopkins
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package sevenUnitsGUI;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+
+import sevenUnits.unit.UnitType;
+import sevenUnits.utils.NameSymbol;
+import sevenUnits.utils.Nameable;
+
+/**
+ * A class that simulates a View (supports both unit and expression conversion)
+ * for testing. Getters and setters work as expected.
+ *
+ * @author Adrien Hopkins
+ * @since 2022-01-29
+ */
+public final class ViewBot
+ implements UnitConversionView, ExpressionConversionView {
+ /**
+ * A record of the parameters given to
+ * {@link View#showPrefix(NameSymbol, String)}, for testing.
+ *
+ * @since 2022-04-16
+ */
+ public static final class PrefixViewingRecord implements Nameable {
+ private final NameSymbol nameSymbol;
+ private final String multiplierString;
+
+ /**
+ * @param nameSymbol
+ * @param multiplierString
+ * @since 2022-04-16
+ */
+ public PrefixViewingRecord(NameSymbol nameSymbol,
+ String multiplierString) {
+ this.nameSymbol = nameSymbol;
+ this.multiplierString = multiplierString;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (!(obj instanceof PrefixViewingRecord))
+ return false;
+ final PrefixViewingRecord other = (PrefixViewingRecord) obj;
+ return Objects.equals(this.multiplierString, other.multiplierString)
+ && Objects.equals(this.nameSymbol, other.nameSymbol);
+ }
+
+ @Override
+ public NameSymbol getNameSymbol() {
+ return this.nameSymbol;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(this.multiplierString, this.nameSymbol);
+ }
+
+ public String multiplierString() {
+ return this.multiplierString;
+ }
+
+ public NameSymbol nameSymbol() {
+ return this.nameSymbol;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder builder = new StringBuilder();
+ builder.append("PrefixViewingRecord [nameSymbol=");
+ builder.append(this.nameSymbol);
+ builder.append(", multiplierString=");
+ builder.append(this.multiplierString);
+ builder.append("]");
+ return builder.toString();
+ }
+ }
+
+ /**
+ * A record of the parameters given to
+ * {@link View#showUnit(NameSymbol, String, String, UnitType)}, for testing.
+ *
+ * @since 2022-04-16
+ */
+ public static final class UnitViewingRecord implements Nameable {
+ private final NameSymbol nameSymbol;
+ private final String definition;
+ private final String dimensionName;
+ private final UnitType unitType;
+
+ /**
+ * @since 2022-04-16
+ */
+ public UnitViewingRecord(NameSymbol nameSymbol, String definition,
+ String dimensionName, UnitType unitType) {
+ this.nameSymbol = nameSymbol;
+ this.definition = definition;
+ this.dimensionName = dimensionName;
+ this.unitType = unitType;
+ }
+
+ /**
+ * @return the definition
+ * @since 2022-04-16
+ */
+ public String definition() {
+ return this.definition;
+ }
+
+ /**
+ * @return the dimensionName
+ * @since 2022-04-16
+ */
+ public String dimensionName() {
+ return this.dimensionName;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (!(obj instanceof UnitViewingRecord))
+ return false;
+ final UnitViewingRecord other = (UnitViewingRecord) obj;
+ return Objects.equals(this.definition, other.definition)
+ && Objects.equals(this.dimensionName, other.dimensionName)
+ && Objects.equals(this.nameSymbol, other.nameSymbol)
+ && this.unitType == other.unitType;
+ }
+
+ /**
+ * @return the nameSymbol
+ * @since 2022-04-16
+ */
+ @Override
+ public NameSymbol getNameSymbol() {
+ return this.nameSymbol;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(this.definition, this.dimensionName,
+ this.nameSymbol, this.unitType);
+ }
+
+ public NameSymbol nameSymbol() {
+ return this.nameSymbol;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder builder = new StringBuilder();
+ builder.append("UnitViewingRecord [nameSymbol=");
+ builder.append(this.nameSymbol);
+ builder.append(", definition=");
+ builder.append(this.definition);
+ builder.append(", dimensionName=");
+ builder.append(this.dimensionName);
+ builder.append(", unitType=");
+ builder.append(this.unitType);
+ builder.append("]");
+ return builder.toString();
+ }
+
+ /**
+ * @return the unitType
+ * @since 2022-04-16
+ */
+ public UnitType unitType() {
+ return this.unitType;
+ }
+ }
+
+ /** The presenter that works with this ViewBot */
+ private final Presenter presenter;
+
+ /** The dimensions available to select from */
+ private Set<String> dimensionNames = Set.of();
+ /** The expression in the From field */
+ private String fromExpression = "";
+ /** The expression in the To field */
+ private String toExpression = "";
+ /**
+ * The user-provided string representing the value in {@code fromSelection}
+ */
+ private String inputValue = "";
+ /** The unit selected in the From selection */
+ private Optional<String> fromSelection = Optional.empty();
+ /** The unit selected in the To selection */
+ private Optional<String> toSelection = Optional.empty();
+ /** The currently selected dimension */
+ private Optional<String> selectedDimensionName = Optional.empty();
+ /** The units available in the From selection */
+ private Set<String> fromUnits = Set.of();
+ /** The units available in the To selection */
+ private Set<String> toUnits = Set.of();
+
+ /** The selected unit in the unit viewer */
+ private Optional<String> unitViewerSelection = Optional.empty();
+ /** The selected unit in the prefix viewer */
+ private Optional<String> prefixViewerSelection = Optional.empty();
+
+ /** Saved outputs of all unit conversions */
+ private final List<UnitConversionRecord> unitConversions;
+ /** Saved outputs of all unit expressions */
+ private final List<UnitConversionRecord> expressionConversions;
+ /** Saved outputs of all unit viewings */
+ private final List<UnitViewingRecord> unitViewingRecords;
+ /** Saved outputs of all prefix viewings */
+ private final List<PrefixViewingRecord> prefixViewingRecords;
+
+ /**
+ * Creates a new {@code ViewBot} with a new presenter.
+ *
+ * @since 2022-01-29
+ */
+ public ViewBot() {
+ this.presenter = new Presenter(this);
+
+ this.unitConversions = new ArrayList<>();
+ this.expressionConversions = new ArrayList<>();
+ this.unitViewingRecords = new ArrayList<>();
+ this.prefixViewingRecords = new ArrayList<>();
+ }
+
+ /**
+ * @return list of records of expression conversions done by this bot
+ * @since 2022-04-09
+ */
+ public List<UnitConversionRecord> expressionConversionList() {
+ return Collections.unmodifiableList(this.expressionConversions);
+ }
+
+ /**
+ * @return the available dimensions
+ * @since 2022-01-29
+ */
+ @Override
+ public Set<String> getDimensionNames() {
+ return this.dimensionNames;
+ }
+
+ @Override
+ public String getFromExpression() {
+ return this.fromExpression;
+ }
+
+ @Override
+ public Optional<String> getFromSelection() {
+ return this.fromSelection;
+ }
+
+ /**
+ * @return the units available for selection in From
+ * @since 2022-01-29
+ */
+ @Override
+ public Set<String> getFromUnitNames() {
+ return Collections.unmodifiableSet(this.fromUnits);
+ }
+
+ @Override
+ public String getInputValue() {
+ return this.inputValue;
+ }
+
+ /**
+ * @return the presenter associated with tihs view
+ * @since 2022-01-29
+ */
+ @Override
+ public Presenter getPresenter() {
+ return this.presenter;
+ }
+
+ @Override
+ public Optional<String> getSelectedDimensionName() {
+ return this.selectedDimensionName;
+ }
+
+ @Override
+ public String getToExpression() {
+ return this.toExpression;
+ }
+
+ @Override
+ public Optional<String> getToSelection() {
+ return this.toSelection;
+ }
+
+ /**
+ * @return the units available for selection in To
+ * @since 2022-01-29
+ */
+ @Override
+ public Set<String> getToUnitNames() {
+ return Collections.unmodifiableSet(this.toUnits);
+ }
+
+ @Override
+ public Optional<String> getViewedPrefixName() {
+ return this.prefixViewerSelection;
+ }
+
+ @Override
+ public Optional<String> getViewedUnitName() {
+ return this.unitViewerSelection;
+ }
+
+ /**
+ * @return list of records of this viewBot's prefix views
+ * @since 2022-04-16
+ */
+ public List<PrefixViewingRecord> prefixViewList() {
+ return Collections.unmodifiableList(this.prefixViewingRecords);
+ }
+
+ @Override
+ public void setDimensionNames(Set<String> dimensionNames) {
+ this.dimensionNames = Objects.requireNonNull(dimensionNames,
+ "dimensions may not be null");
+ }
+
+ /**
+ * Sets the From expression (as in {@link #getFromExpression}).
+ *
+ * @param fromExpression the expression to convert from
+ * @throws NullPointerException if {@code fromExpression} is null
+ * @since 2022-01-29
+ */
+ public void setFromExpression(String fromExpression) {
+ this.fromExpression = Objects.requireNonNull(fromExpression,
+ "fromExpression cannot be null.");
+ }
+
+ /**
+ * @param fromSelection the fromSelection to set
+ * @since 2022-01-29
+ */
+ public void setFromSelection(Optional<String> fromSelection) {
+ this.fromSelection = Objects.requireNonNull(fromSelection,
+ "fromSelection cannot be null");
+ }
+
+ /**
+ * @param fromSelection the fromSelection to set
+ * @since 2022-02-10
+ */
+ public void setFromSelection(String fromSelection) {
+ this.setFromSelection(Optional.of(fromSelection));
+ }
+
+ @Override
+ public void setFromUnitNames(Set<String> units) {
+ this.fromUnits = Objects.requireNonNull(units, "units may not be null");
+ }
+
+ /**
+ * @param inputValue the inputValue to set
+ * @since 2022-01-29
+ */
+ public void setInputValue(String inputValue) {
+ this.inputValue = inputValue;
+ }
+
+ /**
+ * @param selectedDimension the selectedDimension to set
+ * @since 2022-01-29
+ */
+ public void setSelectedDimensionName(
+ Optional<String> selectedDimensionName) {
+ this.selectedDimensionName = selectedDimensionName;
+ }
+
+ public void setSelectedDimensionName(String selectedDimensionName) {
+ this.setSelectedDimensionName(Optional.of(selectedDimensionName));
+ }
+
+ /**
+ * Sets the To expression (as in {@link #getToExpression}).
+ *
+ * @param toExpression the expression to convert to
+ * @throws NullPointerException if {@code toExpression} is null
+ * @since 2022-01-29
+ */
+ public void setToExpression(String toExpression) {
+ this.toExpression = Objects.requireNonNull(toExpression,
+ "toExpression cannot be null.");
+ }
+
+ /**
+ * @param toSelection the toSelection to set
+ * @since 2022-01-29
+ */
+ public void setToSelection(Optional<String> toSelection) {
+ this.toSelection = Objects.requireNonNull(toSelection,
+ "toSelection cannot be null.");
+ }
+
+ public void setToSelection(String toSelection) {
+ this.setToSelection(Optional.of(toSelection));
+ }
+
+ @Override
+ public void setToUnitNames(Set<String> units) {
+ this.toUnits = Objects.requireNonNull(units, "units may not be null");
+ }
+
+ @Override
+ public void setViewablePrefixNames(Set<String> prefixNames) {
+ // do nothing, ViewBot supports selecting any prefix
+ }
+
+ @Override
+ public void setViewableUnitNames(Set<String> unitNames) {
+ // do nothing, ViewBot supports selecting any unit
+ }
+
+ public void setViewedPrefixName(Optional<String> viewedPrefixName) {
+ this.prefixViewerSelection = viewedPrefixName;
+ }
+
+ public void setViewedPrefixName(String viewedPrefixName) {
+ this.setViewedPrefixName(Optional.of(viewedPrefixName));
+ }
+
+ public void setViewedUnitName(Optional<String> viewedUnitName) {
+ this.unitViewerSelection = viewedUnitName;
+ }
+
+ public void setViewedUnitName(String viewedUnitName) {
+ this.setViewedUnitName(Optional.of(viewedUnitName));
+ }
+
+ @Override
+ public void showErrorMessage(String title, String message) {
+ System.err.printf("%s: %s%n", title, message);
+ }
+
+ @Override
+ public void showExpressionConversionOutput(UnitConversionRecord uc) {
+ this.expressionConversions.add(uc);
+ System.out.println("Expression Conversion: " + uc);
+ }
+
+ @Override
+ public void showPrefix(NameSymbol name, String multiplierString) {
+ this.prefixViewingRecords
+ .add(new PrefixViewingRecord(name, multiplierString));
+ }
+
+ @Override
+ public void showUnit(NameSymbol name, String definition,
+ String dimensionName, UnitType type) {
+ this.unitViewingRecords
+ .add(new UnitViewingRecord(name, definition, dimensionName, type));
+ }
+
+ @Override
+ public void showUnitConversionOutput(UnitConversionRecord uc) {
+ this.unitConversions.add(uc);
+ System.out.println("Unit Conversion: " + uc);
+ }
+
+ @Override
+ public String toString() {
+ return super.toString() + String.format("[presenter=%s]", this.presenter);
+ }
+
+ /**
+ * @return list of records of every unit conversion made by this bot
+ * @since 2022-04-09
+ */
+ public List<UnitConversionRecord> unitConversionList() {
+ return Collections.unmodifiableList(this.unitConversions);
+ }
+
+ /**
+ * @return list of records of unit viewings made by this bot
+ * @since 2022-04-16
+ */
+ public List<UnitViewingRecord> unitViewList() {
+ return Collections.unmodifiableList(this.unitViewingRecords);
+ }
+}
diff --git a/src/main/java/sevenUnits/converterGUI/package-info.java b/src/main/java/sevenUnitsGUI/package-info.java
index 784664f..cff1ded 100644
--- a/src/main/java/sevenUnits/converterGUI/package-info.java
+++ b/src/main/java/sevenUnitsGUI/package-info.java
@@ -1,5 +1,5 @@
/**
- * Copyright (C) 2019 Adrien Hopkins
+ * Copyright (C) 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
@@ -15,10 +15,9 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
/**
- * The GUI interface of the Unit Converter.
+ * The MVP GUI of SevenUnits
*
* @author Adrien Hopkins
- * @since 2019-01-25
- * @since v0.2.0
+ * @since 2021-12-15
*/
-package sevenUnits.converterGUI; \ No newline at end of file
+package sevenUnitsGUI; \ No newline at end of file
diff --git a/src/main/resources/about.txt b/src/main/resources/about.txt
index f175396..7780db3 100644
--- a/src/main/resources/about.txt
+++ b/src/main/resources/about.txt
@@ -2,7 +2,7 @@ About 7Units v[VERSION]
Copyright Notice:
-Unit Converter Copyright (C) 2018-2021 Adrien Hopkins
+Unit Converter Copyright (C) 2018-2022 Adrien Hopkins
This program comes with ABSOLUTELY NO WARRANTY;
for details read the LICENSE file, section 15
diff --git a/src/main/resources/dimensionfile.txt b/src/main/resources/dimensionfile.txt
index 3485de5..a946677 100644
--- a/src/main/resources/dimensionfile.txt
+++ b/src/main/resources/dimensionfile.txt
@@ -12,7 +12,7 @@ TIME !
TEMPERATURE !
# Derived Dimensions
-AREA LENGTH^2
-VOLUME LENGTH^3
-VELOCITY LENGTH / TIME
-ENERGY MASS * VELOCITY^2 \ No newline at end of file
+Area LENGTH^2
+Volume LENGTH^3
+Velocity LENGTH / TIME
+Energy MASS * Velocity^2 \ No newline at end of file
diff --git a/src/test/java/sevenUnits/unit/UnitDatabaseTest.java b/src/test/java/sevenUnits/unit/UnitDatabaseTest.java
index 2276d7c..4be33dd 100644
--- a/src/test/java/sevenUnits/unit/UnitDatabaseTest.java
+++ b/src/test/java/sevenUnits/unit/UnitDatabaseTest.java
@@ -39,6 +39,7 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
+import sevenUnits.utils.NameSymbol;
import sevenUnits.utils.UncertainDouble;
/**
@@ -595,7 +596,7 @@ class UnitDatabaseTest {
database.addPrefix("C", C);
final int NUM_UNITS = database.unitMapPrefixless(true).size();
- final int NUM_PREFIXES = database.prefixMap().size();
+ final int NUM_PREFIXES = database.prefixMap(true).size();
final Iterator<String> nameIterator = database.unitMap().keySet()
.iterator();
diff --git a/src/test/java/sevenUnits/unit/UnitTest.java b/src/test/java/sevenUnits/unit/UnitTest.java
index bb2e6a4..d3699ca 100644
--- a/src/test/java/sevenUnits/unit/UnitTest.java
+++ b/src/test/java/sevenUnits/unit/UnitTest.java
@@ -21,12 +21,14 @@ 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 java.math.RoundingMode;
import java.util.Random;
import java.util.concurrent.ThreadLocalRandom;
import org.junit.jupiter.api.Test;
import sevenUnits.utils.DecimalComparison;
+import sevenUnits.utils.NameSymbol;
import sevenUnits.utils.UncertainDouble;
/**
@@ -163,8 +165,9 @@ class UnitTest {
UncertainDouble.of(10, 0.24));
assertEquals("(10.0 ± 0.2) m", value.toString());
- assertEquals("(10.0 ± 0.2) m", value.toString(true));
- assertEquals("10.0 m", value.toString(false));
+ assertEquals("(10.0 ± 0.2) m",
+ value.toString(true, RoundingMode.HALF_EVEN));
+ assertEquals("10.0 m", value.toString(false, RoundingMode.HALF_EVEN));
}
/**
@@ -178,8 +181,9 @@ class UnitTest {
UncertainDouble.of(10, 0));
assertEquals("10.0 m", value.toString());
- assertEquals("(10.0 ± 0.0) m", value.toString(true));
- assertEquals("10.0 m", value.toString(false));
+ assertEquals("(10.0 ± 0.0) m",
+ value.toString(true, RoundingMode.HALF_EVEN));
+ assertEquals("10.0 m", value.toString(false, RoundingMode.HALF_EVEN));
}
/**
@@ -193,7 +197,8 @@ class UnitTest {
Metric.METRE.withName(NameSymbol.EMPTY),
UncertainDouble.of(10, 0.24));
- assertEquals("10.0 unnamed unit (= 10.0 m)", value.toString(false));
+ assertEquals("10.0 unnamed unit (= 10.0 m)",
+ value.toString(false, RoundingMode.HALF_EVEN));
}
/**
diff --git a/src/test/java/sevenUnits/utils/SemanticVersionTest.java b/src/test/java/sevenUnits/utils/SemanticVersionTest.java
new file mode 100644
index 0000000..877b258
--- /dev/null
+++ b/src/test/java/sevenUnits/utils/SemanticVersionTest.java
@@ -0,0 +1,399 @@
+/**
+ * Copyright (C) 2022 Adrien Hopkins
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package sevenUnits.utils;
+
+import 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 sevenUnits.utils.SemanticVersionNumber.BUILD_METADATA_COMPARATOR;
+import static sevenUnits.utils.SemanticVersionNumber.builder;
+import static sevenUnits.utils.SemanticVersionNumber.fromString;
+import static sevenUnits.utils.SemanticVersionNumber.isValidVersionString;
+import static sevenUnits.utils.SemanticVersionNumber.preRelease;
+import static sevenUnits.utils.SemanticVersionNumber.stableVersion;
+
+import java.util.List;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests for {@link SemanticVersionNumber}
+ *
+ * @since 2022-02-19
+ */
+public final class SemanticVersionTest {
+ /**
+ * Test for {@link SemanticVersionNumber#compatible}
+ *
+ * @since 2022-02-20
+ */
+ @Test
+ public void testCompatibility() {
+ assertTrue(stableVersion(1, 0, 0).compatibleWith(stableVersion(1, 0, 5)),
+ "1.0.0 not compatible with 1.0.5");
+ assertTrue(stableVersion(1, 3, 1).compatibleWith(stableVersion(1, 4, 0)),
+ "1.3.1 not compatible with 1.4.0");
+
+ // 0.y.z should not be compatible with any other version
+ assertFalse(stableVersion(0, 4, 0).compatibleWith(stableVersion(0, 4, 1)),
+ "0.4.0 compatible with 0.4.1 (0.y.z versions should be treated as unstable/incompatbile)");
+
+ // upgrading major version should = incompatible
+ assertFalse(stableVersion(1, 0, 0).compatibleWith(stableVersion(2, 0, 0)),
+ "1.0.0 compatible with 2.0.0");
+
+ // dowgrade should = incompatible
+ assertFalse(stableVersion(1, 1, 0).compatibleWith(stableVersion(1, 0, 0)),
+ "1.1.0 compatible with 1.0.0");
+ }
+
+ /**
+ * Tests {@link SemanticVersionNumber#toString} for complex version numbers
+ *
+ * @since 2022-02-19
+ */
+ @Test
+ public void testComplexToString() {
+ final SemanticVersionNumber v1 = builder(1, 2, 3).preRelease(1, 2, 3)
+ .build();
+ assertEquals("1.2.3-1.2.3", v1.toString());
+ final SemanticVersionNumber v2 = builder(4, 5, 6).preRelease("abc", 123)
+ .buildMetadata("2022-02-19").build();
+ assertEquals("4.5.6-abc.123+2022-02-19", v2.toString());
+ final SemanticVersionNumber v3 = builder(1, 0, 0)
+ .preRelease("x-y-z", "--").build();
+ assertEquals("1.0.0-x-y-z.--", v3.toString());
+ }
+
+ /**
+ * Tests that complex version can be created and their parts read
+ *
+ * @since 2022-02-19
+ */
+ @Test
+ public void testComplexVersions() {
+ final SemanticVersionNumber v1 = builder(1, 2, 3).preRelease(1, 2, 3)
+ .build();
+ assertEquals(1, v1.majorVersion());
+ assertEquals(2, v1.minorVersion());
+ assertEquals(3, v1.patchVersion());
+ assertEquals(List.of("1", "2", "3"), v1.preReleaseIdentifiers());
+ assertEquals(List.of(), v1.buildMetadata());
+
+ final SemanticVersionNumber v2 = builder(4, 5, 6).preRelease("abc", 123)
+ .buildMetadata("2022-02-19").build();
+ assertEquals(4, v2.majorVersion());
+ assertEquals(5, v2.minorVersion());
+ assertEquals(6, v2.patchVersion());
+ assertEquals(List.of("abc", "123"), v2.preReleaseIdentifiers());
+ assertEquals(List.of("2022-02-19"), v2.buildMetadata());
+
+ final SemanticVersionNumber v3 = builder(1, 0, 0)
+ .preRelease("x-y-z", "--").build();
+ assertEquals(1, v3.majorVersion());
+ assertEquals(0, v3.minorVersion());
+ assertEquals(0, v3.patchVersion());
+ assertEquals(List.of("x-y-z", "--"), v3.preReleaseIdentifiers());
+ assertEquals(List.of(), v3.buildMetadata());
+ }
+
+ /**
+ * Test that semantic version strings can be parsed correctly
+ *
+ * @since 2022-02-19
+ * @see SemanticVersionNumber#fromString
+ * @see SemanticVersionNumber#isValidVersionString
+ */
+ @Test
+ public void testFromString() {
+ // test that the regex can match version strings
+ assertTrue(isValidVersionString("1.0.0"), "1.0.0 is treated as invalid");
+ assertTrue(isValidVersionString("1.3.9"), "1.3.9 is treated as invalid");
+ assertTrue(isValidVersionString("2.0.0-a.1"),
+ "2.0.0-a.1 is treated as invalid");
+ assertTrue(isValidVersionString("1.0.0-a.b.c.d"),
+ "1.0.0-a.b.c.d is treated as invalid");
+ assertTrue(isValidVersionString("1.0.0+abc"),
+ "1.0.0+abc is treated as invalid");
+ assertTrue(isValidVersionString("1.0.0-abc+def"),
+ "1.0.0-abc+def is treated as invalid");
+
+ // test that invalid versions don't match
+ assertFalse(isValidVersionString("1.0"),
+ "1.0 is treated as valid (patch should be required)");
+ assertFalse(isValidVersionString("1.A.0"),
+ "1.A.0 is treated as valid (main versions must be numbers)");
+ assertFalse(isValidVersionString("1.0.0-"),
+ "1.0.0- is treated as valid (pre-release must not be empty)");
+ assertFalse(isValidVersionString("1.0.0+"),
+ "1.0.0+ is treated as valid (build metadata must not be empty)");
+
+ // test that versions can be parsed
+ assertEquals(stableVersion(1, 0, 0), fromString("1.0.0"),
+ "Could not parse 1.0.0");
+ assertEquals(
+ builder(1, 2, 3).preRelease("abc", "56", "def")
+ .buildMetadata("2022abc99").build(),
+ fromString("1.2.3-abc.56.def+2022abc99"),
+ "Could not parse 1.2.3-abc.56.def+2022abc99");
+ }
+
+ /**
+ * Ensures it is impossible to create invalid version numbers
+ */
+ @Test
+ public void testInvalidVersionNumbers() {
+ // stableVersion()
+ assertThrows(IllegalArgumentException.class,
+ () -> stableVersion(1, 0, -1),
+ "Negative patch tolerated by stableVersion");
+ assertThrows(IllegalArgumentException.class,
+ () -> stableVersion(1, -2, 1),
+ "Negative minor version number tolerated by stableVersion");
+ assertThrows(IllegalArgumentException.class,
+ () -> stableVersion(-3, 0, 7),
+ "Negative major version number tolerated by stableVersion");
+
+ // preRelease()
+ assertThrows(IllegalArgumentException.class,
+ () -> preRelease(1, 0, -1, "test", 2),
+ "Negative patch tolerated by preRelease");
+ assertThrows(IllegalArgumentException.class,
+ () -> preRelease(1, -2, 1, "test", 2),
+ "Negative minor version number tolerated by preRelease");
+ assertThrows(IllegalArgumentException.class,
+ () -> preRelease(-3, 0, 7, "test", 2),
+ "Negative major version number tolerated by preRelease");
+ assertThrows(IllegalArgumentException.class,
+ () -> preRelease(1, 0, 0, "test", -1),
+ "Negative pre release number tolerated by preRelease");
+ assertThrows(NullPointerException.class,
+ () -> preRelease(1, 0, 0, null, 1), "Null tolerated by preRelease");
+ assertThrows(IllegalArgumentException.class,
+ () -> preRelease(1, 0, 0, "", 1),
+ "Empty string tolerated by preRelease");
+ assertThrows(IllegalArgumentException.class,
+ () -> preRelease(1, 0, 0, "abc+cde", 1),
+ "Invalid string tolerated by preRelease");
+
+ // builder()
+ assertThrows(IllegalArgumentException.class, () -> builder(1, 0, -1),
+ "Negative patch tolerated by builder");
+ assertThrows(IllegalArgumentException.class, () -> builder(1, -2, 1),
+ "Negative minor version number tolerated by builder");
+ assertThrows(IllegalArgumentException.class, () -> builder(-3, 0, 7),
+ "Negative major version number tolerated by builder");
+
+ final SemanticVersionNumber.Builder testBuilder = builder(1, 2, 3);
+ // note: builder.buildMetadata(null) doesn't even compile lol
+ // builder.buildMetadata
+ assertThrows(NullPointerException.class,
+ () -> testBuilder.buildMetadata(null, "abc"),
+ "Null tolerated by builder.buildMetadata(String...)");
+ assertThrows(IllegalArgumentException.class,
+ () -> testBuilder.buildMetadata(""),
+ "Empty string tolerated by builder.buildMetadata(String...)");
+ assertThrows(IllegalArgumentException.class,
+ () -> testBuilder.buildMetadata("c%4"),
+ "Invalid string tolerated by builder.buildMetadata(String...)");
+ assertThrows(NullPointerException.class,
+ () -> testBuilder.buildMetadata(List.of("abc", null)),
+ "Null tolerated by builder.buildMetadata(List<String>)");
+ assertThrows(IllegalArgumentException.class,
+ () -> testBuilder.buildMetadata(List.of("")),
+ "Empty string tolerated by builder.buildMetadata(List<String>)");
+ assertThrows(IllegalArgumentException.class,
+ () -> testBuilder.buildMetadata(List.of("")),
+ "Invalid string tolerated by builder.buildMetadata(List<String>)");
+
+ // builder.preRelease
+ assertThrows(NullPointerException.class,
+ () -> testBuilder.preRelease(null, "abc"),
+ "Null tolerated by builder.preRelease(String...)");
+ assertThrows(IllegalArgumentException.class,
+ () -> testBuilder.preRelease(""),
+ "Empty string tolerated by builder.preRelease(String...)");
+ assertThrows(IllegalArgumentException.class,
+ () -> testBuilder.preRelease("c%4"),
+ "Invalid string tolerated by builder.preRelease(String...)");
+ assertThrows(NullPointerException.class,
+ () -> testBuilder.preRelease(List.of("abc", null)),
+ "Null tolerated by builder.preRelease(List<String>)");
+ assertThrows(IllegalArgumentException.class,
+ () -> testBuilder.preRelease(List.of("")),
+ "Empty string tolerated by builder.preRelease(List<String>)");
+ assertThrows(IllegalArgumentException.class,
+ () -> testBuilder.preRelease(List.of("")),
+ "Invalid string tolerated by builder.preRelease(List<String>)");
+
+ // the overloadings that accept numeric arguments
+ assertThrows(IllegalArgumentException.class,
+ () -> testBuilder.preRelease(-1),
+ "Negative number tolerated by builder.preRelease(int...)");
+ assertThrows(IllegalArgumentException.class,
+ () -> testBuilder.preRelease("abc", -1),
+ "Negative number tolerated by builder.preRelease(String, int)");
+ assertThrows(NullPointerException.class,
+ () -> testBuilder.preRelease(null, 1),
+ "Null tolerated by builder.preRelease(String, int)");
+ assertThrows(IllegalArgumentException.class,
+ () -> testBuilder.preRelease("", 1),
+ "Empty string tolerated by builder.preRelease(String, int)");
+ assertThrows(IllegalArgumentException.class,
+ () -> testBuilder.preRelease("#$#c", 1),
+ "Invalid string tolerated by builder.preRelease(String, int)");
+
+ // ensure all these attempts didn't change the builder
+ assertEquals(builder(1, 2, 3), testBuilder,
+ "Attempts at making invalid version number succeeded despite throwing errors");
+ }
+
+ /**
+ * Test for {@link SemanticVersionNumber#isStable}
+ *
+ * @since 2022-02-19
+ */
+ @Test
+ public void testIsStable() {
+ assertTrue(stableVersion(1, 0, 0).isStable(),
+ "1.0.0 should be stable but is not");
+ assertFalse(stableVersion(0, 1, 2).isStable(),
+ "0.1.2 should not be stable but is");
+ assertFalse(preRelease(1, 2, 3, "alpha", 5).isStable(),
+ "1.2.3a5 should not be stable but is");
+ assertTrue(
+ builder(9, 9, 99)
+ .buildMetadata("lots-of-metadata", "abc123", "2022").build()
+ .isStable(),
+ "9.9.99+lots-of-metadata.abc123.2022 should be stable but is not");
+ }
+
+ /**
+ * Tests that the versions are ordered by
+ * {@link SemanticVersionNumber#compareTo} according to official rules. Tests
+ * all of the versions compared in section 11 of the SemVer 2.0.0 document
+ * and some more.
+ *
+ * @since 2022-02-19
+ */
+ @Test
+ public void testOrder() {
+ final SemanticVersionNumber v100a = builder(1, 0, 0).preRelease("alpha")
+ .build(); // 1.0.0-alpha
+ final SemanticVersionNumber v100a1 = preRelease(1, 0, 0, "alpha", 1); // 1.0.0-alpha.1
+ final SemanticVersionNumber v100ab = builder(1, 0, 0)
+ .preRelease("alpha", "beta").build(); // 1.0.0-alpha.beta
+ final SemanticVersionNumber v100b = builder(1, 0, 0).preRelease("beta")
+ .build(); // 1.0.0-alpha
+ final SemanticVersionNumber v100b2 = preRelease(1, 0, 0, "beta", 2); // 1.0.0-beta.2
+ final SemanticVersionNumber v100b11 = preRelease(1, 0, 0, "beta", 11); // 1.0.0-beta.11
+ final SemanticVersionNumber v100rc1 = preRelease(1, 0, 0, "rc", 1); // 1.0.0-rc.1
+ final SemanticVersionNumber v100 = stableVersion(1, 0, 0);
+ final SemanticVersionNumber v100plus = builder(1, 0, 0)
+ .buildMetadata("blah", "blah", "blah").build(); // 1.0.0+blah.blah.blah
+ final SemanticVersionNumber v200 = stableVersion(2, 0, 0);
+ final SemanticVersionNumber v201 = stableVersion(2, 0, 1);
+ final SemanticVersionNumber v210 = stableVersion(2, 1, 0);
+ final SemanticVersionNumber v211 = stableVersion(2, 1, 1);
+ final SemanticVersionNumber v300 = stableVersion(3, 0, 0);
+
+ // test order of version numbers
+ assertTrue(v100a.compareTo(v100a1) < 0, "1.0.0-alpha >= 1.0.0-alpha.1");
+ assertTrue(v100a1.compareTo(v100ab) < 0,
+ "1.0.0-alpha.1 >= 1.0.0-alpha.beta");
+ assertTrue(v100ab.compareTo(v100b) < 0, "1.0.0-alpha.beta >= 1.0.0-beta");
+ assertTrue(v100b.compareTo(v100b2) < 0, "1.0.0-beta >= 1.0.0-beta.2");
+ assertTrue(v100b2.compareTo(v100b11) < 0,
+ "1.0.0-beta.2 >= 1.0.0-beta.11");
+ assertTrue(v100b11.compareTo(v100rc1) < 0, "1.0.0-beta.11 >= 1.0.0-rc.1");
+ assertTrue(v100rc1.compareTo(v100) < 0, "1.0.0-rc.1 >= 1.0.0");
+ assertTrue(v100.compareTo(v200) < 0, "1.0.0 >= 2.0.0");
+ assertTrue(v200.compareTo(v201) < 0, "2.0.0 >= 2.0.1");
+ assertTrue(v201.compareTo(v210) < 0, "2.0.1 >= 2.1.0");
+ assertTrue(v210.compareTo(v211) < 0, "2.1.0 >= 2.1.1");
+ assertTrue(v211.compareTo(v300) < 0, "2.1.1 >= 3.0.0");
+
+ // test symmetry - assume previous tests passed
+ assertTrue(v100a1.compareTo(v100a) > 0, "1.0.0-alpha.1 <= 1.0.0-alpha");
+ assertTrue(v100.compareTo(v100rc1) > 0, "1.0.0 <= 1.0.0-rc.1");
+ assertTrue(v300.compareTo(v211) > 0, "3.0.0 <= 2.1.1");
+
+ // test transitivity
+ assertTrue(v100a.compareTo(v100b11) < 0, "1.0.0-alpha >= 1.0.0-beta.11");
+ assertTrue(v100b.compareTo(v200) < 0, "1.0.0-beta >= 2.0.0");
+ assertTrue(v100.compareTo(v300) < 0, "1.0.0 >= 3.0.0");
+ assertTrue(v100a.compareTo(v300) < 0, "1.0.0-alpha >= 3.0.0");
+
+ // test metadata is ignored
+ assertEquals(0, v100.compareTo(v100plus), "Build metadata not ignored");
+ // test metadata is NOT ignored by alternative comparator
+ assertTrue(BUILD_METADATA_COMPARATOR.compare(v100, v100plus) > 0,
+ "Build metadata ignored by BUILD_METADATA_COMPARATOR");
+ }
+
+ /**
+ * Tests that simple stable versions can be created and their parts read
+ *
+ * @since 2022-02-19
+ */
+ @Test
+ public void testSimpleStableVersions() {
+ final SemanticVersionNumber v100 = stableVersion(1, 0, 0);
+ assertEquals(1, v100.majorVersion());
+ assertEquals(0, v100.minorVersion());
+ assertEquals(0, v100.patchVersion());
+
+ final SemanticVersionNumber v925 = stableVersion(9, 2, 5);
+ assertEquals(9, v925.majorVersion());
+ assertEquals(2, v925.minorVersion());
+ assertEquals(5, v925.patchVersion());
+ }
+
+ /**
+ * Tests that {@link SemanticVersionNumber#toString} works for simple version
+ * numbers
+ *
+ * @since 2022-02-19
+ */
+ @Test
+ public void testSimpleToString() {
+ final SemanticVersionNumber v100 = stableVersion(1, 0, 0);
+ assertEquals("1.0.0", v100.toString());
+
+ final SemanticVersionNumber v845a1 = preRelease(8, 4, 5, "alpha", 1);
+ assertEquals("8.4.5-alpha.1", v845a1.toString());
+ }
+
+ /**
+ * Tests that simple unstable versions can be created and their parts read
+ *
+ * @since 2022-02-19
+ */
+ @Test
+ public void testSimpleUnstableVersions() {
+ final SemanticVersionNumber v350a1 = preRelease(3, 5, 0, "alpha", 1);
+ assertEquals(3, v350a1.majorVersion(),
+ "Incorrect major version for v3.5.0a1");
+ assertEquals(5, v350a1.minorVersion(),
+ "Incorrect minor version for v3.5.0a1");
+ assertEquals(0, v350a1.patchVersion(),
+ "Incorrect patch version for v3.5.0a1");
+ assertEquals(List.of("alpha", "1"), v350a1.preReleaseIdentifiers(),
+ "Incorrect pre-release identifiers for v3.5.0a1");
+ }
+}
diff --git a/src/test/java/sevenUnits/utils/UncertainDoubleTest.java b/src/test/java/sevenUnits/utils/UncertainDoubleTest.java
index c891f20..0e18461 100644
--- a/src/test/java/sevenUnits/utils/UncertainDoubleTest.java
+++ b/src/test/java/sevenUnits/utils/UncertainDoubleTest.java
@@ -19,6 +19,7 @@ package sevenUnits.utils;
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 static sevenUnits.utils.UncertainDouble.fromRoundedString;
import static sevenUnits.utils.UncertainDouble.fromString;
import static sevenUnits.utils.UncertainDouble.of;
@@ -66,6 +67,16 @@ class UncertainDoubleTest {
x.toExponentExact(Math.E).value());
}
+ /**
+ * Test for {@link UncertainDouble#fromRoundedString}
+ *
+ * @since 2022-04-18
+ */
+ @Test
+ final void testFromRoundedString() {
+ assertEquals(of(12345.678, 0.001), fromRoundedString("12345.678"));
+ }
+
@Test
final void testFromString() {
// valid strings
diff --git a/src/test/java/sevenUnitsGUI/PresenterTest.java b/src/test/java/sevenUnitsGUI/PresenterTest.java
new file mode 100644
index 0000000..3364e83
--- /dev/null
+++ b/src/test/java/sevenUnitsGUI/PresenterTest.java
@@ -0,0 +1,356 @@
+/**
+ * Copyright (C) 2022 Adrien Hopkins
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package sevenUnitsGUI;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.math.RoundingMode;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import sevenUnits.unit.BaseDimension;
+import sevenUnits.unit.BritishImperial;
+import sevenUnits.unit.LinearUnitValue;
+import sevenUnits.unit.Metric;
+import sevenUnits.unit.Unit;
+import sevenUnits.unit.UnitType;
+import sevenUnits.utils.NameSymbol;
+import sevenUnits.utils.Nameable;
+import sevenUnits.utils.ObjectProduct;
+import sevenUnits.utils.UncertainDouble;
+
+/**
+ * @author Adrien Hopkins
+ *
+ * @since 2022-02-10
+ */
+public final class PresenterTest {
+ private static final Path TEST_SETTINGS = Path.of("src", "test", "resources",
+ "test-settings.txt");
+ static final Set<Unit> testUnits = Set.of(Metric.METRE, Metric.KILOMETRE,
+ Metric.METRE_PER_SECOND, Metric.KILOMETRE_PER_HOUR);
+
+ static final Set<ObjectProduct<BaseDimension>> testDimensions = Set
+ .of(Metric.Dimensions.LENGTH, Metric.Dimensions.VELOCITY);
+
+ /**
+ * @return rounding rules used by {@link #testRoundingRules}
+ * @since 2022-04-16
+ */
+ private static final Stream<Function<UncertainDouble, String>> getRoundingRules() {
+ final var SCIENTIFIC_ROUNDING = StandardDisplayRules.uncertaintyBased();
+ final var INTEGER_ROUNDING = StandardDisplayRules.fixedDecimals(0);
+ final var SIG_FIG_ROUNDING = StandardDisplayRules.fixedPrecision(4);
+
+ return Stream.of(SCIENTIFIC_ROUNDING, INTEGER_ROUNDING, SIG_FIG_ROUNDING);
+ }
+
+ private static final Set<String> names(Set<? extends Nameable> units) {
+ return units.stream().map(Nameable::getName).collect(Collectors.toSet());
+ }
+
+ /**
+ * Test method for {@link Presenter#convertExpressions}
+ *
+ * @since 2022-02-12
+ */
+ @Test
+ void testConvertExpressions() {
+ // setup
+ final ViewBot viewBot = new ViewBot();
+ final Presenter presenter = new Presenter(viewBot);
+
+ viewBot.setFromExpression("10000.0 m");
+ viewBot.setToExpression("km");
+
+ // convert expression
+ presenter.convertExpressions();
+
+ // test result
+ final List<UnitConversionRecord> outputs = viewBot
+ .expressionConversionList();
+ assertEquals("10000.0 m = 10.00000 km",
+ outputs.get(outputs.size() - 1).toString());
+ }
+
+ /**
+ * Tests that unit-conversion Views can correctly convert units
+ *
+ * @since 2022-02-12
+ */
+ @Test
+ void testConvertUnits() {
+ // setup
+ final ViewBot viewBot = new ViewBot();
+ final Presenter presenter = new Presenter(viewBot);
+
+ viewBot.setFromUnitNames(names(testUnits));
+ viewBot.setToUnitNames(names(testUnits));
+ viewBot.setFromSelection("metre");
+ viewBot.setToSelection("kilometre");
+ viewBot.setInputValue("10000.0");
+
+ // convert units
+ presenter.convertUnits();
+
+ /*
+ * use result from system as expected - I'm not testing unit conversion
+ * here (that's for the backend tests), I'm just testing that it correctly
+ * calls the unit conversion system
+ */
+ final LinearUnitValue expectedInput = LinearUnitValue.of(Metric.METRE,
+ UncertainDouble.fromRoundedString("10000.0"));
+ final LinearUnitValue expectedOutput = expectedInput
+ .convertTo(Metric.KILOMETRE);
+ final UnitConversionRecord expectedUC = UnitConversionRecord.valueOf(
+ expectedInput.getUnit().getName(),
+ expectedOutput.getUnit().getName(), "10000.0",
+ expectedOutput.getValue().toString(false, RoundingMode.HALF_EVEN));
+ assertEquals(List.of(expectedUC), viewBot.unitConversionList());
+ }
+
+ /**
+ * Tests that duplicate units are successfully removed, if that is asked for
+ *
+ * @since 2022-04-16
+ */
+ @Test
+ void testDuplicateUnits() {
+ final var metre = Metric.METRE;
+ final var meter = Metric.METRE.withName(NameSymbol.of("meter", "m"));
+
+ // load 2 duplicate units
+ final var viewBot = new ViewBot();
+ final var presenter = new Presenter(viewBot);
+ presenter.database.clear();
+ presenter.database.addUnit("metre", metre);
+ presenter.database.addUnit("meter", meter);
+ presenter.setOneWayConversionEnabled(false);
+
+ // test that only one of them is included if duplicate units disabled
+ presenter.setShowDuplicates(false);
+ presenter.updateView();
+ assertEquals(1, viewBot.getFromUnitNames().size());
+ assertEquals(1, viewBot.getToUnitNames().size());
+
+ // test that both of them is included if duplicate units enabled
+ presenter.setShowDuplicates(true);
+ presenter.updateView();
+ assertEquals(2, viewBot.getFromUnitNames().size());
+ assertEquals(2, viewBot.getToUnitNames().size());
+ }
+
+ /**
+ * Tests that one-way conversion correctly filters From and To units
+ *
+ * @since 2022-04-16
+ */
+ @Test
+ void testOneWayConversion() {
+ // metre is metric, inch is non-metric, tempC is semi-metric
+ final var allNames = Set.of("metre", "inch", "tempC");
+ final var metricNames = Set.of("metre", "tempC");
+ final var nonMetricNames = Set.of("inch", "tempC");
+
+ // load view with one metric and one non-metric unit
+ final var viewBot = new ViewBot();
+ final var presenter = new Presenter(viewBot);
+ presenter.database.clear();
+ presenter.database.addUnit("metre", Metric.METRE);
+ presenter.database.addUnit("inch", BritishImperial.Length.INCH);
+ presenter.database.addUnit("tempC", Metric.CELSIUS);
+
+ // test that units are removed from each side when one-way conversion is
+ // enabled
+ presenter.setOneWayConversionEnabled(true);
+ assertEquals(nonMetricNames, viewBot.getFromUnitNames());
+ assertEquals(metricNames, viewBot.getToUnitNames());
+
+ // test that units are kept when one-way conversion is disabled
+ presenter.setOneWayConversionEnabled(false);
+ assertEquals(allNames, viewBot.getFromUnitNames());
+ assertEquals(allNames, viewBot.getToUnitNames());
+ }
+
+ /**
+ * Tests the prefix-viewing functionality.
+ *
+ * @since 2022-04-16
+ */
+ @Test
+ void testPrefixViewing() {
+ // setup
+ final var viewBot = new ViewBot();
+ final var presenter = new Presenter(viewBot);
+ viewBot.setViewablePrefixNames(Set.of("kilo", "milli"));
+ presenter.setNumberDisplayRule(UncertainDouble::toString);
+
+ // view prefix
+ viewBot.setViewedPrefixName("kilo");
+ presenter.prefixSelected(); // just in case
+
+ // get correct values
+ final var expectedNameSymbol = presenter.database.getPrefix("kilo")
+ .getNameSymbol();
+ final var expectedMultiplierString = String
+ .valueOf(Metric.KILO.getMultiplier());
+
+ // test that presenter's values are correct
+ final var prefixRecord = viewBot.prefixViewList().get(0);
+ assertEquals(expectedNameSymbol, prefixRecord.getNameSymbol());
+ assertEquals(expectedMultiplierString, prefixRecord.multiplierString());
+ }
+
+ /**
+ * Tests that rounding rules are used correctly.
+ *
+ * @since 2022-04-16
+ */
+ @ParameterizedTest
+ @MethodSource("getRoundingRules")
+ void testRoundingRules(Function<UncertainDouble, String> roundingRule) {
+ // setup
+ final var viewBot = new ViewBot();
+ final var presenter = new Presenter(viewBot);
+ presenter.setNumberDisplayRule(roundingRule);
+
+ // convert and round
+ viewBot.setInputValue("12345.6789");
+ viewBot.setFromSelection("metre");
+ viewBot.setToSelection("kilometre");
+ presenter.convertUnits();
+
+ // test the result of the rounding
+ final String expectedOutputString = roundingRule
+ .apply(UncertainDouble.fromRoundedString("12.3456789"));
+ final String actualOutputString = viewBot.unitConversionList().get(0)
+ .outputValueString();
+ assertEquals(expectedOutputString, actualOutputString);
+ }
+
+ /**
+ * Tests that settings can be saved to and loaded from a file.
+ *
+ * @since 2022-04-16
+ */
+ @Test
+ void testSettingsSaving() {
+ // setup
+ final var viewBot = new ViewBot();
+ final var presenter = new Presenter(viewBot);
+
+ // set and save custom settings
+ presenter.setOneWayConversionEnabled(true);
+ presenter.setShowDuplicates(true);
+ presenter.setNumberDisplayRule(StandardDisplayRules.fixedPrecision(11));
+ presenter.setPrefixRepetitionRule(
+ DefaultPrefixRepetitionRule.COMPLEX_REPETITION);
+ presenter.saveSettings(TEST_SETTINGS);
+
+ // overwrite custom settings
+ presenter.setOneWayConversionEnabled(false);
+ presenter.setShowDuplicates(false);
+ presenter.setNumberDisplayRule(StandardDisplayRules.uncertaintyBased());
+
+ // load settings & test that they're the same
+ presenter.loadSettings(TEST_SETTINGS);
+ assertTrue(presenter.oneWayConversionEnabled());
+ assertTrue(presenter.duplicatesShown());
+ assertEquals(StandardDisplayRules.fixedPrecision(11),
+ presenter.getNumberDisplayRule());
+ }
+
+ /**
+ * Ensures the Presenter generates the correct data upon a unit-viewing.
+ *
+ * @since 2022-04-16
+ */
+ @Test
+ void testUnitViewing() {
+ // setup
+ final var viewBot = new ViewBot();
+ final var presenter = new Presenter(viewBot);
+ viewBot.setViewableUnitNames(names(testUnits));
+
+ // view unit
+ viewBot.setViewedUnitName("metre");
+ presenter.unitNameSelected(); // just in case this isn't triggered
+ // automatically
+
+ // get correct values
+ final var expectedNameSymbol = presenter.database.getUnit("metre")
+ .getNameSymbol();
+ final var expectedDefinition = "(Base unit)";
+ final var expectedDimensionName = presenter
+ .getDimensionName(Metric.METRE.getDimension());
+ final var expectedUnitType = UnitType.METRIC;
+
+ // test for correctness
+ final var viewRecord = viewBot.unitViewList().get(0);
+ assertEquals(expectedNameSymbol, viewRecord.getNameSymbol());
+ assertEquals(expectedDefinition, viewRecord.definition());
+ assertEquals(expectedDimensionName, viewRecord.dimensionName());
+ assertEquals(expectedUnitType, viewRecord.unitType());
+ }
+
+ /**
+ * Test for {@link Presenter#updateView()}
+ *
+ * @since 2022-02-12
+ */
+ @Test
+ void testUpdateView() {
+ // setup
+ final ViewBot viewBot = new ViewBot();
+ final Presenter presenter = new Presenter(viewBot);
+ presenter.setOneWayConversionEnabled(false);
+
+ // override default database units
+ presenter.database.clear();
+ for (final Unit unit : testUnits) {
+ presenter.database.addUnit(unit.getPrimaryName().orElseThrow(), unit);
+ }
+ for (final var dimension : testDimensions) {
+ presenter.database.addDimension(
+ dimension.getPrimaryName().orElseThrow(), dimension);
+ }
+
+ // set from and to units
+ viewBot.setFromUnitNames(names(testUnits));
+ viewBot.setToUnitNames(names(testUnits));
+ viewBot.setDimensionNames(names(testDimensions));
+ viewBot.setSelectedDimensionName(Metric.Dimensions.LENGTH.getName());
+
+ // filter to length units only, then get the filtered sets of units
+ presenter.updateView();
+ final Set<String> fromUnits = viewBot.getFromUnitNames();
+ final Set<String> toUnits = viewBot.getToUnitNames();
+
+ // test that fromUnits/toUnits is [METRE, KILOMETRE]
+ assertEquals(Set.of("metre", "kilometre"), fromUnits);
+ assertEquals(Set.of("metre", "kilometre"), toUnits);
+ }
+}
diff --git a/src/test/resources/test-settings.txt b/src/test/resources/test-settings.txt
new file mode 100644
index 0000000..a0f494a
--- /dev/null
+++ b/src/test/resources/test-settings.txt
@@ -0,0 +1,4 @@
+number_display_rule=Round to 11 significant figures
+prefix_rule=COMPLEX_REPETITION
+one_way=true
+include_duplicates=true