Skip to content

Commit 9a0371a

Browse files
committed
fix: Handle java.sql date types compatibility in Excel read and write operations
1 parent 28f2933 commit 9a0371a

File tree

8 files changed

+100
-16
lines changed

8 files changed

+100
-16
lines changed

fesod/src/main/java/org/apache/fesod/sheet/converters/date/DateNumberConverter.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import org.apache.fesod.sheet.metadata.data.ReadCellData;
2828
import org.apache.fesod.sheet.metadata.data.WriteCellData;
2929
import org.apache.fesod.sheet.metadata.property.ExcelContentProperty;
30+
import org.apache.fesod.sheet.util.BooleanUtils;
3031
import org.apache.fesod.sheet.util.DateUtils;
3132
import org.apache.poi.ss.usermodel.DateUtil;
3233

@@ -52,11 +53,13 @@ public Date convertToJavaData(
5253
ReadCellData<?> cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) {
5354
if (contentProperty == null || contentProperty.getDateTimeFormatProperty() == null) {
5455
return DateUtils.getJavaDate(
55-
cellData.getNumberValue().doubleValue(), globalConfiguration.getUse1904windowing());
56+
cellData.getNumberValue().doubleValue(), globalConfiguration.getUse1904windowing(), null);
5657
} else {
5758
return DateUtils.getJavaDate(
5859
cellData.getNumberValue().doubleValue(),
59-
contentProperty.getDateTimeFormatProperty().getUse1904windowing());
60+
BooleanUtils.isTrue(
61+
contentProperty.getDateTimeFormatProperty().getUse1904windowing()),
62+
contentProperty.getDateTimeFormatProperty().getFormat());
6063
}
6164
}
6265

fesod/src/main/java/org/apache/fesod/sheet/metadata/data/WriteCellData.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -164,8 +164,14 @@ public WriteCellData(Date dateValue) {
164164
throw new IllegalArgumentException("DateValue can not be null");
165165
}
166166
setType(CellDataTypeEnum.DATE);
167-
// Use getTime() which works for both java.util.Date and java.sql.Date
168-
this.dateValue = LocalDateTime.ofInstant(Instant.ofEpochMilli(dateValue.getTime()), ZoneId.systemDefault());
167+
// sql.Date and sql.Time don't support toInstant() so use getTime() which provides millisecond precision
168+
if (dateValue.getClass() == java.sql.Date.class || dateValue.getClass() == java.sql.Time.class) {
169+
this.dateValue = LocalDateTime.ofInstant(Instant.ofEpochMilli(dateValue.getTime()), ZoneId.systemDefault());
170+
} else {
171+
// util.Date and sql.Timestamp support toInstant() which preserves full precision
172+
// LocalDateTime stores nanoseconds internally, Excel stores milliseconds when written
173+
this.dateValue = LocalDateTime.ofInstant(dateValue.toInstant(), ZoneId.systemDefault());
174+
}
169175
}
170176

171177
/**

fesod/src/main/java/org/apache/fesod/sheet/metadata/format/DataFormatter.java

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@ private Format getFormat(Double data, Short dataFormat, String dataFormatString)
218218
&&
219219
// don't try to handle Date value 0, let a 3 or 4-part format take care of it
220220
data.doubleValue() != 0.0) {
221-
cellValueO = DateUtils.getJavaDate(data, use1904windowing);
221+
cellValueO = DateUtils.getJavaDate(data, use1904windowing, formatStr);
222222
}
223223
// Wrap and return (non-cachable - CellFormat does that)
224224
return new CellFormatResultWrapper(cfmt.apply(cellValueO));
@@ -640,7 +640,8 @@ private String getFormattedDateString(Double data, Short dataFormat, String data
640640
// Hint about the raw excel value
641641
((ExcelStyleDateFormatter) dateFormat).setDateToBeFormatted(data);
642642
}
643-
return performDateFormatting(DateUtils.getJavaDate(data, use1904windowing), dateFormat);
643+
// Use format-aware getJavaDate to preserve milliseconds when format includes .S or .0 patterns
644+
return performDateFormatting(DateUtils.getJavaDate(data, use1904windowing, dataFormatString), dateFormat);
644645
}
645646

646647
/**
@@ -671,7 +672,10 @@ private String getFormattedNumberString(BigDecimal data, Short dataFormat, Strin
671672
*/
672673
public String format(BigDecimal data, Short dataFormat, String dataFormatString) {
673674
if (DateUtils.isADateFormat(dataFormat, dataFormatString)) {
674-
return getFormattedDateString(data.doubleValue(), dataFormat, dataFormatString);
675+
// Convert Java SimpleDateFormat pattern to Excel format code for milliseconds
676+
// Java uses .SSS while Excel uses .000 to represent milliseconds
677+
String excelFormatString = dataFormatString.replace(".SSS", ".000");
678+
return getFormattedDateString(data.doubleValue(), dataFormat, excelFormatString);
675679
}
676680
return getFormattedNumberString(data, dataFormat, dataFormatString);
677681
}

fesod/src/main/java/org/apache/fesod/sheet/util/DateUtils.java

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,17 @@ public static String format(LocalDateTime date, String dateFormat) {
279279
return format(date, dateFormat, null);
280280
}
281281

282+
/**
283+
* Check if date format pattern includes millisecond precision indicators.
284+
* Returns true if format contains .S (Java SimpleDateFormat) or .0 (Excel format code).
285+
*
286+
* @param dateFormat The date format pattern to check
287+
* @return true if format includes millisecond patterns, false otherwise
288+
*/
289+
private static boolean hasMillisecondPattern(String dateFormat) {
290+
return dateFormat != null && (dateFormat.contains(".S") || dateFormat.contains(".0"));
291+
}
292+
282293
/**
283294
* Format date
284295
*
@@ -290,8 +301,12 @@ public static String format(BigDecimal date, Boolean use1904windowing, String da
290301
if (date == null) {
291302
return null;
292303
}
304+
// Only preserve fractional seconds when format includes millisecond patterns
305+
// Otherwise round to maintain backward compatibility
306+
boolean roundSeconds = !hasMillisecondPattern(dateFormat);
307+
293308
LocalDateTime localDateTime =
294-
DateUtil.getLocalDateTime(date.doubleValue(), BooleanUtils.isTrue(use1904windowing), true);
309+
DateUtil.getLocalDateTime(date.doubleValue(), BooleanUtils.isTrue(use1904windowing), roundSeconds);
295310
return format(localDateTime, dateFormat);
296311
}
297312

@@ -326,7 +341,23 @@ private static DateFormat getCacheDateFormat(String dateFormat) {
326341
* @return Java representation of the date, or null if date is not a valid Excel date
327342
*/
328343
public static Date getJavaDate(double date, boolean use1904windowing) {
329-
Calendar calendar = getJavaCalendar(date, use1904windowing, null, true);
344+
return getJavaDate(date, use1904windowing, null);
345+
}
346+
347+
/**
348+
* Given an Excel date with either 1900 or 1904 date windowing,
349+
* converts it to a java.util.Date with conditional rounding based on format pattern.
350+
* Only preserves fractional seconds when format includes millisecond patterns.
351+
*
352+
* @param date The Excel date.
353+
* @param use1904windowing true if date uses 1904 windowing,
354+
* or false if using 1900 date windowing.
355+
* @param dateFormat The format pattern to determine if milliseconds should be preserved.
356+
* @return Java representation of the date, or null if date is not a valid Excel date
357+
*/
358+
public static Date getJavaDate(double date, boolean use1904windowing, String dateFormat) {
359+
boolean roundSeconds = !hasMillisecondPattern(dateFormat);
360+
Calendar calendar = getJavaCalendar(date, use1904windowing, null, roundSeconds);
330361
return calendar == null ? null : calendar.getTime();
331362
}
332363

@@ -540,9 +571,9 @@ public static boolean isADateFormatUncached(Short formatIndex, String formatStri
540571
*/
541572
public static boolean isInternalDateFormat(short format) {
542573
switch (format) {
543-
// Internal Date Formats as described on page 427 in
544-
// Microsoft Excel Dev's Kit...
545-
// 14-22
574+
// Internal Date Formats as described on page 427 in
575+
// Microsoft Excel Dev's Kit...
576+
// 14-22
546577
case 0x0e:
547578
case 0x0f:
548579
case 0x10:
@@ -552,7 +583,7 @@ public static boolean isInternalDateFormat(short format) {
552583
case 0x14:
553584
case 0x15:
554585
case 0x16:
555-
// 45-47
586+
// 45-47
556587
case 0x2d:
557588
case 0x2e:
558589
case 0x2f:

fesod/src/test/java/org/apache/fesod/sheet/celldata/CellDataDataListener.java

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import org.apache.fesod.sheet.context.AnalysisContext;
2727
import org.apache.fesod.sheet.event.AnalysisEventListener;
2828
import org.apache.fesod.sheet.support.ExcelTypeEnum;
29+
import org.apache.fesod.sheet.util.DateUtils;
2930
import org.junit.jupiter.api.Assertions;
3031

3132
/**
@@ -46,9 +47,24 @@ public void doAfterAllAnalysed(AnalysisContext context) {
4647
Assertions.assertEquals(1, list.size());
4748
CellDataReadData cellDataData = list.get(0);
4849

49-
Assertions.assertEquals("2020-01-01", cellDataData.getDate().getData());
50+
// Verify util.Date preserves seconds
51+
Assertions.assertEquals("2020-01-01 01:01:01", cellDataData.getDate().getData());
52+
53+
// Verify sql.Date contains date only
5054
Assertions.assertEquals("2020-01-01", cellDataData.getSqlDate().getData());
5155

56+
// Verify sql.Timestamp preserves milliseconds
57+
Assertions.assertEquals(
58+
"2020-01-01 01:01:01.789", cellDataData.getSqlTimestamp().getData());
59+
60+
// Verify sql.Time contains time only
61+
Assertions.assertEquals("01:01:01", cellDataData.getSqlTime().getData());
62+
63+
// Verify sql.Timestamp read as Date type preserves milliseconds
64+
Assertions.assertEquals(
65+
"2020-01-01 01:01:01.789",
66+
DateUtils.format(cellDataData.getSqlTimestampAsDate(), "yyyy-MM-dd HH:mm:ss.SSS"));
67+
5268
Assertions.assertEquals(2L, (long) cellDataData.getInteger1().getData());
5369
Assertions.assertEquals(2L, (long) cellDataData.getInteger2());
5470

fesod/src/test/java/org/apache/fesod/sheet/celldata/CellDataDataTest.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,9 @@ private List<CellDataWriteData> data() throws Exception {
7979

8080
cellDataData.setDate(new WriteCellData<>(DateUtils.parseDate("2020-01-01 01:01:01")));
8181
cellDataData.setSqlDate(new WriteCellData<>(java.sql.Date.valueOf("2020-01-01")));
82+
cellDataData.setSqlTimestamp(new WriteCellData<>(java.sql.Timestamp.valueOf("2020-01-01 01:01:01.789")));
83+
cellDataData.setSqlTime(new WriteCellData<>(java.sql.Time.valueOf("01:01:01")));
84+
cellDataData.setSqlTimestampAsDate(java.sql.Timestamp.valueOf("2020-01-01 01:01:01.789"));
8285

8386
WriteCellData<Integer> integer1 = new WriteCellData<>();
8487
integer1.setType(CellDataTypeEnum.NUMBER);

fesod/src/test/java/org/apache/fesod/sheet/celldata/CellDataReadData.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
package org.apache.fesod.sheet.celldata;
2121

22+
import java.util.Date;
2223
import lombok.EqualsAndHashCode;
2324
import lombok.Getter;
2425
import lombok.Setter;
@@ -32,12 +33,22 @@
3233
@Setter
3334
@EqualsAndHashCode
3435
public class CellDataReadData {
35-
@DateTimeFormat("yyyy-MM-dd")
36+
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
3637
private ReadCellData<String> date;
3738

3839
@DateTimeFormat("yyyy-MM-dd")
3940
private ReadCellData<String> sqlDate;
4041

42+
@DateTimeFormat("yyyy-MM-dd HH:mm:ss.SSS")
43+
private ReadCellData<String> sqlTimestamp;
44+
45+
@DateTimeFormat("HH:mm:ss")
46+
private ReadCellData<String> sqlTime;
47+
48+
// Read as Date type to test DateNumberConverter preserves milliseconds
49+
@DateTimeFormat("yyyy-MM-dd HH:mm:ss.SSS")
50+
private Date sqlTimestampAsDate;
51+
4152
private ReadCellData<Integer> integer1;
4253
private Integer integer2;
4354
private ReadCellData<?> formulaValue;

fesod/src/test/java/org/apache/fesod/sheet/celldata/CellDataWriteData.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,22 @@
3333
@Setter
3434
@EqualsAndHashCode
3535
public class CellDataWriteData {
36-
@DateTimeFormat("yyyy-MM-dd")
36+
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
3737
private WriteCellData<Date> date;
3838

3939
@DateTimeFormat("yyyy-MM-dd")
4040
private WriteCellData<Date> sqlDate;
4141

42+
@DateTimeFormat("yyyy-MM-dd HH:mm:ss.SSS")
43+
private WriteCellData<Date> sqlTimestamp;
44+
45+
@DateTimeFormat("HH:mm:ss")
46+
private WriteCellData<Date> sqlTime;
47+
48+
// Write as plain Date to test DateNumberConverter
49+
@DateTimeFormat("yyyy-MM-dd HH:mm:ss.SSS")
50+
private Date sqlTimestampAsDate;
51+
4252
private WriteCellData<Integer> integer1;
4353
private Integer integer2;
4454
private WriteCellData<?> formulaValue;

0 commit comments

Comments
 (0)