From 07c86e02be29aa3d3d878adce62c5c0a9a458e47 Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Sat, 26 Feb 2022 09:53:24 -0500 Subject: Implemented unit conversion, with a few problems TabbedView now displays its units, but with their toString method which shows their definition in addition to their name --- src/main/java/sevenUnits/unit/UnitDatabase.java | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) (limited to 'src/main/java/sevenUnits/unit/UnitDatabase.java') diff --git a/src/main/java/sevenUnits/unit/UnitDatabase.java b/src/main/java/sevenUnits/unit/UnitDatabase.java index 18ac619..b029539 100644 --- a/src/main/java/sevenUnits/unit/UnitDatabase.java +++ b/src/main/java/sevenUnits/unit/UnitDatabase.java @@ -47,6 +47,8 @@ import java.util.regex.Pattern; import sevenUnits.utils.ConditionalExistenceCollections; import sevenUnits.utils.DecimalComparison; import sevenUnits.utils.ExpressionParser; +import sevenUnits.utils.NameSymbol; +import sevenUnits.utils.NamedObjectProduct; import sevenUnits.utils.ObjectProduct; import sevenUnits.utils.UncertainDouble; @@ -1197,7 +1199,7 @@ public final class UnitDatabase { * @since 2019-03-14 * @since v0.2.0 */ - private final Map> dimensions; + private final Map> dimensions; /** * A map mapping strings to units (including prefixes) @@ -1313,9 +1315,16 @@ public final class UnitDatabase { */ public void addDimension(final String name, final ObjectProduct 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"); + if (dimension instanceof NamedObjectProduct) { + this.dimensions.put(name, + (NamedObjectProduct) dimension); + } else { + final NamedObjectProduct namedDimension = dimension + .withName(NameSymbol.ofName(name)); + this.dimensions.put(name, namedDimension); + } } /** @@ -1367,7 +1376,7 @@ public final class UnitDatabase { throw e; } - this.addDimension(name, dimension); + this.addDimension(name, dimension.withName(NameSymbol.ofName(name))); } } @@ -1463,7 +1472,7 @@ public final class UnitDatabase { throw e; } - this.addUnit(name, unit); + this.addUnit(name, unit.withName(NameSymbol.ofName(name))); } } } @@ -1510,7 +1519,7 @@ public final class UnitDatabase { * @since 2019-04-13 * @since v0.2.0 */ - public Map> dimensionMap() { + public Map> dimensionMap() { return Collections.unmodifiableMap(this.dimensions); } -- cgit v1.2.3 From 934213e08e85cc20bd994d0f39567426c21b89eb Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Sat, 26 Feb 2022 11:15:04 -0500 Subject: Implemented expression conversion, tests now pass --- CHANGELOG.org | 2 +- src/main/java/sevenUnits/unit/UnitDatabase.java | 11 +++ src/main/java/sevenUnits/utils/NameSymbol.java | 9 -- src/main/java/sevenUnits/utils/Nameable.java | 20 ++++ src/main/java/sevenUnits/utils/ObjectProduct.java | 6 +- src/main/java/sevenUnitsGUI/Presenter.java | 77 ++++++++++----- src/main/java/sevenUnitsGUI/TabbedView.java | 15 +-- .../java/sevenUnitsGUI/UnitConversionView.java | 6 +- src/main/java/sevenUnitsGUI/ViewBot.java | 36 +++++-- src/test/java/sevenUnitsGUI/PresenterTest.java | 104 ++++++++++++--------- 10 files changed, 195 insertions(+), 91 deletions(-) (limited to 'src/main/java/sevenUnits/unit/UnitDatabase.java') diff --git a/CHANGELOG.org b/CHANGELOG.org index 3bc46c2..681fead 100644 --- a/CHANGELOG.org +++ b/CHANGELOG.org @@ -4,7 +4,7 @@ *** Added - Added tests for the GUI - Added an object for the version numbers (SemanticVersionNumber) - - Added some toString methods to NameSymbol + - 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 instead of String, even though they will always succeed. diff --git a/src/main/java/sevenUnits/unit/UnitDatabase.java b/src/main/java/sevenUnits/unit/UnitDatabase.java index b029539..bf6ae64 100644 --- a/src/main/java/sevenUnits/unit/UnitDatabase.java +++ b/src/main/java/sevenUnits/unit/UnitDatabase.java @@ -1477,6 +1477,17 @@ public final class UnitDatabase { } } + /** + * 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. * diff --git a/src/main/java/sevenUnits/utils/NameSymbol.java b/src/main/java/sevenUnits/utils/NameSymbol.java index aea274b..255e82f 100644 --- a/src/main/java/sevenUnits/utils/NameSymbol.java +++ b/src/main/java/sevenUnits/utils/NameSymbol.java @@ -278,15 +278,6 @@ public final class NameSymbol { return this.primaryName.isEmpty() && this.symbol.isEmpty(); } - /** - * @return a short version of this NameSymbol (defaults to symbol instead of - * primary name) - * @since 2022-02-26 - */ - public String shortName() { - return this.symbol.or(this::getPrimaryName).orElse("EMPTY"); - } - @Override public String toString() { if (this.isEmpty()) diff --git a/src/main/java/sevenUnits/utils/Nameable.java b/src/main/java/sevenUnits/utils/Nameable.java index 3cfc05a..e469d04 100644 --- a/src/main/java/sevenUnits/utils/Nameable.java +++ b/src/main/java/sevenUnits/utils/Nameable.java @@ -26,6 +26,16 @@ import java.util.Set; * @since 2020-09-07 */ 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 @@ -49,6 +59,16 @@ public interface Nameable { return this.getNameSymbol().getPrimaryName(); } + /** + * @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 926ce10..830f9d7 100644 --- a/src/main/java/sevenUnits/utils/ObjectProduct.java +++ b/src/main/java/sevenUnits/utils/ObjectProduct.java @@ -244,9 +244,9 @@ public class ObjectProduct { */ @Override public String toString() { - return this.toString(o -> o instanceof Nameable - ? ((Nameable) o).getNameSymbol().shortName() - : o.toString()); + return this + .toString(o -> o instanceof Nameable ? ((Nameable) o).getShortName() + : o.toString()); } /** diff --git a/src/main/java/sevenUnitsGUI/Presenter.java b/src/main/java/sevenUnitsGUI/Presenter.java index be02364..57d353d 100644 --- a/src/main/java/sevenUnitsGUI/Presenter.java +++ b/src/main/java/sevenUnitsGUI/Presenter.java @@ -23,6 +23,7 @@ import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.NoSuchElementException; import java.util.Optional; import java.util.OptionalDouble; import java.util.Scanner; @@ -34,6 +35,7 @@ import java.util.stream.Collectors; import sevenUnits.ProgramInfo; import sevenUnits.unit.BaseDimension; import sevenUnits.unit.BritishImperial; +import sevenUnits.unit.LinearUnitValue; import sevenUnits.unit.Metric; import sevenUnits.unit.Unit; import sevenUnits.unit.UnitDatabase; @@ -170,7 +172,7 @@ public final class Presenter { /** * The database that this presenter communicates with (effectively the model) */ - private final UnitDatabase database; + final UnitDatabase database; /** * The rule used for parsing input numbers. Any number-string inputted into @@ -274,7 +276,58 @@ public final class Presenter { * {@link ExpressionConversionView}) * @since 2021-12-15 */ - public void convertExpressions() {} + 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 double value = from.asUnitValue().convertTo(to).getValue(); + xcview.showExpressionConversionOutput(fromExpression, toExpression, + value); + } 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 @@ -326,8 +379,7 @@ public final class Presenter { // convert! final UnitValue initialValue = UnitValue.of(fromUnit, value); final UnitValue converted = initialValue.convertTo(toUnit); - ucview.showUnitConversionOutput( - String.format("%s = %s", initialValue, converted)); + ucview.showUnitConversionOutput(initialValue, converted); } else throw new UnsupportedOperationException( "This function can only be called when the view is a UnitConversionView."); @@ -365,23 +417,6 @@ public final class Presenter { */ public void saveSettings() {} - /** - * 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 - */ - boolean unitMatchesDimension(String unitName, String dimensionName) { - final Unit unit = this.database.getUnit(unitName); - final ObjectProduct dimension = this.database - .getDimension(dimensionName); - return unit.getDimension().equals(dimension); - } - void unitNameSelected() {} /** diff --git a/src/main/java/sevenUnitsGUI/TabbedView.java b/src/main/java/sevenUnitsGUI/TabbedView.java index c3a05e2..1d40087 100644 --- a/src/main/java/sevenUnitsGUI/TabbedView.java +++ b/src/main/java/sevenUnitsGUI/TabbedView.java @@ -23,6 +23,7 @@ import java.awt.GridLayout; import java.awt.event.KeyEvent; import java.text.DecimalFormat; import java.text.NumberFormat; +import java.text.ParseException; import java.util.AbstractSet; import java.util.Collections; import java.util.Iterator; @@ -59,6 +60,7 @@ import sevenUnits.ProgramInfo; import sevenUnits.unit.BaseDimension; import sevenUnits.unit.Unit; import sevenUnits.unit.UnitPrefix; +import sevenUnits.unit.UnitValue; import sevenUnits.utils.NamedObjectProduct; import sevenUnits.utils.ObjectProduct; @@ -140,7 +142,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 JTextField valueInput; + private final JFormattedTextField valueInput; /** The panel for "From" in the dimension-based converter */ private final SearchBoxList fromSearch; /** The panel for "To" in the dimension-based converter */ @@ -543,12 +545,13 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { @Override public OptionalDouble getInputValue() { - final String text = this.valueInput.getText(); try { - return OptionalDouble.of(Double.parseDouble(text)); - } catch (final NumberFormatException e) { + this.valueInput.commitEdit(); + } catch (final ParseException e) { return OptionalDouble.empty(); } + return OptionalDouble + .of(((Number) this.valueInput.getValue()).doubleValue()); } @Override @@ -604,8 +607,8 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { } @Override - public void showUnitConversionOutput(String outputString) { - this.unitOutput.setText(outputString); + public void showUnitConversionOutput(UnitValue input, UnitValue output) { + this.unitOutput.setText(input + " = " + output); } } diff --git a/src/main/java/sevenUnitsGUI/UnitConversionView.java b/src/main/java/sevenUnitsGUI/UnitConversionView.java index e411a44..67d3ddc 100644 --- a/src/main/java/sevenUnitsGUI/UnitConversionView.java +++ b/src/main/java/sevenUnitsGUI/UnitConversionView.java @@ -22,6 +22,7 @@ import java.util.Set; import sevenUnits.unit.BaseDimension; import sevenUnits.unit.Unit; +import sevenUnits.unit.UnitValue; import sevenUnits.utils.NamedObjectProduct; import sevenUnits.utils.ObjectProduct; @@ -94,8 +95,9 @@ public interface UnitConversionView extends View { /** * Shows the output of a unit conversion. * - * @param outputString string that shows output of conversion + * @param input input unit & value (obtained from this view) + * @param output output unit & value * @since 2021-12-24 */ - void showUnitConversionOutput(String outputString); + void showUnitConversionOutput(UnitValue input, UnitValue output); } diff --git a/src/main/java/sevenUnitsGUI/ViewBot.java b/src/main/java/sevenUnitsGUI/ViewBot.java index bc5302b..43d73bb 100644 --- a/src/main/java/sevenUnitsGUI/ViewBot.java +++ b/src/main/java/sevenUnitsGUI/ViewBot.java @@ -16,6 +16,7 @@ */ package sevenUnitsGUI; +import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Objects; @@ -25,6 +26,7 @@ import java.util.Set; import sevenUnits.unit.BaseDimension; import sevenUnits.unit.Unit; +import sevenUnits.unit.UnitValue; import sevenUnits.utils.NamedObjectProduct; import sevenUnits.utils.ObjectProduct; @@ -59,8 +61,12 @@ final class ViewBot implements UnitConversionView, ExpressionConversionView { private Set fromUnits; /** The units available in the To selection */ private Set toUnits; + /** Saved input values of all unit conversions */ + private final List unitConversionInputValues; /** Saved output values of all unit conversions */ - private List unitConversionOutputValues; + private final List unitConversionOutputValues; + /** Saved outputs of all unit expressions */ + private final List expressionConversionOutputs; /** * Creates a new {@code ViewBot} with a new presenter. @@ -69,6 +75,10 @@ final class ViewBot implements UnitConversionView, ExpressionConversionView { */ public ViewBot() { this.presenter = new Presenter(this); + + this.unitConversionInputValues = new ArrayList<>(); + this.unitConversionOutputValues = new ArrayList<>(); + this.expressionConversionOutputs = new ArrayList<>(); } /** @@ -81,7 +91,7 @@ final class ViewBot implements UnitConversionView, ExpressionConversionView { } public List getExpressionConversionOutputs() { - throw new UnsupportedOperationException("Not implemented yet"); + return this.expressionConversionOutputs; } @Override @@ -138,11 +148,19 @@ final class ViewBot implements UnitConversionView, ExpressionConversionView { return Collections.unmodifiableSet(this.toUnits); } + /** + * @return the unitConversionInputValues + * @since 2022-02-26 + */ + public List getUnitConversionInputValues() { + return this.unitConversionInputValues; + } + /** * @return the unitConversionOutputValues * @since 2022-02-10 */ - public List getUnitConversionOutputValues() { + public List getUnitConversionOutputValues() { return this.unitConversionOutputValues; } @@ -247,13 +265,17 @@ final class ViewBot implements UnitConversionView, ExpressionConversionView { @Override public void showExpressionConversionOutput(String fromExpression, String toExpression, double value) { - System.out.printf("Expression Conversion: %s = %d * (%s)%n", - fromExpression, value, toExpression); + final String output = String.format("%s = %s %s", fromExpression, value, + toExpression); + this.expressionConversionOutputs.add(output); + System.out.println("Expression Conversion: " + output); } @Override - public void showUnitConversionOutput(String outputString) { - System.out.println("Unit conversion: " + outputString); + public void showUnitConversionOutput(UnitValue input, UnitValue output) { + this.unitConversionInputValues.add(input); + this.unitConversionOutputValues.add(output); + System.out.println("Unit conversion: " + input + " = " + output); } @Override diff --git a/src/test/java/sevenUnitsGUI/PresenterTest.java b/src/test/java/sevenUnitsGUI/PresenterTest.java index ff1450b..deb16d7 100644 --- a/src/test/java/sevenUnitsGUI/PresenterTest.java +++ b/src/test/java/sevenUnitsGUI/PresenterTest.java @@ -19,16 +19,19 @@ package sevenUnitsGUI; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; +import java.util.Collection; import java.util.List; import java.util.Optional; import java.util.OptionalDouble; import java.util.Set; +import java.util.stream.Collectors; import org.junit.jupiter.api.Test; import sevenUnits.unit.BaseDimension; import sevenUnits.unit.Metric; import sevenUnits.unit.Unit; +import sevenUnits.unit.UnitValue; import sevenUnits.utils.NameSymbol; import sevenUnits.utils.NamedObjectProduct; @@ -38,50 +41,19 @@ import sevenUnits.utils.NamedObjectProduct; * @since 2022-02-10 */ public final class PresenterTest { + private static final List unitNames( + Collection units) { + return units.stream().map(Unit::getShortName) + .collect(Collectors.toList()); + } + Set testUnits = Set.of(Metric.METRE, Metric.KILOMETRE, Metric.METRE_PER_SECOND, Metric.KILOMETRE_PER_HOUR); + Set> testDimensions = Set.of( Metric.Dimensions.LENGTH.withName(NameSymbol.ofName("Length")), Metric.Dimensions.VELOCITY.withName(NameSymbol.ofName("Velocity"))); - /** - * Test for {@link Presenter#updateView()} - * - * @since 2022-02-12 - */ - @Test - void testUpdateView() { - // setup - final ViewBot viewBot = new ViewBot(); - final Presenter presenter = new Presenter(viewBot); - - viewBot.setFromUnits(this.testUnits); - viewBot.setToUnits(this.testUnits); - viewBot.setDimensions(this.testDimensions); - viewBot.setSelectedDimension( - Optional.of(this.testDimensions.iterator().next())); - - // filter to length units only, then get the filtered sets of units - presenter.updateView(); - final Set fromUnits = viewBot.getFromUnits(); - final Set toUnits = viewBot.getToUnits(); - - // test that fromUnits/toUnits is [METRE, KILOMETRE] - // HOWEVER I don't care about the order so I'm testing it this way - assertEquals(2, fromUnits.size(), - "Invalid fromUnits (length != 2): " + fromUnits); - assertEquals(2, toUnits.size(), - "Invalid toUnits (length != 2): " + toUnits); - assertTrue(fromUnits.contains(Metric.METRE), - "Invaild fromUnits (METRE missing): " + fromUnits); - assertTrue(toUnits.contains(Metric.METRE), - "Invaild toUnits (METRE missing): " + toUnits); - assertTrue(fromUnits.contains(Metric.KILOMETRE), - "Invaild fromUnits (KILOMETRE missing): " + fromUnits); - assertTrue(toUnits.contains(Metric.KILOMETRE), - "Invaild toUnits (KILOMETRE missing): " + toUnits); - } - /** * Test method for {@link Presenter#convertExpressions} * @@ -129,10 +101,58 @@ public final class PresenterTest { * here (that's for the backend tests), I'm just testing that it correctly * calls the unit conversion system */ - final String expected = String - .valueOf(Metric.METRE.convertTo(Metric.KILOMETRE, 10000.0)); + final UnitValue expectedInput = UnitValue.of(Metric.METRE, 10000.0); + final UnitValue expectedOutput = expectedInput + .convertTo(Metric.KILOMETRE); + + final List inputs = viewBot.getUnitConversionInputValues(); + final List outputs = viewBot.getUnitConversionOutputValues(); + assertEquals(expectedInput, inputs.get(inputs.size() - 1)); + assertEquals(expectedOutput, outputs.get(outputs.size() - 1)); + } + + /** + * Test for {@link Presenter#updateView()} + * + * @since 2022-02-12 + */ + @Test + void testUpdateView() { + // setup + final ViewBot viewBot = new ViewBot(); + final Presenter presenter = new Presenter(viewBot); + + // override default database units + presenter.database.clear(); + for (final Unit unit : this.testUnits) { + presenter.database.addUnit(unit.getPrimaryName().orElseThrow(), unit); + } + + // set from and to units + viewBot.setFromUnits(this.testUnits); + viewBot.setToUnits(this.testUnits); + viewBot.setDimensions(this.testDimensions); + viewBot.setSelectedDimension( + Optional.of(this.testDimensions.iterator().next())); + + // filter to length units only, then get the filtered sets of units + presenter.updateView(); + final Set fromUnits = viewBot.getFromUnits(); + final Set toUnits = viewBot.getToUnits(); - final List outputs = viewBot.getUnitConversionOutputValues(); - assertEquals(expected, outputs.get(outputs.size() - 1)); + // test that fromUnits/toUnits is [METRE, KILOMETRE] + // HOWEVER I don't care about the order so I'm testing it this way + assertEquals(2, fromUnits.size(), + "Invalid fromUnits (length != 2): " + unitNames(fromUnits)); + assertEquals(2, toUnits.size(), + "Invalid toUnits (length != 2): " + unitNames(toUnits)); + assertTrue(fromUnits.contains(Metric.METRE), + "Invaild fromUnits (METRE missing): " + unitNames(fromUnits)); + assertTrue(toUnits.contains(Metric.METRE), + "Invaild toUnits (METRE missing): " + unitNames(toUnits)); + assertTrue(fromUnits.contains(Metric.KILOMETRE), + "Invaild fromUnits (KILOMETRE missing): " + unitNames(fromUnits)); + assertTrue(toUnits.contains(Metric.KILOMETRE), + "Invaild toUnits (KILOMETRE missing): " + unitNames(toUnits)); } } -- cgit v1.2.3 From 4ad68a29f84538d3fb19eec8e0622731f5a5d7c8 Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Tue, 12 Apr 2022 15:17:12 -0500 Subject: Removed NamedObjectProduct in favour of the regular ObjectProduct --- src/main/java/sevenUnits/unit/Metric.java | 21 +++++---- src/main/java/sevenUnits/unit/Unit.java | 4 +- src/main/java/sevenUnits/unit/UnitDatabase.java | 12 +++-- src/main/java/sevenUnits/utils/NameSymbol.java | 2 +- .../java/sevenUnits/utils/NamedObjectProduct.java | 51 ---------------------- src/main/java/sevenUnits/utils/ObjectProduct.java | 30 +++++++++++-- src/test/java/sevenUnitsGUI/PresenterTest.java | 4 +- 7 files changed, 46 insertions(+), 78 deletions(-) delete mode 100644 src/main/java/sevenUnits/utils/NamedObjectProduct.java (limited to 'src/main/java/sevenUnits/unit/UnitDatabase.java') diff --git a/src/main/java/sevenUnits/unit/Metric.java b/src/main/java/sevenUnits/unit/Metric.java index 7ede085..05e82ba 100644 --- a/src/main/java/sevenUnits/unit/Metric.java +++ b/src/main/java/sevenUnits/unit/Metric.java @@ -19,7 +19,6 @@ package sevenUnits.unit; import java.util.Set; import sevenUnits.utils.NameSymbol; -import sevenUnits.utils.NamedObjectProduct; import sevenUnits.utils.ObjectProduct; /** @@ -115,29 +114,29 @@ public final class Metric { public static final class Dimensions { public static final ObjectProduct EMPTY = ObjectProduct .empty(); - public static final NamedObjectProduct LENGTH = ObjectProduct + public static final ObjectProduct LENGTH = ObjectProduct .oneOf(BaseDimensions.LENGTH) .withName(NameSymbol.of("Length", "L")); - public static final NamedObjectProduct MASS = ObjectProduct + public static final ObjectProduct MASS = ObjectProduct .oneOf(BaseDimensions.MASS).withName(NameSymbol.of("Mass", "M")); - public static final NamedObjectProduct TIME = ObjectProduct + public static final ObjectProduct TIME = ObjectProduct .oneOf(BaseDimensions.TIME).withName(NameSymbol.of("Time", "T")); - public static final NamedObjectProduct ELECTRIC_CURRENT = ObjectProduct + public static final ObjectProduct ELECTRIC_CURRENT = ObjectProduct .oneOf(BaseDimensions.ELECTRIC_CURRENT) .withName(NameSymbol.of("Current", "I")); - public static final NamedObjectProduct TEMPERATURE = ObjectProduct + public static final ObjectProduct TEMPERATURE = ObjectProduct .oneOf(BaseDimensions.TEMPERATURE) .withName(NameSymbol.of("Temperature", "\u0398")); - public static final NamedObjectProduct QUANTITY = ObjectProduct + public static final ObjectProduct QUANTITY = ObjectProduct .oneOf(BaseDimensions.QUANTITY) .withName(NameSymbol.of("Quantity", "N")); - public static final NamedObjectProduct LUMINOUS_INTENSITY = ObjectProduct + public static final ObjectProduct LUMINOUS_INTENSITY = ObjectProduct .oneOf(BaseDimensions.LUMINOUS_INTENSITY) .withName(NameSymbol.of("Luminous Intensity", "J")); - public static final NamedObjectProduct INFORMATION = ObjectProduct + public static final ObjectProduct INFORMATION = ObjectProduct .oneOf(BaseDimensions.INFORMATION) .withName(NameSymbol.ofName("Information")); - public static final NamedObjectProduct CURRENCY = ObjectProduct + public static final ObjectProduct CURRENCY = ObjectProduct .oneOf(BaseDimensions.CURRENCY) .withName(NameSymbol.ofName("Currency")); @@ -146,7 +145,7 @@ public final class Metric { .times(LENGTH); public static final ObjectProduct VOLUME = AREA .times(LENGTH); - public static final NamedObjectProduct VELOCITY = LENGTH + public static final ObjectProduct VELOCITY = LENGTH .dividedBy(TIME).withName(NameSymbol.ofName("Velocity")); public static final ObjectProduct ACCELERATION = VELOCITY .dividedBy(TIME); diff --git a/src/main/java/sevenUnits/unit/Unit.java b/src/main/java/sevenUnits/unit/Unit.java index 826b59b..14478ba 100644 --- a/src/main/java/sevenUnits/unit/Unit.java +++ b/src/main/java/sevenUnits/unit/Unit.java @@ -354,8 +354,8 @@ public abstract class Unit implements Nameable { * @since 2022-03-10 */ public String toDefinitionString() { - if (this.unitBase instanceof Nameable) - return "derived from " + ((Nameable) this.unitBase).getName(); + if (!this.unitBase.getNameSymbol().isEmpty()) + return "derived from " + this.unitBase.getName(); else return "derived from " + this.getBase().toString(BaseUnit::getShortName); diff --git a/src/main/java/sevenUnits/unit/UnitDatabase.java b/src/main/java/sevenUnits/unit/UnitDatabase.java index bf6ae64..5591d7d 100644 --- a/src/main/java/sevenUnits/unit/UnitDatabase.java +++ b/src/main/java/sevenUnits/unit/UnitDatabase.java @@ -48,7 +48,6 @@ import sevenUnits.utils.ConditionalExistenceCollections; import sevenUnits.utils.DecimalComparison; import sevenUnits.utils.ExpressionParser; import sevenUnits.utils.NameSymbol; -import sevenUnits.utils.NamedObjectProduct; import sevenUnits.utils.ObjectProduct; import sevenUnits.utils.UncertainDouble; @@ -1199,7 +1198,7 @@ public final class UnitDatabase { * @since 2019-03-14 * @since v0.2.0 */ - private final Map> dimensions; + private final Map> dimensions; /** * A map mapping strings to units (including prefixes) @@ -1317,11 +1316,10 @@ public final class UnitDatabase { final ObjectProduct dimension) { Objects.requireNonNull(name, "name may not be null"); Objects.requireNonNull(dimension, "dimension may not be null"); - if (dimension instanceof NamedObjectProduct) { - this.dimensions.put(name, - (NamedObjectProduct) dimension); + if (!dimension.getNameSymbol().equals(NameSymbol.EMPTY)) { + this.dimensions.put(name, dimension); } else { - final NamedObjectProduct namedDimension = dimension + final ObjectProduct namedDimension = dimension .withName(NameSymbol.ofName(name)); this.dimensions.put(name, namedDimension); } @@ -1530,7 +1528,7 @@ public final class UnitDatabase { * @since 2019-04-13 * @since v0.2.0 */ - public Map> dimensionMap() { + public Map> dimensionMap() { return Collections.unmodifiableMap(this.dimensions); } diff --git a/src/main/java/sevenUnits/utils/NameSymbol.java b/src/main/java/sevenUnits/utils/NameSymbol.java index 41cf41d..955f980 100644 --- a/src/main/java/sevenUnits/utils/NameSymbol.java +++ b/src/main/java/sevenUnits/utils/NameSymbol.java @@ -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 otherNames) { diff --git a/src/main/java/sevenUnits/utils/NamedObjectProduct.java b/src/main/java/sevenUnits/utils/NamedObjectProduct.java deleted file mode 100644 index 89b2fad..0000000 --- a/src/main/java/sevenUnits/utils/NamedObjectProduct.java +++ /dev/null @@ -1,51 +0,0 @@ -/** - * 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 sevenUnits.utils; - -import java.util.Map; - -/** - * An ObjectProduct with name(s) and/or a symbol. Can be created with the - * {@link ObjectProduct#withName} method. - * - * @author Adrien Hopkins - * @since 2021-12-15 - */ -public class NamedObjectProduct extends ObjectProduct - implements Nameable { - private final NameSymbol nameSymbol; - - NamedObjectProduct(final Map exponents, - final NameSymbol nameSymbol) { - super(exponents); - this.nameSymbol = nameSymbol; - } - - @Override - public NameSymbol getNameSymbol() { - return this.nameSymbol; - } - - public final String toDefinitionString() { - return super.toString(); - } - - @Override - public String toString() { - return this.nameSymbol.toString(); - } -} diff --git a/src/main/java/sevenUnits/utils/ObjectProduct.java b/src/main/java/sevenUnits/utils/ObjectProduct.java index 830f9d7..110bdc1 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 class ObjectProduct { +public class ObjectProduct implements Nameable { /** * Returns an empty ObjectProduct of a certain type * @@ -83,15 +83,32 @@ public class ObjectProduct { final Map 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 */ ObjectProduct(final Map 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 exponents, NameSymbol nameSymbol) { this.exponents = Collections.unmodifiableMap( ConditionalExistenceCollections.conditionalExistenceMap(exponents, e -> !Integer.valueOf(0).equals(e.getValue()))); + this.nameSymbol = nameSymbol; } /** @@ -170,6 +187,11 @@ public class ObjectProduct { return this.exponents.getOrDefault(dimension, 0); } + @Override + public NameSymbol getNameSymbol() { + return this.nameSymbol; + } + @Override public int hashCode() { return Objects.hash(this.exponents); @@ -288,7 +310,7 @@ public class ObjectProduct { * {@code nameSymbol} * @since 2021-12-15 */ - public NamedObjectProduct withName(NameSymbol nameSymbol) { - return new NamedObjectProduct<>(this.exponents, nameSymbol); + public ObjectProduct withName(NameSymbol nameSymbol) { + return new ObjectProduct<>(this.exponents, nameSymbol); } } diff --git a/src/test/java/sevenUnitsGUI/PresenterTest.java b/src/test/java/sevenUnitsGUI/PresenterTest.java index dc2fb57..3fe7e47 100644 --- a/src/test/java/sevenUnitsGUI/PresenterTest.java +++ b/src/test/java/sevenUnitsGUI/PresenterTest.java @@ -30,7 +30,7 @@ import sevenUnits.unit.Metric; import sevenUnits.unit.Unit; import sevenUnits.unit.UnitValue; import sevenUnits.utils.Nameable; -import sevenUnits.utils.NamedObjectProduct; +import sevenUnits.utils.ObjectProduct; /** * @author Adrien Hopkins @@ -41,7 +41,7 @@ public final class PresenterTest { static final Set testUnits = Set.of(Metric.METRE, Metric.KILOMETRE, Metric.METRE_PER_SECOND, Metric.KILOMETRE_PER_HOUR); - static final Set> testDimensions = Set + static final Set> testDimensions = Set .of(Metric.Dimensions.LENGTH, Metric.Dimensions.VELOCITY); private static final Set names(Set units) { -- cgit v1.2.3 From 855cdf83b91bd3061662e563db6656408cc24a12 Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Sat, 16 Apr 2022 17:00:52 -0500 Subject: Implemented the unit & prefix viewers --- CHANGELOG.org | 2 +- src/main/java/sevenUnits/unit/LinearUnit.java | 5 +- src/main/java/sevenUnits/unit/UnitDatabase.java | 4 +- src/main/java/sevenUnits/utils/ObjectProduct.java | 7 +- src/main/java/sevenUnitsGUI/Presenter.java | 98 ++++++++++++++++++++--- src/main/java/sevenUnitsGUI/TabbedView.java | 15 ++-- src/main/java/sevenUnitsGUI/ViewBot.java | 29 ++++--- src/test/java/sevenUnitsGUI/PresenterTest.java | 11 ++- 8 files changed, 133 insertions(+), 38 deletions(-) (limited to 'src/main/java/sevenUnits/unit/UnitDatabase.java') diff --git a/CHANGELOG.org b/CHANGELOG.org index 7c53032..61d9333 100644 --- a/CHANGELOG.org +++ b/CHANGELOG.org @@ -8,7 +8,7 @@ *** 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 instead of String, even though they will always succeed. - - The UnitDatabase's units and dimensions are now always named + - 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. - Tweaked the look of the unit and expression conversion sections of the view ** v0.3.2 - [2021-12-02 Thu] diff --git a/src/main/java/sevenUnits/unit/LinearUnit.java b/src/main/java/sevenUnits/unit/LinearUnit.java index 3a28261..103b7f6 100644 --- a/src/main/java/sevenUnits/unit/LinearUnit.java +++ b/src/main/java/sevenUnits/unit/LinearUnit.java @@ -372,8 +372,9 @@ public final class LinearUnit extends Unit { @Override public String toDefinitionString() { - return Double.toString(this.conversionFactor) + " " - + this.getBase().toString(BaseUnit::getShortName); + return Double.toString(this.conversionFactor) + + (this.getBase().equals(ObjectProduct.empty()) ? "" + : " " + this.getBase().toString(BaseUnit::getShortName)); } /** diff --git a/src/main/java/sevenUnits/unit/UnitDatabase.java b/src/main/java/sevenUnits/unit/UnitDatabase.java index 5591d7d..7b02ac7 100644 --- a/src/main/java/sevenUnits/unit/UnitDatabase.java +++ b/src/main/java/sevenUnits/unit/UnitDatabase.java @@ -1458,7 +1458,9 @@ 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.withName(NameSymbol.ofName(prefixName))); } else { // it's a unit, get the unit final Unit unit; diff --git a/src/main/java/sevenUnits/utils/ObjectProduct.java b/src/main/java/sevenUnits/utils/ObjectProduct.java index 110bdc1..66bb773 100644 --- a/src/main/java/sevenUnits/utils/ObjectProduct.java +++ b/src/main/java/sevenUnits/utils/ObjectProduct.java @@ -257,9 +257,10 @@ public class ObjectProduct implements Nameable { /** * 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. * *

* {@inheritDoc} diff --git a/src/main/java/sevenUnitsGUI/Presenter.java b/src/main/java/sevenUnitsGUI/Presenter.java index 5c8ce53..981af21 100644 --- a/src/main/java/sevenUnitsGUI/Presenter.java +++ b/src/main/java/sevenUnitsGUI/Presenter.java @@ -33,13 +33,17 @@ 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; @@ -382,6 +386,21 @@ public final class Presenter { return this.showDuplicateUnits; } + /** + * 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 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 @@ -402,14 +421,25 @@ public final class Presenter { } /** - * @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 + * @return type of unit {@code u} + * @since 2022-04-16 */ - public boolean oneWayConversionEnabled() { - return this.oneWayConversionEnabled; + private final UnitType getUnitType(Unit u) { + // determine if u is an exception + final var primaryName = u.getPrimaryName(); + final var symbol = u.getSymbol(); + final boolean isException = primaryName.isPresent() + && this.metricExceptions.contains(primaryName.orElseThrow()) + || symbol.isPresent() + && this.metricExceptions.contains(symbol.orElseThrow()); + + // determine unit type + if (isException) + return UnitType.SEMI_METRIC; + else if (u.isMetric()) + return UnitType.METRIC; + else + return UnitType.NON_METRIC; } /** @@ -421,6 +451,17 @@ public final class Presenter { */ void loadSettings(Path settingsFile) {} + /** + * @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 @@ -434,9 +475,24 @@ public final class Presenter { final UnitConversionView ucview = (UnitConversionView) this.view; ucview.setDimensionNames(this.database.dimensionMap().keySet()); } + + // load units & prefixes into viewers + this.view.setViewableUnitNames( + this.database.unitMapPrefixless(this.showDuplicateUnits).keySet()); + this.view.setViewablePrefixNames(this.database.prefixMap().keySet()); } - void prefixSelected() {} + void prefixSelected() { + final Optional selectedPrefixName = this.view + .getViewedPrefixName(); + final Optional 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 settings to the user settings file. @@ -487,13 +543,37 @@ public final class Presenter { 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 = this.getUnitType(u); + 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() {} + void unitNameSelected() { + // get selected unit, if it's there and valid + final Optional selectedUnitName = this.view.getViewedUnitName(); + final Optional 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 diff --git a/src/main/java/sevenUnitsGUI/TabbedView.java b/src/main/java/sevenUnitsGUI/TabbedView.java index fd48965..d0eb32f 100644 --- a/src/main/java/sevenUnitsGUI/TabbedView.java +++ b/src/main/java/sevenUnitsGUI/TabbedView.java @@ -578,12 +578,12 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { @Override public Optional getViewedPrefixName() { - throw new UnsupportedOperationException("Not implemented yet"); + return this.prefixNameList.getSelectedValue(); } @Override public Optional getViewedUnitName() { - throw new UnsupportedOperationException("Not implemented yet"); + return this.unitNameList.getSelectedValue(); } @Override @@ -606,12 +606,12 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { @Override public void setViewablePrefixNames(Set prefixNames) { - throw new UnsupportedOperationException("Not implemented yet"); + this.prefixNameList.setItems(prefixNames); } @Override public void setViewableUnitNames(Set unitNames) { - throw new UnsupportedOperationException("Not implemented yet"); + this.unitNameList.setItems(unitNames); } @Override @@ -628,13 +628,16 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { @Override public void showPrefix(NameSymbol name, String multiplierString) { - throw new UnsupportedOperationException("Not implemented yet"); + this.prefixTextBox.setText( + String.format("%s%nMultiplier: %s", name, multiplierString)); } @Override public void showUnit(NameSymbol name, String definition, String dimensionName, UnitType type) { - throw new UnsupportedOperationException("Not implemented yet"); + this.unitTextBox.setText( + String.format("%s%nDefinition: %s%nDimension: %s%nType: %s", name, + definition, dimensionName, type)); } @Override diff --git a/src/main/java/sevenUnitsGUI/ViewBot.java b/src/main/java/sevenUnitsGUI/ViewBot.java index 988d1bc..dd9869d 100644 --- a/src/main/java/sevenUnitsGUI/ViewBot.java +++ b/src/main/java/sevenUnitsGUI/ViewBot.java @@ -216,6 +216,11 @@ final class ViewBot implements UnitConversionView, ExpressionConversionView { /** The units available in the To selection */ private Set toUnits; + /** The selected unit in the unit viewer */ + private Optional unitViewerSelection; + /** The selected unit in the prefix viewer */ + private Optional prefixViewerSelection; + /** Saved outputs of all unit conversions */ private final List unitConversions; /** Saved outputs of all unit expressions */ @@ -314,12 +319,12 @@ final class ViewBot implements UnitConversionView, ExpressionConversionView { @Override public Optional getViewedPrefixName() { - throw new UnsupportedOperationException("Not implemented yet"); + return this.prefixViewerSelection; } @Override public Optional getViewedUnitName() { - throw new UnsupportedOperationException("Not implemented yet"); + return this.unitViewerSelection; } /** @@ -423,26 +428,24 @@ final class ViewBot implements UnitConversionView, ExpressionConversionView { @Override public void setViewablePrefixNames(Set prefixNames) { - throw new UnsupportedOperationException("Not implemented yet"); + // do nothing, ViewBot supports selecting any prefix } @Override public void setViewableUnitNames(Set unitNames) { - throw new UnsupportedOperationException("Not implemented yet"); + // do nothing, ViewBot supports selecting any unit } - public void setViewedPrefixName( - @SuppressWarnings("unused") Optional viewedPrefixName) { - throw new UnsupportedOperationException("Not implemented yet"); + public void setViewedPrefixName(Optional viewedPrefixName) { + this.prefixViewerSelection = viewedPrefixName; } 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(Optional viewedUnitName) { + this.unitViewerSelection = viewedUnitName; } public void setViewedUnitName(String viewedUnitName) { @@ -462,13 +465,15 @@ final class ViewBot implements UnitConversionView, ExpressionConversionView { @Override public void showPrefix(NameSymbol name, String multiplierString) { - throw new UnsupportedOperationException("Not implemented yet"); + this.prefixViewingRecords + .add(new PrefixViewingRecord(name, multiplierString)); } @Override public void showUnit(NameSymbol name, String definition, String dimensionName, UnitType type) { - throw new UnsupportedOperationException("Not implemented yet"); + this.unitViewingRecords + .add(new UnitViewingRecord(name, definition, dimensionName, type)); } @Override diff --git a/src/test/java/sevenUnitsGUI/PresenterTest.java b/src/test/java/sevenUnitsGUI/PresenterTest.java index 85ebe09..8446a90 100644 --- a/src/test/java/sevenUnitsGUI/PresenterTest.java +++ b/src/test/java/sevenUnitsGUI/PresenterTest.java @@ -212,7 +212,8 @@ public final class PresenterTest { presenter.prefixSelected(); // just in case // get correct values - final var expectedNameSymbol = Metric.KILO.getNameSymbol(); + final var expectedNameSymbol = presenter.database.getPrefix("kilo") + .getNameSymbol(); final var expectedMultiplierString = String .valueOf(Metric.KILO.getMultiplier()); @@ -298,9 +299,11 @@ public final class PresenterTest { // 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 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 -- 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/sevenUnits/unit/UnitDatabase.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 From 40f7b6e806140fc2fc741c63c71f5ce97b4bd1d2 Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Mon, 18 Apr 2022 17:41:43 -0500 Subject: Implemented one-way conversion, duplicate prefixes can now be hidden --- CHANGELOG.org | 1 + .../sevenUnits/converterGUI/SevenUnitsGUI.java | 5 +- src/main/java/sevenUnits/unit/BritishImperial.java | 2 +- src/main/java/sevenUnits/unit/UnitDatabase.java | 26 +++++--- src/main/java/sevenUnits/unit/UnitType.java | 24 ++++++++ src/main/java/sevenUnitsGUI/Presenter.java | 71 +++++++++++++--------- src/main/java/sevenUnitsGUI/TabbedView.java | 9 ++- .../java/sevenUnits/unit/UnitDatabaseTest.java | 2 +- src/test/java/sevenUnitsGUI/PresenterTest.java | 10 +-- 9 files changed, 97 insertions(+), 53 deletions(-) (limited to 'src/main/java/sevenUnits/unit/UnitDatabase.java') diff --git a/CHANGELOG.org b/CHANGELOG.org index c164d1f..509553b 100644 --- a/CHANGELOG.org +++ b/CHANGELOG.org @@ -12,6 +12,7 @@ - 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 diff --git a/src/main/java/sevenUnits/converterGUI/SevenUnitsGUI.java b/src/main/java/sevenUnits/converterGUI/SevenUnitsGUI.java index e10bab4..309bdb9 100644 --- a/src/main/java/sevenUnits/converterGUI/SevenUnitsGUI.java +++ b/src/main/java/sevenUnits/converterGUI/SevenUnitsGUI.java @@ -298,7 +298,8 @@ final class SevenUnitsGUI { this.database.unitMapPrefixless(true).keySet()); this.unitNames.sort(null); // sorts it using Comparable - this.prefixNames = new ArrayList<>(this.database.prefixMap().keySet()); + this.prefixNames = new ArrayList<>( + this.database.prefixMap(true).keySet()); this.prefixNames.sort(this.prefixNameComparator); // sorts it using my // comparator @@ -611,7 +612,7 @@ final class SevenUnitsGUI { * @since v0.2.0 */ public final Set prefixNameSet() { - return this.database.prefixMap().keySet(); + return this.database.prefixMap(true).keySet(); } /** diff --git a/src/main/java/sevenUnits/unit/BritishImperial.java b/src/main/java/sevenUnits/unit/BritishImperial.java index c6e65fb..0ecba6d 100644 --- a/src/main/java/sevenUnits/unit/BritishImperial.java +++ b/src/main/java/sevenUnits/unit/BritishImperial.java @@ -121,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/UnitDatabase.java b/src/main/java/sevenUnits/unit/UnitDatabase.java index a4f0c44..71676a1 100644 --- a/src/main/java/sevenUnits/unit/UnitDatabase.java +++ b/src/main/java/sevenUnits/unit/UnitDatabase.java @@ -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 unitMap, - Entry entry) { - for (final Entry e : unitMap.entrySet()) { + static boolean isRemovableDuplicate(Map map, + Entry entry) { + for (final Entry 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; @@ -2010,12 +2010,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 prefixMap() { - return Collections.unmodifiableMap(this.prefixes); + public Map 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/UnitType.java b/src/main/java/sevenUnits/unit/UnitType.java index a13051a..7cebf2d 100644 --- a/src/main/java/sevenUnits/unit/UnitType.java +++ b/src/main/java/sevenUnits/unit/UnitType.java @@ -16,6 +16,8 @@ */ package sevenUnits.unit; +import java.util.function.Predicate; + /** * A type of unit, as chosen by the type of system it is in. *

    @@ -31,4 +33,26 @@ package sevenUnits.unit; */ public enum UnitType { METRIC, SEMI_METRIC, NON_METRIC; + + /** + * Determines which type a unit is. The type will be: + *
      + *
    • {@code SEMI_METRIC} if the unit passes the provided predicate + *
    • {@code METRIC} if it fails the predicate but is metric + *
    • {@code NON_METRIC} if it fails the predicate and is not metric + *
    + * + * @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 isSemiMetric) { + if (isSemiMetric.test(u)) + return SEMI_METRIC; + else if (u.isMetric()) + return METRIC; + else + return NON_METRIC; + } } diff --git a/src/main/java/sevenUnitsGUI/Presenter.java b/src/main/java/sevenUnitsGUI/Presenter.java index 85a0ddc..f4f3e3a 100644 --- a/src/main/java/sevenUnitsGUI/Presenter.java +++ b/src/main/java/sevenUnitsGUI/Presenter.java @@ -30,6 +30,7 @@ import java.util.Set; import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; +import java.util.stream.Stream; import sevenUnits.ProgramInfo; import sevenUnits.unit.BaseDimension; @@ -194,10 +195,10 @@ public final class Presenter { private boolean oneWayConversionEnabled; /** - * If this is false, duplicate units will be removed from the unit view in - * views that show units as a list to choose from. + * 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 showDuplicateUnits; + private boolean showDuplicates; /** * Creates a Presenter @@ -416,8 +417,8 @@ public final class Presenter { * @return true iff duplicate units are shown in unit lists * @since 2022-03-30 */ - public boolean duplicateUnitsShown() { - return this.showDuplicateUnits; + public boolean duplicatesShown() { + return this.showDuplicates; } /** @@ -455,25 +456,18 @@ public final class Presenter { } /** - * @return type of unit {@code u} + * @return whether or not the provided unit is semi-metric (i.e. an + * exception) * @since 2022-04-16 */ - private final UnitType getUnitType(Unit u) { + private final boolean isSemiMetric(Unit u) { // determine if u is an exception final var primaryName = u.getPrimaryName(); final var symbol = u.getSymbol(); - final boolean isException = primaryName.isPresent() + return primaryName.isPresent() && this.metricExceptions.contains(primaryName.orElseThrow()) || symbol.isPresent() && this.metricExceptions.contains(symbol.orElseThrow()); - - // determine unit type - if (isException) - return UnitType.SEMI_METRIC; - else if (u.isMetric()) - return UnitType.METRIC; - else - return UnitType.NON_METRIC; } /** @@ -510,10 +504,7 @@ public final class Presenter { ucview.setDimensionNames(this.database.dimensionMap().keySet()); } - // load units & prefixes into viewers - this.view.setViewableUnitNames( - this.database.unitMapPrefixless(this.showDuplicateUnits).keySet()); - this.view.setViewablePrefixNames(this.database.prefixMap().keySet()); + this.updateView(); } void prefixSelected() { @@ -572,8 +563,8 @@ public final class Presenter { * @param showDuplicateUnits whether or not duplicate units should be shown * @since 2022-03-30 */ - public void setShowDuplicateUnits(boolean showDuplicateUnits) { - this.showDuplicateUnits = showDuplicateUnits; + public void setShowDuplicates(boolean showDuplicateUnits) { + this.showDuplicates = showDuplicateUnits; this.updateView(); } @@ -589,7 +580,7 @@ public final class Presenter { || u instanceof LinearUnit && ((LinearUnit) u).isBase(); final var definition = isBase ? "(Base unit)" : u.toDefinitionString(); final var dimensionString = this.getDimensionName(u.getDimension()); - final var unitType = this.getUnitType(u); + final var unitType = UnitType.getType(u, this::isSemiMetric); this.view.showUnit(nameSymbol, definition, dimensionString, unitType); } @@ -621,14 +612,36 @@ public final class Presenter { .getDimension(((UnitConversionView) this.view) .getSelectedDimensionName().orElseThrow()); - final Set units = this.database - .unitMapPrefixless(this.showDuplicateUnits).entrySet().stream() + // 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 + Stream fromUnits = this.database + .unitMapPrefixless(this.showDuplicates).entrySet().stream() .map(Map.Entry::getValue) - .filter(u -> viewDimension.equals(u.getDimension())) - .map(Unit::getName).collect(Collectors.toSet()); + .filter(u -> viewDimension.equals(u.getDimension())); + + Stream toUnits = this.database + .unitMapPrefixless(this.showDuplicates).entrySet().stream() + .map(Map.Entry::getValue) + .filter(u -> viewDimension.equals(u.getDimension())); + + // filter by unit type, if desired + if (this.oneWayConversionEnabled) { + fromUnits = fromUnits.filter(u -> UnitType.getType(u, + this::isSemiMetric) != UnitType.METRIC); + toUnits = toUnits.filter(u -> UnitType.getType(u, + this::isSemiMetric) != UnitType.NON_METRIC); + } - ucview.setFromUnitNames(units); - ucview.setToUnitNames(units); + // set unit names + ucview.setFromUnitNames( + fromUnits.map(Unit::getName).collect(Collectors.toSet())); + ucview.setToUnitNames( + toUnits.map(Unit::getName).collect(Collectors.toSet())); } } diff --git a/src/main/java/sevenUnitsGUI/TabbedView.java b/src/main/java/sevenUnitsGUI/TabbedView.java index 3a951ef..098c374 100644 --- a/src/main/java/sevenUnitsGUI/TabbedView.java +++ b/src/main/java/sevenUnitsGUI/TabbedView.java @@ -634,11 +634,10 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { .setAnchor(GridBagConstraints.LINE_START).build()); final JCheckBox showAllVariations = new JCheckBox( - "Show Duplicates in \"Convert Units\""); - showAllVariations.setSelected(this.presenter.duplicateUnitsShown()); - showAllVariations - .addItemListener(e -> this.presenter.setShowDuplicateUnits( - e.getStateChange() == ItemEvent.SELECTED)); + "Show Duplicate Units & Prefixes"); + showAllVariations.setSelected(this.presenter.duplicatesShown()); + showAllVariations.addItemListener(e -> this.presenter + .setShowDuplicates(e.getStateChange() == ItemEvent.SELECTED)); miscPanel.add(showAllVariations, new GridBagBuilder(0, 1) .setAnchor(GridBagConstraints.LINE_START).build()); diff --git a/src/test/java/sevenUnits/unit/UnitDatabaseTest.java b/src/test/java/sevenUnits/unit/UnitDatabaseTest.java index b8669cb..4be33dd 100644 --- a/src/test/java/sevenUnits/unit/UnitDatabaseTest.java +++ b/src/test/java/sevenUnits/unit/UnitDatabaseTest.java @@ -596,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 nameIterator = database.unitMap().keySet() .iterator(); diff --git a/src/test/java/sevenUnitsGUI/PresenterTest.java b/src/test/java/sevenUnitsGUI/PresenterTest.java index f52d846..362a5c9 100644 --- a/src/test/java/sevenUnitsGUI/PresenterTest.java +++ b/src/test/java/sevenUnitsGUI/PresenterTest.java @@ -147,13 +147,13 @@ public final class PresenterTest { presenter.database.addUnit("meter", meter); // test that only one of them is included if duplicate units disabled - presenter.setShowDuplicateUnits(false); + 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.setShowDuplicateUnits(true); + presenter.setShowDuplicates(true); presenter.updateView(); assertEquals(2, viewBot.getFromUnitNames().size()); assertEquals(2, viewBot.getToUnitNames().size()); @@ -262,19 +262,19 @@ public final class PresenterTest { // set and save custom settings presenter.setOneWayConversionEnabled(true); - presenter.setShowDuplicateUnits(true); + presenter.setShowDuplicates(true); presenter.setNumberDisplayRule(StandardDisplayRules.fixedPrecision(11)); presenter.saveSettings(TEST_SETTINGS); // overwrite custom settings presenter.setOneWayConversionEnabled(false); - presenter.setShowDuplicateUnits(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.duplicateUnitsShown()); + assertTrue(presenter.duplicatesShown()); assertEquals(StandardDisplayRules.fixedPrecision(11), presenter.getNumberDisplayRule()); } -- cgit v1.2.3 From 0aacba9fc8a9140fdf331172ad66afe280d09b5e Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Tue, 19 Apr 2022 16:10:44 -0500 Subject: Implemented prefix settings, saving & loading of settings Also fixed some bugs: - Presenter now has default values for its settings in case they don't load properly - UnitDatabase ensures its units, prefixes and dimensions have all of the names you give it --- src/main/java/sevenUnits/unit/UnitDatabase.java | 29 ++-- src/main/java/sevenUnits/utils/NameSymbol.java | 17 ++ .../sevenUnitsGUI/DefaultPrefixRepetitionRule.java | 95 +++++++++++ src/main/java/sevenUnitsGUI/Presenter.java | 175 ++++++++++++++++++--- src/main/java/sevenUnitsGUI/TabbedView.java | 77 ++++++--- .../java/sevenUnitsGUI/UnitConversionRecord.java | 19 +++ src/main/java/sevenUnitsGUI/View.java | 6 + src/main/java/sevenUnitsGUI/ViewBot.java | 23 +-- src/test/java/sevenUnitsGUI/PresenterTest.java | 25 +-- src/test/resources/test-settings.txt | 4 + 10 files changed, 384 insertions(+), 86 deletions(-) create mode 100644 src/main/java/sevenUnitsGUI/DefaultPrefixRepetitionRule.java create mode 100644 src/test/resources/test-settings.txt (limited to 'src/main/java/sevenUnits/unit/UnitDatabase.java') diff --git a/src/main/java/sevenUnits/unit/UnitDatabase.java b/src/main/java/sevenUnits/unit/UnitDatabase.java index 71676a1..12b78a7 100644 --- a/src/main/java/sevenUnits/unit/UnitDatabase.java +++ b/src/main/java/sevenUnits/unit/UnitDatabase.java @@ -1315,13 +1315,9 @@ public final class UnitDatabase { final ObjectProduct dimension) { Objects.requireNonNull(name, "name may not be null"); Objects.requireNonNull(dimension, "dimension may not be null"); - if (!dimension.getNameSymbol().equals(NameSymbol.EMPTY)) { - this.dimensions.put(name, dimension); - } else { - final ObjectProduct namedDimension = dimension - .withName(NameSymbol.ofName(name)); - this.dimensions.put(name, namedDimension); - } + final ObjectProduct namedDimension = dimension + .withName(dimension.getNameSymbol().withExtraName(name)); + this.dimensions.put(name, namedDimension); } /** @@ -1373,7 +1369,7 @@ public final class UnitDatabase { throw e; } - this.addDimension(name, dimension.withName(NameSymbol.ofName(name))); + this.addDimension(name, dimension); } } @@ -1387,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); } /** @@ -1401,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); } /** @@ -1458,8 +1459,7 @@ public final class UnitDatabase { throw e; } final String prefixName = name.substring(0, name.length() - 1); - this.addPrefix(prefixName, - prefix.withName(NameSymbol.ofName(prefixName))); + this.addPrefix(prefixName, prefix); } else { // it's a unit, get the unit final Unit unit; @@ -1470,8 +1470,7 @@ public final class UnitDatabase { System.err.printf("Parsing error on line %d:%n", lineCounter); throw e; } - - this.addUnit(name, unit.withName(NameSymbol.ofName(name))); + this.addUnit(name, unit); } } } diff --git a/src/main/java/sevenUnits/utils/NameSymbol.java b/src/main/java/sevenUnits/utils/NameSymbol.java index 955f980..7ef2967 100644 --- a/src/main/java/sevenUnits/utils/NameSymbol.java +++ b/src/main/java/sevenUnits/utils/NameSymbol.java @@ -288,4 +288,21 @@ public final class NameSymbol { 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/sevenUnitsGUI/DefaultPrefixRepetitionRule.java b/src/main/java/sevenUnitsGUI/DefaultPrefixRepetitionRule.java new file mode 100644 index 0000000..b56356d --- /dev/null +++ b/src/main/java/sevenUnitsGUI/DefaultPrefixRepetitionRule.java @@ -0,0 +1,95 @@ +/** + * @since 2020-08-26 + */ +package sevenUnitsGUI; + +import java.util.List; +import java.util.function.Predicate; + +import sevenUnits.unit.Metric; +import sevenUnits.unit.UnitPrefix; + +/** + * A rule that specifies whether prefix repetition is allowed + * + * @since 2020-08-26 + */ +public enum DefaultPrefixRepetitionRule implements Predicate> { + NO_REPETITION { + @Override + public boolean test(List prefixes) { + return prefixes.size() <= 1; + } + }, + NO_RESTRICTION { + @Override + public boolean test(List prefixes) { + return true; + } + }, + /** + * You are allowed to have any number of Yotta/Yocto followed by possibly one + * Kilo-Zetta/Milli-Zepto followed by possibly one Deca/Hecto. Same for + * reducing prefixes, don't mix magnifying and reducing. Non-metric + * (including binary) prefixes can't be repeated. + */ + COMPLEX_REPETITION { + @Override + public boolean test(List prefixes) { + // determine whether we are magnifying or reducing + final boolean magnifying; + if (prefixes.isEmpty()) + return true; + else if (prefixes.get(0).getMultiplier() > 1) { + magnifying = true; + } else { + magnifying = false; + } + + // if the first prefix is non-metric (including binary prefixes), + // assume we are using non-metric prefixes + // non-metric prefixes are allowed, but can't be repeated. + if (!Metric.DECIMAL_PREFIXES.contains(prefixes.get(0))) + return NO_REPETITION.test(prefixes); + + int part = 0; // 0=yotta/yoctos, 1=kilo-zetta/milli-zepto, + // 2=deka,hecto,deci,centi + + for (final UnitPrefix prefix : prefixes) { + // check that the current prefix is metric and appropriately + // magnifying/reducing + if (!Metric.DECIMAL_PREFIXES.contains(prefix)) + return false; + if (magnifying != prefix.getMultiplier() > 1) + return false; + + // check if the current prefix is correct + // since part is set *after* this check, part designates the state + // of the *previous* prefix + switch (part) { + case 0: + // do nothing, any prefix is valid after a yotta + break; + case 1: + // after a kilo-zetta, only deka/hecto are valid + if (Metric.THOUSAND_PREFIXES.contains(prefix)) + return false; + break; + case 2: + // deka/hecto must be the last prefix, so this is always invalid + return false; + } + + // set part + if (Metric.YOTTA.equals(prefix) || Metric.YOCTO.equals(prefix)) { + part = 0; + } else if (Metric.THOUSAND_PREFIXES.contains(prefix)) { + part = 1; + } else { + part = 2; + } + } + return true; + } + }; +} diff --git a/src/main/java/sevenUnitsGUI/Presenter.java b/src/main/java/sevenUnitsGUI/Presenter.java index f4f3e3a..fd050b7 100644 --- a/src/main/java/sevenUnitsGUI/Presenter.java +++ b/src/main/java/sevenUnitsGUI/Presenter.java @@ -16,8 +16,10 @@ */ 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; @@ -30,7 +32,6 @@ import java.util.Set; import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; -import java.util.stream.Stream; import sevenUnits.ProgramInfo; import sevenUnits.unit.BaseDimension; @@ -139,13 +140,25 @@ public final class Presenter { 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(index); + return index == -1 ? line : line.substring(0, index); } // ====== SETTINGS ====== @@ -171,14 +184,15 @@ public final class Presenter { * of unit conversions will be put into this function, and the resulting * string will be used in the output. */ - private Function numberDisplayRule; + private Function 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> prefixRepetitionRule; + private Predicate> prefixRepetitionRule = DefaultPrefixRepetitionRule.NO_RESTRICTION; /** * The set of units that is considered neither metric nor nonmetric for the @@ -192,13 +206,13 @@ public final class Presenter { * removed from the From unit list and imperial/USC units removed from the To * unit list. */ - private boolean oneWayConversionEnabled; + 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; + private boolean showDuplicates = false; /** * Creates a Presenter @@ -245,7 +259,19 @@ public final class Presenter { } // set default settings temporarily - this.numberDisplayRule = StandardDisplayRules.uncertaintyBased(); + this.loadSettings(DEFAULT_SETTINGS_FILEPATH); + + // a Predicate that returns true iff the argument is a full base unit + final Predicate 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()); } /** @@ -455,19 +481,36 @@ public final class Presenter { return this.numberParsingRule; } + /** + * @return the rule that determines whether a set of prefixes is valid + * @since 2022-04-19 + */ + public Predicate> 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 */ - private final boolean isSemiMetric(Unit u) { + 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()); + && this.metricExceptions.contains(symbol.orElseThrow()) + || sharesAnyElements(this.metricExceptions, u.getOtherNames()); } /** @@ -477,7 +520,48 @@ public final class Presenter { * @param settingsFile file settings should be loaded from * @since 2021-12-15 */ - void loadSettings(Path settingsFile) {} + 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 @@ -519,13 +603,38 @@ public final class Presenter { 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) {} + 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 @@ -559,6 +668,17 @@ public final class Presenter { this.updateView(); } + /** + * @param prefixRepetitionRule the rule that determines whether a set of + * prefixes is valid + * @since 2022-04-19 + */ + public void setPrefixRepetitionRule( + Predicate> prefixRepetitionRule) { + this.prefixRepetitionRule = prefixRepetitionRule; + this.database.setPrefixRepetitionRule(prefixRepetitionRule); + } + /** * @param showDuplicateUnits whether or not duplicate units should be shown * @since 2022-03-30 @@ -608,9 +728,7 @@ public final class Presenter { public void updateView() { if (this.view instanceof UnitConversionView) { final UnitConversionView ucview = (UnitConversionView) this.view; - final ObjectProduct viewDimension = this.database - .getDimension(((UnitConversionView) this.view) - .getSelectedDimensionName().orElseThrow()); + final var selectedDimensionName = ucview.getSelectedDimensionName(); // load units & prefixes into viewers this.view.setViewableUnitNames( @@ -619,29 +737,34 @@ public final class Presenter { this.database.prefixMap(this.showDuplicates).keySet()); // get From and To units - Stream fromUnits = this.database - .unitMapPrefixless(this.showDuplicates).entrySet().stream() - .map(Map.Entry::getValue) - .filter(u -> viewDimension.equals(u.getDimension())); + var fromUnits = this.database.unitMapPrefixless(this.showDuplicates) + .entrySet().stream(); + var toUnits = this.database.unitMapPrefixless(this.showDuplicates) + .entrySet().stream(); - Stream toUnits = this.database - .unitMapPrefixless(this.showDuplicates).entrySet().stream() - .map(Map.Entry::getValue) - .filter(u -> viewDimension.equals(u.getDimension())); + // 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, + fromUnits = fromUnits.filter(u -> UnitType.getType(u.getValue(), this::isSemiMetric) != UnitType.METRIC); - toUnits = toUnits.filter(u -> UnitType.getType(u, + toUnits = toUnits.filter(u -> UnitType.getType(u.getValue(), this::isSemiMetric) != UnitType.NON_METRIC); } // set unit names ucview.setFromUnitNames( - fromUnits.map(Unit::getName).collect(Collectors.toSet())); + fromUnits.map(Map.Entry::getKey).collect(Collectors.toSet())); ucview.setToUnitNames( - toUnits.map(Unit::getName).collect(Collectors.toSet())); + toUnits.map(Map.Entry::getKey).collect(Collectors.toSet())); } } diff --git a/src/main/java/sevenUnitsGUI/TabbedView.java b/src/main/java/sevenUnitsGUI/TabbedView.java index 098c374..c8e69ee 100644 --- a/src/main/java/sevenUnitsGUI/TabbedView.java +++ b/src/main/java/sevenUnitsGUI/TabbedView.java @@ -543,39 +543,49 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { .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 (this.presenter.prefixRule == DefaultPrefixRepetitionRule.NO_REPETITION) { -// noRepetition.setSelected(true); -// } -// noRepetition -// .addActionListener(e -> this.presenter.setPrefixRepetitionRule( -// DefaultPrefixRepetitionRule.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 (this.presenter.prefixRule == DefaultPrefixRepetitionRule.NO_RESTRICTION) { -// noRestriction.setSelected(true); -// } -// noRestriction -// .addActionListener(e -> this.presenter.setPrefixRepetitionRule( -// DefaultPrefixRepetitionRule.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 (this.presenter.prefixRule == DefaultPrefixRepetitionRule.COMPLEX_REPETITION) { -// customRepetition.setSelected(true); -// } -// customRepetition -// .addActionListener(e -> this.presenter.setPrefixRepetitionRule( -// DefaultPrefixRepetitionRule.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()); @@ -628,16 +638,22 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { final JCheckBox oneWay = new JCheckBox("Convert One Way Only"); oneWay.setSelected(this.presenter.oneWayConversionEnabled()); - oneWay.addItemListener(e -> this.presenter.setOneWayConversionEnabled( - e.getStateChange() == ItemEvent.SELECTED)); + 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)); + 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()); @@ -678,6 +694,11 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { return this.valueInput.getText(); } + @Override + public Presenter getPresenter() { + return this.presenter; + } + /** * @return the precision of the presenter's rounding rule, if that is * meaningful @@ -697,6 +718,17 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { return OptionalInt.empty(); } + /** + * @return presenter's prefix repetition rule + * @since 2022-04-19 + */ + private Optional 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. * @@ -830,5 +862,6 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { 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 index 60675e2..f951f44 100644 --- a/src/main/java/sevenUnitsGUI/UnitConversionRecord.java +++ b/src/main/java/sevenUnitsGUI/UnitConversionRecord.java @@ -16,6 +16,9 @@ */ package sevenUnitsGUI; +import java.math.RoundingMode; + +import sevenUnits.unit.LinearUnitValue; import sevenUnits.unit.UnitValue; /** @@ -24,6 +27,22 @@ import sevenUnits.unit.UnitValue; * @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 * diff --git a/src/main/java/sevenUnitsGUI/View.java b/src/main/java/sevenUnitsGUI/View.java index da3749e..011e87f 100644 --- a/src/main/java/sevenUnitsGUI/View.java +++ b/src/main/java/sevenUnitsGUI/View.java @@ -29,6 +29,12 @@ import sevenUnits.utils.NameSymbol; * @since 2021-12-15 */ public interface View { + /** + * @return the presenter associated with this view + * @since 2022-04-19 + */ + Presenter getPresenter(); + /** * @return name of prefix currently being viewed * @since 2022-04-10 diff --git a/src/main/java/sevenUnitsGUI/ViewBot.java b/src/main/java/sevenUnitsGUI/ViewBot.java index dd9869d..9253ae5 100644 --- a/src/main/java/sevenUnitsGUI/ViewBot.java +++ b/src/main/java/sevenUnitsGUI/ViewBot.java @@ -196,30 +196,30 @@ final class ViewBot implements UnitConversionView, ExpressionConversionView { private final Presenter presenter; /** The dimensions available to select from */ - private Set dimensionNames; + private Set dimensionNames = Set.of(); /** The expression in the From field */ - private String fromExpression; + private String fromExpression = ""; /** The expression in the To field */ - private String toExpression; + private String toExpression = ""; /** * The user-provided string representing the value in {@code fromSelection} */ - private String inputValue; + private String inputValue = ""; /** The unit selected in the From selection */ - private Optional fromSelection; + private Optional fromSelection = Optional.empty(); /** The unit selected in the To selection */ - private Optional toSelection; + private Optional toSelection = Optional.empty(); /** The currently selected dimension */ - private Optional selectedDimensionName; + private Optional selectedDimensionName = Optional.empty(); /** The units available in the From selection */ - private Set fromUnits; + private Set fromUnits = Set.of(); /** The units available in the To selection */ - private Set toUnits; + private Set toUnits = Set.of(); /** The selected unit in the unit viewer */ - private Optional unitViewerSelection; + private Optional unitViewerSelection = Optional.empty(); /** The selected unit in the prefix viewer */ - private Optional prefixViewerSelection; + private Optional prefixViewerSelection = Optional.empty(); /** Saved outputs of all unit conversions */ private final List unitConversions; @@ -289,6 +289,7 @@ final class ViewBot implements UnitConversionView, ExpressionConversionView { * @return the presenter associated with tihs view * @since 2022-01-29 */ + @Override public Presenter getPresenter() { return this.presenter; } diff --git a/src/test/java/sevenUnitsGUI/PresenterTest.java b/src/test/java/sevenUnitsGUI/PresenterTest.java index 362a5c9..f639329 100644 --- a/src/test/java/sevenUnitsGUI/PresenterTest.java +++ b/src/test/java/sevenUnitsGUI/PresenterTest.java @@ -19,6 +19,7 @@ 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; @@ -32,10 +33,10 @@ 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.unit.UnitValue; import sevenUnits.utils.NameSymbol; import sevenUnits.utils.Nameable; import sevenUnits.utils.ObjectProduct; @@ -91,7 +92,7 @@ public final class PresenterTest { // test result final List outputs = viewBot .expressionConversionList(); - assertEquals("10000.0 m = 10.0 km", + assertEquals("10000.0 m = 10.00000 km", outputs.get(outputs.size() - 1).toString()); } @@ -120,12 +121,14 @@ public final class PresenterTest { * here (that's for the backend tests), I'm just testing that it correctly * calls the unit conversion system */ - final UnitValue expectedInput = UnitValue.of(Metric.METRE, 10000.0); - final UnitValue expectedOutput = expectedInput + final LinearUnitValue expectedInput = LinearUnitValue.of(Metric.METRE, + UncertainDouble.fromRoundedString("10000.0")); + final LinearUnitValue expectedOutput = expectedInput .convertTo(Metric.KILOMETRE); - final UnitConversionRecord expectedUC = UnitConversionRecord - .fromValues(expectedInput, expectedOutput); - + 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()); } @@ -182,13 +185,11 @@ public final class PresenterTest { // 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()); + assertEquals(nonMetricNames, viewBot.getFromUnitNames()); + assertEquals(metricNames, 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()); } @@ -243,7 +244,7 @@ public final class PresenterTest { // test the result of the rounding final String expectedOutputString = roundingRule - .apply(UncertainDouble.of(12.3456789, 0)); + .apply(UncertainDouble.fromRoundedString("12.3456789")); final String actualOutputString = viewBot.unitConversionList().get(0) .outputValueString(); assertEquals(expectedOutputString, actualOutputString); diff --git a/src/test/resources/test-settings.txt b/src/test/resources/test-settings.txt new file mode 100644 index 0000000..932221e --- /dev/null +++ b/src/test/resources/test-settings.txt @@ -0,0 +1,4 @@ +number_display_rule=Round to 11 significant figures +prefix_rule=NO_RESTRICTION +one_way=true +include_duplicates=true -- cgit v1.2.3