Skip to content
Open
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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());
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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());
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down Expand Up @@ -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);
}

/**
Expand Down Expand Up @@ -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);
}
Expand Down
35 changes: 33 additions & 2 deletions fesod/src/main/java/org/apache/fesod/sheet/util/DateUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
*
Expand All @@ -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);
}

Expand Down Expand Up @@ -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();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand All @@ -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)));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -76,17 +76,25 @@ private void readAndWrite(File file) throws Exception {
private List<CellDataWriteData> data() throws Exception {
List<CellDataWriteData> 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<Integer> 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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

package org.apache.fesod.sheet.celldata;

import java.util.Date;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
Expand All @@ -32,9 +33,22 @@
@Setter
@EqualsAndHashCode
public class CellDataReadData {
@DateTimeFormat("yyyy年MM月dd日")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private ReadCellData<String> date;

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

@DateTimeFormat("yyyy-MM-dd HH:mm:ss.SSS")
private ReadCellData<String> sqlTimestamp;

@DateTimeFormat("HH:mm:ss")
private ReadCellData<String> sqlTime;

// Read as Date type to test DateNumberConverter preserves milliseconds
@DateTimeFormat("yyyy-MM-dd HH:mm:ss.SSS")
private Date sqlTimestampAsDate;

private ReadCellData<Integer> integer1;
private Integer integer2;
private ReadCellData<?> formulaValue;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,22 @@
@Setter
@EqualsAndHashCode
public class CellDataWriteData {
@DateTimeFormat("yyyy年MM月dd日")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private WriteCellData<Date> date;

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

@DateTimeFormat("yyyy-MM-dd HH:mm:ss.SSS")
private WriteCellData<Date> sqlTimestamp;

@DateTimeFormat("HH:mm:ss")
private WriteCellData<Date> sqlTime;

// Write as plain Date to test DateNumberConverter
@DateTimeFormat("yyyy-MM-dd HH:mm:ss.SSS")
private Date sqlTimestampAsDate;

private WriteCellData<Integer> integer1;
private Integer integer2;
private WriteCellData<?> formulaValue;
Expand Down
Loading