diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/library/types/QuantityType.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/library/types/QuantityType.java index dc0a45afaf598..6470c4a20a672 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/library/types/QuantityType.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/library/types/QuantityType.java @@ -15,6 +15,7 @@ import static org.eclipse.jdt.annotation.DefaultLocation.*; import java.math.BigDecimal; +import java.text.DecimalFormatSymbols; import java.time.Instant; import java.time.ZoneOffset; import java.time.ZonedDateTime; @@ -58,6 +59,7 @@ public class QuantityType> extends Number implements PrimitiveType, State, Command, Comparable> { private static final long serialVersionUID = 8828949721938234629L; + private static final char DOT_DECIMAL_SEPARATOR = '.'; private static final BigDecimal HUNDRED = BigDecimal.valueOf(100); public static final QuantityType ZERO = new QuantityType<>(0, AbstractUnit.ONE); @@ -102,6 +104,14 @@ public QuantityType(String value) { BigDecimal bd = new BigDecimal(value); quantity = (Quantity) Quantities.getQuantity(bd, AbstractUnit.ONE, Scale.RELATIVE); } else { + char defaultDecimalSeparator = DecimalFormatSymbols.getInstance().getDecimalSeparator(); + // The quantity is parsed using a NumberFormat based on the default locale. + // To prevent issues, any dot decimal separators are replaced by the default locale decimal separator. + if (DOT_DECIMAL_SEPARATOR != defaultDecimalSeparator + && formatted.contains(String.valueOf(DOT_DECIMAL_SEPARATOR))) { + formatted = formatted.replace(DOT_DECIMAL_SEPARATOR, defaultDecimalSeparator); + } + Quantity absoluteQuantity = (Quantity) Quantities.getQuantity(formatted); quantity = Quantities.getQuantity(absoluteQuantity.getValue(), absoluteQuantity.getUnit(), Scale.RELATIVE); } diff --git a/bundles/org.openhab.core/src/test/java/org/openhab/core/internal/items/ItemStateConverterImplTest.java b/bundles/org.openhab.core/src/test/java/org/openhab/core/internal/items/ItemStateConverterImplTest.java index 8a56c2aae65a9..5323b6d557a3a 100644 --- a/bundles/org.openhab.core/src/test/java/org/openhab/core/internal/items/ItemStateConverterImplTest.java +++ b/bundles/org.openhab.core/src/test/java/org/openhab/core/internal/items/ItemStateConverterImplTest.java @@ -17,12 +17,16 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.*; +import java.util.Locale; +import java.util.stream.Stream; + import javax.measure.quantity.Length; import javax.measure.quantity.Temperature; import org.eclipse.jdt.annotation.NonNullByDefault; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; import org.openhab.core.i18n.UnitProvider; import org.openhab.core.items.Item; import org.openhab.core.library.items.NumberItem; @@ -44,6 +48,19 @@ public class ItemStateConverterImplTest { private @NonNullByDefault({}) ItemStateConverterImpl itemStateConverter; + /** + * Locales having a different decimal separator to test string parsing and generation. + */ + static Stream locales() { + return Stream.of( + // ٫ (Arabic, Egypt) + Locale.forLanguageTag("ar-EG"), + // , (German, Germany) + Locale.forLanguageTag("de-DE"), + // . (English, United States) + Locale.forLanguageTag("en-US")); + } + @BeforeEach public void setup() { UnitProvider unitProvider = mock(UnitProvider.class); @@ -51,16 +68,22 @@ public void setup() { itemStateConverter = new ItemStateConverterImpl(unitProvider); } - @Test - public void testNullState() { + @ParameterizedTest + @MethodSource("locales") + public void testNullState(Locale locale) { + Locale.setDefault(locale); + State undef = itemStateConverter.convertToAcceptedState(null, null); assertThat(undef, is(UnDefType.NULL)); } - @Test + @ParameterizedTest + @MethodSource("locales") @SuppressWarnings("PMD.CompareObjectsWithEquals") - public void testNoConversion() { + public void testNoConversion(Locale locale) { + Locale.setDefault(locale); + Item item = new NumberItem("number"); State originalState = new DecimalType(12.34); State state = itemStateConverter.convertToAcceptedState(originalState, item); @@ -68,8 +91,11 @@ public void testNoConversion() { assertTrue(originalState == state); } - @Test - public void testStateConversion() { + @ParameterizedTest + @MethodSource("locales") + public void testStateConversion(Locale locale) { + Locale.setDefault(locale); + Item item = new NumberItem("number"); State originalState = new PercentType("42"); State convertedState = itemStateConverter.convertToAcceptedState(originalState, item); @@ -77,8 +103,11 @@ public void testStateConversion() { assertThat(convertedState, is(new DecimalType("0.42"))); } - @Test - public void numberItemWithoutDimensionShouldConvertToDecimalType() { + @ParameterizedTest + @MethodSource("locales") + public void numberItemWithoutDimensionShouldConvertToDecimalType(Locale locale) { + Locale.setDefault(locale); + Item item = new NumberItem("number"); State originalState = new QuantityType<>("12.34 °C"); State convertedState = itemStateConverter.convertToAcceptedState(originalState, item); @@ -86,8 +115,11 @@ public void numberItemWithoutDimensionShouldConvertToDecimalType() { assertThat(convertedState, is(new DecimalType("12.34"))); } - @Test - public void numberItemWitDimensionShouldConvertToItemStateDescriptionUnit() { + @ParameterizedTest + @MethodSource("locales") + public void numberItemWitDimensionShouldConvertToItemStateDescriptionUnit(Locale locale) { + Locale.setDefault(locale); + NumberItem item = mock(NumberItem.class); StateDescription stateDescription = mock(StateDescription.class); when(item.getStateDescription()).thenReturn(stateDescription); @@ -100,8 +132,11 @@ public void numberItemWitDimensionShouldConvertToItemStateDescriptionUnit() { assertThat(convertedState, is(new QuantityType<>("285.49 K"))); } - @Test - public void numberItemWitDimensionShouldConvertToLocaleBasedUnit() { + @ParameterizedTest + @MethodSource("locales") + public void numberItemWitDimensionShouldConvertToLocaleBasedUnit(Locale locale) { + Locale.setDefault(locale); + NumberItem item = mock(NumberItem.class); doReturn(Temperature.class).when(item).getDimension(); @@ -111,8 +146,11 @@ public void numberItemWitDimensionShouldConvertToLocaleBasedUnit() { assertThat(convertedState, is(new QuantityType<>("54.212 °F"))); } - @Test - public void numberItemShouldNotConvertUnitsWhereMeasurmentSystemEquals() { + @ParameterizedTest + @MethodSource("locales") + public void numberItemShouldNotConvertUnitsWhereMeasurmentSystemEquals(Locale locale) { + Locale.setDefault(locale); + NumberItem item = mock(NumberItem.class); doReturn(Length.class).when(item).getDimension(); diff --git a/bundles/org.openhab.core/src/test/java/org/openhab/core/library/types/QuantityTypeArithmeticGroupFunctionTest.java b/bundles/org.openhab.core/src/test/java/org/openhab/core/library/types/QuantityTypeArithmeticGroupFunctionTest.java index 31dbed7250b5c..e4a6bddc4e460 100644 --- a/bundles/org.openhab.core/src/test/java/org/openhab/core/library/types/QuantityTypeArithmeticGroupFunctionTest.java +++ b/bundles/org.openhab.core/src/test/java/org/openhab/core/library/types/QuantityTypeArithmeticGroupFunctionTest.java @@ -15,7 +15,9 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import java.util.LinkedHashSet; +import java.util.Locale; import java.util.Set; +import java.util.stream.Stream; import javax.measure.Quantity; import javax.measure.quantity.Dimensionless; @@ -23,9 +25,10 @@ import javax.measure.quantity.Pressure; import javax.measure.quantity.Temperature; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; +import org.eclipse.jdt.annotation.NonNullByDefault; import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.openhab.core.i18n.UnitProvider; @@ -41,155 +44,204 @@ * @author Henning Treu - Initial contribution */ @ExtendWith(MockitoExtension.class) +@NonNullByDefault public class QuantityTypeArithmeticGroupFunctionTest { - private GroupFunction function; - private Set items; - - private @Mock UnitProvider unitProvider; - - @BeforeEach - public void init() { - items = new LinkedHashSet<>(); + private @NonNullByDefault({}) @Mock UnitProvider unitProvider; + + /** + * Locales having a different decimal separator to test string parsing and generation. + */ + static Stream locales() { + return Stream.of( + // ٫ (Arabic, Egypt) + Locale.forLanguageTag("ar-EG"), + // , (German, Germany) + Locale.forLanguageTag("de-DE"), + // . (English, United States) + Locale.forLanguageTag("en-US")); } - @Test - public void testSumFunctionQuantityType() { + @ParameterizedTest + @MethodSource("locales") + public void testSumFunctionQuantityType(Locale locale) { + Locale.setDefault(locale); + + Set items = new LinkedHashSet<>(); items.add(createNumberItem("TestItem1", Temperature.class, new QuantityType<>("23.54 °C"))); items.add(createNumberItem("TestItem2", Temperature.class, UnDefType.NULL)); items.add(createNumberItem("TestItem3", Temperature.class, new QuantityType<>("89 °C"))); items.add(createNumberItem("TestItem4", Temperature.class, UnDefType.UNDEF)); items.add(createNumberItem("TestItem5", Temperature.class, new QuantityType<>("122.41 °C"))); - function = new QuantityTypeArithmeticGroupFunction.Sum(Temperature.class); + GroupFunction function = new QuantityTypeArithmeticGroupFunction.Sum(Temperature.class); State state = function.calculate(items); assertEquals(new QuantityType<>("234.95 °C"), state); } - @Test - public void testSumFunctionQuantityTypeDifferentUnits() { + @ParameterizedTest + @MethodSource("locales") + public void testSumFunctionQuantityTypeDifferentUnits(Locale locale) { + Locale.setDefault(locale); + + Set items = new LinkedHashSet<>(); items.add(createNumberItem("TestItem1", Temperature.class, new QuantityType<>("23.54 °C"))); items.add(createNumberItem("TestItem2", Temperature.class, UnDefType.NULL)); items.add(createNumberItem("TestItem3", Temperature.class, new QuantityType<>("192.2 °F"))); items.add(createNumberItem("TestItem4", Temperature.class, UnDefType.UNDEF)); items.add(createNumberItem("TestItem5", Temperature.class, new QuantityType<>("395.56 K"))); - function = new QuantityTypeArithmeticGroupFunction.Sum(Temperature.class); + GroupFunction function = new QuantityTypeArithmeticGroupFunction.Sum(Temperature.class); State state = function.calculate(items); assertEquals(new QuantityType<>("234.95 °C"), state); } - @Test - public void testSumFunctionQuantityTypeIncompatibleUnits() { - items = new LinkedHashSet<>(); // we need an ordered set to guarantee the Unit of the first entry + @ParameterizedTest + @MethodSource("locales") + public void testSumFunctionQuantityTypeIncompatibleUnits(Locale locale) { + Locale.setDefault(locale); + + Set items = new LinkedHashSet<>(); // we need an ordered set to guarantee the Unit of the first entry items.add(createNumberItem("TestItem1", Temperature.class, new QuantityType<>("23.54 °C"))); items.add(createNumberItem("TestItem2", Temperature.class, UnDefType.NULL)); items.add(createNumberItem("TestItem3", Pressure.class, new QuantityType<>("192.2 hPa"))); - function = new QuantityTypeArithmeticGroupFunction.Sum(Temperature.class); + GroupFunction function = new QuantityTypeArithmeticGroupFunction.Sum(Temperature.class); State state = function.calculate(items); assertEquals(new QuantityType<>("23.54 °C"), state); } - @Test - public void testAvgFunctionQuantityType() { + @ParameterizedTest + @MethodSource("locales") + public void testAvgFunctionQuantityType(Locale locale) { + Locale.setDefault(locale); + + Set items = new LinkedHashSet<>(); items.add(createNumberItem("TestItem1", Temperature.class, new QuantityType<>("100 °C"))); items.add(createNumberItem("TestItem2", Temperature.class, UnDefType.NULL)); items.add(createNumberItem("TestItem3", Temperature.class, new QuantityType<>("200 °C"))); items.add(createNumberItem("TestItem4", Temperature.class, UnDefType.UNDEF)); items.add(createNumberItem("TestItem5", Temperature.class, new QuantityType<>("300 °C"))); - function = new QuantityTypeArithmeticGroupFunction.Avg(Temperature.class); + GroupFunction function = new QuantityTypeArithmeticGroupFunction.Avg(Temperature.class); State state = function.calculate(items); assertEquals(new QuantityType<>("200 °C"), state); } - @Test - public void testAvgFunctionQuantityTypeDifferentUnits() { + @ParameterizedTest + @MethodSource("locales") + public void testAvgFunctionQuantityTypeDifferentUnits(Locale locale) { + Locale.setDefault(locale); + + Set items = new LinkedHashSet<>(); items.add(createNumberItem("TestItem1", Temperature.class, new QuantityType<>("100 °C"))); items.add(createNumberItem("TestItem2", Temperature.class, UnDefType.NULL)); items.add(createNumberItem("TestItem3", Temperature.class, new QuantityType<>("113 °F"))); items.add(createNumberItem("TestItem4", Temperature.class, UnDefType.UNDEF)); items.add(createNumberItem("TestItem5", Temperature.class, new QuantityType<>("294.15 K"))); - function = new QuantityTypeArithmeticGroupFunction.Avg(Temperature.class); + GroupFunction function = new QuantityTypeArithmeticGroupFunction.Avg(Temperature.class); State state = function.calculate(items); assertEquals(new QuantityType<>("55.33333333333333333333333333333334 °C"), state); } - @Test - public void testAvgFunctionQuantityTypeIncompatibleUnits() { + @ParameterizedTest + @MethodSource("locales") + public void testAvgFunctionQuantityTypeIncompatibleUnits(Locale locale) { + Locale.setDefault(locale); + + Set items = new LinkedHashSet<>(); items.add(createNumberItem("TestItem1", Temperature.class, new QuantityType<>("23.54 °C"))); items.add(createNumberItem("TestItem2", Temperature.class, UnDefType.NULL)); items.add(createNumberItem("TestItem3", Pressure.class, new QuantityType<>("192.2 hPa"))); - function = new QuantityTypeArithmeticGroupFunction.Avg(Temperature.class); + GroupFunction function = new QuantityTypeArithmeticGroupFunction.Avg(Temperature.class); State state = function.calculate(items); assertEquals(new QuantityType<>("23.54 °C"), state); } - @Test - public void testMaxFunctionQuantityType() { + @ParameterizedTest + @MethodSource("locales") + public void testMaxFunctionQuantityType(Locale locale) { + Locale.setDefault(locale); + + Set items = new LinkedHashSet<>(); items.add(createNumberItem("TestItem1", Temperature.class, new QuantityType<>("100 °C"))); items.add(createNumberItem("TestItem2", Temperature.class, UnDefType.NULL)); items.add(createNumberItem("TestItem3", Temperature.class, new QuantityType<>("200 °C"))); items.add(createNumberItem("TestItem4", Temperature.class, UnDefType.UNDEF)); items.add(createNumberItem("TestItem5", Temperature.class, new QuantityType<>("300 °C"))); - function = new QuantityTypeArithmeticGroupFunction.Max(Temperature.class); + GroupFunction function = new QuantityTypeArithmeticGroupFunction.Max(Temperature.class); State state = function.calculate(items); assertEquals(new QuantityType<>("300 °C"), state); } - @Test - public void testMaxFunctionQuantityTypeDifferentUnits() { + @ParameterizedTest + @MethodSource("locales") + public void testMaxFunctionQuantityTypeDifferentUnits(Locale locale) { + Locale.setDefault(locale); + + Set items = new LinkedHashSet<>(); items.add(createNumberItem("TestItem1", Temperature.class, new QuantityType<>("100 °C"))); items.add(createNumberItem("TestItem2", Temperature.class, UnDefType.NULL)); items.add(createNumberItem("TestItem3", Temperature.class, new QuantityType<>("113 °F"))); items.add(createNumberItem("TestItem4", Temperature.class, UnDefType.UNDEF)); items.add(createNumberItem("TestItem5", Temperature.class, new QuantityType<>("294.15 K"))); - function = new QuantityTypeArithmeticGroupFunction.Max(Temperature.class); + GroupFunction function = new QuantityTypeArithmeticGroupFunction.Max(Temperature.class); State state = function.calculate(items); assertEquals(new QuantityType<>("100 °C"), state); } - @Test - public void testMaxFunctionQuantityTypeIncompatibleUnits() { + @ParameterizedTest + @MethodSource("locales") + public void testMaxFunctionQuantityTypeIncompatibleUnits(Locale locale) { + Locale.setDefault(locale); + + Set items = new LinkedHashSet<>(); items.add(createNumberItem("TestItem1", Temperature.class, new QuantityType<>("23.54 °C"))); items.add(createNumberItem("TestItem2", Temperature.class, UnDefType.NULL)); items.add(createNumberItem("TestItem3", Pressure.class, new QuantityType<>("192.2 hPa"))); - function = new QuantityTypeArithmeticGroupFunction.Max(Temperature.class); + GroupFunction function = new QuantityTypeArithmeticGroupFunction.Max(Temperature.class); State state = function.calculate(items); assertEquals(new QuantityType<>("23.54 °C"), state); } - @Test - public void testMinFunctionQuantityType() { + @ParameterizedTest + @MethodSource("locales") + public void testMinFunctionQuantityType(Locale locale) { + Locale.setDefault(locale); + + Set items = new LinkedHashSet<>(); items.add(createNumberItem("TestItem1", Temperature.class, new QuantityType<>("100 °C"))); items.add(createNumberItem("TestItem2", Temperature.class, UnDefType.NULL)); items.add(createNumberItem("TestItem3", Temperature.class, new QuantityType<>("200 °C"))); items.add(createNumberItem("TestItem4", Temperature.class, UnDefType.UNDEF)); items.add(createNumberItem("TestItem5", Temperature.class, new QuantityType<>("300 °C"))); - function = new QuantityTypeArithmeticGroupFunction.Min(Temperature.class); + GroupFunction function = new QuantityTypeArithmeticGroupFunction.Min(Temperature.class); State state = function.calculate(items); assertEquals(new QuantityType<>("100 °C"), state); } - @Test - public void testMaxFunctionQuantityTypeOnDimensionless() { + @ParameterizedTest + @MethodSource("locales") + public void testMaxFunctionQuantityTypeOnDimensionless(Locale locale) { + Locale.setDefault(locale); + + Set items = new LinkedHashSet<>(); items.add(createNumberItem("TestItem1", Dimensionless.class, new QuantityType<>("48 %"))); items.add(createNumberItem("TestItem2", Dimensionless.class, new QuantityType<>("36 %"))); items.add(createNumberItem("TestItem3", Dimensionless.class, new QuantityType<>("0 %"))); @@ -197,44 +249,56 @@ public void testMaxFunctionQuantityTypeOnDimensionless() { items.add(createNumberItem("TestItem5", Dimensionless.class, new QuantityType<>("0 %"))); items.add(createNumberItem("TestItem6", Dimensionless.class, new QuantityType<>("0 %"))); - function = new QuantityTypeArithmeticGroupFunction.Max(Dimensionless.class); + GroupFunction function = new QuantityTypeArithmeticGroupFunction.Max(Dimensionless.class); State state = function.calculate(items); assertEquals(new QuantityType<>("48 %"), state); } - @Test - public void testMinFunctionQuantityTypeDifferentUnits() { + @ParameterizedTest + @MethodSource("locales") + public void testMinFunctionQuantityTypeDifferentUnits(Locale locale) { + Locale.setDefault(locale); + + Set items = new LinkedHashSet<>(); items.add(createNumberItem("TestItem1", Temperature.class, new QuantityType<>("100 °C"))); items.add(createNumberItem("TestItem2", Temperature.class, UnDefType.NULL)); items.add(createNumberItem("TestItem3", Temperature.class, new QuantityType<>("113 °F"))); items.add(createNumberItem("TestItem4", Temperature.class, UnDefType.UNDEF)); items.add(createNumberItem("TestItem5", Temperature.class, new QuantityType<>("294.15 K"))); - function = new QuantityTypeArithmeticGroupFunction.Min(Temperature.class); + GroupFunction function = new QuantityTypeArithmeticGroupFunction.Min(Temperature.class); State state = function.calculate(items); assertEquals(new QuantityType<>("294.15 K"), state); } - @Test - public void testMinFunctionQuantityTypeIncompatibleUnits() { + @ParameterizedTest + @MethodSource("locales") + public void testMinFunctionQuantityTypeIncompatibleUnits(Locale locale) { + Locale.setDefault(locale); + + Set items = new LinkedHashSet<>(); items.add(createNumberItem("TestItem1", Temperature.class, new QuantityType<>("23.54 °C"))); items.add(createNumberItem("TestItem2", Temperature.class, UnDefType.NULL)); items.add(createNumberItem("TestItem3", Pressure.class, new QuantityType<>("192.2 hPa"))); - function = new QuantityTypeArithmeticGroupFunction.Min(Temperature.class); + GroupFunction function = new QuantityTypeArithmeticGroupFunction.Min(Temperature.class); State state = function.calculate(items); assertEquals(new QuantityType<>("23.54 °C"), state); } - @Test - public void testSumFunctionQuantityTypeWithGroups() { + @ParameterizedTest + @MethodSource("locales") + public void testSumFunctionQuantityTypeWithGroups(Locale locale) { + Locale.setDefault(locale); + + Set items = new LinkedHashSet<>(); items.add(createNumberItem("TestItem1", Power.class, new QuantityType<>("5 W"))); items.add(createGroupItem("TestGroup1", Power.class, new QuantityType<>("5 W"))); - function = new QuantityTypeArithmeticGroupFunction.Sum(Power.class); + GroupFunction function = new QuantityTypeArithmeticGroupFunction.Sum(Power.class); State state = function.calculate(items); assertEquals(new QuantityType<>("10 W"), state); diff --git a/bundles/org.openhab.core/src/test/java/org/openhab/core/library/types/QuantityTypeTest.java b/bundles/org.openhab.core/src/test/java/org/openhab/core/library/types/QuantityTypeTest.java index 584707286ef1a..29d1c35af190e 100644 --- a/bundles/org.openhab.core/src/test/java/org/openhab/core/library/types/QuantityTypeTest.java +++ b/bundles/org.openhab.core/src/test/java/org/openhab/core/library/types/QuantityTypeTest.java @@ -20,6 +20,8 @@ import java.lang.reflect.InvocationTargetException; import java.math.BigDecimal; import java.text.DecimalFormatSymbols; +import java.util.Locale; +import java.util.stream.Stream; import javax.measure.format.MeasurementParseException; import javax.measure.quantity.Dimensionless; @@ -31,6 +33,8 @@ import javax.measure.quantity.Time; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; import org.openhab.core.library.dimension.DataAmount; import org.openhab.core.library.dimension.DataTransferRate; import org.openhab.core.library.dimension.Density; @@ -49,8 +53,18 @@ @SuppressWarnings("null") public class QuantityTypeTest { - // we need to get the decimal separator of the default locale for our tests - private static final char SEP = new DecimalFormatSymbols().getDecimalSeparator(); + /** + * Locales having a different decimal separator to test string parsing and generation. + */ + static Stream locales() { + return Stream.of( + // ٫ (Arabic, Egypt) + Locale.forLanguageTag("ar-EG"), + // , (German, Germany) + Locale.forLanguageTag("de-DE"), + // . (English, United States) + Locale.forLanguageTag("en-US")); + } @Test public void testDimensionless() { @@ -128,12 +142,14 @@ public void testFormats() { QuantityType