Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ public void registerCapabilities(ConnectorFunctions connectorFunctions, TapCodec
codecRegistry.registerToTapValue(LocalDate.class, (value, tapType) ->
new TapDateValue(new DateTime(((LocalDate) value).atStartOfDay())));
codecRegistry.registerToTapValue(LocalTime.class, (value, tapType) ->
new TapTimeValue(DateTime.withTimeStr(value.toString())));
new TapTimeValue(DateTime.withTimeStr(CellValueConvert.formatTemporalValue(value))));
codecRegistry.registerFromTapValue(TapRawValue.class, "STRING", tapRawValue -> {
if (tapRawValue != null && tapRawValue.getValue() != null) return toJson(tapRawValue.getValue());
return "null";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,11 @@ public static Object parseValue(Object val, String fieldDataType) {

public static Object parseValue(Object val, Object displayValue, String fieldDataType) {
if (isStringField(fieldDataType)) {
return stringValue(val);
String temporalValue = formatTemporalValue(val);
if (temporalValue != null) {
return temporalValue;
}
return parseValue(displayValue);
}
if (displayValue instanceof String && StringUtils.isBlank((String) displayValue)) {
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ public static Object getCellValue(Cell cell, FormulaEvaluator formulaEvaluator)
return cell.getRichStringCellValue().getString();

case NUMERIC:
if (DateUtil.isCellDateFormatted(cell) || cell.getCellStyle().getDataFormat() == 58) {
if (isCellDateOrTimeFormatted(cell)) {
return parseDateTimeValue(cell);
} else {
return parseNumberValue(cell);
Expand All @@ -190,7 +190,7 @@ public static Object getCellValue(Cell cell, FormulaEvaluator formulaEvaluator)
return cell.getRichStringCellValue().getString();

case NUMERIC:
if (DateUtil.isCellDateFormatted(cell) || cell.getCellStyle().getDataFormat() == 58) {
if (isCellDateOrTimeFormatted(cell)) {
return parseDateTimeValue(cell);
} else {
return parseNumberValue(cell);
Expand All @@ -215,6 +215,15 @@ public static Object getCellValue(Cell cell, FormulaEvaluator formulaEvaluator)
}
}

private static boolean isCellDateOrTimeFormatted(Cell cell) {
CellStyle cellStyle = cell.getCellStyle();
if (DateUtil.isCellDateFormatted(cell) || cellStyle.getDataFormat() == 58) {
return true;
}
TemporalFormat temporalFormat = parseTemporalFormat(cellStyle);
return temporalFormat.hasDate || temporalFormat.hasTime;
}

private static Object parseDateTimeValue(Cell cell) {
LocalDateTime localDateTime = cell.getLocalDateTimeCellValue();
double numericValue = cell.getNumericCellValue();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,25 @@
package io.tapdata.connector.excel;

import io.tapdata.connector.excel.util.CellValueConvert;
import io.tapdata.entity.codec.FromTapValueCodec;
import io.tapdata.entity.codec.TapCodecsRegistry;
import io.tapdata.entity.codec.ToTapValueCodec;
import io.tapdata.entity.schema.TapTable;
import io.tapdata.entity.schema.value.TapTimeValue;
import io.tapdata.pdk.apis.functions.ConnectorFunctions;
import org.junit.jupiter.api.Test;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.LinkedHashMap;
import java.util.Map;

import static org.junit.jupiter.api.Assertions.assertEquals;

class ExcelConnectorTest {
private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm:ss");

@Test
void makeTapTableMapsJavaTimeValuesToExcelDataTypes() {
Expand Down Expand Up @@ -45,4 +52,21 @@ void parseValueUsesDisplayValueForNonTemporalStringFields() {

assertEquals("10.00", CellValueConvert.parseValue(10L, "10.00", "STRING"));
}

@Test
void registerCapabilitiesConvertsEverySecondLocalTimeToTapTime() {
ExcelConnector connector = new ExcelConnector();
TapCodecsRegistry codecRegistry = TapCodecsRegistry.create();
connector.registerCapabilities(new ConnectorFunctions(), codecRegistry);
ToTapValueCodec<?> toTapValueCodec = codecRegistry.getCustomToTapValueCodec(LocalTime.class);
FromTapValueCodec<TapTimeValue> fromTapValueCodec = codecRegistry.getFromTapValueCodec(TapTimeValue.class);

for (int secondOfDay = 0; secondOfDay < 24 * 60 * 60; secondOfDay++) {
LocalTime localTime = LocalTime.ofSecondOfDay(secondOfDay);
TapTimeValue tapTimeValue = (TapTimeValue) toTapValueCodec.toTapValue(localTime, null);
Object value = fromTapValueCodec.fromTapValue(tapTimeValue);

assertEquals(localTime.format(TIME_FORMATTER), value, "secondOfDay=" + secondOfDay);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.ss.util.CellRangeAddress;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.junit.jupiter.api.Test;

Expand Down Expand Up @@ -79,6 +80,23 @@ void getCellValueReadsTimeOnlyCellAsLocalTime() throws Exception {
}
}

@Test
void getCellValueReadsEverySecondTimeOnlyCellAsLocalTime() throws Exception {
try (Workbook workbook = new XSSFWorkbook()) {
Cell cell = createNumericCell(workbook, "hh:mm:ss");

for (int secondOfDay = 0; secondOfDay < 24 * 60 * 60; secondOfDay++) {
LocalTime expected = LocalTime.ofSecondOfDay(secondOfDay);
cell.setCellValue(secondOfDay / (24D * 60D * 60D));

Object value = ExcelUtil.getCellValue(cell, null);

assertEquals(LocalTime.class, value.getClass(), "secondOfDay=" + secondOfDay);
assertEquals(expected, value, "secondOfDay=" + secondOfDay);
}
}
}

@Test
void getCellValueKeepsDateTimeWhenValueContainsDateAndTime() throws Exception {
try (Workbook workbook = new XSSFWorkbook()) {
Expand All @@ -91,6 +109,71 @@ void getCellValueKeepsDateTimeWhenValueContainsDateAndTime() throws Exception {
}
}

@Test
void getCellValueReadsDateOnlyScenariosAsLocalDate() throws Exception {
assertDateOnlyCell(LocalDate.of(1900, 1, 1), "yyyy-mm-dd", "first 1900-date-system day");
assertDateOnlyCell(LocalDate.of(1900, 2, 28), "m/d/yy", "date before excel leap-year boundary");
assertDateOnlyCell(LocalDate.of(2000, 2, 29), "yyyy/mm/dd", "century leap day");
assertDateOnlyCell(LocalDate.of(2024, 2, 29), "[$-409]dd-mmm-yyyy", "locale-prefixed date format");
assertDateOnlyCell(LocalDate.of(2024, 12, 31), "yyyy \"year\" mm \"month\" dd", "quoted-text date format");
assertDateOnlyCell(LocalDate.of(2025, 1, 1), "yyyy-mm-dd;@", "semicolon date format");
assertDateOnlyCellWithBuiltInFormat(LocalDate.of(2024, 5, 17), (short) 58, "built-in date format 58");
assertDateOnlyCellIn1904DateSystem(LocalDate.of(1904, 1, 2), "yyyy-mm-dd", "1904 date system");
}

@Test
void getCellValueKeepsDateTimeScenariosAsLocalDateTime() throws Exception {
assertDateTimeCell(LocalDateTime.of(2024, 5, 17, 13, 45, 30), "yyyy-mm-dd", "date format with time value");
assertDateTimeCell(LocalDateTime.of(2024, 5, 17, 0, 0, 0), "yyyy-mm-dd hh:mm:ss", "datetime format at midnight");
assertDateTimeCell(LocalDateTime.of(2024, 5, 17, 13, 45, 30), "hh:mm:ss", "time format with date value");
assertDateTimeCellWithBuiltInFormat(LocalDateTime.of(2024, 5, 17, 13, 45, 30), (short) 58, "built-in date format 58 with time value");
}

@Test
void getCellValueReadsDateFormulaAsLocalDate() throws Exception {
try (Workbook workbook = new XSSFWorkbook()) {
Cell cell = createNumericCell(workbook, "yyyy-mm-dd");
cell.setCellFormula("DATE(2024,2,29)");

Object value = ExcelUtil.getCellValue(cell, workbook.getCreationHelper().createFormulaEvaluator());

assertEquals(LocalDate.class, value.getClass());
assertEquals(LocalDate.of(2024, 2, 29), value);
}
}

@Test
void getMergedCellValueReadsDateOnlyCellAsLocalDate() throws Exception {
try (Workbook workbook = new XSSFWorkbook()) {
Sheet sheet = workbook.createSheet();
Row firstRow = sheet.createRow(0);
Cell source = firstRow.createCell(0);
setTemporalCellValue(workbook, source, LocalDateTime.of(2024, 5, 17, 0, 0), "yyyy-mm-dd");
sheet.addMergedRegion(new CellRangeAddress(0, 1, 0, 1));
Cell mergedCell = sheet.createRow(1).createCell(1);

Object value = ExcelUtil.getMergedCellValue(sheet.getMergedRegions(), ExcelUtil.getMergedDataMap(sheet), mergedCell, null);

assertEquals(LocalDate.class, value.getClass());
assertEquals(LocalDate.of(2024, 5, 17), value);
}
}

@Test
void getCellValueKeepsTextThatLooksLikeDateAsString() throws Exception {
try (Workbook workbook = new XSSFWorkbook()) {
Sheet sheet = workbook.createSheet();
Row row = sheet.createRow(0);
Cell cell = row.createCell(0);
cell.setCellValue("2024-05-17");

Object value = ExcelUtil.getCellValue(cell, null);

assertEquals(String.class, value.getClass());
assertEquals("2024-05-17", value);
}
}

private Cell createNumericCell(Workbook workbook, String format) {
Sheet sheet = workbook.createSheet();
Row row = sheet.createRow(0);
Expand All @@ -110,4 +193,80 @@ private Cell createTemporalCell(Workbook workbook, LocalDateTime value, String f
cell.setCellValue(value);
return cell;
}

private void assertDateOnlyCell(LocalDate expected, String format, String scenario) throws Exception {
try (Workbook workbook = new XSSFWorkbook()) {
Cell cell = createTemporalCell(workbook, expected.atStartOfDay(), format);

Object value = ExcelUtil.getCellValue(cell, null);

assertEquals(LocalDate.class, value.getClass(), scenario);
assertEquals(expected, value, scenario);
}
}

private void assertDateOnlyCellWithBuiltInFormat(LocalDate expected, short dataFormat, String scenario) throws Exception {
try (Workbook workbook = new XSSFWorkbook()) {
Cell cell = createNumericCell(workbook, null);
setTemporalCellValue(workbook, cell, expected.atStartOfDay(), dataFormat);

Object value = ExcelUtil.getCellValue(cell, null);

assertEquals(LocalDate.class, value.getClass(), scenario);
assertEquals(expected, value, scenario);
}
}

private void assertDateOnlyCellIn1904DateSystem(LocalDate expected, String format, String scenario) throws Exception {
try (XSSFWorkbook workbook = new XSSFWorkbook()) {
if (workbook.getCTWorkbook().getWorkbookPr() == null) {
workbook.getCTWorkbook().addNewWorkbookPr();
}
workbook.getCTWorkbook().getWorkbookPr().setDate1904(true);
Cell cell = createTemporalCell(workbook, expected.atStartOfDay(), format);

Object value = ExcelUtil.getCellValue(cell, null);

assertEquals(LocalDate.class, value.getClass(), scenario);
assertEquals(expected, value, scenario);
}
}

private void assertDateTimeCell(LocalDateTime expected, String format, String scenario) throws Exception {
try (Workbook workbook = new XSSFWorkbook()) {
Cell cell = createTemporalCell(workbook, expected, format);

Object value = ExcelUtil.getCellValue(cell, null);

assertEquals(LocalDateTime.class, value.getClass(), scenario);
assertEquals(expected, value, scenario);
}
}

private void assertDateTimeCellWithBuiltInFormat(LocalDateTime expected, short dataFormat, String scenario) throws Exception {
try (Workbook workbook = new XSSFWorkbook()) {
Cell cell = createNumericCell(workbook, null);
setTemporalCellValue(workbook, cell, expected, dataFormat);

Object value = ExcelUtil.getCellValue(cell, null);

assertEquals(LocalDateTime.class, value.getClass(), scenario);
assertEquals(expected, value, scenario);
}
}

private void setTemporalCellValue(Workbook workbook, Cell cell, LocalDateTime value, String format) {
DataFormat dataFormat = workbook.createDataFormat();
CellStyle cellStyle = workbook.createCellStyle();
cellStyle.setDataFormat(dataFormat.getFormat(format));
cell.setCellStyle(cellStyle);
cell.setCellValue(value);
}

private void setTemporalCellValue(Workbook workbook, Cell cell, LocalDateTime value, short dataFormat) {
CellStyle cellStyle = workbook.createCellStyle();
cellStyle.setDataFormat(dataFormat);
cell.setCellStyle(cellStyle);
cell.setCellValue(value);
}
}