From 3b7ae8fb0eb56016f58f0243b69732df567eebd8 Mon Sep 17 00:00:00 2001 From: 56wj <88013475+56wj@users.noreply.github.com> Date: Tue, 8 Sep 2026 14:54:13 +0800 Subject: [PATCH 1/3] feat: support Instant string conversion --- .../converters/DefaultConverterLoader.java | 5 ++ .../instant/InstantStringConverter.java | 57 +++++++++++++ .../DefaultConverterLoaderTest.java | 17 ++++ .../instant/InstantStringConverterTest.java | 81 +++++++++++++++++++ 4 files changed, 160 insertions(+) create mode 100644 fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/instant/InstantStringConverter.java create mode 100644 fesod-sheet/src/test/java/org/apache/fesod/sheet/converters/instant/InstantStringConverterTest.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..aa36bd529 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 @@ -55,6 +55,7 @@ import org.apache.fesod.sheet.converters.floatconverter.FloatNumberConverter; import org.apache.fesod.sheet.converters.floatconverter.FloatStringConverter; import org.apache.fesod.sheet.converters.inputstream.InputStreamImageConverter; +import org.apache.fesod.sheet.converters.instant.InstantStringConverter; import org.apache.fesod.sheet.converters.integer.IntegerBooleanConverter; import org.apache.fesod.sheet.converters.integer.IntegerNumberConverter; import org.apache.fesod.sheet.converters.integer.IntegerStringConverter; @@ -123,6 +124,8 @@ private static void initAllConverter() { putAllConverter(new LocalTimeNumberConverter()); putAllConverter(new LocalTimeStringConverter()); + putAllConverter(new InstantStringConverter()); + putAllConverter(new DoubleBooleanConverter()); putAllConverter(new DoubleNumberConverter()); putAllConverter(new DoubleStringConverter()); @@ -160,6 +163,7 @@ private static void initDefaultWriteConverter() { putWriteConverter(new LocalDateTimeDateConverter()); putWriteConverter(new LocalDateDateConverter()); putWriteConverter(new LocalTimeDateConverter()); + putWriteConverter(new InstantStringConverter()); putWriteConverter(new DoubleNumberConverter()); putWriteConverter(new FloatNumberConverter()); putWriteConverter(new IntegerNumberConverter()); @@ -181,6 +185,7 @@ private static void initDefaultWriteConverter() { putWriteStringConverter(new LocalDateStringConverter()); putWriteStringConverter(new LocalDateTimeStringConverter()); putWriteStringConverter(new LocalTimeStringConverter()); + putWriteStringConverter(new InstantStringConverter()); putWriteStringConverter(new DoubleStringConverter()); putWriteStringConverter(new FloatStringConverter()); putWriteStringConverter(new IntegerStringConverter()); diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/instant/InstantStringConverter.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/instant/InstantStringConverter.java new file mode 100644 index 000000000..c7e3678ad --- /dev/null +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/instant/InstantStringConverter.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. + */ + +package org.apache.fesod.sheet.converters.instant; + +import java.time.Instant; +import java.time.OffsetDateTime; +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; + +/** + * Instant and ISO-8601 string converter. + */ +public class InstantStringConverter implements Converter { + + @Override + public Class supportJavaTypeKey() { + return Instant.class; + } + + @Override + public CellDataTypeEnum supportExcelTypeKey() { + return CellDataTypeEnum.STRING; + } + + @Override + public Instant convertToJavaData( + ReadCellData cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) { + return OffsetDateTime.parse(cellData.getStringValue()).toInstant(); + } + + @Override + public WriteCellData convertToExcelData( + Instant value, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) { + return new WriteCellData<>(value.toString()); + } +} diff --git a/fesod-sheet/src/test/java/org/apache/fesod/sheet/converters/DefaultConverterLoaderTest.java b/fesod-sheet/src/test/java/org/apache/fesod/sheet/converters/DefaultConverterLoaderTest.java index a30708a52..99bc854fe 100644 --- a/fesod-sheet/src/test/java/org/apache/fesod/sheet/converters/DefaultConverterLoaderTest.java +++ b/fesod-sheet/src/test/java/org/apache/fesod/sheet/converters/DefaultConverterLoaderTest.java @@ -19,9 +19,11 @@ package org.apache.fesod.sheet.converters; +import java.time.Instant; import java.time.LocalTime; import java.util.Map; import org.apache.fesod.sheet.converters.ConverterKeyBuild.ConverterKey; +import org.apache.fesod.sheet.converters.instant.InstantStringConverter; import org.apache.fesod.sheet.converters.localtime.LocalTimeDateConverter; import org.apache.fesod.sheet.converters.localtime.LocalTimeNumberConverter; import org.apache.fesod.sheet.converters.localtime.LocalTimeStringConverter; @@ -67,6 +69,21 @@ void loadConvertersRegistersLocalTimeFamily() { writeConverter.get(ConverterKeyBuild.buildKey(LocalTime.class, CellDataTypeEnum.STRING))); } + @Test + void loadConvertersRegistersInstantStringConverter() { + Map> allConverter = DefaultConverterLoader.loadAllConverter(); + Assertions.assertInstanceOf( + InstantStringConverter.class, + allConverter.get(ConverterKeyBuild.buildKey(Instant.class, CellDataTypeEnum.STRING))); + + Map> writeConverter = DefaultConverterLoader.loadDefaultWriteConverter(); + Assertions.assertInstanceOf( + InstantStringConverter.class, writeConverter.get(ConverterKeyBuild.buildKey(Instant.class))); + Assertions.assertInstanceOf( + InstantStringConverter.class, + writeConverter.get(ConverterKeyBuild.buildKey(Instant.class, CellDataTypeEnum.STRING))); + } + private static void assertLoadIsImmutableAndCopyIsMutable( Map> loaded, Map> copy) { Map.Entry> entry = diff --git a/fesod-sheet/src/test/java/org/apache/fesod/sheet/converters/instant/InstantStringConverterTest.java b/fesod-sheet/src/test/java/org/apache/fesod/sheet/converters/instant/InstantStringConverterTest.java new file mode 100644 index 000000000..50d1f700e --- /dev/null +++ b/fesod-sheet/src/test/java/org/apache/fesod/sheet/converters/instant/InstantStringConverterTest.java @@ -0,0 +1,81 @@ +/* + * 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. + */ + +package org.apache.fesod.sheet.converters.instant; + +import java.time.Instant; +import java.time.format.DateTimeParseException; +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.testkit.Tags; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +/** + * Tests {@link InstantStringConverter}. + */ +@Tag(Tags.UNIT) +class InstantStringConverterTest { + + private static final GlobalConfiguration GLOBAL_CONFIGURATION = new GlobalConfiguration(); + private final InstantStringConverter converter = new InstantStringConverter(); + + @Test + void supportKeys() { + Assertions.assertEquals(Instant.class, converter.supportJavaTypeKey()); + Assertions.assertEquals(CellDataTypeEnum.STRING, converter.supportExcelTypeKey()); + } + + @ParameterizedTest + @CsvSource({ + "1970-01-01T00:00:00Z, 1970-01-01T00:00:00Z", + "2026-09-08T12:34:56.123456789Z, 2026-09-08T12:34:56.123456789Z", + "2026-09-08T20:34:56+08:00, 2026-09-08T12:34:56Z" + }) + void convertToJavaDataParsesIso8601(String value, String expected) { + Instant actual = converter.convertToJavaData(new ReadCellData<>(value), null, GLOBAL_CONFIGURATION); + + Assertions.assertEquals(Instant.parse(expected), actual); + } + + @Test + void convertRoundTripPreservesNanoseconds() { + Instant value = Instant.parse("2026-09-08T12:34:56.123456789Z"); + + WriteCellData written = converter.convertToExcelData(value, null, GLOBAL_CONFIGURATION); + Instant actual = + converter.convertToJavaData(new ReadCellData<>(written.getStringValue()), null, GLOBAL_CONFIGURATION); + + Assertions.assertEquals(CellDataTypeEnum.STRING, written.getType()); + Assertions.assertEquals("2026-09-08T12:34:56.123456789Z", written.getStringValue()); + Assertions.assertEquals(value, actual); + } + + @Test + void convertToJavaDataRejectsInvalidInstant() { + Assertions.assertThrows( + DateTimeParseException.class, + () -> converter.convertToJavaData(new ReadCellData<>("not-an-instant"), null, GLOBAL_CONFIGURATION)); + } +} From f0af0eb752654a440d4528ed954e0f393aabcf27 Mon Sep 17 00:00:00 2001 From: 56wj <88013475+56wj@users.noreply.github.com> Date: Sun, 13 Sep 2026 21:28:57 +0800 Subject: [PATCH 2/3] fix: reject non-UTC offsets in Instant converter --- .../converters/instant/InstantStringConverter.java | 8 +++++++- .../instant/InstantStringConverterTest.java | 12 ++++++++++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/instant/InstantStringConverter.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/instant/InstantStringConverter.java index c7e3678ad..8924ff0cc 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/instant/InstantStringConverter.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/instant/InstantStringConverter.java @@ -21,6 +21,7 @@ import java.time.Instant; import java.time.OffsetDateTime; +import java.time.format.DateTimeParseException; import org.apache.fesod.sheet.converters.Converter; import org.apache.fesod.sheet.enums.CellDataTypeEnum; import org.apache.fesod.sheet.metadata.GlobalConfiguration; @@ -46,7 +47,12 @@ public CellDataTypeEnum supportExcelTypeKey() { @Override public Instant convertToJavaData( ReadCellData cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) { - return OffsetDateTime.parse(cellData.getStringValue()).toInstant(); + String value = cellData.getStringValue(); + OffsetDateTime offsetDateTime = OffsetDateTime.parse(value); + if (offsetDateTime.getOffset().getTotalSeconds() != 0) { + throw new DateTimeParseException("Instant value must use a UTC offset", value, 0); + } + return offsetDateTime.toInstant(); } @Override diff --git a/fesod-sheet/src/test/java/org/apache/fesod/sheet/converters/instant/InstantStringConverterTest.java b/fesod-sheet/src/test/java/org/apache/fesod/sheet/converters/instant/InstantStringConverterTest.java index 50d1f700e..f0b876ee2 100644 --- a/fesod-sheet/src/test/java/org/apache/fesod/sheet/converters/instant/InstantStringConverterTest.java +++ b/fesod-sheet/src/test/java/org/apache/fesod/sheet/converters/instant/InstantStringConverterTest.java @@ -51,14 +51,22 @@ void supportKeys() { @CsvSource({ "1970-01-01T00:00:00Z, 1970-01-01T00:00:00Z", "2026-09-08T12:34:56.123456789Z, 2026-09-08T12:34:56.123456789Z", - "2026-09-08T20:34:56+08:00, 2026-09-08T12:34:56Z" + "2026-09-08T12:34:56+00:00, 2026-09-08T12:34:56Z" }) - void convertToJavaDataParsesIso8601(String value, String expected) { + void convertToJavaDataParsesUtcIso8601(String value, String expected) { Instant actual = converter.convertToJavaData(new ReadCellData<>(value), null, GLOBAL_CONFIGURATION); Assertions.assertEquals(Instant.parse(expected), actual); } + @Test + void convertToJavaDataRejectsNonUtcOffset() { + Assertions.assertThrows( + DateTimeParseException.class, + () -> converter.convertToJavaData( + new ReadCellData<>("2026-09-08T20:34:56+08:00"), null, GLOBAL_CONFIGURATION)); + } + @Test void convertRoundTripPreservesNanoseconds() { Instant value = Instant.parse("2026-09-08T12:34:56.123456789Z"); From 4a7c6bb14edd64c72619ff001368019691107a78 Mon Sep 17 00:00:00 2001 From: 56wj <88013475+56wj@users.noreply.github.com> Date: Mon, 14 Sep 2026 09:55:26 +0800 Subject: [PATCH 3/3] refactor: compare Instant offset with UTC --- .../fesod/sheet/converters/instant/InstantStringConverter.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/instant/InstantStringConverter.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/instant/InstantStringConverter.java index 8924ff0cc..d6d10cdca 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/instant/InstantStringConverter.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/instant/InstantStringConverter.java @@ -21,6 +21,7 @@ import java.time.Instant; import java.time.OffsetDateTime; +import java.time.ZoneOffset; import java.time.format.DateTimeParseException; import org.apache.fesod.sheet.converters.Converter; import org.apache.fesod.sheet.enums.CellDataTypeEnum; @@ -49,7 +50,7 @@ public Instant convertToJavaData( ReadCellData cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) { String value = cellData.getStringValue(); OffsetDateTime offsetDateTime = OffsetDateTime.parse(value); - if (offsetDateTime.getOffset().getTotalSeconds() != 0) { + if (!offsetDateTime.getOffset().equals(ZoneOffset.UTC)) { throw new DateTimeParseException("Instant value must use a UTC offset", value, 0); } return offsetDateTime.toInstant();