diff options
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 @@ -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 |