From 4f754b0bc136b972607aa1b39f2358bd98dc4e1c Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Wed, 15 Dec 2021 17:52:43 -0500 Subject: Designed the API for a new GUI Currently "supports" unit conversion, expression conversion and settings loading --- .../newGUI/ExpressionConversionView.java | 48 +++++++++++ src/main/java/sevenUnits/newGUI/Presenter.java | 97 ++++++++++++++++++++++ .../java/sevenUnits/newGUI/UnitConversionView.java | 97 ++++++++++++++++++++++ src/main/java/sevenUnits/newGUI/UserSettings.java | 27 ++++++ src/main/java/sevenUnits/newGUI/View.java | 51 ++++++++++++ src/main/java/sevenUnits/newGUI/package-info.java | 23 +++++ .../java/sevenUnits/utils/NamedObjectProduct.java | 46 ++++++++++ src/main/java/sevenUnits/utils/ObjectProduct.java | 15 +++- 8 files changed, 402 insertions(+), 2 deletions(-) create mode 100644 src/main/java/sevenUnits/newGUI/ExpressionConversionView.java create mode 100644 src/main/java/sevenUnits/newGUI/Presenter.java create mode 100644 src/main/java/sevenUnits/newGUI/UnitConversionView.java create mode 100644 src/main/java/sevenUnits/newGUI/UserSettings.java create mode 100644 src/main/java/sevenUnits/newGUI/View.java create mode 100644 src/main/java/sevenUnits/newGUI/package-info.java create mode 100644 src/main/java/sevenUnits/utils/NamedObjectProduct.java diff --git a/src/main/java/sevenUnits/newGUI/ExpressionConversionView.java b/src/main/java/sevenUnits/newGUI/ExpressionConversionView.java new file mode 100644 index 0000000..0f00090 --- /dev/null +++ b/src/main/java/sevenUnits/newGUI/ExpressionConversionView.java @@ -0,0 +1,48 @@ +/** + * 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.newGUI; + +/** + * A View that can convert unit expressions + * + * @author Adrien Hopkins + * @since 2021-12-15 + */ +public interface ExpressionConversionView extends View { + /** + * @return unit expression to convert from + * @since 2021-12-15 + */ + String getFromExpression(); + + /** + * @return unit expression to convert to + * @since 2021-12-15 + */ + String getToExpression(); + + /** + * Shows the output of an expression conversion to the user. + * + * @param fromExpression expression converted from + * @param toExpression expression converted to + * @param value conversion factor between two expressions + * @since 2021-12-15 + */ + void showExpressionConversionOutput(String fromExpression, + String toExpression, double value); +} diff --git a/src/main/java/sevenUnits/newGUI/Presenter.java b/src/main/java/sevenUnits/newGUI/Presenter.java new file mode 100644 index 0000000..6f7d34a --- /dev/null +++ b/src/main/java/sevenUnits/newGUI/Presenter.java @@ -0,0 +1,97 @@ +/** + * 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.newGUI; + +/** + * An object that handles interactions between the view and the backend code + * + * @author Adrien Hopkins + * @since 2021-12-15 + */ +public final class Presenter { + /** + * The view that this presenter communicates with + */ + private final View view; + + /** + * Creates a Presenter + * + * @param view the view that this presenter communicates with + * @since 2021-12-15 + */ + public Presenter(View view) { + this.view = view; + } + + /** + * Sets the dimension of the view's From and To units. + * + * @throws UnsupportedOperationException if the view does not support + * unit-based conversion (does not + * implement + * {@link UnitConversionView}) + * @since 2021-12-15 + */ + public void applyDimensionFilter() {} + + /** + * Gets settings from the view and applies them to both view and presenter. + * + * @since 2021-12-15 + */ + public void applySettings() {} + + /** + * Converts from the view's input expression to its output expression. + * Displays an error message if any of the required fields are invalid. + * + * @throws UnsupportedOperationException if the view does not support + * expression-based conversion (does + * not implement + * {@link ExpressionConversionView}) + * @since 2021-12-15 + */ + public void convertExpressions() {} + + /** + * Converts from the view's input unit to its output unit. Displays an error + * message if any of the required fields are invalid. + * + * @throws UnsupportedOperationException if the view does not support + * unit-based conversion (does not + * implement + * {@link UnitConversionView}) + * @since 2021-12-15 + */ + public void convertUnits() {} + + /** + * Loads settings from the user's settings file and applies them to the view. + * + * @since 2021-12-15 + */ + public void loadSettings() {} + + /** + * Gets user settings from the view then saves them to the user's settings + * file. + * + * @since 2021-12-15 + */ + public void saveSettings() {} +} diff --git a/src/main/java/sevenUnits/newGUI/UnitConversionView.java b/src/main/java/sevenUnits/newGUI/UnitConversionView.java new file mode 100644 index 0000000..dafd461 --- /dev/null +++ b/src/main/java/sevenUnits/newGUI/UnitConversionView.java @@ -0,0 +1,97 @@ +/** + * 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.newGUI; + +import java.util.List; +import java.util.Optional; +import java.util.OptionalDouble; + +import sevenUnits.unit.BaseDimension; +import sevenUnits.unit.Unit; +import sevenUnits.unit.UnitValue; +import sevenUnits.utils.NamedObjectProduct; +import sevenUnits.utils.ObjectProduct; + +/** + * A View that supports single unit-based conversion + * + * @author Adrien Hopkins + * @since 2021-12-15 + */ +public interface UnitConversionView extends View { + /** + * @return unit to convert from + * @since 2021-12-15 + */ + Optional getFromSelection(); + + /** + * @return value to convert between the units + * @since 2021-12-15 + */ + OptionalDouble getInputValue(); + + /** + * @return selected dimension + * @since 2021-12-15 + */ + Optional> getSelectedDimension(); + + /** + * @return unit to convert to + * @since 2021-12-15 + */ + Optional getToSelection(); + + /** + * Sets the available dimensions for filtering. + * + * @param dimensions dimensions to use + * @since 2021-12-15 + */ + void setDimensions(List> dimensions); + + /** + * Sets the available units to convert from. {@link #getFromSelection} is not + * required to use one of these units; this method is to be used for views + * that allow the user to select units from a list. + * + * @param units units to convert from + * @since 2021-12-15 + */ + void setFromUnits(List units); + + /** + * Sets the available units to convert to. {@link #getToSelection} is not + * required to use one of these units; this method is to be used for views + * that allow the user to select units from a list. + * + * @param units units to convert to + * @since 2021-12-15 + */ + void setToUnits(List units); + + /** + * Shows the output of a unit conversion. + * + * @param input unit value before conversion, as obtained from the view + * @param output unit value after conversion + * @throws NullPointerException if either argument is null + * @since 2021-12-15 + */ + void showUnitConversionOutput(UnitValue input, UnitValue output); +} diff --git a/src/main/java/sevenUnits/newGUI/UserSettings.java b/src/main/java/sevenUnits/newGUI/UserSettings.java new file mode 100644 index 0000000..207ef11 --- /dev/null +++ b/src/main/java/sevenUnits/newGUI/UserSettings.java @@ -0,0 +1,27 @@ +/** + * 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.newGUI; + +/** + * An object containing all of the user settings for a 7Units session. + * + * @author Adrien Hopkins + * @since 2021-12-15 + */ +public final class UserSettings { + +} diff --git a/src/main/java/sevenUnits/newGUI/View.java b/src/main/java/sevenUnits/newGUI/View.java new file mode 100644 index 0000000..8758a41 --- /dev/null +++ b/src/main/java/sevenUnits/newGUI/View.java @@ -0,0 +1,51 @@ +/** + * 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.newGUI; + +/** + * An object that controls user interaction with 7Units + * + * @author Adrien Hopkins + * @since 2021-12-15 + */ +public interface View { + /** + * @return settings user has set in the view + * @since 2021-12-15 + */ + UserSettings getUserSettings(); + + /** + * Sets the user settings on the settings page, if this view has one. Does + * not update settings internally; this is intended for loading settings from + * a file. + * + * @param settings settings to set + * @since 2021-12-15 + */ + void setUserSettings(UserSettings settings); + + /** + * Shows an error message. + * + * @param title title of error message; on any view that uses an error + * dialog, this should be the title of the error dialog. + * @param message error message + * @since 2021-12-15 + */ + void showErrorMessage(String title, String message); +} diff --git a/src/main/java/sevenUnits/newGUI/package-info.java b/src/main/java/sevenUnits/newGUI/package-info.java new file mode 100644 index 0000000..b58dccc --- /dev/null +++ b/src/main/java/sevenUnits/newGUI/package-info.java @@ -0,0 +1,23 @@ +/** + * 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 . + */ +/** + * The MVP GUI of SevenUnits + * + * @author Adrien Hopkins + * @since 2021-12-15 + */ +package sevenUnits.newGUI; \ No newline at end of file diff --git a/src/main/java/sevenUnits/utils/NamedObjectProduct.java b/src/main/java/sevenUnits/utils/NamedObjectProduct.java new file mode 100644 index 0000000..514f0b1 --- /dev/null +++ b/src/main/java/sevenUnits/utils/NamedObjectProduct.java @@ -0,0 +1,46 @@ +/** + * 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; + +import sevenUnits.unit.NameSymbol; +import sevenUnits.unit.Nameable; + +/** + * 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; + } + +} diff --git a/src/main/java/sevenUnits/utils/ObjectProduct.java b/src/main/java/sevenUnits/utils/ObjectProduct.java index 5b1b739..d4f88b9 100644 --- a/src/main/java/sevenUnits/utils/ObjectProduct.java +++ b/src/main/java/sevenUnits/utils/ObjectProduct.java @@ -26,6 +26,8 @@ import java.util.Objects; import java.util.Set; import java.util.function.Function; +import sevenUnits.unit.NameSymbol; + /** * An immutable product of multiple objects of a type, such as base units. The * objects can be multiplied and exponentiated. @@ -33,7 +35,7 @@ import java.util.function.Function; * @author Adrien Hopkins * @since 2019-10-16 */ -public final class ObjectProduct { +public class ObjectProduct { /** * Returns an empty ObjectProduct of a certain type * @@ -88,7 +90,7 @@ public final class ObjectProduct { * @param exponents objects that make up this product * @since 2019-10-16 */ - private ObjectProduct(final Map exponents) { + ObjectProduct(final Map exponents) { this.exponents = Collections.unmodifiableMap( ConditionalExistenceCollections.conditionalExistenceMap(exponents, e -> !Integer.valueOf(0).equals(e.getValue()))); @@ -280,4 +282,13 @@ public final class ObjectProduct { return positiveString + negativeString; } + + /** + * @return named version of this {@code ObjectProduct}, using data from + * {@code nameSymbol} + * @since 2021-12-15 + */ + public NamedObjectProduct withName(NameSymbol nameSymbol) { + return new NamedObjectProduct<>(this.exponents, nameSymbol); + } } -- cgit v1.2.3 From da3a5098602f8177f6d5dac4a322f70d6fdf9126 Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Fri, 24 Dec 2021 16:03:26 -0500 Subject: Did some API design for user settings, and moved GUI to a new package --- src/main/java/sevenUnits/ProgramInfo.java | 6 +- .../newGUI/ExpressionConversionView.java | 48 ------- src/main/java/sevenUnits/newGUI/Presenter.java | 97 -------------- .../java/sevenUnits/newGUI/UnitConversionView.java | 97 -------------- src/main/java/sevenUnits/newGUI/UserSettings.java | 27 ---- src/main/java/sevenUnits/newGUI/View.java | 51 ------- src/main/java/sevenUnits/newGUI/package-info.java | 23 ---- .../sevenUnitsGUI/ExpressionConversionView.java | 48 +++++++ src/main/java/sevenUnitsGUI/Presenter.java | 146 +++++++++++++++++++++ .../java/sevenUnitsGUI/StandardDisplayRules.java | 74 +++++++++++ .../java/sevenUnitsGUI/UnitConversionView.java | 94 +++++++++++++ src/main/java/sevenUnitsGUI/View.java | 35 +++++ src/main/java/sevenUnitsGUI/package-info.java | 23 ++++ 13 files changed, 425 insertions(+), 344 deletions(-) delete mode 100644 src/main/java/sevenUnits/newGUI/ExpressionConversionView.java delete mode 100644 src/main/java/sevenUnits/newGUI/Presenter.java delete mode 100644 src/main/java/sevenUnits/newGUI/UnitConversionView.java delete mode 100644 src/main/java/sevenUnits/newGUI/UserSettings.java delete mode 100644 src/main/java/sevenUnits/newGUI/View.java delete mode 100644 src/main/java/sevenUnits/newGUI/package-info.java create mode 100644 src/main/java/sevenUnitsGUI/ExpressionConversionView.java create mode 100644 src/main/java/sevenUnitsGUI/Presenter.java create mode 100644 src/main/java/sevenUnitsGUI/StandardDisplayRules.java create mode 100644 src/main/java/sevenUnitsGUI/UnitConversionView.java create mode 100644 src/main/java/sevenUnitsGUI/View.java create mode 100644 src/main/java/sevenUnitsGUI/package-info.java diff --git a/src/main/java/sevenUnits/ProgramInfo.java b/src/main/java/sevenUnits/ProgramInfo.java index 31e43c7..ba6bc7a 100644 --- a/src/main/java/sevenUnits/ProgramInfo.java +++ b/src/main/java/sevenUnits/ProgramInfo.java @@ -26,6 +26,10 @@ public final class ProgramInfo { public static final String VERSION = "0.3.2"; - private ProgramInfo() {} + private ProgramInfo() { + // this class is only for static variables, you shouldn't be able to + // construct an instance + throw new AssertionError(); + } } diff --git a/src/main/java/sevenUnits/newGUI/ExpressionConversionView.java b/src/main/java/sevenUnits/newGUI/ExpressionConversionView.java deleted file mode 100644 index 0f00090..0000000 --- a/src/main/java/sevenUnits/newGUI/ExpressionConversionView.java +++ /dev/null @@ -1,48 +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.newGUI; - -/** - * A View that can convert unit expressions - * - * @author Adrien Hopkins - * @since 2021-12-15 - */ -public interface ExpressionConversionView extends View { - /** - * @return unit expression to convert from - * @since 2021-12-15 - */ - String getFromExpression(); - - /** - * @return unit expression to convert to - * @since 2021-12-15 - */ - String getToExpression(); - - /** - * Shows the output of an expression conversion to the user. - * - * @param fromExpression expression converted from - * @param toExpression expression converted to - * @param value conversion factor between two expressions - * @since 2021-12-15 - */ - void showExpressionConversionOutput(String fromExpression, - String toExpression, double value); -} diff --git a/src/main/java/sevenUnits/newGUI/Presenter.java b/src/main/java/sevenUnits/newGUI/Presenter.java deleted file mode 100644 index 6f7d34a..0000000 --- a/src/main/java/sevenUnits/newGUI/Presenter.java +++ /dev/null @@ -1,97 +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.newGUI; - -/** - * An object that handles interactions between the view and the backend code - * - * @author Adrien Hopkins - * @since 2021-12-15 - */ -public final class Presenter { - /** - * The view that this presenter communicates with - */ - private final View view; - - /** - * Creates a Presenter - * - * @param view the view that this presenter communicates with - * @since 2021-12-15 - */ - public Presenter(View view) { - this.view = view; - } - - /** - * Sets the dimension of the view's From and To units. - * - * @throws UnsupportedOperationException if the view does not support - * unit-based conversion (does not - * implement - * {@link UnitConversionView}) - * @since 2021-12-15 - */ - public void applyDimensionFilter() {} - - /** - * Gets settings from the view and applies them to both view and presenter. - * - * @since 2021-12-15 - */ - public void applySettings() {} - - /** - * Converts from the view's input expression to its output expression. - * Displays an error message if any of the required fields are invalid. - * - * @throws UnsupportedOperationException if the view does not support - * expression-based conversion (does - * not implement - * {@link ExpressionConversionView}) - * @since 2021-12-15 - */ - public void convertExpressions() {} - - /** - * Converts from the view's input unit to its output unit. Displays an error - * message if any of the required fields are invalid. - * - * @throws UnsupportedOperationException if the view does not support - * unit-based conversion (does not - * implement - * {@link UnitConversionView}) - * @since 2021-12-15 - */ - public void convertUnits() {} - - /** - * Loads settings from the user's settings file and applies them to the view. - * - * @since 2021-12-15 - */ - public void loadSettings() {} - - /** - * Gets user settings from the view then saves them to the user's settings - * file. - * - * @since 2021-12-15 - */ - public void saveSettings() {} -} diff --git a/src/main/java/sevenUnits/newGUI/UnitConversionView.java b/src/main/java/sevenUnits/newGUI/UnitConversionView.java deleted file mode 100644 index dafd461..0000000 --- a/src/main/java/sevenUnits/newGUI/UnitConversionView.java +++ /dev/null @@ -1,97 +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.newGUI; - -import java.util.List; -import java.util.Optional; -import java.util.OptionalDouble; - -import sevenUnits.unit.BaseDimension; -import sevenUnits.unit.Unit; -import sevenUnits.unit.UnitValue; -import sevenUnits.utils.NamedObjectProduct; -import sevenUnits.utils.ObjectProduct; - -/** - * A View that supports single unit-based conversion - * - * @author Adrien Hopkins - * @since 2021-12-15 - */ -public interface UnitConversionView extends View { - /** - * @return unit to convert from - * @since 2021-12-15 - */ - Optional getFromSelection(); - - /** - * @return value to convert between the units - * @since 2021-12-15 - */ - OptionalDouble getInputValue(); - - /** - * @return selected dimension - * @since 2021-12-15 - */ - Optional> getSelectedDimension(); - - /** - * @return unit to convert to - * @since 2021-12-15 - */ - Optional getToSelection(); - - /** - * Sets the available dimensions for filtering. - * - * @param dimensions dimensions to use - * @since 2021-12-15 - */ - void setDimensions(List> dimensions); - - /** - * Sets the available units to convert from. {@link #getFromSelection} is not - * required to use one of these units; this method is to be used for views - * that allow the user to select units from a list. - * - * @param units units to convert from - * @since 2021-12-15 - */ - void setFromUnits(List units); - - /** - * Sets the available units to convert to. {@link #getToSelection} is not - * required to use one of these units; this method is to be used for views - * that allow the user to select units from a list. - * - * @param units units to convert to - * @since 2021-12-15 - */ - void setToUnits(List units); - - /** - * Shows the output of a unit conversion. - * - * @param input unit value before conversion, as obtained from the view - * @param output unit value after conversion - * @throws NullPointerException if either argument is null - * @since 2021-12-15 - */ - void showUnitConversionOutput(UnitValue input, UnitValue output); -} diff --git a/src/main/java/sevenUnits/newGUI/UserSettings.java b/src/main/java/sevenUnits/newGUI/UserSettings.java deleted file mode 100644 index 207ef11..0000000 --- a/src/main/java/sevenUnits/newGUI/UserSettings.java +++ /dev/null @@ -1,27 +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.newGUI; - -/** - * An object containing all of the user settings for a 7Units session. - * - * @author Adrien Hopkins - * @since 2021-12-15 - */ -public final class UserSettings { - -} diff --git a/src/main/java/sevenUnits/newGUI/View.java b/src/main/java/sevenUnits/newGUI/View.java deleted file mode 100644 index 8758a41..0000000 --- a/src/main/java/sevenUnits/newGUI/View.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.newGUI; - -/** - * An object that controls user interaction with 7Units - * - * @author Adrien Hopkins - * @since 2021-12-15 - */ -public interface View { - /** - * @return settings user has set in the view - * @since 2021-12-15 - */ - UserSettings getUserSettings(); - - /** - * Sets the user settings on the settings page, if this view has one. Does - * not update settings internally; this is intended for loading settings from - * a file. - * - * @param settings settings to set - * @since 2021-12-15 - */ - void setUserSettings(UserSettings settings); - - /** - * Shows an error message. - * - * @param title title of error message; on any view that uses an error - * dialog, this should be the title of the error dialog. - * @param message error message - * @since 2021-12-15 - */ - void showErrorMessage(String title, String message); -} diff --git a/src/main/java/sevenUnits/newGUI/package-info.java b/src/main/java/sevenUnits/newGUI/package-info.java deleted file mode 100644 index b58dccc..0000000 --- a/src/main/java/sevenUnits/newGUI/package-info.java +++ /dev/null @@ -1,23 +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 . - */ -/** - * The MVP GUI of SevenUnits - * - * @author Adrien Hopkins - * @since 2021-12-15 - */ -package sevenUnits.newGUI; \ No newline at end of file diff --git a/src/main/java/sevenUnitsGUI/ExpressionConversionView.java b/src/main/java/sevenUnitsGUI/ExpressionConversionView.java new file mode 100644 index 0000000..5a587d4 --- /dev/null +++ b/src/main/java/sevenUnitsGUI/ExpressionConversionView.java @@ -0,0 +1,48 @@ +/** + * Copyright (C) 2021 Adrien Hopkins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package sevenUnitsGUI; + +/** + * A View that can convert unit expressions + * + * @author Adrien Hopkins + * @since 2021-12-15 + */ +public interface ExpressionConversionView extends View { + /** + * @return unit expression to convert from + * @since 2021-12-15 + */ + String getFromExpression(); + + /** + * @return unit expression to convert to + * @since 2021-12-15 + */ + String getToExpression(); + + /** + * Shows the output of an expression conversion to the user. + * + * @param fromExpression expression converted from + * @param toExpression expression converted to + * @param value conversion factor between two expressions + * @since 2021-12-15 + */ + void showExpressionConversionOutput(String fromExpression, + String toExpression, double value); +} diff --git a/src/main/java/sevenUnitsGUI/Presenter.java b/src/main/java/sevenUnitsGUI/Presenter.java new file mode 100644 index 0000000..4373049 --- /dev/null +++ b/src/main/java/sevenUnitsGUI/Presenter.java @@ -0,0 +1,146 @@ +/** + * Copyright (C) 2021 Adrien Hopkins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package sevenUnitsGUI; + +import java.util.List; +import java.util.function.Function; +import java.util.function.Predicate; + +import sevenUnits.unit.UnitDatabase; +import sevenUnits.unit.UnitPrefix; +import sevenUnits.utils.UncertainDouble; + +/** + * An object that handles interactions between the view and the backend code + * + * @author Adrien Hopkins + * @since 2021-12-15 + */ +public final class Presenter { + /** + * The view that this presenter communicates with + */ + private final View view; + + /** + * The database that this presenter communicates with (effectively the model) + */ + private final UnitDatabase database; + + // ====== SETTINGS ====== + + /** + * The rule used for parsing input numbers. Any number-string inputted into + * this program will be parsed using this method. + */ + 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 + * string will be used in the output. + */ + private Function numberDisplayRule; + + /** + * 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; + + /** + * 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 oneWayConversion; + + /** + * If this is false, duplicate units will be removed from the unit view in + * views that show units as a list to choose from. + */ + private boolean showDuplicateUnits; + + /** + * Creates a Presenter + * + * @param view the view that this presenter communicates with + * @since 2021-12-15 + */ + public Presenter(View view) { + this.view = view; + this.database = new UnitDatabase(); + } + + /** + * Sets the dimension of the view's From and To units. + * + * @throws UnsupportedOperationException if the view does not support + * unit-based conversion (does not + * implement + * {@link UnitConversionView}) + * @since 2021-12-15 + */ + public void applyDimensionFilter() {} + + /** + * Gets settings from the view and applies them to both view and presenter. + * + * @since 2021-12-15 + */ + public void applySettings() {} + + /** + * Converts from the view's input expression to its output expression. + * Displays an error message if any of the required fields are invalid. + * + * @throws UnsupportedOperationException if the view does not support + * expression-based conversion (does + * not implement + * {@link ExpressionConversionView}) + * @since 2021-12-15 + */ + public void convertExpressions() {} + + /** + * Converts from the view's input unit to its output unit. Displays an error + * message if any of the required fields are invalid. + * + * @throws UnsupportedOperationException if the view does not support + * unit-based conversion (does not + * implement + * {@link UnitConversionView}) + * @since 2021-12-15 + */ + public void convertUnits() {} + + /** + * Loads settings from the user's settings file and applies them to the view. + * + * @since 2021-12-15 + */ + public void loadSettings() {} + + /** + * Gets user settings from the view then saves them to the user's settings + * file. + * + * @since 2021-12-15 + */ + public void saveSettings() {} +} diff --git a/src/main/java/sevenUnitsGUI/StandardDisplayRules.java b/src/main/java/sevenUnitsGUI/StandardDisplayRules.java new file mode 100644 index 0000000..331f598 --- /dev/null +++ b/src/main/java/sevenUnitsGUI/StandardDisplayRules.java @@ -0,0 +1,74 @@ +/** + * Copyright (C) 2021 Adrien Hopkins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package sevenUnitsGUI; + +import java.util.function.Function; + +import sevenUnits.utils.UncertainDouble; + +/** + * The default rules for displaying numbers. + * + * Unless otherwise stated, all of this class's functions throw + * {@link NullPointerException} when they receive a null parameter. + * + * @since 2021-12-24 + */ +final class StandardDisplayRules { + /** + * Rounds using UncertainDouble's toString method. + */ + private static final Function SCIENTIFIC_ROUNDING_RULE = new Function<>() { + @Override + public String apply(UncertainDouble t) { + return t.toString(false); + } + + @Override + public String toString() { + return "Scientific Rounding"; + } + }; + + /** + * @return a rule that rounds using UncertainDouble's own toString(false) + * function. + * @since 2021-12-24 + */ + public static final Function getScientificRule() { + return SCIENTIFIC_ROUNDING_RULE; + } + + /** + * Gets one of the standard rules from its string representation. + * + * @param ruleToString string representation of the display rule + * @return display rule + * @throws IllegalArgumentException if the provided string is not that of a + * standard rule. + * @since 2021-12-24 + */ + public static final Function getStandardRule( + String ruleToString) { + throw new UnsupportedOperationException("Not implemented yet"); + } + + private StandardDisplayRules() { + throw new AssertionError( + "This is a static utility class, you may not get instances of it."); + } +} diff --git a/src/main/java/sevenUnitsGUI/UnitConversionView.java b/src/main/java/sevenUnitsGUI/UnitConversionView.java new file mode 100644 index 0000000..f653051 --- /dev/null +++ b/src/main/java/sevenUnitsGUI/UnitConversionView.java @@ -0,0 +1,94 @@ +/** + * Copyright (C) 2021 Adrien Hopkins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package sevenUnitsGUI; + +import java.util.List; +import java.util.Optional; + +import sevenUnits.unit.BaseDimension; +import sevenUnits.unit.Unit; +import sevenUnits.utils.NamedObjectProduct; +import sevenUnits.utils.ObjectProduct; + +/** + * A View that supports single unit-based conversion + * + * @author Adrien Hopkins + * @since 2021-12-15 + */ +public interface UnitConversionView extends View { + /** + * @return unit to convert from + * @since 2021-12-15 + */ + Optional getFromSelection(); + + /** + * @return value to convert between the units (specifically, the numeric + * string provided by the user) + * @since 2021-12-15 + */ + Optional getInputValue(); + + /** + * @return selected dimension + * @since 2021-12-15 + */ + Optional> getSelectedDimension(); + + /** + * @return unit to convert to + * @since 2021-12-15 + */ + Optional getToSelection(); + + /** + * Sets the available dimensions for filtering. + * + * @param dimensions dimensions to use + * @since 2021-12-15 + */ + void setDimensions(List> dimensions); + + /** + * Sets the available units to convert from. {@link #getFromSelection} is not + * required to use one of these units; this method is to be used for views + * that allow the user to select units from a list. + * + * @param units units to convert from + * @since 2021-12-15 + */ + void setFromUnits(List units); + + /** + * Sets the available units to convert to. {@link #getToSelection} is not + * required to use one of these units; this method is to be used for views + * that allow the user to select units from a list. + * + * @param units units to convert to + * @since 2021-12-15 + */ + void setToUnits(List units); + + /** + * Shows the output of a unit conversion. + * + * @param outputString string that shows output of conversion + * @since 2021-12-24 + */ + void showUnitConversionOutput(String outputString); +} diff --git a/src/main/java/sevenUnitsGUI/View.java b/src/main/java/sevenUnitsGUI/View.java new file mode 100644 index 0000000..a93c76a --- /dev/null +++ b/src/main/java/sevenUnitsGUI/View.java @@ -0,0 +1,35 @@ +/** + * Copyright (C) 2021 Adrien Hopkins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package sevenUnitsGUI; + +/** + * An object that controls user interaction with 7Units + * + * @author Adrien Hopkins + * @since 2021-12-15 + */ +public interface View { + /** + * Shows an error message. + * + * @param title title of error message; on any view that uses an error + * dialog, this should be the title of the error dialog. + * @param message error message + * @since 2021-12-15 + */ + void showErrorMessage(String title, String message); +} diff --git a/src/main/java/sevenUnitsGUI/package-info.java b/src/main/java/sevenUnitsGUI/package-info.java new file mode 100644 index 0000000..cff1ded --- /dev/null +++ b/src/main/java/sevenUnitsGUI/package-info.java @@ -0,0 +1,23 @@ +/** + * 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 . + */ +/** + * The MVP GUI of SevenUnits + * + * @author Adrien Hopkins + * @since 2021-12-15 + */ +package sevenUnitsGUI; \ No newline at end of file -- cgit v1.2.3 From 47b71bb5170fc2c3e1e052452864890826e6848d Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Sat, 29 Jan 2022 15:51:26 -0500 Subject: Created the ViewBot in preparation for GUI testing --- .../java/sevenUnitsGUI/UnitConversionView.java | 10 +- src/main/java/sevenUnitsGUI/ViewBot.java | 230 +++++++++++++++++++++ 2 files changed, 238 insertions(+), 2 deletions(-) create mode 100644 src/main/java/sevenUnitsGUI/ViewBot.java diff --git a/src/main/java/sevenUnitsGUI/UnitConversionView.java b/src/main/java/sevenUnitsGUI/UnitConversionView.java index f653051..97ec30f 100644 --- a/src/main/java/sevenUnitsGUI/UnitConversionView.java +++ b/src/main/java/sevenUnitsGUI/UnitConversionView.java @@ -31,6 +31,12 @@ import sevenUnits.utils.ObjectProduct; * @since 2021-12-15 */ public interface UnitConversionView extends View { + /** + * @return dimensions available for filtering + * @since 2022-01-29 + */ + List> getDimensions(); + /** * @return unit to convert from * @since 2021-12-15 @@ -42,13 +48,13 @@ public interface UnitConversionView extends View { * string provided by the user) * @since 2021-12-15 */ - Optional getInputValue(); + String getInputValue(); /** * @return selected dimension * @since 2021-12-15 */ - Optional> getSelectedDimension(); + Optional> getSelectedDimension(); /** * @return unit to convert to diff --git a/src/main/java/sevenUnitsGUI/ViewBot.java b/src/main/java/sevenUnitsGUI/ViewBot.java new file mode 100644 index 0000000..bc4103c --- /dev/null +++ b/src/main/java/sevenUnitsGUI/ViewBot.java @@ -0,0 +1,230 @@ +/** + * Copyright (C) 2022 Adrien Hopkins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package sevenUnitsGUI; + +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +import sevenUnits.unit.BaseDimension; +import sevenUnits.unit.Unit; +import sevenUnits.utils.NamedObjectProduct; +import sevenUnits.utils.ObjectProduct; + +/** + * A class that simulates a View (supports both unit and expression conversion) + * for testing. Getters and setters work as expected. + * + * @author Adrien Hopkins + * @since 2022-01-29 + */ +final class ViewBot implements UnitConversionView, ExpressionConversionView { + /** The presenter that works with this ViewBot */ + private final Presenter presenter; + + /** The dimensions available to select from */ + private List> dimensions; + /** The expression in the From field */ + private String fromExpression; + /** The expression in the To field */ + private String toExpression; + /** + * The user-provided string representing the value in {@code fromSelection} + */ + private String inputValue; + /** The unit selected in the From selection */ + private Optional fromSelection; + /** The unit selected in the To selection */ + private Optional toSelection; + /** The currently selected dimension */ + private Optional> selectedDimension; + /** The units available in the From selection */ + private List fromUnits; + /** The units available in the To selection */ + private List toUnits; + + /** + * Creates a new {@code ViewBot} with a new presenter. + * + * @since 2022-01-29 + */ + public ViewBot() { + this.presenter = new Presenter(this); + } + + /** + * @return the available dimensions + * @since 2022-01-29 + */ + @Override + public List> getDimensions() { + return this.dimensions; + } + + @Override + public String getFromExpression() { + return this.fromExpression; + } + + @Override + public Optional getFromSelection() { + return this.fromSelection; + } + + /** + * @return the units available for selection in From + * @since 2022-01-29 + */ + public List getFromUnits() { + return this.fromUnits; + } + + @Override + public String getInputValue() { + return this.inputValue; + } + + /** + * @return the presenter associated with tihs view + * @since 2022-01-29 + */ + public Presenter getPresenter() { + return this.presenter; + } + + @Override + public Optional> getSelectedDimension() { + return this.selectedDimension; + } + + @Override + public String getToExpression() { + return this.toExpression; + } + + @Override + public Optional getToSelection() { + return this.toSelection; + } + + /** + * @return the units available for selection in To + * @since 2022-01-29 + */ + public List getToUnits() { + return this.toUnits; + } + + @Override + public void setDimensions( + List> dimensions) { + this.dimensions = Objects.requireNonNull(dimensions, + "dimensions may not be null"); + } + + /** + * Sets the From expression (as in {@link #getFromExpression}). + * + * @param fromExpression the expression to convert from + * @throws NullPointerException if {@code fromExpression} is null + * @since 2022-01-29 + */ + public void setFromExpression(String fromExpression) { + this.fromExpression = Objects.requireNonNull(fromExpression, + "fromExpression cannot be null."); + } + + /** + * @param fromSelection the fromSelection to set + * @since 2022-01-29 + */ + public void setFromSelection(Optional fromSelection) { + this.fromSelection = Objects.requireNonNull(fromSelection, + "fromSelection cannot be null"); + } + + @Override + public void setFromUnits(List units) { + this.fromUnits = Objects.requireNonNull(units, "units may not be null"); + } + + /** + * @param inputValue the inputValue to set + * @since 2022-01-29 + */ + public void setInputValue(String inputValue) { + this.inputValue = inputValue; + } + + /** + * @param selectedDimension the selectedDimension to set + * @since 2022-01-29 + */ + public void setSelectedDimension( + Optional> selectedDimension) { + this.selectedDimension = selectedDimension; + } + + /** + * Sets the To expression (as in {@link #getToExpression}). + * + * @param toExpression the expression to convert to + * @throws NullPointerException if {@code toExpression} is null + * @since 2022-01-29 + */ + public void setToExpression(String toExpression) { + this.toExpression = Objects.requireNonNull(toExpression, + "toExpression cannot be null."); + } + + /** + * @param toSelection the toSelection to set + * @since 2022-01-29 + */ + public void setToSelection(Optional toSelection) { + this.toSelection = Objects.requireNonNull(toSelection, + "toSelection cannot be null."); + } + + @Override + public void setToUnits(List units) { + this.toUnits = Objects.requireNonNull(units, "units may not be null"); + } + + @Override + public void showErrorMessage(String title, String message) { + System.err.printf("%s: %s%n", title, message); + } + + @Override + public void showExpressionConversionOutput(String fromExpression, + String toExpression, double value) { + System.out.printf("Expression Conversion: %s = %d * (%s)%n", + fromExpression, value, toExpression); + } + + @Override + public void showUnitConversionOutput(String outputString) { + System.out.println("Unit conversion: " + outputString); + } + + @Override + public String toString() { + return super.toString() + String.format("[presenter=%s]", this.presenter); + } + +} -- cgit v1.2.3 From e42994251882208e2a2ad0ce5f318b4c90823991 Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Sat, 12 Feb 2022 14:23:30 -0500 Subject: Made some basic Presenter tests --- src/main/java/sevenUnitsGUI/ViewBot.java | 44 ++++++-- src/test/java/sevenUnitsGUI/PresenterTest.java | 135 +++++++++++++++++++++++++ src/test/java/sevenUnitsGUI/package-info.java | 23 +++++ 3 files changed, 196 insertions(+), 6 deletions(-) create mode 100644 src/test/java/sevenUnitsGUI/PresenterTest.java create mode 100644 src/test/java/sevenUnitsGUI/package-info.java diff --git a/src/main/java/sevenUnitsGUI/ViewBot.java b/src/main/java/sevenUnitsGUI/ViewBot.java index bc4103c..cc070e2 100644 --- a/src/main/java/sevenUnitsGUI/ViewBot.java +++ b/src/main/java/sevenUnitsGUI/ViewBot.java @@ -16,6 +16,7 @@ */ package sevenUnitsGUI; +import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.Optional; @@ -56,6 +57,8 @@ final class ViewBot implements UnitConversionView, ExpressionConversionView { private List fromUnits; /** The units available in the To selection */ private List toUnits; + /** Saved output values of all unit conversions */ + private List unitConversionOutputValues; /** * Creates a new {@code ViewBot} with a new presenter. @@ -75,6 +78,10 @@ final class ViewBot implements UnitConversionView, ExpressionConversionView { return this.dimensions; } + public List getExpressionConversionOutputs() { + throw new UnsupportedOperationException("Not implemented yet"); + } + @Override public String getFromExpression() { return this.fromExpression; @@ -89,8 +96,8 @@ final class ViewBot implements UnitConversionView, ExpressionConversionView { * @return the units available for selection in From * @since 2022-01-29 */ - public List getFromUnits() { - return this.fromUnits; + public List getFromUnits() { + return Collections.unmodifiableList(this.fromUnits); } @Override @@ -107,8 +114,8 @@ final class ViewBot implements UnitConversionView, ExpressionConversionView { } @Override - public Optional> getSelectedDimension() { - return this.selectedDimension; + public Optional> getSelectedDimension() { + return this.selectedDimension.map(x -> x); } @Override @@ -125,8 +132,16 @@ final class ViewBot implements UnitConversionView, ExpressionConversionView { * @return the units available for selection in To * @since 2022-01-29 */ - public List getToUnits() { - return this.toUnits; + public List getToUnits() { + return Collections.unmodifiableList(this.toUnits); + } + + /** + * @return the unitConversionOutputValues + * @since 2022-02-10 + */ + public List getUnitConversionOutputValues() { + return this.unitConversionOutputValues; } @Override @@ -157,6 +172,14 @@ final class ViewBot implements UnitConversionView, ExpressionConversionView { "fromSelection cannot be null"); } + /** + * @param fromSelection the fromSelection to set + * @since 2022-02-10 + */ + public void setFromSelection(Unit fromSelection) { + this.setFromSelection(Optional.of(fromSelection)); + } + @Override public void setFromUnits(List units) { this.fromUnits = Objects.requireNonNull(units, "units may not be null"); @@ -170,6 +193,11 @@ final class ViewBot implements UnitConversionView, ExpressionConversionView { this.inputValue = inputValue; } + public void setSelectedDimension( + ObjectProduct selectedDimension) { + this.setSelectedDimension(Optional.of(selectedDimension)); + } + /** * @param selectedDimension the selectedDimension to set * @since 2022-01-29 @@ -200,6 +228,10 @@ final class ViewBot implements UnitConversionView, ExpressionConversionView { "toSelection cannot be null."); } + public void setToSelection(Unit toSelection) { + this.setToSelection(Optional.of(toSelection)); + } + @Override public void setToUnits(List units) { this.toUnits = Objects.requireNonNull(units, "units may not be null"); diff --git a/src/test/java/sevenUnitsGUI/PresenterTest.java b/src/test/java/sevenUnitsGUI/PresenterTest.java new file mode 100644 index 0000000..675e3ab --- /dev/null +++ b/src/test/java/sevenUnitsGUI/PresenterTest.java @@ -0,0 +1,135 @@ +/** + * Copyright (C) 2022 Adrien Hopkins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package sevenUnitsGUI; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.Optional; + +import org.junit.jupiter.api.Test; + +import sevenUnits.unit.BaseDimension; +import sevenUnits.unit.Metric; +import sevenUnits.unit.NameSymbol; +import sevenUnits.unit.Unit; +import sevenUnits.utils.NamedObjectProduct; + +/** + * @author Adrien Hopkins + * + * @since 2022-02-10 + */ +public final class PresenterTest { + List testUnits = List.of(Metric.METRE, Metric.KILOMETRE, + Metric.METRE_PER_SECOND, Metric.KILOMETRE_PER_HOUR); + List> testDimensions = List.of( + Metric.Dimensions.LENGTH.withName(NameSymbol.ofName("Length")), + Metric.Dimensions.VELOCITY.withName(NameSymbol.ofName("Velocity"))); + + /** + * Test for {@link Presenter#applyDimensionFilter()} + * + * @since 2022-02-12 + */ + @Test + void testApplyDimensionFilter() { + // 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.get(0))); + + // filter to length units only, then get the filtered sets of units + presenter.applyDimensionFilter(); + final List fromUnits = viewBot.getFromUnits(); + final List 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} + * + * @since 2022-02-12 + */ + @Test + void testConvertExpressions() { + // setup + final ViewBot viewBot = new ViewBot(); + final Presenter presenter = new Presenter(viewBot); + + viewBot.setFromExpression("10000.0 m"); + viewBot.setToExpression("km"); + + // convert expression + presenter.convertExpressions(); + + // test result + final List outputs = viewBot.getExpressionConversionOutputs(); + assertEquals("10000.0 m = 10.0 km", outputs.get(outputs.size() - 1)); + } + + /** + * Tests that unit-conversion Views can correctly convert units + * + * @since 2022-02-12 + */ + @Test + void testConvertUnits() { + // setup + final ViewBot viewBot = new ViewBot(); + final Presenter presenter = new Presenter(viewBot); + + viewBot.setFromUnits(this.testUnits); + viewBot.setToUnits(this.testUnits); + viewBot.setFromSelection(Optional.of(Metric.METRE)); + viewBot.setToSelection(Optional.of(Metric.KILOMETRE)); + viewBot.setInputValue("10000.0"); + + // convert units + presenter.convertUnits(); + + /* + * use result from system as expected - I'm not testing unit conversion + * here (that's for the backend tests), I'm just testing that it correctly + * calls the unit conversion system + */ + final String expected = String + .valueOf(Metric.METRE.convertTo(Metric.KILOMETRE, 10000.0)); + + final List outputs = viewBot.getUnitConversionOutputValues(); + assertEquals(expected, outputs.get(outputs.size() - 1)); + } +} diff --git a/src/test/java/sevenUnitsGUI/package-info.java b/src/test/java/sevenUnitsGUI/package-info.java new file mode 100644 index 0000000..96bdbd9 --- /dev/null +++ b/src/test/java/sevenUnitsGUI/package-info.java @@ -0,0 +1,23 @@ +/** + * Copyright (C) 2022 Adrien Hopkins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +/** + * Tests for the new 7Units GUI + * + * @author Adrien Hopkins + * @since 2022-01-29 + */ +package sevenUnitsGUI; \ No newline at end of file -- cgit v1.2.3 From b179f3720fcd569c07f5fe95ee00d7ccfe12639d Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Sat, 12 Feb 2022 14:25:47 -0500 Subject: Updated changelog --- CHANGELOG.org | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.org b/CHANGELOG.org index 5630737..54983fa 100644 --- a/CHANGELOG.org +++ b/CHANGELOG.org @@ -1,5 +1,10 @@ * Changelog All notable changes in this project will be shown in this file. +** Unreleased +*** Added + - Added tests for the GUI +*** Changed + - Rewrote the GUI code internally using an MVP model to make it easier to maintain and improve ** v0.3.2 - [2021-12-02 Thu] *** Added - Added lots more tests for the backend and utilities -- cgit v1.2.3 From 540b798e397fb787fd81c8e6e636a2343655a42f Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Sat, 19 Feb 2022 16:59:26 -0500 Subject: Made barebones GUI (TabbedView) --- CHANGELOG.org | 1 + src/main/java/sevenUnits/ProgramInfo.java | 2 +- .../sevenUnits/converterGUI/SevenUnitsGUI.java | 2 +- src/main/java/sevenUnitsGUI/DelegateListModel.java | 242 +++++++++ src/main/java/sevenUnitsGUI/FilterComparator.java | 128 +++++ src/main/java/sevenUnitsGUI/GridBagBuilder.java | 479 ++++++++++++++++ src/main/java/sevenUnitsGUI/MutablePredicate.java | 70 +++ src/main/java/sevenUnitsGUI/Presenter.java | 88 ++- src/main/java/sevenUnitsGUI/SearchBoxList.java | 331 +++++++++++ src/main/java/sevenUnitsGUI/TabbedView.java | 605 +++++++++++++++++++++ .../java/sevenUnitsGUI/UnitConversionView.java | 10 +- src/main/java/sevenUnitsGUI/ViewBot.java | 23 +- src/main/resources/about.txt | 2 +- src/test/java/sevenUnitsGUI/PresenterTest.java | 12 +- 14 files changed, 1969 insertions(+), 26 deletions(-) create mode 100644 src/main/java/sevenUnitsGUI/DelegateListModel.java create mode 100644 src/main/java/sevenUnitsGUI/FilterComparator.java create mode 100644 src/main/java/sevenUnitsGUI/GridBagBuilder.java create mode 100644 src/main/java/sevenUnitsGUI/MutablePredicate.java create mode 100644 src/main/java/sevenUnitsGUI/SearchBoxList.java create mode 100644 src/main/java/sevenUnitsGUI/TabbedView.java diff --git a/CHANGELOG.org b/CHANGELOG.org index 54983fa..bb6185e 100644 --- a/CHANGELOG.org +++ b/CHANGELOG.org @@ -5,6 +5,7 @@ - Added tests for the GUI *** Changed - Rewrote the GUI code internally using an MVP model to make it easier to maintain and improve + - Tweaked the look of the unit and expression conversion sections of the view ** v0.3.2 - [2021-12-02 Thu] *** Added - Added lots more tests for the backend and utilities diff --git a/src/main/java/sevenUnits/ProgramInfo.java b/src/main/java/sevenUnits/ProgramInfo.java index ba6bc7a..0d67824 100644 --- a/src/main/java/sevenUnits/ProgramInfo.java +++ b/src/main/java/sevenUnits/ProgramInfo.java @@ -24,7 +24,7 @@ package sevenUnits; */ public final class ProgramInfo { - public static final String VERSION = "0.3.2"; + public static final String VERSION = "0.4.0-dev"; private ProgramInfo() { // this class is only for static variables, you shouldn't be able to diff --git a/src/main/java/sevenUnits/converterGUI/SevenUnitsGUI.java b/src/main/java/sevenUnits/converterGUI/SevenUnitsGUI.java index bfd5974..e21c25f 100644 --- a/src/main/java/sevenUnits/converterGUI/SevenUnitsGUI.java +++ b/src/main/java/sevenUnits/converterGUI/SevenUnitsGUI.java @@ -69,8 +69,8 @@ import sevenUnits.unit.BaseDimension; import sevenUnits.unit.BritishImperial; import sevenUnits.unit.LinearUnit; import sevenUnits.unit.LinearUnitValue; -import sevenUnits.unit.NameSymbol; import sevenUnits.unit.Metric; +import sevenUnits.unit.NameSymbol; import sevenUnits.unit.Unit; import sevenUnits.unit.UnitDatabase; import sevenUnits.unit.UnitPrefix; diff --git a/src/main/java/sevenUnitsGUI/DelegateListModel.java b/src/main/java/sevenUnitsGUI/DelegateListModel.java new file mode 100644 index 0000000..5938b59 --- /dev/null +++ b/src/main/java/sevenUnitsGUI/DelegateListModel.java @@ -0,0 +1,242 @@ +/** + * Copyright (C) 2018 Adrien Hopkins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package sevenUnitsGUI; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; + +import javax.swing.AbstractListModel; + +/** + * A list model that delegates to a list. + *

+ * It is recommended to use the delegate methods in DelegateListModel instead of the delegated list's methods because + * the delegate methods handle updating the list. + *

+ * + * @author Adrien Hopkins + * @since 2019-01-14 + * @since v0.1.0 + */ +final class DelegateListModel extends AbstractListModel implements List { + /** + * @since 2019-01-14 + * @since v0.1.0 + */ + private static final long serialVersionUID = 8985494428224810045L; + + /** + * The list that this model is a delegate to. + * + * @since 2019-01-14 + * @since v0.1.0 + */ + private final List delegate; + + /** + * Creates an empty {@code DelegateListModel}. + * + * @since 2019-04-13 + */ + public DelegateListModel() { + this(new ArrayList<>()); + } + + /** + * Creates the {@code DelegateListModel}. + * + * @param delegate + * list to delegate + * @since 2019-01-14 + * @since v0.1.0 + */ + public DelegateListModel(final List delegate) { + this.delegate = delegate; + } + + @Override + public boolean add(final E element) { + final int index = this.delegate.size(); + final boolean success = this.delegate.add(element); + this.fireIntervalAdded(this, index, index); + return success; + } + + @Override + public void add(final int index, final E element) { + this.delegate.add(index, element); + this.fireIntervalAdded(this, index, index); + } + + @Override + public boolean addAll(final Collection c) { + boolean changed = false; + for (final E e : c) { + if (this.add(e)) { + changed = true; + } + } + return changed; + } + + @Override + public boolean addAll(final int index, final Collection c) { + for (final E e : c) { + this.add(index, e); + } + return !c.isEmpty(); // Since this is a list, it will always change if c has elements. + } + + @Override + public void clear() { + final int oldSize = this.delegate.size(); + this.delegate.clear(); + if (oldSize >= 1) { + this.fireIntervalRemoved(this, 0, oldSize - 1); + } + } + + @Override + public boolean contains(final Object elem) { + return this.delegate.contains(elem); + } + + @Override + public boolean containsAll(final Collection c) { + for (final Object e : c) { + if (!c.contains(e)) + return false; + } + return true; + } + + @Override + public E get(final int index) { + return this.delegate.get(index); + } + + @Override + public E getElementAt(final int index) { + return this.delegate.get(index); + } + + @Override + public int getSize() { + return this.delegate.size(); + } + + @Override + public int indexOf(final Object elem) { + return this.delegate.indexOf(elem); + } + + @Override + public boolean isEmpty() { + return this.delegate.isEmpty(); + } + + @Override + public Iterator iterator() { + return this.delegate.iterator(); + } + + @Override + public int lastIndexOf(final Object elem) { + return this.delegate.lastIndexOf(elem); + } + + @Override + public ListIterator listIterator() { + return this.delegate.listIterator(); + } + + @Override + public ListIterator listIterator(final int index) { + return this.delegate.listIterator(index); + } + + @Override + public E remove(final int index) { + final E returnValue = this.delegate.get(index); + this.delegate.remove(index); + this.fireIntervalRemoved(this, index, index); + return returnValue; + } + + @Override + public boolean remove(final Object o) { + final int index = this.delegate.indexOf(o); + final boolean returnValue = this.delegate.remove(o); + this.fireIntervalRemoved(this, index, index); + return returnValue; + } + + @Override + public boolean removeAll(final Collection c) { + boolean changed = false; + for (final Object e : c) { + if (this.remove(e)) { + changed = true; + } + } + return changed; + } + + @Override + public boolean retainAll(final Collection c) { + final int oldSize = this.size(); + final boolean returnValue = this.delegate.retainAll(c); + this.fireIntervalRemoved(this, this.size(), oldSize - 1); + return returnValue; + } + + @Override + public E set(final int index, final E element) { + final E returnValue = this.delegate.get(index); + this.delegate.set(index, element); + this.fireContentsChanged(this, index, index); + return returnValue; + } + + @Override + public int size() { + return this.delegate.size(); + } + + @Override + public List subList(final int fromIndex, final int toIndex) { + return this.delegate.subList(fromIndex, toIndex); + } + + @Override + public Object[] toArray() { + return this.delegate.toArray(); + } + + @Override + public T[] toArray(final T[] a) { + return this.delegate.toArray(a); + } + + @Override + public String toString() { + return this.delegate.toString(); + } +} diff --git a/src/main/java/sevenUnitsGUI/FilterComparator.java b/src/main/java/sevenUnitsGUI/FilterComparator.java new file mode 100644 index 0000000..f34d0c0 --- /dev/null +++ b/src/main/java/sevenUnitsGUI/FilterComparator.java @@ -0,0 +1,128 @@ +/** + * Copyright (C) 2018 Adrien Hopkins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package sevenUnitsGUI; + +import java.util.Comparator; +import java.util.Objects; + +/** + * A comparator that compares strings using a filter. + * + * @param type of element being compared + * + * @author Adrien Hopkins + * @since 2019-01-15 + * @since v0.1.0 + */ +final class FilterComparator implements Comparator { + /** + * The filter that the comparator is filtered by. + * + * @since 2019-01-15 + * @since v0.1.0 + */ + private final String filter; + /** + * The comparator to use if the arguments are otherwise equal. + * + * @since 2019-01-15 + * @since v0.1.0 + */ + private final Comparator comparator; + /** + * Whether or not the comparison is case-sensitive. + * + * @since 2019-04-14 + * @since v0.2.0 + */ + private final boolean caseSensitive; + + /** + * Creates the {@code FilterComparator}. + * + * @param filter + * @since 2019-01-15 + * @since v0.1.0 + */ + public FilterComparator(final String filter) { + this(filter, null); + } + + /** + * Creates the {@code FilterComparator}. + * + * @param filter string to filter by + * @param comparator comparator to fall back to if all else fails, null is + * compareTo. + * @throws NullPointerException if filter is null + * @since 2019-01-15 + * @since v0.1.0 + */ + public FilterComparator(final String filter, + final Comparator comparator) { + this(filter, comparator, false); + } + + /** + * Creates the {@code FilterComparator}. + * + * @param filter string to filter by + * @param comparator comparator to fall back to if all else fails, null is + * compareTo. + * @param caseSensitive whether or not the comparator is case-sensitive + * @throws NullPointerException if filter is null + * @since 2019-04-14 + * @since v0.2.0 + */ + public FilterComparator(final String filter, final Comparator comparator, + final boolean caseSensitive) { + this.filter = Objects.requireNonNull(filter, "filter must not be null."); + this.comparator = comparator; + this.caseSensitive = caseSensitive; + } + + @Override + public int compare(final T arg0, final T arg1) { + // if this is case insensitive, make them lowercase + final String str0, str1; + if (this.caseSensitive) { + str0 = arg0.toString(); + str1 = arg1.toString(); + } else { + str0 = arg0.toString().toLowerCase(); + str1 = arg1.toString().toLowerCase(); + } + + // elements that start with the filter always go first + if (str0.startsWith(this.filter) && !str1.startsWith(this.filter)) + return -1; + else if (!str0.startsWith(this.filter) && str1.startsWith(this.filter)) + return 1; + + // elements that contain the filter but don't start with them go next + if (str0.contains(this.filter) && !str1.contains(this.filter)) + return -1; + else if (!str0.contains(this.filter) && !str1.contains(this.filter)) + return 1; + + // other elements go last + if (this.comparator == null) + return str0.compareTo(str1); + else + return this.comparator.compare(arg0, arg1); + } +} diff --git a/src/main/java/sevenUnitsGUI/GridBagBuilder.java b/src/main/java/sevenUnitsGUI/GridBagBuilder.java new file mode 100644 index 0000000..32e94d7 --- /dev/null +++ b/src/main/java/sevenUnitsGUI/GridBagBuilder.java @@ -0,0 +1,479 @@ +/** + * Copyright (C) 2018 Adrien Hopkins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package sevenUnitsGUI; + +import java.awt.GridBagConstraints; +import java.awt.Insets; + +/** + * A builder for Java's {@link java.awt.GridBagConstraints} class. + * + * @author Adrien Hopkins + * @since 2018-11-30 + * @since v0.1.0 + */ +final class GridBagBuilder { + /** + * The built {@code GridBagConstraints}'s {@code gridx} property. + *

+ * Specifies the cell containing the leading edge of the component's display area, where the first cell in a row has + * gridx=0. The leading edge of a component's display area is its left edge for a horizontal, + * left-to-right container and its right edge for a horizontal, right-to-left container. The value + * RELATIVE specifies that the component be placed immediately following the component that was added + * to the container just before this component was added. + *

+ * The default value is RELATIVE. gridx should be a non-negative value. + * + * @serial + * @see #clone() + * @see java.awt.GridBagConstraints#gridy + * @see java.awt.ComponentOrientation + */ + private final int gridx; + + /** + * The built {@code GridBagConstraints}'s {@code gridy} property. + *

+ * Specifies the cell at the top of the component's display area, where the topmost cell has gridy=0. + * The value RELATIVE specifies that the component be placed just below the component that was added to + * the container just before this component was added. + *

+ * The default value is RELATIVE. gridy should be a non-negative value. + * + * @serial + * @see #clone() + * @see java.awt.GridBagConstraints#gridx + */ + private final int gridy; + + /** + * The built {@code GridBagConstraints}'s {@code gridwidth} property. + *

+ * Specifies the number of cells in a row for the component's display area. + *

+ * Use REMAINDER to specify that the component's display area will be from gridx to the + * last cell in the row. Use RELATIVE to specify that the component's display area will be from + * gridx to the next to the last one in its row. + *

+ * gridwidth should be non-negative and the default value is 1. + * + * @serial + * @see #clone() + * @see java.awt.GridBagConstraints#gridheight + */ + private final int gridwidth; + + /** + * The built {@code GridBagConstraints}'s {@code gridheight} property. + *

+ * Specifies the number of cells in a column for the component's display area. + *

+ * Use REMAINDER to specify that the component's display area will be from gridy to the + * last cell in the column. Use RELATIVE to specify that the component's display area will be from + * gridy to the next to the last one in its column. + *

+ * gridheight should be a non-negative value and the default value is 1. + * + * @serial + * @see #clone() + * @see java.awt.GridBagConstraints#gridwidth + */ + private final int gridheight; + + /** + * The built {@code GridBagConstraints}'s {@code weightx} property. + *

+ * Specifies how to distribute extra horizontal space. + *

+ * The grid bag layout manager calculates the weight of a column to be the maximum weightx of all the + * components in a column. If the resulting layout is smaller horizontally than the area it needs to fill, the extra + * space is distributed to each column in proportion to its weight. A column that has a weight of zero receives no + * extra space. + *

+ * If all the weights are zero, all the extra space appears between the grids of the cell and the left and right + * edges. + *

+ * The default value of this field is 0. weightx should be a non-negative value. + * + * @serial + * @see #clone() + * @see java.awt.GridBagConstraints#weighty + */ + private double weightx; + + /** + * The built {@code GridBagConstraints}'s {@code weighty} property. + *

+ * Specifies how to distribute extra vertical space. + *

+ * The grid bag layout manager calculates the weight of a row to be the maximum weighty of all the + * components in a row. If the resulting layout is smaller vertically than the area it needs to fill, the extra + * space is distributed to each row in proportion to its weight. A row that has a weight of zero receives no extra + * space. + *

+ * If all the weights are zero, all the extra space appears between the grids of the cell and the top and bottom + * edges. + *

+ * The default value of this field is 0. weighty should be a non-negative value. + * + * @serial + * @see #clone() + * @see java.awt.GridBagConstraints#weightx + */ + private double weighty; + + /** + * The built {@code GridBagConstraints}'s {@code anchor} property. + *

+ * This field is used when the component is smaller than its display area. It determines where, within the display + * area, to place the component. + *

+ * There are three kinds of possible values: orientation relative, baseline relative and absolute. Orientation + * relative values are interpreted relative to the container's component orientation property, baseline relative + * values are interpreted relative to the baseline and absolute values are not. The absolute values are: + * CENTER, NORTH, NORTHEAST, EAST, SOUTHEAST, + * SOUTH, SOUTHWEST, WEST, and NORTHWEST. The orientation + * relative values are: PAGE_START, PAGE_END, LINE_START, + * LINE_END, FIRST_LINE_START, FIRST_LINE_END, LAST_LINE_START + * and LAST_LINE_END. The baseline relative values are: BASELINE, + * BASELINE_LEADING, BASELINE_TRAILING, ABOVE_BASELINE, + * ABOVE_BASELINE_LEADING, ABOVE_BASELINE_TRAILING, BELOW_BASELINE, + * BELOW_BASELINE_LEADING, and BELOW_BASELINE_TRAILING. The default value is + * CENTER. + * + * @serial + * @see #clone() + * @see java.awt.ComponentOrientation + */ + private int anchor; + + /** + * The built {@code GridBagConstraints}'s {@code fill} property. + *

+ * This field is used when the component's display area is larger than the component's requested size. It determines + * whether to resize the component, and if so, how. + *

+ * The following values are valid for fill: + * + *

    + *
  • NONE: Do not resize the component. + *
  • HORIZONTAL: Make the component wide enough to fill its display area horizontally, but do not + * change its height. + *
  • VERTICAL: Make the component tall enough to fill its display area vertically, but do not change + * its width. + *
  • BOTH: Make the component fill its display area entirely. + *
+ *

+ * The default value is NONE. + * + * @serial + * @see #clone() + */ + private int fill; + + /** + * The built {@code GridBagConstraints}'s {@code insets} property. + *

+ * This field specifies the external padding of the component, the minimum amount of space between the component and + * the edges of its display area. + *

+ * The default value is new Insets(0, 0, 0, 0). + * + * @serial + * @see #clone() + */ + private Insets insets; + + /** + * The built {@code GridBagConstraints}'s {@code ipadx} property. + *

+ * This field specifies the internal padding of the component, how much space to add to the minimum width of the + * component. The width of the component is at least its minimum width plus ipadx pixels. + *

+ * The default value is 0. + * + * @serial + * @see #clone() + * @see java.awt.GridBagConstraints#ipady + */ + private int ipadx; + + /** + * The built {@code GridBagConstraints}'s {@code ipady} property. + *

+ * This field specifies the internal padding, that is, how much space to add to the minimum height of the component. + * The height of the component is at least its minimum height plus ipady pixels. + *

+ * The default value is 0. + * + * @serial + * @see #clone() + * @see java.awt.GridBagConstraints#ipadx + */ + private int ipady; + + /** + * @param gridx + * x position + * @param gridy + * y position + * @since 2018-11-30 + * @since v0.1.0 + */ + public GridBagBuilder(final int gridx, final int gridy) { + this(gridx, gridy, 1, 1); + } + + /** + * @param gridx + * x position + * @param gridy + * y position + * @param gridwidth + * number of cells occupied horizontally + * @param gridheight + * number of cells occupied vertically + * @since 2018-11-30 + * @since v0.1.0 + */ + public GridBagBuilder(final int gridx, final int gridy, final int gridwidth, final int gridheight) { + this(gridx, gridy, gridwidth, gridheight, 0.0, 0.0, GridBagConstraints.CENTER, GridBagConstraints.NONE, + new Insets(0, 0, 0, 0), 0, 0); + } + + /** + * @param gridx + * x position + * @param gridy + * y position + * @param gridwidth + * number of cells occupied horizontally + * @param gridheight + * number of cells occupied vertically + * @param weightx + * @param weighty + * @param anchor + * @param fill + * @param insets + * @param ipadx + * @param ipady + * @since 2018-11-30 + * @since v0.1.0 + */ + private GridBagBuilder(final int gridx, final int gridy, final int gridwidth, final int gridheight, + final double weightx, final double weighty, final int anchor, final int fill, final Insets insets, + final int ipadx, final int ipady) { + super(); + this.gridx = gridx; + this.gridy = gridy; + this.gridwidth = gridwidth; + this.gridheight = gridheight; + this.weightx = weightx; + this.weighty = weighty; + this.anchor = anchor; + this.fill = fill; + this.insets = (Insets) insets.clone(); + this.ipadx = ipadx; + this.ipady = ipady; + } + + /** + * @return {@code GridBagConstraints} created by this builder + * @since 2018-11-30 + * @since v0.1.0 + */ + public GridBagConstraints build() { + return new GridBagConstraints(this.gridx, this.gridy, this.gridwidth, this.gridheight, this.weightx, + this.weighty, this.anchor, this.fill, this.insets, this.ipadx, this.ipady); + } + + /** + * @return anchor + * @since 2018-11-30 + * @since v0.1.0 + */ + public int getAnchor() { + return this.anchor; + } + + /** + * @return fill + * @since 2018-11-30 + * @since v0.1.0 + */ + public int getFill() { + return this.fill; + } + + /** + * @return gridheight + * @since 2018-11-30 + * @since v0.1.0 + */ + public int getGridheight() { + return this.gridheight; + } + + /** + * @return gridwidth + * @since 2018-11-30 + * @since v0.1.0 + */ + public int getGridwidth() { + return this.gridwidth; + } + + /** + * @return gridx + * @since 2018-11-30 + * @since v0.1.0 + */ + public int getGridx() { + return this.gridx; + } + + /** + * @return gridy + * @since 2018-11-30 + * @since v0.1.0 + */ + public int getGridy() { + return this.gridy; + } + + /** + * @return insets + * @since 2018-11-30 + * @since v0.1.0 + */ + public Insets getInsets() { + return this.insets; + } + + /** + * @return ipadx + * @since 2018-11-30 + * @since v0.1.0 + */ + public int getIpadx() { + return this.ipadx; + } + + /** + * @return ipady + * @since 2018-11-30 + * @since v0.1.0 + */ + public int getIpady() { + return this.ipady; + } + + /** + * @return weightx + * @since 2018-11-30 + * @since v0.1.0 + */ + public double getWeightx() { + return this.weightx; + } + + /** + * @return weighty + * @since 2018-11-30 + * @since v0.1.0 + */ + public double getWeighty() { + return this.weighty; + } + + /** + * @param anchor + * anchor to set + * @since 2018-11-30 + * @since v0.1.0 + */ + public GridBagBuilder setAnchor(final int anchor) { + this.anchor = anchor; + return this; + } + + /** + * @param fill + * fill to set + * @since 2018-11-30 + * @since v0.1.0 + */ + public GridBagBuilder setFill(final int fill) { + this.fill = fill; + return this; + } + + /** + * @param insets + * insets to set + * @since 2018-11-30 + * @since v0.1.0 + */ + public GridBagBuilder setInsets(final Insets insets) { + this.insets = insets; + return this; + } + + /** + * @param ipadx + * ipadx to set + * @since 2018-11-30 + * @since v0.1.0 + */ + public GridBagBuilder setIpadx(final int ipadx) { + this.ipadx = ipadx; + return this; + } + + /** + * @param ipady + * ipady to set + * @since 2018-11-30 + * @since v0.1.0 + */ + public GridBagBuilder setIpady(final int ipady) { + this.ipady = ipady; + return this; + } + + /** + * @param weightx + * weightx to set + * @since 2018-11-30 + * @since v0.1.0 + */ + public GridBagBuilder setWeightx(final double weightx) { + this.weightx = weightx; + return this; + } + + /** + * @param weighty + * weighty to set + * @since 2018-11-30 + * @since v0.1.0 + */ + public GridBagBuilder setWeighty(final double weighty) { + this.weighty = weighty; + return this; + } +} diff --git a/src/main/java/sevenUnitsGUI/MutablePredicate.java b/src/main/java/sevenUnitsGUI/MutablePredicate.java new file mode 100644 index 0000000..6cb8689 --- /dev/null +++ b/src/main/java/sevenUnitsGUI/MutablePredicate.java @@ -0,0 +1,70 @@ +/** + * Copyright (C) 2019 Adrien Hopkins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package sevenUnitsGUI; + +import java.util.function.Predicate; + +/** + * A container for a predicate, which can be changed later. + * + * @author Adrien Hopkins + * @since 2019-04-13 + * @since v0.2.0 + */ +final class MutablePredicate implements Predicate { + /** + * The predicate stored in this {@code MutablePredicate} + * + * @since 2019-04-13 + * @since v0.2.0 + */ + private Predicate predicate; + + /** + * Creates the {@code MutablePredicate}. + * + * @since 2019-04-13 + * @since v0.2.0 + */ + public MutablePredicate(final Predicate predicate) { + this.predicate = predicate; + } + + /** + * @return predicate + * @since 2019-04-13 + * @since v0.2.0 + */ + public final Predicate getPredicate() { + return this.predicate; + } + + /** + * @param predicate + * new value of predicate + * @since 2019-04-13 + * @since v0.2.0 + */ + public final void setPredicate(final Predicate predicate) { + this.predicate = predicate; + } + + @Override + public boolean test(final T t) { + return this.predicate.test(t); + } +} diff --git a/src/main/java/sevenUnitsGUI/Presenter.java b/src/main/java/sevenUnitsGUI/Presenter.java index 4373049..07671e4 100644 --- a/src/main/java/sevenUnitsGUI/Presenter.java +++ b/src/main/java/sevenUnitsGUI/Presenter.java @@ -16,12 +16,21 @@ */ package sevenUnitsGUI; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; import java.util.List; +import java.util.Scanner; import java.util.function.Function; import java.util.function.Predicate; +import java.util.stream.Collectors; +import sevenUnits.ProgramInfo; +import sevenUnits.unit.BaseDimension; +import sevenUnits.unit.Unit; import sevenUnits.unit.UnitDatabase; import sevenUnits.unit.UnitPrefix; +import sevenUnits.utils.ObjectProduct; import sevenUnits.utils.UncertainDouble; /** @@ -31,6 +40,62 @@ import sevenUnits.utils.UncertainDouble; * @since 2021-12-15 */ public final class Presenter { + /** + * @return text in About file + * @since 2022-02-19 + */ + static final String getAboutText() { + return Presenter.getLinesFromResource("/about.txt").stream() + .map(Presenter::withoutComments).collect(Collectors.joining("\n")) + .replaceAll("\\[VERSION\\]", ProgramInfo.VERSION); + } + + /** + * Gets the text of a resource file as a set of strings (each one is one line + * of the text). + * + * @param filename filename to get resource from + * @return contents of file + * @since 2021-03-27 + */ + private static final List getLinesFromResource(String filename) { + final List lines = new ArrayList<>(); + + try (InputStream stream = inputStream(filename); + Scanner scanner = new Scanner(stream)) { + while (scanner.hasNextLine()) { + lines.add(scanner.nextLine()); + } + } catch (final IOException e) { + throw new AssertionError( + "Error occurred while loading file " + filename, e); + } + + return lines; + } + + /** + * Gets an input stream for a resource file. + * + * @param filepath file to use as resource + * @return obtained Path + * @since 2021-03-27 + */ + private static final InputStream inputStream(String filepath) { + return Presenter.class.getResourceAsStream(filepath); + } + + /** + * @return {@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); + } + + // ====== SETTINGS ====== + /** * The view that this presenter communicates with */ @@ -41,8 +106,6 @@ public final class Presenter { */ private final UnitDatabase database; - // ====== SETTINGS ====== - /** * The rule used for parsing input numbers. Any number-string inputted into * this program will be parsed using this method. @@ -136,6 +199,8 @@ public final class Presenter { */ public void loadSettings() {} + void prefixSelected() {} + /** * Gets user settings from the view then saves them to the user's settings * file. @@ -143,4 +208,23 @@ public final class Presenter { * @since 2021-12-15 */ 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/SearchBoxList.java b/src/main/java/sevenUnitsGUI/SearchBoxList.java new file mode 100644 index 0000000..2b935d0 --- /dev/null +++ b/src/main/java/sevenUnitsGUI/SearchBoxList.java @@ -0,0 +1,331 @@ +/** + * Copyright (C) 2019 Adrien Hopkins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package sevenUnitsGUI; + +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.event.FocusEvent; +import java.awt.event.FocusListener; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.List; +import java.util.Optional; +import java.util.function.Predicate; + +import javax.swing.JList; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTextField; + +/** + * @param type of element in list + * @author Adrien Hopkins + * @since 2019-04-13 + * @since v0.2.0 + */ +final class SearchBoxList extends JPanel { + + /** + * @since 2019-04-13 + * @since v0.2.0 + */ + private static final long serialVersionUID = 6226930279415983433L; + + /** + * The text to place in an empty search box. + * + * @since 2019-04-13 + * @since v0.2.0 + */ + private static final String EMPTY_TEXT = "Search..."; + + /** + * The color to use for an empty foreground. + * + * @since 2019-04-13 + * @since v0.2.0 + */ + private static final Color EMPTY_FOREGROUND = new Color(192, 192, 192); + + // the components + private final Collection itemsToFilter; + private final DelegateListModel listModel; + private final JTextField searchBox; + private final JList searchItems; + + private boolean searchBoxEmpty = true; + + // I need to do this because, for some reason, Swing is auto-focusing my + // search box without triggering a focus + // event. + private boolean searchBoxFocused = false; + + private Predicate customSearchFilter = o -> true; + private final Comparator defaultOrdering; + private final boolean caseSensitive; + + /** + * Creates an empty SearchBoxList + * + * @since 2022-02-19 + */ + public SearchBoxList() { + this(List.of(), null, false); + } + + /** + * Creates the {@code SearchBoxList}. + * + * @param itemsToFilter items to put in the list + * @since 2019-04-14 + */ + public SearchBoxList(final Collection itemsToFilter) { + this(itemsToFilter, null, false); + } + + /** + * Creates the {@code SearchBoxList}. + * + * @param itemsToFilter items to put in the list + * @param defaultOrdering default ordering of items after filtration + * (null=Comparable) + * @param caseSensitive whether or not the filtration is case-sensitive + * + * @since 2019-04-13 + * @since v0.2.0 + */ + public SearchBoxList(final Collection itemsToFilter, + final Comparator defaultOrdering, final boolean caseSensitive) { + super(new BorderLayout(), true); + this.itemsToFilter = new ArrayList<>(itemsToFilter); + this.defaultOrdering = defaultOrdering; + this.caseSensitive = caseSensitive; + + // create the components + this.listModel = new DelegateListModel<>(new ArrayList<>(itemsToFilter)); + this.searchItems = new JList<>(this.listModel); + + this.searchBox = new JTextField(EMPTY_TEXT); + this.searchBox.setForeground(EMPTY_FOREGROUND); + + // add them to the panel + this.add(this.searchBox, BorderLayout.PAGE_START); + this.add(new JScrollPane(this.searchItems), BorderLayout.CENTER); + + // set up the search box + this.searchBox.addFocusListener(new FocusListener() { + @Override + public void focusGained(final FocusEvent e) { + SearchBoxList.this.searchBoxFocusGained(e); + } + + @Override + public void focusLost(final FocusEvent e) { + SearchBoxList.this.searchBoxFocusLost(e); + } + }); + + this.searchBox.addCaretListener(e -> this.searchBoxTextChanged()); + this.searchBoxEmpty = true; + } + + /** + * Adds an additional filter for searching. + * + * @param filter filter to add. + * @since 2019-04-13 + * @since v0.2.0 + */ + public void addSearchFilter(final Predicate filter) { + this.customSearchFilter = this.customSearchFilter.and(filter); + } + + /** + * Resets the search filter. + * + * @since 2019-04-13 + * @since v0.2.0 + */ + public void clearSearchFilters() { + this.customSearchFilter = o -> true; + } + + /** + * @return this component's search box component + * @since 2019-04-14 + * @since v0.2.0 + */ + public final JTextField getSearchBox() { + return this.searchBox; + } + + /** + * @param searchText text to search for + * @return a filter that filters out that text, based on this list's case + * sensitive setting + * @since 2019-04-14 + * @since v0.2.0 + */ + private Predicate getSearchFilter(final String searchText) { + if (this.caseSensitive) + return item -> item.toString().contains(searchText); + else + return item -> item.toString().toLowerCase() + .contains(searchText.toLowerCase()); + } + + /** + * @return this component's list component + * @since 2019-04-14 + * @since v0.2.0 + */ + public final JList getSearchList() { + return this.searchItems; + } + + /** + * @return index selected in item list, -1 if no selection + * @since 2019-04-14 + * @since v0.2.0 + */ + public int getSelectedIndex() { + return this.searchItems.getSelectedIndex(); + } + + /** + * @return value selected in item list + * @since 2019-04-13 + * @since v0.2.0 + */ + public Optional getSelectedValue() { + return Optional.ofNullable(this.searchItems.getSelectedValue()); + } + + /** + * Re-applies the filters. + * + * @since 2019-04-13 + * @since v0.2.0 + */ + public void reapplyFilter() { + final String searchText = this.searchBoxEmpty ? "" + : this.searchBox.getText(); + final FilterComparator comparator = new FilterComparator<>(searchText, + this.defaultOrdering, this.caseSensitive); + final Predicate searchFilter = this.getSearchFilter(searchText); + + this.listModel.clear(); + this.itemsToFilter.forEach(item -> { + if (searchFilter.test(item)) { + this.listModel.add(item); + } + }); + + // applies the custom filters + this.listModel.removeIf(this.customSearchFilter.negate()); + + // sorts the remaining items + this.listModel.sort(comparator); + } + + /** + * Runs whenever the search box gains focus. + * + * @param e focus event + * @since 2019-04-13 + * @since v0.2.0 + */ + private void searchBoxFocusGained(final FocusEvent e) { + this.searchBoxFocused = true; + if (this.searchBoxEmpty) { + this.searchBox.setText(""); + this.searchBox.setForeground(Color.BLACK); + } + } + + /** + * Runs whenever the search box loses focus. + * + * @param e focus event + * @since 2019-04-13 + * @since v0.2.0 + */ + private void searchBoxFocusLost(final FocusEvent e) { + this.searchBoxFocused = false; + if (this.searchBoxEmpty) { + this.searchBox.setText(EMPTY_TEXT); + this.searchBox.setForeground(EMPTY_FOREGROUND); + } + } + + /** + * Runs whenever the text in the search box is changed. + *

+ * Reapplies the search filter, and custom filters. + *

+ * + * @since 2019-04-14 + * @since v0.2.0 + */ + private void searchBoxTextChanged() { + if (this.searchBoxFocused) { + this.searchBoxEmpty = this.searchBox.getText().equals(""); + } + final String searchText = this.searchBoxEmpty ? "" + : this.searchBox.getText(); + final FilterComparator comparator = new FilterComparator<>(searchText, + this.defaultOrdering, this.caseSensitive); + final Predicate searchFilter = this.getSearchFilter(searchText); + + // initialize list with items that match the filter then sort + this.listModel.clear(); + this.itemsToFilter.forEach(string -> { + if (searchFilter.test(string)) { + this.listModel.add(string); + } + }); + + // applies the custom filters + this.listModel.removeIf(this.customSearchFilter.negate()); + + // sorts the remaining items + this.listModel.sort(comparator); + } + + /** + * Resets the search box list's contents to the provided items, removing any + * old items + * + * @param newItems new items to put in list + * @since 2021-05-22 + */ + public void setItems(Collection newItems) { + this.itemsToFilter.clear(); + this.itemsToFilter.addAll(newItems); + this.reapplyFilter(); + } + + /** + * Manually updates the search box's item list. + * + * @since 2020-08-27 + */ + public void updateList() { + this.searchBoxTextChanged(); + } +} diff --git a/src/main/java/sevenUnitsGUI/TabbedView.java b/src/main/java/sevenUnitsGUI/TabbedView.java new file mode 100644 index 0000000..e92b661 --- /dev/null +++ b/src/main/java/sevenUnitsGUI/TabbedView.java @@ -0,0 +1,605 @@ +/** + * Copyright (C) 2022 Adrien Hopkins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package sevenUnitsGUI; + +import java.awt.BorderLayout; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.GridLayout; +import java.awt.event.KeyEvent; +import java.text.DecimalFormat; +import java.text.NumberFormat; +import java.util.AbstractSet; +import java.util.Collections; +import java.util.Iterator; +import java.util.NoSuchElementException; +import java.util.Optional; +import java.util.Set; + +import javax.swing.BorderFactory; +import javax.swing.BoxLayout; +import javax.swing.ButtonGroup; +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JComboBox; +import javax.swing.JFormattedTextField; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JRadioButton; +import javax.swing.JScrollPane; +import javax.swing.JSlider; +import javax.swing.JTabbedPane; +import javax.swing.JTextArea; +import javax.swing.JTextField; +import javax.swing.SwingConstants; +import javax.swing.UIManager; +import javax.swing.UnsupportedLookAndFeelException; +import javax.swing.WindowConstants; +import javax.swing.border.EmptyBorder; +import javax.swing.border.TitledBorder; + +import sevenUnits.ProgramInfo; +import sevenUnits.unit.BaseDimension; +import sevenUnits.unit.Unit; +import sevenUnits.unit.UnitPrefix; +import sevenUnits.utils.NamedObjectProduct; +import sevenUnits.utils.ObjectProduct; + +/** + * A View that separates its functions into multiple tabs + * + * @since 2022-02-19 + */ +final class TabbedView implements ExpressionConversionView, UnitConversionView { + /** + * A List-like view of a JComboBox's items + * + * @param type of item in list + * + * @since 2022-02-19 + */ + private static final class JComboBoxItemSet extends AbstractSet { + private final JComboBox comboBox; + + /** + * @param comboBox combo box to get items from + * @since 2022-02-19 + */ + 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()) + return JComboBoxItemSet.this.comboBox.getItemAt(this.index++); + else + throw new NoSuchElementException( + "Iterator has finished iteration"); + } + }; + } + + @Override + public int size() { + return this.comboBox.getItemCount(); + } + + } + + private static final NumberFormat NUMBER_FORMATTER = new DecimalFormat(); + + /** + * Creates a TabbedView. + * + * @param args command line arguments + * @since 2022-02-19 + */ + public static void main(String[] args) { + // This view doesn't need to do anything, the side effects of creating it + // are enough to start the program + @SuppressWarnings("unused") + final View view = new TabbedView(); + } + + /** The Presenter that handles this View */ + final Presenter presenter; + /** The frame that this view lives on */ + final JFrame frame; + /** The tabbed pane that contains all of the components */ + final JTabbedPane masterPane; + + // DIMENSION-BASED CONVERTER + /** The combo box that selects dimensions */ + private final JComboBox> dimensionSelector; + /** The panel for inputting values in the dimension-based converter */ + private final JTextField valueInput; + /** The panel for "From" in the dimension-based converter */ + private final SearchBoxList fromSearch; + /** The panel for "To" in the dimension-based converter */ + private final SearchBoxList toSearch; + /** The output area in the dimension-based converter */ + private final JTextArea unitOutput; + + // EXPRESSION-BASED CONVERTER + /** The "From" entry in the conversion panel */ + private final JTextField fromEntry; + /** The "To" entry in the conversion panel */ + private final JTextField toEntry; + /** The output area in the conversion panel */ + private final JTextArea expressionOutput; + + // UNIT AND PREFIX VIEWERS + /** The searchable list of unit names in the unit viewer */ + private final SearchBoxList unitNameList; + /** The searchable list of prefix names in the prefix viewer */ + private final SearchBoxList prefixNameList; + /** The text box for unit data in the unit viewer */ + private final JTextArea unitTextBox; + /** The text box for prefix data in the prefix viewer */ + private final JTextArea prefixTextBox; + + /** + * Creates the view and makes it visible to the user + * + * @since 2022-02-19 + */ + public TabbedView() { + // enable system look and feel + try { + UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + } catch (ClassNotFoundException | InstantiationException + | IllegalAccessException | UnsupportedLookAndFeelException e) { + // oh well, just use default theme + System.err.println("Failed to enable system look-and-feel."); + e.printStackTrace(); + } + + // initialize important components + this.presenter = new Presenter(this); + this.frame = new JFrame("7Units " + ProgramInfo.VERSION); + this.frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); + + // master components (those that contain everything else within them) + this.masterPane = new JTabbedPane(); + this.frame.add(this.masterPane); + + // ============ UNIT CONVERSION TAB ============ + final JPanel convertUnitPanel = new JPanel(); + this.masterPane.addTab("Convert Units", convertUnitPanel); + this.masterPane.setMnemonicAt(0, KeyEvent.VK_U); + convertUnitPanel.setLayout(new BorderLayout()); + + { // panel for input part + final JPanel inputPanel = new JPanel(); + convertUnitPanel.add(inputPanel, BorderLayout.CENTER); + inputPanel.setLayout(new GridLayout(1, 3)); + inputPanel.setBorder(new EmptyBorder(6, 6, 3, 6)); + + this.fromSearch = new SearchBoxList<>(); + inputPanel.add(this.fromSearch); + + final JPanel inBetweenPanel = new JPanel(); + inputPanel.add(inBetweenPanel); + inBetweenPanel.setLayout(new BorderLayout()); + + this.dimensionSelector = new JComboBox<>(); + inBetweenPanel.add(this.dimensionSelector, BorderLayout.PAGE_START); + this.dimensionSelector + .addItemListener(e -> this.presenter.applyDimensionFilter()); + + 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 JFormattedTextField(NUMBER_FORMATTER); + outputPanel.add(this.valueInput, BorderLayout.CENTER); + + // conversion button + final JButton convertButton = new JButton("Convert"); + outputPanel.add(convertButton, BorderLayout.LINE_END); + convertButton.addActionListener(e -> this.presenter.convertUnits()); + convertButton.setMnemonic(KeyEvent.VK_ENTER); + + // conversion output + this.unitOutput = new JTextArea(2, 32); + outputPanel.add(this.unitOutput, BorderLayout.PAGE_END); + this.unitOutput.setEditable(false); + } + + // ============ EXPRESSION CONVERSION TAB ============ + final JPanel convertExpressionPanel = new JPanel(); + this.masterPane.addTab("Convert Unit Expressions", + convertExpressionPanel); + this.masterPane.setMnemonicAt(1, KeyEvent.VK_E); + convertExpressionPanel.setLayout(new GridLayout(4, 1)); + + // from and to expressions + this.fromEntry = new JTextField(); + convertExpressionPanel.add(this.fromEntry); + this.fromEntry.setBorder(BorderFactory.createTitledBorder("From")); + + this.toEntry = new JTextField(); + convertExpressionPanel.add(this.toEntry); + this.toEntry.setBorder(BorderFactory.createTitledBorder("To")); + + // button to convert + final JButton convertButton = new JButton("Convert"); + convertExpressionPanel.add(convertButton); + + convertButton.addActionListener(e -> this.presenter.convertExpressions()); + convertButton.setMnemonic(KeyEvent.VK_ENTER); + + // output of conversion + this.expressionOutput = new JTextArea(2, 32); + convertExpressionPanel.add(this.expressionOutput); + this.expressionOutput + .setBorder(BorderFactory.createTitledBorder("Output")); + this.expressionOutput.setEditable(false); + + // =========== UNIT VIEWER =========== + final JPanel unitLookupPanel = new JPanel(); + this.masterPane.addTab("Unit Viewer", unitLookupPanel); + this.masterPane.setMnemonicAt(2, KeyEvent.VK_V); + unitLookupPanel.setLayout(new GridLayout()); + + this.unitNameList = new SearchBoxList<>(); + unitLookupPanel.add(this.unitNameList); + this.unitNameList.getSearchList() + .addListSelectionListener(e -> this.presenter.unitNameSelected()); + + // the text box for unit's toString + this.unitTextBox = new JTextArea(); + unitLookupPanel.add(this.unitTextBox); + this.unitTextBox.setEditable(false); + this.unitTextBox.setLineWrap(true); + + // ============ PREFIX VIEWER ============= + final JPanel prefixLookupPanel = new JPanel(); + this.masterPane.addTab("Prefix Viewer", prefixLookupPanel); + this.masterPane.setMnemonicAt(3, KeyEvent.VK_P); + prefixLookupPanel.setLayout(new GridLayout(1, 2)); + + this.prefixNameList = new SearchBoxList<>(); + prefixLookupPanel.add(this.prefixNameList); + this.prefixNameList.getSearchList() + .addListSelectionListener(e -> this.presenter.prefixSelected()); + + // the text box for prefix's toString + this.prefixTextBox = new JTextArea(); + prefixLookupPanel.add(this.prefixTextBox); + this.prefixTextBox.setEditable(false); + this.prefixTextBox.setLineWrap(true); + + 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.frame.pack(); + this.frame.setVisible(true); + + } + + /** + * Creates and returns the settings panel (in its own function to make this + * code more organized, as this function is massive!) + * + * @since 2022-02-19 + */ + private JPanel createSettingsPanel() { + final JPanel settingsPanel = new JPanel(); + + settingsPanel + .setLayout(new BoxLayout(settingsPanel, BoxLayout.PAGE_AXIS)); + + // ============ ROUNDING SETTINGS ============ + { + final JPanel roundingPanel = new JPanel(); + settingsPanel.add(roundingPanel); + roundingPanel.setBorder(new TitledBorder("Rounding Settings")); + roundingPanel.setLayout(new GridBagLayout()); + + // rounding rule selection + final ButtonGroup roundingRuleButtons = new ButtonGroup(); + + final JLabel roundingRuleLabel = new JLabel("Rounding Rule:"); + roundingPanel.add(roundingRuleLabel, new GridBagBuilder(0, 0) + .setAnchor(GridBagConstraints.LINE_START).build()); + + final JRadioButton fixedPrecision = new JRadioButton( + "Fixed Precision"); +// if (this.presenter.roundingType == RoundingType.SIGNIFICANT_DIGITS) { +// fixedPrecision.setSelected(true); +// } +// fixedPrecision.addActionListener(e -> this.presenter +// .setRoundingType(RoundingType.SIGNIFICANT_DIGITS)); + roundingRuleButtons.add(fixedPrecision); + roundingPanel.add(fixedPrecision, new GridBagBuilder(0, 1) + .setAnchor(GridBagConstraints.LINE_START).build()); + + final JRadioButton fixedDecimals = new JRadioButton( + "Fixed Decimal Places"); +// if (this.presenter.roundingType == RoundingType.DECIMAL_PLACES) { +// fixedDecimals.setSelected(true); +// } +// fixedDecimals.addActionListener(e -> this.presenter +// .setRoundingType(RoundingType.DECIMAL_PLACES)); + roundingRuleButtons.add(fixedDecimals); + roundingPanel.add(fixedDecimals, new GridBagBuilder(0, 2) + .setAnchor(GridBagConstraints.LINE_START).build()); + + final JRadioButton relativePrecision = new JRadioButton( + "Scientific Precision"); +// if (this.presenter.roundingType == RoundingType.SCIENTIFIC) { +// relativePrecision.setSelected(true); +// } +// relativePrecision.addActionListener( +// e -> this.presenter.setRoundingType(RoundingType.SCIENTIFIC)); + roundingRuleButtons.add(relativePrecision); + roundingPanel.add(relativePrecision, new GridBagBuilder(0, 3) + .setAnchor(GridBagConstraints.LINE_START).build()); + + final JLabel sliderLabel = new JLabel("Precision:"); + roundingPanel.add(sliderLabel, new GridBagBuilder(0, 4) + .setAnchor(GridBagConstraints.LINE_START).build()); + + final JSlider sigDigSlider = new JSlider(0, 12); + roundingPanel.add(sigDigSlider, new GridBagBuilder(0, 5) + .setAnchor(GridBagConstraints.LINE_START).build()); + + sigDigSlider.setMajorTickSpacing(4); + sigDigSlider.setMinorTickSpacing(1); + sigDigSlider.setSnapToTicks(true); + sigDigSlider.setPaintTicks(true); + sigDigSlider.setPaintLabels(true); +// sigDigSlider.setValue(this.presenter.precision); + +// sigDigSlider.addChangeListener( +// e -> this.presenter.setPrecision(sigDigSlider.getValue())); + } + + // ============ PREFIX REPETITION SETTINGS ============ + { + final JPanel prefixRepetitionPanel = new JPanel(); + settingsPanel.add(prefixRepetitionPanel); + prefixRepetitionPanel + .setBorder(new TitledBorder("Prefix Repetition Settings")); + prefixRepetitionPanel.setLayout(new GridBagLayout()); + + // prefix rules + final ButtonGroup prefixRuleButtons = new ButtonGroup(); + + final JRadioButton noRepetition = new JRadioButton("No Repetition"); +// if (this.presenter.prefixRule == DefaultPrefixRepetitionRule.NO_REPETITION) { +// noRepetition.setSelected(true); +// } +// noRepetition +// .addActionListener(e -> this.presenter.setPrefixRepetitionRule( +// DefaultPrefixRepetitionRule.NO_REPETITION)); + prefixRuleButtons.add(noRepetition); + prefixRepetitionPanel.add(noRepetition, new GridBagBuilder(0, 0) + .setAnchor(GridBagConstraints.LINE_START).build()); + + final JRadioButton noRestriction = new JRadioButton("No Restriction"); +// if (this.presenter.prefixRule == DefaultPrefixRepetitionRule.NO_RESTRICTION) { +// noRestriction.setSelected(true); +// } +// noRestriction +// .addActionListener(e -> this.presenter.setPrefixRepetitionRule( +// DefaultPrefixRepetitionRule.NO_RESTRICTION)); + prefixRuleButtons.add(noRestriction); + prefixRepetitionPanel.add(noRestriction, new GridBagBuilder(0, 1) + .setAnchor(GridBagConstraints.LINE_START).build()); + + final JRadioButton customRepetition = new JRadioButton( + "Complex Repetition"); +// if (this.presenter.prefixRule == DefaultPrefixRepetitionRule.COMPLEX_REPETITION) { +// customRepetition.setSelected(true); +// } +// customRepetition +// .addActionListener(e -> this.presenter.setPrefixRepetitionRule( +// DefaultPrefixRepetitionRule.COMPLEX_REPETITION)); + prefixRuleButtons.add(customRepetition); + prefixRepetitionPanel.add(customRepetition, new GridBagBuilder(0, 2) + .setAnchor(GridBagConstraints.LINE_START).build()); + } + + // ============ SEARCH SETTINGS ============ + { + final JPanel searchingPanel = new JPanel(); + settingsPanel.add(searchingPanel); + searchingPanel.setBorder(new TitledBorder("Search Settings")); + searchingPanel.setLayout(new GridBagLayout()); + + // searching rules + final ButtonGroup searchRuleButtons = new ButtonGroup(); + + final JRadioButton noPrefixes = new JRadioButton( + "Never Include Prefixed Units"); + noPrefixes.setEnabled(false); + searchRuleButtons.add(noPrefixes); + searchingPanel.add(noPrefixes, new GridBagBuilder(0, 0) + .setAnchor(GridBagConstraints.LINE_START).build()); + + final JRadioButton fixedPrefixes = new JRadioButton( + "Include Some Prefixes"); + fixedPrefixes.setEnabled(false); + searchRuleButtons.add(fixedPrefixes); + searchingPanel.add(fixedPrefixes, new GridBagBuilder(0, 1) + .setAnchor(GridBagConstraints.LINE_START).build()); + + final JRadioButton explicitPrefixes = new JRadioButton( + "Include Explicit Prefixes"); + explicitPrefixes.setEnabled(false); + searchRuleButtons.add(explicitPrefixes); + searchingPanel.add(explicitPrefixes, new GridBagBuilder(0, 2) + .setAnchor(GridBagConstraints.LINE_START).build()); + + final JRadioButton alwaysInclude = new JRadioButton( + "Include All Single Prefixes"); + alwaysInclude.setEnabled(false); + searchRuleButtons.add(alwaysInclude); + searchingPanel.add(alwaysInclude, new GridBagBuilder(0, 3) + .setAnchor(GridBagConstraints.LINE_START).build()); + } + + // ============ 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.oneWay); +// oneWay.addItemListener( +// e -> this.presenter.setOneWay(e.getStateChange() == 1)); + miscPanel.add(oneWay, new GridBagBuilder(0, 0) + .setAnchor(GridBagConstraints.LINE_START).build()); + + final JCheckBox showAllVariations = new JCheckBox( + "Show Duplicates in \"Convert Units\""); +// showAllVariations.setSelected(this.presenter.includeDuplicateUnits); +// showAllVariations.addItemListener(e -> this.presenter +// .setIncludeDuplicateUnits(e.getStateChange() == 1)); + miscPanel.add(showAllVariations, new GridBagBuilder(0, 1) + .setAnchor(GridBagConstraints.LINE_START).build()); + + final JButton unitFileButton = new JButton("Manage Unit Data Files"); + unitFileButton.setEnabled(false); + miscPanel.add(unitFileButton, new GridBagBuilder(0, 2) + .setAnchor(GridBagConstraints.LINE_START).build()); + } + + return settingsPanel; + } + + @Override + public Set> getDimensions() { + 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 String getInputValue() { + return this.valueInput.getText(); + } + + @Override + public Optional> getSelectedDimension() { + // this must work because this function can only return items that are in + // the selector, which are all of type ObjectProduct + @SuppressWarnings("unchecked") + final ObjectProduct selectedItem = (ObjectProduct) 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 void setDimensions( + Set> dimensions) { + this.dimensionSelector.removeAllItems(); + for (final NamedObjectProduct d : dimensions) { + this.dimensionSelector.addItem(d); + } + } + + @Override + public void setFromUnits(Set units) { + this.fromSearch.setItems(units); + } + + @Override + public void setToUnits(Set units) { + this.toSearch.setItems(units); + } + + @Override + public void showErrorMessage(String title, String message) { + JOptionPane.showMessageDialog(this.frame, message, title, + JOptionPane.ERROR_MESSAGE); + } + + @Override + public void showExpressionConversionOutput(String fromExpression, + String toExpression, double value) { + this.expressionOutput.setText( + String.format("%s = %s %s", fromExpression, value, toExpression)); + } + + @Override + public void showUnitConversionOutput(String outputString) { + this.unitOutput.setText(outputString); + } + +} diff --git a/src/main/java/sevenUnitsGUI/UnitConversionView.java b/src/main/java/sevenUnitsGUI/UnitConversionView.java index 97ec30f..5fd5a82 100644 --- a/src/main/java/sevenUnitsGUI/UnitConversionView.java +++ b/src/main/java/sevenUnitsGUI/UnitConversionView.java @@ -16,8 +16,8 @@ */ package sevenUnitsGUI; -import java.util.List; import java.util.Optional; +import java.util.Set; import sevenUnits.unit.BaseDimension; import sevenUnits.unit.Unit; @@ -35,7 +35,7 @@ public interface UnitConversionView extends View { * @return dimensions available for filtering * @since 2022-01-29 */ - List> getDimensions(); + Set> getDimensions(); /** * @return unit to convert from @@ -68,7 +68,7 @@ public interface UnitConversionView extends View { * @param dimensions dimensions to use * @since 2021-12-15 */ - void setDimensions(List> dimensions); + void setDimensions(Set> dimensions); /** * Sets the available units to convert from. {@link #getFromSelection} is not @@ -78,7 +78,7 @@ public interface UnitConversionView extends View { * @param units units to convert from * @since 2021-12-15 */ - void setFromUnits(List units); + void setFromUnits(Set units); /** * Sets the available units to convert to. {@link #getToSelection} is not @@ -88,7 +88,7 @@ public interface UnitConversionView extends View { * @param units units to convert to * @since 2021-12-15 */ - void setToUnits(List units); + void setToUnits(Set units); /** * Shows the output of a unit conversion. diff --git a/src/main/java/sevenUnitsGUI/ViewBot.java b/src/main/java/sevenUnitsGUI/ViewBot.java index cc070e2..0c0d189 100644 --- a/src/main/java/sevenUnitsGUI/ViewBot.java +++ b/src/main/java/sevenUnitsGUI/ViewBot.java @@ -20,6 +20,7 @@ import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.Optional; +import java.util.Set; import sevenUnits.unit.BaseDimension; import sevenUnits.unit.Unit; @@ -38,7 +39,7 @@ final class ViewBot implements UnitConversionView, ExpressionConversionView { private final Presenter presenter; /** The dimensions available to select from */ - private List> dimensions; + private Set> dimensions; /** The expression in the From field */ private String fromExpression; /** The expression in the To field */ @@ -54,9 +55,9 @@ final class ViewBot implements UnitConversionView, ExpressionConversionView { /** The currently selected dimension */ private Optional> selectedDimension; /** The units available in the From selection */ - private List fromUnits; + private Set fromUnits; /** The units available in the To selection */ - private List toUnits; + private Set toUnits; /** Saved output values of all unit conversions */ private List unitConversionOutputValues; @@ -74,7 +75,7 @@ final class ViewBot implements UnitConversionView, ExpressionConversionView { * @since 2022-01-29 */ @Override - public List> getDimensions() { + public Set> getDimensions() { return this.dimensions; } @@ -96,8 +97,8 @@ final class ViewBot implements UnitConversionView, ExpressionConversionView { * @return the units available for selection in From * @since 2022-01-29 */ - public List getFromUnits() { - return Collections.unmodifiableList(this.fromUnits); + public Set getFromUnits() { + return Collections.unmodifiableSet(this.fromUnits); } @Override @@ -132,8 +133,8 @@ final class ViewBot implements UnitConversionView, ExpressionConversionView { * @return the units available for selection in To * @since 2022-01-29 */ - public List getToUnits() { - return Collections.unmodifiableList(this.toUnits); + public Set getToUnits() { + return Collections.unmodifiableSet(this.toUnits); } /** @@ -146,7 +147,7 @@ final class ViewBot implements UnitConversionView, ExpressionConversionView { @Override public void setDimensions( - List> dimensions) { + Set> dimensions) { this.dimensions = Objects.requireNonNull(dimensions, "dimensions may not be null"); } @@ -181,7 +182,7 @@ final class ViewBot implements UnitConversionView, ExpressionConversionView { } @Override - public void setFromUnits(List units) { + public void setFromUnits(Set units) { this.fromUnits = Objects.requireNonNull(units, "units may not be null"); } @@ -233,7 +234,7 @@ final class ViewBot implements UnitConversionView, ExpressionConversionView { } @Override - public void setToUnits(List units) { + public void setToUnits(Set units) { this.toUnits = Objects.requireNonNull(units, "units may not be null"); } diff --git a/src/main/resources/about.txt b/src/main/resources/about.txt index f175396..7780db3 100644 --- a/src/main/resources/about.txt +++ b/src/main/resources/about.txt @@ -2,7 +2,7 @@ About 7Units v[VERSION] Copyright Notice: -Unit Converter Copyright (C) 2018-2021 Adrien Hopkins +Unit Converter Copyright (C) 2018-2022 Adrien Hopkins This program comes with ABSOLUTELY NO WARRANTY; for details read the LICENSE file, section 15 diff --git a/src/test/java/sevenUnitsGUI/PresenterTest.java b/src/test/java/sevenUnitsGUI/PresenterTest.java index 675e3ab..3e7c2b5 100644 --- a/src/test/java/sevenUnitsGUI/PresenterTest.java +++ b/src/test/java/sevenUnitsGUI/PresenterTest.java @@ -21,6 +21,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.List; import java.util.Optional; +import java.util.Set; import org.junit.jupiter.api.Test; @@ -36,9 +37,9 @@ import sevenUnits.utils.NamedObjectProduct; * @since 2022-02-10 */ public final class PresenterTest { - List testUnits = List.of(Metric.METRE, Metric.KILOMETRE, + Set testUnits = Set.of(Metric.METRE, Metric.KILOMETRE, Metric.METRE_PER_SECOND, Metric.KILOMETRE_PER_HOUR); - List> testDimensions = List.of( + Set> testDimensions = Set.of( Metric.Dimensions.LENGTH.withName(NameSymbol.ofName("Length")), Metric.Dimensions.VELOCITY.withName(NameSymbol.ofName("Velocity"))); @@ -56,12 +57,13 @@ public final class PresenterTest { viewBot.setFromUnits(this.testUnits); viewBot.setToUnits(this.testUnits); viewBot.setDimensions(this.testDimensions); - viewBot.setSelectedDimension(Optional.of(this.testDimensions.get(0))); + viewBot.setSelectedDimension( + Optional.of(this.testDimensions.iterator().next())); // filter to length units only, then get the filtered sets of units presenter.applyDimensionFilter(); - final List fromUnits = viewBot.getFromUnits(); - final List toUnits = viewBot.getToUnits(); + 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 -- cgit v1.2.3 From 63740b955b5baf955cac4f720a4c75f576d645f4 Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Sun, 20 Feb 2022 10:30:55 -0500 Subject: Made the version number an object, changing it to 0.4.0-alpha+dev --- CHANGELOG.org | 1 + src/main/java/sevenUnits/ProgramInfo.java | 4 +- .../java/sevenUnits/SemanticVersionNumber.java | 691 +++++++++++++++++++++ .../sevenUnits/converterGUI/SevenUnitsGUI.java | 4 +- src/main/java/sevenUnitsGUI/Presenter.java | 2 +- src/test/java/sevenUnits/SemanticVersionTest.java | 399 ++++++++++++ 6 files changed, 1097 insertions(+), 4 deletions(-) create mode 100644 src/main/java/sevenUnits/SemanticVersionNumber.java create mode 100644 src/test/java/sevenUnits/SemanticVersionTest.java diff --git a/CHANGELOG.org b/CHANGELOG.org index bb6185e..2abee52 100644 --- a/CHANGELOG.org +++ b/CHANGELOG.org @@ -3,6 +3,7 @@ ** Unreleased *** Added - Added tests for the GUI + - Added an object for the version numbers (SemanticVersionNumber) *** Changed - Rewrote the GUI code internally using an MVP model to make it easier to maintain and improve - Tweaked the look of the unit and expression conversion sections of the view diff --git a/src/main/java/sevenUnits/ProgramInfo.java b/src/main/java/sevenUnits/ProgramInfo.java index 0d67824..876367d 100644 --- a/src/main/java/sevenUnits/ProgramInfo.java +++ b/src/main/java/sevenUnits/ProgramInfo.java @@ -24,7 +24,9 @@ package sevenUnits; */ public final class ProgramInfo { - public static final String VERSION = "0.4.0-dev"; + /** The version number (0.4.0-alpha+dev) */ + public static final SemanticVersionNumber VERSION = SemanticVersionNumber + .builder(0, 4, 0).preRelease("alpha").buildMetadata("dev").build(); private ProgramInfo() { // this class is only for static variables, you shouldn't be able to diff --git a/src/main/java/sevenUnits/SemanticVersionNumber.java b/src/main/java/sevenUnits/SemanticVersionNumber.java new file mode 100644 index 0000000..01aeb27 --- /dev/null +++ b/src/main/java/sevenUnits/SemanticVersionNumber.java @@ -0,0 +1,691 @@ +/** + * Copyright (C) 2022 Adrien Hopkins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package sevenUnits; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * A version number in the Semantic Versioning + * scheme + *

+ * Each version number has three main parts: + *

    + *
  1. The major version, which increments when backwards incompatible changes + * are made + *
  2. The minor version, which increments when backwards compatible feature + * changes are made + *
  3. The patch version, which increments when backwards compatible bug fixes + * are made + *
+ * + * @since 2022-02-19 + */ +public final class SemanticVersionNumber + implements Comparable { + /** + * A builder that can be used to create complex version numbers. + *

+ * Note: None of this builder's methods tolerate null arguments, arrays + * containing nulls, negative numbers, or non-alphanumeric identifiers. Nulls + * throw NullPointerExceptions, everything else throws + * IllegalArgumentException. + * + * @since 2022-02-19 + */ + public static final class Builder { + private final int major; + private final int minor; + private final int patch; + private final List preReleaseIdentifiers; + private final List buildMetadata; + + /** + * Creates a builder which can be used to create a + * {@code SemanticVersionNumber} + * + * @param major major version number of final version + * @param minor minor version number of final version + * @param patch patch version number of final version + * @since 2022-02-19 + */ + private Builder(int major, int minor, int patch) { + this.major = major; + this.minor = minor; + this.patch = patch; + this.preReleaseIdentifiers = new ArrayList<>(); + this.buildMetadata = new ArrayList<>(); + } + + /** + * @return version number created by this builder + * @since 2022-02-19 + */ + public SemanticVersionNumber build() { + return new SemanticVersionNumber(this.major, this.minor, this.patch, + this.preReleaseIdentifiers, this.buildMetadata); + } + + /** + * Adds one or more build metadata identifiers + * + * @param identifiers build metadata + * @return this builder + * @since 2022-02-19 + */ + public Builder buildMetadata(List identifiers) { + Objects.requireNonNull(identifiers, "identifiers may not be null"); + for (final String identifier : identifiers) { + Objects.requireNonNull(identifier, "identifier may not be null"); + if (!VALID_IDENTIFIER.matcher(identifier).matches()) + throw new IllegalArgumentException( + String.format("Invalid identifier \"%s\"", identifier)); + this.buildMetadata.add(identifier); + } + return this; + } + + /** + * Adds one or more build metadata identifiers + * + * @param identifiers build metadata + * @return this builder + * @since 2022-02-19 + */ + public Builder buildMetadata(String... identifiers) { + Objects.requireNonNull(identifiers, "identifiers may not be null"); + for (final String identifier : identifiers) { + Objects.requireNonNull(identifier, "identifier may not be null"); + if (!VALID_IDENTIFIER.matcher(identifier).matches()) + throw new IllegalArgumentException( + String.format("Invalid identifier \"%s\"", identifier)); + this.buildMetadata.add(identifier); + } + return this; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (!(obj instanceof Builder)) + return false; + final Builder other = (Builder) obj; + return Objects.equals(this.buildMetadata, other.buildMetadata) + && this.major == other.major && this.minor == other.minor + && this.patch == other.patch && Objects.equals( + this.preReleaseIdentifiers, other.preReleaseIdentifiers); + } + + @Override + public int hashCode() { + return Objects.hash(this.buildMetadata, this.major, this.minor, + this.patch, this.preReleaseIdentifiers); + } + + /** + * Adds one or more numeric identifiers to the version number + * + * @param identifiers pre-release identifier(s) to add + * @return this builder + * @since 2022-02-19 + */ + public Builder preRelease(int... identifiers) { + Objects.requireNonNull(identifiers, "identifiers may not be null"); + for (final int identifier : identifiers) { + if (identifier < 0) + throw new IllegalArgumentException( + "Numeric identifiers may not be negative"); + this.preReleaseIdentifiers.add(Integer.toString(identifier)); + } + return this; + } + + /** + * Adds one or more pre-release identifier(s) to the version number + * + * @param identifiers pre-release identifier(s) to add + * @return this builder + * @since 2022-02-19 + */ + public Builder preRelease(List identifiers) { + Objects.requireNonNull(identifiers, "identifiers may not be null"); + for (final String identifier : identifiers) { + Objects.requireNonNull(identifier, "identifier may not be null"); + if (!VALID_IDENTIFIER.matcher(identifier).matches()) + throw new IllegalArgumentException( + String.format("Invalid identifier \"%s\"", identifier)); + this.preReleaseIdentifiers.add(identifier); + } + return this; + } + + /** + * Adds one or more pre-release identifier(s) to the version number + * + * @param identifiers pre-release identifier(s) to add + * @return this builder + * @since 2022-02-19 + */ + public Builder preRelease(String... identifiers) { + Objects.requireNonNull(identifiers, "identifiers may not be null"); + for (final String identifier : identifiers) { + Objects.requireNonNull(identifier, "identifier may not be null"); + if (!VALID_IDENTIFIER.matcher(identifier).matches()) + throw new IllegalArgumentException( + String.format("Invalid identifier \"%s\"", identifier)); + this.preReleaseIdentifiers.add(identifier); + } + return this; + } + + /** + * Adds a string identifier and an integer identifer to pre-release data + * + * @param identifier1 first identifier + * @param identifier2 second identifier + * @return this builder + * @since 2022-02-19 + */ + public Builder preRelease(String identifier1, int identifier2) { + Objects.requireNonNull(identifier1, "identifier1 may not be null"); + if (!VALID_IDENTIFIER.matcher(identifier1).matches()) + throw new IllegalArgumentException( + String.format("Invalid identifier \"%s\"", identifier1)); + if (identifier2 < 0) + throw new IllegalArgumentException( + "Integer identifier cannot be negative"); + this.preReleaseIdentifiers.add(identifier1); + this.preReleaseIdentifiers.add(Integer.toString(identifier2)); + return this; + } + + @Override + public String toString() { + return "Semantic Version Builder: " + this.build().toString(); + } + } + + /** + * An alternative comparison method for version numbers. This uses the + * version's natural order, but the build metadata will be compared (using + * the same rules as pre-release identifiers) if everything else is equal. + *

+ * This ordering is consistent with equals, unlike + * {@code SemanticVersionNumber}'s natural ordering. + */ + public static final Comparator BUILD_METADATA_COMPARATOR = new Comparator<>() { + @Override + public int compare(SemanticVersionNumber o1, SemanticVersionNumber o2) { + Objects.requireNonNull(o1, "o1 may not be null"); + Objects.requireNonNull(o2, "o2 may not be null"); + final int naturalComparison = o1.compareTo(o2); + if (naturalComparison == 0) + return SemanticVersionNumber.compare(o1.buildMetadata, + o2.buildMetadata); + else + return naturalComparison; + }; + }; + + /** The alphanumeric pattern all identifiers must follow */ + private static final Pattern VALID_IDENTIFIER = Pattern + .compile("[0-9A-Za-z-]+"); + + /** The numeric pattern which causes special behaviour */ + private static final Pattern NUMERIC_IDENTIFER = Pattern.compile("[0-9]+"); + + /** The pattern for a version number */ + private static final Pattern VERSION_NUMBER = Pattern + .compile("(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)" // main + // version + + "(?:-([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?" // pre-release + + "(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?"); // build data + + /** + * Creates a builder that can be used to create a version number + * + * @param major major version number of final version + * @param minor minor version number of final version + * @param patch patch version number of final version + * @return version number builder + * @throws IllegalArgumentException if any argument is negative + * @since 2022-02-19 + */ + public static final SemanticVersionNumber.Builder builder(int major, + int minor, int patch) { + if (major < 0) + throw new IllegalArgumentException( + "Major version must be non-negative."); + if (minor < 0) + throw new IllegalArgumentException( + "Minor version must be non-negative."); + if (patch < 0) + throw new IllegalArgumentException( + "Patch version must be non-negative."); + return new SemanticVersionNumber.Builder(major, minor, patch); + } + + /** + * Compares two lists of strings based on SemVer's precedence rules + * + * @param a first list + * @param b second list + * @return result of comparison as in a comparator + * @see Comparator + * @since 2022-02-20 + */ + private static final int compare(List a, List b) { + // test pre-release size + final int aSize = a.size(); + final int bSize = b.size(); + + // no identifiers is greater than any identifiers + if (aSize != 0 && bSize == 0) + return -1; + else if (aSize == 0 && bSize != 0) + return 1; + + // test identifiers one by one + for (int i = 0; i < Math.min(aSize, bSize); i++) { + final String aElement = a.get(i); + final String bElement = b.get(i); + + if (NUMERIC_IDENTIFER.matcher(aElement).matches()) { + if (NUMERIC_IDENTIFER.matcher(bElement).matches()) { + // both are numbers, compare them + final int aNumber = Integer.parseInt(aElement); + final int bNumber = Integer.parseInt(bElement); + + if (aNumber < bNumber) + return -1; + else if (aNumber > bNumber) + return 1; + } else + // aElement is a number and bElement is not a number + // by the rules, a goes before b + return -1; + } else { + if (NUMERIC_IDENTIFER.matcher(bElement).matches()) + // aElement is not a number but bElement is + // by the rules, a goes after b + return 1; + else { + // both are not numbers, compare them + final int comparison = aElement.compareTo(bElement); + if (comparison != 0) + return comparison; + } + } + } + + // we just tested the stuff that's in common, maybe someone has more + if (aSize < bSize) + return -1; + else if (aSize > bSize) + return 1; + else + return 0; + } + + /** + * Gets a version number from a string in the official format + * + * @param versionString string to parse + * @return {@code SemanticVersionNumber} instance + * @since 2022-02-19 + * @see {@link #toString} + */ + public static final SemanticVersionNumber fromString(String versionString) { + // parse & validate version string + Objects.requireNonNull(versionString, "versionString may not be null"); + final Matcher m = VERSION_NUMBER.matcher(versionString); + if (!m.matches()) + throw new IllegalArgumentException( + String.format("Provided string \"%s\" is not a version number", + versionString)); + + // main parts + final int major = Integer.parseInt(m.group(1)); + final int minor = Integer.parseInt(m.group(2)); + final int patch = Integer.parseInt(m.group(3)); + + // pre release + final List preRelease; + if (m.group(4) == null) { + preRelease = List.of(); + } else { + preRelease = Arrays.asList(m.group(4).split("\\.")); + } + + // build metadata + final List buildMetadata; + if (m.group(5) == null) { + buildMetadata = List.of(); + } else { + buildMetadata = Arrays.asList(m.group(5).split("\\.")); + } + + // return number + return new SemanticVersionNumber(major, minor, patch, preRelease, + buildMetadata); + } + + /** + * Tests whether a string is a valid Semantic Version string + * + * @param versionString string to test + * @return true iff string is valid + * @since 2022-02-19 + */ + public static final boolean isValidVersionString(String versionString) { + return VERSION_NUMBER.matcher(versionString).matches(); + } + + /** + * Creates a simple pre-release version number of the form + * MAJOR.MINOR.PATH-TYPE.NUMBER (e.g. 1.2.3-alpha.4). + * + * @param major major version number + * @param minor minor version number + * @param patch patch version number + * @param preReleaseType first pre-release element + * @param preReleaseNumber second pre-release element + * @return {@code SemanticVersionNumber} instance + * @throws IllegalArgumentException if any argument is negative or if the + * preReleaseType is null, empty or not + * alphanumeric (0-9, A-Z, a-z, - only) + * @since 2022-02-19 + */ + public static final SemanticVersionNumber preRelease(int major, int minor, + int patch, String preReleaseType, int preReleaseNumber) { + if (major < 0) + throw new IllegalArgumentException( + "Major version must be non-negative."); + if (minor < 0) + throw new IllegalArgumentException( + "Minor version must be non-negative."); + if (patch < 0) + throw new IllegalArgumentException( + "Patch version must be non-negative."); + Objects.requireNonNull(preReleaseType, "preReleaseType may not be null"); + if (!VALID_IDENTIFIER.matcher(preReleaseType).matches()) + throw new IllegalArgumentException( + String.format("Invalid identifier \"%s\".", preReleaseType)); + if (preReleaseNumber < 0) + throw new IllegalArgumentException( + "Pre-release number must be non-negative."); + return new SemanticVersionNumber(major, minor, patch, + List.of(preReleaseType, Integer.toString(preReleaseNumber)), + List.of()); + } + + /** + * Creates a {@code SemanticVersionNumber} instance without pre-release + * identifiers or build metadata. + *

+ * Note: this method allows you to create versions with major version number + * 0, even though these versions would not be considered stable. + * + * @param major major version number + * @param minor minor version number + * @param patch patch version number + * @return {@code SemanticVersionNumber} instance + * @throws IllegalArgumentException if any argument is negative + * @since 2022-02-19 + */ + public static final SemanticVersionNumber stableVersion(int major, int minor, + int patch) { + if (major < 0) + throw new IllegalArgumentException( + "Major version must be non-negative."); + if (minor < 0) + throw new IllegalArgumentException( + "Minor version must be non-negative."); + if (patch < 0) + throw new IllegalArgumentException( + "Patch version must be non-negative."); + return new SemanticVersionNumber(major, minor, patch, List.of(), + List.of()); + } + + // parts of the version number + private final int major; + private final int minor; + private final int patch; + private final List preReleaseIdentifiers; + private final List buildMetadata; + + /** + * Creates a version number + * + * @param major major version number + * @param minor minor version number + * @param patch patch version number + * @param preReleaseIdentifiers pre-release version data + * @param buildMetadata build metadata + * @since 2022-02-19 + */ + private SemanticVersionNumber(int major, int minor, int patch, + List preReleaseIdentifiers, List buildMetadata) { + this.major = major; + this.minor = minor; + this.patch = patch; + this.preReleaseIdentifiers = preReleaseIdentifiers; + this.buildMetadata = buildMetadata; + } + + /** + * @return build metadata (empty if there is none) + * @since 2022-02-19 + */ + public List buildMetadata() { + return Collections.unmodifiableList(this.buildMetadata); + } + + /** + * Compares two version numbers according to the official Semantic Versioning + * order. + *

+ * Note: this ordering is not consistent with equals. Specifically, two + * versions that are identical except for their build metadata will be + * considered different by equals but the same by this method. This is + * required to follow the official Semantic Versioning specification. + *

+ */ + @Override + public int compareTo(SemanticVersionNumber o) { + // test the three big numbers in order first + if (this.major < o.major) + return -1; + else if (this.major > o.major) + return 1; + + if (this.minor < o.minor) + return -1; + else if (this.minor > o.minor) + return 1; + + if (this.patch < o.patch) + return -1; + else if (this.patch > o.patch) + return 1; + + // now we just compare pre-release identifiers + // (remember: build metadata is ignored) + return SemanticVersionNumber.compare(this.preReleaseIdentifiers, + o.preReleaseIdentifiers); + } + + /** + * Determines the compatibility of code written for this version to + * {@code other}. More specifically: + *

+ * If this function returns true, then there should be no problems + * upgrading code written for this version to version {@code other} as long + * as: + *

    + *
  • Semantic Versioning is being used properly + *
  • Your code doesn't depend on unintended features (if it does, it isn't + * necessarily compatible with any other version) + *
+ * If this function returns false, you may have to change your code to + * upgrade it to {@code other} + * + *

+ * Two version numbers that are identical (ignoring build metadata) are + * always compatible. Different version numbers are compatible as long as: + *

    + *
  • The major version number is not 0 (if it is, the API is considered + * unstable and any upgrade can be backwards compatible) + *
  • The major version number is the same (changing the major version + * number implies bacwards incompatible changes) + *
  • This version comes before the other one in the official precedence + * order (downgrading can remove features you depend on) + *
+ * + * @param other version to compare with + * @return true if you can definitely upgrade to {@code other} without + * changing code + * @since 2022-02-20 + */ + public boolean compatibleWith(SemanticVersionNumber other) { + Objects.requireNonNull(other, "other may not be null"); + + return this.compareTo(other) == 0 || this.major != 0 + && this.major == other.major && this.compareTo(other) < 0; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (!(obj instanceof SemanticVersionNumber)) + return false; + final SemanticVersionNumber other = (SemanticVersionNumber) obj; + if (this.buildMetadata == null) { + if (other.buildMetadata != null) + return false; + } else if (!this.buildMetadata.equals(other.buildMetadata)) + return false; + if (this.major != other.major) + return false; + if (this.minor != other.minor) + return false; + if (this.patch != other.patch) + return false; + if (this.preReleaseIdentifiers == null) { + if (other.preReleaseIdentifiers != null) + return false; + } else if (!this.preReleaseIdentifiers + .equals(other.preReleaseIdentifiers)) + return false; + return true; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + + (this.buildMetadata == null ? 0 : this.buildMetadata.hashCode()); + result = prime * result + this.major; + result = prime * result + this.minor; + result = prime * result + this.patch; + result = prime * result + (this.preReleaseIdentifiers == null ? 0 + : this.preReleaseIdentifiers.hashCode()); + return result; + } + + /** + * @return true iff this version is stable (major version > 0 and not a + * pre-release) + * @since 2022-02-19 + */ + public boolean isStable() { + return this.major > 0 && this.preReleaseIdentifiers.isEmpty(); + } + + /** + * @return the MAJOR version number, incremented when you make backwards + * incompatible API changes + * @since 2022-02-19 + */ + public int majorVersion() { + return this.major; + } + + /** + * @return the MINOR version number, incremented when you add backwards + * compatible functionality + * @since 2022-02-19 + */ + public int minorVersion() { + return this.minor; + } + + /** + * @return the PATCH version number, incremented when you make backwards + * compatible bug fixes + * @since 2022-02-19 + */ + public int patchVersion() { + return this.patch; + } + + /** + * @return identifiers describing this pre-release (empty if not a + * pre-release) + * @since 2022-02-19 + */ + public List preReleaseIdentifiers() { + return Collections.unmodifiableList(this.preReleaseIdentifiers); + } + + /** + * Converts a version number to a string using the official SemVer format. + * The core of a version is MAJOR.MINOR.PATCH, without zero-padding. If + * pre-release identifiers are present, they are separated by periods and + * added after a '-'. If build metadata is present, it is separated by + * periods and added after a '+'. Pre-release identifiers go before version + * metadata. + *

+ * For example, the version with major number 3, minor number 2, patch number + * 1, pre-release identifiers "alpha" and "1" and build metadata "2022-02-19" + * has a string representation "3.2.1-alpha.1+2022-02-19". + * + * @see The official SemVer specification + */ + @Override + public String toString() { + String versionString = String.format("%d.%d.%d", this.major, this.minor, + this.patch); + if (!this.preReleaseIdentifiers.isEmpty()) { + versionString += "-" + String.join(".", this.preReleaseIdentifiers); + } + if (!this.buildMetadata.isEmpty()) { + versionString += "+" + String.join(".", this.buildMetadata); + } + return versionString; + } +} diff --git a/src/main/java/sevenUnits/converterGUI/SevenUnitsGUI.java b/src/main/java/sevenUnits/converterGUI/SevenUnitsGUI.java index e21c25f..9c6ae0a 100644 --- a/src/main/java/sevenUnits/converterGUI/SevenUnitsGUI.java +++ b/src/main/java/sevenUnits/converterGUI/SevenUnitsGUI.java @@ -1221,8 +1221,8 @@ final class SevenUnitsGUI { final String infoText = Presenter .getLinesFromResource("/about.txt").stream() .map(Presenter::withoutComments) - .collect(Collectors.joining("\n")) - .replaceAll("\\[VERSION\\]", ProgramInfo.VERSION); + .collect(Collectors.joining("\n")).replaceAll( + "\\[VERSION\\]", ProgramInfo.VERSION.toString()); infoTextArea.setText(infoText); } diff --git a/src/main/java/sevenUnitsGUI/Presenter.java b/src/main/java/sevenUnitsGUI/Presenter.java index 07671e4..23a631d 100644 --- a/src/main/java/sevenUnitsGUI/Presenter.java +++ b/src/main/java/sevenUnitsGUI/Presenter.java @@ -47,7 +47,7 @@ public final class Presenter { static final String getAboutText() { return Presenter.getLinesFromResource("/about.txt").stream() .map(Presenter::withoutComments).collect(Collectors.joining("\n")) - .replaceAll("\\[VERSION\\]", ProgramInfo.VERSION); + .replaceAll("\\[VERSION\\]", ProgramInfo.VERSION.toString()); } /** diff --git a/src/test/java/sevenUnits/SemanticVersionTest.java b/src/test/java/sevenUnits/SemanticVersionTest.java new file mode 100644 index 0000000..9202ef9 --- /dev/null +++ b/src/test/java/sevenUnits/SemanticVersionTest.java @@ -0,0 +1,399 @@ +/** + * Copyright (C) 2022 Adrien Hopkins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package sevenUnits; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static sevenUnits.SemanticVersionNumber.BUILD_METADATA_COMPARATOR; +import static sevenUnits.SemanticVersionNumber.builder; +import static sevenUnits.SemanticVersionNumber.fromString; +import static sevenUnits.SemanticVersionNumber.isValidVersionString; +import static sevenUnits.SemanticVersionNumber.preRelease; +import static sevenUnits.SemanticVersionNumber.stableVersion; + +import java.util.List; + +import org.junit.jupiter.api.Test; + +/** + * Tests for {@link SemanticVersionNumber} + * + * @since 2022-02-19 + */ +public final class SemanticVersionTest { + /** + * Test for {@link SemanticVersionNumber#compatible} + * + * @since 2022-02-20 + */ + @Test + public void testCompatibility() { + assertTrue(stableVersion(1, 0, 0).compatibleWith(stableVersion(1, 0, 5)), + "1.0.0 not compatible with 1.0.5"); + assertTrue(stableVersion(1, 3, 1).compatibleWith(stableVersion(1, 4, 0)), + "1.3.1 not compatible with 1.4.0"); + + // 0.y.z should not be compatible with any other version + assertFalse(stableVersion(0, 4, 0).compatibleWith(stableVersion(0, 4, 1)), + "0.4.0 compatible with 0.4.1 (0.y.z versions should be treated as unstable/incompatbile)"); + + // upgrading major version should = incompatible + assertFalse(stableVersion(1, 0, 0).compatibleWith(stableVersion(2, 0, 0)), + "1.0.0 compatible with 2.0.0"); + + // dowgrade should = incompatible + assertFalse(stableVersion(1, 1, 0).compatibleWith(stableVersion(1, 0, 0)), + "1.1.0 compatible with 1.0.0"); + } + + /** + * Tests {@link SemanticVersionNumber#toString} for complex version numbers + * + * @since 2022-02-19 + */ + @Test + public void testComplexToString() { + final SemanticVersionNumber v1 = builder(1, 2, 3).preRelease(1, 2, 3) + .build(); + assertEquals("1.2.3-1.2.3", v1.toString()); + final SemanticVersionNumber v2 = builder(4, 5, 6).preRelease("abc", 123) + .buildMetadata("2022-02-19").build(); + assertEquals("4.5.6-abc.123+2022-02-19", v2.toString()); + final SemanticVersionNumber v3 = builder(1, 0, 0) + .preRelease("x-y-z", "--").build(); + assertEquals("1.0.0-x-y-z.--", v3.toString()); + } + + /** + * Tests that complex version can be created and their parts read + * + * @since 2022-02-19 + */ + @Test + public void testComplexVersions() { + final SemanticVersionNumber v1 = builder(1, 2, 3).preRelease(1, 2, 3) + .build(); + assertEquals(1, v1.majorVersion()); + assertEquals(2, v1.minorVersion()); + assertEquals(3, v1.patchVersion()); + assertEquals(List.of("1", "2", "3"), v1.preReleaseIdentifiers()); + assertEquals(List.of(), v1.buildMetadata()); + + final SemanticVersionNumber v2 = builder(4, 5, 6).preRelease("abc", 123) + .buildMetadata("2022-02-19").build(); + assertEquals(4, v2.majorVersion()); + assertEquals(5, v2.minorVersion()); + assertEquals(6, v2.patchVersion()); + assertEquals(List.of("abc", "123"), v2.preReleaseIdentifiers()); + assertEquals(List.of("2022-02-19"), v2.buildMetadata()); + + final SemanticVersionNumber v3 = builder(1, 0, 0) + .preRelease("x-y-z", "--").build(); + assertEquals(1, v3.majorVersion()); + assertEquals(0, v3.minorVersion()); + assertEquals(0, v3.patchVersion()); + assertEquals(List.of("x-y-z", "--"), v3.preReleaseIdentifiers()); + assertEquals(List.of(), v3.buildMetadata()); + } + + /** + * Test that semantic version strings can be parsed correctly + * + * @since 2022-02-19 + * @see SemanticVersionNumber#fromString + * @see SemanticVersionNumber#isValidVersionString + */ + @Test + public void testFromString() { + // test that the regex can match version strings + assertTrue(isValidVersionString("1.0.0"), "1.0.0 is treated as invalid"); + assertTrue(isValidVersionString("1.3.9"), "1.3.9 is treated as invalid"); + assertTrue(isValidVersionString("2.0.0-a.1"), + "2.0.0-a.1 is treated as invalid"); + assertTrue(isValidVersionString("1.0.0-a.b.c.d"), + "1.0.0-a.b.c.d is treated as invalid"); + assertTrue(isValidVersionString("1.0.0+abc"), + "1.0.0+abc is treated as invalid"); + assertTrue(isValidVersionString("1.0.0-abc+def"), + "1.0.0-abc+def is treated as invalid"); + + // test that invalid versions don't match + assertFalse(isValidVersionString("1.0"), + "1.0 is treated as valid (patch should be required)"); + assertFalse(isValidVersionString("1.A.0"), + "1.A.0 is treated as valid (main versions must be numbers)"); + assertFalse(isValidVersionString("1.0.0-"), + "1.0.0- is treated as valid (pre-release must not be empty)"); + assertFalse(isValidVersionString("1.0.0+"), + "1.0.0+ is treated as valid (build metadata must not be empty)"); + + // test that versions can be parsed + assertEquals(stableVersion(1, 0, 0), fromString("1.0.0"), + "Could not parse 1.0.0"); + assertEquals( + builder(1, 2, 3).preRelease("abc", "56", "def") + .buildMetadata("2022abc99").build(), + fromString("1.2.3-abc.56.def+2022abc99"), + "Could not parse 1.2.3-abc.56.def+2022abc99"); + } + + /** + * Ensures it is impossible to create invalid version numbers + */ + @Test + public void testInvalidVersionNumbers() { + // stableVersion() + assertThrows(IllegalArgumentException.class, + () -> stableVersion(1, 0, -1), + "Negative patch tolerated by stableVersion"); + assertThrows(IllegalArgumentException.class, + () -> stableVersion(1, -2, 1), + "Negative minor version number tolerated by stableVersion"); + assertThrows(IllegalArgumentException.class, + () -> stableVersion(-3, 0, 7), + "Negative major version number tolerated by stableVersion"); + + // preRelease() + assertThrows(IllegalArgumentException.class, + () -> preRelease(1, 0, -1, "test", 2), + "Negative patch tolerated by preRelease"); + assertThrows(IllegalArgumentException.class, + () -> preRelease(1, -2, 1, "test", 2), + "Negative minor version number tolerated by preRelease"); + assertThrows(IllegalArgumentException.class, + () -> preRelease(-3, 0, 7, "test", 2), + "Negative major version number tolerated by preRelease"); + assertThrows(IllegalArgumentException.class, + () -> preRelease(1, 0, 0, "test", -1), + "Negative pre release number tolerated by preRelease"); + assertThrows(NullPointerException.class, + () -> preRelease(1, 0, 0, null, 1), "Null tolerated by preRelease"); + assertThrows(IllegalArgumentException.class, + () -> preRelease(1, 0, 0, "", 1), + "Empty string tolerated by preRelease"); + assertThrows(IllegalArgumentException.class, + () -> preRelease(1, 0, 0, "abc+cde", 1), + "Invalid string tolerated by preRelease"); + + // builder() + assertThrows(IllegalArgumentException.class, () -> builder(1, 0, -1), + "Negative patch tolerated by builder"); + assertThrows(IllegalArgumentException.class, () -> builder(1, -2, 1), + "Negative minor version number tolerated by builder"); + assertThrows(IllegalArgumentException.class, () -> builder(-3, 0, 7), + "Negative major version number tolerated by builder"); + + final SemanticVersionNumber.Builder testBuilder = builder(1, 2, 3); + // note: builder.buildMetadata(null) doesn't even compile lol + // builder.buildMetadata + assertThrows(NullPointerException.class, + () -> testBuilder.buildMetadata(null, "abc"), + "Null tolerated by builder.buildMetadata(String...)"); + assertThrows(IllegalArgumentException.class, + () -> testBuilder.buildMetadata(""), + "Empty string tolerated by builder.buildMetadata(String...)"); + assertThrows(IllegalArgumentException.class, + () -> testBuilder.buildMetadata("c%4"), + "Invalid string tolerated by builder.buildMetadata(String...)"); + assertThrows(NullPointerException.class, + () -> testBuilder.buildMetadata(List.of("abc", null)), + "Null tolerated by builder.buildMetadata(List)"); + assertThrows(IllegalArgumentException.class, + () -> testBuilder.buildMetadata(List.of("")), + "Empty string tolerated by builder.buildMetadata(List)"); + assertThrows(IllegalArgumentException.class, + () -> testBuilder.buildMetadata(List.of("")), + "Invalid string tolerated by builder.buildMetadata(List)"); + + // builder.preRelease + assertThrows(NullPointerException.class, + () -> testBuilder.preRelease(null, "abc"), + "Null tolerated by builder.preRelease(String...)"); + assertThrows(IllegalArgumentException.class, + () -> testBuilder.preRelease(""), + "Empty string tolerated by builder.preRelease(String...)"); + assertThrows(IllegalArgumentException.class, + () -> testBuilder.preRelease("c%4"), + "Invalid string tolerated by builder.preRelease(String...)"); + assertThrows(NullPointerException.class, + () -> testBuilder.preRelease(List.of("abc", null)), + "Null tolerated by builder.preRelease(List)"); + assertThrows(IllegalArgumentException.class, + () -> testBuilder.preRelease(List.of("")), + "Empty string tolerated by builder.preRelease(List)"); + assertThrows(IllegalArgumentException.class, + () -> testBuilder.preRelease(List.of("")), + "Invalid string tolerated by builder.preRelease(List)"); + + // the overloadings that accept numeric arguments + assertThrows(IllegalArgumentException.class, + () -> testBuilder.preRelease(-1), + "Negative number tolerated by builder.preRelease(int...)"); + assertThrows(IllegalArgumentException.class, + () -> testBuilder.preRelease("abc", -1), + "Negative number tolerated by builder.preRelease(String, int)"); + assertThrows(NullPointerException.class, + () -> testBuilder.preRelease(null, 1), + "Null tolerated by builder.preRelease(String, int)"); + assertThrows(IllegalArgumentException.class, + () -> testBuilder.preRelease("", 1), + "Empty string tolerated by builder.preRelease(String, int)"); + assertThrows(IllegalArgumentException.class, + () -> testBuilder.preRelease("#$#c", 1), + "Invalid string tolerated by builder.preRelease(String, int)"); + + // ensure all these attempts didn't change the builder + assertEquals(builder(1, 2, 3), testBuilder, + "Attempts at making invalid version number succeeded despite throwing errors"); + } + + /** + * Test for {@link SemanticVersionNumber#isStable} + * + * @since 2022-02-19 + */ + @Test + public void testIsStable() { + assertTrue(stableVersion(1, 0, 0).isStable(), + "1.0.0 should be stable but is not"); + assertFalse(stableVersion(0, 1, 2).isStable(), + "0.1.2 should not be stable but is"); + assertFalse(preRelease(1, 2, 3, "alpha", 5).isStable(), + "1.2.3a5 should not be stable but is"); + assertTrue( + builder(9, 9, 99) + .buildMetadata("lots-of-metadata", "abc123", "2022").build() + .isStable(), + "9.9.99+lots-of-metadata.abc123.2022 should be stable but is not"); + } + + /** + * Tests that the versions are ordered by + * {@link SemanticVersionNumber#compareTo} according to official rules. Tests + * all of the versions compared in section 11 of the SemVer 2.0.0 document + * and some more. + * + * @since 2022-02-19 + */ + @Test + public void testOrder() { + final SemanticVersionNumber v100a = builder(1, 0, 0).preRelease("alpha") + .build(); // 1.0.0-alpha + final SemanticVersionNumber v100a1 = preRelease(1, 0, 0, "alpha", 1); // 1.0.0-alpha.1 + final SemanticVersionNumber v100ab = builder(1, 0, 0) + .preRelease("alpha", "beta").build(); // 1.0.0-alpha.beta + final SemanticVersionNumber v100b = builder(1, 0, 0).preRelease("beta") + .build(); // 1.0.0-alpha + final SemanticVersionNumber v100b2 = preRelease(1, 0, 0, "beta", 2); // 1.0.0-beta.2 + final SemanticVersionNumber v100b11 = preRelease(1, 0, 0, "beta", 11); // 1.0.0-beta.11 + final SemanticVersionNumber v100rc1 = preRelease(1, 0, 0, "rc", 1); // 1.0.0-rc.1 + final SemanticVersionNumber v100 = stableVersion(1, 0, 0); + final SemanticVersionNumber v100plus = builder(1, 0, 0) + .buildMetadata("blah", "blah", "blah").build(); // 1.0.0+blah.blah.blah + final SemanticVersionNumber v200 = stableVersion(2, 0, 0); + final SemanticVersionNumber v201 = stableVersion(2, 0, 1); + final SemanticVersionNumber v210 = stableVersion(2, 1, 0); + final SemanticVersionNumber v211 = stableVersion(2, 1, 1); + final SemanticVersionNumber v300 = stableVersion(3, 0, 0); + + // test order of version numbers + assertTrue(v100a.compareTo(v100a1) < 0, "1.0.0-alpha >= 1.0.0-alpha.1"); + assertTrue(v100a1.compareTo(v100ab) < 0, + "1.0.0-alpha.1 >= 1.0.0-alpha.beta"); + assertTrue(v100ab.compareTo(v100b) < 0, "1.0.0-alpha.beta >= 1.0.0-beta"); + assertTrue(v100b.compareTo(v100b2) < 0, "1.0.0-beta >= 1.0.0-beta.2"); + assertTrue(v100b2.compareTo(v100b11) < 0, + "1.0.0-beta.2 >= 1.0.0-beta.11"); + assertTrue(v100b11.compareTo(v100rc1) < 0, "1.0.0-beta.11 >= 1.0.0-rc.1"); + assertTrue(v100rc1.compareTo(v100) < 0, "1.0.0-rc.1 >= 1.0.0"); + assertTrue(v100.compareTo(v200) < 0, "1.0.0 >= 2.0.0"); + assertTrue(v200.compareTo(v201) < 0, "2.0.0 >= 2.0.1"); + assertTrue(v201.compareTo(v210) < 0, "2.0.1 >= 2.1.0"); + assertTrue(v210.compareTo(v211) < 0, "2.1.0 >= 2.1.1"); + assertTrue(v211.compareTo(v300) < 0, "2.1.1 >= 3.0.0"); + + // test symmetry - assume previous tests passed + assertTrue(v100a1.compareTo(v100a) > 0, "1.0.0-alpha.1 <= 1.0.0-alpha"); + assertTrue(v100.compareTo(v100rc1) > 0, "1.0.0 <= 1.0.0-rc.1"); + assertTrue(v300.compareTo(v211) > 0, "3.0.0 <= 2.1.1"); + + // test transitivity + assertTrue(v100a.compareTo(v100b11) < 0, "1.0.0-alpha >= 1.0.0-beta.11"); + assertTrue(v100b.compareTo(v200) < 0, "1.0.0-beta >= 2.0.0"); + assertTrue(v100.compareTo(v300) < 0, "1.0.0 >= 3.0.0"); + assertTrue(v100a.compareTo(v300) < 0, "1.0.0-alpha >= 3.0.0"); + + // test metadata is ignored + assertEquals(0, v100.compareTo(v100plus), "Build metadata not ignored"); + // test metadata is NOT ignored by alternative comparator + assertTrue(BUILD_METADATA_COMPARATOR.compare(v100, v100plus) > 0, + "Build metadata ignored by BUILD_METADATA_COMPARATOR"); + } + + /** + * Tests that simple stable versions can be created and their parts read + * + * @since 2022-02-19 + */ + @Test + public void testSimpleStableVersions() { + final SemanticVersionNumber v100 = stableVersion(1, 0, 0); + assertEquals(1, v100.majorVersion()); + assertEquals(0, v100.minorVersion()); + assertEquals(0, v100.patchVersion()); + + final SemanticVersionNumber v925 = stableVersion(9, 2, 5); + assertEquals(9, v925.majorVersion()); + assertEquals(2, v925.minorVersion()); + assertEquals(5, v925.patchVersion()); + } + + /** + * Tests that {@link SemanticVersionNumber#toString} works for simple version + * numbers + * + * @since 2022-02-19 + */ + @Test + public void testSimpleToString() { + final SemanticVersionNumber v100 = stableVersion(1, 0, 0); + assertEquals("1.0.0", v100.toString()); + + final SemanticVersionNumber v845a1 = preRelease(8, 4, 5, "alpha", 1); + assertEquals("8.4.5-alpha.1", v845a1.toString()); + } + + /** + * Tests that simple unstable versions can be created and their parts read + * + * @since 2022-02-19 + */ + @Test + public void testSimpleUnstableVersions() { + final SemanticVersionNumber v350a1 = preRelease(3, 5, 0, "alpha", 1); + assertEquals(3, v350a1.majorVersion(), + "Incorrect major version for v3.5.0a1"); + assertEquals(5, v350a1.minorVersion(), + "Incorrect minor version for v3.5.0a1"); + assertEquals(0, v350a1.patchVersion(), + "Incorrect patch version for v3.5.0a1"); + assertEquals(List.of("alpha", "1"), v350a1.preReleaseIdentifiers(), + "Incorrect pre-release identifiers for v3.5.0a1"); + } +} -- cgit v1.2.3 From b7eee33a5b162b4057d04d28f45738e3048bf01d Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Thu, 24 Feb 2022 16:44:13 -0500 Subject: Moved SemanticVersionNumber to sevenUnits.utils --- src/main/java/sevenUnits/ProgramInfo.java | 2 + .../java/sevenUnits/SemanticVersionNumber.java | 691 --------------------- .../sevenUnits/utils/SemanticVersionNumber.java | 691 +++++++++++++++++++++ src/test/java/sevenUnits/SemanticVersionTest.java | 399 ------------ .../java/sevenUnits/utils/SemanticVersionTest.java | 399 ++++++++++++ src/test/java/sevenUnitsGUI/package-info.java | 23 - 6 files changed, 1092 insertions(+), 1113 deletions(-) delete mode 100644 src/main/java/sevenUnits/SemanticVersionNumber.java create mode 100644 src/main/java/sevenUnits/utils/SemanticVersionNumber.java delete mode 100644 src/test/java/sevenUnits/SemanticVersionTest.java create mode 100644 src/test/java/sevenUnits/utils/SemanticVersionTest.java delete mode 100644 src/test/java/sevenUnitsGUI/package-info.java diff --git a/src/main/java/sevenUnits/ProgramInfo.java b/src/main/java/sevenUnits/ProgramInfo.java index 876367d..6407d7c 100644 --- a/src/main/java/sevenUnits/ProgramInfo.java +++ b/src/main/java/sevenUnits/ProgramInfo.java @@ -16,6 +16,8 @@ */ package sevenUnits; +import sevenUnits.utils.SemanticVersionNumber; + /** * Information about 7Units * diff --git a/src/main/java/sevenUnits/SemanticVersionNumber.java b/src/main/java/sevenUnits/SemanticVersionNumber.java deleted file mode 100644 index 01aeb27..0000000 --- a/src/main/java/sevenUnits/SemanticVersionNumber.java +++ /dev/null @@ -1,691 +0,0 @@ -/** - * Copyright (C) 2022 Adrien Hopkins - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package sevenUnits; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; -import java.util.Objects; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -/** - * A version number in the Semantic Versioning - * scheme - *

- * Each version number has three main parts: - *

    - *
  1. The major version, which increments when backwards incompatible changes - * are made - *
  2. The minor version, which increments when backwards compatible feature - * changes are made - *
  3. The patch version, which increments when backwards compatible bug fixes - * are made - *
- * - * @since 2022-02-19 - */ -public final class SemanticVersionNumber - implements Comparable { - /** - * A builder that can be used to create complex version numbers. - *

- * Note: None of this builder's methods tolerate null arguments, arrays - * containing nulls, negative numbers, or non-alphanumeric identifiers. Nulls - * throw NullPointerExceptions, everything else throws - * IllegalArgumentException. - * - * @since 2022-02-19 - */ - public static final class Builder { - private final int major; - private final int minor; - private final int patch; - private final List preReleaseIdentifiers; - private final List buildMetadata; - - /** - * Creates a builder which can be used to create a - * {@code SemanticVersionNumber} - * - * @param major major version number of final version - * @param minor minor version number of final version - * @param patch patch version number of final version - * @since 2022-02-19 - */ - private Builder(int major, int minor, int patch) { - this.major = major; - this.minor = minor; - this.patch = patch; - this.preReleaseIdentifiers = new ArrayList<>(); - this.buildMetadata = new ArrayList<>(); - } - - /** - * @return version number created by this builder - * @since 2022-02-19 - */ - public SemanticVersionNumber build() { - return new SemanticVersionNumber(this.major, this.minor, this.patch, - this.preReleaseIdentifiers, this.buildMetadata); - } - - /** - * Adds one or more build metadata identifiers - * - * @param identifiers build metadata - * @return this builder - * @since 2022-02-19 - */ - public Builder buildMetadata(List identifiers) { - Objects.requireNonNull(identifiers, "identifiers may not be null"); - for (final String identifier : identifiers) { - Objects.requireNonNull(identifier, "identifier may not be null"); - if (!VALID_IDENTIFIER.matcher(identifier).matches()) - throw new IllegalArgumentException( - String.format("Invalid identifier \"%s\"", identifier)); - this.buildMetadata.add(identifier); - } - return this; - } - - /** - * Adds one or more build metadata identifiers - * - * @param identifiers build metadata - * @return this builder - * @since 2022-02-19 - */ - public Builder buildMetadata(String... identifiers) { - Objects.requireNonNull(identifiers, "identifiers may not be null"); - for (final String identifier : identifiers) { - Objects.requireNonNull(identifier, "identifier may not be null"); - if (!VALID_IDENTIFIER.matcher(identifier).matches()) - throw new IllegalArgumentException( - String.format("Invalid identifier \"%s\"", identifier)); - this.buildMetadata.add(identifier); - } - return this; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) - return true; - if (!(obj instanceof Builder)) - return false; - final Builder other = (Builder) obj; - return Objects.equals(this.buildMetadata, other.buildMetadata) - && this.major == other.major && this.minor == other.minor - && this.patch == other.patch && Objects.equals( - this.preReleaseIdentifiers, other.preReleaseIdentifiers); - } - - @Override - public int hashCode() { - return Objects.hash(this.buildMetadata, this.major, this.minor, - this.patch, this.preReleaseIdentifiers); - } - - /** - * Adds one or more numeric identifiers to the version number - * - * @param identifiers pre-release identifier(s) to add - * @return this builder - * @since 2022-02-19 - */ - public Builder preRelease(int... identifiers) { - Objects.requireNonNull(identifiers, "identifiers may not be null"); - for (final int identifier : identifiers) { - if (identifier < 0) - throw new IllegalArgumentException( - "Numeric identifiers may not be negative"); - this.preReleaseIdentifiers.add(Integer.toString(identifier)); - } - return this; - } - - /** - * Adds one or more pre-release identifier(s) to the version number - * - * @param identifiers pre-release identifier(s) to add - * @return this builder - * @since 2022-02-19 - */ - public Builder preRelease(List identifiers) { - Objects.requireNonNull(identifiers, "identifiers may not be null"); - for (final String identifier : identifiers) { - Objects.requireNonNull(identifier, "identifier may not be null"); - if (!VALID_IDENTIFIER.matcher(identifier).matches()) - throw new IllegalArgumentException( - String.format("Invalid identifier \"%s\"", identifier)); - this.preReleaseIdentifiers.add(identifier); - } - return this; - } - - /** - * Adds one or more pre-release identifier(s) to the version number - * - * @param identifiers pre-release identifier(s) to add - * @return this builder - * @since 2022-02-19 - */ - public Builder preRelease(String... identifiers) { - Objects.requireNonNull(identifiers, "identifiers may not be null"); - for (final String identifier : identifiers) { - Objects.requireNonNull(identifier, "identifier may not be null"); - if (!VALID_IDENTIFIER.matcher(identifier).matches()) - throw new IllegalArgumentException( - String.format("Invalid identifier \"%s\"", identifier)); - this.preReleaseIdentifiers.add(identifier); - } - return this; - } - - /** - * Adds a string identifier and an integer identifer to pre-release data - * - * @param identifier1 first identifier - * @param identifier2 second identifier - * @return this builder - * @since 2022-02-19 - */ - public Builder preRelease(String identifier1, int identifier2) { - Objects.requireNonNull(identifier1, "identifier1 may not be null"); - if (!VALID_IDENTIFIER.matcher(identifier1).matches()) - throw new IllegalArgumentException( - String.format("Invalid identifier \"%s\"", identifier1)); - if (identifier2 < 0) - throw new IllegalArgumentException( - "Integer identifier cannot be negative"); - this.preReleaseIdentifiers.add(identifier1); - this.preReleaseIdentifiers.add(Integer.toString(identifier2)); - return this; - } - - @Override - public String toString() { - return "Semantic Version Builder: " + this.build().toString(); - } - } - - /** - * An alternative comparison method for version numbers. This uses the - * version's natural order, but the build metadata will be compared (using - * the same rules as pre-release identifiers) if everything else is equal. - *

- * This ordering is consistent with equals, unlike - * {@code SemanticVersionNumber}'s natural ordering. - */ - public static final Comparator BUILD_METADATA_COMPARATOR = new Comparator<>() { - @Override - public int compare(SemanticVersionNumber o1, SemanticVersionNumber o2) { - Objects.requireNonNull(o1, "o1 may not be null"); - Objects.requireNonNull(o2, "o2 may not be null"); - final int naturalComparison = o1.compareTo(o2); - if (naturalComparison == 0) - return SemanticVersionNumber.compare(o1.buildMetadata, - o2.buildMetadata); - else - return naturalComparison; - }; - }; - - /** The alphanumeric pattern all identifiers must follow */ - private static final Pattern VALID_IDENTIFIER = Pattern - .compile("[0-9A-Za-z-]+"); - - /** The numeric pattern which causes special behaviour */ - private static final Pattern NUMERIC_IDENTIFER = Pattern.compile("[0-9]+"); - - /** The pattern for a version number */ - private static final Pattern VERSION_NUMBER = Pattern - .compile("(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)" // main - // version - + "(?:-([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?" // pre-release - + "(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?"); // build data - - /** - * Creates a builder that can be used to create a version number - * - * @param major major version number of final version - * @param minor minor version number of final version - * @param patch patch version number of final version - * @return version number builder - * @throws IllegalArgumentException if any argument is negative - * @since 2022-02-19 - */ - public static final SemanticVersionNumber.Builder builder(int major, - int minor, int patch) { - if (major < 0) - throw new IllegalArgumentException( - "Major version must be non-negative."); - if (minor < 0) - throw new IllegalArgumentException( - "Minor version must be non-negative."); - if (patch < 0) - throw new IllegalArgumentException( - "Patch version must be non-negative."); - return new SemanticVersionNumber.Builder(major, minor, patch); - } - - /** - * Compares two lists of strings based on SemVer's precedence rules - * - * @param a first list - * @param b second list - * @return result of comparison as in a comparator - * @see Comparator - * @since 2022-02-20 - */ - private static final int compare(List a, List b) { - // test pre-release size - final int aSize = a.size(); - final int bSize = b.size(); - - // no identifiers is greater than any identifiers - if (aSize != 0 && bSize == 0) - return -1; - else if (aSize == 0 && bSize != 0) - return 1; - - // test identifiers one by one - for (int i = 0; i < Math.min(aSize, bSize); i++) { - final String aElement = a.get(i); - final String bElement = b.get(i); - - if (NUMERIC_IDENTIFER.matcher(aElement).matches()) { - if (NUMERIC_IDENTIFER.matcher(bElement).matches()) { - // both are numbers, compare them - final int aNumber = Integer.parseInt(aElement); - final int bNumber = Integer.parseInt(bElement); - - if (aNumber < bNumber) - return -1; - else if (aNumber > bNumber) - return 1; - } else - // aElement is a number and bElement is not a number - // by the rules, a goes before b - return -1; - } else { - if (NUMERIC_IDENTIFER.matcher(bElement).matches()) - // aElement is not a number but bElement is - // by the rules, a goes after b - return 1; - else { - // both are not numbers, compare them - final int comparison = aElement.compareTo(bElement); - if (comparison != 0) - return comparison; - } - } - } - - // we just tested the stuff that's in common, maybe someone has more - if (aSize < bSize) - return -1; - else if (aSize > bSize) - return 1; - else - return 0; - } - - /** - * Gets a version number from a string in the official format - * - * @param versionString string to parse - * @return {@code SemanticVersionNumber} instance - * @since 2022-02-19 - * @see {@link #toString} - */ - public static final SemanticVersionNumber fromString(String versionString) { - // parse & validate version string - Objects.requireNonNull(versionString, "versionString may not be null"); - final Matcher m = VERSION_NUMBER.matcher(versionString); - if (!m.matches()) - throw new IllegalArgumentException( - String.format("Provided string \"%s\" is not a version number", - versionString)); - - // main parts - final int major = Integer.parseInt(m.group(1)); - final int minor = Integer.parseInt(m.group(2)); - final int patch = Integer.parseInt(m.group(3)); - - // pre release - final List preRelease; - if (m.group(4) == null) { - preRelease = List.of(); - } else { - preRelease = Arrays.asList(m.group(4).split("\\.")); - } - - // build metadata - final List buildMetadata; - if (m.group(5) == null) { - buildMetadata = List.of(); - } else { - buildMetadata = Arrays.asList(m.group(5).split("\\.")); - } - - // return number - return new SemanticVersionNumber(major, minor, patch, preRelease, - buildMetadata); - } - - /** - * Tests whether a string is a valid Semantic Version string - * - * @param versionString string to test - * @return true iff string is valid - * @since 2022-02-19 - */ - public static final boolean isValidVersionString(String versionString) { - return VERSION_NUMBER.matcher(versionString).matches(); - } - - /** - * Creates a simple pre-release version number of the form - * MAJOR.MINOR.PATH-TYPE.NUMBER (e.g. 1.2.3-alpha.4). - * - * @param major major version number - * @param minor minor version number - * @param patch patch version number - * @param preReleaseType first pre-release element - * @param preReleaseNumber second pre-release element - * @return {@code SemanticVersionNumber} instance - * @throws IllegalArgumentException if any argument is negative or if the - * preReleaseType is null, empty or not - * alphanumeric (0-9, A-Z, a-z, - only) - * @since 2022-02-19 - */ - public static final SemanticVersionNumber preRelease(int major, int minor, - int patch, String preReleaseType, int preReleaseNumber) { - if (major < 0) - throw new IllegalArgumentException( - "Major version must be non-negative."); - if (minor < 0) - throw new IllegalArgumentException( - "Minor version must be non-negative."); - if (patch < 0) - throw new IllegalArgumentException( - "Patch version must be non-negative."); - Objects.requireNonNull(preReleaseType, "preReleaseType may not be null"); - if (!VALID_IDENTIFIER.matcher(preReleaseType).matches()) - throw new IllegalArgumentException( - String.format("Invalid identifier \"%s\".", preReleaseType)); - if (preReleaseNumber < 0) - throw new IllegalArgumentException( - "Pre-release number must be non-negative."); - return new SemanticVersionNumber(major, minor, patch, - List.of(preReleaseType, Integer.toString(preReleaseNumber)), - List.of()); - } - - /** - * Creates a {@code SemanticVersionNumber} instance without pre-release - * identifiers or build metadata. - *

- * Note: this method allows you to create versions with major version number - * 0, even though these versions would not be considered stable. - * - * @param major major version number - * @param minor minor version number - * @param patch patch version number - * @return {@code SemanticVersionNumber} instance - * @throws IllegalArgumentException if any argument is negative - * @since 2022-02-19 - */ - public static final SemanticVersionNumber stableVersion(int major, int minor, - int patch) { - if (major < 0) - throw new IllegalArgumentException( - "Major version must be non-negative."); - if (minor < 0) - throw new IllegalArgumentException( - "Minor version must be non-negative."); - if (patch < 0) - throw new IllegalArgumentException( - "Patch version must be non-negative."); - return new SemanticVersionNumber(major, minor, patch, List.of(), - List.of()); - } - - // parts of the version number - private final int major; - private final int minor; - private final int patch; - private final List preReleaseIdentifiers; - private final List buildMetadata; - - /** - * Creates a version number - * - * @param major major version number - * @param minor minor version number - * @param patch patch version number - * @param preReleaseIdentifiers pre-release version data - * @param buildMetadata build metadata - * @since 2022-02-19 - */ - private SemanticVersionNumber(int major, int minor, int patch, - List preReleaseIdentifiers, List buildMetadata) { - this.major = major; - this.minor = minor; - this.patch = patch; - this.preReleaseIdentifiers = preReleaseIdentifiers; - this.buildMetadata = buildMetadata; - } - - /** - * @return build metadata (empty if there is none) - * @since 2022-02-19 - */ - public List buildMetadata() { - return Collections.unmodifiableList(this.buildMetadata); - } - - /** - * Compares two version numbers according to the official Semantic Versioning - * order. - *

- * Note: this ordering is not consistent with equals. Specifically, two - * versions that are identical except for their build metadata will be - * considered different by equals but the same by this method. This is - * required to follow the official Semantic Versioning specification. - *

- */ - @Override - public int compareTo(SemanticVersionNumber o) { - // test the three big numbers in order first - if (this.major < o.major) - return -1; - else if (this.major > o.major) - return 1; - - if (this.minor < o.minor) - return -1; - else if (this.minor > o.minor) - return 1; - - if (this.patch < o.patch) - return -1; - else if (this.patch > o.patch) - return 1; - - // now we just compare pre-release identifiers - // (remember: build metadata is ignored) - return SemanticVersionNumber.compare(this.preReleaseIdentifiers, - o.preReleaseIdentifiers); - } - - /** - * Determines the compatibility of code written for this version to - * {@code other}. More specifically: - *

- * If this function returns true, then there should be no problems - * upgrading code written for this version to version {@code other} as long - * as: - *

    - *
  • Semantic Versioning is being used properly - *
  • Your code doesn't depend on unintended features (if it does, it isn't - * necessarily compatible with any other version) - *
- * If this function returns false, you may have to change your code to - * upgrade it to {@code other} - * - *

- * Two version numbers that are identical (ignoring build metadata) are - * always compatible. Different version numbers are compatible as long as: - *

    - *
  • The major version number is not 0 (if it is, the API is considered - * unstable and any upgrade can be backwards compatible) - *
  • The major version number is the same (changing the major version - * number implies bacwards incompatible changes) - *
  • This version comes before the other one in the official precedence - * order (downgrading can remove features you depend on) - *
- * - * @param other version to compare with - * @return true if you can definitely upgrade to {@code other} without - * changing code - * @since 2022-02-20 - */ - public boolean compatibleWith(SemanticVersionNumber other) { - Objects.requireNonNull(other, "other may not be null"); - - return this.compareTo(other) == 0 || this.major != 0 - && this.major == other.major && this.compareTo(other) < 0; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) - return true; - if (!(obj instanceof SemanticVersionNumber)) - return false; - final SemanticVersionNumber other = (SemanticVersionNumber) obj; - if (this.buildMetadata == null) { - if (other.buildMetadata != null) - return false; - } else if (!this.buildMetadata.equals(other.buildMetadata)) - return false; - if (this.major != other.major) - return false; - if (this.minor != other.minor) - return false; - if (this.patch != other.patch) - return false; - if (this.preReleaseIdentifiers == null) { - if (other.preReleaseIdentifiers != null) - return false; - } else if (!this.preReleaseIdentifiers - .equals(other.preReleaseIdentifiers)) - return false; - return true; - } - - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result - + (this.buildMetadata == null ? 0 : this.buildMetadata.hashCode()); - result = prime * result + this.major; - result = prime * result + this.minor; - result = prime * result + this.patch; - result = prime * result + (this.preReleaseIdentifiers == null ? 0 - : this.preReleaseIdentifiers.hashCode()); - return result; - } - - /** - * @return true iff this version is stable (major version > 0 and not a - * pre-release) - * @since 2022-02-19 - */ - public boolean isStable() { - return this.major > 0 && this.preReleaseIdentifiers.isEmpty(); - } - - /** - * @return the MAJOR version number, incremented when you make backwards - * incompatible API changes - * @since 2022-02-19 - */ - public int majorVersion() { - return this.major; - } - - /** - * @return the MINOR version number, incremented when you add backwards - * compatible functionality - * @since 2022-02-19 - */ - public int minorVersion() { - return this.minor; - } - - /** - * @return the PATCH version number, incremented when you make backwards - * compatible bug fixes - * @since 2022-02-19 - */ - public int patchVersion() { - return this.patch; - } - - /** - * @return identifiers describing this pre-release (empty if not a - * pre-release) - * @since 2022-02-19 - */ - public List preReleaseIdentifiers() { - return Collections.unmodifiableList(this.preReleaseIdentifiers); - } - - /** - * Converts a version number to a string using the official SemVer format. - * The core of a version is MAJOR.MINOR.PATCH, without zero-padding. If - * pre-release identifiers are present, they are separated by periods and - * added after a '-'. If build metadata is present, it is separated by - * periods and added after a '+'. Pre-release identifiers go before version - * metadata. - *

- * For example, the version with major number 3, minor number 2, patch number - * 1, pre-release identifiers "alpha" and "1" and build metadata "2022-02-19" - * has a string representation "3.2.1-alpha.1+2022-02-19". - * - * @see The official SemVer specification - */ - @Override - public String toString() { - String versionString = String.format("%d.%d.%d", this.major, this.minor, - this.patch); - if (!this.preReleaseIdentifiers.isEmpty()) { - versionString += "-" + String.join(".", this.preReleaseIdentifiers); - } - if (!this.buildMetadata.isEmpty()) { - versionString += "+" + String.join(".", this.buildMetadata); - } - return versionString; - } -} diff --git a/src/main/java/sevenUnits/utils/SemanticVersionNumber.java b/src/main/java/sevenUnits/utils/SemanticVersionNumber.java new file mode 100644 index 0000000..06417c5 --- /dev/null +++ b/src/main/java/sevenUnits/utils/SemanticVersionNumber.java @@ -0,0 +1,691 @@ +/** + * Copyright (C) 2022 Adrien Hopkins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package sevenUnits.utils; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * A version number in the Semantic Versioning + * scheme + *

+ * Each version number has three main parts: + *

    + *
  1. The major version, which increments when backwards incompatible changes + * are made + *
  2. The minor version, which increments when backwards compatible feature + * changes are made + *
  3. The patch version, which increments when backwards compatible bug fixes + * are made + *
+ * + * @since 2022-02-19 + */ +public final class SemanticVersionNumber + implements Comparable { + /** + * A builder that can be used to create complex version numbers. + *

+ * Note: None of this builder's methods tolerate null arguments, arrays + * containing nulls, negative numbers, or non-alphanumeric identifiers. Nulls + * throw NullPointerExceptions, everything else throws + * IllegalArgumentException. + * + * @since 2022-02-19 + */ + public static final class Builder { + private final int major; + private final int minor; + private final int patch; + private final List preReleaseIdentifiers; + private final List buildMetadata; + + /** + * Creates a builder which can be used to create a + * {@code SemanticVersionNumber} + * + * @param major major version number of final version + * @param minor minor version number of final version + * @param patch patch version number of final version + * @since 2022-02-19 + */ + private Builder(int major, int minor, int patch) { + this.major = major; + this.minor = minor; + this.patch = patch; + this.preReleaseIdentifiers = new ArrayList<>(); + this.buildMetadata = new ArrayList<>(); + } + + /** + * @return version number created by this builder + * @since 2022-02-19 + */ + public SemanticVersionNumber build() { + return new SemanticVersionNumber(this.major, this.minor, this.patch, + this.preReleaseIdentifiers, this.buildMetadata); + } + + /** + * Adds one or more build metadata identifiers + * + * @param identifiers build metadata + * @return this builder + * @since 2022-02-19 + */ + public Builder buildMetadata(List identifiers) { + Objects.requireNonNull(identifiers, "identifiers may not be null"); + for (final String identifier : identifiers) { + Objects.requireNonNull(identifier, "identifier may not be null"); + if (!VALID_IDENTIFIER.matcher(identifier).matches()) + throw new IllegalArgumentException( + String.format("Invalid identifier \"%s\"", identifier)); + this.buildMetadata.add(identifier); + } + return this; + } + + /** + * Adds one or more build metadata identifiers + * + * @param identifiers build metadata + * @return this builder + * @since 2022-02-19 + */ + public Builder buildMetadata(String... identifiers) { + Objects.requireNonNull(identifiers, "identifiers may not be null"); + for (final String identifier : identifiers) { + Objects.requireNonNull(identifier, "identifier may not be null"); + if (!VALID_IDENTIFIER.matcher(identifier).matches()) + throw new IllegalArgumentException( + String.format("Invalid identifier \"%s\"", identifier)); + this.buildMetadata.add(identifier); + } + return this; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (!(obj instanceof Builder)) + return false; + final Builder other = (Builder) obj; + return Objects.equals(this.buildMetadata, other.buildMetadata) + && this.major == other.major && this.minor == other.minor + && this.patch == other.patch && Objects.equals( + this.preReleaseIdentifiers, other.preReleaseIdentifiers); + } + + @Override + public int hashCode() { + return Objects.hash(this.buildMetadata, this.major, this.minor, + this.patch, this.preReleaseIdentifiers); + } + + /** + * Adds one or more numeric identifiers to the version number + * + * @param identifiers pre-release identifier(s) to add + * @return this builder + * @since 2022-02-19 + */ + public Builder preRelease(int... identifiers) { + Objects.requireNonNull(identifiers, "identifiers may not be null"); + for (final int identifier : identifiers) { + if (identifier < 0) + throw new IllegalArgumentException( + "Numeric identifiers may not be negative"); + this.preReleaseIdentifiers.add(Integer.toString(identifier)); + } + return this; + } + + /** + * Adds one or more pre-release identifier(s) to the version number + * + * @param identifiers pre-release identifier(s) to add + * @return this builder + * @since 2022-02-19 + */ + public Builder preRelease(List identifiers) { + Objects.requireNonNull(identifiers, "identifiers may not be null"); + for (final String identifier : identifiers) { + Objects.requireNonNull(identifier, "identifier may not be null"); + if (!VALID_IDENTIFIER.matcher(identifier).matches()) + throw new IllegalArgumentException( + String.format("Invalid identifier \"%s\"", identifier)); + this.preReleaseIdentifiers.add(identifier); + } + return this; + } + + /** + * Adds one or more pre-release identifier(s) to the version number + * + * @param identifiers pre-release identifier(s) to add + * @return this builder + * @since 2022-02-19 + */ + public Builder preRelease(String... identifiers) { + Objects.requireNonNull(identifiers, "identifiers may not be null"); + for (final String identifier : identifiers) { + Objects.requireNonNull(identifier, "identifier may not be null"); + if (!VALID_IDENTIFIER.matcher(identifier).matches()) + throw new IllegalArgumentException( + String.format("Invalid identifier \"%s\"", identifier)); + this.preReleaseIdentifiers.add(identifier); + } + return this; + } + + /** + * Adds a string identifier and an integer identifer to pre-release data + * + * @param identifier1 first identifier + * @param identifier2 second identifier + * @return this builder + * @since 2022-02-19 + */ + public Builder preRelease(String identifier1, int identifier2) { + Objects.requireNonNull(identifier1, "identifier1 may not be null"); + if (!VALID_IDENTIFIER.matcher(identifier1).matches()) + throw new IllegalArgumentException( + String.format("Invalid identifier \"%s\"", identifier1)); + if (identifier2 < 0) + throw new IllegalArgumentException( + "Integer identifier cannot be negative"); + this.preReleaseIdentifiers.add(identifier1); + this.preReleaseIdentifiers.add(Integer.toString(identifier2)); + return this; + } + + @Override + public String toString() { + return "Semantic Version Builder: " + this.build().toString(); + } + } + + /** + * An alternative comparison method for version numbers. This uses the + * version's natural order, but the build metadata will be compared (using + * the same rules as pre-release identifiers) if everything else is equal. + *

+ * This ordering is consistent with equals, unlike + * {@code SemanticVersionNumber}'s natural ordering. + */ + public static final Comparator BUILD_METADATA_COMPARATOR = new Comparator<>() { + @Override + public int compare(SemanticVersionNumber o1, SemanticVersionNumber o2) { + Objects.requireNonNull(o1, "o1 may not be null"); + Objects.requireNonNull(o2, "o2 may not be null"); + final int naturalComparison = o1.compareTo(o2); + if (naturalComparison == 0) + return SemanticVersionNumber.compareIdentifiers(o1.buildMetadata, + o2.buildMetadata); + else + return naturalComparison; + }; + }; + + /** The alphanumeric pattern all identifiers must follow */ + private static final Pattern VALID_IDENTIFIER = Pattern + .compile("[0-9A-Za-z-]+"); + + /** The numeric pattern which causes special behaviour */ + private static final Pattern NUMERIC_IDENTIFER = Pattern.compile("[0-9]+"); + + /** The pattern for a version number */ + private static final Pattern VERSION_NUMBER = Pattern + .compile("(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)" // main + // version + + "(?:-([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?" // pre-release + + "(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?"); // build data + + /** + * Creates a builder that can be used to create a version number + * + * @param major major version number of final version + * @param minor minor version number of final version + * @param patch patch version number of final version + * @return version number builder + * @throws IllegalArgumentException if any argument is negative + * @since 2022-02-19 + */ + public static final SemanticVersionNumber.Builder builder(int major, + int minor, int patch) { + if (major < 0) + throw new IllegalArgumentException( + "Major version must be non-negative."); + if (minor < 0) + throw new IllegalArgumentException( + "Minor version must be non-negative."); + if (patch < 0) + throw new IllegalArgumentException( + "Patch version must be non-negative."); + return new SemanticVersionNumber.Builder(major, minor, patch); + } + + /** + * Compares two lists of strings based on SemVer's precedence rules + * + * @param a first list + * @param b second list + * @return result of comparison as in a comparator + * @see Comparator + * @since 2022-02-20 + */ + private static final int compareIdentifiers(List a, List b) { + // test pre-release size + final int aSize = a.size(); + final int bSize = b.size(); + + // no identifiers is greater than any identifiers + if (aSize != 0 && bSize == 0) + return -1; + else if (aSize == 0 && bSize != 0) + return 1; + + // test identifiers one by one + for (int i = 0; i < Math.min(aSize, bSize); i++) { + final String aElement = a.get(i); + final String bElement = b.get(i); + + if (NUMERIC_IDENTIFER.matcher(aElement).matches()) { + if (NUMERIC_IDENTIFER.matcher(bElement).matches()) { + // both are numbers, compare them + final int aNumber = Integer.parseInt(aElement); + final int bNumber = Integer.parseInt(bElement); + + if (aNumber < bNumber) + return -1; + else if (aNumber > bNumber) + return 1; + } else + // aElement is a number and bElement is not a number + // by the rules, a goes before b + return -1; + } else { + if (NUMERIC_IDENTIFER.matcher(bElement).matches()) + // aElement is not a number but bElement is + // by the rules, a goes after b + return 1; + else { + // both are not numbers, compare them + final int comparison = aElement.compareTo(bElement); + if (comparison != 0) + return comparison; + } + } + } + + // we just tested the stuff that's in common, maybe someone has more + if (aSize < bSize) + return -1; + else if (aSize > bSize) + return 1; + else + return 0; + } + + /** + * Gets a version number from a string in the official format + * + * @param versionString string to parse + * @return {@code SemanticVersionNumber} instance + * @since 2022-02-19 + * @see {@link #toString} + */ + public static final SemanticVersionNumber fromString(String versionString) { + // parse & validate version string + Objects.requireNonNull(versionString, "versionString may not be null"); + final Matcher m = VERSION_NUMBER.matcher(versionString); + if (!m.matches()) + throw new IllegalArgumentException( + String.format("Provided string \"%s\" is not a version number", + versionString)); + + // main parts + final int major = Integer.parseInt(m.group(1)); + final int minor = Integer.parseInt(m.group(2)); + final int patch = Integer.parseInt(m.group(3)); + + // pre release + final List preRelease; + if (m.group(4) == null) { + preRelease = List.of(); + } else { + preRelease = Arrays.asList(m.group(4).split("\\.")); + } + + // build metadata + final List buildMetadata; + if (m.group(5) == null) { + buildMetadata = List.of(); + } else { + buildMetadata = Arrays.asList(m.group(5).split("\\.")); + } + + // return number + return new SemanticVersionNumber(major, minor, patch, preRelease, + buildMetadata); + } + + /** + * Tests whether a string is a valid Semantic Version string + * + * @param versionString string to test + * @return true iff string is valid + * @since 2022-02-19 + */ + public static final boolean isValidVersionString(String versionString) { + return VERSION_NUMBER.matcher(versionString).matches(); + } + + /** + * Creates a simple pre-release version number of the form + * MAJOR.MINOR.PATH-TYPE.NUMBER (e.g. 1.2.3-alpha.4). + * + * @param major major version number + * @param minor minor version number + * @param patch patch version number + * @param preReleaseType first pre-release element + * @param preReleaseNumber second pre-release element + * @return {@code SemanticVersionNumber} instance + * @throws IllegalArgumentException if any argument is negative or if the + * preReleaseType is null, empty or not + * alphanumeric (0-9, A-Z, a-z, - only) + * @since 2022-02-19 + */ + public static final SemanticVersionNumber preRelease(int major, int minor, + int patch, String preReleaseType, int preReleaseNumber) { + if (major < 0) + throw new IllegalArgumentException( + "Major version must be non-negative."); + if (minor < 0) + throw new IllegalArgumentException( + "Minor version must be non-negative."); + if (patch < 0) + throw new IllegalArgumentException( + "Patch version must be non-negative."); + Objects.requireNonNull(preReleaseType, "preReleaseType may not be null"); + if (!VALID_IDENTIFIER.matcher(preReleaseType).matches()) + throw new IllegalArgumentException( + String.format("Invalid identifier \"%s\".", preReleaseType)); + if (preReleaseNumber < 0) + throw new IllegalArgumentException( + "Pre-release number must be non-negative."); + return new SemanticVersionNumber(major, minor, patch, + List.of(preReleaseType, Integer.toString(preReleaseNumber)), + List.of()); + } + + /** + * Creates a {@code SemanticVersionNumber} instance without pre-release + * identifiers or build metadata. + *

+ * Note: this method allows you to create versions with major version number + * 0, even though these versions would not be considered stable. + * + * @param major major version number + * @param minor minor version number + * @param patch patch version number + * @return {@code SemanticVersionNumber} instance + * @throws IllegalArgumentException if any argument is negative + * @since 2022-02-19 + */ + public static final SemanticVersionNumber stableVersion(int major, int minor, + int patch) { + if (major < 0) + throw new IllegalArgumentException( + "Major version must be non-negative."); + if (minor < 0) + throw new IllegalArgumentException( + "Minor version must be non-negative."); + if (patch < 0) + throw new IllegalArgumentException( + "Patch version must be non-negative."); + return new SemanticVersionNumber(major, minor, patch, List.of(), + List.of()); + } + + // parts of the version number + private final int major; + private final int minor; + private final int patch; + private final List preReleaseIdentifiers; + private final List buildMetadata; + + /** + * Creates a version number + * + * @param major major version number + * @param minor minor version number + * @param patch patch version number + * @param preReleaseIdentifiers pre-release version data + * @param buildMetadata build metadata + * @since 2022-02-19 + */ + private SemanticVersionNumber(int major, int minor, int patch, + List preReleaseIdentifiers, List buildMetadata) { + this.major = major; + this.minor = minor; + this.patch = patch; + this.preReleaseIdentifiers = preReleaseIdentifiers; + this.buildMetadata = buildMetadata; + } + + /** + * @return build metadata (empty if there is none) + * @since 2022-02-19 + */ + public List buildMetadata() { + return Collections.unmodifiableList(this.buildMetadata); + } + + /** + * Compares two version numbers according to the official Semantic Versioning + * order. + *

+ * Note: this ordering is not consistent with equals. Specifically, two + * versions that are identical except for their build metadata will be + * considered different by equals but the same by this method. This is + * required to follow the official Semantic Versioning specification. + *

+ */ + @Override + public int compareTo(SemanticVersionNumber o) { + // test the three big numbers in order first + if (this.major < o.major) + return -1; + else if (this.major > o.major) + return 1; + + if (this.minor < o.minor) + return -1; + else if (this.minor > o.minor) + return 1; + + if (this.patch < o.patch) + return -1; + else if (this.patch > o.patch) + return 1; + + // now we just compare pre-release identifiers + // (remember: build metadata is ignored) + return SemanticVersionNumber.compareIdentifiers(this.preReleaseIdentifiers, + o.preReleaseIdentifiers); + } + + /** + * Determines the compatibility of code written for this version to + * {@code other}. More specifically: + *

+ * If this function returns true, then there should be no problems + * upgrading code written for this version to version {@code other} as long + * as: + *

    + *
  • Semantic Versioning is being used properly + *
  • Your code doesn't depend on unintended features (if it does, it isn't + * necessarily compatible with any other version) + *
+ * If this function returns false, you may have to change your code to + * upgrade it to {@code other} + * + *

+ * Two version numbers that are identical (ignoring build metadata) are + * always compatible. Different version numbers are compatible as long as: + *

    + *
  • The major version number is not 0 (if it is, the API is considered + * unstable and any upgrade can be backwards compatible) + *
  • The major version number is the same (changing the major version + * number implies bacwards incompatible changes) + *
  • This version comes before the other one in the official precedence + * order (downgrading can remove features you depend on) + *
+ * + * @param other version to compare with + * @return true if you can definitely upgrade to {@code other} without + * changing code + * @since 2022-02-20 + */ + public boolean compatibleWith(SemanticVersionNumber other) { + Objects.requireNonNull(other, "other may not be null"); + + return this.compareTo(other) == 0 || this.major != 0 + && this.major == other.major && this.compareTo(other) < 0; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (!(obj instanceof SemanticVersionNumber)) + return false; + final SemanticVersionNumber other = (SemanticVersionNumber) obj; + if (this.buildMetadata == null) { + if (other.buildMetadata != null) + return false; + } else if (!this.buildMetadata.equals(other.buildMetadata)) + return false; + if (this.major != other.major) + return false; + if (this.minor != other.minor) + return false; + if (this.patch != other.patch) + return false; + if (this.preReleaseIdentifiers == null) { + if (other.preReleaseIdentifiers != null) + return false; + } else if (!this.preReleaseIdentifiers + .equals(other.preReleaseIdentifiers)) + return false; + return true; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + + (this.buildMetadata == null ? 0 : this.buildMetadata.hashCode()); + result = prime * result + this.major; + result = prime * result + this.minor; + result = prime * result + this.patch; + result = prime * result + (this.preReleaseIdentifiers == null ? 0 + : this.preReleaseIdentifiers.hashCode()); + return result; + } + + /** + * @return true iff this version is stable (major version > 0 and not a + * pre-release) + * @since 2022-02-19 + */ + public boolean isStable() { + return this.major > 0 && this.preReleaseIdentifiers.isEmpty(); + } + + /** + * @return the MAJOR version number, incremented when you make backwards + * incompatible API changes + * @since 2022-02-19 + */ + public int majorVersion() { + return this.major; + } + + /** + * @return the MINOR version number, incremented when you add backwards + * compatible functionality + * @since 2022-02-19 + */ + public int minorVersion() { + return this.minor; + } + + /** + * @return the PATCH version number, incremented when you make backwards + * compatible bug fixes + * @since 2022-02-19 + */ + public int patchVersion() { + return this.patch; + } + + /** + * @return identifiers describing this pre-release (empty if not a + * pre-release) + * @since 2022-02-19 + */ + public List preReleaseIdentifiers() { + return Collections.unmodifiableList(this.preReleaseIdentifiers); + } + + /** + * Converts a version number to a string using the official SemVer format. + * The core of a version is MAJOR.MINOR.PATCH, without zero-padding. If + * pre-release identifiers are present, they are separated by periods and + * added after a '-'. If build metadata is present, it is separated by + * periods and added after a '+'. Pre-release identifiers go before version + * metadata. + *

+ * For example, the version with major number 3, minor number 2, patch number + * 1, pre-release identifiers "alpha" and "1" and build metadata "2022-02-19" + * has a string representation "3.2.1-alpha.1+2022-02-19". + * + * @see The official SemVer specification + */ + @Override + public String toString() { + String versionString = String.format("%d.%d.%d", this.major, this.minor, + this.patch); + if (!this.preReleaseIdentifiers.isEmpty()) { + versionString += "-" + String.join(".", this.preReleaseIdentifiers); + } + if (!this.buildMetadata.isEmpty()) { + versionString += "+" + String.join(".", this.buildMetadata); + } + return versionString; + } +} diff --git a/src/test/java/sevenUnits/SemanticVersionTest.java b/src/test/java/sevenUnits/SemanticVersionTest.java deleted file mode 100644 index 9202ef9..0000000 --- a/src/test/java/sevenUnits/SemanticVersionTest.java +++ /dev/null @@ -1,399 +0,0 @@ -/** - * Copyright (C) 2022 Adrien Hopkins - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package sevenUnits; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static sevenUnits.SemanticVersionNumber.BUILD_METADATA_COMPARATOR; -import static sevenUnits.SemanticVersionNumber.builder; -import static sevenUnits.SemanticVersionNumber.fromString; -import static sevenUnits.SemanticVersionNumber.isValidVersionString; -import static sevenUnits.SemanticVersionNumber.preRelease; -import static sevenUnits.SemanticVersionNumber.stableVersion; - -import java.util.List; - -import org.junit.jupiter.api.Test; - -/** - * Tests for {@link SemanticVersionNumber} - * - * @since 2022-02-19 - */ -public final class SemanticVersionTest { - /** - * Test for {@link SemanticVersionNumber#compatible} - * - * @since 2022-02-20 - */ - @Test - public void testCompatibility() { - assertTrue(stableVersion(1, 0, 0).compatibleWith(stableVersion(1, 0, 5)), - "1.0.0 not compatible with 1.0.5"); - assertTrue(stableVersion(1, 3, 1).compatibleWith(stableVersion(1, 4, 0)), - "1.3.1 not compatible with 1.4.0"); - - // 0.y.z should not be compatible with any other version - assertFalse(stableVersion(0, 4, 0).compatibleWith(stableVersion(0, 4, 1)), - "0.4.0 compatible with 0.4.1 (0.y.z versions should be treated as unstable/incompatbile)"); - - // upgrading major version should = incompatible - assertFalse(stableVersion(1, 0, 0).compatibleWith(stableVersion(2, 0, 0)), - "1.0.0 compatible with 2.0.0"); - - // dowgrade should = incompatible - assertFalse(stableVersion(1, 1, 0).compatibleWith(stableVersion(1, 0, 0)), - "1.1.0 compatible with 1.0.0"); - } - - /** - * Tests {@link SemanticVersionNumber#toString} for complex version numbers - * - * @since 2022-02-19 - */ - @Test - public void testComplexToString() { - final SemanticVersionNumber v1 = builder(1, 2, 3).preRelease(1, 2, 3) - .build(); - assertEquals("1.2.3-1.2.3", v1.toString()); - final SemanticVersionNumber v2 = builder(4, 5, 6).preRelease("abc", 123) - .buildMetadata("2022-02-19").build(); - assertEquals("4.5.6-abc.123+2022-02-19", v2.toString()); - final SemanticVersionNumber v3 = builder(1, 0, 0) - .preRelease("x-y-z", "--").build(); - assertEquals("1.0.0-x-y-z.--", v3.toString()); - } - - /** - * Tests that complex version can be created and their parts read - * - * @since 2022-02-19 - */ - @Test - public void testComplexVersions() { - final SemanticVersionNumber v1 = builder(1, 2, 3).preRelease(1, 2, 3) - .build(); - assertEquals(1, v1.majorVersion()); - assertEquals(2, v1.minorVersion()); - assertEquals(3, v1.patchVersion()); - assertEquals(List.of("1", "2", "3"), v1.preReleaseIdentifiers()); - assertEquals(List.of(), v1.buildMetadata()); - - final SemanticVersionNumber v2 = builder(4, 5, 6).preRelease("abc", 123) - .buildMetadata("2022-02-19").build(); - assertEquals(4, v2.majorVersion()); - assertEquals(5, v2.minorVersion()); - assertEquals(6, v2.patchVersion()); - assertEquals(List.of("abc", "123"), v2.preReleaseIdentifiers()); - assertEquals(List.of("2022-02-19"), v2.buildMetadata()); - - final SemanticVersionNumber v3 = builder(1, 0, 0) - .preRelease("x-y-z", "--").build(); - assertEquals(1, v3.majorVersion()); - assertEquals(0, v3.minorVersion()); - assertEquals(0, v3.patchVersion()); - assertEquals(List.of("x-y-z", "--"), v3.preReleaseIdentifiers()); - assertEquals(List.of(), v3.buildMetadata()); - } - - /** - * Test that semantic version strings can be parsed correctly - * - * @since 2022-02-19 - * @see SemanticVersionNumber#fromString - * @see SemanticVersionNumber#isValidVersionString - */ - @Test - public void testFromString() { - // test that the regex can match version strings - assertTrue(isValidVersionString("1.0.0"), "1.0.0 is treated as invalid"); - assertTrue(isValidVersionString("1.3.9"), "1.3.9 is treated as invalid"); - assertTrue(isValidVersionString("2.0.0-a.1"), - "2.0.0-a.1 is treated as invalid"); - assertTrue(isValidVersionString("1.0.0-a.b.c.d"), - "1.0.0-a.b.c.d is treated as invalid"); - assertTrue(isValidVersionString("1.0.0+abc"), - "1.0.0+abc is treated as invalid"); - assertTrue(isValidVersionString("1.0.0-abc+def"), - "1.0.0-abc+def is treated as invalid"); - - // test that invalid versions don't match - assertFalse(isValidVersionString("1.0"), - "1.0 is treated as valid (patch should be required)"); - assertFalse(isValidVersionString("1.A.0"), - "1.A.0 is treated as valid (main versions must be numbers)"); - assertFalse(isValidVersionString("1.0.0-"), - "1.0.0- is treated as valid (pre-release must not be empty)"); - assertFalse(isValidVersionString("1.0.0+"), - "1.0.0+ is treated as valid (build metadata must not be empty)"); - - // test that versions can be parsed - assertEquals(stableVersion(1, 0, 0), fromString("1.0.0"), - "Could not parse 1.0.0"); - assertEquals( - builder(1, 2, 3).preRelease("abc", "56", "def") - .buildMetadata("2022abc99").build(), - fromString("1.2.3-abc.56.def+2022abc99"), - "Could not parse 1.2.3-abc.56.def+2022abc99"); - } - - /** - * Ensures it is impossible to create invalid version numbers - */ - @Test - public void testInvalidVersionNumbers() { - // stableVersion() - assertThrows(IllegalArgumentException.class, - () -> stableVersion(1, 0, -1), - "Negative patch tolerated by stableVersion"); - assertThrows(IllegalArgumentException.class, - () -> stableVersion(1, -2, 1), - "Negative minor version number tolerated by stableVersion"); - assertThrows(IllegalArgumentException.class, - () -> stableVersion(-3, 0, 7), - "Negative major version number tolerated by stableVersion"); - - // preRelease() - assertThrows(IllegalArgumentException.class, - () -> preRelease(1, 0, -1, "test", 2), - "Negative patch tolerated by preRelease"); - assertThrows(IllegalArgumentException.class, - () -> preRelease(1, -2, 1, "test", 2), - "Negative minor version number tolerated by preRelease"); - assertThrows(IllegalArgumentException.class, - () -> preRelease(-3, 0, 7, "test", 2), - "Negative major version number tolerated by preRelease"); - assertThrows(IllegalArgumentException.class, - () -> preRelease(1, 0, 0, "test", -1), - "Negative pre release number tolerated by preRelease"); - assertThrows(NullPointerException.class, - () -> preRelease(1, 0, 0, null, 1), "Null tolerated by preRelease"); - assertThrows(IllegalArgumentException.class, - () -> preRelease(1, 0, 0, "", 1), - "Empty string tolerated by preRelease"); - assertThrows(IllegalArgumentException.class, - () -> preRelease(1, 0, 0, "abc+cde", 1), - "Invalid string tolerated by preRelease"); - - // builder() - assertThrows(IllegalArgumentException.class, () -> builder(1, 0, -1), - "Negative patch tolerated by builder"); - assertThrows(IllegalArgumentException.class, () -> builder(1, -2, 1), - "Negative minor version number tolerated by builder"); - assertThrows(IllegalArgumentException.class, () -> builder(-3, 0, 7), - "Negative major version number tolerated by builder"); - - final SemanticVersionNumber.Builder testBuilder = builder(1, 2, 3); - // note: builder.buildMetadata(null) doesn't even compile lol - // builder.buildMetadata - assertThrows(NullPointerException.class, - () -> testBuilder.buildMetadata(null, "abc"), - "Null tolerated by builder.buildMetadata(String...)"); - assertThrows(IllegalArgumentException.class, - () -> testBuilder.buildMetadata(""), - "Empty string tolerated by builder.buildMetadata(String...)"); - assertThrows(IllegalArgumentException.class, - () -> testBuilder.buildMetadata("c%4"), - "Invalid string tolerated by builder.buildMetadata(String...)"); - assertThrows(NullPointerException.class, - () -> testBuilder.buildMetadata(List.of("abc", null)), - "Null tolerated by builder.buildMetadata(List)"); - assertThrows(IllegalArgumentException.class, - () -> testBuilder.buildMetadata(List.of("")), - "Empty string tolerated by builder.buildMetadata(List)"); - assertThrows(IllegalArgumentException.class, - () -> testBuilder.buildMetadata(List.of("")), - "Invalid string tolerated by builder.buildMetadata(List)"); - - // builder.preRelease - assertThrows(NullPointerException.class, - () -> testBuilder.preRelease(null, "abc"), - "Null tolerated by builder.preRelease(String...)"); - assertThrows(IllegalArgumentException.class, - () -> testBuilder.preRelease(""), - "Empty string tolerated by builder.preRelease(String...)"); - assertThrows(IllegalArgumentException.class, - () -> testBuilder.preRelease("c%4"), - "Invalid string tolerated by builder.preRelease(String...)"); - assertThrows(NullPointerException.class, - () -> testBuilder.preRelease(List.of("abc", null)), - "Null tolerated by builder.preRelease(List)"); - assertThrows(IllegalArgumentException.class, - () -> testBuilder.preRelease(List.of("")), - "Empty string tolerated by builder.preRelease(List)"); - assertThrows(IllegalArgumentException.class, - () -> testBuilder.preRelease(List.of("")), - "Invalid string tolerated by builder.preRelease(List)"); - - // the overloadings that accept numeric arguments - assertThrows(IllegalArgumentException.class, - () -> testBuilder.preRelease(-1), - "Negative number tolerated by builder.preRelease(int...)"); - assertThrows(IllegalArgumentException.class, - () -> testBuilder.preRelease("abc", -1), - "Negative number tolerated by builder.preRelease(String, int)"); - assertThrows(NullPointerException.class, - () -> testBuilder.preRelease(null, 1), - "Null tolerated by builder.preRelease(String, int)"); - assertThrows(IllegalArgumentException.class, - () -> testBuilder.preRelease("", 1), - "Empty string tolerated by builder.preRelease(String, int)"); - assertThrows(IllegalArgumentException.class, - () -> testBuilder.preRelease("#$#c", 1), - "Invalid string tolerated by builder.preRelease(String, int)"); - - // ensure all these attempts didn't change the builder - assertEquals(builder(1, 2, 3), testBuilder, - "Attempts at making invalid version number succeeded despite throwing errors"); - } - - /** - * Test for {@link SemanticVersionNumber#isStable} - * - * @since 2022-02-19 - */ - @Test - public void testIsStable() { - assertTrue(stableVersion(1, 0, 0).isStable(), - "1.0.0 should be stable but is not"); - assertFalse(stableVersion(0, 1, 2).isStable(), - "0.1.2 should not be stable but is"); - assertFalse(preRelease(1, 2, 3, "alpha", 5).isStable(), - "1.2.3a5 should not be stable but is"); - assertTrue( - builder(9, 9, 99) - .buildMetadata("lots-of-metadata", "abc123", "2022").build() - .isStable(), - "9.9.99+lots-of-metadata.abc123.2022 should be stable but is not"); - } - - /** - * Tests that the versions are ordered by - * {@link SemanticVersionNumber#compareTo} according to official rules. Tests - * all of the versions compared in section 11 of the SemVer 2.0.0 document - * and some more. - * - * @since 2022-02-19 - */ - @Test - public void testOrder() { - final SemanticVersionNumber v100a = builder(1, 0, 0).preRelease("alpha") - .build(); // 1.0.0-alpha - final SemanticVersionNumber v100a1 = preRelease(1, 0, 0, "alpha", 1); // 1.0.0-alpha.1 - final SemanticVersionNumber v100ab = builder(1, 0, 0) - .preRelease("alpha", "beta").build(); // 1.0.0-alpha.beta - final SemanticVersionNumber v100b = builder(1, 0, 0).preRelease("beta") - .build(); // 1.0.0-alpha - final SemanticVersionNumber v100b2 = preRelease(1, 0, 0, "beta", 2); // 1.0.0-beta.2 - final SemanticVersionNumber v100b11 = preRelease(1, 0, 0, "beta", 11); // 1.0.0-beta.11 - final SemanticVersionNumber v100rc1 = preRelease(1, 0, 0, "rc", 1); // 1.0.0-rc.1 - final SemanticVersionNumber v100 = stableVersion(1, 0, 0); - final SemanticVersionNumber v100plus = builder(1, 0, 0) - .buildMetadata("blah", "blah", "blah").build(); // 1.0.0+blah.blah.blah - final SemanticVersionNumber v200 = stableVersion(2, 0, 0); - final SemanticVersionNumber v201 = stableVersion(2, 0, 1); - final SemanticVersionNumber v210 = stableVersion(2, 1, 0); - final SemanticVersionNumber v211 = stableVersion(2, 1, 1); - final SemanticVersionNumber v300 = stableVersion(3, 0, 0); - - // test order of version numbers - assertTrue(v100a.compareTo(v100a1) < 0, "1.0.0-alpha >= 1.0.0-alpha.1"); - assertTrue(v100a1.compareTo(v100ab) < 0, - "1.0.0-alpha.1 >= 1.0.0-alpha.beta"); - assertTrue(v100ab.compareTo(v100b) < 0, "1.0.0-alpha.beta >= 1.0.0-beta"); - assertTrue(v100b.compareTo(v100b2) < 0, "1.0.0-beta >= 1.0.0-beta.2"); - assertTrue(v100b2.compareTo(v100b11) < 0, - "1.0.0-beta.2 >= 1.0.0-beta.11"); - assertTrue(v100b11.compareTo(v100rc1) < 0, "1.0.0-beta.11 >= 1.0.0-rc.1"); - assertTrue(v100rc1.compareTo(v100) < 0, "1.0.0-rc.1 >= 1.0.0"); - assertTrue(v100.compareTo(v200) < 0, "1.0.0 >= 2.0.0"); - assertTrue(v200.compareTo(v201) < 0, "2.0.0 >= 2.0.1"); - assertTrue(v201.compareTo(v210) < 0, "2.0.1 >= 2.1.0"); - assertTrue(v210.compareTo(v211) < 0, "2.1.0 >= 2.1.1"); - assertTrue(v211.compareTo(v300) < 0, "2.1.1 >= 3.0.0"); - - // test symmetry - assume previous tests passed - assertTrue(v100a1.compareTo(v100a) > 0, "1.0.0-alpha.1 <= 1.0.0-alpha"); - assertTrue(v100.compareTo(v100rc1) > 0, "1.0.0 <= 1.0.0-rc.1"); - assertTrue(v300.compareTo(v211) > 0, "3.0.0 <= 2.1.1"); - - // test transitivity - assertTrue(v100a.compareTo(v100b11) < 0, "1.0.0-alpha >= 1.0.0-beta.11"); - assertTrue(v100b.compareTo(v200) < 0, "1.0.0-beta >= 2.0.0"); - assertTrue(v100.compareTo(v300) < 0, "1.0.0 >= 3.0.0"); - assertTrue(v100a.compareTo(v300) < 0, "1.0.0-alpha >= 3.0.0"); - - // test metadata is ignored - assertEquals(0, v100.compareTo(v100plus), "Build metadata not ignored"); - // test metadata is NOT ignored by alternative comparator - assertTrue(BUILD_METADATA_COMPARATOR.compare(v100, v100plus) > 0, - "Build metadata ignored by BUILD_METADATA_COMPARATOR"); - } - - /** - * Tests that simple stable versions can be created and their parts read - * - * @since 2022-02-19 - */ - @Test - public void testSimpleStableVersions() { - final SemanticVersionNumber v100 = stableVersion(1, 0, 0); - assertEquals(1, v100.majorVersion()); - assertEquals(0, v100.minorVersion()); - assertEquals(0, v100.patchVersion()); - - final SemanticVersionNumber v925 = stableVersion(9, 2, 5); - assertEquals(9, v925.majorVersion()); - assertEquals(2, v925.minorVersion()); - assertEquals(5, v925.patchVersion()); - } - - /** - * Tests that {@link SemanticVersionNumber#toString} works for simple version - * numbers - * - * @since 2022-02-19 - */ - @Test - public void testSimpleToString() { - final SemanticVersionNumber v100 = stableVersion(1, 0, 0); - assertEquals("1.0.0", v100.toString()); - - final SemanticVersionNumber v845a1 = preRelease(8, 4, 5, "alpha", 1); - assertEquals("8.4.5-alpha.1", v845a1.toString()); - } - - /** - * Tests that simple unstable versions can be created and their parts read - * - * @since 2022-02-19 - */ - @Test - public void testSimpleUnstableVersions() { - final SemanticVersionNumber v350a1 = preRelease(3, 5, 0, "alpha", 1); - assertEquals(3, v350a1.majorVersion(), - "Incorrect major version for v3.5.0a1"); - assertEquals(5, v350a1.minorVersion(), - "Incorrect minor version for v3.5.0a1"); - assertEquals(0, v350a1.patchVersion(), - "Incorrect patch version for v3.5.0a1"); - assertEquals(List.of("alpha", "1"), v350a1.preReleaseIdentifiers(), - "Incorrect pre-release identifiers for v3.5.0a1"); - } -} diff --git a/src/test/java/sevenUnits/utils/SemanticVersionTest.java b/src/test/java/sevenUnits/utils/SemanticVersionTest.java new file mode 100644 index 0000000..877b258 --- /dev/null +++ b/src/test/java/sevenUnits/utils/SemanticVersionTest.java @@ -0,0 +1,399 @@ +/** + * Copyright (C) 2022 Adrien Hopkins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package sevenUnits.utils; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static sevenUnits.utils.SemanticVersionNumber.BUILD_METADATA_COMPARATOR; +import static sevenUnits.utils.SemanticVersionNumber.builder; +import static sevenUnits.utils.SemanticVersionNumber.fromString; +import static sevenUnits.utils.SemanticVersionNumber.isValidVersionString; +import static sevenUnits.utils.SemanticVersionNumber.preRelease; +import static sevenUnits.utils.SemanticVersionNumber.stableVersion; + +import java.util.List; + +import org.junit.jupiter.api.Test; + +/** + * Tests for {@link SemanticVersionNumber} + * + * @since 2022-02-19 + */ +public final class SemanticVersionTest { + /** + * Test for {@link SemanticVersionNumber#compatible} + * + * @since 2022-02-20 + */ + @Test + public void testCompatibility() { + assertTrue(stableVersion(1, 0, 0).compatibleWith(stableVersion(1, 0, 5)), + "1.0.0 not compatible with 1.0.5"); + assertTrue(stableVersion(1, 3, 1).compatibleWith(stableVersion(1, 4, 0)), + "1.3.1 not compatible with 1.4.0"); + + // 0.y.z should not be compatible with any other version + assertFalse(stableVersion(0, 4, 0).compatibleWith(stableVersion(0, 4, 1)), + "0.4.0 compatible with 0.4.1 (0.y.z versions should be treated as unstable/incompatbile)"); + + // upgrading major version should = incompatible + assertFalse(stableVersion(1, 0, 0).compatibleWith(stableVersion(2, 0, 0)), + "1.0.0 compatible with 2.0.0"); + + // dowgrade should = incompatible + assertFalse(stableVersion(1, 1, 0).compatibleWith(stableVersion(1, 0, 0)), + "1.1.0 compatible with 1.0.0"); + } + + /** + * Tests {@link SemanticVersionNumber#toString} for complex version numbers + * + * @since 2022-02-19 + */ + @Test + public void testComplexToString() { + final SemanticVersionNumber v1 = builder(1, 2, 3).preRelease(1, 2, 3) + .build(); + assertEquals("1.2.3-1.2.3", v1.toString()); + final SemanticVersionNumber v2 = builder(4, 5, 6).preRelease("abc", 123) + .buildMetadata("2022-02-19").build(); + assertEquals("4.5.6-abc.123+2022-02-19", v2.toString()); + final SemanticVersionNumber v3 = builder(1, 0, 0) + .preRelease("x-y-z", "--").build(); + assertEquals("1.0.0-x-y-z.--", v3.toString()); + } + + /** + * Tests that complex version can be created and their parts read + * + * @since 2022-02-19 + */ + @Test + public void testComplexVersions() { + final SemanticVersionNumber v1 = builder(1, 2, 3).preRelease(1, 2, 3) + .build(); + assertEquals(1, v1.majorVersion()); + assertEquals(2, v1.minorVersion()); + assertEquals(3, v1.patchVersion()); + assertEquals(List.of("1", "2", "3"), v1.preReleaseIdentifiers()); + assertEquals(List.of(), v1.buildMetadata()); + + final SemanticVersionNumber v2 = builder(4, 5, 6).preRelease("abc", 123) + .buildMetadata("2022-02-19").build(); + assertEquals(4, v2.majorVersion()); + assertEquals(5, v2.minorVersion()); + assertEquals(6, v2.patchVersion()); + assertEquals(List.of("abc", "123"), v2.preReleaseIdentifiers()); + assertEquals(List.of("2022-02-19"), v2.buildMetadata()); + + final SemanticVersionNumber v3 = builder(1, 0, 0) + .preRelease("x-y-z", "--").build(); + assertEquals(1, v3.majorVersion()); + assertEquals(0, v3.minorVersion()); + assertEquals(0, v3.patchVersion()); + assertEquals(List.of("x-y-z", "--"), v3.preReleaseIdentifiers()); + assertEquals(List.of(), v3.buildMetadata()); + } + + /** + * Test that semantic version strings can be parsed correctly + * + * @since 2022-02-19 + * @see SemanticVersionNumber#fromString + * @see SemanticVersionNumber#isValidVersionString + */ + @Test + public void testFromString() { + // test that the regex can match version strings + assertTrue(isValidVersionString("1.0.0"), "1.0.0 is treated as invalid"); + assertTrue(isValidVersionString("1.3.9"), "1.3.9 is treated as invalid"); + assertTrue(isValidVersionString("2.0.0-a.1"), + "2.0.0-a.1 is treated as invalid"); + assertTrue(isValidVersionString("1.0.0-a.b.c.d"), + "1.0.0-a.b.c.d is treated as invalid"); + assertTrue(isValidVersionString("1.0.0+abc"), + "1.0.0+abc is treated as invalid"); + assertTrue(isValidVersionString("1.0.0-abc+def"), + "1.0.0-abc+def is treated as invalid"); + + // test that invalid versions don't match + assertFalse(isValidVersionString("1.0"), + "1.0 is treated as valid (patch should be required)"); + assertFalse(isValidVersionString("1.A.0"), + "1.A.0 is treated as valid (main versions must be numbers)"); + assertFalse(isValidVersionString("1.0.0-"), + "1.0.0- is treated as valid (pre-release must not be empty)"); + assertFalse(isValidVersionString("1.0.0+"), + "1.0.0+ is treated as valid (build metadata must not be empty)"); + + // test that versions can be parsed + assertEquals(stableVersion(1, 0, 0), fromString("1.0.0"), + "Could not parse 1.0.0"); + assertEquals( + builder(1, 2, 3).preRelease("abc", "56", "def") + .buildMetadata("2022abc99").build(), + fromString("1.2.3-abc.56.def+2022abc99"), + "Could not parse 1.2.3-abc.56.def+2022abc99"); + } + + /** + * Ensures it is impossible to create invalid version numbers + */ + @Test + public void testInvalidVersionNumbers() { + // stableVersion() + assertThrows(IllegalArgumentException.class, + () -> stableVersion(1, 0, -1), + "Negative patch tolerated by stableVersion"); + assertThrows(IllegalArgumentException.class, + () -> stableVersion(1, -2, 1), + "Negative minor version number tolerated by stableVersion"); + assertThrows(IllegalArgumentException.class, + () -> stableVersion(-3, 0, 7), + "Negative major version number tolerated by stableVersion"); + + // preRelease() + assertThrows(IllegalArgumentException.class, + () -> preRelease(1, 0, -1, "test", 2), + "Negative patch tolerated by preRelease"); + assertThrows(IllegalArgumentException.class, + () -> preRelease(1, -2, 1, "test", 2), + "Negative minor version number tolerated by preRelease"); + assertThrows(IllegalArgumentException.class, + () -> preRelease(-3, 0, 7, "test", 2), + "Negative major version number tolerated by preRelease"); + assertThrows(IllegalArgumentException.class, + () -> preRelease(1, 0, 0, "test", -1), + "Negative pre release number tolerated by preRelease"); + assertThrows(NullPointerException.class, + () -> preRelease(1, 0, 0, null, 1), "Null tolerated by preRelease"); + assertThrows(IllegalArgumentException.class, + () -> preRelease(1, 0, 0, "", 1), + "Empty string tolerated by preRelease"); + assertThrows(IllegalArgumentException.class, + () -> preRelease(1, 0, 0, "abc+cde", 1), + "Invalid string tolerated by preRelease"); + + // builder() + assertThrows(IllegalArgumentException.class, () -> builder(1, 0, -1), + "Negative patch tolerated by builder"); + assertThrows(IllegalArgumentException.class, () -> builder(1, -2, 1), + "Negative minor version number tolerated by builder"); + assertThrows(IllegalArgumentException.class, () -> builder(-3, 0, 7), + "Negative major version number tolerated by builder"); + + final SemanticVersionNumber.Builder testBuilder = builder(1, 2, 3); + // note: builder.buildMetadata(null) doesn't even compile lol + // builder.buildMetadata + assertThrows(NullPointerException.class, + () -> testBuilder.buildMetadata(null, "abc"), + "Null tolerated by builder.buildMetadata(String...)"); + assertThrows(IllegalArgumentException.class, + () -> testBuilder.buildMetadata(""), + "Empty string tolerated by builder.buildMetadata(String...)"); + assertThrows(IllegalArgumentException.class, + () -> testBuilder.buildMetadata("c%4"), + "Invalid string tolerated by builder.buildMetadata(String...)"); + assertThrows(NullPointerException.class, + () -> testBuilder.buildMetadata(List.of("abc", null)), + "Null tolerated by builder.buildMetadata(List)"); + assertThrows(IllegalArgumentException.class, + () -> testBuilder.buildMetadata(List.of("")), + "Empty string tolerated by builder.buildMetadata(List)"); + assertThrows(IllegalArgumentException.class, + () -> testBuilder.buildMetadata(List.of("")), + "Invalid string tolerated by builder.buildMetadata(List)"); + + // builder.preRelease + assertThrows(NullPointerException.class, + () -> testBuilder.preRelease(null, "abc"), + "Null tolerated by builder.preRelease(String...)"); + assertThrows(IllegalArgumentException.class, + () -> testBuilder.preRelease(""), + "Empty string tolerated by builder.preRelease(String...)"); + assertThrows(IllegalArgumentException.class, + () -> testBuilder.preRelease("c%4"), + "Invalid string tolerated by builder.preRelease(String...)"); + assertThrows(NullPointerException.class, + () -> testBuilder.preRelease(List.of("abc", null)), + "Null tolerated by builder.preRelease(List)"); + assertThrows(IllegalArgumentException.class, + () -> testBuilder.preRelease(List.of("")), + "Empty string tolerated by builder.preRelease(List)"); + assertThrows(IllegalArgumentException.class, + () -> testBuilder.preRelease(List.of("")), + "Invalid string tolerated by builder.preRelease(List)"); + + // the overloadings that accept numeric arguments + assertThrows(IllegalArgumentException.class, + () -> testBuilder.preRelease(-1), + "Negative number tolerated by builder.preRelease(int...)"); + assertThrows(IllegalArgumentException.class, + () -> testBuilder.preRelease("abc", -1), + "Negative number tolerated by builder.preRelease(String, int)"); + assertThrows(NullPointerException.class, + () -> testBuilder.preRelease(null, 1), + "Null tolerated by builder.preRelease(String, int)"); + assertThrows(IllegalArgumentException.class, + () -> testBuilder.preRelease("", 1), + "Empty string tolerated by builder.preRelease(String, int)"); + assertThrows(IllegalArgumentException.class, + () -> testBuilder.preRelease("#$#c", 1), + "Invalid string tolerated by builder.preRelease(String, int)"); + + // ensure all these attempts didn't change the builder + assertEquals(builder(1, 2, 3), testBuilder, + "Attempts at making invalid version number succeeded despite throwing errors"); + } + + /** + * Test for {@link SemanticVersionNumber#isStable} + * + * @since 2022-02-19 + */ + @Test + public void testIsStable() { + assertTrue(stableVersion(1, 0, 0).isStable(), + "1.0.0 should be stable but is not"); + assertFalse(stableVersion(0, 1, 2).isStable(), + "0.1.2 should not be stable but is"); + assertFalse(preRelease(1, 2, 3, "alpha", 5).isStable(), + "1.2.3a5 should not be stable but is"); + assertTrue( + builder(9, 9, 99) + .buildMetadata("lots-of-metadata", "abc123", "2022").build() + .isStable(), + "9.9.99+lots-of-metadata.abc123.2022 should be stable but is not"); + } + + /** + * Tests that the versions are ordered by + * {@link SemanticVersionNumber#compareTo} according to official rules. Tests + * all of the versions compared in section 11 of the SemVer 2.0.0 document + * and some more. + * + * @since 2022-02-19 + */ + @Test + public void testOrder() { + final SemanticVersionNumber v100a = builder(1, 0, 0).preRelease("alpha") + .build(); // 1.0.0-alpha + final SemanticVersionNumber v100a1 = preRelease(1, 0, 0, "alpha", 1); // 1.0.0-alpha.1 + final SemanticVersionNumber v100ab = builder(1, 0, 0) + .preRelease("alpha", "beta").build(); // 1.0.0-alpha.beta + final SemanticVersionNumber v100b = builder(1, 0, 0).preRelease("beta") + .build(); // 1.0.0-alpha + final SemanticVersionNumber v100b2 = preRelease(1, 0, 0, "beta", 2); // 1.0.0-beta.2 + final SemanticVersionNumber v100b11 = preRelease(1, 0, 0, "beta", 11); // 1.0.0-beta.11 + final SemanticVersionNumber v100rc1 = preRelease(1, 0, 0, "rc", 1); // 1.0.0-rc.1 + final SemanticVersionNumber v100 = stableVersion(1, 0, 0); + final SemanticVersionNumber v100plus = builder(1, 0, 0) + .buildMetadata("blah", "blah", "blah").build(); // 1.0.0+blah.blah.blah + final SemanticVersionNumber v200 = stableVersion(2, 0, 0); + final SemanticVersionNumber v201 = stableVersion(2, 0, 1); + final SemanticVersionNumber v210 = stableVersion(2, 1, 0); + final SemanticVersionNumber v211 = stableVersion(2, 1, 1); + final SemanticVersionNumber v300 = stableVersion(3, 0, 0); + + // test order of version numbers + assertTrue(v100a.compareTo(v100a1) < 0, "1.0.0-alpha >= 1.0.0-alpha.1"); + assertTrue(v100a1.compareTo(v100ab) < 0, + "1.0.0-alpha.1 >= 1.0.0-alpha.beta"); + assertTrue(v100ab.compareTo(v100b) < 0, "1.0.0-alpha.beta >= 1.0.0-beta"); + assertTrue(v100b.compareTo(v100b2) < 0, "1.0.0-beta >= 1.0.0-beta.2"); + assertTrue(v100b2.compareTo(v100b11) < 0, + "1.0.0-beta.2 >= 1.0.0-beta.11"); + assertTrue(v100b11.compareTo(v100rc1) < 0, "1.0.0-beta.11 >= 1.0.0-rc.1"); + assertTrue(v100rc1.compareTo(v100) < 0, "1.0.0-rc.1 >= 1.0.0"); + assertTrue(v100.compareTo(v200) < 0, "1.0.0 >= 2.0.0"); + assertTrue(v200.compareTo(v201) < 0, "2.0.0 >= 2.0.1"); + assertTrue(v201.compareTo(v210) < 0, "2.0.1 >= 2.1.0"); + assertTrue(v210.compareTo(v211) < 0, "2.1.0 >= 2.1.1"); + assertTrue(v211.compareTo(v300) < 0, "2.1.1 >= 3.0.0"); + + // test symmetry - assume previous tests passed + assertTrue(v100a1.compareTo(v100a) > 0, "1.0.0-alpha.1 <= 1.0.0-alpha"); + assertTrue(v100.compareTo(v100rc1) > 0, "1.0.0 <= 1.0.0-rc.1"); + assertTrue(v300.compareTo(v211) > 0, "3.0.0 <= 2.1.1"); + + // test transitivity + assertTrue(v100a.compareTo(v100b11) < 0, "1.0.0-alpha >= 1.0.0-beta.11"); + assertTrue(v100b.compareTo(v200) < 0, "1.0.0-beta >= 2.0.0"); + assertTrue(v100.compareTo(v300) < 0, "1.0.0 >= 3.0.0"); + assertTrue(v100a.compareTo(v300) < 0, "1.0.0-alpha >= 3.0.0"); + + // test metadata is ignored + assertEquals(0, v100.compareTo(v100plus), "Build metadata not ignored"); + // test metadata is NOT ignored by alternative comparator + assertTrue(BUILD_METADATA_COMPARATOR.compare(v100, v100plus) > 0, + "Build metadata ignored by BUILD_METADATA_COMPARATOR"); + } + + /** + * Tests that simple stable versions can be created and their parts read + * + * @since 2022-02-19 + */ + @Test + public void testSimpleStableVersions() { + final SemanticVersionNumber v100 = stableVersion(1, 0, 0); + assertEquals(1, v100.majorVersion()); + assertEquals(0, v100.minorVersion()); + assertEquals(0, v100.patchVersion()); + + final SemanticVersionNumber v925 = stableVersion(9, 2, 5); + assertEquals(9, v925.majorVersion()); + assertEquals(2, v925.minorVersion()); + assertEquals(5, v925.patchVersion()); + } + + /** + * Tests that {@link SemanticVersionNumber#toString} works for simple version + * numbers + * + * @since 2022-02-19 + */ + @Test + public void testSimpleToString() { + final SemanticVersionNumber v100 = stableVersion(1, 0, 0); + assertEquals("1.0.0", v100.toString()); + + final SemanticVersionNumber v845a1 = preRelease(8, 4, 5, "alpha", 1); + assertEquals("8.4.5-alpha.1", v845a1.toString()); + } + + /** + * Tests that simple unstable versions can be created and their parts read + * + * @since 2022-02-19 + */ + @Test + public void testSimpleUnstableVersions() { + final SemanticVersionNumber v350a1 = preRelease(3, 5, 0, "alpha", 1); + assertEquals(3, v350a1.majorVersion(), + "Incorrect major version for v3.5.0a1"); + assertEquals(5, v350a1.minorVersion(), + "Incorrect minor version for v3.5.0a1"); + assertEquals(0, v350a1.patchVersion(), + "Incorrect patch version for v3.5.0a1"); + assertEquals(List.of("alpha", "1"), v350a1.preReleaseIdentifiers(), + "Incorrect pre-release identifiers for v3.5.0a1"); + } +} diff --git a/src/test/java/sevenUnitsGUI/package-info.java b/src/test/java/sevenUnitsGUI/package-info.java deleted file mode 100644 index 96bdbd9..0000000 --- a/src/test/java/sevenUnitsGUI/package-info.java +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Copyright (C) 2022 Adrien Hopkins - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -/** - * Tests for the new 7Units GUI - * - * @author Adrien Hopkins - * @since 2022-01-29 - */ -package sevenUnitsGUI; \ No newline at end of file -- cgit v1.2.3 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 --- CHANGELOG.org | 3 + .../sevenUnits/converterGUI/SevenUnitsGUI.java | 2 +- src/main/java/sevenUnits/unit/BaseDimension.java | 50 ++-- src/main/java/sevenUnits/unit/BaseUnit.java | 2 + src/main/java/sevenUnits/unit/BritishImperial.java | 2 + src/main/java/sevenUnits/unit/FunctionalUnit.java | 1 + .../java/sevenUnits/unit/FunctionalUnitlike.java | 1 + src/main/java/sevenUnits/unit/LinearUnit.java | 1 + src/main/java/sevenUnits/unit/Metric.java | 1 + src/main/java/sevenUnits/unit/MultiUnit.java | 1 + src/main/java/sevenUnits/unit/NameSymbol.java | 280 ------------------- src/main/java/sevenUnits/unit/Nameable.java | 59 ---- src/main/java/sevenUnits/unit/Unit.java | 2 + src/main/java/sevenUnits/unit/UnitDatabase.java | 23 +- src/main/java/sevenUnits/unit/UnitPrefix.java | 1 + src/main/java/sevenUnits/unit/UnitValue.java | 2 + src/main/java/sevenUnits/unit/Unitlike.java | 2 + src/main/java/sevenUnits/unit/UnitlikeValue.java | 2 + src/main/java/sevenUnits/utils/NameSymbol.java | 299 +++++++++++++++++++++ src/main/java/sevenUnits/utils/Nameable.java | 59 ++++ .../java/sevenUnits/utils/NamedObjectProduct.java | 7 +- src/main/java/sevenUnits/utils/ObjectProduct.java | 6 +- src/main/java/sevenUnitsGUI/Presenter.java | 202 +++++++++++++- src/main/java/sevenUnitsGUI/TabbedView.java | 14 +- .../java/sevenUnitsGUI/UnitConversionView.java | 3 +- src/main/java/sevenUnitsGUI/ViewBot.java | 7 +- .../java/sevenUnits/unit/UnitDatabaseTest.java | 1 + src/test/java/sevenUnits/unit/UnitTest.java | 1 + src/test/java/sevenUnitsGUI/PresenterTest.java | 11 +- 29 files changed, 636 insertions(+), 409 deletions(-) delete mode 100644 src/main/java/sevenUnits/unit/NameSymbol.java delete mode 100644 src/main/java/sevenUnits/unit/Nameable.java create mode 100644 src/main/java/sevenUnits/utils/NameSymbol.java create mode 100644 src/main/java/sevenUnits/utils/Nameable.java diff --git a/CHANGELOG.org b/CHANGELOG.org index 2abee52..3bc46c2 100644 --- a/CHANGELOG.org +++ b/CHANGELOG.org @@ -4,8 +4,11 @@ *** Added - Added tests for the GUI - Added an object for the version numbers (SemanticVersionNumber) + - Added some toString methods to NameSymbol *** 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 - 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 9c6ae0a..55e1546 100644 --- a/src/main/java/sevenUnits/converterGUI/SevenUnitsGUI.java +++ b/src/main/java/sevenUnits/converterGUI/SevenUnitsGUI.java @@ -70,12 +70,12 @@ import sevenUnits.unit.BritishImperial; import sevenUnits.unit.LinearUnit; import sevenUnits.unit.LinearUnitValue; import sevenUnits.unit.Metric; -import sevenUnits.unit.NameSymbol; import sevenUnits.unit.Unit; import sevenUnits.unit.UnitDatabase; import sevenUnits.unit.UnitPrefix; import sevenUnits.unit.UnitValue; import sevenUnits.utils.ConditionalExistenceCollections; +import sevenUnits.utils.NameSymbol; import sevenUnits.utils.ObjectProduct; /** diff --git a/src/main/java/sevenUnits/unit/BaseDimension.java b/src/main/java/sevenUnits/unit/BaseDimension.java index d5e98ca..bcd57d9 100644 --- a/src/main/java/sevenUnits/unit/BaseDimension.java +++ b/src/main/java/sevenUnits/unit/BaseDimension.java @@ -18,70 +18,58 @@ package sevenUnits.unit; import java.util.Objects; +import sevenUnits.utils.NameSymbol; +import sevenUnits.utils.Nameable; + /** * A dimension that defines a {@code BaseUnit} * * @author Adrien Hopkins * @since 2019-10-16 */ -public final class BaseDimension { +public final class BaseDimension implements Nameable { /** * Gets a {@code BaseDimension} with the provided name and symbol. * - * @param name - * name of dimension - * @param symbol - * symbol used for dimension + * @param name name of dimension + * @param symbol symbol used for dimension * @return dimension * @since 2019-10-16 */ public static BaseDimension valueOf(final String name, final String symbol) { return new BaseDimension(name, symbol); } - + /** * The name of the dimension. */ private final String name; /** - * The symbol used by the dimension. Symbols should be short, generally one or two characters. + * The symbol used by the dimension. Symbols should be short, generally one + * or two characters. */ private final String symbol; - + /** * Creates the {@code BaseDimension}. * - * @param name - * name of unit - * @param symbol - * symbol of unit - * @throws NullPointerException - * if any argument is null + * @param name name of unit + * @param symbol symbol of unit + * @throws NullPointerException if any argument is null * @since 2019-10-16 */ private BaseDimension(final String name, final String symbol) { this.name = Objects.requireNonNull(name, "name must not be null."); this.symbol = Objects.requireNonNull(symbol, "symbol must not be null."); } - - /** - * @return name - * @since 2019-10-16 - */ - public final String getName() { - return this.name; - } - - /** - * @return symbol - * @since 2019-10-16 - */ - public final String getSymbol() { - return this.symbol; + + @Override + public NameSymbol getNameSymbol() { + return NameSymbol.of(this.name, this.symbol); } - + @Override public String toString() { - return String.format("%s (%s)", this.getName(), this.getSymbol()); + return String.format("%s (%s)", this.name, this.symbol); } } diff --git a/src/main/java/sevenUnits/unit/BaseUnit.java b/src/main/java/sevenUnits/unit/BaseUnit.java index ee2c277..dba7f52 100644 --- a/src/main/java/sevenUnits/unit/BaseUnit.java +++ b/src/main/java/sevenUnits/unit/BaseUnit.java @@ -20,6 +20,8 @@ import java.util.HashSet; import java.util.Objects; import java.util.Set; +import sevenUnits.utils.NameSymbol; + /** * A unit that other units are defined by. *

diff --git a/src/main/java/sevenUnits/unit/BritishImperial.java b/src/main/java/sevenUnits/unit/BritishImperial.java index 743beeb..c6e65fb 100644 --- a/src/main/java/sevenUnits/unit/BritishImperial.java +++ b/src/main/java/sevenUnits/unit/BritishImperial.java @@ -16,6 +16,8 @@ */ package sevenUnits.unit; +import sevenUnits.utils.NameSymbol; + /** * A static utility class that contains units in the British Imperial system. * diff --git a/src/main/java/sevenUnits/unit/FunctionalUnit.java b/src/main/java/sevenUnits/unit/FunctionalUnit.java index df457e4..720b0af 100644 --- a/src/main/java/sevenUnits/unit/FunctionalUnit.java +++ b/src/main/java/sevenUnits/unit/FunctionalUnit.java @@ -19,6 +19,7 @@ package sevenUnits.unit; import java.util.Objects; import java.util.function.DoubleUnaryOperator; +import sevenUnits.utils.NameSymbol; import sevenUnits.utils.ObjectProduct; /** diff --git a/src/main/java/sevenUnits/unit/FunctionalUnitlike.java b/src/main/java/sevenUnits/unit/FunctionalUnitlike.java index 2ee9e19..d6046c0 100644 --- a/src/main/java/sevenUnits/unit/FunctionalUnitlike.java +++ b/src/main/java/sevenUnits/unit/FunctionalUnitlike.java @@ -19,6 +19,7 @@ package sevenUnits.unit; import java.util.function.DoubleFunction; import java.util.function.ToDoubleFunction; +import sevenUnits.utils.NameSymbol; import sevenUnits.utils.ObjectProduct; /** diff --git a/src/main/java/sevenUnits/unit/LinearUnit.java b/src/main/java/sevenUnits/unit/LinearUnit.java index 25c2e2e..deefc9a 100644 --- a/src/main/java/sevenUnits/unit/LinearUnit.java +++ b/src/main/java/sevenUnits/unit/LinearUnit.java @@ -19,6 +19,7 @@ package sevenUnits.unit; import java.util.Objects; import sevenUnits.utils.DecimalComparison; +import sevenUnits.utils.NameSymbol; import sevenUnits.utils.ObjectProduct; import sevenUnits.utils.UncertainDouble; diff --git a/src/main/java/sevenUnits/unit/Metric.java b/src/main/java/sevenUnits/unit/Metric.java index 3c4d291..78e3769 100644 --- a/src/main/java/sevenUnits/unit/Metric.java +++ b/src/main/java/sevenUnits/unit/Metric.java @@ -18,6 +18,7 @@ package sevenUnits.unit; import java.util.Set; +import sevenUnits.utils.NameSymbol; import sevenUnits.utils.ObjectProduct; /** diff --git a/src/main/java/sevenUnits/unit/MultiUnit.java b/src/main/java/sevenUnits/unit/MultiUnit.java index 83cdb03..bc240e3 100644 --- a/src/main/java/sevenUnits/unit/MultiUnit.java +++ b/src/main/java/sevenUnits/unit/MultiUnit.java @@ -20,6 +20,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import sevenUnits.utils.NameSymbol; import sevenUnits.utils.ObjectProduct; /** diff --git a/src/main/java/sevenUnits/unit/NameSymbol.java b/src/main/java/sevenUnits/unit/NameSymbol.java deleted file mode 100644 index 3e26138..0000000 --- a/src/main/java/sevenUnits/unit/NameSymbol.java +++ /dev/null @@ -1,280 +0,0 @@ -/** - * Copyright (C) 2019 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.unit; - -import java.util.Arrays; -import java.util.Collections; -import java.util.HashSet; -import java.util.Iterator; -import java.util.Objects; -import java.util.Optional; -import java.util.Set; - -/** - * A class that can be used to specify names and a symbol for a unit. - * - * @author Adrien Hopkins - * @since 2019-10-21 - */ -public final class NameSymbol { - public static final NameSymbol EMPTY = new NameSymbol(Optional.empty(), - Optional.empty(), new HashSet<>()); - - /** - * 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. - */ - private static final NameSymbol create(final String name, - final String symbol, final Set otherNames) { - final Optional primaryName; - - if (name == null && !otherNames.isEmpty()) { - // get primary name and remove it from savedNames - final Iterator it = otherNames.iterator(); - assert it.hasNext(); - primaryName = Optional.of(it.next()); - otherNames.remove(primaryName.get()); - } else { - primaryName = Optional.ofNullable(name); - } - - return new NameSymbol(primaryName, Optional.ofNullable(symbol), - otherNames); - } - - /** - * Gets a {@code NameSymbol} with a primary name, a symbol and no other - * names. - * - * @param name name to use - * @param symbol symbol to use - * @return NameSymbol instance - * @since 2019-10-21 - * @throws NullPointerException if name or symbol is null - */ - public static final NameSymbol of(final String name, final String symbol) { - return new NameSymbol(Optional.of(name), Optional.of(symbol), - new HashSet<>()); - } - - /** - * Gets a {@code NameSymbol} with a primary name, a symbol and additional - * names. - * - * @param name name to use - * @param symbol symbol to use - * @param otherNames other names to use - * @return NameSymbol instance - * @since 2019-10-21 - * @throws NullPointerException if any argument is null - */ - public static final NameSymbol of(final String name, final String symbol, - final Set otherNames) { - return new NameSymbol(Optional.of(name), Optional.of(symbol), - new HashSet<>(Objects.requireNonNull(otherNames, - "otherNames must not be null."))); - } - - /** - * h * Gets a {@code NameSymbol} with a primary name, a symbol and additional - * names. - * - * @param name name to use - * @param symbol symbol to use - * @param otherNames other names to use - * @return NameSymbol instance - * @since 2019-10-21 - * @throws NullPointerException if any argument is null - */ - public static final NameSymbol of(final String name, final String symbol, - final String... otherNames) { - return new NameSymbol(Optional.of(name), Optional.of(symbol), - new HashSet<>(Arrays.asList(Objects.requireNonNull(otherNames, - "otherNames must not be null.")))); - } - - /** - * Gets a {@code NameSymbol} with a primary name, no symbol, and no other - * names. - * - * @param name name to use - * @return NameSymbol instance - * @since 2019-10-21 - * @throws NullPointerException if name is null - */ - public static final NameSymbol ofName(final String name) { - return new NameSymbol(Optional.of(name), Optional.empty(), - new HashSet<>()); - } - - /** - * Gets a {@code NameSymbol} with a primary name, a symbol and additional - * names. - *

- * If any argument is null, this static factory replaces it with an empty - * Optional or empty Set. - *

- * If {@code name} is null and {@code otherNames} is not empty, a primary - * name will be picked from {@code otherNames}. This name will not appear in - * getOtherNames(). - * - * @param name name to use - * @param symbol symbol to use - * @param otherNames other names to use - * @return NameSymbol instance - * @since 2019-11-26 - */ - public static final NameSymbol ofNullable(final String name, - final String symbol, final Set otherNames) { - return NameSymbol.create(name, symbol, - otherNames == null ? new HashSet<>() : new HashSet<>(otherNames)); - } - - /** - * h * Gets a {@code NameSymbol} with a primary name, a symbol and additional - * names. - *

- * If any argument is null, this static factory replaces it with an empty - * Optional or empty Set. - *

- * If {@code name} is null and {@code otherNames} is not empty, a primary - * name will be picked from {@code otherNames}. This name will not appear in - * getOtherNames(). - * - * @param name name to use - * @param symbol symbol to use - * @param otherNames other names to use - * @return NameSymbol instance - * @since 2019-11-26 - */ - public static final NameSymbol ofNullable(final String name, - final String symbol, final String... otherNames) { - return create(name, symbol, otherNames == null ? new HashSet<>() - : new HashSet<>(Arrays.asList(otherNames))); - } - - /** - * Gets a {@code NameSymbol} with a symbol and no names. - * - * @param symbol symbol to use - * @return NameSymbol instance - * @since 2019-10-21 - * @throws NullPointerException if symbol is null - */ - public static final NameSymbol ofSymbol(final String symbol) { - return new NameSymbol(Optional.empty(), Optional.of(symbol), - new HashSet<>()); - } - - private final Optional primaryName; - private final Optional symbol; - - private final Set otherNames; - - /** - * Creates the {@code NameSymbol}. - * - * @param primaryName primary name of unit - * @param symbol symbol used to represent unit - * @param otherNames other names and/or spellings, should be a mutable copy - * of the argument - * @since 2019-10-21 - */ - private NameSymbol(final Optional primaryName, - final Optional symbol, final Set otherNames) { - this.primaryName = primaryName; - this.symbol = symbol; - otherNames.remove(null); - this.otherNames = Collections.unmodifiableSet(otherNames); - - if (this.primaryName.isEmpty()) { - assert this.otherNames.isEmpty(); - } - } - - @Override - public boolean equals(Object obj) { - if (this == obj) - return true; - if (!(obj instanceof NameSymbol)) - return false; - final NameSymbol other = (NameSymbol) obj; - if (this.otherNames == null) { - if (other.otherNames != null) - return false; - } else if (!this.otherNames.equals(other.otherNames)) - return false; - if (this.primaryName == null) { - if (other.primaryName != null) - return false; - } else if (!this.primaryName.equals(other.primaryName)) - return false; - if (this.symbol == null) { - if (other.symbol != null) - return false; - } else if (!this.symbol.equals(other.symbol)) - return false; - return true; - } - - /** - * @return otherNames - * @since 2019-10-21 - */ - public final Set getOtherNames() { - return this.otherNames; - } - - /** - * @return primaryName - * @since 2019-10-21 - */ - public final Optional getPrimaryName() { - return this.primaryName; - } - - /** - * @return symbol - * @since 2019-10-21 - */ - public final Optional getSymbol() { - return this.symbol; - } - - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result - + (this.otherNames == null ? 0 : this.otherNames.hashCode()); - result = prime * result - + (this.primaryName == null ? 0 : this.primaryName.hashCode()); - result = prime * result - + (this.symbol == null ? 0 : this.symbol.hashCode()); - return result; - } - - /** - * @return true iff this {@code NameSymbol} contains no names or symbols. - */ - public final boolean isEmpty() { - // if primaryName is empty, otherNames must also be empty - return this.primaryName.isEmpty() && this.symbol.isEmpty(); - } -} \ No newline at end of file diff --git a/src/main/java/sevenUnits/unit/Nameable.java b/src/main/java/sevenUnits/unit/Nameable.java deleted file mode 100644 index ed23687..0000000 --- a/src/main/java/sevenUnits/unit/Nameable.java +++ /dev/null @@ -1,59 +0,0 @@ -/** - * Copyright (C) 2020 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.unit; - -import java.util.Optional; -import java.util.Set; - -/** - * An object that can hold one or more names, and possibly a symbol. The name - * and symbol data should be immutable. - * - * @since 2020-09-07 - */ -public interface Nameable { - /** - * @return a {@code NameSymbol} that contains this object's primary name, - * symbol and other names - * @since 2020-09-07 - */ - NameSymbol getNameSymbol(); - - /** - * @return set of alternate names - * @since 2020-09-07 - */ - default Set getOtherNames() { - return this.getNameSymbol().getOtherNames(); - } - - /** - * @return preferred name of object - * @since 2020-09-07 - */ - default Optional getPrimaryName() { - return this.getNameSymbol().getPrimaryName(); - } - - /** - * @return short symbol representing object - * @since 2020-09-07 - */ - default Optional getSymbol() { - return this.getNameSymbol().getSymbol(); - } -} diff --git a/src/main/java/sevenUnits/unit/Unit.java b/src/main/java/sevenUnits/unit/Unit.java index 005b6f7..9866e9c 100644 --- a/src/main/java/sevenUnits/unit/Unit.java +++ b/src/main/java/sevenUnits/unit/Unit.java @@ -22,6 +22,8 @@ import java.util.Objects; import java.util.function.DoubleUnaryOperator; import sevenUnits.utils.DecimalComparison; +import sevenUnits.utils.NameSymbol; +import sevenUnits.utils.Nameable; import sevenUnits.utils.ObjectProduct; /** 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); } diff --git a/src/main/java/sevenUnits/unit/UnitPrefix.java b/src/main/java/sevenUnits/unit/UnitPrefix.java index 308f4b0..bf9d1fd 100644 --- a/src/main/java/sevenUnits/unit/UnitPrefix.java +++ b/src/main/java/sevenUnits/unit/UnitPrefix.java @@ -21,6 +21,7 @@ import java.util.Optional; import java.util.Set; import sevenUnits.utils.DecimalComparison; +import sevenUnits.utils.NameSymbol; /** * A prefix that can be applied to a {@code LinearUnit} to multiply it by some value diff --git a/src/main/java/sevenUnits/unit/UnitValue.java b/src/main/java/sevenUnits/unit/UnitValue.java index f6d18f8..339263d 100644 --- a/src/main/java/sevenUnits/unit/UnitValue.java +++ b/src/main/java/sevenUnits/unit/UnitValue.java @@ -19,6 +19,8 @@ package sevenUnits.unit; import java.util.Objects; import java.util.Optional; +import sevenUnits.utils.NameSymbol; + /** * A value expressed in a unit. * diff --git a/src/main/java/sevenUnits/unit/Unitlike.java b/src/main/java/sevenUnits/unit/Unitlike.java index d2dcbbb..68de2c2 100644 --- a/src/main/java/sevenUnits/unit/Unitlike.java +++ b/src/main/java/sevenUnits/unit/Unitlike.java @@ -22,6 +22,8 @@ import java.util.Objects; import java.util.function.DoubleFunction; import java.util.function.ToDoubleFunction; +import sevenUnits.utils.NameSymbol; +import sevenUnits.utils.Nameable; import sevenUnits.utils.ObjectProduct; /** diff --git a/src/main/java/sevenUnits/unit/UnitlikeValue.java b/src/main/java/sevenUnits/unit/UnitlikeValue.java index edc13ca..26354b1 100644 --- a/src/main/java/sevenUnits/unit/UnitlikeValue.java +++ b/src/main/java/sevenUnits/unit/UnitlikeValue.java @@ -18,6 +18,8 @@ package sevenUnits.unit; import java.util.Optional; +import sevenUnits.utils.NameSymbol; + /** * * @since 2020-09-07 diff --git a/src/main/java/sevenUnits/utils/NameSymbol.java b/src/main/java/sevenUnits/utils/NameSymbol.java new file mode 100644 index 0000000..aea274b --- /dev/null +++ b/src/main/java/sevenUnits/utils/NameSymbol.java @@ -0,0 +1,299 @@ +/** + * Copyright (C) 2019 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.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; + +/** + * A class that can be used to specify names and a symbol for a unit. + * + * @author Adrien Hopkins + * @since 2019-10-21 + */ +public final class NameSymbol { + public static final NameSymbol EMPTY = new NameSymbol(Optional.empty(), + Optional.empty(), new HashSet<>()); + + /** + * 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. + */ + private static final NameSymbol create(final String name, + final String symbol, final Set otherNames) { + final Optional primaryName; + + if (name == null && !otherNames.isEmpty()) { + // get primary name and remove it from savedNames + final Iterator it = otherNames.iterator(); + assert it.hasNext(); + primaryName = Optional.of(it.next()); + otherNames.remove(primaryName.get()); + } else { + primaryName = Optional.ofNullable(name); + } + + return new NameSymbol(primaryName, Optional.ofNullable(symbol), + otherNames); + } + + /** + * Gets a {@code NameSymbol} with a primary name, a symbol and no other + * names. + * + * @param name name to use + * @param symbol symbol to use + * @return NameSymbol instance + * @since 2019-10-21 + * @throws NullPointerException if name or symbol is null + */ + public static final NameSymbol of(final String name, final String symbol) { + return new NameSymbol(Optional.of(name), Optional.of(symbol), + new HashSet<>()); + } + + /** + * Gets a {@code NameSymbol} with a primary name, a symbol and additional + * names. + * + * @param name name to use + * @param symbol symbol to use + * @param otherNames other names to use + * @return NameSymbol instance + * @since 2019-10-21 + * @throws NullPointerException if any argument is null + */ + public static final NameSymbol of(final String name, final String symbol, + final Set otherNames) { + return new NameSymbol(Optional.of(name), Optional.of(symbol), + new HashSet<>(Objects.requireNonNull(otherNames, + "otherNames must not be null."))); + } + + /** + * h * Gets a {@code NameSymbol} with a primary name, a symbol and additional + * names. + * + * @param name name to use + * @param symbol symbol to use + * @param otherNames other names to use + * @return NameSymbol instance + * @since 2019-10-21 + * @throws NullPointerException if any argument is null + */ + public static final NameSymbol of(final String name, final String symbol, + final String... otherNames) { + return new NameSymbol(Optional.of(name), Optional.of(symbol), + new HashSet<>(Arrays.asList(Objects.requireNonNull(otherNames, + "otherNames must not be null.")))); + } + + /** + * Gets a {@code NameSymbol} with a primary name, no symbol, and no other + * names. + * + * @param name name to use + * @return NameSymbol instance + * @since 2019-10-21 + * @throws NullPointerException if name is null + */ + public static final NameSymbol ofName(final String name) { + return new NameSymbol(Optional.of(name), Optional.empty(), + new HashSet<>()); + } + + /** + * Gets a {@code NameSymbol} with a primary name, a symbol and additional + * names. + *

+ * If any argument is null, this static factory replaces it with an empty + * Optional or empty Set. + *

+ * If {@code name} is null and {@code otherNames} is not empty, a primary + * name will be picked from {@code otherNames}. This name will not appear in + * getOtherNames(). + * + * @param name name to use + * @param symbol symbol to use + * @param otherNames other names to use + * @return NameSymbol instance + * @since 2019-11-26 + */ + public static final NameSymbol ofNullable(final String name, + final String symbol, final Set otherNames) { + return NameSymbol.create(name, symbol, + otherNames == null ? new HashSet<>() : new HashSet<>(otherNames)); + } + + /** + * h * Gets a {@code NameSymbol} with a primary name, a symbol and additional + * names. + *

+ * If any argument is null, this static factory replaces it with an empty + * Optional or empty Set. + *

+ * If {@code name} is null and {@code otherNames} is not empty, a primary + * name will be picked from {@code otherNames}. This name will not appear in + * getOtherNames(). + * + * @param name name to use + * @param symbol symbol to use + * @param otherNames other names to use + * @return NameSymbol instance + * @since 2019-11-26 + */ + public static final NameSymbol ofNullable(final String name, + final String symbol, final String... otherNames) { + return create(name, symbol, otherNames == null ? new HashSet<>() + : new HashSet<>(Arrays.asList(otherNames))); + } + + /** + * Gets a {@code NameSymbol} with a symbol and no names. + * + * @param symbol symbol to use + * @return NameSymbol instance + * @since 2019-10-21 + * @throws NullPointerException if symbol is null + */ + public static final NameSymbol ofSymbol(final String symbol) { + return new NameSymbol(Optional.empty(), Optional.of(symbol), + new HashSet<>()); + } + + private final Optional primaryName; + private final Optional symbol; + + private final Set otherNames; + + /** + * Creates the {@code NameSymbol}. + * + * @param primaryName primary name of unit + * @param symbol symbol used to represent unit + * @param otherNames other names and/or spellings, should be a mutable copy + * of the argument + * @since 2019-10-21 + */ + private NameSymbol(final Optional primaryName, + final Optional symbol, final Set otherNames) { + this.primaryName = primaryName; + this.symbol = symbol; + otherNames.remove(null); + this.otherNames = Collections.unmodifiableSet(otherNames); + + if (this.primaryName.isEmpty()) { + assert this.otherNames.isEmpty(); + } + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (!(obj instanceof NameSymbol)) + return false; + final NameSymbol other = (NameSymbol) obj; + if (this.otherNames == null) { + if (other.otherNames != null) + return false; + } else if (!this.otherNames.equals(other.otherNames)) + return false; + if (this.primaryName == null) { + if (other.primaryName != null) + return false; + } else if (!this.primaryName.equals(other.primaryName)) + return false; + if (this.symbol == null) { + if (other.symbol != null) + return false; + } else if (!this.symbol.equals(other.symbol)) + return false; + return true; + } + + /** + * @return otherNames + * @since 2019-10-21 + */ + public final Set getOtherNames() { + return this.otherNames; + } + + /** + * @return primaryName + * @since 2019-10-21 + */ + public final Optional getPrimaryName() { + return this.primaryName; + } + + /** + * @return symbol + * @since 2019-10-21 + */ + public final Optional getSymbol() { + return this.symbol; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + + (this.otherNames == null ? 0 : this.otherNames.hashCode()); + result = prime * result + + (this.primaryName == null ? 0 : this.primaryName.hashCode()); + result = prime * result + + (this.symbol == null ? 0 : this.symbol.hashCode()); + return result; + } + + /** + * @return true iff this {@code NameSymbol} contains no names or symbols. + */ + public final boolean isEmpty() { + // if primaryName is empty, otherNames must also be empty + 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()) + return "NameSymbol.EMPTY"; + else if (this.primaryName.isPresent() && this.symbol.isPresent()) + return this.primaryName + " (" + this.symbol + ")"; + else + return this.primaryName.orElseGet(this.symbol::orElseThrow); + } +} \ No newline at end of file diff --git a/src/main/java/sevenUnits/utils/Nameable.java b/src/main/java/sevenUnits/utils/Nameable.java new file mode 100644 index 0000000..3cfc05a --- /dev/null +++ b/src/main/java/sevenUnits/utils/Nameable.java @@ -0,0 +1,59 @@ +/** + * Copyright (C) 2020 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.Optional; +import java.util.Set; + +/** + * An object that can hold one or more names, and possibly a symbol. The name + * and symbol data should be immutable. + * + * @since 2020-09-07 + */ +public interface Nameable { + /** + * @return a {@code NameSymbol} that contains this object's primary name, + * symbol and other names + * @since 2020-09-07 + */ + NameSymbol getNameSymbol(); + + /** + * @return set of alternate names + * @since 2020-09-07 + */ + default Set getOtherNames() { + return this.getNameSymbol().getOtherNames(); + } + + /** + * @return preferred name of object + * @since 2020-09-07 + */ + default Optional getPrimaryName() { + return this.getNameSymbol().getPrimaryName(); + } + + /** + * @return short symbol representing object + * @since 2020-09-07 + */ + default Optional getSymbol() { + return this.getNameSymbol().getSymbol(); + } +} diff --git a/src/main/java/sevenUnits/utils/NamedObjectProduct.java b/src/main/java/sevenUnits/utils/NamedObjectProduct.java index 514f0b1..9c3079c 100644 --- a/src/main/java/sevenUnits/utils/NamedObjectProduct.java +++ b/src/main/java/sevenUnits/utils/NamedObjectProduct.java @@ -18,9 +18,6 @@ package sevenUnits.utils; import java.util.Map; -import sevenUnits.unit.NameSymbol; -import sevenUnits.unit.Nameable; - /** * An ObjectProduct with name(s) and/or a symbol. Can be created with the * {@link ObjectProduct#withName} method. @@ -43,4 +40,8 @@ public class NamedObjectProduct extends ObjectProduct return this.nameSymbol; } + @Override + public String toString() { + return this.nameSymbol.toString() + ", " + super.toString(); + } } diff --git a/src/main/java/sevenUnits/utils/ObjectProduct.java b/src/main/java/sevenUnits/utils/ObjectProduct.java index d4f88b9..926ce10 100644 --- a/src/main/java/sevenUnits/utils/ObjectProduct.java +++ b/src/main/java/sevenUnits/utils/ObjectProduct.java @@ -26,8 +26,6 @@ import java.util.Objects; import java.util.Set; import java.util.function.Function; -import sevenUnits.unit.NameSymbol; - /** * An immutable product of multiple objects of a type, such as base units. The * objects can be multiplied and exponentiated. @@ -246,7 +244,9 @@ public class ObjectProduct { */ @Override public String toString() { - return this.toString(Object::toString); + return this.toString(o -> o instanceof Nameable + ? ((Nameable) o).getNameSymbol().shortName() + : o.toString()); } /** diff --git a/src/main/java/sevenUnitsGUI/Presenter.java b/src/main/java/sevenUnitsGUI/Presenter.java index 23a631d..be02364 100644 --- a/src/main/java/sevenUnitsGUI/Presenter.java +++ b/src/main/java/sevenUnitsGUI/Presenter.java @@ -19,17 +19,26 @@ package sevenUnitsGUI; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.OptionalDouble; import java.util.Scanner; +import java.util.Set; import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; import sevenUnits.ProgramInfo; import sevenUnits.unit.BaseDimension; +import sevenUnits.unit.BritishImperial; +import sevenUnits.unit.Metric; import sevenUnits.unit.Unit; import sevenUnits.unit.UnitDatabase; import sevenUnits.unit.UnitPrefix; +import sevenUnits.unit.UnitValue; import sevenUnits.utils.ObjectProduct; import sevenUnits.utils.UncertainDouble; @@ -40,6 +49,44 @@ import sevenUnits.utils.UncertainDouble; * @since 2021-12-15 */ public final class Presenter { + /** The default place where settings are stored. */ + private static final String DEFAULT_SETTINGS_FILEPATH = "settings.txt"; + /** The default place where units are stored. */ + private static final String DEFAULT_UNITS_FILEPATH = "/unitsfile.txt"; + /** The default place where dimensions are stored. */ + private static final String DEFAULT_DIMENSIONS_FILEPATH = "/dimensionfile.txt"; + /** The default place where exceptions are stored. */ + private static final String DEFAULT_EXCEPTIONS_FILEPATH = "/metric_exceptions.txt"; + + /** + * Adds default units and dimensions to a database. + * + * @param database database to add to + * @since 2019-04-14 + * @since v0.2.0 + */ + private static void addDefaults(final UnitDatabase database) { + database.addUnit("metre", Metric.METRE); + database.addUnit("kilogram", Metric.KILOGRAM); + database.addUnit("gram", Metric.KILOGRAM.dividedBy(1000)); + database.addUnit("second", Metric.SECOND); + database.addUnit("ampere", Metric.AMPERE); + database.addUnit("kelvin", Metric.KELVIN); + database.addUnit("mole", Metric.MOLE); + database.addUnit("candela", Metric.CANDELA); + database.addUnit("bit", Metric.BIT); + database.addUnit("unit", Metric.ONE); + // nonlinear units - must be loaded manually + database.addUnit("tempCelsius", Metric.CELSIUS); + database.addUnit("tempFahrenheit", BritishImperial.FAHRENHEIT); + + // load initial dimensions + database.addDimension("LENGTH", Metric.Dimensions.LENGTH); + database.addDimension("MASS", Metric.Dimensions.MASS); + database.addDimension("TIME", Metric.Dimensions.TIME); + database.addDimension("TEMPERATURE", Metric.Dimensions.TEMPERATURE); + } + /** * @return text in About file * @since 2022-02-19 @@ -85,6 +132,25 @@ public final class Presenter { return Presenter.class.getResourceAsStream(filepath); } + /** + * Accepts a collection and returns a set with the unique elements in that + * collection + * + * @param type of element in collection + * @param collection collection to uniquify + * @return unique collection + * @since 2022-02-26 + */ + private static Set unique(Collection collection) { + final Set uniqueSet = new HashSet<>(); + for (final E e : collection) { + if (!uniqueSet.contains(e)) { + uniqueSet.add(e); + } + } + return uniqueSet; + } + /** * @return {@code line} with any comments removed. * @since 2021-03-13 @@ -126,6 +192,13 @@ public final class Presenter { */ private Predicate> prefixRepetitionRule; + /** + * 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 @@ -148,19 +221,42 @@ public final class Presenter { public Presenter(View view) { this.view = view; this.database = new UnitDatabase(); + addDefaults(this.database); + + // load units and prefixes + try (final InputStream units = inputStream(DEFAULT_UNITS_FILEPATH)) { + this.database.loadUnitsFromStream(units); + } catch (final IOException e) { + throw new AssertionError("Loading of unitsfile.txt failed.", e); + } + + // load dimensions + try (final InputStream dimensions = inputStream( + DEFAULT_DIMENSIONS_FILEPATH)) { + this.database.loadDimensionsFromStream(dimensions); + } catch (final IOException e) { + throw new AssertionError("Loading of dimensionfile.txt failed.", e); + } + + // load metric exceptions + try { + this.metricExceptions = new HashSet<>(); + try (InputStream exceptions = inputStream(DEFAULT_EXCEPTIONS_FILEPATH); + Scanner scanner = new Scanner(exceptions)) { + while (scanner.hasNextLine()) { + final String line = Presenter + .withoutComments(scanner.nextLine()); + if (!line.isBlank()) { + this.metricExceptions.add(line); + } + } + } + } catch (final IOException e) { + throw new AssertionError("Loading of metric_exceptions.txt failed.", + e); + } } - /** - * Sets the dimension of the view's From and To units. - * - * @throws UnsupportedOperationException if the view does not support - * unit-based conversion (does not - * implement - * {@link UnitConversionView}) - * @since 2021-12-15 - */ - public void applyDimensionFilter() {} - /** * Gets settings from the view and applies them to both view and presenter. * @@ -190,7 +286,52 @@ public final class Presenter { * {@link UnitConversionView}) * @since 2021-12-15 */ - public void convertUnits() {} + 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 OptionalDouble valueOptional = ucview.getInputValue(); + + // ensure everything is obtained + final Unit fromUnit, toUnit; + final double value; + if (fromUnitOptional.isPresent()) { + fromUnit = fromUnitOptional.orElseThrow(); + } else { + this.view.showErrorMessage("Unit Conversion Error", + "Please specify a From unit"); + return; + } + if (toUnitOptional.isPresent()) { + toUnit = toUnitOptional.orElseThrow(); + } else { + this.view.showErrorMessage("Unit Conversion Error", + "Please specify a To unit"); + return; + } + if (valueOptional.isPresent()) { + value = valueOptional.orElseThrow(); + } else { + this.view.showErrorMessage("Unit Conversion Error", + "Please specify a valid value"); + return; + } + + if (!fromUnit.canConvertTo(toUnit)) + throw new AssertionError( + "From and To units incompatible (should be impossible)"); + + // convert! + final UnitValue initialValue = UnitValue.of(fromUnit, value); + final UnitValue converted = initialValue.convertTo(toUnit); + ucview.showUnitConversionOutput( + String.format("%s = %s", initialValue, converted)); + } else + throw new UnsupportedOperationException( + "This function can only be called when the view is a UnitConversionView."); + } /** * Loads settings from the user's settings file and applies them to the view. @@ -199,6 +340,21 @@ public final class Presenter { */ public void loadSettings() {} + /** + * Completes creation of the presenter. This part of the initialization + * depends on the view's functions, so it cannot be run if the components + * they depend on are not created yet. + * + * @since 2022-02-26 + */ + public void postViewInitialize() { + // unit conversion specific stuff + if (this.view instanceof UnitConversionView) { + final UnitConversionView ucview = (UnitConversionView) this.view; + ucview.setDimensions(unique(this.database.dimensionMap().values())); + } + } + void prefixSelected() {} /** @@ -227,4 +383,26 @@ public final class Presenter { } void unitNameSelected() {} + + /** + * Updates the view's From and To units, if it has some + * + * @since 2021-12-15 + */ + public void updateView() { + if (this.view instanceof UnitConversionView) { + final UnitConversionView ucview = (UnitConversionView) this.view; + final ObjectProduct viewDimension = ucview + .getSelectedDimension().orElseThrow(); + + final Set units = this.database + .unitMapPrefixless(this.showDuplicateUnits).entrySet().stream() + .map(Map.Entry::getValue) + .filter(u -> viewDimension.equals(u.getDimension())) + .collect(Collectors.toSet()); + + ucview.setFromUnits(units); + ucview.setToUnits(units); + } + } } diff --git a/src/main/java/sevenUnitsGUI/TabbedView.java b/src/main/java/sevenUnitsGUI/TabbedView.java index e92b661..c3a05e2 100644 --- a/src/main/java/sevenUnitsGUI/TabbedView.java +++ b/src/main/java/sevenUnitsGUI/TabbedView.java @@ -28,6 +28,7 @@ import java.util.Collections; import java.util.Iterator; import java.util.NoSuchElementException; import java.util.Optional; +import java.util.OptionalDouble; import java.util.Set; import javax.swing.BorderFactory; @@ -212,7 +213,7 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { this.dimensionSelector = new JComboBox<>(); inBetweenPanel.add(this.dimensionSelector, BorderLayout.PAGE_START); this.dimensionSelector - .addItemListener(e -> this.presenter.applyDimensionFilter()); + .addItemListener(e -> this.presenter.updateView()); final JLabel arrowLabel = new JLabel("-->"); inBetweenPanel.add(arrowLabel, BorderLayout.CENTER); @@ -326,9 +327,9 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { this.masterPane.setMnemonicAt(5, KeyEvent.VK_S); // ============ FINALIZE CREATION OF VIEW ============ + this.presenter.postViewInitialize(); this.frame.pack(); this.frame.setVisible(true); - } /** @@ -541,8 +542,13 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { } @Override - public String getInputValue() { - return this.valueInput.getText(); + public OptionalDouble getInputValue() { + final String text = this.valueInput.getText(); + try { + return OptionalDouble.of(Double.parseDouble(text)); + } catch (final NumberFormatException e) { + return OptionalDouble.empty(); + } } @Override diff --git a/src/main/java/sevenUnitsGUI/UnitConversionView.java b/src/main/java/sevenUnitsGUI/UnitConversionView.java index 5fd5a82..e411a44 100644 --- a/src/main/java/sevenUnitsGUI/UnitConversionView.java +++ b/src/main/java/sevenUnitsGUI/UnitConversionView.java @@ -17,6 +17,7 @@ package sevenUnitsGUI; import java.util.Optional; +import java.util.OptionalDouble; import java.util.Set; import sevenUnits.unit.BaseDimension; @@ -48,7 +49,7 @@ public interface UnitConversionView extends View { * string provided by the user) * @since 2021-12-15 */ - String getInputValue(); + OptionalDouble getInputValue(); /** * @return selected dimension diff --git a/src/main/java/sevenUnitsGUI/ViewBot.java b/src/main/java/sevenUnitsGUI/ViewBot.java index 0c0d189..bc5302b 100644 --- a/src/main/java/sevenUnitsGUI/ViewBot.java +++ b/src/main/java/sevenUnitsGUI/ViewBot.java @@ -20,6 +20,7 @@ import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.Optional; +import java.util.OptionalDouble; import java.util.Set; import sevenUnits.unit.BaseDimension; @@ -47,7 +48,7 @@ final class ViewBot implements UnitConversionView, ExpressionConversionView { /** * The user-provided string representing the value in {@code fromSelection} */ - private String inputValue; + private OptionalDouble inputValue; /** The unit selected in the From selection */ private Optional fromSelection; /** The unit selected in the To selection */ @@ -102,7 +103,7 @@ final class ViewBot implements UnitConversionView, ExpressionConversionView { } @Override - public String getInputValue() { + public OptionalDouble getInputValue() { return this.inputValue; } @@ -190,7 +191,7 @@ final class ViewBot implements UnitConversionView, ExpressionConversionView { * @param inputValue the inputValue to set * @since 2022-01-29 */ - public void setInputValue(String inputValue) { + public void setInputValue(OptionalDouble inputValue) { this.inputValue = inputValue; } diff --git a/src/test/java/sevenUnits/unit/UnitDatabaseTest.java b/src/test/java/sevenUnits/unit/UnitDatabaseTest.java index 2276d7c..b8669cb 100644 --- a/src/test/java/sevenUnits/unit/UnitDatabaseTest.java +++ b/src/test/java/sevenUnits/unit/UnitDatabaseTest.java @@ -39,6 +39,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; +import sevenUnits.utils.NameSymbol; import sevenUnits.utils.UncertainDouble; /** diff --git a/src/test/java/sevenUnits/unit/UnitTest.java b/src/test/java/sevenUnits/unit/UnitTest.java index bb2e6a4..f174e7c 100644 --- a/src/test/java/sevenUnits/unit/UnitTest.java +++ b/src/test/java/sevenUnits/unit/UnitTest.java @@ -27,6 +27,7 @@ import java.util.concurrent.ThreadLocalRandom; import org.junit.jupiter.api.Test; import sevenUnits.utils.DecimalComparison; +import sevenUnits.utils.NameSymbol; import sevenUnits.utils.UncertainDouble; /** diff --git a/src/test/java/sevenUnitsGUI/PresenterTest.java b/src/test/java/sevenUnitsGUI/PresenterTest.java index 3e7c2b5..ff1450b 100644 --- a/src/test/java/sevenUnitsGUI/PresenterTest.java +++ b/src/test/java/sevenUnitsGUI/PresenterTest.java @@ -21,14 +21,15 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.List; import java.util.Optional; +import java.util.OptionalDouble; import java.util.Set; import org.junit.jupiter.api.Test; import sevenUnits.unit.BaseDimension; import sevenUnits.unit.Metric; -import sevenUnits.unit.NameSymbol; import sevenUnits.unit.Unit; +import sevenUnits.utils.NameSymbol; import sevenUnits.utils.NamedObjectProduct; /** @@ -44,12 +45,12 @@ public final class PresenterTest { Metric.Dimensions.VELOCITY.withName(NameSymbol.ofName("Velocity"))); /** - * Test for {@link Presenter#applyDimensionFilter()} + * Test for {@link Presenter#updateView()} * * @since 2022-02-12 */ @Test - void testApplyDimensionFilter() { + void testUpdateView() { // setup final ViewBot viewBot = new ViewBot(); final Presenter presenter = new Presenter(viewBot); @@ -61,7 +62,7 @@ public final class PresenterTest { Optional.of(this.testDimensions.iterator().next())); // filter to length units only, then get the filtered sets of units - presenter.applyDimensionFilter(); + presenter.updateView(); final Set fromUnits = viewBot.getFromUnits(); final Set toUnits = viewBot.getToUnits(); @@ -118,7 +119,7 @@ public final class PresenterTest { viewBot.setToUnits(this.testUnits); viewBot.setFromSelection(Optional.of(Metric.METRE)); viewBot.setToSelection(Optional.of(Metric.KILOMETRE)); - viewBot.setInputValue("10000.0"); + viewBot.setInputValue(OptionalDouble.of(10000.0)); // convert units presenter.convertUnits(); -- 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(-) 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 c5b209d48ef38b733e3fd8fd8ef86ae13a552821 Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Thu, 10 Mar 2022 06:55:59 -0500 Subject: Simplified toString of common unit classes --- CHANGELOG.org | 1 + src/main/java/sevenUnits/unit/LinearUnit.java | 20 ++++-------- src/main/java/sevenUnits/unit/Unit.java | 36 +++++++++++++++++----- .../java/sevenUnits/utils/NamedObjectProduct.java | 6 +++- src/main/java/sevenUnitsGUI/TabbedView.java | 2 +- 5 files changed, 41 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.org b/CHANGELOG.org index 681fead..7c53032 100644 --- a/CHANGELOG.org +++ b/CHANGELOG.org @@ -9,6 +9,7 @@ - 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 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] *** Added diff --git a/src/main/java/sevenUnits/unit/LinearUnit.java b/src/main/java/sevenUnits/unit/LinearUnit.java index deefc9a..3a28261 100644 --- a/src/main/java/sevenUnits/unit/LinearUnit.java +++ b/src/main/java/sevenUnits/unit/LinearUnit.java @@ -370,6 +370,12 @@ public final class LinearUnit extends Unit { this.getConversionFactor() * multiplier.getConversionFactor()); } + @Override + public String toDefinitionString() { + return Double.toString(this.conversionFactor) + " " + + this.getBase().toString(BaseUnit::getShortName); + } + /** * Returns this unit but to an exponent. * @@ -383,20 +389,6 @@ public final class LinearUnit extends Unit { Math.pow(this.conversionFactor, exponent)); } - /** - * @return a string providing a definition of this unit - * @since 2019-10-21 - */ - @Override - public String toString() { - return this.getPrimaryName().orElse("Unnamed unit") - + (this.getSymbol().isPresent() - ? String.format(" (%s)", this.getSymbol().get()) - : "") - + ", " + Double.toString(this.conversionFactor) + " * " - + this.getBase().toString(u -> u.getSymbol().get()); - } - @Override public LinearUnit withName(final NameSymbol ns) { return valueOf(this.getBase(), this.getConversionFactor(), ns); diff --git a/src/main/java/sevenUnits/unit/Unit.java b/src/main/java/sevenUnits/unit/Unit.java index 9866e9c..b80ccbd 100644 --- a/src/main/java/sevenUnits/unit/Unit.java +++ b/src/main/java/sevenUnits/unit/Unit.java @@ -24,6 +24,7 @@ import java.util.function.DoubleUnaryOperator; import sevenUnits.utils.DecimalComparison; import sevenUnits.utils.NameSymbol; import sevenUnits.utils.Nameable; +import sevenUnits.utils.NamedObjectProduct; import sevenUnits.utils.ObjectProduct; /** @@ -349,16 +350,35 @@ public abstract class Unit implements Nameable { .equals(Math.log10(linear.getConversionFactor()) % 1.0, 0); } + /** + * @return a string representing this unit's definition + * @since 2022-03-10 + */ + public String toDefinitionString() { + if (this.unitBase instanceof NamedObjectProduct) + return "derived from " + + ((NamedObjectProduct) this.unitBase).getName(); + else + return "derived from " + + this.getBase().toString(BaseUnit::getShortName); + } + + /** + * @return a string containing both this unit's name and its definition + * @since 2022-03-10 + */ + public final String toFullString() { + return this.toString() + " (" + this.toDefinitionString() + ")"; + } + @Override public String toString() { - return this.getPrimaryName().orElse("Unnamed unit") - + (this.getSymbol().isPresent() - ? String.format(" (%s)", this.getSymbol().get()) - : "") - + ", derived from " - + this.getBase().toString(u -> u.getSymbol().get()) - + (this.getOtherNames().isEmpty() ? "" - : ", also called " + String.join(", ", this.getOtherNames())); + if (this.nameSymbol.getPrimaryName().isPresent() + && this.nameSymbol.getSymbol().isPresent()) + return this.nameSymbol.getPrimaryName().orElseThrow() + " (" + + this.nameSymbol.getSymbol().orElseThrow() + ")"; + else + return this.getName(); } /** diff --git a/src/main/java/sevenUnits/utils/NamedObjectProduct.java b/src/main/java/sevenUnits/utils/NamedObjectProduct.java index 9c3079c..89b2fad 100644 --- a/src/main/java/sevenUnits/utils/NamedObjectProduct.java +++ b/src/main/java/sevenUnits/utils/NamedObjectProduct.java @@ -40,8 +40,12 @@ public class NamedObjectProduct extends ObjectProduct return this.nameSymbol; } + public final String toDefinitionString() { + return super.toString(); + } + @Override public String toString() { - return this.nameSymbol.toString() + ", " + super.toString(); + return this.nameSymbol.toString(); } } diff --git a/src/main/java/sevenUnitsGUI/TabbedView.java b/src/main/java/sevenUnitsGUI/TabbedView.java index 1d40087..0461cb6 100644 --- a/src/main/java/sevenUnitsGUI/TabbedView.java +++ b/src/main/java/sevenUnitsGUI/TabbedView.java @@ -71,7 +71,7 @@ import sevenUnits.utils.ObjectProduct; */ final class TabbedView implements ExpressionConversionView, UnitConversionView { /** - * A List-like view of a JComboBox's items + * A Set-like view of a JComboBox's items * * @param type of item in list * -- cgit v1.2.3 From 91f87da88f98de996e167f0ff6809356f6d57e11 Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Thu, 10 Mar 2022 15:14:33 -0500 Subject: Named the metric dimensions & fixed NameSymbol.toString --- src/main/java/sevenUnits/unit/Metric.java | 153 ++++++++++++++++--------- src/main/java/sevenUnits/utils/NameSymbol.java | 3 +- src/main/resources/dimensionfile.txt | 8 +- src/test/java/sevenUnitsGUI/PresenterTest.java | 29 +++-- 4 files changed, 117 insertions(+), 76 deletions(-) diff --git a/src/main/java/sevenUnits/unit/Metric.java b/src/main/java/sevenUnits/unit/Metric.java index 78e3769..7ede085 100644 --- a/src/main/java/sevenUnits/unit/Metric.java +++ b/src/main/java/sevenUnits/unit/Metric.java @@ -19,6 +19,7 @@ package sevenUnits.unit; import java.util.Set; import sevenUnits.utils.NameSymbol; +import sevenUnits.utils.NamedObjectProduct; import sevenUnits.utils.ObjectProduct; /** @@ -114,32 +115,39 @@ public final class Metric { public static final class Dimensions { public static final ObjectProduct EMPTY = ObjectProduct .empty(); - public static final ObjectProduct LENGTH = ObjectProduct - .oneOf(BaseDimensions.LENGTH); - public static final ObjectProduct MASS = ObjectProduct - .oneOf(BaseDimensions.MASS); - public static final ObjectProduct TIME = ObjectProduct - .oneOf(BaseDimensions.TIME); - public static final ObjectProduct ELECTRIC_CURRENT = ObjectProduct - .oneOf(BaseDimensions.ELECTRIC_CURRENT); - public static final ObjectProduct TEMPERATURE = ObjectProduct - .oneOf(BaseDimensions.TEMPERATURE); - public static final ObjectProduct QUANTITY = ObjectProduct - .oneOf(BaseDimensions.QUANTITY); - public static final ObjectProduct LUMINOUS_INTENSITY = ObjectProduct - .oneOf(BaseDimensions.LUMINOUS_INTENSITY); - public static final ObjectProduct INFORMATION = ObjectProduct - .oneOf(BaseDimensions.INFORMATION); - public static final ObjectProduct CURRENCY = ObjectProduct - .oneOf(BaseDimensions.CURRENCY); + public static final NamedObjectProduct LENGTH = ObjectProduct + .oneOf(BaseDimensions.LENGTH) + .withName(NameSymbol.of("Length", "L")); + public static final NamedObjectProduct MASS = ObjectProduct + .oneOf(BaseDimensions.MASS).withName(NameSymbol.of("Mass", "M")); + public static final NamedObjectProduct TIME = ObjectProduct + .oneOf(BaseDimensions.TIME).withName(NameSymbol.of("Time", "T")); + public static final NamedObjectProduct ELECTRIC_CURRENT = ObjectProduct + .oneOf(BaseDimensions.ELECTRIC_CURRENT) + .withName(NameSymbol.of("Current", "I")); + public static final NamedObjectProduct TEMPERATURE = ObjectProduct + .oneOf(BaseDimensions.TEMPERATURE) + .withName(NameSymbol.of("Temperature", "\u0398")); + public static final NamedObjectProduct QUANTITY = ObjectProduct + .oneOf(BaseDimensions.QUANTITY) + .withName(NameSymbol.of("Quantity", "N")); + public static final NamedObjectProduct LUMINOUS_INTENSITY = ObjectProduct + .oneOf(BaseDimensions.LUMINOUS_INTENSITY) + .withName(NameSymbol.of("Luminous Intensity", "J")); + public static final NamedObjectProduct INFORMATION = ObjectProduct + .oneOf(BaseDimensions.INFORMATION) + .withName(NameSymbol.ofName("Information")); + public static final NamedObjectProduct CURRENCY = ObjectProduct + .oneOf(BaseDimensions.CURRENCY) + .withName(NameSymbol.ofName("Currency")); // derived dimensions without named SI units public static final ObjectProduct AREA = LENGTH .times(LENGTH); public static final ObjectProduct VOLUME = AREA .times(LENGTH); - public static final ObjectProduct VELOCITY = LENGTH - .dividedBy(TIME); + public static final NamedObjectProduct VELOCITY = LENGTH + .dividedBy(TIME).withName(NameSymbol.ofName("Velocity")); public static final ObjectProduct ACCELERATION = VELOCITY .dividedBy(TIME); public static final ObjectProduct WAVENUMBER = EMPTY @@ -403,54 +411,89 @@ public final class Metric { .withName(NameSymbol.of("exbi", "Ei")); // a few prefixed units - public static final LinearUnit MICROMETRE = Metric.METRE.withPrefix(Metric.MICRO); - public static final LinearUnit MILLIMETRE = Metric.METRE.withPrefix(Metric.MILLI); - public static final LinearUnit KILOMETRE = Metric.METRE.withPrefix(Metric.KILO); - public static final LinearUnit MEGAMETRE = Metric.METRE.withPrefix(Metric.MEGA); + public static final LinearUnit MICROMETRE = Metric.METRE + .withPrefix(Metric.MICRO); + public static final LinearUnit MILLIMETRE = Metric.METRE + .withPrefix(Metric.MILLI); + public static final LinearUnit KILOMETRE = Metric.METRE + .withPrefix(Metric.KILO); + public static final LinearUnit MEGAMETRE = Metric.METRE + .withPrefix(Metric.MEGA); - public static final LinearUnit MICROLITRE = Metric.LITRE.withPrefix(Metric.MICRO); - public static final LinearUnit MILLILITRE = Metric.LITRE.withPrefix(Metric.MILLI); - public static final LinearUnit KILOLITRE = Metric.LITRE.withPrefix(Metric.KILO); - public static final LinearUnit MEGALITRE = Metric.LITRE.withPrefix(Metric.MEGA); + public static final LinearUnit MICROLITRE = Metric.LITRE + .withPrefix(Metric.MICRO); + public static final LinearUnit MILLILITRE = Metric.LITRE + .withPrefix(Metric.MILLI); + public static final LinearUnit KILOLITRE = Metric.LITRE + .withPrefix(Metric.KILO); + public static final LinearUnit MEGALITRE = Metric.LITRE + .withPrefix(Metric.MEGA); - public static final LinearUnit MICROSECOND = Metric.SECOND.withPrefix(Metric.MICRO); - public static final LinearUnit MILLISECOND = Metric.SECOND.withPrefix(Metric.MILLI); - public static final LinearUnit KILOSECOND = Metric.SECOND.withPrefix(Metric.KILO); - public static final LinearUnit MEGASECOND = Metric.SECOND.withPrefix(Metric.MEGA); + public static final LinearUnit MICROSECOND = Metric.SECOND + .withPrefix(Metric.MICRO); + public static final LinearUnit MILLISECOND = Metric.SECOND + .withPrefix(Metric.MILLI); + public static final LinearUnit KILOSECOND = Metric.SECOND + .withPrefix(Metric.KILO); + public static final LinearUnit MEGASECOND = Metric.SECOND + .withPrefix(Metric.MEGA); - public static final LinearUnit MICROGRAM = Metric.GRAM.withPrefix(Metric.MICRO); - public static final LinearUnit MILLIGRAM = Metric.GRAM.withPrefix(Metric.MILLI); - public static final LinearUnit MEGAGRAM = Metric.GRAM.withPrefix(Metric.MEGA); + public static final LinearUnit MICROGRAM = Metric.GRAM + .withPrefix(Metric.MICRO); + public static final LinearUnit MILLIGRAM = Metric.GRAM + .withPrefix(Metric.MILLI); + public static final LinearUnit MEGAGRAM = Metric.GRAM + .withPrefix(Metric.MEGA); - public static final LinearUnit MICRONEWTON = Metric.NEWTON.withPrefix(Metric.MICRO); - public static final LinearUnit MILLINEWTON = Metric.NEWTON.withPrefix(Metric.MILLI); - public static final LinearUnit KILONEWTON = Metric.NEWTON.withPrefix(Metric.KILO); - public static final LinearUnit MEGANEWTON = Metric.NEWTON.withPrefix(Metric.MEGA); + public static final LinearUnit MICRONEWTON = Metric.NEWTON + .withPrefix(Metric.MICRO); + public static final LinearUnit MILLINEWTON = Metric.NEWTON + .withPrefix(Metric.MILLI); + public static final LinearUnit KILONEWTON = Metric.NEWTON + .withPrefix(Metric.KILO); + public static final LinearUnit MEGANEWTON = Metric.NEWTON + .withPrefix(Metric.MEGA); - public static final LinearUnit MICROJOULE = Metric.JOULE.withPrefix(Metric.MICRO); - public static final LinearUnit MILLIJOULE = Metric.JOULE.withPrefix(Metric.MILLI); - public static final LinearUnit KILOJOULE = Metric.JOULE.withPrefix(Metric.KILO); - public static final LinearUnit MEGAJOULE = Metric.JOULE.withPrefix(Metric.MEGA); + public static final LinearUnit MICROJOULE = Metric.JOULE + .withPrefix(Metric.MICRO); + public static final LinearUnit MILLIJOULE = Metric.JOULE + .withPrefix(Metric.MILLI); + public static final LinearUnit KILOJOULE = Metric.JOULE + .withPrefix(Metric.KILO); + public static final LinearUnit MEGAJOULE = Metric.JOULE + .withPrefix(Metric.MEGA); - public static final LinearUnit MICROWATT = Metric.WATT.withPrefix(Metric.MICRO); - public static final LinearUnit MILLIWATT = Metric.WATT.withPrefix(Metric.MILLI); - public static final LinearUnit KILOWATT = Metric.WATT.withPrefix(Metric.KILO); - public static final LinearUnit MEGAWATT = Metric.WATT.withPrefix(Metric.MEGA); + public static final LinearUnit MICROWATT = Metric.WATT + .withPrefix(Metric.MICRO); + public static final LinearUnit MILLIWATT = Metric.WATT + .withPrefix(Metric.MILLI); + public static final LinearUnit KILOWATT = Metric.WATT + .withPrefix(Metric.KILO); + public static final LinearUnit MEGAWATT = Metric.WATT + .withPrefix(Metric.MEGA); public static final LinearUnit MICROCOULOMB = Metric.COULOMB .withPrefix(Metric.MICRO); public static final LinearUnit MILLICOULOMB = Metric.COULOMB .withPrefix(Metric.MILLI); - public static final LinearUnit KILOCOULOMB = Metric.COULOMB.withPrefix(Metric.KILO); - public static final LinearUnit MEGACOULOMB = Metric.COULOMB.withPrefix(Metric.MEGA); + public static final LinearUnit KILOCOULOMB = Metric.COULOMB + .withPrefix(Metric.KILO); + public static final LinearUnit MEGACOULOMB = Metric.COULOMB + .withPrefix(Metric.MEGA); - public static final LinearUnit MICROAMPERE = Metric.AMPERE.withPrefix(Metric.MICRO); - public static final LinearUnit MILLIAMPERE = Metric.AMPERE.withPrefix(Metric.MILLI); + public static final LinearUnit MICROAMPERE = Metric.AMPERE + .withPrefix(Metric.MICRO); + public static final LinearUnit MILLIAMPERE = Metric.AMPERE + .withPrefix(Metric.MILLI); - public static final LinearUnit MICROVOLT = Metric.VOLT.withPrefix(Metric.MICRO); - public static final LinearUnit MILLIVOLT = Metric.VOLT.withPrefix(Metric.MILLI); - public static final LinearUnit KILOVOLT = Metric.VOLT.withPrefix(Metric.KILO); - public static final LinearUnit MEGAVOLT = Metric.VOLT.withPrefix(Metric.MEGA); + public static final LinearUnit MICROVOLT = Metric.VOLT + .withPrefix(Metric.MICRO); + public static final LinearUnit MILLIVOLT = Metric.VOLT + .withPrefix(Metric.MILLI); + public static final LinearUnit KILOVOLT = Metric.VOLT + .withPrefix(Metric.KILO); + public static final LinearUnit MEGAVOLT = Metric.VOLT + .withPrefix(Metric.MEGA); public static final LinearUnit KILOOHM = Metric.OHM.withPrefix(Metric.KILO); public static final LinearUnit MEGAOHM = Metric.OHM.withPrefix(Metric.MEGA); diff --git a/src/main/java/sevenUnits/utils/NameSymbol.java b/src/main/java/sevenUnits/utils/NameSymbol.java index 255e82f..41cf41d 100644 --- a/src/main/java/sevenUnits/utils/NameSymbol.java +++ b/src/main/java/sevenUnits/utils/NameSymbol.java @@ -283,7 +283,8 @@ public final class NameSymbol { if (this.isEmpty()) return "NameSymbol.EMPTY"; else if (this.primaryName.isPresent() && this.symbol.isPresent()) - return this.primaryName + " (" + this.symbol + ")"; + return this.primaryName.orElseThrow() + " (" + + this.symbol.orElseThrow() + ")"; else return this.primaryName.orElseGet(this.symbol::orElseThrow); } diff --git a/src/main/resources/dimensionfile.txt b/src/main/resources/dimensionfile.txt index 3485de5..a946677 100644 --- a/src/main/resources/dimensionfile.txt +++ b/src/main/resources/dimensionfile.txt @@ -12,7 +12,7 @@ TIME ! TEMPERATURE ! # Derived Dimensions -AREA LENGTH^2 -VOLUME LENGTH^3 -VELOCITY LENGTH / TIME -ENERGY MASS * VELOCITY^2 \ No newline at end of file +Area LENGTH^2 +Volume LENGTH^3 +Velocity LENGTH / TIME +Energy MASS * Velocity^2 \ No newline at end of file diff --git a/src/test/java/sevenUnitsGUI/PresenterTest.java b/src/test/java/sevenUnitsGUI/PresenterTest.java index deb16d7..82842d8 100644 --- a/src/test/java/sevenUnitsGUI/PresenterTest.java +++ b/src/test/java/sevenUnitsGUI/PresenterTest.java @@ -32,7 +32,6 @@ import sevenUnits.unit.BaseDimension; import sevenUnits.unit.Metric; import sevenUnits.unit.Unit; import sevenUnits.unit.UnitValue; -import sevenUnits.utils.NameSymbol; import sevenUnits.utils.NamedObjectProduct; /** @@ -41,19 +40,18 @@ import sevenUnits.utils.NamedObjectProduct; * @since 2022-02-10 */ 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 + .of(Metric.Dimensions.LENGTH, Metric.Dimensions.VELOCITY); + 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 method for {@link Presenter#convertExpressions} * @@ -87,8 +85,8 @@ public final class PresenterTest { final ViewBot viewBot = new ViewBot(); final Presenter presenter = new Presenter(viewBot); - viewBot.setFromUnits(this.testUnits); - viewBot.setToUnits(this.testUnits); + viewBot.setFromUnits(testUnits); + viewBot.setToUnits(testUnits); viewBot.setFromSelection(Optional.of(Metric.METRE)); viewBot.setToSelection(Optional.of(Metric.KILOMETRE)); viewBot.setInputValue(OptionalDouble.of(10000.0)); @@ -124,16 +122,15 @@ public final class PresenterTest { // override default database units presenter.database.clear(); - for (final Unit unit : this.testUnits) { + for (final Unit unit : 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())); + viewBot.setFromUnits(testUnits); + viewBot.setToUnits(testUnits); + viewBot.setDimensions(testDimensions); + viewBot.setSelectedDimension(Optional.of(Metric.Dimensions.LENGTH)); // filter to length units only, then get the filtered sets of units presenter.updateView(); -- cgit v1.2.3 From c421e474a7b0d0d453e4a527907f327f2ddef320 Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Sat, 9 Apr 2022 11:32:43 -0500 Subject: View now sends and recieves Strings instead of data --- .../sevenUnitsGUI/ExpressionConversionView.java | 7 +- src/main/java/sevenUnitsGUI/Presenter.java | 153 +++++++++++------- src/main/java/sevenUnitsGUI/SearchBoxList.java | 10 ++ src/main/java/sevenUnitsGUI/TabbedView.java | 76 +++++---- .../java/sevenUnitsGUI/UnitConversionRecord.java | 180 +++++++++++++++++++++ .../java/sevenUnitsGUI/UnitConversionView.java | 47 +++--- src/main/java/sevenUnitsGUI/View.java | 2 +- src/main/java/sevenUnitsGUI/ViewBot.java | 141 +++++++--------- src/test/java/sevenUnitsGUI/PresenterTest.java | 85 +++++----- 9 files changed, 460 insertions(+), 241 deletions(-) create mode 100644 src/main/java/sevenUnitsGUI/UnitConversionRecord.java diff --git a/src/main/java/sevenUnitsGUI/ExpressionConversionView.java b/src/main/java/sevenUnitsGUI/ExpressionConversionView.java index 5a587d4..872ca10 100644 --- a/src/main/java/sevenUnitsGUI/ExpressionConversionView.java +++ b/src/main/java/sevenUnitsGUI/ExpressionConversionView.java @@ -38,11 +38,8 @@ public interface ExpressionConversionView extends View { /** * Shows the output of an expression conversion to the user. * - * @param fromExpression expression converted from - * @param toExpression expression converted to - * @param value conversion factor between two expressions + * @param uc unit conversion to show * @since 2021-12-15 */ - void showExpressionConversionOutput(String fromExpression, - String toExpression, double value); + void showExpressionConversionOutput(UnitConversionRecord uc); } diff --git a/src/main/java/sevenUnitsGUI/Presenter.java b/src/main/java/sevenUnitsGUI/Presenter.java index 57d353d..9f8fe69 100644 --- a/src/main/java/sevenUnitsGUI/Presenter.java +++ b/src/main/java/sevenUnitsGUI/Presenter.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2021 Adrien Hopkins + * Copyright (C) 2021-2022 Adrien Hopkins * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -19,13 +19,11 @@ package sevenUnitsGUI; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; -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; import java.util.Set; import java.util.function.Function; @@ -134,25 +132,6 @@ public final class Presenter { return Presenter.class.getResourceAsStream(filepath); } - /** - * Accepts a collection and returns a set with the unique elements in that - * collection - * - * @param type of element in collection - * @param collection collection to uniquify - * @return unique collection - * @since 2022-02-26 - */ - private static Set unique(Collection collection) { - final Set uniqueSet = new HashSet<>(); - for (final E e : collection) { - if (!uniqueSet.contains(e)) { - uniqueSet.add(e); - } - } - return uniqueSet; - } - /** * @return {@code line} with any comments removed. * @since 2021-03-13 @@ -206,7 +185,7 @@ public final class Presenter { * removed from the From unit list and imperial/USC units removed from the To * unit list. */ - private boolean oneWayConversion; + private boolean oneWayConversionEnabled; /** * If this is false, duplicate units will be removed from the unit view in @@ -259,13 +238,6 @@ public final class Presenter { } } - /** - * Gets settings from the view and applies them to both view and presenter. - * - * @since 2021-12-15 - */ - public void applySettings() {} - /** * Converts from the view's input expression to its output expression. * Displays an error message if any of the required fields are invalid. @@ -316,8 +288,9 @@ public final class Presenter { // convert and show output if (from.getUnit().canConvertTo(to)) { final double value = from.asUnitValue().convertTo(to).getValue(); - xcview.showExpressionConversionOutput(fromExpression, toExpression, - value); + final UnitConversionRecord uc = UnitConversionRecord.valueOf( + fromExpression, toExpression, "", String.valueOf(value)); + xcview.showExpressionConversionOutput(uc); } else { this.view.showErrorMessage("Conversion Error", "Cannot convert between \"" + fromExpression + "\" and \"" @@ -343,48 +316,81 @@ public final class Presenter { if (this.view instanceof UnitConversionView) { final UnitConversionView ucview = (UnitConversionView) this.view; - final Optional fromUnitOptional = ucview.getFromSelection(); - final Optional toUnitOptional = ucview.getToSelection(); - final OptionalDouble valueOptional = ucview.getInputValue(); + final Optional fromUnitOptional = ucview.getFromSelection(); + final Optional toUnitOptional = ucview.getToSelection(); + final String valueString = ucview.getInputValue(); - // ensure everything is obtained - final Unit fromUnit, toUnit; - final double value; + // extract values from optionals + final String fromUnitString, toUnitString; if (fromUnitOptional.isPresent()) { - fromUnit = fromUnitOptional.orElseThrow(); + fromUnitString = fromUnitOptional.orElseThrow(); } else { - this.view.showErrorMessage("Unit Conversion Error", + this.view.showErrorMessage("Unit Selection Error", "Please specify a From unit"); return; } if (toUnitOptional.isPresent()) { - toUnit = toUnitOptional.orElseThrow(); + toUnitString = toUnitOptional.orElseThrow(); } else { - this.view.showErrorMessage("Unit Conversion Error", + this.view.showErrorMessage("Unit Selection Error", "Please specify a To unit"); return; } - if (valueOptional.isPresent()) { - value = valueOptional.orElseThrow(); - } else { - this.view.showErrorMessage("Unit Conversion Error", - "Please specify a valid value"); + + // convert strings to data, checking if anything is invalid + final Unit fromUnit, toUnit; + final double value; + + if (this.database.containsUnitName(fromUnitString)) { + fromUnit = this.database.getUnit(fromUnitString); + } else + throw this.viewError("Nonexistent From unit: %s", fromUnitString); + if (this.database.containsUnitName(toUnitString)) { + toUnit = this.database.getUnit(toUnitString); + } else + throw this.viewError("Nonexistent To unit: %s", toUnitString); + try { + value = Double.parseDouble(valueString); + } catch (final NumberFormatException e) { + this.view.showErrorMessage("Value Error", + "Invalid value " + valueString); return; } if (!fromUnit.canConvertTo(toUnit)) - throw new AssertionError( - "From and To units incompatible (should be impossible)"); + 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); - ucview.showUnitConversionOutput(initialValue, converted); + + ucview.showUnitConversionOutput( + UnitConversionRecord.fromValues(initialValue, converted)); } else throw new UnsupportedOperationException( "This function can only be called when the view is a UnitConversionView."); } + /** + * @return true iff duplicate units are shown in unit lists + * @since 2022-03-30 + */ + public boolean duplicateUnitsShown() { + return this.showDuplicateUnits; + } + + /** + * @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 isOneWayConversionEnabled() { + return this.oneWayConversionEnabled; + } + /** * Loads settings from the user's settings file and applies them to the view. * @@ -403,7 +409,7 @@ public final class Presenter { // unit conversion specific stuff if (this.view instanceof UnitConversionView) { final UnitConversionView ucview = (UnitConversionView) this.view; - ucview.setDimensions(unique(this.database.dimensionMap().values())); + ucview.setDimensionNames(this.database.dimensionMap().keySet()); } } @@ -417,6 +423,26 @@ public final class Presenter { */ public void saveSettings() {} + /** + * @param oneWayConversionEnabled whether not one-way conversion should be + * enabled + * @since 2022-03-30 + * @see {@link #isOneWayConversionEnabled} + */ + public void setOneWayConversionEnabled(boolean oneWayConversionEnabled) { + this.oneWayConversionEnabled = oneWayConversionEnabled; + this.updateView(); + } + + /** + * @param showDuplicateUnits whether or not duplicate units should be shown + * @since 2022-03-30 + */ + public void setShowDuplicateUnits(boolean showDuplicateUnits) { + this.showDuplicateUnits = showDuplicateUnits; + this.updateView(); + } + void unitNameSelected() {} /** @@ -427,17 +453,30 @@ public final class Presenter { public void updateView() { if (this.view instanceof UnitConversionView) { final UnitConversionView ucview = (UnitConversionView) this.view; - final ObjectProduct viewDimension = ucview - .getSelectedDimension().orElseThrow(); + final ObjectProduct viewDimension = this.database + .getDimension(((UnitConversionView) this.view) + .getSelectedDimensionName().orElseThrow()); - final Set units = this.database + final Set units = this.database .unitMapPrefixless(this.showDuplicateUnits).entrySet().stream() .map(Map.Entry::getValue) .filter(u -> viewDimension.equals(u.getDimension())) - .collect(Collectors.toSet()); + .map(Unit::getName).collect(Collectors.toSet()); - ucview.setFromUnits(units); - ucview.setToUnits(units); + ucview.setFromUnitNames(units); + ucview.setToUnitNames(units); } } + + /** + * @param message message to add + * @param args string formatting arguments for message + * @return AssertionError stating that an error has happened in the view's + * code + * @since 2022-04-09 + */ + private AssertionError viewError(String message, Object... args) { + return new AssertionError("View Programming Error (from " + this.view + + "): " + String.format(message, args)); + } } diff --git a/src/main/java/sevenUnitsGUI/SearchBoxList.java b/src/main/java/sevenUnitsGUI/SearchBoxList.java index 2b935d0..9b41601 100644 --- a/src/main/java/sevenUnitsGUI/SearchBoxList.java +++ b/src/main/java/sevenUnitsGUI/SearchBoxList.java @@ -22,6 +22,7 @@ import java.awt.event.FocusEvent; import java.awt.event.FocusListener; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Optional; @@ -165,6 +166,15 @@ final class SearchBoxList extends JPanel { this.customSearchFilter = o -> true; } + /** + * @return items available in search list, including items that are hidden by + * the search filter + * @since 2022-03-30 + */ + public Collection getItems() { + return Collections.unmodifiableCollection(this.itemsToFilter); + } + /** * @return this component's search box component * @since 2019-04-14 diff --git a/src/main/java/sevenUnitsGUI/TabbedView.java b/src/main/java/sevenUnitsGUI/TabbedView.java index 0461cb6..ed45011 100644 --- a/src/main/java/sevenUnitsGUI/TabbedView.java +++ b/src/main/java/sevenUnitsGUI/TabbedView.java @@ -23,13 +23,12 @@ 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.HashSet; import java.util.Iterator; import java.util.NoSuchElementException; import java.util.Optional; -import java.util.OptionalDouble; import java.util.Set; import javax.swing.BorderFactory; @@ -57,12 +56,6 @@ import javax.swing.border.EmptyBorder; import javax.swing.border.TitledBorder; 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; /** * A View that separates its functions into multiple tabs @@ -140,13 +133,13 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { // DIMENSION-BASED CONVERTER /** The combo box that selects dimensions */ - private final JComboBox> dimensionSelector; + private final JComboBox dimensionSelector; /** The panel for inputting values in the dimension-based converter */ private final JFormattedTextField valueInput; /** The panel for "From" in the dimension-based converter */ - private final SearchBoxList fromSearch; + private final SearchBoxList fromSearch; /** The panel for "To" in the dimension-based converter */ - private final SearchBoxList toSearch; + private final SearchBoxList toSearch; /** The output area in the dimension-based converter */ private final JTextArea unitOutput; @@ -160,9 +153,9 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { // UNIT AND PREFIX VIEWERS /** The searchable list of unit names in the unit viewer */ - private final SearchBoxList unitNameList; + private final SearchBoxList unitNameList; /** The searchable list of prefix names in the prefix viewer */ - private final SearchBoxList prefixNameList; + private final SearchBoxList prefixNameList; /** The text box for unit data in the unit viewer */ private final JTextArea unitTextBox; /** The text box for prefix data in the prefix viewer */ @@ -528,7 +521,7 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { } @Override - public Set> getDimensions() { + public Set getDimensionNames() { return Collections .unmodifiableSet(new JComboBoxItemSet<>(this.dimensionSelector)); } @@ -539,27 +532,25 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { } @Override - public Optional getFromSelection() { + public Optional getFromSelection() { return this.fromSearch.getSelectedValue(); } @Override - public OptionalDouble getInputValue() { - try { - this.valueInput.commitEdit(); - } catch (final ParseException e) { - return OptionalDouble.empty(); - } - return OptionalDouble - .of(((Number) this.valueInput.getValue()).doubleValue()); + 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 Optional> getSelectedDimension() { - // this must work because this function can only return items that are in - // the selector, which are all of type ObjectProduct - @SuppressWarnings("unchecked") - final ObjectProduct selectedItem = (ObjectProduct) this.dimensionSelector + public Optional getSelectedDimensionName() { + final String selectedItem = (String) this.dimensionSelector .getSelectedItem(); return Optional.ofNullable(selectedItem); } @@ -570,26 +561,32 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { } @Override - public Optional getToSelection() { + public Optional getToSelection() { return this.toSearch.getSelectedValue(); } @Override - public void setDimensions( - Set> dimensions) { + 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 void setDimensionNames(Set dimensionNames) { this.dimensionSelector.removeAllItems(); - for (final NamedObjectProduct d : dimensions) { + for (final String d : dimensionNames) { this.dimensionSelector.addItem(d); } } @Override - public void setFromUnits(Set units) { + public void setFromUnitNames(Set units) { this.fromSearch.setItems(units); } @Override - public void setToUnits(Set units) { + public void setToUnitNames(Set units) { this.toSearch.setItems(units); } @@ -600,15 +597,14 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { } @Override - public void showExpressionConversionOutput(String fromExpression, - String toExpression, double value) { - this.expressionOutput.setText( - String.format("%s = %s %s", fromExpression, value, toExpression)); + public void showExpressionConversionOutput(UnitConversionRecord uc) { + this.expressionOutput.setText(String.format("%s = %s %s", uc.fromName(), + uc.outputValueString(), uc.toName())); } @Override - public void showUnitConversionOutput(UnitValue input, UnitValue output) { - this.unitOutput.setText(input + " = " + output); + public void showUnitConversionOutput(UnitConversionRecord uc) { + this.unitOutput.setText(uc.toString()); } } diff --git a/src/main/java/sevenUnitsGUI/UnitConversionRecord.java b/src/main/java/sevenUnitsGUI/UnitConversionRecord.java new file mode 100644 index 0000000..60675e2 --- /dev/null +++ b/src/main/java/sevenUnitsGUI/UnitConversionRecord.java @@ -0,0 +1,180 @@ +/** + * Copyright (C) 2022 Adrien Hopkins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package sevenUnitsGUI; + +import sevenUnits.unit.UnitValue; + +/** + * A record of a conversion between units or expressions + * + * @since 2022-04-09 + */ +public final class UnitConversionRecord { + /** + * Gets a {@code UnitConversionRecord} from two unit values + * + * @param input input unit & value + * @param output output unit & value + * @return unit conversion record + * @since 2022-04-09 + */ + public static UnitConversionRecord fromValues(UnitValue input, + UnitValue output) { + return UnitConversionRecord.valueOf(input.getUnit().getName(), + output.getUnit().getName(), String.valueOf(input.getValue()), + String.valueOf(output.getValue())); + } + + /** + * Gets a {@code UnitConversionRecord} + * + * @param fromName name of unit or expression that was converted + * from + * @param toName name of unit or expression that was converted to + * @param inputValueString string representing input value + * @param outputValueString string representing output value + * @return unit conversion record + * @since 2022-04-09 + */ + public static UnitConversionRecord valueOf(String fromName, String toName, + String inputValueString, String outputValueString) { + return new UnitConversionRecord(fromName, toName, inputValueString, + outputValueString); + } + + /** + * The name of the unit or expression that was converted from + */ + private final String fromName; + /** + * The name of the unit or expression that was converted to + */ + private final String toName; + + /** + * A string representing the input value. It doesn't need to be the same as + * the input value's string representation; it could be rounded, for example. + */ + private final String inputValueString; + /** + * A string representing the input value. It doesn't need to be the same as + * the input value's string representation; it could be rounded, for example. + */ + private final String outputValueString; + + /** + * @param fromName name of unit or expression that was converted + * from + * @param toName name of unit or expression that was converted to + * @param inputValueString string representing input value + * @param outputValueString string representing output value + * @since 2022-04-09 + */ + private UnitConversionRecord(String fromName, String toName, + String inputValueString, String outputValueString) { + this.fromName = fromName; + this.toName = toName; + this.inputValueString = inputValueString; + this.outputValueString = outputValueString; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (!(obj instanceof UnitConversionRecord)) + return false; + final UnitConversionRecord other = (UnitConversionRecord) obj; + if (this.fromName == null) { + if (other.fromName != null) + return false; + } else if (!this.fromName.equals(other.fromName)) + return false; + if (this.inputValueString == null) { + if (other.inputValueString != null) + return false; + } else if (!this.inputValueString.equals(other.inputValueString)) + return false; + if (this.outputValueString == null) { + if (other.outputValueString != null) + return false; + } else if (!this.outputValueString.equals(other.outputValueString)) + return false; + if (this.toName == null) { + if (other.toName != null) + return false; + } else if (!this.toName.equals(other.toName)) + return false; + return true; + } + + /** + * @return name of unit or expression that was converted from + * @since 2022-04-09 + */ + public String fromName() { + return this.fromName; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + + (this.fromName == null ? 0 : this.fromName.hashCode()); + result = prime * result + (this.inputValueString == null ? 0 + : this.inputValueString.hashCode()); + result = prime * result + (this.outputValueString == null ? 0 + : this.outputValueString.hashCode()); + result = prime * result + + (this.toName == null ? 0 : this.toName.hashCode()); + return result; + } + + /** + * @return string representing input value + * @since 2022-04-09 + */ + public String inputValueString() { + return this.inputValueString; + } + + /** + * @return string representing output value + * @since 2022-04-09 + */ + public String outputValueString() { + return this.outputValueString; + } + + /** + * @return name of unit or expression that was converted to + * @since 2022-04-09 + */ + public String toName() { + return this.toName; + } + + @Override + public String toString() { + final String inputString = this.inputValueString.isBlank() ? this.fromName + : this.inputValueString + " " + this.fromName; + final String outputString = this.outputValueString.isBlank() ? this.toName + : this.outputValueString + " " + this.toName; + return inputString + " = " + outputString; + } +} diff --git a/src/main/java/sevenUnitsGUI/UnitConversionView.java b/src/main/java/sevenUnitsGUI/UnitConversionView.java index 67d3ddc..9d3a67b 100644 --- a/src/main/java/sevenUnitsGUI/UnitConversionView.java +++ b/src/main/java/sevenUnitsGUI/UnitConversionView.java @@ -17,15 +17,8 @@ package sevenUnitsGUI; import java.util.Optional; -import java.util.OptionalDouble; import java.util.Set; -import sevenUnits.unit.BaseDimension; -import sevenUnits.unit.Unit; -import sevenUnits.unit.UnitValue; -import sevenUnits.utils.NamedObjectProduct; -import sevenUnits.utils.ObjectProduct; - /** * A View that supports single unit-based conversion * @@ -37,60 +30,72 @@ public interface UnitConversionView extends View { * @return dimensions available for filtering * @since 2022-01-29 */ - Set> getDimensions(); + Set getDimensionNames(); /** - * @return unit to convert from + * @return name of unit to convert from * @since 2021-12-15 */ - Optional getFromSelection(); + Optional getFromSelection(); + + /** + * @return list of names of units available to convert from + * @since 2022-03-30 + */ + Set getFromUnitNames(); /** * @return value to convert between the units (specifically, the numeric * string provided by the user) * @since 2021-12-15 */ - OptionalDouble getInputValue(); + String getInputValue(); /** * @return selected dimension * @since 2021-12-15 */ - Optional> getSelectedDimension(); + Optional getSelectedDimensionName(); /** - * @return unit to convert to + * @return name of unit to convert to * @since 2021-12-15 */ - Optional getToSelection(); + Optional getToSelection(); + + /** + * @return list of names of units available to convert to + * @since 2022-03-30 + */ + Set getToUnitNames(); /** * Sets the available dimensions for filtering. * - * @param dimensions dimensions to use + * @param dimensionNames names of dimensions to use * @since 2021-12-15 */ - void setDimensions(Set> dimensions); + void setDimensionNames(Set dimensionNames); /** * Sets the available units to convert from. {@link #getFromSelection} is not * required to use one of these units; this method is to be used for views * that allow the user to select units from a list. * - * @param units units to convert from + * @param unitNames names of units to convert from * @since 2021-12-15 */ - void setFromUnits(Set units); + void setFromUnitNames(Set unitNames); /** * Sets the available units to convert to. {@link #getToSelection} is not * required to use one of these units; this method is to be used for views * that allow the user to select units from a list. * - * @param units units to convert to + * @param unitNames names of units to convert to * @since 2021-12-15 */ - void setToUnits(Set units); + void setToUnitNames(Set unitNames); /** * Shows the output of a unit conversion. @@ -99,5 +104,5 @@ public interface UnitConversionView extends View { * @param output output unit & value * @since 2021-12-24 */ - void showUnitConversionOutput(UnitValue input, UnitValue output); + void showUnitConversionOutput(UnitConversionRecord uc); } diff --git a/src/main/java/sevenUnitsGUI/View.java b/src/main/java/sevenUnitsGUI/View.java index a93c76a..e78c9cc 100644 --- a/src/main/java/sevenUnitsGUI/View.java +++ b/src/main/java/sevenUnitsGUI/View.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2021 Adrien Hopkins + * Copyright (C) 2021-2022 Adrien Hopkins * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by diff --git a/src/main/java/sevenUnitsGUI/ViewBot.java b/src/main/java/sevenUnitsGUI/ViewBot.java index 43d73bb..0195dd6 100644 --- a/src/main/java/sevenUnitsGUI/ViewBot.java +++ b/src/main/java/sevenUnitsGUI/ViewBot.java @@ -21,15 +21,8 @@ import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.Optional; -import java.util.OptionalDouble; import java.util.Set; -import sevenUnits.unit.BaseDimension; -import sevenUnits.unit.Unit; -import sevenUnits.unit.UnitValue; -import sevenUnits.utils.NamedObjectProduct; -import sevenUnits.utils.ObjectProduct; - /** * A class that simulates a View (supports both unit and expression conversion) * for testing. Getters and setters work as expected. @@ -42,7 +35,7 @@ final class ViewBot implements UnitConversionView, ExpressionConversionView { private final Presenter presenter; /** The dimensions available to select from */ - private Set> dimensions; + private Set dimensionNames; /** The expression in the From field */ private String fromExpression; /** The expression in the To field */ @@ -50,23 +43,22 @@ final class ViewBot implements UnitConversionView, ExpressionConversionView { /** * The user-provided string representing the value in {@code fromSelection} */ - private OptionalDouble inputValue; + private String inputValue; /** The unit selected in the From selection */ - private Optional fromSelection; + private Optional fromSelection; /** The unit selected in the To selection */ - private Optional toSelection; + private Optional toSelection; /** The currently selected dimension */ - private Optional> selectedDimension; + private Optional selectedDimensionName; /** The units available in the From selection */ - private Set fromUnits; + 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 final List unitConversionOutputValues; + private Set toUnits; + + /** Saved outputs of all unit conversions */ + private final List unitConversions; /** Saved outputs of all unit expressions */ - private final List expressionConversionOutputs; + private final List expressionConversions; /** * Creates a new {@code ViewBot} with a new presenter. @@ -76,9 +68,16 @@ 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<>(); + this.unitConversions = new ArrayList<>(); + this.expressionConversions = new ArrayList<>(); + } + + /** + * @return list of records of expression conversions done by this bot + * @since 2022-04-09 + */ + public List expressionConversionList() { + return this.expressionConversions; } /** @@ -86,12 +85,8 @@ final class ViewBot implements UnitConversionView, ExpressionConversionView { * @since 2022-01-29 */ @Override - public Set> getDimensions() { - return this.dimensions; - } - - public List getExpressionConversionOutputs() { - return this.expressionConversionOutputs; + public Set getDimensionNames() { + return this.dimensionNames; } @Override @@ -100,7 +95,7 @@ final class ViewBot implements UnitConversionView, ExpressionConversionView { } @Override - public Optional getFromSelection() { + public Optional getFromSelection() { return this.fromSelection; } @@ -108,12 +103,13 @@ final class ViewBot implements UnitConversionView, ExpressionConversionView { * @return the units available for selection in From * @since 2022-01-29 */ - public Set getFromUnits() { + @Override + public Set getFromUnitNames() { return Collections.unmodifiableSet(this.fromUnits); } @Override - public OptionalDouble getInputValue() { + public String getInputValue() { return this.inputValue; } @@ -126,8 +122,8 @@ final class ViewBot implements UnitConversionView, ExpressionConversionView { } @Override - public Optional> getSelectedDimension() { - return this.selectedDimension.map(x -> x); + public Optional getSelectedDimensionName() { + return this.selectedDimensionName; } @Override @@ -136,7 +132,7 @@ final class ViewBot implements UnitConversionView, ExpressionConversionView { } @Override - public Optional getToSelection() { + public Optional getToSelection() { return this.toSelection; } @@ -144,30 +140,14 @@ final class ViewBot implements UnitConversionView, ExpressionConversionView { * @return the units available for selection in To * @since 2022-01-29 */ - public Set getToUnits() { + @Override + public Set getToUnitNames() { 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() { - return this.unitConversionOutputValues; - } - @Override - public void setDimensions( - Set> dimensions) { - this.dimensions = Objects.requireNonNull(dimensions, + public void setDimensionNames(Set dimensionNames) { + this.dimensionNames = Objects.requireNonNull(dimensionNames, "dimensions may not be null"); } @@ -187,7 +167,7 @@ final class ViewBot implements UnitConversionView, ExpressionConversionView { * @param fromSelection the fromSelection to set * @since 2022-01-29 */ - public void setFromSelection(Optional fromSelection) { + public void setFromSelection(Optional fromSelection) { this.fromSelection = Objects.requireNonNull(fromSelection, "fromSelection cannot be null"); } @@ -196,12 +176,12 @@ final class ViewBot implements UnitConversionView, ExpressionConversionView { * @param fromSelection the fromSelection to set * @since 2022-02-10 */ - public void setFromSelection(Unit fromSelection) { + public void setFromSelection(String fromSelection) { this.setFromSelection(Optional.of(fromSelection)); } @Override - public void setFromUnits(Set units) { + public void setFromUnitNames(Set units) { this.fromUnits = Objects.requireNonNull(units, "units may not be null"); } @@ -209,22 +189,21 @@ final class ViewBot implements UnitConversionView, ExpressionConversionView { * @param inputValue the inputValue to set * @since 2022-01-29 */ - public void setInputValue(OptionalDouble inputValue) { + public void setInputValue(String inputValue) { this.inputValue = inputValue; } - public void setSelectedDimension( - ObjectProduct selectedDimension) { - this.setSelectedDimension(Optional.of(selectedDimension)); - } - /** * @param selectedDimension the selectedDimension to set * @since 2022-01-29 */ - public void setSelectedDimension( - Optional> selectedDimension) { - this.selectedDimension = selectedDimension; + public void setSelectedDimensionName( + Optional selectedDimensionName) { + this.selectedDimensionName = selectedDimensionName; + } + + public void setSelectedDimensionName(String selectedDimensionName) { + this.setSelectedDimensionName(Optional.of(selectedDimensionName)); } /** @@ -243,17 +222,17 @@ final class ViewBot implements UnitConversionView, ExpressionConversionView { * @param toSelection the toSelection to set * @since 2022-01-29 */ - public void setToSelection(Optional toSelection) { + public void setToSelection(Optional toSelection) { this.toSelection = Objects.requireNonNull(toSelection, "toSelection cannot be null."); } - public void setToSelection(Unit toSelection) { + public void setToSelection(String toSelection) { this.setToSelection(Optional.of(toSelection)); } @Override - public void setToUnits(Set units) { + public void setToUnitNames(Set units) { this.toUnits = Objects.requireNonNull(units, "units may not be null"); } @@ -263,19 +242,15 @@ final class ViewBot implements UnitConversionView, ExpressionConversionView { } @Override - public void showExpressionConversionOutput(String fromExpression, - String toExpression, double value) { - final String output = String.format("%s = %s %s", fromExpression, value, - toExpression); - this.expressionConversionOutputs.add(output); - System.out.println("Expression Conversion: " + output); + public void showExpressionConversionOutput(UnitConversionRecord uc) { + this.expressionConversions.add(uc); + System.out.println("Expression Conversion: " + uc); } @Override - public void showUnitConversionOutput(UnitValue input, UnitValue output) { - this.unitConversionInputValues.add(input); - this.unitConversionOutputValues.add(output); - System.out.println("Unit conversion: " + input + " = " + output); + public void showUnitConversionOutput(UnitConversionRecord uc) { + this.unitConversions.add(uc); + System.out.println("Unit Conversion: " + uc); } @Override @@ -283,4 +258,12 @@ final class ViewBot implements UnitConversionView, ExpressionConversionView { return super.toString() + String.format("[presenter=%s]", this.presenter); } + /** + * @return list of records of every unit conversion made by this bot + * @since 2022-04-09 + */ + public List unitConversionList() { + return Collections.unmodifiableList(this.unitConversions); + } + } diff --git a/src/test/java/sevenUnitsGUI/PresenterTest.java b/src/test/java/sevenUnitsGUI/PresenterTest.java index 82842d8..dc2fb57 100644 --- a/src/test/java/sevenUnitsGUI/PresenterTest.java +++ b/src/test/java/sevenUnitsGUI/PresenterTest.java @@ -17,12 +17,9 @@ package sevenUnitsGUI; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assumptions.assumeTrue; -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; @@ -32,6 +29,7 @@ import sevenUnits.unit.BaseDimension; import sevenUnits.unit.Metric; import sevenUnits.unit.Unit; import sevenUnits.unit.UnitValue; +import sevenUnits.utils.Nameable; import sevenUnits.utils.NamedObjectProduct; /** @@ -46,10 +44,8 @@ public final class PresenterTest { static final Set> testDimensions = Set .of(Metric.Dimensions.LENGTH, Metric.Dimensions.VELOCITY); - private static final List unitNames( - Collection units) { - return units.stream().map(Unit::getShortName) - .collect(Collectors.toList()); + private static final Set names(Set units) { + return units.stream().map(Nameable::getName).collect(Collectors.toSet()); } /** @@ -70,8 +66,10 @@ public final class PresenterTest { presenter.convertExpressions(); // test result - final List outputs = viewBot.getExpressionConversionOutputs(); - assertEquals("10000.0 m = 10.0 km", outputs.get(outputs.size() - 1)); + final List outputs = viewBot + .expressionConversionList(); + assertEquals("10000.0 m = 10.0 km", + outputs.get(outputs.size() - 1).toString()); } /** @@ -85,11 +83,11 @@ public final class PresenterTest { final ViewBot viewBot = new ViewBot(); final Presenter presenter = new Presenter(viewBot); - viewBot.setFromUnits(testUnits); - viewBot.setToUnits(testUnits); - viewBot.setFromSelection(Optional.of(Metric.METRE)); - viewBot.setToSelection(Optional.of(Metric.KILOMETRE)); - viewBot.setInputValue(OptionalDouble.of(10000.0)); + viewBot.setFromUnitNames(names(testUnits)); + viewBot.setToUnitNames(names(testUnits)); + viewBot.setFromSelection("metre"); + viewBot.setToSelection("kilometre"); + viewBot.setInputValue("10000.0"); // convert units presenter.convertUnits(); @@ -102,11 +100,29 @@ public final class PresenterTest { final UnitValue expectedInput = UnitValue.of(Metric.METRE, 10000.0); final UnitValue expectedOutput = expectedInput .convertTo(Metric.KILOMETRE); + final UnitConversionRecord expectedUC = UnitConversionRecord + .fromValues(expectedInput, expectedOutput); - final List inputs = viewBot.getUnitConversionInputValues(); - final List outputs = viewBot.getUnitConversionOutputValues(); - assertEquals(expectedInput, inputs.get(inputs.size() - 1)); - assertEquals(expectedOutput, outputs.get(outputs.size() - 1)); + assertEquals(List.of(expectedUC), viewBot.unitConversionList()); + } + + @Test + void testDuplicateUnits() { + assumeTrue(false, "Not yet implemented"); + /* + * enable and disable duplicate units and check for those in From and To, + * include duplicate units in the input set + */ + } + + @Test + void testOneWayConversion() { + assumeTrue(false, "Not yet implemented"); + /* + * enable and disable one-way conversion, testing the units in From and To + * on each setting to ensure they match the rule. Include at least one + * metric exception. + */ } /** @@ -125,31 +141,24 @@ public final class PresenterTest { for (final Unit unit : testUnits) { presenter.database.addUnit(unit.getPrimaryName().orElseThrow(), unit); } + for (final var dimension : testDimensions) { + presenter.database.addDimension( + dimension.getPrimaryName().orElseThrow(), dimension); + } // set from and to units - viewBot.setFromUnits(testUnits); - viewBot.setToUnits(testUnits); - viewBot.setDimensions(testDimensions); - viewBot.setSelectedDimension(Optional.of(Metric.Dimensions.LENGTH)); + viewBot.setFromUnitNames(names(testUnits)); + viewBot.setToUnitNames(names(testUnits)); + viewBot.setDimensionNames(names(testDimensions)); + viewBot.setSelectedDimensionName(Metric.Dimensions.LENGTH.getName()); // filter to length units only, then get the filtered sets of units presenter.updateView(); - final Set fromUnits = viewBot.getFromUnits(); - final Set toUnits = viewBot.getToUnits(); + final Set fromUnits = viewBot.getFromUnitNames(); + final Set toUnits = viewBot.getToUnitNames(); // 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)); + assertEquals(Set.of("metre", "kilometre"), fromUnits); + assertEquals(Set.of("metre", "kilometre"), toUnits); } } -- cgit v1.2.3 From b1affe92460637211f560d70ee5c8770f3952822 Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Sun, 10 Apr 2022 14:22:29 -0500 Subject: Created API for settings and unit/prefix viewing --- src/main/java/sevenUnits/unit/Unit.java | 14 +++-- src/main/java/sevenUnits/unit/UnitType.java | 34 ++++++++++++ src/main/java/sevenUnitsGUI/Presenter.java | 60 +++++++++++++++++++--- src/main/java/sevenUnitsGUI/TabbedView.java | 36 ++++++++++++- .../java/sevenUnitsGUI/UnitConversionView.java | 2 +- src/main/java/sevenUnitsGUI/View.java | 56 ++++++++++++++++++++ src/main/java/sevenUnitsGUI/ViewBot.java | 35 ++++++++++++- 7 files changed, 219 insertions(+), 18 deletions(-) create mode 100644 src/main/java/sevenUnits/unit/UnitType.java diff --git a/src/main/java/sevenUnits/unit/Unit.java b/src/main/java/sevenUnits/unit/Unit.java index b80ccbd..826b59b 100644 --- a/src/main/java/sevenUnits/unit/Unit.java +++ b/src/main/java/sevenUnits/unit/Unit.java @@ -24,7 +24,6 @@ import java.util.function.DoubleUnaryOperator; import sevenUnits.utils.DecimalComparison; import sevenUnits.utils.NameSymbol; import sevenUnits.utils.Nameable; -import sevenUnits.utils.NamedObjectProduct; import sevenUnits.utils.ObjectProduct; /** @@ -191,7 +190,7 @@ public abstract class Unit implements Nameable { * * @implSpec This method is used by {@link #convertTo}, and its behaviour * affects the behaviour of {@code convertTo}. - * + * * @param value value expressed in base unit * @return value expressed in this unit * @since 2018-12-22 @@ -207,7 +206,7 @@ public abstract class Unit implements Nameable { * {@code other.convertFromBase(this.convertToBase(value))}. * Therefore, overriding either of those methods will change the * output of this method. - * + * * @param other unit to convert to * @param value value to convert * @return converted value @@ -234,7 +233,7 @@ public abstract class Unit implements Nameable { * {@code other.convertFromBase(this.convertToBase(value))}. * Therefore, overriding either of those methods will change the * output of this method. - * + * * @param other unitlike form to convert to * @param value value to convert * @param type of value to convert to @@ -269,7 +268,7 @@ public abstract class Unit implements Nameable { * * @implSpec This method is used by {@link #convertTo}, and its behaviour * affects the behaviour of {@code convertTo}. - * + * * @param value value expressed in this unit * @return value expressed in base unit * @since 2018-12-22 @@ -355,9 +354,8 @@ public abstract class Unit implements Nameable { * @since 2022-03-10 */ public String toDefinitionString() { - if (this.unitBase instanceof NamedObjectProduct) - return "derived from " - + ((NamedObjectProduct) this.unitBase).getName(); + if (this.unitBase instanceof Nameable) + return "derived from " + ((Nameable) this.unitBase).getName(); else return "derived from " + this.getBase().toString(BaseUnit::getShortName); diff --git a/src/main/java/sevenUnits/unit/UnitType.java b/src/main/java/sevenUnits/unit/UnitType.java new file mode 100644 index 0000000..a13051a --- /dev/null +++ b/src/main/java/sevenUnits/unit/UnitType.java @@ -0,0 +1,34 @@ +/** + * Copyright (C) 2022 Adrien Hopkins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package sevenUnits.unit; + +/** + * A type of unit, as chosen by the type of system it is in. + *

    + *
  • {@code METRIC} refers to metric/SI units that pass {@link Unit#isMetric} + *
  • {@code SEMI_METRIC} refers to the degree Celsius (which is an official SI + * unit but does not pass {@link Unit#isMetric}) and non-metric units intended + * for use with the SI. + *
  • {@code NON_METRIC} refers to units that are neither metric nor intended + * for use with the metric system (e.g. imperial and customary units) + *
+ * + * @since 2022-04-10 + */ +public enum UnitType { + METRIC, SEMI_METRIC, NON_METRIC; +} diff --git a/src/main/java/sevenUnitsGUI/Presenter.java b/src/main/java/sevenUnitsGUI/Presenter.java index 9f8fe69..b38f90b 100644 --- a/src/main/java/sevenUnitsGUI/Presenter.java +++ b/src/main/java/sevenUnitsGUI/Presenter.java @@ -91,7 +91,7 @@ public final class Presenter { * @return text in About file * @since 2022-02-19 */ - static final String getAboutText() { + public static final String getAboutText() { return Presenter.getLinesFromResource("/about.txt").stream() .map(Presenter::withoutComments).collect(Collectors.joining("\n")) .replaceAll("\\[VERSION\\]", ProgramInfo.VERSION.toString()); @@ -155,7 +155,7 @@ public final class Presenter { /** * The rule used for parsing input numbers. Any number-string inputted into - * this program will be parsed using this method. + * this program will be parsed using this method. Not implemented yet. */ private Function numberParsingRule; @@ -380,6 +380,25 @@ public final class Presenter { return this.showDuplicateUnits; } + /** + * @return the rule that is used by this presenter to convert numbers into + * strings + * @since 2022-04-10 + */ + public Function getNumberDisplayRule() { + return this.numberDisplayRule; + } + + /** + * @return the rule that is used by this presenter to convert strings into + * numbers + * @since 2022-04-10 + */ + @SuppressWarnings("unused") // not implemented yet + private Function getNumberParsingRule() { + return this.numberParsingRule; + } + /** * @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 @@ -392,11 +411,12 @@ public final class Presenter { } /** - * Loads settings from the user's settings file and applies them to the view. + * Loads settings from the user's settings file and applies them to the + * presenter. * * @since 2021-12-15 */ - public void loadSettings() {} + private void loadSettings() {} /** * Completes creation of the presenter. This part of the initialization @@ -416,12 +436,32 @@ public final class Presenter { void prefixSelected() {} /** - * Gets user settings from the view then saves them to the user's settings - * file. + * Saves the presenter's settings to the user settings file. * * @since 2021-12-15 */ - public void saveSettings() {} + private void saveSettings() {} + + /** + * @param numberDisplayRule the new rule that will be used by this presenter + * to convert numbers into strings + * @since 2022-04-10 + */ + public void setNumberDisplayRule( + Function numberDisplayRule) { + this.numberDisplayRule = numberDisplayRule; + } + + /** + * @param numberParsingRule the new rule that will be used by this presenter + * to convert strings into numbers + * @since 2022-04-10 + */ + @SuppressWarnings("unused") // not implemented yet + private void setNumberParsingRule( + Function numberParsingRule) { + this.numberParsingRule = numberParsingRule; + } /** * @param oneWayConversionEnabled whether not one-way conversion should be @@ -443,6 +483,12 @@ public final class Presenter { this.updateView(); } + /** + * 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() {} /** diff --git a/src/main/java/sevenUnitsGUI/TabbedView.java b/src/main/java/sevenUnitsGUI/TabbedView.java index ed45011..fd48965 100644 --- a/src/main/java/sevenUnitsGUI/TabbedView.java +++ b/src/main/java/sevenUnitsGUI/TabbedView.java @@ -56,6 +56,8 @@ import javax.swing.border.EmptyBorder; import javax.swing.border.TitledBorder; import sevenUnits.ProgramInfo; +import sevenUnits.unit.UnitType; +import sevenUnits.utils.NameSymbol; /** * A View that separates its functions into multiple tabs @@ -306,6 +308,8 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { 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)); @@ -572,6 +576,16 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { return new HashSet<>(this.toSearch.getItems()); } + @Override + public Optional getViewedPrefixName() { + throw new UnsupportedOperationException("Not implemented yet"); + } + + @Override + public Optional getViewedUnitName() { + throw new UnsupportedOperationException("Not implemented yet"); + } + @Override public void setDimensionNames(Set dimensionNames) { this.dimensionSelector.removeAllItems(); @@ -590,6 +604,16 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { this.toSearch.setItems(units); } + @Override + public void setViewablePrefixNames(Set prefixNames) { + throw new UnsupportedOperationException("Not implemented yet"); + } + + @Override + public void setViewableUnitNames(Set unitNames) { + throw new UnsupportedOperationException("Not implemented yet"); + } + @Override public void showErrorMessage(String title, String message) { JOptionPane.showMessageDialog(this.frame, message, title, @@ -602,9 +626,19 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { uc.outputValueString(), uc.toName())); } + @Override + public void showPrefix(NameSymbol name, String multiplierString) { + throw new UnsupportedOperationException("Not implemented yet"); + } + + @Override + public void showUnit(NameSymbol name, String definition, + String dimensionName, UnitType type) { + throw new UnsupportedOperationException("Not implemented yet"); + } + @Override public void showUnitConversionOutput(UnitConversionRecord uc) { this.unitOutput.setText(uc.toString()); } - } diff --git a/src/main/java/sevenUnitsGUI/UnitConversionView.java b/src/main/java/sevenUnitsGUI/UnitConversionView.java index 9d3a67b..6a95aa5 100644 --- a/src/main/java/sevenUnitsGUI/UnitConversionView.java +++ b/src/main/java/sevenUnitsGUI/UnitConversionView.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2021 Adrien Hopkins + * Copyright (C) 2021-2022 Adrien Hopkins * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by diff --git a/src/main/java/sevenUnitsGUI/View.java b/src/main/java/sevenUnitsGUI/View.java index e78c9cc..da3749e 100644 --- a/src/main/java/sevenUnitsGUI/View.java +++ b/src/main/java/sevenUnitsGUI/View.java @@ -16,6 +16,12 @@ */ package sevenUnitsGUI; +import java.util.Optional; +import java.util.Set; + +import sevenUnits.unit.UnitType; +import sevenUnits.utils.NameSymbol; + /** * An object that controls user interaction with 7Units * @@ -23,6 +29,35 @@ package sevenUnitsGUI; * @since 2021-12-15 */ public interface View { + /** + * @return name of prefix currently being viewed + * @since 2022-04-10 + */ + Optional getViewedPrefixName(); + + /** + * @return name of unit currently being viewed + * @since 2022-04-10 + */ + Optional getViewedUnitName(); + + /** + * Sets the list of prefixes that are available to be viewed in a prefix + * viewer + * + * @param prefixNames prefix names to view + * @since 2022-04-10 + */ + void setViewablePrefixNames(Set prefixNames); + + /** + * Sets the list of units that are available to be viewed in a unit viewer + * + * @param unitNames unit names to view + * @since 2022-04-10 + */ + void setViewableUnitNames(Set unitNames); + /** * Shows an error message. * @@ -32,4 +67,25 @@ public interface View { * @since 2021-12-15 */ void showErrorMessage(String title, String message); + + /** + * Shows information about a prefix to the user. + * + * @param name name(s) and symbol of prefix + * @param multiplierString string representation of prefix multiplier + * @since 2022-04-10 + */ + void showPrefix(NameSymbol name, String multiplierString); + + /** + * Shows information about a unit to the user. + * + * @param name name(s) and symbol of unit + * @param definition unit's definition string + * @param dimensionName name of unit's dimension + * @param type type of unit (metric/semi-metric/non-metric) + * @since 2022-04-10 + */ + void showUnit(NameSymbol name, String definition, String dimensionName, + UnitType type); } diff --git a/src/main/java/sevenUnitsGUI/ViewBot.java b/src/main/java/sevenUnitsGUI/ViewBot.java index 0195dd6..9f9a524 100644 --- a/src/main/java/sevenUnitsGUI/ViewBot.java +++ b/src/main/java/sevenUnitsGUI/ViewBot.java @@ -23,6 +23,9 @@ import java.util.Objects; import java.util.Optional; import java.util.Set; +import sevenUnits.unit.UnitType; +import sevenUnits.utils.NameSymbol; + /** * A class that simulates a View (supports both unit and expression conversion) * for testing. Getters and setters work as expected. @@ -145,6 +148,16 @@ final class ViewBot implements UnitConversionView, ExpressionConversionView { return Collections.unmodifiableSet(this.toUnits); } + @Override + public Optional getViewedPrefixName() { + throw new UnsupportedOperationException("Not implemented yet"); + } + + @Override + public Optional getViewedUnitName() { + throw new UnsupportedOperationException("Not implemented yet"); + } + @Override public void setDimensionNames(Set dimensionNames) { this.dimensionNames = Objects.requireNonNull(dimensionNames, @@ -236,6 +249,16 @@ final class ViewBot implements UnitConversionView, ExpressionConversionView { this.toUnits = Objects.requireNonNull(units, "units may not be null"); } + @Override + public void setViewablePrefixNames(Set prefixNames) { + throw new UnsupportedOperationException("Not implemented yet"); + } + + @Override + public void setViewableUnitNames(Set unitNames) { + throw new UnsupportedOperationException("Not implemented yet"); + } + @Override public void showErrorMessage(String title, String message) { System.err.printf("%s: %s%n", title, message); @@ -247,6 +270,17 @@ final class ViewBot implements UnitConversionView, ExpressionConversionView { System.out.println("Expression Conversion: " + uc); } + @Override + public void showPrefix(NameSymbol name, String multiplierString) { + throw new UnsupportedOperationException("Not implemented yet"); + } + + @Override + public void showUnit(NameSymbol name, String definition, + String dimensionName, UnitType type) { + throw new UnsupportedOperationException("Not implemented yet"); + } + @Override public void showUnitConversionOutput(UnitConversionRecord uc) { this.unitConversions.add(uc); @@ -265,5 +299,4 @@ final class ViewBot implements UnitConversionView, ExpressionConversionView { public List unitConversionList() { return Collections.unmodifiableList(this.unitConversions); } - } -- 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 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 4aaf6a8b60fbec63c2e0bee624b3859ded0ecde3 Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Sat, 16 Apr 2022 15:57:00 -0500 Subject: Added a full suite of frontend tests (Added tests for the settings and unit/prefix viewer parts of the GUI, which are not yet implemented) --- src/main/java/sevenUnits/unit/UnitPrefix.java | 135 +++++-------- src/main/java/sevenUnitsGUI/Presenter.java | 14 +- .../java/sevenUnitsGUI/StandardDisplayRules.java | 26 +++ src/main/java/sevenUnitsGUI/ViewBot.java | 200 +++++++++++++++++++- src/test/java/sevenUnitsGUI/PresenterTest.java | 210 +++++++++++++++++++-- 5 files changed, 482 insertions(+), 103 deletions(-) diff --git a/src/main/java/sevenUnits/unit/UnitPrefix.java b/src/main/java/sevenUnits/unit/UnitPrefix.java index bf9d1fd..e1f7788 100644 --- a/src/main/java/sevenUnits/unit/UnitPrefix.java +++ b/src/main/java/sevenUnits/unit/UnitPrefix.java @@ -17,69 +17,59 @@ package sevenUnits.unit; import java.util.Objects; -import java.util.Optional; -import java.util.Set; import sevenUnits.utils.DecimalComparison; import sevenUnits.utils.NameSymbol; +import sevenUnits.utils.Nameable; /** - * A prefix that can be applied to a {@code LinearUnit} to multiply it by some value + * A prefix that can be applied to a {@code LinearUnit} to multiply it by some + * value * * @author Adrien Hopkins * @since 2019-10-16 */ -public final class UnitPrefix { +public final class UnitPrefix implements Nameable { /** * Gets a {@code UnitPrefix} from a multiplier * - * @param multiplier - * multiplier of prefix + * @param multiplier multiplier of prefix * @return prefix * @since 2019-10-16 */ public static UnitPrefix valueOf(final double multiplier) { return new UnitPrefix(multiplier, NameSymbol.EMPTY); } - + /** * Gets a {@code UnitPrefix} from a multiplier and a name * - * @param multiplier - * multiplier of prefix - * @param ns - * name(s) and symbol of prefix + * @param multiplier multiplier of prefix + * @param ns name(s) and symbol of prefix * @return prefix * @since 2019-10-16 - * @throws NullPointerException - * if ns is null + * @throws NullPointerException if ns is null */ - public static UnitPrefix valueOf(final double multiplier, final NameSymbol ns) { - return new UnitPrefix(multiplier, Objects.requireNonNull(ns, "ns must not be null.")); + public static UnitPrefix valueOf(final double multiplier, + final NameSymbol ns) { + return new UnitPrefix(multiplier, + Objects.requireNonNull(ns, "ns must not be null.")); } - - /** - * This prefix's primary name - */ - private final Optional primaryName; - - /** - * This prefix's symbol - */ - private final Optional symbol; - + /** - * Other names and symbols used by this prefix + * This prefix's name(s) and symbol. + * + * @since 2022-04-16 */ - private final Set otherNames; - + private final NameSymbol nameSymbol; + /** * The number that this prefix multiplies units by * * @since 2019-10-16 */ private final double multiplier; - + /** * Creates the {@code DefaultUnitPrefix}. * @@ -89,28 +79,24 @@ public final class UnitPrefix { */ private UnitPrefix(final double multiplier, final NameSymbol ns) { this.multiplier = multiplier; - this.primaryName = ns.getPrimaryName(); - this.symbol = ns.getSymbol(); - this.otherNames = ns.getOtherNames(); + this.nameSymbol = ns; } - + /** * Divides this prefix by a scalar * - * @param divisor - * number to divide by + * @param divisor number to divide by * @return quotient of prefix and scalar * @since 2019-10-16 */ public UnitPrefix dividedBy(final double divisor) { return valueOf(this.getMultiplier() / divisor); } - + /** * Divides this prefix by {@code other}. * - * @param other - * prefix to divide by + * @param other prefix to divide by * @return quotient of prefixes * @since 2019-04-13 * @since v0.2.0 @@ -118,7 +104,7 @@ public final class UnitPrefix { public UnitPrefix dividedBy(final UnitPrefix other) { return valueOf(this.getMultiplier() / other.getMultiplier()); } - + /** * {@inheritDoc} * @@ -133,9 +119,10 @@ public final class UnitPrefix { if (!(obj instanceof UnitPrefix)) return false; final UnitPrefix other = (UnitPrefix) obj; - return DecimalComparison.equals(this.getMultiplier(), other.getMultiplier()); + return DecimalComparison.equals(this.getMultiplier(), + other.getMultiplier()); } - + /** * @return prefix's multiplier * @since 2019-11-26 @@ -143,31 +130,12 @@ public final class UnitPrefix { public double getMultiplier() { return this.multiplier; } - - /** - * @return other names - * @since 2019-11-26 - */ - public final Set getOtherNames() { - return this.otherNames; - } - - /** - * @return primary name - * @since 2019-11-26 - */ - public final Optional getPrimaryName() { - return this.primaryName; - } - - /** - * @return symbol - * @since 2019-11-26 - */ - public final Optional getSymbol() { - return this.symbol; + + @Override + public NameSymbol getNameSymbol() { + return this.nameSymbol; } - + /** * {@inheritDoc} * @@ -177,24 +145,22 @@ public final class UnitPrefix { public int hashCode() { return DecimalComparison.hash(this.getMultiplier()); } - + /** * Multiplies this prefix by a scalar * - * @param multiplicand - * number to multiply by + * @param multiplicand number to multiply by * @return product of prefix and scalar * @since 2019-10-16 */ public UnitPrefix times(final double multiplicand) { return valueOf(this.getMultiplier() * multiplicand); } - + /** * Multiplies this prefix by {@code other}. * - * @param other - * prefix to multiply by + * @param other prefix to multiply by * @return product of prefixes * @since 2019-04-13 * @since v0.2.0 @@ -202,12 +168,11 @@ public final class UnitPrefix { public UnitPrefix times(final UnitPrefix other) { return valueOf(this.getMultiplier() * other.getMultiplier()); } - + /** * Raises this prefix to an exponent. * - * @param exponent - * exponent to raise to + * @param exponent exponent to raise to * @return result of exponentiation. * @since 2019-04-13 * @since v0.2.0 @@ -215,27 +180,27 @@ public final class UnitPrefix { public UnitPrefix toExponent(final double exponent) { return valueOf(Math.pow(this.getMultiplier(), exponent)); } - + /** * @return a string describing the prefix and its multiplier */ @Override public String toString() { - if (this.primaryName.isPresent()) - return String.format("%s (\u00D7 %s)", this.primaryName.get(), this.multiplier); - else if (this.symbol.isPresent()) - return String.format("%s (\u00D7 %s)", this.symbol.get(), this.multiplier); + if (this.getPrimaryName().isPresent()) + return String.format("%s (\u00D7 %s)", this.getPrimaryName().get(), + this.multiplier); + else if (this.getSymbol().isPresent()) + return String.format("%s (\u00D7 %s)", this.getSymbol().get(), + this.multiplier); else return String.format("Unit Prefix (\u00D7 %s)", this.multiplier); } - + /** - * @param ns - * name(s) and symbol to use + * @param ns name(s) and symbol to use * @return copy of this prefix with provided name(s) and symbol * @since 2019-11-26 - * @throws NullPointerException - * if ns is null + * @throws NullPointerException if ns is null */ public UnitPrefix withName(final NameSymbol ns) { return valueOf(this.multiplier, ns); diff --git a/src/main/java/sevenUnitsGUI/Presenter.java b/src/main/java/sevenUnitsGUI/Presenter.java index b38f90b..5c8ce53 100644 --- a/src/main/java/sevenUnitsGUI/Presenter.java +++ b/src/main/java/sevenUnitsGUI/Presenter.java @@ -18,6 +18,7 @@ package sevenUnitsGUI; import java.io.IOException; import java.io.InputStream; +import java.nio.file.Path; import java.util.ArrayList; import java.util.HashSet; import java.util.List; @@ -50,7 +51,8 @@ import sevenUnits.utils.UncertainDouble; */ public final class Presenter { /** The default place where settings are stored. */ - private static final String DEFAULT_SETTINGS_FILEPATH = "settings.txt"; + private static final Path DEFAULT_SETTINGS_FILEPATH = Path + .of("settings.txt"); /** The default place where units are stored. */ private static final String DEFAULT_UNITS_FILEPATH = "/unitsfile.txt"; /** The default place where dimensions are stored. */ @@ -406,17 +408,18 @@ public final class Presenter { * * @since 2022-03-30 */ - public boolean isOneWayConversionEnabled() { + public boolean oneWayConversionEnabled() { return this.oneWayConversionEnabled; } /** * Loads settings from the user's settings file and applies them to the * presenter. - * + * + * @param settingsFile file settings should be loaded from * @since 2021-12-15 */ - private void loadSettings() {} + void loadSettings(Path settingsFile) {} /** * Completes creation of the presenter. This part of the initialization @@ -438,9 +441,10 @@ public final class Presenter { /** * Saves the presenter's settings to the user settings file. * + * @param settingsFile file settings should be saved to * @since 2021-12-15 */ - private void saveSettings() {} + void saveSettings(Path settingsFile) {} /** * @param numberDisplayRule the new rule that will be used by this presenter diff --git a/src/main/java/sevenUnitsGUI/StandardDisplayRules.java b/src/main/java/sevenUnitsGUI/StandardDisplayRules.java index 331f598..f6272c8 100644 --- a/src/main/java/sevenUnitsGUI/StandardDisplayRules.java +++ b/src/main/java/sevenUnitsGUI/StandardDisplayRules.java @@ -44,6 +44,32 @@ final class StandardDisplayRules { } }; + /** + * Gets a display rule that rounds numbers to a fixed number of decimal + * places. + * + * @param decimalPlaces number of decimal places + * @return display rule + * @since 2022-04-16 + */ + public static final Function getFixedPlacesRule( + int decimalPlaces) { + throw new UnsupportedOperationException("Not implemented yet"); + } + + /** + * Gets a display rule that rounds numbers to a fixed number of significant + * figures. + * + * @param significantFigures number of significant figures + * @return display rule + * @since 2022-04-16 + */ + public static final Function getFixedPrecisionRule( + int significantFigures) { + throw new UnsupportedOperationException("Not implemented yet"); + } + /** * @return a rule that rounds using UncertainDouble's own toString(false) * function. diff --git a/src/main/java/sevenUnitsGUI/ViewBot.java b/src/main/java/sevenUnitsGUI/ViewBot.java index 9f9a524..988d1bc 100644 --- a/src/main/java/sevenUnitsGUI/ViewBot.java +++ b/src/main/java/sevenUnitsGUI/ViewBot.java @@ -25,6 +25,7 @@ import java.util.Set; import sevenUnits.unit.UnitType; import sevenUnits.utils.NameSymbol; +import sevenUnits.utils.Nameable; /** * A class that simulates a View (supports both unit and expression conversion) @@ -34,6 +35,163 @@ import sevenUnits.utils.NameSymbol; * @since 2022-01-29 */ final class ViewBot implements UnitConversionView, ExpressionConversionView { + /** + * A record of the parameters given to + * {@link View#showPrefix(NameSymbol, String)}, for testing. + * + * @since 2022-04-16 + */ + public static final class PrefixViewingRecord implements Nameable { + private final NameSymbol nameSymbol; + private final String multiplierString; + + /** + * @param nameSymbol + * @param multiplierString + * @since 2022-04-16 + */ + public PrefixViewingRecord(NameSymbol nameSymbol, + String multiplierString) { + this.nameSymbol = nameSymbol; + this.multiplierString = multiplierString; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (!(obj instanceof PrefixViewingRecord)) + return false; + final PrefixViewingRecord other = (PrefixViewingRecord) obj; + return Objects.equals(this.multiplierString, other.multiplierString) + && Objects.equals(this.nameSymbol, other.nameSymbol); + } + + @Override + public NameSymbol getNameSymbol() { + return this.nameSymbol; + } + + @Override + public int hashCode() { + return Objects.hash(this.multiplierString, this.nameSymbol); + } + + public String multiplierString() { + return this.multiplierString; + } + + public NameSymbol nameSymbol() { + return this.nameSymbol; + } + + @Override + public String toString() { + final StringBuilder builder = new StringBuilder(); + builder.append("PrefixViewingRecord [nameSymbol="); + builder.append(this.nameSymbol); + builder.append(", multiplierString="); + builder.append(this.multiplierString); + builder.append("]"); + return builder.toString(); + } + } + + /** + * A record of the parameters given to + * {@link View#showUnit(NameSymbol, String, String, UnitType)}, for testing. + * + * @since 2022-04-16 + */ + public static final class UnitViewingRecord implements Nameable { + private final NameSymbol nameSymbol; + private final String definition; + private final String dimensionName; + private final UnitType unitType; + + /** + * @since 2022-04-16 + */ + public UnitViewingRecord(NameSymbol nameSymbol, String definition, + String dimensionName, UnitType unitType) { + this.nameSymbol = nameSymbol; + this.definition = definition; + this.dimensionName = dimensionName; + this.unitType = unitType; + } + + /** + * @return the definition + * @since 2022-04-16 + */ + public String definition() { + return this.definition; + } + + /** + * @return the dimensionName + * @since 2022-04-16 + */ + public String dimensionName() { + return this.dimensionName; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (!(obj instanceof UnitViewingRecord)) + return false; + final UnitViewingRecord other = (UnitViewingRecord) obj; + return Objects.equals(this.definition, other.definition) + && Objects.equals(this.dimensionName, other.dimensionName) + && Objects.equals(this.nameSymbol, other.nameSymbol) + && this.unitType == other.unitType; + } + + /** + * @return the nameSymbol + * @since 2022-04-16 + */ + @Override + public NameSymbol getNameSymbol() { + return this.nameSymbol; + } + + @Override + public int hashCode() { + return Objects.hash(this.definition, this.dimensionName, + this.nameSymbol, this.unitType); + } + + public NameSymbol nameSymbol() { + return this.nameSymbol; + } + + @Override + public String toString() { + final StringBuilder builder = new StringBuilder(); + builder.append("UnitViewingRecord [nameSymbol="); + builder.append(this.nameSymbol); + builder.append(", definition="); + builder.append(this.definition); + builder.append(", dimensionName="); + builder.append(this.dimensionName); + builder.append(", unitType="); + builder.append(this.unitType); + builder.append("]"); + return builder.toString(); + } + + /** + * @return the unitType + * @since 2022-04-16 + */ + public UnitType unitType() { + return this.unitType; + } + } + /** The presenter that works with this ViewBot */ private final Presenter presenter; @@ -62,6 +220,10 @@ final class ViewBot implements UnitConversionView, ExpressionConversionView { private final List unitConversions; /** Saved outputs of all unit expressions */ private final List expressionConversions; + /** Saved outputs of all unit viewings */ + private final List unitViewingRecords; + /** Saved outputs of all prefix viewings */ + private final List prefixViewingRecords; /** * Creates a new {@code ViewBot} with a new presenter. @@ -73,6 +235,8 @@ final class ViewBot implements UnitConversionView, ExpressionConversionView { this.unitConversions = new ArrayList<>(); this.expressionConversions = new ArrayList<>(); + this.unitViewingRecords = new ArrayList<>(); + this.prefixViewingRecords = new ArrayList<>(); } /** @@ -80,7 +244,7 @@ final class ViewBot implements UnitConversionView, ExpressionConversionView { * @since 2022-04-09 */ public List expressionConversionList() { - return this.expressionConversions; + return Collections.unmodifiableList(this.expressionConversions); } /** @@ -158,6 +322,14 @@ final class ViewBot implements UnitConversionView, ExpressionConversionView { throw new UnsupportedOperationException("Not implemented yet"); } + /** + * @return list of records of this viewBot's prefix views + * @since 2022-04-16 + */ + public List prefixViewList() { + return Collections.unmodifiableList(this.prefixViewingRecords); + } + @Override public void setDimensionNames(Set dimensionNames) { this.dimensionNames = Objects.requireNonNull(dimensionNames, @@ -259,6 +431,24 @@ final class ViewBot implements UnitConversionView, ExpressionConversionView { throw new UnsupportedOperationException("Not implemented yet"); } + public void setViewedPrefixName( + @SuppressWarnings("unused") Optional viewedPrefixName) { + throw new UnsupportedOperationException("Not implemented yet"); + } + + public void setViewedPrefixName(String viewedPrefixName) { + this.setViewedPrefixName(Optional.of(viewedPrefixName)); + } + + public void setViewedUnitName( + @SuppressWarnings("unused") Optional viewedUnitName) { + throw new UnsupportedOperationException("Not implemented yet"); + } + + public void setViewedUnitName(String viewedUnitName) { + this.setViewedUnitName(Optional.of(viewedUnitName)); + } + @Override public void showErrorMessage(String title, String message) { System.err.printf("%s: %s%n", title, message); @@ -299,4 +489,12 @@ final class ViewBot implements UnitConversionView, ExpressionConversionView { public List unitConversionList() { return Collections.unmodifiableList(this.unitConversions); } + + /** + * @return list of records of unit viewings made by this bot + * @since 2022-04-16 + */ + public List unitViewList() { + return Collections.unmodifiableList(this.unitViewingRecords); + } } diff --git a/src/test/java/sevenUnitsGUI/PresenterTest.java b/src/test/java/sevenUnitsGUI/PresenterTest.java index 3fe7e47..85ebe09 100644 --- a/src/test/java/sevenUnitsGUI/PresenterTest.java +++ b/src/test/java/sevenUnitsGUI/PresenterTest.java @@ -17,20 +17,29 @@ package sevenUnitsGUI; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assumptions.assumeTrue; +import static org.junit.jupiter.api.Assertions.assertTrue; +import java.nio.file.Path; import java.util.List; import java.util.Set; +import java.util.function.Function; import java.util.stream.Collectors; +import java.util.stream.Stream; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; import sevenUnits.unit.BaseDimension; +import sevenUnits.unit.BritishImperial; import sevenUnits.unit.Metric; import sevenUnits.unit.Unit; +import sevenUnits.unit.UnitType; import sevenUnits.unit.UnitValue; +import sevenUnits.utils.NameSymbol; import sevenUnits.utils.Nameable; import sevenUnits.utils.ObjectProduct; +import sevenUnits.utils.UncertainDouble; /** * @author Adrien Hopkins @@ -38,12 +47,27 @@ import sevenUnits.utils.ObjectProduct; * @since 2022-02-10 */ public final class PresenterTest { + private static final Path TEST_SETTINGS = Path.of("src", "test", "resources", + "test-settings.txt"); static final Set testUnits = Set.of(Metric.METRE, Metric.KILOMETRE, Metric.METRE_PER_SECOND, Metric.KILOMETRE_PER_HOUR); static final Set> testDimensions = Set .of(Metric.Dimensions.LENGTH, Metric.Dimensions.VELOCITY); + /** + * @return rounding rules used by {@link #testRoundingRules} + * @since 2022-04-16 + */ + private static final Stream> getRoundingRules() { + final var SCIENTIFIC_ROUNDING = StandardDisplayRules.getScientificRule(); + final var INTEGER_ROUNDING = StandardDisplayRules.getFixedPlacesRule(0); + final var SIG_FIG_ROUNDING = StandardDisplayRules + .getFixedPrecisionRule(4); + + return Stream.of(SCIENTIFIC_ROUNDING, INTEGER_ROUNDING, SIG_FIG_ROUNDING); + } + private static final Set names(Set units) { return units.stream().map(Nameable::getName).collect(Collectors.toSet()); } @@ -106,23 +130,185 @@ public final class PresenterTest { assertEquals(List.of(expectedUC), viewBot.unitConversionList()); } + /** + * Tests that duplicate units are successfully removed, if that is asked for + * + * @since 2022-04-16 + */ @Test void testDuplicateUnits() { - assumeTrue(false, "Not yet implemented"); - /* - * enable and disable duplicate units and check for those in From and To, - * include duplicate units in the input set - */ + final var metre = Metric.METRE; + final var meter = Metric.METRE.withName(NameSymbol.of("meter", "m")); + + // load 2 duplicate units + final var viewBot = new ViewBot(); + final var presenter = new Presenter(viewBot); + presenter.database.clear(); + presenter.database.addUnit("metre", metre); + presenter.database.addUnit("meter", meter); + + // test that only one of them is included if duplicate units disabled + presenter.setShowDuplicateUnits(false); + presenter.updateView(); + assertEquals(1, viewBot.getFromUnitNames().size()); + assertEquals(1, viewBot.getToUnitNames().size()); + + // test that both of them is included if duplicate units enabled + presenter.setShowDuplicateUnits(true); + presenter.updateView(); + assertEquals(2, viewBot.getFromUnitNames().size()); + assertEquals(2, viewBot.getToUnitNames().size()); } + /** + * Tests that one-way conversion correctly filters From and To units + * + * @since 2022-04-16 + */ @Test void testOneWayConversion() { - assumeTrue(false, "Not yet implemented"); - /* - * enable and disable one-way conversion, testing the units in From and To - * on each setting to ensure they match the rule. Include at least one - * metric exception. - */ + // metre is metric, inch is non-metric, tempC is semi-metric + final var allNames = Set.of("metre", "inch", "tempC"); + final var metricNames = Set.of("metre", "tempC"); + final var nonMetricNames = Set.of("inch", "tempC"); + + // load view with one metric and one non-metric unit + final var viewBot = new ViewBot(); + final var presenter = new Presenter(viewBot); + presenter.database.clear(); + presenter.database.addUnit("metre", Metric.METRE); + presenter.database.addUnit("inch", BritishImperial.Length.INCH); + presenter.database.addUnit("tempC", Metric.CELSIUS); + + // test that units are removed from each side when one-way conversion is + // enabled + presenter.setOneWayConversionEnabled(true); + presenter.updateView(); + assertEquals(metricNames, viewBot.getFromUnitNames()); + assertEquals(nonMetricNames, viewBot.getToUnitNames()); + + // test that units are kept when one-way conversion is disabled + presenter.setOneWayConversionEnabled(false); + presenter.updateView(); + assertEquals(allNames, viewBot.getFromUnitNames()); + assertEquals(allNames, viewBot.getToUnitNames()); + } + + /** + * Tests the prefix-viewing functionality. + * + * @since 2022-04-16 + */ + @Test + void testPrefixViewing() { + // setup + final var viewBot = new ViewBot(); + final var presenter = new Presenter(viewBot); + viewBot.setViewablePrefixNames(Set.of("kilo", "milli")); + presenter.setNumberDisplayRule(UncertainDouble::toString); + + // view prefix + viewBot.setViewedPrefixName("kilo"); + presenter.prefixSelected(); // just in case + + // get correct values + final var expectedNameSymbol = Metric.KILO.getNameSymbol(); + final var expectedMultiplierString = String + .valueOf(Metric.KILO.getMultiplier()); + + // test that presenter's values are correct + final var prefixRecord = viewBot.prefixViewList().get(0); + assertEquals(expectedNameSymbol, prefixRecord.getNameSymbol()); + assertEquals(expectedMultiplierString, prefixRecord.multiplierString()); + } + + /** + * Tests that rounding rules are used correctly. + * + * @since 2022-04-16 + */ + @ParameterizedTest + @MethodSource("getRoundingRules") + void testRoundingRules(Function roundingRule) { + // setup + final var viewBot = new ViewBot(); + final var presenter = new Presenter(viewBot); + presenter.setNumberDisplayRule(roundingRule); + + // convert and round + viewBot.setInputValue("12345.6789"); + viewBot.setFromSelection("metre"); + viewBot.setToSelection("kilometre"); + presenter.convertUnits(); + + // test the result of the rounding + final String expectedOutputString = roundingRule + .apply(UncertainDouble.of(12.3456789, 0)); + final String actualOutputString = viewBot.unitConversionList().get(0) + .outputValueString(); + assertEquals(expectedOutputString, actualOutputString); + } + + /** + * Tests that settings can be saved to and loaded from a file. + * + * @since 2022-04-16 + */ + @Test + void testSettingsSaving() { + // setup + final var viewBot = new ViewBot(); + final var presenter = new Presenter(viewBot); + + // set and save custom settings + presenter.setOneWayConversionEnabled(true); + presenter.setShowDuplicateUnits(true); + presenter.setNumberDisplayRule( + StandardDisplayRules.getFixedPrecisionRule(11)); + presenter.saveSettings(TEST_SETTINGS); + + // overwrite custom settings + presenter.setOneWayConversionEnabled(false); + presenter.setShowDuplicateUnits(false); + presenter.setNumberDisplayRule(StandardDisplayRules.getScientificRule()); + + // load settings & test that they're the same + presenter.loadSettings(TEST_SETTINGS); + assertTrue(presenter.oneWayConversionEnabled()); + assertTrue(presenter.duplicateUnitsShown()); + assertEquals(StandardDisplayRules.getFixedPlacesRule(11), + presenter.getNumberDisplayRule()); + } + + /** + * Ensures the Presenter generates the correct data upon a unit-viewing. + * + * @since 2022-04-16 + */ + @Test + void testUnitViewing() { + // setup + final var viewBot = new ViewBot(); + final var presenter = new Presenter(viewBot); + viewBot.setViewableUnitNames(names(testUnits)); + + // view unit + viewBot.setViewedUnitName("metre"); + presenter.unitNameSelected(); // just in case this isn't triggered + // automatically + + // get correct values + final var expectedNameSymbol = Metric.METRE.getNameSymbol(); + final var expectedDefinition = Metric.METRE.toDefinitionString(); + final var expectedDimensionName = Metric.METRE.getDimension().getName(); + final var expectedUnitType = UnitType.METRIC; + + // test for correctness + final var viewRecord = viewBot.unitViewList().get(0); + assertEquals(expectedNameSymbol, viewRecord.getNameSymbol()); + assertEquals(expectedDefinition, viewRecord.definition()); + assertEquals(expectedDimensionName, viewRecord.dimensionName()); + assertEquals(expectedUnitType, viewRecord.unitType()); } /** -- cgit v1.2.3 From 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(-) 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(-) 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(-) 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 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 From 8cc60583134a4d01e9967424e5a51332de6cc38b Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Tue, 19 Apr 2022 16:35:43 -0500 Subject: Finalized version 0.4.0-alpha.1 --- README.org | 2 +- build.gradle | 2 +- src/main/java/sevenUnits/ProgramInfo.java | 2 +- .../converterGUI/DefaultPrefixRepetitionRule.java | 95 -- .../sevenUnits/converterGUI/DelegateListModel.java | 242 ---- .../sevenUnits/converterGUI/FilterComparator.java | 129 -- .../sevenUnits/converterGUI/GridBagBuilder.java | 479 ------- .../sevenUnits/converterGUI/MutablePredicate.java | 70 - .../sevenUnits/converterGUI/SearchBoxList.java | 320 ----- .../sevenUnits/converterGUI/SevenUnitsGUI.java | 1506 -------------------- .../java/sevenUnits/converterGUI/package-info.java | 24 - src/main/java/sevenUnitsGUI/Main.java | 34 + src/main/java/sevenUnitsGUI/Presenter.java | 2 +- src/main/java/sevenUnitsGUI/TabbedView.java | 76 +- src/main/java/sevenUnitsGUI/View.java | 8 + src/main/java/sevenUnitsGUI/ViewBot.java | 3 +- src/test/java/sevenUnitsGUI/PresenterTest.java | 4 + src/test/resources/test-settings.txt | 2 +- 18 files changed, 56 insertions(+), 2944 deletions(-) delete mode 100644 src/main/java/sevenUnits/converterGUI/DefaultPrefixRepetitionRule.java delete mode 100644 src/main/java/sevenUnits/converterGUI/DelegateListModel.java delete mode 100644 src/main/java/sevenUnits/converterGUI/FilterComparator.java delete mode 100644 src/main/java/sevenUnits/converterGUI/GridBagBuilder.java delete mode 100644 src/main/java/sevenUnits/converterGUI/MutablePredicate.java delete mode 100644 src/main/java/sevenUnits/converterGUI/SearchBoxList.java delete mode 100644 src/main/java/sevenUnits/converterGUI/SevenUnitsGUI.java delete mode 100644 src/main/java/sevenUnits/converterGUI/package-info.java create mode 100644 src/main/java/sevenUnitsGUI/Main.java diff --git a/README.org b/README.org index 2b4ffa0..f891bdc 100644 --- a/README.org +++ b/README.org @@ -1,4 +1,4 @@ -* 7Units v0.3.2 +* 7Units v0.4.0a1 (this project uses Semantic Versioning) ** What is it? This is a unit converter, which allows you to convert between different units, and includes a GUI which can read unit data from a file (using some unit math) and convert between units that you type in, and has a unit and prefix viewer to check the units that have been loaded in. diff --git a/build.gradle b/build.gradle index 4484e9d..8b90060 100644 --- a/build.gradle +++ b/build.gradle @@ -8,7 +8,7 @@ java { sourceCompatibility = JavaVersion.VERSION_11 } -mainClassName = "sevenUnits.converterGUI.SevenUnitsGUI" +mainClassName = "sevenUnitsGUI.Main" repositories { mavenCentral() diff --git a/src/main/java/sevenUnits/ProgramInfo.java b/src/main/java/sevenUnits/ProgramInfo.java index 6407d7c..f32d2c7 100644 --- a/src/main/java/sevenUnits/ProgramInfo.java +++ b/src/main/java/sevenUnits/ProgramInfo.java @@ -28,7 +28,7 @@ public final class ProgramInfo { /** The version number (0.4.0-alpha+dev) */ public static final SemanticVersionNumber VERSION = SemanticVersionNumber - .builder(0, 4, 0).preRelease("alpha").buildMetadata("dev").build(); + .preRelease(0, 4, 0, "alpha", 1); private ProgramInfo() { // this class is only for static variables, you shouldn't be able to diff --git a/src/main/java/sevenUnits/converterGUI/DefaultPrefixRepetitionRule.java b/src/main/java/sevenUnits/converterGUI/DefaultPrefixRepetitionRule.java deleted file mode 100644 index 6b6abf0..0000000 --- a/src/main/java/sevenUnits/converterGUI/DefaultPrefixRepetitionRule.java +++ /dev/null @@ -1,95 +0,0 @@ -/** - * @since 2020-08-26 - */ -package sevenUnits.converterGUI; - -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 - */ -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/sevenUnits/converterGUI/DelegateListModel.java b/src/main/java/sevenUnits/converterGUI/DelegateListModel.java deleted file mode 100644 index dd8cc97..0000000 --- a/src/main/java/sevenUnits/converterGUI/DelegateListModel.java +++ /dev/null @@ -1,242 +0,0 @@ -/** - * Copyright (C) 2018 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.converterGUI; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.Iterator; -import java.util.List; -import java.util.ListIterator; - -import javax.swing.AbstractListModel; - -/** - * A list model that delegates to a list. - *

    - * It is recommended to use the delegate methods in DelegateListModel instead of the delegated list's methods because - * the delegate methods handle updating the list. - *

    - * - * @author Adrien Hopkins - * @since 2019-01-14 - * @since v0.1.0 - */ -final class DelegateListModel extends AbstractListModel implements List { - /** - * @since 2019-01-14 - * @since v0.1.0 - */ - private static final long serialVersionUID = 8985494428224810045L; - - /** - * The list that this model is a delegate to. - * - * @since 2019-01-14 - * @since v0.1.0 - */ - private final List delegate; - - /** - * Creates an empty {@code DelegateListModel}. - * - * @since 2019-04-13 - */ - public DelegateListModel() { - this(new ArrayList<>()); - } - - /** - * Creates the {@code DelegateListModel}. - * - * @param delegate - * list to delegate - * @since 2019-01-14 - * @since v0.1.0 - */ - public DelegateListModel(final List delegate) { - this.delegate = delegate; - } - - @Override - public boolean add(final E element) { - final int index = this.delegate.size(); - final boolean success = this.delegate.add(element); - this.fireIntervalAdded(this, index, index); - return success; - } - - @Override - public void add(final int index, final E element) { - this.delegate.add(index, element); - this.fireIntervalAdded(this, index, index); - } - - @Override - public boolean addAll(final Collection c) { - boolean changed = false; - for (final E e : c) { - if (this.add(e)) { - changed = true; - } - } - return changed; - } - - @Override - public boolean addAll(final int index, final Collection c) { - for (final E e : c) { - this.add(index, e); - } - return !c.isEmpty(); // Since this is a list, it will always change if c has elements. - } - - @Override - public void clear() { - final int oldSize = this.delegate.size(); - this.delegate.clear(); - if (oldSize >= 1) { - this.fireIntervalRemoved(this, 0, oldSize - 1); - } - } - - @Override - public boolean contains(final Object elem) { - return this.delegate.contains(elem); - } - - @Override - public boolean containsAll(final Collection c) { - for (final Object e : c) { - if (!c.contains(e)) - return false; - } - return true; - } - - @Override - public E get(final int index) { - return this.delegate.get(index); - } - - @Override - public E getElementAt(final int index) { - return this.delegate.get(index); - } - - @Override - public int getSize() { - return this.delegate.size(); - } - - @Override - public int indexOf(final Object elem) { - return this.delegate.indexOf(elem); - } - - @Override - public boolean isEmpty() { - return this.delegate.isEmpty(); - } - - @Override - public Iterator iterator() { - return this.delegate.iterator(); - } - - @Override - public int lastIndexOf(final Object elem) { - return this.delegate.lastIndexOf(elem); - } - - @Override - public ListIterator listIterator() { - return this.delegate.listIterator(); - } - - @Override - public ListIterator listIterator(final int index) { - return this.delegate.listIterator(index); - } - - @Override - public E remove(final int index) { - final E returnValue = this.delegate.get(index); - this.delegate.remove(index); - this.fireIntervalRemoved(this, index, index); - return returnValue; - } - - @Override - public boolean remove(final Object o) { - final int index = this.delegate.indexOf(o); - final boolean returnValue = this.delegate.remove(o); - this.fireIntervalRemoved(this, index, index); - return returnValue; - } - - @Override - public boolean removeAll(final Collection c) { - boolean changed = false; - for (final Object e : c) { - if (this.remove(e)) { - changed = true; - } - } - return changed; - } - - @Override - public boolean retainAll(final Collection c) { - final int oldSize = this.size(); - final boolean returnValue = this.delegate.retainAll(c); - this.fireIntervalRemoved(this, this.size(), oldSize - 1); - return returnValue; - } - - @Override - public E set(final int index, final E element) { - final E returnValue = this.delegate.get(index); - this.delegate.set(index, element); - this.fireContentsChanged(this, index, index); - return returnValue; - } - - @Override - public int size() { - return this.delegate.size(); - } - - @Override - public List subList(final int fromIndex, final int toIndex) { - return this.delegate.subList(fromIndex, toIndex); - } - - @Override - public Object[] toArray() { - return this.delegate.toArray(); - } - - @Override - public T[] toArray(final T[] a) { - return this.delegate.toArray(a); - } - - @Override - public String toString() { - return this.delegate.toString(); - } -} diff --git a/src/main/java/sevenUnits/converterGUI/FilterComparator.java b/src/main/java/sevenUnits/converterGUI/FilterComparator.java deleted file mode 100644 index edd00e2..0000000 --- a/src/main/java/sevenUnits/converterGUI/FilterComparator.java +++ /dev/null @@ -1,129 +0,0 @@ -/** - * Copyright (C) 2018 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.converterGUI; - -import java.util.Comparator; -import java.util.Objects; - -/** - * A comparator that compares strings using a filter. - * - * @author Adrien Hopkins - * @since 2019-01-15 - * @since v0.1.0 - */ -final class FilterComparator implements Comparator { - /** - * The filter that the comparator is filtered by. - * - * @since 2019-01-15 - * @since v0.1.0 - */ - private final String filter; - /** - * The comparator to use if the arguments are otherwise equal. - * - * @since 2019-01-15 - * @since v0.1.0 - */ - private final Comparator comparator; - /** - * Whether or not the comparison is case-sensitive. - * - * @since 2019-04-14 - * @since v0.2.0 - */ - private final boolean caseSensitive; - - /** - * Creates the {@code FilterComparator}. - * - * @param filter - * @since 2019-01-15 - * @since v0.1.0 - */ - public FilterComparator(final String filter) { - this(filter, null); - } - - /** - * Creates the {@code FilterComparator}. - * - * @param filter - * string to filter by - * @param comparator - * comparator to fall back to if all else fails, null is compareTo. - * @throws NullPointerException - * if filter is null - * @since 2019-01-15 - * @since v0.1.0 - */ - public FilterComparator(final String filter, final Comparator comparator) { - this(filter, comparator, false); - } - - /** - * Creates the {@code FilterComparator}. - * - * @param filter - * string to filter by - * @param comparator - * comparator to fall back to if all else fails, null is compareTo. - * @param caseSensitive - * whether or not the comparator is case-sensitive - * @throws NullPointerException - * if filter is null - * @since 2019-04-14 - * @since v0.2.0 - */ - public FilterComparator(final String filter, final Comparator comparator, final boolean caseSensitive) { - this.filter = Objects.requireNonNull(filter, "filter must not be null."); - this.comparator = comparator; - this.caseSensitive = caseSensitive; - } - - @Override - public int compare(final String arg0, final String arg1) { - // if this is case insensitive, make them lowercase - final String str0, str1; - if (this.caseSensitive) { - str0 = arg0; - str1 = arg1; - } else { - str0 = arg0.toLowerCase(); - str1 = arg1.toLowerCase(); - } - - // elements that start with the filter always go first - if (str0.startsWith(this.filter) && !str1.startsWith(this.filter)) - return -1; - else if (!str0.startsWith(this.filter) && str1.startsWith(this.filter)) - return 1; - - // elements that contain the filter but don't start with them go next - if (str0.contains(this.filter) && !str1.contains(this.filter)) - return -1; - else if (!str0.contains(this.filter) && !str1.contains(this.filter)) - return 1; - - // other elements go last - if (this.comparator == null) - return str0.compareTo(str1); - else - return this.comparator.compare(str0, str1); - } -} diff --git a/src/main/java/sevenUnits/converterGUI/GridBagBuilder.java b/src/main/java/sevenUnits/converterGUI/GridBagBuilder.java deleted file mode 100644 index 0b71d78..0000000 --- a/src/main/java/sevenUnits/converterGUI/GridBagBuilder.java +++ /dev/null @@ -1,479 +0,0 @@ -/** - * Copyright (C) 2018 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.converterGUI; - -import java.awt.GridBagConstraints; -import java.awt.Insets; - -/** - * A builder for Java's {@link java.awt.GridBagConstraints} class. - * - * @author Adrien Hopkins - * @since 2018-11-30 - * @since v0.1.0 - */ -final class GridBagBuilder { - /** - * The built {@code GridBagConstraints}'s {@code gridx} property. - *

    - * Specifies the cell containing the leading edge of the component's display area, where the first cell in a row has - * gridx=0. The leading edge of a component's display area is its left edge for a horizontal, - * left-to-right container and its right edge for a horizontal, right-to-left container. The value - * RELATIVE specifies that the component be placed immediately following the component that was added - * to the container just before this component was added. - *

    - * The default value is RELATIVE. gridx should be a non-negative value. - * - * @serial - * @see #clone() - * @see java.awt.GridBagConstraints#gridy - * @see java.awt.ComponentOrientation - */ - private final int gridx; - - /** - * The built {@code GridBagConstraints}'s {@code gridy} property. - *

    - * Specifies the cell at the top of the component's display area, where the topmost cell has gridy=0. - * The value RELATIVE specifies that the component be placed just below the component that was added to - * the container just before this component was added. - *

    - * The default value is RELATIVE. gridy should be a non-negative value. - * - * @serial - * @see #clone() - * @see java.awt.GridBagConstraints#gridx - */ - private final int gridy; - - /** - * The built {@code GridBagConstraints}'s {@code gridwidth} property. - *

    - * Specifies the number of cells in a row for the component's display area. - *

    - * Use REMAINDER to specify that the component's display area will be from gridx to the - * last cell in the row. Use RELATIVE to specify that the component's display area will be from - * gridx to the next to the last one in its row. - *

    - * gridwidth should be non-negative and the default value is 1. - * - * @serial - * @see #clone() - * @see java.awt.GridBagConstraints#gridheight - */ - private final int gridwidth; - - /** - * The built {@code GridBagConstraints}'s {@code gridheight} property. - *

    - * Specifies the number of cells in a column for the component's display area. - *

    - * Use REMAINDER to specify that the component's display area will be from gridy to the - * last cell in the column. Use RELATIVE to specify that the component's display area will be from - * gridy to the next to the last one in its column. - *

    - * gridheight should be a non-negative value and the default value is 1. - * - * @serial - * @see #clone() - * @see java.awt.GridBagConstraints#gridwidth - */ - private final int gridheight; - - /** - * The built {@code GridBagConstraints}'s {@code weightx} property. - *

    - * Specifies how to distribute extra horizontal space. - *

    - * The grid bag layout manager calculates the weight of a column to be the maximum weightx of all the - * components in a column. If the resulting layout is smaller horizontally than the area it needs to fill, the extra - * space is distributed to each column in proportion to its weight. A column that has a weight of zero receives no - * extra space. - *

    - * If all the weights are zero, all the extra space appears between the grids of the cell and the left and right - * edges. - *

    - * The default value of this field is 0. weightx should be a non-negative value. - * - * @serial - * @see #clone() - * @see java.awt.GridBagConstraints#weighty - */ - private double weightx; - - /** - * The built {@code GridBagConstraints}'s {@code weighty} property. - *

    - * Specifies how to distribute extra vertical space. - *

    - * The grid bag layout manager calculates the weight of a row to be the maximum weighty of all the - * components in a row. If the resulting layout is smaller vertically than the area it needs to fill, the extra - * space is distributed to each row in proportion to its weight. A row that has a weight of zero receives no extra - * space. - *

    - * If all the weights are zero, all the extra space appears between the grids of the cell and the top and bottom - * edges. - *

    - * The default value of this field is 0. weighty should be a non-negative value. - * - * @serial - * @see #clone() - * @see java.awt.GridBagConstraints#weightx - */ - private double weighty; - - /** - * The built {@code GridBagConstraints}'s {@code anchor} property. - *

    - * This field is used when the component is smaller than its display area. It determines where, within the display - * area, to place the component. - *

    - * There are three kinds of possible values: orientation relative, baseline relative and absolute. Orientation - * relative values are interpreted relative to the container's component orientation property, baseline relative - * values are interpreted relative to the baseline and absolute values are not. The absolute values are: - * CENTER, NORTH, NORTHEAST, EAST, SOUTHEAST, - * SOUTH, SOUTHWEST, WEST, and NORTHWEST. The orientation - * relative values are: PAGE_START, PAGE_END, LINE_START, - * LINE_END, FIRST_LINE_START, FIRST_LINE_END, LAST_LINE_START - * and LAST_LINE_END. The baseline relative values are: BASELINE, - * BASELINE_LEADING, BASELINE_TRAILING, ABOVE_BASELINE, - * ABOVE_BASELINE_LEADING, ABOVE_BASELINE_TRAILING, BELOW_BASELINE, - * BELOW_BASELINE_LEADING, and BELOW_BASELINE_TRAILING. The default value is - * CENTER. - * - * @serial - * @see #clone() - * @see java.awt.ComponentOrientation - */ - private int anchor; - - /** - * The built {@code GridBagConstraints}'s {@code fill} property. - *

    - * This field is used when the component's display area is larger than the component's requested size. It determines - * whether to resize the component, and if so, how. - *

    - * The following values are valid for fill: - * - *

      - *
    • NONE: Do not resize the component. - *
    • HORIZONTAL: Make the component wide enough to fill its display area horizontally, but do not - * change its height. - *
    • VERTICAL: Make the component tall enough to fill its display area vertically, but do not change - * its width. - *
    • BOTH: Make the component fill its display area entirely. - *
    - *

    - * The default value is NONE. - * - * @serial - * @see #clone() - */ - private int fill; - - /** - * The built {@code GridBagConstraints}'s {@code insets} property. - *

    - * This field specifies the external padding of the component, the minimum amount of space between the component and - * the edges of its display area. - *

    - * The default value is new Insets(0, 0, 0, 0). - * - * @serial - * @see #clone() - */ - private Insets insets; - - /** - * The built {@code GridBagConstraints}'s {@code ipadx} property. - *

    - * This field specifies the internal padding of the component, how much space to add to the minimum width of the - * component. The width of the component is at least its minimum width plus ipadx pixels. - *

    - * The default value is 0. - * - * @serial - * @see #clone() - * @see java.awt.GridBagConstraints#ipady - */ - private int ipadx; - - /** - * The built {@code GridBagConstraints}'s {@code ipady} property. - *

    - * This field specifies the internal padding, that is, how much space to add to the minimum height of the component. - * The height of the component is at least its minimum height plus ipady pixels. - *

    - * The default value is 0. - * - * @serial - * @see #clone() - * @see java.awt.GridBagConstraints#ipadx - */ - private int ipady; - - /** - * @param gridx - * x position - * @param gridy - * y position - * @since 2018-11-30 - * @since v0.1.0 - */ - public GridBagBuilder(final int gridx, final int gridy) { - this(gridx, gridy, 1, 1); - } - - /** - * @param gridx - * x position - * @param gridy - * y position - * @param gridwidth - * number of cells occupied horizontally - * @param gridheight - * number of cells occupied vertically - * @since 2018-11-30 - * @since v0.1.0 - */ - public GridBagBuilder(final int gridx, final int gridy, final int gridwidth, final int gridheight) { - this(gridx, gridy, gridwidth, gridheight, 0.0, 0.0, GridBagConstraints.CENTER, GridBagConstraints.NONE, - new Insets(0, 0, 0, 0), 0, 0); - } - - /** - * @param gridx - * x position - * @param gridy - * y position - * @param gridwidth - * number of cells occupied horizontally - * @param gridheight - * number of cells occupied vertically - * @param weightx - * @param weighty - * @param anchor - * @param fill - * @param insets - * @param ipadx - * @param ipady - * @since 2018-11-30 - * @since v0.1.0 - */ - private GridBagBuilder(final int gridx, final int gridy, final int gridwidth, final int gridheight, - final double weightx, final double weighty, final int anchor, final int fill, final Insets insets, - final int ipadx, final int ipady) { - super(); - this.gridx = gridx; - this.gridy = gridy; - this.gridwidth = gridwidth; - this.gridheight = gridheight; - this.weightx = weightx; - this.weighty = weighty; - this.anchor = anchor; - this.fill = fill; - this.insets = (Insets) insets.clone(); - this.ipadx = ipadx; - this.ipady = ipady; - } - - /** - * @return {@code GridBagConstraints} created by this builder - * @since 2018-11-30 - * @since v0.1.0 - */ - public GridBagConstraints build() { - return new GridBagConstraints(this.gridx, this.gridy, this.gridwidth, this.gridheight, this.weightx, - this.weighty, this.anchor, this.fill, this.insets, this.ipadx, this.ipady); - } - - /** - * @return anchor - * @since 2018-11-30 - * @since v0.1.0 - */ - public int getAnchor() { - return this.anchor; - } - - /** - * @return fill - * @since 2018-11-30 - * @since v0.1.0 - */ - public int getFill() { - return this.fill; - } - - /** - * @return gridheight - * @since 2018-11-30 - * @since v0.1.0 - */ - public int getGridheight() { - return this.gridheight; - } - - /** - * @return gridwidth - * @since 2018-11-30 - * @since v0.1.0 - */ - public int getGridwidth() { - return this.gridwidth; - } - - /** - * @return gridx - * @since 2018-11-30 - * @since v0.1.0 - */ - public int getGridx() { - return this.gridx; - } - - /** - * @return gridy - * @since 2018-11-30 - * @since v0.1.0 - */ - public int getGridy() { - return this.gridy; - } - - /** - * @return insets - * @since 2018-11-30 - * @since v0.1.0 - */ - public Insets getInsets() { - return this.insets; - } - - /** - * @return ipadx - * @since 2018-11-30 - * @since v0.1.0 - */ - public int getIpadx() { - return this.ipadx; - } - - /** - * @return ipady - * @since 2018-11-30 - * @since v0.1.0 - */ - public int getIpady() { - return this.ipady; - } - - /** - * @return weightx - * @since 2018-11-30 - * @since v0.1.0 - */ - public double getWeightx() { - return this.weightx; - } - - /** - * @return weighty - * @since 2018-11-30 - * @since v0.1.0 - */ - public double getWeighty() { - return this.weighty; - } - - /** - * @param anchor - * anchor to set - * @since 2018-11-30 - * @since v0.1.0 - */ - public GridBagBuilder setAnchor(final int anchor) { - this.anchor = anchor; - return this; - } - - /** - * @param fill - * fill to set - * @since 2018-11-30 - * @since v0.1.0 - */ - public GridBagBuilder setFill(final int fill) { - this.fill = fill; - return this; - } - - /** - * @param insets - * insets to set - * @since 2018-11-30 - * @since v0.1.0 - */ - public GridBagBuilder setInsets(final Insets insets) { - this.insets = insets; - return this; - } - - /** - * @param ipadx - * ipadx to set - * @since 2018-11-30 - * @since v0.1.0 - */ - public GridBagBuilder setIpadx(final int ipadx) { - this.ipadx = ipadx; - return this; - } - - /** - * @param ipady - * ipady to set - * @since 2018-11-30 - * @since v0.1.0 - */ - public GridBagBuilder setIpady(final int ipady) { - this.ipady = ipady; - return this; - } - - /** - * @param weightx - * weightx to set - * @since 2018-11-30 - * @since v0.1.0 - */ - public GridBagBuilder setWeightx(final double weightx) { - this.weightx = weightx; - return this; - } - - /** - * @param weighty - * weighty to set - * @since 2018-11-30 - * @since v0.1.0 - */ - public GridBagBuilder setWeighty(final double weighty) { - this.weighty = weighty; - return this; - } -} diff --git a/src/main/java/sevenUnits/converterGUI/MutablePredicate.java b/src/main/java/sevenUnits/converterGUI/MutablePredicate.java deleted file mode 100644 index ae6b7a1..0000000 --- a/src/main/java/sevenUnits/converterGUI/MutablePredicate.java +++ /dev/null @@ -1,70 +0,0 @@ -/** - * Copyright (C) 2019 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.converterGUI; - -import java.util.function.Predicate; - -/** - * A container for a predicate, which can be changed later. - * - * @author Adrien Hopkins - * @since 2019-04-13 - * @since v0.2.0 - */ -final class MutablePredicate implements Predicate { - /** - * The predicate stored in this {@code MutablePredicate} - * - * @since 2019-04-13 - * @since v0.2.0 - */ - private Predicate predicate; - - /** - * Creates the {@code MutablePredicate}. - * - * @since 2019-04-13 - * @since v0.2.0 - */ - public MutablePredicate(final Predicate predicate) { - this.predicate = predicate; - } - - /** - * @return predicate - * @since 2019-04-13 - * @since v0.2.0 - */ - public final Predicate getPredicate() { - return this.predicate; - } - - /** - * @param predicate - * new value of predicate - * @since 2019-04-13 - * @since v0.2.0 - */ - public final void setPredicate(final Predicate predicate) { - this.predicate = predicate; - } - - @Override - public boolean test(final T t) { - return this.predicate.test(t); - } -} diff --git a/src/main/java/sevenUnits/converterGUI/SearchBoxList.java b/src/main/java/sevenUnits/converterGUI/SearchBoxList.java deleted file mode 100644 index 2aa9fce..0000000 --- a/src/main/java/sevenUnits/converterGUI/SearchBoxList.java +++ /dev/null @@ -1,320 +0,0 @@ -/** - * Copyright (C) 2019 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.converterGUI; - -import java.awt.BorderLayout; -import java.awt.Color; -import java.awt.event.FocusEvent; -import java.awt.event.FocusListener; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Comparator; -import java.util.function.Predicate; - -import javax.swing.JList; -import javax.swing.JPanel; -import javax.swing.JScrollPane; -import javax.swing.JTextField; - -/** - * @author Adrien Hopkins - * @since 2019-04-13 - * @since v0.2.0 - */ -final class SearchBoxList extends JPanel { - - /** - * @since 2019-04-13 - * @since v0.2.0 - */ - private static final long serialVersionUID = 6226930279415983433L; - - /** - * The text to place in an empty search box. - * - * @since 2019-04-13 - * @since v0.2.0 - */ - private static final String EMPTY_TEXT = "Search..."; - - /** - * The color to use for an empty foreground. - * - * @since 2019-04-13 - * @since v0.2.0 - */ - private static final Color EMPTY_FOREGROUND = new Color(192, 192, 192); - - // the components - private final Collection itemsToFilter; - private final DelegateListModel listModel; - private final JTextField searchBox; - private final JList searchItems; - - private boolean searchBoxEmpty = true; - - // I need to do this because, for some reason, Swing is auto-focusing my - // search box without triggering a focus - // event. - private boolean searchBoxFocused = false; - - private Predicate customSearchFilter = o -> true; - private final Comparator defaultOrdering; - private final boolean caseSensitive; - - /** - * Creates the {@code SearchBoxList}. - * - * @param itemsToFilter items to put in the list - * @since 2019-04-14 - */ - public SearchBoxList(final Collection itemsToFilter) { - this(itemsToFilter, null, false); - } - - /** - * Creates the {@code SearchBoxList}. - * - * @param itemsToFilter items to put in the list - * @param defaultOrdering default ordering of items after filtration - * (null=Comparable) - * @param caseSensitive whether or not the filtration is case-sensitive - * - * @since 2019-04-13 - * @since v0.2.0 - */ - public SearchBoxList(final Collection itemsToFilter, - final Comparator defaultOrdering, - final boolean caseSensitive) { - super(new BorderLayout(), true); - this.itemsToFilter = new ArrayList<>(itemsToFilter); - this.defaultOrdering = defaultOrdering; - this.caseSensitive = caseSensitive; - - // create the components - this.listModel = new DelegateListModel<>(new ArrayList<>(itemsToFilter)); - this.searchItems = new JList<>(this.listModel); - - this.searchBox = new JTextField(EMPTY_TEXT); - this.searchBox.setForeground(EMPTY_FOREGROUND); - - // add them to the panel - this.add(this.searchBox, BorderLayout.PAGE_START); - this.add(new JScrollPane(this.searchItems), BorderLayout.CENTER); - - // set up the search box - this.searchBox.addFocusListener(new FocusListener() { - @Override - public void focusGained(final FocusEvent e) { - SearchBoxList.this.searchBoxFocusGained(e); - } - - @Override - public void focusLost(final FocusEvent e) { - SearchBoxList.this.searchBoxFocusLost(e); - } - }); - - this.searchBox.addCaretListener(e -> this.searchBoxTextChanged()); - this.searchBoxEmpty = true; - } - - /** - * Adds an additional filter for searching. - * - * @param filter filter to add. - * @since 2019-04-13 - * @since v0.2.0 - */ - public void addSearchFilter(final Predicate filter) { - this.customSearchFilter = this.customSearchFilter.and(filter); - } - - /** - * Resets the search filter. - * - * @since 2019-04-13 - * @since v0.2.0 - */ - public void clearSearchFilters() { - this.customSearchFilter = o -> true; - } - - /** - * @return this component's search box component - * @since 2019-04-14 - * @since v0.2.0 - */ - public final JTextField getSearchBox() { - return this.searchBox; - } - - /** - * @param searchText text to search for - * @return a filter that filters out that text, based on this list's case - * sensitive setting - * @since 2019-04-14 - * @since v0.2.0 - */ - private Predicate getSearchFilter(final String searchText) { - if (this.caseSensitive) - return string -> string.contains(searchText); - else - return string -> string.toLowerCase() - .contains(searchText.toLowerCase()); - } - - /** - * @return this component's list component - * @since 2019-04-14 - * @since v0.2.0 - */ - public final JList getSearchList() { - return this.searchItems; - } - - /** - * @return index selected in item list - * @since 2019-04-14 - * @since v0.2.0 - */ - public int getSelectedIndex() { - return this.searchItems.getSelectedIndex(); - } - - /** - * @return value selected in item list - * @since 2019-04-13 - * @since v0.2.0 - */ - public String getSelectedValue() { - return this.searchItems.getSelectedValue(); - } - - /** - * Re-applies the filters. - * - * @since 2019-04-13 - * @since v0.2.0 - */ - public void reapplyFilter() { - final String searchText = this.searchBoxEmpty ? "" - : this.searchBox.getText(); - final FilterComparator comparator = new FilterComparator(searchText, - this.defaultOrdering, this.caseSensitive); - final Predicate searchFilter = this.getSearchFilter(searchText); - - this.listModel.clear(); - this.itemsToFilter.forEach(string -> { - if (searchFilter.test(string)) { - this.listModel.add(string); - } - }); - - // applies the custom filters - this.listModel.removeIf(this.customSearchFilter.negate()); - - // sorts the remaining items - this.listModel.sort(comparator); - } - - /** - * Runs whenever the search box gains focus. - * - * @param e focus event - * @since 2019-04-13 - * @since v0.2.0 - */ - private void searchBoxFocusGained(final FocusEvent e) { - this.searchBoxFocused = true; - if (this.searchBoxEmpty) { - this.searchBox.setText(""); - this.searchBox.setForeground(Color.BLACK); - } - } - - /** - * Runs whenever the search box loses focus. - * - * @param e focus event - * @since 2019-04-13 - * @since v0.2.0 - */ - private void searchBoxFocusLost(final FocusEvent e) { - this.searchBoxFocused = false; - if (this.searchBoxEmpty) { - this.searchBox.setText(EMPTY_TEXT); - this.searchBox.setForeground(EMPTY_FOREGROUND); - } - } - - /** - * Runs whenever the text in the search box is changed. - *

    - * Reapplies the search filter, and custom filters. - *

    - * - * @since 2019-04-14 - * @since v0.2.0 - */ - private void searchBoxTextChanged() { - if (this.searchBoxFocused) { - this.searchBoxEmpty = this.searchBox.getText().equals(""); - } - final String searchText = this.searchBoxEmpty ? "" - : this.searchBox.getText(); - final FilterComparator comparator = new FilterComparator(searchText, - this.defaultOrdering, this.caseSensitive); - final Predicate searchFilter = this.getSearchFilter(searchText); - - // initialize list with items that match the filter then sort - this.listModel.clear(); - this.itemsToFilter.forEach(string -> { - if (searchFilter.test(string)) { - this.listModel.add(string); - } - }); - - // applies the custom filters - this.listModel.removeIf(this.customSearchFilter.negate()); - - // sorts the remaining items - this.listModel.sort(comparator); - } - - /** - * Resets the search box list's contents to the provided items, removing any - * old items - * - * @param newItems new items to put in list - * @since 2021-05-22 - */ - public void setItems(Collection newItems) { - this.itemsToFilter.clear(); - this.itemsToFilter.addAll(newItems); - this.reapplyFilter(); - } - - /** - * Manually updates the search box's item list. - * - * @since 2020-08-27 - */ - public void updateList() { - this.searchBoxTextChanged(); - } -} diff --git a/src/main/java/sevenUnits/converterGUI/SevenUnitsGUI.java b/src/main/java/sevenUnits/converterGUI/SevenUnitsGUI.java deleted file mode 100644 index 309bdb9..0000000 --- a/src/main/java/sevenUnits/converterGUI/SevenUnitsGUI.java +++ /dev/null @@ -1,1506 +0,0 @@ -/** - * Copyright (C) 2018-2021 Adrien Hopkins - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package sevenUnits.converterGUI; - -import java.awt.BorderLayout; -import java.awt.GridBagConstraints; -import java.awt.GridBagLayout; -import java.awt.GridLayout; -import java.awt.event.KeyEvent; -import java.io.BufferedWriter; -import java.io.IOException; -import java.io.InputStream; -import java.math.BigDecimal; -import java.math.MathContext; -import java.math.RoundingMode; -import java.nio.file.Files; -import java.nio.file.Path; -import java.text.DecimalFormat; -import java.text.NumberFormat; -import java.text.ParseException; -import java.util.ArrayList; -import java.util.Comparator; -import java.util.HashSet; -import java.util.List; -import java.util.NoSuchElementException; -import java.util.Scanner; -import java.util.Set; -import java.util.function.Predicate; -import java.util.stream.Collectors; - -import javax.swing.BorderFactory; -import javax.swing.BoxLayout; -import javax.swing.ButtonGroup; -import javax.swing.JButton; -import javax.swing.JCheckBox; -import javax.swing.JComboBox; -import javax.swing.JFormattedTextField; -import javax.swing.JFrame; -import javax.swing.JLabel; -import javax.swing.JOptionPane; -import javax.swing.JPanel; -import javax.swing.JRadioButton; -import javax.swing.JScrollPane; -import javax.swing.JSlider; -import javax.swing.JTabbedPane; -import javax.swing.JTextArea; -import javax.swing.JTextField; -import javax.swing.UIManager; -import javax.swing.UnsupportedLookAndFeelException; -import javax.swing.WindowConstants; -import javax.swing.border.TitledBorder; - -import sevenUnits.ProgramInfo; -import sevenUnits.unit.BaseDimension; -import sevenUnits.unit.BritishImperial; -import sevenUnits.unit.LinearUnit; -import sevenUnits.unit.LinearUnitValue; -import sevenUnits.unit.Metric; -import sevenUnits.unit.Unit; -import sevenUnits.unit.UnitDatabase; -import sevenUnits.unit.UnitPrefix; -import sevenUnits.unit.UnitValue; -import sevenUnits.utils.ConditionalExistenceCollections; -import sevenUnits.utils.NameSymbol; -import sevenUnits.utils.ObjectProduct; - -/** - * @author Adrien Hopkins - * @since 2018-12-27 - * @since v0.1.0 - */ -final class SevenUnitsGUI { - /** - * A tab in the View. - */ - private enum Pane { - UNIT_CONVERTER, EXPRESSION_CONVERTER, UNIT_VIEWER, PREFIX_VIEWER, ABOUT, - SETTINGS; - } - - private static class Presenter { - /** The default place where settings are stored. */ - private static final String DEFAULT_SETTINGS_FILEPATH = "settings.txt"; - /** The default place where units are stored. */ - private static final String DEFAULT_UNITS_FILEPATH = "/unitsfile.txt"; - /** The default place where dimensions are stored. */ - private static final String DEFAULT_DIMENSIONS_FILEPATH = "/dimensionfile.txt"; - /** The default place where exceptions are stored. */ - private static final String DEFAULT_EXCEPTIONS_FILEPATH = "/metric_exceptions.txt"; - - /** - * Adds default units and dimensions to a database. - * - * @param database database to add to - * @since 2019-04-14 - * @since v0.2.0 - */ - private static void addDefaults(final UnitDatabase database) { - database.addUnit("metre", Metric.METRE); - database.addUnit("kilogram", Metric.KILOGRAM); - database.addUnit("gram", Metric.KILOGRAM.dividedBy(1000)); - database.addUnit("second", Metric.SECOND); - database.addUnit("ampere", Metric.AMPERE); - database.addUnit("kelvin", Metric.KELVIN); - database.addUnit("mole", Metric.MOLE); - database.addUnit("candela", Metric.CANDELA); - database.addUnit("bit", Metric.BIT); - database.addUnit("unit", Metric.ONE); - // nonlinear units - must be loaded manually - database.addUnit("tempCelsius", Metric.CELSIUS); - database.addUnit("tempFahrenheit", BritishImperial.FAHRENHEIT); - - // load initial dimensions - database.addDimension("LENGTH", Metric.Dimensions.LENGTH); - database.addDimension("MASS", Metric.Dimensions.MASS); - database.addDimension("TIME", Metric.Dimensions.TIME); - database.addDimension("TEMPERATURE", Metric.Dimensions.TEMPERATURE); - } - - /** - * Gets the text of a resource file as a set of strings (each one is one - * line of the text). - * - * @param filename filename to get resource from - * @return contents of file - * @since 2021-03-27 - */ - public static final List getLinesFromResource(String filename) { - final List lines = new ArrayList<>(); - - try (InputStream stream = inputStream(filename); - Scanner scanner = new Scanner(stream)) { - while (scanner.hasNextLine()) { - lines.add(scanner.nextLine()); - } - } catch (final IOException e) { - throw new AssertionError( - "Error occurred while loading file " + filename, e); - } - - return lines; - } - - /** - * Gets an input stream for a resource file. - * - * @param filepath file to use as resource - * @return obtained Path - * @since 2021-03-27 - */ - private static final InputStream inputStream(String filepath) { - return SevenUnitsGUI.class.getResourceAsStream(filepath); - } - - /** - * @return {@code line} with any comments removed. - * @since 2021-03-13 - */ - private static final String withoutComments(String line) { - final int index = line.indexOf('#'); - return index == -1 ? line : line.substring(index); - } - - /** The presenter's associated view. */ - private final View view; - - /** The units known by the program. */ - private final UnitDatabase database; - - /** The names of all of the units */ - private final List unitNames; - - /** The names of all of the prefixes */ - private final List prefixNames; - - /** The names of all of the dimensions */ - private final List dimensionNames; - - /** Unit names that are ignored by the metric-only/imperial-only filter */ - private final Set metricExceptions; - - private final Comparator prefixNameComparator; - - /** A boolean remembering whether or not one-way conversion is on */ - private boolean oneWay = true; - /** The prefix rule */ - private DefaultPrefixRepetitionRule prefixRule = null; - - // conditions for existence of From and To entries - // used for one-way conversion - private final MutablePredicate fromExistenceCondition = new MutablePredicate<>( - s -> true); - - private final MutablePredicate toExistenceCondition = new MutablePredicate<>( - s -> true); - - /* - * Rounding-related settings. I am using my own system, and not - * MathContext, because MathContext does not support decimal place based - * or scientific rounding, only significant digit based rounding. - */ - private int precision = 6; - - private RoundingType roundingType = RoundingType.SIGNIFICANT_DIGITS; - - // The "include duplicate units" setting - private boolean includeDuplicateUnits = true; - - /** - * Creates the presenter. - * - * @param view presenter's associated view - * @since 2018-12-27 - * @since v0.1.0 - */ - Presenter(final View view) { - this.view = view; - - // load initial units - this.database = new UnitDatabase( - DefaultPrefixRepetitionRule.NO_RESTRICTION); - Presenter.addDefaults(this.database); - - // load units and prefixes - try (final InputStream units = inputStream(DEFAULT_UNITS_FILEPATH)) { - this.database.loadUnitsFromStream(units); - } catch (final IOException e) { - throw new AssertionError("Loading of unitsfile.txt failed.", e); - } - - // load dimensions - try (final InputStream dimensions = inputStream( - DEFAULT_DIMENSIONS_FILEPATH)) { - this.database.loadDimensionsFromStream(dimensions); - } catch (final IOException e) { - throw new AssertionError("Loading of dimensionfile.txt failed.", e); - } - - // load metric exceptions - try { - this.metricExceptions = new HashSet<>(); - try (InputStream exceptions = inputStream( - DEFAULT_EXCEPTIONS_FILEPATH); - Scanner scanner = new Scanner(exceptions)) { - while (scanner.hasNextLine()) { - final String line = Presenter - .withoutComments(scanner.nextLine()); - if (!line.isBlank()) { - this.metricExceptions.add(line); - } - } - } - } catch (final IOException e) { - throw new AssertionError("Loading of metric_exceptions.txt failed.", - e); - } - - // load settings - requires database to exist - if (Files.exists(this.getSettingsFile())) { - this.loadSettings(); - } - - // a comparator that can be used to compare prefix names - // any name that does not exist is less than a name that does. - // otherwise, they are compared by value - this.prefixNameComparator = (o1, o2) -> { - if (!Presenter.this.database.containsPrefixName(o1)) - return -1; - else if (!Presenter.this.database.containsPrefixName(o2)) - return 1; - - final UnitPrefix p1 = Presenter.this.database.getPrefix(o1); - final UnitPrefix p2 = Presenter.this.database.getPrefix(o2); - - if (p1.getMultiplier() < p2.getMultiplier()) - return -1; - else if (p1.getMultiplier() > p2.getMultiplier()) - return 1; - - return o1.compareTo(o2); - }; - - this.unitNames = new ArrayList<>( - this.database.unitMapPrefixless(true).keySet()); - this.unitNames.sort(null); // sorts it using Comparable - - this.prefixNames = new ArrayList<>( - this.database.prefixMap(true).keySet()); - this.prefixNames.sort(this.prefixNameComparator); // sorts it using my - // comparator - - this.dimensionNames = new DelegateListModel<>( - new ArrayList<>(this.database.dimensionMap().keySet())); - this.dimensionNames.sort(null); // sorts it using Comparable - - // a Predicate that returns true iff the argument is a full base unit - final Predicate isFullBase = unit -> unit instanceof LinearUnit - && ((LinearUnit) unit).isBase(); - - // print out unit counts - System.out.printf( - "Successfully loaded %d units with %d unit names (%d base units).%n", - this.database.unitMapPrefixless(false).size(), - this.database.unitMapPrefixless(true).size(), - this.database.unitMapPrefixless(false).values().stream() - .filter(isFullBase).count()); - } - - /** - * Converts in the dimension-based converter - * - * @since 2019-04-13 - * @since v0.2.0 - */ - public final void convertDimensionBased() { - final String fromSelection = this.view.getFromSelection(); - if (fromSelection == null) { - this.view.showErrorDialog("Error", - "No unit selected in From field"); - return; - } - final String toSelection = this.view.getToSelection(); - if (toSelection == null) { - this.view.showErrorDialog("Error", "No unit selected in To field"); - return; - } - - final Unit from = this.database.getUnit(fromSelection); - final Unit to = this.database.getUnit(toSelection) - .withName(NameSymbol.ofName(toSelection)); - - final UnitValue beforeValue; - try { - beforeValue = UnitValue.of(from, - this.view.getDimensionConverterInput()); - } catch (final ParseException e) { - this.view.showErrorDialog("Error", - "Error in parsing: " + e.getMessage()); - return; - } - final UnitValue value = beforeValue.convertTo(to); - - final String output = this.getRoundedString(value); - - this.view.setDimensionConverterOutputText( - String.format("%s = %s", beforeValue, output)); - } - - /** - * Runs whenever the convert button is pressed. - * - *

    - * Reads and parses a unit expression from the from and to boxes, then - * converts {@code from} to {@code to}. Any errors are shown in - * JOptionPanes. - *

    - * - * @since 2019-01-26 - * @since v0.1.0 - */ - public final void convertExpressions() { - final String fromUnitString = this.view.getFromText(); - final String toUnitString = this.view.getToText(); - - if (fromUnitString.isEmpty()) { - this.view.showErrorDialog("Parse Error", - "Please enter a unit expression in the From: box."); - return; - } - if (toUnitString.isEmpty()) { - this.view.showErrorDialog("Parse Error", - "Please enter a unit expression in the To: box."); - return; - } - - final LinearUnitValue from; - final Unit to; - try { - from = this.database.evaluateUnitExpression(fromUnitString); - } catch (final IllegalArgumentException | NoSuchElementException e) { - this.view.showErrorDialog("Parse Error", - "Could not recognize text in From entry: " + e.getMessage()); - return; - } - try { - to = this.database.getUnitFromExpression(toUnitString); - } catch (final IllegalArgumentException | NoSuchElementException e) { - this.view.showErrorDialog("Parse Error", - "Could not recognize text in To entry: " + e.getMessage()); - return; - } - - if (to instanceof LinearUnit) { - // convert to LinearUnitValue - final LinearUnitValue from2; - final LinearUnit to2 = ((LinearUnit) to) - .withName(NameSymbol.ofName(toUnitString)); - final boolean useSlash; - - if (from.canConvertTo(to2)) { - from2 = from; - useSlash = false; - } else if (LinearUnitValue.ONE.dividedBy(from).canConvertTo(to2)) { - from2 = LinearUnitValue.ONE.dividedBy(from); - useSlash = true; - } else { - // if I can't convert, leave - this.view.showErrorDialog("Conversion Error", - String.format("Cannot convert between %s and %s", - fromUnitString, toUnitString)); - return; - } - - final LinearUnitValue converted = from2.convertTo(to2); - this.view.setExpressionConverterOutputText((useSlash ? "1 / " : "") - + String.format("%s = %s", fromUnitString, - this.getRoundedString(converted, false))); - return; - } else { - // convert to UnitValue - final UnitValue from2 = from.asUnitValue(); - if (from2.canConvertTo(to)) { - final UnitValue converted = from2.convertTo(to); - - this.view - .setExpressionConverterOutputText(String.format("%s = %s", - fromUnitString, this.getRoundedString(converted))); - } else { - // if I can't convert, leave - this.view.showErrorDialog("Conversion Error", - String.format("Cannot convert between %s and %s", - fromUnitString, toUnitString)); - } - } - } - - /** - * @return a list of all of the unit dimensions - * @since 2019-04-13 - * @since v0.2.0 - */ - public final List dimensionNameList() { - return this.dimensionNames; - } - - /** - * @return a list of all the entries in the dimension-based converter's - * From box - * @since 2020-08-27 - */ - public final Set fromEntries() { - return ConditionalExistenceCollections.conditionalExistenceSet( - this.unitNameSet(), this.fromExistenceCondition); - } - - /** - * @return a comparator to compare prefix names - * @since 2019-04-14 - * @since v0.2.0 - */ - public final Comparator getPrefixNameComparator() { - return this.prefixNameComparator; - } - - /** - * Like {@link LinearUnitValue#toString(boolean)}, but obeys this unit - * converter's rounding settings. - * - * @since 2020-08-04 - */ - private final String getRoundedString(final LinearUnitValue value, - boolean showUncertainty) { - switch (this.roundingType) { - case DECIMAL_PLACES: - case SIGNIFICANT_DIGITS: - return this.getRoundedString(value.asUnitValue()); - case SCIENTIFIC: - return value.toString(showUncertainty, RoundingMode.HALF_EVEN); - default: - throw new AssertionError("Invalid switch condition."); - } - } - - /** - * Like {@link UnitValue#toString()}, but obeys this unit converter's - * rounding settings. - * - * @since 2020-08-04 - */ - private final String getRoundedString(final UnitValue value) { - final BigDecimal unrounded = new BigDecimal(value.getValue()); - final BigDecimal rounded; - int precision = this.precision; - - switch (this.roundingType) { - case DECIMAL_PLACES: - rounded = unrounded.setScale(precision, RoundingMode.HALF_EVEN); - break; - case SCIENTIFIC: - precision = 12; - //$FALL-THROUGH$ - case SIGNIFICANT_DIGITS: - rounded = unrounded - .round(new MathContext(precision, RoundingMode.HALF_EVEN)); - break; - default: - throw new AssertionError("Invalid switch condition."); - } - - String output = rounded.toString(); - - // remove trailing zeroes - if (output.contains(".")) { - while (output.endsWith("0")) { - output = output.substring(0, output.length() - 1); - } - if (output.endsWith(".")) { - output = output.substring(0, output.length() - 1); - } - } - - return output + " " + value.getUnit().getPrimaryName().get(); - } - - /** - * @return The file where settings are stored; - * @since 2020-12-11 - */ - private final Path getSettingsFile() { - return Path.of(DEFAULT_SETTINGS_FILEPATH); - } - - /** - * Loads settings from the settings file. - * - * @since 2021-02-17 - */ - public final void loadSettings() { - try { - // read file line by line - final int lineNum = 0; - for (final String line : Files - .readAllLines(this.getSettingsFile())) { - final int equalsIndex = line.indexOf('='); - if (equalsIndex == -1) - throw new IllegalStateException( - "Settings file is malformed at line " + lineNum); - - final String param = line.substring(0, equalsIndex); - final String value = line.substring(equalsIndex + 1); - - switch (param) { - // set manually to avoid the unnecessary saving of the non-manual - // methods - case "precision": - this.precision = Integer.valueOf(value); - break; - case "rounding_type": - this.roundingType = RoundingType.valueOf(value); - break; - case "prefix_rule": - this.prefixRule = DefaultPrefixRepetitionRule.valueOf(value); - this.database.setPrefixRepetitionRule(this.prefixRule); - break; - case "one_way": - this.oneWay = Boolean.valueOf(value); - if (this.oneWay) { - this.fromExistenceCondition.setPredicate( - unitName -> this.metricExceptions.contains(unitName) - || !this.database.getUnit(unitName) - .isMetric()); - this.toExistenceCondition.setPredicate( - unitName -> this.metricExceptions.contains(unitName) - || this.database.getUnit(unitName).isMetric()); - } else { - this.fromExistenceCondition.setPredicate(unitName -> true); - this.toExistenceCondition.setPredicate(unitName -> true); - } - break; - case "include_duplicates": - this.includeDuplicateUnits = Boolean.valueOf(value); - if (this.view.presenter != null) { - this.view.update(); - } - break; - default: - System.err.printf("Warning: unrecognized setting \"%s\".", - param); - break; - } - } - } catch (final IOException e) {} - } - - /** - * @return a set of all prefix names in the database - * @since 2019-04-14 - * @since v0.2.0 - */ - public final Set prefixNameSet() { - return this.database.prefixMap(true).keySet(); - } - - /** - * Runs whenever a prefix is selected in the viewer. - *

    - * Shows its information in the text box to the right. - *

    - * - * @since 2019-01-15 - * @since v0.1.0 - */ - public final void prefixSelected() { - final String prefixName = this.view.getPrefixViewerSelection(); - if (prefixName == null) - return; - else { - final UnitPrefix prefix = this.database.getPrefix(prefixName); - - this.view.setPrefixTextBoxText(String.format("%s%nMultiplier: %s", - prefixName, prefix.getMultiplier())); - } - } - - /** - * Saves the settings to the settings file. - * - * @since 2021-02-17 - */ - public final void saveSettings() { - try (BufferedWriter writer = Files - .newBufferedWriter(this.getSettingsFile())) { - writer.write(String.format("precision=%d\n", this.precision)); - writer.write( - String.format("rounding_type=%s\n", this.roundingType)); - writer.write(String.format("prefix_rule=%s\n", this.prefixRule)); - writer.write(String.format("one_way=%s\n", this.oneWay)); - writer.write(String.format("include_duplicates=%s\n", - this.includeDuplicateUnits)); - } catch (final IOException e) { - e.printStackTrace(); - this.view.showErrorDialog("I/O Error", - "Error occurred while saving settings: " - + e.getLocalizedMessage()); - } - } - - public final void setIncludeDuplicateUnits( - boolean includeDuplicateUnits) { - this.includeDuplicateUnits = includeDuplicateUnits; - - this.view.update(); - this.saveSettings(); - } - - /** - * Enables or disables one-way conversion. - * - * @param oneWay whether one-way conversion should be on (true) or off - * (false) - * @since 2020-08-27 - */ - public final void setOneWay(boolean oneWay) { - this.oneWay = oneWay; - if (oneWay) { - this.fromExistenceCondition.setPredicate( - unitName -> this.metricExceptions.contains(unitName) - || !this.database.getUnit(unitName).isMetric()); - this.toExistenceCondition.setPredicate( - unitName -> this.metricExceptions.contains(unitName) - || this.database.getUnit(unitName).isMetric()); - } else { - this.fromExistenceCondition.setPredicate(unitName -> true); - this.toExistenceCondition.setPredicate(unitName -> true); - } - - this.saveSettings(); - } - - /** - * @param precision new value of precision - * @since 2019-01-15 - * @since v0.1.0 - */ - public final void setPrecision(final int precision) { - this.precision = precision; - - this.saveSettings(); - } - - /** - * @param prefixRepetitionRule the prefixRepetitionRule to set - * @since 2020-08-26 - */ - public void setPrefixRepetitionRule( - Predicate> prefixRepetitionRule) { - if (prefixRepetitionRule instanceof DefaultPrefixRepetitionRule) { - this.prefixRule = (DefaultPrefixRepetitionRule) prefixRepetitionRule; - } else { - this.prefixRule = null; - } - this.database.setPrefixRepetitionRule(prefixRepetitionRule); - - this.saveSettings(); - } - - /** - * @param roundingType the roundingType to set - * @since 2020-07-16 - */ - public final void setRoundingType(RoundingType roundingType) { - this.roundingType = roundingType; - - this.saveSettings(); - } - - /** - * @return a list of all the entries in the dimension-based converter's To - * box - * @since 2020-08-27 - */ - public final Set toEntries() { - return ConditionalExistenceCollections.conditionalExistenceSet( - this.unitNameSet(), this.toExistenceCondition); - } - - /** - * Returns true if and only if the unit represented by {@code unitName} - * has the dimension represented by {@code dimensionName}. - * - * @param unitName name of unit to test - * @param dimensionName name of dimension to test - * @return whether unit has dimenision - * @since 2019-04-13 - * @since v0.2.0 - */ - public final boolean unitMatchesDimension(final String unitName, - final String dimensionName) { - final Unit unit = this.database.getUnit(unitName); - final ObjectProduct dimension = this.database - .getDimension(dimensionName); - return unit.getDimension().equals(dimension); - } - - /** - * Runs whenever a unit is selected in the viewer. - *

    - * Shows its information in the text box to the right. - *

    - * - * @since 2019-01-15 - * @since v0.1.0 - */ - public final void unitNameSelected() { - final String unitName = this.view.getUnitViewerSelection(); - if (unitName == null) - return; - else { - final Unit unit = this.database.getUnit(unitName); - - this.view.setUnitTextBoxText(unit.toString()); - } - } - - /** - * @return a set of all of the unit names - * @since 2019-04-14 - * @since v0.2.0 - */ - public final Set unitNameSet() { - return this.database.unitMapPrefixless(this.includeDuplicateUnits) - .keySet(); - } - } - - /** - * Different types of rounding. - * - * Significant digits: Rounds to a number of digits. i.e. with precision 5, - * 12345.6789 rounds to 12346. Decimal places: Rounds to a number of digits - * after the decimal point, i.e. with precision 5, 12345.6789 rounds to - * 12345.67890. Scientific: Rounds based on the number of digits and - * operations, following standard scientific rounding. - */ - private static enum RoundingType { - SIGNIFICANT_DIGITS, DECIMAL_PLACES, SCIENTIFIC; - } - - private static class View { - private static final NumberFormat NUMBER_FORMATTER = new DecimalFormat(); - - /** The view's frame. */ - private final JFrame frame; - /** The view's associated presenter. */ - private final Presenter presenter; - /** The master pane containing all of the tabs. */ - private final JTabbedPane masterPane; - - // DIMENSION-BASED CONVERTER - /** The panel for inputting values in the dimension-based converter */ - private final JTextField valueInput; - /** The panel for "From" in the dimension-based converter */ - private final SearchBoxList fromSearch; - /** The panel for "To" in the dimension-based converter */ - private final SearchBoxList toSearch; - /** The output area in the dimension-based converter */ - private final JTextArea dimensionBasedOutput; - - // EXPRESSION-BASED CONVERTER - /** The "From" entry in the conversion panel */ - private final JTextField fromEntry; - /** The "To" entry in the conversion panel */ - private final JTextField toEntry; - /** The output area in the conversion panel */ - private final JTextArea output; - - // UNIT AND PREFIX VIEWERS - /** The searchable list of unit names in the unit viewer */ - private final SearchBoxList unitNameList; - /** The searchable list of prefix names in the prefix viewer */ - private final SearchBoxList prefixNameList; - /** The text box for unit data in the unit viewer */ - private final JTextArea unitTextBox; - /** The text box for prefix data in the prefix viewer */ - private final JTextArea prefixTextBox; - - /** - * Creates the {@code View}. - * - * @since 2019-01-14 - * @since v0.1.0 - */ - public View() { - this.presenter = new Presenter(this); - this.frame = new JFrame("7Units"); - this.frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); - - // enable system look and feel - try { - UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); - } catch (ClassNotFoundException | InstantiationException - | IllegalAccessException | UnsupportedLookAndFeelException e) { - // oh well, just use default theme - System.err.println("Failed to enable system look-and-feel."); - e.printStackTrace(); - } - - // create the components - this.masterPane = new JTabbedPane(); - this.unitNameList = new SearchBoxList(this.presenter.unitNameSet()); - this.prefixNameList = new SearchBoxList(this.presenter.prefixNameSet(), - this.presenter.getPrefixNameComparator(), true); - this.unitTextBox = new JTextArea(); - this.prefixTextBox = new JTextArea(); - this.fromSearch = new SearchBoxList(this.presenter.fromEntries()); - this.toSearch = new SearchBoxList(this.presenter.toEntries()); - this.valueInput = new JFormattedTextField(NUMBER_FORMATTER); - this.dimensionBasedOutput = new JTextArea(2, 32); - this.fromEntry = new JTextField(); - this.toEntry = new JTextField(); - this.output = new JTextArea(2, 32); - - // create more components - this.initComponents(); - - this.frame.pack(); - } - - /** - * @return the currently selected pane. - * @throws AssertionError if no pane (or an invalid pane) is selected - */ - public Pane getActivePane() { - switch (this.masterPane.getSelectedIndex()) { - case 0: - return Pane.UNIT_CONVERTER; - case 1: - return Pane.EXPRESSION_CONVERTER; - case 2: - return Pane.UNIT_VIEWER; - case 3: - return Pane.PREFIX_VIEWER; - case 4: - return Pane.ABOUT; - case 5: - return Pane.SETTINGS; - default: - throw new AssertionError("No selected pane, or invalid pane."); - } - } - - /** - * @return value in dimension-based converter - * @throws ParseException - * @since 2020-07-07 - */ - public double getDimensionConverterInput() throws ParseException { - final Number value = NUMBER_FORMATTER.parse(this.valueInput.getText()); - if (value instanceof Double) - return (double) value; - else if (value instanceof Long) - return ((Long) value).longValue(); - else - throw new AssertionError(); - } - - /** - * @return selection in "From" selector in dimension-based converter - * @since 2019-04-13 - * @since v0.2.0 - */ - public String getFromSelection() { - return this.fromSearch.getSelectedValue(); - } - - /** - * @return text in "From" box in converter panel - * @since 2019-01-15 - * @since v0.1.0 - */ - public String getFromText() { - return this.fromEntry.getText(); - } - - /** - * @return index of selected prefix in prefix viewer - * @since 2019-01-15 - * @since v0.1.0 - */ - public String getPrefixViewerSelection() { - return this.prefixNameList.getSelectedValue(); - } - - /** - * @return selection in "To" selector in dimension-based converter - * @since 2019-04-13 - * @since v0.2.0 - */ - public String getToSelection() { - return this.toSearch.getSelectedValue(); - } - - /** - * @return text in "To" box in converter panel - * @since 2019-01-26 - * @since v0.1.0 - */ - public String getToText() { - return this.toEntry.getText(); - } - - /** - * @return index of selected unit in unit viewer - * @since 2019-01-15 - * @since v0.1.0 - */ - public String getUnitViewerSelection() { - return this.unitNameList.getSelectedValue(); - } - - /** - * Starts up the application. - * - * @since 2018-12-27 - * @since v0.1.0 - */ - public final void init() { - this.frame.setVisible(true); - } - - /** - * Initializes the view's components. - * - * @since 2018-12-27 - * @since v0.1.0 - */ - private final void initComponents() { - final JPanel masterPanel = new JPanel(); - this.frame.add(masterPanel); - - masterPanel.setLayout(new BorderLayout()); - - { // pane with all of the tabs - masterPanel.add(this.masterPane, BorderLayout.CENTER); - - // update stuff - this.masterPane.addChangeListener(e -> this.update()); - - { // a panel for unit conversion using a selector - final JPanel convertUnitPanel = new JPanel(); - this.masterPane.addTab("Convert Units", convertUnitPanel); - this.masterPane.setMnemonicAt(0, KeyEvent.VK_U); - - convertUnitPanel.setLayout(new BorderLayout()); - - { // panel for input part - final JPanel inputPanel = new JPanel(); - convertUnitPanel.add(inputPanel, BorderLayout.CENTER); - - inputPanel.setLayout(new GridLayout(1, 3)); - - final JComboBox dimensionSelector = new JComboBox<>( - this.presenter.dimensionNameList() - .toArray(new String[0])); - dimensionSelector.setSelectedItem("LENGTH"); - - // handle dimension filter - final MutablePredicate dimensionFilter = new MutablePredicate<>( - s -> true); - - // panel for From things - inputPanel.add(this.fromSearch); - - this.fromSearch.addSearchFilter(dimensionFilter); - - { // for dimension selector and arrow that represents - // conversion - final JPanel inBetweenPanel = new JPanel(); - inputPanel.add(inBetweenPanel); - - inBetweenPanel.setLayout(new BorderLayout()); - - { // dimension selector - inBetweenPanel.add(dimensionSelector, - BorderLayout.PAGE_START); - } - - { // the arrow in the middle - final JLabel arrowLabel = new JLabel("->"); - inBetweenPanel.add(arrowLabel, BorderLayout.CENTER); - } - } - - // panel for To things - - inputPanel.add(this.toSearch); - - this.toSearch.addSearchFilter(dimensionFilter); - - // code for dimension filter - dimensionSelector.addItemListener(e -> { - dimensionFilter.setPredicate(string -> View.this.presenter - .unitMatchesDimension(string, - (String) dimensionSelector.getSelectedItem())); - this.fromSearch.reapplyFilter(); - this.toSearch.reapplyFilter(); - }); - - // apply the item listener once because I have a default - // selection - dimensionFilter.setPredicate(string -> View.this.presenter - .unitMatchesDimension(string, - (String) dimensionSelector.getSelectedItem())); - this.fromSearch.reapplyFilter(); - this.toSearch.reapplyFilter(); - } - - { // panel for submit and output, and also value entry - final JPanel outputPanel = new JPanel(); - convertUnitPanel.add(outputPanel, BorderLayout.PAGE_END); - - outputPanel.setLayout(new GridLayout(3, 1)); - - { // unit input - final JPanel valueInputPanel = new JPanel(); - outputPanel.add(valueInputPanel); - - valueInputPanel.setLayout(new BorderLayout()); - - { // prompt - final JLabel valuePrompt = new JLabel( - "Value to convert: "); - valueInputPanel.add(valuePrompt, - BorderLayout.LINE_START); - } - - { // value to convert - valueInputPanel.add(this.valueInput, - BorderLayout.CENTER); - } - } - - { // button to convert - final JButton convertButton = new JButton("Convert"); - outputPanel.add(convertButton); - - convertButton.addActionListener( - e -> this.presenter.convertDimensionBased()); - convertButton.setMnemonic(KeyEvent.VK_ENTER); - } - - { // output of conversion - outputPanel.add(this.dimensionBasedOutput); - this.dimensionBasedOutput.setEditable(false); - } - } - } - - { // panel for unit conversion using expressions - final JPanel convertExpressionPanel = new JPanel(); - this.masterPane.addTab("Convert Unit Expressions", - convertExpressionPanel); - this.masterPane.setMnemonicAt(1, KeyEvent.VK_E); - - convertExpressionPanel.setLayout(new GridLayout(4, 1)); - - { // panel for units to convert from - final JPanel fromPanel = new JPanel(); - convertExpressionPanel.add(fromPanel); - - fromPanel.setBorder(BorderFactory.createTitledBorder("From")); - fromPanel.setLayout(new GridLayout(1, 1)); - - { // entry for units - fromPanel.add(this.fromEntry); - } - } - - { // panel for units to convert to - final JPanel toPanel = new JPanel(); - convertExpressionPanel.add(toPanel); - - toPanel.setBorder(BorderFactory.createTitledBorder("To")); - toPanel.setLayout(new GridLayout(1, 1)); - - { // entry for units - toPanel.add(this.toEntry); - } - } - - { // button to convert - final JButton convertButton = new JButton("Convert"); - convertExpressionPanel.add(convertButton); - - convertButton.addActionListener( - e -> this.presenter.convertExpressions()); - convertButton.setMnemonic(KeyEvent.VK_ENTER); - } - - { // output of conversion - final JPanel outputPanel = new JPanel(); - convertExpressionPanel.add(outputPanel); - - outputPanel - .setBorder(BorderFactory.createTitledBorder("Output")); - outputPanel.setLayout(new GridLayout(1, 1)); - - { // output - outputPanel.add(this.output); - this.output.setEditable(false); - } - } - } - - { // panel to look up units - final JPanel unitLookupPanel = new JPanel(); - this.masterPane.addTab("Unit Viewer", unitLookupPanel); - this.masterPane.setMnemonicAt(2, KeyEvent.VK_V); - - unitLookupPanel.setLayout(new GridLayout()); - - { // search panel - unitLookupPanel.add(this.unitNameList); - - this.unitNameList.getSearchList().addListSelectionListener( - e -> this.presenter.unitNameSelected()); - } - - { // the text box for unit's toString - unitLookupPanel.add(this.unitTextBox); - this.unitTextBox.setEditable(false); - this.unitTextBox.setLineWrap(true); - } - } - - { // panel to look up prefixes - final JPanel prefixLookupPanel = new JPanel(); - this.masterPane.addTab("Prefix Viewer", prefixLookupPanel); - this.masterPane.setMnemonicAt(3, KeyEvent.VK_P); - - prefixLookupPanel.setLayout(new GridLayout(1, 2)); - - { // panel for listing and seaching - prefixLookupPanel.add(this.prefixNameList); - - this.prefixNameList.getSearchList().addListSelectionListener( - e -> this.presenter.prefixSelected()); - } - - { // the text box for prefix's toString - prefixLookupPanel.add(this.prefixTextBox); - this.prefixTextBox.setEditable(false); - this.prefixTextBox.setLineWrap(true); - } - } - - { // Info panel - final JPanel infoPanel = new JPanel(); - this.masterPane.addTab("\uD83D\uDEC8", // info (i) character - new JScrollPane(infoPanel)); - - final JTextArea infoTextArea = new JTextArea(); - infoTextArea.setEditable(false); - infoTextArea.setOpaque(false); - infoPanel.add(infoTextArea); - - // get info text - final String infoText = Presenter - .getLinesFromResource("/about.txt").stream() - .map(Presenter::withoutComments) - .collect(Collectors.joining("\n")).replaceAll( - "\\[VERSION\\]", ProgramInfo.VERSION.toString()); - infoTextArea.setText(infoText); - } - - { // Settings panel - final JPanel settingsPanel = new JPanel(); - this.masterPane.addTab("\u2699", new JScrollPane(settingsPanel)); - this.masterPane.setMnemonicAt(5, KeyEvent.VK_S); - - settingsPanel.setLayout( - new BoxLayout(settingsPanel, BoxLayout.PAGE_AXIS)); - - { // rounding settings - final JPanel roundingPanel = new JPanel(); - settingsPanel.add(roundingPanel); - roundingPanel - .setBorder(new TitledBorder("Rounding Settings")); - roundingPanel.setLayout(new GridBagLayout()); - - // rounding rule selection - final ButtonGroup roundingRuleButtons = new ButtonGroup(); - - final JLabel roundingRuleLabel = new JLabel("Rounding Rule:"); - roundingPanel.add(roundingRuleLabel, new GridBagBuilder(0, 0) - .setAnchor(GridBagConstraints.LINE_START).build()); - - final JRadioButton fixedPrecision = new JRadioButton( - "Fixed Precision"); - if (this.presenter.roundingType == RoundingType.SIGNIFICANT_DIGITS) { - fixedPrecision.setSelected(true); - } - fixedPrecision.addActionListener(e -> this.presenter - .setRoundingType(RoundingType.SIGNIFICANT_DIGITS)); - roundingRuleButtons.add(fixedPrecision); - roundingPanel.add(fixedPrecision, new GridBagBuilder(0, 1) - .setAnchor(GridBagConstraints.LINE_START).build()); - - final JRadioButton fixedDecimals = new JRadioButton( - "Fixed Decimal Places"); - if (this.presenter.roundingType == RoundingType.DECIMAL_PLACES) { - fixedDecimals.setSelected(true); - } - fixedDecimals.addActionListener(e -> this.presenter - .setRoundingType(RoundingType.DECIMAL_PLACES)); - roundingRuleButtons.add(fixedDecimals); - roundingPanel.add(fixedDecimals, new GridBagBuilder(0, 2) - .setAnchor(GridBagConstraints.LINE_START).build()); - - final JRadioButton relativePrecision = new JRadioButton( - "Scientific Precision"); - if (this.presenter.roundingType == RoundingType.SCIENTIFIC) { - relativePrecision.setSelected(true); - } - relativePrecision.addActionListener(e -> this.presenter - .setRoundingType(RoundingType.SCIENTIFIC)); - roundingRuleButtons.add(relativePrecision); - roundingPanel.add(relativePrecision, new GridBagBuilder(0, 3) - .setAnchor(GridBagConstraints.LINE_START).build()); - - final JLabel sliderLabel = new JLabel("Precision:"); - roundingPanel.add(sliderLabel, new GridBagBuilder(0, 4) - .setAnchor(GridBagConstraints.LINE_START).build()); - - final JSlider sigDigSlider = new JSlider(0, 12); - roundingPanel.add(sigDigSlider, new GridBagBuilder(0, 5) - .setAnchor(GridBagConstraints.LINE_START).build()); - - sigDigSlider.setMajorTickSpacing(4); - sigDigSlider.setMinorTickSpacing(1); - sigDigSlider.setSnapToTicks(true); - sigDigSlider.setPaintTicks(true); - sigDigSlider.setPaintLabels(true); - sigDigSlider.setValue(this.presenter.precision); - - sigDigSlider.addChangeListener(e -> this.presenter - .setPrecision(sigDigSlider.getValue())); - } - - { // prefix repetition settings - final JPanel prefixRepetitionPanel = new JPanel(); - settingsPanel.add(prefixRepetitionPanel); - prefixRepetitionPanel.setBorder( - new TitledBorder("Prefix Repetition Settings")); - prefixRepetitionPanel.setLayout(new GridBagLayout()); - - // prefix rules - final ButtonGroup prefixRuleButtons = new ButtonGroup(); - - final JRadioButton noRepetition = new JRadioButton( - "No Repetition"); - if (this.presenter.prefixRule == DefaultPrefixRepetitionRule.NO_REPETITION) { - noRepetition.setSelected(true); - } - noRepetition.addActionListener( - e -> this.presenter.setPrefixRepetitionRule( - DefaultPrefixRepetitionRule.NO_REPETITION)); - prefixRuleButtons.add(noRepetition); - prefixRepetitionPanel.add(noRepetition, - new GridBagBuilder(0, 0) - .setAnchor(GridBagConstraints.LINE_START) - .build()); - - final JRadioButton noRestriction = new JRadioButton( - "No Restriction"); - if (this.presenter.prefixRule == DefaultPrefixRepetitionRule.NO_RESTRICTION) { - noRestriction.setSelected(true); - } - noRestriction.addActionListener( - e -> this.presenter.setPrefixRepetitionRule( - DefaultPrefixRepetitionRule.NO_RESTRICTION)); - prefixRuleButtons.add(noRestriction); - prefixRepetitionPanel.add(noRestriction, - new GridBagBuilder(0, 1) - .setAnchor(GridBagConstraints.LINE_START) - .build()); - - final JRadioButton customRepetition = new JRadioButton( - "Complex Repetition"); - if (this.presenter.prefixRule == DefaultPrefixRepetitionRule.COMPLEX_REPETITION) { - customRepetition.setSelected(true); - } - customRepetition.addActionListener( - e -> this.presenter.setPrefixRepetitionRule( - DefaultPrefixRepetitionRule.COMPLEX_REPETITION)); - prefixRuleButtons.add(customRepetition); - prefixRepetitionPanel.add(customRepetition, - new GridBagBuilder(0, 2) - .setAnchor(GridBagConstraints.LINE_START) - .build()); - } - - { // search settings - final JPanel searchingPanel = new JPanel(); - settingsPanel.add(searchingPanel); - searchingPanel.setBorder(new TitledBorder("Search Settings")); - searchingPanel.setLayout(new GridBagLayout()); - - // searching rules - final ButtonGroup searchRuleButtons = new ButtonGroup(); - - final JRadioButton noPrefixes = new JRadioButton( - "Never Include Prefixed Units"); - noPrefixes.setEnabled(false); - searchRuleButtons.add(noPrefixes); - searchingPanel.add(noPrefixes, new GridBagBuilder(0, 0) - .setAnchor(GridBagConstraints.LINE_START).build()); - - final JRadioButton fixedPrefixes = new JRadioButton( - "Include Some Prefixes"); - fixedPrefixes.setEnabled(false); - searchRuleButtons.add(fixedPrefixes); - searchingPanel.add(fixedPrefixes, new GridBagBuilder(0, 1) - .setAnchor(GridBagConstraints.LINE_START).build()); - - final JRadioButton explicitPrefixes = new JRadioButton( - "Include Explicit Prefixes"); - explicitPrefixes.setEnabled(false); - searchRuleButtons.add(explicitPrefixes); - searchingPanel.add(explicitPrefixes, new GridBagBuilder(0, 2) - .setAnchor(GridBagConstraints.LINE_START).build()); - - final JRadioButton alwaysInclude = new JRadioButton( - "Include All Single Prefixes"); - alwaysInclude.setEnabled(false); - searchRuleButtons.add(alwaysInclude); - searchingPanel.add(alwaysInclude, new GridBagBuilder(0, 3) - .setAnchor(GridBagConstraints.LINE_START).build()); - } - - { // miscellaneous settings - final JPanel miscPanel = new JPanel(); - settingsPanel.add(miscPanel); - miscPanel - .setBorder(new TitledBorder("Miscellaneous Settings")); - miscPanel.setLayout(new GridBagLayout()); - - final JCheckBox oneWay = new JCheckBox( - "Convert One Way Only"); - oneWay.setSelected(this.presenter.oneWay); - oneWay.addItemListener( - e -> this.presenter.setOneWay(e.getStateChange() == 1)); - miscPanel.add(oneWay, new GridBagBuilder(0, 0) - .setAnchor(GridBagConstraints.LINE_START).build()); - - final JCheckBox showAllVariations = new JCheckBox( - "Show Duplicates in \"Convert Units\""); - showAllVariations - .setSelected(this.presenter.includeDuplicateUnits); - showAllVariations.addItemListener(e -> this.presenter - .setIncludeDuplicateUnits(e.getStateChange() == 1)); - miscPanel.add(showAllVariations, new GridBagBuilder(0, 1) - .setAnchor(GridBagConstraints.LINE_START).build()); - - final JButton unitFileButton = new JButton( - "Manage Unit Data Files"); - unitFileButton.setEnabled(false); - miscPanel.add(unitFileButton, new GridBagBuilder(0, 2) - .setAnchor(GridBagConstraints.LINE_START).build()); - } - } - } - } - - /** - * Sets the text in the output of the dimension-based converter. - * - * @param text text to set - * @since 2019-04-13 - * @since v0.2.0 - */ - public void setDimensionConverterOutputText(final String text) { - this.dimensionBasedOutput.setText(text); - } - - /** - * Sets the text in the output of the conversion panel. - * - * @param text text to set - * @since 2019-01-15 - * @since v0.1.0 - */ - public void setExpressionConverterOutputText(final String text) { - this.output.setText(text); - } - - /** - * Sets the text of the prefix text box in the prefix viewer. - * - * @param text text to set - * @since 2019-01-15 - * @since v0.1.0 - */ - public void setPrefixTextBoxText(final String text) { - this.prefixTextBox.setText(text); - } - - /** - * Sets the text of the unit text box in the unit viewer. - * - * @param text text to set - * @since 2019-01-15 - * @since v0.1.0 - */ - public void setUnitTextBoxText(final String text) { - this.unitTextBox.setText(text); - } - - /** - * Shows an error dialog. - * - * @param title title of dialog - * @param message message in dialog - * @since 2019-01-14 - * @since v0.1.0 - */ - public void showErrorDialog(final String title, final String message) { - JOptionPane.showMessageDialog(this.frame, message, title, - JOptionPane.ERROR_MESSAGE); - } - - public void update() { - this.unitNameList.setItems(this.presenter.unitNameSet()); - this.fromSearch.setItems(this.presenter.fromEntries()); - this.toSearch.setItems(this.presenter.toEntries()); - - switch (this.getActivePane()) { - case UNIT_CONVERTER: - this.fromSearch.updateList(); - this.toSearch.updateList(); - break; - default: - // do nothing, for now - break; - } - } - } - - public static void main(final String[] args) { - new View().init(); - } -} diff --git a/src/main/java/sevenUnits/converterGUI/package-info.java b/src/main/java/sevenUnits/converterGUI/package-info.java deleted file mode 100644 index 784664f..0000000 --- a/src/main/java/sevenUnits/converterGUI/package-info.java +++ /dev/null @@ -1,24 +0,0 @@ -/** - * Copyright (C) 2019 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 . - */ -/** - * The GUI interface of the Unit Converter. - * - * @author Adrien Hopkins - * @since 2019-01-25 - * @since v0.2.0 - */ -package sevenUnits.converterGUI; \ No newline at end of file diff --git a/src/main/java/sevenUnitsGUI/Main.java b/src/main/java/sevenUnitsGUI/Main.java new file mode 100644 index 0000000..b5a896f --- /dev/null +++ b/src/main/java/sevenUnitsGUI/Main.java @@ -0,0 +1,34 @@ +/** + * Copyright (C) 2022 Adrien Hopkins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package sevenUnitsGUI; + +/** + * The main code for the 7Units GUI + * + * @since 2022-04-19 + */ +public final class Main { + + /** + * @param args + * @since 2022-04-19 + */ + public static void main(String[] args) { + View.createTabbedView(); + } + +} diff --git a/src/main/java/sevenUnitsGUI/Presenter.java b/src/main/java/sevenUnitsGUI/Presenter.java index fd050b7..4feea44 100644 --- a/src/main/java/sevenUnitsGUI/Presenter.java +++ b/src/main/java/sevenUnitsGUI/Presenter.java @@ -567,7 +567,7 @@ 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 */ public boolean oneWayConversionEnabled() { diff --git a/src/main/java/sevenUnitsGUI/TabbedView.java b/src/main/java/sevenUnitsGUI/TabbedView.java index c8e69ee..be80ccb 100644 --- a/src/main/java/sevenUnitsGUI/TabbedView.java +++ b/src/main/java/sevenUnitsGUI/TabbedView.java @@ -124,87 +124,17 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { * 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); - } - }, + SIGNIFICANT_DIGITS, /** * 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); - } - }, + DECIMAL_PLACES, /** * 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; - } + UNCERTAINTY; } /** diff --git a/src/main/java/sevenUnitsGUI/View.java b/src/main/java/sevenUnitsGUI/View.java index 011e87f..b2d2b94 100644 --- a/src/main/java/sevenUnitsGUI/View.java +++ b/src/main/java/sevenUnitsGUI/View.java @@ -29,6 +29,14 @@ import sevenUnits.utils.NameSymbol; * @since 2021-12-15 */ public interface View { + /** + * @return a new tabbed view + * @since 2022-04-19 + */ + static View createTabbedView() { + return new TabbedView(); + } + /** * @return the presenter associated with this view * @since 2022-04-19 diff --git a/src/main/java/sevenUnitsGUI/ViewBot.java b/src/main/java/sevenUnitsGUI/ViewBot.java index 9253ae5..a3ba7a2 100644 --- a/src/main/java/sevenUnitsGUI/ViewBot.java +++ b/src/main/java/sevenUnitsGUI/ViewBot.java @@ -34,7 +34,8 @@ import sevenUnits.utils.Nameable; * @author Adrien Hopkins * @since 2022-01-29 */ -final class ViewBot implements UnitConversionView, ExpressionConversionView { +public final class ViewBot + implements UnitConversionView, ExpressionConversionView { /** * A record of the parameters given to * {@link View#showPrefix(NameSymbol, String)}, for testing. diff --git a/src/test/java/sevenUnitsGUI/PresenterTest.java b/src/test/java/sevenUnitsGUI/PresenterTest.java index f639329..3364e83 100644 --- a/src/test/java/sevenUnitsGUI/PresenterTest.java +++ b/src/test/java/sevenUnitsGUI/PresenterTest.java @@ -148,6 +148,7 @@ public final class PresenterTest { presenter.database.clear(); presenter.database.addUnit("metre", metre); presenter.database.addUnit("meter", meter); + presenter.setOneWayConversionEnabled(false); // test that only one of them is included if duplicate units disabled presenter.setShowDuplicates(false); @@ -265,6 +266,8 @@ public final class PresenterTest { presenter.setOneWayConversionEnabled(true); presenter.setShowDuplicates(true); presenter.setNumberDisplayRule(StandardDisplayRules.fixedPrecision(11)); + presenter.setPrefixRepetitionRule( + DefaultPrefixRepetitionRule.COMPLEX_REPETITION); presenter.saveSettings(TEST_SETTINGS); // overwrite custom settings @@ -323,6 +326,7 @@ public final class PresenterTest { // setup final ViewBot viewBot = new ViewBot(); final Presenter presenter = new Presenter(viewBot); + presenter.setOneWayConversionEnabled(false); // override default database units presenter.database.clear(); diff --git a/src/test/resources/test-settings.txt b/src/test/resources/test-settings.txt index 932221e..a0f494a 100644 --- a/src/test/resources/test-settings.txt +++ b/src/test/resources/test-settings.txt @@ -1,4 +1,4 @@ number_display_rule=Round to 11 significant figures -prefix_rule=NO_RESTRICTION +prefix_rule=COMPLEX_REPETITION one_way=true include_duplicates=true -- cgit v1.2.3