git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1083173 13f79535-47bb-0310-9956-ffa450edef68tags/REL_3_8_BETA2
@@ -34,6 +34,7 @@ | |||
<changes> | |||
<release version="3.8-beta2" date="2011-??-??"> | |||
<action dev="poi-developers" type="fix">50841 - Improved SpreadSheet DataFormatter to handle scientific notation, invalid dates and format spacers</action> | |||
<action dev="poi-developers" type="fix">49381 - Correct createFreezePane in XSSF, so that the left row/column matches the documentation + HSSF</action> | |||
<action dev="poi-developers" type="fix">49253 - When setting repeating rows and columns for XSSF, don't break the print settings if they were already there</action> | |||
<action dev="poi-developers" type="fix">49219 - ExternalNameRecord support for DDE Link entries without an operation</action> |
@@ -389,7 +389,7 @@ public class ToCSV { | |||
// formatted String encapsulating the cells contents. | |||
this.workbook = WorkbookFactory.create(fis); | |||
this.evaluator = this.workbook.getCreationHelper().createFormulaEvaluator(); | |||
this.formatter = new DataFormatter(); | |||
this.formatter = new DataFormatter(true); | |||
} | |||
finally { | |||
if(fis != null) { |
@@ -63,8 +63,27 @@ import java.text.*; | |||
* default format will only be used when a Format cannot be created from the | |||
* cell's data format string. | |||
* | |||
* <p> | |||
* Note that by default formatted numeric values are trimmed. | |||
* Excel formats can contain spacers and padding and the default behavior is to strip them off. | |||
* </p> | |||
* <p>Example:</p> | |||
* <p> | |||
* Consider a numeric cell with a value <code>12.343</code> and format <code>"##.##_ "</code>. | |||
* The trailing underscore and space ("_ ") in the format adds a space to the end and Excel formats this cell as <code>"12.34 "</code>, | |||
* but <code>DataFormatter</code> trims the formatted value and returns <code>"12.34"</code>. | |||
* </p> | |||
* You can enable spaces by passing the <code>emulateCsv=true</code> flag in the <code>DateFormatter</code> cosntructor. | |||
* If set to true, then the output tries to conform to what you get when you take an xls or xlsx in Excel and Save As CSV file: | |||
* <ul> | |||
* <li>returned values are not trimmed</li> | |||
* <li>Invalid dates are formatted as 255 pound signs ("#")</li> | |||
* <li>simulate Excel's handling of a format string of all # when the value is 0. | |||
* Excel will output "", <code>DataFormatter</code> will output "0". | |||
* </ul> | |||
* @author James May (james dot may at fmr dot com) | |||
* | |||
* @author Robert Kish | |||
* | |||
*/ | |||
public class DataFormatter { | |||
@@ -80,7 +99,7 @@ public class DataFormatter { | |||
/** A regex to find patterns like [$$-1009] and [$?-452]. */ | |||
private static final Pattern specialPatternGroup = 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) | |||
@@ -89,7 +108,18 @@ public class DataFormatter { | |||
Pattern.compile("(\\[BLACK\\])|(\\[BLUE\\])|(\\[CYAN\\])|(\\[GREEN\\])|" + | |||
"(\\[MAGENTA\\])|(\\[RED\\])|(\\[WHITE\\])|(\\[YELLOW\\])|" + | |||
"(\\[COLOR\\s*\\d\\])|(\\[COLOR\\s*[0-5]\\d\\])", Pattern.CASE_INSENSITIVE); | |||
/** | |||
* 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. | |||
*/ | |||
@@ -115,11 +145,33 @@ public class DataFormatter { | |||
*/ | |||
private final Map<String,Format> formats; | |||
private boolean emulateCsv = false; | |||
/** | |||
* Creates a formatter using the {@link Locale#getDefault() default locale}. | |||
*/ | |||
public DataFormatter() { | |||
this(false); | |||
} | |||
/** | |||
* Creates a formatter using the {@link Locale#getDefault() default locale}. | |||
* | |||
* @param emulateCsv whether to emulate CSV output. | |||
*/ | |||
public DataFormatter(boolean emulateCsv) { | |||
this(Locale.getDefault()); | |||
this.emulateCsv = emulateCsv; | |||
} | |||
/** | |||
* Creates a formatter using the given locale. | |||
* | |||
* @param emulateCsv whether to emulate CSV output. | |||
*/ | |||
public DataFormatter(Locale locale, boolean emulateCsv) { | |||
this(locale); | |||
this.emulateCsv = emulateCsv; | |||
} | |||
/** | |||
@@ -177,27 +229,42 @@ public class DataFormatter { | |||
return getFormat(cell.getNumericCellValue(), formatIndex, formatStr); | |||
} | |||
private Format getFormat(double cellValue, int formatIndex, String formatStr) { | |||
// Excel supports positive/negative/zero, but java | |||
// doesn't, so we need to do it specially | |||
if(formatStr.indexOf(';') != formatStr.lastIndexOf(';')) { | |||
int lastAt = formatStr.lastIndexOf(';'); | |||
String zeroFormat = formatStr.substring(lastAt+1); | |||
String normalFormat = formatStr.substring(0,lastAt); | |||
if(cellValue == 0.0) { | |||
formatStr = zeroFormat; | |||
} else { | |||
formatStr = normalFormat; | |||
} | |||
private Format getFormat(double cellValue, int formatIndex, String formatStrIn) { | |||
String formatStr = formatStrIn; | |||
// Excel supports positive/negative/zero, but java | |||
// doesn't, so we need to do it specially | |||
final int firstAt = formatStr.indexOf(';'); | |||
final int lastAt = formatStr.lastIndexOf(';'); | |||
// p and p;n are ok by default. p;n;z and p;n;z;s need to be fixed. | |||
if (firstAt != -1 && firstAt != lastAt) { | |||
final int secondAt = formatStr.indexOf(';', firstAt + 1); | |||
if (secondAt == lastAt) { // p;n;z | |||
if (cellValue == 0.0) { | |||
formatStr = formatStr.substring(lastAt + 1); | |||
} else { | |||
formatStr = formatStr.substring(0, lastAt); | |||
} | |||
} else { | |||
if (cellValue == 0.0) { // p;n;z;s | |||
formatStr = formatStr.substring(secondAt + 1, lastAt); | |||
} else { | |||
formatStr = formatStr.substring(0, secondAt); | |||
} | |||
} | |||
} | |||
// 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 | |||
// See if we already have it cached | |||
Format format = formats.get(formatStr); | |||
if (format != null) { | |||
return format; | |||
} | |||
if ("General".equalsIgnoreCase(formatStr) || "@".equals(formatStr)) { | |||
if (DataFormatter.isWholeNumber(cellValue)) { | |||
if (isWholeNumber(cellValue)) { | |||
return generalWholeNumFormat; | |||
} | |||
return generalDecimalNumFormat; | |||
@@ -261,7 +328,6 @@ public class DataFormatter { | |||
return getDefaultFormat(cellValue); | |||
} | |||
if(DateUtil.isADateFormat(formatIndex,formatStr) && | |||
DateUtil.isValidExcelDate(cellValue)) { | |||
return createDateFormat(formatStr, cellValue); | |||
@@ -269,6 +335,10 @@ public class DataFormatter { | |||
if (numPattern.matcher(formatStr).find()) { | |||
return createNumberFormat(formatStr, cellValue); | |||
} | |||
if (emulateCsv) { | |||
return new ConstantStringFormat(cleanFormatForNumber(formatStr)); | |||
} | |||
// TODO - when does this occur? | |||
return null; | |||
} | |||
@@ -311,9 +381,33 @@ public class DataFormatter { | |||
char[] chars = formatStr.toCharArray(); | |||
boolean mIsMonth = true; | |||
List<Integer> ms = new ArrayList<Integer>(); | |||
boolean isElapsed = false; | |||
for(int j=0; j<chars.length; j++) { | |||
char c = chars[j]; | |||
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'); | |||
@@ -373,32 +467,61 @@ public class DataFormatter { | |||
} | |||
private Format createNumberFormat(String formatStr, double cellValue) { | |||
private String cleanFormatForNumber(String formatStr) { | |||
StringBuffer sb = new StringBuffer(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; | |||
} else { | |||
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); | |||
} | |||
if (emulateCsv) { | |||
// Requested spacers with "_" are replaced by a single space. | |||
// Full-column-width padding "*" are removed. | |||
// Not processing fractions at this time. Replace ? with space. | |||
// This matches CSV output. | |||
for (int i = 0; i < sb.length(); i++) { | |||
char c = sb.charAt(i); | |||
if (c == '_' || c == '*' || c == '?') { | |||
if (i > 0 && sb.charAt((i - 1)) == '\\') { | |||
// It's escaped, don't worry | |||
continue; | |||
} | |||
if (c == '?') { | |||
sb.setCharAt(i, ' '); | |||
} else if (i < sb.length() - 1) { | |||
// Remove the character we're supposed | |||
// to match the space of / pad to the | |||
// column width with | |||
if (c == '_') { | |||
sb.setCharAt(i + 1, ' '); | |||
} else { | |||
sb.deleteCharAt(i + 1); | |||
} | |||
// Remove the character too | |||
sb.deleteCharAt(i); | |||
} | |||
} | |||
} | |||
} else { | |||
// 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); | |||
} | |||
} | |||
} | |||
// Now, handle the other aspects like | |||
// quoting and scientific notation | |||
for(int i = 0; i < sb.length(); i++) { | |||
@@ -415,8 +538,14 @@ public class DataFormatter { | |||
} | |||
} | |||
return sb.toString(); | |||
} | |||
private Format createNumberFormat(String formatStr, double cellValue) { | |||
final String format = cleanFormatForNumber(formatStr); | |||
try { | |||
DecimalFormat df = new DecimalFormat(sb.toString(), decimalSymbols); | |||
DecimalFormat df = new DecimalFormat(format, decimalSymbols); | |||
setExcelStyleRoundingMode(df); | |||
return df; | |||
} catch(IllegalArgumentException iae) { | |||
@@ -522,22 +651,32 @@ public class DataFormatter { | |||
*/ | |||
public String formatRawCellContents(double value, int formatIndex, String formatString, boolean use1904Windowing) { | |||
// Is it a date? | |||
if(DateUtil.isADateFormat(formatIndex,formatString) && | |||
DateUtil.isValidExcelDate(value)) { | |||
Format dateFormat = getFormat(value, formatIndex, formatString); | |||
if(dateFormat instanceof ExcelStyleDateFormatter) { | |||
// Hint about the raw excel value | |||
((ExcelStyleDateFormatter)dateFormat).setDateToBeFormatted(value); | |||
if(DateUtil.isADateFormat(formatIndex,formatString)) { | |||
if(DateUtil.isValidExcelDate(value)) { | |||
Format dateFormat = getFormat(value, formatIndex, formatString); | |||
if(dateFormat instanceof ExcelStyleDateFormatter) { | |||
// Hint about the raw excel value | |||
((ExcelStyleDateFormatter)dateFormat).setDateToBeFormatted(value); | |||
} | |||
Date d = DateUtil.getJavaDate(value, use1904Windowing); | |||
return performDateFormatting(d, dateFormat); | |||
} | |||
Date d = DateUtil.getJavaDate(value, use1904Windowing); | |||
return performDateFormatting(d, dateFormat); | |||
// RK: Invalid dates are 255 #s. | |||
if (emulateCsv) { | |||
return invalidDateTimeString; | |||
} | |||
} | |||
// else Number | |||
Format numberFormat = getFormat(value, formatIndex, formatString); | |||
if (numberFormat == null) { | |||
return String.valueOf(value); | |||
} | |||
return numberFormat.format(new Double(value)); | |||
Format numberFormat = getFormat(value, formatIndex, formatString); | |||
if (numberFormat == null) { | |||
return String.valueOf(value); | |||
} | |||
// RK: This hack handles scientific notation by adding the missing + back. | |||
String result = numberFormat.format(new Double(value)); | |||
if (result.contains("E") && !result.contains("E-")) { | |||
result = result.replaceFirst("E", "E+"); | |||
} | |||
return result; | |||
} | |||
/** | |||
@@ -669,9 +808,21 @@ public class DataFormatter { | |||
* on Java 1.5. | |||
*/ | |||
public static void setExcelStyleRoundingMode(DecimalFormat format) { | |||
setExcelStyleRoundingMode(format, RoundingMode.HALF_UP); | |||
} | |||
/** | |||
* Enables custom rounding mode | |||
* on the Decimal Format if possible. | |||
* This will work for Java 1.6, but isn't possible | |||
* on Java 1.5. | |||
* @param format DecimalFormat | |||
* @param roundingMode RoundingMode | |||
*/ | |||
public static void setExcelStyleRoundingMode(DecimalFormat format, RoundingMode roundingMode) { | |||
try { | |||
Method srm = format.getClass().getMethod("setRoundingMode", RoundingMode.class); | |||
srm.invoke(format, RoundingMode.HALF_UP); | |||
srm.invoke(format, roundingMode); | |||
} catch(NoSuchMethodException e) { | |||
// Java 1.5 | |||
} catch(IllegalAccessException iae) { | |||
@@ -684,7 +835,7 @@ public class DataFormatter { | |||
// Not much we can do here | |||
} | |||
} | |||
/** | |||
* Format class for Excel's SSN format. This class mimics Excel's built-in | |||
* SSN formatting. | |||
@@ -794,4 +945,31 @@ public class DataFormatter { | |||
return df.parseObject(source, pos); | |||
} | |||
} | |||
/** | |||
* Format class that does nothing and always returns a constant string. | |||
* | |||
* This format is used to simulate Excel's handling of a format string | |||
* of all # when the value is 0. Excel will output "", Java will output "0". | |||
* | |||
* @see DataFormatter#createFormat(double, int, String) | |||
*/ | |||
@SuppressWarnings("serial") | |||
private static final class ConstantStringFormat extends Format { | |||
private static final DecimalFormat df = createIntegerOnlyFormat("##########"); | |||
private final String str; | |||
public ConstantStringFormat(String s) { | |||
str = s; | |||
} | |||
@Override | |||
public StringBuffer format(Object obj, StringBuffer toAppendTo, FieldPosition pos) { | |||
return toAppendTo.append(str); | |||
} | |||
@Override | |||
public Object parseObject(String source, ParsePosition pos) { | |||
return df.parseObject(source, pos); | |||
} | |||
} | |||
} |
@@ -52,7 +52,9 @@ public class DateUtil { | |||
*/ | |||
private static final Pattern date_ptrn1 = Pattern.compile("^\\[\\$\\-.*?\\]"); | |||
private static final Pattern date_ptrn2 = Pattern.compile("^\\[[a-zA-Z]+\\]"); | |||
private static final Pattern date_ptrn3 = Pattern.compile("^[yYmMdDhHsS\\-/,. :\"\\\\]+0?[ampAMP/]*$"); | |||
private static final Pattern date_ptrn3 = Pattern.compile("^[\\[\\]yYmMdDhHsS\\-/,. :\"\\\\]+0*[ampAMP/]*$"); | |||
// elapsed time patterns: [h],[m] and [s] | |||
private static final Pattern date_ptrn4 = Pattern.compile("^\\[([hH]+|[mM]+|[sS]+)\\]"); | |||
/** | |||
* Given a Date, converts it into a double representing its internal Excel representation, | |||
@@ -258,7 +260,12 @@ public class DateUtil { | |||
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 [$-...], then could be a date, but | |||
// who knows what that starting bit is all about | |||
fs = date_ptrn1.matcher(fs).replaceAll(""); |
@@ -16,130 +16,157 @@ | |||
==================================================================== */ | |||
package org.apache.poi.ss.usermodel; | |||
import java.util.regex.Pattern; | |||
import java.util.regex.Matcher; | |||
import java.util.*; | |||
import java.math.RoundingMode; | |||
import java.text.*; | |||
/** | |||
* A wrapper around a {@link SimpleDateFormat} instance, | |||
* which handles a few Excel-style extensions that | |||
* are not supported by {@link SimpleDateFormat}. | |||
* which handles a few Excel-style extensions that | |||
* are not supported by {@link SimpleDateFormat}. | |||
* Currently, the extensions are around the handling | |||
* of elapsed time, eg rendering 1 day 2 hours | |||
* as 26 hours. | |||
* of elapsed time, eg rendering 1 day 2 hours | |||
* as 26 hours. | |||
*/ | |||
public class ExcelStyleDateFormatter extends SimpleDateFormat { | |||
public static final char MMMMM_START_SYMBOL = '\ue001'; | |||
public static final char MMMMM_TRUNCATE_SYMBOL = '\ue002'; | |||
public static final char H_BRACKET_SYMBOL = '\ue010'; | |||
public static final char HH_BRACKET_SYMBOL = '\ue011'; | |||
public static final char M_BRACKET_SYMBOL = '\ue012'; | |||
public static final char MM_BRACKET_SYMBOL = '\ue013'; | |||
public static final char S_BRACKET_SYMBOL = '\ue014'; | |||
public static final char SS_BRACKET_SYMBOL = '\ue015'; | |||
private DecimalFormat format1digit = new DecimalFormat("0"); | |||
private DecimalFormat format2digits = new DecimalFormat("00"); | |||
{ | |||
DataFormatter.setExcelStyleRoundingMode(format1digit); | |||
DataFormatter.setExcelStyleRoundingMode(format2digits); | |||
} | |||
private double dateToBeFormatted = 0.0; | |||
public ExcelStyleDateFormatter() { | |||
super(); | |||
} | |||
public ExcelStyleDateFormatter(String pattern) { | |||
super(processFormatPattern(pattern)); | |||
} | |||
public ExcelStyleDateFormatter(String pattern, | |||
DateFormatSymbols formatSymbols) { | |||
super(processFormatPattern(pattern), formatSymbols); | |||
} | |||
public ExcelStyleDateFormatter(String pattern, Locale locale) { | |||
super(processFormatPattern(pattern), locale); | |||
} | |||
/** | |||
* Takes a format String, and replaces Excel specific bits | |||
* with our detection sequences | |||
*/ | |||
private static String processFormatPattern(String f) { | |||
f = f.replaceAll("MMMMM", MMMMM_START_SYMBOL + "MMM" + MMMMM_TRUNCATE_SYMBOL); | |||
f = f.replaceAll("\\[H\\]", String.valueOf(H_BRACKET_SYMBOL)); | |||
f = f.replaceAll("\\[HH\\]", String.valueOf(HH_BRACKET_SYMBOL)); | |||
f = f.replaceAll("\\[m\\]", String.valueOf(M_BRACKET_SYMBOL)); | |||
f = f.replaceAll("\\[mm\\]", String.valueOf(MM_BRACKET_SYMBOL)); | |||
f = f.replaceAll("\\[s\\]", String.valueOf(S_BRACKET_SYMBOL)); | |||
f = f.replaceAll("\\[ss\\]", String.valueOf(SS_BRACKET_SYMBOL)); | |||
return f; | |||
} | |||
/** | |||
* Used to let us know what the date being | |||
* formatted is, in Excel terms, which we | |||
* may wish to use when handling elapsed | |||
* times. | |||
*/ | |||
public void setDateToBeFormatted(double date) { | |||
this.dateToBeFormatted = date; | |||
} | |||
@Override | |||
public StringBuffer format(Date date, StringBuffer paramStringBuffer, | |||
FieldPosition paramFieldPosition) { | |||
// Do the normal format | |||
String s = super.format(date, paramStringBuffer, paramFieldPosition).toString(); | |||
// Now handle our special cases | |||
if(s.indexOf(MMMMM_START_SYMBOL) != -1) { | |||
s = s.replaceAll( | |||
MMMMM_START_SYMBOL + "(\\w)\\w+" + MMMMM_TRUNCATE_SYMBOL, | |||
"$1" | |||
); | |||
} | |||
if(s.indexOf(H_BRACKET_SYMBOL) != -1 || | |||
s.indexOf(HH_BRACKET_SYMBOL) != -1) { | |||
double hours = dateToBeFormatted * 24; | |||
s = s.replaceAll( | |||
String.valueOf(H_BRACKET_SYMBOL), | |||
format1digit.format(hours) | |||
); | |||
s = s.replaceAll( | |||
String.valueOf(HH_BRACKET_SYMBOL), | |||
format2digits.format(hours) | |||
); | |||
} | |||
if(s.indexOf(M_BRACKET_SYMBOL) != -1 || | |||
s.indexOf(MM_BRACKET_SYMBOL) != -1) { | |||
double minutes = dateToBeFormatted * 24 * 60; | |||
s = s.replaceAll( | |||
String.valueOf(M_BRACKET_SYMBOL), | |||
format1digit.format(minutes) | |||
); | |||
s = s.replaceAll( | |||
String.valueOf(MM_BRACKET_SYMBOL), | |||
format2digits.format(minutes) | |||
); | |||
} | |||
if(s.indexOf(S_BRACKET_SYMBOL) != -1 || | |||
s.indexOf(SS_BRACKET_SYMBOL) != -1) { | |||
double seconds = dateToBeFormatted * 24 * 60 * 60; | |||
s = s.replaceAll( | |||
String.valueOf(S_BRACKET_SYMBOL), | |||
format1digit.format(seconds) | |||
); | |||
s = s.replaceAll( | |||
String.valueOf(SS_BRACKET_SYMBOL), | |||
format2digits.format(seconds) | |||
); | |||
} | |||
return new StringBuffer(s); | |||
} | |||
public static final char MMMMM_START_SYMBOL = '\ue001'; | |||
public static final char MMMMM_TRUNCATE_SYMBOL = '\ue002'; | |||
public static final char H_BRACKET_SYMBOL = '\ue010'; | |||
public static final char HH_BRACKET_SYMBOL = '\ue011'; | |||
public static final char M_BRACKET_SYMBOL = '\ue012'; | |||
public static final char MM_BRACKET_SYMBOL = '\ue013'; | |||
public static final char S_BRACKET_SYMBOL = '\ue014'; | |||
public static final char SS_BRACKET_SYMBOL = '\ue015'; | |||
public static final char L_BRACKET_SYMBOL = '\ue016'; | |||
public static final char LL_BRACKET_SYMBOL = '\ue017'; | |||
private DecimalFormat format1digit = new DecimalFormat("0"); | |||
private DecimalFormat format2digits = new DecimalFormat("00"); | |||
private DecimalFormat format3digit = new DecimalFormat("0"); | |||
private DecimalFormat format4digits = new DecimalFormat("00"); | |||
{ | |||
DataFormatter.setExcelStyleRoundingMode(format1digit, RoundingMode.DOWN); | |||
DataFormatter.setExcelStyleRoundingMode(format2digits, RoundingMode.DOWN); | |||
DataFormatter.setExcelStyleRoundingMode(format3digit); | |||
DataFormatter.setExcelStyleRoundingMode(format4digits); | |||
} | |||
private double dateToBeFormatted = 0.0; | |||
public ExcelStyleDateFormatter() { | |||
super(); | |||
} | |||
public ExcelStyleDateFormatter(String pattern) { | |||
super(processFormatPattern(pattern)); | |||
} | |||
public ExcelStyleDateFormatter(String pattern, | |||
DateFormatSymbols formatSymbols) { | |||
super(processFormatPattern(pattern), formatSymbols); | |||
} | |||
public ExcelStyleDateFormatter(String pattern, Locale locale) { | |||
super(processFormatPattern(pattern), locale); | |||
} | |||
/** | |||
* Takes a format String, and replaces Excel specific bits | |||
* with our detection sequences | |||
*/ | |||
private static String processFormatPattern(String f) { | |||
String t = f.replaceAll("MMMMM", MMMMM_START_SYMBOL + "MMM" + MMMMM_TRUNCATE_SYMBOL); | |||
t = t.replaceAll("\\[H\\]", String.valueOf(H_BRACKET_SYMBOL)); | |||
t = t.replaceAll("\\[HH\\]", String.valueOf(HH_BRACKET_SYMBOL)); | |||
t = t.replaceAll("\\[m\\]", String.valueOf(M_BRACKET_SYMBOL)); | |||
t = t.replaceAll("\\[mm\\]", String.valueOf(MM_BRACKET_SYMBOL)); | |||
t = t.replaceAll("\\[s\\]", String.valueOf(S_BRACKET_SYMBOL)); | |||
t = t.replaceAll("\\[ss\\]", String.valueOf(SS_BRACKET_SYMBOL)); | |||
t = t.replaceAll("s.000", "s.S"); | |||
t = t.replaceAll("s.00", "s." + LL_BRACKET_SYMBOL); | |||
t = t.replaceAll("s.0", "s." + L_BRACKET_SYMBOL); | |||
return t; | |||
} | |||
/** | |||
* Used to let us know what the date being | |||
* formatted is, in Excel terms, which we | |||
* may wish to use when handling elapsed | |||
* times. | |||
*/ | |||
public void setDateToBeFormatted(double date) { | |||
this.dateToBeFormatted = date; | |||
} | |||
@Override | |||
public StringBuffer format(Date date, StringBuffer paramStringBuffer, | |||
FieldPosition paramFieldPosition) { | |||
// Do the normal format | |||
String s = super.format(date, paramStringBuffer, paramFieldPosition).toString(); | |||
// Now handle our special cases | |||
if (s.indexOf(MMMMM_START_SYMBOL) != -1) { | |||
s = s.replaceAll( | |||
MMMMM_START_SYMBOL + "(\\w)\\w+" + MMMMM_TRUNCATE_SYMBOL, | |||
"$1" | |||
); | |||
} | |||
if (s.indexOf(H_BRACKET_SYMBOL) != -1 || | |||
s.indexOf(HH_BRACKET_SYMBOL) != -1) { | |||
float hours = (float) dateToBeFormatted * 24; | |||
s = s.replaceAll( | |||
String.valueOf(H_BRACKET_SYMBOL), | |||
format1digit.format(hours) | |||
); | |||
s = s.replaceAll( | |||
String.valueOf(HH_BRACKET_SYMBOL), | |||
format2digits.format(hours) | |||
); | |||
} | |||
if (s.indexOf(M_BRACKET_SYMBOL) != -1 || | |||
s.indexOf(MM_BRACKET_SYMBOL) != -1) { | |||
float minutes = (float) dateToBeFormatted * 24 * 60; | |||
s = s.replaceAll( | |||
String.valueOf(M_BRACKET_SYMBOL), | |||
format1digit.format(minutes) | |||
); | |||
s = s.replaceAll( | |||
String.valueOf(MM_BRACKET_SYMBOL), | |||
format2digits.format(minutes) | |||
); | |||
} | |||
if (s.indexOf(S_BRACKET_SYMBOL) != -1 || | |||
s.indexOf(SS_BRACKET_SYMBOL) != -1) { | |||
float seconds = (float) (dateToBeFormatted * 24.0 * 60.0 * 60.0); | |||
s = s.replaceAll( | |||
String.valueOf(S_BRACKET_SYMBOL), | |||
format1digit.format(seconds) | |||
); | |||
s = s.replaceAll( | |||
String.valueOf(SS_BRACKET_SYMBOL), | |||
format2digits.format(seconds) | |||
); | |||
} | |||
if (s.indexOf(L_BRACKET_SYMBOL) != -1 || | |||
s.indexOf(LL_BRACKET_SYMBOL) != -1) { | |||
float millisTemp = (float) ((dateToBeFormatted - Math.floor(dateToBeFormatted)) * 24.0 * 60.0 * 60.0); | |||
float millis = (millisTemp - (int) millisTemp); | |||
s = s.replaceAll( | |||
String.valueOf(L_BRACKET_SYMBOL), | |||
format3digit.format(millis * 10) | |||
); | |||
s = s.replaceAll( | |||
String.valueOf(LL_BRACKET_SYMBOL), | |||
format4digits.format(millis * 100) | |||
); | |||
} | |||
return new StringBuffer(s); | |||
} | |||
} |
@@ -271,7 +271,8 @@ public final class TestHSSFDateUtil extends TestCase { | |||
"mm/dd HH:MM PM", "mm/dd HH:MM pm", | |||
"m/d/yy h:mm AM/PM", | |||
"hh:mm:ss", "hh:mm:ss.0", "mm:ss.0", | |||
//support elapsed time [h],[m],[s] | |||
"[hh]", "[mm]", "[ss]", "[SS]", "[red][hh]" | |||
}; | |||
for(int i=0; i<formats.length; i++) { | |||
assertTrue( | |||
@@ -286,6 +287,7 @@ public final class TestHSSFDateUtil extends TestCase { | |||
"0.0", "0.000", | |||
"0%", "0.0%", | |||
"[]Foo", "[BLACK]0.00%", | |||
"[ms]", "[Mh]", | |||
"", null | |||
}; | |||
for(int i=0; i<formats.length; i++) { |
@@ -27,7 +27,7 @@ import org.apache.poi.hssf.usermodel.TestHSSFDataFormatter; | |||
/** | |||
* Tests of {@link DataFormatter} | |||
* | |||
* See {@link TestHSSFDataFormatter} too for | |||
* See {@link TestHSSFDataFormatter} too for | |||
* more tests. | |||
*/ | |||
public class TestDataFormatter extends TestCase { | |||
@@ -179,6 +179,38 @@ public class TestDataFormatter extends TestCase { | |||
assertEquals("12.34", dfUS.formatRawCellContents(12.343, -1, "*-##.##")); | |||
} | |||
/** | |||
* DataFormatter is the CSV mode preserves spaces | |||
*/ | |||
public void testPaddingSpacesCSV() { | |||
DataFormatter dfUS = new DataFormatter(Locale.US, true); | |||
assertEquals("12.34 ", dfUS.formatRawCellContents(12.343, -1, "##.##_ ")); | |||
assertEquals("-12.34 ", dfUS.formatRawCellContents(-12.343, -1, "##.##_ ")); | |||
assertEquals(". ", dfUS.formatRawCellContents(0.0, -1, "##.##_ ")); | |||
assertEquals("12.34 ", dfUS.formatRawCellContents(12.343, -1, "##.##_1")); | |||
assertEquals("-12.34 ", dfUS.formatRawCellContents(-12.343, -1, "##.##_1")); | |||
assertEquals(". ", dfUS.formatRawCellContents(0.0, -1, "##.##_1")); | |||
assertEquals("12.34 ", dfUS.formatRawCellContents(12.343, -1, "##.##_)")); | |||
assertEquals("-12.34 ", dfUS.formatRawCellContents(-12.343, -1, "##.##_)")); | |||
assertEquals(". ", dfUS.formatRawCellContents(0.0, -1, "##.##_)")); | |||
assertEquals(" 12.34", dfUS.formatRawCellContents(12.343, -1, "_-##.##")); | |||
assertEquals("- 12.34", dfUS.formatRawCellContents(-12.343, -1, "_-##.##")); | |||
assertEquals(" .", dfUS.formatRawCellContents(0.0, -1, "_-##.##")); | |||
assertEquals("12.34", dfUS.formatRawCellContents(12.343, -1, "##.##* ")); | |||
assertEquals("-12.34", dfUS.formatRawCellContents(-12.343, -1, "##.##* ")); | |||
assertEquals(".", dfUS.formatRawCellContents(0.0, -1, "##.##* ")); | |||
assertEquals("12.34", dfUS.formatRawCellContents(12.343, -1, "##.##*1")); | |||
assertEquals("-12.34", dfUS.formatRawCellContents(-12.343, -1, "##.##*1")); | |||
assertEquals(".", dfUS.formatRawCellContents(0.0, -1, "##.##*1")); | |||
assertEquals("12.34", dfUS.formatRawCellContents(12.343, -1, "##.##*)")); | |||
assertEquals("-12.34", dfUS.formatRawCellContents(-12.343, -1, "##.##*)")); | |||
assertEquals(".", dfUS.formatRawCellContents(0.0, -1, "##.##*)")); | |||
assertEquals("12.34", dfUS.formatRawCellContents(12.343, -1, "*-##.##")); | |||
assertEquals("-12.34", dfUS.formatRawCellContents(-12.343, -1, "*-##.##")); | |||
assertEquals(".", dfUS.formatRawCellContents(0.0, -1, "*-##.##")); | |||
} | |||
/** | |||
* Test that the special Excel month format MMMMM | |||
* gets turned into the first letter of the month | |||
@@ -196,14 +228,14 @@ public class TestDataFormatter extends TestCase { | |||
} | |||
/** | |||
* Test that we can handle elapsed time, | |||
* Test that we can handle elapsed time, | |||
* eg formatting 1 day 4 hours as 28 hours | |||
*/ | |||
public void testElapsedTime() { | |||
DataFormatter dfUS = new DataFormatter(Locale.US); | |||
double hour = 1.0/24.0; | |||
assertEquals("01:00", dfUS.formatRawCellContents(1*hour, -1, "hh:mm")); | |||
assertEquals("05:00", dfUS.formatRawCellContents(5*hour, -1, "hh:mm")); | |||
assertEquals("20:00", dfUS.formatRawCellContents(20*hour, -1, "hh:mm")); | |||
@@ -212,7 +244,7 @@ public class TestDataFormatter extends TestCase { | |||
assertEquals("02:00", dfUS.formatRawCellContents(26*hour, -1, "hh:mm")); | |||
assertEquals("20:00", dfUS.formatRawCellContents(44*hour, -1, "hh:mm")); | |||
assertEquals("02:00", dfUS.formatRawCellContents(50*hour, -1, "hh:mm")); | |||
assertEquals("01:00", dfUS.formatRawCellContents(1*hour, -1, "[hh]:mm")); | |||
assertEquals("05:00", dfUS.formatRawCellContents(5*hour, -1, "[hh]:mm")); | |||
assertEquals("20:00", dfUS.formatRawCellContents(20*hour, -1, "[hh]:mm")); | |||
@@ -221,12 +253,73 @@ public class TestDataFormatter extends TestCase { | |||
assertEquals("26:00", dfUS.formatRawCellContents(26*hour, -1, "[hh]:mm")); | |||
assertEquals("44:00", dfUS.formatRawCellContents(44*hour, -1, "[hh]:mm")); | |||
assertEquals("50:00", dfUS.formatRawCellContents(50*hour, -1, "[hh]:mm")); | |||
assertEquals("30:00", dfUS.formatRawCellContents(0.5*hour, -1, "[mm]:ss")); | |||
assertEquals("60:00", dfUS.formatRawCellContents(1*hour, -1, "[mm]:ss")); | |||
assertEquals("120:00", dfUS.formatRawCellContents(2*hour, -1, "[mm]:ss")); | |||
assertEquals("01", dfUS.formatRawCellContents(1*hour, -1, "[hh]")); | |||
assertEquals("05", dfUS.formatRawCellContents(5*hour, -1, "[hh]")); | |||
assertEquals("20", dfUS.formatRawCellContents(20*hour, -1, "[hh]")); | |||
assertEquals("23", dfUS.formatRawCellContents(23*hour, -1, "[hh]")); | |||
assertEquals("24", dfUS.formatRawCellContents(24*hour, -1, "[hh]")); | |||
assertEquals("26", dfUS.formatRawCellContents(26*hour, -1, "[hh]")); | |||
assertEquals("44", dfUS.formatRawCellContents(44*hour, -1, "[hh]")); | |||
assertEquals("50", dfUS.formatRawCellContents(50*hour, -1, "[hh]")); | |||
double minute = 1.0/24.0/60.0; | |||
assertEquals("01:00", dfUS.formatRawCellContents(1*minute, -1, "[mm]:ss")); | |||
assertEquals("05:00", dfUS.formatRawCellContents(5*minute, -1, "[mm]:ss")); | |||
assertEquals("20:00", dfUS.formatRawCellContents(20*minute, -1, "[mm]:ss")); | |||
assertEquals("23:00", dfUS.formatRawCellContents(23*minute, -1, "[mm]:ss")); | |||
assertEquals("24:00", dfUS.formatRawCellContents(24*minute, -1, "[mm]:ss")); | |||
assertEquals("26:00", dfUS.formatRawCellContents(26*minute, -1, "[mm]:ss")); | |||
assertEquals("44:00", dfUS.formatRawCellContents(44*minute, -1, "[mm]:ss")); | |||
assertEquals("50:00", dfUS.formatRawCellContents(50*minute, -1, "[mm]:ss")); | |||
assertEquals("59:00", dfUS.formatRawCellContents(59*minute, -1, "[mm]:ss")); | |||
assertEquals("60:00", dfUS.formatRawCellContents(60*minute, -1, "[mm]:ss")); | |||
assertEquals("61:00", dfUS.formatRawCellContents(61*minute, -1, "[mm]:ss")); | |||
assertEquals("119:00", dfUS.formatRawCellContents(119*minute, -1, "[mm]:ss")); | |||
assertEquals("120:00", dfUS.formatRawCellContents(120*minute, -1, "[mm]:ss")); | |||
assertEquals("121:00", dfUS.formatRawCellContents(121*minute, -1, "[mm]:ss")); | |||
assertEquals("01", dfUS.formatRawCellContents(1*minute, -1, "[mm]")); | |||
assertEquals("05", dfUS.formatRawCellContents(5*minute, -1, "[mm]")); | |||
assertEquals("20", dfUS.formatRawCellContents(20*minute, -1, "[mm]")); | |||
assertEquals("23", dfUS.formatRawCellContents(23*minute, -1, "[mm]")); | |||
assertEquals("24", dfUS.formatRawCellContents(24*minute, -1, "[mm]")); | |||
assertEquals("26", dfUS.formatRawCellContents(26*minute, -1, "[mm]")); | |||
assertEquals("44", dfUS.formatRawCellContents(44*minute, -1, "[mm]")); | |||
assertEquals("50", dfUS.formatRawCellContents(50*minute, -1, "[mm]")); | |||
assertEquals("59", dfUS.formatRawCellContents(59*minute, -1, "[mm]")); | |||
assertEquals("60", dfUS.formatRawCellContents(60*minute, -1, "[mm]")); | |||
assertEquals("61", dfUS.formatRawCellContents(61*minute, -1, "[mm]")); | |||
assertEquals("119", dfUS.formatRawCellContents(119*minute, -1, "[mm]")); | |||
assertEquals("120", dfUS.formatRawCellContents(120*minute, -1, "[mm]")); | |||
assertEquals("121", dfUS.formatRawCellContents(121*minute, -1, "[mm]")); | |||
double second = 1.0/24.0/60.0/60.0; | |||
assertEquals("86400", dfUS.formatRawCellContents(86400*second, -1, "[ss]")); | |||
assertEquals("01", dfUS.formatRawCellContents(1*second, -1, "[ss]")); | |||
assertEquals("05", dfUS.formatRawCellContents(5*second, -1, "[ss]")); | |||
assertEquals("20", dfUS.formatRawCellContents(20*second, -1, "[ss]")); | |||
assertEquals("23", dfUS.formatRawCellContents(23*second, -1, "[ss]")); | |||
assertEquals("24", dfUS.formatRawCellContents(24*second, -1, "[ss]")); | |||
assertEquals("26", dfUS.formatRawCellContents(26*second, -1, "[ss]")); | |||
assertEquals("44", dfUS.formatRawCellContents(44*second, -1, "[ss]")); | |||
assertEquals("50", dfUS.formatRawCellContents(50*second, -1, "[ss]")); | |||
assertEquals("59", dfUS.formatRawCellContents(59*second, -1, "[ss]")); | |||
assertEquals("60", dfUS.formatRawCellContents(60*second, -1, "[ss]")); | |||
assertEquals("61", dfUS.formatRawCellContents(61*second, -1, "[ss]")); | |||
assertEquals("119", dfUS.formatRawCellContents(119*second, -1, "[ss]")); | |||
assertEquals("120", dfUS.formatRawCellContents(120*second, -1, "[ss]")); | |||
assertEquals("121", dfUS.formatRawCellContents(121*second, -1, "[ss]")); | |||
assertEquals("28:48:00",dfUS.formatRawCellContents(1.2, -1, "[h]:mm:ss")); | |||
assertEquals("57:07.2", dfUS.formatRawCellContents(.123, -1, "mm:ss.0;@")); | |||
assertEquals("57:41.8", dfUS.formatRawCellContents(.1234, -1, "mm:ss.0;@")); | |||
assertEquals("57:41.76", dfUS.formatRawCellContents(.1234, -1, "mm:ss.00;@")); | |||
assertEquals("57:41.760", dfUS.formatRawCellContents(.1234, -1, "mm:ss.000;@")); | |||
assertEquals("24:00.0", dfUS.formatRawCellContents(123456.6, -1, "mm:ss.0")); | |||
} | |||
public void testDateWindowing() { | |||
DataFormatter dfUS = new DataFormatter(Locale.US); | |||
@@ -234,4 +327,31 @@ public class TestDataFormatter extends TestCase { | |||
assertEquals("1899-12-31 00:00:00", dfUS.formatRawCellContents(0.0, -1, "yyyy-mm-dd hh:mm:ss", false)); | |||
assertEquals("1904-01-01 00:00:00", dfUS.formatRawCellContents(0.0, -1, "yyyy-mm-dd hh:mm:ss", true)); | |||
} | |||
public void testScientificNotation() { | |||
DataFormatter dfUS = new DataFormatter(Locale.US); | |||
assertEquals("1.23E+01", dfUS.formatRawCellContents(12.343, -1, "0.00E+00")); | |||
assertEquals("-1.23E+01", dfUS.formatRawCellContents(-12.343, -1, "0.00E+00")); | |||
assertEquals("0.00E+00", dfUS.formatRawCellContents(0.0, -1, "0.00E+00")); | |||
} | |||
public void testInvalidDate() { | |||
DataFormatter df1 = new DataFormatter(Locale.US); | |||
assertEquals("-1.0", df1.formatRawCellContents(-1, -1, "mm/dd/yyyy")); | |||
DataFormatter df2 = new DataFormatter(Locale.US, true); | |||
assertEquals("###############################################################################################################################################################################################################################################################", | |||
df2.formatRawCellContents(-1, -1, "mm/dd/yyyy")); | |||
} | |||
public void testOther() { | |||
DataFormatter dfUS = new DataFormatter(Locale.US, true); | |||
assertEquals(" 12.34 ", dfUS.formatRawCellContents(12.34, -1, "_-* #,##0.00_-;-* #,##0.00_-;_-* \"-\"??_-;_-@_-")); | |||
assertEquals("-12.34 ", dfUS.formatRawCellContents(-12.34, -1, "_-* #,##0.00_-;-* #,##0.00_-;_-* \"-\"??_-;_-@_-")); | |||
assertEquals(" - ", dfUS.formatRawCellContents(0.0, -1, "_-* #,##0.00_-;-* #,##0.00_-;_-* \"-\"??_-;_-@_-")); | |||
assertEquals(" $- ", dfUS.formatRawCellContents(0.0, -1, "_-$* #,##0.00_-;-$* #,##0.00_-;_-$* \"-\"??_-;_-@_-")); | |||
} | |||
} |