From 6349242e7a26e342d48b1aa7ad060cd5ff11bfcf Mon Sep 17 00:00:00 2001 From: Benjamin Peter Date: Thu, 21 Nov 2024 15:30:03 +0100 Subject: [PATCH 1/4] Make NumberFormatterImpl work with UTF8 and allow optional exponential separator --- .../chartfx/utils/NumberFormatterImpl.java | 49 ++++++++++++++----- .../utils/NumberFormatterImplTest.java | 20 +++++++- 2 files changed, 54 insertions(+), 15 deletions(-) diff --git a/chartfx-chart/src/main/java/io/fair_acc/chartfx/utils/NumberFormatterImpl.java b/chartfx-chart/src/main/java/io/fair_acc/chartfx/utils/NumberFormatterImpl.java index 470539486..80dcb4d60 100644 --- a/chartfx-chart/src/main/java/io/fair_acc/chartfx/utils/NumberFormatterImpl.java +++ b/chartfx-chart/src/main/java/io/fair_acc/chartfx/utils/NumberFormatterImpl.java @@ -1,18 +1,24 @@ package io.fair_acc.chartfx.utils; -import static java.lang.Math.*; - -import static io.fair_acc.chartfx.utils.Schubfach.*; - +import static io.fair_acc.chartfx.utils.Schubfach.H_DOUBLE; +import static io.fair_acc.chartfx.utils.Schubfach.getDecimalLength; +import static io.fair_acc.chartfx.utils.Schubfach.getNormalizationScale; +import static java.lang.Math.multiplyHigh; + +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.text.DecimalFormatSymbols; +import java.util.Arrays; import java.util.Objects; import javafx.util.StringConverter; public class NumberFormatterImpl extends StringConverter implements NumberFormatter { - public final static char DEFAULT_DECIMAL_SEPARATOR = ' '; - + + private static final Charset CHARSET = StandardCharsets.UTF_8; + public NumberFormatterImpl() { super(); setDecimalFormatSymbols(DecimalFormatSymbols.getInstance()); @@ -38,6 +44,10 @@ public int getDecimalPlaces() { public boolean isExponentialForm() { return isExponentialForm; } + + public final char getExponentialSeparator() { + return CHARSET.decode(ByteBuffer.wrap(exponentialSeparator)).get(); + } @Override public NumberFormatter setExponentialForm(final boolean state) { @@ -50,6 +60,10 @@ public NumberFormatter setDecimalPlaces(final int decimalPlaces) { this.decimalPlaces = Math.max(ALL_DIGITS, decimalPlaces); return this; } + + public final void setExponentialSeparator(char separator) { + exponentialSeparator = charToBytes(separator); + } @Override public String toString(final double val) { @@ -123,6 +137,7 @@ private void toExponentialFormat(int h, int m, int l, int e) { append8Digits(m); lowDigits(l); } + append(exponentialSeparator); exponent(e - 1); } @@ -277,8 +292,11 @@ private void removeTrailingZeroes() { length--; } // remove trailing comma - if (bytes[length - 1] == DOT) { - length--; + if (length >= DOT.length && + Arrays.equals( + bytes, (length - DOT.length), length - DOT.length + 1, + DOT, 0, DOT.length)) { + length -= DOT.length; } } @@ -318,7 +336,11 @@ private void exponent(int e) { } private String bytesToString() { - return new String(bytes, 0, length, StandardCharsets.ISO_8859_1); + return new String(bytes, 0, length, CHARSET); + } + + private static byte[] charToBytes(char c) { + return CHARSET.encode(CharBuffer.wrap(new char[] { c })).array(); } static final int ALL_DIGITS = -1; @@ -346,6 +368,7 @@ private String bytesToString() { int length = 0; boolean isExponentialForm = false; + private byte[] exponentialSeparator = {}; // Used for left-to-tight digit extraction. private static final int MASK_28 = (1 << 28) - 1; @@ -355,14 +378,14 @@ public NumberFormatterImpl setDecimalFormatSymbols(DecimalFormatSymbols symbols) if (exp.length() > MAX_EXP_LENGTH) { throw new IllegalArgumentException("Exponent separator can't be longer than " + MAX_EXP_LENGTH); } - this.EXP = Objects.equals(exp, "E") ? DEFAULT_EXP : exp.getBytes(StandardCharsets.ISO_8859_1); - this.DOT = (byte) symbols.getDecimalSeparator(); + this.EXP = Objects.equals(exp, "E") ? DEFAULT_EXP : exp.getBytes(CHARSET); + this.DOT = charToBytes(symbols.getDecimalSeparator()); return this; } - byte DOT = '.'; + byte[] DOT = charToBytes('.'); byte[] EXP = DEFAULT_EXP; - private static final byte[] DEFAULT_EXP = new byte[] { 'E' }; + private static final byte[] DEFAULT_EXP = charToBytes('E'); private static final byte ZERO = (byte) '0'; private static final byte MINUS = (byte) '-'; } diff --git a/chartfx-chart/src/test/java/io/fair_acc/chartfx/utils/NumberFormatterImplTest.java b/chartfx-chart/src/test/java/io/fair_acc/chartfx/utils/NumberFormatterImplTest.java index b128fdcac..0fa29c439 100644 --- a/chartfx-chart/src/test/java/io/fair_acc/chartfx/utils/NumberFormatterImplTest.java +++ b/chartfx-chart/src/test/java/io/fair_acc/chartfx/utils/NumberFormatterImplTest.java @@ -5,6 +5,7 @@ import static io.fair_acc.chartfx.utils.NumberFormatterImpl.*; import java.util.Locale; +import java.util.Optional; import java.util.function.DoubleFunction; import org.junit.jupiter.api.Test; @@ -109,11 +110,26 @@ void testPlainRounding() { assertEquals("0.01", formatter.apply(0.010000000000000004)); assertEquals("0.00", formatter.apply(0.001000000000000004)); } - - private static DoubleFunction createFormatter(boolean exponentialForm, int decimalPlaces) { + + @Test + void exponentialSeparator() { + Locale.setDefault(Locale.US); + var formatter = createFormatter(true, 5, Optional.of('\u202F')); + assertEquals("2.10000\u202FE0", formatter.apply(2.1)); + assertEquals("1.00000\u202FE1", formatter.apply(10)); + assertEquals("1.23457\u202FE14", formatter.apply(123.456789E12)); + assertEquals("1.23457\u202FE-10", formatter.apply(123.456789E-12)); + } + + private static DoubleFunction createFormatter(boolean exponentialForm, int decimalPlaces, Optional exponentialSeparator) { var formatter = new NumberFormatterImpl(); formatter.setExponentialForm(exponentialForm); formatter.setDecimalPlaces(decimalPlaces); + exponentialSeparator.ifPresent(sep -> formatter.setExponentialSeparator(sep)); return formatter::toString; } + + private static DoubleFunction createFormatter(boolean exponentialForm, int decimalPlaces) { + return createFormatter(exponentialForm, decimalPlaces, Optional.empty()); + } } From 0ebc17f100ea3c5244444f5ae52f35e500e6d31c Mon Sep 17 00:00:00 2001 From: Benjamin Peter Date: Thu, 21 Nov 2024 16:11:11 +0100 Subject: [PATCH 2/4] Make NumberFormatterImpl#fromString work if whitespace is used as exponential separator --- .../chartfx/utils/NumberFormatterImpl.java | 16 +++++++- .../axes/spi/format/DefaultFormatterTest.java | 41 +++++++++++++++++++ .../utils/NumberFormatterImplTest.java | 39 ++++++++++++++---- 3 files changed, 87 insertions(+), 9 deletions(-) create mode 100644 chartfx-chart/src/test/java/io/fair_acc/chartfx/axes/spi/format/DefaultFormatterTest.java diff --git a/chartfx-chart/src/main/java/io/fair_acc/chartfx/utils/NumberFormatterImpl.java b/chartfx-chart/src/main/java/io/fair_acc/chartfx/utils/NumberFormatterImpl.java index 80dcb4d60..62762ed7d 100644 --- a/chartfx-chart/src/main/java/io/fair_acc/chartfx/utils/NumberFormatterImpl.java +++ b/chartfx-chart/src/main/java/io/fair_acc/chartfx/utils/NumberFormatterImpl.java @@ -12,6 +12,7 @@ import java.text.DecimalFormatSymbols; import java.util.Arrays; import java.util.Objects; +import java.util.regex.Pattern; import javafx.util.StringConverter; @@ -19,6 +20,8 @@ public class NumberFormatterImpl extends StringConverter implements Numb private static final Charset CHARSET = StandardCharsets.UTF_8; + private static final Pattern UNICODE_WHITESPACE_PATTERN = Pattern.compile("(?U)\\s"); + public NumberFormatterImpl() { super(); setDecimalFormatSymbols(DecimalFormatSymbols.getInstance()); @@ -32,7 +35,12 @@ public NumberFormatterImpl(final int decimalPlaces, final boolean exponentialFor @Override public Number fromString(final String string) { - return Double.parseDouble(string); + if (exponentialSeparator.length == 0) { + return Double.parseDouble(string); + } + + String cleanedString = UNICODE_WHITESPACE_PATTERN.matcher(string).replaceAll(""); + return Double.parseDouble(cleanedString); } @Override @@ -61,6 +69,12 @@ public NumberFormatter setDecimalPlaces(final int decimalPlaces) { return this; } + /** + * Sets the separator to use between decimal places and exponential e.g. {@code 1_HERE_E-10}. This can break the + * functionality of {@link #fromString(String)} if a non-whitespace character is used. Default: none. + * + * @param separator Character to use + */ public final void setExponentialSeparator(char separator) { exponentialSeparator = charToBytes(separator); } diff --git a/chartfx-chart/src/test/java/io/fair_acc/chartfx/axes/spi/format/DefaultFormatterTest.java b/chartfx-chart/src/test/java/io/fair_acc/chartfx/axes/spi/format/DefaultFormatterTest.java new file mode 100644 index 000000000..5131e8c50 --- /dev/null +++ b/chartfx-chart/src/test/java/io/fair_acc/chartfx/axes/spi/format/DefaultFormatterTest.java @@ -0,0 +1,41 @@ +package io.fair_acc.chartfx.axes.spi.format; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +import io.fair_acc.dataset.spi.fastutil.DoubleArrayList; + +class DefaultFormatterTest { + + @Test + void formatAndParsingWorks() throws Exception { + DefaultFormatter formatter = new DefaultFormatter(); + formatter.updateFormatter(DoubleArrayList.wrap(new double[]{1e-5, 10}), 1.); + + final double value = 0.01; + String formatted = formatter.toString(value); + assertEquals("1E-2", formatted); + + Number parsed = formatter.fromString(formatted); + assertEquals(value, parsed.doubleValue()); + } + + @Test + void formatAndParsingWorksWithExponentialSeparator() throws Exception { + DefaultFormatter formatter = new DefaultFormatter() { + { + formatter.setExponentialSeparator('\u202F'); + } + }; + formatter.updateFormatter(DoubleArrayList.wrap(new double[]{1e-5, 10}), 1.); + + final double value = 0.01; + String formatted = formatter.toString(value); + assertEquals("1\u202FE-2", formatted); + + Number parsed = formatter.fromString(formatted); + assertEquals(value, parsed.doubleValue()); + } + +} diff --git a/chartfx-chart/src/test/java/io/fair_acc/chartfx/utils/NumberFormatterImplTest.java b/chartfx-chart/src/test/java/io/fair_acc/chartfx/utils/NumberFormatterImplTest.java index 0fa29c439..3e63664d8 100644 --- a/chartfx-chart/src/test/java/io/fair_acc/chartfx/utils/NumberFormatterImplTest.java +++ b/chartfx-chart/src/test/java/io/fair_acc/chartfx/utils/NumberFormatterImplTest.java @@ -55,6 +55,15 @@ void exponentialFormat() { assertEquals("1.23456789E14", formatter.apply(123.456789E12)); assertEquals("1.23456789E-10", formatter.apply(123.456789E-12)); } + + @Test + void exponentialFormatParsing() { + Locale.setDefault(Locale.US); + var formatter = createFormatterImpl(true, ALL_DIGITS, Optional.empty()); + assertEquals(0., formatter.fromString("0E0")); + assertEquals(2.1,formatter.fromString("2.1E0")); + assertEquals(123.456789E-12, formatter.fromString("1.23456789E-10")); + } @Test void plain5Decimals() { @@ -114,22 +123,36 @@ void testPlainRounding() { @Test void exponentialSeparator() { Locale.setDefault(Locale.US); - var formatter = createFormatter(true, 5, Optional.of('\u202F')); - assertEquals("2.10000\u202FE0", formatter.apply(2.1)); - assertEquals("1.00000\u202FE1", formatter.apply(10)); - assertEquals("1.23457\u202FE14", formatter.apply(123.456789E12)); - assertEquals("1.23457\u202FE-10", formatter.apply(123.456789E-12)); + var formatter = createFormatterImpl(true, 5, Optional.of('\u202F')); + assertEquals("2.10000\u202FE0", formatter.toString(2.1)); + assertEquals("1.00000\u202FE1", formatter.toString(10)); + assertEquals("1.23457\u202FE14", formatter.toString(123.456789E12)); + assertEquals("1.23457\u202FE-10", formatter.toString(123.456789E-12)); + } + + @Test + void exponentialSeparatorParsing() { + Locale.setDefault(Locale.US); + var formatter = createFormatterImpl(true, 5, Optional.of('\u202F')); + assertEquals(2.1, formatter.fromString("2.10000\u202FE0")); + assertEquals(10., formatter.fromString("1.00000\u202FE1")); + assertEquals(1.23457E14, formatter.fromString("1.23457\u202FE14")); + assertEquals(1.23457E-10, formatter.fromString("1.23457\u202FE-10")); } - private static DoubleFunction createFormatter(boolean exponentialForm, int decimalPlaces, Optional exponentialSeparator) { + private static NumberFormatterImpl createFormatterImpl( + boolean exponentialForm, + int decimalPlaces, + Optional exponentialSeparator) { var formatter = new NumberFormatterImpl(); formatter.setExponentialForm(exponentialForm); formatter.setDecimalPlaces(decimalPlaces); exponentialSeparator.ifPresent(sep -> formatter.setExponentialSeparator(sep)); - return formatter::toString; + return formatter; } private static DoubleFunction createFormatter(boolean exponentialForm, int decimalPlaces) { - return createFormatter(exponentialForm, decimalPlaces, Optional.empty()); + var formatter = createFormatterImpl(exponentialForm, decimalPlaces, Optional.empty()); + return formatter::toString; } } From b2e5413c09e3768c9020d9865557a3736eb94ba4 Mon Sep 17 00:00:00 2001 From: Benjamin Peter Date: Mon, 9 Dec 2024 08:46:18 +0100 Subject: [PATCH 3/4] NumberFormatterImpl: Rename fields to pass quality gate --- .../chartfx/utils/NumberFormatterImpl.java | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/chartfx-chart/src/main/java/io/fair_acc/chartfx/utils/NumberFormatterImpl.java b/chartfx-chart/src/main/java/io/fair_acc/chartfx/utils/NumberFormatterImpl.java index 62762ed7d..b96e74473 100644 --- a/chartfx-chart/src/main/java/io/fair_acc/chartfx/utils/NumberFormatterImpl.java +++ b/chartfx-chart/src/main/java/io/fair_acc/chartfx/utils/NumberFormatterImpl.java @@ -144,10 +144,10 @@ private void encodeDouble(boolean negative, long f, int e) { private void toExponentialFormat(int h, int m, int l, int e) { appendDigit(h); if (decimalPlaces > 0) { - append(DOT); + append(dot); appendNDigits(m, l, decimalPlaces); } else if (decimalPlaces == ALL_DIGITS) { - append(DOT); + append(dot); append8Digits(m); lowDigits(l); } @@ -168,7 +168,7 @@ private void toPlainFormat(int h, int m, int l, int e) { if (decimalPlaces == 0) { return; } - append(DOT); + append(dot); if (decimalPlaces == ALL_DIGITS) { for (; i <= 8; ++i) { t = 10 * y; @@ -193,7 +193,7 @@ private void toPlainFormatWithLeadingZeros(int h, int m, int l, int e) { if (decimalPlaces == 0) { return; } - append(DOT); + append(dot); int spaceLeft = bytes.length - length; if (decimalPlaces == ALL_DIGITS) { for (; e < 0 && spaceLeft > 0; ++e) { @@ -222,13 +222,13 @@ private void encodeZero() { length = 0; append(ZERO); if (decimalPlaces > 0) { - append(DOT); + append(dot); for (int i = 0; i < decimalPlaces; i++) { append(ZERO); } } if (isExponentialForm) { - append(EXP); + append(exp); append(ZERO); } } @@ -306,11 +306,11 @@ private void removeTrailingZeroes() { length--; } // remove trailing comma - if (length >= DOT.length && + if (length >= dot.length && Arrays.equals( - bytes, (length - DOT.length), length - DOT.length + 1, - DOT, 0, DOT.length)) { - length -= DOT.length; + bytes, (length - dot.length), length - dot.length + 1, + dot, 0, dot.length)) { + length -= dot.length; } } @@ -331,7 +331,7 @@ private int y(int a) { } private void exponent(int e) { - append(EXP); + append(exp); if (e < 0) { append(MINUS); e = -e; @@ -388,17 +388,17 @@ private static byte[] charToBytes(char c) { private static final int MASK_28 = (1 << 28) - 1; public NumberFormatterImpl setDecimalFormatSymbols(DecimalFormatSymbols symbols) { - String exp = symbols.getExponentSeparator(); - if (exp.length() > MAX_EXP_LENGTH) { + String expString = symbols.getExponentSeparator(); + if (expString.length() > MAX_EXP_LENGTH) { throw new IllegalArgumentException("Exponent separator can't be longer than " + MAX_EXP_LENGTH); } - this.EXP = Objects.equals(exp, "E") ? DEFAULT_EXP : exp.getBytes(CHARSET); - this.DOT = charToBytes(symbols.getDecimalSeparator()); + this.exp = Objects.equals(expString, "E") ? DEFAULT_EXP : expString.getBytes(CHARSET); + this.dot = charToBytes(symbols.getDecimalSeparator()); return this; } - byte[] DOT = charToBytes('.'); - byte[] EXP = DEFAULT_EXP; + byte[] dot = charToBytes('.'); + byte[] exp = DEFAULT_EXP; private static final byte[] DEFAULT_EXP = charToBytes('E'); private static final byte ZERO = (byte) '0'; private static final byte MINUS = (byte) '-'; From 295bc963bd969df4c6d2527213fd7eeb59578d19 Mon Sep 17 00:00:00 2001 From: Benjamin Peter Date: Mon, 9 Dec 2024 08:46:39 +0100 Subject: [PATCH 4/4] DefaultFormatterTest: remove throws declaration to pass quality gate --- .../chartfx/axes/spi/format/DefaultFormatterTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/chartfx-chart/src/test/java/io/fair_acc/chartfx/axes/spi/format/DefaultFormatterTest.java b/chartfx-chart/src/test/java/io/fair_acc/chartfx/axes/spi/format/DefaultFormatterTest.java index 5131e8c50..dc874213a 100644 --- a/chartfx-chart/src/test/java/io/fair_acc/chartfx/axes/spi/format/DefaultFormatterTest.java +++ b/chartfx-chart/src/test/java/io/fair_acc/chartfx/axes/spi/format/DefaultFormatterTest.java @@ -9,7 +9,7 @@ class DefaultFormatterTest { @Test - void formatAndParsingWorks() throws Exception { + void formatAndParsingWorks() { DefaultFormatter formatter = new DefaultFormatter(); formatter.updateFormatter(DoubleArrayList.wrap(new double[]{1e-5, 10}), 1.); @@ -22,7 +22,7 @@ void formatAndParsingWorks() throws Exception { } @Test - void formatAndParsingWorksWithExponentialSeparator() throws Exception { + void formatAndParsingWorksWithExponentialSeparator() { DefaultFormatter formatter = new DefaultFormatter() { { formatter.setExponentialSeparator('\u202F');