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 b0372279a..88e0990ab 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 @@ -56,6 +56,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; @@ -137,6 +138,8 @@ private static void initAllConverter() { putAllConverter(new OffsetDateTimeNumberConverter()); putAllConverter(new OffsetDateTimeStringConverter()); + putAllConverter(new InstantStringConverter()); + putAllConverter(new DoubleBooleanConverter()); putAllConverter(new DoubleNumberConverter()); putAllConverter(new DoubleStringConverter()); @@ -177,6 +180,7 @@ private static void initDefaultWriteConverter() { putWriteConverter(new LocalDateTimeDateConverter()); putWriteConverter(new LocalDateDateConverter()); putWriteConverter(new LocalTimeDateConverter()); + putWriteConverter(new InstantStringConverter()); putWriteConverter(new SqlTimeDateConverter()); putWriteConverter(new OffsetDateTimeDateConverter()); putWriteConverter(new DoubleNumberConverter()); @@ -202,6 +206,7 @@ private static void initDefaultWriteConverter() { putWriteStringConverter(new LocalDateStringConverter()); putWriteStringConverter(new LocalDateTimeStringConverter()); putWriteStringConverter(new LocalTimeStringConverter()); + putWriteStringConverter(new InstantStringConverter()); putWriteStringConverter(new SqlTimeStringConverter()); putWriteStringConverter(new OffsetDateTimeStringConverter()); putWriteStringConverter(new DoubleStringConverter()); 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..d6d10cdca --- /dev/null +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/instant/InstantStringConverter.java @@ -0,0 +1,64 @@ +/* + * 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 java.time.ZoneOffset; +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; +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) { + String value = cellData.getStringValue(); + OffsetDateTime offsetDateTime = OffsetDateTime.parse(value); + if (!offsetDateTime.getOffset().equals(ZoneOffset.UTC)) { + throw new DateTimeParseException("Instant value must use a UTC offset", value, 0); + } + return offsetDateTime.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 a9240a120..819c28623 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 @@ -20,9 +20,11 @@ package org.apache.fesod.sheet.converters; import java.sql.Time; +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; @@ -71,6 +73,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))); + } + @Test void loadConvertersRegistersSqlTimeFamily() { Map> allConverter = DefaultConverterLoader.loadAllConverter(); 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..f0b876ee2 --- /dev/null +++ b/fesod-sheet/src/test/java/org/apache/fesod/sheet/converters/instant/InstantStringConverterTest.java @@ -0,0 +1,89 @@ +/* + * 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-08T12:34:56+00:00, 2026-09-08T12:34:56Z" + }) + 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"); + + 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)); + } +}