From: Ugo Cei Date: Sat, 8 Mar 2008 11:49:00 +0000 (+0000) Subject: Merged revisions 627779-634630 via svnmerge from X-Git-Tag: REL_3_5_BETA2~205 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=913a1ad96e6029a1155b3715a47d398f0f37529c;p=poi.git Merged revisions 627779-634630 via svnmerge from https://svn.apache.org/repos/asf/poi/trunk ........ r627779 | nick | 2008-02-14 16:32:49 +0100 (Thu, 14 Feb 2008) | 1 line In the interests of sanity, stop having hssf test data files in scratchpad and main, go to just having them in main ........ r627788 | nick | 2008-02-14 17:01:10 +0100 (Thu, 14 Feb 2008) | 1 line Big formula update from Josh from bug #44364 - support for Match, NA and SumProduct functions, and initial error support in functions ........ r627999 | nick | 2008-02-15 11:30:10 +0100 (Fri, 15 Feb 2008) | 1 line To avoid confusion and repeated changes in svn, update the TestDataValidation test to output its file (that needs opening in excel to check to output) into the system tmp directory ........ r628027 | nick | 2008-02-15 12:45:13 +0100 (Fri, 15 Feb 2008) | 1 line Fix for bug #44403 - Have mid use the third argument properly, and test ........ r628029 | nick | 2008-02-15 12:53:25 +0100 (Fri, 15 Feb 2008) | 1 line Fix for bug #44413 from Josh - Fix for circular references in INDEX, OFFSET, VLOOKUP formulas, where a cell is actually allowed to reference itself ........ r628033 | nick | 2008-02-15 13:04:42 +0100 (Fri, 15 Feb 2008) | 1 line Fix from Josh from bug #44417 - Improved handling of references for the need to quote the sheet name for some formulas, but not when fetching a sheet by name ........ r628035 | nick | 2008-02-15 13:13:25 +0100 (Fri, 15 Feb 2008) | 1 line Fix from Josh from bug #44421 - Update Match function to properly support Area references ........ r628044 | nick | 2008-02-15 13:59:40 +0100 (Fri, 15 Feb 2008) | 1 line Partial fix for bug #44410 - support whole column ranges such as C:C in the formula evaluator (so SUM(D:D) will now work). However, the formula string will still be displayed wrong ........ r628065 | nick | 2008-02-15 14:50:38 +0100 (Fri, 15 Feb 2008) | 1 line Further support for whole-column references, including formula strings and the evaluator. Also has some new tests for it ........ r628714 | nick | 2008-02-18 14:08:16 +0100 (Mon, 18 Feb 2008) | 1 line Update notice for latest guidance on ooxml xsd licence, and update getting involved to link to the newly released binary file format docs ........ r629552 | nick | 2008-02-20 19:14:30 +0100 (Wed, 20 Feb 2008) | 1 line Patch from Josh from bug #44403 - Further support for unusual, but valid, arguments to the Mid function ........ r629738 | nick | 2008-02-21 11:36:08 +0100 (Thu, 21 Feb 2008) | 1 line Fix from Josh from bug #44456 - Update contrib SViewer to not fail if a HSSFRow is null ........ r629742 | nick | 2008-02-21 11:49:25 +0100 (Thu, 21 Feb 2008) | 1 line Use the right way to figure out how many rows on a sheet, so we display the row number for all of them on the left hand side. Also, tidy up some imports ........ r629755 | nick | 2008-02-21 12:34:25 +0100 (Thu, 21 Feb 2008) | 1 line Fix bug 38921, where HSSFPalette.findSimilar() wasn't working properly, and add tests for it ........ r629821 | nick | 2008-02-21 16:08:44 +0100 (Thu, 21 Feb 2008) | 1 line Patch from Josh from bug #44371 - support for OFFSET function, and various tweaks to the formula evaluator to support this ........ r629829 | nick | 2008-02-21 16:35:59 +0100 (Thu, 21 Feb 2008) | 1 line Patch from Josh from bug #44366 - InputStreams passed to POIFSFileSystem are now automatically closed. A warning is generated for people who might've relied on them not being closed before, and a wrapper to restore the old behaviour is supplied ........ r629831 | nick | 2008-02-21 16:40:34 +0100 (Thu, 21 Feb 2008) | 1 line Patch from Josh from bug #44437 - improved unit test for poifs ........ r629832 | nick | 2008-02-21 16:42:06 +0100 (Thu, 21 Feb 2008) | 1 line Patch from Josh from bug #44437 - improved unit test for poifs ........ r629837 | nick | 2008-02-21 16:48:52 +0100 (Thu, 21 Feb 2008) | 1 line Patch from Josh from bug #44449 - Handle SharedFormulas better, for where there are formulas for the same area on two sheets, and when the shared formula flag is set incorrectly ........ r629849 | nick | 2008-02-21 17:22:18 +0100 (Thu, 21 Feb 2008) | 1 line Add a disabled test for a file with whacky StyleRecords that trigger an AIOOB ........ r629865 | nick | 2008-02-21 17:44:46 +0100 (Thu, 21 Feb 2008) | 1 line At the request of legal-discuss, shuffle the ooxml xsd licence details into LICENSE from NOTICE ........ r630160 | nick | 2008-02-22 12:23:50 +0100 (Fri, 22 Feb 2008) | 1 line Patch from Josh from bug #44450 - VLookup and HLookup support, and improvements to Lookup and Offset ........ r630164 | nick | 2008-02-22 12:40:00 +0100 (Fri, 22 Feb 2008) | 1 line Bug #44471 - Crystal Reports generates files with short StyleRecords, which isn't allowed in the spec. Work around this ........ r633114 | nick | 2008-03-03 16:01:18 +0100 (Mon, 03 Mar 2008) | 1 line Patch from Paolo from bug #44481 - getVerticallyCenter shouldn't take a parameter, but leave the old version in as deprecated for now ........ r633118 | nick | 2008-03-03 16:10:46 +0100 (Mon, 03 Mar 2008) | 1 line Fix from Yegor from bug #44491 - don't have the new style handy POIDocument property stuff break old style hpsf+hssf use ........ r633126 | nick | 2008-03-03 16:26:38 +0100 (Mon, 03 Mar 2008) | 1 line Patch from Josh from bug #44495 - Handle named cell ranges in formulas that have lower case parts ........ r633151 | nick | 2008-03-03 17:09:02 +0100 (Mon, 03 Mar 2008) | 1 line Patch from Josh from bug #44510 - Fix how DVALRecord works with dropdowns ........ r633169 | nick | 2008-03-03 17:55:00 +0100 (Mon, 03 Mar 2008) | 1 line Patch from Josh from bug #44508 - Fix formula evaluation with evaluateInCell on boolean formulas ........ r633205 | nick | 2008-03-03 18:47:36 +0100 (Mon, 03 Mar 2008) | 1 line Fix indent, add more documentation, and make the error message more helpful ........ r633505 | nick | 2008-03-04 16:06:29 +0100 (Tue, 04 Mar 2008) | 1 line Problem files from bug #44501 ........ r633547 | nick | 2008-03-04 17:53:32 +0100 (Tue, 04 Mar 2008) | 1 line Big patch from Josh from bug #44504 - lots of formula parser improvements ........ r633548 | nick | 2008-03-04 17:59:02 +0100 (Tue, 04 Mar 2008) | 1 line Changelog update for last patch ........ r634318 | nick | 2008-03-06 16:54:06 +0100 (Thu, 06 Mar 2008) | 1 line Change the behaviour on short last blocks to be a warning not an exception, as some people seem to have "real" valid files that trigger this. Fixed bug #28231 ........ r634371 | nick | 2008-03-06 19:06:48 +0100 (Thu, 06 Mar 2008) | 1 line Embeded files from bug #44524 ........ r634372 | nick | 2008-03-06 19:13:47 +0100 (Thu, 06 Mar 2008) | 1 line Add broken test for bug #43901 ........ r634617 | nick | 2008-03-07 12:18:02 +0100 (Fri, 07 Mar 2008) | 1 line Patch from Josh from bug #43901 - Correctly update the internal last cell number when adding and removing cells (previously sometimes off-by-one) ........ r634619 | nick | 2008-03-07 12:36:14 +0100 (Fri, 07 Mar 2008) | 1 line Improved support for read-only recommended workbooks, fixing bug #44536 ........ r634630 | nick | 2008-03-07 13:06:18 +0100 (Fri, 07 Mar 2008) | 1 line Patch largely from Josh from bug #44539 - Support for area references in formulas of rows >= 32768 ........ git-svn-id: https://svn.apache.org/repos/asf/poi/branches/ooxml@634936 13f79535-47bb-0310-9956-ffa450edef68 --- diff --git a/build.xml b/build.xml index 8905dc13f5..3c40743da6 100644 --- a/build.xml +++ b/build.xml @@ -557,8 +557,7 @@ under the License. - - + @@ -663,7 +662,7 @@ under the License. - + @@ -698,7 +697,7 @@ under the License. - + diff --git a/legal/LICENSE b/legal/LICENSE index d645695673..b547443dea 100755 --- a/legal/LICENSE +++ b/legal/LICENSE @@ -200,3 +200,20 @@ 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. + + + + +Office Open XML (OOXML) xsds: +----------------------------- + +These were downloaded as part of the Office Open XML ECMA Specification +from + +These are included within the Apache POI distribution, and are available +under compatible licensing terms. + +Copyright - ECMA International, "made available without restriction" + http://www.ecma-international.org/memento/Ecmabylaws.htm - section 9.4 +Patent License - Microsoft Open Specification Promise (OSP) + http://www.microsoft.com/interop/osp/ diff --git a/legal/NOTICE b/legal/NOTICE index 41c966ff27..d5d7883b35 100644 --- a/legal/NOTICE +++ b/legal/NOTICE @@ -19,17 +19,14 @@ Since this is a data file, and has no compiled version (the original See http://www.gnome.ru/projects/vsdump_en.html -Office Open XML experimental support: - XML Beans - http://xmlbeans.apache.org/ - Apache Licence Version 2.0 - http://www.apache.org/licenses/LICENSE-2.0 - DOM4J - http://www.dom4j.org/ - BSD Licence - http://www.dom4j.org/license.html - Jaxen - http://jaxen.org/ - Apache Style Licence - http://jaxen.org/license.html - OpenXml4J - http://www.openxml4j.org/ - BSD Licence or Apache Licence Version 2.0 - - http://www.openxml4j.org/Licensing/Default.html - Office Open XML ECMA Specification - - http://www.ecma-international.org/publications/standards/Ecma-376.htm - Microsoft Open Specification Promise (OSP) - - http://www.microsoft.com/interop/osp/ +The Office Open XML experimental support had additional dependencies, +with their own licensing: + * XML Beans - http://xmlbeans.apache.org/ + Apache Licence Version 2.0 - http://www.apache.org/licenses/LICENSE-2.0 + * DOM4J - http://www.dom4j.org/ + BSD Licence - http://www.dom4j.org/license.html + * Jaxen - http://jaxen.org/ + Apache Style Licence - http://jaxen.org/license.html + * OpenXml4J - http://www.openxml4j.org/ + BSD Licence or Apache Licence Version 2.0 - + http://www.openxml4j.org/Licensing/Default.html diff --git a/src/contrib/src/org/apache/poi/hssf/contrib/view/SVRowHeader.java b/src/contrib/src/org/apache/poi/hssf/contrib/view/SVRowHeader.java index f53d9cd89d..fe63dfcc8a 100644 --- a/src/contrib/src/org/apache/poi/hssf/contrib/view/SVRowHeader.java +++ b/src/contrib/src/org/apache/poi/hssf/contrib/view/SVRowHeader.java @@ -21,11 +21,8 @@ package org.apache.poi.hssf.contrib.view; import java.awt.*; -import java.awt.event.*; -import java.io.*; import javax.swing.*; import javax.swing.table.*; -import javax.swing.event.*; import org.apache.poi.hssf.usermodel.*; @@ -47,7 +44,9 @@ public class SVRowHeader extends JList { this.sheet = sheet; } - public int getSize() { return sheet.getPhysicalNumberOfRows(); } + public int getSize() { + return sheet.getLastRowNum() + 1; + } public Object getElementAt(int index) { return Integer.toString(index+1); } @@ -73,7 +72,13 @@ public class SVRowHeader extends JList { public Component getListCellRendererComponent( JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) { Dimension d = getPreferredSize(); - int rowHeight = (int)sheet.getRow(index).getHeightInPoints(); + HSSFRow row = sheet.getRow(index); + int rowHeight; + if(row == null) { + rowHeight = (int)sheet.getDefaultRowHeightInPoints(); + } else { + rowHeight = (int)row.getHeightInPoints(); + } d.height = rowHeight+extraHeight; setPreferredSize(d); setText((value == null) ? "" : value.toString()); diff --git a/src/contrib/src/org/apache/poi/hssf/contrib/view/SVTableCellEditor.java b/src/contrib/src/org/apache/poi/hssf/contrib/view/SVTableCellEditor.java index a433a7a11c..8b421ceaf1 100644 --- a/src/contrib/src/org/apache/poi/hssf/contrib/view/SVTableCellEditor.java +++ b/src/contrib/src/org/apache/poi/hssf/contrib/view/SVTableCellEditor.java @@ -20,11 +20,9 @@ package org.apache.poi.hssf.contrib.view; import java.awt.*; import java.awt.event.*; -import java.text.*; import java.util.*; import javax.swing.*; -import javax.swing.border.*; import javax.swing.table.*; import org.apache.poi.hssf.usermodel.*; diff --git a/src/contrib/src/org/apache/poi/hssf/contrib/view/SVTableCellRenderer.java b/src/contrib/src/org/apache/poi/hssf/contrib/view/SVTableCellRenderer.java index 99333874ed..0e4873b5de 100644 --- a/src/contrib/src/org/apache/poi/hssf/contrib/view/SVTableCellRenderer.java +++ b/src/contrib/src/org/apache/poi/hssf/contrib/view/SVTableCellRenderer.java @@ -19,8 +19,6 @@ package org.apache.poi.hssf.contrib.view; -import java.util.Hashtable; - import javax.swing.*; import javax.swing.table.TableCellRenderer; import javax.swing.border.*; @@ -28,14 +26,11 @@ import javax.swing.border.*; import java.awt.Component; import java.awt.Color; import java.awt.Rectangle; -import java.awt.Font; import java.io.Serializable; import java.text.*; import org.apache.poi.hssf.usermodel.*; -import org.apache.poi.hssf.util.HSSFColor; - /** diff --git a/src/contrib/src/org/apache/poi/hssf/contrib/view/SViewer.java b/src/contrib/src/org/apache/poi/hssf/contrib/view/SViewer.java index 7d451d7c76..a3668f6490 100644 --- a/src/contrib/src/org/apache/poi/hssf/contrib/view/SViewer.java +++ b/src/contrib/src/org/apache/poi/hssf/contrib/view/SViewer.java @@ -23,13 +23,10 @@ package org.apache.poi.hssf.contrib.view; import java.awt.*; import java.awt.event.*; import java.net.*; -import java.applet.*; import java.io.*; import javax.swing.*; import org.apache.poi.hssf.usermodel.HSSFWorkbook; -import org.apache.poi.hssf.usermodel.HSSFSheet; -import org.apache.poi.hssf.usermodel.HSSFCell; /** * Sheet Viewer - Views XLS files via HSSF. Can be used as an applet with @@ -143,6 +140,10 @@ public class SViewer extends JApplet { /**Main method*/ public static void main(String[] args) { + if(args.length < 1) { + throw new IllegalArgumentException("A filename to view must be supplied as the first argument, but none was given"); + } + SViewer applet = new SViewer(); applet.isStandalone = true; applet.filename = args[0]; diff --git a/src/contrib/src/org/apache/poi/hssf/contrib/view/SViewerPanel.java b/src/contrib/src/org/apache/poi/hssf/contrib/view/SViewerPanel.java index 3cd2c1c062..c134ffd546 100644 --- a/src/contrib/src/org/apache/poi/hssf/contrib/view/SViewerPanel.java +++ b/src/contrib/src/org/apache/poi/hssf/contrib/view/SViewerPanel.java @@ -25,7 +25,6 @@ import java.awt.event.*; import java.io.*; import javax.swing.*; import javax.swing.table.*; -import javax.swing.event.*; import org.apache.poi.hssf.usermodel.*; @@ -260,6 +259,9 @@ public class SViewerPanel extends JPanel { /**Main method*/ public static void main(String[] args) { + if(args.length < 1) { + throw new IllegalArgumentException("A filename to view must be supplied as the first argument, but none was given"); + } try { FileInputStream in = new FileInputStream(args[0]); HSSFWorkbook wb = new HSSFWorkbook(in); diff --git a/src/documentation/content/xdocs/changes.xml b/src/documentation/content/xdocs/changes.xml index 34345df5d0..c355afd440 100644 --- a/src/documentation/content/xdocs/changes.xml +++ b/src/documentation/content/xdocs/changes.xml @@ -36,6 +36,31 @@ + 44539 - Support for area references in formulas of rows >= 32768 + 44536 - Improved support for detecting read-only recommended files + 43901 - Correctly update the internal last cell number when adding and removing cells (previously sometimes off-by-one) + 28231 - For apparently truncated files, which are somehow still valid, now issue a truncation warning but carry on, rather than giving an exception as before + 44504 - Added initial support for recognising external functions like YEARFRAC and ISEVEN (using NameXPtg), via LinkTable support + 44504 - Improvements to FormulaParser - operators, precedence, error literals, quotes in string literals, range checking on IntPtg, formulas with extra un-parsed stuff at the end, improved parse error handling + 44504 - Fixed number conversion inconsistencies in many functions, and improved RefEval + 44508 - Fix formula evaluation with evaluateInCell on boolean formulas + 44510 - Fix how DVALRecord works with dropdowns + 44495 - Handle named cell ranges in formulas that have lower case parts + 44491 - Don't have the new-style "HPSF properties are always available" affect the old-style use of HPSF alongside HSSF + 44471 - Crystal Reports generates files with short StyleRecords, which isn't allowed in the spec. Work around this + 44450 - Support for Lookup, HLookup and VLookup functions + 44449 - Avoid getting confused when two sheets have shared formulas for the same areas, and when the shared formula is set incorrectly + 44366 - InputStreams passed to POIFSFileSystem are now automatically closed. A warning is generated for people who might've relied on them not being closed before, and a wrapper to restore the old behaviour is supplied + 44371 - Support for the Offset function + 38921 - Have HSSFPalette.findSimilar() work properly + 44456 - Fix the contrib SViewer / SViewerPanel to not fail on sheets with missing rows + 44403 - Further support for unusual, but valid, arguments to the Mid function + 44410 - Support for whole-column ranges, such as C:C, in formula strings and the formula evaluator + 44421 - Update Match function to properly support Area references + 44417 - Improved handling of references for the need to quote the sheet name for some formulas, but not when fetching a sheet by name + 44413 - Fix for circular references in INDEX, OFFSET, VLOOKUP formulas, where a cell is actually allowed to reference itself + 44403 - Fix for Mid function handling its arguments wrong + 44364 - Support for Match, NA and SumProduct functions, as well as initial function error support 44375 - Cope with a broken dictionary in Document Summary Information stream. RuntimeExceptions that occured when trying to read bogus data are now caught. Dictionary entries up to but not including the bogus one are preserved, the rest is ignored. 38641 - Handle timezones better with cell.setCellValue(Calendar), so now 20:00-03:00, 20:00+00:00 and 20:00+03:00 will all be recorded as 20:00, and not 17:00 / 20:00 / 23:00 (pass a Date not a Calendar for old behaviour) 44373 - Have HSSFDateUtil.isADateFormat recognize more formats as being dates diff --git a/src/documentation/content/xdocs/getinvolved/index.xml b/src/documentation/content/xdocs/getinvolved/index.xml index 71ba941342..eeab7c6e79 100644 --- a/src/documentation/content/xdocs/getinvolved/index.xml +++ b/src/documentation/content/xdocs/getinvolved/index.xml @@ -53,6 +53,43 @@ license.

+
Publicly Available Information on the file formats +

+ In early 2008, Microsoft made a fairly complete set of documentation + on the binary file formats freely and publicly available. These were + released under the Open + Specification Promise, which does allow us to use them for + building open source software under the + Apache Software License. +

+

+ You can download the documentation on Excel, Word, PowerPoint and + Escher (drawing) from + http://www.microsoft.com/interop/docs/OfficeBinaryFormats.mspx. + Documentation on a few of the supporting technologies used in these + file formats can be downloaded from + http://www.microsoft.com/interop/docs/supportingtechnologies.mspx. +

+

+ Previously, Microsoft published a book on the Excel 97 file format. + It can still be of plenty of use, and is handy dead tree form. Pick up + a copy of "Excel 97 Developer's Kit" from your favourite second hand + book store. +

+

+ The newer Office Open XML (ooxml) file formats are documented as part + of the ECMA / ISO standardisation effort for the formats. This + documentation is quite large, but you can normally find the bit you + need without too much effort! This can be downloaded from + http://www.ecma-international.org/publications/standards/Ecma-376.htm, + and is also under the OSP. +

+

+ It is also worth checking the documentation and code of the other + open source implementations of the file formats. +

+
I just signed an NDA to get a spec from Microsoft and I'd like to contribute

In short, stay away, stay far far away. Implementing these file formats @@ -66,13 +103,14 @@

If you've ever received information regarding the OLE 2 Compound Document Format under any type of exclusionary agreement from Microsoft, or - (probably illegally) received such information from a person bound by - such an agreement, you cannot participate in this project. (Sorry) + (possibly illegally) received such information from a person bound by + such an agreement, you cannot participate in this project. (Sorry)

Those submitting patches that show insight into the file format may be - asked to state explicitly that they are eligible or possibly sign an - agreement. + asked to state explicitly that they have only ever read the publicly + available file format information, and not any received under an NDA + or similar.

@@ -86,7 +124,9 @@
  • Documentation is always the best place to start contributing, maybe you found that if the documentation just told you how to do X then it would make more sense, modify the documentation.
  • Get used to building POI, you'll be doing it a lot, be one with the build, know its targets, etc.
  • Write Unit Tests. Great way to understand POI. Look for classes that aren't tested, or aren't tested on a public/protected method level, start there.
  • -
  • (HSSF)Get the Excel 97 Developer's Kit - its out of print but its dirt cheap (seen copies for under $15 (US)) used on amazon. It explains the Excel file format.
  • +
  • Download the file format documentation from Microsoft - + OLE2 Binary File Formats or + OOXML XML File Formats
  • Submit patches (see below) of your contributions, modifications.
  • Fill out new features, see Bug database for suggestions.
  • diff --git a/src/documentation/content/xdocs/hslf/index.xml b/src/documentation/content/xdocs/hslf/index.xml index a6eaf67117..438aab3f13 100755 --- a/src/documentation/content/xdocs/hslf/index.xml +++ b/src/documentation/content/xdocs/hslf/index.xml @@ -45,8 +45,8 @@ scratchpad area of the POI SVN repository. Ensure that you have the scratchpad jar or the scratchpad - build area in your - classpath before experimenting with this code. + build area in your classpath before experimenting with + this code - the main POI jar is not enough.

    The quick guide documentation provides information on using this API. Comments and fixes gratefully accepted on the POI diff --git a/src/documentation/content/xdocs/status.xml b/src/documentation/content/xdocs/status.xml index 91641b13ba..c59cae1a3d 100644 --- a/src/documentation/content/xdocs/status.xml +++ b/src/documentation/content/xdocs/status.xml @@ -33,6 +33,36 @@ + 44539 - Support for area references in formulas of rows >= 32768 + 44536 - Improved support for detecting read-only recommended files + 43901 - Correctly update the internal last cell number when adding and removing cells (previously sometimes off-by-one) + 44504 - Added initial support for recognising external functions like YEARFRAC and ISEVEN (using NameXPtg), via LinkTable support + 44504 - Improvements to FormulaParser - operators, precedence, error literals, quotes in string literals, range checking on IntPtg, formulas with extra un-parsed stuff at the end, improved parse error handling + 44504 - Fixed number conversion inconsistencies in many functions, and improved RefEval + 44504 - Added initial support for recognising external functions like YEARFRAC and ISEVEN (using NameXPtg), via LinkTable support + 44504 - Improvements to FormulaParser - operators, precedence, error literals, quotes in string literals, range checking on IntPtg, formulas with extra un-parsed stuff at the end, improved parse error handling + 44504 - Fixed number conversion inconsistencies in many functions, and improved RefEval + 44508 - Fix formula evaluation with evaluateInCell on boolean formulas + 44510 - Fix how DVALRecord works with dropdowns + 44495 - Handle named cell ranges in formulas that have lower case parts + 44491 - Don't have the new-style "HPSF properties are always available" affect the old-style use of HPSF alongside HSSF + 44471 - Crystal Reports generates files with short StyleRecords, which isn't allowed in the spec. Work around this + 44495 - Handle named cell ranges in formulas that have lower case parts + 44491 - Don't have the new-style "HPSF properties are always available" affect the old-style use of HPSF alongside HSSF + 44471 - Crystal Reports generates files with short StyleRecords, which isn't allowed in the spec. Work around this + 44450 - Support for Lookup, HLookup and VLookup functions + 44449 - Avoid getting confused when two sheets have shared formulas for the same areas, and when the shared formula is set incorrectly + 44366 - InputStreams passed to POIFSFileSystem are now automatically closed. A warning is generated for people who might've relied on them not being closed before, and a wrapper to restore the old behaviour is supplied + 44371 - Support for the Offset function + 38921 - Have HSSFPalette.findSimilar() work properly + 44456 - Fix the contrib SViewer / SViewerPanel to not fail on sheets with missing rows + 44403 - Further support for unusual, but valid, arguments to the Mid function + 44410 - Support for whole-column ranges, such as C:C, in formula strings and the formula evaluator + 44421 - Update Match function to properly support Area references + 44417 - Improved handling of references for the need to quote the sheet name for some formulas, but not when fetching a sheet by name + 44413 - Fix for circular references in INDEX, OFFSET, VLOOKUP formulas, where a cell is actually allowed to reference itself + 44403 - Fix for Mid function handling its arguments wrong + 44364 - Support for Match, NA and SumProduct functions, as well as initial function error support 44375 - Cope with a broken dictionary in Document Summary Information stream. RuntimeExceptions that occured when trying to read bogus data are now caught. Dictionary entries up to but not including the bogus one are preserved, the rest is ignored. 38641 - Handle timezones better with cell.setCellValue(Calendar), so now 20:00-03:00, 20:00+00:00 and 20:00+03:00 will all be recorded as 20:00, and not 17:00 / 20:00 / 23:00 (pass a Date not a Calendar for old behaviour) 44373 - Have HSSFDateUtil.isADateFormat recognize more formats as being dates diff --git a/src/java/org/apache/poi/POIDocument.java b/src/java/org/apache/poi/POIDocument.java index 8d91c06e79..075fa45381 100644 --- a/src/java/org/apache/poi/POIDocument.java +++ b/src/java/org/apache/poi/POIDocument.java @@ -54,16 +54,24 @@ public abstract class POIDocument { /** For our own logging use */ protected POILogger logger = POILogFactory.getLogger(this.getClass()); - - /** + /* Have the property streams been read yet? (Only done on-demand) */ + protected boolean initialized = false; + + /** * Fetch the Document Summary Information of the document */ - public DocumentSummaryInformation getDocumentSummaryInformation() { return dsInf; } + public DocumentSummaryInformation getDocumentSummaryInformation() { + if(!initialized) readProperties(); + return dsInf; + } /** * Fetch the Summary Information of the document */ - public SummaryInformation getSummaryInformation() { return sInf; } + public SummaryInformation getSummaryInformation() { + if(!initialized) readProperties(); + return sInf; + } /** * Find, and create objects for, the standard @@ -89,6 +97,9 @@ public abstract class POIDocument { } else if(ps != null) { logger.log(POILogger.WARN, "SummaryInformation property set came back with wrong class - ", ps.getClass()); } + + // Mark the fact that we've now loaded up the properties + initialized = true; } /** @@ -133,7 +144,7 @@ public abstract class POIDocument { * @param writtenEntries a list of POIFS entries to add the property names too */ protected void writeProperties(POIFSFileSystem outFS, List writtenEntries) throws IOException { - if(sInf != null) { + if(sInf != null) { writePropertySet(SummaryInformation.DEFAULT_STREAM_NAME,sInf,outFS); if(writtenEntries != null) { writtenEntries.add(SummaryInformation.DEFAULT_STREAM_NAME); diff --git a/src/java/org/apache/poi/hssf/model/FormulaParser.java b/src/java/org/apache/poi/hssf/model/FormulaParser.java index 832dbdef7a..7b89b90d81 100644 --- a/src/java/org/apache/poi/hssf/model/FormulaParser.java +++ b/src/java/org/apache/poi/hssf/model/FormulaParser.java @@ -1,4 +1,3 @@ - /* ==================================================================== Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with @@ -15,15 +14,13 @@ See the License for the specific language governing permissions and limitations under the License. ==================================================================== */ - - package org.apache.poi.hssf.model; import java.util.ArrayList; import java.util.Iterator; -import java.util.LinkedList; import java.util.List; +import java.util.Stack; import java.util.regex.Pattern; //import PTG's .. since we need everything, import * @@ -33,7 +30,7 @@ import org.apache.poi.hssf.record.formula.*; /** * This class parses a formula string into a List of tokens in RPN order. - * Inspired by + * Inspired by * Lets Build a Compiler, by Jack Crenshaw * BNF for the formula expression is : * ::= [ ]* @@ -48,138 +45,124 @@ import org.apache.poi.hssf.record.formula.*; * @author Peter M. Murray (pete at quantrix dot com) * @author Pavel Krupets (pkrupets at palmtreebusiness dot com) */ -public class FormulaParser { +public final class FormulaParser { + /** + * Specific exception thrown when a supplied formula does not parse properly.
    + * Primarily used by test cases when testing for specific parsing exceptions.

    + * + */ + static final class FormulaParseException extends RuntimeException { + // This class was given package scope until it would become clear that it is useful to + // general client code. + public FormulaParseException(String msg) { + super(msg); + } + } + public static int FORMULA_TYPE_CELL = 0; public static int FORMULA_TYPE_SHARED = 1; public static int FORMULA_TYPE_ARRAY =2; public static int FORMULA_TYPE_CONDFOMRAT = 3; public static int FORMULA_TYPE_NAMEDRANGE = 4; - - private String formulaString; - private int pointer=0; - private int formulaLength; - - private List tokens = new java.util.Stack(); - - /** - * Using an unsynchronized linkedlist to implement a stack since we're not multi-threaded. - */ - private List functionTokens = new LinkedList(); + + private final String formulaString; + private final int formulaLength; + private int pointer; + + private final List tokens = new Stack(); /** * Used for spotting if we have a cell reference, * or a named range */ private final static Pattern CELL_REFERENCE_PATTERN = Pattern.compile("(?:('?)[^:\\\\/\\?\\*\\[\\]]+\\1!)?\\$?[A-Za-z]+\\$?[\\d]+"); - + private static char TAB = '\t'; - private static char CR = '\n'; - - private char look; // Lookahead Character - - private Workbook book; - - - /** + + /** + * Lookahead Character. + * gets value '\0' when the input string is exhausted + */ + private char look; + + private Workbook book; + + + /** * Create the formula parser, with the string that is to be * parsed against the supplied workbook. * A later call the parse() method to return ptg list in * rpn order, then call the getRPNPtg() to retrive the * parse results. * This class is recommended only for single threaded use. - * + * * If you only have a usermodel.HSSFWorkbook, and not a * model.Workbook, then use the convenience method on - * usermodel.HSSFFormulaEvaluator + * usermodel.HSSFFormulaEvaluator */ public FormulaParser(String formula, Workbook book){ formulaString = formula; pointer=0; this.book = book; - formulaLength = formulaString.length(); + formulaLength = formulaString.length(); + } + + public static Ptg[] parse(String formula, Workbook book) { + FormulaParser fp = new FormulaParser(formula, book); + fp.parse(); + return fp.getRPNPtg(); } - /** Read New Character From Input Stream */ private void GetChar() { // Check to see if we've walked off the end of the string. - // Just return if so and reset Look to smoething to keep - // SkipWhitespace from spinning - if (pointer == formulaLength) { + if (pointer > formulaLength) { + throw new RuntimeException("too far"); + } + if (pointer < formulaLength) { + look=formulaString.charAt(pointer); + } else { + // Just return if so and reset 'look' to something to keep + // SkipWhitespace from spinning look = (char)0; - return; - } - look=formulaString.charAt(pointer++); + } + pointer++; //System.out.println("Got char: "+ look); } - - - /** Report an Error */ - private void Error(String s) { - System.out.println("Error: "+s); - } - - - - /** Report Error and Halt */ - private void Abort(String s) { - Error(s); - //System.exit(1); //throw exception?? - throw new RuntimeException("Cannot Parse, sorry : " + s + " @ " + pointer + " [Formula String was: '" + formulaString + "']"); - } - - /** Report What Was Expected */ - private void Expected(String s) { - Abort(s + " Expected"); + private RuntimeException expected(String s) { + return new FormulaParseException(s + " Expected"); } - - - + + + /** Recognize an Alpha Character */ private boolean IsAlpha(char c) { return Character.isLetter(c) || c == '$' || c=='_'; } - - - + + + /** Recognize a Decimal Digit */ private boolean IsDigit(char c) { //System.out.println("Checking digit for"+c); return Character.isDigit(c); } - - + + /** Recognize an Alphanumeric */ private boolean IsAlNum(char c) { return (IsAlpha(c) || IsDigit(c)); } - - - /** Recognize an Addop */ - private boolean IsAddop( char c) { - return (c =='+' || c =='-'); - } - /** Recognize White Space */ private boolean IsWhite( char c) { return (c ==' ' || c== TAB); } - - /** - * Determines special characters;primarily in use for definition of string literals - * @param c - * @return boolean - */ - private boolean IsSpecialChar(char c) { - return (c == '>' || c== '<' || c== '=' || c=='&' || c=='[' || c==']'); - } - /** Skip Over Leading White Space */ private void SkipWhite() { @@ -187,108 +170,84 @@ public class FormulaParser { GetChar(); } } - - - /** Match a Specific Input Character */ + /** + * Consumes the next input character if it is equal to the one specified otherwise throws an + * unchecked exception. This method does not consume whitespace (before or after the + * matched character). + */ private void Match(char x) { if (look != x) { - Expected("" + x + ""); - }else { - GetChar(); - SkipWhite(); + throw expected("'" + x + "'"); } + GetChar(); } - + /** Get an Identifier */ private String GetName() { StringBuffer Token = new StringBuffer(); if (!IsAlpha(look) && look != '\'') { - Expected("Name"); + throw expected("Name"); } if(look == '\'') { - Match('\''); - boolean done = look == '\''; - while(!done) - { - Token.append(Character.toUpperCase(look)); - GetChar(); - if(look == '\'') - { - Match('\''); - done = look != '\''; - } - } + Match('\''); + boolean done = look == '\''; + while(!done) + { + Token.append(look); + GetChar(); + if(look == '\'') + { + Match('\''); + done = look != '\''; + } + } } else { - while (IsAlNum(look)) { - Token.append(Character.toUpperCase(look)); - GetChar(); - } - } - SkipWhite(); - return Token.toString(); - } - - /**Get an Identifier AS IS, without stripping white spaces or - converting to uppercase; used for literals */ - private String GetNameAsIs() { - StringBuffer Token = new StringBuffer(); - - while (IsAlNum(look) || IsWhite(look) || IsSpecialChar(look)) { - Token = Token.append(look); - GetChar(); + while (IsAlNum(look)) { + Token.append(look); + GetChar(); + } } return Token.toString(); } - - + + /** Get a Number */ private String GetNum() { StringBuffer value = new StringBuffer(); - + while (IsDigit(this.look)){ value.append(this.look); GetChar(); } - - SkipWhite(); - return value.length() == 0 ? null : value.toString(); } - - - /** Output a String with Tab */ - private void Emit(String s){ - System.out.print(TAB+s); - } - /** Output a String with Tab and CRLF */ - private void EmitLn(String s) { - Emit(s); - System.out.println();; - } - /** Parse and Translate a String Identifier */ - private void Ident() { + private Ptg parseIdent() { String name; name = GetName(); if (look == '('){ //This is a function - function(name); - } else if (look == ':' || look == '.') { // this is a AreaReference + return function(name); + } + + if (look == ':' || look == '.') { // this is a AreaReference GetChar(); - + while (look == '.') { // formulas can have . or .. or ... instead of : GetChar(); } - + String first = name; String second = GetName(); - tokens.add(new AreaPtg(first+":"+second)); - } else if (look == '!') { + return new AreaPtg(first+":"+second); + } + + if (look == '!') { Match('!'); String sheetName = name; String first = GetName(); @@ -297,79 +256,78 @@ public class FormulaParser { Match(':'); String second=GetName(); if (look == '!') { - //The sheet name was included in both of the areas. Only really - //need it once - Match('!'); - String third=GetName(); - - if (!sheetName.equals(second)) - throw new RuntimeException("Unhandled double sheet reference."); - - tokens.add(new Area3DPtg(first+":"+third,externIdx)); - } else { - tokens.add(new Area3DPtg(first+":"+second,externIdx)); + //The sheet name was included in both of the areas. Only really + //need it once + Match('!'); + String third=GetName(); + + if (!sheetName.equals(second)) + throw new RuntimeException("Unhandled double sheet reference."); + + return new Area3DPtg(first+":"+third,externIdx); } - } else { - tokens.add(new Ref3DPtg(first,externIdx)); + return new Area3DPtg(first+":"+second,externIdx); } - } else { - // This can be either a cell ref or a named range - // Try to spot which it is - boolean cellRef = CELL_REFERENCE_PATTERN.matcher(name).matches(); - boolean boolLit = (name.equals("TRUE") || name.equals("FALSE")); - - if (boolLit) { - tokens.add(new BoolPtg(name)); - } else if (cellRef) { - tokens.add(new ReferencePtg(name)); - } else { - boolean nameRecordExists = false; - for(int i = 0; i < book.getNumNames(); i++) { - // Our formula will by now contain an upper-cased - // version of any named range names - if(book.getNameRecord(i).getNameText().toUpperCase().equals(name)) { - nameRecordExists = true; - } - } - if(!nameRecordExists) - Abort("Found reference to named range \"" + name + "\", but that named range wasn't defined!"); - tokens.add(new NamePtg(name, book)); + return new Ref3DPtg(first,externIdx); + } + if (name.equalsIgnoreCase("TRUE") || name.equalsIgnoreCase("FALSE")) { + return new BoolPtg(name.toUpperCase()); + } + + // This can be either a cell ref or a named range + // Try to spot which it is + boolean cellRef = CELL_REFERENCE_PATTERN.matcher(name).matches(); + + if (cellRef) { + return new ReferencePtg(name); + } + + for(int i = 0; i < book.getNumNames(); i++) { + // named range name matching is case insensitive + if(book.getNameRecord(i).getNameText().equalsIgnoreCase(name)) { + return new NamePtg(name, book); } } + throw new FormulaParseException("Found reference to named range \"" + + name + "\", but that named range wasn't defined!"); } - + /** * Adds a pointer to the last token to the latest function argument list. * @param obj */ - private void addArgumentPointer() { - if (this.functionTokens.size() > 0) { - //no bounds check because this method should not be called unless a token array is setup by function() - List arguments = (List)this.functionTokens.get(0); - arguments.add(tokens.get(tokens.size()-1)); - } + private void addArgumentPointer(List argumentPointers) { + argumentPointers.add(tokens.get(tokens.size()-1)); } - - private void function(String name) { - //average 2 args per function - this.functionTokens.add(0, new ArrayList(2)); - + + /** + * Note - Excel function names are 'case aware but not case sensitive'. This method may end + * up creating a defined name record in the workbook if the specified name is not an internal + * Excel function, and has not been encountered before. + * + * @param name case preserved function name (as it was entered/appeared in the formula). + */ + private Ptg function(String name) { + int numArgs =0 ; + // Note regarding parameter - + if(!AbstractFunctionPtg.isInternalFunctionName(name)) { + // external functions get a Name token which points to a defined name record + NamePtg nameToken = new NamePtg(name, this.book); + + // in the token tree, the name is more or less the first argument + numArgs++; + tokens.add(nameToken); + } + //average 2 args per function + List argumentPointers = new ArrayList(2); + Match('('); - int numArgs = Arguments(); + numArgs += Arguments(argumentPointers); Match(')'); - - AbstractFunctionPtg functionPtg = getFunction(name,(byte)numArgs); - - tokens.add(functionPtg); - - if (functionPtg.getName().equals("externalflag")) { - tokens.add(new NamePtg(name, this.book)); - } - //remove what we just put in - this.functionTokens.remove(0); + return getFunction(name, numArgs, argumentPointers); } - + /** * Adds the size of all the ptgs after the provided index (inclusive). *

    @@ -378,17 +336,17 @@ public class FormulaParser { * @return int */ private int getPtgSize(int index) { - int count = 0; - - Iterator ptgIterator = tokens.listIterator(index); - while (ptgIterator.hasNext()) { - Ptg ptg = (Ptg)ptgIterator.next(); - count+=ptg.getSize(); - } - - return count; + int count = 0; + + Iterator ptgIterator = tokens.listIterator(index); + while (ptgIterator.hasNext()) { + Ptg ptg = (Ptg)ptgIterator.next(); + count+=ptg.getSize(); + } + + return count; } - + private int getPtgSize(int start, int end) { int count = 0; int index = start; @@ -398,390 +356,430 @@ public class FormulaParser { count+=ptg.getSize(); index++; } - + return count; } /** * Generates the variable function ptg for the formula. *

    - * For IF Formulas, additional PTGs are added to the tokens + * For IF Formulas, additional PTGs are added to the tokens * @param name * @param numArgs * @return Ptg a null is returned if we're in an IF formula, it needs extreme manipulation and is handled in this function */ - private AbstractFunctionPtg getFunction(String name, byte numArgs) { - AbstractFunctionPtg retval = null; - - if (name.equals("IF")) { - retval = new FuncVarPtg(AbstractFunctionPtg.ATTR_NAME, numArgs); - - //simulated pop, no bounds checking because this list better be populated by function() - List argumentPointers = (List)this.functionTokens.get(0); - - - AttrPtg ifPtg = new AttrPtg(); - ifPtg.setData((short)7); //mirroring excel output - ifPtg.setOptimizedIf(true); - - if (argumentPointers.size() != 2 && argumentPointers.size() != 3) { - throw new IllegalArgumentException("["+argumentPointers.size()+"] Arguments Found - An IF formula requires 2 or 3 arguments. IF(CONDITION, TRUE_VALUE, FALSE_VALUE [OPTIONAL]"); - } - - //Biffview of an IF formula record indicates the attr ptg goes after the condition ptgs and are - //tracked in the argument pointers - //The beginning first argument pointer is the last ptg of the condition - int ifIndex = tokens.indexOf(argumentPointers.get(0))+1; - tokens.add(ifIndex, ifPtg); - - //we now need a goto ptgAttr to skip to the end of the formula after a true condition - //the true condition is should be inserted after the last ptg in the first argument - - int gotoIndex = tokens.indexOf(argumentPointers.get(1))+1; - - AttrPtg goto1Ptg = new AttrPtg(); - goto1Ptg.setGoto(true); - - - tokens.add(gotoIndex, goto1Ptg); - - - if (numArgs > 2) { //only add false jump if there is a false condition - - //second goto to skip past the function ptg - AttrPtg goto2Ptg = new AttrPtg(); - goto2Ptg.setGoto(true); - goto2Ptg.setData((short)(retval.getSize()-1)); - //Page 472 of the Microsoft Excel Developer's kit states that: - //The b(or w) field specifies the number byes (or words to skip, minus 1 - - tokens.add(goto2Ptg); //this goes after all the arguments are defined - } - - //data portion of the if ptg points to the false subexpression (Page 472 of MS Excel Developer's kit) - //count the number of bytes after the ifPtg to the False Subexpression - //doesn't specify -1 in the documentation - ifPtg.setData((short)(getPtgSize(ifIndex+1, gotoIndex))); - - //count all the additional (goto) ptgs but dont count itself - int ptgCount = this.getPtgSize(gotoIndex)-goto1Ptg.getSize()+retval.getSize(); - if (ptgCount > (int)Short.MAX_VALUE) { - throw new RuntimeException("Ptg Size exceeds short when being specified for a goto ptg in an if"); - } - - goto1Ptg.setData((short)(ptgCount-1)); - - } else { - - retval = new FuncVarPtg(name,numArgs); + private AbstractFunctionPtg getFunction(String name, int numArgs, List argumentPointers) { + + AbstractFunctionPtg retval = new FuncVarPtg(name, (byte)numArgs); + if (!name.equals(AbstractFunctionPtg.FUNCTION_NAME_IF)) { + // early return for everything else besides IF() + return retval; } - + + + AttrPtg ifPtg = new AttrPtg(); + ifPtg.setData((short)7); //mirroring excel output + ifPtg.setOptimizedIf(true); + + if (argumentPointers.size() != 2 && argumentPointers.size() != 3) { + throw new IllegalArgumentException("["+argumentPointers.size()+"] Arguments Found - An IF formula requires 2 or 3 arguments. IF(CONDITION, TRUE_VALUE, FALSE_VALUE [OPTIONAL]"); + } + + //Biffview of an IF formula record indicates the attr ptg goes after the condition ptgs and are + //tracked in the argument pointers + //The beginning first argument pointer is the last ptg of the condition + int ifIndex = tokens.indexOf(argumentPointers.get(0))+1; + tokens.add(ifIndex, ifPtg); + + //we now need a goto ptgAttr to skip to the end of the formula after a true condition + //the true condition is should be inserted after the last ptg in the first argument + + int gotoIndex = tokens.indexOf(argumentPointers.get(1))+1; + + AttrPtg goto1Ptg = new AttrPtg(); + goto1Ptg.setGoto(true); + + + tokens.add(gotoIndex, goto1Ptg); + + + if (numArgs > 2) { //only add false jump if there is a false condition + + //second goto to skip past the function ptg + AttrPtg goto2Ptg = new AttrPtg(); + goto2Ptg.setGoto(true); + goto2Ptg.setData((short)(retval.getSize()-1)); + //Page 472 of the Microsoft Excel Developer's kit states that: + //The b(or w) field specifies the number byes (or words to skip, minus 1 + + tokens.add(goto2Ptg); //this goes after all the arguments are defined + } + + //data portion of the if ptg points to the false subexpression (Page 472 of MS Excel Developer's kit) + //count the number of bytes after the ifPtg to the False Subexpression + //doesn't specify -1 in the documentation + ifPtg.setData((short)(getPtgSize(ifIndex+1, gotoIndex))); + + //count all the additional (goto) ptgs but dont count itself + int ptgCount = this.getPtgSize(gotoIndex)-goto1Ptg.getSize()+retval.getSize(); + if (ptgCount > Short.MAX_VALUE) { + throw new RuntimeException("Ptg Size exceeds short when being specified for a goto ptg in an if"); + } + + goto1Ptg.setData((short)(ptgCount-1)); + return retval; } + + private static boolean isArgumentDelimiter(char ch) { + return ch == ',' || ch == ')'; + } /** get arguments to a function */ - private int Arguments() { - int numArgs = 0; - if (look != ')') { - numArgs++; - Expression(); - addArgumentPointer(); + private int Arguments(List argumentPointers) { + SkipWhite(); + if(look == ')') { + return 0; } - while (look == ',' || look == ';') { //TODO handle EmptyArgs - if(look == ',') { - Match(','); - } - else { - Match(';'); + + boolean missedPrevArg = true; + + int numArgs = 0; + while(true) { + SkipWhite(); + if(isArgumentDelimiter(look)) { + if(missedPrevArg) { + tokens.add(new MissingArgPtg()); + addArgumentPointer(argumentPointers); + numArgs++; + } + if(look == ')') { + break; + } + Match(','); + missedPrevArg = true; + continue; } - Expression(); - addArgumentPointer(); + comparisonExpression(); + addArgumentPointer(argumentPointers); numArgs++; + missedPrevArg = false; } return numArgs; } /** Parse and Translate a Math Factor */ - private void Factor() { - if (look == '-') - { - Match('-'); - Factor(); - tokens.add(new UnaryMinusPtg()); - } - else if (look == '+') { - Match('+'); - Factor(); - tokens.add(new UnaryPlusPtg()); + private void powerFactor() { + percentFactor(); + while(true) { + SkipWhite(); + if(look != '^') { + return; + } + Match('^'); + percentFactor(); + tokens.add(new PowerPtg()); } - else if (look == '(' ) { - Match('('); - Expression(); - Match(')'); - tokens.add(new ParenthesisPtg()); - } else if (IsAlpha(look) || look == '\''){ - Ident(); - } else if(look == '"') { - StringLiteral(); - } else if (look == ')' || look == ',') { - tokens.add(new MissingArgPtg()); - } else { - String number2 = null; - String exponent = null; - String number1 = GetNum(); - - if (look == '.') { - GetChar(); - number2 = GetNum(); + } + + private void percentFactor() { + tokens.add(parseSimpleFactor()); + while(true) { + SkipWhite(); + if(look != '%') { + return; } - - if (look == 'E') { + Match('%'); + tokens.add(new PercentPtg()); + } + } + + + /** + * factors (without ^ or % ) + */ + private Ptg parseSimpleFactor() { + SkipWhite(); + switch(look) { + case '#': + return parseErrorLiteral(); + case '-': + Match('-'); + powerFactor(); + return new UnaryMinusPtg(); + case '+': + Match('+'); + powerFactor(); + return new UnaryPlusPtg(); + case '(': + Match('('); + comparisonExpression(); + Match(')'); + return new ParenthesisPtg(); + case '"': + return parseStringLiteral(); + case ',': + case ')': + return new MissingArgPtg(); // TODO - not quite the right place to recognise a missing arg + } + if (IsAlpha(look) || look == '\''){ + return parseIdent(); + } + // else - assume number + return parseNumber(); + } + + + private Ptg parseNumber() { + String number2 = null; + String exponent = null; + String number1 = GetNum(); + + if (look == '.') { + GetChar(); + number2 = GetNum(); + } + + if (look == 'E') { + GetChar(); + + String sign = ""; + if (look == '+') { GetChar(); - - String sign = ""; - if (look == '+') { - GetChar(); - } else if (look == '-') { - GetChar(); - sign = "-"; - } - - String number = GetNum(); - if (number == null) { - Expected("Integer"); - } - exponent = sign + number; + } else if (look == '-') { + GetChar(); + sign = "-"; } - - if (number1 == null && number2 == null) { - Expected("Integer"); + + String number = GetNum(); + if (number == null) { + throw expected("Integer"); } - - tokens.add(getNumberPtgFromString(number1, number2, exponent)); + exponent = sign + number; + } + + if (number1 == null && number2 == null) { + throw expected("Integer"); } + + return getNumberPtgFromString(number1, number2, exponent); } - - /** - * Get a PTG for an integer from its string representation. - * return Int or Number Ptg based on size of input - */ - private Ptg getNumberPtgFromString(String number1, String number2, String exponent) { + + + private ErrPtg parseErrorLiteral() { + Match('#'); + String part1 = GetName().toUpperCase(); + + switch(part1.charAt(0)) { + case 'V': + if(part1.equals("VALUE")) { + Match('!'); + return ErrPtg.VALUE_INVALID; + } + throw expected("#VALUE!"); + case 'R': + if(part1.equals("REF")) { + Match('!'); + return ErrPtg.REF_INVALID; + } + throw expected("#REF!"); + case 'D': + if(part1.equals("DIV")) { + Match('/'); + Match('0'); + Match('!'); + return ErrPtg.DIV_ZERO; + } + throw expected("#DIV/0!"); + case 'N': + if(part1.equals("NAME")) { + Match('?'); // only one that ends in '?' + return ErrPtg.NAME_INVALID; + } + if(part1.equals("NUM")) { + Match('!'); + return ErrPtg.NUM_ERROR; + } + if(part1.equals("NULL")) { + Match('!'); + return ErrPtg.NULL_INTERSECTION; + } + if(part1.equals("N")) { + Match('/'); + if(look != 'A' && look != 'a') { + throw expected("#N/A"); + } + Match(look); + // Note - no '!' or '?' suffix + return ErrPtg.N_A; + } + throw expected("#NAME?, #NUM!, #NULL! or #N/A"); + + } + throw expected("#VALUE!, #REF!, #DIV/0!, #NAME?, #NUM!, #NULL! or #N/A"); + } + + + /** + * Get a PTG for an integer from its string representation. + * return Int or Number Ptg based on size of input + */ + private static Ptg getNumberPtgFromString(String number1, String number2, String exponent) { StringBuffer number = new StringBuffer(); - - if (number2 == null) { - number.append(number1); - - if (exponent != null) { - number.append('E'); - number.append(exponent); - } - + + if (number2 == null) { + number.append(number1); + + if (exponent != null) { + number.append('E'); + number.append(exponent); + } + String numberStr = number.toString(); - + int intVal; try { - return new IntPtg(numberStr); + intVal = Integer.parseInt(numberStr); } catch (NumberFormatException e) { return new NumberPtg(numberStr); } - } else { - if (number1 != null) { - number.append(number1); - } - - number.append('.'); - number.append(number2); - - if (exponent != null) { - number.append('E'); - number.append(exponent); + if (IntPtg.isInRange(intVal)) { + return new IntPtg(intVal); } - - return new NumberPtg(number.toString()); - } - } - - - private void StringLiteral() - { - // Can't use match here 'cuz it consumes whitespace - // which we need to preserve inside the string. - // - pete - // Match('"'); - if (look != '"') - Expected("\""); - else - { - GetChar(); - StringBuffer Token = new StringBuffer(); - for (;;) - { - if (look == '"') - { - GetChar(); - SkipWhite(); //potential white space here since it doesnt matter up to the operator - if (look == '"') - Token.append("\""); - else - break; - } - else if (look == 0) - { - break; - } - else - { - Token.append(look); - GetChar(); - } - } - tokens.add(new StringPtg(Token.toString())); - } - } - - /** Recognize and Translate a Multiply */ - private void Multiply(){ - Match('*'); - Factor(); - tokens.add(new MultiplyPtg()); - - } - - - /** Recognize and Translate a Divide */ - private void Divide() { - Match('/'); - Factor(); - tokens.add(new DividePtg()); + return new NumberPtg(numberStr); + } + if (number1 != null) { + number.append(number1); + } + + number.append('.'); + number.append(number2); + + if (exponent != null) { + number.append('E'); + number.append(exponent); + } + + return new NumberPtg(number.toString()); } - - - /** Parse and Translate a Math Term */ - private void Term(){ - Factor(); - while (look == '*' || look == '/' || look == '^' || look == '&') { + + + private StringPtg parseStringLiteral() + { + Match('"'); - ///TODO do we need to do anything here?? - if (look == '*') Multiply(); - else if (look == '/') Divide(); - else if (look == '^') Power(); - else if (look == '&') Concat(); + StringBuffer token = new StringBuffer(); + while (true) { + if (look == '"') { + GetChar(); + if (look != '"') { + break; + } + } + token.append(look); + GetChar(); } + return new StringPtg(token.toString()); } - - /** Recognize and Translate an Add */ - private void Add() { - Match('+'); - Term(); - tokens.add(new AddPtg()); + + /** Parse and Translate a Math Term */ + private void Term() { + powerFactor(); + while(true) { + SkipWhite(); + switch(look) { + case '*': + Match('*'); + powerFactor(); + tokens.add(new MultiplyPtg()); + continue; + case '/': + Match('/'); + powerFactor(); + tokens.add(new DividePtg()); + continue; + } + return; // finished with Term + } } - /** Recognize and Translate a Concatination */ - private void Concat() { - Match('&'); - Term(); - tokens.add(new ConcatPtg()); + private void comparisonExpression() { + concatExpression(); + while (true) { + SkipWhite(); + switch(look) { + case '=': + case '>': + case '<': + Ptg comparisonToken = getComparisonToken(); + concatExpression(); + tokens.add(comparisonToken); + continue; + } + return; // finished with predicate expression + } } - - /** Recognize and Translate a test for Equality */ - private void Equal() { - Match('='); - Expression(); - tokens.add(new EqualPtg()); + + private Ptg getComparisonToken() { + if(look == '=') { + Match(look); + return new EqualPtg(); + } + boolean isGreater = look == '>'; + Match(look); + if(isGreater) { + if(look == '=') { + Match('='); + return new GreaterEqualPtg(); + } + return new GreaterThanPtg(); + } + switch(look) { + case '=': + Match('='); + return new LessEqualPtg(); + case '>': + Match('>'); + return new NotEqualPtg(); + } + return new LessThanPtg(); } - /** Recognize and Translate a Subtract */ - private void Subtract() { - Match('-'); - Term(); - tokens.add(new SubtractPtg()); - } - private void Power() { - Match('^'); - Term(); - tokens.add(new PowerPtg()); + private void concatExpression() { + additiveExpression(); + while (true) { + SkipWhite(); + if(look != '&') { + break; // finished with concat expression + } + Match('&'); + additiveExpression(); + tokens.add(new ConcatPtg()); + } } - + /** Parse and Translate an Expression */ - private void Expression() { + private void additiveExpression() { Term(); - while (IsAddop(look)) { - if (look == '+' ) Add(); - else if (look == '-') Subtract(); + while (true) { + SkipWhite(); + switch(look) { + case '+': + Match('+'); + Term(); + tokens.add(new AddPtg()); + continue; + case '-': + Match('-'); + Term(); + tokens.add(new SubtractPtg()); + continue; + } + return; // finished with additive expression } - - /* - * This isn't quite right since it would allow multiple comparison operators. - */ - - if(look == '=' || look == '>' || look == '<') { - if (look == '=') Equal(); - else if (look == '>') GreaterThan(); - else if (look == '<') LessThan(); - return; - } - - } - - /** Recognize and Translate a Greater Than */ - private void GreaterThan() { - Match('>'); - if(look == '=') - GreaterEqual(); - else { - Expression(); - tokens.add(new GreaterThanPtg()); - } - } - - /** Recognize and Translate a Less Than */ - private void LessThan() { - Match('<'); - if(look == '=') - LessEqual(); - else if(look == '>') - NotEqual(); - else { - Expression(); - tokens.add(new LessThanPtg()); - } - - } - - /** - * Recognize and translate Greater than or Equal - * - */ - private void GreaterEqual() { - Match('='); - Expression(); - tokens.add(new GreaterEqualPtg()); - } - - /** - * Recognize and translate Less than or Equal - * - */ - - private void LessEqual() { - Match('='); - Expression(); - tokens.add(new LessEqualPtg()); - } - - /** - * Recognize and not Equal - * - */ - - private void NotEqual() { - Match('>'); - Expression(); - tokens.add(new NotEqualPtg()); - } - + //{--------------------------------------------------------------} //{ Parse and Translate an Assignment Statement } /** @@ -794,48 +792,46 @@ begin end; **/ - - - /** Initialize */ - - private void init() { - GetChar(); - SkipWhite(); - } - + + /** API call to execute the parsing of the formula * */ public void parse() { - synchronized (tokens) { - init(); - Expression(); + pointer=0; + GetChar(); + comparisonExpression(); + + if(pointer <= formulaLength) { + String msg = "Unused input [" + formulaString.substring(pointer-1) + + "] after attempting to parse the formula [" + formulaString + "]"; + throw new FormulaParseException(msg); } } - - + + /********************************* * PARSER IMPLEMENTATION ENDS HERE * EXCEL SPECIFIC METHODS BELOW *******************************/ - - /** API call to retrive the array of Ptgs created as + + /** API call to retrive the array of Ptgs created as * a result of the parsing */ public Ptg[] getRPNPtg() { return getRPNPtg(FORMULA_TYPE_CELL); } - + public Ptg[] getRPNPtg(int formulaType) { Node node = createTree(); setRootLevelRVA(node, formulaType); setParameterRVA(node,formulaType); return (Ptg[]) tokens.toArray(new Ptg[0]); } - + private void setRootLevelRVA(Node n, int formulaType) { //Pg 16, excelfileformat.pdf @ openoffice.org - Ptg p = (Ptg) n.getValue(); + Ptg p = n.getValue(); if (formulaType == FormulaParser.FORMULA_TYPE_NAMEDRANGE) { if (p.getDefaultOperandClass() == Ptg.CLASS_REF) { setClass(n,Ptg.CLASS_REF); @@ -845,9 +841,9 @@ end; } else { setClass(n,Ptg.CLASS_VALUE); } - + } - + private void setParameterRVA(Node n, int formulaType) { Ptg p = n.getValue(); int numOperands = n.getNumChildren(); @@ -863,11 +859,11 @@ end; for (int i =0;i 0; j--) { - //TODO: catch stack underflow and throw parse exception. - operands[j - 1] = (String) stack.pop(); - } - - stack.push(o.toFormulaString(operands)); - if (!(o instanceof AbstractFunctionPtg)) continue; + if (ptg instanceof AttrPtg && ((AttrPtg) ptg).isOptimizedIf()) { + continue; + } - final AbstractFunctionPtg f = (AbstractFunctionPtg) o; - final String fname = f.getName(); - if (fname == null) continue; + final OperationPtg o = (OperationPtg) ptg; + int nOperands = o.getNumberOfOperands(); + final String[] operands = new String[nOperands]; - if ((ifptg != null) && (fname.equals("specialflag"))) { - // this special case will be way different. - stack.push(ifptg.toFormulaString(new String[]{(String) stack.pop()})); - continue; - } - if (fname.equals("externalflag")) { - final String top = (String) stack.pop(); - final int paren = top.indexOf('('); - final int comma = top.indexOf(','); - if (comma == -1) { - final int rparen = top.indexOf(')'); - stack.push(top.substring(paren + 1, rparen) + "()"); - } - else { - stack.push(top.substring(paren + 1, comma) + '(' + - top.substring(comma + 1)); + for (int j = nOperands-1; j >= 0; j--) { + if(stack.isEmpty()) { + //TODO: write junit to prove this works + String msg = "Too few arguments suppled to operation token (" + + o.getClass().getName() + "). Expected (" + nOperands + + " but got " + (nOperands - j + 1); + throw new FormulaParseException(msg); + } + operands[j] = (String) stack.pop(); } + stack.push(o.toFormulaString(operands)); } - } - // TODO: catch stack underflow and throw parse exception. - return (String) stack.pop(); + if(stack.isEmpty()) { + // inspection of the code above reveals that every stack.pop() is followed by a + // stack.push(). So this is either an internal error or impossible. + throw new IllegalStateException("Stack underflow"); + } + String result = (String) stack.pop(); + if(!stack.isEmpty()) { + // Might be caused by some tokens like AttrPtg and Mem*Ptg, which really shouldn't + // put anything on the stack + throw new IllegalStateException("too much stuff left on the stack"); + } + return result; } /** * Static method to convert an array of Ptgs in RPN order @@ -1005,7 +1014,7 @@ end; * @return a human readable String */ public String toFormulaString(Ptg[] ptgs) { - return toFormulaString(book, ptgs); + return toFormulaString(book, ptgs); } @@ -1013,19 +1022,19 @@ end; *used to run the class(RVA) change algo */ private Node createTree() { - java.util.Stack stack = new java.util.Stack(); + Stack stack = new Stack(); int numPtgs = tokens.size(); OperationPtg o; int numOperands; Node[] operands; for (int i=0;i + * + * The main data of all types of references is stored in the Link Table inside the Workbook Globals + * Substream (4.2.5). The Link Table itself is optional and occurs only, if there are any + * references in the document. + *

    + * + * In BIFF8 the Link Table consists of + *

      + *
    • one or more EXTERNALBOOK Blocks

      + * each consisting of + *

        + *
      • exactly one EXTERNALBOOK (0x01AE) record
      • + *
      • zero or more EXTERNALNAME (0x0023) records
      • + *
      • zero or more CRN Blocks

        + * each consisting of + *

          + *
        • exactly one XCT (0x0059)record
        • + *
        • zero or more CRN (0x005A) records (documentation says one or more)
        • + *
        + *
      • + *
      + *
    • + *
    • exactly one EXTERNSHEET (0x0017) record
    • + *
    • zero or more DEFINEDNAME (0x0018) records
    • + *
    + * + * + * @author Josh Micich + */ +final class LinkTable { + + private static final class CRNBlock { + + private final CRNCountRecord _countRecord; + private final CRNRecord[] _crns; + + public CRNBlock(RecordStream rs) { + _countRecord = (CRNCountRecord) rs.getNext(); + int nCRNs = _countRecord.getNumberOfCRNs(); + CRNRecord[] crns = new CRNRecord[nCRNs]; + for (int i = 0; i < crns.length; i++) { + crns[i] = (CRNRecord) rs.getNext(); + } + _crns = crns; + } + public CRNRecord[] getCrns() { + return (CRNRecord[]) _crns.clone(); + } + } + + private static final class ExternalBookBlock { + private final SupBookRecord _externalBookRecord; + private final ExternalNameRecord[] _externalNameRecords; + private final CRNBlock[] _crnBlocks; + + public ExternalBookBlock(RecordStream rs) { + _externalBookRecord = (SupBookRecord) rs.getNext(); + List temp = new ArrayList(); + while(rs.peekNextClass() == ExternalNameRecord.class) { + temp.add(rs.getNext()); + } + _externalNameRecords = new ExternalNameRecord[temp.size()]; + temp.toArray(_externalNameRecords); + + temp.clear(); + + while(rs.peekNextClass() == CRNCountRecord.class) { + temp.add(new CRNBlock(rs)); + } + _crnBlocks = new CRNBlock[temp.size()]; + temp.toArray(_crnBlocks); + } + + public ExternalBookBlock(short numberOfSheets) { + _externalBookRecord = SupBookRecord.createInternalReferences(numberOfSheets); + _externalNameRecords = new ExternalNameRecord[0]; + _crnBlocks = new CRNBlock[0]; + } + + public SupBookRecord getExternalBookRecord() { + return _externalBookRecord; + } + + public String getNameText(int definedNameIndex) { + return _externalNameRecords[definedNameIndex].getText(); + } + } + + private final ExternalBookBlock[] _externalBookBlocks; + private final ExternSheetRecord _externSheetRecord; + private final List _definedNames; + private final int _recordCount; + private final WorkbookRecordList _workbookRecordList; // TODO - would be nice to remove this + + public LinkTable(List inputList, int startIndex, WorkbookRecordList workbookRecordList) { + + _workbookRecordList = workbookRecordList; + RecordStream rs = new RecordStream(inputList, startIndex); + + List temp = new ArrayList(); + while(rs.peekNextClass() == SupBookRecord.class) { + temp.add(new ExternalBookBlock(rs)); + } + if(temp.size() < 1) { + throw new RuntimeException("Need at least one EXTERNALBOOK blocks"); + } + _externalBookBlocks = new ExternalBookBlock[temp.size()]; + temp.toArray(_externalBookBlocks); + temp.clear(); + + // If link table is present, there is always 1 of ExternSheetRecord + Record next = rs.getNext(); + _externSheetRecord = (ExternSheetRecord)next; + _definedNames = new ArrayList(); + // collect zero or more DEFINEDNAMEs id=0x18 + while(rs.peekNextClass() == NameRecord.class) { + NameRecord nr = (NameRecord)rs.getNext(); + _definedNames.add(nr); + } + + _recordCount = rs.getCountRead(); + _workbookRecordList.getRecords().addAll(inputList.subList(startIndex, startIndex + _recordCount)); + } + + public LinkTable(short numberOfSheets, WorkbookRecordList workbookRecordList) { + _workbookRecordList = workbookRecordList; + _definedNames = new ArrayList(); + _externalBookBlocks = new ExternalBookBlock[] { + new ExternalBookBlock(numberOfSheets), + }; + _externSheetRecord = new ExternSheetRecord(); + _recordCount = 2; + + // tell _workbookRecordList about the 2 new records + + SupBookRecord supbook = _externalBookBlocks[0].getExternalBookRecord(); + + int idx = findFirstRecordLocBySid(CountryRecord.sid); + if(idx < 0) { + throw new RuntimeException("CountryRecord not found"); + } + _workbookRecordList.add(idx+1, _externSheetRecord); + _workbookRecordList.add(idx+1, supbook); + } + + /** + * TODO - would not be required if calling code used RecordStream or similar + */ + public int getRecordCount() { + return _recordCount; + } + + + public NameRecord getSpecificBuiltinRecord(byte name, int sheetIndex) { + + Iterator iterator = _definedNames.iterator(); + while (iterator.hasNext()) { + NameRecord record = ( NameRecord ) iterator.next(); + + //print areas are one based + if (record.getBuiltInName() == name && record.getIndexToSheet() == sheetIndex) { + return record; + } + } + + return null; + } + + public void removeBuiltinRecord(byte name, int sheetIndex) { + //the name array is smaller so searching through it should be faster than + //using the findFirstXXXX methods + NameRecord record = getSpecificBuiltinRecord(name, sheetIndex); + if (record != null) { + _definedNames.remove(record); + } + // TODO - do we need "Workbook.records.remove(...);" similar to that in Workbook.removeName(int namenum) {}? + } + + public int getNumNames() { + return _definedNames.size(); + } + + public NameRecord getNameRecord(int index) { + return (NameRecord) _definedNames.get(index); + } + + public void addName(NameRecord name) { + _definedNames.add(name); + + // TODO - this is messy + // Not the most efficient way but the other way was causing too many bugs + int idx = findFirstRecordLocBySid(ExternSheetRecord.sid); + if (idx == -1) idx = findFirstRecordLocBySid(SupBookRecord.sid); + if (idx == -1) idx = findFirstRecordLocBySid(CountryRecord.sid); + int countNames = _definedNames.size(); + _workbookRecordList.add(idx+countNames, name); + + } + + public void removeName(int namenum) { + _definedNames.remove(namenum); + } + + public short getIndexToSheet(short num) { + return _externSheetRecord.getREFRecordAt(num).getIndexToFirstSupBook(); + } + + public int getSheetIndexFromExternSheetIndex(int externSheetNumber) { + if (externSheetNumber >= _externSheetRecord.getNumOfREFStructures()) { + return -1; + } + return _externSheetRecord.getREFRecordAt(externSheetNumber).getIndexToFirstSupBook(); + } + + public short addSheetIndexToExternSheet(short sheetNumber) { + + ExternSheetSubRecord record = new ExternSheetSubRecord(); + record.setIndexToFirstSupBook(sheetNumber); + record.setIndexToLastSupBook(sheetNumber); + _externSheetRecord.addREFRecord(record); + _externSheetRecord.setNumOfREFStructures((short)(_externSheetRecord.getNumOfREFStructures() + 1)); + return (short)(_externSheetRecord.getNumOfREFStructures() - 1); + } + + public short checkExternSheet(int sheetNumber) { + + //Trying to find reference to this sheet + int nESRs = _externSheetRecord.getNumOfREFStructures(); + for(short i=0; i< nESRs; i++) { + ExternSheetSubRecord esr = _externSheetRecord.getREFRecordAt(i); + + if (esr.getIndexToFirstSupBook() == sheetNumber + && esr.getIndexToLastSupBook() == sheetNumber){ + return i; + } + } + + //We Haven't found reference to this sheet + return addSheetIndexToExternSheet((short) sheetNumber); + } + + + /** + * copied from Workbook + */ + private int findFirstRecordLocBySid(short sid) { + int index = 0; + for (Iterator iterator = _workbookRecordList.iterator(); iterator.hasNext(); ) { + Record record = ( Record ) iterator.next(); + + if (record.getSid() == sid) { + return index; + } + index ++; + } + return -1; + } + + public int getNumberOfREFStructures() { + return _externSheetRecord.getNumOfREFStructures(); + } + + public String resolveNameXText(int refIndex, int definedNameIndex) { + short extBookIndex = _externSheetRecord.getREFRecordAt(refIndex).getIndexToSupBook(); + return _externalBookBlocks[extBookIndex].getNameText(definedNameIndex); + } +} diff --git a/src/java/org/apache/poi/hssf/model/RecordStream.java b/src/java/org/apache/poi/hssf/model/RecordStream.java new file mode 100755 index 0000000000..03177c7c22 --- /dev/null +++ b/src/java/org/apache/poi/hssf/model/RecordStream.java @@ -0,0 +1,65 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +package org.apache.poi.hssf.model; + +import java.util.List; + +import org.apache.poi.hssf.record.Record; +/** + * Simplifies iteration over a sequence of Record objects. + * + * @author Josh Micich + */ +final class RecordStream { + + private final List _list; + private int _nextIndex; + private int _countRead; + + public RecordStream(List inputList, int startIndex) { + _list = inputList; + _nextIndex = startIndex; + _countRead = 0; + } + + public boolean hasNext() { + return _nextIndex < _list.size(); + } + + public Record getNext() { + if(_nextIndex >= _list.size()) { + throw new RuntimeException("Attempt to read past end of record stream"); + } + _countRead ++; + return (Record) _list.get(_nextIndex++); + } + + /** + * @return the {@link Class} of the next Record. null if this stream is exhausted. + */ + public Class peekNextClass() { + if(_nextIndex >= _list.size()) { + return null; + } + return _list.get(_nextIndex).getClass(); + } + + public int getCountRead() { + return _countRead; + } +} diff --git a/src/java/org/apache/poi/hssf/model/Workbook.java b/src/java/org/apache/poi/hssf/model/Workbook.java index 2ba50857ca..8fa3010a4b 100644 --- a/src/java/org/apache/poi/hssf/model/Workbook.java +++ b/src/java/org/apache/poi/hssf/model/Workbook.java @@ -79,10 +79,8 @@ public class Workbook implements Model */ protected SSTRecord sst = null; - /** - * Holds the Extern Sheet with references to bound sheets - */ - protected ExternSheetRecord externSheet= null; + + private LinkTable linkTable; // optionally occurs if there are references in the document. (4.10.3) /** * holds the "boundsheet" records (aka bundlesheet) so that they can have their @@ -92,8 +90,6 @@ public class Workbook implements Model protected ArrayList formats = new ArrayList(); - protected ArrayList names = new ArrayList(); - protected ArrayList hyperlinks = new ArrayList(); protected int numxfs = 0; // hold the number of extended format records @@ -134,6 +130,7 @@ public class Workbook implements Model new Integer(recs.size())); Workbook retval = new Workbook(); ArrayList records = new ArrayList(recs.size() / 3); + retval.records.setRecords(records); int k; for (k = 0; k < recs.size(); k++) { @@ -192,21 +189,16 @@ public class Workbook implements Model retval.records.setBackuppos( k ); break; case ExternSheetRecord.sid : - if (log.check( POILogger.DEBUG )) - log.log(DEBUG, "found extern sheet record at " + k); - retval.externSheet = ( ExternSheetRecord ) rec; - break; + throw new RuntimeException("Extern sheet is part of LinkTable"); case NameRecord.sid : - if (log.check( POILogger.DEBUG )) - log.log(DEBUG, "found name record at " + k); - retval.names.add(rec); - // retval.records.namepos = k; - break; + throw new RuntimeException("DEFINEDNAME is part of LinkTable"); case SupBookRecord.sid : if (log.check( POILogger.DEBUG )) log.log(DEBUG, "found SupBook record at " + k); + retval.linkTable = new LinkTable(recs, k, retval.records); // retval.records.supbookpos = k; - break; + k+=retval.linkTable.getRecordCount() - 1; + continue; case FormatRecord.sid : if (log.check( POILogger.DEBUG )) log.log(DEBUG, "found format record at " + k); @@ -262,8 +254,6 @@ public class Workbook implements Model break; } } - - retval.records.setRecords(records); if (retval.windowOne == null) { retval.windowOne = (WindowOneRecord) retval.createWindowOne(); @@ -283,6 +273,7 @@ public class Workbook implements Model log.log( DEBUG, "creating new workbook from scratch" ); Workbook retval = new Workbook(); ArrayList records = new ArrayList( 30 ); + retval.records.setRecords(records); ArrayList formats = new ArrayList( 8 ); records.add( retval.createBOF() ); @@ -339,8 +330,9 @@ public class Workbook implements Model records.add( retval.createStyle( k ) ); } records.add( retval.createUseSelFS() ); - for ( int k = 0; k < 1; k++ ) - { // now just do 1 + + int nBoundSheets = 1; // now just do 1 + for ( int k = 0; k < nBoundSheets; k++ ) { BoundSheetRecord bsr = (BoundSheetRecord) retval.createBoundSheet( k ); @@ -351,12 +343,14 @@ public class Workbook implements Model // retval.records.supbookpos = retval.records.bspos + 1; // retval.records.namepos = retval.records.supbookpos + 2; records.add( retval.createCountry() ); + for ( int k = 0; k < nBoundSheets; k++ ) { + retval.getOrCreateLinkTable().checkExternSheet(k); + } retval.sst = (SSTRecord) retval.createSST(); records.add( retval.sst ); records.add( retval.createExtendedSST() ); records.add( retval.createEOF() ); - retval.records.setRecords(records); if (log.check( POILogger.DEBUG )) log.log( DEBUG, "exit create new workbook from scratch" ); return retval; @@ -369,36 +363,20 @@ public class Workbook implements Model * @param sheetIndex Index to match * @return null if no builtin NameRecord matches */ - public NameRecord getSpecificBuiltinRecord(byte name, int sheetIndex) - { - Iterator iterator = names.iterator(); - while (iterator.hasNext()) { - NameRecord record = ( NameRecord ) iterator.next(); - - //print areas are one based - if (record.getBuiltInName() == name && record.getIndexToSheet() == sheetIndex) { - return record; - } - } - - return null; - - } + public NameRecord getSpecificBuiltinRecord(byte name, int sheetIndex) + { + return getOrCreateLinkTable().getSpecificBuiltinRecord(name, sheetIndex); + } /** * Removes the specified Builtin NameRecord that matches the name and index * @param name byte representation of the builtin to match * @param sheetIndex zero-based sheet reference */ - public void removeBuiltinRecord(byte name, int sheetIndex) { - //the name array is smaller so searching through it should be faster than - //using the findFirstXXXX methods - NameRecord record = getSpecificBuiltinRecord(name, sheetIndex); - if (record != null) { - names.remove(record); - } - - } + public void removeBuiltinRecord(byte name, int sheetIndex) { + linkTable.removeBuiltinRecord(name, sheetIndex); + // TODO - do we need "this.records.remove(...);" similar to that in this.removeName(int namenum) {}? + } public int getNumRecords() { return records.size(); @@ -614,6 +592,7 @@ public class Workbook implements Model records.add(records.getBspos()+1, bsr); records.setBspos( records.getBspos() + 1 ); boundsheets.add(bsr); + getOrCreateLinkTable().checkExternSheet(sheetnum); fixTabIdRecord(); } } @@ -1824,14 +1803,26 @@ public class Workbook implements Model protected Record createEOF() { return new EOFRecord(); } + + /** + * lazy initialization + * Note - creating the link table causes creation of 1 EXTERNALBOOK and 1 EXTERNALSHEET record + */ + private LinkTable getOrCreateLinkTable() { + if(linkTable == null) { + linkTable = new LinkTable((short) getNumSheets(), records); + } + return linkTable; + } public SheetReferences getSheetReferences() { SheetReferences refs = new SheetReferences(); - if (externSheet != null) { - for (int k = 0; k < externSheet.getNumOfREFStructures(); k++) { + if (linkTable != null) { + int numRefStructures = linkTable.getNumberOfREFStructures(); + for (short k = 0; k < numRefStructures; k++) { - String sheetName = findSheetNameFromExternSheet((short)k); + String sheetName = findSheetNameFromExternSheet(k); refs.addSheetReference(sheetName, k); } @@ -1846,7 +1837,8 @@ public class Workbook implements Model public String findSheetNameFromExternSheet(short num){ String result=""; - short indexToSheet = externSheet.getREFRecordAt(num).getIndexToFirstSupBook(); + short indexToSheet = linkTable.getIndexToSheet(num); + if (indexToSheet>-1) { //error check, bail out gracefully! result = getSheetName(indexToSheet); } @@ -1861,10 +1853,7 @@ public class Workbook implements Model */ public int getSheetIndexFromExternSheetIndex(int externSheetNumber) { - if (externSheetNumber >= externSheet.getNumOfREFStructures()) - return -1; - else - return externSheet.getREFRecordAt(externSheetNumber).getIndexToFirstSupBook(); + return linkTable.getSheetIndexFromExternSheetIndex(externSheetNumber); } /** returns the extern sheet number for specific sheet number , @@ -1873,58 +1862,17 @@ public class Workbook implements Model * @return index to extern sheet */ public short checkExternSheet(int sheetNumber){ - - int i = 0; - boolean flag = false; - short result = 0; - - if (externSheet == null) { - externSheet = createExternSheet(); - } - - //Trying to find reference to this sheet - while (i < externSheet.getNumOfREFStructures() && !flag){ - ExternSheetSubRecord record = externSheet.getREFRecordAt(i); - - if (record.getIndexToFirstSupBook() == sheetNumber && - record.getIndexToLastSupBook() == sheetNumber){ - flag = true; - result = (short) i; - } - - ++i; - } - - //We Havent found reference to this sheet - if (!flag) { - result = addSheetIndexToExternSheet((short) sheetNumber); - } - - return result; - } - - private short addSheetIndexToExternSheet(short sheetNumber){ - short result; - - ExternSheetSubRecord record = new ExternSheetSubRecord(); - record.setIndexToFirstSupBook(sheetNumber); - record.setIndexToLastSupBook(sheetNumber); - externSheet.addREFRecord(record); - externSheet.setNumOfREFStructures((short)(externSheet.getNumOfREFStructures() + 1)); - result = (short)(externSheet.getNumOfREFStructures() - 1); - - return result; + return getOrCreateLinkTable().checkExternSheet(sheetNumber); } - - /** gets the total number of names * @return number of names */ public int getNumNames(){ - int result = names.size(); - - return result; + if(linkTable == null) { + return 0; + } + return linkTable.getNumNames(); } /** gets the name record @@ -1932,28 +1880,14 @@ public class Workbook implements Model * @return name record */ public NameRecord getNameRecord(int index){ - NameRecord result = (NameRecord) names.get(index); - - return result; - + return linkTable.getNameRecord(index); } /** creates new name * @return new name record */ public NameRecord createName(){ - - NameRecord name = new NameRecord(); - - // Not the most efficient way but the other way was causing too many bugs - int idx = findFirstRecordLocBySid(ExternSheetRecord.sid); - if (idx == -1) idx = findFirstRecordLocBySid(SupBookRecord.sid); - if (idx == -1) idx = findFirstRecordLocBySid(CountryRecord.sid); - - records.add(idx+names.size()+1, name); - names.add(name); - - return name; + return addName(new NameRecord()); } @@ -1962,67 +1896,41 @@ public class Workbook implements Model */ public NameRecord addName(NameRecord name) { - // Not the most efficient way but the other way was causing too many bugs - int idx = findFirstRecordLocBySid(ExternSheetRecord.sid); - if (idx == -1) idx = findFirstRecordLocBySid(SupBookRecord.sid); - if (idx == -1) idx = findFirstRecordLocBySid(CountryRecord.sid); - records.add(idx+names.size()+1, name); - names.add(name); + + getOrCreateLinkTable().addName(name); return name; } - /**Generates a NameRecord to represent a built-in region - * @return a new NameRecord unless the index is invalid - */ - public NameRecord createBuiltInName(byte builtInName, int index) - { - if (index == -1 || index+1 > (int)Short.MAX_VALUE) - throw new IllegalArgumentException("Index is not valid ["+index+"]"); - - NameRecord name = new NameRecord(builtInName, (short)(index)); - - addName(name); - - return name; - } + /**Generates a NameRecord to represent a built-in region + * @return a new NameRecord unless the index is invalid + */ + public NameRecord createBuiltInName(byte builtInName, int index) + { + if (index == -1 || index+1 > Short.MAX_VALUE) + throw new IllegalArgumentException("Index is not valid ["+index+"]"); + + NameRecord name = new NameRecord(builtInName, (short)(index)); + + addName(name); + + return name; + } /** removes the name * @param namenum name index */ public void removeName(int namenum){ - if (names.size() > namenum) { + + if (linkTable.getNumNames() > namenum) { int idx = findFirstRecordLocBySid(NameRecord.sid); records.remove(idx + namenum); - names.remove(namenum); + linkTable.removeName(namenum); } } - /** creates a new extern sheet record - * @return the new extern sheet record - */ - protected ExternSheetRecord createExternSheet(){ - ExternSheetRecord externSheet = new ExternSheetRecord(); - - int idx = findFirstRecordLocBySid(CountryRecord.sid); - - records.add(idx+1, externSheet); -// records.add(records.supbookpos + 1 , rec); - - //We also adds the supBook for internal reference - SupBookRecord supbook = new SupBookRecord(); - - supbook.setNumberOfSheets((short)getNumSheets()); - //supbook.setFlag(); - - records.add(idx+1, supbook); -// records.add(records.supbookpos + 1 , supbook); - - return externSheet; - } - /** * Returns a format index that matches the passed in format. It does not tie into HSSFDataFormat. * @param format the format string @@ -2392,6 +2300,17 @@ public class Workbook implements Model } return this.fileShare; } + + /** + * is the workbook protected with a password (not encrypted)? + */ + public boolean isWriteProtected() { + if (this.fileShare == null) { + return false; + } + FileSharingRecord frec = getFileSharing(); + return (frec.getReadOnly() == 1); + } /** * protect a workbook with a password (not encypted, just sets writeprotect @@ -2419,5 +2338,14 @@ public class Workbook implements Model writeProtect = null; } + /** + * @param refIndex Index to REF entry in EXTERNSHEET record in the Link Table + * @param definedNameIndex zero-based to DEFINEDNAME or EXTERNALNAME record + * @return the string representation of the defined or external name + */ + public String resolveNameXText(int refIndex, int definedNameIndex) { + return linkTable.resolveNameXText(refIndex, definedNameIndex); + } } + diff --git a/src/java/org/apache/poi/hssf/record/CRNCountRecord.java b/src/java/org/apache/poi/hssf/record/CRNCountRecord.java new file mode 100755 index 0000000000..4c9e4425cd --- /dev/null +++ b/src/java/org/apache/poi/hssf/record/CRNCountRecord.java @@ -0,0 +1,94 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +package org.apache.poi.hssf.record; + +import org.apache.poi.util.LittleEndian; +/** + * XCT – CRN Count

    + * + * REFERENCE: 5.114

    + * + * @author Josh Micich + */ +public final class CRNCountRecord extends Record { + public final static short sid = 0x59; + + private static final short BASE_RECORD_SIZE = 4; + + + private int field_1_number_crn_records; + private int field_2_sheet_table_index; + + public CRNCountRecord() { + throw new RuntimeException("incomplete code"); + } + + public CRNCountRecord(RecordInputStream in) { + super(in); + } + + protected void validateSid(short id) { + if (id != sid) { + throw new RecordFormatException("NOT An XCT RECORD"); + } + } + + public int getNumberOfCRNs() { + return field_1_number_crn_records; + } + + + protected void fillFields(RecordInputStream in) { + field_1_number_crn_records = in.readShort(); + if(field_1_number_crn_records < 0) { + // TODO - seems like the sign bit of this field might be used for some other purpose + // see example file for test case "TestBugs.test19599()" + field_1_number_crn_records = (short)-field_1_number_crn_records; + } + field_2_sheet_table_index = in.readShort(); + } + + + public String toString() { + StringBuffer sb = new StringBuffer(); + sb.append(getClass().getName()).append(" [XCT"); + sb.append(" nCRNs=").append(field_1_number_crn_records); + sb.append(" sheetIx=").append(field_2_sheet_table_index); + sb.append("]"); + return sb.toString(); + } + + public int serialize(int offset, byte [] data) { + LittleEndian.putShort(data, 0 + offset, sid); + LittleEndian.putShort(data, 2 + offset, BASE_RECORD_SIZE); + LittleEndian.putShort(data, 4 + offset, (short)field_1_number_crn_records); + LittleEndian.putShort(data, 6 + offset, (short)field_2_sheet_table_index); + return getRecordSize(); + } + + public int getRecordSize() { + return BASE_RECORD_SIZE + 4; + } + + /** + * return the non static version of the id for this record. + */ + public short getSid() { + return sid; + } +} diff --git a/src/java/org/apache/poi/hssf/record/CRNRecord.java b/src/java/org/apache/poi/hssf/record/CRNRecord.java new file mode 100755 index 0000000000..73b9e42dfa --- /dev/null +++ b/src/java/org/apache/poi/hssf/record/CRNRecord.java @@ -0,0 +1,99 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +package org.apache.poi.hssf.record; + +import org.apache.poi.hssf.record.constant.ConstantValueParser; +import org.apache.poi.util.LittleEndian; + +/** + * Title: CRN

    + * Description: This record stores the contents of an external cell or cell range

    + * REFERENCE: 5.23

    + * + * @author josh micich + */ +public final class CRNRecord extends Record { + public final static short sid = 0x5A; + + private int field_1_last_column_index; + private int field_2_first_column_index; + private int field_3_row_index; + private Object[] field_4_constant_values; + + public CRNRecord() { + throw new RuntimeException("incomplete code"); + } + + public CRNRecord(RecordInputStream in) { + super(in); + } + + protected void validateSid(short id) { + if (id != sid) { + throw new RecordFormatException("NOT An XCT RECORD"); + } + } + + public int getNumberOfCRNs() { + return field_1_last_column_index; + } + + + protected void fillFields(RecordInputStream in) { + field_1_last_column_index = in.readByte() & 0x00FF; + field_2_first_column_index = in.readByte() & 0x00FF; + field_3_row_index = in.readShort(); + int nValues = field_1_last_column_index - field_2_first_column_index + 1; + field_4_constant_values = ConstantValueParser.parse(in, nValues); + } + + + public String toString() { + StringBuffer sb = new StringBuffer(); + sb.append(getClass().getName()).append(" [CRN"); + sb.append(" rowIx=").append(field_3_row_index); + sb.append(" firstColIx=").append(field_2_first_column_index); + sb.append(" lastColIx=").append(field_1_last_column_index); + sb.append("]"); + return sb.toString(); + } + private int getDataSize() { + return 4 + ConstantValueParser.getEncodedSize(field_4_constant_values); + } + + public int serialize(int offset, byte [] data) { + int dataSize = getDataSize(); + LittleEndian.putShort(data, 0 + offset, sid); + LittleEndian.putShort(data, 2 + offset, (short) dataSize); + LittleEndian.putByte(data, 4 + offset, field_1_last_column_index); + LittleEndian.putByte(data, 5 + offset, field_2_first_column_index); + LittleEndian.putShort(data, 6 + offset, (short) field_3_row_index); + return getRecordSize(); + } + + public int getRecordSize() { + return getDataSize() + 4; + } + + /** + * return the non static version of the id for this record. + */ + public short getSid() { + return sid; + } +} diff --git a/src/java/org/apache/poi/hssf/record/DVALRecord.java b/src/java/org/apache/poi/hssf/record/DVALRecord.java index 2846f5066c..011c0a4355 100644 --- a/src/java/org/apache/poi/hssf/record/DVALRecord.java +++ b/src/java/org/apache/poi/hssf/record/DVALRecord.java @@ -1,4 +1,3 @@ - /* ==================================================================== Copyright 2002-2004 Apache Software Foundation @@ -20,13 +19,11 @@ package org.apache.poi.hssf.record; import org.apache.poi.util.LittleEndian; /** - * Title: DVAL Record

    + * Title: DATAVALIDATIONS Record

    * Description: used in data validation ; - * This record is the list header of all data validation records in the current sheet. + * This record is the list header of all data validation records (0x01BE) in the current sheet. * @author Dragos Buleandra (dragos.buleandra@trade2b.ro) - * @version 2.0-pre */ - public class DVALRecord extends Record { public final static short sid = 0x01B2; @@ -41,13 +38,14 @@ public class DVALRecord extends Record /** Object ID of the drop down arrow object for list boxes ; * in our case this will be always FFFF , until * MSODrawingGroup and MSODrawing records are implemented */ - private int field_cbo_id = 0xFFFFFFFF; + private int field_cbo_id; /** Number of following DV Records */ - private int field_5_dv_no = 0x00000000; + private int field_5_dv_no; - public DVALRecord() - { + public DVALRecord() { + field_cbo_id = 0xFFFFFFFF; + field_5_dv_no = 0x00000000; } /** diff --git a/src/java/org/apache/poi/hssf/record/ExternalNameRecord.java b/src/java/org/apache/poi/hssf/record/ExternalNameRecord.java new file mode 100755 index 0000000000..771603c859 --- /dev/null +++ b/src/java/org/apache/poi/hssf/record/ExternalNameRecord.java @@ -0,0 +1,179 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +package org.apache.poi.hssf.record; + +import java.util.List; +import java.util.Stack; + +import org.apache.poi.hssf.record.formula.Ptg; +import org.apache.poi.util.LittleEndian; +import org.apache.poi.util.StringUtil; + +/** + * EXTERNALNAME

    + * + * @author josh micich + */ +public final class ExternalNameRecord extends Record { + + public final static short sid = 0x23; // as per BIFF8. (some old versions used 0x223) + + + private static final int OPT_BUILTIN_NAME = 0x0001; + private static final int OPT_AUTOMATIC_LINK = 0x0002; + private static final int OPT_PICTURE_LINK = 0x0004; + private static final int OPT_STD_DOCUMENT_NAME = 0x0008; + private static final int OPT_OLE_LINK = 0x0010; +// private static final int OPT_CLIP_FORMAT_MASK = 0x7FE0; + private static final int OPT_ICONIFIED_PICTURE_LINK = 0x8000; + + + private short field_1_option_flag; + private short field_2_index; + private short field_3_not_used; + private String field_4_name; + private Stack field_5_name_definition; + + + public ExternalNameRecord(RecordInputStream in) { + super(in); + } + + /** + * Convenience Function to determine if the name is a built-in name + */ + public boolean isBuiltInName() { + return (field_1_option_flag & OPT_BUILTIN_NAME) != 0; + } + /** + * For OLE and DDE, links can be either 'automatic' or 'manual' + */ + public boolean isAutomaticLink() { + return (field_1_option_flag & OPT_AUTOMATIC_LINK) != 0; + } + /** + * only for OLE and DDE + */ + public boolean isPicureLink() { + return (field_1_option_flag & OPT_PICTURE_LINK) != 0; + } + /** + * DDE links only. If true, this denotes the 'StdDocumentName' + */ + public boolean isStdDocumentNameIdentifier() { + return (field_1_option_flag & OPT_STD_DOCUMENT_NAME) != 0; + } + public boolean isOLELink() { + return (field_1_option_flag & OPT_OLE_LINK) != 0; + } + public boolean isIconifiedPictureLink() { + return (field_1_option_flag & OPT_ICONIFIED_PICTURE_LINK) != 0; + } + /** + * @return the standard String representation of this name + */ + public String getText() { + return field_4_name; + } + + + /** + * called by constructor, should throw runtime exception in the event of a + * record passed with a differing ID. + * + * @param id alleged id for this record + */ + protected void validateSid(short id) { + if (id != sid) { + throw new RecordFormatException("NOT A valid ExternalName RECORD"); + } + } + + private int getDataSize(){ + return 2 + 2 + field_4_name.length() + 2 + getNameDefinitionSize(); + } + + /** + * called by the class that is responsible for writing this sucker. + * Subclasses should implement this so that their data is passed back in a + * byte array. + * + * @param offset to begin writing at + * @param data byte array containing instance data + * @return number of bytes written + */ + public int serialize( int offset, byte[] data ) { + // TODO - junit tests + int dataSize = getDataSize(); + + LittleEndian.putShort( data, 0 + offset, sid ); + LittleEndian.putShort( data, 2 + offset, (short) dataSize ); + LittleEndian.putShort( data, 4 + offset, field_1_option_flag ); + LittleEndian.putShort( data, 6 + offset, field_2_index ); + LittleEndian.putShort( data, 8 + offset, field_3_not_used ); + short nameLen = (short) field_4_name.length(); + LittleEndian.putShort( data, 10 + offset, nameLen ); + StringUtil.putCompressedUnicode( field_4_name, data, 10 + offset ); + short defLen = (short) getNameDefinitionSize(); + LittleEndian.putShort( data, 12 + nameLen + offset, defLen ); + Ptg.serializePtgStack(field_5_name_definition, data, 12 + nameLen + offset ); + return dataSize + 4; + } + + private int getNameDefinitionSize() { + int result = 0; + List list = field_5_name_definition; + + for (int k = 0; k < list.size(); k++) + { + Ptg ptg = ( Ptg ) list.get(k); + + result += ptg.getSize(); + } + return result; + } + + + public int getRecordSize(){ + return 6 + 2 + field_4_name.length() + 2 + getNameDefinitionSize(); + } + + + protected void fillFields(RecordInputStream in) { + field_1_option_flag = in.readShort(); + field_2_index = in.readShort(); + field_3_not_used = in.readShort(); + short nameLength = in.readShort(); + field_4_name = in.readCompressedUnicode(nameLength); + short formulaLen = in.readShort(); + field_5_name_definition = Ptg.createParsedExpressionTokens(formulaLen, in); + } + + public short getSid() { + return sid; + } + + public String toString() { + StringBuffer sb = new StringBuffer(); + sb.append(getClass().getName()).append(" [EXTERNALNAME "); + sb.append(" ").append(field_4_name); + sb.append(" ix=").append(field_2_index); + sb.append("]"); + return sb.toString(); + } +} diff --git a/src/java/org/apache/poi/hssf/record/FileSharingRecord.java b/src/java/org/apache/poi/hssf/record/FileSharingRecord.java index 17f8cda64a..2f56eb092a 100644 --- a/src/java/org/apache/poi/hssf/record/FileSharingRecord.java +++ b/src/java/org/apache/poi/hssf/record/FileSharingRecord.java @@ -19,6 +19,8 @@ package org.apache.poi.hssf.record; import org.apache.poi.util.LittleEndian; +import org.apache.poi.util.POILogFactory; +import org.apache.poi.util.POILogger; import org.apache.poi.util.StringUtil; /** @@ -29,6 +31,8 @@ import org.apache.poi.util.StringUtil; */ public class FileSharingRecord extends Record { + private static POILogger logger = POILogFactory.getLogger(FileSharingRecord.class); + public final static short sid = 0x5b; private short field_1_readonly; private short field_2_password; @@ -58,8 +62,23 @@ public class FileSharingRecord extends Record { field_1_readonly = in.readShort(); field_2_password = in.readShort(); field_3_username_length = in.readByte(); + + // Is this really correct? The latest docs + // seem to hint there's nothing between the + // username length and the username string field_4_unknown = in.readShort(); - field_5_username = in.readCompressedUnicode(field_3_username_length); + + // Ensure we don't try to read more data than + // there actually is + if(field_3_username_length > in.remaining()) { + logger.log(POILogger.WARN, "FileSharingRecord defined a username of length " + field_3_username_length + ", but only " + in.remaining() + " bytes were left, truncating"); + field_3_username_length = (byte)in.remaining(); + } + if(field_3_username_length > 0) { + field_5_username = in.readCompressedUnicode(field_3_username_length); + } else { + field_5_username = ""; + } } //this is the world's lamest "security". thanks to Wouter van Vugt for making me diff --git a/src/java/org/apache/poi/hssf/record/NameRecord.java b/src/java/org/apache/poi/hssf/record/NameRecord.java index 3b1c413d58..a06bc8aedd 100644 --- a/src/java/org/apache/poi/hssf/record/NameRecord.java +++ b/src/java/org/apache/poi/hssf/record/NameRecord.java @@ -726,7 +726,7 @@ public class NameRecord extends Record { for(int i=0; i 0) { + field_3_string_options = in.readByte(); + + byte[] string = in.readRemainder(); + if (fHighByte.isSet(field_3_string_options)) { + field_4_name= StringUtil.getFromUnicodeBE(string, 0, field_2_name_length); + } else { + field_4_name=StringUtil.getFromCompressedUnicode(string, 0, field_2_name_length); + } } } diff --git a/src/java/org/apache/poi/hssf/record/SupBookRecord.java b/src/java/org/apache/poi/hssf/record/SupBookRecord.java index 91a16f0793..6755aa6f8b 100644 --- a/src/java/org/apache/poi/hssf/record/SupBookRecord.java +++ b/src/java/org/apache/poi/hssf/record/SupBookRecord.java @@ -1,4 +1,3 @@ - /* ==================================================================== Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with @@ -15,72 +14,159 @@ See the License for the specific language governing permissions and limitations under the License. ==================================================================== */ - package org.apache.poi.hssf.record; +import org.apache.poi.hssf.record.UnicodeString.UnicodeRecordStats; import org.apache.poi.util.LittleEndian; /** - * Title: Sup Book

    - * Description: A Extrenal Workbook Deciption (Sup Book) + * Title: Sup Book (EXTERNALBOOK)

    + * Description: A External Workbook Description (Suplemental Book) * Its only a dummy record for making new ExternSheet Record

    - * REFERENCE:

    + * REFERENCE: 5.38

    * @author Libin Roman (Vista Portal LDT. Developer) * @author Andrew C. Oliver (acoliver@apache.org) * */ -public class SupBookRecord extends Record -{ +public final class SupBookRecord extends Record { + public final static short sid = 0x1AE; - private short field_1_number_of_sheets; - private short field_2_flag; + private static final short SMALL_RECORD_SIZE = 4; + private static final short TAG_INTERNAL_REFERENCES = 0x0401; + private static final short TAG_ADD_IN_FUNCTIONS = 0x3A01; - public SupBookRecord() - { - setFlag((short)0x401); + private short field_1_number_of_sheets; + private UnicodeString field_2_encoded_url; + private UnicodeString[] field_3_sheet_names; + private boolean _isAddInFunctions; + + + public static SupBookRecord createInternalReferences(short numberOfSheets) { + return new SupBookRecord(false, numberOfSheets); + } + public static SupBookRecord createAddInFunctions() { + return new SupBookRecord(true, (short)0); + } + public static SupBookRecord createExternalReferences(UnicodeString url, UnicodeString[] sheetNames) { + return new SupBookRecord(url, sheetNames); + } + private SupBookRecord(boolean isAddInFuncs, short numberOfSheets) { + // else not 'External References' + field_1_number_of_sheets = numberOfSheets; + field_2_encoded_url = null; + field_3_sheet_names = null; + _isAddInFunctions = isAddInFuncs; + } + public SupBookRecord(UnicodeString url, UnicodeString[] sheetNames) { + field_1_number_of_sheets = (short) sheetNames.length; + field_2_encoded_url = url; + field_3_sheet_names = sheetNames; + _isAddInFunctions = false; } /** * Constructs a Extern Sheet record and sets its fields appropriately. * - * @param in the RecordInputstream to read the record from + * @param id id must be 0x16 or an exception will be throw upon validation + * @param size the size of the data area of the record + * @param data data of the record (should not contain sid/len) */ - public SupBookRecord(RecordInputStream in) - { + public SupBookRecord(RecordInputStream in) { super(in); } - protected void validateSid(short id) - { - if (id != sid) - { - throw new RecordFormatException("NOT An Supbook RECORD"); + protected void validateSid(short id) { + if (id != sid) { + throw new RecordFormatException("NOT An ExternSheet RECORD"); } } + public boolean isExternalReferences() { + return field_3_sheet_names != null; + } + public boolean isInternalReferences() { + return field_3_sheet_names == null && !_isAddInFunctions; + } + public boolean isAddInFunctions() { + return field_3_sheet_names == null && _isAddInFunctions; + } /** - * @param in the RecordInputstream to read the record from + * called by the constructor, should set class level fields. Should throw + * runtime exception for bad/incomplete data. + * + * @param data raw data + * @param size size of data + * @param offset of the record's data (provided a big array of the file) */ - protected void fillFields(RecordInputStream in) - { - //For now We use it only for one case - //When we need to add an named range when no named ranges was - //before it + protected void fillFields(RecordInputStream in) { field_1_number_of_sheets = in.readShort(); - field_2_flag = in.readShort(); - } - + + if(in.getLength() > SMALL_RECORD_SIZE) { + // 5.38.1 External References + _isAddInFunctions = false; + + field_2_encoded_url = in.readUnicodeString(); + UnicodeString[] sheetNames = new UnicodeString[field_1_number_of_sheets]; + for (int i = 0; i < sheetNames.length; i++) { + sheetNames[i] = in.readUnicodeString(); + } + field_3_sheet_names = sheetNames; + return; + } + // else not 'External References' + field_2_encoded_url = null; + field_3_sheet_names = null; + + short nextShort = in.readShort(); + if(nextShort == TAG_INTERNAL_REFERENCES) { + // 5.38.2 'Internal References' + _isAddInFunctions = false; + } else if(nextShort == TAG_ADD_IN_FUNCTIONS) { + // 5.38.3 'Add-In Functions' + _isAddInFunctions = true; + if(field_1_number_of_sheets != 1) { + throw new RuntimeException("Expected 0x0001 for number of sheets field in 'Add-In Functions' but got (" + + field_1_number_of_sheets + ")"); + } + } else { + throw new RuntimeException("invalid EXTERNALBOOK code (" + + Integer.toHexString(nextShort) + ")"); + } + } - public String toString() - { - StringBuffer buffer = new StringBuffer(); - buffer.append("[SUPBOOK]\n"); - buffer.append("numberosheets = ").append(getNumberOfSheets()).append('\n'); - buffer.append("flag = ").append(getFlag()).append('\n'); - buffer.append("[/SUPBOOK]\n"); - return buffer.toString(); + public String toString() { + StringBuffer sb = new StringBuffer(); + sb.append(getClass().getName()).append(" [SUPBOOK "); + + if(isExternalReferences()) { + sb.append("External References"); + sb.append(" nSheets=").append(field_1_number_of_sheets); + sb.append(" url=").append(field_2_encoded_url); + } else if(_isAddInFunctions) { + sb.append("Add-In Functions"); + } else { + sb.append("Internal References "); + sb.append(" nSheets= ").append(field_1_number_of_sheets); + } + return sb.toString(); + } + private int getDataSize() { + if(!isExternalReferences()) { + return SMALL_RECORD_SIZE; + } + int sum = 2; // u16 number of sheets + UnicodeRecordStats urs = new UnicodeRecordStats(); + field_2_encoded_url.getRecordSize(urs); + sum += urs.recordSize; + + for(int i=0; i=0;i--) { + // TODO - there is no junit test case to justify this reversed loop + // perhaps it could just run in the normal direction? SharedFormulaRecord shrd = (SharedFormulaRecord)sharedFormulas.get(i); if (shrd.isFormulaInShared(formula)) { shrd.convertSharedFormulaRecord(formula); @@ -164,9 +170,7 @@ public class ValueRecordsAggregate } } if (!found) { - //Sometimes the shared formula flag "seems" to be errornously set, - //cant really do much about that. - //throw new RecordFormatException("Could not find appropriate shared formula"); + handleMissingSharedFormulaRecord(formula); } } @@ -185,6 +189,24 @@ public class ValueRecordsAggregate return k; } + /** + * Sometimes the shared formula flag "seems" to be erroneously set, in which case there is no + * call to SharedFormulaRecord.convertSharedFormulaRecord and hence the + * parsedExpression field of this FormulaRecord will not get updated.
    + * As it turns out, this is not a problem, because in these circumstances, the existing value + * for parsedExpression is perfectly OK.

    + * + * This method may also be used for setting breakpoints to help diagnose issues regarding the + * abnormally-set 'shared formula' flags. + * (see TestValueRecordsAggregate.testSpuriousSharedFormulaFlag()).

    + * + * The method currently does nothing but do not delete it without finding a nice home for this + * comment. + */ + private static void handleMissingSharedFormulaRecord(FormulaRecord formula) { + // could log an info message here since this is a fairly unusual occurrence. + } + /** * called by the class that is responsible for writing this sucker. * Subclasses should implement this so that their data is passed back in a @@ -300,7 +322,7 @@ public class ValueRecordsAggregate return rec; } - public class MyIterator implements Iterator { + private final class MyIterator implements Iterator { short nextColumn=-1; int nextRow,lastRow; diff --git a/src/java/org/apache/poi/hssf/record/constant/ConstantValueParser.java b/src/java/org/apache/poi/hssf/record/constant/ConstantValueParser.java new file mode 100755 index 0000000000..6ec831e4a0 --- /dev/null +++ b/src/java/org/apache/poi/hssf/record/constant/ConstantValueParser.java @@ -0,0 +1,111 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +package org.apache.poi.hssf.record.constant; + +import org.apache.poi.hssf.record.RecordInputStream; +import org.apache.poi.hssf.record.UnicodeString; +import org.apache.poi.hssf.record.UnicodeString.UnicodeRecordStats; + +/** + * To support Constant Values (2.5.7) as required by the CRN record. + * This class should probably also be used for two dimensional arrays which are encoded by + * EXTERNALNAME (5.39) records and Array tokens.

    + * TODO - code in ArrayPtg should be merged with this code. It currently supports only 2 of the constant types + * + * @author Josh Micich + */ +public final class ConstantValueParser { + // note - value 3 seems to be unused + private static final int TYPE_EMPTY = 0; + private static final int TYPE_NUMBER = 1; + private static final int TYPE_STRING = 2; + private static final int TYPE_BOOLEAN = 4; + + private static final int TRUE_ENCODING = 1; + private static final int FALSE_ENCODING = 0; + + // TODO - is this the best way to represent 'EMPTY'? + private static final Object EMPTY_REPRESENTATION = null; + + private ConstantValueParser() { + // no instances of this class + } + + public static Object[] parse(RecordInputStream in, int nValues) { + Object[] result = new Object[nValues]; + for (int i = 0; i < result.length; i++) { + result[i] = readAConstantValue(in); + } + return result; + } + + private static Object readAConstantValue(RecordInputStream in) { + byte grbit = in.readByte(); + switch(grbit) { + case TYPE_EMPTY: + in.readLong(); // 8 byte 'not used' field + return EMPTY_REPRESENTATION; + case TYPE_NUMBER: + return new Double(in.readDouble()); + case TYPE_STRING: + return in.readUnicodeString(); + case TYPE_BOOLEAN: + return readBoolean(in); + } + return null; + } + + private static Object readBoolean(RecordInputStream in) { + byte val = in.readByte(); + in.readLong(); // 8 byte 'not used' field + switch(val) { + case FALSE_ENCODING: + return Boolean.FALSE; + case TRUE_ENCODING: + return Boolean.TRUE; + } + // Don't tolerate unusual boolean encoded values (unless it becomes evident that they occur) + throw new RuntimeException("unexpected boolean encoding (" + val + ")"); + } + + public static int getEncodedSize(Object[] values) { + // start with one byte 'type' code for each value + int result = values.length * 1; + for (int i = 0; i < values.length; i++) { + result += getEncodedSize(values[i]); + } + return 0; + } + + /** + * @return encoded size without the 'type' code byte + */ + private static int getEncodedSize(Object object) { + if(object == EMPTY_REPRESENTATION) { + return 8; + } + Class cls = object.getClass(); + if(cls == Boolean.class || cls == Double.class) { + return 8; + } + UnicodeString strVal = (UnicodeString)object; + UnicodeRecordStats urs = new UnicodeRecordStats(); + strVal.getRecordSize(urs); + return urs.recordSize; + } +} diff --git a/src/java/org/apache/poi/hssf/record/formula/AbstractFunctionPtg.java b/src/java/org/apache/poi/hssf/record/formula/AbstractFunctionPtg.java index 555c502949..c7c57280d3 100644 --- a/src/java/org/apache/poi/hssf/record/formula/AbstractFunctionPtg.java +++ b/src/java/org/apache/poi/hssf/record/formula/AbstractFunctionPtg.java @@ -29,10 +29,13 @@ import org.apache.poi.hssf.model.Workbook; * @author Andrew C. Oliver (acoliver at apache dot org) */ public abstract class AbstractFunctionPtg extends OperationPtg { - //constant used allow a ptgAttr to be mapped properly for its functionPtg - public static final String ATTR_NAME = "specialflag"; - - public static final short INDEX_EXTERNAL = 255; + + /** + * The name of the IF function (i.e. "IF"). Extracted as a constant for clarity. + */ + public static final String FUNCTION_NAME_IF = "IF"; + /** All external functions have function index 255 */ + private static final short FUNCTION_INDEX_EXTERNAL = 255; private static BinaryTree map = produceHash(); protected static Object[][] functionData = produceFunctionData(); @@ -66,6 +69,13 @@ public abstract class AbstractFunctionPtg extends OperationPtg { public String getName() { return lookupName(field_2_fnc_index); } + /** + * external functions get some special processing + * @return true if this is an external function + */ + public boolean isExternalFunction() { + return field_2_fnc_index == FUNCTION_INDEX_EXTERNAL; + } public String toFormulaString(Workbook book) { return getName(); @@ -73,39 +83,57 @@ public abstract class AbstractFunctionPtg extends OperationPtg { public String toFormulaString(String[] operands) { StringBuffer buf = new StringBuffer(); - - if (field_2_fnc_index != 1) { - buf.append(getName()); - buf.append('('); - } - if (operands.length >0) { - for (int i=0;ifirstArgIx) { + buf.append(','); + } + buf.append(operands[i]); + } + buf.append(")"); + } public abstract void writeBytes(byte[] array, int offset); public abstract int getSize(); - - + /** + * Used to detect whether a function name found in a formula is one of the standard excel functions + *

    + * The name matching is case insensitive. + * @return true if the name specifies a standard worksheet function, + * false if the name should be assumed to be an external function. + */ + public static final boolean isInternalFunctionName(String name) { + return map.containsValue(name.toUpperCase()); + } protected String lookupName(short index) { return ((String)map.get(new Integer(index))); } - protected short lookupIndex(String name) { - Integer index = (Integer) map.getKeyForValue(name); + /** + * Resolves internal function names into function indexes. + *

    + * The name matching is case insensitive. + * @return the standard worksheet function index if found, otherwise FUNCTION_INDEX_EXTERNAL + */ + protected static short lookupIndex(String name) { + Integer index = (Integer) map.getKeyForValue(name.toUpperCase()); if (index != null) return index.shortValue(); - return INDEX_EXTERNAL; + return FUNCTION_INDEX_EXTERNAL; } /** @@ -115,7 +143,7 @@ public abstract class AbstractFunctionPtg extends OperationPtg { BinaryTree dmap = new BinaryTree(); dmap.put(new Integer(0),"COUNT"); - dmap.put(new Integer(1),"specialflag"); + dmap.put(new Integer(1),FUNCTION_NAME_IF); dmap.put(new Integer(2),"ISNA"); dmap.put(new Integer(3),"ISERROR"); dmap.put(new Integer(4),"SUM"); @@ -354,7 +382,7 @@ public abstract class AbstractFunctionPtg extends OperationPtg { dmap.put(new Integer(252),"FREQUENCY"); dmap.put(new Integer(253),"ADDTOOLBAR"); dmap.put(new Integer(254),"DELETETOOLBAR"); - dmap.put(new Integer(255),"externalflag"); + dmap.put(new Integer(FUNCTION_INDEX_EXTERNAL),"externalflag"); dmap.put(new Integer(256),"RESETTOOLBAR"); dmap.put(new Integer(257),"EVALUATE"); dmap.put(new Integer(258),"GETTOOLBAR"); diff --git a/src/java/org/apache/poi/hssf/record/formula/Area3DPtg.java b/src/java/org/apache/poi/hssf/record/formula/Area3DPtg.java index ac260ffa4e..33278e25ed 100644 --- a/src/java/org/apache/poi/hssf/record/formula/Area3DPtg.java +++ b/src/java/org/apache/poi/hssf/record/formula/Area3DPtg.java @@ -17,15 +17,13 @@ package org.apache.poi.hssf.record.formula; -import org.apache.poi.util.LittleEndian; -import org.apache.poi.hssf.util.AreaReference; -import org.apache.poi.hssf.util.CellReference; -import org.apache.poi.hssf.util.SheetReferences; - import org.apache.poi.hssf.model.Workbook; import org.apache.poi.hssf.record.RecordInputStream; +import org.apache.poi.hssf.util.AreaReference; +import org.apache.poi.hssf.util.CellReference; import org.apache.poi.util.BitField; import org.apache.poi.util.BitFieldFactory; +import org.apache.poi.util.LittleEndian; /** @@ -38,15 +36,15 @@ import org.apache.poi.util.BitFieldFactory; * @version 1.0-pre */ -public class Area3DPtg extends Ptg +public class Area3DPtg extends Ptg implements AreaI { public final static byte sid = 0x3b; private final static int SIZE = 11; // 10 + 1 for Ptg private short field_1_index_extern_sheet; - private short field_2_first_row; - private short field_3_last_row; - private short field_4_first_column; - private short field_5_last_column; + private int field_2_first_row; + private int field_3_last_row; + private int field_4_first_column; + private int field_5_last_column; private BitField rowRelative = BitFieldFactory.getInstance( 0x8000 ); private BitField colRelative = BitFieldFactory.getInstance( 0x4000 ); @@ -66,10 +64,24 @@ public class Area3DPtg extends Ptg public Area3DPtg(RecordInputStream in) { field_1_index_extern_sheet = in.readShort(); - field_2_first_row = in.readShort(); - field_3_last_row = in.readShort(); - field_4_first_column = in.readShort(); - field_5_last_column = in.readShort(); + field_2_first_row = in.readUShort(); + field_3_last_row = in.readUShort(); + field_4_first_column = in.readUShort(); + field_5_last_column = in.readUShort(); + } + + public Area3DPtg(short firstRow, short lastRow, short firstColumn, short lastColumn, + boolean firstRowRelative, boolean lastRowRelative, boolean firstColRelative, boolean lastColRelative, + short externalSheetIndex) { + setFirstRow(firstRow); + setLastRow(lastRow); + setFirstColumn(firstColumn); + setLastColumn(lastColumn); + setFirstRowRelative(firstRowRelative); + setLastRowRelative(lastRowRelative); + setFirstColRelative(firstColRelative); + setLastColRelative(lastColRelative); + setExternSheetIndex(externalSheetIndex); } public String toString() @@ -87,7 +99,7 @@ public class Area3DPtg extends Ptg buffer.append( "lastColRowRel = " + isLastRowRelative() ).append( "\n" ); buffer.append( "firstColRel = " + isFirstColRelative() ).append( "\n" ); - buffer.append( "lastColRel = " + isLastColRelative() ).append( "\n" ); + buffer.append( "lastColRel = " + isLastColRelative() ).append( "\n" ); return buffer.toString(); } @@ -95,10 +107,10 @@ public class Area3DPtg extends Ptg { array[0 + offset] = (byte) ( sid + ptgClass ); LittleEndian.putShort( array, 1 + offset, getExternSheetIndex() ); - LittleEndian.putShort( array, 3 + offset, getFirstRow() ); - LittleEndian.putShort( array, 5 + offset, getLastRow() ); - LittleEndian.putShort( array, 7 + offset, getFirstColumnRaw() ); - LittleEndian.putShort( array, 9 + offset, getLastColumnRaw() ); + LittleEndian.putShort( array, 3 + offset, (short)getFirstRow() ); + LittleEndian.putShort( array, 5 + offset, (short)getLastRow() ); + LittleEndian.putShort( array, 7 + offset, (short)getFirstColumnRaw() ); + LittleEndian.putShort( array, 9 + offset, (short)getLastColumnRaw() ); } public int getSize() @@ -116,32 +128,32 @@ public class Area3DPtg extends Ptg field_1_index_extern_sheet = index; } - public short getFirstRow() + public int getFirstRow() { return field_2_first_row; } - public void setFirstRow( short row ) + public void setFirstRow( int row ) { field_2_first_row = row; } - public short getLastRow() + public int getLastRow() { return field_3_last_row; } - public void setLastRow( short row ) + public void setLastRow( int row ) { field_3_last_row = row; } - public short getFirstColumn() + public int getFirstColumn() { - return (short) ( field_4_first_column & 0xFF ); + return field_4_first_column & 0xFF; } - public short getFirstColumnRaw() + public int getFirstColumnRaw() { return field_4_first_column; } @@ -167,12 +179,12 @@ public class Area3DPtg extends Ptg field_4_first_column = column; } - public short getLastColumn() + public int getLastColumn() { - return (short) ( field_5_last_column & 0xFF ); + return field_5_last_column & 0xFF; } - public short getLastColumnRaw() + public int getLastColumnRaw() { return field_5_last_column; } @@ -204,7 +216,7 @@ public class Area3DPtg extends Ptg */ public void setFirstRowRelative( boolean rel ) { - field_4_first_column = rowRelative.setShortBoolean( field_4_first_column, rel ); + field_4_first_column = rowRelative.setBoolean( field_4_first_column, rel ); } /** @@ -212,7 +224,7 @@ public class Area3DPtg extends Ptg */ public void setFirstColRelative( boolean rel ) { - field_4_first_column = colRelative.setShortBoolean( field_4_first_column, rel ); + field_4_first_column = colRelative.setBoolean( field_4_first_column, rel ); } /** @@ -221,7 +233,7 @@ public class Area3DPtg extends Ptg */ public void setLastRowRelative( boolean rel ) { - field_5_last_column = rowRelative.setShortBoolean( field_5_last_column, rel ); + field_5_last_column = rowRelative.setBoolean( field_5_last_column, rel ); } /** @@ -229,7 +241,7 @@ public class Area3DPtg extends Ptg */ public void setLastColRelative( boolean rel ) { - field_5_last_column = colRelative.setShortBoolean( field_5_last_column, rel ); + field_5_last_column = colRelative.setBoolean( field_5_last_column, rel ); } @@ -243,39 +255,38 @@ public class Area3DPtg extends Ptg public void setArea( String ref ) { AreaReference ar = new AreaReference( ref ); - CellReference[] crs = ar.getCells(); - CellReference firstCell = crs[0]; - CellReference lastCell = firstCell; - if(crs.length > 1) { - lastCell = crs[1]; - } - - setFirstRow( (short) firstCell.getRow() ); - setFirstColumn( (short) firstCell.getCol() ); - setLastRow( (short) lastCell.getRow() ); - setLastColumn( (short) lastCell.getCol() ); - setFirstColRelative( !firstCell.isColAbsolute() ); + CellReference frstCell = ar.getFirstCell(); + CellReference lastCell = ar.getLastCell(); + + setFirstRow( (short) frstCell.getRow() ); + setFirstColumn( frstCell.getCol() ); + setLastRow( (short) lastCell.getRow() ); + setLastColumn( lastCell.getCol() ); + setFirstColRelative( !frstCell.isColAbsolute() ); setLastColRelative( !lastCell.isColAbsolute() ); - setFirstRowRelative( !firstCell.isRowAbsolute() ); + setFirstRowRelative( !frstCell.isRowAbsolute() ); setLastRowRelative( !lastCell.isRowAbsolute() ); } - /** - * @return text representation of this area reference that can be used in text - * formulas. The sheet name will get properly delimited if required. - */ + /** + * @return text representation of this area reference that can be used in text + * formulas. The sheet name will get properly delimited if required. + */ public String toFormulaString(Workbook book) { + // First do the sheet name StringBuffer retval = new StringBuffer(); String sheetName = Ref3DPtg.getSheetName(book, field_1_index_extern_sheet); if(sheetName != null) { SheetNameFormatter.appendFormat(retval, sheetName); retval.append( '!' ); } - retval.append( ( new CellReference( getFirstRow(), getFirstColumn(), !isFirstRowRelative(), !isFirstColRelative() ) ).toString() ); - retval.append( ':' ); - retval.append( ( new CellReference( getLastRow(), getLastColumn(), !isLastRowRelative(), !isLastColRelative() ) ).toString() ); + + // Now the normal area bit + retval.append( AreaPtg.toFormulaString(this, book) ); + + // All done return retval.toString(); } @@ -292,7 +303,7 @@ public class Area3DPtg extends Ptg ptg.field_3_last_row = field_3_last_row; ptg.field_4_first_column = field_4_first_column; ptg.field_5_last_column = field_5_last_column; - ptg.setClass(ptgClass); + ptg.setClass(ptgClass); return ptg; } diff --git a/src/java/org/apache/poi/hssf/record/formula/AreaAPtg.java b/src/java/org/apache/poi/hssf/record/formula/AreaAPtg.java index 57628f19b8..515d07dd41 100644 --- a/src/java/org/apache/poi/hssf/record/formula/AreaAPtg.java +++ b/src/java/org/apache/poi/hssf/record/formula/AreaAPtg.java @@ -36,16 +36,14 @@ import org.apache.poi.hssf.model.Workbook; * @author Jason Height (jheight at chariot dot net dot au) */ -public class AreaAPtg - extends AreaPtg -{ +public final class AreaAPtg extends AreaPtg { public final static short sid = 0x65; protected AreaAPtg() { //Required for clone methods } - public AreaAPtg(short firstRow, short lastRow, short firstColumn, short lastColumn, boolean firstRowRelative, boolean lastRowRelative, boolean firstColRelative, boolean lastColRelative) { + public AreaAPtg(int firstRow, int lastRow, int firstColumn, int lastColumn, boolean firstRowRelative, boolean lastRowRelative, boolean firstColRelative, boolean lastColRelative) { super(firstRow, lastRow, firstColumn, lastColumn, firstRowRelative, lastRowRelative, firstColRelative, lastColRelative); } diff --git a/src/java/org/apache/poi/hssf/record/formula/AreaI.java b/src/java/org/apache/poi/hssf/record/formula/AreaI.java new file mode 100644 index 0000000000..5a0a21e861 --- /dev/null +++ b/src/java/org/apache/poi/hssf/record/formula/AreaI.java @@ -0,0 +1,60 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ +package org.apache.poi.hssf.record.formula; + +/** + * Common interface for AreaPtg and Area3DPtg, and their + * child classes. + */ +public interface AreaI { + /** + * @return the first row in the area + */ + public int getFirstRow(); + + /** + * @return last row in the range (x2 in x1,y1-x2,y2) + */ + public int getLastRow(); + + /** + * @return the first column number in the area. + */ + public int getFirstColumn(); + + /** + * @return lastcolumn in the area + */ + public int getLastColumn(); + + /** + * @return isrelative first column to relative or not + */ + public boolean isFirstColRelative(); + /** + * @return lastcol relative or not + */ + public boolean isLastColRelative(); + /** + * @return whether or not the first row is a relative reference or not. + */ + public boolean isFirstRowRelative(); + /** + * @return last row relative or not + */ + public boolean isLastRowRelative(); +} \ No newline at end of file diff --git a/src/java/org/apache/poi/hssf/record/formula/AreaPtg.java b/src/java/org/apache/poi/hssf/record/formula/AreaPtg.java index 908c8d5e3c..be34e0074a 100644 --- a/src/java/org/apache/poi/hssf/record/formula/AreaPtg.java +++ b/src/java/org/apache/poi/hssf/record/formula/AreaPtg.java @@ -34,14 +34,18 @@ import org.apache.poi.hssf.record.RecordInputStream; */ public class AreaPtg - extends Ptg + extends Ptg implements AreaI { public final static short sid = 0x25; private final static int SIZE = 9; - private short field_1_first_row; - private short field_2_last_row; - private short field_3_first_column; - private short field_4_last_column; + /** zero based, unsigned 16 bit */ + private int field_1_first_row; + /** zero based, unsigned 16 bit */ + private int field_2_last_row; + /** zero based, unsigned 8 bit */ + private int field_3_first_column; + /** zero based, unsigned 8 bit */ + private int field_4_last_column; private final static BitField rowRelative = BitFieldFactory.getInstance(0x8000); private final static BitField colRelative = BitFieldFactory.getInstance(0x4000); @@ -53,17 +57,25 @@ public class AreaPtg public AreaPtg(String arearef) { AreaReference ar = new AreaReference(arearef); - setFirstRow((short)ar.getCells()[0].getRow()); - setFirstColumn((short)ar.getCells()[0].getCol()); - setLastRow((short)ar.getCells()[1].getRow()); - setLastColumn((short)ar.getCells()[1].getCol()); - setFirstColRelative(!ar.getCells()[0].isColAbsolute()); - setLastColRelative(!ar.getCells()[1].isColAbsolute()); - setFirstRowRelative(!ar.getCells()[0].isRowAbsolute()); - setLastRowRelative(!ar.getCells()[1].isRowAbsolute()); + CellReference firstCell = ar.getFirstCell(); + CellReference lastCell = ar.getLastCell(); + setFirstRow(firstCell.getRow()); + setFirstColumn(firstCell.getCol()); + setLastRow(lastCell.getRow()); + setLastColumn(lastCell.getCol()); + setFirstColRelative(!firstCell.isColAbsolute()); + setLastColRelative(!lastCell.isColAbsolute()); + setFirstRowRelative(!firstCell.isRowAbsolute()); + setLastRowRelative(!lastCell.isRowAbsolute()); } - public AreaPtg(short firstRow, short lastRow, short firstColumn, short lastColumn, boolean firstRowRelative, boolean lastRowRelative, boolean firstColRelative, boolean lastColRelative) { + public AreaPtg(int firstRow, int lastRow, int firstColumn, int lastColumn, + boolean firstRowRelative, boolean lastRowRelative, boolean firstColRelative, boolean lastColRelative) { + + checkColumnBounds(firstColumn); + checkColumnBounds(lastColumn); + checkRowBounds(firstRow); + checkRowBounds(lastRow); setFirstRow(firstRow); setLastRow(lastRow); setFirstColumn(firstColumn); @@ -74,12 +86,23 @@ public class AreaPtg setLastColRelative(lastColRelative); } + private static void checkColumnBounds(int colIx) { + if((colIx & 0x0FF) != colIx) { + throw new IllegalArgumentException("colIx (" + colIx + ") is out of range"); + } + } + private static void checkRowBounds(int rowIx) { + if((rowIx & 0x0FFFF) != rowIx) { + throw new IllegalArgumentException("rowIx (" + rowIx + ") is out of range"); + } + } + public AreaPtg(RecordInputStream in) { - field_1_first_row = in.readShort(); - field_2_last_row = in.readShort(); - field_3_first_column = in.readShort(); - field_4_last_column = in.readShort(); + field_1_first_row = in.readUShort(); + field_2_last_row = in.readUShort(); + field_3_first_column = in.readUShort(); + field_4_last_column = in.readUShort(); //System.out.println(toString()); } @@ -108,10 +131,10 @@ public class AreaPtg public void writeBytes(byte [] array, int offset) { array[offset] = (byte) (sid + ptgClass); - LittleEndian.putShort(array,offset+1,field_1_first_row); - LittleEndian.putShort(array,offset+3,field_2_last_row); - LittleEndian.putShort(array,offset+5,field_3_first_column); - LittleEndian.putShort(array,offset+7,field_4_last_column); + LittleEndian.putShort(array,offset+1,(short)field_1_first_row); + LittleEndian.putShort(array,offset+3,(short)field_2_last_row); + LittleEndian.putShort(array,offset+5,(short)field_3_first_column); + LittleEndian.putShort(array,offset+7,(short)field_4_last_column); } public int getSize() @@ -122,42 +145,42 @@ public class AreaPtg /** * @return the first row in the area */ - public short getFirstRow() + public int getFirstRow() { return field_1_first_row; } /** * sets the first row - * @param row number (0-based) + * @param rowIx number (0-based) */ - public void setFirstRow(short row) - { - field_1_first_row = row; + public void setFirstRow(int rowIx) { + checkRowBounds(rowIx); + field_1_first_row = rowIx; } /** * @return last row in the range (x2 in x1,y1-x2,y2) */ - public short getLastRow() + public int getLastRow() { return field_2_last_row; } /** - * @param row last row number in the area + * @param rowIx last row number in the area */ - public void setLastRow(short row) - { - field_2_last_row = row; + public void setLastRow(int rowIx) { + checkRowBounds(rowIx); + field_2_last_row = rowIx; } /** * @return the first column number in the area. */ - public short getFirstColumn() + public int getFirstColumn() { - return columnMask.getShortValue(field_3_first_column); + return columnMask.getValue(field_3_first_column); } /** @@ -165,7 +188,7 @@ public class AreaPtg */ public short getFirstColumnRaw() { - return field_3_first_column; + return (short) field_3_first_column; // TODO } /** @@ -181,7 +204,7 @@ public class AreaPtg * @param rel is relative or not. */ public void setFirstRowRelative(boolean rel) { - field_3_first_column=rowRelative.setShortBoolean(field_3_first_column,rel); + field_3_first_column=rowRelative.setBoolean(field_3_first_column,rel); } /** @@ -196,21 +219,21 @@ public class AreaPtg * set whether the first column is relative */ public void setFirstColRelative(boolean rel) { - field_3_first_column=colRelative.setShortBoolean(field_3_first_column,rel); + field_3_first_column=colRelative.setBoolean(field_3_first_column,rel); } /** * set the first column in the area */ - public void setFirstColumn(short column) - { - field_3_first_column=columnMask.setShortValue(field_3_first_column, column); + public void setFirstColumn(int colIx) { + checkColumnBounds(colIx); + field_3_first_column=columnMask.setValue(field_3_first_column, colIx); } /** * set the first column irespective of the bitmasks */ - public void setFirstColumnRaw(short column) + public void setFirstColumnRaw(int column) { field_3_first_column = column; } @@ -218,9 +241,9 @@ public class AreaPtg /** * @return lastcolumn in the area */ - public short getLastColumn() + public int getLastColumn() { - return columnMask.getShortValue(field_4_last_column); + return columnMask.getValue(field_4_last_column); } /** @@ -228,7 +251,7 @@ public class AreaPtg */ public short getLastColumnRaw() { - return field_4_last_column; + return (short) field_4_last_column; } /** @@ -245,7 +268,7 @@ public class AreaPtg * false */ public void setLastRowRelative(boolean rel) { - field_4_last_column=rowRelative.setShortBoolean(field_4_last_column,rel); + field_4_last_column=rowRelative.setBoolean(field_4_last_column,rel); } /** @@ -260,16 +283,16 @@ public class AreaPtg * set whether the last column should be relative or not */ public void setLastColRelative(boolean rel) { - field_4_last_column=colRelative.setShortBoolean(field_4_last_column,rel); + field_4_last_column=colRelative.setBoolean(field_4_last_column,rel); } /** * set the last column in the area */ - public void setLastColumn(short column) - { - field_4_last_column=columnMask.setShortValue(field_4_last_column, column); + public void setLastColumn(int colIx) { + checkColumnBounds(colIx); + field_4_last_column=columnMask.setValue(field_4_last_column, colIx); } /** @@ -279,11 +302,20 @@ public class AreaPtg { field_4_last_column = column; } - + public String toFormulaString(Workbook book) { - return (new CellReference(getFirstRow(),getFirstColumn(),!isFirstRowRelative(),!isFirstColRelative())).toString() + ":" + - (new CellReference(getLastRow(),getLastColumn(),!isLastRowRelative(),!isLastColRelative())).toString(); + return toFormulaString(this, book); + } + protected static String toFormulaString(AreaI area, Workbook book) { + CellReference topLeft = new CellReference(area.getFirstRow(),area.getFirstColumn(),!area.isFirstRowRelative(),!area.isFirstColRelative()); + CellReference botRight = new CellReference(area.getLastRow(),area.getLastColumn(),!area.isLastRowRelative(),!area.isLastColRelative()); + + if(AreaReference.isWholeColumnReference(topLeft, botRight)) { + return (new AreaReference(topLeft, botRight)).formatAsString(); + } else { + return topLeft.formatAsString() + ":" + botRight.formatAsString(); + } } public byte getDefaultOperandClass() { diff --git a/src/java/org/apache/poi/hssf/record/formula/AreaVPtg.java b/src/java/org/apache/poi/hssf/record/formula/AreaVPtg.java index 2974eec64e..42dc11fa32 100644 --- a/src/java/org/apache/poi/hssf/record/formula/AreaVPtg.java +++ b/src/java/org/apache/poi/hssf/record/formula/AreaVPtg.java @@ -36,7 +36,7 @@ import org.apache.poi.hssf.model.Workbook; * @author Jason Height (jheight at chariot dot net dot au) */ -public class AreaVPtg +public final class AreaVPtg extends AreaPtg { public final static short sid = 0x45; @@ -45,7 +45,7 @@ public class AreaVPtg //Required for clone methods } - public AreaVPtg(short firstRow, short lastRow, short firstColumn, short lastColumn, boolean firstRowRelative, boolean lastRowRelative, boolean firstColRelative, boolean lastColRelative) { + public AreaVPtg(int firstRow, int lastRow, int firstColumn, int lastColumn, boolean firstRowRelative, boolean lastRowRelative, boolean firstColRelative, boolean lastColRelative) { super(firstRow, lastRow, firstColumn, lastColumn, firstRowRelative, lastRowRelative, firstColRelative, lastColRelative); } diff --git a/src/java/org/apache/poi/hssf/record/formula/ArrayPtg.java b/src/java/org/apache/poi/hssf/record/formula/ArrayPtg.java index 372b1850e4..12166b7967 100644 --- a/src/java/org/apache/poi/hssf/record/formula/ArrayPtg.java +++ b/src/java/org/apache/poi/hssf/record/formula/ArrayPtg.java @@ -72,8 +72,10 @@ public class ArrayPtg extends Ptg field_7_reserved = in.readByte(); } - /** Read in the actual token (array) values. This occurs AFTER the last - * Ptg in the expression. + /** + * Read in the actual token (array) values. This occurs + * AFTER the last Ptg in the expression. + * See page 304-305 of Excel97-2007BinaryFileFormat(xls)Specification.pdf */ public void readTokenValues(RecordInputStream in) { token_1_columns = (short)(0x00ff & in.readByte()); @@ -88,18 +90,17 @@ public class ArrayPtg extends Ptg token_3_arrayValues = new Object[token_1_columns][token_2_rows]; for (int x=0;x#NULL! - Intersection of two cell ranges is empty */ + public static final ErrPtg NULL_INTERSECTION = new ErrPtg(EC.ERROR_NULL); + /** #DIV/0! - Division by zero */ + public static final ErrPtg DIV_ZERO = new ErrPtg(EC.ERROR_DIV_0); + /** #VALUE! - Wrong type of operand */ + public static final ErrPtg VALUE_INVALID = new ErrPtg(EC.ERROR_VALUE); + /** #REF! - Illegal or deleted cell reference */ + public static final ErrPtg REF_INVALID = new ErrPtg(EC.ERROR_REF); + /** #NAME? - Wrong function or range name */ + public static final ErrPtg NAME_INVALID = new ErrPtg(EC.ERROR_NAME); + /** #NUM! - Value range overflow */ + public static final ErrPtg NUM_ERROR = new ErrPtg(EC.ERROR_NUM); + /** #N/A - Argument or function not available */ + public static final ErrPtg N_A = new ErrPtg(EC.ERROR_NA); + + public static final short sid = 0x1c; private static final int SIZE = 2; - private byte field_1_error_code; + private int field_1_error_code; /** Creates new ErrPtg */ - public ErrPtg() - { + public ErrPtg(int errorCode) { + if(!HSSFErrorConstants.isValidCode(errorCode)) { + throw new IllegalArgumentException("Invalid error code (" + errorCode + ")"); + } + field_1_error_code = errorCode; } - - public ErrPtg(RecordInputStream in) - { - field_1_error_code = in.readByte(); + + public ErrPtg(RecordInputStream in) { + this(in.readByte()); } public void writeBytes(byte [] array, int offset) { array[offset] = (byte) (sid + ptgClass); - array[offset + 1] = field_1_error_code; + array[offset + 1] = (byte)field_1_error_code; } - public String toFormulaString(Workbook book) - { - switch(field_1_error_code) - { - case HSSFErrorConstants.ERROR_NULL: - return "#NULL!"; - case HSSFErrorConstants.ERROR_DIV_0: - return "#DIV/0!"; - case HSSFErrorConstants.ERROR_VALUE: - return "#VALUE!"; - case HSSFErrorConstants.ERROR_REF: - return "#REF!"; - case HSSFErrorConstants.ERROR_NAME: - return "#NAME?"; - case HSSFErrorConstants.ERROR_NUM: - return "#NUM!"; - case HSSFErrorConstants.ERROR_NA: - return "#N/A"; - } - - // Shouldn't happen anyway. Excel docs say that this is returned for all other codes. - return "#N/A"; + public String toFormulaString(Workbook book) { + return HSSFErrorConstants.getText(field_1_error_code); } - public int getSize() - { + public int getSize() { return SIZE; } - public byte getDefaultOperandClass() - { + public byte getDefaultOperandClass() { return Ptg.CLASS_VALUE; } public Object clone() { - ErrPtg ptg = new ErrPtg(); - ptg.field_1_error_code = field_1_error_code; - return ptg; + return new ErrPtg(field_1_error_code); + } + + public int getErrorCode() { + return field_1_error_code; } } diff --git a/src/java/org/apache/poi/hssf/record/formula/FuncPtg.java b/src/java/org/apache/poi/hssf/record/formula/FuncPtg.java index 410364971c..1123fc803a 100644 --- a/src/java/org/apache/poi/hssf/record/formula/FuncPtg.java +++ b/src/java/org/apache/poi/hssf/record/formula/FuncPtg.java @@ -62,6 +62,10 @@ public class FuncPtg extends AbstractFunctionPtg{ numParams=0; } + } + public FuncPtg(int functionIndex, int numberOfParameters) { + field_2_fnc_index = (short) functionIndex; + numParams = numberOfParameters; } public void writeBytes(byte[] array, int offset) { diff --git a/src/java/org/apache/poi/hssf/record/formula/IntPtg.java b/src/java/org/apache/poi/hssf/record/formula/IntPtg.java index 257c089df8..f4106b6aa8 100644 --- a/src/java/org/apache/poi/hssf/record/formula/IntPtg.java +++ b/src/java/org/apache/poi/hssf/record/formula/IntPtg.java @@ -1,4 +1,3 @@ - /* ==================================================================== Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with @@ -16,12 +15,6 @@ limitations under the License. ==================================================================== */ - -/* - * IntPtg.java - * - * Created on October 29, 2001, 7:37 PM - */ package org.apache.poi.hssf.record.formula; import org.apache.poi.util.LittleEndian; @@ -29,64 +22,45 @@ import org.apache.poi.hssf.model.Workbook; import org.apache.poi.hssf.record.RecordInputStream; /** - * Integer (unsigned short intger) + * Integer (unsigned short integer) * Stores an unsigned short value (java int) in a formula * @author Andrew C. Oliver (acoliver at apache dot org) * @author Jason Height (jheight at chariot dot net dot au) */ +public final class IntPtg extends Ptg { + // 16 bit unsigned integer + private static final int MIN_VALUE = 0x0000; + private static final int MAX_VALUE = 0xFFFF; + + /** + * Excel represents integers 0..65535 with the tInt token. + * @return true if the specified value is within the range of values + * IntPtg can represent. + */ + public static boolean isInRange(int i) { + return i>=MIN_VALUE && i <=MAX_VALUE; + } -public class IntPtg - extends Ptg -{ public final static int SIZE = 3; public final static byte sid = 0x1e; private int field_1_value; - private IntPtg() { - //Required for clone methods + public IntPtg(RecordInputStream in) { + this(in.readUShort()); } - public IntPtg(RecordInputStream in) - { - setValue(in.readUShort()); - } - - - // IntPtg should be able to create itself, shouldnt have to call setValue - public IntPtg(String formulaToken) { - setValue(Integer.parseInt(formulaToken)); - } - /** - * Sets the wrapped value. - * Normally you should call with a positive int. - */ - public void setValue(int value) - { - if(value < 0 || value > (Short.MAX_VALUE + 1)*2 ) - throw new IllegalArgumentException("Unsigned short is out of range: " + value); + public IntPtg(int value) { + if(!isInRange(value)) { + throw new IllegalArgumentException("value is out of range: " + value); + } field_1_value = value; } - /** - * Returns the value as a short, which may have - * been wrapped into negative numbers - */ - public int getValue() - { + public int getValue() { return field_1_value; } - /** - * Returns the value as an unsigned positive int. - */ - public int getValueAsInt() - { - if(field_1_value < 0) { - return (Short.MAX_VALUE + 1)*2 + field_1_value; - } - return field_1_value; - } public void writeBytes(byte [] array, int offset) { @@ -94,20 +68,25 @@ public class IntPtg LittleEndian.putUShort(array, offset + 1, getValue()); } - public int getSize() - { + public int getSize() { return SIZE; } - public String toFormulaString(Workbook book) - { - return "" + getValue(); + public String toFormulaString(Workbook book) { + return String.valueOf(getValue()); + } + public byte getDefaultOperandClass() { + return Ptg.CLASS_VALUE; } - public byte getDefaultOperandClass() {return Ptg.CLASS_VALUE;} - public Object clone() { - IntPtg ptg = new IntPtg(); - ptg.field_1_value = field_1_value; - return ptg; - } + public Object clone() { + return new IntPtg(field_1_value); + } + public String toString() { + StringBuffer sb = new StringBuffer(64); + sb.append(getClass().getName()).append(" ["); + sb.append(field_1_value); + sb.append("]"); + return sb.toString(); + } } diff --git a/src/java/org/apache/poi/hssf/record/formula/NamePtg.java b/src/java/org/apache/poi/hssf/record/formula/NamePtg.java index d418afa7e7..5405481a09 100644 --- a/src/java/org/apache/poi/hssf/record/formula/NamePtg.java +++ b/src/java/org/apache/poi/hssf/record/formula/NamePtg.java @@ -33,6 +33,7 @@ public class NamePtg { public final static short sid = 0x23; private final static int SIZE = 5; + /** one-based index to defined name record */ private short field_1_label_index; private short field_2_zero; // reserved must be 0 boolean xtra=false; @@ -42,24 +43,32 @@ public class NamePtg //Required for clone methods } - /** Creates new NamePtg */ - - public NamePtg(String name, Workbook book) - { - final short n = (short) (book.getNumNames() + 1); + /** + * Creates new NamePtg and sets its name index to that of the corresponding defined name record + * in the workbook. The search for the name record is case insensitive. If it is not found, + * it gets created. + */ + public NamePtg(String name, Workbook book) { + field_1_label_index = (short)(1+getOrCreateNameRecord(book, name)); // convert to 1-based + } + /** + * @return zero based index of the found or newly created defined name record. + */ + private static final int getOrCreateNameRecord(Workbook book, String name) { + // perhaps this logic belongs in Workbook + int countNames = book.getNumNames(); NameRecord rec; - for (short i = 1; i < n; i++) { - rec = book.getNameRecord(i - 1); - if (name.equals(rec.getNameText())) { - field_1_label_index = i; - return; + for (int i = 0; i < countNames; i++) { + rec = book.getNameRecord(i); + if (name.equalsIgnoreCase(rec.getNameText())) { + return i; } } rec = new NameRecord(); rec.setNameText(name); rec.setNameTextLength((byte) name.length()); book.addName(rec); - field_1_label_index = n; + return countNames; } /** Creates new NamePtg */ @@ -71,6 +80,13 @@ public class NamePtg field_2_zero = in.readShort(); //if (data[offset+6]==0) xtra=true; } + + /** + * @return zero based index to a defined name record in the LinkTable + */ + public int getIndex() { + return field_1_label_index-1; // convert to zero based + } public void writeBytes(byte [] array, int offset) { diff --git a/src/java/org/apache/poi/hssf/record/formula/NameXPtg.java b/src/java/org/apache/poi/hssf/record/formula/NameXPtg.java index b1f280a017..ccf5ab6fcd 100644 --- a/src/java/org/apache/poi/hssf/record/formula/NameXPtg.java +++ b/src/java/org/apache/poi/hssf/record/formula/NameXPtg.java @@ -25,13 +25,11 @@ import org.apache.poi.hssf.record.RecordInputStream; * * @author aviks */ - -public class NameXPtg extends Ptg -{ +public final class NameXPtg extends Ptg { public final static short sid = 0x39; private final static int SIZE = 7; - private short field_1_ixals; // index to externsheet record - private short field_2_ilbl; //index to name or externname table(1 based) + private short field_1_ixals; // index to REF entry in externsheet record + private short field_2_ilbl; //index to defined name or externname table(1 based) private short field_3_reserved; // reserved must be 0 @@ -41,13 +39,6 @@ public class NameXPtg extends Ptg /** Creates new NamePtg */ - public NameXPtg(String name) - { - //TODO - } - - /** Creates new NamePtg */ - public NameXPtg(RecordInputStream in) { field_1_ixals = in.readShort(); @@ -72,7 +63,8 @@ public class NameXPtg extends Ptg public String toFormulaString(Workbook book) { - return "NO IDEA - NAME"; + // -1 to convert definedNameIndex from 1-based to zero-based + return book.resolveNameXText(field_1_ixals, field_2_ilbl-1); } public byte getDefaultOperandClass() {return Ptg.CLASS_VALUE;} diff --git a/src/java/org/apache/poi/hssf/record/formula/Ref3DPtg.java b/src/java/org/apache/poi/hssf/record/formula/Ref3DPtg.java index 510eebb037..84ff659b33 100644 --- a/src/java/org/apache/poi/hssf/record/formula/Ref3DPtg.java +++ b/src/java/org/apache/poi/hssf/record/formula/Ref3DPtg.java @@ -18,16 +18,14 @@ package org.apache.poi.hssf.record.formula; -import org.apache.poi.util.LittleEndian; - -import org.apache.poi.hssf.util.RangeAddress; +import org.apache.poi.hssf.model.Workbook; +import org.apache.poi.hssf.record.RecordInputStream; import org.apache.poi.hssf.util.CellReference; +import org.apache.poi.hssf.util.RangeAddress; import org.apache.poi.hssf.util.SheetReferences; -import org.apache.poi.hssf.model.Workbook; import org.apache.poi.util.BitField; import org.apache.poi.util.BitFieldFactory; -import org.apache.poi.hssf.model.Workbook; -import org.apache.poi.hssf.record.RecordInputStream; +import org.apache.poi.util.LittleEndian; /** * Title: Reference 3D Ptg

    @@ -42,8 +40,14 @@ public class Ref3DPtg extends Ptg { public final static byte sid = 0x3a; private final static int SIZE = 7; // 6 + 1 for Ptg private short field_1_index_extern_sheet; - private short field_2_row; - private short field_3_column; + /** The row index - zero based unsigned 16 bit value */ + private int field_2_row; + /** Field 2 + * - lower 8 bits is the zero based unsigned byte column index + * - bit 16 - isRowRelative + * - bit 15 - isColumnRelative + */ + private int field_3_column; private BitField rowRelative = BitFieldFactory.getInstance(0x8000); private BitField colRelative = BitFieldFactory.getInstance(0x4000); @@ -58,8 +62,8 @@ public class Ref3DPtg extends Ptg { public Ref3DPtg(String cellref, short externIdx ) { CellReference c= new CellReference(cellref); - setRow((short) c.getRow()); - setColumn((short) c.getCol()); + setRow(c.getRow()); + setColumn(c.getCol()); setColRelative(!c.isColAbsolute()); setRowRelative(!c.isRowAbsolute()); setExternSheetIndex(externIdx); @@ -81,8 +85,8 @@ public class Ref3DPtg extends Ptg { public void writeBytes(byte [] array, int offset) { array[ 0 + offset ] = (byte) (sid + ptgClass); LittleEndian.putShort(array, 1 + offset , getExternSheetIndex()); - LittleEndian.putShort(array, 3 + offset , getRow()); - LittleEndian.putShort(array, 5 + offset , getColumnRaw()); + LittleEndian.putShort(array, 3 + offset , (short)getRow()); + LittleEndian.putShort(array, 5 + offset , (short)getColumnRaw()); } public int getSize() { @@ -97,19 +101,19 @@ public class Ref3DPtg extends Ptg { field_1_index_extern_sheet = index; } - public short getRow() { + public int getRow() { return field_2_row; } - public void setRow(short row) { + public void setRow(int row) { field_2_row = row; } - public short getColumn() { - return ( short ) (field_3_column & 0xFF); + public int getColumn() { + return field_3_column & 0xFF; } - public short getColumnRaw() { + public int getColumnRaw() { return field_3_column; } @@ -119,7 +123,7 @@ public class Ref3DPtg extends Ptg { } public void setRowRelative(boolean rel) { - field_3_column=rowRelative.setShortBoolean(field_3_column,rel); + field_3_column=rowRelative.setBoolean(field_3_column,rel); } public boolean isColRelative() @@ -128,7 +132,7 @@ public class Ref3DPtg extends Ptg { } public void setColRelative(boolean rel) { - field_3_column=colRelative.setShortBoolean(field_3_column,rel); + field_3_column=colRelative.setBoolean(field_3_column,rel); } public void setColumn(short column) { field_3_column &= 0xFF00; @@ -183,7 +187,7 @@ public class Ref3DPtg extends Ptg { SheetNameFormatter.appendFormat(retval, sheetName); retval.append( '!' ); } - retval.append((new CellReference(getRow(),getColumn(),!isRowRelative(),!isColRelative())).toString()); + retval.append((new CellReference(getRow(),getColumn(),!isRowRelative(),!isColRelative())).formatAsString()); return retval.toString(); } @@ -197,5 +201,4 @@ public class Ref3DPtg extends Ptg { ptg.setClass(ptgClass); return ptg; } - } diff --git a/src/java/org/apache/poi/hssf/record/formula/RefAPtg.java b/src/java/org/apache/poi/hssf/record/formula/RefAPtg.java index 996f40e393..596b386235 100644 --- a/src/java/org/apache/poi/hssf/record/formula/RefAPtg.java +++ b/src/java/org/apache/poi/hssf/record/formula/RefAPtg.java @@ -1,4 +1,3 @@ - /* ==================================================================== Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with @@ -16,34 +15,23 @@ limitations under the License. ==================================================================== */ -/* - * ValueReferencePtg.java - * - * Created on November 21, 2001, 5:27 PM - */ package org.apache.poi.hssf.record.formula; -import org.apache.poi.util.LittleEndian; -import org.apache.poi.util.BitField; - import org.apache.poi.hssf.record.RecordInputStream; -import org.apache.poi.hssf.util.CellReference; -import org.apache.poi.hssf.model.Workbook; /** * RefNAPtg * @author Jason Height (jheight at chariot dot net dot au) */ -public class RefAPtg extends ReferencePtg -{ +public final class RefAPtg extends ReferencePtg { public final static byte sid = 0x64; protected RefAPtg() { super(); } - public RefAPtg(short row, short column, boolean isRowRelative, boolean isColumnRelative) { + public RefAPtg(int row, int column, boolean isRowRelative, boolean isColumnRelative) { super(row, column, isRowRelative, isColumnRelative); } diff --git a/src/java/org/apache/poi/hssf/record/formula/RefVPtg.java b/src/java/org/apache/poi/hssf/record/formula/RefVPtg.java index b9d55a09ec..8a6b2c03b4 100644 --- a/src/java/org/apache/poi/hssf/record/formula/RefVPtg.java +++ b/src/java/org/apache/poi/hssf/record/formula/RefVPtg.java @@ -1,4 +1,3 @@ - /* ==================================================================== Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with @@ -18,27 +17,20 @@ package org.apache.poi.hssf.record.formula; -import org.apache.poi.util.LittleEndian; -import org.apache.poi.util.BitField; - import org.apache.poi.hssf.record.RecordInputStream; -import org.apache.poi.hssf.util.CellReference; -import org.apache.poi.hssf.model.Workbook; /** * RefVPtg * @author Jason Height (jheight at chariot dot net dot au) */ - -public class RefVPtg extends ReferencePtg -{ +public final class RefVPtg extends ReferencePtg { public final static byte sid = 0x44; protected RefVPtg() { super(); } - public RefVPtg(short row, short column, boolean isRowRelative, boolean isColumnRelative) { + public RefVPtg(int row, int column, boolean isRowRelative, boolean isColumnRelative) { super(row, column, isRowRelative, isColumnRelative); } diff --git a/src/java/org/apache/poi/hssf/record/formula/ReferencePtg.java b/src/java/org/apache/poi/hssf/record/formula/ReferencePtg.java index df3e5a70bc..4983c9d070 100644 --- a/src/java/org/apache/poi/hssf/record/formula/ReferencePtg.java +++ b/src/java/org/apache/poi/hssf/record/formula/ReferencePtg.java @@ -31,26 +31,22 @@ import org.apache.poi.hssf.record.RecordInputStream; * @author Jason Height (jheight at chariot dot net dot au) */ -public class ReferencePtg extends Ptg -{ +public class ReferencePtg extends Ptg { private final static int SIZE = 5; public final static byte sid = 0x24; private final static int MAX_ROW_NUMBER = 65536; - //public final static byte sid = 0x44; - /** - * The row number, between 0 and 65535, but stored as a signed - * short between -32767 and 32768. - * Take care about which version you fetch back! + /** The row index - zero based unsigned 16 bit value */ + private int field_1_row; + /** Field 2 + * - lower 8 bits is the zero based unsigned byte column index + * - bit 16 - isRowRelative + * - bit 15 - isColumnRelative */ - private short field_1_row; - /** - * The column number, between 0 and ?? - */ - private short field_2_col; - private BitField rowRelative = BitFieldFactory.getInstance(0x8000); - private BitField colRelative = BitFieldFactory.getInstance(0x4000); - private BitField column = BitFieldFactory.getInstance(0x3FFF); + private int field_2_col; + private static final BitField rowRelative = BitFieldFactory.getInstance(0x8000); + private static final BitField colRelative = BitFieldFactory.getInstance(0x4000); + private static final BitField column = BitFieldFactory.getInstance(0x00FF); protected ReferencePtg() { //Required for clone methods @@ -62,13 +58,13 @@ public class ReferencePtg extends Ptg */ public ReferencePtg(String cellref) { CellReference c= new CellReference(cellref); - setRow((short) c.getRow()); - setColumn((short) c.getCol()); + setRow(c.getRow()); + setColumn(c.getCol()); setColRelative(!c.isColAbsolute()); setRowRelative(!c.isRowAbsolute()); } - public ReferencePtg(short row, short column, boolean isRowRelative, boolean isColumnRelative) { + public ReferencePtg(int row, int column, boolean isRowRelative, boolean isColumnRelative) { setRow(row); setColumn(column); setRowRelative(isRowRelative); @@ -79,8 +75,8 @@ public class ReferencePtg extends Ptg public ReferencePtg(RecordInputStream in) { - field_1_row = in.readShort(); - field_2_col = in.readShort(); + field_1_row = in.readUShort(); + field_2_col = in.readUShort(); } public String getRefPtgName() { @@ -104,33 +100,23 @@ public class ReferencePtg extends Ptg { array[offset] = (byte) (sid + ptgClass); - LittleEndian.putShort(array,offset+1,field_1_row); - LittleEndian.putShort(array,offset+3,field_2_col); + LittleEndian.putShort(array, offset+1, (short)field_1_row); + LittleEndian.putShort(array, offset+3, (short)field_2_col); } - public void setRow(short row) - { - field_1_row = row; - } public void setRow(int row) { if(row < 0 || row >= MAX_ROW_NUMBER) { throw new IllegalArgumentException("The row number, when specified as an integer, must be between 0 and " + MAX_ROW_NUMBER); } - - // Save, wrapping as needed - if(row > Short.MAX_VALUE) { - field_1_row = (short)(row - MAX_ROW_NUMBER); - } else { - field_1_row = (short)row; - } + field_1_row = row; } /** * Returns the row number as a short, which will be * wrapped (negative) for values between 32769 and 65535 */ - public short getRow() + public int getRow() { return field_1_row; } @@ -151,7 +137,7 @@ public class ReferencePtg extends Ptg } public void setRowRelative(boolean rel) { - field_2_col=rowRelative.setShortBoolean(field_2_col,rel); + field_2_col=rowRelative.setBoolean(field_2_col,rel); } public boolean isColRelative() @@ -160,27 +146,29 @@ public class ReferencePtg extends Ptg } public void setColRelative(boolean rel) { - field_2_col=colRelative.setShortBoolean(field_2_col,rel); + field_2_col=colRelative.setBoolean(field_2_col,rel); } - public void setColumnRaw(short col) + public void setColumnRaw(int col) { field_2_col = col; } - public short getColumnRaw() + public int getColumnRaw() { return field_2_col; } - public void setColumn(short col) + public void setColumn(int col) { - field_2_col = column.setShortValue(field_2_col, col); + if(col < 0 || col > 0x100) { + throw new IllegalArgumentException("Specified colIx (" + col + ") is out of range"); + } + field_2_col = column.setValue(field_2_col, col); } - public short getColumn() - { - return column.getShortValue(field_2_col); + public int getColumn() { + return column.getValue(field_2_col); } public int getSize() @@ -191,7 +179,7 @@ public class ReferencePtg extends Ptg public String toFormulaString(Workbook book) { //TODO -- should we store a cellreference instance in this ptg?? but .. memory is an issue, i believe! - return (new CellReference(getRowAsInt(),getColumn(),!isRowRelative(),!isColRelative())).toString(); + return (new CellReference(getRowAsInt(),getColumn(),!isRowRelative(),!isColRelative())).formatAsString(); } public byte getDefaultOperandClass() { diff --git a/src/java/org/apache/poi/hssf/record/formula/SheetNameFormatter.java b/src/java/org/apache/poi/hssf/record/formula/SheetNameFormatter.java index ba796db3b2..8e47cbe7a0 100755 --- a/src/java/org/apache/poi/hssf/record/formula/SheetNameFormatter.java +++ b/src/java/org/apache/poi/hssf/record/formula/SheetNameFormatter.java @@ -26,7 +26,7 @@ import java.util.regex.Pattern; * * @author Josh Micich */ -final class SheetNameFormatter { +public final class SheetNameFormatter { private static final String BIFF8_LAST_COLUMN = "IV"; private static final int BIFF8_LAST_COLUMN_TEXT_LEN = BIFF8_LAST_COLUMN.length(); diff --git a/src/java/org/apache/poi/hssf/usermodel/HSSFCell.java b/src/java/org/apache/poi/hssf/usermodel/HSSFCell.java index 67f4557972..f906e91a49 100644 --- a/src/java/org/apache/poi/hssf/usermodel/HSSFCell.java +++ b/src/java/org/apache/poi/hssf/usermodel/HSSFCell.java @@ -456,7 +456,7 @@ public class HSSFCell implements Cell boolRec.setColumn(col); if (setValue) { - boolRec.setValue(getBooleanCellValue()); + boolRec.setValue(convertCellValueToBoolean()); } boolRec.setXFIndex(styleIndex); boolRec.setRow(row); @@ -644,7 +644,7 @@ public class HSSFCell implements Cell //only set to default if there is no extended format index already set if (rec.getXFIndex() == (short)0) rec.setXFIndex(( short ) 0x0f); - FormulaParser fp = new FormulaParser(formula+";",book); + FormulaParser fp = new FormulaParser(formula, book); fp.parse(); Ptg[] ptg = fp.getRPNPtg(); int size = 0; @@ -830,6 +830,34 @@ public class HSSFCell implements Cell } (( BoolErrRecord ) record).setValue(value); } + /** + * Chooses a new boolean value for the cell when its type is changing.

    + * + * Usually the caller is calling setCellType() with the intention of calling + * setCellValue(boolean) straight afterwards. This method only exists to give + * the cell a somewhat reasonable value until the setCellValue() call (if at all). + * TODO - perhaps a method like setCellTypeAndValue(int, Object) should be introduced to avoid this + */ + private boolean convertCellValueToBoolean() { + + switch (cellType) { + case CELL_TYPE_BOOLEAN: + return (( BoolErrRecord ) record).getBooleanValue(); + case CELL_TYPE_STRING: + return Boolean.valueOf(((StringRecord)record).getString()).booleanValue(); + case CELL_TYPE_NUMERIC: + return ((NumberRecord)record).getValue() != 0; + + // All other cases convert to false + // These choices are not well justified. + case CELL_TYPE_FORMULA: + // should really evaluate, but HSSFCell can't call HSSFFormulaEvaluator + case CELL_TYPE_ERROR: + case CELL_TYPE_BLANK: + return false; + } + throw new RuntimeException("Unexpected cell type (" + cellType + ")"); + } /** * get the value of the cell as a boolean. For strings, numbers, and errors, we throw an exception. diff --git a/src/java/org/apache/poi/hssf/usermodel/HSSFErrorConstants.java b/src/java/org/apache/poi/hssf/usermodel/HSSFErrorConstants.java index 1f5ec13c3a..89c25d1e87 100644 --- a/src/java/org/apache/poi/hssf/usermodel/HSSFErrorConstants.java +++ b/src/java/org/apache/poi/hssf/usermodel/HSSFErrorConstants.java @@ -15,26 +15,68 @@ limitations under the License. ==================================================================== */ - -/* - * HSSFErrorConstants.java - * - * Created on January 19, 2002, 9:30 AM - */ package org.apache.poi.hssf.usermodel; /** - * contains constants representing Excel error codes. + * Contains raw Excel error codes (as defined in OOO's excelfileformat.pdf (2.5.6) + * * @author Michael Harhen */ - -public interface HSSFErrorConstants -{ - public static final byte ERROR_NULL = 0x00; // #NULL! - public static final byte ERROR_DIV_0 = 0x07; // #DIV/0! - public static final byte ERROR_VALUE = 0x0f; // #VALUE! - public static final byte ERROR_REF = 0x17; // #REF! - public static final byte ERROR_NAME = 0x1d; // #NAME? - public static final byte ERROR_NUM = 0x24; // #NUM! - public static final byte ERROR_NA = 0x2a; // #N/A +public final class HSSFErrorConstants { + private HSSFErrorConstants() { + // no instances of this class + } + + /** #NULL! - Intersection of two cell ranges is empty */ + public static final int ERROR_NULL = 0x00; + /** #DIV/0! - Division by zero */ + public static final int ERROR_DIV_0 = 0x07; + /** #VALUE! - Wrong type of operand */ + public static final int ERROR_VALUE = 0x0F; + /** #REF! - Illegal or deleted cell reference */ + public static final int ERROR_REF = 0x17; + /** #NAME? - Wrong function or range name */ + public static final int ERROR_NAME = 0x1D; + /** #NUM! - Value range overflow */ + public static final int ERROR_NUM = 0x24; + /** #N/A - Argument or function not available */ + public static final int ERROR_NA = 0x2A; + + + /** + * @return Standard Excel error literal for the specified error code. + * @throws IllegalArgumentException if the specified error code is not one of the 7 + * standard error codes + */ + public static final String getText(int errorCode) { + switch(errorCode) { + case ERROR_NULL: return "#NULL!"; + case ERROR_DIV_0: return "#DIV/0!"; + case ERROR_VALUE: return "#VALUE!"; + case ERROR_REF: return "#REF!"; + case ERROR_NAME: return "#NAME?"; + case ERROR_NUM: return "#NUM!"; + case ERROR_NA: return "#N/A"; + } + throw new IllegalArgumentException("Bad error code (" + errorCode + ")"); + } + + /** + * @return true if the specified error code is a standard Excel error code. + */ + public static final boolean isValidCode(int errorCode) { + // This method exists because it would be bad to force clients to catch + // IllegalArgumentException if there were potential for passing an invalid error code. + switch(errorCode) { + case ERROR_NULL: + case ERROR_DIV_0: + case ERROR_VALUE: + case ERROR_REF: + case ERROR_NAME: + case ERROR_NUM: + case ERROR_NA: + return true; + } + return false; + } } diff --git a/src/java/org/apache/poi/hssf/usermodel/HSSFPalette.java b/src/java/org/apache/poi/hssf/usermodel/HSSFPalette.java index 42773d4a33..0a31728899 100644 --- a/src/java/org/apache/poi/hssf/usermodel/HSSFPalette.java +++ b/src/java/org/apache/poi/hssf/usermodel/HSSFPalette.java @@ -100,9 +100,11 @@ public class HSSFPalette implements Palette for (short i = (short) PaletteRecord.FIRST_COLOR_INDEX; b != null; b = palette.getColor(++i)) { - int colorDistance = red - b[0] + green - b[1] + blue - b[2]; + int colorDistance = Math.abs(red - b[0]) + + Math.abs(green - b[1]) + Math.abs(blue - b[2]); if (colorDistance < minColorDistance) { + minColorDistance = colorDistance; result = getColor(i); } } diff --git a/src/java/org/apache/poi/hssf/usermodel/HSSFRow.java b/src/java/org/apache/poi/hssf/usermodel/HSSFRow.java index 0c9807b5aa..54229a16ab 100644 --- a/src/java/org/apache/poi/hssf/usermodel/HSSFRow.java +++ b/src/java/org/apache/poi/hssf/usermodel/HSSFRow.java @@ -159,28 +159,25 @@ public class HSSFRow * @param cell to remove */ public void removeCell(Cell cell) { - removeCell(cell, true); + removeCell((HSSFCell) cell, true); } - - private void removeCell(Cell cell, boolean alsoRemoveRecords) { - - HSSFCell hcell = (HSSFCell) cell; + private void removeCell(HSSFCell cell, boolean alsoRemoveRecords) { if(alsoRemoveRecords) { - CellValueRecordInterface cval = hcell.getCellValueRecord(); + CellValueRecordInterface cval = cell.getCellValueRecord(); sheet.removeValueRecord(getRowNum(), cval); } - short column=hcell.getCellNum(); - if(hcell!=null && column + * The area reference must be contiguous (i.e. represent a single rectangle, not a union of rectangles) */ public AreaReference(String reference) { if(! isContiguous(reference)) { - throw new IllegalArgumentException("References passed to the AreaReference must be contiguous, use generateContiguous(ref) if you have non-contiguous references"); + throw new IllegalArgumentException( + "References passed to the AreaReference must be contiguous, " + + "use generateContiguous(ref) if you have non-contiguous references"); } - String[] refs = seperateAreaRefs(reference); - dim = refs.length; - cells = new CellReference[dim]; - for (int i=0;i= 'A' && parts[0].charAt(0) <= 'Z' && + parts[1].charAt(0) >= 'A' && parts[1].charAt(0) <= 'Z') { + // Represented internally as x$1 to x$65536 + // which is the maximum range of rows + parts[0] = parts[0] + "$1"; + parts[1] = parts[1] + "$65536"; + } + + _firstCell = new CellReference(parts[0]); + + if(parts.length == 2) { + _lastCell = new CellReference(parts[1]); + _isSingleCell = false; + } else { + _lastCell = _firstCell; + _isSingleCell = true; } } + + /** + * Creates an area ref from a pair of Cell References. + */ + public AreaReference(CellReference topLeft, CellReference botRight) { + _firstCell = topLeft; + _lastCell = botRight; + _isSingleCell = false; + } /** * Is the reference for a contiguous (i.e. @@ -57,6 +92,24 @@ private int dim; } return false; } + + /** + * Is the reference for a whole-column reference, + * such as C:C or D:G ? + */ + public static boolean isWholeColumnReference(CellReference topLeft, CellReference botRight) { + // These are represented as something like + // C$1:C$65535 or D$1:F$0 + // i.e. absolute from 1st row to 0th one + if(topLeft.getRow() == 0 && topLeft.isRowAbsolute() && + botRight.getRow() == 65535 && botRight.isRowAbsolute()) { + return true; + } + return false; + } + public boolean isWholeColumnReference() { + return isWholeColumnReference(_firstCell, _lastCell); + } /** * Takes a non-contiguous area reference, and @@ -73,75 +126,171 @@ private int dim; return (AreaReference[])refs.toArray(new AreaReference[refs.size()]); } - //not sure if we need to be flexible here! - /** return the dimensions of this area - **/ - public int getDim() { - return dim; + /** + * @return false if this area reference involves more than one cell + */ + public boolean isSingleCell() { + return _isSingleCell; } - /** - * Return the cell references that define this area - * (i.e. the two corners) + + /** + * @return the first cell reference which defines this area. Usually this cell is in the upper + * left corner of the area (but this is not a requirement). */ - public CellReference[] getCells() { - return cells; + public CellReference getFirstCell() { + return _firstCell; + } + + /** + * Note - if this area reference refers to a single cell, the return value of this method will + * be identical to that of getFirstCell() + * @return the second cell reference which defines this area. For multi-cell areas, this is + * cell diagonally opposite the 'first cell'. Usually this cell is in the lower right corner + * of the area (but this is not a requirement). + */ + public CellReference getLastCell() { + return _lastCell; } /** * Returns a reference to every cell covered by this area */ public CellReference[] getAllReferencedCells() { // Special case for single cell reference - if(cells.length == 1) { - return cells; + if(_isSingleCell) { + return new CellReference[] { _firstCell, }; } + // Interpolate between the two - int minRow = Math.min(cells[0].getRow(), cells[1].getRow()); - int maxRow = Math.max(cells[0].getRow(), cells[1].getRow()); - int minCol = Math.min(cells[0].getCol(), cells[1].getCol()); - int maxCol = Math.max(cells[0].getCol(), cells[1].getCol()); + int minRow = Math.min(_firstCell.getRow(), _lastCell.getRow()); + int maxRow = Math.max(_firstCell.getRow(), _lastCell.getRow()); + int minCol = Math.min(_firstCell.getCol(), _lastCell.getCol()); + int maxCol = Math.max(_firstCell.getCol(), _lastCell.getCol()); + String sheetName = _firstCell.getSheetName(); ArrayList refs = new ArrayList(); for(int row=minRow; row<=maxRow; row++) { for(int col=minCol; col<=maxCol; col++) { - CellReference ref = new CellReference(row, col, cells[0].isRowAbsolute(), cells[0].isColAbsolute()); - ref.setSheetName(cells[0].getSheetName()); + CellReference ref = new CellReference(sheetName, row, col, _firstCell.isRowAbsolute(), _firstCell.isColAbsolute()); refs.add(ref); } } return (CellReference[])refs.toArray(new CellReference[refs.size()]); } - public String toString() { - StringBuffer retval = new StringBuffer(); - for (int i=0;i + * ResultComment + * A1:A1Single cell area reference without sheet + * A1:$C$1Multi-cell area reference without sheet + * Sheet1!A$1:B4Standard sheet name + * 'O''Brien''s Sales'!B5:C6' Sheet name with special characters + * + * @return the text representation of this area reference as it would appear in a formula. + */ + public String formatAsString() { + // Special handling for whole-column references + if(isWholeColumnReference()) { + return + CellReference.convertNumToColString(_firstCell.getCol()) + + ":" + + CellReference.convertNumToColString(_lastCell.getCol()); + } + + StringBuffer sb = new StringBuffer(32); + sb.append(_firstCell.formatAsString()); + if(!_isSingleCell) { + sb.append(CELL_DELIMITER); + if(_lastCell.getSheetName() == null) { + sb.append(_lastCell.formatAsString()); + } else { + // don't want to include the sheet name twice + _lastCell.appendCellReference(sb); + } } - retval.deleteCharAt(0); - return retval.toString(); + return sb.toString(); + } + public String toString() { + StringBuffer sb = new StringBuffer(64); + sb.append(getClass().getName()).append(" ["); + sb.append(formatAsString()); + sb.append("]"); + return sb.toString(); } /** - * seperates Area refs in two parts and returns them as seperate elements in a - * String array + * Separates Area refs in two parts and returns them as separate elements in a String array, + * each qualified with the sheet name (if present) + * + * @return array with one or two elements. never null */ - private String[] seperateAreaRefs(String reference) { - String[] retval = null; - - int length = reference.length(); - - int loc = reference.indexOf(':',0); - if(loc == -1){ - retval = new String[1]; - retval[0] = reference; + private static String[] separateAreaRefs(String reference) { + // TODO - refactor cell reference parsing logic to one place. + // Current known incarnations: + // FormulaParser.GetName() + // CellReference.separateRefParts() + // AreaReference.separateAreaRefs() (here) + // SheetNameFormatter.format() (inverse) + + + int len = reference.length(); + int delimiterPos = -1; + boolean insideDelimitedName = false; + for(int i=0; i=0) { + throw new IllegalArgumentException("More than one cell delimiter '" + + CELL_DELIMITER + "' appears in area reference '" + reference + "'"); + } + delimiterPos = i; + } + default: + continue; + case SPECIAL_NAME_DELIMITER: + // fall through + } + if(!insideDelimitedName) { + insideDelimitedName = true; + continue; + } + + if(i >= len-1) { + // reference ends with the delimited name. + // Assume names like: "Sheet1!'A1'" are never legal. + throw new IllegalArgumentException("Area reference '" + reference + + "' ends with special name delimiter '" + SPECIAL_NAME_DELIMITER + "'"); + } + if(reference.charAt(i+1) == SPECIAL_NAME_DELIMITER) { + // two consecutive quotes is the escape sequence for a single one + i++; // skip this and keep parsing the special name + } else { + // this is the end of the delimited name + insideDelimitedName = false; + } + } + if(delimiterPos < 0) { + return new String[] { reference, }; } - else{ - retval = new String[2]; - int sheetStart = reference.indexOf("!"); - retval[0] = reference.substring(0, sheetStart+1) + reference.substring(sheetStart + 1,loc); - retval[1] = reference.substring(0, sheetStart+1) + reference.substring(loc+1); + String partA = reference.substring(0, delimiterPos); + String partB = reference.substring(delimiterPos+1); + if(partB.indexOf(SHEET_NAME_DELIMITER) >=0) { + // TODO - are references like "Sheet1!A1:Sheet1:B2" ever valid? + // FormulaParser has code to handle that. + + throw new RuntimeException("Unexpected " + SHEET_NAME_DELIMITER + + " in second cell reference of '" + reference + "'"); + } + + int plingPos = partA.lastIndexOf(SHEET_NAME_DELIMITER); + if(plingPos < 0) { + return new String [] { partA, partB, }; } - return retval; + + String sheetName = partA.substring(0, plingPos + 1); // +1 to include delimiter + + return new String [] { partA, sheetName + partB, }; } } \ No newline at end of file diff --git a/src/java/org/apache/poi/hssf/util/CellReference.java b/src/java/org/apache/poi/hssf/util/CellReference.java index def58472a8..47d579d94f 100644 --- a/src/java/org/apache/poi/hssf/util/CellReference.java +++ b/src/java/org/apache/poi/hssf/util/CellReference.java @@ -15,75 +15,99 @@ limitations under the License. ==================================================================== */ - package org.apache.poi.hssf.util; +import org.apache.poi.hssf.record.formula.SheetNameFormatter; + /** * * @author Avik Sengupta * @author Dennis Doubleday (patch to seperateRowColumns()) */ -public class CellReference { +public final class CellReference { + /** The character ($) that signifies a row or column value is absolute instead of relative */ + private static final char ABSOLUTE_REFERENCE_MARKER = '$'; + /** The character (!) that separates sheet names from cell references */ + private static final char SHEET_NAME_DELIMITER = '!'; + /** The character (') used to quote sheet names when they contain special characters */ + private static final char SPECIAL_NAME_DELIMITER = '\''; + - /** Creates new CellReference */ - private int row; - private int col; - private String sheetName; - private boolean rowAbs; - private boolean colAbs; + private final int _rowIndex; + private final int _colIndex; + private final String _sheetName; + private final boolean _isRowAbs; + private final boolean _isColAbs; + /** + * Create an cell ref from a string representation. Sheet names containing special characters should be + * delimited and escaped as per normal syntax rules for formulas. + */ public CellReference(String cellRef) { String[] parts = separateRefParts(cellRef); - sheetName = parts[0]; - String ref = parts[1]; - if ((ref == null)||("".equals(ref))) - throw new IllegalArgumentException("Invalid Formula cell reference: '"+cellRef+"'"); - if (ref.charAt(0) == '$') { - colAbs=true; - ref=ref.substring(1); - } - col = convertColStringToNum(ref); - ref=parts[2]; - if ((ref == null)||("".equals(ref))) - throw new IllegalArgumentException("Invalid Formula cell reference: '"+cellRef+"'"); - if (ref.charAt(0) == '$') { - rowAbs=true; - ref=ref.substring(1); - } - row = Integer.parseInt(ref)-1; - } - - public CellReference(int pRow, int pCol) { - this(pRow,pCol,false,false); + _sheetName = parts[0]; + String colRef = parts[1]; + if (colRef.length() < 1) { + throw new IllegalArgumentException("Invalid Formula cell reference: '"+cellRef+"'"); + } + _isColAbs = colRef.charAt(0) == '$'; + if (_isColAbs) { + colRef=colRef.substring(1); + } + _colIndex = convertColStringToNum(colRef); + + String rowRef=parts[2]; + if (rowRef.length() < 1) { + throw new IllegalArgumentException("Invalid Formula cell reference: '"+cellRef+"'"); + } + _isRowAbs = rowRef.charAt(0) == '$'; + if (_isRowAbs) { + rowRef=rowRef.substring(1); + } + _rowIndex = Integer.parseInt(rowRef)-1; // -1 to convert 1-based to zero-based } public CellReference(int pRow, int pCol, boolean pAbsRow, boolean pAbsCol) { - row=pRow;col=pCol; - rowAbs = pAbsRow; - colAbs=pAbsCol; - + this(null, pRow, pCol, pAbsRow, pAbsCol); } - - public int getRow(){return row;} - public short getCol(){return (short) col;} - public boolean isRowAbsolute(){return rowAbs;} - public boolean isColAbsolute(){return colAbs;} - public String getSheetName(){return sheetName;} - - protected void setSheetName(String sheetName) { - this.sheetName = sheetName; + public CellReference(String pSheetName, int pRow, int pCol, boolean pAbsRow, boolean pAbsCol) { + // TODO - "-1" is a special value being temporarily used for whole row and whole column area references. + // so these checks are currently N.Q.R. + if(pRow < -1) { + throw new IllegalArgumentException("row index may not be negative"); + } + if(pCol < -1) { + throw new IllegalArgumentException("column index may not be negative"); + } + _sheetName = pSheetName; + _rowIndex=pRow; + _colIndex=pCol; + _isRowAbs = pAbsRow; + _isColAbs=pAbsCol; } + public int getRow(){return _rowIndex;} + public short getCol(){return (short) _colIndex;} + public boolean isRowAbsolute(){return _isRowAbs;} + public boolean isColAbsolute(){return _isColAbs;} + /** + * @return possibly null if this is a 2D reference. Special characters are not + * escaped or delimited + */ + public String getSheetName(){ + return _sheetName; + } + /** * takes in a column reference portion of a CellRef and converts it from * ALPHA-26 number format to 0-based base 10. */ private int convertColStringToNum(String ref) { - int len = ref.length(); + int lastIx = ref.length()-1; int retval=0; int pos = 0; - for (int k = ref.length()-1; k > -1; k--) { + for (int k = lastIx; k > -1; k--) { char thechar = ref.charAt(k); if ( pos == 0) { retval += (Character.getNumericValue(thechar)-9); @@ -97,42 +121,86 @@ public class CellReference { /** - * Seperates the row from the columns and returns an array. Element in - * position one is the substring containing the columns still in ALPHA-26 - * number format. + * Separates the row from the columns and returns an array of three Strings. The first element + * is the sheet name. Only the first element may be null. The second element in is the column + * name still in ALPHA-26 number format. The third element is the row. */ - private String[] separateRefParts(String reference) { - - // Look for end of sheet name. This will either set - // start to 0 (if no sheet name present) or the - // index after the sheet reference ends. - String retval[] = new String[3]; - - int start = reference.indexOf("!"); - if (start != -1) retval[0] = reference.substring(0, start); - start += 1; + private static String[] separateRefParts(String reference) { + + int plingPos = reference.lastIndexOf(SHEET_NAME_DELIMITER); + String sheetName = parseSheetName(reference, plingPos); + int start = plingPos+1; int length = reference.length(); - char[] chars = reference.toCharArray(); int loc = start; - if (chars[loc]=='$') loc++; - for (; loc < chars.length; loc++) { - if (Character.isDigit(chars[loc]) || chars[loc] == '$') { + // skip initial dollars + if (reference.charAt(loc)==ABSOLUTE_REFERENCE_MARKER) { + loc++; + } + // step over column name chars until first digit (or dollars) for row number. + for (; loc < length; loc++) { + char ch = reference.charAt(loc); + if (Character.isDigit(ch) || ch == ABSOLUTE_REFERENCE_MARKER) { break; } } + return new String[] { + sheetName, + reference.substring(start,loc), + reference.substring(loc), + }; + } - retval[1] = reference.substring(start,loc); - retval[2] = reference.substring(loc); - return retval; + private static String parseSheetName(String reference, int indexOfSheetNameDelimiter) { + if(indexOfSheetNameDelimiter < 0) { + return null; + } + + boolean isQuoted = reference.charAt(0) == SPECIAL_NAME_DELIMITER; + if(!isQuoted) { + return reference.substring(0, indexOfSheetNameDelimiter); + } + int lastQuotePos = indexOfSheetNameDelimiter-1; + if(reference.charAt(lastQuotePos) != SPECIAL_NAME_DELIMITER) { + throw new RuntimeException("Mismatched quotes: (" + reference + ")"); + } + + // TODO - refactor cell reference parsing logic to one place. + // Current known incarnations: + // FormulaParser.GetName() + // CellReference.parseSheetName() (here) + // AreaReference.separateAreaRefs() + // SheetNameFormatter.format() (inverse) + + StringBuffer sb = new StringBuffer(indexOfSheetNameDelimiter); + + for(int i=1; i D */ - private static String convertNumToColString(int col) { + protected static String convertNumToColString(int col) { String retval = null; int mod = col % 26; int div = col / 26; @@ -148,14 +216,46 @@ public class CellReference { return retval; } - + /** + * Example return values: + * + * + * + * + * + *
    ResultComment
    A1Cell reference without sheet
    Sheet1!A1Standard sheet name
    'O''Brien''s Sales'!A1' Sheet name with special characters
    + * @return the text representation of this cell reference as it would appear in a formula. + */ + public String formatAsString() { + StringBuffer sb = new StringBuffer(32); + if(_sheetName != null) { + SheetNameFormatter.appendFormat(sb, _sheetName); + sb.append(SHEET_NAME_DELIMITER); + } + appendCellReference(sb); + return sb.toString(); + } + public String toString() { - StringBuffer retval = new StringBuffer(); - retval.append( (colAbs)?"$":""); - retval.append( convertNumToColString(col)); - retval.append((rowAbs)?"$":""); - retval.append(row+1); + StringBuffer sb = new StringBuffer(64); + sb.append(getClass().getName()).append(" ["); + sb.append(formatAsString()); + sb.append("]"); + return sb.toString(); + } - return retval.toString(); + /** + * Appends cell reference with '$' markers for absolute values as required. + * Sheet name is not included. + */ + /* package */ void appendCellReference(StringBuffer sb) { + if(_isColAbs) { + sb.append(ABSOLUTE_REFERENCE_MARKER); + } + sb.append( convertNumToColString(_colIndex)); + if(_isRowAbs) { + sb.append(ABSOLUTE_REFERENCE_MARKER); + } + sb.append(_rowIndex+1); } } diff --git a/src/java/org/apache/poi/poifs/filesystem/POIFSFileSystem.java b/src/java/org/apache/poi/poifs/filesystem/POIFSFileSystem.java index 771db767be..ef9acfe60b 100644 --- a/src/java/org/apache/poi/poifs/filesystem/POIFSFileSystem.java +++ b/src/java/org/apache/poi/poifs/filesystem/POIFSFileSystem.java @@ -19,6 +19,7 @@ package org.apache.poi.poifs.filesystem; +import java.io.ByteArrayInputStream; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; @@ -30,6 +31,8 @@ import java.util.Collections; import java.util.Iterator; import java.util.List; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.apache.poi.poifs.dev.POIFSViewable; import org.apache.poi.poifs.property.DirectoryProperty; import org.apache.poi.poifs.property.Property; @@ -58,6 +61,33 @@ import org.apache.poi.util.LongField; public class POIFSFileSystem implements POIFSViewable { + private static final Log _logger = LogFactory.getLog(POIFSFileSystem.class); + + + private static final class CloseIgnoringInputStream extends InputStream { + + private final InputStream _is; + public CloseIgnoringInputStream(InputStream is) { + _is = is; + } + public int read() throws IOException { + return _is.read(); + } + public int read(byte[] b, int off, int len) throws IOException { + return _is.read(b, off, len); + } + public void close() { + // do nothing + } + } + + /** + * Convenience method for clients that want to avoid the auto-close behaviour of the constructor. + */ + public static InputStream createNonClosingInputStream(InputStream is) { + return new CloseIgnoringInputStream(is); + } + private PropertyTable _property_table; private List _documents; private DirectoryNode _root; @@ -74,23 +104,52 @@ public class POIFSFileSystem } /** - * Create a POIFSFileSystem from an InputStream + * Create a POIFSFileSystem from an InputStream. Normally the stream is read until + * EOF. The stream is always closed.

    + * + * Some streams are usable after reaching EOF (typically those that return true + * for markSupported()). In the unlikely case that the caller has such a stream + * and needs to use it after this constructor completes, a work around is to wrap the + * stream in order to trap the close() call. A convenience method ( + * createNonClosingInputStream()) has been provided for this purpose: + *

    +     * InputStream wrappedStream = POIFSFileSystem.createNonClosingInputStream(is);
    +     * HSSFWorkbook wb = new HSSFWorkbook(wrappedStream);
    +     * is.reset(); 
    +     * doSomethingElse(is); 
    +     * 
    + * Note also the special case of ByteArrayInputStream for which the close() + * method does nothing. + *
    +     * ByteArrayInputStream bais = ...
    +     * HSSFWorkbook wb = new HSSFWorkbook(bais); // calls bais.close() !
    +     * bais.reset(); // no problem
    +     * doSomethingElse(bais);
    +     * 
    * * @param stream the InputStream from which to read the data * * @exception IOException on errors reading, or on invalid data */ - public POIFSFileSystem(final InputStream stream) + public POIFSFileSystem(InputStream stream) throws IOException { this(); + boolean success = false; // read the header block from the stream - HeaderBlockReader header_block_reader = new HeaderBlockReader(stream); - + HeaderBlockReader header_block_reader; // read the rest of the stream into blocks - RawDataBlockList data_blocks = new RawDataBlockList(stream); + RawDataBlockList data_blocks; + try { + header_block_reader = new HeaderBlockReader(stream); + data_blocks = new RawDataBlockList(stream); + success = true; + } finally { + closeInputStream(stream, success); + } + // set up the block allocation table (necessary for the // data_blocks to be manageable @@ -112,7 +171,32 @@ public class POIFSFileSystem .getSBATStart()), data_blocks, properties.getRoot() .getChildren(), null); } - + /** + * @param stream the stream to be closed + * @param success false if an exception is currently being thrown in the calling method + */ + private void closeInputStream(InputStream stream, boolean success) { + + if(stream.markSupported() && !(stream instanceof ByteArrayInputStream)) { + String msg = "POIFS is closing the supplied input stream of type (" + + stream.getClass().getName() + ") which supports mark/reset. " + + "This will be a problem for the caller if the stream will still be used. " + + "If that is the case the caller should wrap the input stream to avoid this close logic. " + + "This warning is only temporary and will not be present in future versions of POI."; + _logger.warn(msg); + } + try { + stream.close(); + } catch (IOException e) { + if(success) { + throw new RuntimeException(e); + } + // else not success? Try block did not complete normally + // just print stack trace and leave original ex to be thrown + e.printStackTrace(); + } + } + /** * Checks that the supplied InputStream (which MUST * support mark and reset, or be a PushbackInputStream) @@ -123,23 +207,23 @@ public class POIFSFileSystem * @param inp An InputStream which supports either mark/reset, or is a PushbackInputStream */ public static boolean hasPOIFSHeader(InputStream inp) throws IOException { - // We want to peek at the first 8 bytes - inp.mark(8); + // We want to peek at the first 8 bytes + inp.mark(8); - byte[] header = new byte[8]; - IOUtils.readFully(inp, header); + byte[] header = new byte[8]; + IOUtils.readFully(inp, header); LongField signature = new LongField(HeaderBlockConstants._signature_offset, header); // Wind back those 8 bytes if(inp instanceof PushbackInputStream) { - PushbackInputStream pin = (PushbackInputStream)inp; - pin.unread(header); + PushbackInputStream pin = (PushbackInputStream)inp; + pin.unread(header); } else { - inp.reset(); + inp.reset(); } - - // Did it match the signature? - return (signature.get() == HeaderBlockConstants._signature); + + // Did it match the signature? + return (signature.get() == HeaderBlockConstants._signature); } /** diff --git a/src/java/org/apache/poi/poifs/storage/RawDataBlock.java b/src/java/org/apache/poi/poifs/storage/RawDataBlock.java index d554298400..472fd8b8b5 100644 --- a/src/java/org/apache/poi/poifs/storage/RawDataBlock.java +++ b/src/java/org/apache/poi/poifs/storage/RawDataBlock.java @@ -21,6 +21,8 @@ package org.apache.poi.poifs.storage; import org.apache.poi.poifs.common.POIFSConstants; import org.apache.poi.util.IOUtils; +import org.apache.poi.util.POILogFactory; +import org.apache.poi.util.POILogger; import java.io.*; @@ -35,6 +37,7 @@ public class RawDataBlock { private byte[] _data; private boolean _eof; + private static POILogger log = POILogFactory.getLogger(RawDataBlock.class); /** * Constructor RawDataBlock @@ -75,9 +78,12 @@ public class RawDataBlock String type = " byte" + ((count == 1) ? ("") : ("s")); - throw new IOException("Unable to read entire block; " + count - + type + " read before EOF; expected " - + blockSize + " bytes"); + log.log(POILogger.ERROR, + "Unable to read entire block; " + count + + type + " read before EOF; expected " + + blockSize + " bytes. Your document" + + " has probably been truncated!" + ); } else { _eof = false; diff --git a/src/java/org/apache/poi/util/LittleEndian.java b/src/java/org/apache/poi/util/LittleEndian.java index b883d71b18..2278649cb8 100644 --- a/src/java/org/apache/poi/util/LittleEndian.java +++ b/src/java/org/apache/poi/util/LittleEndian.java @@ -245,6 +245,16 @@ public class LittleEndian putNumber(data, offset, value, SHORT_SIZE); } + /** + * executes:

    + * + * data[offset] = (byte)value; + *

    + * Added for consistency with other put~() methods + */ + public static void putByte(byte[] data, int offset, int value) { + putNumber(data, offset, value, LittleEndianConsts.BYTE_SIZE); + } /** * put a array of shorts into a byte array diff --git a/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/AddEval.java b/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/AddEval.java index 5bc0d3e9a9..6562263d54 100644 --- a/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/AddEval.java +++ b/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/AddEval.java @@ -43,8 +43,6 @@ public class AddEval extends NumericOperationEval { private static final ValueEvalToNumericXlator NUM_XLATOR = new ValueEvalToNumericXlator((short) ( ValueEvalToNumericXlator.BOOL_IS_PARSED - | ValueEvalToNumericXlator.EVALUATED_REF_BOOL_IS_PARSED - | ValueEvalToNumericXlator.EVALUATED_REF_STRING_IS_PARSED | ValueEvalToNumericXlator.REF_BOOL_IS_PARSED | ValueEvalToNumericXlator.STRING_IS_PARSED | ValueEvalToNumericXlator.REF_STRING_IS_PARSED @@ -59,33 +57,31 @@ public class AddEval extends NumericOperationEval { } - public Eval evaluate(Eval[] operands, int srcRow, short srcCol) { - Eval retval = null; + public Eval evaluate(Eval[] args, int srcRow, short srcCol) { + if(args.length != 2) { + return ErrorEval.VALUE_INVALID; + } + double d = 0; - switch (operands.length) { - default: // will rarely happen. currently the parser itself fails. - retval = ErrorEval.UNKNOWN_ERROR; - break; - case 2: - for (int i = 0, iSize = 2; retval==null && i < iSize; i++) { - ValueEval ve = singleOperandEvaluate(operands[i], srcRow, srcCol); - if (ve instanceof NumericValueEval) { - d += ((NumericValueEval) ve).getNumberValue(); - } - else if (ve instanceof BlankEval) { - // do nothing - } - else { - retval = ErrorEval.VALUE_INVALID; - } - } // end for inside case - } // end switch - - if (retval == null) { - retval = Double.isNaN(d) ? (ValueEval) ErrorEval.VALUE_INVALID : new NumberEval(d); + for (int i = 0; i < 2; i++) { + ValueEval ve = singleOperandEvaluate(args[i], srcRow, srcCol); + if(ve instanceof ErrorEval) { + return ve; + } + if (ve instanceof NumericValueEval) { + d += ((NumericValueEval) ve).getNumberValue(); + } + else if (ve instanceof BlankEval) { + // do nothing + } + else { + return ErrorEval.VALUE_INVALID; + } } - - return retval; + if(Double.isNaN(d) || Double.isInfinite(d)) { + return ErrorEval.NUM_ERROR; + } + return new NumberEval(d); } public int getNumberOfOperands() { diff --git a/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/Area2DEval.java b/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/Area2DEval.java index 179698dc8d..4b9a64c1c0 100644 --- a/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/Area2DEval.java +++ b/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/Area2DEval.java @@ -14,10 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -/* - * Created on May 8, 2005 - * - */ + package org.apache.poi.hssf.record.formula.eval; import org.apache.poi.hssf.record.formula.AreaPtg; @@ -27,48 +24,60 @@ import org.apache.poi.hssf.record.formula.Ptg; * @author Amol S. Deshmukh < amolweb at ya hoo dot com > * */ -public class Area2DEval implements AreaEval { - - private AreaPtg delegate; +public final class Area2DEval implements AreaEval { +// TODO -refactor with Area3DEval + private final AreaPtg _delegate; - private ValueEval[] values; + private final ValueEval[] _values; public Area2DEval(Ptg ptg, ValueEval[] values) { - this.delegate = (AreaPtg) ptg; - this.values = values; + if(ptg == null) { + throw new IllegalArgumentException("ptg must not be null"); + } + if(values == null) { + throw new IllegalArgumentException("values must not be null"); + } + for(int i=values.length-1; i>=0; i--) { + if(values[i] == null) { + throw new IllegalArgumentException("value array elements must not be null"); + } + } + // TODO - check size of array vs size of AreaPtg + _delegate = (AreaPtg) ptg; + _values = values; } - public short getFirstColumn() { - return delegate.getFirstColumn(); + public int getFirstColumn() { + return _delegate.getFirstColumn(); } public int getFirstRow() { - return delegate.getFirstRow(); + return _delegate.getFirstRow(); } - public short getLastColumn() { - return delegate.getLastColumn(); + public int getLastColumn() { + return _delegate.getLastColumn(); } public int getLastRow() { - return delegate.getLastRow(); + return _delegate.getLastRow(); } public ValueEval[] getValues() { - return values; + return _values; } - public ValueEval getValueAt(int row, short col) { + public ValueEval getValueAt(int row, int col) { ValueEval retval; int index = ((row-getFirstRow())*(getLastColumn()-getFirstColumn()+1))+(col-getFirstColumn()); - if (index <0 || index >= values.length) + if (index <0 || index >= _values.length) retval = ErrorEval.VALUE_INVALID; else - retval = values[index]; + retval = _values[index]; return retval; } - public boolean contains(int row, short col) { + public boolean contains(int row, int col) { return (getFirstRow() <= row) && (getLastRow() >= row) && (getFirstColumn() <= col) && (getLastColumn() >= col); } @@ -82,10 +91,10 @@ public class Area2DEval implements AreaEval { } public boolean isColumn() { - return delegate.getFirstColumn() == delegate.getLastColumn(); + return _delegate.getFirstColumn() == _delegate.getLastColumn(); } public boolean isRow() { - return delegate.getFirstRow() == delegate.getLastRow(); + return _delegate.getFirstRow() == _delegate.getLastRow(); } } diff --git a/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/Area3DEval.java b/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/Area3DEval.java index d371a09855..2f539142d1 100644 --- a/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/Area3DEval.java +++ b/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/Area3DEval.java @@ -14,10 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -/* - * Created on May 8, 2005 - * - */ + package org.apache.poi.hssf.record.formula.eval; import org.apache.poi.hssf.record.formula.Area3DPtg; @@ -27,48 +24,60 @@ import org.apache.poi.hssf.record.formula.Ptg; * @author Amol S. Deshmukh < amolweb at ya hoo dot com > * */ -public class Area3DEval implements AreaEval { +public final class Area3DEval implements AreaEval { + // TODO -refactor with Area3DEval + private final Area3DPtg _delegate; - private Area3DPtg delegate; - - private ValueEval[] values; + private final ValueEval[] _values; public Area3DEval(Ptg ptg, ValueEval[] values) { - this.values = values; - this.delegate = (Area3DPtg) ptg; + if(ptg == null) { + throw new IllegalArgumentException("ptg must not be null"); + } + if(values == null) { + throw new IllegalArgumentException("values must not be null"); + } + for(int i=values.length-1; i>=0; i--) { + if(values[i] == null) { + throw new IllegalArgumentException("value array elements must not be null"); + } + } + // TODO - check size of array vs size of AreaPtg + _values = values; + _delegate = (Area3DPtg) ptg; } - public short getFirstColumn() { - return delegate.getFirstColumn(); + public int getFirstColumn() { + return _delegate.getFirstColumn(); } public int getFirstRow() { - return delegate.getFirstRow(); + return _delegate.getFirstRow(); } - public short getLastColumn() { - return delegate.getLastColumn(); + public int getLastColumn() { + return (short) _delegate.getLastColumn(); } public int getLastRow() { - return delegate.getLastRow(); + return _delegate.getLastRow(); } public ValueEval[] getValues() { - return values; + return _values; } - public ValueEval getValueAt(int row, short col) { + public ValueEval getValueAt(int row, int col) { ValueEval retval; int index = (row-getFirstRow())*(col-getFirstColumn()); - if (index <0 || index >= values.length) + if (index <0 || index >= _values.length) retval = ErrorEval.VALUE_INVALID; else - retval = values[index]; + retval = _values[index]; return retval; } - public boolean contains(int row, short col) { + public boolean contains(int row, int col) { return (getFirstRow() <= row) && (getLastRow() >= row) && (getFirstColumn() <= col) && (getLastColumn() >= col); } @@ -83,11 +92,14 @@ public class Area3DEval implements AreaEval { public boolean isColumn() { - return delegate.getFirstColumn() == delegate.getLastColumn(); + return _delegate.getFirstColumn() == _delegate.getLastColumn(); } public boolean isRow() { - return delegate.getFirstRow() == delegate.getLastRow(); + return _delegate.getFirstRow() == _delegate.getLastRow(); } + public int getExternSheetIndex() { + return _delegate.getExternSheetIndex(); + } } diff --git a/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/AreaEval.java b/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/AreaEval.java index f60d63c131..82cc8a9b40 100644 --- a/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/AreaEval.java +++ b/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/AreaEval.java @@ -42,13 +42,13 @@ public interface AreaEval extends ValueEval { * returns the 0-based index of the first col in * this area. */ - public short getFirstColumn(); + public int getFirstColumn(); /** * returns the 0-based index of the last col in * this area. */ - public short getLastColumn(); + public int getLastColumn(); /** * returns true if the Area's start and end row indexes @@ -80,7 +80,7 @@ public interface AreaEval extends ValueEval { * @param row * @param col */ - public ValueEval getValueAt(int row, short col); + public ValueEval getValueAt(int row, int col); /** * returns true if the cell at row and col specified @@ -89,7 +89,7 @@ public interface AreaEval extends ValueEval { * @param row * @param col */ - public boolean contains(int row, short col); + public boolean contains(int row, int col); /** * returns true if the specified col is in range diff --git a/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/BoolEval.java b/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/BoolEval.java index 6a04068b76..7b625aaa98 100644 --- a/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/BoolEval.java +++ b/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/BoolEval.java @@ -34,6 +34,16 @@ public class BoolEval implements NumericValueEval, StringValueEval { public static final BoolEval FALSE = new BoolEval(false); public static final BoolEval TRUE = new BoolEval(true); + + /** + * Convenience method for the following:
    + * (b ? BoolEval.TRUE : BoolEval.FALSE) + * @return a BoolEval instance representing b. + */ + public static final BoolEval valueOf(boolean b) { + // TODO - find / replace all occurrences + return b ? TRUE : FALSE; + } public BoolEval(Ptg ptg) { this.value = ((BoolPtg) ptg).getValue(); @@ -48,10 +58,17 @@ public class BoolEval implements NumericValueEval, StringValueEval { } public double getNumberValue() { - return value ? (short) 1 : (short) 0; + return value ? 1 : 0; } public String getStringValue() { return value ? "TRUE" : "FALSE"; } + public String toString() { + StringBuffer sb = new StringBuffer(64); + sb.append(getClass().getName()).append(" ["); + sb.append(getStringValue()); + sb.append("]"); + return sb.toString(); + } } diff --git a/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/ConcatEval.java b/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/ConcatEval.java index 2d8c58ef3e..e54cd483f1 100644 --- a/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/ConcatEval.java +++ b/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/ConcatEval.java @@ -14,10 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -/* - * Created on May 8, 2005 - * - */ + package org.apache.poi.hssf.record.formula.eval; import org.apache.poi.hssf.record.formula.ConcatPtg; @@ -27,7 +24,7 @@ import org.apache.poi.hssf.record.formula.Ptg; * @author Amol S. Deshmukh < amolweb at ya hoo dot com > * */ -public class ConcatEval extends StringOperationEval { +public final class ConcatEval extends StringOperationEval { private ConcatPtg delegate; @@ -35,36 +32,27 @@ public class ConcatEval extends StringOperationEval { this.delegate = (ConcatPtg) ptg; } - public Eval evaluate(Eval[] operands, int srcRow, short srcCol) { - Eval retval = null; - StringBuffer sb = null; - - switch (operands.length) { - default: // paranoid check :) - retval = ErrorEval.UNKNOWN_ERROR; - break; - case 2: - sb = new StringBuffer(); - for (int i = 0, iSize = 2; retval == null && i < iSize; i++) { - - ValueEval ve = singleOperandEvaluate(operands[i], srcRow, srcCol); - if (ve instanceof StringValueEval) { - StringValueEval sve = (StringValueEval) ve; - sb.append(sve.getStringValue()); - } - else if (ve instanceof BlankEval) { - // do nothing - } - else { // must be an error eval - retval = ve; - } + public Eval evaluate(Eval[] args, int srcRow, short srcCol) { + if(args.length != 2) { + return ErrorEval.VALUE_INVALID; + } + StringBuffer sb = new StringBuffer(); + for (int i = 0; i < 2; i++) { + + ValueEval ve = singleOperandEvaluate(args[i], srcRow, srcCol); + if (ve instanceof StringValueEval) { + StringValueEval sve = (StringValueEval) ve; + sb.append(sve.getStringValue()); + } + else if (ve instanceof BlankEval) { + // do nothing + } + else { // must be an error eval + return ve; } } - if (retval == null) { - retval = new StringEval(sb.toString()); - } - return retval; + return new StringEval(sb.toString()); } public int getNumberOfOperands() { diff --git a/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/DivideEval.java b/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/DivideEval.java index 6dd3db23de..021168ad79 100644 --- a/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/DivideEval.java +++ b/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/DivideEval.java @@ -14,10 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -/* - * Created on May 8, 2005 - * - */ + package org.apache.poi.hssf.record.formula.eval; import org.apache.poi.hssf.record.formula.Ptg; @@ -27,15 +24,13 @@ import org.apache.poi.hssf.record.formula.DividePtg; * @author Amol S. Deshmukh < amolweb at ya hoo dot com > * */ -public class DivideEval extends NumericOperationEval { +public final class DivideEval extends NumericOperationEval { private DividePtg delegate; private static final ValueEvalToNumericXlator NUM_XLATOR = new ValueEvalToNumericXlator((short) ( ValueEvalToNumericXlator.BOOL_IS_PARSED - | ValueEvalToNumericXlator.EVALUATED_REF_BOOL_IS_PARSED - | ValueEvalToNumericXlator.EVALUATED_REF_STRING_IS_PARSED | ValueEvalToNumericXlator.REF_BOOL_IS_PARSED | ValueEvalToNumericXlator.STRING_IS_PARSED | ValueEvalToNumericXlator.REF_STRING_IS_PARSED @@ -49,18 +44,28 @@ public class DivideEval extends NumericOperationEval { return NUM_XLATOR; } - public Eval evaluate(Eval[] operands, int srcRow, short srcCol) { + public Eval evaluate(Eval[] args, int srcRow, short srcCol) { + if(args.length != 2) { + return ErrorEval.VALUE_INVALID; + } Eval retval = null; double d0 = 0; double d1 = 0; - switch (operands.length) { - default: // will rarely happen. currently the parser itself fails. - retval = ErrorEval.UNKNOWN_ERROR; - break; - case 2: - ValueEval ve = singleOperandEvaluate(operands[0], srcRow, srcCol); + ValueEval ve = singleOperandEvaluate(args[0], srcRow, srcCol); + if (ve instanceof NumericValueEval) { + d0 = ((NumericValueEval) ve).getNumberValue(); + } + else if (ve instanceof BlankEval) { + // do nothing + } + else { + retval = ErrorEval.VALUE_INVALID; + } + + if (retval == null) { // no error yet + ve = singleOperandEvaluate(args[1], srcRow, srcCol); if (ve instanceof NumericValueEval) { - d0 = ((NumericValueEval) ve).getNumberValue(); + d1 = ((NumericValueEval) ve).getNumberValue(); } else if (ve instanceof BlankEval) { // do nothing @@ -68,20 +73,7 @@ public class DivideEval extends NumericOperationEval { else { retval = ErrorEval.VALUE_INVALID; } - - if (retval == null) { // no error yet - ve = singleOperandEvaluate(operands[1], srcRow, srcCol); - if (ve instanceof NumericValueEval) { - d1 = ((NumericValueEval) ve).getNumberValue(); - } - else if (ve instanceof BlankEval) { - // do nothing - } - else { - retval = ErrorEval.VALUE_INVALID; - } - } - } // end switch + } if (retval == null) { retval = (d1 == 0) diff --git a/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/ErrorEval.java b/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/ErrorEval.java index 43ef6c5127..e8e197d201 100644 --- a/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/ErrorEval.java +++ b/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/ErrorEval.java @@ -14,51 +14,100 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -/* - * Created on May 8, 2005 - * - */ + package org.apache.poi.hssf.record.formula.eval; +import org.apache.poi.hssf.usermodel.HSSFErrorConstants; + /** * @author Amol S. Deshmukh < amolweb at ya hoo dot com > - * + * */ -public class ErrorEval implements ValueEval { +public final class ErrorEval implements ValueEval { - private int errorCode; + // convenient access to namespace + private static final HSSFErrorConstants EC = null; + /** #NULL! - Intersection of two cell ranges is empty */ + public static final ErrorEval NULL_INTERSECTION = new ErrorEval(EC.ERROR_NULL); + /** #DIV/0! - Division by zero */ + public static final ErrorEval DIV_ZERO = new ErrorEval(EC.ERROR_DIV_0); + /** #VALUE! - Wrong type of operand */ + public static final ErrorEval VALUE_INVALID = new ErrorEval(EC.ERROR_VALUE); + /** #REF! - Illegal or deleted cell reference */ + public static final ErrorEval REF_INVALID = new ErrorEval(EC.ERROR_REF); + /** #NAME? - Wrong function or range name */ + public static final ErrorEval NAME_INVALID = new ErrorEval(EC.ERROR_NAME); + /** #NUM! - Value range overflow */ + public static final ErrorEval NUM_ERROR = new ErrorEval(EC.ERROR_NUM); + /** #N/A - Argument or function not available */ + public static final ErrorEval NA = new ErrorEval(EC.ERROR_NA); - public static final ErrorEval NAME_INVALID = new ErrorEval(525); - public static final ErrorEval VALUE_INVALID = new ErrorEval(519); + // POI internal error codes + private static final int CIRCULAR_REF_ERROR_CODE = 0xFFFFFFC4; + private static final int FUNCTION_NOT_IMPLEMENTED_CODE = 0xFFFFFFE2; - - // Non std error codes - public static final ErrorEval UNKNOWN_ERROR = new ErrorEval(-20); + public static final ErrorEval FUNCTION_NOT_IMPLEMENTED = new ErrorEval(FUNCTION_NOT_IMPLEMENTED_CODE); + // Note - Excel does not seem to represent this condition with an error code + public static final ErrorEval CIRCULAR_REF_ERROR = new ErrorEval(CIRCULAR_REF_ERROR_CODE); - public static final ErrorEval FUNCTION_NOT_IMPLEMENTED = new ErrorEval(-30); - public static final ErrorEval REF_INVALID = new ErrorEval(-40); + /** + * Translates an Excel internal error code into the corresponding POI ErrorEval instance + * @param errorCode + */ + public static ErrorEval valueOf(int errorCode) { + switch(errorCode) { + case HSSFErrorConstants.ERROR_NULL: return NULL_INTERSECTION; + case HSSFErrorConstants.ERROR_DIV_0: return DIV_ZERO; + case HSSFErrorConstants.ERROR_VALUE: return VALUE_INVALID; + case HSSFErrorConstants.ERROR_REF: return REF_INVALID; + case HSSFErrorConstants.ERROR_NAME: return NAME_INVALID; + case HSSFErrorConstants.ERROR_NUM: return NUM_ERROR; + case HSSFErrorConstants.ERROR_NA: return NA; + // non-std errors (conditions modeled as errors by POI) + case CIRCULAR_REF_ERROR_CODE: return CIRCULAR_REF_ERROR; + case FUNCTION_NOT_IMPLEMENTED_CODE: return FUNCTION_NOT_IMPLEMENTED; + } + throw new RuntimeException("Unexpected error code (" + errorCode + ")"); + } - public static final ErrorEval NA = new ErrorEval(-50); - - public static final ErrorEval CIRCULAR_REF_ERROR = new ErrorEval(-60); - - public static final ErrorEval DIV_ZERO = new ErrorEval(-70); - - public static final ErrorEval NUM_ERROR = new ErrorEval(-80); + /** + * Converts error codes to text. Handles non-standard error codes OK. + * For debug/test purposes (and for formatting error messages). + * @return the String representation of the specified Excel error code. + */ + public static String getText(int errorCode) { + if(HSSFErrorConstants.isValidCode(errorCode)) { + return HSSFErrorConstants.getText(errorCode); + } + // It is desirable to make these (arbitrary) strings look clearly different from any other + // value expression that might appear in a formula. In addition these error strings should + // look unlike the standard Excel errors. Hence tilde ('~') was used. + switch(errorCode) { + case CIRCULAR_REF_ERROR_CODE: return "~CIRCULAR~REF~"; + case FUNCTION_NOT_IMPLEMENTED_CODE: return "~FUNCTION~NOT~IMPLEMENTED~"; + } + return "~non~std~err(" + errorCode + ")~"; + } + private int _errorCode; + /** + * @param errorCode an 8-bit value + */ private ErrorEval(int errorCode) { - this.errorCode = errorCode; + _errorCode = errorCode; } public int getErrorCode() { - return errorCode; + return _errorCode; } - - public String getStringValue() { - return "Err:" + Integer.toString(errorCode); + public String toString() { + StringBuffer sb = new StringBuffer(64); + sb.append(getClass().getName()).append(" ["); + sb.append(getText(_errorCode)); + sb.append("]"); + return sb.toString(); } - } diff --git a/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/EvaluationException.java b/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/EvaluationException.java new file mode 100755 index 0000000000..7a23901b25 --- /dev/null +++ b/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/EvaluationException.java @@ -0,0 +1,134 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +package org.apache.poi.hssf.record.formula.eval; + +/** + * This class is used to simplify error handling logic within operator and function + * implementations. Note - OperationEval.evaluate() and Function.evaluate() + * method signatures do not throw this exception so it cannot propagate outside.

    + * + * Here is an example coded without EvaluationException, to show how it can help: + *

    + * public Eval evaluate(Eval[] args, int srcRow, short srcCol) {
    + *	// ...
    + *	Eval arg0 = args[0];
    + *	if(arg0 instanceof ErrorEval) {
    + *		return arg0;
    + *	}
    + *	if(!(arg0 instanceof AreaEval)) {
    + *		return ErrorEval.VALUE_INVALID;
    + *	}
    + *	double temp = 0;
    + *	AreaEval area = (AreaEval)arg0;
    + *	ValueEval[] values = area.getValues();
    + *	for (int i = 0; i < values.length; i++) {
    + *		ValueEval ve = values[i];
    + *		if(ve instanceof ErrorEval) {
    + *			return ve;
    + *		}
    + *		if(!(ve instanceof NumericValueEval)) {
    + *			return ErrorEval.VALUE_INVALID;
    + *		}
    + *		temp += ((NumericValueEval)ve).getNumberValue();
    + *	}
    + *	// ...
    + * }	 
    + * 
    + * In this example, if any error is encountered while processing the arguments, an error is + * returned immediately. This code is difficult to refactor due to all the points where errors + * are returned.
    + * Using EvaluationException allows the error returning code to be consolidated to one + * place.

    + *

    + * public Eval evaluate(Eval[] args, int srcRow, short srcCol) {
    + *	try {
    + *		// ...
    + *		AreaEval area = getAreaArg(args[0]);
    + *		double temp = sumValues(area.getValues());
    + *		// ...
    + *	} catch (EvaluationException e) {
    + *		return e.getErrorEval();
    + *	}
    + *}
    + *
    + *private static AreaEval getAreaArg(Eval arg0) throws EvaluationException {
    + *	if (arg0 instanceof ErrorEval) {
    + *		throw new EvaluationException((ErrorEval) arg0);
    + *	}
    + *	if (arg0 instanceof AreaEval) {
    + *		return (AreaEval) arg0;
    + *	}
    + *	throw EvaluationException.invalidValue();
    + *}
    + *
    + *private double sumValues(ValueEval[] values) throws EvaluationException {
    + *	double temp = 0;
    + *	for (int i = 0; i < values.length; i++) {
    + *		ValueEval ve = values[i];
    + *		if (ve instanceof ErrorEval) {
    + *			throw new EvaluationException((ErrorEval) ve);
    + *		}
    + *		if (!(ve instanceof NumericValueEval)) {
    + *			throw EvaluationException.invalidValue();
    + *		}
    + *		temp += ((NumericValueEval) ve).getNumberValue();
    + *	}
    + *	return temp;
    + *}
    + * 
    + * It is not mandatory to use EvaluationException, doing so might give the following advantages:
    + * - Methods can more easily be extracted, allowing for re-use.
    + * - Type management (typecasting etc) is simpler because error conditions have been separated from + * intermediate calculation values.
    + * - Fewer local variables are required. Local variables can have stronger types.
    + * - It is easier to mimic common Excel error handling behaviour (exit upon encountering first + * error), because exceptions conveniently propagate up the call stack regardless of execution + * points or the number of levels of nested calls.

    + * + * Note - Only standard evaluation errors are represented by EvaluationException ( + * i.e. conditions expected to be encountered when evaluating arbitrary Excel formulas). Conditions + * that could never occur in an Excel spreadsheet should result in runtime exceptions. Care should + * be taken to not translate any POI internal error into an Excel evaluation error code. + * + * @author Josh Micich + */ +public final class EvaluationException extends Exception { + private final ErrorEval _errorEval; + + public EvaluationException(ErrorEval errorEval) { + _errorEval = errorEval; + } + // some convenience factory methods + + /** #VALUE! - Wrong type of operand */ + public static EvaluationException invalidValue() { + return new EvaluationException(ErrorEval.VALUE_INVALID); + } + /** #REF! - Illegal or deleted cell reference */ + public static EvaluationException invalidRef() { + return new EvaluationException(ErrorEval.REF_INVALID); + } + /** #NUM! - Value range overflow */ + public static EvaluationException numberError() { + return new EvaluationException(ErrorEval.NUM_ERROR); + } + + public ErrorEval getErrorEval() { + return _errorEval; + } +} diff --git a/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/ExternalFunction.java b/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/ExternalFunction.java new file mode 100755 index 0000000000..b1d81e6524 --- /dev/null +++ b/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/ExternalFunction.java @@ -0,0 +1,81 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +package org.apache.poi.hssf.record.formula.eval; + +import org.apache.poi.hssf.record.formula.functions.FreeRefFunction; +import org.apache.poi.hssf.usermodel.HSSFSheet; +import org.apache.poi.hssf.usermodel.HSSFWorkbook; +/** + * + * Common entry point for all external functions (where + * AbstractFunctionPtg.field_2_fnc_index == 255) + * + * @author Josh Micich + */ +final class ExternalFunction implements FreeRefFunction { + + public ValueEval evaluate(Eval[] args, int srcCellRow, short srcCellCol, HSSFWorkbook workbook, HSSFSheet sheet) { + + int nIncomingArgs = args.length; + if(nIncomingArgs < 1) { + throw new RuntimeException("function name argument missing"); + } + + if (!(args[0] instanceof NameEval)) { + throw new RuntimeException("First argument should be a NameEval, but got (" + + args[0].getClass().getName() + ")"); + } + NameEval functionNameEval = (NameEval) args[0]; + + int nOutGoingArgs = nIncomingArgs -1; + Eval[] outGoingArgs = new Eval[nOutGoingArgs]; + System.arraycopy(args, 1, outGoingArgs, 0, nOutGoingArgs); + + FreeRefFunction targetFunc; + try { + targetFunc = findTargetFunction(workbook, functionNameEval); + } catch (EvaluationException e) { + return e.getErrorEval(); + } + + return targetFunc.evaluate(outGoingArgs, srcCellRow, srcCellCol, workbook, sheet); + } + + private FreeRefFunction findTargetFunction(HSSFWorkbook workbook, NameEval functionNameEval) throws EvaluationException { + + int numberOfNames = workbook.getNumberOfNames(); + + int nameIndex = functionNameEval.getIndex(); + if(nameIndex < 0 || nameIndex >= numberOfNames) { + throw new RuntimeException("Bad name index (" + nameIndex + + "). Allowed range is (0.." + (numberOfNames-1) + ")"); + } + + String functionName = workbook.getNameName(nameIndex); + if(false) { + System.out.println("received call to external function index (" + functionName + ")"); + } + // TODO - detect if the NameRecord corresponds to a named range, function, or something undefined + // throw the right errors in these cases + + // TODO find the implementation for the external function e.g. "YEARFRAC" or "ISEVEN" + + throw new EvaluationException(ErrorEval.FUNCTION_NOT_IMPLEMENTED); + } + +} diff --git a/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/FunctionEval.java b/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/FunctionEval.java index d1420b2e85..533c604a0c 100644 --- a/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/FunctionEval.java +++ b/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/FunctionEval.java @@ -20,6 +20,9 @@ */ package org.apache.poi.hssf.record.formula.eval; +import java.util.HashMap; +import java.util.Map; + import org.apache.poi.hssf.record.formula.functions.*; /** @@ -27,12 +30,49 @@ import org.apache.poi.hssf.record.formula.functions.*; * */ public abstract class FunctionEval implements OperationEval { + /** + * Some function IDs that require special treatment + */ + private static final class FunctionID { + /** 78 */ + public static final int OFFSET = 78; + /** 148 */ + public static final int INDIRECT = 148; + /** 255 */ + public static final int EXTERNAL_FUNC = 255; + } + // convenient access to namespace + private static final FunctionID ID = null; + protected static Function[] functions = produceFunctions(); + private static Map freeRefFunctionsByIdMap; + + static { + Map m = new HashMap(); + addMapping(m, ID.OFFSET, new Offset()); + addMapping(m, ID.INDIRECT, new Indirect()); + addMapping(m, ID.EXTERNAL_FUNC, new ExternalFunction()); + freeRefFunctionsByIdMap = m; + } + private static void addMapping(Map m, int offset, FreeRefFunction frf) { + m.put(createFRFKey(offset), frf); + } + private static Integer createFRFKey(int functionIndex) { + return new Integer(functionIndex); + } + + public Function getFunction() { short fidx = getFunctionIndex(); return functions[fidx]; } + public boolean isFreeRefFunction() { + return freeRefFunctionsByIdMap.containsKey(createFRFKey(getFunctionIndex())); + } + public FreeRefFunction getFreeRefFunction() { + return (FreeRefFunction) freeRefFunctionsByIdMap.get(createFRFKey(getFunctionIndex())); + } public abstract short getFunctionIndex(); @@ -115,7 +155,7 @@ public abstract class FunctionEval implements OperationEval { retval[75] = new Areas(); // AREAS retval[76] = new Rows(); // ROWS retval[77] = new Columns(); // COLUMNS - retval[78] = new Offset(); // OFFSET + retval[ID.OFFSET] = null; // Offset.evaluate has a different signature retval[79] = new Absref(); // ABSREF retval[80] = new Relref(); // RELREF retval[81] = new Argument(); // ARGUMENT @@ -185,7 +225,7 @@ public abstract class FunctionEval implements OperationEval { retval[145] = new NotImplementedFunction(); // GETDEF retval[146] = new Reftext(); // REFTEXT retval[147] = new Textref(); // TEXTREF - retval[148] = new Indirect(); // INDIRECT + retval[ID.INDIRECT] = null; // Indirect.evaluate has different signature retval[149] = new NotImplementedFunction(); // REGISTER retval[150] = new Call(); // CALL retval[151] = new NotImplementedFunction(); // ADDBAR @@ -278,7 +318,7 @@ public abstract class FunctionEval implements OperationEval { retval[252] = new Frequency(); // FREQUENCY retval[253] = new NotImplementedFunction(); // ADDTOOLBAR retval[254] = new NotImplementedFunction(); // DELETETOOLBAR - retval[255] = new NotImplementedFunction(); // EXTERNALFLAG + retval[ID.EXTERNAL_FUNC] = null; // ExternalFunction is a FreeREfFunction retval[256] = new NotImplementedFunction(); // RESETTOOLBAR retval[257] = new Evaluate(); // EVALUATE retval[258] = new NotImplementedFunction(); // GETTOOLBAR diff --git a/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/MultiplyEval.java b/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/MultiplyEval.java index ecada85d56..22d87b7e4d 100644 --- a/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/MultiplyEval.java +++ b/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/MultiplyEval.java @@ -14,10 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -/* - * Created on May 8, 2005 - * - */ + package org.apache.poi.hssf.record.formula.eval; import org.apache.poi.hssf.record.formula.Ptg; @@ -27,15 +24,13 @@ import org.apache.poi.hssf.record.formula.MultiplyPtg; * @author Amol S. Deshmukh < amolweb at ya hoo dot com > * */ -public class MultiplyEval extends NumericOperationEval { +public final class MultiplyEval extends NumericOperationEval { private MultiplyPtg delegate; private static final ValueEvalToNumericXlator NUM_XLATOR = new ValueEvalToNumericXlator((short) ( ValueEvalToNumericXlator.BOOL_IS_PARSED - | ValueEvalToNumericXlator.EVALUATED_REF_BOOL_IS_PARSED - | ValueEvalToNumericXlator.EVALUATED_REF_STRING_IS_PARSED | ValueEvalToNumericXlator.REF_BOOL_IS_PARSED | ValueEvalToNumericXlator.STRING_IS_PARSED | ValueEvalToNumericXlator.REF_STRING_IS_PARSED @@ -49,46 +44,39 @@ public class MultiplyEval extends NumericOperationEval { return NUM_XLATOR; } - public Eval evaluate(Eval[] operands, int srcRow, short srcCol) { - Eval retval = null; + public Eval evaluate(Eval[] args, int srcRow, short srcCol) { + if(args.length != 2) { + return ErrorEval.VALUE_INVALID; + } + double d0 = 0; double d1 = 0; - switch (operands.length) { - default: // will rarely happen. currently the parser itself fails. - retval = ErrorEval.UNKNOWN_ERROR; - break; - case 2: - ValueEval ve = singleOperandEvaluate(operands[0], srcRow, srcCol); - if (ve instanceof NumericValueEval) { - d0 = ((NumericValueEval) ve).getNumberValue(); - } - else if (ve instanceof BlankEval) { - // do nothing - } - else { - retval = ErrorEval.VALUE_INVALID; - } - - if (retval == null) { // no error yet - ve = singleOperandEvaluate(operands[1], srcRow, srcCol); - if (ve instanceof NumericValueEval) { - d1 = ((NumericValueEval) ve).getNumberValue(); - } - else if (ve instanceof BlankEval) { - // do nothing - } - else { - retval = ErrorEval.VALUE_INVALID; - } - } - } // end switch - - if (retval == null) { - retval = (Double.isNaN(d0) || Double.isNaN(d1)) - ? (ValueEval) ErrorEval.VALUE_INVALID - : new NumberEval(d0 * d1); + ValueEval ve = singleOperandEvaluate(args[0], srcRow, srcCol); + if (ve instanceof NumericValueEval) { + d0 = ((NumericValueEval) ve).getNumberValue(); + } + else if (ve instanceof BlankEval) { + // do nothing + } + else { + return ErrorEval.VALUE_INVALID; + } + + ve = singleOperandEvaluate(args[1], srcRow, srcCol); + if (ve instanceof NumericValueEval) { + d1 = ((NumericValueEval) ve).getNumberValue(); + } + else if (ve instanceof BlankEval) { + // do nothing + } + else { + return ErrorEval.VALUE_INVALID; } - return retval; + + if (Double.isNaN(d0) || Double.isNaN(d1)) { + return ErrorEval.NUM_ERROR; + } + return new NumberEval(d0 * d1); } public int getNumberOfOperands() { diff --git a/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/NameEval.java b/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/NameEval.java new file mode 100755 index 0000000000..682394b3c2 --- /dev/null +++ b/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/NameEval.java @@ -0,0 +1,48 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +package org.apache.poi.hssf.record.formula.eval; + +/** + * @author Josh Micich + */ +public final class NameEval implements Eval { + + private final int _index; + + /** + * @param index zero based index to a defined name record + */ + public NameEval(int index) { + _index = index; + } + + /** + * @return zero based index to a defined name record + */ + public int getIndex() { + return _index; + } + + public String toString() { + StringBuffer sb = new StringBuffer(64); + sb.append(getClass().getName()).append(" ["); + sb.append(_index); + sb.append("]"); + return sb.toString(); + } +} diff --git a/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/OperandResolver.java b/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/OperandResolver.java new file mode 100755 index 0000000000..be1cda5f8e --- /dev/null +++ b/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/OperandResolver.java @@ -0,0 +1,277 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +package org.apache.poi.hssf.record.formula.eval; + +/** + * Provides functionality for evaluating arguments to functions and operators. + * + * @author Josh Micich + */ +public final class OperandResolver { + + private OperandResolver() { + // no instances of this class + } + + /** + * Retrieves a single value from a variety of different argument types according to standard + * Excel rules. Does not perform any type conversion. + * @param arg the evaluated argument as passed to the function or operator. + * @param srcCellRow used when arg is a single column AreaRef + * @param srcCellCol used when arg is a single row AreaRef + * @return a NumberEval, StringEval, BoolEval or BlankEval. + * Never null or ErrorEval. + * @throws EvaluationException(#VALUE!) if srcCellRow or srcCellCol do not properly index into + * an AreaEval. If the actual value retrieved is an ErrorEval, a corresponding + * EvaluationException is thrown. + */ + public static ValueEval getSingleValue(Eval arg, int srcCellRow, short srcCellCol) + throws EvaluationException { + Eval result; + if (arg instanceof RefEval) { + result = ((RefEval) arg).getInnerValueEval(); + } else if (arg instanceof AreaEval) { + result = chooseSingleElementFromArea((AreaEval) arg, srcCellRow, srcCellCol); + } else { + result = arg; + } + if (result instanceof ErrorEval) { + throw new EvaluationException((ErrorEval) result); + } + if (result instanceof ValueEval) { + return (ValueEval) result; + } + throw new RuntimeException("Unexpected eval type (" + result.getClass().getName() + ")"); + } + + /** + * Implements (some perhaps not well known) Excel functionality to select a single cell from an + * area depending on the coordinates of the calling cell. Here is an example demonstrating + * both selection from a single row area and a single column area in the same formula. + * + * + * + * + * + * + * + *
      A  B  C  D 
    1152025 
    2   200
    3   300
    3   400
    + * + * If the formula "=1000+A1:B1+D2:D3" is put into the 9 cells from A2 to C4, the spreadsheet + * will look like this: + * + * + * + * + * + * + * + *
      A  B  C  D 
    1152025 
    212151220#VALUE!200
    313151320#VALUE!300
    4#VALUE!#VALUE!#VALUE!400
    + * + * Note that the row area (A1:B1) does not include column C and the column area (D2:D3) does + * not include row 4, so the values in C1(=25) and D4(=400) are not accessible to the formula + * as written, but in the 4 cells A2:B3, the row and column selection works ok.

    + * + * The same concept is extended to references across sheets, such that even multi-row, + * multi-column areas can be useful.

    + * + * Of course with carefully (or carelessly) chosen parameters, cyclic references can occur and + * hence this method can throw a 'circular reference' EvaluationException. Note that + * this method does not attempt to detect cycles. Every cell in the specified Area ae + * has already been evaluated prior to this method call. Any cell (or cells) part of + * ae that would incur a cyclic reference error if selected by this method, will + * already have the value ErrorEval.CIRCULAR_REF_ERROR upon entry to this method. It + * is assumed logic exists elsewhere to produce this behaviour. + * + * @return whatever the selected cell's evaluated value is. Never null. Never + * ErrorEval. + * @throws EvaluationException if there is a problem with indexing into the area, or if the + * evaluated cell has an error. + */ + public static ValueEval chooseSingleElementFromArea(AreaEval ae, + int srcCellRow, short srcCellCol) throws EvaluationException { + ValueEval result = chooseSingleElementFromAreaInternal(ae, srcCellRow, srcCellCol); + if(result == null) { + // This seems to be required because AreaEval.values() array may contain nulls. + // perhaps that should not be allowed. + result = BlankEval.INSTANCE; + } + if (result instanceof ErrorEval) { + throw new EvaluationException((ErrorEval) result); + + } + return result; + } + + /** + * @return possibly ErrorEval, and null + */ + private static ValueEval chooseSingleElementFromAreaInternal(AreaEval ae, + int srcCellRow, short srcCellCol) throws EvaluationException { + + if(false) { + // this is too simplistic + if(ae.containsRow(srcCellRow) && ae.containsColumn(srcCellCol)) { + throw new EvaluationException(ErrorEval.CIRCULAR_REF_ERROR); + } + /* + Circular references are not dealt with directly here, but it is worth noting some issues. + + ANY one of the return statements in this method could return a cell that is identical + to the one immediately being evaluated. The evaluating cell is identified by srcCellRow, + srcCellRow AND sheet. The sheet is not available in any nearby calling method, so that's + one reason why circular references are not easy to detect here. (The sheet of the returned + cell can be obtained from ae if it is an Area3DEval.) + + Another reason there's little value in attempting to detect circular references here is + that only direct circular references could be detected. If the cycle involved two or more + cells this method could not detect it. + + Logic to detect evaluation cycles of all kinds has been coded in EvaluationCycleDetector + (and HSSFFormulaEvaluator). + */ + } + + if (ae.isColumn()) { + if(ae.isRow()) { + return ae.getValues()[0]; + } + if(!ae.containsRow(srcCellRow)) { + throw EvaluationException.invalidValue(); + } + return ae.getValueAt(srcCellRow, ae.getFirstColumn()); + } + if(!ae.isRow()) { + // multi-column, multi-row area + if(ae.containsRow(srcCellRow) && ae.containsColumn(srcCellCol)) { + return ae.getValueAt(ae.getFirstRow(), ae.getFirstColumn()); + } + throw EvaluationException.invalidValue(); + } + if(!ae.containsColumn(srcCellCol)) { + throw EvaluationException.invalidValue(); + } + return ae.getValueAt(ae.getFirstRow(), srcCellCol); + } + + /** + * Applies some conversion rules if the supplied value is not already an integer.
    + * Value is first coerced to a double ( See coerceValueToDouble() ).

    + * + * Excel typically converts doubles to integers by truncating toward negative infinity.
    + * The equivalent java code is:
    + *   return (int)Math.floor(d);
    + * not:
    + *   return (int)d; // wrong - rounds toward zero + * + */ + public static int coerceValueToInt(ValueEval ev) throws EvaluationException { + double d = coerceValueToDouble(ev); + // Note - the standard java type conversion from double to int truncates toward zero. + // but Math.floor() truncates toward negative infinity + return (int)Math.floor(d); + } + + /** + * Applies some conversion rules if the supplied value is not already a number. + * Note - BlankEval is not supported and must be handled by the caller. + * @param ev must be a NumberEval, StringEval or BoolEval + * @return actual, parsed or interpreted double value (respectively). + * @throws EvaluationException(#VALUE!) only if a StringEval is supplied and cannot be parsed + * as a double (See parseDouble() for allowable formats). + * @throws RuntimeException if the supplied parameter is not NumberEval, + * StringEval or BoolEval + */ + public static double coerceValueToDouble(ValueEval ev) throws EvaluationException { + + if (ev instanceof NumericValueEval) { + // this also handles booleans + return ((NumericValueEval)ev).getNumberValue(); + } + if (ev instanceof StringEval) { + Double dd = parseDouble(((StringEval) ev).getStringValue()); + if (dd == null) { + throw EvaluationException.invalidValue(); + } + return dd.doubleValue(); + } + throw new RuntimeException("Unexpected arg eval type (" + ev.getClass().getName() + ")"); + } + + /** + * Converts a string to a double using standard rules that Excel would use.
    + * Tolerates currency prefixes, commas, leading and trailing spaces.

    + * + * Some examples:
    + * " 123 " -> 123.0
    + * ".123" -> 0.123
    + * These not supported yet:
    + * " $ 1,000.00 " -> 1000.0
    + * "$1.25E4" -> 12500.0
    + * "5**2" -> 500
    + * "250%" -> 2.5
    + * + * @param text + * @return null if the specified text cannot be parsed as a number + */ + public static Double parseDouble(String pText) { + String text = pText.trim(); + if(text.length() < 1) { + return null; + } + boolean isPositive = true; + if(text.charAt(0) == '-') { + isPositive = false; + text= text.substring(1).trim(); + } + + if(!Character.isDigit(text.charAt(0))) { + // avoid using NumberFormatException to tell when string is not a number + return null; + } + // TODO - support notation like '1E3' (==1000) + + double val; + try { + val = Double.parseDouble(text); + } catch (NumberFormatException e) { + return null; + } + return new Double(isPositive ? +val : -val); + } + + /** + * @param ve must be a NumberEval, StringEval, BoolEval, or BlankEval + * @return the converted string value. never null + */ + public static String coerceValueToString(ValueEval ve) { + if (ve instanceof StringValueEval) { + StringValueEval sve = (StringValueEval) ve; + return sve.getStringValue(); + } + if (ve instanceof NumberEval) { + NumberEval neval = (NumberEval) ve; + return neval.getStringValue(); + } + + if (ve instanceof BlankEval) { + return ""; + } + throw new IllegalArgumentException("Unexpected eval class (" + ve.getClass().getName() + ")"); + } +} diff --git a/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/PowerEval.java b/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/PowerEval.java index 437c24e40d..651c5d2aa2 100644 --- a/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/PowerEval.java +++ b/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/PowerEval.java @@ -14,10 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -/* - * Created on May 8, 2005 - * - */ + package org.apache.poi.hssf.record.formula.eval; import org.apache.poi.hssf.record.formula.Ptg; @@ -27,15 +24,13 @@ import org.apache.poi.hssf.record.formula.PowerPtg; * @author Amol S. Deshmukh < amolweb at ya hoo dot com > * */ -public class PowerEval extends NumericOperationEval { +public final class PowerEval extends NumericOperationEval { private PowerPtg delegate; private static final ValueEvalToNumericXlator NUM_XLATOR = new ValueEvalToNumericXlator((short) ( ValueEvalToNumericXlator.BOOL_IS_PARSED - | ValueEvalToNumericXlator.EVALUATED_REF_BOOL_IS_PARSED - | ValueEvalToNumericXlator.EVALUATED_REF_STRING_IS_PARSED | ValueEvalToNumericXlator.REF_BOOL_IS_PARSED | ValueEvalToNumericXlator.STRING_IS_PARSED | ValueEvalToNumericXlator.REF_STRING_IS_PARSED @@ -49,48 +44,40 @@ public class PowerEval extends NumericOperationEval { return NUM_XLATOR; } - public Eval evaluate(Eval[] operands, int srcRow, short srcCol) { - Eval retval = null; + public Eval evaluate(Eval[] args, int srcRow, short srcCol) { + if(args.length != 2) { + return ErrorEval.VALUE_INVALID; + } double d0 = 0; double d1 = 0; - switch (operands.length) { - default: // will rarely happen. currently the parser itself fails. - retval = ErrorEval.UNKNOWN_ERROR; - break; - case 2: - ValueEval ve = singleOperandEvaluate(operands[0], srcRow, srcCol); - if (ve instanceof NumericValueEval) { - d0 = ((NumericValueEval) ve).getNumberValue(); - } - else if (ve instanceof BlankEval) { - // do nothing - } - else { - retval = ErrorEval.VALUE_INVALID; - } - - if (retval == null) { // no error yet - ve = singleOperandEvaluate(operands[1], srcRow, srcCol); - if (ve instanceof NumericValueEval) { - d1 = ((NumericValueEval) ve).getNumberValue(); - } - else if (ve instanceof BlankEval) { - // do nothing - } - else { - retval = ErrorEval.VALUE_INVALID; - } - } - } // end switch - - if (retval == null) { - double p = Math.pow(d0, d1); - retval = (Double.isNaN(p)) - ? (ValueEval) ErrorEval.VALUE_INVALID - : new NumberEval(p); + ValueEval ve = singleOperandEvaluate(args[0], srcRow, srcCol); + if (ve instanceof NumericValueEval) { + d0 = ((NumericValueEval) ve).getNumberValue(); + } + else if (ve instanceof BlankEval) { + // do nothing + } + else { + return ErrorEval.VALUE_INVALID; } - return retval; + + ve = singleOperandEvaluate(args[1], srcRow, srcCol); + if (ve instanceof NumericValueEval) { + d1 = ((NumericValueEval) ve).getNumberValue(); + } + else if (ve instanceof BlankEval) { + // do nothing + } + else { + return ErrorEval.VALUE_INVALID; + } + + double p = Math.pow(d0, d1); + if (Double.isNaN(p)) { + return ErrorEval.VALUE_INVALID; + } + return new NumberEval(p); } public int getNumberOfOperands() { diff --git a/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/Ref2DEval.java b/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/Ref2DEval.java index 7b24cb062b..898d7a8618 100644 --- a/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/Ref2DEval.java +++ b/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/Ref2DEval.java @@ -14,47 +14,37 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -/* - * Created on May 9, 2005 - * - */ + package org.apache.poi.hssf.record.formula.eval; -import org.apache.poi.hssf.record.formula.Ptg; import org.apache.poi.hssf.record.formula.ReferencePtg; /** * @author adeshmukh * */ -public class Ref2DEval implements RefEval { +public final class Ref2DEval implements RefEval { - private ValueEval value; - - private ReferencePtg delegate; + private final ValueEval value; + private final ReferencePtg delegate; - private boolean evaluated; - - public Ref2DEval(Ptg ptg, ValueEval value, boolean evaluated) { - this.value = value; - this.delegate = (ReferencePtg) ptg; - this.evaluated = evaluated; + public Ref2DEval(ReferencePtg ptg, ValueEval ve) { + if(ve == null) { + throw new IllegalArgumentException("ve must not be null"); + } + if(false && ptg == null) { // TODO - fix dodgy code in MultiOperandNumericFunction + throw new IllegalArgumentException("ptg must not be null"); + } + value = ve; + delegate = ptg; } - public ValueEval getInnerValueEval() { return value; } - - public short getRow() { + public int getRow() { return delegate.getRow(); } - - public short getColumn() { + public int getColumn() { return delegate.getColumn(); } - - public boolean isEvaluated() { - return evaluated; - } - } diff --git a/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/Ref3DEval.java b/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/Ref3DEval.java index acedbe766d..622d686329 100644 --- a/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/Ref3DEval.java +++ b/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/Ref3DEval.java @@ -14,47 +14,40 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -/* - * Created on May 9, 2005 - * - */ + package org.apache.poi.hssf.record.formula.eval; -import org.apache.poi.hssf.record.formula.Ptg; import org.apache.poi.hssf.record.formula.Ref3DPtg; /** * @author Amol S. Deshmukh * */ -public class Ref3DEval implements RefEval { - - private ValueEval value; - - private Ref3DPtg delegate; - - private boolean evaluated; - - public Ref3DEval(Ptg ptg, ValueEval value, boolean evaluated) { - this.value = value; - this.delegate = (Ref3DPtg) ptg; - this.evaluated = evaluated; +public final class Ref3DEval implements RefEval { + + private final ValueEval value; + private final Ref3DPtg delegate; + + public Ref3DEval(Ref3DPtg ptg, ValueEval ve) { + if(ve == null) { + throw new IllegalArgumentException("ve must not be null"); + } + if(ptg == null) { + throw new IllegalArgumentException("ptg must not be null"); + } + value = ve; + delegate = ptg; } - public ValueEval getInnerValueEval() { return value; } - - public short getRow() { + public int getRow() { return delegate.getRow(); } - - public short getColumn() { + public int getColumn() { return delegate.getColumn(); } - - public boolean isEvaluated() { - return evaluated; + public int getExternSheetIndex() { + return delegate.getExternSheetIndex(); } - } diff --git a/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/RefEval.java b/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/RefEval.java index bb72adc4a0..e462586d72 100644 --- a/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/RefEval.java +++ b/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/RefEval.java @@ -14,11 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -/* - * Created on May 9, 2005 - * - * - */ + package org.apache.poi.hssf.record.formula.eval; /** @@ -44,26 +40,12 @@ public interface RefEval extends ValueEval { public ValueEval getInnerValueEval(); /** - * returns the column index. + * returns the zero based column index. */ - public short getColumn(); + public int getColumn(); /** - * returns the row index. + * returns the zero based row index. */ - public short getRow(); - - /** - * returns true if this RefEval contains an - * evaluated value instead of a direct value. - * eg. say cell A1 has the value: ="test" - * Then the RefEval representing A1 will return - * isEvaluated() equal to false. On the other - * hand, say cell A1 has the value: =B1 and - * B1 has the value "test", then the RefEval - * representing A1 will return isEvaluated() - * equal to true. - */ - public boolean isEvaluated(); - + public int getRow(); } diff --git a/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/StringEval.java b/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/StringEval.java index 01af4e8436..27a9c6a627 100644 --- a/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/StringEval.java +++ b/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/StringEval.java @@ -14,10 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -/* - * Created on May 8, 2005 - * - */ + package org.apache.poi.hssf.record.formula.eval; import org.apache.poi.hssf.record.formula.Ptg; @@ -27,21 +24,31 @@ import org.apache.poi.hssf.record.formula.StringPtg; * @author Amol S. Deshmukh < amolweb at ya hoo dot com > * */ -public class StringEval implements StringValueEval { +public final class StringEval implements StringValueEval { public static final StringEval EMPTY_INSTANCE = new StringEval(""); - private String value; + private final String value; public StringEval(Ptg ptg) { - this.value = ((StringPtg) ptg).getValue(); + this(((StringPtg) ptg).getValue()); } public StringEval(String value) { + if(value == null) { + throw new IllegalArgumentException("value must not be null"); + } this.value = value; } public String getStringValue() { return value; } + public String toString() { + StringBuffer sb = new StringBuffer(64); + sb.append(getClass().getName()).append(" ["); + sb.append(value); + sb.append("]"); + return sb.toString(); + } } diff --git a/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/StringValueEval.java b/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/StringValueEval.java index b692f01ea2..46c12236b9 100644 --- a/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/StringValueEval.java +++ b/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/StringValueEval.java @@ -14,10 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -/* - * Created on May 8, 2005 - * - */ + package org.apache.poi.hssf.record.formula.eval; /** @@ -26,5 +23,8 @@ package org.apache.poi.hssf.record.formula.eval; */ public interface StringValueEval extends ValueEval { - public String getStringValue(); + /** + * @return never null, possibly empty string. + */ + String getStringValue(); } diff --git a/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/SubtractEval.java b/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/SubtractEval.java index 4bd77029f7..85a3845299 100644 --- a/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/SubtractEval.java +++ b/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/SubtractEval.java @@ -14,10 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -/* - * Created on May 8, 2005 - * - */ + package org.apache.poi.hssf.record.formula.eval; import org.apache.poi.hssf.record.formula.Ptg; @@ -27,15 +24,13 @@ import org.apache.poi.hssf.record.formula.SubtractPtg; * @author Amol S. Deshmukh < amolweb at ya hoo dot com > * */ -public class SubtractEval extends NumericOperationEval { +public final class SubtractEval extends NumericOperationEval { private SubtractPtg delegate; private static final ValueEvalToNumericXlator NUM_XLATOR = new ValueEvalToNumericXlator((short) ( ValueEvalToNumericXlator.BOOL_IS_PARSED - | ValueEvalToNumericXlator.EVALUATED_REF_BOOL_IS_PARSED - | ValueEvalToNumericXlator.EVALUATED_REF_STRING_IS_PARSED | ValueEvalToNumericXlator.REF_BOOL_IS_PARSED | ValueEvalToNumericXlator.STRING_IS_PARSED | ValueEvalToNumericXlator.REF_STRING_IS_PARSED @@ -49,18 +44,28 @@ public class SubtractEval extends NumericOperationEval { return NUM_XLATOR; } - public Eval evaluate(Eval[] operands, int srcRow, short srcCol) { + public Eval evaluate(Eval[] args, int srcRow, short srcCol) { + if(args.length != 2) { + return ErrorEval.VALUE_INVALID; + } Eval retval = null; double d0 = 0; double d1 = 0; - switch (operands.length) { - default: // will rarely happen. currently the parser itself fails. - retval = ErrorEval.UNKNOWN_ERROR; - break; - case 2: - ValueEval ve = singleOperandEvaluate(operands[0], srcRow, srcCol); + ValueEval ve = singleOperandEvaluate(args[0], srcRow, srcCol); + if (ve instanceof NumericValueEval) { + d0 = ((NumericValueEval) ve).getNumberValue(); + } + else if (ve instanceof BlankEval) { + // do nothing + } + else { + retval = ErrorEval.VALUE_INVALID; + } + + if (retval == null) { // no error yet + ve = singleOperandEvaluate(args[1], srcRow, srcCol); if (ve instanceof NumericValueEval) { - d0 = ((NumericValueEval) ve).getNumberValue(); + d1 = ((NumericValueEval) ve).getNumberValue(); } else if (ve instanceof BlankEval) { // do nothing @@ -68,21 +73,8 @@ public class SubtractEval extends NumericOperationEval { else { retval = ErrorEval.VALUE_INVALID; } - - if (retval == null) { // no error yet - ve = singleOperandEvaluate(operands[1], srcRow, srcCol); - if (ve instanceof NumericValueEval) { - d1 = ((NumericValueEval) ve).getNumberValue(); - } - else if (ve instanceof BlankEval) { - // do nothing - } - else { - retval = ErrorEval.VALUE_INVALID; - } - } - } // end switch - + } + if (retval == null) { retval = (Double.isNaN(d0) || Double.isNaN(d1)) ? (ValueEval) ErrorEval.VALUE_INVALID diff --git a/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/UnaryMinusEval.java b/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/UnaryMinusEval.java index b4975eefcf..ef6f533ea5 100644 --- a/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/UnaryMinusEval.java +++ b/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/UnaryMinusEval.java @@ -14,10 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -/* - * Created on May 8, 2005 - * - */ + package org.apache.poi.hssf.record.formula.eval; import org.apache.poi.hssf.record.formula.Ptg; @@ -27,14 +24,12 @@ import org.apache.poi.hssf.record.formula.UnaryMinusPtg; * @author Amol S. Deshmukh < amolweb at ya hoo dot com > * */ -public class UnaryMinusEval extends NumericOperationEval { +public final class UnaryMinusEval extends NumericOperationEval { private UnaryMinusPtg delegate; private static final ValueEvalToNumericXlator NUM_XLATOR = new ValueEvalToNumericXlator((short) ( ValueEvalToNumericXlator.BOOL_IS_PARSED - | ValueEvalToNumericXlator.EVALUATED_REF_BOOL_IS_PARSED - | ValueEvalToNumericXlator.EVALUATED_REF_STRING_IS_PARSED | ValueEvalToNumericXlator.REF_BOOL_IS_PARSED | ValueEvalToNumericXlator.STRING_IS_PARSED | ValueEvalToNumericXlator.REF_STRING_IS_PARSED @@ -49,32 +44,24 @@ public class UnaryMinusEval extends NumericOperationEval { return NUM_XLATOR; } - public Eval evaluate(Eval[] operands, int srcRow, short srcCol) { - ValueEval retval = null; + public Eval evaluate(Eval[] args, int srcRow, short srcCol) { + if(args.length != 1) { + return ErrorEval.VALUE_INVALID; + } double d = 0; - switch (operands.length) { - default: - retval = ErrorEval.UNKNOWN_ERROR; - break; - case 1: - ValueEval ve = singleOperandEvaluate(operands[0], srcRow, srcCol); - if (ve instanceof NumericValueEval) { - d = ((NumericValueEval) ve).getNumberValue(); - } - else if (ve instanceof BlankEval) { - // do nothing - } - else if (ve instanceof ErrorEval) { - retval = ve; - } + ValueEval ve = singleOperandEvaluate(args[0], srcRow, srcCol); + if (ve instanceof NumericValueEval) { + d = ((NumericValueEval) ve).getNumberValue(); } - - if (retval == null) { - retval = new NumberEval(-d); + else if (ve instanceof BlankEval) { + // do nothing } - - return retval; + else if (ve instanceof ErrorEval) { + return ve; + } + + return new NumberEval(-d); } public int getNumberOfOperands() { diff --git a/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/UnaryPlusEval.java b/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/UnaryPlusEval.java index 847aa56fa6..edcc7bee79 100644 --- a/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/UnaryPlusEval.java +++ b/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/UnaryPlusEval.java @@ -14,10 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -/* - * Created on May 8, 2005 - * - */ + package org.apache.poi.hssf.record.formula.eval; import org.apache.poi.hssf.record.formula.Ptg; @@ -27,111 +24,38 @@ import org.apache.poi.hssf.record.formula.UnaryPlusPtg; * @author Amol S. Deshmukh < amolweb at ya hoo dot com > * */ -public class UnaryPlusEval implements OperationEval /*extends NumericOperationEval*/ { +public final class UnaryPlusEval implements OperationEval { private UnaryPlusPtg delegate; - /* - * COMMENT FOR COMMENTED CODE IN THIS FILE - * - * In excel the programmer seems to not have cared to - * think about how strings were handled in other numeric - * operations when he/she was implementing this operation :P - * - * Here's what I mean: - * - * Q. If the formula -"hello" evaluates to #VALUE! in excel, what should - * the formula +"hello" evaluate to? - * - * A. +"hello" evaluates to "hello" (what the...?) - * + /** + * called by reflection */ - - -// private static final ValueEvalToNumericXlator NUM_XLATOR = -// new ValueEvalToNumericXlator((short) -// ( ValueEvalToNumericXlator.BOOL_IS_PARSED -// | ValueEvalToNumericXlator.EVALUATED_REF_BOOL_IS_PARSED -// | ValueEvalToNumericXlator.EVALUATED_REF_STRING_IS_PARSED -// | ValueEvalToNumericXlator.REF_BOOL_IS_PARSED -// | ValueEvalToNumericXlator.STRING_IS_PARSED -// )); - - public UnaryPlusEval(Ptg ptg) { this.delegate = (UnaryPlusPtg) ptg; } - -// protected ValueEvalToNumericXlator getXlator() { -// return NUM_XLATOR; -// } - - public Eval evaluate(Eval[] operands, int srcRow, short srcCol) { - ValueEval retval = null; - - switch (operands.length) { - default: - retval = ErrorEval.UNKNOWN_ERROR; - break; - case 1: -// ValueEval ve = singleOperandEvaluate(operands[0], srcRow, srcCol); -// if (ve instanceof NumericValueEval) { -// d = ((NumericValueEval) ve).getNumberValue(); -// } -// else if (ve instanceof BlankEval) { -// // do nothing -// } -// else if (ve instanceof ErrorEval) { -// retval = ve; -// } - if (operands[0] instanceof RefEval) { - RefEval re = (RefEval) operands[0]; - retval = re.getInnerValueEval(); - } - else if (operands[0] instanceof AreaEval) { - AreaEval ae = (AreaEval) operands[0]; - if (ae.contains(srcRow, srcCol)) { // circular ref! - retval = ErrorEval.CIRCULAR_REF_ERROR; - } - else if (ae.isRow()) { - if (ae.containsColumn(srcCol)) { - ValueEval ve = ae.getValueAt(ae.getFirstRow(), srcCol); - if (ve instanceof RefEval) { - ve = ((RefEval) ve).getInnerValueEval(); - } - retval = ve; - } - else { - retval = ErrorEval.VALUE_INVALID; - } - } - else if (ae.isColumn()) { - if (ae.containsRow(srcRow)) { - ValueEval ve = ae.getValueAt(ae.getFirstRow(), srcCol); - if (ve instanceof RefEval) { - ve = ((RefEval) ve).getInnerValueEval(); - } - retval = ve; - } - else { - retval = ErrorEval.VALUE_INVALID; - } - } - else { - retval = ErrorEval.VALUE_INVALID; - } - } - else { - retval = (ValueEval) operands[0]; - } - } - - if (retval instanceof BlankEval) { - retval = new NumberEval(0); - } - - return retval; + public Eval evaluate(Eval[] args, int srcCellRow, short srcCellCol) { + if(args.length != 1) { + return ErrorEval.VALUE_INVALID; + } + double d; + try { + ValueEval ve = OperandResolver.getSingleValue(args[0], srcCellRow, srcCellCol); + if(ve instanceof BlankEval) { + return NumberEval.ZERO; + } + if(ve instanceof StringEval) { + // Note - asymmetric with UnaryMinus + // -"hello" evaluates to #VALUE! + // but +"hello" evaluates to "hello" + return ve; + } + d = OperandResolver.coerceValueToDouble(ve); + } catch (EvaluationException e) { + return e.getErrorEval(); + } + return new NumberEval(+d); } public int getNumberOfOperands() { @@ -141,5 +65,4 @@ public class UnaryPlusEval implements OperationEval /*extends NumericOperationEv public int getType() { return delegate.getType(); } - } diff --git a/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/ValueEvalToNumericXlator.java b/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/ValueEvalToNumericXlator.java index 5ffa2faeef..1abcf34d2b 100644 --- a/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/ValueEvalToNumericXlator.java +++ b/src/scratchpad/src/org/apache/poi/hssf/record/formula/eval/ValueEvalToNumericXlator.java @@ -24,7 +24,7 @@ package org.apache.poi.hssf.record.formula.eval; * @author Amol S. Deshmukh < amolweb at ya hoo dot com > * */ -public class ValueEvalToNumericXlator { +public final class ValueEvalToNumericXlator { public static final int STRING_IS_PARSED = 0x0001; public static final int BOOL_IS_PARSED = 0x0002; @@ -34,26 +34,18 @@ public class ValueEvalToNumericXlator { public static final int REF_BOOL_IS_PARSED = 0x0010; public static final int REF_BLANK_IS_PARSED = 0x0020; - public static final int EVALUATED_REF_STRING_IS_PARSED = 0x0040; - public static final int EVALUATED_REF_BOOL_IS_PARSED = 0x0080; - public static final int EVALUATED_REF_BLANK_IS_PARSED = 0x0100; - - public static final int STRING_TO_BOOL_IS_PARSED = 0x0200; - public static final int REF_STRING_TO_BOOL_IS_PARSED = 0x0400; - public static final int STRING_IS_INVALID_VALUE = 0x0800; - public static final int REF_STRING_IS_INVALID_VALUE = 0x1000; - -// public static final int BOOL_IS_BLANK = 0x2000; -// public static final int REF_BOOL_IS_BLANK = 0x4000; -// public static final int STRING_IS_BLANK = 0x8000; -// public static final int REF_STRING_IS_BLANK = 0x10000; private final int flags; public ValueEvalToNumericXlator(int flags) { - this.flags = flags; + + if (false) { // uncomment to see who is using this class + System.err.println(new Throwable().getStackTrace()[1].getClassName() + "\t0x" + + Integer.toHexString(flags).toUpperCase()); + } + this.flags = flags; } /** @@ -71,7 +63,7 @@ public class ValueEvalToNumericXlator { // most common case - least worries :) else if (eval instanceof NumberEval) { - retval = (NumberEval) eval; + retval = eval; } // booleval @@ -125,50 +117,33 @@ public class ValueEvalToNumericXlator { * @param eval */ private ValueEval xlateRefEval(RefEval reval) { - ValueEval retval = null; - ValueEval eval = (ValueEval) reval.getInnerValueEval(); + ValueEval eval = reval.getInnerValueEval(); // most common case - least worries :) if (eval instanceof NumberEval) { - retval = (NumberEval) eval; + return eval; } - // booleval - else if (eval instanceof BoolEval) { - retval = ((flags & REF_BOOL_IS_PARSED) > 0) + if (eval instanceof BoolEval) { + return ((flags & REF_BOOL_IS_PARSED) > 0) ? (ValueEval) eval : BlankEval.INSTANCE; } - // stringeval - else if (eval instanceof StringEval) { - retval = xlateRefStringEval((StringEval) eval); - } - - // erroreval - else if (eval instanceof ErrorEval) { - retval = eval; - } - - // refeval - else if (eval instanceof RefEval) { - RefEval re = (RefEval) eval; - retval = xlateRefEval(re); + if (eval instanceof StringEval) { + return xlateRefStringEval((StringEval) eval); } - else if (eval instanceof BlankEval) { - retval = xlateBlankEval(reval.isEvaluated() ? EVALUATED_REF_BLANK_IS_PARSED : REF_BLANK_IS_PARSED); + if (eval instanceof ErrorEval) { + return eval; } - // probably AreaEval ? then not acceptable. - else { - throw new RuntimeException("Invalid ValueEval type passed for conversion: " + eval.getClass()); + if (eval instanceof BlankEval) { + return xlateBlankEval(REF_BLANK_IS_PARSED); } - - - - return retval; + throw new RuntimeException("Invalid ValueEval type passed for conversion: (" + + eval.getClass().getName() + ")"); } /** @@ -176,93 +151,38 @@ public class ValueEvalToNumericXlator { * @param eval */ private ValueEval xlateStringEval(StringEval eval) { - ValueEval retval = null; + if ((flags & STRING_IS_PARSED) > 0) { String s = eval.getStringValue(); - try { - double d = Double.parseDouble(s); - retval = new NumberEval(d); - } - catch (Exception e) { - if ((flags & STRING_TO_BOOL_IS_PARSED) > 0) { - try { - boolean b = Boolean.getBoolean(s); - retval = b ? BoolEval.TRUE : BoolEval.FALSE; - } - catch (Exception e2) { retval = ErrorEval.VALUE_INVALID; } - } - else { - retval = ErrorEval.VALUE_INVALID; - } + Double d = OperandResolver.parseDouble(s); + if(d == null) { + return ErrorEval.VALUE_INVALID; } + return new NumberEval(d.doubleValue()); } - else if ((flags & STRING_TO_BOOL_IS_PARSED) > 0) { - String s = eval.getStringValue(); - try { - boolean b = Boolean.getBoolean(s); - retval = b ? BoolEval.TRUE : BoolEval.FALSE; - } - catch (Exception e) { retval = ErrorEval.VALUE_INVALID; } - } - // strings are errors? - else if ((flags & STRING_IS_INVALID_VALUE) > 0) { - retval = ErrorEval.VALUE_INVALID; + if ((flags & STRING_IS_INVALID_VALUE) > 0) { + return ErrorEval.VALUE_INVALID; } // ignore strings - else { - retval = xlateBlankEval(BLANK_IS_PARSED); - } - return retval; + return xlateBlankEval(BLANK_IS_PARSED); } /** * uses the relevant flags to decode the StringEval * @param eval */ - private ValueEval xlateRefStringEval(StringEval eval) { - ValueEval retval = null; + private ValueEval xlateRefStringEval(StringEval sve) { if ((flags & REF_STRING_IS_PARSED) > 0) { - StringEval sve = (StringEval) eval; String s = sve.getStringValue(); - try { - double d = Double.parseDouble(s); - retval = new NumberEval(d); - } - catch (Exception e) { - if ((flags & REF_STRING_TO_BOOL_IS_PARSED) > 0) { - try { - boolean b = Boolean.getBoolean(s); - retval = b ? BoolEval.TRUE : BoolEval.FALSE; - } - catch (Exception e2) { retval = ErrorEval.VALUE_INVALID; } - } - else { - retval = ErrorEval.VALUE_INVALID; - } + Double d = OperandResolver.parseDouble(s); + if(d == null) { + return ErrorEval.VALUE_INVALID; } + return new NumberEval(d.doubleValue()); } - else if ((flags & REF_STRING_TO_BOOL_IS_PARSED) > 0) { - StringEval sve = (StringEval) eval; - String s = sve.getStringValue(); - try { - boolean b = Boolean.getBoolean(s); - retval = b ? BoolEval.TRUE : BoolEval.FALSE;; - } - catch (Exception e) { retval = ErrorEval.VALUE_INVALID; } - } - - // strings are errors? - else if ((flags & REF_STRING_IS_INVALID_VALUE) > 0) { - retval = ErrorEval.VALUE_INVALID; - } - // strings are blanks - else { - retval = BlankEval.INSTANCE; - } - return retval; + return BlankEval.INSTANCE; } - } diff --git a/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Avedev.java b/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Avedev.java index 592402b80d..bf888b97df 100644 --- a/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Avedev.java +++ b/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Avedev.java @@ -33,8 +33,8 @@ import org.apache.poi.hssf.record.formula.eval.ValueEvalToNumericXlator; public class Avedev extends MultiOperandNumericFunction { private static final ValueEvalToNumericXlator DEFAULT_NUM_XLATOR = - new ValueEvalToNumericXlator((short) (0 - // ValueEvalToNumericXlator.BOOL_IS_PARSED + new ValueEvalToNumericXlator((short) ( + ValueEvalToNumericXlator.BOOL_IS_PARSED //| ValueEvalToNumericXlator.REF_BOOL_IS_PARSED //| ValueEvalToNumericXlator.EVALUATED_REF_BOOL_IS_PARSED | ValueEvalToNumericXlator.STRING_IS_PARSED @@ -44,7 +44,6 @@ public class Avedev extends MultiOperandNumericFunction { //| ValueEvalToNumericXlator.REF_STRING_TO_BOOL_IS_PARSED //| ValueEvalToNumericXlator.STRING_IS_INVALID_VALUE //| ValueEvalToNumericXlator.REF_STRING_IS_INVALID_VALUE - | ValueEvalToNumericXlator.EVALUATED_REF_BLANK_IS_PARSED )); /** diff --git a/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Average.java b/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Average.java index 3491109177..4043040713 100644 --- a/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Average.java +++ b/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Average.java @@ -33,8 +33,8 @@ import org.apache.poi.hssf.record.formula.eval.ValueEvalToNumericXlator; public class Average extends MultiOperandNumericFunction { private static final ValueEvalToNumericXlator DEFAULT_NUM_XLATOR = - new ValueEvalToNumericXlator((short) (0 - // ValueEvalToNumericXlator.BOOL_IS_PARSED + new ValueEvalToNumericXlator((short) ( + ValueEvalToNumericXlator.BOOL_IS_PARSED //| ValueEvalToNumericXlator.REF_BOOL_IS_PARSED //| ValueEvalToNumericXlator.EVALUATED_REF_BOOL_IS_PARSED | ValueEvalToNumericXlator.STRING_IS_PARSED @@ -44,7 +44,6 @@ public class Average extends MultiOperandNumericFunction { //| ValueEvalToNumericXlator.REF_STRING_TO_BOOL_IS_PARSED //| ValueEvalToNumericXlator.STRING_IS_INVALID_VALUE //| ValueEvalToNumericXlator.REF_STRING_IS_INVALID_VALUE - | ValueEvalToNumericXlator.EVALUATED_REF_BLANK_IS_PARSED )); /** diff --git a/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/FinanceFunction.java b/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/FinanceFunction.java index 8eb7e841d6..c054c6dac4 100644 --- a/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/FinanceFunction.java +++ b/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/FinanceFunction.java @@ -14,10 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -/* - * Created on Jun 20, 2005 - * - */ + package org.apache.poi.hssf.record.formula.functions; import org.apache.poi.hssf.record.formula.eval.BoolEval; @@ -38,13 +35,10 @@ public abstract class FinanceFunction extends NumericFunction { new ValueEvalToNumericXlator((short) (0 | ValueEvalToNumericXlator.BOOL_IS_PARSED | ValueEvalToNumericXlator.REF_BOOL_IS_PARSED - | ValueEvalToNumericXlator.EVALUATED_REF_BOOL_IS_PARSED | ValueEvalToNumericXlator.STRING_IS_PARSED | ValueEvalToNumericXlator.REF_STRING_IS_PARSED - | ValueEvalToNumericXlator.EVALUATED_REF_STRING_IS_PARSED | ValueEvalToNumericXlator.BLANK_IS_PARSED | ValueEvalToNumericXlator.REF_BLANK_IS_PARSED - | ValueEvalToNumericXlator.EVALUATED_REF_BLANK_IS_PARSED //| ValueEvalToNumericXlator.REF_STRING_TO_BOOL_IS_PARSED //| ValueEvalToNumericXlator.STRING_IS_INVALID_VALUE //| ValueEvalToNumericXlator.REF_STRING_IS_INVALID_VALUE @@ -56,11 +50,11 @@ public abstract class FinanceFunction extends NumericFunction { * if they desire to return a different ValueEvalToNumericXlator instance * than the default. */ - protected ValueEvalToNumericXlator getXlator() { + protected final ValueEvalToNumericXlator getXlator() { return DEFAULT_NUM_XLATOR; } - protected ValueEval singleOperandNumericAsBoolean(Eval eval, int srcRow, short srcCol) { + protected final ValueEval singleOperandNumericAsBoolean(Eval eval, int srcRow, short srcCol) { ValueEval retval = null; retval = singleOperandEvaluate(eval, srcRow, srcCol); if (retval instanceof NumericValueEval) { @@ -74,5 +68,4 @@ public abstract class FinanceFunction extends NumericFunction { } return retval; } - } diff --git a/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/FreeRefFunction.java b/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/FreeRefFunction.java new file mode 100755 index 0000000000..56d285543e --- /dev/null +++ b/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/FreeRefFunction.java @@ -0,0 +1,57 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +package org.apache.poi.hssf.record.formula.functions; + +import org.apache.poi.hssf.record.formula.eval.Eval; +import org.apache.poi.hssf.record.formula.eval.ValueEval; +import org.apache.poi.hssf.usermodel.HSSFSheet; +import org.apache.poi.hssf.usermodel.HSSFWorkbook; + + +/** + * For most Excel functions, involving references ((cell, area), (2d, 3d)), the references are + * passed in as arguments, and the exact location remains fixed. However, a select few Excel + * functions have the ability to access cells that were not part of any reference passed as an + * argument.
    + * Two important functions with this feature are INDIRECT and OFFSET

    + * + * In POI, the HSSFFormulaEvaluator evaluates every cell in each reference argument before + * calling the function. This means that functions using fixed references do not need access to + * the rest of the workbook to execute. Hence the evaluate() method on the common + * interface Function does not take a workbook parameter.

    + * + * This interface recognises the requirement of some functions to freely create and evaluate + * references beyond those passed in as arguments. + * + * @author Josh Micich + */ +public interface FreeRefFunction { + /** + * + * @param args the pre-evaluated arguments for this function. args is never null, + * nor are any of its elements. + * @param srcCellRow zero based row index of the cell containing the currently evaluating formula + * @param srcCellCol zero based column index of the cell containing the currently evaluating formula + * @param workbook is the workbook containing the formula/cell being evaluated + * @param sheet is the sheet containing the formula/cell being evaluated + * @return never null. Possibly an instance of ErrorEval in the case of + * a specified Excel error (Exceptions are never thrown to represent Excel errors). + * + */ + ValueEval evaluate(Eval[] args, int srcCellRow, short srcCellCol, HSSFWorkbook workbook, HSSFSheet sheet); +} diff --git a/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Hlookup.java b/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Hlookup.java index 8bac3d0c02..40ed1da490 100644 --- a/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Hlookup.java +++ b/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Hlookup.java @@ -1,25 +1,123 @@ -/* -* 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. -*/ -/* - * Created on May 15, 2005 - * - */ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + package org.apache.poi.hssf.record.formula.functions; -public class Hlookup extends NotImplementedFunction { +import org.apache.poi.hssf.record.formula.eval.AreaEval; +import org.apache.poi.hssf.record.formula.eval.ErrorEval; +import org.apache.poi.hssf.record.formula.eval.Eval; +import org.apache.poi.hssf.record.formula.eval.EvaluationException; +import org.apache.poi.hssf.record.formula.eval.OperandResolver; +import org.apache.poi.hssf.record.formula.eval.ValueEval; +import org.apache.poi.hssf.record.formula.functions.LookupUtils.ValueVector; +/** + * Implementation of the VLOOKUP() function.

    + * + * HLOOKUP finds a column in a lookup table by the first row value and returns the value from another row. + * + * Syntax:
    + * HLOOKUP(lookup_value, table_array, row_index_num, range_lookup)

    + * + * lookup_value The value to be found in the first column of the table array.
    + * table_array An area reference for the lookup data.
    + * row_index_num a 1 based index specifying which row value of the lookup data will be returned.
    + * range_lookup If TRUE (default), HLOOKUP finds the largest value less than or equal to + * the lookup_value. If FALSE, only exact matches will be considered
    + * + * @author Josh Micich + */ +public final class Hlookup implements Function { + + private static final class RowVector implements ValueVector { + + private final AreaEval _tableArray; + private final int _size; + private final int _rowAbsoluteIndex; + private final int _firstColumnAbsoluteIndex; + + public RowVector(AreaEval tableArray, int rowIndex) { + _rowAbsoluteIndex = tableArray.getFirstRow() + rowIndex; + if(!tableArray.containsRow(_rowAbsoluteIndex)) { + int lastRowIx = tableArray.getLastRow() - tableArray.getFirstRow(); + throw new IllegalArgumentException("Specified row index (" + rowIndex + + ") is outside the allowed range (0.." + lastRowIx + ")"); + } + _tableArray = tableArray; + _size = tableArray.getLastColumn() - tableArray.getFirstColumn() + 1; + if(_size < 1) { + throw new RuntimeException("bad table array size zero"); + } + _firstColumnAbsoluteIndex = tableArray.getFirstColumn(); + } + + public ValueEval getItem(int index) { + if(index>_size) { + throw new ArrayIndexOutOfBoundsException("Specified index (" + index + + ") is outside the allowed range (0.." + (_size-1) + ")"); + } + return _tableArray.getValueAt(_rowAbsoluteIndex, (short) (_firstColumnAbsoluteIndex + index)); + } + public int getSize() { + return _size; + } + } + + public Eval evaluate(Eval[] args, int srcCellRow, short srcCellCol) { + Eval arg3 = null; + switch(args.length) { + case 4: + arg3 = args[3]; // important: assumed array element is never null + case 3: + break; + default: + // wrong number of arguments + return ErrorEval.VALUE_INVALID; + } + try { + // Evaluation order: + // arg0 lookup_value, arg1 table_array, arg3 range_lookup, find lookup value, arg2 row_index, fetch result + ValueEval lookupValue = OperandResolver.getSingleValue(args[0], srcCellRow, srcCellCol); + AreaEval tableArray = LookupUtils.resolveTableArrayArg(args[1]); + boolean isRangeLookup = LookupUtils.resolveRangeLookupArg(arg3, srcCellRow, srcCellCol); + int colIndex = LookupUtils.lookupIndexOfValue(lookupValue, new RowVector(tableArray, 0), isRangeLookup); + ValueEval veColIndex = OperandResolver.getSingleValue(args[2], srcCellRow, srcCellCol); + int rowIndex = LookupUtils.resolveRowOrColIndexArg(veColIndex); + ValueVector resultCol = createResultColumnVector(tableArray, rowIndex); + return resultCol.getItem(colIndex); + } catch (EvaluationException e) { + return e.getErrorEval(); + } + } + + /** + * Returns one column from an AreaEval + * + * @throws EvaluationException (#VALUE!) if colIndex is negative, (#REF!) if colIndex is too high + */ + private ValueVector createResultColumnVector(AreaEval tableArray, int colIndex) throws EvaluationException { + if(colIndex < 0) { + throw EvaluationException.invalidValue(); + } + int nCols = tableArray.getLastColumn() - tableArray.getFirstRow() + 1; + + if(colIndex >= nCols) { + throw EvaluationException.invalidRef(); + } + return new RowVector(tableArray, colIndex); + } } diff --git a/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/If.java b/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/If.java index 90dbd591b7..7aba5db72a 100644 --- a/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/If.java +++ b/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/If.java @@ -28,28 +28,22 @@ import org.apache.poi.hssf.record.formula.eval.Eval; * @author Amol S. Deshmukh < amolweb at ya hoo dot com > * */ -public class If implements Function { +public final class If implements Function { + + public Eval evaluate(Eval[] args, int srcCellRow, short srcCellCol) { - public Eval evaluate(Eval[] evals, int srcCellRow, short srcCellCol) { - Eval retval = null; Eval evalWhenFalse = BoolEval.FALSE; - switch (evals.length) { + switch (args.length) { case 3: - evalWhenFalse = evals[2]; + evalWhenFalse = args[2]; case 2: - BoolEval beval = (BoolEval) evals[0]; + BoolEval beval = (BoolEval) args[0]; // TODO - class cast exception if (beval.getBooleanValue()) { - retval = evals[1]; - } - else { - retval = evalWhenFalse; + return args[1]; } - break; + return evalWhenFalse; default: - retval = ErrorEval.UNKNOWN_ERROR; + return ErrorEval.VALUE_INVALID; } - return retval; } - - } diff --git a/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Indirect.java b/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Indirect.java index c7464ffed8..935e7cdbbd 100644 --- a/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Indirect.java +++ b/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Indirect.java @@ -14,12 +14,36 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -/* - * Created on May 15, 2005 - * - */ + package org.apache.poi.hssf.record.formula.functions; -public class Indirect extends NotImplementedFunction { +import org.apache.poi.hssf.record.formula.eval.ErrorEval; +import org.apache.poi.hssf.record.formula.eval.Eval; +import org.apache.poi.hssf.record.formula.eval.ValueEval; +import org.apache.poi.hssf.usermodel.HSSFSheet; +import org.apache.poi.hssf.usermodel.HSSFWorkbook; + +/** + * Implementation for Excel function INDIRECT

    + * + * INDIRECT() returns the cell or area reference denoted by the text argument.

    + * + * Syntax:
    + * INDIRECT(ref_text,isA1Style)

    + * + * ref_text a string representation of the desired reference as it would normally be written + * in a cell formula.
    + * isA1Style (default TRUE) specifies whether the ref_text should be interpreted as A1-style + * or R1C1-style. + * + * + * @author Josh Micich + */ +public final class Indirect implements FreeRefFunction { + + public ValueEval evaluate(Eval[] args, int srcCellRow, short srcCellCol, HSSFWorkbook workbook, HSSFSheet sheet) { + // TODO - implement INDIRECT() + return ErrorEval.FUNCTION_NOT_IMPLEMENTED; + } } diff --git a/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Isblank.java b/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Isblank.java index 6e8f84b342..c0e482e5a8 100644 --- a/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Isblank.java +++ b/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Isblank.java @@ -14,79 +14,35 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -/* - * Created on May 15, 2005 - * - */ + package org.apache.poi.hssf.record.formula.functions; -import org.apache.poi.hssf.record.formula.eval.AreaEval; import org.apache.poi.hssf.record.formula.eval.BlankEval; import org.apache.poi.hssf.record.formula.eval.BoolEval; import org.apache.poi.hssf.record.formula.eval.ErrorEval; import org.apache.poi.hssf.record.formula.eval.Eval; -import org.apache.poi.hssf.record.formula.eval.RefEval; +import org.apache.poi.hssf.record.formula.eval.EvaluationException; +import org.apache.poi.hssf.record.formula.eval.OperandResolver; import org.apache.poi.hssf.record.formula.eval.ValueEval; /** * @author Amol S. Deshmukh < amolweb at ya hoo dot com > * */ -public class Isblank implements Function { +public final class Isblank implements Function { - public Eval evaluate(Eval[] operands, int srcCellRow, short srcCellCol) { - ValueEval retval = null; - boolean b = false; - - switch (operands.length) { - default: - retval = ErrorEval.VALUE_INVALID; - break; - case 1: - if (operands[0] instanceof BlankEval) { - b = true; - } - else if (operands[0] instanceof AreaEval) { - AreaEval ae = (AreaEval) operands[0]; - if (ae.contains(srcCellRow, srcCellCol)) { // circular ref! - retval = ErrorEval.CIRCULAR_REF_ERROR; - } - else if (ae.isRow()) { - if (ae.containsColumn(srcCellCol)) { - ValueEval ve = ae.getValueAt(ae.getFirstRow(), srcCellCol); - b = (ve instanceof BlankEval); - } - else { - b = false; - } - } - else if (ae.isColumn()) { - if (ae.containsRow(srcCellRow)) { - ValueEval ve = ae.getValueAt(srcCellRow, ae.getFirstColumn()); - b = (ve instanceof BlankEval); - } - else { - b = false; - } - } - else { - b = false; - } - } - else if (operands[0] instanceof RefEval) { - RefEval re = (RefEval) operands[0]; - b = (!re.isEvaluated()) && re.getInnerValueEval() instanceof BlankEval; - } - else { - b = false; - } - } - - if (retval == null) { - retval = b - ? BoolEval.TRUE - : BoolEval.FALSE; - } - return retval; - } + public Eval evaluate(Eval[] args, int srcCellRow, short srcCellCol) { + if(args.length != 1) { + return ErrorEval.VALUE_INVALID; + } + Eval arg = args[0]; + + ValueEval singleCellValue; + try { + singleCellValue = OperandResolver.getSingleValue(arg, srcCellRow, srcCellCol); + } catch (EvaluationException e) { + return BoolEval.FALSE; + } + return BoolEval.valueOf(singleCellValue instanceof BlankEval); + } } diff --git a/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Len.java b/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Len.java index c0cb39b268..0bc49b4070 100644 --- a/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Len.java +++ b/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Len.java @@ -14,125 +14,36 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -/* - * Created on May 15, 2005 - * - */ + package org.apache.poi.hssf.record.formula.functions; -import org.apache.poi.hssf.record.formula.eval.AreaEval; -import org.apache.poi.hssf.record.formula.eval.BlankEval; import org.apache.poi.hssf.record.formula.eval.ErrorEval; import org.apache.poi.hssf.record.formula.eval.Eval; +import org.apache.poi.hssf.record.formula.eval.EvaluationException; import org.apache.poi.hssf.record.formula.eval.NumberEval; -import org.apache.poi.hssf.record.formula.eval.RefEval; -import org.apache.poi.hssf.record.formula.eval.StringValueEval; +import org.apache.poi.hssf.record.formula.eval.OperandResolver; import org.apache.poi.hssf.record.formula.eval.ValueEval; /** * @author Amol S. Deshmukh < amolweb at ya hoo dot com > * */ -public class Len extends TextFunction { - +public final class Len extends TextFunction { - public Eval evaluate(Eval[] operands, int srcCellRow, short srcCellCol) { - ValueEval retval = null; - String s = null; - - switch (operands.length) { - default: - retval = ErrorEval.VALUE_INVALID; - break; - case 1: - ValueEval ve = singleOperandEvaluate(operands[0], srcCellRow, srcCellCol); - if (ve instanceof StringValueEval) { - StringValueEval sve = (StringValueEval) ve; - s = sve.getStringValue(); - } - else if (ve instanceof RefEval) { - RefEval re = (RefEval) ve; - ValueEval ive = re.getInnerValueEval(); - if (ive instanceof BlankEval) { - s = re.isEvaluated() ? "0" : null; - } - else if (ive instanceof StringValueEval) { - s = ((StringValueEval) ive).getStringValue(); - } - else if (ive instanceof BlankEval) {} - else { - retval = ErrorEval.VALUE_INVALID; - } - } - else if (ve instanceof BlankEval) {} - else { - retval = ErrorEval.VALUE_INVALID; - break; - } - } - - if (retval == null) { - s = (s == null) ? EMPTY_STRING : s; - retval = new NumberEval(s.length()); - } - - return retval; - } - - - protected ValueEval singleOperandEvaluate(Eval eval, int srcRow, short srcCol) { - ValueEval retval; - if (eval instanceof AreaEval) { - AreaEval ae = (AreaEval) eval; - if (ae.contains(srcRow, srcCol)) { // circular ref! - retval = ErrorEval.CIRCULAR_REF_ERROR; - } - else if (ae.isRow()) { - if (ae.containsColumn(srcCol)) { - ValueEval ve = ae.getValueAt(ae.getFirstRow(), srcCol); - retval = attemptXlateToText(ve); - } - else { - retval = ErrorEval.VALUE_INVALID; - } - } - else if (ae.isColumn()) { - if (ae.containsRow(srcRow)) { - ValueEval ve = ae.getValueAt(srcRow, ae.getFirstColumn()); - retval = attemptXlateToText(ve); - } - else { - retval = ErrorEval.VALUE_INVALID; - } - } - else { - retval = ErrorEval.VALUE_INVALID; - } - } - else { - retval = attemptXlateToText((ValueEval) eval); - } - return retval; - } + public Eval evaluate(Eval[] args, int srcCellRow, short srcCellCol) { + + if(args.length != 1) { + return ErrorEval.VALUE_INVALID; + } + + try { + ValueEval veval = OperandResolver.getSingleValue(args[0], srcCellRow, srcCellCol); - - /** - * converts from Different ValueEval types to StringEval. - * Note: AreaEvals are not handled, if arg is an AreaEval, - * the returned value is ErrorEval.VALUE_INVALID - * @param ve - */ - protected ValueEval attemptXlateToText(ValueEval ve) { - ValueEval retval; - if (ve instanceof StringValueEval || ve instanceof RefEval) { - retval = ve; - } - else if (ve instanceof BlankEval) { - retval = ve; - } - else { - retval = ErrorEval.VALUE_INVALID; - } - return retval; - } + String str = OperandResolver.coerceValueToString(veval); + + return new NumberEval(str.length()); + } catch (EvaluationException e) { + return e.getErrorEval(); + } + } } diff --git a/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Lookup.java b/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Lookup.java index f98ccca7e3..be1d0d0f94 100644 --- a/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Lookup.java +++ b/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Lookup.java @@ -1,25 +1,96 @@ -/* -* 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. -*/ -/* - * Created on May 15, 2005 - * - */ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + package org.apache.poi.hssf.record.formula.functions; -public class Lookup extends NotImplementedFunction { +import org.apache.poi.hssf.record.formula.eval.AreaEval; +import org.apache.poi.hssf.record.formula.eval.ErrorEval; +import org.apache.poi.hssf.record.formula.eval.Eval; +import org.apache.poi.hssf.record.formula.eval.EvaluationException; +import org.apache.poi.hssf.record.formula.eval.OperandResolver; +import org.apache.poi.hssf.record.formula.eval.ValueEval; +import org.apache.poi.hssf.record.formula.functions.LookupUtils.ValueVector; + +/** + * Implementation of Excel function LOOKUP.

    + * + * LOOKUP finds an index row in a lookup table by the first column value and returns the value from another column. + * + * Syntax:
    + * VLOOKUP(lookup_value, lookup_vector, result_vector)

    + * + * lookup_value The value to be found in the lookup vector.
    + * lookup_vector An area reference for the lookup data.
    + * result_vector Single row or single column area reference from which the result value is chosen.
    + * + * @author Josh Micich + */ +public final class Lookup implements Function { + private static final class SimpleValueVector implements ValueVector { + private final ValueEval[] _values; + + public SimpleValueVector(ValueEval[] values) { + _values = values; + } + public ValueEval getItem(int index) { + return _values[index]; + } + public int getSize() { + return _values.length; + } + } + + public Eval evaluate(Eval[] args, int srcCellRow, short srcCellCol) { + switch(args.length) { + case 3: + break; + case 2: + // complex rules to choose lookupVector and resultVector from the single area ref + throw new RuntimeException("Two arg version of LOOKUP not supported yet"); + default: + return ErrorEval.VALUE_INVALID; + } + + + try { + ValueEval lookupValue = OperandResolver.getSingleValue(args[0], srcCellRow, srcCellCol); + AreaEval aeLookupVector = LookupUtils.resolveTableArrayArg(args[1]); + AreaEval aeResultVector = LookupUtils.resolveTableArrayArg(args[2]); + + ValueVector lookupVector = createVector(aeLookupVector); + ValueVector resultVector = createVector(aeResultVector); + if(lookupVector.getSize() > resultVector.getSize()) { + // Excel seems to handle this by accessing past the end of the result vector. + throw new RuntimeException("Lookup vector and result vector of differing sizes not supported yet"); + } + int index = LookupUtils.lookupIndexOfValue(lookupValue, lookupVector, true); + + return resultVector.getItem(index); + } catch (EvaluationException e) { + return e.getErrorEval(); + } + } + private static ValueVector createVector(AreaEval ae) { + + if(!ae.isRow() && !ae.isColumn()) { + // extra complexity required to emulate the way LOOKUP can handles these abnormal cases. + throw new RuntimeException("non-vector lookup or result areas not supported yet"); + } + return new SimpleValueVector(ae.getValues()); + } } diff --git a/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/LookupUtils.java b/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/LookupUtils.java new file mode 100644 index 0000000000..d6a8489623 --- /dev/null +++ b/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/LookupUtils.java @@ -0,0 +1,530 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +package org.apache.poi.hssf.record.formula.functions; + +import org.apache.poi.hssf.record.formula.AreaPtg; +import org.apache.poi.hssf.record.formula.eval.Area2DEval; +import org.apache.poi.hssf.record.formula.eval.AreaEval; +import org.apache.poi.hssf.record.formula.eval.BlankEval; +import org.apache.poi.hssf.record.formula.eval.BoolEval; +import org.apache.poi.hssf.record.formula.eval.ErrorEval; +import org.apache.poi.hssf.record.formula.eval.Eval; +import org.apache.poi.hssf.record.formula.eval.EvaluationException; +import org.apache.poi.hssf.record.formula.eval.NumberEval; +import org.apache.poi.hssf.record.formula.eval.NumericValueEval; +import org.apache.poi.hssf.record.formula.eval.OperandResolver; +import org.apache.poi.hssf.record.formula.eval.RefEval; +import org.apache.poi.hssf.record.formula.eval.StringEval; +import org.apache.poi.hssf.record.formula.eval.ValueEval; + +/** + * Common functionality used by VLOOKUP, HLOOKUP, LOOKUP and MATCH + * + * @author Josh Micich + */ +final class LookupUtils { + + /** + * Represents a single row or column within an AreaEval. + */ + public interface ValueVector { + ValueEval getItem(int index); + int getSize(); + } + /** + * Enumeration to support 4 valued comparison results.

    + * Excel lookup functions have complex behaviour in the case where the lookup array has mixed + * types, and/or is unordered. Contrary to suggestions in some Excel documentation, there + * does not appear to be a universal ordering across types. The binary search algorithm used + * changes behaviour when the evaluated 'mid' value has a different type to the lookup value.

    + * + * A simple int might have done the same job, but there is risk in confusion with the well + * known Comparable.compareTo() and Comparator.compare() which both use + * a ubiquitous 3 value result encoding. + */ + public static final class CompareResult { + private final boolean _isTypeMismatch; + private final boolean _isLessThan; + private final boolean _isEqual; + private final boolean _isGreaterThan; + + private CompareResult(boolean isTypeMismatch, int simpleCompareResult) { + if(isTypeMismatch) { + _isTypeMismatch = true; + _isLessThan = false; + _isEqual = false; + _isGreaterThan = false; + } else { + _isTypeMismatch = false; + _isLessThan = simpleCompareResult < 0; + _isEqual = simpleCompareResult == 0; + _isGreaterThan = simpleCompareResult > 0; + } + } + public static final CompareResult TYPE_MISMATCH = new CompareResult(true, 0); + public static final CompareResult LESS_THAN = new CompareResult(false, -1); + public static final CompareResult EQUAL = new CompareResult(false, 0); + public static final CompareResult GREATER_THAN = new CompareResult(false, +1); + + public static final CompareResult valueOf(int simpleCompareResult) { + if(simpleCompareResult < 0) { + return LESS_THAN; + } + if(simpleCompareResult > 0) { + return GREATER_THAN; + } + return EQUAL; + } + + public boolean isTypeMismatch() { + return _isTypeMismatch; + } + public boolean isLessThan() { + return _isLessThan; + } + public boolean isEqual() { + return _isEqual; + } + public boolean isGreaterThan() { + return _isGreaterThan; + } + public String toString() { + StringBuffer sb = new StringBuffer(64); + sb.append(getClass().getName()).append(" ["); + sb.append(formatAsString()); + sb.append("]"); + return sb.toString(); + } + + private String formatAsString() { + if(_isTypeMismatch) { + return "TYPE_MISMATCH"; + } + if(_isLessThan) { + return "LESS_THAN"; + } + if(_isEqual) { + return "EQUAL"; + } + if(_isGreaterThan) { + return "GREATER_THAN"; + } + // toString must be reliable + return "??error??"; + } + } + + public interface LookupValueComparer { + /** + * @return one of 4 instances or CompareResult: LESS_THAN, EQUAL, + * GREATER_THAN or TYPE_MISMATCH + */ + CompareResult compareTo(ValueEval other); + } + + private static abstract class LookupValueComparerBase implements LookupValueComparer { + + private final Class _targetClass; + protected LookupValueComparerBase(ValueEval targetValue) { + if(targetValue == null) { + throw new RuntimeException("targetValue cannot be null"); + } + _targetClass = targetValue.getClass(); + } + public final CompareResult compareTo(ValueEval other) { + if (other == null) { + throw new RuntimeException("compare to value cannot be null"); + } + if (_targetClass != other.getClass()) { + return CompareResult.TYPE_MISMATCH; + } + if (_targetClass == StringEval.class) { + + } + return compareSameType(other); + } + public String toString() { + StringBuffer sb = new StringBuffer(64); + sb.append(getClass().getName()).append(" ["); + sb.append(getValueAsString()); + sb.append("]"); + return sb.toString(); + } + protected abstract CompareResult compareSameType(ValueEval other); + /** used only for debug purposes */ + protected abstract String getValueAsString(); + } + + private static final class StringLookupComparer extends LookupValueComparerBase { + private String _value; + + protected StringLookupComparer(StringEval se) { + super(se); + _value = se.getStringValue(); + } + protected CompareResult compareSameType(ValueEval other) { + StringEval se = (StringEval) other; + return CompareResult.valueOf(_value.compareToIgnoreCase(se.getStringValue())); + } + protected String getValueAsString() { + return _value; + } + } + private static final class NumberLookupComparer extends LookupValueComparerBase { + private double _value; + + protected NumberLookupComparer(NumberEval ne) { + super(ne); + _value = ne.getNumberValue(); + } + protected CompareResult compareSameType(ValueEval other) { + NumberEval ne = (NumberEval) other; + return CompareResult.valueOf(Double.compare(_value, ne.getNumberValue())); + } + protected String getValueAsString() { + return String.valueOf(_value); + } + } + private static final class BooleanLookupComparer extends LookupValueComparerBase { + private boolean _value; + + protected BooleanLookupComparer(BoolEval be) { + super(be); + _value = be.getBooleanValue(); + } + protected CompareResult compareSameType(ValueEval other) { + BoolEval be = (BoolEval) other; + boolean otherVal = be.getBooleanValue(); + if(_value == otherVal) { + return CompareResult.EQUAL; + } + // TRUE > FALSE + if(_value) { + return CompareResult.GREATER_THAN; + } + return CompareResult.LESS_THAN; + } + protected String getValueAsString() { + return String.valueOf(_value); + } + } + + /** + * Processes the third argument to VLOOKUP, or HLOOKUP (col_index_num + * or row_index_num respectively).
    + * Sample behaviour: + * + * + * + * + * + * + * + * + * + * + * + * + * + *
    Input   ReturnValue  Thrown Error
    54 
    2.92 
    "5"4 
    "2.18e1"21 
    "-$2"-3*
    FALSE-1*
    TRUE0 
    "TRUE" #REF!
    "abc" #REF!
    "" #REF!
    <blank> #VALUE!

    + * + * * Note - out of range errors (both too high and too low) are handled by the caller. + * @return column or row index as a zero-based value + * + */ + public static int resolveRowOrColIndexArg(ValueEval veRowColIndexArg) throws EvaluationException { + if(veRowColIndexArg == null) { + throw new IllegalArgumentException("argument must not be null"); + } + if(veRowColIndexArg instanceof BlankEval) { + throw EvaluationException.invalidValue(); + } + if(veRowColIndexArg instanceof StringEval) { + StringEval se = (StringEval) veRowColIndexArg; + String strVal = se.getStringValue(); + Double dVal = OperandResolver.parseDouble(strVal); + if(dVal == null) { + // String does not resolve to a number. Raise #VALUE! error. + throw EvaluationException.invalidRef(); + // This includes text booleans "TRUE" and "FALSE". They are not valid. + } + // else - numeric value parses OK + } + // actual BoolEval values get interpreted as FALSE->0 and TRUE->1 + return OperandResolver.coerceValueToInt(veRowColIndexArg) - 1; + } + + + + /** + * The second argument (table_array) should be an area ref, but can actually be a cell ref, in + * which case it is interpreted as a 1x1 area ref. Other scalar values cause #VALUE! error. + */ + public static AreaEval resolveTableArrayArg(Eval eval) throws EvaluationException { + if (eval instanceof AreaEval) { + return (AreaEval) eval; + } + + if(eval instanceof RefEval) { + RefEval refEval = (RefEval) eval; + // Make this cell ref look like a 1x1 area ref. + + // It doesn't matter if eval is a 2D or 3D ref, because that detail is never asked of AreaEval. + // This code only requires the value array item. + // anything would be ok for rowIx and colIx, but may as well get it right. + int rowIx = refEval.getRow(); + int colIx = refEval.getColumn(); + AreaPtg ap = new AreaPtg(rowIx, rowIx, colIx, colIx, false, false, false, false); + ValueEval value = refEval.getInnerValueEval(); + return new Area2DEval(ap, new ValueEval[] { value, }); + } + throw EvaluationException.invalidValue(); + } + + + /** + * Resolves the last (optional) parameter (range_lookup) to the VLOOKUP and HLOOKUP functions. + * @param rangeLookupArg + * @param srcCellRow + * @param srcCellCol + * @return + * @throws EvaluationException + */ + public static boolean resolveRangeLookupArg(Eval rangeLookupArg, int srcCellRow, short srcCellCol) throws EvaluationException { + if(rangeLookupArg == null) { + // range_lookup arg not provided + return true; // default is TRUE + } + ValueEval valEval = OperandResolver.getSingleValue(rangeLookupArg, srcCellRow, srcCellCol); + if(valEval instanceof BlankEval) { + // Tricky: + // fourth arg supplied but evaluates to blank + // this does not get the default value + return false; + } + if(valEval instanceof BoolEval) { + // Happy day flow + BoolEval boolEval = (BoolEval) valEval; + return boolEval.getBooleanValue(); + } + + if (valEval instanceof StringEval) { + String stringValue = ((StringEval) valEval).getStringValue(); + if(stringValue.length() < 1) { + // More trickiness: + // Empty string is not the same as BlankEval. It causes #VALUE! error + throw EvaluationException.invalidValue(); + } + // TODO move parseBoolean to OperandResolver + Boolean b = Countif.parseBoolean(stringValue); + if(b != null) { + // string converted to boolean OK + return b.booleanValue(); + } + // Even more trickiness: + // Note - even if the StringEval represents a number value (for example "1"), + // Excel does not resolve it to a boolean. + throw EvaluationException.invalidValue(); + // This is in contrast to the code below,, where NumberEvals values (for + // example 0.01) *do* resolve to equivalent boolean values. + } + if (valEval instanceof NumericValueEval) { + NumericValueEval nve = (NumericValueEval) valEval; + // zero is FALSE, everything else is TRUE + return 0.0 != nve.getNumberValue(); + } + throw new RuntimeException("Unexpected eval type (" + valEval.getClass().getName() + ")"); + } + + public static int lookupIndexOfValue(ValueEval lookupValue, ValueVector vector, boolean isRangeLookup) throws EvaluationException { + LookupValueComparer lookupComparer = createLookupComparer(lookupValue); + int result; + if(isRangeLookup) { + result = performBinarySearch(vector, lookupComparer); + } else { + result = lookupIndexOfExactValue(lookupComparer, vector); + } + if(result < 0) { + throw new EvaluationException(ErrorEval.NA); + } + return result; + } + + + /** + * Finds first (lowest index) exact occurrence of specified value. + * @param lookupValue the value to be found in column or row vector + * @param vector the values to be searched. For VLOOKUP this is the first column of the + * tableArray. For HLOOKUP this is the first row of the tableArray. + * @return zero based index into the vector, -1 if value cannot be found + */ + private static int lookupIndexOfExactValue(LookupValueComparer lookupComparer, ValueVector vector) { + + // find first occurrence of lookup value + int size = vector.getSize(); + for (int i = 0; i < size; i++) { + if(lookupComparer.compareTo(vector.getItem(i)).isEqual()) { + return i; + } + } + return -1; + } + + + /** + * Encapsulates some standard binary search functionality so the unusual Excel behaviour can + * be clearly distinguished. + */ + private static final class BinarySearchIndexes { + + private int _lowIx; + private int _highIx; + + public BinarySearchIndexes(int highIx) { + _lowIx = -1; + _highIx = highIx; + } + + /** + * @return -1 if the search range is empty + */ + public int getMidIx() { + int ixDiff = _highIx - _lowIx; + if(ixDiff < 2) { + return -1; + } + return _lowIx + (ixDiff / 2); + } + + public int getLowIx() { + return _lowIx; + } + public int getHighIx() { + return _highIx; + } + public void narrowSearch(int midIx, boolean isLessThan) { + if(isLessThan) { + _highIx = midIx; + } else { + _lowIx = midIx; + } + } + } + /** + * Excel has funny behaviour when the some elements in the search vector are the wrong type. + * + */ + private static int performBinarySearch(ValueVector vector, LookupValueComparer lookupComparer) { + // both low and high indexes point to values assumed too low and too high. + BinarySearchIndexes bsi = new BinarySearchIndexes(vector.getSize()); + + while(true) { + int midIx = bsi.getMidIx(); + + if(midIx < 0) { + return bsi.getLowIx(); + } + CompareResult cr = lookupComparer.compareTo(vector.getItem(midIx)); + if(cr.isTypeMismatch()) { + int newMidIx = handleMidValueTypeMismatch(lookupComparer, vector, bsi, midIx); + if(newMidIx < 0) { + continue; + } + midIx = newMidIx; + cr = lookupComparer.compareTo(vector.getItem(midIx)); + } + if(cr.isEqual()) { + return findLastIndexInRunOfEqualValues(lookupComparer, vector, midIx, bsi.getHighIx()); + } + bsi.narrowSearch(midIx, cr.isLessThan()); + } + } + /** + * Excel seems to handle mismatched types initially by just stepping 'mid' ix forward to the + * first compatible value. + * @param midIx 'mid' index (value which has the wrong type) + * @return usually -1, signifying that the BinarySearchIndex has been narrowed to the new mid + * index. Zero or greater signifies that an exact match for the lookup value was found + */ + private static int handleMidValueTypeMismatch(LookupValueComparer lookupComparer, ValueVector vector, + BinarySearchIndexes bsi, int midIx) { + int newMid = midIx; + int highIx = bsi.getHighIx(); + + while(true) { + newMid++; + if(newMid == highIx) { + // every element from midIx to highIx was the wrong type + // move highIx down to the low end of the mid values + bsi.narrowSearch(midIx, true); + return -1; + } + CompareResult cr = lookupComparer.compareTo(vector.getItem(newMid)); + if(cr.isLessThan() && newMid == highIx-1) { + // move highIx down to the low end of the mid values + bsi.narrowSearch(midIx, true); + return -1; + // but only when "newMid == highIx-1"? slightly weird. + // It would seem more efficient to always do this. + } + if(cr.isTypeMismatch()) { + // keep stepping over values until the right type is found + continue; + } + if(cr.isEqual()) { + return newMid; + } + // Note - if moving highIx down (due to lookup + * + * Syntax:
    + * MATCH(lookup_value, lookup_array, match_type)

    + * + * Returns a 1-based index specifying at what position in the lookup_array the specified + * lookup_value is found.

    + * + * Specific matching behaviour can be modified with the optional match_type parameter. + * + * + * + * + * + * + *
    ValueMatching Behaviour
    1(default) find the largest value that is less than or equal to lookup_value. + * The lookup_array must be in ascending order*.
    0find the first value that is exactly equal to lookup_value. + * The lookup_array can be in any order.
    -1find the smallest value that is greater than or equal to lookup_value. + * The lookup_array must be in descending order*.
    + * + * * Note regarding order - For the match_type cases that require the lookup_array to + * be ordered, MATCH() can produce incorrect results if this requirement is not met. Observed + * behaviour in Excel is to return the lowest index value for which every item after that index + * breaks the match rule.
    + * The (ascending) sort order expected by MATCH() is:
    + * numbers (low to high), strings (A to Z), boolean (FALSE to TRUE)
    + * MATCH() ignores all elements in the lookup_array with a different type to the lookup_value. + * Type conversion of the lookup_array elements is never performed. + * + * + * @author Josh Micich + */ +public final class Match implements Function { + + + public Eval evaluate(Eval[] args, int srcCellRow, short srcCellCol) { + + double match_type = 1; // default + + switch(args.length) { + case 3: + try { + match_type = evaluateMatchTypeArg(args[2], srcCellRow, srcCellCol); + } catch (EvaluationException e) { + // Excel/MATCH() seems to have slightly abnormal handling of errors with + // the last parameter. Errors do not propagate up. Every error gets + // translated into #REF! + return ErrorEval.REF_INVALID; + } + case 2: + break; + default: + return ErrorEval.VALUE_INVALID; + } + + boolean matchExact = match_type == 0; + // Note - Excel does not strictly require -1 and +1 + boolean findLargestLessThanOrEqual = match_type > 0; + + + try { + ValueEval lookupValue = OperandResolver.getSingleValue(args[0], srcCellRow, srcCellCol); + ValueEval[] lookupRange = evaluateLookupRange(args[1]); + int index = findIndexOfValue(lookupValue, lookupRange, matchExact, findLargestLessThanOrEqual); + return new NumberEval(index + 1); // +1 to convert to 1-based + } catch (EvaluationException e) { + return e.getErrorEval(); + } + } + + private static ValueEval[] evaluateLookupRange(Eval eval) throws EvaluationException { + if (eval instanceof RefEval) { + RefEval re = (RefEval) eval; + return new ValueEval[] { re.getInnerValueEval(), }; + } + if (eval instanceof AreaEval) { + AreaEval ae = (AreaEval) eval; + if(!ae.isColumn() && !ae.isRow()) { + throw new EvaluationException(ErrorEval.NA); + } + return ae.getValues(); + } + + // Error handling for lookup_range arg is also unusual + if(eval instanceof NumericValueEval) { + throw new EvaluationException(ErrorEval.NA); + } + if (eval instanceof StringEval) { + StringEval se = (StringEval) eval; + Double d = OperandResolver.parseDouble(se.getStringValue()); + if(d == null) { + // plain string + throw new EvaluationException(ErrorEval.VALUE_INVALID); + } + // else looks like a number + throw new EvaluationException(ErrorEval.NA); + } + throw new RuntimeException("Unexpected eval type (" + eval.getClass().getName() + ")"); + } + + + + private static double evaluateMatchTypeArg(Eval arg, int srcCellRow, short srcCellCol) + throws EvaluationException { + Eval match_type = OperandResolver.getSingleValue(arg, srcCellRow, srcCellCol); + + if(match_type instanceof ErrorEval) { + throw new EvaluationException((ErrorEval)match_type); + } + if(match_type instanceof NumericValueEval) { + NumericValueEval ne = (NumericValueEval) match_type; + return ne.getNumberValue(); + } + if (match_type instanceof StringEval) { + StringEval se = (StringEval) match_type; + Double d = OperandResolver.parseDouble(se.getStringValue()); + if(d == null) { + // plain string + throw new EvaluationException(ErrorEval.VALUE_INVALID); + } + // if the string parses as a number, it is OK + return d.doubleValue(); + } + throw new RuntimeException("Unexpected match_type type (" + match_type.getClass().getName() + ")"); + } + + /** + * @return zero based index + */ + private static int findIndexOfValue(ValueEval lookupValue, ValueEval[] lookupRange, + boolean matchExact, boolean findLargestLessThanOrEqual) throws EvaluationException { + + LookupValueComparer lookupComparer = createLookupComparer(lookupValue, matchExact); + + if(matchExact) { + for (int i = 0; i < lookupRange.length; i++) { + if(lookupComparer.compareTo(lookupRange[i]).isEqual()) { + return i; + } + } + throw new EvaluationException(ErrorEval.NA); + } + + if(findLargestLessThanOrEqual) { + // Note - backward iteration + for (int i = lookupRange.length - 1; i>=0; i--) { + CompareResult cmp = lookupComparer.compareTo(lookupRange[i]); + if(cmp.isTypeMismatch()) { + continue; + } + if(!cmp.isLessThan()) { + return i; + } + } + throw new EvaluationException(ErrorEval.NA); + } + + // else - find smallest greater than or equal to + // TODO - is binary search used for (match_type==+1) ? + for (int i = 0; i=0 || stringValue.indexOf('*') >=0) { + return true; + } + return false; + } } diff --git a/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Maxa.java b/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Maxa.java index 21e30de0c2..e25db7b746 100644 --- a/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Maxa.java +++ b/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Maxa.java @@ -14,10 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -/* - * Created on May 15, 2005 - * - */ + package org.apache.poi.hssf.record.formula.functions; import org.apache.poi.hssf.record.formula.eval.ErrorEval; @@ -30,12 +27,11 @@ import org.apache.poi.hssf.record.formula.eval.ValueEvalToNumericXlator; * @author Amol S. Deshmukh < amolweb at ya hoo dot com > * */ -public class Maxa extends MultiOperandNumericFunction { +public final class Maxa extends MultiOperandNumericFunction { private static final ValueEvalToNumericXlator DEFAULT_NUM_XLATOR = new ValueEvalToNumericXlator((short) ( ValueEvalToNumericXlator.BOOL_IS_PARSED | ValueEvalToNumericXlator.REF_BOOL_IS_PARSED - | ValueEvalToNumericXlator.EVALUATED_REF_BOOL_IS_PARSED | ValueEvalToNumericXlator.STRING_IS_PARSED //| ValueEvalToNumericXlator.REF_STRING_IS_PARSED //| ValueEvalToNumericXlator.EVALUATED_REF_STRING_IS_PARSED diff --git a/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Mid.java b/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Mid.java index d6c4399ae3..7f30aa4cec 100644 --- a/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Mid.java +++ b/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Mid.java @@ -1,99 +1,92 @@ /* -* 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. -*/ -/* - * Created on May 15, 2005 + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ + package org.apache.poi.hssf.record.formula.functions; import org.apache.poi.hssf.record.formula.eval.BlankEval; import org.apache.poi.hssf.record.formula.eval.ErrorEval; import org.apache.poi.hssf.record.formula.eval.Eval; -import org.apache.poi.hssf.record.formula.eval.NumericValueEval; +import org.apache.poi.hssf.record.formula.eval.EvaluationException; +import org.apache.poi.hssf.record.formula.eval.OperandResolver; import org.apache.poi.hssf.record.formula.eval.StringEval; -import org.apache.poi.hssf.record.formula.eval.StringValueEval; import org.apache.poi.hssf.record.formula.eval.ValueEval; /** - * An implementation of the MID function: - * Returns a specific number of characters from a text string, - * starting at the position you specify, based on the number - * of characters you specify. + * An implementation of the MID function
    MID returns a specific number of + * characters from a text string, starting at the specified position.

    + * + * Syntax:
    MID(text, start_num, + * num_chars)
    + * * @author Manda Wilson < wilson at c bio dot msk cc dot org > */ -public class Mid extends TextFunction { +public class Mid implements Function { /** - * Returns a specific number of characters from a text string, - * starting at the position you specify, based on the number - * of characters you specify. + * Returns a specific number of characters from a text string, starting at + * the position you specify, based on the number of characters you specify. * * @see org.apache.poi.hssf.record.formula.eval.Eval */ - public Eval evaluate(Eval[] operands, int srcCellRow, short srcCellCol) { - Eval retval = null; - String str = null; - int startNum = 0; - int numChars = 0; - - switch (operands.length) { - default: - retval = ErrorEval.VALUE_INVALID; - case 3: - // first operand is text string containing characters to extract - // second operand is position of first character to extract - // third operand is the number of characters to return - ValueEval firstveval = singleOperandEvaluate(operands[0], srcCellRow, srcCellCol); - ValueEval secondveval = singleOperandEvaluate(operands[1], srcCellRow, srcCellCol); - ValueEval thirdveval = singleOperandEvaluate(operands[2], srcCellRow, srcCellCol); - if (firstveval instanceof StringValueEval - && secondveval instanceof NumericValueEval - && thirdveval instanceof NumericValueEval) { - - StringValueEval strEval = (StringValueEval) firstveval; - str = strEval.getStringValue(); - - NumericValueEval startNumEval = (NumericValueEval) secondveval; - // NOTE: it is safe to cast to int here - // because in Excel =MID("test", 1, 1.7) returns t - // so 1.7 must be truncated to 1 - // and =MID("test", 1.9, 2) returns te - // so 1.9 must be truncated to 1 - startNum = (int) startNumEval.getNumberValue(); - - NumericValueEval numCharsEval = (NumericValueEval) thirdveval; - numChars = (int) numCharsEval.getNumberValue(); - - } else { - retval = ErrorEval.VALUE_INVALID; - } - } - - if (retval == null) { - if (startNum < 1 || numChars < 0) { - retval = ErrorEval.VALUE_INVALID; - } else if (startNum > str.length() || numChars == 0) { - retval = BlankEval.INSTANCE; - } else if (startNum + numChars > str.length()) { - retval = new StringEval(str.substring(startNum - 1)); - } else { - retval = new StringEval(str.substring(startNum - 1, numChars)); - } - } - return retval; - } + public Eval evaluate(Eval[] args, int srcCellRow, short srcCellCol) { + if (args.length != 3) { + return ErrorEval.VALUE_INVALID; + } + + String text; + int startIx; // zero based + int numChars; + + try { + ValueEval evText = OperandResolver.getSingleValue(args[0], srcCellRow, srcCellCol); + text = OperandResolver.coerceValueToString(evText); + int startCharNum = evaluateNumberArg(args[1], srcCellRow, srcCellCol); + numChars = evaluateNumberArg(args[2], srcCellRow, srcCellCol); + startIx = startCharNum - 1; // convert to zero-based + } catch (EvaluationException e) { + return e.getErrorEval(); + } + + int len = text.length(); + if (startIx < 0) { + return ErrorEval.VALUE_INVALID; + } + if (numChars < 0) { + return ErrorEval.VALUE_INVALID; + } + if (numChars < 0 || startIx > len) { + return new StringEval(""); + } + int endIx = startIx + numChars; + if (endIx > len) { + endIx = len; + } + String result = text.substring(startIx, endIx); + return new StringEval(result); + + } + + private static int evaluateNumberArg(Eval arg, int srcCellRow, short srcCellCol) throws EvaluationException { + ValueEval ev = OperandResolver.getSingleValue(arg, srcCellRow, srcCellCol); + if (ev instanceof BlankEval) { + // Note - for start_num arg, blank causes error(#VALUE!), + // but for num_chars causes empty string to be returned. + return 0; + } -} + return OperandResolver.coerceValueToInt(ev); + } +} \ No newline at end of file diff --git a/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Mina.java b/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Mina.java index a998a870f6..21ba47b569 100644 --- a/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Mina.java +++ b/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Mina.java @@ -14,10 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -/* - * Created on May 15, 2005 - * - */ + package org.apache.poi.hssf.record.formula.functions; import org.apache.poi.hssf.record.formula.eval.ErrorEval; @@ -35,7 +32,6 @@ public class Mina extends MultiOperandNumericFunction { new ValueEvalToNumericXlator((short) ( ValueEvalToNumericXlator.BOOL_IS_PARSED | ValueEvalToNumericXlator.REF_BOOL_IS_PARSED - | ValueEvalToNumericXlator.EVALUATED_REF_BOOL_IS_PARSED | ValueEvalToNumericXlator.STRING_IS_PARSED //| ValueEvalToNumericXlator.REF_STRING_IS_PARSED //| ValueEvalToNumericXlator.EVALUATED_REF_STRING_IS_PARSED diff --git a/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/MultiOperandNumericFunction.java b/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/MultiOperandNumericFunction.java index 2840c89883..0e7cce217e 100644 --- a/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/MultiOperandNumericFunction.java +++ b/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/MultiOperandNumericFunction.java @@ -14,10 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -/* - * Created on May 22, 2005 - * - */ + package org.apache.poi.hssf.record.formula.functions; import org.apache.poi.hssf.record.formula.eval.AreaEval; @@ -36,32 +33,52 @@ import org.apache.poi.hssf.record.formula.eval.ValueEvalToNumericXlator; * where the order of operands does not matter */ public abstract class MultiOperandNumericFunction extends NumericFunction { + static final double[] EMPTY_DOUBLE_ARRAY = { }; + + private static class DoubleList { + private double[] _array; + private int _count; + + public DoubleList() { + _array = new double[8]; + _count = 0; + } + + public double[] toArray() { + if(_count < 1) { + return EMPTY_DOUBLE_ARRAY; + } + double[] result = new double[_count]; + System.arraycopy(_array, 0, result, 0, _count); + return result; + } + + public void add(double[] values) { + int addLen = values.length; + ensureCapacity(_count + addLen); + System.arraycopy(values, 0, _array, _count, addLen); + _count += addLen; + } - private static final ValueEvalToNumericXlator DEFAULT_NUM_XLATOR = - new ValueEvalToNumericXlator((short) ( - ValueEvalToNumericXlator.BOOL_IS_PARSED - | ValueEvalToNumericXlator.REF_BOOL_IS_PARSED - | ValueEvalToNumericXlator.EVALUATED_REF_BOOL_IS_PARSED - //| ValueEvalToNumericXlator.STRING_IS_PARSED - | ValueEvalToNumericXlator.REF_STRING_IS_PARSED - | ValueEvalToNumericXlator.EVALUATED_REF_STRING_IS_PARSED - //| ValueEvalToNumericXlator.STRING_TO_BOOL_IS_PARSED - //| ValueEvalToNumericXlator.REF_STRING_TO_BOOL_IS_PARSED - //| ValueEvalToNumericXlator.STRING_IS_INVALID_VALUE - //| ValueEvalToNumericXlator.REF_STRING_IS_INVALID_VALUE - )); + private void ensureCapacity(int reqSize) { + if(reqSize > _array.length) { + int newSize = reqSize * 3 / 2; // grow with 50% extra + double[] newArr = new double[newSize]; + System.arraycopy(_array, 0, newArr, 0, _count); + _array = newArr; + } + } + + public void add(double value) { + ensureCapacity(_count + 1); + _array[_count] = value; + _count++; + } + } private static final int DEFAULT_MAX_NUM_OPERANDS = 30; - /** - * this is the default impl for the factory method getXlator - * of the super class NumericFunction. Subclasses can override this method - * if they desire to return a different ValueEvalToNumericXlator instance - * than the default. - */ - protected ValueEvalToNumericXlator getXlator() { - return DEFAULT_NUM_XLATOR; - } + protected abstract ValueEvalToNumericXlator getXlator(); /** * Maximum number of operands accepted by this function. @@ -76,40 +93,26 @@ public abstract class MultiOperandNumericFunction extends NumericFunction { * from among the list of operands. Blanks and Blank equivalent cells * are ignored. Error operands or cells containing operands of type * that are considered invalid and would result in #VALUE! error in - * excel cause this function to return null. + * excel cause this function to return null. * * @param operands * @param srcRow * @param srcCol */ protected double[] getNumberArray(Eval[] operands, int srcRow, short srcCol) { - double[] retval = new double[30]; - int count = 0; + if (operands.length > getMaxNumOperands()) { + return null; + } + DoubleList retval = new DoubleList(); - outer: do { // goto simulator loop - if (operands.length > getMaxNumOperands()) { - break outer; + for (int i=0, iSize=operands.length; i indicate to calling subclass that error occurred - break; + return null; // indicate to calling subclass that error occurred } } + return retval.toArray(); } - else { // for ValueEvals other than AreaEval - retval = new double[1]; - ValueEval ve = singleOperandEvaluate(operand, srcRow, srcCol); - - if (ve instanceof NumericValueEval) { - NumericValueEval nve = (NumericValueEval) ve; - retval = putInArray(retval, count++, nve.getNumberValue()); - } - else if (ve instanceof BlankEval) {} // ignore operand - else { - retval = null; // null => indicate to calling subclass that error occurred - } - } - if (retval != null && retval.length >= 1) { - double[] temp = retval; - retval = new double[count]; - System.arraycopy(temp, 0, retval, 0, count); + // for ValueEvals other than AreaEval + ValueEval ve = singleOperandEvaluate(operand, srcRow, srcCol); + + if (ve instanceof NumericValueEval) { + NumericValueEval nve = (NumericValueEval) ve; + return new double[] { nve.getNumberValue(), }; } - return retval; + if (ve instanceof BlankEval) { + // ignore blanks + return EMPTY_DOUBLE_ARRAY; + } + return null; } /** - * puts d at position pos in array arr. If pos is greater than arr, the - * array is dynamically resized (using a simple doubling rule). - * @param arr - * @param pos - * @param d + * Ensures that a two dimensional array has all sub-arrays present and the same length + * @return false if any sub-array is missing, or is of different length */ - private static double[] putInArray(double[] arr, int pos, double d) { - double[] tarr = arr; - while (pos >= arr.length) { - arr = new double[arr.length << 1]; - } - if (tarr.length != arr.length) { - System.arraycopy(tarr, 0, arr, 0, tarr.length); - } - arr[pos] = d; - return arr; - } - - private static double[] putInArray(double[] arr, int pos, double[] d) { - double[] tarr = arr; - while (pos+d.length >= arr.length) { - arr = new double[arr.length << 1]; + protected static final boolean areSubArraysConsistent(double[][] values) { + + if (values == null || values.length < 1) { + // TODO this doesn't seem right. Fix or add comment. + return true; } - if (tarr.length != arr.length) { - System.arraycopy(tarr, 0, arr, 0, tarr.length); + + if (values[0] == null) { + return false; } - for (int i=0, iSize=d.length; i 0) { - if (values[0] == null) - break outer; - int len = values[0].length; - for (int i=1, iSize=values.length; i + * + * OFFSET returns an area reference that is a specified number of rows and columns from a + * reference cell or area.

    + * + * Syntax:
    + * OFFSET(reference, rows, cols, height, width)

    + * reference is the base reference.
    + * rows is the number of rows up or down from the base reference.
    + * cols is the number of columns left or right from the base reference.
    + * height (default same height as base reference) is the row count for the returned area reference.
    + * width (default same width as base reference) is the column count for the returned area reference.
    + * + * @author Josh Micich + */ +public final class Offset implements FreeRefFunction { + // These values are specific to BIFF8 + private static final int LAST_VALID_ROW_INDEX = 0xFFFF; + private static final int LAST_VALID_COLUMN_INDEX = 0xFF; + + + /** + * Exceptions are used within this class to help simplify flow control when error conditions + * are encountered + */ + private static final class EvalEx extends Exception { + private final ErrorEval _error; + + public EvalEx(ErrorEval error) { + _error = error; + } + public ErrorEval getError() { + return _error; + } + } + + /** + * A one dimensional base + offset. Represents either a row range or a column range. + * Two instances of this class together specify an area range. + */ + /* package */ static final class LinearOffsetRange { + + private final int _offset; + private final int _length; + + public LinearOffsetRange(int offset, int length) { + if(length == 0) { + // handled that condition much earlier + throw new RuntimeException("length may not be zero"); + } + _offset = offset; + _length = length; + } + + public short getFirstIndex() { + return (short) _offset; + } + public short getLastIndex() { + return (short) (_offset + _length - 1); + } + /** + * Moves the range by the specified translation amount.

    + * + * This method also 'normalises' the range: Excel specifies that the width and height + * parameters (length field here) cannot be negative. However, OFFSET() does produce + * sensible results in these cases. That behavior is replicated here.

    + * + * @param translationAmount may be zero negative or positive + * + * @return the equivalent LinearOffsetRange with a positive length, moved by the + * specified translationAmount. + */ + public LinearOffsetRange normaliseAndTranslate(int translationAmount) { + if (_length > 0) { + if(translationAmount == 0) { + return this; + } + return new LinearOffsetRange(translationAmount + _offset, _length); + } + return new LinearOffsetRange(translationAmount + _offset + _length + 1, -_length); + } + + public boolean isOutOfBounds(int lowValidIx, int highValidIx) { + if(_offset < lowValidIx) { + return true; + } + if(getLastIndex() > highValidIx) { + return true; + } + return false; + } + public String toString() { + StringBuffer sb = new StringBuffer(64); + sb.append(getClass().getName()).append(" ["); + sb.append(_offset).append("...").append(getLastIndex()); + sb.append("]"); + return sb.toString(); + } + } + + + /** + * Encapsulates either an area or cell reference which may be 2d or 3d. + */ + private static final class BaseRef { + private static final int INVALID_SHEET_INDEX = -1; + private final int _firstRowIndex; + private final int _firstColumnIndex; + private final int _width; + private final int _height; + private final int _externalSheetIndex; + + public BaseRef(RefEval re) { + _firstRowIndex = re.getRow(); + _firstColumnIndex = re.getColumn(); + _height = 1; + _width = 1; + if (re instanceof Ref3DEval) { + Ref3DEval r3e = (Ref3DEval) re; + _externalSheetIndex = r3e.getExternSheetIndex(); + } else { + _externalSheetIndex = INVALID_SHEET_INDEX; + } + } + + public BaseRef(AreaEval ae) { + _firstRowIndex = ae.getFirstRow(); + _firstColumnIndex = ae.getFirstColumn(); + _height = ae.getLastRow() - ae.getFirstRow() + 1; + _width = ae.getLastColumn() - ae.getFirstColumn() + 1; + if (ae instanceof Area3DEval) { + Area3DEval a3e = (Area3DEval) ae; + _externalSheetIndex = a3e.getExternSheetIndex(); + } else { + _externalSheetIndex = INVALID_SHEET_INDEX; + } + } + + public int getWidth() { + return _width; + } + + public int getHeight() { + return _height; + } + + public int getFirstRowIndex() { + return _firstRowIndex; + } + + public int getFirstColumnIndex() { + return _firstColumnIndex; + } + + public boolean isIs3d() { + return _externalSheetIndex > 0; + } + + public short getExternalSheetIndex() { + if(_externalSheetIndex < 0) { + throw new IllegalStateException("external sheet index only available for 3d refs"); + } + return (short) _externalSheetIndex; + } + + } + + public ValueEval evaluate(Eval[] args, int srcCellRow, short srcCellCol, HSSFWorkbook workbook, HSSFSheet sheet) { + + if(args.length < 3 || args.length > 5) { + return ErrorEval.VALUE_INVALID; + } + + + try { + BaseRef baseRef = evaluateBaseRef(args[0]); + int rowOffset = evaluateIntArg(args[1], srcCellRow, srcCellCol); + int columnOffset = evaluateIntArg(args[2], srcCellRow, srcCellCol); + int height = baseRef.getHeight(); + int width = baseRef.getWidth(); + switch(args.length) { + case 5: + width = evaluateIntArg(args[4], srcCellRow, srcCellCol); + case 4: + height = evaluateIntArg(args[3], srcCellRow, srcCellCol); + } + // Zero height or width raises #REF! error + if(height == 0 || width == 0) { + return ErrorEval.REF_INVALID; + } + LinearOffsetRange rowOffsetRange = new LinearOffsetRange(rowOffset, height); + LinearOffsetRange colOffsetRange = new LinearOffsetRange(columnOffset, width); + return createOffset(baseRef, rowOffsetRange, colOffsetRange, workbook, sheet); + } catch (EvalEx e) { + return e.getError(); + } + } + + + private static AreaEval createOffset(BaseRef baseRef, + LinearOffsetRange rowOffsetRange, LinearOffsetRange colOffsetRange, + HSSFWorkbook workbook, HSSFSheet sheet) throws EvalEx { + + LinearOffsetRange rows = rowOffsetRange.normaliseAndTranslate(baseRef.getFirstRowIndex()); + LinearOffsetRange cols = colOffsetRange.normaliseAndTranslate(baseRef.getFirstColumnIndex()); + + if(rows.isOutOfBounds(0, LAST_VALID_ROW_INDEX)) { + throw new EvalEx(ErrorEval.REF_INVALID); + } + if(cols.isOutOfBounds(0, LAST_VALID_COLUMN_INDEX)) { + throw new EvalEx(ErrorEval.REF_INVALID); + } + if(baseRef.isIs3d()) { + Area3DPtg a3dp = new Area3DPtg(rows.getFirstIndex(), rows.getLastIndex(), + cols.getFirstIndex(), cols.getLastIndex(), + false, false, false, false, + baseRef.getExternalSheetIndex()); + return HSSFFormulaEvaluator.evaluateArea3dPtg(workbook, a3dp); + } + + AreaPtg ap = new AreaPtg(rows.getFirstIndex(), rows.getLastIndex(), + cols.getFirstIndex(), cols.getLastIndex(), + false, false, false, false); + return HSSFFormulaEvaluator.evaluateAreaPtg(sheet, workbook, ap); + } + + + private static BaseRef evaluateBaseRef(Eval eval) throws EvalEx { + + if(eval instanceof RefEval) { + return new BaseRef((RefEval)eval); + } + if(eval instanceof AreaEval) { + return new BaseRef((AreaEval)eval); + } + if (eval instanceof ErrorEval) { + throw new EvalEx((ErrorEval) eval); + } + throw new EvalEx(ErrorEval.VALUE_INVALID); + } + + + /** + * OFFSET's numeric arguments (2..5) have similar processing rules + */ + private static int evaluateIntArg(Eval eval, int srcCellRow, short srcCellCol) throws EvalEx { + + double d = evaluateDoubleArg(eval, srcCellRow, srcCellCol); + return convertDoubleToInt(d); + } + + /** + * Fractional values are silently truncated by Excel. + * Truncation is toward negative infinity. + */ + /* package */ static int convertDoubleToInt(double d) { + // Note - the standard java type conversion from double to int truncates toward zero. + // but Math.floor() truncates toward negative infinity + return (int)Math.floor(d); + } + + + private static double evaluateDoubleArg(Eval eval, int srcCellRow, short srcCellCol) throws EvalEx { + ValueEval ve = evaluateSingleValue(eval, srcCellRow, srcCellCol); + + if (ve instanceof NumericValueEval) { + return ((NumericValueEval) ve).getNumberValue(); + } + if (ve instanceof StringEval) { + StringEval se = (StringEval) ve; + Double d = parseDouble(se.getStringValue()); + if(d == null) { + throw new EvalEx(ErrorEval.VALUE_INVALID); + } + return d.doubleValue(); + } + if (ve instanceof BoolEval) { + // in the context of OFFSET, booleans resolve to 0 and 1. + if(((BoolEval) ve).getBooleanValue()) { + return 1; + } + return 0; + } + throw new RuntimeException("Unexpected eval type (" + ve.getClass().getName() + ")"); + } + + private static Double parseDouble(String s) { + // TODO - find a home for this method + // TODO - support various number formats: sign char, dollars, commas + // OFFSET and COUNTIF seem to handle these + return Countif.parseDouble(s); + } + + private static ValueEval evaluateSingleValue(Eval eval, int srcCellRow, short srcCellCol) throws EvalEx { + if(eval instanceof RefEval) { + return ((RefEval)eval).getInnerValueEval(); + } + if(eval instanceof AreaEval) { + return chooseSingleElementFromArea((AreaEval)eval, srcCellRow, srcCellCol); + } + if (eval instanceof ValueEval) { + return (ValueEval) eval; + } + throw new RuntimeException("Unexpected eval type (" + eval.getClass().getName() + ")"); + } + // TODO - this code seems to get repeated a bit + private static ValueEval chooseSingleElementFromArea(AreaEval ae, int srcCellRow, short srcCellCol) throws EvalEx { + if (ae.isColumn()) { + if (ae.isRow()) { + return ae.getValues()[0]; + } + if (!ae.containsRow(srcCellRow)) { + throw new EvalEx(ErrorEval.VALUE_INVALID); + } + return ae.getValueAt(srcCellRow, ae.getFirstColumn()); + } + if (!ae.isRow()) { + throw new EvalEx(ErrorEval.VALUE_INVALID); + } + if (!ae.containsColumn(srcCellCol)) { + throw new EvalEx(ErrorEval.VALUE_INVALID); + } + return ae.getValueAt(ae.getFirstRow(), srcCellCol); + } } diff --git a/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Rounddown.java b/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Rounddown.java index ab446c9f2d..13522294fd 100644 --- a/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Rounddown.java +++ b/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Rounddown.java @@ -40,6 +40,9 @@ public class Rounddown extends NumericFunction { break; case 2: ValueEval ve = singleOperandEvaluate(operands[0], srcRow, srcCol); + if(ve instanceof ErrorEval) { + return ve; + } if (ve instanceof NumericValueEval) { NumericValueEval ne = (NumericValueEval) ve; d0 = ne.getNumberValue(); diff --git a/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Roundup.java b/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Roundup.java index 3d8cc1ae34..4dae76d981 100644 --- a/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Roundup.java +++ b/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Roundup.java @@ -40,6 +40,9 @@ public class Roundup extends NumericFunction { break; case 2: ValueEval ve = singleOperandEvaluate(operands[0], srcRow, srcCol); + if(ve instanceof ErrorEval) { + return ve; + } if (ve instanceof NumericValueEval) { NumericValueEval ne = (NumericValueEval) ve; d0 = ne.getNumberValue(); diff --git a/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Rows.java b/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Rows.java index 6a4eb8edb7..aabffab2f6 100644 --- a/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Rows.java +++ b/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Rows.java @@ -25,7 +25,7 @@ import org.apache.poi.hssf.record.formula.eval.NumberEval; import org.apache.poi.hssf.record.formula.eval.RefEval; /** - * Implementation for Excel COLUMNS function. + * Implementation for Excel ROWS function. * * @author Josh Micich */ diff --git a/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Stdev.java b/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Stdev.java index fef7e03464..7995e66c34 100644 --- a/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Stdev.java +++ b/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Stdev.java @@ -33,8 +33,8 @@ import org.apache.poi.hssf.record.formula.eval.ValueEvalToNumericXlator; public class Stdev extends MultiOperandNumericFunction { private static final ValueEvalToNumericXlator DEFAULT_NUM_XLATOR = - new ValueEvalToNumericXlator((short) (0 - // ValueEvalToNumericXlator.BOOL_IS_PARSED + new ValueEvalToNumericXlator((short) ( + ValueEvalToNumericXlator.BOOL_IS_PARSED //| ValueEvalToNumericXlator.REF_BOOL_IS_PARSED //| ValueEvalToNumericXlator.EVALUATED_REF_BOOL_IS_PARSED | ValueEvalToNumericXlator.STRING_IS_PARSED @@ -44,7 +44,6 @@ public class Stdev extends MultiOperandNumericFunction { //| ValueEvalToNumericXlator.REF_STRING_TO_BOOL_IS_PARSED //| ValueEvalToNumericXlator.STRING_IS_INVALID_VALUE //| ValueEvalToNumericXlator.REF_STRING_IS_INVALID_VALUE - | ValueEvalToNumericXlator.EVALUATED_REF_BLANK_IS_PARSED )); /** diff --git a/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Sumproduct.java b/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Sumproduct.java index 12fa5d7bd5..9f6eafa4dc 100644 --- a/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Sumproduct.java +++ b/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Sumproduct.java @@ -14,16 +14,228 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -/* - * Created on May 15, 2005 - * - */ + + package org.apache.poi.hssf.record.formula.functions; +import org.apache.poi.hssf.record.formula.eval.AreaEval; +import org.apache.poi.hssf.record.formula.eval.BlankEval; +import org.apache.poi.hssf.record.formula.eval.ErrorEval; +import org.apache.poi.hssf.record.formula.eval.Eval; +import org.apache.poi.hssf.record.formula.eval.NumberEval; +import org.apache.poi.hssf.record.formula.eval.NumericValueEval; +import org.apache.poi.hssf.record.formula.eval.RefEval; +import org.apache.poi.hssf.record.formula.eval.StringEval; +import org.apache.poi.hssf.record.formula.eval.ValueEval; + /** - * @author Amol S. Deshmukh < amolweb at ya hoo dot com > - * + * Implementation for the Excel function SUMPRODUCT

    + * + * Syntax :
    + * SUMPRODUCT ( array1[, array2[, array3[, ...]]]) + * + * + *
    array1, ... arrayN  typically area references, + * possibly cell references or scalar values

    + * + * Let An(i,j) represent the element in the ith row jth column + * of the nth array
    + * Assuming each array has the same dimensions (W, H), the result is defined as:
    + * SUMPRODUCT = Σi: 1..H   + * (  Σj: 1..W   + * (  Πn: 1..N + * An(i,j)  + * )  + * ) + * + * @author Josh Micich */ -public class Sumproduct extends NotImplementedFunction { +public final class Sumproduct implements Function { + + private static final class EvalEx extends Exception { + private final ErrorEval _error; + + public EvalEx(ErrorEval error) { + _error = error; + } + public ErrorEval getError() { + return _error; + } + } + + public Eval evaluate(Eval[] args, int srcCellRow, short srcCellCol) { + + int maxN = args.length; + + if(maxN < 1) { + return ErrorEval.VALUE_INVALID; + } + Eval firstArg = args[0]; + try { + if(firstArg instanceof NumericValueEval) { + return evaluateSingleProduct(args); + } + if(firstArg instanceof RefEval) { + return evaluateSingleProduct(args); + } + if(firstArg instanceof AreaEval) { + AreaEval ae = (AreaEval) firstArg; + if(ae.isRow() && ae.isColumn()) { + return evaluateSingleProduct(args); + } + return evaluateAreaSumProduct(args); + } + } catch (EvalEx e) { + return e.getError(); + } + throw new RuntimeException("Invalid arg type for SUMPRODUCT: (" + + firstArg.getClass().getName() + ")"); + } + + private Eval evaluateSingleProduct(Eval[] evalArgs) throws EvalEx { + int maxN = evalArgs.length; + + double term = 1D; + for(int n=0; ndouble value for the specified ValueEval. + * @param isScalarProduct false for SUMPRODUCTs over area refs. + * @throws EvalEx if ve represents an error value. + *

    + * Note - string values and empty cells are interpreted differently depending on + * isScalarProduct. For scalar products, if any term is blank or a string, the + * error (#VALUE!) is raised. For area (sum)products, if any term is blank or a string, the + * result is zero. + */ + private static double getProductTerm(ValueEval ve, boolean isScalarProduct) throws EvalEx { + + if(ve instanceof BlankEval || ve == null) { + // TODO - shouldn't BlankEval.INSTANCE be used always instead of null? + // null seems to occur when the blank cell is part of an area ref (but not reliably) + if(isScalarProduct) { + throw new EvalEx(ErrorEval.VALUE_INVALID); + } + return 0; + } + + if(ve instanceof ErrorEval) { + throw new EvalEx((ErrorEval)ve); + } + if(ve instanceof StringEval) { + if(isScalarProduct) { + throw new EvalEx(ErrorEval.VALUE_INVALID); + } + // Note for area SUMPRODUCTs, string values are interpreted as zero + // even if they would parse as valid numeric values + return 0; + } + if(ve instanceof NumericValueEval) { + NumericValueEval nve = (NumericValueEval) ve; + return nve.getNumberValue(); + } + throw new RuntimeException("Unexpected value eval class (" + + ve.getClass().getName() + ")"); + } } diff --git a/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Sumsq.java b/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Sumsq.java index f4e1959be6..b74b4161ac 100644 --- a/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Sumsq.java +++ b/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Sumsq.java @@ -33,18 +33,18 @@ import org.apache.poi.hssf.record.formula.eval.ValueEvalToNumericXlator; public class Sumsq extends MultiOperandNumericFunction { private static final ValueEvalToNumericXlator DEFAULT_NUM_XLATOR = new ValueEvalToNumericXlator((short) ( - // ValueEvalToNumericXlator.BOOL_IS_PARSED + ValueEvalToNumericXlator.BOOL_IS_PARSED //| ValueEvalToNumericXlator.REF_BOOL_IS_PARSED //| ValueEvalToNumericXlator.EVALUATED_REF_BOOL_IS_PARSED - //| ValueEvalToNumericXlator.STRING_IS_PARSED + | ValueEvalToNumericXlator.STRING_IS_PARSED //| ValueEvalToNumericXlator.REF_STRING_IS_PARSED //| ValueEvalToNumericXlator.EVALUATED_REF_STRING_IS_PARSED //| ValueEvalToNumericXlator.STRING_TO_BOOL_IS_PARSED //| ValueEvalToNumericXlator.REF_STRING_TO_BOOL_IS_PARSED //| ValueEvalToNumericXlator.STRING_IS_INVALID_VALUE //| ValueEvalToNumericXlator.REF_STRING_IS_INVALID_VALUE - ValueEvalToNumericXlator.REF_BLANK_IS_PARSED - | ValueEvalToNumericXlator.BLANK_IS_PARSED + //| ValueEvalToNumericXlator.REF_BLANK_IS_PARSED + //| ValueEvalToNumericXlator.BLANK_IS_PARSED )); protected ValueEvalToNumericXlator getXlator() { diff --git a/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Sumx2my2.java b/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Sumx2my2.java index 8e31224078..30ad5ec230 100644 --- a/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Sumx2my2.java +++ b/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Sumx2my2.java @@ -14,50 +14,23 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -/* - * Created on May 15, 2005 - * - */ -package org.apache.poi.hssf.record.formula.functions; -import org.apache.poi.hssf.record.formula.eval.ErrorEval; -import org.apache.poi.hssf.record.formula.eval.Eval; -import org.apache.poi.hssf.record.formula.eval.NumberEval; -import org.apache.poi.hssf.record.formula.eval.ValueEval; +package org.apache.poi.hssf.record.formula.functions; /** + * Implementation of Excel function SUMX2MY2()

    + * + * Calculates the sum of differences of squares in two arrays of the same size.
    + * Syntax:
    + * SUMX2MY2(arrayX, arrayY)

    + * + * result = Σi: 0..n(xi2-yi2) + * * @author Amol S. Deshmukh < amolweb at ya hoo dot com > - * */ -public class Sumx2my2 extends XYNumericFunction { +public final class Sumx2my2 extends XYNumericFunction { - - public Eval evaluate(Eval[] operands, int srcCellRow, short srcCellCol) { - ValueEval retval = null; - double[][] values = null; - - int checkLen = 0; // check to see that all array lengths are equal - switch (operands.length) { - default: - retval = ErrorEval.VALUE_INVALID; - break; - case 2: - values = getValues(operands, srcCellRow, srcCellCol); - if (values==null - || values[X] == null || values[Y] == null - || values[X].length == 0 || values[Y].length == 0 - || values[X].length != values[Y].length) { - retval = ErrorEval.VALUE_INVALID; - } - } - - if (retval == null) { - double d = MathX.sumx2my2(values[X], values[Y]); - retval = (Double.isNaN(d) || Double.isInfinite(d)) - ? (ValueEval) ErrorEval.NUM_ERROR - : new NumberEval(d); - } - - return retval; + protected double evaluate(double[] xArray, double[] yArray) { + return MathX.sumx2my2(xArray, yArray); } } diff --git a/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Sumx2py2.java b/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Sumx2py2.java index deb7675a4e..dfd730d12c 100644 --- a/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Sumx2py2.java +++ b/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Sumx2py2.java @@ -14,50 +14,23 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -/* - * Created on May 15, 2005 - * - */ -package org.apache.poi.hssf.record.formula.functions; -import org.apache.poi.hssf.record.formula.eval.ErrorEval; -import org.apache.poi.hssf.record.formula.eval.Eval; -import org.apache.poi.hssf.record.formula.eval.NumberEval; -import org.apache.poi.hssf.record.formula.eval.ValueEval; +package org.apache.poi.hssf.record.formula.functions; /** + * Implementation of Excel function SUMX2PY2()

    + * + * Calculates the sum of squares in two arrays of the same size.
    + * Syntax:
    + * SUMX2PY2(arrayX, arrayY)

    + * + * result = Σi: 0..n(xi2+yi2) + * * @author Amol S. Deshmukh < amolweb at ya hoo dot com > - * */ -public class Sumx2py2 extends XYNumericFunction { +public final class Sumx2py2 extends XYNumericFunction { - - public Eval evaluate(Eval[] operands, int srcCellRow, short srcCellCol) { - ValueEval retval = null; - double[][] values = null; - - int checkLen = 0; // check to see that all array lengths are equal - switch (operands.length) { - default: - retval = ErrorEval.VALUE_INVALID; - break; - case 2: - values = getValues(operands, srcCellRow, srcCellCol); - if (values==null - || values[X] == null || values[Y] == null - || values[X].length == 0 || values[Y].length == 0 - || values[X].length != values[Y].length) { - retval = ErrorEval.VALUE_INVALID; - } - } - - if (retval == null) { - double d = MathX.sumx2py2(values[X], values[Y]); - retval = (Double.isNaN(d) || Double.isInfinite(d)) - ? (ValueEval) ErrorEval.NUM_ERROR - : new NumberEval(d); - } - - return retval; + protected double evaluate(double[] xArray, double[] yArray) { + return MathX.sumx2py2(xArray, yArray); } } diff --git a/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Sumxmy2.java b/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Sumxmy2.java index c62a0b7622..a1b2fec9b2 100644 --- a/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Sumxmy2.java +++ b/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Sumxmy2.java @@ -14,50 +14,23 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -/* - * Created on May 15, 2005 - * - */ -package org.apache.poi.hssf.record.formula.functions; -import org.apache.poi.hssf.record.formula.eval.ErrorEval; -import org.apache.poi.hssf.record.formula.eval.Eval; -import org.apache.poi.hssf.record.formula.eval.NumberEval; -import org.apache.poi.hssf.record.formula.eval.ValueEval; +package org.apache.poi.hssf.record.formula.functions; /** + * Implementation of Excel function SUMXMY2()

    + * + * Calculates the sum of squares of differences between two arrays of the same size.
    + * Syntax:
    + * SUMXMY2(arrayX, arrayY)

    + * + * result = Σi: 0..n(xi-yi)2 + * * @author Amol S. Deshmukh < amolweb at ya hoo dot com > - * */ -public class Sumxmy2 extends XYNumericFunction { +public final class Sumxmy2 extends XYNumericFunction { - - public Eval evaluate(Eval[] operands, int srcCellRow, short srcCellCol) { - ValueEval retval = null; - double[][] values = null; - - int checkLen = 0; // check to see that all array lengths are equal - switch (operands.length) { - default: - retval = ErrorEval.VALUE_INVALID; - break; - case 2: - values = getValues(operands, srcCellRow, srcCellCol); - if (values==null - || values[X] == null || values[Y] == null - || values[X].length == 0 || values[Y].length == 0 - || values[X].length != values[Y].length) { - retval = ErrorEval.VALUE_INVALID; - } - } - - if (retval == null) { - double d = MathX.sumxmy2(values[X], values[Y]); - retval = (Double.isNaN(d) || Double.isInfinite(d)) - ? (ValueEval) ErrorEval.NUM_ERROR - : new NumberEval(d); - } - - return retval; + protected double evaluate(double[] xArray, double[] yArray) { + return MathX.sumxmy2(xArray, yArray); } } diff --git a/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/T.java b/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/T.java index 686c40b627..b322cd985c 100644 --- a/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/T.java +++ b/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/T.java @@ -22,28 +22,34 @@ package org.apache.poi.hssf.record.formula.functions; import org.apache.poi.hssf.record.formula.eval.ErrorEval; import org.apache.poi.hssf.record.formula.eval.Eval; +import org.apache.poi.hssf.record.formula.eval.RefEval; import org.apache.poi.hssf.record.formula.eval.StringEval; -import org.apache.poi.hssf.record.formula.eval.ValueEval; -public class T implements Function { - - +public final class T implements Function { - public Eval evaluate(Eval[] operands, int srcCellRow, short srcCellCol) { - ValueEval retval = null; - switch (operands.length) { - default: - retval = ErrorEval.VALUE_INVALID; - break; - case 1: - if (operands[0] instanceof StringEval - || operands[0] instanceof ErrorEval) { - retval = (ValueEval) operands[0]; - } - else if (operands[0] instanceof ErrorEval) { - retval = StringEval.EMPTY_INSTANCE; - } + public Eval evaluate(Eval[] args, int srcCellRow, short srcCellCol) { + switch (args.length) { + default: + return ErrorEval.VALUE_INVALID; + case 1: + break; } - return retval; + Eval arg = args[0]; + if (arg instanceof RefEval) { + RefEval re = (RefEval) arg; + arg = re.getInnerValueEval(); + } + + if (arg instanceof StringEval) { + // Text values are returned unmodified + return arg; + } + + if (arg instanceof ErrorEval) { + // Error values also returned unmodified + return arg; + } + // for all other argument types the result is empty string + return StringEval.EMPTY_INSTANCE; } } diff --git a/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Trim.java b/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Trim.java index 5e9d91c7cc..87e29ee34d 100644 --- a/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Trim.java +++ b/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Trim.java @@ -16,12 +16,11 @@ */ package org.apache.poi.hssf.record.formula.functions; -import org.apache.poi.hssf.record.formula.eval.BlankEval; import org.apache.poi.hssf.record.formula.eval.ErrorEval; import org.apache.poi.hssf.record.formula.eval.Eval; -import org.apache.poi.hssf.record.formula.eval.NumberEval; +import org.apache.poi.hssf.record.formula.eval.EvaluationException; +import org.apache.poi.hssf.record.formula.eval.OperandResolver; import org.apache.poi.hssf.record.formula.eval.StringEval; -import org.apache.poi.hssf.record.formula.eval.StringValueEval; import org.apache.poi.hssf.record.formula.eval.ValueEval; /** @@ -30,46 +29,25 @@ import org.apache.poi.hssf.record.formula.eval.ValueEval; * value is string. * @author Manda Wilson < wilson at c bio dot msk cc dot org > */ -public class Trim extends TextFunction { +public final class Trim extends TextFunction { - /** - * Removes leading and trailing spaces from value if evaluated - * operand value is string. - * Returns StringEval only if evaluated operand is of type string - * (and is not blank or null) or number. If evaluated operand is - * of type string and is blank or null, or if evaluated operand is - * of type blank, returns BlankEval. Otherwise returns ErrorEval. - * - * @see org.apache.poi.hssf.record.formula.eval.Eval - */ - public Eval evaluate(Eval[] operands, int srcCellRow, short srcCellCol) { - Eval retval = ErrorEval.VALUE_INVALID; - String str = null; - - switch (operands.length) { - default: - break; - case 1: - ValueEval veval = singleOperandEvaluate(operands[0], srcCellRow, srcCellCol); - if (veval instanceof StringValueEval) { - StringValueEval sve = (StringValueEval) veval; - str = sve.getStringValue(); - if (str == null || str.trim().equals("")) { - return BlankEval.INSTANCE; - } - } - else if (veval instanceof NumberEval) { - NumberEval neval = (NumberEval) veval; - str = neval.getStringValue(); - } - else if (veval instanceof BlankEval) { - return BlankEval.INSTANCE; - } - } - - if (str != null) { - retval = new StringEval(str.trim()); - } - return retval; - } + public Eval evaluate(Eval[] args, int srcCellRow, short srcCellCol) { + + if(args.length != 1) { + return ErrorEval.VALUE_INVALID; + } + + try { + ValueEval veval = OperandResolver.getSingleValue(args[0], srcCellRow, srcCellCol); + + String str = OperandResolver.coerceValueToString(veval); + str = str.trim(); + if(str.length() < 1) { + return StringEval.EMPTY_INSTANCE; + } + return new StringEval(str); + } catch (EvaluationException e) { + return e.getErrorEval(); + } + } } diff --git a/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Vlookup.java b/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Vlookup.java index ad8b88daf9..7d27491df1 100644 --- a/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Vlookup.java +++ b/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Vlookup.java @@ -1,25 +1,123 @@ -/* -* 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. -*/ -/* - * Created on May 15, 2005 - * - */ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + package org.apache.poi.hssf.record.formula.functions; -public class Vlookup extends NotImplementedFunction { +import org.apache.poi.hssf.record.formula.eval.AreaEval; +import org.apache.poi.hssf.record.formula.eval.ErrorEval; +import org.apache.poi.hssf.record.formula.eval.Eval; +import org.apache.poi.hssf.record.formula.eval.EvaluationException; +import org.apache.poi.hssf.record.formula.eval.OperandResolver; +import org.apache.poi.hssf.record.formula.eval.ValueEval; +import org.apache.poi.hssf.record.formula.functions.LookupUtils.ValueVector; +/** + * Implementation of the VLOOKUP() function.

    + * + * VLOOKUP finds a row in a lookup table by the first column value and returns the value from another column. + * + * Syntax:
    + * VLOOKUP(lookup_value, table_array, col_index_num, range_lookup)

    + * + * lookup_value The value to be found in the first column of the table array.
    + * table_array An area reference for the lookup data.
    + * col_index_num a 1 based index specifying which column value of the lookup data will be returned.
    + * range_lookup If TRUE (default), VLOOKUP finds the largest value less than or equal to + * the lookup_value. If FALSE, only exact matches will be considered
    + * + * @author Josh Micich + */ +public final class Vlookup implements Function { + + private static final class ColumnVector implements ValueVector { + + private final AreaEval _tableArray; + private final int _size; + private final int _columnAbsoluteIndex; + private final int _firstRowAbsoluteIndex; + + public ColumnVector(AreaEval tableArray, int columnIndex) { + _columnAbsoluteIndex = tableArray.getFirstColumn() + columnIndex; + if(!tableArray.containsColumn((short)_columnAbsoluteIndex)) { + int lastColIx = tableArray.getLastColumn() - tableArray.getFirstColumn(); + throw new IllegalArgumentException("Specified column index (" + columnIndex + + ") is outside the allowed range (0.." + lastColIx + ")"); + } + _tableArray = tableArray; + _size = tableArray.getLastRow() - tableArray.getFirstRow() + 1; + if(_size < 1) { + throw new RuntimeException("bad table array size zero"); + } + _firstRowAbsoluteIndex = tableArray.getFirstRow(); + } + + public ValueEval getItem(int index) { + if(index>_size) { + throw new ArrayIndexOutOfBoundsException("Specified index (" + index + + ") is outside the allowed range (0.." + (_size-1) + ")"); + } + return _tableArray.getValueAt(_firstRowAbsoluteIndex + index, (short)_columnAbsoluteIndex); + } + public int getSize() { + return _size; + } + } + + public Eval evaluate(Eval[] args, int srcCellRow, short srcCellCol) { + Eval arg3 = null; + switch(args.length) { + case 4: + arg3 = args[3]; // important: assumed array element is never null + case 3: + break; + default: + // wrong number of arguments + return ErrorEval.VALUE_INVALID; + } + try { + // Evaluation order: + // arg0 lookup_value, arg1 table_array, arg3 range_lookup, find lookup value, arg2 col_index, fetch result + ValueEval lookupValue = OperandResolver.getSingleValue(args[0], srcCellRow, srcCellCol); + AreaEval tableArray = LookupUtils.resolveTableArrayArg(args[1]); + boolean isRangeLookup = LookupUtils.resolveRangeLookupArg(arg3, srcCellRow, srcCellCol); + int rowIndex = LookupUtils.lookupIndexOfValue(lookupValue, new ColumnVector(tableArray, 0), isRangeLookup); + ValueEval veColIndex = OperandResolver.getSingleValue(args[2], srcCellRow, srcCellCol); + int colIndex = LookupUtils.resolveRowOrColIndexArg(veColIndex); + ValueVector resultCol = createResultColumnVector(tableArray, colIndex); + return resultCol.getItem(rowIndex); + } catch (EvaluationException e) { + return e.getErrorEval(); + } + } + + /** + * Returns one column from an AreaEval + * + * @throws EvaluationException (#VALUE!) if colIndex is negative, (#REF!) if colIndex is too high + */ + private ValueVector createResultColumnVector(AreaEval tableArray, int colIndex) throws EvaluationException { + if(colIndex < 0) { + throw EvaluationException.invalidValue(); + } + int nCols = tableArray.getLastColumn() - tableArray.getFirstColumn() + 1; + + if(colIndex >= nCols) { + throw EvaluationException.invalidRef(); + } + return new ColumnVector(tableArray, colIndex); + } } diff --git a/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/XYNumericFunction.java b/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/XYNumericFunction.java index 1e6955ad94..b989c33a26 100644 --- a/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/XYNumericFunction.java +++ b/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/XYNumericFunction.java @@ -14,154 +14,151 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -/* - * Created on May 29, 2005 - * - */ + package org.apache.poi.hssf.record.formula.functions; import org.apache.poi.hssf.record.formula.eval.AreaEval; import org.apache.poi.hssf.record.formula.eval.ErrorEval; import org.apache.poi.hssf.record.formula.eval.Eval; +import org.apache.poi.hssf.record.formula.eval.EvaluationException; import org.apache.poi.hssf.record.formula.eval.NumberEval; import org.apache.poi.hssf.record.formula.eval.RefEval; import org.apache.poi.hssf.record.formula.eval.ValueEval; -import org.apache.poi.hssf.record.formula.eval.ValueEvalToNumericXlator; /** * @author Amol S. Deshmukh < amolweb at ya hoo dot com > * */ -public abstract class XYNumericFunction extends NumericFunction { +public abstract class XYNumericFunction implements Function { protected static final int X = 0; protected static final int Y = 1; + + protected static final class DoubleArrayPair { - private static final ValueEvalToNumericXlator DEFAULT_NUM_XLATOR = - new ValueEvalToNumericXlator((short) ( - ValueEvalToNumericXlator.BOOL_IS_PARSED - | ValueEvalToNumericXlator.REF_BOOL_IS_PARSED - | ValueEvalToNumericXlator.EVALUATED_REF_BOOL_IS_PARSED - //| ValueEvalToNumericXlator.STRING_IS_PARSED - | ValueEvalToNumericXlator.REF_STRING_IS_PARSED - | ValueEvalToNumericXlator.EVALUATED_REF_STRING_IS_PARSED - //| ValueEvalToNumericXlator.STRING_TO_BOOL_IS_PARSED - //| ValueEvalToNumericXlator.REF_STRING_TO_BOOL_IS_PARSED - //| ValueEvalToNumericXlator.STRING_IS_INVALID_VALUE - //| ValueEvalToNumericXlator.REF_STRING_IS_INVALID_VALUE - )); + private final double[] _xArray; + private final double[] _yArray; - /** - * this is the default impl for the factory method getXlator - * of the super class NumericFunction. Subclasses can override this method - * if they desire to return a different ValueEvalToNumericXlator instance - * than the default. - */ - protected ValueEvalToNumericXlator getXlator() { - return DEFAULT_NUM_XLATOR; - } - - protected int getMaxNumOperands() { - return 30; + public DoubleArrayPair(double[] xArray, double[] yArray) { + _xArray = xArray; + _yArray = yArray; + } + public double[] getXArray() { + return _xArray; + } + public double[] getYArray() { + return _yArray; + } } + + public final Eval evaluate(Eval[] args, int srcCellRow, short srcCellCol) { + if(args.length != 2) { + return ErrorEval.VALUE_INVALID; + } + + double[][] values; + try { + values = getValues(args[0], args[1]); + } catch (EvaluationException e) { + return e.getErrorEval(); + } + if (values==null + || values[X] == null || values[Y] == null + || values[X].length == 0 || values[Y].length == 0 + || values[X].length != values[Y].length) { + return ErrorEval.VALUE_INVALID; + } + + double d = evaluate(values[X], values[Y]); + if (Double.isNaN(d) || Double.isInfinite(d)) { + return ErrorEval.NUM_ERROR; + } + return new NumberEval(d); + } + protected abstract double evaluate(double[] xArray, double[] yArray); + /** * Returns a double array that contains values for the numeric cells * from among the list of operands. Blanks and Blank equivalent cells * are ignored. Error operands or cells containing operands of type * that are considered invalid and would result in #VALUE! error in * excel cause this function to return null. - * - * @param xops - * @param yops - * @param srcRow - * @param srcCol */ - protected double[][] getNumberArray(Eval[] xops, Eval[] yops, int srcRow, short srcCol) { - double[][] retval = new double[2][30]; + private static double[][] getNumberArray(Eval[] xops, Eval[] yops) throws EvaluationException { + + // check for errors first: size mismatch, value errors in x, value errors in y + + int nArrayItems = xops.length; + if(nArrayItems != yops.length) { + throw new EvaluationException(ErrorEval.NA); + } + for (int i = 0; i < xops.length; i++) { + Eval eval = xops[i]; + if (eval instanceof ErrorEval) { + throw new EvaluationException((ErrorEval) eval); + } + } + for (int i = 0; i < yops.length; i++) { + Eval eval = yops[i]; + if (eval instanceof ErrorEval) { + throw new EvaluationException((ErrorEval) eval); + } + } + + double[] xResult = new double[nArrayItems]; + double[] yResult = new double[nArrayItems]; + int count = 0; - if (xops.length > getMaxNumOperands() - || yops.length > getMaxNumOperands() - || xops.length != yops.length) { - retval = null; - } - else { - - for (int i=0, iSize=xops.length; i= arr.length) { - arr = new double[arr.length << 2]; - } - if (temp.length != arr.length) - System.arraycopy(temp, 0, arr, 0, temp.length); - return arr; + private static double[][] getValues(Eval argX, Eval argY) throws EvaluationException { + + if (argX instanceof ErrorEval) { + throw new EvaluationException((ErrorEval) argX); + } + if (argY instanceof ErrorEval) { + throw new EvaluationException((ErrorEval) argY); + } + + Eval[] xEvals; + Eval[] yEvals; + if (argX instanceof AreaEval) { + AreaEval ae = (AreaEval) argX; + xEvals = ae.getValues(); + } else { + xEvals = new Eval[] { argX, }; + } + + if (argY instanceof AreaEval) { + AreaEval ae = (AreaEval) argY; + yEvals = ae.getValues(); + } else { + yEvals = new Eval[] { argY, }; + } + + return getNumberArray(xEvals, yEvals); } - protected static double[] trimToSize(double[] arr, int len) { + private static double[] trimToSize(double[] arr, int len) { double[] tarr = arr; if (arr.length > len) { tarr = new double[len]; @@ -170,7 +167,7 @@ public abstract class XYNumericFunction extends NumericFunction { return tarr; } - protected static boolean isNumberEval(Eval eval) { + private static boolean isNumberEval(Eval eval) { boolean retval = false; if (eval instanceof NumberEval) { @@ -185,7 +182,7 @@ public abstract class XYNumericFunction extends NumericFunction { return retval; } - protected static double getDoubleValue(Eval eval) { + private static double getDoubleValue(Eval eval) { double retval = 0; if (eval instanceof NumberEval) { NumberEval ne = (NumberEval) eval; diff --git a/src/scratchpad/src/org/apache/poi/hssf/usermodel/EvaluationCycleDetector.java b/src/scratchpad/src/org/apache/poi/hssf/usermodel/EvaluationCycleDetector.java new file mode 100755 index 0000000000..90f5807ff5 --- /dev/null +++ b/src/scratchpad/src/org/apache/poi/hssf/usermodel/EvaluationCycleDetector.java @@ -0,0 +1,150 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +package org.apache.poi.hssf.usermodel; + +import java.util.ArrayList; +import java.util.List; + +/** + * Instances of this class keep track of multiple dependent cell evaluations due + * to recursive calls to HSSFFormulaEvaluator.internalEvaluate(). + * The main purpose of this class is to detect an attempt to evaluate a cell + * that is already being evaluated. In other words, it detects circular + * references in spreadsheet formulas. + * + * @author Josh Micich + */ +final class EvaluationCycleDetector { + + /** + * Stores the parameters that identify the evaluation of one cell.
    + */ + private static final class CellEvaluationFrame { + + private final HSSFWorkbook _workbook; + private final HSSFSheet _sheet; + private final int _srcRowNum; + private final int _srcColNum; + + public CellEvaluationFrame(HSSFWorkbook workbook, HSSFSheet sheet, int srcRowNum, int srcColNum) { + if (workbook == null) { + throw new IllegalArgumentException("workbook must not be null"); + } + if (sheet == null) { + throw new IllegalArgumentException("sheet must not be null"); + } + _workbook = workbook; + _sheet = sheet; + _srcRowNum = srcRowNum; + _srcColNum = srcColNum; + } + + public boolean equals(Object obj) { + CellEvaluationFrame other = (CellEvaluationFrame) obj; + if (_workbook != other._workbook) { + return false; + } + if (_sheet != other._sheet) { + return false; + } + if (_srcRowNum != other._srcRowNum) { + return false; + } + if (_srcColNum != other._srcColNum) { + return false; + } + return true; + } + + /** + * @return human readable string for debug purposes + */ + public String formatAsString() { + return "R=" + _srcRowNum + " C=" + _srcColNum + " ShIx=" + _workbook.getSheetIndex(_sheet); + } + + public String toString() { + StringBuffer sb = new StringBuffer(64); + sb.append(getClass().getName()).append(" ["); + sb.append(formatAsString()); + sb.append("]"); + return sb.toString(); + } + } + + private final List _evaluationFrames; + + public EvaluationCycleDetector() { + _evaluationFrames = new ArrayList(); + } + + /** + * Notifies this evaluation tracker that evaluation of the specified cell is + * about to start.
    + * + * In the case of a true return code, the caller should + * continue evaluation of the specified cell, and also be sure to call + * endEvaluate() when complete.
    + * + * In the case of a false return code, the caller should + * return an evaluation result of + * ErrorEval.CIRCULAR_REF_ERROR, and not call endEvaluate(). + *
    + * @return true if the specified cell has not been visited yet in the current + * evaluation. false if the specified cell is already being evaluated. + */ + public boolean startEvaluate(HSSFWorkbook workbook, HSSFSheet sheet, int srcRowNum, int srcColNum) { + CellEvaluationFrame cef = new CellEvaluationFrame(workbook, sheet, srcRowNum, srcColNum); + if (_evaluationFrames.contains(cef)) { + return false; + } + _evaluationFrames.add(cef); + return true; + } + + /** + * Notifies this evaluation tracker that the evaluation of the specified + * cell is complete.

    + * + * Every successful call to startEvaluate must be followed by a + * call to endEvaluate (recommended in a finally block) to enable + * proper tracking of which cells are being evaluated at any point in time.

    + * + * Assuming a well behaved client, parameters to this method would not be + * required. However, they have been included to assert correct behaviour, + * and form more meaningful error messages. + */ + public void endEvaluate(HSSFWorkbook workbook, HSSFSheet sheet, int srcRowNum, int srcColNum) { + int nFrames = _evaluationFrames.size(); + if (nFrames < 1) { + throw new IllegalStateException("Call to endEvaluate without matching call to startEvaluate"); + } + + nFrames--; + CellEvaluationFrame cefExpected = (CellEvaluationFrame) _evaluationFrames.get(nFrames); + CellEvaluationFrame cefActual = new CellEvaluationFrame(workbook, sheet, srcRowNum, srcColNum); + if (!cefActual.equals(cefExpected)) { + throw new RuntimeException("Wrong cell specified. " + + "Corresponding startEvaluate() call was for cell {" + + cefExpected.formatAsString() + "} this endEvaluate() call is for cell {" + + cefActual.formatAsString() + "}"); + } + // else - no problems so pop current frame + _evaluationFrames.remove(nFrames); + } +} diff --git a/src/scratchpad/src/org/apache/poi/hssf/usermodel/EvaluationCycleDetectorManager.java b/src/scratchpad/src/org/apache/poi/hssf/usermodel/EvaluationCycleDetectorManager.java new file mode 100755 index 0000000000..a06cd201e2 --- /dev/null +++ b/src/scratchpad/src/org/apache/poi/hssf/usermodel/EvaluationCycleDetectorManager.java @@ -0,0 +1,46 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +package org.apache.poi.hssf.usermodel; + +/** + * This class makes an EvaluationCycleDetector instance available to + * each thread via a ThreadLocal in order to avoid adding a parameter + * to a few protected methods within HSSFFormulaEvaluator. + * + * @author Josh Micich + */ +final class EvaluationCycleDetectorManager { + + ThreadLocal tl = null; + private static ThreadLocal _tlEvaluationTracker = new ThreadLocal() { + protected synchronized Object initialValue() { + return new EvaluationCycleDetector(); + } + }; + + /** + * @return + */ + public static EvaluationCycleDetector getTracker() { + return (EvaluationCycleDetector) _tlEvaluationTracker.get(); + } + + private EvaluationCycleDetectorManager() { + // no instances of this class + } +} diff --git a/src/scratchpad/src/org/apache/poi/hssf/usermodel/HSSFFormulaEvaluator.java b/src/scratchpad/src/org/apache/poi/hssf/usermodel/HSSFFormulaEvaluator.java index f60a6adaac..3fce306557 100644 --- a/src/scratchpad/src/org/apache/poi/hssf/usermodel/HSSFFormulaEvaluator.java +++ b/src/scratchpad/src/org/apache/poi/hssf/usermodel/HSSFFormulaEvaluator.java @@ -14,10 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -/* - * Created on May 5, 2005 - * - */ + package org.apache.poi.hssf.usermodel; import java.lang.reflect.Constructor; @@ -74,11 +71,13 @@ import org.apache.poi.hssf.record.formula.eval.EqualEval; import org.apache.poi.hssf.record.formula.eval.ErrorEval; import org.apache.poi.hssf.record.formula.eval.Eval; import org.apache.poi.hssf.record.formula.eval.FuncVarEval; +import org.apache.poi.hssf.record.formula.eval.FunctionEval; import org.apache.poi.hssf.record.formula.eval.GreaterEqualEval; import org.apache.poi.hssf.record.formula.eval.GreaterThanEval; import org.apache.poi.hssf.record.formula.eval.LessEqualEval; import org.apache.poi.hssf.record.formula.eval.LessThanEval; import org.apache.poi.hssf.record.formula.eval.MultiplyEval; +import org.apache.poi.hssf.record.formula.eval.NameEval; import org.apache.poi.hssf.record.formula.eval.NotEqualEval; import org.apache.poi.hssf.record.formula.eval.NumberEval; import org.apache.poi.hssf.record.formula.eval.OperationEval; @@ -91,13 +90,10 @@ import org.apache.poi.hssf.record.formula.eval.SubtractEval; import org.apache.poi.hssf.record.formula.eval.UnaryMinusEval; import org.apache.poi.hssf.record.formula.eval.UnaryPlusEval; import org.apache.poi.hssf.record.formula.eval.ValueEval; -import org.apache.poi.hssf.usermodel.HSSFSheet; /** * @author Amol S. Deshmukh < amolweb at ya hoo dot com > * - * Limitations: Unfortunately, cyclic references will cause stackoverflow - * exception */ public class HSSFFormulaEvaluator { @@ -173,7 +169,7 @@ public class HSSFFormulaEvaluator { * formula evaluated. */ public static FormulaParser getUnderlyingParser(HSSFWorkbook workbook, String formula) { - return new FormulaParser(formula, workbook.getWorkbook()); + return new FormulaParser(formula, workbook.getWorkbook()); } /** @@ -286,19 +282,19 @@ public class HSSFFormulaEvaluator { CellValue cv = getCellValueForEval(internalEvaluate(cell, row, sheet, workbook)); switch (cv.getCellType()) { case HSSFCell.CELL_TYPE_BOOLEAN: - cell.setCellType(HSSFCell.CELL_TYPE_BOOLEAN); + cell.setCellType(HSSFCell.CELL_TYPE_BOOLEAN); cell.setCellValue(cv.getBooleanValue()); break; case HSSFCell.CELL_TYPE_ERROR: - cell.setCellType(HSSFCell.CELL_TYPE_ERROR); + cell.setCellType(HSSFCell.CELL_TYPE_ERROR); cell.setCellValue(cv.getErrorValue()); break; case HSSFCell.CELL_TYPE_NUMERIC: - cell.setCellType(HSSFCell.CELL_TYPE_NUMERIC); + cell.setCellType(HSSFCell.CELL_TYPE_NUMERIC); cell.setCellValue(cv.getNumberValue()); break; case HSSFCell.CELL_TYPE_STRING: - cell.setCellType(HSSFCell.CELL_TYPE_STRING); + cell.setCellType(HSSFCell.CELL_TYPE_STRING); cell.setCellValue(cv.getRichTextStringValue()); break; case HSSFCell.CELL_TYPE_BLANK: @@ -337,6 +333,11 @@ public class HSSFFormulaEvaluator { else if (eval instanceof BlankEval) { retval = new CellValue(HSSFCell.CELL_TYPE_BLANK); } + else if (eval instanceof ErrorEval) { + retval = new CellValue(HSSFCell.CELL_TYPE_ERROR); + retval.setErrorValue((byte)((ErrorEval)eval).getErrorCode()); +// retval.setRichTextStringValue(new HSSFRichTextString("#An error occurred. check cell.getErrorCode()")); + } else { retval = new CellValue(HSSFCell.CELL_TYPE_ERROR); } @@ -348,16 +349,26 @@ public class HSSFFormulaEvaluator { * Dev. Note: Internal evaluate must be passed only a formula cell * else a runtime exception will be thrown somewhere inside the method. * (Hence this is a private method.) - * - * @param srcCell - * @param srcRow - * @param sheet - * @param workbook */ - protected static ValueEval internalEvaluate(HSSFCell srcCell, HSSFRow srcRow, HSSFSheet sheet, HSSFWorkbook workbook) { + private static ValueEval internalEvaluate(HSSFCell srcCell, HSSFRow srcRow, HSSFSheet sheet, HSSFWorkbook workbook) { int srcRowNum = srcRow.getRowNum(); short srcColNum = srcCell.getCellNum(); - FormulaParser parser = new FormulaParser(srcCell.getCellFormula(), workbook.getWorkbook()); + + + EvaluationCycleDetector tracker = EvaluationCycleDetectorManager.getTracker(); + + if(!tracker.startEvaluate(workbook, sheet, srcRowNum, srcColNum)) { + return ErrorEval.CIRCULAR_REF_ERROR; + } + try { + return evaluateCell(workbook, sheet, srcRowNum, srcColNum, srcCell.getCellFormula()); + } finally { + tracker.endEvaluate(workbook, sheet, srcRowNum, srcColNum); + } + } + private static ValueEval evaluateCell(HSSFWorkbook workbook, HSSFSheet sheet, + int srcRowNum, short srcColNum, String cellFormulaText) { + FormulaParser parser = new FormulaParser(cellFormulaText, workbook.getWorkbook()); parser.parse(); Ptg[] ptgs = parser.getRPNPtg(); // -- parsing over -- @@ -366,16 +377,25 @@ public class HSSFFormulaEvaluator { Stack stack = new Stack(); for (int i = 0, iSize = ptgs.length; i < iSize; i++) { - // since we dont know how to handle these yet :( - if (ptgs[i] instanceof ControlPtg) { continue; } - if (ptgs[i] instanceof MemErrPtg) { continue; } - if (ptgs[i] instanceof MissingArgPtg) { continue; } - if (ptgs[i] instanceof NamePtg) { continue; } - if (ptgs[i] instanceof NameXPtg) { continue; } - if (ptgs[i] instanceof UnknownPtg) { continue; } + // since we don't know how to handle these yet :( + Ptg ptg = ptgs[i]; + if (ptg instanceof ControlPtg) { continue; } + if (ptg instanceof MemErrPtg) { continue; } + if (ptg instanceof MissingArgPtg) { continue; } + if (ptg instanceof NamePtg) { + // named ranges, macro functions + NamePtg namePtg = (NamePtg) ptg; + stack.push(new NameEval(namePtg.getIndex())); + continue; + } + if (ptg instanceof NameXPtg) { + // TODO - external functions + continue; + } + if (ptg instanceof UnknownPtg) { continue; } - if (ptgs[i] instanceof OperationPtg) { - OperationPtg optg = (OperationPtg) ptgs[i]; + if (ptg instanceof OperationPtg) { + OperationPtg optg = (OperationPtg) ptg; // parens can be ignored since we have RPN tokens if (optg instanceof ParenthesisPtg) { continue; } @@ -392,85 +412,151 @@ public class HSSFFormulaEvaluator { Eval p = (Eval) stack.pop(); ops[j] = p; } - Eval opresult = operation.evaluate(ops, srcRowNum, srcColNum); + Eval opresult = invokeOperation(operation, ops, srcRowNum, srcColNum, workbook, sheet); stack.push(opresult); } - else if (ptgs[i] instanceof ReferencePtg) { - ReferencePtg ptg = (ReferencePtg) ptgs[i]; - short colnum = ptg.getColumn(); - short rownum = ptg.getRow(); - HSSFRow row = sheet.getRow(rownum); - HSSFCell cell = (row != null) ? row.getCell(colnum) : null; - pushRef2DEval(ptg, stack, cell, row, sheet, workbook); + else if (ptg instanceof ReferencePtg) { + ReferencePtg refPtg = (ReferencePtg) ptg; + int colIx = refPtg.getColumn(); + int rowIx = refPtg.getRow(); + HSSFRow row = sheet.getRow(rowIx); + HSSFCell cell = (row != null) ? row.getCell(colIx) : null; + stack.push(createRef2DEval(refPtg, cell, row, sheet, workbook)); } - else if (ptgs[i] instanceof Ref3DPtg) { - Ref3DPtg ptg = (Ref3DPtg) ptgs[i]; - short colnum = ptg.getColumn(); - short rownum = ptg.getRow(); + else if (ptg instanceof Ref3DPtg) { + Ref3DPtg refPtg = (Ref3DPtg) ptg; + int colIx = refPtg.getColumn(); + int rowIx = refPtg.getRow(); Workbook wb = workbook.getWorkbook(); - HSSFSheet xsheet = workbook.getSheetAt(wb.getSheetIndexFromExternSheetIndex(ptg.getExternSheetIndex())); - HSSFRow row = xsheet.getRow(rownum); - HSSFCell cell = (row != null) ? row.getCell(colnum) : null; - pushRef3DEval(ptg, stack, cell, row, xsheet, workbook); + HSSFSheet xsheet = workbook.getSheetAt(wb.getSheetIndexFromExternSheetIndex(refPtg.getExternSheetIndex())); + HSSFRow row = xsheet.getRow(rowIx); + HSSFCell cell = (row != null) ? row.getCell(colIx) : null; + stack.push(createRef3DEval(refPtg, cell, row, xsheet, workbook)); } - else if (ptgs[i] instanceof AreaPtg) { - AreaPtg ap = (AreaPtg) ptgs[i]; - short row0 = ap.getFirstRow(); - short col0 = ap.getFirstColumn(); - short row1 = ap.getLastRow(); - short col1 = ap.getLastColumn(); - ValueEval[] values = new ValueEval[(row1 - row0 + 1) * (col1 - col0 + 1)]; - for (short x = row0; sheet != null && x < row1 + 1; x++) { - HSSFRow row = sheet.getRow(x); - for (short y = col0; row != null && y < col1 + 1; y++) { - values[(x - row0) * (col1 - col0 + 1) + (y - col0)] = - getEvalForCell(row.getCell(y), row, sheet, workbook); - } - } - AreaEval ae = new Area2DEval(ap, values); + else if (ptg instanceof AreaPtg) { + AreaPtg ap = (AreaPtg) ptg; + AreaEval ae = evaluateAreaPtg(sheet, workbook, ap); stack.push(ae); } - else if (ptgs[i] instanceof Area3DPtg) { - Area3DPtg a3dp = (Area3DPtg) ptgs[i]; - short row0 = a3dp.getFirstRow(); - short col0 = a3dp.getFirstColumn(); - short row1 = a3dp.getLastRow(); - short col1 = a3dp.getLastColumn(); - Workbook wb = workbook.getWorkbook(); - HSSFSheet xsheet = workbook.getSheetAt(wb.getSheetIndexFromExternSheetIndex(a3dp.getExternSheetIndex())); - ValueEval[] values = new ValueEval[(row1 - row0 + 1) * (col1 - col0 + 1)]; - for (short x = row0; xsheet != null && x < row1 + 1; x++) { - HSSFRow row = xsheet.getRow(x); - for (short y = col0; row != null && y < col1 + 1; y++) { - values[(x - row0) * (col1 - col0 + 1) + (y - col0)] = - getEvalForCell(row.getCell(y), row, xsheet, workbook); - } - } - AreaEval ae = new Area3DEval(a3dp, values); + else if (ptg instanceof Area3DPtg) { + Area3DPtg a3dp = (Area3DPtg) ptg; + AreaEval ae = evaluateArea3dPtg(workbook, a3dp); stack.push(ae); } else { - Eval ptgEval = getEvalForPtg(ptgs[i]); + Eval ptgEval = getEvalForPtg(ptg); stack.push(ptgEval); } } + ValueEval value = ((ValueEval) stack.pop()); - if (value instanceof RefEval) { - RefEval rv = (RefEval) value; - value = rv.getInnerValueEval(); - } - else if (value instanceof AreaEval) { - AreaEval ae = (AreaEval) value; - if (ae.isRow()) - value = ae.getValueAt(ae.getFirstRow(), srcColNum); - else if (ae.isColumn()) - value = ae.getValueAt(srcRowNum, ae.getFirstColumn()); - else - value = ErrorEval.VALUE_INVALID; + if (!stack.isEmpty()) { + throw new IllegalStateException("evaluation stack not empty"); + } + value = dereferenceValue(value, srcRowNum, srcColNum); + if (value instanceof BlankEval) { + // Note Excel behaviour here. A blank final final value is converted to zero. + return NumberEval.ZERO; + // Formulas _never_ evaluate to blank. If a formula appears to have evaluated to + // blank, the actual value is empty string. This can be verified with ISBLANK(). } return value; } + /** + * Dereferences a single value from any AreaEval or RefEval evaluation result. + * If the supplied evaluationResult is just a plain value, it is returned as-is. + * @return a NumberEval, StringEval, BoolEval, + * BlankEval or ErrorEval. Never null. + */ + private static ValueEval dereferenceValue(ValueEval evaluationResult, int srcRowNum, short srcColNum) { + if (evaluationResult instanceof RefEval) { + RefEval rv = (RefEval) evaluationResult; + return rv.getInnerValueEval(); + } + if (evaluationResult instanceof AreaEval) { + AreaEval ae = (AreaEval) evaluationResult; + if (ae.isRow()) { + if(ae.isColumn()) { + return ae.getValues()[0]; + } + return ae.getValueAt(ae.getFirstRow(), srcColNum); + } + if (ae.isColumn()) { + return ae.getValueAt(srcRowNum, ae.getFirstColumn()); + } + return ErrorEval.VALUE_INVALID; + } + return evaluationResult; + } + + private static Eval invokeOperation(OperationEval operation, Eval[] ops, int srcRowNum, short srcColNum, + HSSFWorkbook workbook, HSSFSheet sheet) { + + if(operation instanceof FunctionEval) { + FunctionEval fe = (FunctionEval) operation; + if(fe.isFreeRefFunction()) { + return fe.getFreeRefFunction().evaluate(ops, srcRowNum, srcColNum, workbook, sheet); + } + } + return operation.evaluate(ops, srcRowNum, srcColNum); + } + + public static AreaEval evaluateAreaPtg(HSSFSheet sheet, HSSFWorkbook workbook, AreaPtg ap) { + int row0 = ap.getFirstRow(); + int col0 = ap.getFirstColumn(); + int row1 = ap.getLastRow(); + int col1 = ap.getLastColumn(); + + // If the last row is -1, then the + // reference is for the rest of the column + // (eg C:C) + // TODO: Handle whole column ranges properly + if(row1 == -1 && row0 >= 0) { + row1 = (short)sheet.getLastRowNum(); + } + ValueEval[] values = evalArea(workbook, sheet, row0, col0, row1, col1); + return new Area2DEval(ap, values); + } + + public static AreaEval evaluateArea3dPtg(HSSFWorkbook workbook, Area3DPtg a3dp) { + int row0 = a3dp.getFirstRow(); + int col0 = a3dp.getFirstColumn(); + int row1 = a3dp.getLastRow(); + int col1 = a3dp.getLastColumn(); + Workbook wb = workbook.getWorkbook(); + HSSFSheet xsheet = workbook.getSheetAt(wb.getSheetIndexFromExternSheetIndex(a3dp.getExternSheetIndex())); + + // If the last row is -1, then the + // reference is for the rest of the column + // (eg C:C) + // TODO: Handle whole column ranges properly + if(row1 == -1 && row0 >= 0) { + row1 = (short)xsheet.getLastRowNum(); + } + + ValueEval[] values = evalArea(workbook, xsheet, row0, col0, row1, col1); + return new Area3DEval(a3dp, values); + } + + private static ValueEval[] evalArea(HSSFWorkbook workbook, HSSFSheet sheet, + int row0, int col0, int row1, int col1) { + ValueEval[] values = new ValueEval[(row1 - row0 + 1) * (col1 - col0 + 1)]; + for (int x = row0; sheet != null && x < row1 + 1; x++) { + HSSFRow row = sheet.getRow(x); + for (int y = col0; y < col1 + 1; y++) { + ValueEval cellEval; + if(row == null) { + cellEval = BlankEval.INSTANCE; + } else { + cellEval = getEvalForCell(row.getCell(y), row, sheet, workbook); + } + values[(x - row0) * (col1 - col0 + 1) + (y - col0)] = cellEval; + } + } + return values; + } + /** * returns the OperationEval concrete impl instance corresponding * to the suplied operationPtg @@ -544,104 +630,77 @@ public class HSSFFormulaEvaluator { * @param workbook */ protected static ValueEval getEvalForCell(HSSFCell cell, HSSFRow row, HSSFSheet sheet, HSSFWorkbook workbook) { - ValueEval retval = BlankEval.INSTANCE; - if (cell != null) { - switch (cell.getCellType()) { + + if (cell == null) { + return BlankEval.INSTANCE; + } + switch (cell.getCellType()) { case HSSFCell.CELL_TYPE_NUMERIC: - retval = new NumberEval(cell.getNumericCellValue()); - break; + return new NumberEval(cell.getNumericCellValue()); case HSSFCell.CELL_TYPE_STRING: - retval = new StringEval(cell.getRichStringCellValue().getString()); - break; + return new StringEval(cell.getRichStringCellValue().getString()); case HSSFCell.CELL_TYPE_FORMULA: - retval = internalEvaluate(cell, row, sheet, workbook); - break; + return internalEvaluate(cell, row, sheet, workbook); case HSSFCell.CELL_TYPE_BOOLEAN: - retval = cell.getBooleanCellValue() ? BoolEval.TRUE : BoolEval.FALSE; - break; + return BoolEval.valueOf(cell.getBooleanCellValue()); case HSSFCell.CELL_TYPE_BLANK: - retval = BlankEval.INSTANCE; - break; + return BlankEval.INSTANCE; case HSSFCell.CELL_TYPE_ERROR: - retval = ErrorEval.UNKNOWN_ERROR; // TODO: think about this... - break; - } + return ErrorEval.valueOf(cell.getErrorCellValue()); } - return retval; + throw new RuntimeException("Unexpected cell type (" + cell.getCellType() + ")"); } /** - * create a Ref2DEval for ReferencePtg and push it on the stack. + * Creates a Ref2DEval for ReferencePtg. * Non existent cells are treated as RefEvals containing BlankEval. - * @param ptg - * @param stack - * @param cell - * @param sheet - * @param workbook */ - protected static void pushRef2DEval(ReferencePtg ptg, Stack stack, - HSSFCell cell, HSSFRow row, HSSFSheet sheet, HSSFWorkbook workbook) { - if (cell != null) - switch (cell.getCellType()) { + private static Ref2DEval createRef2DEval(ReferencePtg ptg, HSSFCell cell, + HSSFRow row, HSSFSheet sheet, HSSFWorkbook workbook) { + if (cell == null) { + return new Ref2DEval(ptg, BlankEval.INSTANCE); + } + + switch (cell.getCellType()) { case HSSFCell.CELL_TYPE_NUMERIC: - stack.push(new Ref2DEval(ptg, new NumberEval(cell.getNumericCellValue()), false)); - break; + return new Ref2DEval(ptg, new NumberEval(cell.getNumericCellValue())); case HSSFCell.CELL_TYPE_STRING: - stack.push(new Ref2DEval(ptg, new StringEval(cell.getRichStringCellValue().getString()), false)); - break; + return new Ref2DEval(ptg, new StringEval(cell.getRichStringCellValue().getString())); case HSSFCell.CELL_TYPE_FORMULA: - stack.push(new Ref2DEval(ptg, internalEvaluate(cell, row, sheet, workbook), true)); - break; + return new Ref2DEval(ptg, internalEvaluate(cell, row, sheet, workbook)); case HSSFCell.CELL_TYPE_BOOLEAN: - stack.push(new Ref2DEval(ptg, cell.getBooleanCellValue() ? BoolEval.TRUE : BoolEval.FALSE, false)); - break; + return new Ref2DEval(ptg, BoolEval.valueOf(cell.getBooleanCellValue())); case HSSFCell.CELL_TYPE_BLANK: - stack.push(new Ref2DEval(ptg, BlankEval.INSTANCE, false)); - break; + return new Ref2DEval(ptg, BlankEval.INSTANCE); case HSSFCell.CELL_TYPE_ERROR: - stack.push(new Ref2DEval(ptg, ErrorEval.UNKNOWN_ERROR, false)); // TODO: think abt this - break; - } - else { - stack.push(new Ref2DEval(ptg, BlankEval.INSTANCE, false)); + return new Ref2DEval(ptg, ErrorEval.valueOf(cell.getErrorCellValue())); } + throw new RuntimeException("Unexpected cell type (" + cell.getCellType() + ")"); } /** - * create a Ref3DEval for Ref3DPtg and push it on the stack. - * - * @param ptg - * @param stack - * @param cell - * @param sheet - * @param workbook + * create a Ref3DEval for Ref3DPtg. */ - protected static void pushRef3DEval(Ref3DPtg ptg, Stack stack, HSSFCell cell, + private static Ref3DEval createRef3DEval(Ref3DPtg ptg, HSSFCell cell, HSSFRow row, HSSFSheet sheet, HSSFWorkbook workbook) { - if (cell != null) - switch (cell.getCellType()) { + if (cell == null) { + return new Ref3DEval(ptg, BlankEval.INSTANCE); + } + switch (cell.getCellType()) { case HSSFCell.CELL_TYPE_NUMERIC: - stack.push(new Ref3DEval(ptg, new NumberEval(cell.getNumericCellValue()), false)); - break; + return new Ref3DEval(ptg, new NumberEval(cell.getNumericCellValue())); case HSSFCell.CELL_TYPE_STRING: - stack.push(new Ref3DEval(ptg, new StringEval(cell.getRichStringCellValue().getString()), false)); - break; + return new Ref3DEval(ptg, new StringEval(cell.getRichStringCellValue().getString())); case HSSFCell.CELL_TYPE_FORMULA: - stack.push(new Ref3DEval(ptg, internalEvaluate(cell, row, sheet, workbook), true)); - break; + return new Ref3DEval(ptg, internalEvaluate(cell, row, sheet, workbook)); case HSSFCell.CELL_TYPE_BOOLEAN: - stack.push(new Ref3DEval(ptg, cell.getBooleanCellValue() ? BoolEval.TRUE : BoolEval.FALSE, false)); - break; + return new Ref3DEval(ptg, BoolEval.valueOf(cell.getBooleanCellValue())); case HSSFCell.CELL_TYPE_BLANK: - stack.push(new Ref3DEval(ptg, BlankEval.INSTANCE, false)); - break; + return new Ref3DEval(ptg, BlankEval.INSTANCE); case HSSFCell.CELL_TYPE_ERROR: - stack.push(new Ref3DEval(ptg, ErrorEval.UNKNOWN_ERROR, false)); // TODO: think abt this - break; - } - else { - stack.push(new Ref3DEval(ptg, BlankEval.INSTANCE, false)); + return new Ref3DEval(ptg, ErrorEval.valueOf(cell.getErrorCellValue())); } + throw new RuntimeException("Unexpected cell type (" + cell.getCellType() + ")"); } /** @@ -726,15 +785,15 @@ public class HSSFFormulaEvaluator { /** * @return Returns the richTextStringValue. */ - public HSSFRichTextString getRichTextStringValue() { - return richTextStringValue; - } + public HSSFRichTextString getRichTextStringValue() { + return richTextStringValue; + } /** * @param richTextStringValue The richTextStringValue to set. */ - public void setRichTextStringValue(HSSFRichTextString richTextStringValue) { - this.richTextStringValue = richTextStringValue; - } + public void setRichTextStringValue(HSSFRichTextString richTextStringValue) { + this.richTextStringValue = richTextStringValue; + } } /** diff --git a/src/scratchpad/testcases/org/apache/poi/hdgf/data/ShortChunk1.vsd b/src/scratchpad/testcases/org/apache/poi/hdgf/data/ShortChunk1.vsd new file mode 100755 index 0000000000..2c1632ebae Binary files /dev/null and b/src/scratchpad/testcases/org/apache/poi/hdgf/data/ShortChunk1.vsd differ diff --git a/src/scratchpad/testcases/org/apache/poi/hdgf/data/ShortChunk2.vsd b/src/scratchpad/testcases/org/apache/poi/hdgf/data/ShortChunk2.vsd new file mode 100755 index 0000000000..7d9a3cefc6 Binary files /dev/null and b/src/scratchpad/testcases/org/apache/poi/hdgf/data/ShortChunk2.vsd differ diff --git a/src/scratchpad/testcases/org/apache/poi/hdgf/data/ShortChunk3.vsd b/src/scratchpad/testcases/org/apache/poi/hdgf/data/ShortChunk3.vsd new file mode 100755 index 0000000000..c8bd7a190d Binary files /dev/null and b/src/scratchpad/testcases/org/apache/poi/hdgf/data/ShortChunk3.vsd differ diff --git a/src/scratchpad/testcases/org/apache/poi/hssf/data/42464-ExpPtg-bad.xls b/src/scratchpad/testcases/org/apache/poi/hssf/data/42464-ExpPtg-bad.xls deleted file mode 100644 index 54a7edb404..0000000000 Binary files a/src/scratchpad/testcases/org/apache/poi/hssf/data/42464-ExpPtg-bad.xls and /dev/null differ diff --git a/src/scratchpad/testcases/org/apache/poi/hssf/data/42464-ExpPtg-ok.xls b/src/scratchpad/testcases/org/apache/poi/hssf/data/42464-ExpPtg-ok.xls deleted file mode 100644 index 5ae84bc90e..0000000000 Binary files a/src/scratchpad/testcases/org/apache/poi/hssf/data/42464-ExpPtg-ok.xls and /dev/null differ diff --git a/src/scratchpad/testcases/org/apache/poi/hssf/data/44297.xls b/src/scratchpad/testcases/org/apache/poi/hssf/data/44297.xls deleted file mode 100755 index bc65efd4e0..0000000000 Binary files a/src/scratchpad/testcases/org/apache/poi/hssf/data/44297.xls and /dev/null differ diff --git a/src/scratchpad/testcases/org/apache/poi/hssf/data/FormulaEvalTestData.xls b/src/scratchpad/testcases/org/apache/poi/hssf/data/FormulaEvalTestData.xls deleted file mode 100644 index cf4b6fa501..0000000000 Binary files a/src/scratchpad/testcases/org/apache/poi/hssf/data/FormulaEvalTestData.xls and /dev/null differ diff --git a/src/scratchpad/testcases/org/apache/poi/hssf/data/MissingBits.xls b/src/scratchpad/testcases/org/apache/poi/hssf/data/MissingBits.xls deleted file mode 100644 index 4dba467a4f..0000000000 Binary files a/src/scratchpad/testcases/org/apache/poi/hssf/data/MissingBits.xls and /dev/null differ diff --git a/src/scratchpad/testcases/org/apache/poi/hssf/eventusermodel/TestMissingRecordAwareHSSFListener.java b/src/scratchpad/testcases/org/apache/poi/hssf/eventusermodel/TestMissingRecordAwareHSSFListener.java index 9c2cb2b87f..a038a964ee 100644 --- a/src/scratchpad/testcases/org/apache/poi/hssf/eventusermodel/TestMissingRecordAwareHSSFListener.java +++ b/src/scratchpad/testcases/org/apache/poi/hssf/eventusermodel/TestMissingRecordAwareHSSFListener.java @@ -16,14 +16,14 @@ ==================================================================== */ package org.apache.poi.hssf.eventusermodel; -import org.apache.poi.hssf.eventusermodel.HSSFEventFactory; -import org.apache.poi.hssf.eventusermodel.HSSFListener; - import java.io.File; import java.io.FileInputStream; +import java.io.IOException; import java.util.ArrayList; +import java.util.List; + +import junit.framework.TestCase; -import org.apache.poi.hssf.eventusermodel.HSSFRequest; import org.apache.poi.hssf.eventusermodel.dummyrecord.LastCellOfRowDummyRecord; import org.apache.poi.hssf.eventusermodel.dummyrecord.MissingCellDummyRecord; import org.apache.poi.hssf.eventusermodel.dummyrecord.MissingRowDummyRecord; @@ -31,31 +31,33 @@ import org.apache.poi.hssf.record.LabelSSTRecord; import org.apache.poi.hssf.record.Record; import org.apache.poi.hssf.record.RowRecord; import org.apache.poi.poifs.filesystem.POIFSFileSystem; - -import junit.framework.TestCase; - -public class TestMissingRecordAwareHSSFListener extends TestCase { - private String dirname; - - public TestMissingRecordAwareHSSFListener() { - dirname = System.getProperty("HSSF.testdata.path"); - } +/** + * Tests for MissingRecordAwareHSSFListener + */ +public final class TestMissingRecordAwareHSSFListener extends TestCase { - public void testMissingRowRecords() throws Exception { + private Record[] r; + + public void setUp() { + String dirname = System.getProperty("HSSF.testdata.path"); File f = new File(dirname + "/MissingBits.xls"); - + HSSFRequest req = new HSSFRequest(); MockHSSFListener mockListen = new MockHSSFListener(); MissingRecordAwareHSSFListener listener = new MissingRecordAwareHSSFListener(mockListen); req.addListenerForAllRecords(listener); - POIFSFileSystem fs = new POIFSFileSystem(new FileInputStream(f)); HSSFEventFactory factory = new HSSFEventFactory(); - factory.processWorkbookEvents(req, fs); - - // Check we got the dummy records - Record[] r = (Record[]) - mockListen.records.toArray(new Record[mockListen.records.size()]); + try { + POIFSFileSystem fs = new POIFSFileSystem(new FileInputStream(f)); + factory.processWorkbookEvents(req, fs); + } catch (IOException e) { + throw new RuntimeException(e); + } + r = mockListen.getRecords(); + } + + public void testMissingRowRecords() throws Exception { // We have rows 0, 1, 2, 20 and 21 int row0 = -1; @@ -105,20 +107,6 @@ public class TestMissingRecordAwareHSSFListener extends TestCase { } public void testEndOfRowRecords() throws Exception { - File f = new File(dirname + "/MissingBits.xls"); - - HSSFRequest req = new HSSFRequest(); - MockHSSFListener mockListen = new MockHSSFListener(); - MissingRecordAwareHSSFListener listener = new MissingRecordAwareHSSFListener(mockListen); - req.addListenerForAllRecords(listener); - - POIFSFileSystem fs = new POIFSFileSystem(new FileInputStream(f)); - HSSFEventFactory factory = new HSSFEventFactory(); - factory.processWorkbookEvents(req, fs); - - // Check we got the dummy records - Record[] r = (Record[]) - mockListen.records.toArray(new Record[mockListen.records.size()]); // Find the cell at 0,0 int cell00 = -1; @@ -240,20 +228,6 @@ public class TestMissingRecordAwareHSSFListener extends TestCase { public void testMissingCellRecords() throws Exception { - File f = new File(dirname + "/MissingBits.xls"); - - HSSFRequest req = new HSSFRequest(); - MockHSSFListener mockListen = new MockHSSFListener(); - MissingRecordAwareHSSFListener listener = new MissingRecordAwareHSSFListener(mockListen); - req.addListenerForAllRecords(listener); - - POIFSFileSystem fs = new POIFSFileSystem(new FileInputStream(f)); - HSSFEventFactory factory = new HSSFEventFactory(); - factory.processWorkbookEvents(req, fs); - - // Check we got the dummy records - Record[] r = (Record[]) - mockListen.records.toArray(new Record[mockListen.records.size()]); // Find the cell at 0,0 int cell00 = -1; @@ -352,25 +326,35 @@ public class TestMissingRecordAwareHSSFListener extends TestCase { assertEquals(10, mc.getColumn()); } - private static class MockHSSFListener implements HSSFListener { - private MockHSSFListener() {} - private ArrayList records = new ArrayList(); + private static final class MockHSSFListener implements HSSFListener { + public MockHSSFListener() {} + private final List _records = new ArrayList(); public void processRecord(Record record) { - records.add(record); + _records.add(record); if(record instanceof MissingRowDummyRecord) { MissingRowDummyRecord mr = (MissingRowDummyRecord)record; - System.out.println("Got dummy row " + mr.getRowNumber()); + log("Got dummy row " + mr.getRowNumber()); } if(record instanceof MissingCellDummyRecord) { MissingCellDummyRecord mc = (MissingCellDummyRecord)record; - System.out.println("Got dummy cell " + mc.getRow() + " " + mc.getColumn()); + log("Got dummy cell " + mc.getRow() + " " + mc.getColumn()); } if(record instanceof LastCellOfRowDummyRecord) { LastCellOfRowDummyRecord lc = (LastCellOfRowDummyRecord)record; - System.out.println("Got end-of row, row was " + lc.getRow() + ", last column was " + lc.getLastColumnNumber()); + log("Got end-of row, row was " + lc.getRow() + ", last column was " + lc.getLastColumnNumber()); + } + } + private static void log(String msg) { + if(false) { // successful tests should be quiet + System.out.println(msg); } } + public Record[] getRecords() { + Record[] result = new Record[_records.size()]; + _records.toArray(result); + return result; + } } } diff --git a/src/scratchpad/testcases/org/apache/poi/hssf/model/TestFormulaParserSP.java b/src/scratchpad/testcases/org/apache/poi/hssf/model/TestFormulaParserSP.java index aa73714a0f..0141e1b2a4 100644 --- a/src/scratchpad/testcases/org/apache/poi/hssf/model/TestFormulaParserSP.java +++ b/src/scratchpad/testcases/org/apache/poi/hssf/model/TestFormulaParserSP.java @@ -1,4 +1,3 @@ - /* ==================================================================== Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with @@ -15,30 +14,30 @@ See the License for the specific language governing permissions and limitations under the License. ==================================================================== */ - + package org.apache.poi.hssf.model; import junit.framework.TestCase; +import org.apache.poi.hssf.model.FormulaParser.FormulaParseException; import org.apache.poi.hssf.record.formula.FuncVarPtg; import org.apache.poi.hssf.record.formula.NamePtg; import org.apache.poi.hssf.record.formula.Ptg; +import org.apache.poi.hssf.usermodel.HSSFCell; import org.apache.poi.hssf.usermodel.HSSFFormulaEvaluator; import org.apache.poi.hssf.usermodel.HSSFName; +import org.apache.poi.hssf.usermodel.HSSFRow; import org.apache.poi.hssf.usermodel.HSSFSheet; import org.apache.poi.hssf.usermodel.HSSFWorkbook; +import org.apache.poi.hssf.usermodel.HSSFFormulaEvaluator.CellValue; /** * Test the low level formula parser functionality, * but using parts which need to use the * HSSFFormulaEvaluator, which is in scratchpad */ -public class TestFormulaParserSP extends TestCase { +public final class TestFormulaParserSP extends TestCase { - public TestFormulaParserSP(String name) { - super(name); - } - public void testWithNamedRange() throws Exception { HSSFWorkbook workbook = new HSSFWorkbook(); FormulaParser fp; @@ -80,4 +79,32 @@ public class TestFormulaParserSP extends TestCase { assertEquals(FuncVarPtg.class, ptgs[1].getClass()); } + public void testEvaluateFormulaWithRowBeyond32768_Bug44539() { + + HSSFWorkbook wb = new HSSFWorkbook(); + HSSFSheet sheet = wb.createSheet(); + wb.setSheetName(0, "Sheet1"); + + HSSFRow row = sheet.createRow(0); + HSSFCell cell = row.createCell((short)0); + cell.setCellFormula("SUM(A32769:A32770)"); + + // put some values in the cells to make the evaluation more interesting + sheet.createRow(32768).createCell((short)0).setCellValue(31); + sheet.createRow(32769).createCell((short)0).setCellValue(11); + + HSSFFormulaEvaluator fe = new HSSFFormulaEvaluator(sheet, wb); + fe.setCurrentRow(row); + CellValue result; + try { + result = fe.evaluate(cell); + } catch (FormulaParseException e) { + if(e.getMessage().equals("Found reference to named range \"A\", but that named range wasn't defined!")) { + fail("Identifed bug 44539"); + } + throw new RuntimeException(e); + } + assertEquals(HSSFCell.CELL_TYPE_NUMERIC, result.getCellType()); + assertEquals(42.0, result.getNumberValue(), 0.0); + } } diff --git a/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/eval/AllFormulaEvalTests.java b/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/eval/AllFormulaEvalTests.java new file mode 100755 index 0000000000..3260c371c6 --- /dev/null +++ b/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/eval/AllFormulaEvalTests.java @@ -0,0 +1,38 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +package org.apache.poi.hssf.record.formula.eval; + +import junit.framework.Test; +import junit.framework.TestSuite; + +/** + * Collects all tests the package org.apache.poi.hssf.record.formula.eval. + * + * @author Josh Micich + */ +public class AllFormulaEvalTests { + + public static Test suite() { + TestSuite result = new TestSuite("Tests for org.apache.poi.hssf.record.formula.eval"); + result.addTestSuite(TestCircularReferences.class); + result.addTestSuite(TestExternalFunction.class); + result.addTestSuite(TestFormulasFromSpreadsheet.class); + result.addTestSuite(TestUnaryPlusEval.class); + return result; + } +} diff --git a/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/eval/GenericFormulaTestCase.java b/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/eval/GenericFormulaTestCase.java deleted file mode 100644 index b53e8ebc3a..0000000000 --- a/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/eval/GenericFormulaTestCase.java +++ /dev/null @@ -1,147 +0,0 @@ -/* -* 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. -*/ -/* - * Created on May 11, 2005 - * - */ -package org.apache.poi.hssf.record.formula.eval; - -import java.io.FileInputStream; - -import junit.framework.AssertionFailedError; -import junit.framework.TestCase; - -import org.apache.poi.hssf.record.formula.functions.TestMathX; -import org.apache.poi.hssf.usermodel.HSSFCell; -import org.apache.poi.hssf.usermodel.HSSFFormulaEvaluator; -import org.apache.poi.hssf.usermodel.HSSFRow; -import org.apache.poi.hssf.usermodel.HSSFSheet; -import org.apache.poi.hssf.usermodel.HSSFWorkbook; -import org.apache.poi.hssf.util.CellReference; - -/** - * @author Amol S. Deshmukh < amolweb at ya hoo dot com > - * - */ -public class GenericFormulaTestCase extends TestCase { - - protected final static String FILENAME = System.getProperty("HSSF.testdata.path")+ "/FormulaEvalTestData.xls"; - - protected static HSSFWorkbook workbook = null; - - protected CellReference beginCell; - protected int getBeginRow() { - return beginCell.getRow(); - } - - protected short getBeginCol() { - return beginCell.getCol(); - } - - protected final HSSFCell getExpectedValueCell(HSSFSheet sheet, HSSFRow row, HSSFCell cell) { - HSSFCell retval = null; - if (sheet != null) { - row = sheet.getRow(row.getRowNum()+1); - if (row != null) { - retval = row.getCell(cell.getCellNum()); - } - } - - return retval; - } - - protected void assertEquals(String msg, HSSFCell expected, HSSFFormulaEvaluator.CellValue actual) { - if (expected != null && actual!=null) { - if (expected!=null && expected.getCellType() == HSSFCell.CELL_TYPE_STRING) { - String value = expected.getRichStringCellValue().getString(); - if (value.startsWith("#")) { - expected.setCellType(HSSFCell.CELL_TYPE_ERROR); - } - } - if (!(expected == null || actual == null)) { - switch (expected.getCellType()) { - case HSSFCell.CELL_TYPE_BLANK: - assertEquals(msg, HSSFCell.CELL_TYPE_BLANK, actual.getCellType()); - break; - case HSSFCell.CELL_TYPE_BOOLEAN: - assertEquals(msg, HSSFCell.CELL_TYPE_BOOLEAN, actual.getCellType()); - assertEquals(msg, expected.getBooleanCellValue(), actual.getBooleanValue()); - break; - case HSSFCell.CELL_TYPE_ERROR: - assertEquals(msg, HSSFCell.CELL_TYPE_ERROR, actual.getCellType()); // TODO: check if exact error matches - break; - case HSSFCell.CELL_TYPE_FORMULA: // will never be used, since we will call method after formula evaluation - throw new AssertionFailedError("Cannot expect formula as result of formula evaluation: " + msg); - case HSSFCell.CELL_TYPE_NUMERIC: - assertEquals(msg, HSSFCell.CELL_TYPE_NUMERIC, actual.getCellType()); - TestMathX.assertEquals(msg, expected.getNumericCellValue(), actual.getNumberValue(), TestMathX.POS_ZERO, TestMathX.DIFF_TOLERANCE_FACTOR); -// double delta = Math.abs(expected.getNumericCellValue()-actual.getNumberValue()); -// double pctExpected = Math.abs(0.00001*expected.getNumericCellValue()); -// assertTrue(msg, delta <= pctExpected); - break; - case HSSFCell.CELL_TYPE_STRING: - assertEquals(msg, HSSFCell.CELL_TYPE_STRING, actual.getCellType()); - assertEquals(msg, expected.getRichStringCellValue().getString(), actual.getRichTextStringValue().getString()); - break; - } - } - else { - throw new AssertionFailedError("expected: " + expected + " got:" + actual); - } - } - } - - public GenericFormulaTestCase(String beginCell) throws Exception { - super("genericTest"); - if (workbook == null) { - FileInputStream fin = new FileInputStream( FILENAME ); - workbook = new HSSFWorkbook( fin ); - fin.close(); - } - this.beginCell = new CellReference(beginCell); - } - - public void setUp() { - } - - public void genericTest() throws Exception { - HSSFSheet s = workbook.getSheetAt( 0 ); - HSSFRow r = s.getRow(getBeginRow()); - short endcolnum = r.getLastCellNum(); - HSSFFormulaEvaluator evaluator = new HSSFFormulaEvaluator(s, workbook); - evaluator.setCurrentRow(r); - - HSSFCell c = null; - for (short colnum=getBeginCol(); colnum < endcolnum; colnum++) { - try { - c = r.getCell(colnum); - if (c==null || c.getCellType() != HSSFCell.CELL_TYPE_FORMULA) - continue; - - HSSFFormulaEvaluator.CellValue actualValue = evaluator.evaluate(c); - - HSSFCell expectedValueCell = getExpectedValueCell(s, r, c); - assertEquals("Formula: " + c.getCellFormula() - + " @ " + getBeginRow() + ":" + colnum, - expectedValueCell, actualValue); - } catch (RuntimeException re) { - throw new RuntimeException("CELL["+getBeginRow()+","+colnum+"]: "+re.getMessage(), re); - } - } - } - -} diff --git a/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/eval/TestCircularReferences.java b/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/eval/TestCircularReferences.java new file mode 100755 index 0000000000..72db658f77 --- /dev/null +++ b/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/eval/TestCircularReferences.java @@ -0,0 +1,125 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +package org.apache.poi.hssf.record.formula.eval; + +import junit.framework.AssertionFailedError; +import junit.framework.TestCase; + +import org.apache.poi.hssf.usermodel.HSSFCell; +import org.apache.poi.hssf.usermodel.HSSFFormulaEvaluator; +import org.apache.poi.hssf.usermodel.HSSFRow; +import org.apache.poi.hssf.usermodel.HSSFSheet; +import org.apache.poi.hssf.usermodel.HSSFWorkbook; +import org.apache.poi.hssf.usermodel.HSSFFormulaEvaluator.CellValue; +/** + * Tests HSSFFormulaEvaluator for its handling of cell formula circular references. + * + * @author Josh Micich + */ +public final class TestCircularReferences extends TestCase { + /** + * Translates StackOverflowError into AssertionFailedError + */ + private static CellValue evaluateWithCycles(HSSFWorkbook wb, HSSFSheet sheet, HSSFRow row, HSSFCell testCell) + throws AssertionFailedError { + HSSFFormulaEvaluator evaluator = new HSSFFormulaEvaluator(sheet, wb); + evaluator.setCurrentRow(row); + try { + return evaluator.evaluate(testCell); + } catch (StackOverflowError e) { + throw new AssertionFailedError( "circular reference caused stack overflow error"); + } + } + /** + * Makes sure that the specified evaluated cell value represents a circular reference error. + */ + private static void confirmCycleErrorCode(CellValue cellValue) { + assertTrue(cellValue.getCellType() == HSSFCell.CELL_TYPE_ERROR); + assertEquals(ErrorEval.CIRCULAR_REF_ERROR.getErrorCode(), cellValue.getErrorValue()); + } + + + /** + * ASF Bugzilla Bug 44413 + * "INDEX() formula cannot contain its own location in the data array range" + */ + public void testIndexFormula() { + + HSSFWorkbook wb = new HSSFWorkbook(); + HSSFSheet sheet = wb.createSheet("Sheet1"); + + short colB = 1; + sheet.createRow(0).createCell(colB).setCellValue(1); + sheet.createRow(1).createCell(colB).setCellValue(2); + sheet.createRow(2).createCell(colB).setCellValue(3); + HSSFRow row4 = sheet.createRow(3); + HSSFCell testCell = row4.createCell((short)0); + // This formula should evaluate to the contents of B2, + testCell.setCellFormula("INDEX(A1:B4,2,2)"); + // However the range A1:B4 also includes the current cell A4. If the other parameters + // were 4 and 1, this would represent a circular reference. Since POI 'fully' evaluates + // arguments before invoking operators, POI must handle such potential cycles gracefully. + + + CellValue cellValue = evaluateWithCycles(wb, sheet, row4, testCell); + + assertTrue(cellValue.getCellType() == HSSFCell.CELL_TYPE_NUMERIC); + assertEquals(2, cellValue.getNumberValue(), 0); + } + + /** + * Cell A1 has formula "=A1" + */ + public void testSimpleCircularReference() { + + HSSFWorkbook wb = new HSSFWorkbook(); + HSSFSheet sheet = wb.createSheet("Sheet1"); + + HSSFRow row = sheet.createRow(0); + HSSFCell testCell = row.createCell((short)0); + testCell.setCellFormula("A1"); + + HSSFFormulaEvaluator evaluator = new HSSFFormulaEvaluator(sheet, wb); + evaluator.setCurrentRow(row); + CellValue cellValue = evaluateWithCycles(wb, sheet, row, testCell); + + confirmCycleErrorCode(cellValue); + } + + /** + * A1=B1, B1=C1, C1=D1, D1=A1 + */ + public void testMultiLevelCircularReference() { + + HSSFWorkbook wb = new HSSFWorkbook(); + HSSFSheet sheet = wb.createSheet("Sheet1"); + + HSSFRow row = sheet.createRow(0); + row.createCell((short)0).setCellFormula("B1"); + row.createCell((short)1).setCellFormula("C1"); + row.createCell((short)2).setCellFormula("D1"); + HSSFCell testCell = row.createCell((short)3); + testCell.setCellFormula("A1"); + + HSSFFormulaEvaluator evaluator = new HSSFFormulaEvaluator(sheet, wb); + evaluator.setCurrentRow(row); + CellValue cellValue = evaluateWithCycles(wb, sheet, row, testCell); + + confirmCycleErrorCode(cellValue); + } +} diff --git a/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/eval/TestEverything.java b/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/eval/TestEverything.java deleted file mode 100644 index ac3ca2eb29..0000000000 --- a/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/eval/TestEverything.java +++ /dev/null @@ -1,59 +0,0 @@ -/* -* 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. -*/ -/* - * Created on May 11, 2005 - * - */ -package org.apache.poi.hssf.record.formula.eval; - -import junit.framework.TestSuite; - -/** - * This is a test of all the Eval functions we have implemented. - * Add newly implemented Eval functions in here to have them - * tested. - * For newly implemented functions, - * @see org.apache.poi.hssf.record.formula.functions.TestEverything - * - * @author Amol S. Deshmukh < amolweb at ya hoo dot com > - */ -public class TestEverything extends TestSuite { - - public static TestSuite suite() throws Exception { - TestSuite suite = new TestSuite("Tests for OperationEval concrete implementation classes."); - suite.addTest(new GenericFormulaTestCase("D23")); // Add - suite.addTest(new GenericFormulaTestCase("D27")); // ConcatEval - suite.addTest(new GenericFormulaTestCase("D31")); // DivideEval - suite.addTest(new GenericFormulaTestCase("D35")); // EqualEval - suite.addTest(new GenericFormulaTestCase("D39")); // GreaterEqualEval - suite.addTest(new GenericFormulaTestCase("D43")); // GreaterThanEval - suite.addTest(new GenericFormulaTestCase("D47")); // LessEqualEval - suite.addTest(new GenericFormulaTestCase("D51")); // LessThanEval - suite.addTest(new GenericFormulaTestCase("D55")); // MultiplyEval - suite.addTest(new GenericFormulaTestCase("D59")); // NotEqualEval - suite.addTest(new GenericFormulaTestCase("D63")); // PowerEval - suite.addTest(new GenericFormulaTestCase("D67")); // SubtractEval - suite.addTest(new GenericFormulaTestCase("D71")); // UnaryMinusEval - suite.addTest(new GenericFormulaTestCase("D75")); // UnaryPlusEval - - // Add newly implemented Eval functions here - // (Formula functions go in - // @see org.apache.poi.hssf.record.formula.functions.TestEverything ) - - return suite; - } -} diff --git a/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/eval/TestExternalFunction.java b/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/eval/TestExternalFunction.java new file mode 100755 index 0000000000..27e3338652 --- /dev/null +++ b/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/eval/TestExternalFunction.java @@ -0,0 +1,61 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +package org.apache.poi.hssf.record.formula.eval; + +import junit.framework.TestCase; + +import org.apache.poi.hssf.usermodel.HSSFCell; +import org.apache.poi.hssf.usermodel.HSSFFormulaEvaluator; +import org.apache.poi.hssf.usermodel.HSSFName; +import org.apache.poi.hssf.usermodel.HSSFRow; +import org.apache.poi.hssf.usermodel.HSSFSheet; +import org.apache.poi.hssf.usermodel.HSSFWorkbook; +import org.apache.poi.hssf.usermodel.HSSFFormulaEvaluator.CellValue; +/** + * + * @author Josh Micich + */ +public final class TestExternalFunction extends TestCase { + + /** + * Checks that an external function can get invoked from the formula evaluator. + */ + public void testInvoke() { + HSSFWorkbook wb = new HSSFWorkbook(); + HSSFSheet sheet = wb.createSheet(); + wb.setSheetName(0, "Sheet1"); + HSSFRow row = sheet.createRow(0); + HSSFCell cell = row.createCell((short)0); + + HSSFName hssfName = wb.createName(); + hssfName.setNameName("myFunc"); + + cell.setCellFormula("myFunc()"); + String actualFormula=cell.getCellFormula(); + assertEquals("myFunc()", actualFormula); + + HSSFFormulaEvaluator fe = new HSSFFormulaEvaluator(sheet, wb); + fe.setCurrentRow(row); + CellValue evalResult = fe.evaluate(cell); + + // Check the return value from ExternalFunction.evaluate() + // TODO - make this test assert something more interesting as soon as ExternalFunction works a bit better + assertEquals(HSSFCell.CELL_TYPE_ERROR, evalResult.getCellType()); + assertEquals(ErrorEval.FUNCTION_NOT_IMPLEMENTED.getErrorCode(), evalResult.getErrorValue()); + } +} diff --git a/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/eval/TestFormulasFromSpreadsheet.java b/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/eval/TestFormulasFromSpreadsheet.java new file mode 100644 index 0000000000..f57221c9b0 --- /dev/null +++ b/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/eval/TestFormulasFromSpreadsheet.java @@ -0,0 +1,329 @@ +/* +* Licensed to the Apache Software Foundation (ASF) under one or more +* contributor license agreements. See the NOTICE file distributed with +* this work for additional information regarding copyright ownership. +* The ASF licenses this file to You under the Apache License, Version 2.0 +* (the "License"); you may not use this file except in compliance with +* the License. You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + + +package org.apache.poi.hssf.record.formula.eval; + +import java.io.FileInputStream; +import java.io.PrintStream; + +import junit.framework.Assert; +import junit.framework.AssertionFailedError; +import junit.framework.TestCase; + +import org.apache.poi.hssf.record.formula.functions.TestMathX; +import org.apache.poi.hssf.usermodel.HSSFCell; +import org.apache.poi.hssf.usermodel.HSSFFormulaEvaluator; +import org.apache.poi.hssf.usermodel.HSSFRow; +import org.apache.poi.hssf.usermodel.HSSFSheet; +import org.apache.poi.hssf.usermodel.HSSFWorkbook; + +/** + * Tests formulas and operators as loaded from a test data spreadsheet.

    + * This class does not test implementors of Function and OperationEval in + * isolation. Much of the evaluation engine (i.e. HSSFFormulaEvaluator, ...) gets + * exercised as well. Tests for bug fixes and specific/tricky behaviour can be found in the + * corresponding test class (TestXxxx) of the target (Xxxx) implementor, + * where execution can be observed more easily. + * + * @author Amol S. Deshmukh < amolweb at ya hoo dot com > + */ +public final class TestFormulasFromSpreadsheet extends TestCase { + + private static final class Result { + public static final int SOME_EVALUATIONS_FAILED = -1; + public static final int ALL_EVALUATIONS_SUCCEEDED = +1; + public static final int NO_EVALUATIONS_FOUND = 0; + } + + /** + * This class defines constants for navigating around the test data spreadsheet used for these tests. + */ + private static final class SS { + + /** + * Name of the test spreadsheet (found in the standard test data folder) + */ + public final static String FILENAME = "FormulaEvalTestData.xls"; + /** + * Row (zero-based) in the test spreadsheet where the operator examples start. + */ + public static final int START_OPERATORS_ROW_INDEX = 22; // Row '23' + /** + * Row (zero-based) in the test spreadsheet where the function examples start. + */ + public static final int START_FUNCTIONS_ROW_INDEX = 83; // Row '84' + /** + * Index of the column that contains the function names + */ + public static final short COLUMN_INDEX_FUNCTION_NAME = 1; // Column 'B' + + /** + * Used to indicate when there are no more functions left + */ + public static final String FUNCTION_NAMES_END_SENTINEL = ""; + + /** + * Index of the column where the test values start (for each function) + */ + public static final short COLUMN_INDEX_FIRST_TEST_VALUE = 3; // Column 'D' + + /** + * Each function takes 4 rows in the test spreadsheet + */ + public static final int NUMBER_OF_ROWS_PER_FUNCTION = 4; + } + + private HSSFWorkbook workbook; + private HSSFSheet sheet; + // Note - multiple failures are aggregated before ending. + // If one or more functions fail, a single AssertionFailedError is thrown at the end + private int _functionFailureCount; + private int _functionSuccessCount; + private int _evaluationFailureCount; + private int _evaluationSuccessCount; + + private static final HSSFCell getExpectedValueCell(HSSFRow row, short columnIndex) { + if (row == null) { + return null; + } + return row.getCell(columnIndex); + } + + + private static void confirmExpectedResult(String msg, HSSFCell expected, HSSFFormulaEvaluator.CellValue actual) { + if (expected == null) { + throw new AssertionFailedError(msg + " - Bad setup data expected value is null"); + } + if(actual == null) { + throw new AssertionFailedError(msg + " - actual value was null"); + } + + if (expected.getCellType() == HSSFCell.CELL_TYPE_STRING) { + String value = expected.getRichStringCellValue().getString(); + if (value.startsWith("#")) { + // TODO - this code never called + expected.setCellType(HSSFCell.CELL_TYPE_ERROR); + // expected.setCellErrorValue(...?); + } + } + + switch (expected.getCellType()) { + case HSSFCell.CELL_TYPE_BLANK: + assertEquals(msg, HSSFCell.CELL_TYPE_BLANK, actual.getCellType()); + break; + case HSSFCell.CELL_TYPE_BOOLEAN: + assertEquals(msg, HSSFCell.CELL_TYPE_BOOLEAN, actual.getCellType()); + assertEquals(msg, expected.getBooleanCellValue(), actual.getBooleanValue()); + break; + case HSSFCell.CELL_TYPE_ERROR: + assertEquals(msg, HSSFCell.CELL_TYPE_ERROR, actual.getCellType()); + if(false) { // TODO: fix ~45 functions which are currently returning incorrect error values + assertEquals(msg, expected.getErrorCellValue(), actual.getErrorValue()); + } + break; + case HSSFCell.CELL_TYPE_FORMULA: // will never be used, since we will call method after formula evaluation + throw new AssertionFailedError("Cannot expect formula as result of formula evaluation: " + msg); + case HSSFCell.CELL_TYPE_NUMERIC: + assertEquals(msg, HSSFCell.CELL_TYPE_NUMERIC, actual.getCellType()); + TestMathX.assertEquals(msg, expected.getNumericCellValue(), actual.getNumberValue(), TestMathX.POS_ZERO, TestMathX.DIFF_TOLERANCE_FACTOR); +// double delta = Math.abs(expected.getNumericCellValue()-actual.getNumberValue()); +// double pctExpected = Math.abs(0.00001*expected.getNumericCellValue()); +// assertTrue(msg, delta <= pctExpected); + break; + case HSSFCell.CELL_TYPE_STRING: + assertEquals(msg, HSSFCell.CELL_TYPE_STRING, actual.getCellType()); + assertEquals(msg, expected.getRichStringCellValue().getString(), actual.getRichTextStringValue().getString()); + break; + } + } + + + protected void setUp() throws Exception { + if (workbook == null) { + String filePath = System.getProperty("HSSF.testdata.path")+ "/" + SS.FILENAME; + FileInputStream fin = new FileInputStream( filePath ); + workbook = new HSSFWorkbook( fin ); + sheet = workbook.getSheetAt( 0 ); + } + _functionFailureCount = 0; + _functionSuccessCount = 0; + _evaluationFailureCount = 0; + _evaluationSuccessCount = 0; + } + + public void testFunctionsFromTestSpreadsheet() { + + processFunctionGroup(SS.START_OPERATORS_ROW_INDEX, null); + processFunctionGroup(SS.START_FUNCTIONS_ROW_INDEX, null); + // example for debugging individual functions/operators: +// processFunctionGroup(SS.START_OPERATORS_ROW_INDEX, "ConcatEval"); +// processFunctionGroup(SS.START_FUNCTIONS_ROW_INDEX, "AVERAGE"); + + // confirm results + String successMsg = "There were " + + _evaluationSuccessCount + " successful evaluation(s) and " + + _functionSuccessCount + " function(s) without error"; + if(_functionFailureCount > 0) { + String msg = _functionFailureCount + " function(s) failed in " + + _evaluationFailureCount + " evaluation(s). " + successMsg; + throw new AssertionFailedError(msg); + } + if(false) { // normally no output for successful tests + System.out.println(getClass().getName() + ": " + successMsg); + } + } + + /** + * @param startRowIndex row index in the spreadsheet where the first function/operator is found + * @param testFocusFunctionName name of a single function/operator to test alone. + * Typically pass null to test all functions + */ + private void processFunctionGroup(int startRowIndex, String testFocusFunctionName) { + + HSSFFormulaEvaluator evaluator = new HSSFFormulaEvaluator(sheet, workbook); + + int rowIndex = startRowIndex; + while (true) { + HSSFRow r = sheet.getRow(rowIndex); + String targetFunctionName = getTargetFunctionName(r); + if(targetFunctionName == null) { + throw new AssertionFailedError("Test spreadsheet cell empty on row (" + + (rowIndex+1) + "). Expected function name or '" + + SS.FUNCTION_NAMES_END_SENTINEL + "'"); + } + if(targetFunctionName.equals(SS.FUNCTION_NAMES_END_SENTINEL)) { + // found end of functions list + break; + } + if(testFocusFunctionName == null || targetFunctionName.equalsIgnoreCase(testFocusFunctionName)) { + + // expected results are on the row below + HSSFRow expectedValuesRow = sheet.getRow(rowIndex + 1); + if(expectedValuesRow == null) { + int missingRowNum = rowIndex + 2; //+1 for 1-based, +1 for next row + throw new AssertionFailedError("Missing expected values row for function '" + + targetFunctionName + " (row " + missingRowNum + ")"); + } + switch(processFunctionRow(evaluator, targetFunctionName, r, expectedValuesRow)) { + case Result.ALL_EVALUATIONS_SUCCEEDED: _functionSuccessCount++; break; + case Result.SOME_EVALUATIONS_FAILED: _functionFailureCount++; break; + default: + throw new RuntimeException("unexpected result"); + case Result.NO_EVALUATIONS_FOUND: // do nothing + } + } + rowIndex += SS.NUMBER_OF_ROWS_PER_FUNCTION; + } + } + + /** + * + * @return a constant from the local Result class denoting whether there were any evaluation + * cases, and whether they all succeeded. + */ + private int processFunctionRow(HSSFFormulaEvaluator evaluator, String targetFunctionName, + HSSFRow formulasRow, HSSFRow expectedValuesRow) { + + int result = Result.NO_EVALUATIONS_FOUND; // so far + short endcolnum = formulasRow.getLastCellNum(); + evaluator.setCurrentRow(formulasRow); + + // iterate across the row for all the evaluation cases + for (short colnum=SS.COLUMN_INDEX_FIRST_TEST_VALUE; colnum < endcolnum; colnum++) { + HSSFCell c = formulasRow.getCell(colnum); + if (c == null || c.getCellType() != HSSFCell.CELL_TYPE_FORMULA) { + continue; + } + + HSSFFormulaEvaluator.CellValue actualValue = evaluator.evaluate(c); + + HSSFCell expectedValueCell = getExpectedValueCell(expectedValuesRow, colnum); + try { + confirmExpectedResult("Function '" + targetFunctionName + "': Formula: " + c.getCellFormula() + " @ " + formulasRow.getRowNum() + ":" + colnum, + expectedValueCell, actualValue); + _evaluationSuccessCount ++; + if(result != Result.SOME_EVALUATIONS_FAILED) { + result = Result.ALL_EVALUATIONS_SUCCEEDED; + } + } catch (AssertionFailedError e) { + _evaluationFailureCount ++; + printShortStackTrace(System.err, e); + result = Result.SOME_EVALUATIONS_FAILED; + } + } + return result; + } + + /** + * Useful to keep output concise when expecting many failures to be reported by this test case + */ + private static void printShortStackTrace(PrintStream ps, AssertionFailedError e) { + StackTraceElement[] stes = e.getStackTrace(); + + int startIx = 0; + // skip any top frames inside junit.framework.Assert + while(startIx= endIx) { + // something went wrong. just print the whole stack trace + e.printStackTrace(ps); + } + endIx -= 4; // skip 4 frames of reflection invocation + ps.println(e.toString()); + for(int i=startIx; inull if cell is missing, empty or blank + */ + private static String getTargetFunctionName(HSSFRow r) { + if(r == null) { + System.err.println("Warning - given null row, can't figure out function name"); + return null; + } + HSSFCell cell = r.getCell(SS.COLUMN_INDEX_FUNCTION_NAME); + if(cell == null) { + System.err.println("Warning - Row " + r.getRowNum() + " has no cell " + SS.COLUMN_INDEX_FUNCTION_NAME + ", can't figure out function name"); + return null; + } + if(cell.getCellType() == HSSFCell.CELL_TYPE_BLANK) { + return null; + } + if(cell.getCellType() == HSSFCell.CELL_TYPE_STRING) { + return cell.getRichStringCellValue().getString(); + } + + throw new AssertionFailedError("Bad cell type for 'function name' column: (" + + cell.getCellType() + ") row (" + (r.getRowNum() +1) + ")"); + } +} diff --git a/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/eval/TestUnaryPlusEval.java b/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/eval/TestUnaryPlusEval.java new file mode 100755 index 0000000000..724c54cd90 --- /dev/null +++ b/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/eval/TestUnaryPlusEval.java @@ -0,0 +1,61 @@ +/* +* Licensed to the Apache Software Foundation (ASF) under one or more +* contributor license agreements. See the NOTICE file distributed with +* this work for additional information regarding copyright ownership. +* The ASF licenses this file to You under the Apache License, Version 2.0 +* (the "License"); you may not use this file except in compliance with +* the License. You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + + +package org.apache.poi.hssf.record.formula.eval; + +import org.apache.poi.hssf.record.formula.AreaPtg; +import org.apache.poi.hssf.record.formula.UnaryPlusPtg; +import org.apache.poi.hssf.record.formula.functions.NumericFunctionInvoker; + +import junit.framework.TestCase; + +/** + * Test for unary plus operator evaluator. + * + * @author Josh Micich + */ +public final class TestUnaryPlusEval extends TestCase { + + /** + * Test for bug observable at svn revision 618865 (5-Feb-2008)
    + * The code for handling column operands had been copy-pasted from the row handling code. + */ + public void testColumnOperand() { + + short firstRow = (short)8; + short lastRow = (short)12; + short colNum = (short)5; + AreaPtg areaPtg = new AreaPtg(firstRow, lastRow, colNum, colNum, false, false, false, false); + ValueEval[] values = { + new NumberEval(27), + new NumberEval(29), + new NumberEval(35), // value in row 10 + new NumberEval(37), + new NumberEval(38), + }; + Eval areaEval = new Area2DEval(areaPtg, values); + Eval[] args = { + areaEval, + }; + + double result = NumericFunctionInvoker.invoke(new UnaryPlusEval(new UnaryPlusPtg()), args, 10, (short)20); + + assertEquals(35, result, 0); + } + +} diff --git a/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/functions/AllIndividualFunctionEvaluationTests.java b/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/functions/AllIndividualFunctionEvaluationTests.java index b5e0843671..d3e9c4c412 100755 --- a/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/functions/AllIndividualFunctionEvaluationTests.java +++ b/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/functions/AllIndividualFunctionEvaluationTests.java @@ -31,13 +31,23 @@ public final class AllIndividualFunctionEvaluationTests { // TODO - have this suite incorporated into a higher level one public static Test suite() { TestSuite result = new TestSuite("Tests for org.apache.poi.hssf.record.formula.functions"); + result.addTestSuite(TestAverage.class); result.addTestSuite(TestCountFuncs.class); result.addTestSuite(TestDate.class); result.addTestSuite(TestFinanceLib.class); result.addTestSuite(TestIndex.class); + result.addTestSuite(TestIsBlank.class); + result.addTestSuite(TestLen.class); + result.addTestSuite(TestMid.class); result.addTestSuite(TestMathX.class); + result.addTestSuite(TestMatch.class); + result.addTestSuite(TestOffset.class); result.addTestSuite(TestRowCol.class); + result.addTestSuite(TestSumproduct.class); result.addTestSuite(TestStatsLib.class); + result.addTestSuite(TestTFunc.class); + result.addTestSuite(TestTrim.class); + result.addTestSuite(TestXYNumericFunction.class); return result; } diff --git a/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/functions/EvalFactory.java b/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/functions/EvalFactory.java index 958c486649..a6e262b868 100755 --- a/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/functions/EvalFactory.java +++ b/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/functions/EvalFactory.java @@ -58,6 +58,6 @@ final class EvalFactory { * Creates a single RefEval (with value zero) */ public static RefEval createRefEval(String refStr) { - return new Ref2DEval(new ReferencePtg(refStr), ZERO, true); + return new Ref2DEval(new ReferencePtg(refStr), ZERO); } } diff --git a/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/functions/NumericFunctionInvoker.java b/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/functions/NumericFunctionInvoker.java index 87405a4918..d477231349 100755 --- a/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/functions/NumericFunctionInvoker.java +++ b/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/functions/NumericFunctionInvoker.java @@ -23,13 +23,14 @@ import junit.framework.AssertionFailedError; import org.apache.poi.hssf.record.formula.eval.ErrorEval; import org.apache.poi.hssf.record.formula.eval.Eval; import org.apache.poi.hssf.record.formula.eval.NumericValueEval; +import org.apache.poi.hssf.record.formula.eval.OperationEval; /** * Test helper class for invoking functions with numeric results. * * @author Josh Micich */ -final class NumericFunctionInvoker { +public final class NumericFunctionInvoker { private NumericFunctionInvoker() { // no instances of this class @@ -59,13 +60,37 @@ final class NumericFunctionInvoker { + ") failed: " + e.getMessage()); } + } + /** + * Invokes the specified operator with the arguments. + *

    + * This method cannot be used for confirming error return codes. Any non-numeric evaluation + * result causes the current junit test to fail. + */ + public static double invoke(OperationEval f, Eval[] args, int srcCellRow, int srcCellCol) { + try { + return invokeInternal(f, args, srcCellRow, srcCellCol); + } catch (NumericEvalEx e) { + throw new AssertionFailedError("Evaluation of function (" + f.getClass().getName() + + ") failed: " + e.getMessage()); + } + } /** * Formats nicer error messages for the junit output */ - private static double invokeInternal(Function f, Eval[] args, int srcCellRow, int srcCellCol) + private static double invokeInternal(Object target, Eval[] args, int srcCellRow, int srcCellCol) throws NumericEvalEx { - Eval evalResult = f.evaluate(args, srcCellRow, (short)srcCellCol); + Eval evalResult; + // TODO - make OperationEval extend Function + if (target instanceof Function) { + Function ff = (Function) target; + evalResult = ff.evaluate(args, srcCellRow, (short)srcCellCol); + } else { + OperationEval ff = (OperationEval) target; + evalResult = ff.evaluate(args, srcCellRow, (short)srcCellCol); + } + if(evalResult == null) { throw new NumericEvalEx("Result object was null"); } @@ -86,8 +111,8 @@ final class NumericFunctionInvoker { if(errorCodesAreEqual(ee, ErrorEval.FUNCTION_NOT_IMPLEMENTED)) { return "Function not implemented"; } - if(errorCodesAreEqual(ee, ErrorEval.UNKNOWN_ERROR)) { - return "Unknown error"; + if(errorCodesAreEqual(ee, ErrorEval.VALUE_INVALID)) { + return "Error code: #VALUE! (invalid value)"; } return "Error code=" + ee.getErrorCode(); } diff --git a/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/functions/TestAverage.java b/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/functions/TestAverage.java new file mode 100755 index 0000000000..4f0e5fff2c --- /dev/null +++ b/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/functions/TestAverage.java @@ -0,0 +1,103 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +package org.apache.poi.hssf.record.formula.functions; + +import junit.framework.TestCase; + +import org.apache.poi.hssf.record.formula.eval.BlankEval; +import org.apache.poi.hssf.record.formula.eval.BoolEval; +import org.apache.poi.hssf.record.formula.eval.ErrorEval; +import org.apache.poi.hssf.record.formula.eval.Eval; +import org.apache.poi.hssf.record.formula.eval.NumberEval; +import org.apache.poi.hssf.record.formula.eval.ValueEval; +/** + * Tests for Excel function AVERAGE() + * + * @author Josh Micich + */ +public final class TestAverage extends TestCase { + + + private static Eval invokeAverage(Eval[] args) { + return new Average().evaluate(args, -1, (short)-1); + } + + private void confirmAverage(Eval[] args, double expected) { + Eval result = invokeAverage(args); + assertEquals(NumberEval.class, result.getClass()); + assertEquals(expected, ((NumberEval)result).getNumberValue(), 0); + } + + private void confirmAverage(Eval[] args, ErrorEval expectedError) { + Eval result = invokeAverage(args); + assertEquals(ErrorEval.class, result.getClass()); + assertEquals(expectedError.getErrorCode(), ((ErrorEval)result).getErrorCode()); + } + + public void testBasic() { + + ValueEval[] values = { + new NumberEval(1), + new NumberEval(2), + new NumberEval(3), + new NumberEval(4), + }; + + confirmAverage(values, 2.5); + + values = new ValueEval[] { + new NumberEval(1), + new NumberEval(2), + BlankEval.INSTANCE, + new NumberEval(3), + BlankEval.INSTANCE, + new NumberEval(4), + BlankEval.INSTANCE, + }; + + confirmAverage(values, 2.5); + } + + /** + * Valid cases where values are not pure numbers + */ + public void testUnusualArgs() { + ValueEval[] values = { + new NumberEval(1), + new NumberEval(2), + BoolEval.TRUE, + BoolEval.FALSE, + }; + + confirmAverage(values, 1.0); + + } + + // currently disabled because MultiOperandNumericFunction.getNumberArray(Eval[], int, short) + // does not handle error values properly yet + public void XtestErrors() { + ValueEval[] values = { + new NumberEval(1), + ErrorEval.NAME_INVALID, + new NumberEval(3), + ErrorEval.DIV_ZERO, + }; + confirmAverage(values, ErrorEval.NAME_INVALID); + + } +} diff --git a/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/functions/TestCountFuncs.java b/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/functions/TestCountFuncs.java index fbaace9210..ae93a2d41b 100755 --- a/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/functions/TestCountFuncs.java +++ b/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/functions/TestCountFuncs.java @@ -125,7 +125,7 @@ public final class TestCountFuncs extends TestCase { }; Area2DEval arg0 = new Area2DEval(new AreaPtg("C1:C6"), values); - Ref2DEval criteriaArg = new Ref2DEval(new ReferencePtg("A1"), new NumberEval(25), true); + Ref2DEval criteriaArg = new Ref2DEval(new ReferencePtg("A1"), new NumberEval(25)); Eval[] args= { arg0, criteriaArg, }; double actual = NumericFunctionInvoker.invoke(new Countif(), args); diff --git a/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/functions/TestEverything.java b/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/functions/TestEverything.java deleted file mode 100644 index 8337810216..0000000000 --- a/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/functions/TestEverything.java +++ /dev/null @@ -1,48 +0,0 @@ -/* -* 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. -*/ -/* - * Created on May 11, 2005 - * - */ -package org.apache.poi.hssf.record.formula.functions; - -import org.apache.poi.hssf.record.formula.eval.GenericFormulaTestCase; - -import junit.framework.TestSuite; - -/** - * This is a test of all the normal formula functions we have implemented. - * It should pick up newly implemented functions which are correctly added - * to the test formula excel file, but tweak the rows below if you - * add any past the end of what's currently checked. - * For newly implemented eval functions, - * @see org.apache.poi.hssf.record.formula.eval.TestEverything - * - * @author Amol S. Deshmukh < amolweb at ya hoo dot com > - */ -public class TestEverything extends TestSuite { - public static TestSuite suite() throws Exception { - TestSuite suite = new TestSuite("Tests for individual function classes"); - String s; - for(int i=80; i<1485;i=i+4) { - s = "D"+Integer.toString(i).trim(); - suite.addTest(new GenericFormulaTestCase(s)); - } -// suite.addTest(new GenericFormulaTestCase("D1164")); - return suite; - } -} diff --git a/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/functions/TestIsBlank.java b/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/functions/TestIsBlank.java new file mode 100755 index 0000000000..7ce2bd245b --- /dev/null +++ b/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/functions/TestIsBlank.java @@ -0,0 +1,62 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +package org.apache.poi.hssf.record.formula.functions; + +import junit.framework.TestCase; + +import org.apache.poi.hssf.usermodel.HSSFCell; +import org.apache.poi.hssf.usermodel.HSSFFormulaEvaluator; +import org.apache.poi.hssf.usermodel.HSSFRow; +import org.apache.poi.hssf.usermodel.HSSFSheet; +import org.apache.poi.hssf.usermodel.HSSFWorkbook; +import org.apache.poi.hssf.usermodel.HSSFFormulaEvaluator.CellValue; +/** + * Tests for Excel function ISBLANK() + * + * @author Josh Micich + */ +public final class TestIsBlank extends TestCase { + + + + public void test3DArea() { + HSSFWorkbook wb = new HSSFWorkbook(); + HSSFSheet sheet1 = wb.createSheet(); + wb.setSheetName(0, "Sheet1"); + wb.createSheet(); + wb.setSheetName(1, "Sheet2"); + HSSFRow row = sheet1.createRow(0); + HSSFCell cell = row.createCell((short)0); + + + cell.setCellFormula("isblank(Sheet2!A1:A1)"); + + HSSFFormulaEvaluator fe = new HSSFFormulaEvaluator(sheet1, wb); + fe.setCurrentRow(row); + CellValue result = fe.evaluate(cell); + assertEquals(HSSFCell.CELL_TYPE_BOOLEAN, result.getCellType()); + assertEquals(true, result.getBooleanValue()); + + cell.setCellFormula("isblank(D7:D7)"); + + result = fe.evaluate(cell); + assertEquals(HSSFCell.CELL_TYPE_BOOLEAN, result.getCellType()); + assertEquals(true, result.getBooleanValue()); + + } +} diff --git a/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/functions/TestLen.java b/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/functions/TestLen.java new file mode 100755 index 0000000000..a96fb4e2b0 --- /dev/null +++ b/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/functions/TestLen.java @@ -0,0 +1,73 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +package org.apache.poi.hssf.record.formula.functions; + +import junit.framework.TestCase; + +import org.apache.poi.hssf.record.formula.eval.BlankEval; +import org.apache.poi.hssf.record.formula.eval.BoolEval; +import org.apache.poi.hssf.record.formula.eval.ErrorEval; +import org.apache.poi.hssf.record.formula.eval.Eval; +import org.apache.poi.hssf.record.formula.eval.NumberEval; +import org.apache.poi.hssf.record.formula.eval.StringEval; +/** + * Tests for Excel function LEN() + * + * @author Josh Micich + */ +public final class TestLen extends TestCase { + + + private static Eval invokeLen(Eval text) { + Eval[] args = new Eval[] { text, }; + return new Len().evaluate(args, -1, (short)-1); + } + + private void confirmLen(Eval text, int expected) { + Eval result = invokeLen(text); + assertEquals(NumberEval.class, result.getClass()); + assertEquals(expected, ((NumberEval)result).getNumberValue(), 0); + } + + private void confirmLen(Eval text, ErrorEval expectedError) { + Eval result = invokeLen(text); + assertEquals(ErrorEval.class, result.getClass()); + assertEquals(expectedError.getErrorCode(), ((ErrorEval)result).getErrorCode()); + } + + public void testBasic() { + + confirmLen(new StringEval("galactic"), 8); + } + + /** + * Valid cases where text arg is not exactly a string + */ + public void testUnusualArgs() { + + // text (first) arg type is number, other args are strings with fractional digits + confirmLen(new NumberEval(123456), 6); + confirmLen(BoolEval.FALSE, 5); + confirmLen(BoolEval.TRUE, 4); + confirmLen(BlankEval.INSTANCE, 0); + } + + public void testErrors() { + confirmLen(ErrorEval.NAME_INVALID, ErrorEval.NAME_INVALID); + } +} diff --git a/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/functions/TestLookupFunctionsFromSpreadsheet.java b/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/functions/TestLookupFunctionsFromSpreadsheet.java new file mode 100644 index 0000000000..071ca0f7d8 --- /dev/null +++ b/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/functions/TestLookupFunctionsFromSpreadsheet.java @@ -0,0 +1,385 @@ +/* +* Licensed to the Apache Software Foundation (ASF) under one or more +* contributor license agreements. See the NOTICE file distributed with +* this work for additional information regarding copyright ownership. +* The ASF licenses this file to You under the Apache License, Version 2.0 +* (the "License"); you may not use this file except in compliance with +* the License. You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + + +package org.apache.poi.hssf.record.formula.functions; + +import java.io.FileInputStream; +import java.io.IOException; +import java.io.PrintStream; + +import junit.framework.Assert; +import junit.framework.AssertionFailedError; +import junit.framework.TestCase; + +import org.apache.poi.hssf.record.formula.eval.ErrorEval; +import org.apache.poi.hssf.usermodel.HSSFCell; +import org.apache.poi.hssf.usermodel.HSSFFormulaEvaluator; +import org.apache.poi.hssf.usermodel.HSSFRow; +import org.apache.poi.hssf.usermodel.HSSFSheet; +import org.apache.poi.hssf.usermodel.HSSFWorkbook; +import org.apache.poi.hssf.usermodel.HSSFFormulaEvaluator.CellValue; +import org.apache.poi.hssf.util.CellReference; + +/** + * Tests lookup functions (VLOOKUP, HLOOKUP, LOOKUP, MATCH) as loaded from a test data spreadsheet.

    + * These tests have been separated from the common function and operator tests because the lookup + * functions have more complex test cases and test data setup. + * + * Tests for bug fixes and specific/tricky behaviour can be found in the corresponding test class + * (TestXxxx) of the target (Xxxx) implementor, where execution can be observed + * more easily. + * + * @author Josh Micich + */ +public final class TestLookupFunctionsFromSpreadsheet extends TestCase { + + private static final class Result { + public static final int SOME_EVALUATIONS_FAILED = -1; + public static final int ALL_EVALUATIONS_SUCCEEDED = +1; + public static final int NO_EVALUATIONS_FOUND = 0; + } + + /** + * This class defines constants for navigating around the test data spreadsheet used for these tests. + */ + private static final class SS { + + /** Name of the test spreadsheet (found in the standard test data folder) */ + public final static String FILENAME = "LookupFunctionsTestCaseData.xls"; + + /** Name of the first sheet in the spreadsheet (contains comments) */ + public final static String README_SHEET_NAME = "Read Me"; + + + /** Row (zero-based) in each sheet where the evaluation cases start. */ + public static final int START_TEST_CASES_ROW_INDEX = 4; // Row '5' + /** Index of the column that contains the function names */ + public static final short COLUMN_INDEX_MARKER = 0; // Column 'A' + public static final short COLUMN_INDEX_EVALUATION = 1; // Column 'B' + public static final short COLUMN_INDEX_EXPECTED_RESULT = 2; // Column 'C' + public static final short COLUMN_ROW_COMMENT = 3; // Column 'D' + + /** Used to indicate when there are no more test cases on the current sheet */ + public static final String TEST_CASES_END_MARKER = ""; + /** Used to indicate that the test on the current row should be ignored */ + public static final String SKIP_CURRENT_TEST_CASE_MARKER = ""; + + } + + // Note - multiple failures are aggregated before ending. + // If one or more functions fail, a single AssertionFailedError is thrown at the end + private int _sheetFailureCount; + private int _sheetSuccessCount; + private int _evaluationFailureCount; + private int _evaluationSuccessCount; + + + + private static void confirmExpectedResult(String msg, HSSFCell expected, HSSFFormulaEvaluator.CellValue actual) { + if (expected == null) { + throw new AssertionFailedError(msg + " - Bad setup data expected value is null"); + } + if(actual == null) { + throw new AssertionFailedError(msg + " - actual value was null"); + } + if(expected.getCellType() == HSSFCell.CELL_TYPE_ERROR) { + confirmErrorResult(msg, expected.getErrorCellValue(), actual); + return; + } + if(actual.getCellType() == HSSFCell.CELL_TYPE_ERROR) { + throw unexpectedError(msg, expected, actual.getErrorValue()); + } + if(actual.getCellType() != expected.getCellType()) { + throw wrongTypeError(msg, expected, actual); + } + + + switch (expected.getCellType()) { + case HSSFCell.CELL_TYPE_BOOLEAN: + assertEquals(msg, expected.getBooleanCellValue(), actual.getBooleanValue()); + break; + case HSSFCell.CELL_TYPE_FORMULA: // will never be used, since we will call method after formula evaluation + throw new AssertionFailedError("Cannot expect formula as result of formula evaluation: " + msg); + case HSSFCell.CELL_TYPE_NUMERIC: + assertEquals(expected.getNumericCellValue(), actual.getNumberValue(), 0.0); + break; + case HSSFCell.CELL_TYPE_STRING: + assertEquals(msg, expected.getRichStringCellValue().getString(), actual.getRichTextStringValue().getString()); + break; + } + } + + + private static AssertionFailedError wrongTypeError(String msgPrefix, HSSFCell expectedCell, CellValue actualValue) { + return new AssertionFailedError(msgPrefix + " Result type mismatch. Evaluated result was " + + formatValue(actualValue) + + " but the expected result was " + + formatValue(expectedCell) + ); + } + private static AssertionFailedError unexpectedError(String msgPrefix, HSSFCell expected, int actualErrorCode) { + return new AssertionFailedError(msgPrefix + " Error code (" + + ErrorEval.getText(actualErrorCode) + + ") was evaluated, but the expected result was " + + formatValue(expected) + ); + } + + + private static void confirmErrorResult(String msgPrefix, int expectedErrorCode, CellValue actual) { + if(actual.getCellType() != HSSFCell.CELL_TYPE_ERROR) { + throw new AssertionFailedError(msgPrefix + " Expected cell error (" + + ErrorEval.getText(expectedErrorCode) + ") but actual value was " + + formatValue(actual)); + } + if(expectedErrorCode != actual.getErrorValue()) { + throw new AssertionFailedError(msgPrefix + " Expected cell error code (" + + ErrorEval.getText(expectedErrorCode) + + ") but actual error code was (" + + ErrorEval.getText(actual.getErrorValue()) + + ")"); + } + } + + + private static String formatValue(HSSFCell expecedCell) { + switch (expecedCell.getCellType()) { + case HSSFCell.CELL_TYPE_BLANK: return ""; + case HSSFCell.CELL_TYPE_BOOLEAN: return String.valueOf(expecedCell.getBooleanCellValue()); + case HSSFCell.CELL_TYPE_NUMERIC: return String.valueOf(expecedCell.getNumericCellValue()); + case HSSFCell.CELL_TYPE_STRING: return expecedCell.getRichStringCellValue().getString(); + } + throw new RuntimeException("Unexpected cell type of expected value (" + expecedCell.getCellType() + ")"); + } + private static String formatValue(CellValue actual) { + switch (actual.getCellType()) { + case HSSFCell.CELL_TYPE_BLANK: return ""; + case HSSFCell.CELL_TYPE_BOOLEAN: return String.valueOf(actual.getBooleanValue()); + case HSSFCell.CELL_TYPE_NUMERIC: return String.valueOf(actual.getNumberValue()); + case HSSFCell.CELL_TYPE_STRING: return actual.getRichTextStringValue().getString(); + } + throw new RuntimeException("Unexpected cell type of evaluated value (" + actual.getCellType() + ")"); + } + + + protected void setUp() throws Exception { + _sheetFailureCount = 0; + _sheetSuccessCount = 0; + _evaluationFailureCount = 0; + _evaluationSuccessCount = 0; + } + + public void testFunctionsFromTestSpreadsheet() { + String filePath = System.getProperty("HSSF.testdata.path")+ "/" + SS.FILENAME; + HSSFWorkbook workbook; + try { + FileInputStream fin = new FileInputStream( filePath ); + workbook = new HSSFWorkbook( fin ); + } catch (IOException e) { + throw new RuntimeException(e); + } + + confirmReadMeSheet(workbook); + int nSheets = workbook.getNumberOfSheets(); + for(int i=1; i< nSheets; i++) { + int sheetResult = processTestSheet(workbook, i, workbook.getSheetName(i)); + switch(sheetResult) { + case Result.ALL_EVALUATIONS_SUCCEEDED: _sheetSuccessCount ++; break; + case Result.SOME_EVALUATIONS_FAILED: _sheetFailureCount ++; break; + } + } + + // confirm results + String successMsg = "There were " + + _sheetSuccessCount + " successful sheets(s) and " + + _evaluationSuccessCount + " function(s) without error"; + if(_sheetFailureCount > 0) { + String msg = _sheetFailureCount + " sheets(s) failed with " + + _evaluationFailureCount + " evaluation(s). " + successMsg; + throw new AssertionFailedError(msg); + } + if(false) { // normally no output for successful tests + System.out.println(getClass().getName() + ": " + successMsg); + } + } + + private int processTestSheet(HSSFWorkbook workbook, int sheetIndex, String sheetName) { + HSSFSheet sheet = workbook.getSheetAt(sheetIndex); + HSSFFormulaEvaluator evaluator = new HSSFFormulaEvaluator(sheet, workbook); + int maxRows = sheet.getLastRowNum()+1; + int result = Result.NO_EVALUATIONS_FOUND; // so far + + String currentGroupComment = null; + for(int rowIndex=SS.START_TEST_CASES_ROW_INDEX; rowIndex= endIx) { + // something went wrong. just print the whole stack trace + e.printStackTrace(ps); + } + endIx -= 4; // skip 4 frames of reflection invocation + ps.println(e.toString()); + for(int i=startIx; inull if cell is missing, empty or blank + */ + private static String getCellTextValue(HSSFRow r, int colIndex, String columnName) { + if(r == null) { + return null; + } + HSSFCell cell = r.getCell((short) colIndex); + if(cell == null) { + return null; + } + if(cell.getCellType() == HSSFCell.CELL_TYPE_BLANK) { + return null; + } + if(cell.getCellType() == HSSFCell.CELL_TYPE_STRING) { + return cell.getRichStringCellValue().getString(); + } + + throw new RuntimeException("Bad cell type for '" + columnName + "' column: (" + + cell.getCellType() + ") row (" + (r.getRowNum() +1) + ")"); + } +} diff --git a/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/functions/TestMatch.java b/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/functions/TestMatch.java new file mode 100755 index 0000000000..d275e5f333 --- /dev/null +++ b/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/functions/TestMatch.java @@ -0,0 +1,215 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +package org.apache.poi.hssf.record.formula.functions; + +import junit.framework.TestCase; + +import org.apache.poi.hssf.record.formula.AreaPtg; +import org.apache.poi.hssf.record.formula.eval.Area2DEval; +import org.apache.poi.hssf.record.formula.eval.AreaEval; +import org.apache.poi.hssf.record.formula.eval.BoolEval; +import org.apache.poi.hssf.record.formula.eval.ErrorEval; +import org.apache.poi.hssf.record.formula.eval.Eval; +import org.apache.poi.hssf.record.formula.eval.NumberEval; +import org.apache.poi.hssf.record.formula.eval.NumericValueEval; +import org.apache.poi.hssf.record.formula.eval.StringEval; +import org.apache.poi.hssf.record.formula.eval.ValueEval; + +/** + * Test cases for MATCH() + * + * @author Josh Micich + */ +public final class TestMatch extends TestCase { + /** less than or equal to */ + private static final NumberEval MATCH_LARGEST_LTE = new NumberEval(1); + private static final NumberEval MATCH_EXACT = new NumberEval(0); + /** greater than or equal to */ + private static final NumberEval MATCH_SMALLEST_GTE = new NumberEval(-1); + + + private static Eval invokeMatch(Eval lookup_value, Eval lookup_array, Eval match_type) { + Eval[] args = { lookup_value, lookup_array, match_type, }; + return new Match().evaluate(args, -1, (short)-1); + } + private static void confirmInt(int expected, Eval actualEval) { + if(!(actualEval instanceof NumericValueEval)) { + fail("Expected numeric result"); + } + NumericValueEval nve = (NumericValueEval)actualEval; + assertEquals(expected, nve.getNumberValue(), 0); + } + /** + * Convenience method + * @return new Area2DEval(new AreaPtg(ref), values) + */ + private static AreaEval createAreaEval(String ref, ValueEval[] values) { + return new Area2DEval(new AreaPtg(ref), values); + } + + public void testSimpleNumber() { + + ValueEval[] values = { + new NumberEval(4), + new NumberEval(5), + new NumberEval(10), + new NumberEval(10), + new NumberEval(25), + }; + + AreaEval ae = createAreaEval("A1:A5", values); + + confirmInt(2, invokeMatch(new NumberEval(5), ae, MATCH_LARGEST_LTE)); + confirmInt(2, invokeMatch(new NumberEval(5), ae, MATCH_EXACT)); + confirmInt(4, invokeMatch(new NumberEval(10), ae, MATCH_LARGEST_LTE)); + confirmInt(3, invokeMatch(new NumberEval(10), ae, MATCH_EXACT)); + confirmInt(4, invokeMatch(new NumberEval(20), ae, MATCH_LARGEST_LTE)); + assertEquals(ErrorEval.NA, invokeMatch(new NumberEval(20), ae, MATCH_EXACT)); + } + + public void testReversedNumber() { + + ValueEval[] values = { + new NumberEval(25), + new NumberEval(10), + new NumberEval(10), + new NumberEval(10), + new NumberEval(4), + }; + + AreaEval ae = createAreaEval("A1:A5", values); + + confirmInt(2, invokeMatch(new NumberEval(10), ae, MATCH_SMALLEST_GTE)); + confirmInt(2, invokeMatch(new NumberEval(10), ae, MATCH_EXACT)); + confirmInt(4, invokeMatch(new NumberEval(9), ae, MATCH_SMALLEST_GTE)); + confirmInt(1, invokeMatch(new NumberEval(20), ae, MATCH_SMALLEST_GTE)); + assertEquals(ErrorEval.NA, invokeMatch(new NumberEval(20), ae, MATCH_EXACT)); + assertEquals(ErrorEval.NA, invokeMatch(new NumberEval(26), ae, MATCH_SMALLEST_GTE)); + } + + public void testSimpleString() { + + ValueEval[] values = { + new StringEval("Albert"), + new StringEval("Charles"), + new StringEval("Ed"), + new StringEval("Greg"), + new StringEval("Ian"), + }; + + AreaEval ae = createAreaEval("A1:A5", values); + + // Note String comparisons are case insensitive + confirmInt(3, invokeMatch(new StringEval("Ed"), ae, MATCH_LARGEST_LTE)); + confirmInt(3, invokeMatch(new StringEval("eD"), ae, MATCH_LARGEST_LTE)); + confirmInt(3, invokeMatch(new StringEval("Ed"), ae, MATCH_EXACT)); + confirmInt(3, invokeMatch(new StringEval("ed"), ae, MATCH_EXACT)); + confirmInt(4, invokeMatch(new StringEval("Hugh"), ae, MATCH_LARGEST_LTE)); + assertEquals(ErrorEval.NA, invokeMatch(new StringEval("Hugh"), ae, MATCH_EXACT)); + } + + public void testSimpleBoolean() { + + ValueEval[] values = { + BoolEval.FALSE, + BoolEval.FALSE, + BoolEval.TRUE, + BoolEval.TRUE, + }; + + AreaEval ae = createAreaEval("A1:A4", values); + + // Note String comparisons are case insensitive + confirmInt(2, invokeMatch(BoolEval.FALSE, ae, MATCH_LARGEST_LTE)); + confirmInt(1, invokeMatch(BoolEval.FALSE, ae, MATCH_EXACT)); + confirmInt(4, invokeMatch(BoolEval.TRUE, ae, MATCH_LARGEST_LTE)); + confirmInt(3, invokeMatch(BoolEval.TRUE, ae, MATCH_EXACT)); + } + + public void testHeterogeneous() { + + ValueEval[] values = { + new NumberEval(4), + BoolEval.FALSE, + new NumberEval(5), + new StringEval("Albert"), + BoolEval.FALSE, + BoolEval.TRUE, + new NumberEval(10), + new StringEval("Charles"), + new StringEval("Ed"), + new NumberEval(10), + new NumberEval(25), + BoolEval.TRUE, + new StringEval("Ed"), + }; + + AreaEval ae = createAreaEval("A1:A13", values); + + assertEquals(ErrorEval.NA, invokeMatch(new StringEval("Aaron"), ae, MATCH_LARGEST_LTE)); + + confirmInt(5, invokeMatch(BoolEval.FALSE, ae, MATCH_LARGEST_LTE)); + confirmInt(2, invokeMatch(BoolEval.FALSE, ae, MATCH_EXACT)); + confirmInt(3, invokeMatch(new NumberEval(5), ae, MATCH_LARGEST_LTE)); + confirmInt(3, invokeMatch(new NumberEval(5), ae, MATCH_EXACT)); + + confirmInt(8, invokeMatch(new StringEval("CHARLES"), ae, MATCH_EXACT)); + + confirmInt(4, invokeMatch(new StringEval("Ben"), ae, MATCH_LARGEST_LTE)); + + confirmInt(13, invokeMatch(new StringEval("ED"), ae, MATCH_LARGEST_LTE)); + confirmInt(9, invokeMatch(new StringEval("ED"), ae, MATCH_EXACT)); + + confirmInt(13, invokeMatch(new StringEval("Hugh"), ae, MATCH_LARGEST_LTE)); + assertEquals(ErrorEval.NA, invokeMatch(new StringEval("Hugh"), ae, MATCH_EXACT)); + + confirmInt(11, invokeMatch(new NumberEval(30), ae, MATCH_LARGEST_LTE)); + confirmInt(12, invokeMatch(BoolEval.TRUE, ae, MATCH_LARGEST_LTE)); + } + + + /** + * Ensures that the match_type argument can be an AreaEval.
    + * Bugzilla 44421 + */ + public void testMatchArgTypeArea() { + + ValueEval[] values = { + new NumberEval(4), + new NumberEval(5), + new NumberEval(10), + new NumberEval(10), + new NumberEval(25), + }; + + AreaEval ae = createAreaEval("A1:A5", values); + + AreaEval matchAE = createAreaEval("C1:C1", new ValueEval[] { MATCH_LARGEST_LTE, }); + + try { + confirmInt(4, invokeMatch(new NumberEval(10), ae, matchAE)); + } catch (RuntimeException e) { + if(e.getMessage().startsWith("Unexpected match_type type")) { + // identified bug 44421 + fail(e.getMessage()); + } + // some other error ?? + throw e; + } + } +} diff --git a/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/functions/TestMid.java b/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/functions/TestMid.java new file mode 100755 index 0000000000..dc3d595aed --- /dev/null +++ b/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/functions/TestMid.java @@ -0,0 +1,115 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +package org.apache.poi.hssf.record.formula.functions; + +import org.apache.poi.hssf.record.formula.AreaPtg; +import org.apache.poi.hssf.record.formula.ReferencePtg; +import org.apache.poi.hssf.record.formula.eval.Area2DEval; +import org.apache.poi.hssf.record.formula.eval.AreaEval; +import org.apache.poi.hssf.record.formula.eval.BlankEval; +import org.apache.poi.hssf.record.formula.eval.BoolEval; +import org.apache.poi.hssf.record.formula.eval.ErrorEval; +import org.apache.poi.hssf.record.formula.eval.Eval; +import org.apache.poi.hssf.record.formula.eval.NumberEval; +import org.apache.poi.hssf.record.formula.eval.Ref2DEval; +import org.apache.poi.hssf.record.formula.eval.RefEval; +import org.apache.poi.hssf.record.formula.eval.StringEval; +import org.apache.poi.hssf.record.formula.eval.ValueEval; + +import junit.framework.TestCase; +/** + * Tests for Excel function MID() + * + * @author Josh Micich + */ +public final class TestMid extends TestCase { + + + private static Eval invokeMid(Eval text, Eval startPos, Eval numChars) { + Eval[] args = new Eval[] { text, startPos, numChars, }; + return new Mid().evaluate(args, -1, (short)-1); + } + + private void confirmMid(Eval text, Eval startPos, Eval numChars, String expected) { + Eval result = invokeMid(text, startPos, numChars); + assertEquals(StringEval.class, result.getClass()); + assertEquals(expected, ((StringEval)result).getStringValue()); + } + + private void confirmMid(Eval text, Eval startPos, Eval numChars, ErrorEval expectedError) { + Eval result = invokeMid(text, startPos, numChars); + assertEquals(ErrorEval.class, result.getClass()); + assertEquals(expectedError.getErrorCode(), ((ErrorEval)result).getErrorCode()); + } + + public void testBasic() { + + confirmMid(new StringEval("galactic"), new NumberEval(3), new NumberEval(4), "lact"); + } + + /** + * Valid cases where args are not precisely (string, int, int) but can be resolved OK. + */ + public void testUnusualArgs() { + // startPos with fractional digits + confirmMid(new StringEval("galactic"), new NumberEval(3.1), new NumberEval(4), "lact"); + + // string startPos + confirmMid(new StringEval("galactic"), new StringEval("3"), new NumberEval(4), "lact"); + + // text (first) arg type is number, other args are strings with fractional digits + confirmMid(new NumberEval(123456), new StringEval("3.1"), new StringEval("2.9"), "34"); + + // startPos is 1x1 area ref, numChars is cell ref + AreaEval aeStart = new Area2DEval(new AreaPtg("A1:A1"), new ValueEval[] { new NumberEval(2), } ); + RefEval reNumChars = new Ref2DEval(new ReferencePtg("B1"), new NumberEval(3)); + confirmMid(new StringEval("galactic"), aeStart, reNumChars, "ala"); + + confirmMid(new StringEval("galactic"), new NumberEval(3.1), BlankEval.INSTANCE, ""); + + confirmMid(new StringEval("galactic"), new NumberEval(3), BoolEval.FALSE, ""); + confirmMid(new StringEval("galactic"), new NumberEval(3), BoolEval.TRUE, "l"); + confirmMid(BlankEval.INSTANCE, new NumberEval(3), BoolEval.TRUE, ""); + + } + + /** + * Extreme values for startPos and numChars + */ + public void testExtremes() { + confirmMid(new StringEval("galactic"), new NumberEval(4), new NumberEval(400), "actic"); + + confirmMid(new StringEval("galactic"), new NumberEval(30), new NumberEval(4), ""); + confirmMid(new StringEval("galactic"), new NumberEval(3), new NumberEval(0), ""); + } + + /** + * All sorts of ways to make MID return defined errors. + */ + public void testErrors() { + confirmMid(ErrorEval.NAME_INVALID, new NumberEval(3), new NumberEval(4), ErrorEval.NAME_INVALID); + confirmMid(new StringEval("galactic"), ErrorEval.NAME_INVALID, new NumberEval(4), ErrorEval.NAME_INVALID); + confirmMid(new StringEval("galactic"), new NumberEval(3), ErrorEval.NAME_INVALID, ErrorEval.NAME_INVALID); + confirmMid(new StringEval("galactic"), ErrorEval.DIV_ZERO, ErrorEval.NAME_INVALID, ErrorEval.DIV_ZERO); + + confirmMid(new StringEval("galactic"), BlankEval.INSTANCE, new NumberEval(3.1), ErrorEval.VALUE_INVALID); + + confirmMid(new StringEval("galactic"), new NumberEval(0), new NumberEval(4), ErrorEval.VALUE_INVALID); + confirmMid(new StringEval("galactic"), new NumberEval(1), new NumberEval(-1), ErrorEval.VALUE_INVALID); + } +} diff --git a/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/functions/TestOffset.java b/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/functions/TestOffset.java new file mode 100755 index 0000000000..f114662985 --- /dev/null +++ b/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/functions/TestOffset.java @@ -0,0 +1,92 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +package org.apache.poi.hssf.record.formula.functions; + +import junit.framework.TestCase; + +import org.apache.poi.hssf.record.formula.functions.Offset.LinearOffsetRange; + +/** + * Tests for OFFSET function implementation + * + * @author Josh Micich + */ +public final class TestOffset extends TestCase { + + + private static void confirmDoubleConvert(double doubleVal, int expected) { + assertEquals(expected, Offset.convertDoubleToInt(doubleVal)); + } + /** + * Excel's double to int conversion (for function 'OFFSET()') behaves more like Math.floor(). + * Note - negative values are not symmetrical + */ + public void testDoubleConversion() { + + confirmDoubleConvert(100.09, 100); + confirmDoubleConvert(100.01, 100); + confirmDoubleConvert(100.00, 100); + confirmDoubleConvert(99.99, 99); + + confirmDoubleConvert(+2.01, +2); + confirmDoubleConvert(+2.00, +2); + confirmDoubleConvert(+1.99, +1); + confirmDoubleConvert(+1.01, +1); + confirmDoubleConvert(+1.00, +1); + confirmDoubleConvert(+0.99, 0); + confirmDoubleConvert(+0.01, 0); + confirmDoubleConvert( 0.00, 0); + confirmDoubleConvert(-0.01, -1); + confirmDoubleConvert(-0.99, -1); + confirmDoubleConvert(-1.00, -1); + confirmDoubleConvert(-1.01, -2); + confirmDoubleConvert(-1.99, -2); + confirmDoubleConvert(-2.00, -2); + confirmDoubleConvert(-2.01, -3); + } + + public void testLinearOffsetRange() { + LinearOffsetRange lor; + + lor = new LinearOffsetRange(3, 2); + assertEquals(3, lor.getFirstIndex()); + assertEquals(4, lor.getLastIndex()); + lor = lor.normaliseAndTranslate(0); // expected no change + assertEquals(3, lor.getFirstIndex()); + assertEquals(4, lor.getLastIndex()); + + lor = lor.normaliseAndTranslate(5); + assertEquals(8, lor.getFirstIndex()); + assertEquals(9, lor.getLastIndex()); + + // negative length + + lor = new LinearOffsetRange(6, -4).normaliseAndTranslate(0); + assertEquals(3, lor.getFirstIndex()); + assertEquals(6, lor.getLastIndex()); + + + // bounds checking + lor = new LinearOffsetRange(0, 100); + assertFalse(lor.isOutOfBounds(0, 16383)); + lor = lor.normaliseAndTranslate(16300); + assertTrue(lor.isOutOfBounds(0, 16383)); + assertFalse(lor.isOutOfBounds(0, 65535)); + } + +} diff --git a/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/functions/TestRoundFuncs.java b/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/functions/TestRoundFuncs.java new file mode 100755 index 0000000000..a6ce345aeb --- /dev/null +++ b/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/functions/TestRoundFuncs.java @@ -0,0 +1,49 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +package org.apache.poi.hssf.record.formula.functions; + +import org.apache.poi.hssf.record.formula.eval.ErrorEval; +import org.apache.poi.hssf.record.formula.eval.Eval; +import org.apache.poi.hssf.record.formula.eval.NumberEval; +import org.apache.poi.hssf.record.formula.eval.StringEval; + +import junit.framework.TestCase; + +/** + * Test cases for ROUND(), ROUNDUP(), ROUNDDOWN() + * + * @author Josh Micich + */ +public final class TestRoundFuncs extends TestCase { + public void testRounddownWithStringArg() { + + Eval strArg = new StringEval("abc"); + Eval[] args = { strArg, new NumberEval(2), }; + Eval result = new Rounddown().evaluate(args, -1, (short)-1); + assertEquals(ErrorEval.VALUE_INVALID, result); + } + + public void testRoundupWithStringArg() { + + Eval strArg = new StringEval("abc"); + Eval[] args = { strArg, new NumberEval(2), }; + Eval result = new Roundup().evaluate(args, -1, (short)-1); + assertEquals(ErrorEval.VALUE_INVALID, result); + } + +} diff --git a/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/functions/TestSumproduct.java b/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/functions/TestSumproduct.java new file mode 100755 index 0000000000..73043911f4 --- /dev/null +++ b/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/functions/TestSumproduct.java @@ -0,0 +1,120 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +package org.apache.poi.hssf.record.formula.functions; + +import org.apache.poi.hssf.record.formula.ReferencePtg; +import org.apache.poi.hssf.record.formula.eval.AreaEval; +import org.apache.poi.hssf.record.formula.eval.ErrorEval; +import org.apache.poi.hssf.record.formula.eval.Eval; +import org.apache.poi.hssf.record.formula.eval.NumberEval; +import org.apache.poi.hssf.record.formula.eval.NumericValueEval; +import org.apache.poi.hssf.record.formula.eval.Ref2DEval; +import org.apache.poi.hssf.record.formula.eval.RefEval; +import org.apache.poi.hssf.record.formula.eval.ValueEval; + +import junit.framework.TestCase; + +/** + * Test cases for SUMPRODUCT() + * + * @author Josh Micich + */ +public final class TestSumproduct extends TestCase { + + private static Eval invokeSumproduct(Eval[] args) { + // srcCellRow and srcCellColumn are ignored by SUMPRODUCT + return new Sumproduct().evaluate(args, -1, (short)-1); + } + private static void confirmDouble(double expected, Eval actualEval) { + if(!(actualEval instanceof NumericValueEval)) { + fail("Expected numeric result"); + } + NumericValueEval nve = (NumericValueEval)actualEval; + assertEquals(expected, nve.getNumberValue(), 0); + } + + public void testScalarSimple() { + + RefEval refEval = new Ref2DEval(new ReferencePtg("A1"), new NumberEval(3)); + Eval[] args = { + refEval, + new NumberEval(2), + }; + Eval result = invokeSumproduct(args); + confirmDouble(6D, result); + } + + + public void testAreaSimple() { + + AreaEval aeA = EvalFactory.createAreaEval("A1:A3", 1, 3); + AreaEval aeB = EvalFactory.createAreaEval("B1:B3", 1, 3); + ValueEval[] aValues = aeA.getValues(); + ValueEval[] bValues = aeB.getValues(); + aValues[0] = new NumberEval(2); + aValues[1] = new NumberEval(4); + aValues[2] = new NumberEval(5); + bValues[0] = new NumberEval(3); + bValues[1] = new NumberEval(6); + bValues[2] = new NumberEval(7); + + Eval[] args = { aeA, aeB, }; + Eval result = invokeSumproduct(args); + confirmDouble(65D, result); + } + + /** + * For scalar products, the terms may be 1x1 area refs + */ + public void testOneByOneArea() { + + AreaEval ae = EvalFactory.createAreaEval("A1:A1", 1, 1); + ae.getValues()[0] = new NumberEval(7); + + Eval[] args = { + ae, + new NumberEval(2), + }; + Eval result = invokeSumproduct(args); + confirmDouble(14D, result); + } + + + public void testMismatchAreaDimensions() { + + AreaEval aeA = EvalFactory.createAreaEval("A1:A3", 1, 3); + AreaEval aeB = EvalFactory.createAreaEval("B1:D1", 3, 1); + + Eval[] args; + args = new Eval[] { aeA, aeB, }; + assertEquals(ErrorEval.VALUE_INVALID, invokeSumproduct(args)); + + args = new Eval[] { aeA, new NumberEval(5), }; + assertEquals(ErrorEval.VALUE_INVALID, invokeSumproduct(args)); + } + + public void testAreaWithErrorCell() { + AreaEval aeA = EvalFactory.createAreaEval("A1:A2", 1, 2); + AreaEval aeB = EvalFactory.createAreaEval("B1:B2", 1, 2); + aeB.getValues()[1] = ErrorEval.REF_INVALID; + + Eval[] args = { aeA, aeB, }; + assertEquals(ErrorEval.REF_INVALID, invokeSumproduct(args)); + } + +} diff --git a/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/functions/TestTFunc.java b/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/functions/TestTFunc.java new file mode 100755 index 0000000000..4d63cad1c5 --- /dev/null +++ b/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/functions/TestTFunc.java @@ -0,0 +1,118 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +package org.apache.poi.hssf.record.formula.functions; + +import org.apache.poi.hssf.record.formula.ReferencePtg; +import org.apache.poi.hssf.record.formula.eval.BlankEval; +import org.apache.poi.hssf.record.formula.eval.BoolEval; +import org.apache.poi.hssf.record.formula.eval.ErrorEval; +import org.apache.poi.hssf.record.formula.eval.Eval; +import org.apache.poi.hssf.record.formula.eval.NumberEval; +import org.apache.poi.hssf.record.formula.eval.Ref2DEval; +import org.apache.poi.hssf.record.formula.eval.StringEval; +import org.apache.poi.hssf.record.formula.eval.ValueEval; + +import junit.framework.TestCase; + +/** + * Test cases for Excel function T() + * + * @author Josh Micich + */ +public final class TestTFunc extends TestCase { + + /** + * @return the result of calling function T() with the specified argument + */ + private static Eval invokeT(Eval arg) { + Eval[] args = { arg, }; + Eval result = new T().evaluate(args, -1, (short)-1); + assertNotNull("result may never be null", result); + return result; + } + /** + * Simulates call: T(A1) + * where cell A1 has the specified innerValue + */ + private Eval invokeTWithReference(ValueEval innerValue) { + Eval arg = new Ref2DEval(new ReferencePtg((short)1, (short)1, false, false), innerValue); + return invokeT(arg); + } + + private static void confirmText(String text) { + Eval arg = new StringEval(text); + Eval eval = invokeT(arg); + StringEval se = (StringEval) eval; + assertEquals(text, se.getStringValue()); + } + + public void testTextValues() { + + confirmText("abc"); + confirmText(""); + confirmText(" "); + confirmText("~"); + confirmText("123"); + confirmText("TRUE"); + } + + private static void confirmError(Eval arg) { + Eval eval = invokeT(arg); + assertTrue(arg == eval); + } + + public void testErrorValues() { + + confirmError(ErrorEval.VALUE_INVALID); + confirmError(ErrorEval.NA); + confirmError(ErrorEval.REF_INVALID); + } + + private static void confirmString(Eval eval, String expected) { + assertTrue(eval instanceof StringEval); + assertEquals(expected, ((StringEval)eval).getStringValue()); + } + + private static void confirmOther(Eval arg) { + Eval eval = invokeT(arg); + confirmString(eval, ""); + } + + public void testOtherValues() { + confirmOther(new NumberEval(2)); + confirmOther(BoolEval.FALSE); + confirmOther(BlankEval.INSTANCE); // can this particular case be verified? + } + + public void testRefValues() { + Eval eval; + + eval = invokeTWithReference(new StringEval("def")); + confirmString(eval, "def"); + eval = invokeTWithReference(new StringEval(" ")); + confirmString(eval, " "); + + eval = invokeTWithReference(new NumberEval(2)); + confirmString(eval, ""); + eval = invokeTWithReference(BoolEval.TRUE); + confirmString(eval, ""); + + eval = invokeTWithReference(ErrorEval.NAME_INVALID); + assertTrue(eval == ErrorEval.NAME_INVALID); + } +} diff --git a/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/functions/TestTrim.java b/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/functions/TestTrim.java new file mode 100755 index 0000000000..076ac1fc7e --- /dev/null +++ b/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/functions/TestTrim.java @@ -0,0 +1,78 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +package org.apache.poi.hssf.record.formula.functions; + +import junit.framework.TestCase; + +import org.apache.poi.hssf.record.formula.eval.BlankEval; +import org.apache.poi.hssf.record.formula.eval.BoolEval; +import org.apache.poi.hssf.record.formula.eval.ErrorEval; +import org.apache.poi.hssf.record.formula.eval.Eval; +import org.apache.poi.hssf.record.formula.eval.NumberEval; +import org.apache.poi.hssf.record.formula.eval.StringEval; +/** + * Tests for Excel function TRIM() + * + * @author Josh Micich + */ +public final class TestTrim extends TestCase { + + + private static Eval invokeTrim(Eval text) { + Eval[] args = new Eval[] { text, }; + return new Trim().evaluate(args, -1, (short)-1); + } + + private void confirmTrim(Eval text, String expected) { + Eval result = invokeTrim(text); + assertEquals(StringEval.class, result.getClass()); + assertEquals(expected, ((StringEval)result).getStringValue()); + } + + private void confirmTrim(Eval text, ErrorEval expectedError) { + Eval result = invokeTrim(text); + assertEquals(ErrorEval.class, result.getClass()); + assertEquals(expectedError.getErrorCode(), ((ErrorEval)result).getErrorCode()); + } + + public void testBasic() { + + confirmTrim(new StringEval(" hi "), "hi"); + confirmTrim(new StringEval("hi "), "hi"); + confirmTrim(new StringEval(" hi"), "hi"); + confirmTrim(new StringEval(" hi there "), "hi there"); + confirmTrim(new StringEval(""), ""); + confirmTrim(new StringEval(" "), ""); + } + + /** + * Valid cases where text arg is not exactly a string + */ + public void testUnusualArgs() { + + // text (first) arg type is number, other args are strings with fractional digits + confirmTrim(new NumberEval(123456), "123456"); + confirmTrim(BoolEval.FALSE, "FALSE"); + confirmTrim(BoolEval.TRUE, "TRUE"); + confirmTrim(BlankEval.INSTANCE, ""); + } + + public void testErrors() { + confirmTrim(ErrorEval.NAME_INVALID, ErrorEval.NAME_INVALID); + } +} diff --git a/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/functions/TestXYNumericFunction.java b/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/functions/TestXYNumericFunction.java new file mode 100755 index 0000000000..c9f043bd3b --- /dev/null +++ b/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/functions/TestXYNumericFunction.java @@ -0,0 +1,139 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +package org.apache.poi.hssf.record.formula.functions; + +import junit.framework.TestCase; + +import org.apache.poi.hssf.record.formula.AreaPtg; +import org.apache.poi.hssf.record.formula.eval.Area2DEval; +import org.apache.poi.hssf.record.formula.eval.ErrorEval; +import org.apache.poi.hssf.record.formula.eval.Eval; +import org.apache.poi.hssf.record.formula.eval.NumberEval; +import org.apache.poi.hssf.record.formula.eval.ValueEval; +/** + * Tests for Excel functions SUMX2MY2(), SUMX2PY2(), SUMXMY2() + * + * @author Josh Micich + */ +public final class TestXYNumericFunction extends TestCase { + private static final Function SUM_SQUARES = new Sumx2py2(); + private static final Function DIFF_SQUARES = new Sumx2my2(); + private static final Function SUM_SQUARES_OF_DIFFS = new Sumxmy2(); + + private static Eval invoke(Function function, Eval xArray, Eval yArray) { + Eval[] args = new Eval[] { xArray, yArray, }; + return function.evaluate(args, -1, (short)-1); + } + + private void confirm(Function function, Eval xArray, Eval yArray, double expected) { + Eval result = invoke(function, xArray, yArray); + assertEquals(NumberEval.class, result.getClass()); + assertEquals(expected, ((NumberEval)result).getNumberValue(), 0); + } + private void confirmError(Function function, Eval xArray, Eval yArray, ErrorEval expectedError) { + Eval result = invoke(function, xArray, yArray); + assertEquals(ErrorEval.class, result.getClass()); + assertEquals(expectedError.getErrorCode(), ((ErrorEval)result).getErrorCode()); + } + + private void confirmError(Eval xArray, Eval yArray, ErrorEval expectedError) { + confirmError(SUM_SQUARES, xArray, yArray, expectedError); + confirmError(DIFF_SQUARES, xArray, yArray, expectedError); + confirmError(SUM_SQUARES_OF_DIFFS, xArray, yArray, expectedError); + } + + public void testBasic() { + ValueEval[] xValues = { + new NumberEval(1), + new NumberEval(2), + }; + ValueEval areaEvalX = createAreaEval(xValues); + confirm(SUM_SQUARES, areaEvalX, areaEvalX, 10.0); + confirm(DIFF_SQUARES, areaEvalX, areaEvalX, 0.0); + confirm(SUM_SQUARES_OF_DIFFS, areaEvalX, areaEvalX, 0.0); + + ValueEval[] yValues = { + new NumberEval(3), + new NumberEval(4), + }; + ValueEval areaEvalY = createAreaEval(yValues); + confirm(SUM_SQUARES, areaEvalX, areaEvalY, 30.0); + confirm(DIFF_SQUARES, areaEvalX, areaEvalY, -20.0); + confirm(SUM_SQUARES_OF_DIFFS, areaEvalX, areaEvalY, 8.0); + } + + /** + * number of items in array is not limited to 30 + */ + public void testLargeArrays() { + ValueEval[] xValues = createMockNumberArray(100, 3); + ValueEval[] yValues = createMockNumberArray(100, 2); + + confirm(SUM_SQUARES, createAreaEval(xValues), createAreaEval(yValues), 1300.0); + confirm(DIFF_SQUARES, createAreaEval(xValues), createAreaEval(yValues), 500.0); + confirm(SUM_SQUARES_OF_DIFFS, createAreaEval(xValues), createAreaEval(yValues), 100.0); + } + + + private ValueEval[] createMockNumberArray(int size, double value) { + ValueEval[] result = new ValueEval[size]; + for (int i = 0; i < result.length; i++) { + result[i] = new NumberEval(value); + } + return result; + } + + private static ValueEval createAreaEval(ValueEval[] values) { + String refStr = "A1:A" + values.length; + return new Area2DEval(new AreaPtg(refStr), values); + } + + public void testErrors() { + ValueEval[] xValues = { + ErrorEval.REF_INVALID, + new NumberEval(2), + }; + ValueEval areaEvalX = createAreaEval(xValues); + ValueEval[] yValues = { + new NumberEval(2), + ErrorEval.NULL_INTERSECTION, + }; + ValueEval areaEvalY = createAreaEval(yValues); + ValueEval[] zValues = { // wrong size + new NumberEval(2), + }; + ValueEval areaEvalZ = createAreaEval(zValues); + + // if either arg is an error, that error propagates + confirmError(ErrorEval.REF_INVALID, ErrorEval.NAME_INVALID, ErrorEval.REF_INVALID); + confirmError(areaEvalX, ErrorEval.NAME_INVALID, ErrorEval.NAME_INVALID); + confirmError(ErrorEval.NAME_INVALID, areaEvalX, ErrorEval.NAME_INVALID); + + // array sizes must match + confirmError(areaEvalX, areaEvalZ, ErrorEval.NA); + confirmError(areaEvalZ, areaEvalY, ErrorEval.NA); + + // any error in an array item propagates up + confirmError(areaEvalX, areaEvalX, ErrorEval.REF_INVALID); + + // search for errors array by array, not pair by pair + confirmError(areaEvalX, areaEvalY, ErrorEval.REF_INVALID); + confirmError(areaEvalY, areaEvalX, ErrorEval.NULL_INTERSECTION); + + } +} diff --git a/src/scratchpad/testcases/org/apache/poi/hssf/usermodel/TestBug42464.java b/src/scratchpad/testcases/org/apache/poi/hssf/usermodel/TestBug42464.java index 3b31cc03a3..c849fd4369 100644 --- a/src/scratchpad/testcases/org/apache/poi/hssf/usermodel/TestBug42464.java +++ b/src/scratchpad/testcases/org/apache/poi/hssf/usermodel/TestBug42464.java @@ -21,14 +21,14 @@ import java.io.FileInputStream; import java.util.Iterator; import java.util.List; +import junit.framework.TestCase; + import org.apache.poi.hssf.record.FormulaRecord; import org.apache.poi.hssf.record.aggregates.FormulaRecordAggregate; -import org.apache.poi.hssf.record.formula.ExpPtg; +import org.apache.poi.hssf.usermodel.HSSFFormulaEvaluator.CellValue; import org.apache.poi.hssf.util.CellReference; -import junit.framework.TestCase; - -public class TestBug42464 extends TestCase { +public final class TestBug42464 extends TestCase { String dirname; protected void setUp() throws Exception { @@ -68,26 +68,27 @@ public class TestBug42464 extends TestCase { Iterator it = row.cellIterator(); while(it.hasNext()) { HSSFCell cell = (HSSFCell)it.next(); - if(cell.getCellType() == HSSFCell.CELL_TYPE_FORMULA) { - FormulaRecordAggregate record = (FormulaRecordAggregate) - cell.getCellValueRecord(); - FormulaRecord r = record.getFormulaRecord(); - List ptgs = r.getParsedExpression(); - - String cellRef = (new CellReference(row.getRowNum(), cell.getCellNum())).toString(); - if(cellRef.equals("BP24")) { - System.out.print(cellRef); - System.out.println(" - has " + r.getNumberOfExpressionTokens() + " ptgs over " + r.getExpressionLength() + " tokens:"); - for(int i=0; i " + cell.getCellFormula()); + if(cell.getCellType() != HSSFCell.CELL_TYPE_FORMULA) { + continue; + } + FormulaRecordAggregate record = (FormulaRecordAggregate) cell.getCellValueRecord(); + FormulaRecord r = record.getFormulaRecord(); + List ptgs = r.getParsedExpression(); + + String cellRef = new CellReference(row.getRowNum(), cell.getCellNum(), false, false).formatAsString(); + if(false && cellRef.equals("BP24")) { // TODO - replace System.out.println()s with asserts + System.out.print(cellRef); + System.out.println(" - has " + r.getNumberOfExpressionTokens() + + " ptgs over " + r.getExpressionLength() + " tokens:"); + for(int i=0; i " + cell.getCellFormula()); } + + CellValue evalResult = eval.evaluate(cell); + assertNotNull(evalResult); } } } diff --git a/src/scratchpad/testcases/org/apache/poi/hssf/usermodel/TestBug44410.java b/src/scratchpad/testcases/org/apache/poi/hssf/usermodel/TestBug44410.java new file mode 100644 index 0000000000..27c3bdc387 --- /dev/null +++ b/src/scratchpad/testcases/org/apache/poi/hssf/usermodel/TestBug44410.java @@ -0,0 +1,100 @@ +package org.apache.poi.hssf.usermodel; +/* ==================================================================== + 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. +==================================================================== */ + +import junit.framework.TestCase; + +import java.io.IOException; +import java.io.FileInputStream; +import java.io.File; +import java.util.List; + +import org.apache.poi.hssf.record.aggregates.FormulaRecordAggregate; +import org.apache.poi.hssf.record.formula.AreaPtg; +import org.apache.poi.hssf.record.formula.FuncVarPtg; + +/** + * Bug 44410: SUM(C:C) is valid in excel, and means a sum + * of all the rows in Column C + * + * @author Nick Burch + */ + +public class TestBug44410 extends TestCase { + protected String cwd = System.getProperty("HSSF.testdata.path"); + + public void test44410() throws IOException { + FileInputStream in = new FileInputStream(new File(cwd, "SingleLetterRanges.xls")); + HSSFWorkbook wb = new HSSFWorkbook(in); + in.close(); + + HSSFSheet sheet = wb.getSheetAt(0); + + HSSFFormulaEvaluator eva = new HSSFFormulaEvaluator(sheet, wb); + + // =index(C:C,2,1) -> 2 + HSSFRow rowIDX = (HSSFRow)sheet.getRow(3); + // =sum(C:C) -> 6 + HSSFRow rowSUM = (HSSFRow)sheet.getRow(4); + // =sum(C:D) -> 66 + HSSFRow rowSUM2D = (HSSFRow)sheet.getRow(5); + + // Test the sum + HSSFCell cellSUM = rowSUM.getCell((short)0); + + FormulaRecordAggregate frec = + (FormulaRecordAggregate)cellSUM.getCellValueRecord(); + List ops = frec.getFormulaRecord().getParsedExpression(); + assertEquals(2, ops.size()); + assertEquals(AreaPtg.class, ops.get(0).getClass()); + assertEquals(FuncVarPtg.class, ops.get(1).getClass()); + + // Actually stored as C1 to C65536 + // (last row is -1 === 65535) + AreaPtg ptg = (AreaPtg)ops.get(0); + assertEquals(2, ptg.getFirstColumn()); + assertEquals(2, ptg.getLastColumn()); + assertEquals(0, ptg.getFirstRow()); + assertEquals(65535, ptg.getLastRow()); + assertEquals("C:C", ptg.toFormulaString(wb.getWorkbook())); + + // Will show as C:C, but won't know how many + // rows it covers as we don't have the sheet + // to hand when turning the Ptgs into a string + assertEquals("SUM(C:C)", cellSUM.getCellFormula()); + eva.setCurrentRow(rowSUM); + + // But the evaluator knows the sheet, so it + // can do it properly + assertEquals(6, eva.evaluate(cellSUM).getNumberValue(), 0); + + + // Test the index + // Again, the formula string will be right but + // lacking row count, evaluated will be right + HSSFCell cellIDX = rowIDX.getCell((short)0); + assertEquals("INDEX(C:C,2,1)", cellIDX.getCellFormula()); + eva.setCurrentRow(rowIDX); + assertEquals(2, eva.evaluate(cellIDX).getNumberValue(), 0); + + // Across two colums + HSSFCell cellSUM2D = rowSUM2D.getCell((short)0); + assertEquals("SUM(C:D)", cellSUM2D.getCellFormula()); + eva.setCurrentRow(rowSUM2D); + assertEquals(66, eva.evaluate(cellSUM2D).getNumberValue(), 0); + } +} diff --git a/src/scratchpad/testcases/org/apache/poi/hssf/usermodel/TestBug44508.java b/src/scratchpad/testcases/org/apache/poi/hssf/usermodel/TestBug44508.java new file mode 100644 index 0000000000..3362f3c3e7 --- /dev/null +++ b/src/scratchpad/testcases/org/apache/poi/hssf/usermodel/TestBug44508.java @@ -0,0 +1,42 @@ +package org.apache.poi.hssf.usermodel; +/* ==================================================================== + 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. +==================================================================== */ + +import junit.framework.TestCase; + +public class TestBug44508 extends TestCase { + protected String cwd = System.getProperty("HSSF.testdata.path"); + + public void testEvaluateBooleanInCell_bug44508() { + HSSFWorkbook wb = new HSSFWorkbook(); + HSSFSheet sheet = wb.createSheet(); + wb.setSheetName(0, "Sheet1"); + HSSFRow row = sheet.createRow(0); + HSSFCell cell = row.createCell((short)0); + + cell.setCellFormula("1=1"); + + HSSFFormulaEvaluator fe = new HSSFFormulaEvaluator(sheet, wb); + fe.setCurrentRow(row); + try { + fe.evaluateInCell(cell); + } catch (NumberFormatException e) { + fail("Identified bug 44508"); + } + assertEquals(true, cell.getBooleanCellValue()); + } +} diff --git a/src/testcases/org/apache/poi/AllPOITests.java b/src/testcases/org/apache/poi/AllPOITests.java new file mode 100755 index 0000000000..191b719500 --- /dev/null +++ b/src/testcases/org/apache/poi/AllPOITests.java @@ -0,0 +1,44 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +package org.apache.poi; + +import org.apache.poi.ddf.AllPOIDDFTests; +import org.apache.poi.hpsf.basic.AllPOIHPSFBasicTests; +import org.apache.poi.hssf.HSSFTests; +import org.apache.poi.poifs.AllPOIFSTests; +import org.apache.poi.util.AllPOIUtilTests; + +import junit.framework.Test; +import junit.framework.TestSuite; +/** + * Root Test Suite for entire POI project. (Includes all sub-packages of org.apache.poi)
    + * + * @author Josh Micich + */ +public final class AllPOITests { + public static Test suite() { + TestSuite result = new TestSuite("Tests for org.apache.poi"); + result.addTestSuite(TestPOIDocumentMain.class); + result.addTest(AllPOIDDFTests.suite()); + result.addTest(AllPOIHPSFBasicTests.suite()); + result.addTest(HSSFTests.suite()); + result.addTest(AllPOIFSTests.suite()); + result.addTest(AllPOIUtilTests.suite()); + return result; + } +} diff --git a/src/testcases/org/apache/poi/TestPOIDocumentMain.java b/src/testcases/org/apache/poi/TestPOIDocumentMain.java index be3d9b0a78..5d4d7df19e 100644 --- a/src/testcases/org/apache/poi/TestPOIDocumentMain.java +++ b/src/testcases/org/apache/poi/TestPOIDocumentMain.java @@ -85,7 +85,8 @@ public class TestPOIDocumentMain extends TestCase { public void testWriteProperties() throws Exception { // Just check we can write them back out into a filesystem POIFSFileSystem outFS = new POIFSFileSystem(); - doc.writeProperties(outFS); + doc.readProperties(); + doc.writeProperties(outFS); // Should now hold them assertNotNull( @@ -101,7 +102,8 @@ public class TestPOIDocumentMain extends TestCase { // Write them out POIFSFileSystem outFS = new POIFSFileSystem(); - doc.writeProperties(outFS); + doc.readProperties(); + doc.writeProperties(outFS); outFS.writeFilesystem(baos); // Create a new version diff --git a/src/testcases/org/apache/poi/ddf/AllPOIDDFTests.java b/src/testcases/org/apache/poi/ddf/AllPOIDDFTests.java new file mode 100755 index 0000000000..9b5fc0bfef --- /dev/null +++ b/src/testcases/org/apache/poi/ddf/AllPOIDDFTests.java @@ -0,0 +1,47 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +package org.apache.poi.ddf; + +import junit.framework.Test; +import junit.framework.TestSuite; +/** + * Tests for org.apache.poi.ddf
    + * + * @author Josh Micich + */ +public final class AllPOIDDFTests { + public static Test suite() { + TestSuite result = new TestSuite("Tests for org.apache.poi.ddf"); + result.addTestSuite(TestEscherBlipWMFRecord.class); + result.addTestSuite(TestEscherBoolProperty.class); + result.addTestSuite(TestEscherBSERecord.class); + result.addTestSuite(TestEscherChildAnchorRecord.class); + result.addTestSuite(TestEscherClientAnchorRecord.class); + result.addTestSuite(TestEscherClientDataRecord.class); + result.addTestSuite(TestEscherContainerRecord.class); + result.addTestSuite(TestEscherDggRecord.class); + result.addTestSuite(TestEscherDgRecord.class); + result.addTestSuite(TestEscherOptRecord.class); + result.addTestSuite(TestEscherPropertyFactory.class); + result.addTestSuite(TestEscherSpgrRecord.class); + result.addTestSuite(TestEscherSplitMenuColorsRecord.class); + result.addTestSuite(TestEscherSpRecord.class); + result.addTestSuite(TestUnknownEscherRecord.class); + return result; + } +} diff --git a/src/testcases/org/apache/poi/hpsf/basic/AllPOIHPSFBasicTests.java b/src/testcases/org/apache/poi/hpsf/basic/AllPOIHPSFBasicTests.java new file mode 100755 index 0000000000..5954c9ca5f --- /dev/null +++ b/src/testcases/org/apache/poi/hpsf/basic/AllPOIHPSFBasicTests.java @@ -0,0 +1,39 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +package org.apache.poi.hpsf.basic; + +import junit.framework.Test; +import junit.framework.TestSuite; +/** + * Test suite for org.apache.poi.hpsf.basic + * + * @author Josh Micich + */ +public final class AllPOIHPSFBasicTests { + public static Test suite() { + TestSuite result = new TestSuite("Tests for org.apache.poi.hpsf.basic"); + result.addTestSuite(TestBasic.class); + result.addTestSuite(TestClassID.class); + result.addTestSuite(TestEmptyProperties.class); + result.addTestSuite(TestMetaDataIPI.class); + result.addTestSuite(TestUnicode.class); + result.addTestSuite(TestWrite.class); + result.addTestSuite(TestWriteWellKnown.class); + return result; + } +} diff --git a/src/testcases/org/apache/poi/hssf/HSSFTests.java b/src/testcases/org/apache/poi/hssf/HSSFTests.java index 1e0edd6825..5b597a67c8 100644 --- a/src/testcases/org/apache/poi/hssf/HSSFTests.java +++ b/src/testcases/org/apache/poi/hssf/HSSFTests.java @@ -1,4 +1,3 @@ - /* ==================================================================== Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with @@ -15,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. ==================================================================== */ - + package org.apache.poi.hssf; import junit.framework.Test; @@ -26,80 +25,8 @@ import org.apache.poi.hssf.eventmodel.TestModelFactory; import org.apache.poi.hssf.model.TestDrawingManager; import org.apache.poi.hssf.model.TestFormulaParser; import org.apache.poi.hssf.model.TestSheet; -import org.apache.poi.hssf.record.TestAreaFormatRecord; -import org.apache.poi.hssf.record.TestAreaRecord; -import org.apache.poi.hssf.record.TestAxisLineFormatRecord; -import org.apache.poi.hssf.record.TestAxisOptionsRecord; -import org.apache.poi.hssf.record.TestAxisParentRecord; -import org.apache.poi.hssf.record.TestAxisRecord; -import org.apache.poi.hssf.record.TestAxisUsedRecord; -import org.apache.poi.hssf.record.TestBarRecord; -import org.apache.poi.hssf.record.TestBoundSheetRecord; -import org.apache.poi.hssf.record.TestCategorySeriesAxisRecord; -import org.apache.poi.hssf.record.TestChartRecord; -import org.apache.poi.hssf.record.TestDatRecord; -import org.apache.poi.hssf.record.TestDataFormatRecord; -import org.apache.poi.hssf.record.TestDefaultDataLabelTextPropertiesRecord; -import org.apache.poi.hssf.record.TestFontBasisRecord; -import org.apache.poi.hssf.record.TestFontIndexRecord; -import org.apache.poi.hssf.record.TestFormulaRecord; -import org.apache.poi.hssf.record.TestFrameRecord; -import org.apache.poi.hssf.record.TestLegendRecord; -import org.apache.poi.hssf.record.TestLineFormatRecord; -import org.apache.poi.hssf.record.TestLinkedDataRecord; -import org.apache.poi.hssf.record.TestNameRecord; -import org.apache.poi.hssf.record.TestNumberFormatIndexRecord; -import org.apache.poi.hssf.record.TestObjectLinkRecord; -import org.apache.poi.hssf.record.TestPaletteRecord; -import org.apache.poi.hssf.record.TestPlotAreaRecord; -import org.apache.poi.hssf.record.TestPlotGrowthRecord; -import org.apache.poi.hssf.record.TestRecordFactory; -import org.apache.poi.hssf.record.TestSCLRecord; -import org.apache.poi.hssf.record.TestSSTDeserializer; -import org.apache.poi.hssf.record.TestSSTRecord; -import org.apache.poi.hssf.record.TestSSTRecordSizeCalculator; -import org.apache.poi.hssf.record.TestSeriesChartGroupIndexRecord; -import org.apache.poi.hssf.record.TestSeriesIndexRecord; -import org.apache.poi.hssf.record.TestSeriesLabelsRecord; -import org.apache.poi.hssf.record.TestSeriesListRecord; -import org.apache.poi.hssf.record.TestSeriesRecord; -import org.apache.poi.hssf.record.TestSeriesTextRecord; -import org.apache.poi.hssf.record.TestSeriesToChartGroupRecord; -import org.apache.poi.hssf.record.TestSheetPropertiesRecord; -import org.apache.poi.hssf.record.TestStringRecord; -import org.apache.poi.hssf.record.TestSupBookRecord; -import org.apache.poi.hssf.record.TestTextRecord; -import org.apache.poi.hssf.record.TestTickRecord; -import org.apache.poi.hssf.record.TestUnicodeString; -import org.apache.poi.hssf.record.TestUnitsRecord; -import org.apache.poi.hssf.record.TestValueRangeRecord; -import org.apache.poi.hssf.record.aggregates.TestRowRecordsAggregate; -import org.apache.poi.hssf.record.aggregates.TestValueRecordsAggregate; -import org.apache.poi.hssf.record.formula.AllFormulaTests; -import org.apache.poi.hssf.usermodel.TestBugs; -import org.apache.poi.hssf.usermodel.TestCellStyle; -import org.apache.poi.hssf.usermodel.TestCloneSheet; -import org.apache.poi.hssf.usermodel.TestEscherGraphics; -import org.apache.poi.hssf.usermodel.TestEscherGraphics2d; -import org.apache.poi.hssf.usermodel.TestFontDetails; -import org.apache.poi.hssf.usermodel.TestFormulas; -import org.apache.poi.hssf.usermodel.TestHSSFCell; -import org.apache.poi.hssf.usermodel.TestHSSFClientAnchor; -import org.apache.poi.hssf.usermodel.TestHSSFComment; -import org.apache.poi.hssf.usermodel.TestHSSFDateUtil; -import org.apache.poi.hssf.usermodel.TestHSSFHeaderFooter; -import org.apache.poi.hssf.usermodel.TestHSSFPalette; -import org.apache.poi.hssf.usermodel.TestHSSFRichTextString; -import org.apache.poi.hssf.usermodel.TestHSSFRow; -import org.apache.poi.hssf.usermodel.TestHSSFSheet; -import org.apache.poi.hssf.usermodel.TestHSSFSheetOrder; -import org.apache.poi.hssf.usermodel.TestHSSFSheetSetOrder; -import org.apache.poi.hssf.usermodel.TestHSSFWorkbook; -import org.apache.poi.hssf.usermodel.TestNamedRange; -import org.apache.poi.hssf.usermodel.TestReadWriteChart; -import org.apache.poi.hssf.usermodel.TestSanityChecker; -import org.apache.poi.hssf.usermodel.TestSheetShiftRows; -import org.apache.poi.hssf.usermodel.TestWorkbook; +import org.apache.poi.hssf.record.AllRecordTests; +import org.apache.poi.hssf.usermodel.AllUserModelTests; import org.apache.poi.hssf.util.TestAreaReference; import org.apache.poi.hssf.util.TestCellReference; import org.apache.poi.hssf.util.TestRKUtil; @@ -107,118 +34,36 @@ import org.apache.poi.hssf.util.TestRangeAddress; import org.apache.poi.hssf.util.TestSheetReferences; /** - * Test Suite for running just HSSF tests. Mostly - * this is for my convienience. + * Test Suite for all sub-packages of org.apache.poi.hssf
    + * + * Mostly this is for my convenience. * * @author Andrew C. Oliver acoliver@apache.org */ -public class HSSFTests -{ +public final class HSSFTests { - public static void main(String[] args) - { + public static void main(String[] args) { junit.textui.TestRunner.run(suite()); } - public static Test suite() - { - TestSuite suite = - new TestSuite("Test for org.apache.poi.hssf.usermodel"); - //$JUnit-BEGIN$ - - suite.addTest(new TestSuite(TestBugs.class)); - suite.addTest(new TestSuite(TestCloneSheet.class)); - suite.addTest(new TestSuite(TestEscherGraphics.class)); - suite.addTest(new TestSuite(TestEscherGraphics2d.class)); - suite.addTest(new TestSuite(TestFontDetails.class)); - suite.addTest(new TestSuite(TestHSSFClientAnchor.class)); - suite.addTest(new TestSuite(TestHSSFHeaderFooter.class)); - suite.addTest(new TestSuite(TestHSSFRichTextString.class)); - suite.addTest(new TestSuite(TestHSSFSheetOrder.class)); - suite.addTest(new TestSuite(TestHSSFSheetSetOrder.class)); - suite.addTest(new TestSuite(TestHSSFWorkbook.class)); - suite.addTest(new TestSuite(TestSanityChecker.class)); - suite.addTest(new TestSuite(TestSheetShiftRows.class)); - - suite.addTest(new TestSuite(TestCellStyle.class)); - suite.addTest(new TestSuite(TestFormulas.class)); - suite.addTest(new TestSuite(TestHSSFCell.class)); - suite.addTest(new TestSuite(TestHSSFDateUtil.class)); - suite.addTest(new TestSuite(TestHSSFPalette.class)); - suite.addTest(new TestSuite(TestHSSFRow.class)); - suite.addTest(new TestSuite(TestHSSFSheet.class)); - suite.addTest(new TestSuite(TestNamedRange.class)); - suite.addTest(new TestSuite(TestReadWriteChart.class)); - suite.addTest(new TestSuite(TestWorkbook.class)); - + public static Test suite() { + TestSuite suite = new TestSuite("Tests for org.apache.poi.hssf"); + // $JUnit-BEGIN$ + suite.addTest(AllUserModelTests.suite()); + suite.addTest(AllRecordTests.suite()); suite.addTest(new TestSuite(TestFormulaParser.class)); - suite.addTest(new TestSuite(TestAreaFormatRecord.class)); - suite.addTest(new TestSuite(TestAreaRecord.class)); - suite.addTest(new TestSuite(TestAxisLineFormatRecord.class)); - suite.addTest(new TestSuite(TestAxisOptionsRecord.class)); - suite.addTest(new TestSuite(TestAxisParentRecord.class)); - suite.addTest(new TestSuite(TestAxisRecord.class)); - suite.addTest(new TestSuite(TestAxisUsedRecord.class)); - suite.addTest(new TestSuite(TestBarRecord.class)); - suite.addTest(new TestSuite(TestBoundSheetRecord.class)); - suite.addTest(new TestSuite(TestCategorySeriesAxisRecord.class)); - suite.addTest(new TestSuite(TestChartRecord.class)); - suite.addTest(new TestSuite(TestDatRecord.class)); - suite.addTest(new TestSuite(TestDataFormatRecord.class)); - suite.addTest( - new TestSuite(TestDefaultDataLabelTextPropertiesRecord.class)); - suite.addTest(new TestSuite(TestFontBasisRecord.class)); - suite.addTest(new TestSuite(TestFontIndexRecord.class)); - suite.addTest(new TestSuite(TestFormulaRecord.class)); - suite.addTest(new TestSuite(TestFrameRecord.class)); - suite.addTest(new TestSuite(TestLegendRecord.class)); - suite.addTest(new TestSuite(TestLineFormatRecord.class)); - suite.addTest(new TestSuite(TestLinkedDataRecord.class)); - suite.addTest(new TestSuite(TestNumberFormatIndexRecord.class)); - suite.addTest(new TestSuite(TestObjectLinkRecord.class)); - suite.addTest(new TestSuite(TestPaletteRecord.class)); - suite.addTest(new TestSuite(TestPlotAreaRecord.class)); - suite.addTest(new TestSuite(TestPlotGrowthRecord.class)); - suite.addTest(new TestSuite(TestRecordFactory.class)); - suite.addTest(new TestSuite(TestSCLRecord.class)); - suite.addTest(new TestSuite(TestSSTDeserializer.class)); - suite.addTest(new TestSuite(TestSSTRecord.class)); - suite.addTest(new TestSuite(TestSSTRecordSizeCalculator.class)); - suite.addTest(new TestSuite(TestSeriesChartGroupIndexRecord.class)); - suite.addTest(new TestSuite(TestSeriesIndexRecord.class)); - suite.addTest(new TestSuite(TestSeriesLabelsRecord.class)); - suite.addTest(new TestSuite(TestSeriesListRecord.class)); - suite.addTest(new TestSuite(TestSeriesRecord.class)); - suite.addTest(new TestSuite(TestSeriesTextRecord.class)); - suite.addTest(new TestSuite(TestSeriesToChartGroupRecord.class)); - suite.addTest(new TestSuite(TestSheetPropertiesRecord.class)); - suite.addTest(new TestSuite(TestStringRecord.class)); - suite.addTest(new TestSuite(TestSupBookRecord.class)); - suite.addTest(new TestSuite(TestTextRecord.class)); - suite.addTest(new TestSuite(TestTickRecord.class)); - suite.addTest(new TestSuite(TestUnicodeString.class)); - suite.addTest(new TestSuite(TestUnitsRecord.class)); - suite.addTest(new TestSuite(TestValueRangeRecord.class)); - suite.addTest(new TestSuite(TestRowRecordsAggregate.class)); suite.addTest(new TestSuite(TestAreaReference.class)); suite.addTest(new TestSuite(TestCellReference.class)); - suite.addTest(new TestSuite(TestRangeAddress.class)); + suite.addTest(new TestSuite(TestRangeAddress.class)); suite.addTest(new TestSuite(TestRKUtil.class)); suite.addTest(new TestSuite(TestSheetReferences.class)); - - - suite.addTest(AllFormulaTests.suite()); - suite.addTest(new TestSuite(TestValueRecordsAggregate.class)); - suite.addTest(new TestSuite(TestNameRecord.class)); - suite.addTest(new TestSuite(TestEventRecordFactory.class)); - suite.addTest(new TestSuite(TestModelFactory.class)); - suite.addTest(new TestSuite(TestDrawingManager.class)); - suite.addTest(new TestSuite(TestSheet.class)); - - suite.addTest(new TestSuite(TestHSSFComment.class)); - //$JUnit-END$ + suite.addTest(new TestSuite(TestEventRecordFactory.class)); + suite.addTest(new TestSuite(TestModelFactory.class)); + suite.addTest(new TestSuite(TestDrawingManager.class)); + suite.addTest(new TestSuite(TestSheet.class)); + // $JUnit-END$ return suite; } } diff --git a/src/testcases/org/apache/poi/hssf/data/44297.xls b/src/testcases/org/apache/poi/hssf/data/44297.xls new file mode 100755 index 0000000000..bc65efd4e0 Binary files /dev/null and b/src/testcases/org/apache/poi/hssf/data/44297.xls differ diff --git a/src/testcases/org/apache/poi/hssf/data/AbnormalSharedFormulaFlag.xls b/src/testcases/org/apache/poi/hssf/data/AbnormalSharedFormulaFlag.xls new file mode 100755 index 0000000000..788865b3b9 Binary files /dev/null and b/src/testcases/org/apache/poi/hssf/data/AbnormalSharedFormulaFlag.xls differ diff --git a/src/testcases/org/apache/poi/hssf/data/FormulaEvalTestData.xls b/src/testcases/org/apache/poi/hssf/data/FormulaEvalTestData.xls new file mode 100644 index 0000000000..6260d878bc Binary files /dev/null and b/src/testcases/org/apache/poi/hssf/data/FormulaEvalTestData.xls differ diff --git a/src/testcases/org/apache/poi/hssf/data/LookupFunctionsTestCaseData.xls b/src/testcases/org/apache/poi/hssf/data/LookupFunctionsTestCaseData.xls new file mode 100755 index 0000000000..f4b35fb935 Binary files /dev/null and b/src/testcases/org/apache/poi/hssf/data/LookupFunctionsTestCaseData.xls differ diff --git a/src/testcases/org/apache/poi/hssf/data/MissingBits.xls b/src/testcases/org/apache/poi/hssf/data/MissingBits.xls new file mode 100644 index 0000000000..4dba467a4f Binary files /dev/null and b/src/testcases/org/apache/poi/hssf/data/MissingBits.xls differ diff --git a/src/testcases/org/apache/poi/hssf/data/OddStyleRecord.xls b/src/testcases/org/apache/poi/hssf/data/OddStyleRecord.xls new file mode 100644 index 0000000000..dcaa79b5e1 Binary files /dev/null and b/src/testcases/org/apache/poi/hssf/data/OddStyleRecord.xls differ diff --git a/src/testcases/org/apache/poi/hssf/data/ReadOnlyRecommended.xls b/src/testcases/org/apache/poi/hssf/data/ReadOnlyRecommended.xls new file mode 100644 index 0000000000..d479b94f60 Binary files /dev/null and b/src/testcases/org/apache/poi/hssf/data/ReadOnlyRecommended.xls differ diff --git a/src/testcases/org/apache/poi/hssf/data/SingleLetterRanges.xls b/src/testcases/org/apache/poi/hssf/data/SingleLetterRanges.xls new file mode 100644 index 0000000000..e35058d410 Binary files /dev/null and b/src/testcases/org/apache/poi/hssf/data/SingleLetterRanges.xls differ diff --git a/src/testcases/org/apache/poi/hssf/data/TestDataValidation.xls b/src/testcases/org/apache/poi/hssf/data/TestDataValidation.xls deleted file mode 100644 index 0b2a869486..0000000000 Binary files a/src/testcases/org/apache/poi/hssf/data/TestDataValidation.xls and /dev/null differ diff --git a/src/testcases/org/apache/poi/hssf/data/externalFunctionExample.xls b/src/testcases/org/apache/poi/hssf/data/externalFunctionExample.xls new file mode 100755 index 0000000000..07eafb4144 Binary files /dev/null and b/src/testcases/org/apache/poi/hssf/data/externalFunctionExample.xls differ diff --git a/src/testcases/org/apache/poi/hssf/data/logoKarmokar4.png b/src/testcases/org/apache/poi/hssf/data/logoKarmokar4.png new file mode 100755 index 0000000000..90a915a3a9 Binary files /dev/null and b/src/testcases/org/apache/poi/hssf/data/logoKarmokar4.png differ diff --git a/src/testcases/org/apache/poi/hssf/model/TestFormulaParser.java b/src/testcases/org/apache/poi/hssf/model/TestFormulaParser.java index 1b41d87d4d..f922e75d82 100644 --- a/src/testcases/org/apache/poi/hssf/model/TestFormulaParser.java +++ b/src/testcases/org/apache/poi/hssf/model/TestFormulaParser.java @@ -1,4 +1,3 @@ - /* ==================================================================== Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with @@ -18,24 +17,35 @@ package org.apache.poi.hssf.model; +import junit.framework.AssertionFailedError; import junit.framework.TestCase; +import org.apache.poi.hssf.model.FormulaParser.FormulaParseException; import org.apache.poi.hssf.record.formula.AbstractFunctionPtg; import org.apache.poi.hssf.record.formula.AddPtg; +import org.apache.poi.hssf.record.formula.AreaPtg; import org.apache.poi.hssf.record.formula.AttrPtg; import org.apache.poi.hssf.record.formula.BoolPtg; +import org.apache.poi.hssf.record.formula.ConcatPtg; import org.apache.poi.hssf.record.formula.DividePtg; import org.apache.poi.hssf.record.formula.EqualPtg; +import org.apache.poi.hssf.record.formula.ErrPtg; +import org.apache.poi.hssf.record.formula.FuncPtg; import org.apache.poi.hssf.record.formula.FuncVarPtg; import org.apache.poi.hssf.record.formula.IntPtg; import org.apache.poi.hssf.record.formula.LessEqualPtg; import org.apache.poi.hssf.record.formula.LessThanPtg; +import org.apache.poi.hssf.record.formula.MissingArgPtg; +import org.apache.poi.hssf.record.formula.MultiplyPtg; import org.apache.poi.hssf.record.formula.NamePtg; import org.apache.poi.hssf.record.formula.NotEqualPtg; import org.apache.poi.hssf.record.formula.NumberPtg; +import org.apache.poi.hssf.record.formula.PercentPtg; +import org.apache.poi.hssf.record.formula.PowerPtg; import org.apache.poi.hssf.record.formula.Ptg; import org.apache.poi.hssf.record.formula.ReferencePtg; import org.apache.poi.hssf.record.formula.StringPtg; +import org.apache.poi.hssf.record.formula.SubtractPtg; import org.apache.poi.hssf.record.formula.UnaryMinusPtg; import org.apache.poi.hssf.record.formula.UnaryPlusPtg; import org.apache.poi.hssf.usermodel.HSSFCell; @@ -49,27 +59,27 @@ import org.apache.poi.hssf.usermodel.HSSFWorkbook; * Some tests are also done in scratchpad, if they need * HSSFFormulaEvaluator, which is there */ -public class TestFormulaParser extends TestCase { +public final class TestFormulaParser extends TestCase { - public TestFormulaParser(String name) { - super(name); - } - public void setUp(){ - - } - - public void tearDown() { - + /** + * @return parsed token array already confirmed not null + */ + private static Ptg[] parseFormula(String s) { + FormulaParser fp = new FormulaParser(s, null); + fp.parse(); + Ptg[] result = fp.getRPNPtg(); + assertNotNull("Ptg array should not be null", result); + return result; } public void testSimpleFormula() { - FormulaParser fp = new FormulaParser("2+2;",null); + FormulaParser fp = new FormulaParser("2+2",null); fp.parse(); Ptg[] ptgs = fp.getRPNPtg(); assertTrue("three tokens expected, got "+ptgs.length,ptgs.length == 3); } public void testFormulaWithSpace1() { - FormulaParser fp = new FormulaParser(" 2 + 2 ;",null); + FormulaParser fp = new FormulaParser(" 2 + 2 ",null); fp.parse(); Ptg[] ptgs = fp.getRPNPtg(); assertTrue("three tokens expected, got "+ptgs.length,ptgs.length == 3); @@ -82,7 +92,7 @@ public class TestFormulaParser extends TestCase { public void testFormulaWithSpace2() { Ptg[] ptgs; FormulaParser fp; - fp = new FormulaParser("2+ sum( 3 , 4) ;",null); + fp = new FormulaParser("2+ sum( 3 , 4) ",null); fp.parse(); ptgs = fp.getRPNPtg(); assertTrue("five tokens expected, got "+ptgs.length,ptgs.length == 5); @@ -91,7 +101,7 @@ public class TestFormulaParser extends TestCase { public void testFormulaWithSpaceNRef() { Ptg[] ptgs; FormulaParser fp; - fp = new FormulaParser("sum( A2:A3 );",null); + fp = new FormulaParser("sum( A2:A3 )",null); fp.parse(); ptgs = fp.getRPNPtg(); assertTrue("two tokens expected, got "+ptgs.length,ptgs.length == 2); @@ -100,7 +110,7 @@ public class TestFormulaParser extends TestCase { public void testFormulaWithString() { Ptg[] ptgs; FormulaParser fp; - fp = new FormulaParser("\"hello\" & \"world\" ;",null); + fp = new FormulaParser("\"hello\" & \"world\" ",null); fp.parse(); ptgs = fp.getRPNPtg(); assertTrue("three token expected, got " + ptgs.length, ptgs.length == 3); @@ -273,20 +283,21 @@ public class TestFormulaParser extends TestCase { } public void testMacroFunction() { - Workbook w = new Workbook(); + Workbook w = Workbook.createWorkbook(); FormulaParser fp = new FormulaParser("FOO()", w); fp.parse(); Ptg[] ptg = fp.getRPNPtg(); - AbstractFunctionPtg tfunc = (AbstractFunctionPtg) ptg[0]; - assertEquals("externalflag", tfunc.getName()); - - NamePtg tname = (NamePtg) ptg[1]; + // the name gets encoded as the first arg + NamePtg tname = (NamePtg) ptg[0]; assertEquals("FOO", tname.toFormulaString(w)); + + AbstractFunctionPtg tfunc = (AbstractFunctionPtg) ptg[1]; + assertEquals("externalflag", tfunc.getName()); } public void testEmbeddedSlash() { - FormulaParser fp = new FormulaParser("HYPERLINK(\"http://www.jakarta.org\",\"Jakarta\");",null); + FormulaParser fp = new FormulaParser("HYPERLINK(\"http://www.jakarta.org\",\"Jakarta\")",null); fp.parse(); Ptg[] ptg = fp.getRPNPtg(); assertTrue("first ptg is string",ptg[0] instanceof StringPtg); @@ -397,7 +408,7 @@ public class TestFormulaParser extends TestCase { public void testUnderscore() { HSSFWorkbook wb = new HSSFWorkbook(); - wb.createSheet("Cash_Flow");; + wb.createSheet("Cash_Flow"); HSSFSheet sheet = wb.createSheet("Test"); HSSFRow row = sheet.createRow(0); @@ -438,7 +449,7 @@ public class TestFormulaParser extends TestCase { public void testExponentialInSheet() throws Exception { HSSFWorkbook wb = new HSSFWorkbook(); - wb.createSheet("Cash_Flow");; + wb.createSheet("Cash_Flow"); HSSFSheet sheet = wb.createSheet("Test"); HSSFRow row = sheet.createRow(0); @@ -514,7 +525,7 @@ public class TestFormulaParser extends TestCase { public void testNumbers() { HSSFWorkbook wb = new HSSFWorkbook(); - wb.createSheet("Cash_Flow");; + wb.createSheet("Cash_Flow"); HSSFSheet sheet = wb.createSheet("Test"); HSSFRow row = sheet.createRow(0); @@ -553,7 +564,7 @@ public class TestFormulaParser extends TestCase { public void testRanges() { HSSFWorkbook wb = new HSSFWorkbook(); - wb.createSheet("Cash_Flow");; + wb.createSheet("Cash_Flow"); HSSFSheet sheet = wb.createSheet("Test"); HSSFRow row = sheet.createRow(0); @@ -571,5 +582,266 @@ public class TestFormulaParser extends TestCase { cell.setCellFormula("A1...A2"); formula = cell.getCellFormula(); assertEquals("A1:A2", formula); - } + } + + /** + * Test for bug observable at svn revision 618865 (5-Feb-2008)
    + * a formula consisting of a single no-arg function got rendered without the function braces + */ + public void testToFormulaStringZeroArgFunction() { + + Workbook book = Workbook.createWorkbook(); // not really used in this test + + Ptg[] ptgs = { + new FuncPtg(10, 0), + }; + assertEquals("NA()", FormulaParser.toFormulaString(book, ptgs)); + } + + public void testPercent() { + Ptg[] ptgs; + ptgs = parseFormula("5%"); + assertEquals(2, ptgs.length); + assertEquals(ptgs[0].getClass(), IntPtg.class); + assertEquals(ptgs[1].getClass(), PercentPtg.class); + + // spaces OK + ptgs = parseFormula(" 250 % "); + assertEquals(2, ptgs.length); + assertEquals(ptgs[0].getClass(), IntPtg.class); + assertEquals(ptgs[1].getClass(), PercentPtg.class); + + + // double percent OK + ptgs = parseFormula("12345.678%%"); + assertEquals(3, ptgs.length); + assertEquals(ptgs[0].getClass(), NumberPtg.class); + assertEquals(ptgs[1].getClass(), PercentPtg.class); + assertEquals(ptgs[2].getClass(), PercentPtg.class); + + // percent of a bracketed expression + ptgs = parseFormula("(A1+35)%*B1%"); + assertEquals(8, ptgs.length); + assertEquals(ptgs[4].getClass(), PercentPtg.class); + assertEquals(ptgs[6].getClass(), PercentPtg.class); + + // percent of a text quantity + ptgs = parseFormula("\"8.75\"%"); + assertEquals(2, ptgs.length); + assertEquals(ptgs[0].getClass(), StringPtg.class); + assertEquals(ptgs[1].getClass(), PercentPtg.class); + + // percent to the power of + ptgs = parseFormula("50%^3"); + assertEquals(4, ptgs.length); + assertEquals(ptgs[0].getClass(), IntPtg.class); + assertEquals(ptgs[1].getClass(), PercentPtg.class); + assertEquals(ptgs[2].getClass(), IntPtg.class); + assertEquals(ptgs[3].getClass(), PowerPtg.class); + + // + // things that parse OK but would *evaluate* to an error + + ptgs = parseFormula("\"abc\"%"); + assertEquals(2, ptgs.length); + assertEquals(ptgs[0].getClass(), StringPtg.class); + assertEquals(ptgs[1].getClass(), PercentPtg.class); + + ptgs = parseFormula("#N/A%"); + assertEquals(2, ptgs.length); + assertEquals(ptgs[0].getClass(), ErrPtg.class); + assertEquals(ptgs[1].getClass(), PercentPtg.class); + } + + /** + * Tests combinations of various operators in the absence of brackets + */ + public void testPrecedenceAndAssociativity() { + + Class[] expClss; + + // TRUE=TRUE=2=2 evaluates to FALSE + expClss = new Class[] { BoolPtg.class, BoolPtg.class, EqualPtg.class, + IntPtg.class, EqualPtg.class, IntPtg.class, EqualPtg.class, }; + confirmTokenClasses("TRUE=TRUE=2=2", expClss); + + + // 2^3^2 evaluates to 64 not 512 + expClss = new Class[] { IntPtg.class, IntPtg.class, PowerPtg.class, + IntPtg.class, PowerPtg.class, }; + confirmTokenClasses("2^3^2", expClss); + + // "abc" & 2 + 3 & "def" evaluates to "abc5def" + expClss = new Class[] { StringPtg.class, IntPtg.class, IntPtg.class, + AddPtg.class, ConcatPtg.class, StringPtg.class, ConcatPtg.class, }; + confirmTokenClasses("\"abc\"&2+3&\"def\"", expClss); + + + // (1 / 2) - (3 * 4) + expClss = new Class[] { IntPtg.class, IntPtg.class, DividePtg.class, + IntPtg.class, IntPtg.class, MultiplyPtg.class, SubtractPtg.class, }; + confirmTokenClasses("1/2-3*4", expClss); + + // 2 * (2^2) + expClss = new Class[] { IntPtg.class, IntPtg.class, IntPtg.class, PowerPtg.class, MultiplyPtg.class, }; + // NOT: (2 *2) ^ 2 -> int int multiply int power + confirmTokenClasses("2*2^2", expClss); + + // 2^200% -> 2 not 1.6E58 + expClss = new Class[] { IntPtg.class, IntPtg.class, PercentPtg.class, PowerPtg.class, }; + confirmTokenClasses("2^200%", expClss); + } + + private static void confirmTokenClasses(String formula, Class[] expectedClasses) { + Ptg[] ptgs = parseFormula(formula); + assertEquals(expectedClasses.length, ptgs.length); + for (int i = 0; i < expectedClasses.length; i++) { + if(expectedClasses[i] != ptgs[i].getClass()) { + fail("difference at token[" + i + "]: expected (" + + expectedClasses[i].getName() + ") but got (" + + ptgs[i].getClass().getName() + ")"); + } + } + } + + public void testPower() { + confirmTokenClasses("2^5", new Class[] { IntPtg.class, IntPtg.class, PowerPtg.class, }); + } + + private static Ptg parseSingleToken(String formula, Class ptgClass) { + Ptg[] ptgs = parseFormula(formula); + assertEquals(1, ptgs.length); + Ptg result = ptgs[0]; + assertEquals(ptgClass, result.getClass()); + return result; + } + + public void testParseNumber() { + IntPtg ip; + + // bug 33160 + ip = (IntPtg) parseSingleToken("40", IntPtg.class); + assertEquals(40, ip.getValue()); + ip = (IntPtg) parseSingleToken("40000", IntPtg.class); + assertEquals(40000, ip.getValue()); + + // check the upper edge of the IntPtg range: + ip = (IntPtg) parseSingleToken("65535", IntPtg.class); + assertEquals(65535, ip.getValue()); + NumberPtg np = (NumberPtg) parseSingleToken("65536", NumberPtg.class); + assertEquals(65536, np.getValue(), 0); + + np = (NumberPtg) parseSingleToken("65534.6", NumberPtg.class); + assertEquals(65534.6, np.getValue(), 0); + } + + public void testMissingArgs() { + + Class[] expClss; + + expClss = new Class[] { ReferencePtg.class, MissingArgPtg.class, ReferencePtg.class, + FuncVarPtg.class, }; + confirmTokenClasses("if(A1, ,C1)", expClss); + + expClss = new Class[] { MissingArgPtg.class, AreaPtg.class, MissingArgPtg.class, + FuncVarPtg.class, }; + confirmTokenClasses("counta( , A1:B2, )", expClss); + } + + public void testParseErrorLiterals() { + + confirmParseErrorLiteral(ErrPtg.NULL_INTERSECTION, "#NULL!"); + confirmParseErrorLiteral(ErrPtg.DIV_ZERO, "#DIV/0!"); + confirmParseErrorLiteral(ErrPtg.VALUE_INVALID, "#VALUE!"); + confirmParseErrorLiteral(ErrPtg.REF_INVALID, "#REF!"); + confirmParseErrorLiteral(ErrPtg.NAME_INVALID, "#NAME?"); + confirmParseErrorLiteral(ErrPtg.NUM_ERROR, "#NUM!"); + confirmParseErrorLiteral(ErrPtg.N_A, "#N/A"); + } + + private static void confirmParseErrorLiteral(ErrPtg expectedToken, String formula) { + assertEquals(expectedToken, parseSingleToken(formula, ErrPtg.class)); + } + + /** + * To aid readability the parameters have been encoded with single quotes instead of double + * quotes. This method converts single quotes to double quotes before performing the parse + * and result check. + */ + private static void confirmStringParse(String singleQuotedValue) { + // formula: internal quotes become double double, surround with double quotes + String formula = '"' + singleQuotedValue.replaceAll("'", "\"\"") + '"'; + String expectedValue = singleQuotedValue.replace('\'', '"'); + + StringPtg sp = (StringPtg) parseSingleToken(formula, StringPtg.class); + assertEquals(expectedValue, sp.getValue()); + } + + public void testPaseStringLiterals() { + confirmStringParse("goto considered harmful"); + + confirmStringParse("goto 'considered' harmful"); + + confirmStringParse(""); + confirmStringParse("'"); + confirmStringParse("''"); + confirmStringParse("' '"); + confirmStringParse(" ' "); + } + + public void testParseSumIfSum() { + String formulaString; + Ptg[] ptgs; + ptgs = parseFormula("sum(5, 2, if(3>2, sum(A1:A2), 6))"); + formulaString = FormulaParser.toFormulaString(null, ptgs); + assertEquals("SUM(5,2,IF(3>2,SUM(A1:A2),6))", formulaString); + + ptgs = parseFormula("if(1<2,sum(5, 2, if(3>2, sum(A1:A2), 6)),4)"); + formulaString = FormulaParser.toFormulaString(null, ptgs); + assertEquals("IF(1<2,SUM(5,2,IF(3>2,SUM(A1:A2),6)),4)", formulaString); + } + public void testParserErrors() { + parseExpectedException("1 2"); + parseExpectedException(" 12 . 345 "); + parseExpectedException("1 .23 "); + + parseExpectedException("sum(#NAME)"); + parseExpectedException("1 + #N / A * 2"); + parseExpectedException("#value?"); + parseExpectedException("#DIV/ 0+2"); + + + if (false) { // TODO - add functionality to detect func arg count mismatch + parseExpectedException("IF(TRUE)"); + parseExpectedException("countif(A1:B5, C1, D1)"); + } + } + + private static void parseExpectedException(String formula) { + try { + parseFormula(formula); + throw new AssertionFailedError("expected parse exception"); + } catch (FormulaParseException e) { + // expected during successful test + assertNotNull(e.getMessage()); + } catch (RuntimeException e) { + e.printStackTrace(); + fail("Wrong exception:" + e.getMessage()); + } + } + + public void testSetFormulaWithRowBeyond32768_Bug44539() { + + HSSFWorkbook wb = new HSSFWorkbook(); + HSSFSheet sheet = wb.createSheet(); + wb.setSheetName(0, "Sheet1"); + + HSSFRow row = sheet.createRow(0); + HSSFCell cell = row.createCell((short)0); + cell.setCellFormula("SUM(A32769:A32770)"); + if("SUM(A-32767:A-32766)".equals(cell.getCellFormula())) { + fail("Identified bug 44539"); + } + assertEquals("SUM(A32769:A32770)", cell.getCellFormula()); + } } diff --git a/src/testcases/org/apache/poi/hssf/record/AllRecordTests.java b/src/testcases/org/apache/poi/hssf/record/AllRecordTests.java new file mode 100755 index 0000000000..b1acfeafa1 --- /dev/null +++ b/src/testcases/org/apache/poi/hssf/record/AllRecordTests.java @@ -0,0 +1,104 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + + +package org.apache.poi.hssf.record; + +import org.apache.poi.hssf.record.formula.AllFormulaTests; + +import junit.framework.Test; +import junit.framework.TestSuite; + +/** + * Collects all tests for package org.apache.poi.hssf.record. + * + * @author Josh Micich + */ +public class AllRecordTests { + + public static Test suite() { + TestSuite result = new TestSuite("Tests for org.apache.poi.hssf.record"); + + result.addTest(AllFormulaTests.suite()); + + result.addTestSuite(TestAreaFormatRecord.class); + result.addTestSuite(TestAreaRecord.class); + result.addTestSuite(TestAxisLineFormatRecord.class); + result.addTestSuite(TestAxisOptionsRecord.class); + result.addTestSuite(TestAxisParentRecord.class); + result.addTestSuite(TestAxisRecord.class); + result.addTestSuite(TestAxisUsedRecord.class); + result.addTestSuite(TestBOFRecord.class); + result.addTestSuite(TestBarRecord.class); + result.addTestSuite(TestBoundSheetRecord.class); + result.addTestSuite(TestCategorySeriesAxisRecord.class); + result.addTestSuite(TestChartRecord.class); + result.addTestSuite(TestChartTitleFormatRecord.class); + result.addTestSuite(TestCommonObjectDataSubRecord.class); + result.addTestSuite(TestDatRecord.class); + result.addTestSuite(TestDataFormatRecord.class); + result.addTestSuite(TestDefaultDataLabelTextPropertiesRecord.class); + result.addTestSuite(TestDrawingGroupRecord.class); + result.addTestSuite(TestEmbeddedObjectRefSubRecord.class); + result.addTestSuite(TestEndSubRecord.class); + result.addTestSuite(TestEscherAggregate.class); + result.addTestSuite(TestFontBasisRecord.class); + result.addTestSuite(TestFontIndexRecord.class); + result.addTestSuite(TestFormulaRecord.class); + result.addTestSuite(TestFrameRecord.class); + result.addTestSuite(TestHyperlinkRecord.class); + result.addTestSuite(TestLegendRecord.class); + result.addTestSuite(TestLineFormatRecord.class); + result.addTestSuite(TestLinkedDataRecord.class); + result.addTestSuite(TestMergeCellsRecord.class); + result.addTestSuite(TestNameRecord.class); + result.addTestSuite(TestNoteRecord.class); + result.addTestSuite(TestNoteStructureSubRecord.class); + result.addTestSuite(TestNumberFormatIndexRecord.class); + result.addTestSuite(TestObjRecord.class); + result.addTestSuite(TestObjectLinkRecord.class); + result.addTestSuite(TestPaletteRecord.class); + result.addTestSuite(TestPaneRecord.class); + result.addTestSuite(TestPlotAreaRecord.class); + result.addTestSuite(TestPlotGrowthRecord.class); + result.addTestSuite(TestRecordFactory.class); + result.addTestSuite(TestSCLRecord.class); + result.addTestSuite(TestSSTDeserializer.class); + result.addTestSuite(TestSSTRecord.class); + result.addTestSuite(TestSSTRecordSizeCalculator.class); + result.addTestSuite(TestSeriesChartGroupIndexRecord.class); + result.addTestSuite(TestSeriesIndexRecord.class); + result.addTestSuite(TestSeriesLabelsRecord.class); + result.addTestSuite(TestSeriesListRecord.class); + result.addTestSuite(TestSeriesRecord.class); + result.addTestSuite(TestSeriesTextRecord.class); + result.addTestSuite(TestSeriesToChartGroupRecord.class); + result.addTestSuite(TestSheetPropertiesRecord.class); + result.addTestSuite(TestStringRecord.class); + result.addTestSuite(TestSubRecord.class); + result.addTestSuite(TestSupBookRecord.class); + result.addTestSuite(TestTextObjectBaseRecord.class); + result.addTestSuite(TestTextObjectRecord.class); + result.addTestSuite(TestTextRecord.class); + result.addTestSuite(TestTickRecord.class); + result.addTestSuite(TestUnicodeNameRecord.class); + result.addTestSuite(TestUnicodeString.class); + result.addTestSuite(TestUnitsRecord.class); + result.addTestSuite(TestValueRangeRecord.class); + return result; + } +} diff --git a/src/testcases/org/apache/poi/hssf/record/TestDVALRecord.java b/src/testcases/org/apache/poi/hssf/record/TestDVALRecord.java new file mode 100755 index 0000000000..bcfb76645d --- /dev/null +++ b/src/testcases/org/apache/poi/hssf/record/TestDVALRecord.java @@ -0,0 +1,55 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +package org.apache.poi.hssf.record; + +import java.io.ByteArrayInputStream; + +import org.apache.poi.util.LittleEndian; + +import junit.framework.TestCase; + +/** + * + * @author Josh Micich + */ +public final class TestDVALRecord extends TestCase { + public void testRead() { + + byte[] data = new byte[22]; + LittleEndian.putShort(data, 0, DVALRecord.sid); + LittleEndian.putShort(data, 2, (short)18); + LittleEndian.putShort(data, 4, (short)55); + LittleEndian.putInt(data, 6, 56); + LittleEndian.putInt(data, 10, 57); + LittleEndian.putInt(data, 14, 58); + LittleEndian.putInt(data, 18, 59); + + RecordInputStream in = new RecordInputStream(new ByteArrayInputStream(data)); + in.nextRecord(); + DVALRecord dv = new DVALRecord(in); + + assertEquals(55, dv.getOptions()); + assertEquals(56, dv.getHorizontalPos()); + assertEquals(57, dv.getVerticalPos()); + assertEquals(58, dv.getObjectID()); + if(dv.getDVRecNo() == 0) { + fail("Identified bug 44510"); + } + assertEquals(59, dv.getDVRecNo()); + } +} diff --git a/src/testcases/org/apache/poi/hssf/record/TestSupBookRecord.java b/src/testcases/org/apache/poi/hssf/record/TestSupBookRecord.java index fca5bae545..3727989efb 100644 --- a/src/testcases/org/apache/poi/hssf/record/TestSupBookRecord.java +++ b/src/testcases/org/apache/poi/hssf/record/TestSupBookRecord.java @@ -29,15 +29,25 @@ import junit.framework.TestCase; * * @author Andrew C. Oliver (acoliver at apache dot org) */ -public class TestSupBookRecord - extends TestCase -{ +public final class TestSupBookRecord extends TestCase { /** * This contains a fake data section of a SubBookRecord */ - byte[] data = new byte[] { - (byte)0x04,(byte)0x00,(byte)0x01,(byte)0x04 + byte[] dataIR = new byte[] { + (byte)0x04,(byte)0x00,(byte)0x01,(byte)0x04, }; + byte[] dataAIF = new byte[] { + (byte)0x01,(byte)0x00,(byte)0x01,(byte)0x3A, + }; + byte[] dataER = new byte[] { + (byte)0x02,(byte)0x00, + (byte)0x07,(byte)0x00, (byte)0x00, + (byte)'t', (byte)'e', (byte)'s', (byte)'t', (byte)'U', (byte)'R', (byte)'L', + (byte)0x06,(byte)0x00, (byte)0x00, + (byte)'S', (byte)'h', (byte)'e', (byte)'e', (byte)'t', (byte)'1', + (byte)0x06,(byte)0x00, (byte)0x00, + (byte)'S', (byte)'h', (byte)'e', (byte)'e', (byte)'t', (byte)'2', + }; public TestSupBookRecord(String name) { @@ -47,36 +57,67 @@ public class TestSupBookRecord /** * tests that we can load the record */ - public void testLoad() - throws Exception - { + public void testLoadIR() { - SupBookRecord record = new SupBookRecord(new TestcaseRecordInputStream((short)0x01AE, (short)data.length, data)); - assertEquals( 0x401, record.getFlag()); //expected flag + SupBookRecord record = new SupBookRecord(new TestcaseRecordInputStream((short)0x01AE, dataIR)); + assertTrue( record.isInternalReferences() ); //expected flag assertEquals( 0x4, record.getNumberOfSheets() ); //expected # of sheets assertEquals( 8, record.getRecordSize() ); //sid+size+data record.validateSid((short)0x01AE); } - + /** + * tests that we can load the record + */ + public void testLoadER() { + + SupBookRecord record = new SupBookRecord(new TestcaseRecordInputStream((short)0x01AE, dataER)); + assertTrue( record.isExternalReferences() ); //expected flag + assertEquals( 0x2, record.getNumberOfSheets() ); //expected # of sheets + + assertEquals( 34, record.getRecordSize() ); //sid+size+data + + assertEquals("testURL", record.getURL().getString()); + UnicodeString[] sheetNames = record.getSheetNames(); + assertEquals(2, sheetNames.length); + assertEquals("Sheet1", sheetNames[0].getString()); + assertEquals("Sheet2", sheetNames[1].getString()); + + record.validateSid((short)0x01AE); + } + /** + * tests that we can load the record + */ + public void testLoadAIF() { + + SupBookRecord record = new SupBookRecord(new TestcaseRecordInputStream((short)0x01AE, dataAIF)); + assertTrue( record.isAddInFunctions() ); //expected flag + assertEquals( 0x1, record.getNumberOfSheets() ); //expected # of sheets + assertEquals( 8, record.getRecordSize() ); //sid+size+data + record.validateSid((short)0x01AE); + } + /** * Tests that we can store the record * */ - public void testStore() - { - SupBookRecord record = new SupBookRecord(); - record.setFlag( (short) 0x401 ); - record.setNumberOfSheets( (short)0x4 ); - - + public void testStoreIR() { + SupBookRecord record = SupBookRecord.createInternalReferences((short)4); - byte [] recordBytes = record.serialize(); - assertEquals(recordBytes.length - 4, data.length); - for (int i = 0; i < data.length; i++) - assertEquals("At offset " + i, data[i], recordBytes[i+4]); + TestcaseRecordInputStream.confirmRecordEncoding(0x01AE, dataIR, record.serialize()); + } + + public void testStoreER() { + UnicodeString url = new UnicodeString("testURL"); + UnicodeString[] sheetNames = { + new UnicodeString("Sheet1"), + new UnicodeString("Sheet2"), + }; + SupBookRecord record = SupBookRecord.createExternalReferences(url, sheetNames); + + TestcaseRecordInputStream.confirmRecordEncoding(0x01AE, dataER, record.serialize()); } public static void main(String [] args) { @@ -84,6 +125,4 @@ public class TestSupBookRecord .println("Testing org.apache.poi.hssf.record.SupBookRecord"); junit.textui.TestRunner.run(TestSupBookRecord.class); } - - } diff --git a/src/testcases/org/apache/poi/hssf/record/TestcaseRecordInputStream.java b/src/testcases/org/apache/poi/hssf/record/TestcaseRecordInputStream.java index 767f507e73..ecb55ca82c 100755 --- a/src/testcases/org/apache/poi/hssf/record/TestcaseRecordInputStream.java +++ b/src/testcases/org/apache/poi/hssf/record/TestcaseRecordInputStream.java @@ -1,4 +1,3 @@ - /* ==================================================================== Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with @@ -16,11 +15,12 @@ limitations under the License. ==================================================================== */ - - package org.apache.poi.hssf.record; import java.io.ByteArrayInputStream; + +import junit.framework.Assert; + import org.apache.poi.util.LittleEndian; /** @@ -33,6 +33,14 @@ import org.apache.poi.util.LittleEndian; public class TestcaseRecordInputStream extends RecordInputStream { + /** + * Convenience constructor + */ + public TestcaseRecordInputStream(int sid, byte[] data) + { + super(new ByteArrayInputStream(mergeDataAndSid((short)sid, (short)data.length, data))); + nextRecord(); + } public TestcaseRecordInputStream(short sid, short length, byte[] data) { super(new ByteArrayInputStream(mergeDataAndSid(sid, length, data))); @@ -46,4 +54,18 @@ public class TestcaseRecordInputStream System.arraycopy(data, 0, result, 4, data.length); return result; } + /** + * Confirms data sections are equal + * @param expectedData - just raw data (without sid or size short ints) + * @param actualRecordBytes this includes 4 prefix bytes (sid & size) + */ + public static void confirmRecordEncoding(int expectedSid, byte[] expectedData, byte[] actualRecordBytes) { + int expectedDataSize = expectedData.length; + Assert.assertEquals(actualRecordBytes.length - 4, expectedDataSize); + Assert.assertEquals(expectedSid, LittleEndian.getShort(actualRecordBytes, 0)); + Assert.assertEquals(expectedDataSize, LittleEndian.getShort(actualRecordBytes, 2)); + for (int i = 0; i < expectedDataSize; i++) + Assert.assertEquals("At offset " + i, expectedData[i], actualRecordBytes[i+4]); + + } } diff --git a/src/testcases/org/apache/poi/hssf/record/aggregates/TestValueRecordsAggregate.java b/src/testcases/org/apache/poi/hssf/record/aggregates/TestValueRecordsAggregate.java index a3315c2978..8e8a72ece7 100755 --- a/src/testcases/org/apache/poi/hssf/record/aggregates/TestValueRecordsAggregate.java +++ b/src/testcases/org/apache/poi/hssf/record/aggregates/TestValueRecordsAggregate.java @@ -17,15 +17,30 @@ package org.apache.poi.hssf.record.aggregates; -import junit.framework.TestCase; -import org.apache.poi.hssf.record.*; - +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; import java.util.ArrayList; import java.util.Iterator; import java.util.List; +import java.util.zip.CRC32; + +import junit.framework.AssertionFailedError; +import junit.framework.TestCase; + +import org.apache.poi.hssf.record.BlankRecord; +import org.apache.poi.hssf.record.FormulaRecord; +import org.apache.poi.hssf.record.Record; +import org.apache.poi.hssf.record.SharedFormulaRecord; +import org.apache.poi.hssf.record.UnknownRecord; +import org.apache.poi.hssf.record.WindowOneRecord; +import org.apache.poi.hssf.usermodel.HSSFSheet; +import org.apache.poi.hssf.usermodel.HSSFWorkbook; public class TestValueRecordsAggregate extends TestCase { + private static final String ABNORMAL_SHARED_FORMULA_FLAG_TEST_FILE = "AbnormalSharedFormulaFlag.xls"; ValueRecordsAggregate valueRecord = new ValueRecordsAggregate(); /** @@ -203,4 +218,117 @@ public class TestValueRecordsAggregate extends TestCase assertEquals( 36, valueRecord.getRecordSize() ); } + + /** + * Sometimes the 'shared formula' flag (FormulaRecord.isSharedFormula()) is set when + * there is no corresponding SharedFormulaRecord available. SharedFormulaRecord definitions do + * not span multiple sheets. They are are only defined within a sheet, and thus they do not + * have a sheet index field (only row and column range fields).
    + * So it is important that the code which locates the SharedFormulaRecord for each + * FormulaRecord does not allow matches across sheets.
    + * + * Prior to bugzilla 44449 (Feb 2008), POI ValueRecordsAggregate.construct(int, List) + * allowed SharedFormulaRecords to be erroneously used across sheets. That incorrect + * behaviour is shown by this test.

    + * + * Notes on how to produce the test spreadsheet:

    + * The setup for this test (AbnormalSharedFormulaFlag.xls) is rather fragile, insomuchas + * re-saving the file (either with Excel or POI) clears the flag.
    + *
      + *
    1. A new spreadsheet was created in Excel (File | New | Blank Workbook).
    2. + *
    3. Sheet3 was deleted.
    4. + *
    5. Sheet2!A1 formula was set to '="second formula"', and fill-dragged through A1:A8.
    6. + *
    7. Sheet1!A1 formula was set to '="first formula"', and also fill-dragged through A1:A8.
    8. + *
    9. Four rows on Sheet1 "5" through "8" were deleted ('delete rows' alt-E D, not 'clear' Del).
    10. + *
    11. The spreadsheet was saved as AbnormalSharedFormulaFlag.xls.
    12. + *
    + * Prior to the row delete action the spreadsheet has two SharedFormulaRecords. One + * for each sheet. To expose the bug, the shared formulas have been made to overlap.
    + * The row delete action (as described here) seems to to delete the + * SharedFormulaRecord from Sheet1 (but not clear the 'shared formula' flags.
    + * There are other variations on this theme to create the same effect. + * + */ + public void testSpuriousSharedFormulaFlag() { + File dataDir = new File(System.getProperty("HSSF.testdata.path")); + File testFile = new File(dataDir, ABNORMAL_SHARED_FORMULA_FLAG_TEST_FILE); + + long actualCRC = getFileCRC(testFile); + long expectedCRC = 2277445406L; + if(actualCRC != expectedCRC) { + System.err.println("Expected crc " + expectedCRC + " but got " + actualCRC); + throw failUnexpectedTestFileChange(); + } + HSSFWorkbook wb; + try { + FileInputStream in = new FileInputStream(testFile); + wb = new HSSFWorkbook(in); + } catch (IOException e) { + throw new RuntimeException(e); + } + + HSSFSheet s = wb.getSheetAt(0); // Sheet1 + + String cellFormula; + cellFormula = getFormulaFromFirstCell(s, 0); // row "1" + // the problem is not observable in the first row of the shared formula + if(!cellFormula.equals("\"first formula\"")) { + throw new RuntimeException("Something else wrong with this test case"); + } + + // but the problem is observable in rows 2,3,4 + cellFormula = getFormulaFromFirstCell(s, 1); // row "2" + if(cellFormula.equals("\"second formula\"")) { + throw new AssertionFailedError("found bug 44449 (Wrong SharedFormulaRecord was used)."); + } + if(!cellFormula.equals("\"first formula\"")) { + throw new RuntimeException("Something else wrong with this test case"); + } + } + private static String getFormulaFromFirstCell(HSSFSheet s, int rowIx) { + return s.getRow(rowIx).getCell((short)0).getCellFormula(); + } + + /** + * If someone opened this particular test file in Excel and saved it, the peculiar condition + * which causes the target bug would probably disappear. This test would then just succeed + * regardless of whether the fix was present. So a CRC check is performed to make it less easy + * for that to occur. + */ + private static RuntimeException failUnexpectedTestFileChange() { + String msg = "Test file '" + ABNORMAL_SHARED_FORMULA_FLAG_TEST_FILE + "' has changed. " + + "This junit may not be properly testing for the target bug. " + + "Either revert the test file or ensure that the new version " + + "has the right characteristics to test the target bug."; + // A breakpoint in ValueRecordsAggregate.handleMissingSharedFormulaRecord(FormulaRecord) + // should get hit during parsing of Sheet1. + // If the test spreadsheet is created as directed, this condition should occur. + // It is easy to upset the test spreadsheet (for example re-saving will destroy the + // peculiar condition we are testing for). + throw new RuntimeException(msg); + } + + /** + * gets a CRC checksum for the content of a file + */ + private static long getFileCRC(File f) { + CRC32 crc = new CRC32(); + byte[] buf = new byte[2048]; + try { + InputStream is = new FileInputStream(f); + while(true) { + int bytesRead = is.read(buf); + if(bytesRead < 1) { + break; + } + crc.update(buf, 0, bytesRead); + } + is.close(); + } catch (IOException e) { + throw new RuntimeException(e); + } + + return crc.getValue(); + } + } diff --git a/src/testcases/org/apache/poi/hssf/record/formula/AbstractPtgTestCase.java b/src/testcases/org/apache/poi/hssf/record/formula/AbstractPtgTestCase.java index 21e6a8ba3c..0912b97611 100644 --- a/src/testcases/org/apache/poi/hssf/record/formula/AbstractPtgTestCase.java +++ b/src/testcases/org/apache/poi/hssf/record/formula/AbstractPtgTestCase.java @@ -1,4 +1,3 @@ - /* ==================================================================== Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with @@ -28,6 +27,7 @@ import junit.framework.TestCase; import org.apache.poi.hssf.model.Workbook; import org.apache.poi.hssf.usermodel.HSSFWorkbook; +import org.apache.poi.poifs.filesystem.POIFSFileSystem; /** * Convenient abstract class to reduce the amount of boilerplate code needed @@ -35,8 +35,7 @@ import org.apache.poi.hssf.usermodel.HSSFWorkbook; * * @author Daniel Noll (daniel at nuix dot com dot au) */ -public abstract class AbstractPtgTestCase extends TestCase -{ +public abstract class AbstractPtgTestCase extends TestCase { /** Directory containing the test data. */ private static String dataDir = System.getProperty("HSSF.testdata.path"); @@ -51,16 +50,16 @@ public abstract class AbstractPtgTestCase extends TestCase throws IOException { File file = new File(dataDir, filename); InputStream stream = new BufferedInputStream(new FileInputStream(file)); - try - { - return new HSSFWorkbook(stream); - } - finally - { + // TODO - temp workaround to keep stdout quiet due to warning msg in POIFS + // When that warning msg is disabled, remove this wrapper and the close() call, + InputStream wrappedStream = POIFSFileSystem.createNonClosingInputStream(stream); + try { + return new HSSFWorkbook(wrappedStream); + } finally { stream.close(); } } - + /** * Creates a new Workbook and adds one sheet with the specified name */ @@ -73,5 +72,4 @@ public abstract class AbstractPtgTestCase extends TestCase book.setSheetName(0, sheetName); return book; } - } diff --git a/src/testcases/org/apache/poi/hssf/record/formula/AllFormulaTests.java b/src/testcases/org/apache/poi/hssf/record/formula/AllFormulaTests.java index b126813387..92ca4ba044 100644 --- a/src/testcases/org/apache/poi/hssf/record/formula/AllFormulaTests.java +++ b/src/testcases/org/apache/poi/hssf/record/formula/AllFormulaTests.java @@ -14,7 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. ==================================================================== */ - package org.apache.poi.hssf.record.formula; @@ -33,7 +32,8 @@ public class AllFormulaTests { result.addTestSuite(TestArea3DPtg.class); result.addTestSuite(TestAreaErrPtg.class); result.addTestSuite(TestAreaPtg.class); - result.addTestSuite(TestErrPtg.class); + result.addTestSuite(TestErrPtg.class); + result.addTestSuite(TestExternalFunctionFormulas.class); result.addTestSuite(TestFuncPtg.class); result.addTestSuite(TestIntersectionPtg.class); result.addTestSuite(TestPercentPtg.class); diff --git a/src/testcases/org/apache/poi/hssf/record/formula/TestAreaPtg.java b/src/testcases/org/apache/poi/hssf/record/formula/TestAreaPtg.java index 522a5bcf28..3a7f2f29a4 100644 --- a/src/testcases/org/apache/poi/hssf/record/formula/TestAreaPtg.java +++ b/src/testcases/org/apache/poi/hssf/record/formula/TestAreaPtg.java @@ -27,15 +27,12 @@ import org.apache.poi.hssf.model.FormulaParser; * * @author Dmitriy Kumshayev */ -public class TestAreaPtg extends TestCase -{ +public final class TestAreaPtg extends TestCase { AreaPtg relative; AreaPtg absolute; - protected void setUp() throws Exception - { - super.setUp(); + protected void setUp() { short firstRow=5; short lastRow=13; short firstCol=7; @@ -64,10 +61,9 @@ public class TestAreaPtg extends TestCase } - public void resetColumns(AreaPtg aptg) - { - short fc = aptg.getFirstColumn(); - short lc = aptg.getLastColumn(); + private static void resetColumns(AreaPtg aptg) { + int fc = aptg.getFirstColumn(); + int lc = aptg.getLastColumn(); aptg.setFirstColumn(fc); aptg.setLastColumn(lc); assertEquals(fc , aptg.getFirstColumn() ); diff --git a/src/testcases/org/apache/poi/hssf/record/formula/TestExternalFunctionFormulas.java b/src/testcases/org/apache/poi/hssf/record/formula/TestExternalFunctionFormulas.java new file mode 100755 index 0000000000..8c89dadea7 --- /dev/null +++ b/src/testcases/org/apache/poi/hssf/record/formula/TestExternalFunctionFormulas.java @@ -0,0 +1,56 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +package org.apache.poi.hssf.record.formula; + +import java.io.FileInputStream; +import java.io.IOException; + +import junit.framework.TestCase; + +import org.apache.poi.hssf.usermodel.HSSFSheet; +import org.apache.poi.hssf.usermodel.HSSFWorkbook; +/** + * Tests for functions from external workbooks (e.g. YEARFRAC). + * + * + * @author Josh Micich + */ +public final class TestExternalFunctionFormulas extends TestCase { + + + /** + * tests NameXPtg.toFormulaString(Workbook) and logic in Workbook below that + */ + public void testReadFormulaContainingExternalFunction() { + String filePath = System.getProperty("HSSF.testdata.path")+ "/" + + "externalFunctionExample.xls"; + HSSFWorkbook wb; + try { + FileInputStream fin = new FileInputStream(filePath); + wb = new HSSFWorkbook( fin ); + } catch (IOException e) { + throw new RuntimeException(e); + } + + String expectedFormula = "YEARFRAC(B1,C1)"; + HSSFSheet sht = wb.getSheetAt(0); + String cellFormula = sht.getRow(0).getCell((short)0).getCellFormula(); + assertEquals(expectedFormula, cellFormula); + } + +} diff --git a/src/testcases/org/apache/poi/hssf/usermodel/AllUserModelTests.java b/src/testcases/org/apache/poi/hssf/usermodel/AllUserModelTests.java new file mode 100755 index 0000000000..cbc555da36 --- /dev/null +++ b/src/testcases/org/apache/poi/hssf/usermodel/AllUserModelTests.java @@ -0,0 +1,71 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +package org.apache.poi.hssf.usermodel; + +import junit.framework.Test; +import junit.framework.TestSuite; + +/** + * Collects all tests for the org.apache.poi.hssf.usermodel package. + * + * @author Josh Micich + */ +public class AllUserModelTests { + + public static Test suite() { + TestSuite result = new TestSuite("Tests for org.apache.poi.hssf.usermodel"); + + result.addTestSuite(TestBugs.class); + result.addTestSuite(TestCellStyle.class); + result.addTestSuite(TestCloneSheet.class); + result.addTestSuite(TestDataValidation.class); + result.addTestSuite(TestEscherGraphics.class); + result.addTestSuite(TestEscherGraphics2d.class); + result.addTestSuite(TestFontDetails.class); + result.addTestSuite(TestFormulas.class); + result.addTestSuite(TestHSSFCell.class); + result.addTestSuite(TestHSSFClientAnchor.class); + result.addTestSuite(TestHSSFComment.class); + result.addTestSuite(TestHSSFDateUtil.class); + result.addTestSuite(TestHSSFHeaderFooter.class); + result.addTestSuite(TestHSSFHyperlink.class); + result.addTestSuite(TestHSSFPalette.class); + result.addTestSuite(TestHSSFPicture.class); + result.addTestSuite(TestHSSFPictureData.class); + result.addTestSuite(TestHSSFRichTextString.class); + result.addTestSuite(TestHSSFRow.class); + result.addTestSuite(TestHSSFSheet.class); + result.addTestSuite(TestHSSFSheetOrder.class); + result.addTestSuite(TestHSSFSheetSetOrder.class); + result.addTestSuite(TestHSSFWorkbook.class); + result.addTestSuite(TestNamedRange.class); + result.addTestSuite(TestOLE2Embeding.class); + result.addTestSuite(TestReadWriteChart.class); + result.addTestSuite(TestSanityChecker.class); + result.addTestSuite(TestSheetHiding.class); + result.addTestSuite(TestSheetShiftRows.class); + if (false) { // deliberately avoiding this one + result.addTestSuite(TestUnfixedBugs.class); + } + result.addTestSuite(TestUnicodeWorkbook.class); + result.addTestSuite(TestUppercaseWorkbook.class); + result.addTestSuite(TestWorkbook.class); + + return result; + } +} diff --git a/src/testcases/org/apache/poi/hssf/usermodel/TestBugs.java b/src/testcases/org/apache/poi/hssf/usermodel/TestBugs.java index 6dfdddad44..f9bb362c7b 100644 --- a/src/testcases/org/apache/poi/hssf/usermodel/TestBugs.java +++ b/src/testcases/org/apache/poi/hssf/usermodel/TestBugs.java @@ -1089,6 +1089,44 @@ extends TestCase { // "EmptyStackException" //assertEquals("=CHOOSE(2,A2,A3,A4)", c2.getCellFormula()); } + + /** + * Crystal reports generates files with short + * StyleRecords, which is against the spec + */ + public void test44471() throws Exception { + FileInputStream in = new FileInputStream(new File(cwd, "OddStyleRecord.xls")); + + // Used to blow up with an ArrayIndexOutOfBounds + // when creating a StyleRecord + HSSFWorkbook wb = new HSSFWorkbook(in); + in.close(); + + assertEquals(1, wb.getNumberOfSheets()); + } + + /** + * Files with "read only recommended" were giving + * grief on the FileSharingRecord + */ + public void test44536() throws Exception { + FileInputStream in = new FileInputStream(new File(cwd, "ReadOnlyRecommended.xls")); + + // Used to blow up with an IllegalArgumentException + // when creating a FileSharingRecord + HSSFWorkbook wb = new HSSFWorkbook(in); + in.close(); + + // Check read only advised + assertEquals(3, wb.getNumberOfSheets()); + assertTrue(wb.isWriteProtected()); + + // But also check that another wb isn't + in = new FileInputStream(new File(cwd, "SimpleWithChoose.xls")); + wb = new HSSFWorkbook(in); + in.close(); + assertFalse(wb.isWriteProtected()); + } } diff --git a/src/testcases/org/apache/poi/hssf/usermodel/TestDataValidation.java b/src/testcases/org/apache/poi/hssf/usermodel/TestDataValidation.java index f970ff26f1..34885e7a21 100644 --- a/src/testcases/org/apache/poi/hssf/usermodel/TestDataValidation.java +++ b/src/testcases/org/apache/poi/hssf/usermodel/TestDataValidation.java @@ -51,7 +51,7 @@ public class TestDataValidation extends TestCase public void testDataValidation() throws Exception { System.out.println("\nTest no. 2 - Test Excel's Data validation mechanism"); - String resultFile = System.getProperty("HSSF.testdata.path")+"/TestDataValidation.xls"; + String resultFile = System.getProperty("java.io.tmpdir")+File.separator+"TestDataValidation.xls"; HSSFWorkbook wb = new HSSFWorkbook(); HSSFCellStyle style_1 = this.createStyle( wb, HSSFCellStyle.ALIGN_LEFT ); diff --git a/src/testcases/org/apache/poi/hssf/usermodel/TestFormulas.java b/src/testcases/org/apache/poi/hssf/usermodel/TestFormulas.java index f1e8f49777..9eb12bd4e1 100644 --- a/src/testcases/org/apache/poi/hssf/usermodel/TestFormulas.java +++ b/src/testcases/org/apache/poi/hssf/usermodel/TestFormulas.java @@ -302,10 +302,10 @@ extends TestCase { } c = r.getCell((short) y); - CellReference cr= new CellReference(refx1,refy1); - ref=cr.toString(); - cr=new CellReference(refx2,refy2); - ref2=cr.toString(); + CellReference cr= new CellReference(refx1,refy1, false, false); + ref=cr.formatAsString(); + cr=new CellReference(refx2,refy2, false, false); + ref2=cr.formatAsString(); c = r.createCell((short) y); c.setCellFormula("" + ref + operator + ref2); @@ -379,10 +379,10 @@ extends TestCase { } c = r.getCell((short) y); - CellReference cr= new CellReference(refx1,refy1); - ref=cr.toString(); - cr=new CellReference(refx2,refy2); - ref2=cr.toString(); + CellReference cr= new CellReference(refx1, refy1, false, false); + ref=cr.formatAsString(); + cr=new CellReference(refx2,refy2, false, false); + ref2=cr.formatAsString(); assertTrue("loop Formula is as expected "+ref+operator+ref2+"!="+c.getCellFormula(),( diff --git a/src/testcases/org/apache/poi/hssf/usermodel/TestHSSFPalette.java b/src/testcases/org/apache/poi/hssf/usermodel/TestHSSFPalette.java index c5674b9e76..a0f09696b5 100644 --- a/src/testcases/org/apache/poi/hssf/usermodel/TestHSSFPalette.java +++ b/src/testcases/org/apache/poi/hssf/usermodel/TestHSSFPalette.java @@ -160,6 +160,49 @@ public class TestHSSFPalette extends TestCase assertEquals("FFFF:0:FFFF", p.getColor((short)14).getHexString()); } + public void testFindSimilar() throws Exception { + HSSFWorkbook book = new HSSFWorkbook(); + HSSFPalette p = book.getCustomPalette(); + + + // Add a few edge colours in + p.setColorAtIndex((short)8, (byte)-1, (byte)0, (byte)0); + p.setColorAtIndex((short)9, (byte)0, (byte)-1, (byte)0); + p.setColorAtIndex((short)10, (byte)0, (byte)0, (byte)-1); + + // And some near a few of them + p.setColorAtIndex((short)11, (byte)-1, (byte)2, (byte)2); + p.setColorAtIndex((short)12, (byte)-2, (byte)2, (byte)10); + p.setColorAtIndex((short)13, (byte)-4, (byte)0, (byte)0); + p.setColorAtIndex((short)14, (byte)-8, (byte)0, (byte)0); + + assertEquals( + "FFFF:0:0", p.getColor((short)8).getHexString() + ); + + // Now check we get the right stuff back + assertEquals( + p.getColor((short)8).getHexString(), + p.findSimilarColor((byte)-1, (byte)0, (byte)0).getHexString() + ); + assertEquals( + p.getColor((short)8).getHexString(), + p.findSimilarColor((byte)-2, (byte)0, (byte)0).getHexString() + ); + assertEquals( + p.getColor((short)8).getHexString(), + p.findSimilarColor((byte)-1, (byte)1, (byte)0).getHexString() + ); + assertEquals( + p.getColor((short)11).getHexString(), + p.findSimilarColor((byte)-1, (byte)2, (byte)1).getHexString() + ); + assertEquals( + p.getColor((short)12).getHexString(), + p.findSimilarColor((byte)-1, (byte)2, (byte)10).getHexString() + ); + } + /** * Verifies that the generated gnumeric-format string values match the * hardcoded values in the HSSFColor default color palette diff --git a/src/testcases/org/apache/poi/hssf/usermodel/TestHSSFPicture.java b/src/testcases/org/apache/poi/hssf/usermodel/TestHSSFPicture.java index 25d6684c47..fd01001f84 100644 --- a/src/testcases/org/apache/poi/hssf/usermodel/TestHSSFPicture.java +++ b/src/testcases/org/apache/poi/hssf/usermodel/TestHSSFPicture.java @@ -16,25 +16,28 @@ */ package org.apache.poi.hssf.usermodel; -import junit.framework.TestCase; - -import java.io.IOException; -import java.io.FileInputStream; import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; + +import junit.framework.TestCase; /** * Test HSSFPicture. * * @author Yegor Kozlov (yegor at apache.org) */ -public class TestHSSFPicture extends TestCase{ +public final class TestHSSFPicture extends TestCase{ - public void testResize() throws Exception { + public void testResize() { HSSFWorkbook wb = new HSSFWorkbook(); HSSFSheet sh1 = wb.createSheet(); HSSFPatriarch p1 = sh1.createDrawingPatriarch(); - int idx1 = loadPicture( "src/resources/logos/logoKarmokar4.png", wb); + byte[] pictureData = getTestDataFileContent("logoKarmokar4.png"); + int idx1 = wb.addPicture( pictureData, HSSFWorkbook.PICTURE_TYPE_PNG ); HSSFPicture picture1 = p1.createPicture(new HSSFClientAnchor(), idx1); HSSFClientAnchor anchor1 = picture1.getPreferredSize(); @@ -52,28 +55,25 @@ public class TestHSSFPicture extends TestCase{ /** * Copied from org.apache.poi.hssf.usermodel.examples.OfficeDrawing */ - private static int loadPicture( String path, HSSFWorkbook wb ) throws IOException - { - int pictureIndex; - FileInputStream fis = null; - ByteArrayOutputStream bos = null; - try - { - fis = new FileInputStream( path); - bos = new ByteArrayOutputStream( ); - int c; - while ( (c = fis.read()) != -1) - bos.write( c ); - pictureIndex = wb.addPicture( bos.toByteArray(), HSSFWorkbook.PICTURE_TYPE_PNG ); - } - finally - { - if (fis != null) - fis.close(); - if (bos != null) - bos.close(); - } - return pictureIndex; - } + private static byte[] getTestDataFileContent(String fileName) { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + + String readFilename = System.getProperty("HSSF.testdata.path"); + try { + InputStream fis = new FileInputStream(readFilename+File.separator+fileName); + byte[] buf = new byte[512]; + while(true) { + int bytesRead = fis.read(buf); + if(bytesRead < 1) { + break; + } + bos.write(buf, 0, bytesRead); + } + fis.close(); + } catch (IOException e) { + throw new RuntimeException(e); + } + return bos.toByteArray(); + } } diff --git a/src/testcases/org/apache/poi/hssf/usermodel/TestHSSFRow.java b/src/testcases/org/apache/poi/hssf/usermodel/TestHSSFRow.java index 44c03cd20d..b6f22022c6 100644 --- a/src/testcases/org/apache/poi/hssf/usermodel/TestHSSFRow.java +++ b/src/testcases/org/apache/poi/hssf/usermodel/TestHSSFRow.java @@ -1,4 +1,3 @@ - /* ==================================================================== Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with @@ -15,34 +14,22 @@ See the License for the specific language governing permissions and limitations under the License. ==================================================================== */ - package org.apache.poi.hssf.usermodel; -import junit.framework.TestCase; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; -import org.apache.poi.util.TempFile; +import junit.framework.TestCase; /** * Test HSSFRow is okay. * * @author Glen Stampoultzis (glens at apache.org) */ -public class TestHSSFRow - extends TestCase -{ - public TestHSSFRow(String s) - { - super(s); - } +public final class TestHSSFRow extends TestCase { - public void testLastAndFirstColumns() - throws Exception - { + public void testLastAndFirstColumns() { HSSFWorkbook workbook = new HSSFWorkbook(); HSSFSheet sheet = workbook.createSheet(); HSSFRow row = sheet.createRow((short) 0); @@ -51,127 +38,146 @@ public class TestHSSFRow row.createCell((short) 2); assertEquals(2, row.getFirstCellNum()); - assertEquals(2, row.getLastCellNum()); + assertEquals(3, row.getLastCellNum()); row.createCell((short) 1); assertEquals(1, row.getFirstCellNum()); - assertEquals(2, row.getLastCellNum()); - + assertEquals(3, row.getLastCellNum()); + // check the exact case reported in 'bug' 43901 - notice that the cellNum is '0' based row.createCell((short) 3); assertEquals(1, row.getFirstCellNum()); - assertEquals(3, row.getLastCellNum()); - + assertEquals(4, row.getLastCellNum()); } - public void testRemoveCell() - throws Exception - { + public void testRemoveCell() throws Exception { HSSFWorkbook workbook = new HSSFWorkbook(); HSSFSheet sheet = workbook.createSheet(); HSSFRow row = sheet.createRow((short) 0); assertEquals(-1, row.getLastCellNum()); assertEquals(-1, row.getFirstCellNum()); row.createCell((short) 1); - assertEquals(1, row.getLastCellNum()); + assertEquals(2, row.getLastCellNum()); assertEquals(1, row.getFirstCellNum()); row.createCell((short) 3); - assertEquals(3, row.getLastCellNum()); + assertEquals(4, row.getLastCellNum()); assertEquals(1, row.getFirstCellNum()); row.removeCell(row.getCell((short) 3)); - assertEquals(1, row.getLastCellNum()); + assertEquals(2, row.getLastCellNum()); assertEquals(1, row.getFirstCellNum()); row.removeCell(row.getCell((short) 1)); assertEquals(-1, row.getLastCellNum()); assertEquals(-1, row.getFirstCellNum()); - // check the row record actually writes it out as 0's + // all cells on this row have been removed + // so check the row record actually writes it out as 0's byte[] data = new byte[100]; row.getRowRecord().serialize(0, data); assertEquals(0, data[6]); assertEquals(0, data[8]); - File file = TempFile.createTempFile("XXX", "XLS"); - FileOutputStream stream = new FileOutputStream(file); - workbook.write(stream); - stream.close(); - FileInputStream inputStream = new FileInputStream(file); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + workbook.write(baos); + baos.close(); + ByteArrayInputStream inputStream = new ByteArrayInputStream(baos.toByteArray()); workbook = new HSSFWorkbook(inputStream); sheet = workbook.getSheetAt(0); - stream.close(); - file.delete(); + inputStream.close(); + assertEquals(-1, sheet.getRow((short) 0).getLastCellNum()); assertEquals(-1, sheet.getRow((short) 0).getFirstCellNum()); } - - public void testMoveCell() throws Exception { + + public void testMoveCell() { HSSFWorkbook workbook = new HSSFWorkbook(); HSSFSheet sheet = workbook.createSheet(); HSSFRow row = sheet.createRow((short) 0); HSSFRow rowB = sheet.createRow((short) 1); - + HSSFCell cellA2 = rowB.createCell((short)0); assertEquals(0, rowB.getFirstCellNum()); assertEquals(0, rowB.getFirstCellNum()); - + assertEquals(-1, row.getLastCellNum()); assertEquals(-1, row.getFirstCellNum()); HSSFCell cellB2 = row.createCell((short) 1); HSSFCell cellB3 = row.createCell((short) 2); HSSFCell cellB4 = row.createCell((short) 3); - + assertEquals(1, row.getFirstCellNum()); - assertEquals(3, row.getLastCellNum()); - + assertEquals(4, row.getLastCellNum()); + // Try to move to somewhere else that's used try { - row.moveCell(cellB2, (short)3); - fail(); - } catch(IllegalArgumentException e) {} - + row.moveCell(cellB2, (short)3); + fail("IllegalArgumentException should have been thrown"); + } catch(IllegalArgumentException e) { + // expected during successful test + } + // Try to move one off a different row try { - row.moveCell(cellA2, (short)3); - fail(); - } catch(IllegalArgumentException e) {} - + row.moveCell(cellA2, (short)3); + fail("IllegalArgumentException should have been thrown"); + } catch(IllegalArgumentException e) { + // expected during successful test + } + // Move somewhere spare assertNotNull(row.getCell((short)1)); - row.moveCell(cellB2, (short)5); + row.moveCell(cellB2, (short)5); assertNull(row.getCell((short)1)); assertNotNull(row.getCell((short)5)); - - assertEquals(5, cellB2.getCellNum()); + + assertEquals(5, cellB2.getCellNum()); assertEquals(2, row.getFirstCellNum()); - assertEquals(5, row.getLastCellNum()); - + assertEquals(6, row.getLastCellNum()); } - - public void testRowBounds() - throws Exception - { + + public void testRowBounds() { HSSFWorkbook workbook = new HSSFWorkbook(); HSSFSheet sheet = workbook.createSheet(); //Test low row bound - HSSFRow row = sheet.createRow( (short) 0); - //Test low row bound exception - boolean caughtException = false; + sheet.createRow( (short) 0); + //Test low row bound exception try { - row = sheet.createRow(-1); + sheet.createRow(-1); + fail("IndexOutOfBoundsException should have been thrown"); } catch (IndexOutOfBoundsException ex) { - caughtException = true; - } - assertTrue(caughtException); - //Test high row bound - row = sheet.createRow(65535); - //Test high row bound exception - caughtException = false; + // expected during successful test + } + + //Test high row bound + sheet.createRow(65535); + //Test high row bound exception try { - row = sheet.createRow(65536); + sheet.createRow(65536); + fail("IndexOutOfBoundsException should have been thrown"); } catch (IndexOutOfBoundsException ex) { - caughtException = true; - } - assertTrue(caughtException); + // expected during successful test + } + } + + /** + * Prior to patch 43901, POI was producing files with the wrong last-column + * number on the row + */ + public void testLastCellNumIsCorrectAfterAddCell_bug43901(){ + HSSFWorkbook book = new HSSFWorkbook(); + HSSFSheet sheet = book.createSheet("test"); + HSSFRow row = sheet.createRow(0); + + // New row has last col -1 + assertEquals(-1, row.getLastCellNum()); + if(row.getLastCellNum() == 0) { + fail("Identified bug 43901"); + } + + // Create two cells, will return one higher + // than that for the last number + row.createCell((short) 0); + assertEquals(1, row.getLastCellNum()); + row.createCell((short) 255); + assertEquals(256, row.getLastCellNum()); } - } diff --git a/src/testcases/org/apache/poi/hssf/usermodel/TestNamedRange.java b/src/testcases/org/apache/poi/hssf/usermodel/TestNamedRange.java index f22de1758c..afbbde026e 100644 --- a/src/testcases/org/apache/poi/hssf/usermodel/TestNamedRange.java +++ b/src/testcases/org/apache/poi/hssf/usermodel/TestNamedRange.java @@ -578,18 +578,16 @@ public class TestNamedRange // retrieve the cell at the named range and test its contents AreaReference aref = new AreaReference(aNamedCell.getReference()); - CellReference[] crefs = aref.getCells(); - assertNotNull(crefs); - assertEquals("Should be exactly 1 cell in the named cell :'" +cellName+"'", 1, crefs.length); - for (int i=0, iSize=crefs.length; i + * + * @author Josh Micich + */ +public final class AllPOIFSFileSystemTests { + + public static Test suite() { + TestSuite result = new TestSuite("Tests for org.apache.poi.poifs.filesystem"); + result.addTestSuite(TestDirectoryNode.class); + result.addTestSuite(TestDocument.class); + result.addTestSuite(TestDocumentDescriptor.class); + result.addTestSuite(TestDocumentInputStream.class); + result.addTestSuite(TestDocumentNode.class); + result.addTestSuite(TestDocumentOutputStream.class); + result.addTestSuite(TestEmptyDocument.class); + result.addTestSuite(TestOffice2007XMLException.class); + result.addTestSuite(TestPOIFSDocumentPath.class); + result.addTestSuite(TestPropertySorter.class); + return result; + } +} diff --git a/src/testcases/org/apache/poi/poifs/filesystem/TestEmptyDocument.java b/src/testcases/org/apache/poi/poifs/filesystem/TestEmptyDocument.java index 4f67f98767..870d752522 100644 --- a/src/testcases/org/apache/poi/poifs/filesystem/TestEmptyDocument.java +++ b/src/testcases/org/apache/poi/poifs/filesystem/TestEmptyDocument.java @@ -20,6 +20,7 @@ package org.apache.poi.poifs.filesystem; import java.io.IOException; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; +import java.util.Arrays; import junit.framework.TestCase; @@ -157,12 +158,15 @@ public class TestEmptyDocument extends TestCase { DocumentEntry entry = (DocumentEntry) fs.getRoot().getEntry("Empty"); assertEquals("Expected zero size", 0, entry.getSize()); + byte[] actualReadbackData; + actualReadbackData = IOUtils.toByteArray(new DocumentInputStream(entry)); assertEquals("Expected zero read from stream", 0, - IOUtils.toByteArray(new DocumentInputStream(entry)).length); + actualReadbackData.length); entry = (DocumentEntry) fs.getRoot().getEntry("NotEmpty"); + actualReadbackData = IOUtils.toByteArray(new DocumentInputStream(entry)); assertEquals("Expected size was wrong", testData.length, entry.getSize()); - assertEquals("Expected different data read from stream", testData, - IOUtils.toByteArray(new DocumentInputStream(entry))); + assertTrue("Expected different data read from stream", + Arrays.equals(testData, actualReadbackData)); } } diff --git a/src/testcases/org/apache/poi/poifs/filesystem/TestPOIFSFileSystem.java b/src/testcases/org/apache/poi/poifs/filesystem/TestPOIFSFileSystem.java new file mode 100755 index 0000000000..dbc5401beb --- /dev/null +++ b/src/testcases/org/apache/poi/poifs/filesystem/TestPOIFSFileSystem.java @@ -0,0 +1,136 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +package org.apache.poi.poifs.filesystem; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; + +import junit.framework.TestCase; + +/** + * Tests for POIFSFileSystem + * + * @author Josh Micich + */ +public final class TestPOIFSFileSystem extends TestCase { + + public TestPOIFSFileSystem(String testName) { + super(testName); + } + + /** + * Mock exception used to ensure correct error handling + */ + private static final class MyEx extends RuntimeException { + public MyEx() { + // no fields to initialise + } + } + /** + * Helps facilitate testing. Keeps track of whether close() was called. + * Also can throw an exception at a specific point in the stream. + */ + private static final class TestIS extends InputStream { + + private final InputStream _is; + private final int _failIndex; + private int _currentIx; + private boolean _isClosed; + + public TestIS(InputStream is, int failIndex) { + _is = is; + _failIndex = failIndex; + _currentIx = 0; + _isClosed = false; + } + + public int read() throws IOException { + int result = _is.read(); + if(result >=0) { + checkRead(1); + } + return result; + } + public int read(byte[] b, int off, int len) throws IOException { + int result = _is.read(b, off, len); + checkRead(result); + return result; + } + + private void checkRead(int nBytes) { + _currentIx += nBytes; + if(_failIndex > 0 && _currentIx > _failIndex) { + throw new MyEx(); + } + } + public void close() throws IOException { + _isClosed = true; + _is.close(); + } + public boolean isClosed() { + return _isClosed; + } + } + + /** + * Test for undesired behaviour observable as of svn revision 618865 (5-Feb-2008). + * POIFSFileSystem was not closing the input stream. + */ + public void testAlwaysClose() { + + TestIS testIS; + + // Normal case - read until EOF and close + testIS = new TestIS(openSampleStream("13224.xls"), -1); + try { + new POIFSFileSystem(testIS); + } catch (IOException e) { + throw new RuntimeException(e); + } + assertTrue("input stream was not closed", testIS.isClosed()); + + // intended to crash after reading 10000 bytes + testIS = new TestIS(openSampleStream("13224.xls"), 10000); + try { + new POIFSFileSystem(testIS); + fail("ex should have been thrown"); + } catch (IOException e) { + throw new RuntimeException(e); + } catch (MyEx e) { + // expected + } + assertTrue("input stream was not closed", testIS.isClosed()); // but still should close + + } + + private static InputStream openSampleStream(String sampleName) { + String dataDirName = System.getProperty("HSSF.testdata.path"); + if(dataDirName == null) { + throw new RuntimeException("Missing system property '" + "HSSF.testdata.path" + "'"); + } + File f = new File(dataDirName + "/" + sampleName); + try { + return new FileInputStream(f); + } catch (FileNotFoundException e) { + throw new RuntimeException("Sample file '" + f.getAbsolutePath() + "' not found"); + } + } +} diff --git a/src/testcases/org/apache/poi/poifs/property/AllPOIFSPropertyTests.java b/src/testcases/org/apache/poi/poifs/property/AllPOIFSPropertyTests.java new file mode 100755 index 0000000000..a5459ed76f --- /dev/null +++ b/src/testcases/org/apache/poi/poifs/property/AllPOIFSPropertyTests.java @@ -0,0 +1,22 @@ +package org.apache.poi.poifs.property; + +import junit.framework.Test; +import junit.framework.TestSuite; + +/** + * Tests for org.apache.poi.poifs.property
    + * + * @author Josh Micich + */ +public final class AllPOIFSPropertyTests { + + public static Test suite() { + TestSuite result = new TestSuite("Tests for org.apache.poi.poifs.property"); + result.addTestSuite(TestDirectoryProperty.class); + result.addTestSuite(TestDocumentProperty.class); + result.addTestSuite(TestPropertyFactory.class); + result.addTestSuite(TestPropertyTable.class); + result.addTestSuite(TestRootProperty.class); + return result; + } +} diff --git a/src/testcases/org/apache/poi/poifs/storage/AllPOIFSStorageTests.java b/src/testcases/org/apache/poi/poifs/storage/AllPOIFSStorageTests.java new file mode 100755 index 0000000000..8c15d389e3 --- /dev/null +++ b/src/testcases/org/apache/poi/poifs/storage/AllPOIFSStorageTests.java @@ -0,0 +1,47 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +package org.apache.poi.poifs.storage; + +import junit.framework.Test; +import junit.framework.TestSuite; +/** + * Tests for org.apache.poi.poifs.storage
    + * + * @author Josh Micich + */ +public final class AllPOIFSStorageTests { + + public static Test suite() { + TestSuite result = new TestSuite("Tests for org.apache.poi.poifs.storage"); + result.addTestSuite(TestBATBlock.class); + result.addTestSuite(TestBlockAllocationTableReader.class); + result.addTestSuite(TestBlockAllocationTableWriter.class); + result.addTestSuite(TestBlockListImpl.class); + result.addTestSuite(TestDocumentBlock.class); + result.addTestSuite(TestHeaderBlockReader.class); + result.addTestSuite(TestHeaderBlockWriter.class); + result.addTestSuite(TestPropertyBlock.class); + result.addTestSuite(TestRawDataBlock.class); + result.addTestSuite(TestRawDataBlockList.class); + result.addTestSuite(TestSmallBlockTableReader.class); + result.addTestSuite(TestSmallBlockTableWriter.class); + result.addTestSuite(TestSmallDocumentBlock.class); + result.addTestSuite(TestSmallDocumentBlockList.class); + return result; + } +} diff --git a/src/testcases/org/apache/poi/poifs/storage/TestRawDataBlock.java b/src/testcases/org/apache/poi/poifs/storage/TestRawDataBlock.java index 1473fa82ea..4c84f04b71 100644 --- a/src/testcases/org/apache/poi/poifs/storage/TestRawDataBlock.java +++ b/src/testcases/org/apache/poi/poifs/storage/TestRawDataBlock.java @@ -22,6 +22,9 @@ package org.apache.poi.poifs.storage; import java.io.*; import java.util.Random; +import org.apache.poi.util.DummyPOILogger; +import org.apache.poi.util.POILogFactory; + import junit.framework.*; /** @@ -43,6 +46,13 @@ public class TestRawDataBlock public TestRawDataBlock(String name) { super(name); + + // We always want to use our own + // logger + System.setProperty( + "org.apache.poi.util.POILogger", + "org.apache.poi.util.DummyPOILogger" + ); } /** @@ -99,11 +109,19 @@ public class TestRawDataBlock /** * Test creating a short RawDataBlock + * Will trigger a warning, but no longer an IOException, + * as people seem to have "valid" truncated files */ - - public void testShortConstructor() + public void testShortConstructor() throws Exception { - for (int k = 1; k < 512; k++) + // Get the logger to be used + DummyPOILogger logger = (DummyPOILogger)POILogFactory.getLogger( + RawDataBlock.class + ); + assertEquals(0, logger.logged.size()); + + // Test for various data sizes + for (int k = 1; k <= 512; k++) { byte[] data = new byte[ k ]; @@ -112,16 +130,33 @@ public class TestRawDataBlock data[ j ] = ( byte ) j; } RawDataBlock block = null; - - try - { - block = new RawDataBlock(new ByteArrayInputStream(data)); - fail("Should have thrown IOException creating short block"); - } - catch (IOException ignored) - { - - // as expected + + logger.reset(); + assertEquals(0, logger.logged.size()); + + // Have it created + block = new RawDataBlock(new ByteArrayInputStream(data)); + assertNotNull(block); + + // Check for the warning is there for <512 + if(k < 512) { + assertEquals( + "Warning on " + k + " byte short block", + 1, logger.logged.size() + ); + + // Build the expected warning message, and check + String bts = k + " byte"; + if(k > 1) { + bts += "s"; + } + + assertEquals( + "7 - Unable to read entire block; "+bts+" read before EOF; expected 512 bytes. Your document has probably been truncated!", + (String)(logger.logged.get(0)) + ); + } else { + assertEquals(0, logger.logged.size()); } } } @@ -132,6 +167,13 @@ public class TestRawDataBlock * incorrectly think that there's not enough data */ public void testSlowInputStream() throws Exception { + // Get the logger to be used + DummyPOILogger logger = (DummyPOILogger)POILogFactory.getLogger( + RawDataBlock.class + ); + assertEquals(0, logger.logged.size()); + + // Test for various ok data sizes for (int k = 1; k < 512; k++) { byte[] data = new byte[ 512 ]; for (int j = 0; j < data.length; j++) { @@ -153,14 +195,17 @@ public class TestRawDataBlock data[j] = (byte) j; } - // Shouldn't complain, as there is enough data - try { - RawDataBlock block = - new RawDataBlock(new SlowInputStream(data, k)); - fail(); - } catch(IOException e) { - // as expected - } + logger.reset(); + assertEquals(0, logger.logged.size()); + + // Should complain, as there isn't enough data + RawDataBlock block = + new RawDataBlock(new SlowInputStream(data, k)); + assertNotNull(block); + assertEquals( + "Warning on " + k + " byte short block", + 1, logger.logged.size() + ); } } diff --git a/src/testcases/org/apache/poi/poifs/storage/TestRawDataBlockList.java b/src/testcases/org/apache/poi/poifs/storage/TestRawDataBlockList.java index ee63825e24..ac6fc08c05 100644 --- a/src/testcases/org/apache/poi/poifs/storage/TestRawDataBlockList.java +++ b/src/testcases/org/apache/poi/poifs/storage/TestRawDataBlockList.java @@ -21,6 +21,9 @@ package org.apache.poi.poifs.storage; import java.io.*; +import org.apache.poi.util.DummyPOILogger; +import org.apache.poi.util.POILogFactory; + import junit.framework.*; /** @@ -42,6 +45,13 @@ public class TestRawDataBlockList public TestRawDataBlockList(String name) { super(name); + + // We always want to use our own + // logger + System.setProperty( + "org.apache.poi.util.POILogger", + "org.apache.poi.util.DummyPOILogger" + ); } /** @@ -78,8 +88,15 @@ public class TestRawDataBlockList * Test creating a short RawDataBlockList */ - public void testShortConstructor() + public void testShortConstructor() throws Exception { + // Get the logger to be used + DummyPOILogger logger = (DummyPOILogger)POILogFactory.getLogger( + RawDataBlock.class + ); + assertEquals(0, logger.logged.size()); + + // Test for various short sizes for (int k = 2049; k < 2560; k++) { byte[] data = new byte[ k ]; @@ -88,16 +105,11 @@ public class TestRawDataBlockList { data[ j ] = ( byte ) j; } - try - { - new RawDataBlockList(new ByteArrayInputStream(data)); - fail("Should have thrown IOException creating short block"); - } - catch (IOException ignored) - { - // as expected - } + // Check we logged the error + logger.reset(); + new RawDataBlockList(new ByteArrayInputStream(data)); + assertEquals(1, logger.logged.size()); } } diff --git a/src/testcases/org/apache/poi/util/AllPOIUtilTests.java b/src/testcases/org/apache/poi/util/AllPOIUtilTests.java new file mode 100755 index 0000000000..bb6d382047 --- /dev/null +++ b/src/testcases/org/apache/poi/util/AllPOIUtilTests.java @@ -0,0 +1,51 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +package org.apache.poi.util; + +import junit.framework.Test; +import junit.framework.TestSuite; +/** + * Test suite for all sub-packages of org.apache.poi.util
    + * + * @author Josh Micich + */ +public final class AllPOIUtilTests { + + public static Test suite() { + TestSuite result = new TestSuite("Tests for org.apache.poi.util"); + result.addTestSuite(TestArrayUtil.class); + result.addTestSuite(TestBinaryTree.class); + result.addTestSuite(TestBitField.class); + result.addTestSuite(TestByteField.class); + result.addTestSuite(TestDoubleList2d.class); + result.addTestSuite(TestHexDump.class); + result.addTestSuite(TestIntegerField.class); + result.addTestSuite(TestIntList.class); + result.addTestSuite(TestIntList2d.class); + result.addTestSuite(TestList2d.class); + result.addTestSuite(TestLittleEndian.class); + result.addTestSuite(TestLongField.class); + result.addTestSuite(TestPOILogFactory.class); + result.addTestSuite(TestPOILogger.class); + result.addTestSuite(TestShortField.class); + result.addTestSuite(TestShortList.class); + result.addTestSuite(TestStringUtil.class); + result.addTestSuite(TestTempFile.class); + return result; + } +} diff --git a/src/testcases/org/apache/poi/util/DummyPOILogger.java b/src/testcases/org/apache/poi/util/DummyPOILogger.java new file mode 100644 index 0000000000..7efbfac293 --- /dev/null +++ b/src/testcases/org/apache/poi/util/DummyPOILogger.java @@ -0,0 +1,46 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ +package org.apache.poi.util; + +import java.util.ArrayList; +import java.util.List; + +/** + * POILogger which logs into an ArrayList, so that + * tests can see what got logged + */ +public class DummyPOILogger extends POILogger { + public List logged = new ArrayList(); + + public void reset() { + logged = new ArrayList(); + } + + public boolean check(int level) { + return true; + } + + public void initialize(String cat) {} + + public void log(int level, Object obj1) { + logged.add(new String(level + " - " + obj1)); + } + + public void log(int level, Object obj1, Throwable exception) { + logged.add(new String(level + " - " + obj1 + " - " + exception)); + } +}