From b3f68c4eb4f81cfda99bc449b922c12abf348ade Mon Sep 17 00:00:00 2001 From: Nick Burch Date: Sat, 24 Oct 2015 23:34:47 +0000 Subject: [PATCH] #58532 For Excel cell formats with 3+ parts to them (eg +ve,-ve,0), which DataFormatter didn't properly support, call out to the alternate CellFormat instead for the formatting. This also allows us to enable some disabled parts of DataFormatter unit tests We still need to rationalise DataFormatter and CellFormatter though, so we only have one set of cell formatting logic... git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1710399 13f79535-47bb-0310-9956-ffa450edef68 --- .../poi/ss/usermodel/DataFormatter.java | 65 ++++++++++++------ .../xssf/usermodel/TestXSSFDataFormat.java | 10 ++- .../hssf/usermodel/TestHSSFDataFormat.java | 8 +++ .../hssf/usermodel/TestHSSFDataFormatter.java | 10 +-- .../poi/ss/usermodel/BaseTestDataFormat.java | 44 ++++++++++++ .../poi/ss/usermodel/TestDataFormatter.java | 27 ++++---- test-data/spreadsheet/FormatKM.xls | Bin 33280 -> 33280 bytes test-data/spreadsheet/FormatKM.xlsx | Bin 9019 -> 11590 bytes 8 files changed, 127 insertions(+), 37 deletions(-) diff --git a/src/java/org/apache/poi/ss/usermodel/DataFormatter.java b/src/java/org/apache/poi/ss/usermodel/DataFormatter.java index 2f414a1d9e..add3be8cef 100644 --- a/src/java/org/apache/poi/ss/usermodel/DataFormatter.java +++ b/src/java/org/apache/poi/ss/usermodel/DataFormatter.java @@ -40,8 +40,12 @@ import java.util.Observer; import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.apache.poi.ss.format.CellFormat; +import org.apache.poi.ss.format.CellFormatResult; import org.apache.poi.ss.util.NumberToTextConverter; import org.apache.poi.util.LocaleUtil; +import org.apache.poi.util.POILogFactory; +import org.apache.poi.util.POILogger; /** @@ -197,6 +201,9 @@ public class DataFormatter implements Observer { /** the Observable to notify, when the locale has been changed */ private final LocaleChangeObservable localeChangedObervable = new LocaleChangeObservable(); + /** For logging any problems we find */ + private static POILogger logger = POILogFactory.getLogger(DataFormatter.class); + /** * Creates a formatter using the {@link Locale#getDefault() default locale}. */ @@ -270,28 +277,30 @@ public class DataFormatter implements Observer { // String formatStr = (i < formatBits.length) ? formatBits[i] : formatBits[0]; 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 supports 3+ 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 3+ parts, we call out to CellFormat to handle it + // TODO Going forward, we should really merge the logic between the two classes + if (formatStr.indexOf(";") != -1 && + formatStr.indexOf(';') != formatStr.lastIndexOf(';')) { + try { + // Ask CellFormat to get a formatter for it + CellFormat cfmt = CellFormat.getInstance(formatStr); + // CellFormat requires callers to identify date vs not, so do so + Object cellValueO = Double.valueOf(cellValue); + if (DateUtil.isADateFormat(formatIndex, formatStr)) { + cellValueO = DateUtil.getJavaDate(cellValue); } + // Wrap and return (non-cachable - CellFormat does that) + return new CellFormatResultWrapper( cfmt.apply(cellValueO) ); + } catch (Exception e) { + logger.log(POILogger.WARN, "Formatting failed as " + 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("#", ""); @@ -310,7 +319,6 @@ public class DataFormatter implements Observer { // Build a formatter, and cache it format = createFormat(cellValue, formatIndex, formatStr); - formats.put(formatStr, format); return format; } @@ -1116,4 +1124,21 @@ public class DataFormatter implements Observer { return df.parseObject(source, pos); } } + /** + * Workaround until we merge {@link DataFormatter} with {@link CellFormat}. + * Constant, non-cachable wrapper around a {@link CellFormatResult} + */ + @SuppressWarnings("serial") + private static final class CellFormatResultWrapper extends Format { + private final CellFormatResult result; + private CellFormatResultWrapper(CellFormatResult result) { + this.result = result; + } + public StringBuffer format(Object obj, StringBuffer toAppendTo, FieldPosition pos) { + return toAppendTo.append(result.text); + } + public Object parseObject(String source, ParsePosition pos) { + return null; // Not supported + } + } } diff --git a/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFDataFormat.java b/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFDataFormat.java index 32bcaef785..d7d77d8297 100644 --- a/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFDataFormat.java +++ b/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFDataFormat.java @@ -35,7 +35,7 @@ public final class TestXSSFDataFormat extends BaseTestDataFormat { /** * [Bug 49928] formatCellValue returns incorrect value for \u00a3 formatted cells */ - public void test49928(){ + public void test49928() { XSSFWorkbook wb = XSSFTestDataSamples.openSampleWorkbook("49928.xlsx"); doTest49928Core(wb); @@ -49,4 +49,12 @@ public final class TestXSSFDataFormat extends BaseTestDataFormat { assertTrue(customFmtIdx > BuiltinFormats.FIRST_USER_DEFINED_FORMAT_INDEX ); assertEquals("\u00a3##.00[Yellow]", dataFormat.getFormat(customFmtIdx)); } + + /** + * [Bug 58532] Handle formats that go numnum, numK, numM etc + */ + public void test58532() { + XSSFWorkbook wb = XSSFTestDataSamples.openSampleWorkbook("FormatKM.xlsx"); + doTest58532Core(wb); + } } diff --git a/src/testcases/org/apache/poi/hssf/usermodel/TestHSSFDataFormat.java b/src/testcases/org/apache/poi/hssf/usermodel/TestHSSFDataFormat.java index 8761ecf104..6b6fc81a5a 100644 --- a/src/testcases/org/apache/poi/hssf/usermodel/TestHSSFDataFormat.java +++ b/src/testcases/org/apache/poi/hssf/usermodel/TestHSSFDataFormat.java @@ -51,6 +51,14 @@ public final class TestHSSFDataFormat extends BaseTestDataFormat { assertEquals("\u00a3##.00[Yellow]", dataFormat.getFormat(customFmtIdx)); } + /** + * [Bug 58532] Handle formats that go numnum, numK, numM etc + */ + public void test58532() { + HSSFWorkbook wb = HSSFTestDataSamples.openSampleWorkbook("FormatKM.xls"); + doTest58532Core(wb); + } + /** * Bug 51378: getDataFormatString method call crashes when reading the test file */ diff --git a/src/testcases/org/apache/poi/hssf/usermodel/TestHSSFDataFormatter.java b/src/testcases/org/apache/poi/hssf/usermodel/TestHSSFDataFormatter.java index 85ceee2f9b..f42074e99d 100644 --- a/src/testcases/org/apache/poi/hssf/usermodel/TestHSSFDataFormatter.java +++ b/src/testcases/org/apache/poi/hssf/usermodel/TestHSSFDataFormatter.java @@ -265,8 +265,8 @@ public final class TestHSSFDataFormatter { assertTrue( ! "555.47431".equals(fmtval)); // check we found the time properly - assertTrue("Format came out incorrect - " + fmt + ": " + fmtval + ", but expected to find '11:23'", - fmtval.indexOf("11:23") > -1); + assertTrue("Format came out incorrect - " + fmt + " - found " + fmtval + + ", but expected to find '11:23'", fmtval.indexOf("11:23") > -1); } // test number formats @@ -358,8 +358,10 @@ public final class TestHSSFDataFormatter { Cell cell = it.next(); cell.setCellValue(cell.getNumericCellValue() * Math.random() / 1000000 - 1000); log(formatter.formatCellValue(cell)); - assertTrue(formatter.formatCellValue(cell).startsWith("Balance ")); - assertTrue(formatter.formatCellValue(cell).endsWith(" USD")); + + String formatted = formatter.formatCellValue(cell); + assertTrue("Doesn't start with Balance: " + formatted, formatted.startsWith("Balance ")); + assertTrue("Doesn't end with USD: " + formatted, formatted.endsWith(" USD")); } } diff --git a/src/testcases/org/apache/poi/ss/usermodel/BaseTestDataFormat.java b/src/testcases/org/apache/poi/ss/usermodel/BaseTestDataFormat.java index 79b14e9533..bbdc499359 100644 --- a/src/testcases/org/apache/poi/ss/usermodel/BaseTestDataFormat.java +++ b/src/testcases/org/apache/poi/ss/usermodel/BaseTestDataFormat.java @@ -87,4 +87,48 @@ public abstract class BaseTestDataFormat extends TestCase { assertEquals(poundFmtIdx, dataFormat.getFormat(poundFmt)); assertEquals(poundFmt, dataFormat.getFormat(poundFmtIdx)); } + + public abstract void test58532(); + public void doTest58532Core(Workbook wb) { + Sheet s = wb.getSheetAt(0); + DataFormatter fmt = new DataFormatter(); + FormulaEvaluator eval = wb.getCreationHelper().createFormulaEvaluator(); + + // Column A is the raw values + // Column B is the ##/#K/#M values + // Column C is strings of what they should look like + // Column D is the #.##/#.#K/#.#M values + // Column E is strings of what they should look like + + String formatKMWhole = "[>999999]#,,\"M\";[>999]#,\"K\";#"; + String formatKM3dp = "[>999999]#.000,,\"M\";[>999]#.000,\"K\";#.000"; + + // Check the formats are as expected + Row headers = s.getRow(0); + assertNotNull(headers); + assertEquals(formatKMWhole, headers.getCell(1).getStringCellValue()); + assertEquals(formatKM3dp, headers.getCell(3).getStringCellValue()); + + Row r2 = s.getRow(1); + assertNotNull(r2); + assertEquals(formatKMWhole, r2.getCell(1).getCellStyle().getDataFormatString()); + assertEquals(formatKM3dp, r2.getCell(3).getCellStyle().getDataFormatString()); + + // For all of the contents rows, check that DataFormatter is able + // to format the cells to the same value as the one next to it + for (int rn=1; rnEy>|O{2jo^+&5=cO{DGbwz}tHn!;|QV&v6h~PyKL1dNY z>Rg#~y?#G>2mF=EXzt;z8=kgTG(42>k+^YX=j zq)w2K%Nl8Trnba1s6AVxwyC6#Z~4$ z^9RhAn6EJJGJnbZE%Oh|zc43eMzM@!L1Dvm@s0g{_KNH|eC9O!*D_^%=C}44E!@s3 z{4&~zrru!M9OfA&F`E9$ej7`Mi?O`ItBi})T(7r{Gp52?Y63S+4|j7mETw{4v63EH zumnd;ROV)9uiGt=6hErlh|?o?@ZPG>PwclJqifrvaUpEWDe)Tb3#DDM9D7{f!j@%) zs^r)f_BgF#$0res39&;3V7n>&mI?ZJng@T zY>9}7hJtvPoFsZ=dkWFd9er`dU!z3BS=T%G?U^#+@wm^hT6;|)AOwr`n!G==rum2d Sj64t4XU6tQy&in`WAqmSvaPKE delta 601 zcmaiuKWGzi6vuz>moz5<4f83^jTCR{LxeY} zSzI)xu#r&lJMCgD?c>GpDyNF3;o}~A`&pOGRqV-Rp{G?G9rp3wRPi8{#gmaL-f1d6 z>Mo)KE^cTpjwD^|9&~Yx`5Uv(Tw`8g-kWgol6jr^E%SNiem>h~HhK0D^9=Jr!^J7) zpUi9e6uu0pftM+1V{4Y+h>22dVc~-9i$3wA@{i+eJ4O*U`PW?DsUL+NF%5@rmR;iPQOO-`R;4AASiYMx%<1yDkOjL_K{5}T>MJPBum%;67 zYJ8ulb?mSg39FDI8VcfF@*-?j)6~SuoI#Ob)H_QZke5CF+Wnw(a-Hz0u=C3N^DZ&^ S!Mzo{nxD+cUtX})h|wQfFt6zV diff --git a/test-data/spreadsheet/FormatKM.xlsx b/test-data/spreadsheet/FormatKM.xlsx index fe9c0891d7bd0942487213b61d912291d0490c5e..c37c76bd700c668e272807398a7b94a5f3dc35a3 100644 GIT binary patch delta 5499 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 Q5b1UQetoO5T)#&D1;(hBEdT%j delta 2909 zcmZXWc|6qH8^^!HWh{ee#y-}GtPQzHvXeb)mM&Qa*&A6x8cPj|CI-32*hP%BxppaI z8)__}hB9R7)-6NX68Wju@AdL~-QPKXoadbLIe$Fo^*rzA8F2436SlKr{S6AR0~`PV zNCP)Q@&!Xc08ql#BrL;%$?>qFAfZ?$+kr8)PJ=F&Hq_y90YhUazv3 zrNnP^ui0&*OUKE{Au`4{@_4+{a``1ypEHPruM`=0PJK-z=Wy;_Y;8wXj0S7 z?8=&1ZR6Xi^6`fT{#`6m;c;0&FUxoBf_D8un(V{@pUgQ724S%>1oHYr?CI%-XxX^u z?RlWdU`P%$0K8L^ke0GKj?l*CoCMi2ZhN4}Z``8W@$>Ak$D^!rtXy-P`zKirzkibT@d3L62!kIDq97py%#`kw@dR{sm|u=g zcZOlZrENgY{@qxgX5N;3xIW&v!+@$@?4?u1%}@-c+K`Lxp>}$yZ65Qno2L+CgwmL7Ynw;FL0h(^;H;*nMo~t>$A=c00;ObcK*k9oOT>|ZK&+yegRHaF7|@%yO}Hi zkO#(-1Vk`us@Fq9a&l+y%^;6qnM#{*(wp$Y-+C>c-Vv3hS=rcv zzb89!pOi8Q(f#4SA{noa(W4fM8;F(Oxh=GtbiQO>k-tBBVSo4A7mf9_CX%^pJ(uhC z0OZ`)FN>+)>^~5xfU=>Hx1gtg=$Z1#H3;JL6U9=@(_E)OsNUpmin#{YAKy%coGU!xq8M+;X zE-aXR!K)fVzCQF7@X}KigsVJGdtb})bK!#0517=16FV=p4F=wU#I6ppk`@>PBTw5) zh`15-Kg`M+x|WTAIbE-nF}2Q}7V1Ba&JfjB@TbP|V10SU{*vs!H|%_mWAB7R8|9X0 zvm=~j9)c}T($9FejxdSu-5yZfl?)wWRAdmhHUQ@fHyKL^%bERsdJb05Ra!nvzaVL7d+L5G8^`+XF@0-tvmjGEUGa|aLper91cn7%`x&$$xF%c5VbI1@JV}Yl&A0p%ZD0@*Q{9*R+RF%X zF#?hI?pi^o)7n;)th0FoVE%1JXw zlo42esBybiY{XBwrmc@RE^FN)I4sUOsa97ecf_U0;eN7XRcs-W{6^MNbc2>3Pc%Z< z*%Vobe=>Lh#hsR@|D+Cg9SYx`WtJnUnzldxxr=OvZ7k0ampE&VdS)vD0T zJX^8j#yU9%2`%y;16WNZsN`ni}UF)4Q0$u{L z4;_eT=f0gmw7#1U3hpcdb8iyXdj4*93A30w)M3_0`BNUD4?FYNd%?r#hC`(cVJm(`eU4!P^N%FdPH5J>7)7_);eH@foZ(y zwCnbHKaVfk%>sofl~a{xu~<(eLD+Nf>e9SV+HbBjH2x$0 z$a1x^bJPbC`q8v4-_ATd`Z{rOeeeo=i(~rk=0RgM4ZIgqXhv2x{c5^qWHJ4_lYpU8 zq-vZdS+}_(RO7NaaeE3l$TDw0h~6=M(&@AJVzekx?=xq)WB={Npb|_j28?{Xt9X5$ zG5hUIP_a)NTjZ##t{rl?S2b)vhkjx?TDq}JzKAFQd`eRz8pz?&0mpcfU_}4UcoA#HcmTZ zgKP-_XU`0s%@NYd&6>n~E2sO|(2K%ARcPIQ0rBNY`!|5`+vDRRR5#ot*s%qmA#UJ+AJURQPHuBtA%d`Oo1HMPr+crs+M$TlJ5B(eYDX4AL~0dJJimKFI4|9n-30H z0Xr)Yi^?Iz{=Y-SyDQj(D)A%*39+NT1pq`3A0CYn{IbG%5D#8OQ5Tet4^osnEak@_ zNB185_(lOyypWP2%U6Lz6NYzHf{7nhc>oYT5+7RW&|jjWL(#t}d!(}#!gDFhiT@i; zM*