diff --git a/fesod/src/main/java/org/apache/fesod/sheet/converters/date/DateNumberConverter.java b/fesod/src/main/java/org/apache/fesod/sheet/converters/date/DateNumberConverter.java index 3c98be59a..3a838a14f 100644 --- a/fesod/src/main/java/org/apache/fesod/sheet/converters/date/DateNumberConverter.java +++ b/fesod/src/main/java/org/apache/fesod/sheet/converters/date/DateNumberConverter.java @@ -27,6 +27,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.BooleanUtils; import org.apache.fesod.sheet.util.DateUtils; import org.apache.poi.ss.usermodel.DateUtil; @@ -52,11 +53,13 @@ public Date convertToJavaData( ReadCellData cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) { if (contentProperty == null || contentProperty.getDateTimeFormatProperty() == null) { return DateUtils.getJavaDate( - cellData.getNumberValue().doubleValue(), globalConfiguration.getUse1904windowing()); + cellData.getNumberValue().doubleValue(), globalConfiguration.getUse1904windowing(), null); } else { return DateUtils.getJavaDate( cellData.getNumberValue().doubleValue(), - contentProperty.getDateTimeFormatProperty().getUse1904windowing()); + BooleanUtils.isTrue( + contentProperty.getDateTimeFormatProperty().getUse1904windowing()), + contentProperty.getDateTimeFormatProperty().getFormat()); } } diff --git a/fesod/src/main/java/org/apache/fesod/sheet/metadata/data/WriteCellData.java b/fesod/src/main/java/org/apache/fesod/sheet/metadata/data/WriteCellData.java index 49112a1a3..c163ddfcc 100644 --- a/fesod/src/main/java/org/apache/fesod/sheet/metadata/data/WriteCellData.java +++ b/fesod/src/main/java/org/apache/fesod/sheet/metadata/data/WriteCellData.java @@ -20,6 +20,7 @@ package org.apache.fesod.sheet.metadata.data; import java.math.BigDecimal; +import java.time.Instant; import java.time.LocalDateTime; import java.time.ZoneId; import java.util.Date; @@ -163,7 +164,14 @@ public WriteCellData(Date dateValue) { throw new IllegalArgumentException("DateValue can not be null"); } setType(CellDataTypeEnum.DATE); - this.dateValue = LocalDateTime.ofInstant(dateValue.toInstant(), ZoneId.systemDefault()); + // sql.Date and sql.Time don't support toInstant() so use getTime() which provides millisecond precision + if (dateValue.getClass() == java.sql.Date.class || dateValue.getClass() == java.sql.Time.class) { + this.dateValue = LocalDateTime.ofInstant(Instant.ofEpochMilli(dateValue.getTime()), ZoneId.systemDefault()); + } else { + // util.Date and sql.Timestamp support toInstant() which preserves full precision + // LocalDateTime stores nanoseconds internally, Excel stores milliseconds when written + this.dateValue = LocalDateTime.ofInstant(dateValue.toInstant(), ZoneId.systemDefault()); + } } /** diff --git a/fesod/src/main/java/org/apache/fesod/sheet/metadata/format/DataFormatter.java b/fesod/src/main/java/org/apache/fesod/sheet/metadata/format/DataFormatter.java index ec37963dd..5186b2698 100644 --- a/fesod/src/main/java/org/apache/fesod/sheet/metadata/format/DataFormatter.java +++ b/fesod/src/main/java/org/apache/fesod/sheet/metadata/format/DataFormatter.java @@ -218,7 +218,7 @@ private Format getFormat(Double data, Short dataFormat, String dataFormatString) && // don't try to handle Date value 0, let a 3 or 4-part format take care of it data.doubleValue() != 0.0) { - cellValueO = DateUtils.getJavaDate(data, use1904windowing); + cellValueO = DateUtils.getJavaDate(data, use1904windowing, formatStr); } // Wrap and return (non-cachable - CellFormat does that) return new CellFormatResultWrapper(cfmt.apply(cellValueO)); @@ -640,7 +640,8 @@ private String getFormattedDateString(Double data, Short dataFormat, String data // Hint about the raw excel value ((ExcelStyleDateFormatter) dateFormat).setDateToBeFormatted(data); } - return performDateFormatting(DateUtils.getJavaDate(data, use1904windowing), dateFormat); + // Use format-aware getJavaDate to preserve milliseconds when format includes .S or .0 patterns + return performDateFormatting(DateUtils.getJavaDate(data, use1904windowing, dataFormatString), dateFormat); } /** @@ -671,7 +672,10 @@ private String getFormattedNumberString(BigDecimal data, Short dataFormat, Strin */ public String format(BigDecimal data, Short dataFormat, String dataFormatString) { if (DateUtils.isADateFormat(dataFormat, dataFormatString)) { - return getFormattedDateString(data.doubleValue(), dataFormat, dataFormatString); + // Convert Java SimpleDateFormat pattern to Excel format code for milliseconds + // Java uses .SSS while Excel uses .000 to represent milliseconds + String excelFormatString = dataFormatString.replace(".SSS", ".000"); + return getFormattedDateString(data.doubleValue(), dataFormat, excelFormatString); } return getFormattedNumberString(data, dataFormat, dataFormatString); } diff --git a/fesod/src/main/java/org/apache/fesod/sheet/util/DateUtils.java b/fesod/src/main/java/org/apache/fesod/sheet/util/DateUtils.java index 4086bc2b4..654a1fda8 100644 --- a/fesod/src/main/java/org/apache/fesod/sheet/util/DateUtils.java +++ b/fesod/src/main/java/org/apache/fesod/sheet/util/DateUtils.java @@ -279,6 +279,17 @@ public static String format(LocalDateTime date, String dateFormat) { return format(date, dateFormat, null); } + /** + * Check if date format pattern includes millisecond precision indicators. + * Returns true if format contains .S (Java SimpleDateFormat) or .0 (Excel format code). + * + * @param dateFormat The date format pattern to check + * @return true if format includes millisecond patterns, false otherwise + */ + private static boolean hasMillisecondPattern(String dateFormat) { + return dateFormat != null && (dateFormat.contains(".S") || dateFormat.contains(".0")); + } + /** * Format date * @@ -290,8 +301,12 @@ public static String format(BigDecimal date, Boolean use1904windowing, String da if (date == null) { return null; } + // Only preserve fractional seconds when format includes millisecond patterns + // Otherwise round to maintain backward compatibility + boolean roundSeconds = !hasMillisecondPattern(dateFormat); + LocalDateTime localDateTime = - DateUtil.getLocalDateTime(date.doubleValue(), BooleanUtils.isTrue(use1904windowing), true); + DateUtil.getLocalDateTime(date.doubleValue(), BooleanUtils.isTrue(use1904windowing), roundSeconds); return format(localDateTime, dateFormat); } @@ -326,7 +341,23 @@ private static DateFormat getCacheDateFormat(String dateFormat) { * @return Java representation of the date, or null if date is not a valid Excel date */ public static Date getJavaDate(double date, boolean use1904windowing) { - Calendar calendar = getJavaCalendar(date, use1904windowing, null, true); + return getJavaDate(date, use1904windowing, null); + } + + /** + * Given an Excel date with either 1900 or 1904 date windowing, + * converts it to a java.util.Date with conditional rounding based on format pattern. + * Only preserves fractional seconds when format includes millisecond patterns. + * + * @param date The Excel date. + * @param use1904windowing true if date uses 1904 windowing, + * or false if using 1900 date windowing. + * @param dateFormat The format pattern to determine if milliseconds should be preserved. + * @return Java representation of the date, or null if date is not a valid Excel date + */ + public static Date getJavaDate(double date, boolean use1904windowing, String dateFormat) { + boolean roundSeconds = !hasMillisecondPattern(dateFormat); + Calendar calendar = getJavaCalendar(date, use1904windowing, null, roundSeconds); return calendar == null ? null : calendar.getTime(); } diff --git a/fesod/src/test/java/org/apache/fesod/sheet/celldata/CellDataDataListener.java b/fesod/src/test/java/org/apache/fesod/sheet/celldata/CellDataDataListener.java index 7dd76ac7b..c238e783b 100644 --- a/fesod/src/test/java/org/apache/fesod/sheet/celldata/CellDataDataListener.java +++ b/fesod/src/test/java/org/apache/fesod/sheet/celldata/CellDataDataListener.java @@ -26,6 +26,7 @@ import org.apache.fesod.sheet.context.AnalysisContext; import org.apache.fesod.sheet.event.AnalysisEventListener; import org.apache.fesod.sheet.support.ExcelTypeEnum; +import org.apache.fesod.sheet.util.DateUtils; import org.junit.jupiter.api.Assertions; /** @@ -46,15 +47,34 @@ public void doAfterAllAnalysed(AnalysisContext context) { Assertions.assertEquals(1, list.size()); CellDataReadData cellDataData = list.get(0); - Assertions.assertEquals("2020年01月01日", cellDataData.getDate().getData()); + // Verify util.Date preserves seconds + Assertions.assertEquals("2020-01-01 01:01:01", cellDataData.getDate().getData()); + + // Verify sql.Date contains date only + Assertions.assertEquals("2020-01-01", cellDataData.getSqlDate().getData()); + + // Verify sql.Timestamp preserves milliseconds + Assertions.assertEquals( + "2020-01-01 01:01:01.789", cellDataData.getSqlTimestamp().getData()); + + // Verify sql.Time contains time only + Assertions.assertEquals("01:01:01", cellDataData.getSqlTime().getData()); + + // Verify sql.Timestamp read as Date type preserves milliseconds + Assertions.assertEquals( + "2020-01-01 01:01:01.789", + DateUtils.format(cellDataData.getSqlTimestampAsDate(), "yyyy-MM-dd HH:mm:ss.SSS")); + Assertions.assertEquals(2L, (long) cellDataData.getInteger1().getData()); Assertions.assertEquals(2L, (long) cellDataData.getInteger2()); + if (context.readWorkbookHolder().getExcelType() != ExcelTypeEnum.CSV) { Assertions.assertEquals( "B2+C2", cellDataData.getFormulaValue().getFormulaData().getFormulaValue()); } else { Assertions.assertNull(cellDataData.getFormulaValue().getData()); } + log.debug("First row:{}", JSON.toJSONString(list.get(0))); } } diff --git a/fesod/src/test/java/org/apache/fesod/sheet/celldata/CellDataDataTest.java b/fesod/src/test/java/org/apache/fesod/sheet/celldata/CellDataDataTest.java index 81ea1b53f..6c648110d 100644 --- a/fesod/src/test/java/org/apache/fesod/sheet/celldata/CellDataDataTest.java +++ b/fesod/src/test/java/org/apache/fesod/sheet/celldata/CellDataDataTest.java @@ -76,17 +76,25 @@ private void readAndWrite(File file) throws Exception { private List data() throws Exception { List list = new ArrayList<>(); CellDataWriteData cellDataData = new CellDataWriteData(); + cellDataData.setDate(new WriteCellData<>(DateUtils.parseDate("2020-01-01 01:01:01"))); + cellDataData.setSqlDate(new WriteCellData<>(java.sql.Date.valueOf("2020-01-01"))); + cellDataData.setSqlTimestamp(new WriteCellData<>(java.sql.Timestamp.valueOf("2020-01-01 01:01:01.789"))); + cellDataData.setSqlTime(new WriteCellData<>(java.sql.Time.valueOf("01:01:01"))); + cellDataData.setSqlTimestampAsDate(java.sql.Timestamp.valueOf("2020-01-01 01:01:01.789")); + WriteCellData integer1 = new WriteCellData<>(); integer1.setType(CellDataTypeEnum.NUMBER); integer1.setNumberValue(BigDecimal.valueOf(2L)); cellDataData.setInteger1(integer1); cellDataData.setInteger2(2); + WriteCellData formulaValue = new WriteCellData<>(); FormulaData formulaData = new FormulaData(); formulaValue.setFormulaData(formulaData); formulaData.setFormulaValue("B2+C2"); cellDataData.setFormulaValue(formulaValue); + list.add(cellDataData); return list; } diff --git a/fesod/src/test/java/org/apache/fesod/sheet/celldata/CellDataReadData.java b/fesod/src/test/java/org/apache/fesod/sheet/celldata/CellDataReadData.java index ffacf9c76..766e5635a 100644 --- a/fesod/src/test/java/org/apache/fesod/sheet/celldata/CellDataReadData.java +++ b/fesod/src/test/java/org/apache/fesod/sheet/celldata/CellDataReadData.java @@ -19,6 +19,7 @@ package org.apache.fesod.sheet.celldata; +import java.util.Date; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; @@ -32,9 +33,22 @@ @Setter @EqualsAndHashCode public class CellDataReadData { - @DateTimeFormat("yyyy年MM月dd日") + @DateTimeFormat("yyyy-MM-dd HH:mm:ss") private ReadCellData date; + @DateTimeFormat("yyyy-MM-dd") + private ReadCellData sqlDate; + + @DateTimeFormat("yyyy-MM-dd HH:mm:ss.SSS") + private ReadCellData sqlTimestamp; + + @DateTimeFormat("HH:mm:ss") + private ReadCellData sqlTime; + + // Read as Date type to test DateNumberConverter preserves milliseconds + @DateTimeFormat("yyyy-MM-dd HH:mm:ss.SSS") + private Date sqlTimestampAsDate; + private ReadCellData integer1; private Integer integer2; private ReadCellData formulaValue; diff --git a/fesod/src/test/java/org/apache/fesod/sheet/celldata/CellDataWriteData.java b/fesod/src/test/java/org/apache/fesod/sheet/celldata/CellDataWriteData.java index 34923cbf3..181b132e1 100644 --- a/fesod/src/test/java/org/apache/fesod/sheet/celldata/CellDataWriteData.java +++ b/fesod/src/test/java/org/apache/fesod/sheet/celldata/CellDataWriteData.java @@ -33,9 +33,22 @@ @Setter @EqualsAndHashCode public class CellDataWriteData { - @DateTimeFormat("yyyy年MM月dd日") + @DateTimeFormat("yyyy-MM-dd HH:mm:ss") private WriteCellData date; + @DateTimeFormat("yyyy-MM-dd") + private WriteCellData sqlDate; + + @DateTimeFormat("yyyy-MM-dd HH:mm:ss.SSS") + private WriteCellData sqlTimestamp; + + @DateTimeFormat("HH:mm:ss") + private WriteCellData sqlTime; + + // Write as plain Date to test DateNumberConverter + @DateTimeFormat("yyyy-MM-dd HH:mm:ss.SSS") + private Date sqlTimestampAsDate; + private WriteCellData integer1; private Integer integer2; private WriteCellData formulaValue;