From 9c7e06022b0e32d155e2a0fa8808b94791c15188 Mon Sep 17 00:00:00 2001 From: Jiaju Zhuang Date: Mon, 23 Dec 2019 19:10:58 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E6=97=A5=E6=9C=9F=E8=BD=AC?= =?UTF-8?q?=E6=8D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 5 + .../v03/handlers/NumberRecordHandler.java | 10 +- .../v07/handlers/DefaultCellHandler.java | 7 +- .../excel/constant/BuiltinFormats.java | 62 +- .../string/StringNumberConverter.java | 12 +- .../excel/metadata/BasicParameter.java | 13 + .../alibaba/excel/metadata/DataFormatter.java | 786 ++++++++++++++++++ .../excel/metadata/GlobalConfiguration.java | 15 + .../com/alibaba/excel/util/DateUtils.java | 408 ++++----- .../excel/util/NumberDataFormatterUtils.java | 201 ++--- .../excel/util/ThreadLocalCachedUtils.java | 14 + .../test/core/dataformat/DateFormatData.java | 14 + .../test/core/dataformat/DateFormatTest.java | 50 ++ .../test/temp/dataformat/DataFormatTest.java | 63 ++ .../easyexcel/test/temp/poi/PoiWriteTest.java | 61 -- src/test/resources/dataformat/dataformat.xlsx | Bin 0 -> 11489 bytes 16 files changed, 1263 insertions(+), 458 deletions(-) create mode 100644 src/main/java/com/alibaba/excel/metadata/DataFormatter.java create mode 100644 src/main/java/com/alibaba/excel/util/ThreadLocalCachedUtils.java create mode 100644 src/test/java/com/alibaba/easyexcel/test/core/dataformat/DateFormatData.java create mode 100644 src/test/java/com/alibaba/easyexcel/test/core/dataformat/DateFormatTest.java create mode 100644 src/test/resources/dataformat/dataformat.xlsx diff --git a/pom.xml b/pom.xml index e85d044..7e2235c 100644 --- a/pom.xml +++ b/pom.xml @@ -66,6 +66,11 @@ poi-ooxml 3.17 + + org.apache.poi + poi-ooxml-schemas + 3.17 + cglib cglib diff --git a/src/main/java/com/alibaba/excel/analysis/v03/handlers/NumberRecordHandler.java b/src/main/java/com/alibaba/excel/analysis/v03/handlers/NumberRecordHandler.java index 75c3128..d8f3524 100644 --- a/src/main/java/com/alibaba/excel/analysis/v03/handlers/NumberRecordHandler.java +++ b/src/main/java/com/alibaba/excel/analysis/v03/handlers/NumberRecordHandler.java @@ -7,6 +7,7 @@ import org.apache.poi.hssf.record.NumberRecord; import org.apache.poi.hssf.record.Record; import com.alibaba.excel.analysis.v03.AbstractXlsRecordHandler; +import com.alibaba.excel.constant.BuiltinFormats; import com.alibaba.excel.metadata.CellData; /** @@ -32,8 +33,13 @@ public class NumberRecordHandler extends AbstractXlsRecordHandler { this.row = numrec.getRow(); this.column = numrec.getColumn(); this.cellData = new CellData(BigDecimal.valueOf(numrec.getValue())); - this.cellData.setDataFormat(formatListener.getFormatIndex(numrec)); - this.cellData.setDataFormatString(formatListener.getFormatString(numrec)); + int dataFormat = formatListener.getFormatIndex(numrec); + this.cellData.setDataFormat(dataFormat); + if (dataFormat <= BuiltinFormats.builtinFormats.length) { + this.cellData.setDataFormatString(BuiltinFormats.getBuiltinFormat(dataFormat)); + } else { + this.cellData.setDataFormatString(formatListener.getFormatString(numrec)); + } } @Override diff --git a/src/main/java/com/alibaba/excel/analysis/v07/handlers/DefaultCellHandler.java b/src/main/java/com/alibaba/excel/analysis/v07/handlers/DefaultCellHandler.java index e0b9c41..7312b2c 100644 --- a/src/main/java/com/alibaba/excel/analysis/v07/handlers/DefaultCellHandler.java +++ b/src/main/java/com/alibaba/excel/analysis/v07/handlers/DefaultCellHandler.java @@ -13,7 +13,6 @@ import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.Map; -import org.apache.poi.ss.usermodel.BuiltinFormats; import org.apache.poi.xssf.model.StylesTable; import org.apache.poi.xssf.usermodel.XSSFCellStyle; import org.apache.poi.xssf.usermodel.XSSFRichTextString; @@ -21,6 +20,7 @@ import org.xml.sax.Attributes; import com.alibaba.excel.analysis.v07.XlsxCellHandler; import com.alibaba.excel.analysis.v07.XlsxRowResultHolder; +import com.alibaba.excel.constant.BuiltinFormats; import com.alibaba.excel.constant.ExcelXmlConstants; import com.alibaba.excel.context.AnalysisContext; import com.alibaba.excel.enums.CellDataTypeEnum; @@ -87,12 +87,11 @@ public class DefaultCellHandler implements XlsxCellHandler, XlsxRowResultHolder int dateFormatIndexInteger = Integer.parseInt(dateFormatIndex); XSSFCellStyle xssfCellStyle = stylesTable.getStyleAt(dateFormatIndexInteger); int dataFormat = xssfCellStyle.getDataFormat(); - String dataFormatString = xssfCellStyle.getDataFormatString(); currentCellData.setDataFormat(dataFormat); - if (dataFormatString == null) { + if (dataFormat <= BuiltinFormats.builtinFormats.length) { currentCellData.setDataFormatString(BuiltinFormats.getBuiltinFormat(dataFormat)); } else { - currentCellData.setDataFormatString(dataFormatString); + currentCellData.setDataFormatString(xssfCellStyle.getDataFormatString()); } } } diff --git a/src/main/java/com/alibaba/excel/constant/BuiltinFormats.java b/src/main/java/com/alibaba/excel/constant/BuiltinFormats.java index 7b3c5bb..5663054 100644 --- a/src/main/java/com/alibaba/excel/constant/BuiltinFormats.java +++ b/src/main/java/com/alibaba/excel/constant/BuiltinFormats.java @@ -47,7 +47,8 @@ public class BuiltinFormats { // 13 "# ??/??", // 14 - "m/d/yy", + // The official documentation shows "m/d/yy", but the actual test is "yyyy/m/d". + "yyyy/m/d", // 15 "d-mmm-yy", // 16 @@ -63,7 +64,8 @@ public class BuiltinFormats { // 21 "h:mm:ss", // 22 - "m/d/yy h:mm", + // 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-26 No specific correspondence found in the official documentation. // 23 null, @@ -74,25 +76,25 @@ public class BuiltinFormats { // 26 null, // 27 - "yyyy\"5E74\"m\"6708\"", + "yyyy\"年\"m\"月\"", // 28 - "m\"6708\"d\"65E5\"", + "m\"月\"d\"日\"", // 29 - "m\"6708\"d\"65E5\"", + "m\"月\"d\"日\"", // 30 "m-d-yy", // 31 - "yyyy\"5E74\"m\"6708\"d\"65E5\"", + "yyyy\"年\"m\"月\"d\"日\"", // 32 - "h\"65F6\"mm\"5206\"", + "h\"时\"mm\"分\"", // 33 - "h\"65F6\"mm\"5206\"ss\"79D2\"", + "h\"时\"mm\"分\"ss\"秒\"", // 34 - "4E0A5348/4E0B5348h\"65F6\"mm\"5206\"", + "上午/下午h\"时\"mm\"分\"", // 35 - "4E0A5348/4E0B5348h\"65F6\"mm\"5206\"ss\"79D2\"", + "上午/下午h\"时\"mm\"分\"ss\"秒\"", // 36 - "yyyy\"5E74\"m\"6708\"", + "yyyy\"年\"m\"月\"", // 37 "#,##0_);(#,##0)", // 38 @@ -120,23 +122,23 @@ public class BuiltinFormats { // 49 "@", // 50 - "yyyy\"5E74\"m\"6708\"", + "yyyy\"年\"m\"月\"", // 51 - "m\"6708\"d\"65E5\"", + "m\"月\"d\"日\"", // 52 - "yyyy\"5E74\"m\"6708\"", + "yyyy\"年\"m\"月\"", // 53 - "m\"6708\"d\"65E5\"", + "m\"月\"d\"日\"", // 54 - "m\"6708\"d\"65E5\"", + "m\"月\"d\"日\"", // 55 - "4E0A5348/4E0B5348h\"65F6\"mm\"5206\"", + "上午/下午h\"时\"mm\"分\"", // 56 - "4E0A5348/4E0B5348h\"65F6\"mm\"5206\"ss\"79D2\"", + "上午/下午h\"时\"mm\"分\"ss\"秒\"", // 57 - "yyyy\"5E74\"m\"6708\"", + "yyyy\"年\"m\"月\"", // 58 - "m\"6708\"d\"65E5\"", + "m\"月\"d\"日\"", // 59 "t0", // 60 @@ -163,25 +165,25 @@ public class BuiltinFormats { // 70 "t# ??/??", // 71 - "0E27/0E14/0E1B0E1B0E1B0E1B", + "ว/ด/ปปปป", // 72 - "0E27-0E140E140E14-0E1B0E1B", + "ว-ดดด-ปป", // 73 - "0E27-0E140E140E14", + "ว-ดดด", // 74 - "0E140E140E14-0E1B0E1B", + "ดดด-ปป", // 75 - "0E0A:0E190E19", + "ช:นน", // 76 - "0E0A:0E190E19:0E170E17", + "ช:นน:ทท", // 77 - "0E27/0E14/0E1B0E1B0E1B0E1B 0E0A:0E190E19", + "ว/ด/ปปปป ช:นน", // 78 - "0E190E19:0E170E17", + "นน:ทท", // 79 - "[0E0A]:0E190E19:0E170E17", + "[ช]:นน:ทท", // 80 - "0E190E19:0E170E17.0", + "นน:ทท.0", // 81 "d/m/bb", // end diff --git a/src/main/java/com/alibaba/excel/converters/string/StringNumberConverter.java b/src/main/java/com/alibaba/excel/converters/string/StringNumberConverter.java index f536a08..b31bc70 100644 --- a/src/main/java/com/alibaba/excel/converters/string/StringNumberConverter.java +++ b/src/main/java/com/alibaba/excel/converters/string/StringNumberConverter.java @@ -10,7 +10,9 @@ import com.alibaba.excel.metadata.CellData; import com.alibaba.excel.metadata.GlobalConfiguration; import com.alibaba.excel.metadata.property.ExcelContentProperty; import com.alibaba.excel.util.DateUtils; +import com.alibaba.excel.util.NumberDataFormatterUtils; import com.alibaba.excel.util.NumberUtils; +import com.alibaba.excel.util.StringUtils; /** * String and number converter @@ -44,13 +46,9 @@ public class StringNumberConverter implements Converter { return NumberUtils.format(cellData.getNumberValue(), contentProperty); } // Excel defines formatting - if (cellData.getDataFormat() != null) { - if (DateUtil.isADateFormat(cellData.getDataFormat(), cellData.getDataFormatString())) { - return DateUtils.format(DateUtil.getJavaDate(cellData.getNumberValue().doubleValue(), - globalConfiguration.getUse1904windowing(), null)); - } else { - return NumberUtils.format(cellData.getNumberValue(), contentProperty); - } + if (cellData.getDataFormat() != null && !StringUtils.isEmpty(cellData.getDataFormatString())) { + return NumberDataFormatterUtils.format(cellData.getNumberValue().doubleValue(), cellData.getDataFormat(), + cellData.getDataFormatString(), globalConfiguration); } // Default conversion number return NumberUtils.format(cellData.getNumberValue(), contentProperty); diff --git a/src/main/java/com/alibaba/excel/metadata/BasicParameter.java b/src/main/java/com/alibaba/excel/metadata/BasicParameter.java index 40bffe6..d00c93a 100644 --- a/src/main/java/com/alibaba/excel/metadata/BasicParameter.java +++ b/src/main/java/com/alibaba/excel/metadata/BasicParameter.java @@ -1,6 +1,7 @@ package com.alibaba.excel.metadata; import java.util.List; +import java.util.Locale; import com.alibaba.excel.converters.Converter; @@ -34,6 +35,11 @@ public class BasicParameter { * @return */ private Boolean use1904windowing; + /** + * A Locale object represents a specific geographical, political, or cultural region. This parameter is + * used when formatting dates and numbers. + */ + private Locale locale; public List> getHead() { return head; @@ -75,4 +81,11 @@ public class BasicParameter { this.use1904windowing = use1904windowing; } + public Locale getLocale() { + return locale; + } + + public void setLocale(Locale locale) { + this.locale = locale; + } } diff --git a/src/main/java/com/alibaba/excel/metadata/DataFormatter.java b/src/main/java/com/alibaba/excel/metadata/DataFormatter.java new file mode 100644 index 0000000..13bd162 --- /dev/null +++ b/src/main/java/com/alibaba/excel/metadata/DataFormatter.java @@ -0,0 +1,786 @@ +/* + * ==================================================================== 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. + * + * 2012 - Alfresco Software, Ltd. Alfresco Software has modified source of this file The details of changes as svn diff + * can be found in svn at location root/projects/3rd-party/src + * ==================================================================== + */ +package com.alibaba.excel.metadata; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.text.DateFormatSymbols; +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; +import java.text.FieldPosition; +import java.text.Format; +import java.text.ParsePosition; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.apache.poi.ss.usermodel.DateUtil; +import org.apache.poi.ss.usermodel.ExcelGeneralNumberFormat; +import org.apache.poi.ss.usermodel.ExcelStyleDateFormatter; +import org.apache.poi.ss.usermodel.FractionFormat; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.alibaba.excel.util.DateUtils; + +/** + * Written with reference to {@link org.apache.poi.ss.usermodel.DataFormatter}.Made some optimizations for date + * conversion. + *

+ * This is a non-thread-safe class. + * + * @author Jiaju Zhuang + */ +public class DataFormatter { + /** For logging any problems we find */ + private static final Logger LOGGER = LoggerFactory.getLogger(DataFormatter.class); + private static final String defaultFractionWholePartFormat = "#"; + private static final String defaultFractionFractionPartFormat = "#/##"; + /** Pattern to find a number format: "0" or "#" */ + private static final Pattern numPattern = Pattern.compile("[0#]+"); + + /** Pattern to find days of week as text "ddd...." */ + private static final Pattern daysAsText = Pattern.compile("([d]{3,})", Pattern.CASE_INSENSITIVE); + + /** Pattern to find "AM/PM" marker */ + private static final Pattern amPmPattern = Pattern.compile("(([AP])[M/P]*)|(([上下])[午/下]*)", Pattern.CASE_INSENSITIVE); + + /** Pattern to find formats with condition ranges e.g. [>=100] */ + private static final Pattern rangeConditionalPattern = + Pattern.compile(".*\\[\\s*(>|>=|<|<=|=)\\s*[0-9]*\\.*[0-9].*"); + + /** + * A regex to find locale patterns like [$$-1009] and [$?-452]. Note that we don't currently process these into + * locales + */ + private static final Pattern localePatternGroup = Pattern.compile("(\\[\\$[^-\\]]*-[0-9A-Z]+])"); + + /** + * A regex to match the colour formattings rules. Allowed colours are: Black, Blue, Cyan, Green, Magenta, Red, + * White, Yellow, "Color n" (1<=n<=56) + */ + private static final Pattern colorPattern = Pattern.compile( + "(\\[BLACK])|(\\[BLUE])|(\\[CYAN])|(\\[GREEN])|" + "(\\[MAGENTA])|(\\[RED])|(\\[WHITE])|(\\[YELLOW])|" + + "(\\[COLOR\\s*\\d])|(\\[COLOR\\s*[0-5]\\d])|(\\[DBNum(1|2|3)])|(\\[\\$-\\d{0,3}])", + Pattern.CASE_INSENSITIVE); + + /** + * A regex to identify a fraction pattern. This requires that replaceAll("\\?", "#") has already been called + */ + private static final Pattern fractionPattern = Pattern.compile("(?:([#\\d]+)\\s+)?(#+)\\s*/\\s*([#\\d]+)"); + + /** + * A regex to strip junk out of fraction formats + */ + private static final Pattern fractionStripper = Pattern.compile("(\"[^\"]*\")|([^ ?#\\d/]+)"); + + /** + * A regex to detect if an alternate grouping character is used in a numeric format + */ + private static final Pattern alternateGrouping = Pattern.compile("([#0]([^.#0])[#0]{3})"); + + /** + * Cells formatted with a date or time format and which contain invalid date or time values show 255 pound signs + * ("#"). + */ + private static final String invalidDateTimeString; + static { + StringBuilder buf = new StringBuilder(); + for (int i = 0; i < 255; i++) + buf.append('#'); + invalidDateTimeString = buf.toString(); + } + + /** + * The decimal symbols of the locale used for formatting values. + */ + private DecimalFormatSymbols decimalSymbols; + + /** + * The date symbols of the locale used for formatting values. + */ + private DateFormatSymbols dateSymbols; + /** A default format to use when a number pattern cannot be parsed. */ + private Format defaultNumFormat; + /** + * A map to cache formats. Map formats + */ + private final Map formats = new HashMap(); + + /** stores the locale valid it the last formatting call */ + private Locale locale; + /** + * true if date uses 1904 windowing, or false if using 1900 date windowing. + * + * default is false + * + * @return + */ + private Boolean use1904windowing; + + /** + * Creates a formatter using the {@link Locale#getDefault() default locale}. + */ + public DataFormatter() { + this(null, null); + } + + /** + * Creates a formatter using the given locale. + * + */ + public DataFormatter(Locale locale, Boolean use1904windowing) { + this.use1904windowing = use1904windowing != null ? use1904windowing : Boolean.FALSE; + this.locale = locale != null ? locale : Locale.getDefault(); + this.locale = Locale.US; + this.dateSymbols = DateFormatSymbols.getInstance(this.locale); + this.decimalSymbols = DecimalFormatSymbols.getInstance(this.locale); + } + + private Format getFormat(Integer dataFormat, String dataFormatString) { + // See if we already have it cached + Format format = formats.get(dataFormatString); + if (format != null) { + return format; + } + // Is it one of the special built in types, General or @? + if ("General".equalsIgnoreCase(dataFormatString) || "@".equals(dataFormatString)) { + format = getDefaultFormat(); + addFormat(dataFormatString, format); + return format; + } + + // Build a formatter, and cache it + format = createFormat(dataFormat, dataFormatString); + addFormat(dataFormatString, format); + return format; + } + + private Format createFormat(Integer dataFormat, String dataFormatString) { + String formatStr = dataFormatString; + + Format format = checkSpecialConverter(formatStr); + if (format != null) { + return format; + } + + // Remove colour formatting if present + Matcher colourM = colorPattern.matcher(formatStr); + while (colourM.find()) { + String colour = colourM.group(); + + // Paranoid replacement... + int at = formatStr.indexOf(colour); + if (at == -1) + break; + String nFormatStr = formatStr.substring(0, at) + formatStr.substring(at + colour.length()); + if (nFormatStr.equals(formatStr)) + break; + + // Try again in case there's multiple + formatStr = nFormatStr; + colourM = colorPattern.matcher(formatStr); + } + + // Strip off the locale information, we use an instance-wide locale for everything + Matcher m = localePatternGroup.matcher(formatStr); + while (m.find()) { + String match = m.group(); + String symbol = match.substring(match.indexOf('$') + 1, match.indexOf('-')); + if (symbol.indexOf('$') > -1) { + symbol = symbol.substring(0, symbol.indexOf('$')) + '\\' + symbol.substring(symbol.indexOf('$')); + } + formatStr = m.replaceAll(symbol); + m = localePatternGroup.matcher(formatStr); + } + + // Check for special cases + if (formatStr == null || formatStr.trim().length() == 0) { + return getDefaultFormat(); + } + + if ("General".equalsIgnoreCase(formatStr) || "@".equals(formatStr)) { + return getDefaultFormat(); + } + + if (DateUtils.isADateFormat(dataFormat, formatStr)) { + return createDateFormat(formatStr); + } + // Excel supports fractions in format strings, which Java doesn't + if (formatStr.contains("#/") || formatStr.contains("?/")) { + String[] chunks = formatStr.split(";"); + for (String chunk1 : chunks) { + String chunk = chunk1.replaceAll("\\?", "#"); + Matcher matcher = fractionStripper.matcher(chunk); + chunk = matcher.replaceAll(" "); + chunk = chunk.replaceAll(" +", " "); + Matcher fractionMatcher = fractionPattern.matcher(chunk); + // take the first match + if (fractionMatcher.find()) { + String wholePart = (fractionMatcher.group(1) == null) ? "" : defaultFractionWholePartFormat; + return new FractionFormat(wholePart, fractionMatcher.group(3)); + } + } + + // Strip custom text in quotes and escaped characters for now as it can cause performance problems in + // fractions. + // String strippedFormatStr = formatStr.replaceAll("\\\\ ", " ").replaceAll("\\\\.", + // "").replaceAll("\"[^\"]*\"", " ").replaceAll("\\?", "#"); + return new FractionFormat(defaultFractionWholePartFormat, defaultFractionFractionPartFormat); + } + + if (numPattern.matcher(formatStr).find()) { + return createNumberFormat(formatStr); + } + return getDefaultFormat(); + } + + private Format checkSpecialConverter(String dataFormatString) { + if ("00000\\-0000".equals(dataFormatString) || "00000-0000".equals(dataFormatString)) { + return new ZipPlusFourFormat(); + } + if ("[<=9999999]###\\-####;\\(###\\)\\ ###\\-####".equals(dataFormatString) + || "[<=9999999]###-####;(###) ###-####".equals(dataFormatString) + || "###\\-####;\\(###\\)\\ ###\\-####".equals(dataFormatString) + || "###-####;(###) ###-####".equals(dataFormatString)) { + return new PhoneFormat(); + } + if ("000\\-00\\-0000".equals(dataFormatString) || "000-00-0000".equals(dataFormatString)) { + return new SSNFormat(); + } + return null; + } + + private Format createDateFormat(String pFormatStr) { + String formatStr = pFormatStr; + formatStr = formatStr.replaceAll("\\\\-", "-"); + formatStr = formatStr.replaceAll("\\\\,", ","); + formatStr = formatStr.replaceAll("\\\\\\.", "."); // . is a special regexp char + formatStr = formatStr.replaceAll("\\\\ ", " "); + formatStr = formatStr.replaceAll("\\\\/", "/"); // weird: m\\/d\\/yyyy + formatStr = formatStr.replaceAll(";@", ""); + formatStr = formatStr.replaceAll("\"/\"", "/"); // "/" is escaped for no reason in: mm"/"dd"/"yyyy + formatStr = formatStr.replace("\"\"", "'"); // replace Excel quoting with Java style quoting + formatStr = formatStr.replaceAll("\\\\T", "'T'"); // Quote the T is iso8601 style dates + formatStr = formatStr.replace("\"", ""); + + boolean hasAmPm = false; + Matcher amPmMatcher = amPmPattern.matcher(formatStr); + while (amPmMatcher.find()) { + formatStr = amPmMatcher.replaceAll("@"); + hasAmPm = true; + amPmMatcher = amPmPattern.matcher(formatStr); + } + formatStr = formatStr.replaceAll("@", "a"); + + Matcher dateMatcher = daysAsText.matcher(formatStr); + if (dateMatcher.find()) { + String match = dateMatcher.group(0).toUpperCase(Locale.ROOT).replaceAll("D", "E"); + formatStr = dateMatcher.replaceAll(match); + } + + // Convert excel date format to SimpleDateFormat. + // Excel uses lower and upper case 'm' for both minutes and months. + // From Excel help: + /* + The "m" or "mm" code must appear immediately after the "h" or"hh" + code or immediately before the "ss" code; otherwise, Microsoft + Excel displays the month instead of minutes." + */ + StringBuilder sb = new StringBuilder(); + char[] chars = formatStr.toCharArray(); + boolean mIsMonth = true; + List ms = new ArrayList(); + boolean isElapsed = false; + for (int j = 0; j < chars.length; j++) { + char c = chars[j]; + if (c == '\'') { + sb.append(c); + j++; + + // skip until the next quote + while (j < chars.length) { + c = chars[j]; + sb.append(c); + if (c == '\'') { + break; + } + j++; + } + } else if (c == '[' && !isElapsed) { + isElapsed = true; + mIsMonth = false; + sb.append(c); + } else if (c == ']' && isElapsed) { + isElapsed = false; + sb.append(c); + } else if (isElapsed) { + if (c == 'h' || c == 'H') { + sb.append('H'); + } else if (c == 'm' || c == 'M') { + sb.append('m'); + } else if (c == 's' || c == 'S') { + sb.append('s'); + } else { + sb.append(c); + } + } else if (c == 'h' || c == 'H') { + mIsMonth = false; + if (hasAmPm) { + sb.append('h'); + } else { + sb.append('H'); + } + } else if (c == 'm' || c == 'M') { + if (mIsMonth) { + sb.append('M'); + ms.add(Integer.valueOf(sb.length() - 1)); + } else { + sb.append('m'); + } + } else if (c == 's' || c == 'S') { + sb.append('s'); + // if 'M' precedes 's' it should be minutes ('m') + for (int index : ms) { + if (sb.charAt(index) == 'M') { + sb.replace(index, index + 1, "m"); + } + } + mIsMonth = true; + ms.clear(); + } else if (Character.isLetter(c)) { + mIsMonth = true; + ms.clear(); + if (c == 'y' || c == 'Y') { + sb.append('y'); + } else if (c == 'd' || c == 'D') { + sb.append('d'); + } else { + sb.append(c); + } + } else { + if (Character.isWhitespace(c)) { + ms.clear(); + } + sb.append(c); + } + } + formatStr = sb.toString(); + + try { + return new ExcelStyleDateFormatter(formatStr, dateSymbols); + } catch (IllegalArgumentException iae) { + LOGGER.debug("Formatting failed for format {}, falling back", formatStr, iae); + // the pattern could not be parsed correctly, + // so fall back to the default number format + return getDefaultFormat(); + } + + } + + private String cleanFormatForNumber(String formatStr) { + StringBuilder sb = new StringBuilder(formatStr); + // If they requested spacers, with "_", + // remove those as we don't do spacing + // If they requested full-column-width + // padding, with "*", remove those too + for (int i = 0; i < sb.length(); i++) { + char c = sb.charAt(i); + if (c == '_' || c == '*') { + if (i > 0 && sb.charAt((i - 1)) == '\\') { + // It's escaped, don't worry + continue; + } + if (i < sb.length() - 1) { + // Remove the character we're supposed + // to match the space of / pad to the + // column width with + sb.deleteCharAt(i + 1); + } + // Remove the _ too + sb.deleteCharAt(i); + i--; + } + } + + // Now, handle the other aspects like + // quoting and scientific notation + for (int i = 0; i < sb.length(); i++) { + char c = sb.charAt(i); + // remove quotes and back slashes + if (c == '\\' || c == '"') { + sb.deleteCharAt(i); + i--; + + // for scientific/engineering notation + } else if (c == '+' && i > 0 && sb.charAt(i - 1) == 'E') { + sb.deleteCharAt(i); + i--; + } + } + + return sb.toString(); + } + + private static class InternalDecimalFormatWithScale extends Format { + + private static final Pattern endsWithCommas = Pattern.compile("(,+)$"); + private BigDecimal divider; + private static final BigDecimal ONE_THOUSAND = new BigDecimal(1000); + private final DecimalFormat df; + + private static String trimTrailingCommas(String s) { + return s.replaceAll(",+$", ""); + } + + public InternalDecimalFormatWithScale(String pattern, DecimalFormatSymbols symbols) { + df = new DecimalFormat(trimTrailingCommas(pattern), symbols); + setExcelStyleRoundingMode(df); + Matcher endsWithCommasMatcher = endsWithCommas.matcher(pattern); + if (endsWithCommasMatcher.find()) { + String commas = (endsWithCommasMatcher.group(1)); + BigDecimal temp = BigDecimal.ONE; + for (int i = 0; i < commas.length(); ++i) { + temp = temp.multiply(ONE_THOUSAND); + } + divider = temp; + } else { + divider = null; + } + } + + private Object scaleInput(Object obj) { + if (divider != null) { + if (obj instanceof BigDecimal) { + obj = ((BigDecimal)obj).divide(divider, RoundingMode.HALF_UP); + } else if (obj instanceof Double) { + obj = (Double)obj / divider.doubleValue(); + } else { + throw new UnsupportedOperationException(); + } + } + return obj; + } + + @Override + public StringBuffer format(Object obj, StringBuffer toAppendTo, FieldPosition pos) { + obj = scaleInput(obj); + return df.format(obj, toAppendTo, pos); + } + + @Override + public Object parseObject(String source, ParsePosition pos) { + throw new UnsupportedOperationException(); + } + } + + private Format createNumberFormat(String formatStr) { + String format = cleanFormatForNumber(formatStr); + DecimalFormatSymbols symbols = decimalSymbols; + + // Do we need to change the grouping character? + // eg for a format like #'##0 which wants 12'345 not 12,345 + Matcher agm = alternateGrouping.matcher(format); + if (agm.find()) { + char grouping = agm.group(2).charAt(0); + // Only replace the grouping character if it is not the default + // grouping character for the US locale (',') in order to enable + // correct grouping for non-US locales. + if (grouping != ',') { + symbols = DecimalFormatSymbols.getInstance(locale); + + symbols.setGroupingSeparator(grouping); + String oldPart = agm.group(1); + String newPart = oldPart.replace(grouping, ','); + format = format.replace(oldPart, newPart); + } + } + + try { + return new InternalDecimalFormatWithScale(format, symbols); + } catch (IllegalArgumentException iae) { + LOGGER.debug("Formatting failed for format {}, falling back", formatStr, iae); + // the pattern could not be parsed correctly, + // so fall back to the default number format + return getDefaultFormat(); + } + } + + private Format getDefaultFormat() { + // for numeric cells try user supplied default + if (defaultNumFormat != null) { + return defaultNumFormat; + // otherwise use general format + } + defaultNumFormat = new ExcelGeneralNumberFormat(locale); + return defaultNumFormat; + } + + /** + * Performs Excel-style date formatting, using the supplied Date and format + */ + private String performDateFormatting(Date d, Format dateFormat) { + Format df = dateFormat != null ? dateFormat : getDefaultFormat(); + return df.format(d); + } + + /** + * Returns the formatted value of an Excel date as a String based on the cell's DataFormat. + * i.e. "Thursday, January 02, 2003" , "01/02/2003" , "02-Jan" , etc. + *

+ * If any conditional format rules apply, the highest priority with a number format is used. If no rules contain a + * number format, or no rules apply, the cell's style format is used. If the style does not have a format, the + * default date format is applied. + * + * @param data + * to format + * @param dataFormat + * @param dataFormatString + * @return Formatted value + */ + private String getFormattedDateString(Double data, Integer dataFormat, String dataFormatString) { + Format dateFormat = getFormat(dataFormat, dataFormatString); + if (dateFormat instanceof ExcelStyleDateFormatter) { + // Hint about the raw excel value + ((ExcelStyleDateFormatter)dateFormat).setDateToBeFormatted(data); + } + return performDateFormatting(DateUtil.getJavaDate(data, use1904windowing), dateFormat); + } + + /** + * Returns the formatted value of an Excel number as a String based on the cell's DataFormat. + * Supported formats include currency, percents, decimals, phone number, SSN, etc.: "61.54%", "$100.00", "(800) + * 555-1234". + *

+ * Format comes from either the highest priority conditional format rule with a specified format, or from the cell + * style. + * + * @param data + * to format + * @param dataFormat + * @param dataFormatString + * @return a formatted number string + */ + private String getFormattedNumberString(Double data, Integer dataFormat, String dataFormatString) { + Format numberFormat = getFormat(dataFormat, dataFormatString); + String formatted = numberFormat.format(data); + return formatted.replaceFirst("E(\\d)", "E+$1"); // to match Excel's E-notation + } + + /** + * Format data. + * + * @param data + * @param dataFormat + * @param dataFormatString + * @return + */ + public String format(Double data, Integer dataFormat, String dataFormatString) { + if (DateUtils.isADateFormat(dataFormat, dataFormatString)) { + return getFormattedDateString(data, dataFormat, dataFormatString); + } + return getFormattedNumberString(data, dataFormat, dataFormatString); + } + + /** + *

+ * Sets a default number format to be used when the Excel format cannot be parsed successfully. Note: This is + * a fall back for when an error occurs while parsing an Excel number format pattern. This will not affect cells + * with the General format. + *

+ *

+ * The value that will be passed to the Format's format method (specified by java.text.Format#format) + * will be a double value from a numeric cell. Therefore the code in the format method should expect a + * Number value. + *

+ * + * @param format + * A Format instance to be used as a default + * @see Format#format + */ + public void setDefaultNumberFormat(Format format) { + for (Map.Entry entry : formats.entrySet()) { + if (entry.getValue() == defaultNumFormat) { + entry.setValue(format); + } + } + defaultNumFormat = format; + } + + /** + * Adds a new format to the available formats. + *

+ * The value that will be passed to the Format's format method (specified by java.text.Format#format) + * will be a double value from a numeric cell. Therefore the code in the format method should expect a + * Number value. + *

+ * + * @param excelFormatStr + * The data format string + * @param format + * A Format instance + */ + public void addFormat(String excelFormatStr, Format format) { + formats.put(excelFormatStr, format); + } + + // Some custom formats + + /** + * @return a DecimalFormat with parseIntegerOnly set true + */ + private static DecimalFormat createIntegerOnlyFormat(String fmt) { + DecimalFormatSymbols dsf = DecimalFormatSymbols.getInstance(Locale.ROOT); + DecimalFormat result = new DecimalFormat(fmt, dsf); + result.setParseIntegerOnly(true); + return result; + } + + /** + * Enables excel style rounding mode (round half up) on the Decimal Format given. + */ + public static void setExcelStyleRoundingMode(DecimalFormat format) { + setExcelStyleRoundingMode(format, RoundingMode.HALF_UP); + } + + /** + * Enables custom rounding mode on the given Decimal Format. + * + * @param format + * DecimalFormat + * @param roundingMode + * RoundingMode + */ + public static void setExcelStyleRoundingMode(DecimalFormat format, RoundingMode roundingMode) { + format.setRoundingMode(roundingMode); + } + + /** + * Format class for Excel's SSN format. This class mimics Excel's built-in SSN formatting. + * + * @author James May + */ + @SuppressWarnings("serial") + private static final class SSNFormat extends Format { + private static final DecimalFormat df = createIntegerOnlyFormat("000000000"); + + private SSNFormat() { + // enforce singleton + } + + /** Format a number as an SSN */ + public static String format(Number num) { + String result = df.format(num); + return result.substring(0, 3) + '-' + result.substring(3, 5) + '-' + result.substring(5, 9); + } + + @Override + public StringBuffer format(Object obj, StringBuffer toAppendTo, FieldPosition pos) { + return toAppendTo.append(format((Number)obj)); + } + + @Override + public Object parseObject(String source, ParsePosition pos) { + return df.parseObject(source, pos); + } + } + + /** + * Format class for Excel Zip + 4 format. This class mimics Excel's built-in formatting for Zip + 4. + * + * @author James May + */ + @SuppressWarnings("serial") + private static final class ZipPlusFourFormat extends Format { + private static final DecimalFormat df = createIntegerOnlyFormat("000000000"); + + private ZipPlusFourFormat() { + // enforce singleton + } + + /** Format a number as Zip + 4 */ + public static String format(Number num) { + String result = df.format(num); + return result.substring(0, 5) + '-' + result.substring(5, 9); + } + + @Override + public StringBuffer format(Object obj, StringBuffer toAppendTo, FieldPosition pos) { + return toAppendTo.append(format((Number)obj)); + } + + @Override + public Object parseObject(String source, ParsePosition pos) { + return df.parseObject(source, pos); + } + } + + /** + * Format class for Excel phone number format. This class mimics Excel's built-in phone number formatting. + * + * @author James May + */ + @SuppressWarnings("serial") + private static final class PhoneFormat extends Format { + private static final DecimalFormat df = createIntegerOnlyFormat("##########"); + + private PhoneFormat() { + // enforce singleton + } + + /** Format a number as a phone number */ + public static String format(Number num) { + String result = df.format(num); + StringBuilder sb = new StringBuilder(); + String seg1, seg2, seg3; + int len = result.length(); + if (len <= 4) { + return result; + } + + seg3 = result.substring(len - 4, len); + seg2 = result.substring(Math.max(0, len - 7), len - 4); + seg1 = result.substring(Math.max(0, len - 10), Math.max(0, len - 7)); + + if (seg1.trim().length() > 0) { + sb.append('(').append(seg1).append(") "); + } + if (seg2.trim().length() > 0) { + sb.append(seg2).append('-'); + } + sb.append(seg3); + return sb.toString(); + } + + @Override + public StringBuffer format(Object obj, StringBuffer toAppendTo, FieldPosition pos) { + return toAppendTo.append(format((Number)obj)); + } + + @Override + public Object parseObject(String source, ParsePosition pos) { + return df.parseObject(source, pos); + } + } + +} diff --git a/src/main/java/com/alibaba/excel/metadata/GlobalConfiguration.java b/src/main/java/com/alibaba/excel/metadata/GlobalConfiguration.java index 921d70a..12cc8d3 100644 --- a/src/main/java/com/alibaba/excel/metadata/GlobalConfiguration.java +++ b/src/main/java/com/alibaba/excel/metadata/GlobalConfiguration.java @@ -1,5 +1,7 @@ package com.alibaba.excel.metadata; +import java.util.Locale; + /** * Global configuration * @@ -18,6 +20,11 @@ public class GlobalConfiguration { * @return */ private Boolean use1904windowing; + /** + * A Locale object represents a specific geographical, political, or cultural region. This parameter is + * used when formatting dates and numbers. + */ + private Locale locale; public Boolean getUse1904windowing() { return use1904windowing; @@ -34,4 +41,12 @@ public class GlobalConfiguration { public void setAutoTrim(Boolean autoTrim) { this.autoTrim = autoTrim; } + + public Locale getLocale() { + return locale; + } + + public void setLocale(Locale locale) { + this.locale = locale; + } } diff --git a/src/main/java/com/alibaba/excel/util/DateUtils.java b/src/main/java/com/alibaba/excel/util/DateUtils.java index dfc8d20..b375733 100644 --- a/src/main/java/com/alibaba/excel/util/DateUtils.java +++ b/src/main/java/com/alibaba/excel/util/DateUtils.java @@ -1,24 +1,45 @@ package com.alibaba.excel.util; -import java.text.Format; +import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; - -import org.apache.poi.ss.formula.ConditionalFormattingEvaluator; -import org.apache.poi.ss.usermodel.Cell; -import org.apache.poi.ss.usermodel.DateUtil; -import org.apache.poi.ss.usermodel.ExcelNumberFormat; -import org.apache.poi.ss.usermodel.ExcelStyleDateFormatter; +import java.util.HashMap; +import java.util.Map; +import java.util.regex.Pattern; /** * Date utils * * @author Jiaju Zhuang **/ -public class DateUtils { - +public class DateUtils implements ThreadLocalCachedUtils { + /** + * Is a cache of dates + */ + private static final ThreadLocal> DATE_THREAD_LOCAL = + new ThreadLocal>(); + /** + * Is a cache of dates + */ + private static final ThreadLocal> DATE_FORMAT_THREAD_LOCAL = + new ThreadLocal>(); + /** + * The following patterns are used in {@link #isADateFormat(Integer, String)} + */ + private static final Pattern date_ptrn1 = Pattern.compile("^\\[\\$\\-.*?\\]"); + private static final Pattern date_ptrn2 = Pattern.compile("^\\[[a-zA-Z]+\\]"); + private static final Pattern date_ptrn3a = Pattern.compile("[yYmMdDhHsS]"); + // add "\u5e74 \u6708 \u65e5" for Chinese/Japanese date format:2017 \u5e74 2 \u6708 7 \u65e5 + private static final Pattern date_ptrn3b = + Pattern.compile("^[\\[\\]yYmMdDhHsS\\-T/\u5e74\u6708\u65e5,. :\"\\\\]+0*[ampAMP/]*$"); + // elapsed time patterns: [h],[m] and [s] + private static final Pattern date_ptrn4 = Pattern.compile("^\\[([hH]+|[mM]+|[sS]+)\\]"); + // for format which start with "[DBNum1]" or "[DBNum2]" or "[DBNum3]" could be a Chinese date + private static final Pattern date_ptrn5 = Pattern.compile("^\\[DBNum(1|2|3)\\]"); + // for format which start with "年" or "月" or "日" or "时" or "分" or "秒" could be a Chinese date + private static final Pattern date_ptrn6 = Pattern.compile("(年|月|日|时|分|秒)+"); public static final String DATE_FORMAT_10 = "yyyy-MM-dd"; public static final String DATE_FORMAT_14 = "yyyyMMddHHmmss"; @@ -41,7 +62,7 @@ public class DateUtils { if (StringUtils.isEmpty(dateFormat)) { dateFormat = switchDateFormat(dateString); } - return new SimpleDateFormat(dateFormat).parse(dateString); + return getCacheDateFormat(dateFormat).parse(dateString); } /** @@ -107,196 +128,183 @@ public class DateUtils { if (StringUtils.isEmpty(dateFormat)) { dateFormat = DATE_FORMAT_19; } - return new SimpleDateFormat(dateFormat).format(date); + return getCacheDateFormat(dateFormat).format(date); + } + + private static DateFormat getCacheDateFormat(String dateFormat) { + Map dateFormatMap = DATE_FORMAT_THREAD_LOCAL.get(); + if (dateFormatMap == null) { + dateFormatMap = new HashMap(); + DATE_FORMAT_THREAD_LOCAL.set(dateFormatMap); + } else { + SimpleDateFormat dateFormatCached = dateFormatMap.get(dateFormat); + if (dateFormatCached != null) { + return dateFormatCached; + } + } + SimpleDateFormat simpleDateFormat = new SimpleDateFormat(dateFormat); + dateFormatMap.put(dateFormat, simpleDateFormat); + return simpleDateFormat; } -// -// /** -// * Determine if it is a date format. -// * -// * @param dataFormat -// * @param dataFormatString -// * @return -// */ -// public static boolean isDateFormatted(Integer dataFormat, String dataFormatString) { -// if (cell == null) { -// return false; -// } -// boolean isDate = false; -// -// double d = cell.getNumericCellValue(); -// if (DateUtil.isValidExcelDate(d)) { -// ExcelNumberFormat nf = ExcelNumberFormat.from(cell, cfEvaluator); -// if (nf == null) { -// return false; -// } -// bDate = isADateFormat(nf); -// } -// return bDate; -// } -// -// private String getFormattedDateString(Cell cell, ConditionalFormattingEvaluator cfEvaluator) { -// if (cell == null) { -// return null; -// } -// Format dateFormat = getFormat(cell, cfEvaluator); -// synchronized (dateFormat) { -// if(dateFormat instanceof ExcelStyleDateFormatter) { -// // Hint about the raw excel value -// ((ExcelStyleDateFormatter)dateFormat).setDateToBeFormatted( -// cell.getNumericCellValue() -// ); -// } -// Date d = cell.getDateCellValue(); -// return performDateFormatting(d, dateFormat); -// } -// } -// -// -// public static boolean isADateFormat(int formatIndex, String formatString) { -// // First up, is this an internal date format? -// if (isInternalDateFormat(formatIndex)) { -// return true; -// } -// if (StringUtils.isEmpty(formatString)) { -// return false; -// } -// -// // check the cache first -// if (isCached(formatString, formatIndex)) { -// return lastCachedResult.get(); -// } -// -// String fs = formatString; -// /*if (false) { -// // Normalize the format string. The code below is equivalent -// // to the following consecutive regexp replacements: -// -// // Translate \- into just -, before matching -// fs = fs.replaceAll("\\\\-","-"); -// // And \, into , -// fs = fs.replaceAll("\\\\,",","); -// // And \. into . -// fs = fs.replaceAll("\\\\\\.","."); -// // And '\ ' into ' ' -// fs = fs.replaceAll("\\\\ "," "); -// -// // If it end in ;@, that's some crazy dd/mm vs mm/dd -// // switching stuff, which we can ignore -// fs = fs.replaceAll(";@", ""); -// -// // The code above was reworked as suggested in bug 48425: -// // simple loop is more efficient than consecutive regexp replacements. -// }*/ -// final int length = fs.length(); -// StringBuilder sb = new StringBuilder(length); -// for (int i = 0; i < length; i++) { -// char c = fs.charAt(i); -// if (i < length - 1) { -// char nc = fs.charAt(i + 1); -// if (c == '\\') { -// switch (nc) { -// case '-': -// case ',': -// case '.': -// case ' ': -// case '\\': -// // skip current '\' and continue to the next char -// continue; -// } -// } else if (c == ';' && nc == '@') { -// i++; -// // skip ";@" duplets -// continue; -// } -// } -// sb.append(c); -// } -// fs = sb.toString(); -// -// // short-circuit if it indicates elapsed time: [h], [m] or [s] -// if (date_ptrn4.matcher(fs).matches()) { -// cache(formatString, formatIndex, true); -// return true; -// } -// // If it starts with [DBNum1] or [DBNum2] or [DBNum3] -// // then it could be a Chinese date -// fs = date_ptrn5.matcher(fs).replaceAll(""); -// // If it starts with [$-...], then could be a date, but -// // who knows what that starting bit is all about -// fs = date_ptrn1.matcher(fs).replaceAll(""); -// // If it starts with something like [Black] or [Yellow], -// // then it could be a date -// fs = date_ptrn2.matcher(fs).replaceAll(""); -// // You're allowed something like dd/mm/yy;[red]dd/mm/yy -// // which would place dates before 1900/1904 in red -// // For now, only consider the first one -// final int separatorIndex = fs.indexOf(';'); -// if (0 < separatorIndex && separatorIndex < fs.length() - 1) { -// fs = fs.substring(0, separatorIndex); -// } -// -// // Ensure it has some date letters in it -// // (Avoids false positives on the rest of pattern 3) -// if (!date_ptrn3a.matcher(fs).find()) { -// return false; -// } -// -// // If we get here, check it's only made up, in any case, of: -// // y m d h s - \ / , . : [ ] T -// // optionally followed by AM/PM -// -// boolean result = date_ptrn3b.matcher(fs).matches(); -// cache(formatString, formatIndex, result); -// return result; -// } -// -// /** -// * Given a format ID this will check whether the format represents an internal excel date format or not. -// * -// * @see #isADateFormat(int, java.lang.String) -// */ -// public static boolean isInternalDateFormat(int format) { -// switch (format) { -// // Internal Date Formats as described on page 427 in -// // Microsoft Excel Dev's Kit... -// // 14-22 -// case 0x0e: -// case 0x0f: -// case 0x10: -// case 0x11: -// case 0x12: -// case 0x13: -// 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 -// 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; -// } + /** + * Determine if it is a date format. + * + * @param formatIndex + * @param formatString + * @return + */ + public static boolean isADateFormat(Integer formatIndex, String formatString) { + if (formatIndex == null) { + return false; + } + Map isDateCache = DATE_THREAD_LOCAL.get(); + if (isDateCache == null) { + isDateCache = new HashMap(); + DATE_THREAD_LOCAL.set(isDateCache); + } else { + Boolean isDateCachedData = isDateCache.get(formatIndex); + if (isDateCachedData != null) { + return isDateCachedData; + } + } + boolean isDate = isADateFormatUncached(formatIndex, formatString); + isDateCache.put(formatIndex, isDate); + return isDate; + } + + /** + * Determine if it is a date format. + * + * @param formatIndex + * @param formatString + * @return + */ + public static boolean isADateFormatUncached(Integer formatIndex, String formatString) { + // First up, is this an internal date format? + if (isInternalDateFormat(formatIndex)) { + return true; + } + if (StringUtils.isEmpty(formatString)) { + return false; + } + String fs = formatString; + final int length = fs.length(); + StringBuilder sb = new StringBuilder(length); + for (int i = 0; i < length; i++) { + char c = fs.charAt(i); + if (i < length - 1) { + char nc = fs.charAt(i + 1); + if (c == '\\') { + switch (nc) { + case '-': + case ',': + case '.': + case ' ': + case '\\': + // skip current '\' and continue to the next char + continue; + } + } else if (c == ';' && nc == '@') { + i++; + // skip ";@" duplets + continue; + } + } + sb.append(c); + } + fs = sb.toString(); + + // short-circuit if it indicates elapsed time: [h], [m] or [s] + if (date_ptrn4.matcher(fs).matches()) { + return true; + } + // If it starts with [DBNum1] or [DBNum2] or [DBNum3] + // then it could be a Chinese date + fs = date_ptrn5.matcher(fs).replaceAll(""); + // If it starts with [$-...], then could be a date, but + // who knows what that starting bit is all about + fs = date_ptrn1.matcher(fs).replaceAll(""); + // If it starts with something like [Black] or [Yellow], + // then it could be a date + fs = date_ptrn2.matcher(fs).replaceAll(""); + // You're allowed something like dd/mm/yy;[red]dd/mm/yy + // which would place dates before 1900/1904 in red + // For now, only consider the first one + final int separatorIndex = fs.indexOf(';'); + if (0 < separatorIndex && separatorIndex < fs.length() - 1) { + fs = fs.substring(0, separatorIndex); + } + + // Ensure it has some date letters in it + // (Avoids false positives on the rest of pattern 3) + if (!date_ptrn3a.matcher(fs).find()) { + return false; + } + + // If we get here, check it's only made up, in any case, of: + // y m d h s - \ / , . : [ ] T + // optionally followed by AM/PM + boolean result = date_ptrn3b.matcher(fs).matches(); + if (result) { + return true; + } + result = date_ptrn6.matcher(fs).find(); + return result; + } + + /** + * Given a format ID this will check whether the format represents an internal excel date format or not. + * + * @see #isADateFormat(Integer, String) + */ + public static boolean isInternalDateFormat(int format) { + switch (format) { + // Internal Date Formats as described on page 427 in + // Microsoft Excel Dev's Kit... + // 14-22 + case 0x0e: + case 0x0f: + case 0x10: + case 0x11: + case 0x12: + case 0x13: + 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 + 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; + } + + @Override + public void removeThreadLocalCache() { + DATE_THREAD_LOCAL.remove(); + DATE_FORMAT_THREAD_LOCAL.remove(); + } } diff --git a/src/main/java/com/alibaba/excel/util/NumberDataFormatterUtils.java b/src/main/java/com/alibaba/excel/util/NumberDataFormatterUtils.java index 6a4b9c5..52ced2a 100644 --- a/src/main/java/com/alibaba/excel/util/NumberDataFormatterUtils.java +++ b/src/main/java/com/alibaba/excel/util/NumberDataFormatterUtils.java @@ -1,154 +1,47 @@ -//package com.alibaba.excel.util; -// -//import java.text.Format; -// -//import org.apache.poi.ss.format.CellFormat; -//import org.apache.poi.ss.formula.ConditionalFormattingEvaluator; -//import org.apache.poi.ss.usermodel.Cell; -//import org.apache.poi.ss.usermodel.DataFormatter; -//import org.apache.poi.ss.usermodel.DateUtil; -//import org.apache.poi.ss.usermodel.ExcelNumberFormat; -//import org.apache.poi.ss.usermodel.ExcelStyleDateFormatter; -//import org.apache.poi.util.POILogger; -// -///** -// * Convert number data, including date. -// * -// * @author Jiaju Zhuang -// **/ -//public class NumberDataFormatterUtils { -// -// /** -// * -// * @param data -// * Not null. -// * @param dataFormatString -// * Not null. -// * @return -// */ -// public String format(Double data, Integer dataFormat, String dataFormatString) { -// -// if (DateUtil.isCellDateFormatted(cell, cfEvaluator)) { -// return getFormattedDateString(cell, cfEvaluator); -// } -// return getFormattedNumberString(cell, cfEvaluator); -// -// } -// -// private String getFormattedDateString(Double data,String dataFormatString) { -// -// -// if (cell == null) { -// return null; -// } -// Format dateFormat = getFormat(cell, cfEvaluator); -// synchronized (dateFormat) { -// if (dateFormat instanceof ExcelStyleDateFormatter) { -// // Hint about the raw excel value -// ((ExcelStyleDateFormatter)dateFormat).setDateToBeFormatted(cell.getNumericCellValue()); -// } -// Date d = cell.getDateCellValue(); -// return performDateFormatting(d, dateFormat); -// } -// } -// -// -// /** -// * Return a Format for the given cell if one exists, otherwise try to -// * create one. This method will return null if the any of the -// * following is true: -// *
    -// *
  • the cell's style is null
  • -// *
  • the style's data format string is null or empty
  • -// *
  • the format string cannot be recognized as either a number or date
  • -// *
-// * -// * @param cell The cell to retrieve a Format for -// * @return A Format for the format String -// */ -// private Format getFormat(Cell cell, ConditionalFormattingEvaluator cfEvaluator) { -// if (cell == null) return null; -// -// ExcelNumberFormat numFmt = ExcelNumberFormat.from(cell, cfEvaluator); -// -// if ( numFmt == null) { -// return null; -// } -// -// int formatIndex = numFmt.getIdx(); -// String formatStr = numFmt.getFormat(); -// if(formatStr == null || formatStr.trim().length() == 0) { -// return null; -// } -// return getFormat(cell.getNumericCellValue(), formatIndex, formatStr, isDate1904(cell)); -// } -// -// private boolean isDate1904(Cell cell) { -// if ( cell != null && cell.getSheet().getWorkbook() instanceof Date1904Support) { -// return ((Date1904Support)cell.getSheet().getWorkbook()).isDate1904(); -// -// } -// return false; -// } -// -// private Format getFormat(double cellValue, int formatIndex, String formatStrIn, boolean use1904Windowing) { -// localeChangedObservable.checkForLocaleChange(); -// -// // Might be better to separate out the n p and z formats, falling back to p when n and z are not set. -// // That however would require other code to be re factored. -// // String[] formatBits = formatStrIn.split(";"); -// // int i = cellValue > 0.0 ? 0 : cellValue < 0.0 ? 1 : 2; -// // String formatStr = (i < formatBits.length) ? formatBits[i] : formatBits[0]; -// -// String formatStr = formatStrIn; -// -// // Excel supports 2+ part conditional data formats, eg positive/negative/zero, -// // or (>1000),(>0),(0),(negative). As Java doesn't handle these kinds -// // of different formats for different ranges, just +ve/-ve, we need to -// // handle these ourselves in a special way. -// // For now, if we detect 2+ parts, we call out to CellFormat to handle it -// // TODO Going forward, we should really merge the logic between the two classes -// if (formatStr.contains(";") && -// (formatStr.indexOf(';') != formatStr.lastIndexOf(';') -// || rangeConditionalPattern.matcher(formatStr).matches() -// ) ) { -// try { -// // Ask CellFormat to get a formatter for it -// CellFormat cfmt = CellFormat.getInstance(locale, formatStr); -// // CellFormat requires callers to identify date vs not, so do so -// Object cellValueO = Double.valueOf(cellValue); -// if (DateUtil.isADateFormat(formatIndex, formatStr) && -// // don't try to handle Date value 0, let a 3 or 4-part format take care of it -// ((Double)cellValueO).doubleValue() != 0.0) { -// cellValueO = DateUtil.getJavaDate(cellValue, use1904Windowing); -// } -// // Wrap and return (non-cachable - CellFormat does that) -// return new DataFormatter.CellFormatResultWrapper( cfmt.apply(cellValueO) ); -// } catch (Exception e) { -// logger.log(POILogger.WARN, "Formatting failed for format " + formatStr + ", falling back", e); -// } -// } -// -// // Excel's # with value 0 will output empty where Java will output 0. This hack removes the # from the format. -// if (emulateCSV && cellValue == 0.0 && formatStr.contains("#") && !formatStr.contains("0")) { -// formatStr = formatStr.replaceAll("#", ""); -// } -// -// // See if we already have it cached -// Format format = formats.get(formatStr); -// if (format != null) { -// return format; -// } -// -// // Is it one of the special built in types, General or @? -// if ("General".equalsIgnoreCase(formatStr) || "@".equals(formatStr)) { -// return generalNumberFormat; -// } -// -// // Build a formatter, and cache it -// format = createFormat(cellValue, formatIndex, formatStr); -// formats.put(formatStr, format); -// return format; -// } -// -//} +package com.alibaba.excel.util; + +import com.alibaba.excel.metadata.DataFormatter; +import com.alibaba.excel.metadata.GlobalConfiguration; + +/** + * Convert number data, including date. + * + * @author Jiaju Zhuang + **/ +public class NumberDataFormatterUtils implements ThreadLocalCachedUtils { + /** + * Cache DataFormatter. + */ + private static final ThreadLocal DATA_FORMATTER_THREAD_LOCAL = new ThreadLocal(); + + /** + * Format number data. + * + * @param data + * @param dataFormat + * Not null. + * @param dataFormatString + * @param globalConfiguration + * @return + */ + public static String format(Double data, Integer dataFormat, String dataFormatString, + GlobalConfiguration globalConfiguration) { + DataFormatter dataFormatter = DATA_FORMATTER_THREAD_LOCAL.get(); + if (dataFormatter == null) { + if (globalConfiguration != null) { + dataFormatter = + new DataFormatter(globalConfiguration.getLocale(), globalConfiguration.getUse1904windowing()); + } else { + dataFormatter = new DataFormatter(); + } + DATA_FORMATTER_THREAD_LOCAL.set(dataFormatter); + } + return dataFormatter.format(data, dataFormat, dataFormatString); + + } + + @Override + public void removeThreadLocalCache() { + DATA_FORMATTER_THREAD_LOCAL.remove(); + } +} diff --git a/src/main/java/com/alibaba/excel/util/ThreadLocalCachedUtils.java b/src/main/java/com/alibaba/excel/util/ThreadLocalCachedUtils.java new file mode 100644 index 0000000..d136703 --- /dev/null +++ b/src/main/java/com/alibaba/excel/util/ThreadLocalCachedUtils.java @@ -0,0 +1,14 @@ +package com.alibaba.excel.util; + +/** + * Thread local cache in the current tool class. + * + * @author Jiaju Zhuang + **/ +public interface ThreadLocalCachedUtils { + + /** + * Remove remove thread local cached. + */ + void removeThreadLocalCache(); +} diff --git a/src/test/java/com/alibaba/easyexcel/test/core/dataformat/DateFormatData.java b/src/test/java/com/alibaba/easyexcel/test/core/dataformat/DateFormatData.java new file mode 100644 index 0000000..d627e46 --- /dev/null +++ b/src/test/java/com/alibaba/easyexcel/test/core/dataformat/DateFormatData.java @@ -0,0 +1,14 @@ +package com.alibaba.easyexcel.test.core.dataformat; + +import lombok.Data; + +/** + * @author Jiaju Zhuang + */ +@Data +public class DateFormatData { + private String date; + private String dateString; + private String number; + private String numberString; +} diff --git a/src/test/java/com/alibaba/easyexcel/test/core/dataformat/DateFormatTest.java b/src/test/java/com/alibaba/easyexcel/test/core/dataformat/DateFormatTest.java new file mode 100644 index 0000000..69b6709 --- /dev/null +++ b/src/test/java/com/alibaba/easyexcel/test/core/dataformat/DateFormatTest.java @@ -0,0 +1,50 @@ +package com.alibaba.easyexcel.test.core.dataformat; + +import java.io.File; +import java.util.List; + +import org.junit.BeforeClass; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.alibaba.easyexcel.test.util.TestFileUtil; +import com.alibaba.excel.EasyExcel; +import com.alibaba.fastjson.JSON; + +/** + * + * @author Jiaju Zhuang + */ +public class DateFormatTest { + private static final Logger LOGGER = LoggerFactory.getLogger(DateFormatTest.class); + + private static File file07; + private static File file03; + + @BeforeClass + public static void init() { + file07 = TestFileUtil.readFile("dataformat" + File.separator + "dataformat.xlsx"); + file03 = TestFileUtil.readFile("dataformat" + File.separator + "dataformat.xls"); + } + + @Test + public void t01Read07() { + read(file07); + } + + @Test + public void t02Read03() { + read(file03); + } + + private void read(File file) { + List list = EasyExcel.read(file, DateFormatData.class, null).sheet().doReadSync(); + for (DateFormatData data : list) { + if (!data.getDate().equals(data.getDateString())) { + LOGGER.info("返回:{}", JSON.toJSONString(data)); + } + } + } + +} diff --git a/src/test/java/com/alibaba/easyexcel/test/temp/dataformat/DataFormatTest.java b/src/test/java/com/alibaba/easyexcel/test/temp/dataformat/DataFormatTest.java index d9c7bb2..d3db5e0 100644 --- a/src/test/java/com/alibaba/easyexcel/test/temp/dataformat/DataFormatTest.java +++ b/src/test/java/com/alibaba/easyexcel/test/temp/dataformat/DataFormatTest.java @@ -6,10 +6,14 @@ import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.List; +import java.util.Locale; +import java.util.regex.Pattern; +import org.apache.poi.openxml4j.exceptions.InvalidFormatException; import org.apache.poi.ss.usermodel.Cell; import org.apache.poi.ss.usermodel.DataFormatter; import org.apache.poi.ss.usermodel.DateUtil; +import org.apache.poi.ss.usermodel.Row; import org.apache.poi.ss.usermodel.Sheet; import org.apache.poi.xssf.usermodel.XSSFWorkbook; import org.junit.Ignore; @@ -17,7 +21,9 @@ import org.junit.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.alibaba.easyexcel.test.core.dataformat.DateFormatData; import com.alibaba.easyexcel.test.temp.Lock2Test; +import com.alibaba.easyexcel.test.util.TestFileUtil; import com.alibaba.excel.EasyExcel; import com.alibaba.fastjson.JSON; @@ -124,4 +130,61 @@ public class DataFormatTest { System.out.println("end:" + (System.currentTimeMillis() - start)); } + @Test + public void test355() throws IOException, InvalidFormatException { + File file = TestFileUtil.readFile("dataformat" + File.separator + "dataformat.xlsx"); + XSSFWorkbook xssfWorkbook = new XSSFWorkbook(file); + Sheet xssfSheet = xssfWorkbook.getSheetAt(0); + DataFormatter d = new DataFormatter(Locale.CHINA); + + for (int i = 0; i < xssfSheet.getLastRowNum(); i++) { + Row row = xssfSheet.getRow(i); + System.out.println(d.formatCellValue(row.getCell(0))); + } + + } + + @Test + public void test3556() throws IOException, InvalidFormatException { + String file = "D://test/dataformat.xlsx"; + XSSFWorkbook xssfWorkbook = new XSSFWorkbook(file); + Sheet xssfSheet = xssfWorkbook.getSheetAt(0); + DataFormatter d = new DataFormatter(Locale.CHINA); + + for (int i = 0; i < xssfSheet.getLastRowNum(); i++) { + Row row = xssfSheet.getRow(i); + System.out.println(d.formatCellValue(row.getCell(0))); + } + + } + + @Test + public void tests() throws IOException, InvalidFormatException { + SimpleDateFormat s1 = new SimpleDateFormat("yyyy\"5E74\"m\"6708\"d\"65E5\""); + System.out.println(s1.format(new Date())); + s1 = new SimpleDateFormat("yyyy年m月d日"); + System.out.println(s1.format(new Date())); + } + + @Test + public void tests1() throws IOException, InvalidFormatException { + String file = "D://test/dataformat1.xlsx"; + List list = EasyExcel.read(file, DateFormatData.class, null).sheet().doReadSync(); + for (DateFormatData data : list) { + LOGGER.info("返回:{}", JSON.toJSONString(data)); + } + } + + @Test + public void tests3() throws IOException, InvalidFormatException { + SimpleDateFormat s1 = new SimpleDateFormat("ah\"时\"mm\"分\""); + System.out.println(s1.format(new Date())); + } + + private static final Pattern date_ptrn6 = Pattern.compile("^.*(年|月|日|时|分|秒)+.*$"); + + @Test + public void tests34() throws IOException, InvalidFormatException { + System.out.println(date_ptrn6.matcher("2017但是").matches()); + } } diff --git a/src/test/java/com/alibaba/easyexcel/test/temp/poi/PoiWriteTest.java b/src/test/java/com/alibaba/easyexcel/test/temp/poi/PoiWriteTest.java index 3a70d0d..68fff70 100644 --- a/src/test/java/com/alibaba/easyexcel/test/temp/poi/PoiWriteTest.java +++ b/src/test/java/com/alibaba/easyexcel/test/temp/poi/PoiWriteTest.java @@ -2,12 +2,8 @@ package com.alibaba.easyexcel.test.temp.poi; import java.io.FileOutputStream; import java.io.IOException; -import java.lang.reflect.Field; -import java.lang.reflect.ParameterizedType; -import java.lang.reflect.Type; import java.util.regex.Pattern; -import org.apache.poi.ss.formula.functions.T; import org.apache.poi.xssf.streaming.SXSSFCell; import org.apache.poi.xssf.streaming.SXSSFRow; import org.apache.poi.xssf.streaming.SXSSFSheet; @@ -17,11 +13,8 @@ import org.junit.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.alibaba.excel.metadata.CellData; import com.alibaba.fastjson.JSON; -import sun.reflect.generics.reflectiveObjects.ParameterizedTypeImpl; - /** * 测试poi * @@ -99,58 +92,4 @@ public class PoiWriteTest { } - @Test - public void test() throws Exception { - Class clazz = TestCell.class; - - Field field = clazz.getDeclaredField("c2"); - // 通过getDeclaredField可以获得成员变量,但是对于Map来说,仅仅可以知道它是个Map,无法知道键值对各自的数据类型 - - Type gType = field.getGenericType(); - // 获得field的泛型类型 - - // 如果gType是ParameterizedType对象(参数化) - if (gType instanceof ParameterizedType) { - - ParameterizedType pType = (ParameterizedType)gType; - // 就把它转换成ParameterizedType对象 - - Type[] tArgs = pType.getActualTypeArguments(); - // 获得泛型类型的泛型参数(实际类型参数) - ParameterizedTypeImpl c = (ParameterizedTypeImpl)pType.getActualTypeArguments()[0]; - Class ttt = c.getRawType(); - System.out.println(ttt); - } else { - System.out.println("出错!!!"); - } - - } - - @Test - public void test2() throws Exception { - Class clazz = TestCell.class; - - Field field = clazz.getDeclaredField("c2"); - // 通过getDeclaredField可以获得成员变量,但是对于Map来说,仅仅可以知道它是个Map,无法知道键值对各自的数据类型 - - Type gType = field.getGenericType(); - // 获得field的泛型类型 - - // 如果gType是ParameterizedType对象(参数化) - if (gType instanceof ParameterizedType) { - - ParameterizedType pType = (ParameterizedType)gType; - // 就把它转换成ParameterizedType对象 - - Type[] tArgs = pType.getActualTypeArguments(); - // 获得泛型类型的泛型参数(实际类型参数) - ParameterizedTypeImpl c = (ParameterizedTypeImpl)pType.getActualTypeArguments()[0]; - Class ttt = c.getRawType(); - System.out.println(ttt); - } else { - System.out.println("出错!!!"); - } - - } - } diff --git a/src/test/resources/dataformat/dataformat.xlsx b/src/test/resources/dataformat/dataformat.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..370d983a931862faf90274a954ed71e38cb92ac5 GIT binary patch literal 11489 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 literal 0 HcmV?d00001