From e74668b91c247e7395362e8e411c3e1a13ec10d1 Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Thu, 15 Aug 2024 16:51:45 -0500 Subject: Add ability to convert expression to sum of units --- src/main/java/sevenUnits/unit/LinearUnitValue.java | 103 ++-- src/main/java/sevenUnitsGUI/Presenter.java | 606 ++++++++++++--------- src/main/java/sevenUnitsGUI/TabbedView.java | 207 ++++--- .../java/sevenUnitsGUI/UnitConversionRecord.java | 24 +- 4 files changed, 527 insertions(+), 413 deletions(-) (limited to 'src/main/java') diff --git a/src/main/java/sevenUnits/unit/LinearUnitValue.java b/src/main/java/sevenUnits/unit/LinearUnitValue.java index 3a9428b..fad3eb0 100644 --- a/src/main/java/sevenUnits/unit/LinearUnitValue.java +++ b/src/main/java/sevenUnits/unit/LinearUnitValue.java @@ -17,10 +17,13 @@ package sevenUnits.unit; import java.math.RoundingMode; +import java.util.ArrayList; +import java.util.List; import java.util.Objects; import java.util.Optional; import sevenUnits.utils.DecimalComparison; +import sevenUnits.utils.ObjectProduct; import sevenUnits.utils.UncertainDouble; /** @@ -34,7 +37,7 @@ import sevenUnits.utils.UncertainDouble; */ public final class LinearUnitValue { public static final LinearUnitValue ONE = getExact(Metric.ONE, 1); - + /** * Gets an exact {@code LinearUnitValue} * @@ -49,7 +52,7 @@ public final class LinearUnitValue { Objects.requireNonNull(unit, "unit must not be null"), UncertainDouble.of(value, 0)); } - + /** * Gets an uncertain {@code LinearUnitValue} * @@ -65,11 +68,11 @@ public final class LinearUnitValue { Objects.requireNonNull(unit, "unit must not be null"), Objects.requireNonNull(value, "value may not be null")); } - + private final LinearUnit unit; - + private final UncertainDouble value; - + /** * @param unit unit to express as * @param value value to express @@ -79,7 +82,7 @@ public final class LinearUnitValue { this.unit = unit; this.value = value; } - + /** * @return this value as a {@code UnitValue}. All uncertainty information is * removed from the returned value. @@ -88,7 +91,7 @@ public final class LinearUnitValue { public final UnitValue asUnitValue() { return UnitValue.of(this.unit, this.value.value()); } - + /** * @param other a {@code LinearUnit} * @return true iff this value can be represented with {@code other}. @@ -97,7 +100,7 @@ public final class LinearUnitValue { public final boolean canConvertTo(final LinearUnit other) { return this.unit.canConvertTo(other); } - + /** * Returns a LinearUnitValue that represents the same value expressed in a * different unit @@ -109,7 +112,43 @@ public final class LinearUnitValue { public final LinearUnitValue convertTo(final LinearUnit other) { return LinearUnitValue.of(other, this.unit.convertTo(other, this.value)); } - + + /** + * Convert a LinearUnitValue to a sum of multiple units, where all but the + * last have exact integer values. + * + * @param others units to convert to + * @throws IllegalArgumentException if no units are provided or units + * provided have incompatible bases + * @since 2024-08-15 + */ + public final List convertToMultiple( + final List others) { + if (others.size() < 1) + throw new IllegalArgumentException("Must have at least one unit"); + final ObjectProduct unitBase = others.get(0).getBase(); + for (final LinearUnit unit : others) { + if (!unitBase.equals(unit.getBase())) + throw new IllegalArgumentException( + "All units must have the same base."); + } + + LinearUnitValue remaining = this; + final List values = new ArrayList<>(others.size()); + for (final LinearUnit unit : others.subList(0, others.size() - 1)) { + final LinearUnitValue remainingInUnit = remaining.convertTo(unit); + final LinearUnitValue value = getExact(unit, + Math.floor(remainingInUnit.getValueExact())); + values.add(value); + remaining = remaining.minus(value); + } + + final LinearUnitValue lastValue = remaining + .convertTo(others.get(others.size() - 1)); + values.add(lastValue); + return values; + } + /** * Divides this value by a scalar * @@ -120,7 +159,7 @@ public final class LinearUnitValue { public LinearUnitValue dividedBy(final double divisor) { return LinearUnitValue.of(this.unit, this.value.dividedByExact(divisor)); } - + /** * Divides this value by another value * @@ -132,7 +171,7 @@ public final class LinearUnitValue { return LinearUnitValue.of(this.unit.dividedBy(divisor.unit), this.value.dividedBy(divisor.value)); } - + /** * Returns true if this and obj represent the same value, regardless of * whether or not they are expressed in the same unit. So (1000 m).equals(1 @@ -150,7 +189,7 @@ public final class LinearUnitValue { && this.unit.convertToBase(this.value) .equals(other.unit.convertToBase(other.value)); } - + /** * Returns true if this and obj represent the same value, regardless of * whether or not they are expressed in the same unit. So (1000 m).equals(1 @@ -171,7 +210,7 @@ public final class LinearUnitValue { && DecimalComparison.equals(this.unit.convertToBase(this.value), other.unit.convertToBase(other.value)); } - + /** * @param other another {@code LinearUnitValue} * @return true iff this and other are within each other's uncertainty range @@ -185,10 +224,10 @@ public final class LinearUnitValue { final LinearUnit base = LinearUnit.valueOf(this.unit.getBase(), 1); final LinearUnitValue thisBase = this.convertTo(base); final LinearUnitValue otherBase = other.convertTo(base); - + return thisBase.value.equivalent(otherBase.value); } - + /** * @return the unit * @since 2020-09-29 @@ -196,7 +235,7 @@ public final class LinearUnitValue { public final LinearUnit getUnit() { return this.unit; } - + /** * @return the value * @since 2020-09-29 @@ -204,7 +243,7 @@ public final class LinearUnitValue { public final UncertainDouble getValue() { return this.value; } - + /** * @return the exact value * @since 2020-09-07 @@ -212,13 +251,13 @@ public final class LinearUnitValue { public final double getValueExact() { return this.value.value(); } - + @Override public int hashCode() { return Objects.hash(this.unit.getBase(), this.unit.convertToBase(this.getValue())); } - + /** * Returns the difference of this value and another, expressed in this * value's unit @@ -231,17 +270,17 @@ public final class LinearUnitValue { */ public LinearUnitValue minus(final LinearUnitValue subtrahend) { Objects.requireNonNull(subtrahend, "subtrahend may not be null"); - + if (!this.canConvertTo(subtrahend.unit)) throw new IllegalArgumentException(String.format( "Incompatible units for subtraction \"%s\" and \"%s\".", this.unit, subtrahend.unit)); - + final LinearUnitValue otherConverted = subtrahend.convertTo(this.unit); return LinearUnitValue.of(this.unit, this.value.minus(otherConverted.value)); } - + /** * Returns the sum of this value and another, expressed in this value's unit * @@ -253,17 +292,17 @@ public final class LinearUnitValue { */ public LinearUnitValue plus(final LinearUnitValue addend) { Objects.requireNonNull(addend, "addend may not be null"); - + if (!this.canConvertTo(addend.unit)) throw new IllegalArgumentException(String.format( "Incompatible units for addition \"%s\" and \"%s\".", this.unit, addend.unit)); - + final LinearUnitValue otherConverted = addend.convertTo(this.unit); return LinearUnitValue.of(this.unit, this.value.plus(otherConverted.value)); } - + /** * Multiplies this value by a scalar * @@ -274,7 +313,7 @@ public final class LinearUnitValue { public LinearUnitValue times(final double multiplier) { return LinearUnitValue.of(this.unit, this.value.timesExact(multiplier)); } - + /** * Multiplies this value by another value * @@ -286,7 +325,7 @@ public final class LinearUnitValue { return LinearUnitValue.of(this.unit.times(multiplier.unit), this.value.times(multiplier.value)); } - + /** * Raises a value to an exponent * @@ -298,12 +337,12 @@ public final class LinearUnitValue { return LinearUnitValue.of(this.unit.toExponent(exponent), this.value.toExponentExact(exponent)); } - + @Override public String toString() { return this.toString(!this.value.isExact(), RoundingMode.HALF_EVEN); } - + /** * Returns a string representing the object.
* If the attached unit has a name or symbol, the string looks like "12 km". @@ -321,9 +360,9 @@ public final class LinearUnitValue { final Optional primaryName = this.unit.getPrimaryName(); final Optional symbol = this.unit.getSymbol(); final String chosenName = symbol.orElse(primaryName.orElse(null)); - + final UncertainDouble baseValue = this.unit.convertToBase(this.value); - + // get rounded strings // if showUncertainty is true, add brackets around the string final String valueString = (showUncertainty ? "(" : "") @@ -332,7 +371,7 @@ public final class LinearUnitValue { final String baseValueString = (showUncertainty ? "(" : "") + baseValue.toString(showUncertainty, roundingMode) + (showUncertainty ? ")" : ""); - + // create string if (chosenName == null) return String.format("%s unnamed unit (= %s %s)", valueString, diff --git a/src/main/java/sevenUnitsGUI/Presenter.java b/src/main/java/sevenUnitsGUI/Presenter.java index eba8438..8f99649 100644 --- a/src/main/java/sevenUnitsGUI/Presenter.java +++ b/src/main/java/sevenUnitsGUI/Presenter.java @@ -72,30 +72,7 @@ public final class Presenter { private static final String DEFAULT_DIMENSIONS_FILEPATH = "/dimensionfile.txt"; /** The default place where exceptions are stored. */ private static final String DEFAULT_EXCEPTIONS_FILEPATH = "/metric_exceptions.txt"; - - private static final Path userConfigDir() { - if (System.getProperty("os.name").startsWith("Windows")) { - final String envFolder = System.getenv("LOCALAPPDATA"); - if (envFolder == null || "".equals(envFolder)) { - return Path.of(System.getenv("USERPROFILE"), "AppData", "Local"); - } else { - return Path.of(envFolder); - } - } else { - final String envFolder = System.getenv("XDG_CONFIG_HOME"); - if (envFolder == null || "".equals(envFolder)) { - return Path.of(System.getenv("HOME"), ".config"); - } else { - return Path.of(envFolder); - } - } - } - - /** Gets a Path from a pathname in the config file. */ - private static Path pathFromConfig(String pathname) { - return CONFIG_FILE.getParent().resolve(pathname); - } - + /** * Adds default units and dimensions to a database. * @@ -117,14 +94,28 @@ public final class Presenter { // nonlinear units - must be loaded manually database.addUnit("tempCelsius", Metric.CELSIUS); database.addUnit("tempFahrenheit", BritishImperial.FAHRENHEIT); - + // load initial dimensions database.addDimension("Length", Metric.Dimensions.LENGTH); database.addDimension("Mass", Metric.Dimensions.MASS); database.addDimension("Time", Metric.Dimensions.TIME); database.addDimension("Temperature", Metric.Dimensions.TEMPERATURE); } - + + private static String displayRuleToString( + Function numberDisplayRule) { + if (numberDisplayRule instanceof FixedDecimals) + return String.format("FIXED_DECIMALS %d", + ((FixedDecimals) numberDisplayRule).decimalPlaces()); + else if (numberDisplayRule instanceof FixedPrecision) + return String.format("FIXED_PRECISION %d", + ((FixedPrecision) numberDisplayRule).significantFigures()); + else if (numberDisplayRule instanceof UncertaintyBased) + return "UNCERTAINTY_BASED"; + else + return numberDisplayRule.toString(); + } + /** * @return text in About file * @since 2022-02-19 @@ -134,7 +125,7 @@ public final class Presenter { .map(Presenter::withoutComments).collect(Collectors.joining("\n")) .replaceAll("\\[VERSION\\]", ProgramInfo.VERSION.toString()); } - + /** * Gets the text of a resource file as a set of strings (each one is one line * of the text). @@ -145,7 +136,7 @@ public final class Presenter { */ private static final List getLinesFromResource(String filename) { final List lines = new ArrayList<>(); - + try (InputStream stream = inputStream(filename); Scanner scanner = new Scanner(stream)) { while (scanner.hasNextLine()) { @@ -155,10 +146,10 @@ public final class Presenter { throw new AssertionError( "Error occurred while loading file " + filename, e); } - + return lines; } - + /** * Gets an input stream for a resource file. * @@ -169,7 +160,38 @@ public final class Presenter { private static final InputStream inputStream(String filepath) { return Presenter.class.getResourceAsStream(filepath); } - + + private static Map.Entry parseSettingLine(String line) { + final int equalsIndex = line.indexOf('='); + if (equalsIndex == -1) + throw new IllegalStateException( + "Settings file is malformed at line: " + line); + + final String param = line.substring(0, equalsIndex); + final String value = line.substring(equalsIndex + 1); + + return Map.entry(param, value); + } + + /** Gets a Path from a pathname in the config file. */ + private static Path pathFromConfig(String pathname) { + return CONFIG_FILE.getParent().resolve(pathname); + } + + private static String searchRuleToString( + Function, Map> searchRule) { + if (PrefixSearchRule.NO_PREFIXES.equals(searchRule)) + return "NO_PREFIXES"; + else if (PrefixSearchRule.COMMON_PREFIXES.equals(searchRule)) + return "COMMON_PREFIXES"; + else if (PrefixSearchRule.ALL_METRIC_PREFIXES.equals(searchRule)) + return "ALL_METRIC_PREFIXES"; + else + return searchRule.toString(); + } + + // ====== SETTINGS ====== + /** * @return true iff a and b have any elements in common * @since 2022-04-19 @@ -181,7 +203,23 @@ public final class Presenter { } return false; } - + + private static final Path userConfigDir() { + if (System.getProperty("os.name").startsWith("Windows")) { + final String envFolder = System.getenv("LOCALAPPDATA"); + if (envFolder == null || "".equals(envFolder)) + return Path.of(System.getenv("USERPROFILE"), "AppData", "Local"); + else + return Path.of(envFolder); + } else { + final String envFolder = System.getenv("XDG_CONFIG_HOME"); + if (envFolder == null || "".equals(envFolder)) + return Path.of(System.getenv("HOME"), ".config"); + else + return Path.of(envFolder); + } + } + /** * @return {@code line} with any comments removed. * @since 2021-03-13 @@ -190,25 +228,23 @@ public final class Presenter { final int index = line.indexOf('#'); return index == -1 ? line : line.substring(0, index); } - - // ====== SETTINGS ====== - + /** * The view that this presenter communicates with */ private final View view; - + /** * The database that this presenter communicates with (effectively the model) */ final UnitDatabase database; - + /** * The rule used for parsing input numbers. Any number-string inputted into * this program will be parsed using this method. Not implemented yet. */ private Function numberParsingRule; - + /** * The rule used for displaying the results of unit conversions. The result * of unit conversions will be put into this function, and the resulting @@ -216,41 +252,41 @@ public final class Presenter { */ 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 = DefaultPrefixRepetitionRule.NO_RESTRICTION; - + /** * A rule that accepts a prefixless name-unit pair and returns a map mapping * names to prefixed versions of that unit (including the unit itself) that * should be searchable. */ private Function, Map> searchRule = PrefixSearchRule.NO_PREFIXES; - + /** * The set of units that is considered neither metric nor nonmetric for the * purposes of the metric-imperial one-way conversion. These units are * included in both From and To, even if One Way Conversion is enabled. */ private final Set metricExceptions; - + /** * If this is true, views that show units as a list will have metric units * removed from the From unit list and imperial/USC units removed from the To * unit list. */ private boolean oneWayConversionEnabled = false; - + /** * If this is false, duplicate units and prefixes will be removed from the * unit view in views that show units as a list to choose from. */ private boolean showDuplicates = false; - + /** * Creates a Presenter * @@ -261,14 +297,14 @@ public final class Presenter { this.view = view; this.database = new UnitDatabase(); addDefaults(this.database); - + // load units and prefixes try (final InputStream units = inputStream(DEFAULT_UNITS_FILEPATH)) { this.database.loadUnitsFromStream(units); } catch (final IOException e) { throw new AssertionError("Loading of unitsfile.txt failed.", e); } - + // load dimensions try (final InputStream dimensions = inputStream( DEFAULT_DIMENSIONS_FILEPATH)) { @@ -276,7 +312,7 @@ public final class Presenter { } catch (final IOException e) { throw new AssertionError("Loading of dimensionfile.txt failed.", e); } - + // load metric exceptions try { this.metricExceptions = new HashSet<>(); @@ -294,16 +330,16 @@ public final class Presenter { throw new AssertionError("Loading of metric_exceptions.txt failed.", e); } - + // set default settings temporarily if (Files.exists(CONFIG_FILE)) { this.loadSettings(CONFIG_FILE); } - + // 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", @@ -312,7 +348,7 @@ public final class Presenter { this.database.unitMapPrefixless(false).values().stream() .filter(isFullBase).count()); } - + /** * Applies a search rule to an entry in a name-unit map. * @@ -332,7 +368,7 @@ public final class Presenter { } else return Stream.of(e); } - + /** * Converts from the view's input expression to its output expression. * Displays an error message if any of the required fields are invalid. @@ -346,10 +382,10 @@ public final class Presenter { 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", @@ -361,54 +397,131 @@ public final class Presenter { "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; + + final Optional uc; + if (toExpression.contains(";")) { + final String[] toExpressions = toExpression.split(";"); + uc = this.convertExpressionToMultiUnit(fromExpression, + toExpressions); + } else { + uc = this.convertExpressionToExpression(fromExpression, + toExpression); + } + + uc.ifPresent(xcview::showExpressionConversionOutput); + } else + throw new UnsupportedOperationException( + "This function can only be called when the view is an ExpressionConversionView"); + } + + /** + * Converts a unit expression to another expression. + * + * If an error happened, it is shown to the view and Optional.empty() is + * returned. + * + * @since 2024-08-15 + */ + private Optional convertExpressionToExpression( + String fromExpression, String toExpression) { + // 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 Optional.empty(); + } + 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 Optional.empty(); + } + + // convert and show output + if (from.getUnit().canConvertTo(to)) { + final UncertainDouble uncertainValue; + + // uncertainty is meaningless for non-linear units, so we will have + // to erase uncertainty information for them + if (to instanceof LinearUnit) { + final var toLinear = (LinearUnit) to; + uncertainValue = from.convertTo(toLinear).getValue(); + } else { + final double value = from.asUnitValue().convertTo(to).getValue(); + uncertainValue = UncertainDouble.of(value, 0); } + + final UnitConversionRecord uc = UnitConversionRecord.valueOf( + fromExpression, toExpression, "", + this.numberDisplayRule.apply(uncertainValue)); + return Optional.of(uc); + } else { + this.view.showErrorMessage("Conversion Error", + "Cannot convert between \"" + fromExpression + "\" and \"" + + toExpression + "\"."); + return Optional.empty(); + } + + } + + /** + * Convert an expression to a MultiUnit. If an error happened, it is shown to + * the view and Optional.empty() is returned. + * + * @since 2024-08-15 + */ + private Optional convertExpressionToMultiUnit( + String fromExpression, String[] toExpressions) { + final LinearUnitValue from; + 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 Optional.empty(); + } + + final List toUnits = new ArrayList<>(toExpressions.length); + for (int i = 0; i < toExpressions.length; i++) { try { - to = this.database.getUnitFromExpression(toExpression); + final Unit toI = this.database + .getUnitFromExpression(toExpressions[i].trim()); + if (toI instanceof LinearUnit) { + toUnits.add((LinearUnit) toI); + } else { + this.view.showErrorMessage("Unit Type Error", + "Units separated by ';' must be linear; " + toI + + " is not."); + return Optional.empty(); + } } catch (final IllegalArgumentException | NoSuchElementException e) { this.view.showErrorMessage("Parse Error", "Could not recognize text in To entry: " + e.getMessage()); - return; - } - - // convert and show output - if (from.getUnit().canConvertTo(to)) { - final UncertainDouble uncertainValue; - - // uncertainty is meaningless for non-linear units, so we will have - // to erase uncertainty information for them - if (to instanceof LinearUnit) { - final var toLinear = (LinearUnit) to; - uncertainValue = from.convertTo(toLinear).getValue(); - } else { - final double value = from.asUnitValue().convertTo(to).getValue(); - uncertainValue = UncertainDouble.of(value, 0); - } - - final UnitConversionRecord uc = UnitConversionRecord.valueOf( - fromExpression, toExpression, "", - this.numberDisplayRule.apply(uncertainValue)); - xcview.showExpressionConversionOutput(uc); - } else { - this.view.showErrorMessage("Conversion Error", - "Cannot convert between \"" + fromExpression + "\" and \"" - + toExpression + "\"."); + return Optional.empty(); } - - } else - throw new UnsupportedOperationException( - "This function can only be called when the view is an ExpressionConversionView"); + } + + final List toValues; + try { + toValues = from.convertToMultiple(toUnits); + } catch (final IllegalArgumentException e) { + this.view.showErrorMessage("Unit Error", + "Invalid units separated by ';': " + e.getMessage()); + return Optional.empty(); + } + + final String toExpression = toValues.stream().map( + v -> this.numberDisplayRule.apply(v.getValue()) + " " + v.getUnit()) + .collect(Collectors.joining(" + ")); + return Optional.of( + UnitConversionRecord.valueOf(fromExpression, toExpression, "", "")); } - + /** * Converts from the view's input unit to its output unit. Displays an error * message if any of the required fields are invalid. @@ -422,11 +535,11 @@ public final class Presenter { public void convertUnits() { if (this.view instanceof UnitConversionView) { final UnitConversionView ucview = (UnitConversionView) this.view; - + final Optional fromUnitOptional = ucview.getFromSelection(); final Optional toUnitOptional = ucview.getToSelection(); final String inputValueString = ucview.getInputValue(); - + // extract values from optionals final String fromUnitString, toUnitString; if (fromUnitOptional.isPresent()) { @@ -443,11 +556,11 @@ public final class Presenter { "Please specify a To unit"); return; } - + // convert strings to data, checking if anything is invalid final Unit fromUnit, toUnit; final UncertainDouble uncertainValue; - + if (this.database.containsUnitName(fromUnitString)) { fromUnit = this.database.getUnit(fromUnitString); } else @@ -464,11 +577,11 @@ public final class Presenter { "Invalid value " + inputValueString); return; } - + if (!fromUnit.canConvertTo(toUnit)) throw this.viewError("Could not convert between %s and %s", fromUnit, toUnit); - + // convert - we will need to erase uncertainty for non-linear units, so // we need to treat linear and non-linear units differently final String outputValueString; @@ -478,18 +591,18 @@ public final class Presenter { final LinearUnitValue initialValue = LinearUnitValue.of(fromLinear, uncertainValue); final LinearUnitValue converted = initialValue.convertTo(toLinear); - + outputValueString = this.numberDisplayRule .apply(converted.getValue()); } else { final UnitValue initialValue = UnitValue.of(fromUnit, uncertainValue.value()); final UnitValue converted = initialValue.convertTo(toUnit); - + outputValueString = this.numberDisplayRule .apply(UncertainDouble.of(converted.getValue(), 0)); } - + ucview.showUnitConversionOutput( UnitConversionRecord.valueOf(fromUnitString, toUnitString, inputValueString, outputValueString)); @@ -497,7 +610,7 @@ public final class Presenter { throw new UnsupportedOperationException( "This function can only be called when the view is a UnitConversionView."); } - + /** * @return true iff duplicate units are shown in unit lists * @since 2022-03-30 @@ -505,7 +618,7 @@ public final class Presenter { public boolean duplicatesShown() { return this.showDuplicates; } - + /** * Gets a name for this dimension using the database * @@ -520,7 +633,7 @@ public final class Presenter { .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 @@ -529,7 +642,7 @@ public final class Presenter { public Function getNumberDisplayRule() { return this.numberDisplayRule; } - + /** * @return the rule that is used by this presenter to convert strings into * numbers @@ -539,7 +652,7 @@ public final class Presenter { private Function getNumberParsingRule() { return this.numberParsingRule; } - + /** * @return the rule that determines whether a set of prefixes is valid * @since 2022-04-19 @@ -547,7 +660,7 @@ public final class Presenter { public Predicate> getPrefixRepetitionRule() { return this.prefixRepetitionRule; } - + /** * @return the rule that determines which units are prefixed * @since 2022-07-08 @@ -555,7 +668,7 @@ public final class Presenter { public Function, Map> getSearchRule() { return this.searchRule; } - + /** * @return a search rule that shows all single prefixes * @since 2022-07-08 @@ -564,7 +677,7 @@ public final class Presenter { return PrefixSearchRule.getCoherentOnlyRule( new HashSet<>(this.database.prefixMap(true).values())); } - + /** * @return the view associated with this presenter * @since 2022-04-19 @@ -572,7 +685,7 @@ public final class Presenter { public View getView() { return this.view; } - + /** * @return whether or not the provided unit is semi-metric (i.e. an * exception) @@ -588,7 +701,18 @@ public final class Presenter { && this.metricExceptions.contains(symbol.orElseThrow()) || sharesAnyElements(this.metricExceptions, u.getOtherNames()); } - + + private void loadExceptionFile(Path exceptionFile) { + try (Stream lines = Files.lines(exceptionFile)) { + lines.map(Presenter::withoutComments) + .forEach(this.metricExceptions::add); + } catch (final IOException e) { + this.view.showErrorMessage("File Load Error", + "Error loading configured metric exception file \"" + + exceptionFile + "\": " + e.getLocalizedMessage()); + } + } + /** * Loads settings from the user's settings file and applies them to the * presenter. @@ -597,9 +721,10 @@ public final class Presenter { * @since 2021-12-15 */ void loadSettings(Path settingsFile) { - for (Map.Entry setting : settingsFromFile(settingsFile)) { + for (final Map.Entry setting : this + .settingsFromFile(settingsFile)) { final String value = setting.getValue(); - + switch (setting.getKey()) { // set manually to avoid the unnecessary saving of the non-manual // methods @@ -635,86 +760,12 @@ public final class Presenter { break; } } - + if (this.view.getPresenter() != null) { this.updateView(); } } - - private List> settingsFromFile(Path settingsFile) { - try (Stream lines = Files.lines(settingsFile)) { - return lines.map(Presenter::withoutComments) - .filter(line -> !line.isBlank()).map(Presenter::parseSettingLine) - .toList(); - } catch (final IOException e) { - this.view.showErrorMessage("Settings Loading Error", - "Error loading settings file. Using default settings."); - return null; - } - } - - private static Map.Entry parseSettingLine(String line) { - final int equalsIndex = line.indexOf('='); - if (equalsIndex == -1) - throw new IllegalStateException( - "Settings file is malformed at line: " + line); - - final String param = line.substring(0, equalsIndex); - final String value = line.substring(equalsIndex + 1); - - return Map.entry(param, value); - } - - private void setSearchRuleFromString(String ruleString) { - switch (ruleString) { - case "NO_PREFIXES": - this.searchRule = PrefixSearchRule.NO_PREFIXES; - break; - case "COMMON_PREFIXES": - this.searchRule = PrefixSearchRule.COMMON_PREFIXES; - break; - case "ALL_METRIC_PREFIXES": - this.searchRule = PrefixSearchRule.ALL_METRIC_PREFIXES; - break; - default: - System.err.printf( - "Warning: unrecognized value for search_prefix_rule: %s\n", - ruleString); - } - } - - private void setDisplayRuleFromString(String ruleString) { - String[] tokens = ruleString.split(" "); - switch (tokens[0]) { - case "FIXED_DECIMALS": - final int decimals = Integer.parseInt(tokens[1]); - this.numberDisplayRule = StandardDisplayRules.fixedDecimals(decimals); - break; - case "FIXED_PRECISION": - final int sigDigs = Integer.parseInt(tokens[1]); - this.numberDisplayRule = StandardDisplayRules.fixedPrecision(sigDigs); - break; - case "UNCERTAINTY_BASED": - this.numberDisplayRule = StandardDisplayRules.uncertaintyBased(); - break; - default: - this.numberDisplayRule = StandardDisplayRules - .getStandardRule(ruleString); - break; - } - } - - private void loadExceptionFile(Path exceptionFile) { - try (Stream lines = Files.lines(exceptionFile)) { - lines.map(Presenter::withoutComments) - .forEach(this.metricExceptions::add); - } catch (IOException e) { - this.view.showErrorMessage("File Load Error", - "Error loading configured metric exception file \"" - + exceptionFile + "\": " + e.getLocalizedMessage()); - } - } - + /** * @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 @@ -725,7 +776,7 @@ public final class Presenter { 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 @@ -739,10 +790,10 @@ public final class Presenter { final UnitConversionView ucview = (UnitConversionView) this.view; ucview.setDimensionNames(this.database.dimensionMap().keySet()); } - + this.updateView(); } - + void prefixSelected() { final Optional selectedPrefixName = this.view .getViewedPrefixName(); @@ -754,7 +805,7 @@ public final class Presenter { .ifPresent(prefix -> this.view.showPrefix(prefix.getNameSymbol(), String.valueOf(prefix.getMultiplier()))); } - + /** * Saves the presenter's current settings to the config file, creating it if * it doesn't exist. @@ -767,68 +818,35 @@ public final class Presenter { if (!Files.exists(configDir)) { try { Files.createDirectories(configDir); - } catch (IOException e) { + } catch (final IOException e) { return false; } } - + return this.writeSettings(CONFIG_FILE); } - - /** - * Saves the presenter's settings to the user settings file. - * - * @param settingsFile file settings should be saved to - * @since 2021-12-15 - */ - boolean writeSettings(Path settingsFile) { - try (BufferedWriter writer = Files.newBufferedWriter(settingsFile)) { - writer.write(String.format("number_display_rule=%s\n", - displayRuleToString(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)); - writer.write(String.format("search_prefix_rule=%s\n", - searchRuleToString(this.searchRule))); - return true; - } catch (final IOException e) { - e.printStackTrace(); - this.view.showErrorMessage("I/O Error", - "Error occurred while saving settings: " - + e.getLocalizedMessage()); - return false; + + private void setDisplayRuleFromString(String ruleString) { + final String[] tokens = ruleString.split(" "); + switch (tokens[0]) { + case "FIXED_DECIMALS": + final int decimals = Integer.parseInt(tokens[1]); + this.numberDisplayRule = StandardDisplayRules.fixedDecimals(decimals); + break; + case "FIXED_PRECISION": + final int sigDigs = Integer.parseInt(tokens[1]); + this.numberDisplayRule = StandardDisplayRules.fixedPrecision(sigDigs); + break; + case "UNCERTAINTY_BASED": + this.numberDisplayRule = StandardDisplayRules.uncertaintyBased(); + break; + default: + this.numberDisplayRule = StandardDisplayRules + .getStandardRule(ruleString); + break; } } - - private static String searchRuleToString( - Function, Map> searchRule) { - if (PrefixSearchRule.NO_PREFIXES.equals(searchRule)) { - return "NO_PREFIXES"; - } else if (PrefixSearchRule.COMMON_PREFIXES.equals(searchRule)) { - return "COMMON_PREFIXES"; - } else if (PrefixSearchRule.ALL_METRIC_PREFIXES.equals(searchRule)) { - return "ALL_METRIC_PREFIXES"; - } else - return searchRule.toString(); - } - - private static String displayRuleToString( - Function numberDisplayRule) { - if (numberDisplayRule instanceof FixedDecimals) { - return String.format("FIXED_DECIMALS %d", - ((FixedDecimals) numberDisplayRule).decimalPlaces()); - } else if (numberDisplayRule instanceof FixedPrecision) { - return String.format("FIXED_PRECISION %d", - ((FixedPrecision) numberDisplayRule).significantFigures()); - } else if (numberDisplayRule instanceof UncertaintyBased) { - return "UNCERTAINTY_BASED"; - } else - return numberDisplayRule.toString(); - } - + /** * @param numberDisplayRule the new rule that will be used by this presenter * to convert numbers into strings @@ -838,7 +856,7 @@ public final class Presenter { Function numberDisplayRule) { this.numberDisplayRule = numberDisplayRule; } - + /** * @param numberParsingRule the new rule that will be used by this presenter * to convert strings into numbers @@ -849,7 +867,7 @@ public final class Presenter { Function numberParsingRule) { this.numberParsingRule = numberParsingRule; } - + /** * @param oneWayConversionEnabled whether not one-way conversion should be * enabled @@ -860,7 +878,7 @@ public final class Presenter { this.oneWayConversionEnabled = oneWayConversionEnabled; this.updateView(); } - + /** * @param prefixRepetitionRule the rule that determines whether a set of * prefixes is valid @@ -871,7 +889,7 @@ public final class Presenter { this.prefixRepetitionRule = prefixRepetitionRule; this.database.setPrefixRepetitionRule(prefixRepetitionRule); } - + /** * @param searchRule A rule that accepts a prefixless name-unit pair and * returns a map mapping names to prefixed versions of that @@ -883,7 +901,25 @@ public final class Presenter { Function, Map> searchRule) { this.searchRule = searchRule; } - + + private void setSearchRuleFromString(String ruleString) { + switch (ruleString) { + case "NO_PREFIXES": + this.searchRule = PrefixSearchRule.NO_PREFIXES; + break; + case "COMMON_PREFIXES": + this.searchRule = PrefixSearchRule.COMMON_PREFIXES; + break; + case "ALL_METRIC_PREFIXES": + this.searchRule = PrefixSearchRule.ALL_METRIC_PREFIXES; + break; + default: + System.err.printf( + "Warning: unrecognized value for search_prefix_rule: %s\n", + ruleString); + } + } + /** * @param showDuplicateUnits whether or not duplicate units should be shown * @since 2022-03-30 @@ -892,7 +928,19 @@ public final class Presenter { this.showDuplicates = showDuplicateUnits; this.updateView(); } - + + private List> settingsFromFile(Path settingsFile) { + try (Stream lines = Files.lines(settingsFile)) { + return lines.map(Presenter::withoutComments) + .filter(line -> !line.isBlank()).map(Presenter::parseSettingLine) + .toList(); + } catch (final IOException e) { + this.view.showErrorMessage("Settings Loading Error", + "Error loading settings file. Using default settings."); + return null; + } + } + /** * Shows a unit in the unit viewer * @@ -908,7 +956,7 @@ public final class Presenter { final var unitType = UnitType.getType(u, this::isSemiMetric); this.view.showUnit(nameSymbol, definition, dimensionString, unitType); } - + /** * Runs whenever a unit name is selected in the unit viewer. Gets the * description of a unit and displays it. @@ -924,7 +972,7 @@ public final class Presenter { : null); selectedUnit.ifPresent(this::showUnit); } - + /** * Updates the view's From and To units, if it has some * @@ -934,19 +982,19 @@ public final class Presenter { if (this.view instanceof UnitConversionView) { final UnitConversionView ucview = (UnitConversionView) this.view; final var selectedDimensionName = ucview.getSelectedDimensionName(); - + // load units & prefixes into viewers this.view.setViewableUnitNames( this.database.unitMapPrefixless(this.showDuplicates).keySet()); this.view.setViewablePrefixNames( this.database.prefixMap(this.showDuplicates).keySet()); - + // get From and To units var fromUnits = this.database.unitMapPrefixless(this.showDuplicates) .entrySet().stream(); var toUnits = this.database.unitMapPrefixless(this.showDuplicates) .entrySet().stream(); - + // filter by dimension, if one is selected if (selectedDimensionName.isPresent()) { final var viewDimension = this.database @@ -956,7 +1004,7 @@ public final class Presenter { toUnits = toUnits.filter( u -> viewDimension.equals(u.getValue().getDimension())); } - + // filter by unit type, if desired if (this.oneWayConversionEnabled) { fromUnits = fromUnits.filter(u -> UnitType.getType(u.getValue(), @@ -964,7 +1012,7 @@ public final class Presenter { toUnits = toUnits.filter(u -> UnitType.getType(u.getValue(), this::isSemiMetric) != UnitType.NON_METRIC); } - + // set unit names ucview.setFromUnitNames(fromUnits.flatMap(this::applySearchRule) .map(Map.Entry::getKey).collect(Collectors.toSet())); @@ -972,7 +1020,7 @@ public final class Presenter { .map(Map.Entry::getKey).collect(Collectors.toSet())); } } - + /** * @param message message to add * @param args string formatting arguments for message @@ -984,4 +1032,32 @@ public final class Presenter { return new AssertionError("View Programming Error (from " + this.view + "): " + String.format(message, args)); } + + /** + * Saves the presenter's settings to the user settings file. + * + * @param settingsFile file settings should be saved to + * @since 2021-12-15 + */ + boolean writeSettings(Path settingsFile) { + try (BufferedWriter writer = Files.newBufferedWriter(settingsFile)) { + writer.write(String.format("number_display_rule=%s\n", + displayRuleToString(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)); + writer.write(String.format("search_prefix_rule=%s\n", + searchRuleToString(this.searchRule))); + return true; + } catch (final IOException e) { + e.printStackTrace(); + this.view.showErrorMessage("I/O Error", + "Error occurred while saving settings: " + + e.getLocalizedMessage()); + return false; + } + } } diff --git a/src/main/java/sevenUnitsGUI/TabbedView.java b/src/main/java/sevenUnitsGUI/TabbedView.java index 997acc3..a71de44 100644 --- a/src/main/java/sevenUnitsGUI/TabbedView.java +++ b/src/main/java/sevenUnitsGUI/TabbedView.java @@ -78,7 +78,7 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { */ private static final class JComboBoxItemSet extends AbstractSet { private final JComboBox comboBox; - + /** * @param comboBox combo box to get items from * @since 2022-02-19 @@ -86,17 +86,17 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { public JComboBoxItemSet(JComboBox comboBox) { this.comboBox = comboBox; } - + @Override public Iterator iterator() { return new Iterator<>() { private int index = 0; - + @Override public boolean hasNext() { return this.index < JComboBoxItemSet.this.size(); } - + @Override public E next() { if (this.hasNext()) @@ -107,14 +107,14 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { } }; } - + @Override public int size() { return this.comboBox.getItemCount(); } - + } - + /** * The standard types of rounding, corresponding to the options on the * TabbedView's settings panel. @@ -139,7 +139,7 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { */ UNCERTAINTY; } - + /** * Creates a TabbedView. * @@ -153,14 +153,14 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { @SuppressWarnings("unused") final View view = new TabbedView(); } - + /** The Presenter that handles this View */ final Presenter presenter; /** The frame that this view lives on */ final JFrame frame; /** The tabbed pane that contains all of the components */ final JTabbedPane masterPane; - + // DIMENSION-BASED CONVERTER /** The combo box that selects dimensions */ final JComboBox dimensionSelector; @@ -174,7 +174,7 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { final JButton convertUnitButton; /** The output area in the dimension-based converter */ final JTextArea unitOutput; - + // EXPRESSION-BASED CONVERTER /** The "From" entry in the conversion panel */ final JTextField fromEntry; @@ -184,7 +184,7 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { final JButton convertExpressionButton; /** The output area in the conversion panel */ final JTextArea expressionOutput; - + // UNIT AND PREFIX VIEWERS /** The searchable list of unit names in the unit viewer */ private final SearchBoxList unitNameList; @@ -194,11 +194,11 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { private final JTextArea unitTextBox; /** The text box for prefix data in the prefix viewer */ private final JTextArea prefixTextBox; - + // SETTINGS STUFF private StandardRoundingType roundingType; private int precision; - + /** * Creates the view and makes it visible to the user * @@ -215,161 +215,161 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { System.err.println("Failed to enable system look-and-feel."); e.printStackTrace(); } - + // initialize important components this.presenter = new Presenter(this); this.frame = new JFrame("7Units " + ProgramInfo.VERSION); this.frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); - + // master components (those that contain everything else within them) this.masterPane = new JTabbedPane(); this.frame.add(this.masterPane); - + // ============ UNIT CONVERSION TAB ============ final JPanel convertUnitPanel = new JPanel(); this.masterPane.addTab("Convert Units", convertUnitPanel); this.masterPane.setMnemonicAt(0, KeyEvent.VK_U); convertUnitPanel.setLayout(new BorderLayout()); - + { // panel for input part final JPanel inputPanel = new JPanel(); convertUnitPanel.add(inputPanel, BorderLayout.CENTER); inputPanel.setLayout(new GridLayout(1, 3)); inputPanel.setBorder(new EmptyBorder(6, 6, 3, 6)); - + this.fromSearch = new SearchBoxList<>(); inputPanel.add(this.fromSearch); - + final JPanel inBetweenPanel = new JPanel(); inputPanel.add(inBetweenPanel); inBetweenPanel.setLayout(new BorderLayout()); - + this.dimensionSelector = new JComboBox<>(); inBetweenPanel.add(this.dimensionSelector, BorderLayout.PAGE_START); this.dimensionSelector .addItemListener(e -> this.presenter.updateView()); - + final JLabel arrowLabel = new JLabel("-->"); inBetweenPanel.add(arrowLabel, BorderLayout.CENTER); arrowLabel.setHorizontalAlignment(SwingConstants.CENTER); - + this.toSearch = new SearchBoxList<>(); inputPanel.add(this.toSearch); } - + { // panel for submit and output, and also value entry final JPanel outputPanel = new JPanel(); convertUnitPanel.add(outputPanel, BorderLayout.PAGE_END); outputPanel.setLayout(new BorderLayout()); outputPanel.setBorder(new EmptyBorder(3, 6, 6, 6)); - + final JLabel valuePrompt = new JLabel("Value to convert: "); outputPanel.add(valuePrompt, BorderLayout.LINE_START); - + this.valueInput = new JTextField(); outputPanel.add(this.valueInput, BorderLayout.CENTER); - + // conversion button this.convertUnitButton = new JButton("Convert"); outputPanel.add(this.convertUnitButton, BorderLayout.LINE_END); this.convertUnitButton .addActionListener(e -> this.presenter.convertUnits()); this.convertUnitButton.setMnemonic(KeyEvent.VK_ENTER); - + // conversion output this.unitOutput = new JTextArea(2, 32); outputPanel.add(this.unitOutput, BorderLayout.PAGE_END); this.unitOutput.setEditable(false); } - + // ============ EXPRESSION CONVERSION TAB ============ final JPanel convertExpressionPanel = new JPanel(); this.masterPane.addTab("Convert Unit Expressions", convertExpressionPanel); this.masterPane.setMnemonicAt(1, KeyEvent.VK_E); convertExpressionPanel.setLayout(new GridLayout(4, 1)); - + // from and to expressions this.fromEntry = new JTextField(); convertExpressionPanel.add(this.fromEntry); this.fromEntry.setBorder(BorderFactory.createTitledBorder("From")); - + this.toEntry = new JTextField(); convertExpressionPanel.add(this.toEntry); this.toEntry.setBorder(BorderFactory.createTitledBorder("To")); - + // button to convert this.convertExpressionButton = new JButton("Convert"); convertExpressionPanel.add(this.convertExpressionButton); - + this.convertExpressionButton .addActionListener(e -> this.presenter.convertExpressions()); this.convertExpressionButton.setMnemonic(KeyEvent.VK_ENTER); - + // output of conversion this.expressionOutput = new JTextArea(2, 32); convertExpressionPanel.add(this.expressionOutput); this.expressionOutput .setBorder(BorderFactory.createTitledBorder("Output")); this.expressionOutput.setEditable(false); - + // =========== UNIT VIEWER =========== final JPanel unitLookupPanel = new JPanel(); this.masterPane.addTab("Unit Viewer", unitLookupPanel); this.masterPane.setMnemonicAt(2, KeyEvent.VK_V); unitLookupPanel.setLayout(new GridLayout()); - + this.unitNameList = new SearchBoxList<>(); unitLookupPanel.add(this.unitNameList); this.unitNameList.getSearchList() .addListSelectionListener(e -> this.presenter.unitNameSelected()); - + // the text box for unit's toString this.unitTextBox = new JTextArea(); unitLookupPanel.add(this.unitTextBox); this.unitTextBox.setEditable(false); this.unitTextBox.setLineWrap(true); - + // ============ PREFIX VIEWER ============= final JPanel prefixLookupPanel = new JPanel(); this.masterPane.addTab("Prefix Viewer", prefixLookupPanel); this.masterPane.setMnemonicAt(3, KeyEvent.VK_P); prefixLookupPanel.setLayout(new GridLayout(1, 2)); - + this.prefixNameList = new SearchBoxList<>(); prefixLookupPanel.add(this.prefixNameList); this.prefixNameList.getSearchList() .addListSelectionListener(e -> this.presenter.prefixSelected()); - + // the text box for prefix's toString this.prefixTextBox = new JTextArea(); prefixLookupPanel.add(this.prefixTextBox); this.prefixTextBox.setEditable(false); this.prefixTextBox.setLineWrap(true); - + // ============ INFO PANEL ============ - + final JPanel infoPanel = new JPanel(); this.masterPane.addTab("\uD83D\uDEC8", // info (i) character new JScrollPane(infoPanel)); - + final JTextArea infoTextArea = new JTextArea(); infoTextArea.setEditable(false); infoTextArea.setOpaque(false); infoPanel.add(infoTextArea); infoTextArea.setText(Presenter.getAboutText()); - + // ============ SETTINGS PANEL ============ this.masterPane.addTab("\u2699", new JScrollPane(this.createSettingsPanel())); this.masterPane.setMnemonicAt(5, KeyEvent.VK_S); - + // ============ FINALIZE CREATION OF VIEW ============ this.presenter.postViewInitialize(); this.frame.pack(); this.frame.setVisible(true); } - + /** * Creates and returns the settings panel (in its own function to make this * code more organized, as this function is massive!) @@ -378,28 +378,28 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { */ private JPanel createSettingsPanel() { final JPanel settingsPanel = new JPanel(); - + settingsPanel .setLayout(new BoxLayout(settingsPanel, BoxLayout.PAGE_AXIS)); - + // ============ ROUNDING SETTINGS ============ { final JPanel roundingPanel = new JPanel(); settingsPanel.add(roundingPanel); roundingPanel.setBorder(new TitledBorder("Rounding Settings")); roundingPanel.setLayout(new GridBagLayout()); - + // rounding rule selection final ButtonGroup roundingRuleButtons = new ButtonGroup(); this.roundingType = this.getPresenterRoundingType() .orElseThrow(() -> new AssertionError( "Presenter loaded non-standard rounding rule")); this.precision = this.getPresenterPrecision().orElse(6); - + final JLabel roundingRuleLabel = new JLabel("Rounding Rule:"); roundingPanel.add(roundingRuleLabel, new GridBagBuilder(0, 0) .setAnchor(GridBagConstraints.LINE_START).build()); - + // sigDigSlider needs to be first so that the rounding-type buttons can // show and hide it final JLabel sliderLabel = new JLabel("Precision:"); @@ -407,26 +407,26 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { 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"); @@ -442,7 +442,7 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { 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"); @@ -458,7 +458,7 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { roundingRuleButtons.add(fixedDecimals); roundingPanel.add(fixedDecimals, new GridBagBuilder(0, 2) .setAnchor(GridBagConstraints.LINE_START).build()); - + // scientific rounding final JRadioButton relativePrecision = new JRadioButton( "Uncertainty-Based Rounding"); @@ -475,7 +475,7 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { roundingPanel.add(relativePrecision, new GridBagBuilder(0, 3) .setAnchor(GridBagConstraints.LINE_START).build()); } - + // ============ PREFIX REPETITION SETTINGS ============ { final JPanel prefixRepetitionPanel = new JPanel(); @@ -483,14 +483,14 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { prefixRepetitionPanel .setBorder(new TitledBorder("Prefix Repetition Settings")); prefixRepetitionPanel.setLayout(new GridBagLayout()); - + final var prefixRule = this.getPresenterPrefixRule() .orElseThrow(() -> new AssertionError( "Presenter loaded non-standard prefix rule")); - + // prefix rules final ButtonGroup prefixRuleButtons = new ButtonGroup(); - + final JRadioButton noRepetition = new JRadioButton("No Repetition"); if (prefixRule == DefaultPrefixRepetitionRule.NO_REPETITION) { noRepetition.setSelected(true); @@ -503,7 +503,7 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { prefixRuleButtons.add(noRepetition); prefixRepetitionPanel.add(noRepetition, new GridBagBuilder(0, 0) .setAnchor(GridBagConstraints.LINE_START).build()); - + final JRadioButton noRestriction = new JRadioButton("No Restriction"); if (prefixRule == DefaultPrefixRepetitionRule.NO_RESTRICTION) { noRestriction.setSelected(true); @@ -516,7 +516,7 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { prefixRuleButtons.add(noRestriction); prefixRepetitionPanel.add(noRestriction, new GridBagBuilder(0, 1) .setAnchor(GridBagConstraints.LINE_START).build()); - + final JRadioButton customRepetition = new JRadioButton( "Complex Repetition"); if (prefixRule == DefaultPrefixRepetitionRule.COMPLEX_REPETITION) { @@ -531,19 +531,19 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { prefixRepetitionPanel.add(customRepetition, new GridBagBuilder(0, 2) .setAnchor(GridBagConstraints.LINE_START).build()); } - + // ============ SEARCH SETTINGS ============ { final JPanel searchingPanel = new JPanel(); settingsPanel.add(searchingPanel); searchingPanel.setBorder(new TitledBorder("Search Settings")); searchingPanel.setLayout(new GridBagLayout()); - + // searching rules final ButtonGroup searchRuleButtons = new ButtonGroup(); - + final var searchRule = this.presenter.getSearchRule(); - + final JRadioButton noPrefixes = new JRadioButton( "Never Include Prefixed Units"); noPrefixes.addActionListener(e -> { @@ -554,7 +554,7 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { searchRuleButtons.add(noPrefixes); searchingPanel.add(noPrefixes, new GridBagBuilder(0, 0) .setAnchor(GridBagConstraints.LINE_START).build()); - + final JRadioButton commonPrefixes = new JRadioButton( "Include Common Prefixes"); commonPrefixes.addActionListener(e -> { @@ -565,7 +565,7 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { searchRuleButtons.add(commonPrefixes); searchingPanel.add(commonPrefixes, new GridBagBuilder(0, 1) .setAnchor(GridBagConstraints.LINE_START).build()); - + final JRadioButton alwaysInclude = new JRadioButton( "Include All Single Prefixes"); alwaysInclude.addActionListener(e -> { @@ -577,7 +577,7 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { searchRuleButtons.add(alwaysInclude); searchingPanel.add(alwaysInclude, new GridBagBuilder(0, 3) .setAnchor(GridBagConstraints.LINE_START).build()); - + if (PrefixSearchRule.NO_PREFIXES.equals(searchRule)) { noPrefixes.setSelected(true); } else if (PrefixSearchRule.COMMON_PREFIXES.equals(searchRule)) { @@ -589,13 +589,13 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { this.presenter.saveSettings(); } } - + // ============ OTHER SETTINGS ============ { final JPanel miscPanel = new JPanel(); settingsPanel.add(miscPanel); miscPanel.setLayout(new GridBagLayout()); - + final JCheckBox oneWay = new JCheckBox("Convert One Way Only"); oneWay.setSelected(this.presenter.oneWayConversionEnabled()); oneWay.addItemListener(e -> { @@ -605,7 +605,7 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { }); 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()); @@ -616,49 +616,49 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { }); miscPanel.add(showAllVariations, new GridBagBuilder(0, 1) .setAnchor(GridBagConstraints.LINE_START).build()); - + final JButton unitFileButton = new JButton("Manage Unit Data Files"); unitFileButton.setEnabled(false); miscPanel.add(unitFileButton, new GridBagBuilder(0, 2) .setAnchor(GridBagConstraints.LINE_START).build()); } - + return settingsPanel; } - + @Override public Set getDimensionNames() { return Collections .unmodifiableSet(new JComboBoxItemSet<>(this.dimensionSelector)); } - + @Override public String getFromExpression() { return this.fromEntry.getText(); } - + @Override public Optional getFromSelection() { return this.fromSearch.getSelectedValue(); } - + @Override public Set getFromUnitNames() { // this should work because the only way I can mutate the item list is // with setFromUnits which only accepts a Set return new HashSet<>(this.fromSearch.getItems()); } - + @Override public String getInputValue() { return this.valueInput.getText(); } - + @Override public Presenter getPresenter() { return this.presenter; } - + /** * @return the precision of the presenter's rounding rule, if that is * meaningful @@ -678,7 +678,7 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { else return OptionalInt.empty(); } - + /** * @return presenter's prefix repetition rule * @since v0.4.0 @@ -690,7 +690,7 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { ? Optional.of((DefaultPrefixRepetitionRule) prefixRule) : Optional.empty(); } - + /** * Determines which rounding type the presenter is currently using, if any. * @@ -709,41 +709,41 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { else return Optional.empty(); } - + @Override public Optional getSelectedDimensionName() { final String selectedItem = (String) this.dimensionSelector .getSelectedItem(); return Optional.ofNullable(selectedItem); } - + @Override public String getToExpression() { return this.toEntry.getText(); } - + @Override public Optional getToSelection() { return this.toSearch.getSelectedValue(); } - + @Override public Set getToUnitNames() { // this should work because the only way I can mutate the item list is // with setToUnits which only accepts a Set return new HashSet<>(this.toSearch.getItems()); } - + @Override public Optional getViewedPrefixName() { return this.prefixNameList.getSelectedValue(); } - + @Override public Optional getViewedUnitName() { return this.unitNameList.getSelectedValue(); } - + @Override public void setDimensionNames(Set dimensionNames) { this.dimensionSelector.removeAllItems(); @@ -751,45 +751,44 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { this.dimensionSelector.addItem(d); } } - + @Override public void setFromUnitNames(Set units) { this.fromSearch.setItems(units); } - + @Override public void setToUnitNames(Set units) { this.toSearch.setItems(units); } - + @Override public void setViewablePrefixNames(Set prefixNames) { this.prefixNameList.setItems(prefixNames); } - + @Override public void setViewableUnitNames(Set unitNames) { this.unitNameList.setItems(unitNames); } - + @Override public void showErrorMessage(String title, String message) { JOptionPane.showMessageDialog(this.frame, message, title, JOptionPane.ERROR_MESSAGE); } - + @Override public void showExpressionConversionOutput(UnitConversionRecord uc) { - this.expressionOutput.setText(String.format("%s = %s %s", uc.fromName(), - uc.outputValueString(), uc.toName())); + this.expressionOutput.setText(uc.toString()); } - + @Override public void showPrefix(NameSymbol name, String multiplierString) { this.prefixTextBox.setText( String.format("%s%nMultiplier: %s", name, multiplierString)); } - + @Override public void showUnit(NameSymbol name, String definition, String dimensionName, UnitType type) { @@ -797,12 +796,12 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { String.format("%s%nDefinition: %s%nDimension: %s%nType: %s", name, definition, dimensionName, type)); } - + @Override public void showUnitConversionOutput(UnitConversionRecord uc) { this.unitOutput.setText(uc.toString()); } - + /** * Sets the presenter's rounding rule to the one specified by the current * settings diff --git a/src/main/java/sevenUnitsGUI/UnitConversionRecord.java b/src/main/java/sevenUnitsGUI/UnitConversionRecord.java index 43a62e6..fa64ee9 100644 --- a/src/main/java/sevenUnitsGUI/UnitConversionRecord.java +++ b/src/main/java/sevenUnitsGUI/UnitConversionRecord.java @@ -44,7 +44,7 @@ public final class UnitConversionRecord { input.getValue().toString(false, RoundingMode.HALF_EVEN), output.getValue().toString(false, RoundingMode.HALF_EVEN)); } - + /** * Gets a {@code UnitConversionRecord} from two unit values * @@ -60,7 +60,7 @@ public final class UnitConversionRecord { output.getUnit().getName(), String.valueOf(input.getValue()), String.valueOf(output.getValue())); } - + /** * Gets a {@code UnitConversionRecord} * @@ -78,7 +78,7 @@ public final class UnitConversionRecord { return new UnitConversionRecord(fromName, toName, inputValueString, outputValueString); } - + /** * The name of the unit or expression that was converted from */ @@ -87,7 +87,7 @@ public final class UnitConversionRecord { * The name of the unit or expression that was converted to */ private final String toName; - + /** * A string representing the input value. It doesn't need to be the same as * the input value's string representation; it could be rounded, for example. @@ -98,7 +98,7 @@ public final class UnitConversionRecord { * the input value's string representation; it could be rounded, for example. */ private final String outputValueString; - + /** * @param fromName name of unit or expression that was converted * from @@ -114,7 +114,7 @@ public final class UnitConversionRecord { this.inputValueString = inputValueString; this.outputValueString = outputValueString; } - + @Override public boolean equals(Object obj) { if (this == obj) @@ -144,7 +144,7 @@ public final class UnitConversionRecord { return false; return true; } - + /** * @return name of unit or expression that was converted from * @since v0.4.0 @@ -153,7 +153,7 @@ public final class UnitConversionRecord { public String fromName() { return this.fromName; } - + @Override public int hashCode() { final int prime = 31; @@ -168,7 +168,7 @@ public final class UnitConversionRecord { + (this.toName == null ? 0 : this.toName.hashCode()); return result; } - + /** * @return string representing input value * @since v0.4.0 @@ -177,7 +177,7 @@ public final class UnitConversionRecord { public String inputValueString() { return this.inputValueString; } - + /** * @return string representing output value * @since v0.4.0 @@ -186,7 +186,7 @@ public final class UnitConversionRecord { public String outputValueString() { return this.outputValueString; } - + /** * @return name of unit or expression that was converted to * @since v0.4.0 @@ -195,7 +195,7 @@ public final class UnitConversionRecord { public String toName() { return this.toName; } - + @Override public String toString() { final String inputString = this.inputValueString.isBlank() ? this.fromName -- cgit v1.2.3