diff --git a/internationalization/src/main/java/systems/terranatal/omnijfx/internationalization/NumericParsingUtils.java b/internationalization/src/main/java/systems/terranatal/omnijfx/internationalization/NumericParsingUtils.java index d81f753..68206be 100644 --- a/internationalization/src/main/java/systems/terranatal/omnijfx/internationalization/NumericParsingUtils.java +++ b/internationalization/src/main/java/systems/terranatal/omnijfx/internationalization/NumericParsingUtils.java @@ -30,7 +30,10 @@ package systems.terranatal.omnijfx.internationalization; +import java.text.DecimalFormat; import java.text.DecimalFormatSymbols; +import java.text.NumberFormat; +import java.text.ParseException; import java.util.Locale; import java.util.Set; import java.util.stream.Collectors; @@ -67,12 +70,21 @@ static String stripGroupingSymbols(String text, String decimalSeparator) { * @return true if the string can be parsed to a number, false otherwise */ static boolean isParseable(String text, Locale locale) { - var separator = Character.toString(DecimalFormatSymbols.getInstance(locale).getDecimalSeparator()); - var str = stripGroupingSymbols(text, separator); - var localizedRational = "[\\+\\-]?\\d+(\\%s\\d+)?".formatted(separator); - var finalRegex = "%s([Ee]%s)?".formatted(localizedRational, localizedRational); + var symbols = DecimalFormatSymbols.getInstance(locale); + var separator = Character.toString(symbols.getDecimalSeparator()); + var grouping = Character.toString(DecimalFormatSymbols.getInstance(locale).getGroupingSeparator()); + final var scientific = "%s([Ee]%s)?"; - return str.matches(finalRegex); + if (!text.contains(grouping)) { + var localizedRational = "[\\+\\-]?\\d+(\\%s\\d+)?".formatted(separator); + var finalRegex = scientific.formatted(localizedRational, localizedRational); + return text.matches(finalRegex); + } + var gsize = ((DecimalFormat) DecimalFormat.getInstance(locale)).getGroupingSize(); + var rationalWithGrouping = "[\\+\\-]?\\d{1,%d}(\\%s\\d{%d})*(\\%s\\d+)?" + .formatted(gsize, grouping, gsize, separator); + var finalRegex = scientific.formatted(rationalWithGrouping, rationalWithGrouping); + return text.matches(finalRegex); } /** @@ -94,4 +106,23 @@ static boolean isParseable(String text) { static String stripGroupingSymbols(String text) { return stripGroupingSymbols(text, Character.toString(DecimalFormatSymbols.getInstance().getDecimalSeparator())); } + + static boolean hasGrouping(String text, Locale locale) { + var symbols = DecimalFormatSymbols.getInstance(locale); + var grouping = Character.toString(symbols.getGroupingSeparator()); + + return text.contains(grouping); + } + + static boolean hasGrouping(String text) { + return hasGrouping(text, Locale.getDefault()); + } + + static Number parseUnchecked(NumberFormat formatter, String text) { + try { + return formatter.parse(text); + } catch (ParseException e) { + throw new IllegalArgumentException("Could not parse " + text + " as a number.", e); + } + } } diff --git a/internationalization/src/test/java/systems/terranatal/omnijfx/internationalization/TestParsingUtils.java b/internationalization/src/test/java/systems/terranatal/omnijfx/internationalization/TestParsingUtils.java index 7841cab..b6f667b 100644 --- a/internationalization/src/test/java/systems/terranatal/omnijfx/internationalization/TestParsingUtils.java +++ b/internationalization/src/test/java/systems/terranatal/omnijfx/internationalization/TestParsingUtils.java @@ -37,30 +37,32 @@ import org.junit.jupiter.params.provider.ArgumentsProvider; import org.junit.jupiter.params.provider.ArgumentsSource; +import java.text.DecimalFormat; import java.text.DecimalFormatSymbols; import java.util.Locale; import java.util.Optional; +import java.util.Random; import java.util.stream.Stream; public class TestParsingUtils { + private static final Random RANDOM = new Random(); @ParameterizedTest @ArgumentsSource(ParsingStringsProvider.class) - public void testNumericParsingUtils(Optional maybeLocale, String parameter, String expectedResult, + public void testNumericParsingUtils(Optional maybeLocale, String parameter, boolean hasGrouping, boolean expectedParseability) { - var result = ""; + var result = false; var isParseable = false; if (maybeLocale.isPresent()) { - result = NumericParsingUtils.stripGroupingSymbols(parameter, - Character.toString(DecimalFormatSymbols.getInstance(maybeLocale.get()).getDecimalSeparator())); + result = NumericParsingUtils.hasGrouping(parameter, maybeLocale.get()); isParseable = NumericParsingUtils.isParseable(parameter, maybeLocale.get()); } else { - result = NumericParsingUtils.stripGroupingSymbols(parameter); + result = NumericParsingUtils.hasGrouping(parameter); isParseable = NumericParsingUtils.isParseable(parameter); } - Assertions.assertEquals(expectedResult, result); + Assertions.assertEquals(hasGrouping, result); Assertions.assertEquals(expectedParseability, isParseable); } @@ -69,24 +71,49 @@ public static class ParsingStringsProvider implements ArgumentsProvider { @Override public Stream provideArguments(ExtensionContext extensionContext) throws Exception { var platformDecimalSeparator = Character.toString(DecimalFormatSymbols.getInstance().getDecimalSeparator()); + var sampleNr1 = "123" + platformDecimalSeparator + "456"; var badNr1 = "123" + platformDecimalSeparator + "456" + platformDecimalSeparator + "789"; var scientificNr = "456%s321E+0%s987".formatted(platformDecimalSeparator, platformDecimalSeparator); return Stream.of( - Arguments.of(Optional.of(Locale.GERMANY), "123.123,004", "123123,004", true), - Arguments.of(Optional.empty(), "123 456 789", "123456789", true), - Arguments.of(Optional.empty(), sampleNr1, sampleNr1, true), - Arguments.of(Optional.empty(), badNr1, badNr1, false), - Arguments.of(Optional.of(Locale.US), "123,456,789.002", "123456789.002", true), - Arguments.of(Optional.of(Locale.US), "123'456'789", "123456789", true), - Arguments.of(Optional.empty(), scientificNr, scientificNr, true), - Arguments.of(Optional.empty(), scientificNr.toLowerCase(), scientificNr.toLowerCase(), true), - Arguments.of(Optional.of(Locale.GERMANY), "123.986,009E-3,14", "123986,009E-3,14", true), - Arguments.of(Optional.of(Locale.US), "123,986.009e-3.14", "123986.009e-3.14", true), - Arguments.of(Optional.of(Locale.GERMANY), "123,123,123e-3,55,5", "123,123,123e-3,55,5", false), - Arguments.of(Optional.of(Locale.US), "123.123.123e-3.55.5", "123.123.123e-3.55.5", false) + Arguments.of(Optional.of(Locale.GERMANY), "123.123,004", true, true), + Arguments.of(Optional.of(Locale.CHINA), generateRandom(Locale.CHINA, 3), true, true), + Arguments.of(Optional.empty(), "123 456 789", false, false), + Arguments.of(Optional.empty(), sampleNr1, false, true), + Arguments.of(Optional.empty(), badNr1, false, false), + Arguments.of(Optional.of(Locale.US), "123,456,789.002", true, true), + Arguments.of(Optional.of(Locale.US), "123'456'789", false, false), + Arguments.of(Optional.empty(), scientificNr, false, true), + Arguments.of(Optional.empty(), scientificNr.toLowerCase(), false, true), + Arguments.of(Optional.of(Locale.GERMANY), "123.986,009E-3,14", true, true), + Arguments.of(Optional.of(Locale.US), "12,986.009e-3.14", true, true), + Arguments.of(Optional.of(Locale.GERMANY), "123,123,123e-3,55,5", false, false), + Arguments.of(Optional.of(Locale.US), "123.123.123e-3.55.5", false, false), + Arguments.of(Optional.of(Locale.GERMANY), "12.45.89", true, false) ); } } + + public static int randomGroup(Locale locale) { + var platformGSize = ((DecimalFormat) DecimalFormat.getInstance(locale)).getGroupingSize(); + var number = Double.valueOf(Math.pow(10.0, platformGSize)).intValue(); + + return RANDOM.nextInt(number/10, number); + } + + public static String generateRandom(Locale locale, int numberOfGroups) { + if (numberOfGroups < 1) { + return String.valueOf(RANDOM.nextInt(1, 10000000)); + } + + var sb = new StringBuilder(); + sb.append(String.valueOf(randomGroup(locale))); + var grouping = String.valueOf(DecimalFormatSymbols.getInstance(locale).getGroupingSeparator()); + + for (int i = 1; i < numberOfGroups; i++) { + sb.append(grouping + randomGroup(locale)); + } + return sb.toString(); + } } diff --git a/jfx/src/main/java/module-info.java b/jfx/src/main/java/module-info.java index d661c43..405392d 100644 --- a/jfx/src/main/java/module-info.java +++ b/jfx/src/main/java/module-info.java @@ -30,7 +30,8 @@ module omnijfx { requires javafx.controls; requires javafx.graphics; - requires java.logging; + requires omnijfx.internationalization; + requires java.logging; //requires org.junit.jupiter.engine; opens systems.terranatal.omnijfx.jfx.builder; diff --git a/jfx/src/main/java/systems/terranatal/omnijfx/jfx/builder/Builders.java b/jfx/src/main/java/systems/terranatal/omnijfx/jfx/builder/Builders.java index e804e30..5a69c16 100644 --- a/jfx/src/main/java/systems/terranatal/omnijfx/jfx/builder/Builders.java +++ b/jfx/src/main/java/systems/terranatal/omnijfx/jfx/builder/Builders.java @@ -31,14 +31,8 @@ package systems.terranatal.omnijfx.jfx.builder; import javafx.scene.Node; -import javafx.scene.control.Button; -import javafx.scene.control.Label; -import javafx.scene.control.Slider; -import javafx.scene.control.TextField; -import javafx.scene.layout.HBox; -import javafx.scene.layout.Pane; -import javafx.scene.layout.StackPane; -import javafx.scene.layout.VBox; +import javafx.scene.control.*; +import javafx.scene.layout.*; /** * Utility class containing methods to create Node and Pane builders @@ -109,6 +103,23 @@ static Initializer slider(double minimum, double maximum, double initial return node(new Slider(minimum, maximum, initial)); } + /** + * Creates an {@link Initializer} containing a {@link RadioButton} with a label + * @param label the label + * @return an {@link Initializer} containing a {@link RadioButton} with a label + */ + static Initializer radioButton(String label) { + return node(new RadioButton(label)); + } + + /** + * Creates an {@link Initializer} containing a {@link RadioButton} + * @return an {@link Initializer} containing a {@link RadioButton} + */ + static Initializer radioButton() { + return node(new RadioButton()); + } + /** * Specialized method from {@link Builders#pane(Pane)} to create a {@link HBox} initializer * @return an initializer for {@link HBox} @@ -143,4 +154,11 @@ static PaneInitializer stackPane() { return pane(new StackPane()); } + /** + * Creates a {@link GridInitializer}. + * @return a {@link GridInitializer} + */ + static GridInitializer gridPane() { + return new GridInitializer(new GridPane()); + } } diff --git a/jfx/src/main/java/systems/terranatal/omnijfx/jfx/builder/GridInitializer.java b/jfx/src/main/java/systems/terranatal/omnijfx/jfx/builder/GridInitializer.java new file mode 100644 index 0000000..c3db8f7 --- /dev/null +++ b/jfx/src/main/java/systems/terranatal/omnijfx/jfx/builder/GridInitializer.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2024, Rafael Barros Felix de Sousa @ Terranatal Systems + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * * Neither the name of omnijfx nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package systems.terranatal.omnijfx.jfx.builder; + +import javafx.scene.Node; +import javafx.scene.layout.GridPane; + +import java.util.Arrays; +import java.util.Collections; +import java.util.function.Supplier; +import java.util.stream.Stream; + +public class GridInitializer extends PaneInitializer { + public GridInitializer(GridPane instance) { + super(instance); + } + + public GridInitializer(Supplier supplier) { + super(supplier); + } + + public static GridInitializer wrap(GridPane instance) { + return new GridInitializer(instance); + } + + @SafeVarargs + public final GridInitializer addColumn(int colIndex, N... nodes) { + if (colIndex < 0 || nodes.length == 0) return this; + instance.addColumn(colIndex, nodes); + return this; + } + + @SafeVarargs + public final GridInitializer addColumn(int colIndex, Supplier... nodes) { + var stream = Stream.of(nodes).map(Supplier::get); + return addColumn(colIndex, stream.toArray(Node[]::new)); + } + + + @SafeVarargs + public final GridInitializer addRow(int rowIndex, N... nodes) { + if (rowIndex < 0 || nodes.length == 0) { + return this; + } + instance.addRow(rowIndex, nodes); + return this; + } + + @SafeVarargs + public final GridInitializer addRow(int rowIndex, Supplier... nodes) { + var stream = Stream.of(nodes).map(Supplier::get); + return addRow(rowIndex, stream.toArray(Node[]::new)); + } +} diff --git a/jfx/src/main/java/systems/terranatal/omnijfx/jfx/builder/ToggleGroupBuilder.java b/jfx/src/main/java/systems/terranatal/omnijfx/jfx/builder/ToggleGroupBuilder.java new file mode 100644 index 0000000..f5682c1 --- /dev/null +++ b/jfx/src/main/java/systems/terranatal/omnijfx/jfx/builder/ToggleGroupBuilder.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2024, Rafael Barros Felix de Sousa @ Terranatal Systems + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * * Neither the name of omnijfx nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package systems.terranatal.omnijfx.jfx.builder; + +import javafx.scene.control.Toggle; +import javafx.scene.control.ToggleGroup; + +import java.util.function.Supplier; + +/** + * Allows the user to build a {@link ToggleGroup} + * @param any implementation of {@link Toggle} interface + */ +public class ToggleGroupBuilder implements Supplier { + private final ToggleGroup group; + + /** + * Default and only constructor + */ + public ToggleGroupBuilder() { + this.group = new ToggleGroup(); + } + + /** + * Returns the {@link ToggleGroup} that this builder initialized + * @return the {@link ToggleGroup} that this builder initialized + */ + @Override + public ToggleGroup get() { + return group; + } + + /** + * Adds a component that implements {@link Toggle} interface to the {@link ToggleGroup} + * @param toggle a component that implements {@link Toggle} + * @return this builder + */ + public ToggleGroupBuilder addToggle(Toggle toggle) { + toggle.setToggleGroup(group); + return this; + } + + /** + * Overloaded method that accepts any {@link Supplier} of a {@link Toggle} component + * and adds its result to the {@link ToggleGroup}. + * @param toggle a {@link Supplier} of a component that implements {@link Toggle} + * @return this builder + */ + public ToggleGroupBuilder addToggle(Supplier toggle) { + return addToggle(toggle.get()); + } +} diff --git a/jfx/src/test/java/systems/terranatal/omnijfx/jfx/builder/TestGridAndRadioGroups.java b/jfx/src/test/java/systems/terranatal/omnijfx/jfx/builder/TestGridAndRadioGroups.java new file mode 100644 index 0000000..74e1e0b --- /dev/null +++ b/jfx/src/test/java/systems/terranatal/omnijfx/jfx/builder/TestGridAndRadioGroups.java @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2024, Rafael Barros Felix de Sousa @ Terranatal Systems + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * * Neither the name of omnijfx nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package systems.terranatal.omnijfx.jfx.builder; + +import javafx.beans.value.ChangeListener; +import javafx.scene.Scene; +import javafx.scene.control.RadioButton; +import javafx.scene.control.TextField; +import javafx.scene.control.TextInputControl; +import javafx.scene.control.ToggleGroup; +import javafx.stage.Stage; +import javafx.util.StringConverter; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.testfx.api.FxRobot; +import org.testfx.framework.junit5.ApplicationExtension; +import org.testfx.framework.junit5.Start; +import systems.terranatal.omnijfx.internationalization.NumericParsingUtils; +import systems.terranatal.omnijfx.jfx.datautils.Converters; + +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; +import java.text.NumberFormat; +import java.util.function.Consumer; +import java.util.function.Function; + +@ExtendWith(ApplicationExtension.class) +public class TestGridAndRadioGroups { + + private RadioButton monthly, yearly; + private TextField monthlyRate, yearlyRate; + private final NumberFormat decimalFormat = NumberFormat.getInstance(); + + private final StringConverter stringToDouble = Converters.makeStrConverter( + str -> NumericParsingUtils.parseUnchecked(decimalFormat, str).doubleValue(), + decimalFormat::format); + private final Function monthlyToYearly = mRate -> + (Math.pow(mRate/100.0 + 1.0, 12.0) - 1.0) * 100.0; + private final Function yearlyToMonthly = yRate -> + (Math.pow(yRate/100.0 + 1.0, 1.0/12.0) - 1.0) * 100.0; + + private ChangeListener makeChangeListener( + TextField target, RadioButton expected, + Function conversion) { + return (obs, oldVal, newVal) -> { + if (expected.isSelected() && NumericParsingUtils.isParseable(newVal)) { + target.setText(stringToDouble.toString(conversion.apply( + stringToDouble.fromString(newVal)))); + } + }; + } + + @Start + public void start(Stage stage) { + decimalFormat.setMaximumFractionDigits(2); + decimalFormat.setMinimumFractionDigits(2); + monthly = Builders.radioButton("Monthly Rate (%)").get(); + yearly = Builders.radioButton("Yearly Rate (%)").get(); + monthlyRate = Builders.textField("") + .bind(monthly.selectedProperty().not(), TextInputControl::disableProperty).get(); + yearlyRate = Builders.textField("") + .bind(monthly.selectedProperty().not(), TextInputControl::disableProperty).get(); + monthlyRate.textProperty().addListener( + makeChangeListener(yearlyRate, monthly, monthlyToYearly)); + yearlyRate.textProperty().addListener( + makeChangeListener(monthlyRate, yearly, yearlyToMonthly)); + var radioGroup = new ToggleGroupBuilder<>() + .addToggle(monthly) + .addToggle(yearly).get(); + + stage.setScene(makeTestScene()); + stage.show(); + } + + private Scene makeTestScene() { + + var monthIntzr = Builders.node(monthly); + var monthlyRateIntzr = Builders.node(monthlyRate); + var grid = Builders.gridPane() + .addColumn(0, monthIntzr, monthlyRateIntzr) + .addColumn(1, yearly, yearlyRate).get(); + + return new Scene(grid, grid.getWidth(), grid.getHeight()); + } + + @Test + public void testValues(FxRobot robot) { + + var separator = Character.toString(DecimalFormatSymbols.getInstance().getDecimalSeparator()); + monthly.setSelected(true); + monthlyRate.setText(stringToDouble.toString(1.05d)); + Assertions.assertEquals(stringToDouble.toString(13.35d), yearlyRate.getText()); + + yearly.setSelected(true); + yearlyRate.setText(stringToDouble.toString(6.18d)); + Assertions.assertFalse(monthly.isSelected()); + Assertions.assertEquals(stringToDouble.toString(0.50d), monthlyRate.getText()); + } +} diff --git a/kfx/src/test/kotlin/systems/terranatal/omnijfx/kfx/TestInitializers.kt b/kfx/src/test/kotlin/systems/terranatal/omnijfx/kfx/TestInitializers.kt index 8eb8852..2db7026 100644 --- a/kfx/src/test/kotlin/systems/terranatal/omnijfx/kfx/TestInitializers.kt +++ b/kfx/src/test/kotlin/systems/terranatal/omnijfx/kfx/TestInitializers.kt @@ -45,6 +45,7 @@ import systems.terranatal.omnijfx.kfx.extensions.RadioButtons.radioButton import systems.terranatal.omnijfx.kfx.extensions.addRadioButton import systems.terranatal.omnijfx.kfx.extensions.invoke import systems.terranatal.omnijfx.kfx.extensions.toggleGroup +import java.text.NumberFormat import kotlin.math.pow @ExtendWith(ApplicationExtension::class) @@ -56,10 +57,12 @@ class TestInitializers { lateinit var monthlyValue: TextField lateinit var yearlyValue: TextField + val numberFormatter = NumberFormat.getNumberInstance() + private fun makeScene(): Scene { monthlyValue.textProperty().addListener(ChangeListener { observable, oldValue, newValue -> if (monthly.isSelected && NumericParsingUtils.isParseable(newValue)) { - var rate = newValue.toDouble() + var rate = numberFormatter.parse(newValue).toDouble() rate = ((1 + rate/100).pow(12) - 1) * 100 yearlyValue.text = "%.2f".format(rate) } @@ -67,7 +70,7 @@ class TestInitializers { yearlyValue.textProperty().addListener(ChangeListener { observable, oldValue, newValue -> if (yearly.isSelected && NumericParsingUtils.isParseable(newValue)) { - var rate = newValue.toDouble() + var rate = numberFormatter.parse(newValue).toDouble() rate = ((1 + rate/100).pow(1.0/12.0) - 1) * 100 monthlyValue.text = "%.2f".format(rate) } @@ -87,6 +90,8 @@ class TestInitializers { @Start fun start(stage: Stage) { + numberFormatter.maximumFractionDigits = 2 + numberFormatter.minimumFractionDigits = 2 monthly = radioButton("Monthly Interest (%)") yearly = radioButton("Yearly Interest (%)") monthlyValue = TextFields { @@ -104,12 +109,12 @@ class TestInitializers { @Test fun testComponentsInitialization() { Assertions.assertTrue(monthly.isSelected) - monthlyValue { text = "1.05" } - Assertions.assertEquals("13.35", yearlyValue.text) + monthlyValue { text = numberFormatter.format(1.05) } + Assertions.assertEquals(numberFormatter.format(13.35), yearlyValue.text) yearly.isSelected = true - yearlyValue.text = "6.18" - Assertions.assertEquals("0.50", monthlyValue.text) + yearlyValue.text = numberFormatter.format(6.18) + Assertions.assertEquals(numberFormatter.format(0.50), monthlyValue.text) } } \ No newline at end of file