This is an automated email from the ASF dual-hosted git repository.
delei pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/fesod.git
The following commit(s) were added to refs/heads/main by this push:
new 4b426ba3 feat: cache date and number formatters (#948)
4b426ba3 is described below
commit 4b426ba37b9e7b00d093883acbd5669881ab6555
Author: Duansg <[email protected]>
AuthorDate: Fri Jul 17 19:33:56 2026 +0800
feat: cache date and number formatters (#948)
* feat: cache date and number formatters
* modify and supplement
---------
Co-authored-by: DeleiGuo <[email protected]>
---
.../org/apache/fesod/common/util/MapUtils.java | 32 ++++++
.../fesod/sheet/analysis/ExcelAnalyserImpl.java | 2 +
.../fesod/sheet/context/WriteContextImpl.java | 2 +
.../org/apache/fesod/sheet/util/DateUtils.java | 57 ++++++----
.../org/apache/fesod/sheet/util/NumberUtils.java | 50 ++++++++-
.../org/apache/fesod/sheet/util/DateUtilsTest.java | 78 ++++++++++++++
.../apache/fesod/sheet/util/NumberUtilsTest.java | 120 +++++++++++++++++++++
7 files changed, 316 insertions(+), 25 deletions(-)
diff --git
a/fesod-common/src/main/java/org/apache/fesod/common/util/MapUtils.java
b/fesod-common/src/main/java/org/apache/fesod/common/util/MapUtils.java
index a593b218..0d0e5b98 100644
--- a/fesod-common/src/main/java/org/apache/fesod/common/util/MapUtils.java
+++ b/fesod-common/src/main/java/org/apache/fesod/common/util/MapUtils.java
@@ -27,12 +27,44 @@ package org.apache.fesod.common.util;
import java.util.HashMap;
import java.util.LinkedHashMap;
+import java.util.Map;
import java.util.TreeMap;
public class MapUtils {
private MapUtils() {}
+ /**
+ * A {@link LinkedHashMap} bounded to {@code maxEntries}, evicting the
eldest entry on overflow (FIFO). Insertion
+ * order keeps {@link #get} non-mutating on hot paths.
+ */
+ private static final class BoundedLinkedHashMap<K, V> extends
LinkedHashMap<K, V> {
+ private static final long serialVersionUID = 1L;
+
+ private final int maxEntries;
+
+ private BoundedLinkedHashMap(int maxEntries) {
+ super(capacity(maxEntries), 0.75f, false);
+ this.maxEntries = maxEntries;
+ }
+
+ @Override
+ protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
+ return size() > maxEntries;
+ }
+ }
+
+ /**
+ * Creates an empty {@code Map} that retains at most {@code maxEntries}
entries, evicting the eldest on overflow.
+ * Handy for thread-local caches keyed by an unbounded input set.
+ *
+ * @param maxEntries the maximum number of entries to retain; must be
positive
+ * @return a new, empty bounded {@code Map}
+ */
+ public static <K, V> Map<K, V> newBoundedMap(int maxEntries) {
+ return new BoundedLinkedHashMap<>(maxEntries);
+ }
+
/**
* Creates a <i>mutable</i>, empty {@code HashMap} instance.
*
diff --git
a/fesod-sheet/src/main/java/org/apache/fesod/sheet/analysis/ExcelAnalyserImpl.java
b/fesod-sheet/src/main/java/org/apache/fesod/sheet/analysis/ExcelAnalyserImpl.java
index ba9562d8..14ed82bf 100644
---
a/fesod-sheet/src/main/java/org/apache/fesod/sheet/analysis/ExcelAnalyserImpl.java
+++
b/fesod-sheet/src/main/java/org/apache/fesod/sheet/analysis/ExcelAnalyserImpl.java
@@ -53,6 +53,7 @@ import org.apache.fesod.sheet.util.ClassUtils;
import org.apache.fesod.sheet.util.DateUtils;
import org.apache.fesod.sheet.util.FileUtils;
import org.apache.fesod.sheet.util.NumberDataFormatterUtils;
+import org.apache.fesod.sheet.util.NumberUtils;
import org.apache.poi.hssf.OldExcelFormatException;
import org.apache.poi.hssf.record.crypto.Biff8EncryptionKey;
import org.apache.poi.poifs.crypt.Decryptor;
@@ -292,6 +293,7 @@ public class ExcelAnalyserImpl implements ExcelAnalyser {
*/
private void removeThreadLocalCache() {
NumberDataFormatterUtils.removeThreadLocalCache();
+ NumberUtils.removeThreadLocalCache();
DateUtils.removeThreadLocalCache();
ClassUtils.removeThreadLocalCache();
}
diff --git
a/fesod-sheet/src/main/java/org/apache/fesod/sheet/context/WriteContextImpl.java
b/fesod-sheet/src/main/java/org/apache/fesod/sheet/context/WriteContextImpl.java
index 5a176860..e97149e2 100644
---
a/fesod-sheet/src/main/java/org/apache/fesod/sheet/context/WriteContextImpl.java
+++
b/fesod-sheet/src/main/java/org/apache/fesod/sheet/context/WriteContextImpl.java
@@ -45,6 +45,7 @@ import org.apache.fesod.sheet.util.ClassUtils;
import org.apache.fesod.sheet.util.DateUtils;
import org.apache.fesod.sheet.util.FileUtils;
import org.apache.fesod.sheet.util.NumberDataFormatterUtils;
+import org.apache.fesod.sheet.util.NumberUtils;
import org.apache.fesod.sheet.util.WorkBookUtil;
import org.apache.fesod.sheet.util.WriteHandlerUtils;
import org.apache.fesod.sheet.write.handler.context.CellWriteHandlerContext;
@@ -561,6 +562,7 @@ public class WriteContextImpl implements WriteContext {
*/
private void removeThreadLocalCache() {
NumberDataFormatterUtils.removeThreadLocalCache();
+ NumberUtils.removeThreadLocalCache();
DateUtils.removeThreadLocalCache();
ClassUtils.removeThreadLocalCache();
}
diff --git
a/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/DateUtils.java
b/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/DateUtils.java
index 35025d98..53a70c1b 100644
--- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/DateUtils.java
+++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/DateUtils.java
@@ -49,6 +49,11 @@ import org.apache.poi.util.LocaleUtil;
* Date utils
*/
public class DateUtils {
+
+ private static final int MAX_LOCALE_CACHE_SIZE = 8;
+
+ private static final int MAX_FORMAT_CACHE_SIZE = 64;
+
/**
* Is a cache of dates
*/
@@ -58,6 +63,13 @@ public class DateUtils {
*/
private static final ThreadLocal<Map<String, SimpleDateFormat>>
DATE_FORMAT_THREAD_LOCAL = new ThreadLocal<>();
+ /**
+ * Bounded cache of {@link DateTimeFormatter}, nested by resolved locale
then pattern. Both levels evict FIFO, and
+ * the whole cache is cleared by {@link #removeThreadLocalCache()}.
+ */
+ private static final ThreadLocal<Map<Locale, Map<String,
DateTimeFormatter>>> DATE_TIME_FORMATTER_THREAD_LOCAL =
+ new ThreadLocal<>();
+
/**
* The following patterns are used in {@link #isADateFormat(Short, String)}
*/
@@ -126,11 +138,7 @@ public class DateUtils {
if (StringUtils.isEmpty(dateFormat)) {
dateFormat = switchDateFormat(dateString);
}
- if (local == null) {
- return LocalDateTime.parse(dateString,
DateTimeFormatter.ofPattern(dateFormat));
- } else {
- return LocalDateTime.parse(dateString,
DateTimeFormatter.ofPattern(dateFormat, local));
- }
+ return LocalDateTime.parse(dateString,
getCacheDateTimeFormat(dateFormat, local));
}
/**
@@ -145,11 +153,7 @@ public class DateUtils {
if (StringUtils.isEmpty(dateFormat)) {
dateFormat = switchDateFormat(dateString);
}
- if (local == null) {
- return LocalDate.parse(dateString,
DateTimeFormatter.ofPattern(dateFormat));
- } else {
- return LocalDate.parse(dateString,
DateTimeFormatter.ofPattern(dateFormat, local));
- }
+ return LocalDate.parse(dateString, getCacheDateTimeFormat(dateFormat,
local));
}
/**
@@ -238,11 +242,7 @@ public class DateUtils {
if (StringUtils.isEmpty(dateFormat)) {
dateFormat = defaultDateFormat;
}
- if (local == null) {
- return date.format(DateTimeFormatter.ofPattern(dateFormat));
- } else {
- return date.format(DateTimeFormatter.ofPattern(dateFormat, local));
- }
+ return date.format(getCacheDateTimeFormat(dateFormat, local));
}
/**
@@ -270,11 +270,7 @@ public class DateUtils {
if (StringUtils.isEmpty(dateFormat)) {
dateFormat = defaultLocalDateFormat;
}
- if (local == null) {
- return date.format(DateTimeFormatter.ofPattern(dateFormat));
- } else {
- return date.format(DateTimeFormatter.ofPattern(dateFormat, local));
- }
+ return date.format(getCacheDateTimeFormat(dateFormat, local));
}
/**
@@ -304,6 +300,26 @@ public class DateUtils {
return format(localDateTime, dateFormat);
}
+ private static DateTimeFormatter getCacheDateTimeFormat(String dateFormat,
Locale locale) {
+ Locale actualLocale = locale == null ?
Locale.getDefault(Locale.Category.FORMAT) : locale;
+ Map<Locale, Map<String, DateTimeFormatter>> localeCache =
DATE_TIME_FORMATTER_THREAD_LOCAL.get();
+ if (localeCache == null) {
+ localeCache = MapUtils.newBoundedMap(MAX_LOCALE_CACHE_SIZE);
+ DATE_TIME_FORMATTER_THREAD_LOCAL.set(localeCache);
+ }
+ Map<String, DateTimeFormatter> formatCache =
localeCache.get(actualLocale);
+ if (formatCache == null) {
+ formatCache = MapUtils.newBoundedMap(MAX_FORMAT_CACHE_SIZE);
+ localeCache.put(actualLocale, formatCache);
+ }
+ DateTimeFormatter formatter = formatCache.get(dateFormat);
+ if (formatter == null) {
+ formatter = DateTimeFormatter.ofPattern(dateFormat, actualLocale);
+ formatCache.put(dateFormat, formatter);
+ }
+ return formatter;
+ }
+
private static DateFormat getCacheDateFormat(String dateFormat) {
Map<String, SimpleDateFormat> dateFormatMap =
DATE_FORMAT_THREAD_LOCAL.get();
if (dateFormatMap == null) {
@@ -573,5 +589,6 @@ public class DateUtils {
public static void removeThreadLocalCache() {
DATE_THREAD_LOCAL.remove();
DATE_FORMAT_THREAD_LOCAL.remove();
+ DATE_TIME_FORMATTER_THREAD_LOCAL.remove();
}
}
diff --git
a/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/NumberUtils.java
b/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/NumberUtils.java
index b1913e65..4d516370 100644
--- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/NumberUtils.java
+++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/NumberUtils.java
@@ -28,7 +28,11 @@ package org.apache.fesod.sheet.util;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.text.DecimalFormat;
+import java.text.DecimalFormatSymbols;
import java.text.ParseException;
+import java.util.Locale;
+import java.util.Map;
+import org.apache.fesod.common.util.MapUtils;
import org.apache.fesod.common.util.StringUtils;
import org.apache.fesod.sheet.metadata.data.WriteCellData;
import org.apache.fesod.sheet.metadata.property.ExcelContentProperty;
@@ -39,6 +43,18 @@ import
org.apache.fesod.sheet.metadata.property.ExcelContentProperty;
*
*/
public class NumberUtils {
+
+ private static final int MAX_LOCALE_CACHE_SIZE = 4;
+
+ private static final int MAX_FORMAT_CACHE_SIZE = 64;
+
+ /**
+ * Bounded cache of {@link DecimalFormat}, nested by default FORMAT locale
then pattern. {@link DecimalFormat} is
+ * not thread-safe, so it is thread-local; both levels evict FIFO to keep
long-lived threads bounded.
+ */
+ private static final ThreadLocal<Map<Locale, Map<String, DecimalFormat>>>
DECIMAL_FORMAT_THREAD_LOCAL =
+ new ThreadLocal<>();
+
private NumberUtils() {}
/**
@@ -60,9 +76,7 @@ public class NumberUtils {
}
String format = contentProperty.getNumberFormatProperty().getFormat();
RoundingMode roundingMode =
contentProperty.getNumberFormatProperty().getRoundingMode();
- DecimalFormat decimalFormat = new DecimalFormat(format);
- decimalFormat.setRoundingMode(roundingMode);
- return decimalFormat.format(num);
+ return getCacheDecimalFormat(format, roundingMode).format(num);
}
/**
@@ -212,9 +226,35 @@ public class NumberUtils {
private static Number parse(String string, ExcelContentProperty
contentProperty) throws ParseException {
String format = contentProperty.getNumberFormatProperty().getFormat();
RoundingMode roundingMode =
contentProperty.getNumberFormatProperty().getRoundingMode();
- DecimalFormat decimalFormat = new DecimalFormat(format);
- decimalFormat.setRoundingMode(roundingMode);
+ DecimalFormat decimalFormat = getCacheDecimalFormat(format,
roundingMode);
decimalFormat.setParseBigDecimal(true);
return decimalFormat.parse(string);
}
+
+ private static DecimalFormat getCacheDecimalFormat(String format,
RoundingMode roundingMode) {
+ Locale locale = Locale.getDefault(Locale.Category.FORMAT);
+ Map<Locale, Map<String, DecimalFormat>> localeCache =
DECIMAL_FORMAT_THREAD_LOCAL.get();
+ if (localeCache == null) {
+ localeCache = MapUtils.newBoundedMap(MAX_LOCALE_CACHE_SIZE);
+ DECIMAL_FORMAT_THREAD_LOCAL.set(localeCache);
+ }
+ Map<String, DecimalFormat> formatCache = localeCache.get(locale);
+ if (formatCache == null) {
+ formatCache = MapUtils.newBoundedMap(MAX_FORMAT_CACHE_SIZE);
+ localeCache.put(locale, formatCache);
+ }
+ DecimalFormat decimalFormat = formatCache.get(format);
+ if (decimalFormat == null) {
+ decimalFormat = new DecimalFormat(format,
DecimalFormatSymbols.getInstance(locale));
+ formatCache.put(format, decimalFormat);
+ }
+ if (decimalFormat.getRoundingMode() != roundingMode) {
+ decimalFormat.setRoundingMode(roundingMode);
+ }
+ return decimalFormat;
+ }
+
+ public static void removeThreadLocalCache() {
+ DECIMAL_FORMAT_THREAD_LOCAL.remove();
+ }
}
diff --git
a/fesod-sheet/src/test/java/org/apache/fesod/sheet/util/DateUtilsTest.java
b/fesod-sheet/src/test/java/org/apache/fesod/sheet/util/DateUtilsTest.java
index 2f0b2f18..33494369 100644
--- a/fesod-sheet/src/test/java/org/apache/fesod/sheet/util/DateUtilsTest.java
+++ b/fesod-sheet/src/test/java/org/apache/fesod/sheet/util/DateUtilsTest.java
@@ -36,6 +36,8 @@ import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.parallel.ResourceLock;
+import org.junit.jupiter.api.parallel.Resources;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.ValueSource;
@@ -194,6 +196,77 @@ class DateUtilsTest {
Assertions.assertEquals("2026-10-01", defaultFormatResult2);
}
+ @Test
+ @ResourceLock(Resources.LOCALE)
+ void test_dateTimeFormatterCache_distinguishesRootFromDefaultLocale() {
+ Locale originalLocale = Locale.getDefault(Locale.Category.FORMAT);
+ try {
+ Locale.setDefault(Locale.Category.FORMAT, Locale.FRANCE);
+ LocalDate date = LocalDate.of(2026, 7, 13);
+ String format = "MMMM";
+
+ String rootResult = DateUtils.format(date, format, Locale.ROOT);
+ String defaultResult = DateUtils.format(date, format, null);
+
+
Assertions.assertEquals(date.format(DateTimeFormatter.ofPattern(format,
Locale.ROOT)), rootResult);
+
Assertions.assertEquals(date.format(DateTimeFormatter.ofPattern(format,
Locale.FRANCE)), defaultResult);
+ Assertions.assertNotEquals(rootResult, defaultResult);
+ } finally {
+ Locale.setDefault(Locale.Category.FORMAT, originalLocale);
+ }
+ }
+
+ @Test
+ @ResourceLock(Resources.LOCALE)
+ void test_dateTimeFormatterCache_tracksDefaultFormatLocaleChanges() {
+ Locale originalLocale = Locale.getDefault(Locale.Category.FORMAT);
+ try {
+ LocalDate date = LocalDate.of(2026, 7, 13);
+ String format = "MMMM";
+
+ Locale.setDefault(Locale.Category.FORMAT, Locale.FRANCE);
+ String franceResult = DateUtils.format(date, format, null);
+ Locale.setDefault(Locale.Category.FORMAT, Locale.US);
+ String usResult = DateUtils.format(date, format, null);
+
+
Assertions.assertEquals(date.format(DateTimeFormatter.ofPattern(format,
Locale.FRANCE)), franceResult);
+
Assertions.assertEquals(date.format(DateTimeFormatter.ofPattern(format,
Locale.US)), usResult);
+ Assertions.assertNotEquals(franceResult, usResult);
+ } finally {
+ Locale.setDefault(Locale.Category.FORMAT, originalLocale);
+ }
+ }
+
+ @Test
+ @SuppressWarnings("unchecked")
+ void test_dateTimeFormatterCache_isBounded() throws NoSuchFieldException,
IllegalAccessException {
+ int localeCap = readIntConstant("MAX_LOCALE_CACHE_SIZE");
+ int formatCap = readIntConstant("MAX_FORMAT_CACHE_SIZE");
+ LocalDate date = LocalDate.of(2026, 7, 13);
+
+ for (int i = 0; i <= formatCap; i++) {
+ DateUtils.format(date, "yyyy-MM-dd'" + i + "'", Locale.US);
+ }
+
+ Field field =
DateUtils.class.getDeclaredField("DATE_TIME_FORMATTER_THREAD_LOCAL");
+ field.setAccessible(true);
+ ThreadLocal<Map<Locale, Map<String, DateTimeFormatter>>> threadLocal =
+ (ThreadLocal<Map<Locale, Map<String, DateTimeFormatter>>>)
field.get(null);
+ Map<Locale, Map<String, DateTimeFormatter>> localeCache =
threadLocal.get();
+ Assertions.assertEquals(formatCap, localeCache.get(Locale.US).size());
+
+ for (int i = 0; i < localeCap + 2; i++) {
+ DateUtils.format(date, "yyyy-MM-dd", new Locale("en", "X" + i));
+ }
+ Assertions.assertEquals(localeCap, localeCache.size());
+ }
+
+ private static int readIntConstant(String name) throws
NoSuchFieldException, IllegalAccessException {
+ Field field = DateUtils.class.getDeclaredField(name);
+ field.setAccessible(true);
+ return field.getInt(null);
+ }
+
@Test
void test_format_BigDecimal() {
// 43831 = 2020-01-01
@@ -355,19 +428,24 @@ class DateUtilsTest {
@Test
void test_removeThreadLocalCache() throws NoSuchFieldException,
IllegalAccessException {
DateUtils.format(new Date(), "yyyy-MM-dd");
+ DateUtils.format(LocalDate.of(2026, 7, 13), "MMMM", Locale.US);
DateUtils.isADateFormat((short) 100, "yyyy-MM-dd");
Field f1 = DateUtils.class.getDeclaredField("DATE_THREAD_LOCAL");
Field f2 =
DateUtils.class.getDeclaredField("DATE_FORMAT_THREAD_LOCAL");
+ Field f3 =
DateUtils.class.getDeclaredField("DATE_TIME_FORMATTER_THREAD_LOCAL");
f1.setAccessible(true);
f2.setAccessible(true);
+ f3.setAccessible(true);
Assertions.assertNotNull(((ThreadLocal<?>) f1.get(null)).get());
Assertions.assertNotNull(((ThreadLocal<?>) f2.get(null)).get());
+ Assertions.assertNotNull(((ThreadLocal<?>) f3.get(null)).get());
DateUtils.removeThreadLocalCache();
Assertions.assertNull(((ThreadLocal<?>) f1.get(null)).get());
Assertions.assertNull(((ThreadLocal<?>) f2.get(null)).get());
+ Assertions.assertNull(((ThreadLocal<?>) f3.get(null)).get());
}
}
diff --git
a/fesod-sheet/src/test/java/org/apache/fesod/sheet/util/NumberUtilsTest.java
b/fesod-sheet/src/test/java/org/apache/fesod/sheet/util/NumberUtilsTest.java
index 66f939f7..4e16b956 100644
--- a/fesod-sheet/src/test/java/org/apache/fesod/sheet/util/NumberUtilsTest.java
+++ b/fesod-sheet/src/test/java/org/apache/fesod/sheet/util/NumberUtilsTest.java
@@ -19,17 +19,30 @@
package org.apache.fesod.sheet.util;
+import java.io.File;
+import java.lang.reflect.Field;
import java.math.BigDecimal;
import java.math.RoundingMode;
+import java.nio.file.Path;
+import java.text.DecimalFormat;
+import java.text.DecimalFormatSymbols;
import java.text.ParseException;
+import java.util.Collections;
+import java.util.Locale;
+import java.util.Map;
+import org.apache.fesod.sheet.FesodSheet;
import org.apache.fesod.sheet.metadata.data.WriteCellData;
import org.apache.fesod.sheet.metadata.property.ExcelContentProperty;
import org.apache.fesod.sheet.metadata.property.NumberFormatProperty;
import org.apache.fesod.sheet.testkit.Tags;
+import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.io.TempDir;
+import org.junit.jupiter.api.parallel.ResourceLock;
+import org.junit.jupiter.api.parallel.Resources;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
@@ -47,6 +60,11 @@ class NumberUtilsTest {
@Mock
private NumberFormatProperty numberFormatProperty;
+ @AfterEach
+ void tearDown() {
+ NumberUtils.removeThreadLocalCache();
+ }
+
@Test
void test_format_noFormat_BigDecimal() {
BigDecimal bigDecimal = new BigDecimal("0.0000001");
@@ -77,6 +95,71 @@ class NumberUtilsTest {
Assertions.assertEquals("123.46", result);
}
+ @Test
+ @ResourceLock(Resources.LOCALE)
+ void test_formatCache_tracksDefaultFormatLocaleChanges() {
+ Locale originalLocale = Locale.getDefault(Locale.Category.FORMAT);
+ try {
+ String format = "#,##0.00";
+ ExcelContentProperty property = new ExcelContentProperty();
+ property.setNumberFormatProperty(new NumberFormatProperty(format,
RoundingMode.HALF_UP));
+
+ Locale.setDefault(Locale.Category.FORMAT, Locale.FRANCE);
+ String franceResult = NumberUtils.format(1234.5, property);
+ Locale.setDefault(Locale.Category.FORMAT, Locale.US);
+ String usResult = NumberUtils.format(1234.5, property);
+
+ Assertions.assertEquals(
+ new DecimalFormat(format,
DecimalFormatSymbols.getInstance(Locale.FRANCE)).format(1234.5),
+ franceResult);
+ Assertions.assertEquals(
+ new DecimalFormat(format,
DecimalFormatSymbols.getInstance(Locale.US)).format(1234.5), usResult);
+ Assertions.assertNotEquals(franceResult, usResult);
+ } finally {
+ Locale.setDefault(Locale.Category.FORMAT, originalLocale);
+ }
+ }
+
+ @Test
+ @ResourceLock(Resources.LOCALE)
+ @SuppressWarnings("unchecked")
+ void test_formatCache_isBounded() throws NoSuchFieldException,
IllegalAccessException {
+ int localeCap = readIntConstant("MAX_LOCALE_CACHE_SIZE");
+ int formatCap = readIntConstant("MAX_FORMAT_CACHE_SIZE");
+ Locale originalLocale = Locale.getDefault(Locale.Category.FORMAT);
+ try {
+ Locale.setDefault(Locale.Category.FORMAT, Locale.US);
+ ExcelContentProperty property = new ExcelContentProperty();
+ StringBuilder format = new StringBuilder("0.");
+ for (int i = 0; i <= formatCap; i++) {
+ format.append('0');
+ property.setNumberFormatProperty(new
NumberFormatProperty(format.toString(), RoundingMode.HALF_UP));
+ NumberUtils.format(1234.5, property);
+ }
+
+ Field field =
NumberUtils.class.getDeclaredField("DECIMAL_FORMAT_THREAD_LOCAL");
+ field.setAccessible(true);
+ ThreadLocal<Map<Locale, Map<String, DecimalFormat>>> threadLocal =
+ (ThreadLocal<Map<Locale, Map<String, DecimalFormat>>>)
field.get(null);
+ Map<Locale, Map<String, DecimalFormat>> localeCache =
threadLocal.get();
+ Assertions.assertEquals(formatCap,
localeCache.get(Locale.US).size());
+
+ for (int i = 0; i < localeCap + 2; i++) {
+ Locale.setDefault(Locale.Category.FORMAT, new Locale("en", "X"
+ i));
+ NumberUtils.format(1234.5, property);
+ }
+ Assertions.assertEquals(localeCap, localeCache.size());
+ } finally {
+ Locale.setDefault(Locale.Category.FORMAT, originalLocale);
+ }
+ }
+
+ private static int readIntConstant(String name) throws
NoSuchFieldException, IllegalAccessException {
+ Field field = NumberUtils.class.getDeclaredField(name);
+ field.setAccessible(true);
+ return field.getInt(null);
+ }
+
@Test
void test_formatToCellDataString() {
Integer num = 100;
@@ -270,4 +353,41 @@ class NumberUtilsTest {
NumberUtils.parseInteger("not_a_number", contentProperty);
});
}
+
+ @Test
+ void test_removeThreadLocalCache() throws NoSuchFieldException,
IllegalAccessException {
+ ExcelContentProperty property = new ExcelContentProperty();
+ property.setNumberFormatProperty(new NumberFormatProperty("#,##0.00",
RoundingMode.HALF_UP));
+ NumberUtils.format(1234.5, property);
+
+ Field field =
NumberUtils.class.getDeclaredField("DECIMAL_FORMAT_THREAD_LOCAL");
+ field.setAccessible(true);
+ ThreadLocal<?> threadLocal = (ThreadLocal<?>) field.get(null);
+ Assertions.assertNotNull(threadLocal.get());
+
+ NumberUtils.removeThreadLocalCache();
+
+ Assertions.assertNull(threadLocal.get());
+ }
+
+ @Test
+ void test_readAndWriteFinishRemoveThreadLocalCache(@TempDir Path tempDir)
+ throws NoSuchFieldException, IllegalAccessException {
+ Field field =
NumberUtils.class.getDeclaredField("DECIMAL_FORMAT_THREAD_LOCAL");
+ field.setAccessible(true);
+ ThreadLocal<?> threadLocal = (ThreadLocal<?>) field.get(null);
+ ExcelContentProperty property = new ExcelContentProperty();
+ property.setNumberFormatProperty(new NumberFormatProperty("#,##0.00",
RoundingMode.HALF_UP));
+ File file = tempDir.resolve("thread-local-cache.xlsx").toFile();
+
+ NumberUtils.format(1234.5, property);
+ Assertions.assertNotNull(threadLocal.get());
+
FesodSheet.write(file).sheet().doWrite(Collections.singletonList(Collections.singletonList("value")));
+ Assertions.assertNull(threadLocal.get());
+
+ NumberUtils.format(1234.5, property);
+ Assertions.assertNotNull(threadLocal.get());
+ FesodSheet.read(file).sheet().doReadSync();
+ Assertions.assertNull(threadLocal.get());
+ }
}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]