From 38b6bf4ae274033829bc5ceaada5b46b02c1cdcb Mon Sep 17 00:00:00 2001 From: Mikkey-f <178465103+Mikkey-f@users.noreply.github.com> Date: Thu, 20 Aug 2026 19:33:23 +0800 Subject: [PATCH 1/9] feat: add OffsetDateTime converter family (#1017) Add OffsetDateTimeStringConverter, OffsetDateTimeNumberConverter and OffsetDateTimeDateConverter, following the existing LocalDateTime and ZonedDateTime converter patterns: - String conversion preserves the offset in ISO-8601 text by default, with a configurable pattern, falling back to local wall-clock time when the offset is missing. - Number and date conversions drop the offset while preserving the local wall-clock time, consistent with the ZonedDateTime converters. --- .../converters/DefaultConverterLoader.java | 7 + .../OffsetDateTimeDateConverter.java | 57 +++++++ .../OffsetDateTimeNumberConverter.java | 74 +++++++++ .../OffsetDateTimeStringConverter.java | 88 +++++++++++ .../OffsetDateTimeConverterTest.java | 140 ++++++++++++++++++ 5 files changed, 366 insertions(+) create mode 100644 fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/offsetdatetime/OffsetDateTimeDateConverter.java create mode 100644 fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/offsetdatetime/OffsetDateTimeNumberConverter.java create mode 100644 fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/offsetdatetime/OffsetDateTimeStringConverter.java create mode 100644 fesod-sheet/src/test/java/org/apache/fesod/sheet/converter/OffsetDateTimeConverterTest.java diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/DefaultConverterLoader.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/DefaultConverterLoader.java index f17d9b62c..7d1c765d0 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/DefaultConverterLoader.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/DefaultConverterLoader.java @@ -70,6 +70,9 @@ import org.apache.fesod.sheet.converters.longconverter.LongBooleanConverter; import org.apache.fesod.sheet.converters.longconverter.LongNumberConverter; import org.apache.fesod.sheet.converters.longconverter.LongStringConverter; +import org.apache.fesod.sheet.converters.offsetdatetime.OffsetDateTimeDateConverter; +import org.apache.fesod.sheet.converters.offsetdatetime.OffsetDateTimeNumberConverter; +import org.apache.fesod.sheet.converters.offsetdatetime.OffsetDateTimeStringConverter; import org.apache.fesod.sheet.converters.shortconverter.ShortBooleanConverter; import org.apache.fesod.sheet.converters.shortconverter.ShortNumberConverter; import org.apache.fesod.sheet.converters.shortconverter.ShortStringConverter; @@ -122,6 +125,8 @@ private static void initAllConverter() { putAllConverter(new LocalTimeNumberConverter()); putAllConverter(new LocalTimeStringConverter()); + putAllConverter(new OffsetDateTimeNumberConverter()); + putAllConverter(new OffsetDateTimeStringConverter()); putAllConverter(new DoubleBooleanConverter()); putAllConverter(new DoubleNumberConverter()); @@ -160,6 +165,7 @@ private static void initDefaultWriteConverter() { putWriteConverter(new LocalDateTimeDateConverter()); putWriteConverter(new LocalDateDateConverter()); putWriteConverter(new LocalTimeDateConverter()); + putWriteConverter(new OffsetDateTimeDateConverter()); putWriteConverter(new DoubleNumberConverter()); putWriteConverter(new FloatNumberConverter()); putWriteConverter(new IntegerNumberConverter()); @@ -181,6 +187,7 @@ private static void initDefaultWriteConverter() { putWriteStringConverter(new LocalDateStringConverter()); putWriteStringConverter(new LocalDateTimeStringConverter()); putWriteStringConverter(new LocalTimeStringConverter()); + putWriteStringConverter(new OffsetDateTimeStringConverter()); putWriteStringConverter(new DoubleStringConverter()); putWriteStringConverter(new FloatStringConverter()); putWriteStringConverter(new IntegerStringConverter()); diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/offsetdatetime/OffsetDateTimeDateConverter.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/offsetdatetime/OffsetDateTimeDateConverter.java new file mode 100644 index 000000000..3ef34ba94 --- /dev/null +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/offsetdatetime/OffsetDateTimeDateConverter.java @@ -0,0 +1,57 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/* + * This file is part of the Apache Fesod (Incubating) project, which was derived from Alibaba EasyExcel. + * + * Copyright (C) 2018-2024 Alibaba Group Holding Ltd. + */ + +package org.apache.fesod.sheet.converters.offsetdatetime; + +import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import org.apache.fesod.sheet.converters.Converter; +import org.apache.fesod.sheet.metadata.GlobalConfiguration; +import org.apache.fesod.sheet.metadata.data.WriteCellData; +import org.apache.fesod.sheet.metadata.property.ExcelContentProperty; +import org.apache.fesod.sheet.util.DateUtils; +import org.apache.fesod.sheet.util.WorkBookUtil; + +/** OffsetDateTime and date converter. */ +public class OffsetDateTimeDateConverter implements Converter { + @Override + public Class supportJavaTypeKey() { + return OffsetDateTime.class; + } + + @Override + public WriteCellData convertToExcelData( + OffsetDateTime value, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) + throws Exception { + LocalDateTime localDateTime = value.toLocalDateTime(); + WriteCellData cellData = new WriteCellData<>(localDateTime); + String format = null; + if (contentProperty != null && contentProperty.getDateTimeFormatProperty() != null) { + format = contentProperty.getDateTimeFormatProperty().getFormat(); + } + WorkBookUtil.fillDataFormat(cellData, format, DateUtils.defaultDateFormat); + return cellData; + } +} diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/offsetdatetime/OffsetDateTimeNumberConverter.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/offsetdatetime/OffsetDateTimeNumberConverter.java new file mode 100644 index 000000000..d41a17f94 --- /dev/null +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/offsetdatetime/OffsetDateTimeNumberConverter.java @@ -0,0 +1,74 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/* + * This file is part of the Apache Fesod (Incubating) project, which was derived from Alibaba EasyExcel. + * + * Copyright (C) 2018-2024 Alibaba Group Holding Ltd. + */ + +package org.apache.fesod.sheet.converters.offsetdatetime; + +import java.math.BigDecimal; +import java.time.OffsetDateTime; +import java.time.ZoneId; +import org.apache.fesod.sheet.converters.Converter; +import org.apache.fesod.sheet.enums.CellDataTypeEnum; +import org.apache.fesod.sheet.metadata.GlobalConfiguration; +import org.apache.fesod.sheet.metadata.data.ReadCellData; +import org.apache.fesod.sheet.metadata.data.WriteCellData; +import org.apache.fesod.sheet.metadata.property.ExcelContentProperty; +import org.apache.fesod.sheet.util.DateUtils; +import org.apache.poi.ss.usermodel.DateUtil; + +/** OffsetDateTime and number converter. */ +public class OffsetDateTimeNumberConverter implements Converter { + @Override + public Class supportJavaTypeKey() { + return OffsetDateTime.class; + } + + @Override + public CellDataTypeEnum supportExcelTypeKey() { + return CellDataTypeEnum.NUMBER; + } + + @Override + public OffsetDateTime convertToJavaData( + ReadCellData cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) { + boolean use1904windowing = globalConfiguration.getUse1904windowing(); + if (contentProperty != null && contentProperty.getDateTimeFormatProperty() != null) { + use1904windowing = contentProperty.getDateTimeFormatProperty().getUse1904windowing(); + } + return DateUtils.getLocalDateTime(cellData.getNumberValue().doubleValue(), use1904windowing) + .atZone(ZoneId.systemDefault()) + .toOffsetDateTime(); + } + + @Override + public WriteCellData convertToExcelData( + OffsetDateTime value, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) { + boolean use1904windowing = globalConfiguration.getUse1904windowing(); + if (contentProperty != null && contentProperty.getDateTimeFormatProperty() != null) { + use1904windowing = contentProperty.getDateTimeFormatProperty().getUse1904windowing(); + } + return new WriteCellData<>( + BigDecimal.valueOf(DateUtil.getExcelDate(value.toLocalDateTime(), use1904windowing))); + } +} diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/offsetdatetime/OffsetDateTimeStringConverter.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/offsetdatetime/OffsetDateTimeStringConverter.java new file mode 100644 index 000000000..d18a70176 --- /dev/null +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/offsetdatetime/OffsetDateTimeStringConverter.java @@ -0,0 +1,88 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/* + * This file is part of the Apache Fesod (Incubating) project, which was derived from Alibaba EasyExcel. + * + * Copyright (C) 2018-2024 Alibaba Group Holding Ltd. + */ + +package org.apache.fesod.sheet.converters.offsetdatetime; + +import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.Locale; +import org.apache.fesod.common.util.StringUtils; +import org.apache.fesod.sheet.converters.Converter; +import org.apache.fesod.sheet.enums.CellDataTypeEnum; +import org.apache.fesod.sheet.metadata.GlobalConfiguration; +import org.apache.fesod.sheet.metadata.data.ReadCellData; +import org.apache.fesod.sheet.metadata.data.WriteCellData; +import org.apache.fesod.sheet.metadata.property.ExcelContentProperty; + +/** OffsetDateTime and string converter. */ +public class OffsetDateTimeStringConverter implements Converter { + @Override + public Class supportJavaTypeKey() { + return OffsetDateTime.class; + } + + @Override + public CellDataTypeEnum supportExcelTypeKey() { + return CellDataTypeEnum.STRING; + } + + @Override + public OffsetDateTime convertToJavaData( + ReadCellData cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) { + DateTimeFormatter formatter = formatter(contentProperty, globalConfiguration.getLocale()); + String stringValue = cellData.getStringValue(); + try { + return OffsetDateTime.parse(stringValue, formatter); + } catch (DateTimeParseException e) { + LocalDateTime localDateTime; + try { + localDateTime = LocalDateTime.parse(stringValue, formatter); + } catch (DateTimeParseException inner) { + localDateTime = LocalDateTime.parse(stringValue, DateTimeFormatter.ISO_LOCAL_DATE_TIME); + } + return localDateTime.atZone(ZoneId.systemDefault()).toOffsetDateTime(); + } + } + + @Override + public WriteCellData convertToExcelData( + OffsetDateTime value, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) { + return new WriteCellData<>(value.format(formatter(contentProperty, globalConfiguration.getLocale()))); + } + + private DateTimeFormatter formatter(ExcelContentProperty contentProperty, Locale locale) { + if (contentProperty == null + || contentProperty.getDateTimeFormatProperty() == null + || StringUtils.isEmpty( + contentProperty.getDateTimeFormatProperty().getFormat())) { + return DateTimeFormatter.ISO_OFFSET_DATE_TIME; + } + return DateTimeFormatter.ofPattern( + contentProperty.getDateTimeFormatProperty().getFormat(), locale); + } +} diff --git a/fesod-sheet/src/test/java/org/apache/fesod/sheet/converter/OffsetDateTimeConverterTest.java b/fesod-sheet/src/test/java/org/apache/fesod/sheet/converter/OffsetDateTimeConverterTest.java new file mode 100644 index 000000000..76a7eecf7 --- /dev/null +++ b/fesod-sheet/src/test/java/org/apache/fesod/sheet/converter/OffsetDateTimeConverterTest.java @@ -0,0 +1,140 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/* + * This file is part of the Apache Fesod (Incubating) project, which was derived from Alibaba EasyExcel. + * + * Copyright (C) 2018-2024 Alibaba Group Holding Ltd. + */ + +package org.apache.fesod.sheet.converter; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import java.time.OffsetDateTime; +import java.time.ZoneId; +import java.time.ZoneOffset; +import org.apache.fesod.sheet.converters.ConverterKeyBuild; +import org.apache.fesod.sheet.converters.DefaultConverterLoader; +import org.apache.fesod.sheet.converters.offsetdatetime.OffsetDateTimeDateConverter; +import org.apache.fesod.sheet.converters.offsetdatetime.OffsetDateTimeNumberConverter; +import org.apache.fesod.sheet.converters.offsetdatetime.OffsetDateTimeStringConverter; +import org.apache.fesod.sheet.enums.CellDataTypeEnum; +import org.apache.fesod.sheet.metadata.GlobalConfiguration; +import org.apache.fesod.sheet.metadata.data.ReadCellData; +import org.apache.fesod.sheet.metadata.data.WriteCellData; +import org.apache.fesod.sheet.metadata.property.DateTimeFormatProperty; +import org.apache.fesod.sheet.metadata.property.ExcelContentProperty; +import org.apache.fesod.sheet.testkit.Tags; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +@Tag(Tags.UNIT) +class OffsetDateTimeConverterTest { + private static final OffsetDateTime VALUE = OffsetDateTime.of(2020, 1, 2, 3, 4, 5, 0, ZoneOffset.ofHours(8)); + + @Test + void dateConverterDropsOffsetWhilePreservingLocalDateTime() throws Exception { + WriteCellData result = + new OffsetDateTimeDateConverter().convertToExcelData(VALUE, null, new GlobalConfiguration()); + assertEquals(CellDataTypeEnum.DATE, result.getType()); + assertEquals(VALUE.toLocalDateTime(), result.getDateValue()); + } + + @Test + void numberConverterDropsOffsetWhilePreservingLocalDateTime() { + OffsetDateTimeNumberConverter converter = new OffsetDateTimeNumberConverter(); + GlobalConfiguration globalConfiguration = new GlobalConfiguration(); + WriteCellData written = converter.convertToExcelData(VALUE, null, globalConfiguration); + OffsetDateTime read = + converter.convertToJavaData(new ReadCellData<>(written.getNumberValue()), null, globalConfiguration); + assertEquals(VALUE.toLocalDateTime().atZone(ZoneId.systemDefault()).toOffsetDateTime(), read); + } + + @Test + void stringConverterPreservesOffsetInIsoText() throws Exception { + OffsetDateTimeStringConverter converter = new OffsetDateTimeStringConverter(); + GlobalConfiguration globalConfiguration = new GlobalConfiguration(); + WriteCellData written = converter.convertToExcelData(VALUE, null, globalConfiguration); + assertEquals( + VALUE, + converter.convertToJavaData(new ReadCellData<>(written.getStringValue()), null, globalConfiguration)); + } + + @Test + void stringConverterUsesConfiguredPattern() throws Exception { + OffsetDateTimeStringConverter converter = new OffsetDateTimeStringConverter(); + ExcelContentProperty property = new ExcelContentProperty(); + property.setDateTimeFormatProperty(new DateTimeFormatProperty("yyyy-MM-dd HH:mm:ss Z", false)); + GlobalConfiguration globalConfiguration = new GlobalConfiguration(); + assertEquals( + "2020-01-02 03:04:05 +0800", + converter + .convertToExcelData(VALUE, property, globalConfiguration) + .getStringValue()); + } + + @Test + void stringConverterFallsBackToLocalDateTimeWhenOffsetIsMissing() throws Exception { + OffsetDateTimeStringConverter converter = new OffsetDateTimeStringConverter(); + GlobalConfiguration globalConfiguration = new GlobalConfiguration(); + ReadCellData cellData = new ReadCellData<>("2020-01-02T03:04:05"); + assertEquals( + VALUE.toLocalDateTime().atZone(ZoneId.systemDefault()).toOffsetDateTime(), + converter.convertToJavaData(cellData, null, globalConfiguration)); + } + + @Test + void stringConverterWithEmptyOrNullPatternFallsBackToIso() throws Exception { + OffsetDateTimeStringConverter converter = new OffsetDateTimeStringConverter(); + GlobalConfiguration globalConfiguration = new GlobalConfiguration(); + + ExcelContentProperty emptyProperty = new ExcelContentProperty(); + emptyProperty.setDateTimeFormatProperty(new DateTimeFormatProperty("", false)); + WriteCellData writtenEmpty = converter.convertToExcelData(VALUE, emptyProperty, globalConfiguration); + assertEquals( + VALUE, + converter.convertToJavaData( + new ReadCellData<>(writtenEmpty.getStringValue()), emptyProperty, globalConfiguration)); + + ExcelContentProperty nullFormatProperty = new ExcelContentProperty(); + nullFormatProperty.setDateTimeFormatProperty(new DateTimeFormatProperty(null, false)); + WriteCellData writtenNullFormat = + converter.convertToExcelData(VALUE, nullFormatProperty, globalConfiguration); + assertEquals( + VALUE, + converter.convertToJavaData( + new ReadCellData<>(writtenNullFormat.getStringValue()), + nullFormatProperty, + globalConfiguration)); + } + + @Test + void convertersAreRegisteredForSupportedDirections() { + assertEquals( + OffsetDateTimeDateConverter.class, + DefaultConverterLoader.loadDefaultWriteConverter() + .get(ConverterKeyBuild.buildKey(OffsetDateTime.class)) + .getClass()); + assertEquals( + 2, + DefaultConverterLoader.loadAllConverter().entrySet().stream() + .filter(entry -> entry.getKey().getClazz() == OffsetDateTime.class) + .count()); + } +} From 882a1b9c3c121e3f55a961ff1f62eb0e58d67a21 Mon Sep 17 00:00:00 2001 From: Mikkey-f <178465103+Mikkey-f@users.noreply.github.com> Date: Thu, 20 Aug 2026 21:05:14 +0800 Subject: [PATCH 2/9] fix: harden OffsetDateTime converters after review (#1017) - Read fallback now routes through DateUtils.parseLocalDateTime so the default space-separated format written by other date converters is accepted, and text that does not match a configured pattern is rejected. - Return null instead of NPE for invalid Excel serials, matching the LocalDateTime family. - Null-safe use1904windowing resolution and default-locale fallback. --- .../OffsetDateTimeNumberConverter.java | 28 +++++++---- .../OffsetDateTimeStringConverter.java | 38 ++++++++++----- .../OffsetDateTimeConverterTest.java | 48 +++++++++++++++++++ 3 files changed, 92 insertions(+), 22 deletions(-) diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/offsetdatetime/OffsetDateTimeNumberConverter.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/offsetdatetime/OffsetDateTimeNumberConverter.java index d41a17f94..b754fdc00 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/offsetdatetime/OffsetDateTimeNumberConverter.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/offsetdatetime/OffsetDateTimeNumberConverter.java @@ -26,6 +26,7 @@ package org.apache.fesod.sheet.converters.offsetdatetime; import java.math.BigDecimal; +import java.time.LocalDateTime; import java.time.OffsetDateTime; import java.time.ZoneId; import org.apache.fesod.sheet.converters.Converter; @@ -52,23 +53,30 @@ public CellDataTypeEnum supportExcelTypeKey() { @Override public OffsetDateTime convertToJavaData( ReadCellData cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) { - boolean use1904windowing = globalConfiguration.getUse1904windowing(); - if (contentProperty != null && contentProperty.getDateTimeFormatProperty() != null) { - use1904windowing = contentProperty.getDateTimeFormatProperty().getUse1904windowing(); + LocalDateTime localDateTime = DateUtils.getLocalDateTime( + cellData.getNumberValue().doubleValue(), resolveUse1904windowing(contentProperty, globalConfiguration)); + if (localDateTime == null) { + return null; } - return DateUtils.getLocalDateTime(cellData.getNumberValue().doubleValue(), use1904windowing) - .atZone(ZoneId.systemDefault()) - .toOffsetDateTime(); + return localDateTime.atZone(ZoneId.systemDefault()).toOffsetDateTime(); } @Override public WriteCellData convertToExcelData( OffsetDateTime value, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) { - boolean use1904windowing = globalConfiguration.getUse1904windowing(); + return new WriteCellData<>(BigDecimal.valueOf(DateUtil.getExcelDate( + value.toLocalDateTime(), resolveUse1904windowing(contentProperty, globalConfiguration)))); + } + + private boolean resolveUse1904windowing( + ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) { if (contentProperty != null && contentProperty.getDateTimeFormatProperty() != null) { - use1904windowing = contentProperty.getDateTimeFormatProperty().getUse1904windowing(); + Boolean propertyUse1904windowing = + contentProperty.getDateTimeFormatProperty().getUse1904windowing(); + if (propertyUse1904windowing != null) { + return propertyUse1904windowing; + } } - return new WriteCellData<>( - BigDecimal.valueOf(DateUtil.getExcelDate(value.toLocalDateTime(), use1904windowing))); + return globalConfiguration.getUse1904windowing(); } } diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/offsetdatetime/OffsetDateTimeStringConverter.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/offsetdatetime/OffsetDateTimeStringConverter.java index d18a70176..415b73d58 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/offsetdatetime/OffsetDateTimeStringConverter.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/offsetdatetime/OffsetDateTimeStringConverter.java @@ -38,6 +38,7 @@ import org.apache.fesod.sheet.metadata.data.ReadCellData; import org.apache.fesod.sheet.metadata.data.WriteCellData; import org.apache.fesod.sheet.metadata.property.ExcelContentProperty; +import org.apache.fesod.sheet.util.DateUtils; /** OffsetDateTime and string converter. */ public class OffsetDateTimeStringConverter implements Converter { @@ -54,18 +55,24 @@ public CellDataTypeEnum supportExcelTypeKey() { @Override public OffsetDateTime convertToJavaData( ReadCellData cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) { - DateTimeFormatter formatter = formatter(contentProperty, globalConfiguration.getLocale()); String stringValue = cellData.getStringValue(); + String format = format(contentProperty); + DateTimeFormatter formatter = formatter(contentProperty, globalConfiguration.getLocale()); try { return OffsetDateTime.parse(stringValue, formatter); } catch (DateTimeParseException e) { - LocalDateTime localDateTime; try { - localDateTime = LocalDateTime.parse(stringValue, formatter); - } catch (DateTimeParseException inner) { - localDateTime = LocalDateTime.parse(stringValue, DateTimeFormatter.ISO_LOCAL_DATE_TIME); + return DateUtils.parseLocalDateTime(stringValue, format, globalConfiguration.getLocale()) + .atZone(ZoneId.systemDefault()) + .toOffsetDateTime(); + } catch (RuntimeException inner) { + if (StringUtils.isEmpty(format)) { + return LocalDateTime.parse(stringValue, DateTimeFormatter.ISO_LOCAL_DATE_TIME) + .atZone(ZoneId.systemDefault()) + .toOffsetDateTime(); + } + throw inner; } - return localDateTime.atZone(ZoneId.systemDefault()).toOffsetDateTime(); } } @@ -75,14 +82,21 @@ public WriteCellData convertToExcelData( return new WriteCellData<>(value.format(formatter(contentProperty, globalConfiguration.getLocale()))); } + private String format(ExcelContentProperty contentProperty) { + if (contentProperty == null || contentProperty.getDateTimeFormatProperty() == null) { + return null; + } + return contentProperty.getDateTimeFormatProperty().getFormat(); + } + private DateTimeFormatter formatter(ExcelContentProperty contentProperty, Locale locale) { - if (contentProperty == null - || contentProperty.getDateTimeFormatProperty() == null - || StringUtils.isEmpty( - contentProperty.getDateTimeFormatProperty().getFormat())) { + if (locale == null) { + locale = Locale.getDefault(); + } + String format = format(contentProperty); + if (StringUtils.isEmpty(format)) { return DateTimeFormatter.ISO_OFFSET_DATE_TIME; } - return DateTimeFormatter.ofPattern( - contentProperty.getDateTimeFormatProperty().getFormat(), locale); + return DateTimeFormatter.ofPattern(format, locale); } } diff --git a/fesod-sheet/src/test/java/org/apache/fesod/sheet/converter/OffsetDateTimeConverterTest.java b/fesod-sheet/src/test/java/org/apache/fesod/sheet/converter/OffsetDateTimeConverterTest.java index 76a7eecf7..d97a98db2 100644 --- a/fesod-sheet/src/test/java/org/apache/fesod/sheet/converter/OffsetDateTimeConverterTest.java +++ b/fesod-sheet/src/test/java/org/apache/fesod/sheet/converter/OffsetDateTimeConverterTest.java @@ -26,9 +26,12 @@ package org.apache.fesod.sheet.converter; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import java.math.BigDecimal; import java.time.OffsetDateTime; import java.time.ZoneId; import java.time.ZoneOffset; +import java.time.format.DateTimeParseException; import org.apache.fesod.sheet.converters.ConverterKeyBuild; import org.apache.fesod.sheet.converters.DefaultConverterLoader; import org.apache.fesod.sheet.converters.offsetdatetime.OffsetDateTimeDateConverter; @@ -99,6 +102,51 @@ void stringConverterFallsBackToLocalDateTimeWhenOffsetIsMissing() throws Excepti converter.convertToJavaData(cellData, null, globalConfiguration)); } + @Test + void stringConverterParsesDefaultSpaceSeparatedFormat() throws Exception { + OffsetDateTimeStringConverter converter = new OffsetDateTimeStringConverter(); + GlobalConfiguration globalConfiguration = new GlobalConfiguration(); + ReadCellData cellData = new ReadCellData<>("2020-01-02 03:04:05"); + assertEquals( + VALUE.toLocalDateTime().atZone(ZoneId.systemDefault()).toOffsetDateTime(), + converter.convertToJavaData(cellData, null, globalConfiguration)); + } + + @Test + void stringConverterRejectsTextNotMatchingConfiguredPattern() { + OffsetDateTimeStringConverter converter = new OffsetDateTimeStringConverter(); + ExcelContentProperty property = new ExcelContentProperty(); + property.setDateTimeFormatProperty(new DateTimeFormatProperty("yyyy/MM/dd HH:mm:ss", false)); + GlobalConfiguration globalConfiguration = new GlobalConfiguration(); + ReadCellData cellData = new ReadCellData<>("2020-01-02T03:04:05"); + assertThrows( + DateTimeParseException.class, + () -> converter.convertToJavaData(cellData, property, globalConfiguration)); + } + + @Test + void stringConverterUsesDefaultLocaleWhenLocaleIsNull() throws Exception { + OffsetDateTimeStringConverter converter = new OffsetDateTimeStringConverter(); + ExcelContentProperty property = new ExcelContentProperty(); + property.setDateTimeFormatProperty(new DateTimeFormatProperty("yyyy-MM-dd HH:mm:ss Z", false)); + GlobalConfiguration globalConfiguration = new GlobalConfiguration(); + globalConfiguration.setLocale(null); + assertEquals( + "2020-01-02 03:04:05 +0800", + converter + .convertToExcelData(VALUE, property, globalConfiguration) + .getStringValue()); + } + + @Test + void numberConverterReturnsNullForInvalidExcelDate() { + OffsetDateTimeNumberConverter converter = new OffsetDateTimeNumberConverter(); + GlobalConfiguration globalConfiguration = new GlobalConfiguration(); + assertEquals( + null, + converter.convertToJavaData(new ReadCellData<>(BigDecimal.valueOf(-1)), null, globalConfiguration)); + } + @Test void stringConverterWithEmptyOrNullPatternFallsBackToIso() throws Exception { OffsetDateTimeStringConverter converter = new OffsetDateTimeStringConverter(); From 8d15f67be4d6286e961fc5225f06a41b47472d94 Mon Sep 17 00:00:00 2001 From: Mikkey-f <178465103+Mikkey-f@users.noreply.github.com> Date: Thu, 20 Aug 2026 21:07:16 +0800 Subject: [PATCH 3/9] fix: null-safe global use1904windowing default in OffsetDateTime converters (#1017) --- .../OffsetDateTimeNumberConverter.java | 3 ++- .../converter/OffsetDateTimeConverterTest.java | 15 ++++++++++++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/offsetdatetime/OffsetDateTimeNumberConverter.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/offsetdatetime/OffsetDateTimeNumberConverter.java index b754fdc00..697f46640 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/offsetdatetime/OffsetDateTimeNumberConverter.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/offsetdatetime/OffsetDateTimeNumberConverter.java @@ -77,6 +77,7 @@ private boolean resolveUse1904windowing( return propertyUse1904windowing; } } - return globalConfiguration.getUse1904windowing(); + Boolean globalUse1904windowing = globalConfiguration.getUse1904windowing(); + return globalUse1904windowing != null && globalUse1904windowing; } } diff --git a/fesod-sheet/src/test/java/org/apache/fesod/sheet/converter/OffsetDateTimeConverterTest.java b/fesod-sheet/src/test/java/org/apache/fesod/sheet/converter/OffsetDateTimeConverterTest.java index d97a98db2..84b230b9d 100644 --- a/fesod-sheet/src/test/java/org/apache/fesod/sheet/converter/OffsetDateTimeConverterTest.java +++ b/fesod-sheet/src/test/java/org/apache/fesod/sheet/converter/OffsetDateTimeConverterTest.java @@ -26,6 +26,8 @@ package org.apache.fesod.sheet.converter; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import java.math.BigDecimal; import java.time.OffsetDateTime; @@ -142,9 +144,16 @@ void stringConverterUsesDefaultLocaleWhenLocaleIsNull() throws Exception { void numberConverterReturnsNullForInvalidExcelDate() { OffsetDateTimeNumberConverter converter = new OffsetDateTimeNumberConverter(); GlobalConfiguration globalConfiguration = new GlobalConfiguration(); - assertEquals( - null, - converter.convertToJavaData(new ReadCellData<>(BigDecimal.valueOf(-1)), null, globalConfiguration)); + assertNull(converter.convertToJavaData(new ReadCellData<>(BigDecimal.valueOf(-1)), null, globalConfiguration)); + } + + @Test + void numberConverterDefaultsNullUse1904windowingToFalse() { + OffsetDateTimeNumberConverter converter = new OffsetDateTimeNumberConverter(); + GlobalConfiguration globalConfiguration = new GlobalConfiguration(); + globalConfiguration.setUse1904windowing(null); + WriteCellData written = converter.convertToExcelData(VALUE, null, globalConfiguration); + assertNotNull(written.getNumberValue()); } @Test From 860fbf80824d86264bd855dc7b53417e3674b6e1 Mon Sep 17 00:00:00 2001 From: Mikkey-f <178465103+Mikkey-f@users.noreply.github.com> Date: Sun, 23 Aug 2026 18:57:50 +0800 Subject: [PATCH 4/9] fix: normalize empty date format and cache formatters (#1017) Address review comments: - OffsetDateTimeDateConverter: an empty @DateTimeFormat value bypassed WorkBookUtil.fillDataFormat's default format (only null falls back), writing an empty/General number format instead of yyyy-MM-dd HH:mm:ss. Normalize empty formats to null; regression test added. - OffsetDateTimeStringConverter: cache DateTimeFormatter instances per pattern and locale in a thread-local map instead of rebuilding them on every cell conversion in the hot path. --- .../OffsetDateTimeDateConverter.java | 4 ++++ .../OffsetDateTimeStringConverter.java | 18 +++++++++++++++++- .../converter/OffsetDateTimeConverterTest.java | 12 ++++++++++++ 3 files changed, 33 insertions(+), 1 deletion(-) diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/offsetdatetime/OffsetDateTimeDateConverter.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/offsetdatetime/OffsetDateTimeDateConverter.java index 3ef34ba94..8a032cfb1 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/offsetdatetime/OffsetDateTimeDateConverter.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/offsetdatetime/OffsetDateTimeDateConverter.java @@ -27,6 +27,7 @@ import java.time.LocalDateTime; import java.time.OffsetDateTime; +import org.apache.fesod.common.util.StringUtils; import org.apache.fesod.sheet.converters.Converter; import org.apache.fesod.sheet.metadata.GlobalConfiguration; import org.apache.fesod.sheet.metadata.data.WriteCellData; @@ -50,6 +51,9 @@ public WriteCellData convertToExcelData( String format = null; if (contentProperty != null && contentProperty.getDateTimeFormatProperty() != null) { format = contentProperty.getDateTimeFormatProperty().getFormat(); + if (StringUtils.isEmpty(format)) { + format = null; + } } WorkBookUtil.fillDataFormat(cellData, format, DateUtils.defaultDateFormat); return cellData; diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/offsetdatetime/OffsetDateTimeStringConverter.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/offsetdatetime/OffsetDateTimeStringConverter.java index 415b73d58..6bf891425 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/offsetdatetime/OffsetDateTimeStringConverter.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/offsetdatetime/OffsetDateTimeStringConverter.java @@ -31,6 +31,8 @@ import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; 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.converters.Converter; import org.apache.fesod.sheet.enums.CellDataTypeEnum; @@ -42,6 +44,12 @@ /** OffsetDateTime and string converter. */ public class OffsetDateTimeStringConverter implements Converter { + /** + * Thread-local cache of {@link DateTimeFormatter} instances, keyed by pattern and locale, so + * the per-cell hot path does not rebuild formatters for every conversion. + */ + private static final ThreadLocal> FORMATTER_CACHE = new ThreadLocal<>(); + @Override public Class supportJavaTypeKey() { return OffsetDateTime.class; @@ -97,6 +105,14 @@ private DateTimeFormatter formatter(ExcelContentProperty contentProperty, Locale if (StringUtils.isEmpty(format)) { return DateTimeFormatter.ISO_OFFSET_DATE_TIME; } - return DateTimeFormatter.ofPattern(format, locale); + final String pattern = format; + final Locale actualLocale = locale; + Map cache = FORMATTER_CACHE.get(); + if (cache == null) { + cache = MapUtils.newHashMap(); + FORMATTER_CACHE.set(cache); + } + return cache.computeIfAbsent( + pattern + '\0' + actualLocale, key -> DateTimeFormatter.ofPattern(pattern, actualLocale)); } } diff --git a/fesod-sheet/src/test/java/org/apache/fesod/sheet/converter/OffsetDateTimeConverterTest.java b/fesod-sheet/src/test/java/org/apache/fesod/sheet/converter/OffsetDateTimeConverterTest.java index 84b230b9d..ce3fd52e3 100644 --- a/fesod-sheet/src/test/java/org/apache/fesod/sheet/converter/OffsetDateTimeConverterTest.java +++ b/fesod-sheet/src/test/java/org/apache/fesod/sheet/converter/OffsetDateTimeConverterTest.java @@ -46,6 +46,7 @@ import org.apache.fesod.sheet.metadata.property.DateTimeFormatProperty; import org.apache.fesod.sheet.metadata.property.ExcelContentProperty; import org.apache.fesod.sheet.testkit.Tags; +import org.apache.fesod.sheet.util.DateUtils; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; @@ -147,6 +148,17 @@ void numberConverterReturnsNullForInvalidExcelDate() { assertNull(converter.convertToJavaData(new ReadCellData<>(BigDecimal.valueOf(-1)), null, globalConfiguration)); } + @Test + void dateConverterFallsBackToDefaultFormatForEmptyDateTimeFormat() throws Exception { + OffsetDateTimeDateConverter converter = new OffsetDateTimeDateConverter(); + ExcelContentProperty property = new ExcelContentProperty(); + property.setDateTimeFormatProperty(new DateTimeFormatProperty("", false)); + WriteCellData result = converter.convertToExcelData(VALUE, property, new GlobalConfiguration()); + assertEquals( + DateUtils.defaultDateFormat, + result.getWriteCellStyle().getDataFormatData().getFormat()); + } + @Test void numberConverterDefaultsNullUse1904windowingToFalse() { OffsetDateTimeNumberConverter converter = new OffsetDateTimeNumberConverter(); From 570faf75448f972543d3c09f58e785e9451df0c2 Mon Sep 17 00:00:00 2001 From: Mikkey-f <178465103+Mikkey-f@users.noreply.github.com> Date: Mon, 7 Sep 2026 21:58:16 +0800 Subject: [PATCH 5/9] fix: remove EasyExcel-derived license header from new OffsetDateTime converters (#1017) --- .../offsetdatetime/OffsetDateTimeDateConverter.java | 6 ------ .../offsetdatetime/OffsetDateTimeNumberConverter.java | 6 ------ .../offsetdatetime/OffsetDateTimeStringConverter.java | 6 ------ .../fesod/sheet/converter/OffsetDateTimeConverterTest.java | 6 ------ 4 files changed, 24 deletions(-) diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/offsetdatetime/OffsetDateTimeDateConverter.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/offsetdatetime/OffsetDateTimeDateConverter.java index 8a032cfb1..940f38162 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/offsetdatetime/OffsetDateTimeDateConverter.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/offsetdatetime/OffsetDateTimeDateConverter.java @@ -17,12 +17,6 @@ * under the License. */ -/* - * This file is part of the Apache Fesod (Incubating) project, which was derived from Alibaba EasyExcel. - * - * Copyright (C) 2018-2024 Alibaba Group Holding Ltd. - */ - package org.apache.fesod.sheet.converters.offsetdatetime; import java.time.LocalDateTime; diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/offsetdatetime/OffsetDateTimeNumberConverter.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/offsetdatetime/OffsetDateTimeNumberConverter.java index 697f46640..88b4d2689 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/offsetdatetime/OffsetDateTimeNumberConverter.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/offsetdatetime/OffsetDateTimeNumberConverter.java @@ -17,12 +17,6 @@ * under the License. */ -/* - * This file is part of the Apache Fesod (Incubating) project, which was derived from Alibaba EasyExcel. - * - * Copyright (C) 2018-2024 Alibaba Group Holding Ltd. - */ - package org.apache.fesod.sheet.converters.offsetdatetime; import java.math.BigDecimal; diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/offsetdatetime/OffsetDateTimeStringConverter.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/offsetdatetime/OffsetDateTimeStringConverter.java index 6bf891425..c0532ccc4 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/offsetdatetime/OffsetDateTimeStringConverter.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/offsetdatetime/OffsetDateTimeStringConverter.java @@ -17,12 +17,6 @@ * under the License. */ -/* - * This file is part of the Apache Fesod (Incubating) project, which was derived from Alibaba EasyExcel. - * - * Copyright (C) 2018-2024 Alibaba Group Holding Ltd. - */ - package org.apache.fesod.sheet.converters.offsetdatetime; import java.time.LocalDateTime; diff --git a/fesod-sheet/src/test/java/org/apache/fesod/sheet/converter/OffsetDateTimeConverterTest.java b/fesod-sheet/src/test/java/org/apache/fesod/sheet/converter/OffsetDateTimeConverterTest.java index ce3fd52e3..ccfb3f12f 100644 --- a/fesod-sheet/src/test/java/org/apache/fesod/sheet/converter/OffsetDateTimeConverterTest.java +++ b/fesod-sheet/src/test/java/org/apache/fesod/sheet/converter/OffsetDateTimeConverterTest.java @@ -17,12 +17,6 @@ * under the License. */ -/* - * This file is part of the Apache Fesod (Incubating) project, which was derived from Alibaba EasyExcel. - * - * Copyright (C) 2018-2024 Alibaba Group Holding Ltd. - */ - package org.apache.fesod.sheet.converter; import static org.junit.jupiter.api.Assertions.assertEquals; From dc32b0b090c186db9627d2e5354226cdf9573ce1 Mon Sep 17 00:00:00 2001 From: Mikkey-f <178465103+Mikkey-f@users.noreply.github.com> Date: Mon, 7 Sep 2026 22:19:44 +0800 Subject: [PATCH 6/9] refactor: reuse shared DateUtils.isDate1904 in OffsetDateTimeNumberConverter (#1017) --- .../OffsetDateTimeNumberConverter.java | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/offsetdatetime/OffsetDateTimeNumberConverter.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/offsetdatetime/OffsetDateTimeNumberConverter.java index 88b4d2689..d08856300 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/offsetdatetime/OffsetDateTimeNumberConverter.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/offsetdatetime/OffsetDateTimeNumberConverter.java @@ -48,7 +48,7 @@ public CellDataTypeEnum supportExcelTypeKey() { public OffsetDateTime convertToJavaData( ReadCellData cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) { LocalDateTime localDateTime = DateUtils.getLocalDateTime( - cellData.getNumberValue().doubleValue(), resolveUse1904windowing(contentProperty, globalConfiguration)); + cellData.getNumberValue().doubleValue(), DateUtils.isDate1904(contentProperty, globalConfiguration)); if (localDateTime == null) { return null; } @@ -59,19 +59,6 @@ public OffsetDateTime convertToJavaData( public WriteCellData convertToExcelData( OffsetDateTime value, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) { return new WriteCellData<>(BigDecimal.valueOf(DateUtil.getExcelDate( - value.toLocalDateTime(), resolveUse1904windowing(contentProperty, globalConfiguration)))); - } - - private boolean resolveUse1904windowing( - ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) { - if (contentProperty != null && contentProperty.getDateTimeFormatProperty() != null) { - Boolean propertyUse1904windowing = - contentProperty.getDateTimeFormatProperty().getUse1904windowing(); - if (propertyUse1904windowing != null) { - return propertyUse1904windowing; - } - } - Boolean globalUse1904windowing = globalConfiguration.getUse1904windowing(); - return globalUse1904windowing != null && globalUse1904windowing; + value.toLocalDateTime(), DateUtils.isDate1904(contentProperty, globalConfiguration)))); } } From 8ad7959689cc6091c5d47a49fa3960c0a9484e3b Mon Sep 17 00:00:00 2001 From: Mikkey-f <178465103+Mikkey-f@users.noreply.github.com> Date: Mon, 7 Sep 2026 22:23:05 +0800 Subject: [PATCH 7/9] refactor: replace static Assertions imports with fully-qualified calls in OffsetDateTimeConverterTest (#1017) --- .../OffsetDateTimeConverterTest.java | 39 +++++++++---------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/fesod-sheet/src/test/java/org/apache/fesod/sheet/converter/OffsetDateTimeConverterTest.java b/fesod-sheet/src/test/java/org/apache/fesod/sheet/converter/OffsetDateTimeConverterTest.java index ccfb3f12f..08025eac6 100644 --- a/fesod-sheet/src/test/java/org/apache/fesod/sheet/converter/OffsetDateTimeConverterTest.java +++ b/fesod-sheet/src/test/java/org/apache/fesod/sheet/converter/OffsetDateTimeConverterTest.java @@ -19,10 +19,6 @@ package org.apache.fesod.sheet.converter; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrows; import java.math.BigDecimal; import java.time.OffsetDateTime; import java.time.ZoneId; @@ -41,6 +37,7 @@ import org.apache.fesod.sheet.metadata.property.ExcelContentProperty; import org.apache.fesod.sheet.testkit.Tags; import org.apache.fesod.sheet.util.DateUtils; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; @@ -52,8 +49,8 @@ class OffsetDateTimeConverterTest { void dateConverterDropsOffsetWhilePreservingLocalDateTime() throws Exception { WriteCellData result = new OffsetDateTimeDateConverter().convertToExcelData(VALUE, null, new GlobalConfiguration()); - assertEquals(CellDataTypeEnum.DATE, result.getType()); - assertEquals(VALUE.toLocalDateTime(), result.getDateValue()); + Assertions.assertEquals(CellDataTypeEnum.DATE, result.getType()); + Assertions.assertEquals(VALUE.toLocalDateTime(), result.getDateValue()); } @Test @@ -63,7 +60,8 @@ void numberConverterDropsOffsetWhilePreservingLocalDateTime() { WriteCellData written = converter.convertToExcelData(VALUE, null, globalConfiguration); OffsetDateTime read = converter.convertToJavaData(new ReadCellData<>(written.getNumberValue()), null, globalConfiguration); - assertEquals(VALUE.toLocalDateTime().atZone(ZoneId.systemDefault()).toOffsetDateTime(), read); + Assertions.assertEquals( + VALUE.toLocalDateTime().atZone(ZoneId.systemDefault()).toOffsetDateTime(), read); } @Test @@ -71,7 +69,7 @@ void stringConverterPreservesOffsetInIsoText() throws Exception { OffsetDateTimeStringConverter converter = new OffsetDateTimeStringConverter(); GlobalConfiguration globalConfiguration = new GlobalConfiguration(); WriteCellData written = converter.convertToExcelData(VALUE, null, globalConfiguration); - assertEquals( + Assertions.assertEquals( VALUE, converter.convertToJavaData(new ReadCellData<>(written.getStringValue()), null, globalConfiguration)); } @@ -82,7 +80,7 @@ void stringConverterUsesConfiguredPattern() throws Exception { ExcelContentProperty property = new ExcelContentProperty(); property.setDateTimeFormatProperty(new DateTimeFormatProperty("yyyy-MM-dd HH:mm:ss Z", false)); GlobalConfiguration globalConfiguration = new GlobalConfiguration(); - assertEquals( + Assertions.assertEquals( "2020-01-02 03:04:05 +0800", converter .convertToExcelData(VALUE, property, globalConfiguration) @@ -94,7 +92,7 @@ void stringConverterFallsBackToLocalDateTimeWhenOffsetIsMissing() throws Excepti OffsetDateTimeStringConverter converter = new OffsetDateTimeStringConverter(); GlobalConfiguration globalConfiguration = new GlobalConfiguration(); ReadCellData cellData = new ReadCellData<>("2020-01-02T03:04:05"); - assertEquals( + Assertions.assertEquals( VALUE.toLocalDateTime().atZone(ZoneId.systemDefault()).toOffsetDateTime(), converter.convertToJavaData(cellData, null, globalConfiguration)); } @@ -104,7 +102,7 @@ void stringConverterParsesDefaultSpaceSeparatedFormat() throws Exception { OffsetDateTimeStringConverter converter = new OffsetDateTimeStringConverter(); GlobalConfiguration globalConfiguration = new GlobalConfiguration(); ReadCellData cellData = new ReadCellData<>("2020-01-02 03:04:05"); - assertEquals( + Assertions.assertEquals( VALUE.toLocalDateTime().atZone(ZoneId.systemDefault()).toOffsetDateTime(), converter.convertToJavaData(cellData, null, globalConfiguration)); } @@ -116,7 +114,7 @@ void stringConverterRejectsTextNotMatchingConfiguredPattern() { property.setDateTimeFormatProperty(new DateTimeFormatProperty("yyyy/MM/dd HH:mm:ss", false)); GlobalConfiguration globalConfiguration = new GlobalConfiguration(); ReadCellData cellData = new ReadCellData<>("2020-01-02T03:04:05"); - assertThrows( + Assertions.assertThrows( DateTimeParseException.class, () -> converter.convertToJavaData(cellData, property, globalConfiguration)); } @@ -128,7 +126,7 @@ void stringConverterUsesDefaultLocaleWhenLocaleIsNull() throws Exception { property.setDateTimeFormatProperty(new DateTimeFormatProperty("yyyy-MM-dd HH:mm:ss Z", false)); GlobalConfiguration globalConfiguration = new GlobalConfiguration(); globalConfiguration.setLocale(null); - assertEquals( + Assertions.assertEquals( "2020-01-02 03:04:05 +0800", converter .convertToExcelData(VALUE, property, globalConfiguration) @@ -139,7 +137,8 @@ void stringConverterUsesDefaultLocaleWhenLocaleIsNull() throws Exception { void numberConverterReturnsNullForInvalidExcelDate() { OffsetDateTimeNumberConverter converter = new OffsetDateTimeNumberConverter(); GlobalConfiguration globalConfiguration = new GlobalConfiguration(); - assertNull(converter.convertToJavaData(new ReadCellData<>(BigDecimal.valueOf(-1)), null, globalConfiguration)); + Assertions.assertNull( + converter.convertToJavaData(new ReadCellData<>(BigDecimal.valueOf(-1)), null, globalConfiguration)); } @Test @@ -148,7 +147,7 @@ void dateConverterFallsBackToDefaultFormatForEmptyDateTimeFormat() throws Except ExcelContentProperty property = new ExcelContentProperty(); property.setDateTimeFormatProperty(new DateTimeFormatProperty("", false)); WriteCellData result = converter.convertToExcelData(VALUE, property, new GlobalConfiguration()); - assertEquals( + Assertions.assertEquals( DateUtils.defaultDateFormat, result.getWriteCellStyle().getDataFormatData().getFormat()); } @@ -159,7 +158,7 @@ void numberConverterDefaultsNullUse1904windowingToFalse() { GlobalConfiguration globalConfiguration = new GlobalConfiguration(); globalConfiguration.setUse1904windowing(null); WriteCellData written = converter.convertToExcelData(VALUE, null, globalConfiguration); - assertNotNull(written.getNumberValue()); + Assertions.assertNotNull(written.getNumberValue()); } @Test @@ -170,7 +169,7 @@ void stringConverterWithEmptyOrNullPatternFallsBackToIso() throws Exception { ExcelContentProperty emptyProperty = new ExcelContentProperty(); emptyProperty.setDateTimeFormatProperty(new DateTimeFormatProperty("", false)); WriteCellData writtenEmpty = converter.convertToExcelData(VALUE, emptyProperty, globalConfiguration); - assertEquals( + Assertions.assertEquals( VALUE, converter.convertToJavaData( new ReadCellData<>(writtenEmpty.getStringValue()), emptyProperty, globalConfiguration)); @@ -179,7 +178,7 @@ void stringConverterWithEmptyOrNullPatternFallsBackToIso() throws Exception { nullFormatProperty.setDateTimeFormatProperty(new DateTimeFormatProperty(null, false)); WriteCellData writtenNullFormat = converter.convertToExcelData(VALUE, nullFormatProperty, globalConfiguration); - assertEquals( + Assertions.assertEquals( VALUE, converter.convertToJavaData( new ReadCellData<>(writtenNullFormat.getStringValue()), @@ -189,12 +188,12 @@ void stringConverterWithEmptyOrNullPatternFallsBackToIso() throws Exception { @Test void convertersAreRegisteredForSupportedDirections() { - assertEquals( + Assertions.assertEquals( OffsetDateTimeDateConverter.class, DefaultConverterLoader.loadDefaultWriteConverter() .get(ConverterKeyBuild.buildKey(OffsetDateTime.class)) .getClass()); - assertEquals( + Assertions.assertEquals( 2, DefaultConverterLoader.loadAllConverter().entrySet().stream() .filter(entry -> entry.getKey().getClazz() == OffsetDateTime.class) From 146774cfacf9cf7eadb8553d9289e1054c625056 Mon Sep 17 00:00:00 2001 From: Mikkey-f <178465103+Mikkey-f@users.noreply.github.com> Date: Wed, 9 Sep 2026 00:26:50 +0800 Subject: [PATCH 8/9] refactor: delegate OffsetDateTime string conversion to DateUtils helpers (#1017) Addresses second-round review comments on OffsetDateTimeStringConverter. DateUtils now provides parseOffsetDateTime(String, String, Locale) and a format(OffsetDateTime, String, Locale) overload. Both reuse the bounded DATE_TIME_FORMATTER_THREAD_LOCAL cache, which is cleared by removeThreadLocalCache() at the end of each read/write context, instead of a converter-local ThreadLocal that has no cleanup hook. An empty format falls back to ISO_OFFSET_DATE_TIME. Offset-less STRING reads now fail fast with DateTimeParseException instead of silently re-interpreting the text in ZoneId.systemDefault(), which mapped the same text to different instants depending on the server timezone. Tests: the two ZoneId.systemDefault fallback cases now assert failure, the configured-pattern rejection test uses an offset-bearing pattern, and a round-trip case for a configured offset pattern was added. --- .../OffsetDateTimeStringConverter.java | 56 +------------------ .../apache/fesod/sheet/util/DateUtils.java | 34 +++++++++++ .../OffsetDateTimeConverterTest.java | 36 ++++++------ 3 files changed, 57 insertions(+), 69 deletions(-) diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/offsetdatetime/OffsetDateTimeStringConverter.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/offsetdatetime/OffsetDateTimeStringConverter.java index c0532ccc4..e7e10dde1 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/offsetdatetime/OffsetDateTimeStringConverter.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/offsetdatetime/OffsetDateTimeStringConverter.java @@ -19,15 +19,7 @@ package org.apache.fesod.sheet.converters.offsetdatetime; -import java.time.LocalDateTime; import java.time.OffsetDateTime; -import java.time.ZoneId; -import java.time.format.DateTimeFormatter; -import java.time.format.DateTimeParseException; -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.converters.Converter; import org.apache.fesod.sheet.enums.CellDataTypeEnum; import org.apache.fesod.sheet.metadata.GlobalConfiguration; @@ -38,12 +30,6 @@ /** OffsetDateTime and string converter. */ public class OffsetDateTimeStringConverter implements Converter { - /** - * Thread-local cache of {@link DateTimeFormatter} instances, keyed by pattern and locale, so - * the per-cell hot path does not rebuild formatters for every conversion. - */ - private static final ThreadLocal> FORMATTER_CACHE = new ThreadLocal<>(); - @Override public Class supportJavaTypeKey() { return OffsetDateTime.class; @@ -57,31 +43,14 @@ public CellDataTypeEnum supportExcelTypeKey() { @Override public OffsetDateTime convertToJavaData( ReadCellData cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) { - String stringValue = cellData.getStringValue(); - String format = format(contentProperty); - DateTimeFormatter formatter = formatter(contentProperty, globalConfiguration.getLocale()); - try { - return OffsetDateTime.parse(stringValue, formatter); - } catch (DateTimeParseException e) { - try { - return DateUtils.parseLocalDateTime(stringValue, format, globalConfiguration.getLocale()) - .atZone(ZoneId.systemDefault()) - .toOffsetDateTime(); - } catch (RuntimeException inner) { - if (StringUtils.isEmpty(format)) { - return LocalDateTime.parse(stringValue, DateTimeFormatter.ISO_LOCAL_DATE_TIME) - .atZone(ZoneId.systemDefault()) - .toOffsetDateTime(); - } - throw inner; - } - } + return DateUtils.parseOffsetDateTime( + cellData.getStringValue(), format(contentProperty), globalConfiguration.getLocale()); } @Override public WriteCellData convertToExcelData( OffsetDateTime value, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) { - return new WriteCellData<>(value.format(formatter(contentProperty, globalConfiguration.getLocale()))); + return new WriteCellData<>(DateUtils.format(value, format(contentProperty), globalConfiguration.getLocale())); } private String format(ExcelContentProperty contentProperty) { @@ -90,23 +59,4 @@ private String format(ExcelContentProperty contentProperty) { } return contentProperty.getDateTimeFormatProperty().getFormat(); } - - private DateTimeFormatter formatter(ExcelContentProperty contentProperty, Locale locale) { - if (locale == null) { - locale = Locale.getDefault(); - } - String format = format(contentProperty); - if (StringUtils.isEmpty(format)) { - return DateTimeFormatter.ISO_OFFSET_DATE_TIME; - } - final String pattern = format; - final Locale actualLocale = locale; - Map cache = FORMATTER_CACHE.get(); - if (cache == null) { - cache = MapUtils.newHashMap(); - FORMATTER_CACHE.set(cache); - } - return cache.computeIfAbsent( - pattern + '\0' + actualLocale, key -> DateTimeFormatter.ofPattern(pattern, actualLocale)); - } } 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 9fd5365a1..8839dacbe 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 @@ -32,6 +32,7 @@ import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; +import java.time.OffsetDateTime; import java.time.format.DateTimeFormatter; import java.util.Calendar; import java.util.Date; @@ -164,6 +165,21 @@ public static LocalTime parseLocalTime(String timeString, String timeFormat, Loc return LocalTime.parse(timeString, getCacheDateTimeFormat(timeFormat, local)); } + /** + * convert string to date, requiring the string to carry an explicit offset + * + * @param dateString date string, e.g. 2020-01-02T03:04:05+08:00 + * @param dateFormat date format, empty means ISO_OFFSET_DATE_TIME + * @param local local + * @return + */ + public static OffsetDateTime parseOffsetDateTime(String dateString, String dateFormat, Locale local) { + if (StringUtils.isEmpty(dateFormat)) { + return OffsetDateTime.parse(dateString, DateTimeFormatter.ISO_OFFSET_DATE_TIME); + } + return OffsetDateTime.parse(dateString, getCacheDateTimeFormat(dateFormat, local)); + } + /** * convert string to date * @@ -328,6 +344,24 @@ public static String format(LocalTime time, String timeFormat, Locale local) { return time.format(getCacheDateTimeFormat(timeFormat, local)); } + /** + * Format date, preserving the offset in the output + * + * @param date date + * @param dateFormat date format, empty means ISO_OFFSET_DATE_TIME + * @param local local + * @return format string + */ + public static String format(OffsetDateTime date, String dateFormat, Locale local) { + if (date == null) { + return null; + } + if (StringUtils.isEmpty(dateFormat)) { + return date.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME); + } + return date.format(getCacheDateTimeFormat(dateFormat, local)); + } + /** * Format date * diff --git a/fesod-sheet/src/test/java/org/apache/fesod/sheet/converter/OffsetDateTimeConverterTest.java b/fesod-sheet/src/test/java/org/apache/fesod/sheet/converter/OffsetDateTimeConverterTest.java index 08025eac6..4a8e63ddf 100644 --- a/fesod-sheet/src/test/java/org/apache/fesod/sheet/converter/OffsetDateTimeConverterTest.java +++ b/fesod-sheet/src/test/java/org/apache/fesod/sheet/converter/OffsetDateTimeConverterTest.java @@ -88,30 +88,24 @@ void stringConverterUsesConfiguredPattern() throws Exception { } @Test - void stringConverterFallsBackToLocalDateTimeWhenOffsetIsMissing() throws Exception { + void stringConverterRejectsTextWithoutOffset() { OffsetDateTimeStringConverter converter = new OffsetDateTimeStringConverter(); GlobalConfiguration globalConfiguration = new GlobalConfiguration(); - ReadCellData cellData = new ReadCellData<>("2020-01-02T03:04:05"); - Assertions.assertEquals( - VALUE.toLocalDateTime().atZone(ZoneId.systemDefault()).toOffsetDateTime(), - converter.convertToJavaData(cellData, null, globalConfiguration)); - } - - @Test - void stringConverterParsesDefaultSpaceSeparatedFormat() throws Exception { - OffsetDateTimeStringConverter converter = new OffsetDateTimeStringConverter(); - GlobalConfiguration globalConfiguration = new GlobalConfiguration(); - ReadCellData cellData = new ReadCellData<>("2020-01-02 03:04:05"); - Assertions.assertEquals( - VALUE.toLocalDateTime().atZone(ZoneId.systemDefault()).toOffsetDateTime(), - converter.convertToJavaData(cellData, null, globalConfiguration)); + Assertions.assertThrows( + DateTimeParseException.class, + () -> converter.convertToJavaData( + new ReadCellData<>("2020-01-02T03:04:05"), null, globalConfiguration)); + Assertions.assertThrows( + DateTimeParseException.class, + () -> converter.convertToJavaData( + new ReadCellData<>("2020-01-02 03:04:05"), null, globalConfiguration)); } @Test void stringConverterRejectsTextNotMatchingConfiguredPattern() { OffsetDateTimeStringConverter converter = new OffsetDateTimeStringConverter(); ExcelContentProperty property = new ExcelContentProperty(); - property.setDateTimeFormatProperty(new DateTimeFormatProperty("yyyy/MM/dd HH:mm:ss", false)); + property.setDateTimeFormatProperty(new DateTimeFormatProperty("yyyy/MM/dd HH:mm:ss XXX", false)); GlobalConfiguration globalConfiguration = new GlobalConfiguration(); ReadCellData cellData = new ReadCellData<>("2020-01-02T03:04:05"); Assertions.assertThrows( @@ -119,6 +113,16 @@ void stringConverterRejectsTextNotMatchingConfiguredPattern() { () -> converter.convertToJavaData(cellData, property, globalConfiguration)); } + @Test + void stringConverterReadsBackConfiguredPatternWithOffset() { + OffsetDateTimeStringConverter converter = new OffsetDateTimeStringConverter(); + ExcelContentProperty property = new ExcelContentProperty(); + property.setDateTimeFormatProperty(new DateTimeFormatProperty("yyyy-MM-dd HH:mm:ss Z", false)); + GlobalConfiguration globalConfiguration = new GlobalConfiguration(); + ReadCellData cellData = new ReadCellData<>("2020-01-02 03:04:05 +0800"); + Assertions.assertEquals(VALUE, converter.convertToJavaData(cellData, property, globalConfiguration)); + } + @Test void stringConverterUsesDefaultLocaleWhenLocaleIsNull() throws Exception { OffsetDateTimeStringConverter converter = new OffsetDateTimeStringConverter(); From 7653c54240972670258546d62715fade7575f7a3 Mon Sep 17 00:00:00 2001 From: Mikkey-f <178465103+Mikkey-f@users.noreply.github.com> Date: Sat, 12 Sep 2026 14:23:02 +0800 Subject: [PATCH 9/9] test: cover locale handling and DateUtils helper tests per review (#1017) --- .../OffsetDateTimeConverterTest.java | 22 +++++++++++++++ .../fesod/sheet/util/DateUtilsTest.java | 27 +++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/fesod-sheet/src/test/java/org/apache/fesod/sheet/converter/OffsetDateTimeConverterTest.java b/fesod-sheet/src/test/java/org/apache/fesod/sheet/converter/OffsetDateTimeConverterTest.java index 4a8e63ddf..11d665a49 100644 --- a/fesod-sheet/src/test/java/org/apache/fesod/sheet/converter/OffsetDateTimeConverterTest.java +++ b/fesod-sheet/src/test/java/org/apache/fesod/sheet/converter/OffsetDateTimeConverterTest.java @@ -24,6 +24,7 @@ import java.time.ZoneId; import java.time.ZoneOffset; import java.time.format.DateTimeParseException; +import java.util.Locale; import org.apache.fesod.sheet.converters.ConverterKeyBuild; import org.apache.fesod.sheet.converters.DefaultConverterLoader; import org.apache.fesod.sheet.converters.offsetdatetime.OffsetDateTimeDateConverter; @@ -37,6 +38,7 @@ import org.apache.fesod.sheet.metadata.property.ExcelContentProperty; import org.apache.fesod.sheet.testkit.Tags; import org.apache.fesod.sheet.util.DateUtils; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; @@ -45,6 +47,11 @@ class OffsetDateTimeConverterTest { private static final OffsetDateTime VALUE = OffsetDateTime.of(2020, 1, 2, 3, 4, 5, 0, ZoneOffset.ofHours(8)); + @AfterEach + void tearDown() { + DateUtils.removeThreadLocalCache(); + } + @Test void dateConverterDropsOffsetWhilePreservingLocalDateTime() throws Exception { WriteCellData result = @@ -137,6 +144,21 @@ void stringConverterUsesDefaultLocaleWhenLocaleIsNull() throws Exception { .getStringValue()); } + @Test + void stringConverterHonoursConfiguredLocale() throws Exception { + OffsetDateTimeStringConverter converter = new OffsetDateTimeStringConverter(); + ExcelContentProperty property = new ExcelContentProperty(); + property.setDateTimeFormatProperty(new DateTimeFormatProperty("dd MMMM yyyy HH:mm:ss XXX", false)); + GlobalConfiguration globalConfiguration = new GlobalConfiguration(); + globalConfiguration.setLocale(Locale.GERMAN); + WriteCellData written = converter.convertToExcelData(VALUE, property, globalConfiguration); + Assertions.assertEquals("02 Januar 2020 03:04:05 +08:00", written.getStringValue()); + Assertions.assertEquals( + VALUE, + converter.convertToJavaData( + new ReadCellData<>(written.getStringValue()), property, globalConfiguration)); + } + @Test void numberConverterReturnsNullForInvalidExcelDate() { OffsetDateTimeNumberConverter converter = new OffsetDateTimeNumberConverter(); 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 a31454da7..9836bfd8c 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 @@ -26,7 +26,10 @@ import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; import java.util.Calendar; import java.util.Date; import java.util.Locale; @@ -476,6 +479,30 @@ void test_isADateFormat_Cache() throws NoSuchFieldException, IllegalAccessExcept Assertions.assertTrue(res2); } + @Test + void test_parseOffsetDateTime() { + OffsetDateTime expected = OffsetDateTime.of(2020, 1, 2, 3, 4, 5, 0, ZoneOffset.ofHours(8)); + Assertions.assertEquals(expected, DateUtils.parseOffsetDateTime("2020-01-02T03:04:05+08:00", null, Locale.US)); + Assertions.assertEquals(expected, DateUtils.parseOffsetDateTime("2020-01-02T03:04:05+08:00", "", Locale.US)); + Assertions.assertEquals( + expected, + DateUtils.parseOffsetDateTime( + "02 Januar 2020 03:04:05 +08:00", "dd MMMM yyyy HH:mm:ss XXX", Locale.GERMAN)); + Assertions.assertThrows( + DateTimeParseException.class, + () -> DateUtils.parseOffsetDateTime("2020-01-02T03:04:05", null, Locale.US)); + } + + @Test + void test_format_OffsetDateTime() { + OffsetDateTime value = OffsetDateTime.of(2020, 1, 2, 3, 4, 5, 0, ZoneOffset.ofHours(8)); + Assertions.assertNull(DateUtils.format((OffsetDateTime) null, null, Locale.US)); + Assertions.assertEquals("2020-01-02T03:04:05+08:00", DateUtils.format(value, null, Locale.US)); + Assertions.assertEquals("2020-01-02T03:04:05+08:00", DateUtils.format(value, "", Locale.US)); + Assertions.assertEquals( + "02 Januar 2020 03:04:05 +08:00", DateUtils.format(value, "dd MMMM yyyy HH:mm:ss XXX", Locale.GERMAN)); + } + @Test void test_removeThreadLocalCache() throws NoSuchFieldException, IllegalAccessException { DateUtils.format(new Date(), "yyyy-MM-dd");