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 0000000..370d983 Binary files /dev/null and b/src/test/resources/dataformat/dataformat.xlsx differ