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]

Reply via email to