From da3a5098602f8177f6d5dac4a322f70d6fdf9126 Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Fri, 24 Dec 2021 16:03:26 -0500 Subject: Did some API design for user settings, and moved GUI to a new package --- .../java/sevenUnitsGUI/StandardDisplayRules.java | 74 ++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 src/main/java/sevenUnitsGUI/StandardDisplayRules.java (limited to 'src/main/java/sevenUnitsGUI/StandardDisplayRules.java') diff --git a/src/main/java/sevenUnitsGUI/StandardDisplayRules.java b/src/main/java/sevenUnitsGUI/StandardDisplayRules.java new file mode 100644 index 0000000..331f598 --- /dev/null +++ b/src/main/java/sevenUnitsGUI/StandardDisplayRules.java @@ -0,0 +1,74 @@ +/** + * 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 . + */ +package sevenUnitsGUI; + +import java.util.function.Function; + +import sevenUnits.utils.UncertainDouble; + +/** + * The default rules for displaying numbers. + * + * Unless otherwise stated, all of this class's functions throw + * {@link NullPointerException} when they receive a null parameter. + * + * @since 2021-12-24 + */ +final class StandardDisplayRules { + /** + * Rounds using UncertainDouble's toString method. + */ + private static final Function SCIENTIFIC_ROUNDING_RULE = new Function<>() { + @Override + public String apply(UncertainDouble t) { + return t.toString(false); + } + + @Override + public String toString() { + return "Scientific Rounding"; + } + }; + + /** + * @return a rule that rounds using UncertainDouble's own toString(false) + * function. + * @since 2021-12-24 + */ + public static final Function getScientificRule() { + return SCIENTIFIC_ROUNDING_RULE; + } + + /** + * 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 getStandardRule( + String ruleToString) { + throw new UnsupportedOperationException("Not implemented yet"); + } + + private StandardDisplayRules() { + throw new AssertionError( + "This is a static utility class, you may not get instances of it."); + } +} -- cgit v1.2.3 From 4aaf6a8b60fbec63c2e0bee624b3859ded0ecde3 Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Sat, 16 Apr 2022 15:57:00 -0500 Subject: Added a full suite of frontend tests (Added tests for the settings and unit/prefix viewer parts of the GUI, which are not yet implemented) --- src/main/java/sevenUnits/unit/UnitPrefix.java | 135 +++++-------- src/main/java/sevenUnitsGUI/Presenter.java | 14 +- .../java/sevenUnitsGUI/StandardDisplayRules.java | 26 +++ src/main/java/sevenUnitsGUI/ViewBot.java | 200 +++++++++++++++++++- src/test/java/sevenUnitsGUI/PresenterTest.java | 210 +++++++++++++++++++-- 5 files changed, 482 insertions(+), 103 deletions(-) (limited to 'src/main/java/sevenUnitsGUI/StandardDisplayRules.java') diff --git a/src/main/java/sevenUnits/unit/UnitPrefix.java b/src/main/java/sevenUnits/unit/UnitPrefix.java index bf9d1fd..e1f7788 100644 --- a/src/main/java/sevenUnits/unit/UnitPrefix.java +++ b/src/main/java/sevenUnits/unit/UnitPrefix.java @@ -17,69 +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 primaryName; - - /** - * This prefix's symbol - */ - private final Optional symbol; - + /** - * Other names and symbols used by this prefix + * This prefix's name(s) and symbol. + * + * @since 2022-04-16 */ - private final Set 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}. * @@ -89,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 @@ -118,7 +104,7 @@ public final class UnitPrefix { public UnitPrefix dividedBy(final UnitPrefix other) { return valueOf(this.getMultiplier() / other.getMultiplier()); } - + /** * {@inheritDoc} * @@ -133,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 @@ -143,31 +130,12 @@ public final class UnitPrefix { public double getMultiplier() { return this.multiplier; } - - /** - * @return other names - * @since 2019-11-26 - */ - public final Set getOtherNames() { - return this.otherNames; - } - - /** - * @return primary name - * @since 2019-11-26 - */ - public final Optional getPrimaryName() { - return this.primaryName; - } - - /** - * @return symbol - * @since 2019-11-26 - */ - public final Optional getSymbol() { - return this.symbol; + + @Override + public NameSymbol getNameSymbol() { + return this.nameSymbol; } - + /** * {@inheritDoc} * @@ -177,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 @@ -202,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 @@ -215,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/sevenUnitsGUI/Presenter.java b/src/main/java/sevenUnitsGUI/Presenter.java index b38f90b..5c8ce53 100644 --- a/src/main/java/sevenUnitsGUI/Presenter.java +++ b/src/main/java/sevenUnitsGUI/Presenter.java @@ -18,6 +18,7 @@ package sevenUnitsGUI; import java.io.IOException; import java.io.InputStream; +import java.nio.file.Path; import java.util.ArrayList; import java.util.HashSet; import java.util.List; @@ -50,7 +51,8 @@ import sevenUnits.utils.UncertainDouble; */ public final class Presenter { /** The default place where settings are stored. */ - private static final String DEFAULT_SETTINGS_FILEPATH = "settings.txt"; + 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. */ @@ -406,17 +408,18 @@ public final class Presenter { * * @since 2022-03-30 */ - public boolean isOneWayConversionEnabled() { + public boolean oneWayConversionEnabled() { return this.oneWayConversionEnabled; } /** * 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 */ - private void loadSettings() {} + void loadSettings(Path settingsFile) {} /** * Completes creation of the presenter. This part of the initialization @@ -438,9 +441,10 @@ public final class Presenter { /** * Saves the presenter's settings to the user settings file. * + * @param settingsFile file settings should be saved to * @since 2021-12-15 */ - private void saveSettings() {} + void saveSettings(Path settingsFile) {} /** * @param numberDisplayRule the new rule that will be used by this presenter diff --git a/src/main/java/sevenUnitsGUI/StandardDisplayRules.java b/src/main/java/sevenUnitsGUI/StandardDisplayRules.java index 331f598..f6272c8 100644 --- a/src/main/java/sevenUnitsGUI/StandardDisplayRules.java +++ b/src/main/java/sevenUnitsGUI/StandardDisplayRules.java @@ -44,6 +44,32 @@ final class StandardDisplayRules { } }; + /** + * Gets a display rule that rounds numbers to a fixed number of decimal + * places. + * + * @param decimalPlaces number of decimal places + * @return display rule + * @since 2022-04-16 + */ + public static final Function getFixedPlacesRule( + int decimalPlaces) { + throw new UnsupportedOperationException("Not implemented yet"); + } + + /** + * Gets a display rule that rounds numbers to a fixed number of significant + * figures. + * + * @param significantFigures number of significant figures + * @return display rule + * @since 2022-04-16 + */ + public static final Function getFixedPrecisionRule( + int significantFigures) { + throw new UnsupportedOperationException("Not implemented yet"); + } + /** * @return a rule that rounds using UncertainDouble's own toString(false) * function. diff --git a/src/main/java/sevenUnitsGUI/ViewBot.java b/src/main/java/sevenUnitsGUI/ViewBot.java index 9f9a524..988d1bc 100644 --- a/src/main/java/sevenUnitsGUI/ViewBot.java +++ b/src/main/java/sevenUnitsGUI/ViewBot.java @@ -25,6 +25,7 @@ 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) @@ -34,6 +35,163 @@ import sevenUnits.utils.NameSymbol; * @since 2022-01-29 */ 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; @@ -62,6 +220,10 @@ final class ViewBot implements UnitConversionView, ExpressionConversionView { private final List unitConversions; /** Saved outputs of all unit expressions */ private final List expressionConversions; + /** Saved outputs of all unit viewings */ + private final List unitViewingRecords; + /** Saved outputs of all prefix viewings */ + private final List prefixViewingRecords; /** * Creates a new {@code ViewBot} with a new presenter. @@ -73,6 +235,8 @@ final class ViewBot implements UnitConversionView, ExpressionConversionView { this.unitConversions = new ArrayList<>(); this.expressionConversions = new ArrayList<>(); + this.unitViewingRecords = new ArrayList<>(); + this.prefixViewingRecords = new ArrayList<>(); } /** @@ -80,7 +244,7 @@ final class ViewBot implements UnitConversionView, ExpressionConversionView { * @since 2022-04-09 */ public List expressionConversionList() { - return this.expressionConversions; + return Collections.unmodifiableList(this.expressionConversions); } /** @@ -158,6 +322,14 @@ final class ViewBot implements UnitConversionView, ExpressionConversionView { throw new UnsupportedOperationException("Not implemented yet"); } + /** + * @return list of records of this viewBot's prefix views + * @since 2022-04-16 + */ + public List prefixViewList() { + return Collections.unmodifiableList(this.prefixViewingRecords); + } + @Override public void setDimensionNames(Set dimensionNames) { this.dimensionNames = Objects.requireNonNull(dimensionNames, @@ -259,6 +431,24 @@ final class ViewBot implements UnitConversionView, ExpressionConversionView { throw new UnsupportedOperationException("Not implemented yet"); } + public void setViewedPrefixName( + @SuppressWarnings("unused") Optional viewedPrefixName) { + throw new UnsupportedOperationException("Not implemented yet"); + } + + public void setViewedPrefixName(String viewedPrefixName) { + this.setViewedPrefixName(Optional.of(viewedPrefixName)); + } + + public void setViewedUnitName( + @SuppressWarnings("unused") Optional viewedUnitName) { + throw new UnsupportedOperationException("Not implemented yet"); + } + + 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); @@ -299,4 +489,12 @@ final class ViewBot implements UnitConversionView, ExpressionConversionView { public List unitConversionList() { return Collections.unmodifiableList(this.unitConversions); } + + /** + * @return list of records of unit viewings made by this bot + * @since 2022-04-16 + */ + public List unitViewList() { + return Collections.unmodifiableList(this.unitViewingRecords); + } } diff --git a/src/test/java/sevenUnitsGUI/PresenterTest.java b/src/test/java/sevenUnitsGUI/PresenterTest.java index 3fe7e47..85ebe09 100644 --- a/src/test/java/sevenUnitsGUI/PresenterTest.java +++ b/src/test/java/sevenUnitsGUI/PresenterTest.java @@ -17,20 +17,29 @@ package sevenUnitsGUI; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assumptions.assumeTrue; +import static org.junit.jupiter.api.Assertions.assertTrue; +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.Metric; import sevenUnits.unit.Unit; +import sevenUnits.unit.UnitType; import sevenUnits.unit.UnitValue; +import sevenUnits.utils.NameSymbol; import sevenUnits.utils.Nameable; import sevenUnits.utils.ObjectProduct; +import sevenUnits.utils.UncertainDouble; /** * @author Adrien Hopkins @@ -38,12 +47,27 @@ import sevenUnits.utils.ObjectProduct; * @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 testUnits = Set.of(Metric.METRE, Metric.KILOMETRE, Metric.METRE_PER_SECOND, Metric.KILOMETRE_PER_HOUR); static final Set> testDimensions = Set .of(Metric.Dimensions.LENGTH, Metric.Dimensions.VELOCITY); + /** + * @return rounding rules used by {@link #testRoundingRules} + * @since 2022-04-16 + */ + private static final Stream> getRoundingRules() { + final var SCIENTIFIC_ROUNDING = StandardDisplayRules.getScientificRule(); + final var INTEGER_ROUNDING = StandardDisplayRules.getFixedPlacesRule(0); + final var SIG_FIG_ROUNDING = StandardDisplayRules + .getFixedPrecisionRule(4); + + return Stream.of(SCIENTIFIC_ROUNDING, INTEGER_ROUNDING, SIG_FIG_ROUNDING); + } + private static final Set names(Set units) { return units.stream().map(Nameable::getName).collect(Collectors.toSet()); } @@ -106,23 +130,185 @@ public final class PresenterTest { 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() { - assumeTrue(false, "Not yet implemented"); - /* - * enable and disable duplicate units and check for those in From and To, - * include duplicate units in the input set - */ + 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); + + // test that only one of them is included if duplicate units disabled + presenter.setShowDuplicateUnits(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.setShowDuplicateUnits(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() { - assumeTrue(false, "Not yet implemented"); - /* - * enable and disable one-way conversion, testing the units in From and To - * on each setting to ensure they match the rule. Include at least one - * metric exception. - */ + // 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); + presenter.updateView(); + assertEquals(metricNames, viewBot.getFromUnitNames()); + assertEquals(nonMetricNames, viewBot.getToUnitNames()); + + // test that units are kept when one-way conversion is disabled + presenter.setOneWayConversionEnabled(false); + presenter.updateView(); + 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 = Metric.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 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.of(12.3456789, 0)); + 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.setShowDuplicateUnits(true); + presenter.setNumberDisplayRule( + StandardDisplayRules.getFixedPrecisionRule(11)); + presenter.saveSettings(TEST_SETTINGS); + + // overwrite custom settings + presenter.setOneWayConversionEnabled(false); + presenter.setShowDuplicateUnits(false); + presenter.setNumberDisplayRule(StandardDisplayRules.getScientificRule()); + + // load settings & test that they're the same + presenter.loadSettings(TEST_SETTINGS); + assertTrue(presenter.oneWayConversionEnabled()); + assertTrue(presenter.duplicateUnitsShown()); + assertEquals(StandardDisplayRules.getFixedPlacesRule(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 = Metric.METRE.getNameSymbol(); + final var expectedDefinition = Metric.METRE.toDefinitionString(); + final var expectedDimensionName = Metric.METRE.getDimension().getName(); + 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()); } /** -- cgit v1.2.3 From f0541a955b6e4b12d808cffec0874f50a004e8b9 Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Mon, 18 Apr 2022 17:01:54 -0500 Subject: Implemented rounding and duplicate-removal settings into the new GUI --- CHANGELOG.org | 2 + .../sevenUnits/converterGUI/SevenUnitsGUI.java | 2 +- src/main/java/sevenUnits/unit/LinearUnitValue.java | 10 +- src/main/java/sevenUnits/unit/UnitDatabase.java | 6 +- .../java/sevenUnits/utils/UncertainDouble.java | 24 +- src/main/java/sevenUnitsGUI/Presenter.java | 56 ++++- .../java/sevenUnitsGUI/StandardDisplayRules.java | 220 +++++++++++++--- src/main/java/sevenUnitsGUI/TabbedView.java | 280 +++++++++++++++++---- src/test/java/sevenUnits/unit/UnitTest.java | 14 +- .../java/sevenUnits/utils/UncertainDoubleTest.java | 11 + src/test/java/sevenUnitsGUI/PresenterTest.java | 14 +- 11 files changed, 518 insertions(+), 121 deletions(-) (limited to 'src/main/java/sevenUnitsGUI/StandardDisplayRules.java') diff --git a/CHANGELOG.org b/CHANGELOG.org index 61d9333..c164d1f 100644 --- a/CHANGELOG.org +++ b/CHANGELOG.org @@ -10,6 +10,8 @@ - BaseDimension is now Nameable. As a consequence, its name and symbol return Optional 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 - Tweaked the look of the unit and expression conversion sections of the view ** v0.3.2 - [2021-12-02 Thu] *** Added diff --git a/src/main/java/sevenUnits/converterGUI/SevenUnitsGUI.java b/src/main/java/sevenUnits/converterGUI/SevenUnitsGUI.java index 55e1546..e10bab4 100644 --- a/src/main/java/sevenUnits/converterGUI/SevenUnitsGUI.java +++ b/src/main/java/sevenUnits/converterGUI/SevenUnitsGUI.java @@ -488,7 +488,7 @@ final class SevenUnitsGUI { case SIGNIFICANT_DIGITS: return this.getRoundedString(value.asUnitValue()); case SCIENTIFIC: - return value.toString(showUncertainty); + return value.toString(showUncertainty, RoundingMode.HALF_EVEN); default: throw new AssertionError("Invalid switch condition."); } 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 primaryName = this.unit.getPrimaryName(); final Optional 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/UnitDatabase.java b/src/main/java/sevenUnits/unit/UnitDatabase.java index 7b02ac7..a4f0c44 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; @@ -1705,11 +1704,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); } 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 @@ -45,6 +45,21 @@ public final class UncertainDouble implements Comparable { // optional "± [number]" + "(?:\\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. @@ -348,7 +363,7 @@ public final class UncertainDouble implements Comparable { */ @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 { * * @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 { 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/sevenUnitsGUI/Presenter.java b/src/main/java/sevenUnitsGUI/Presenter.java index 981af21..85a0ddc 100644 --- a/src/main/java/sevenUnitsGUI/Presenter.java +++ b/src/main/java/sevenUnitsGUI/Presenter.java @@ -242,6 +242,9 @@ public final class Presenter { throw new AssertionError("Loading of metric_exceptions.txt failed.", e); } + + // set default settings temporarily + this.numberDisplayRule = StandardDisplayRules.uncertaintyBased(); } /** @@ -293,9 +296,21 @@ public final class Presenter { // convert and show output if (from.getUnit().canConvertTo(to)) { - final double value = from.asUnitValue().convertTo(to).getValue(); + 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, "", String.valueOf(value)); + fromExpression, toExpression, "", + this.numberDisplayRule.apply(uncertainValue)); xcview.showExpressionConversionOutput(uc); } else { this.view.showErrorMessage("Conversion Error", @@ -324,7 +339,7 @@ public final class Presenter { final Optional fromUnitOptional = ucview.getFromSelection(); final Optional toUnitOptional = ucview.getToSelection(); - final String valueString = ucview.getInputValue(); + final String inputValueString = ucview.getInputValue(); // extract values from optionals final String fromUnitString, toUnitString; @@ -345,7 +360,7 @@ public final class Presenter { // convert strings to data, checking if anything is invalid final Unit fromUnit, toUnit; - final double value; + final UncertainDouble uncertainValue; if (this.database.containsUnitName(fromUnitString)) { fromUnit = this.database.getUnit(fromUnitString); @@ -356,23 +371,42 @@ public final class Presenter { } else throw this.viewError("Nonexistent To unit: %s", toUnitString); try { - value = Double.parseDouble(valueString); + uncertainValue = UncertainDouble + .fromRoundedString(inputValueString); } catch (final NumberFormatException e) { this.view.showErrorMessage("Value Error", - "Invalid value " + valueString); + "Invalid value " + inputValueString); return; } if (!fromUnit.canConvertTo(toUnit)) throw this.viewError("Could not convert between %s and %s", fromUnit, toUnit); - - // convert! - final UnitValue initialValue = UnitValue.of(fromUnit, value); - final UnitValue converted = initialValue.convertTo(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.fromValues(initialValue, converted)); + UnitConversionRecord.valueOf(fromUnitString, toUnitString, + inputValueString, outputValueString)); } else throw new UnsupportedOperationException( "This function can only be called when the view is a UnitConversionView."); diff --git a/src/main/java/sevenUnitsGUI/StandardDisplayRules.java b/src/main/java/sevenUnitsGUI/StandardDisplayRules.java index f6272c8..0c0ba8e 100644 --- a/src/main/java/sevenUnitsGUI/StandardDisplayRules.java +++ b/src/main/java/sevenUnitsGUI/StandardDisplayRules.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2021 Adrien Hopkins + * 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 @@ -16,67 +16,192 @@ */ 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; /** - * The default rules for displaying numbers. - * - * Unless otherwise stated, all of this class's functions throw - * {@link NullPointerException} when they receive a null parameter. + * A static utility class that can be used to make display rules for the + * presenter. * - * @since 2021-12-24 + * @since 2022-04-18 */ -final class StandardDisplayRules { +public final class StandardDisplayRules { /** - * Rounds using UncertainDouble's toString method. + * A rule that rounds to a fixed number of decimal places. + * + * @since 2022-04-18 */ - private static final Function SCIENTIFIC_ROUNDING_RULE = new Function<>() { + public static final class FixedDecimals + implements Function { + 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) { - return t.toString(false); + 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 "Scientific Rounding"; + return "Round to " + this.decimalPlaces + " decimal places"; } - }; + } /** - * Gets a display rule that rounds numbers to a fixed number of decimal - * places. + * A rule that rounds to a fixed number of significant digits. * - * @param decimalPlaces number of decimal places - * @return display rule - * @since 2022-04-16 + * @since 2022-04-18 */ - public static final Function getFixedPlacesRule( - int decimalPlaces) { - throw new UnsupportedOperationException("Not implemented yet"); + public static final class FixedPrecision + implements Function { + 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"; + } } /** - * Gets a display rule that rounds numbers to a fixed number of 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. * - * @param significantFigures number of significant figures - * @return display rule - * @since 2022-04-16 + * @since 2022-04-18 + */ + public static final class UncertaintyBased + implements Function { + 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 Function getFixedPrecisionRule( - int significantFigures) { - throw new UnsupportedOperationException("Not implemented yet"); + public static final FixedDecimals fixedDecimals(int decimalPlaces) { + return new FixedDecimals(decimalPlaces); } /** - * @return a rule that rounds using UncertainDouble's own toString(false) - * function. - * @since 2021-12-24 + * @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 Function getScientificRule() { - return SCIENTIFIC_ROUNDING_RULE; + public static final FixedPrecision fixedPrecision(int significantFigures) { + return new FixedPrecision(significantFigures); } /** @@ -90,11 +215,32 @@ final class StandardDisplayRules { */ public static final Function getStandardRule( String ruleToString) { - throw new UnsupportedOperationException("Not implemented yet"); + 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."); } - private StandardDisplayRules() { - throw new AssertionError( - "This is a static utility class, you may not get instances of it."); + /** + * @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 index d0eb32f..3a951ef 100644 --- a/src/main/java/sevenUnitsGUI/TabbedView.java +++ b/src/main/java/sevenUnitsGUI/TabbedView.java @@ -20,16 +20,18 @@ 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.text.DecimalFormat; -import java.text.NumberFormat; 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; @@ -37,7 +39,6 @@ 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; @@ -58,6 +59,7 @@ 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 @@ -111,7 +113,99 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { } - private static final NumberFormat NUMBER_FORMATTER = new DecimalFormat(); + /** + * 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(true) { + @Override + public Function getRuleFromPrecision( + int precision) { + return StandardDisplayRules.fixedPrecision(precision); + } + }, + /** + * Rounds to a fixed number of decimal places. Precision is used, + * representing the number of decimal places to round to. + */ + DECIMAL_PLACES(true) { + @Override + public Function getRuleFromPrecision( + int precision) { + return StandardDisplayRules.fixedDecimals(precision); + } + }, + /** + * Rounds according to UncertainDouble's toString method. The specified + * precision is ignored. + */ + UNCERTAINTY(false) { + @Override + public Function getRuleFromPrecision( + int precision) { + return StandardDisplayRules.uncertaintyBased(); + } + }; + + /** + * If true, this type of rounding rule requires you to specify a + * precision. + */ + private final boolean requiresPrecision; + + /** + * @param canCustomizePrecision + * @since 2022-04-18 + */ + private StandardRoundingType(boolean requiresPrecision) { + this.requiresPrecision = requiresPrecision; + } + + /** + * Gets a rounding rule of this type. + * + * @param precision the rounding type's precision. If + * {@link #requiresPrecision} is false, this field will + * be ignored. + * @return rounding rule + * @since 2022-04-18 + */ + public abstract Function getRuleFromPrecision( + int precision); + + /** + * Tries to get this rule without specifying precision. + * + * @throws UnsupportedOperationException if this rule requires specifying + * precision + * @since 2022-04-18 + */ + public final Function getRuleWithoutPrecision() { + if (this.requiresPrecision()) + throw new UnsupportedOperationException("Rounding type " + this + + " requires you to specify precision."); + else + // random number to mess with anyone who lies about whether or not + // precision is required + return this.getRuleFromPrecision(-623546735); + } + + /** + * @return whether or not this rounding type requires you to specify an + * integer precision + * @since 2022-04-18 + */ + public boolean requiresPrecision() { + return this.requiresPrecision; + } + } /** * Creates a TabbedView. @@ -137,7 +231,7 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { /** The combo box that selects dimensions */ private final JComboBox dimensionSelector; /** The panel for inputting values in the dimension-based converter */ - private final JFormattedTextField valueInput; + 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 */ @@ -163,6 +257,10 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { /** 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 * @@ -229,7 +327,7 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { final JLabel valuePrompt = new JLabel("Value to convert: "); outputPanel.add(valuePrompt, BorderLayout.LINE_START); - this.valueInput = new JFormattedTextField(NUMBER_FORMATTER); + this.valueInput = new JTextField(); outputPanel.add(this.valueInput, BorderLayout.CENTER); // conversion button @@ -352,61 +450,89 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { // 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.presenter.roundingType == RoundingType.SIGNIFICANT_DIGITS) { -// fixedPrecision.setSelected(true); -// } -// fixedPrecision.addActionListener(e -> this.presenter -// .setRoundingType(RoundingType.SIGNIFICANT_DIGITS)); + 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.presenter.roundingType == RoundingType.DECIMAL_PLACES) { -// fixedDecimals.setSelected(true); -// } -// fixedDecimals.addActionListener(e -> this.presenter -// .setRoundingType(RoundingType.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( - "Scientific Precision"); -// if (this.presenter.roundingType == RoundingType.SCIENTIFIC) { -// relativePrecision.setSelected(true); -// } -// relativePrecision.addActionListener( -// e -> this.presenter.setRoundingType(RoundingType.SCIENTIFIC)); + "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()); - - 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 ============ @@ -501,17 +627,18 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { 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)); + oneWay.setSelected(this.presenter.oneWayConversionEnabled()); + oneWay.addItemListener(e -> this.presenter.setOneWayConversionEnabled( + e.getStateChange() == ItemEvent.SELECTED)); 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)); + showAllVariations.setSelected(this.presenter.duplicateUnitsShown()); + showAllVariations + .addItemListener(e -> this.presenter.setShowDuplicateUnits( + e.getStateChange() == ItemEvent.SELECTED)); miscPanel.add(showAllVariations, new GridBagBuilder(0, 1) .setAnchor(GridBagConstraints.LINE_START).build()); @@ -552,6 +679,43 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { return this.valueInput.getText(); } + /** + * @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(); + } + + /** + * Determines which rounding type the presenter is currently using, if any. + * + * @since 2022-04-18 + */ + private Optional 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 getSelectedDimensionName() { final String selectedItem = (String) this.dimensionSelector @@ -644,4 +808,28 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { 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 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); + } } diff --git a/src/test/java/sevenUnits/unit/UnitTest.java b/src/test/java/sevenUnits/unit/UnitTest.java index f174e7c..d3699ca 100644 --- a/src/test/java/sevenUnits/unit/UnitTest.java +++ b/src/test/java/sevenUnits/unit/UnitTest.java @@ -21,6 +21,7 @@ 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; @@ -164,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)); } /** @@ -179,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)); } /** @@ -194,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/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 index 8446a90..f52d846 100644 --- a/src/test/java/sevenUnitsGUI/PresenterTest.java +++ b/src/test/java/sevenUnitsGUI/PresenterTest.java @@ -60,10 +60,9 @@ public final class PresenterTest { * @since 2022-04-16 */ private static final Stream> getRoundingRules() { - final var SCIENTIFIC_ROUNDING = StandardDisplayRules.getScientificRule(); - final var INTEGER_ROUNDING = StandardDisplayRules.getFixedPlacesRule(0); - final var SIG_FIG_ROUNDING = StandardDisplayRules - .getFixedPrecisionRule(4); + 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); } @@ -264,20 +263,19 @@ public final class PresenterTest { // set and save custom settings presenter.setOneWayConversionEnabled(true); presenter.setShowDuplicateUnits(true); - presenter.setNumberDisplayRule( - StandardDisplayRules.getFixedPrecisionRule(11)); + presenter.setNumberDisplayRule(StandardDisplayRules.fixedPrecision(11)); presenter.saveSettings(TEST_SETTINGS); // overwrite custom settings presenter.setOneWayConversionEnabled(false); presenter.setShowDuplicateUnits(false); - presenter.setNumberDisplayRule(StandardDisplayRules.getScientificRule()); + presenter.setNumberDisplayRule(StandardDisplayRules.uncertaintyBased()); // load settings & test that they're the same presenter.loadSettings(TEST_SETTINGS); assertTrue(presenter.oneWayConversionEnabled()); assertTrue(presenter.duplicateUnitsShown()); - assertEquals(StandardDisplayRules.getFixedPlacesRule(11), + assertEquals(StandardDisplayRules.fixedPrecision(11), presenter.getNumberDisplayRule()); } -- cgit v1.2.3