diff --git a/README.md b/README.md index 789337c2..25bd3be3 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ easyexcel重写了poi对07版Excel的解析,一个3M的excel用POI sax解析 com.alibaba easyexcel - 4.0.1 + 4.0.3 ``` diff --git a/easyexcel-core/src/main/java/com/alibaba/excel/constant/BuiltinFormats.java b/easyexcel-core/src/main/java/com/alibaba/excel/constant/BuiltinFormats.java index ec7d5c9a..b6fb8e48 100644 --- a/easyexcel-core/src/main/java/com/alibaba/excel/constant/BuiltinFormats.java +++ b/easyexcel-core/src/main/java/com/alibaba/excel/constant/BuiltinFormats.java @@ -4,6 +4,7 @@ import java.util.Locale; import java.util.Map; import com.alibaba.excel.util.MapUtils; +import com.alibaba.excel.util.StringUtils; /** * Excel's built-in format conversion.Currently only supports Chinese. @@ -20,8 +21,116 @@ import com.alibaba.excel.util.MapUtils; **/ public class BuiltinFormats { + private static final String RESERVED = "reserved-"; + public static short GENERAL = 0; + public static final String[] BUILTIN_FORMATS_ALL_LANGUAGES = { + // 0 + "General", + // 1 + "0", + // 2 + "0.00", + // 3 + "#,##0", + // 4 + "#,##0.00", + // 5 + "\"¥\"#,##0_);(\"¥\"#,##0)", + // 6 + "\"¥\"#,##0_);[Red](\"¥\"#,##0)", + // 7 + "\"¥\"#,##0.00_);(\"¥\"#,##0.00)", + // 8 + "\"¥\"#,##0.00_);[Red](\"¥\"#,##0.00)", + // 9 + "0%", + // 10 + "0.00%", + // 11 + "0.00E+00", + // 12 + "# ?/?", + // 13 + "# ??/??", + // 14 + // The official documentation shows "m/d/yy", but the actual test is "yyyy/m/d". + "yyyy/m/d", + // 15 + "d-mmm-yy", + // 16 + "d-mmm", + // 17 + "mmm-yy", + // 18 + "h:mm AM/PM", + // 19 + "h:mm:ss AM/PM", + // 20 + "h:mm", + // 21 + "h:mm:ss", + // 22 + // The official documentation shows "m/d/yy h:mm", but the actual test is "yyyy-m-d h:mm". + "yyyy-m-d h:mm", + // 23-36 No specific correspondence found in the official documentation. + // 23 + null, + // 24 + null, + // 25 + null, + // 26 + null, + // 27 + null, + // 28 + null, + // 29 + null, + // 30 + null, + // 31 + null, + // 32 + null, + // 33 + null, + // 34 + null, + // 35 + null, + // 36 + null, + // 37 + "#,##0_);(#,##0)", + // 38 + "#,##0_);[Red](#,##0)", + // 39 + "#,##0.00_);(#,##0.00)", + // 40 + "#,##0.00_);[Red](#,##0.00)", + // 41 + "_(* #,##0_);_(* (#,##0);_(* \"-\"_);_(@_)", + // 42 + "_(\"¥\"* #,##0_);_(\"¥\"* (#,##0);_(\"¥\"* \"-\"_);_(@_)", + // 43 + "_(* #,##0.00_);_(* (#,##0.00);_(* \"-\"??_);_(@_)", + // 44 + "_(\"¥\"* #,##0.00_);_(\"¥\"* (#,##0.00);_(\"¥\"* \"-\"??_);_(@_)", + // 45 + "mm:ss", + // 46 + "[h]:mm:ss", + // 47 + "mm:ss.0", + // 48 + "##0.0E+0", + // 49 + "@", + }; + public static final String[] BUILTIN_FORMATS_CN = { // 0 "General", @@ -371,8 +480,26 @@ public class BuiltinFormats { public static final short MIN_CUSTOM_DATA_FORMAT_INDEX = 82; public static String getBuiltinFormat(Short index, String defaultFormat, Locale locale) { + if (index == null || index <= 0) { + return defaultFormat; + } + + // Give priority to checking if it is the default value for all languages + if (index < BUILTIN_FORMATS_ALL_LANGUAGES.length) { + String format = BUILTIN_FORMATS_ALL_LANGUAGES[index]; + if (format != null) { + return format; + } + } + + // In other cases, give priority to using the externally provided format + if (!StringUtils.isEmpty(defaultFormat) && !defaultFormat.startsWith(RESERVED)) { + return defaultFormat; + } + + // Finally, try using the built-in format String[] builtinFormat = switchBuiltinFormats(locale); - if (index == null || index < 0 || index >= builtinFormat.length) { + if (index >= builtinFormat.length) { return defaultFormat; } return builtinFormat[index]; diff --git a/easyexcel-core/src/main/java/com/alibaba/excel/util/DateUtils.java b/easyexcel-core/src/main/java/com/alibaba/excel/util/DateUtils.java index cab7e4d0..035b984c 100644 --- a/easyexcel-core/src/main/java/com/alibaba/excel/util/DateUtils.java +++ b/easyexcel-core/src/main/java/com/alibaba/excel/util/DateUtils.java @@ -1,3 +1,20 @@ +/* ==================================================================== + 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 com.alibaba.excel.util; import java.math.BigDecimal; @@ -9,13 +26,16 @@ import java.time.LocalDateTime; import java.time.LocalTime; import java.time.ZoneId; import java.time.format.DateTimeFormatter; +import java.util.Calendar; import java.util.Date; import java.util.HashMap; import java.util.Locale; import java.util.Map; +import java.util.TimeZone; import java.util.regex.Pattern; import org.apache.poi.ss.usermodel.DateUtil; +import org.apache.poi.util.LocaleUtil; /** * Date utils @@ -63,6 +83,15 @@ public class DateUtils { public static String defaultLocalDateFormat = DATE_FORMAT_10; + public static final int SECONDS_PER_MINUTE = 60; + public static final int MINUTES_PER_HOUR = 60; + public static final int HOURS_PER_DAY = 24; + public static final int SECONDS_PER_DAY = (HOURS_PER_DAY * MINUTES_PER_HOUR * SECONDS_PER_MINUTE); + + // used to specify that date is invalid + private static final int BAD_DATE = -1; + public static final long DAY_MILLISECONDS = SECONDS_PER_DAY * 1000L; + private DateUtils() {} /** @@ -301,13 +330,75 @@ public class DateUtils { * @return Java representation of the date, or null if date is not a valid Excel date */ public static Date getJavaDate(double date, boolean use1904windowing) { - //To calculate the Date, in the use of `org.apache.poi.ss.usermodel.DateUtil.getJavaDate(double, boolean, - // java.util.TimeZone, boolean), Date when similar `2023-01-01 00:00:00.500`, returns the`2023-01-01 - // 00:00:01`, but excel in fact shows the `2023-01-01 00:00:00`. - // `org.apache.poi.ss.usermodel.DateUtil.getLocalDateTime(double, boolean, boolean)` There is no problem. - return Date.from(getLocalDateTime(date, use1904windowing).atZone(ZoneId.systemDefault()).toInstant()); + Calendar calendar = getJavaCalendar(date, use1904windowing, null, true); + return calendar == null ? null : calendar.getTime(); } + /** + * Get EXCEL date as Java Calendar with given time zone. + * @param date The Excel date. + * @param use1904windowing true if date uses 1904 windowing, + * or false if using 1900 date windowing. + * @param timeZone The TimeZone to evaluate the date in + * @param roundSeconds round to closest second + * @return Java representation of the date, or null if date is not a valid Excel date + */ + public static Calendar getJavaCalendar(double date, boolean use1904windowing, TimeZone timeZone, boolean roundSeconds) { + if (!isValidExcelDate(date)) { + return null; + } + int wholeDays = (int)Math.floor(date); + int millisecondsInDay = (int)((date - wholeDays) * DAY_MILLISECONDS + 0.5); + Calendar calendar; + if (timeZone != null) { + calendar = LocaleUtil.getLocaleCalendar(timeZone); + } else { + calendar = LocaleUtil.getLocaleCalendar(); // using default time-zone + } + setCalendar(calendar, wholeDays, millisecondsInDay, use1904windowing, roundSeconds); + return calendar; + } + + + public static void setCalendar(Calendar calendar, int wholeDays, + int millisecondsInDay, boolean use1904windowing, boolean roundSeconds) { + int startYear = 1900; + int dayAdjust = -1; // Excel thinks 2/29/1900 is a valid date, which it isn't + if (use1904windowing) { + startYear = 1904; + dayAdjust = 1; // 1904 date windowing uses 1/2/1904 as the first day + } + else if (wholeDays < 61) { + // Date is prior to 3/1/1900, so adjust because Excel thinks 2/29/1900 exists + // If Excel date == 2/29/1900, will become 3/1/1900 in Java representation + dayAdjust = 0; + } + calendar.set(startYear, Calendar.JANUARY, wholeDays + dayAdjust, 0, 0, 0); + calendar.set(Calendar.MILLISECOND, millisecondsInDay); + if (calendar.get(Calendar.MILLISECOND) == 0) { + calendar.clear(Calendar.MILLISECOND); + } + if (roundSeconds) { + // This is different from poi where you need to change 500 to 499 + calendar.add(Calendar.MILLISECOND, 499); + calendar.clear(Calendar.MILLISECOND); + } + } + + + /** + * Given a double, checks if it is a valid Excel date. + * + * @return true if valid + * @param value the double value + */ + + public static boolean isValidExcelDate(double value) + { + return (value > -Double.MIN_VALUE); + } + + /** * Given an Excel date with either 1900 or 1904 date windowing, * converts it to a java.time.LocalDateTime. @@ -470,31 +561,10 @@ public class DateUtils { case 0x14: case 0x15: case 0x16: - // 27-36 - case 0x1b: - case 0x1c: - case 0x1d: - case 0x1e: - case 0x1f: - case 0x20: - case 0x21: - case 0x22: - case 0x23: - case 0x24: - // 45-47 + // 45-47 case 0x2d: case 0x2e: case 0x2f: - // 50-58 - case 0x32: - case 0x33: - case 0x34: - case 0x35: - case 0x36: - case 0x37: - case 0x38: - case 0x39: - case 0x3a: return true; } return false; diff --git a/easyexcel-test/src/test/java/com/alibaba/easyexcel/test/core/dataformat/DateFormatTest.java b/easyexcel-test/src/test/java/com/alibaba/easyexcel/test/core/dataformat/DateFormatTest.java index 1229b238..d8fd95b3 100644 --- a/easyexcel-test/src/test/java/com/alibaba/easyexcel/test/core/dataformat/DateFormatTest.java +++ b/easyexcel-test/src/test/java/com/alibaba/easyexcel/test/core/dataformat/DateFormatTest.java @@ -3,10 +3,12 @@ package com.alibaba.easyexcel.test.core.dataformat; import java.io.File; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.Objects; import com.alibaba.easyexcel.test.util.TestFileUtil; import com.alibaba.excel.EasyExcel; +import com.alibaba.fastjson2.JSON; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.Assertions; @@ -22,6 +24,7 @@ import org.junit.jupiter.api.TestMethodOrder; @Slf4j public class DateFormatTest { + private static File file07V2; private static File file07; private static File file03; @@ -29,6 +32,8 @@ public class DateFormatTest { public static void init() { file07 = TestFileUtil.readFile("dataformat" + File.separator + "dataformat.xlsx"); file03 = TestFileUtil.readFile("dataformat" + File.separator + "dataformat.xls"); + file07V2 = TestFileUtil.readFile("dataformat" + File.separator + "dataformatv2.xlsx"); + } @Test @@ -43,6 +48,19 @@ public class DateFormatTest { readUs(file03); } + @Test + public void t03Read() { + List> dataMap = EasyExcel.read(file07V2).headRowNumber(0).doReadAllSync(); + log.info("dataMap:{}", JSON.toJSONString(dataMap)); + Assertions.assertEquals("15:00", dataMap.get(0).get(0)); + Assertions.assertEquals("2023-1-01 00:00:00", dataMap.get(1).get(0)); + Assertions.assertEquals("2023-1-01 00:00:00", dataMap.get(2).get(0)); + Assertions.assertEquals("2023-1-01 00:00:01", dataMap.get(3).get(0)); + Assertions.assertEquals("2023-1-01 00:00:00", dataMap.get(4).get(0)); + Assertions.assertEquals("2023-1-01 00:00:00", dataMap.get(5).get(0)); + Assertions.assertEquals("2023-1-01 00:00:01", dataMap.get(6).get(0)); + } + private void readCn(File file) { List list = EasyExcel.read(file, DateFormatData.class, null).locale(Locale.CHINA).sheet().doReadSync(); diff --git a/easyexcel-test/src/test/java/com/alibaba/easyexcel/test/temp/Lock2Test.java b/easyexcel-test/src/test/java/com/alibaba/easyexcel/test/temp/Lock2Test.java index db1ead44..eb27bbb9 100644 --- a/easyexcel-test/src/test/java/com/alibaba/easyexcel/test/temp/Lock2Test.java +++ b/easyexcel-test/src/test/java/com/alibaba/easyexcel/test/temp/Lock2Test.java @@ -52,8 +52,7 @@ public class Lock2Test { File file = new File("/Users/zhuangjiaju/IdeaProjects/easyexcel/src/test/resources/converter/converter07.xlsx"); List list = EasyExcel.read( - "/Users/zhuangjiaju/IdeaProjects/easyexcel/easyexcel-test/target/test-classes" - + "/simpleWrite1674051907397.xlsx") + "/Users/zhuangjiaju/Downloads/证券投资基金估值表_外贸信托-稳盈淳享37号集合资金信托计划_2024-07-23(1).xls") //.useDefaultListener(false) .sheet(0) .headRowNumber(0).doReadSync(); diff --git a/easyexcel-test/src/test/java/com/alibaba/easyexcel/test/temp/poi/PoiDateFormatTest.java b/easyexcel-test/src/test/java/com/alibaba/easyexcel/test/temp/poi/PoiDateFormatTest.java new file mode 100644 index 00000000..dbef4a08 --- /dev/null +++ b/easyexcel-test/src/test/java/com/alibaba/easyexcel/test/temp/poi/PoiDateFormatTest.java @@ -0,0 +1,40 @@ +package com.alibaba.easyexcel.test.temp.poi; + +import java.io.IOException; + +import org.apache.poi.ss.usermodel.DateUtil; +import org.apache.poi.xssf.usermodel.XSSFCell; +import org.apache.poi.xssf.usermodel.XSSFRow; +import org.apache.poi.xssf.usermodel.XSSFSheet; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * 测试poi + * + * @author Jiaju Zhuang + **/ + +public class PoiDateFormatTest { + private static final Logger LOGGER = LoggerFactory.getLogger(PoiDateFormatTest.class); + + @Test + public void read() throws IOException { + String file + = "/Users/zhuangjiaju/IdeaProjects/easyexcel/easyexcel-test/src/test/resources/dataformat/dataformat.xlsx"; + XSSFWorkbook xssfWorkbook = new XSSFWorkbook( file); + XSSFSheet xssfSheet = xssfWorkbook.getSheetAt(0); + LOGGER.info("一共行数:{}", xssfSheet.getLastRowNum()); + XSSFRow row = xssfSheet.getRow(7); + XSSFCell cell = row.getCell(0); + LOGGER.info("dd{}", cell.getDateCellValue()); + LOGGER.info("dd{}", cell.getNumericCellValue()); + + LOGGER.info("dd{}", DateUtil.isCellDateFormatted(cell)); + + + } + +} diff --git a/easyexcel-test/src/test/resources/dataformat/dataformatv2.xlsx b/easyexcel-test/src/test/resources/dataformat/dataformatv2.xlsx new file mode 100644 index 00000000..c49aa044 Binary files /dev/null and b/easyexcel-test/src/test/resources/dataformat/dataformatv2.xlsx differ diff --git a/pom.xml b/pom.xml index 44dd0796..ab227234 100644 --- a/pom.xml +++ b/pom.xml @@ -20,7 +20,7 @@ - 4.0.1 + 4.0.3 UTF-8 1.8 true @@ -93,6 +93,12 @@ 3.3.4 + + org.apache.commons + commons-csv + 1.11.0 + + org.apache.poi poi @@ -103,16 +109,17 @@ poi-ooxml 5.2.5 - - org.apache.commons - commons-csv - 1.11.0 - + org.ehcache ehcache 3.9.11 + + commons-io + commons-io + 2.16.1 + org.slf4j slf4j-api diff --git a/update.md b/update.md index 6b141fff..c31e8bdf 100644 --- a/update.md +++ b/update.md @@ -1,3 +1,12 @@ +# 4.0.3 + +* 兼容部分日期格式读取异常的问题 + +# 4.0.2 + +* 兼容某些特殊的xls: 修改了内置的样式导致判断样式错误 +* 重新加回 `commons-io` + # 4.0.1 * `commons-io` 修改为依赖 `poi`的版本