diff --git a/build.xml b/build.xml index 4e33434f4..8905dc13f 100644 --- a/build.xml +++ b/build.xml @@ -167,7 +167,7 @@ under the License. - + + diff --git a/src/documentation/content/xdocs/changes.xml b/src/documentation/content/xdocs/changes.xml index 51aeb2246..df0e5e51a 100644 --- a/src/documentation/content/xdocs/changes.xml +++ b/src/documentation/content/xdocs/changes.xml @@ -35,7 +35,17 @@ - + + 44345 - Implement CountA, CountIf, Index, Rows and Columns functions + 44336 - Properly escape sheet names as required when figuring out the text of formulas + 44326 - Improvements to how SystemOutLogger and CommonsLogger log messages with exceptions, and avoid an infinite loop with certain log messages with exceptions + Support for a completed Record based "pull" stream, via org.apache.poi.hssf.eventusermodel.HSSFRecordStream, to complement the existing "push" Event User Model listener stuff + + + 44297 - IntPtg must operate with unsigned short. Reading signed short results in incorrect formula calculation + 44296 - Fix for reading slide background images + 44293 - Avoid swapping AreaPtgs from relative to absolute + 44292 - Correctly process the last paragraph in a word file 44254 - Avoid some unread byte warnings, and properly understand DVALRecord Add another formula evaluation method, evaluateFormulaCell(cell), which will re-calculate the value for a formula, without affecting the formula itself. 41726 - Fix how we handle signed cell offsets in relative areas and references diff --git a/src/documentation/content/xdocs/status.xml b/src/documentation/content/xdocs/status.xml index feabdf76b..9a7a3f43c 100644 --- a/src/documentation/content/xdocs/status.xml +++ b/src/documentation/content/xdocs/status.xml @@ -32,7 +32,17 @@ - + + 44345 - Implement CountA, CountIf, Index, Rows and Columns functions + 44336 - Properly escape sheet names as required when figuring out the text of formulas + 44326 - Improvements to how SystemOutLogger and CommonsLogger log messages with exceptions, and avoid an infinite loop with certain log messages with exceptions + Support for a completed Record based "pull" stream, via org.apache.poi.hssf.eventusermodel.HSSFRecordStream, to complement the existing "push" Event User Model listener stuff + + + 44297 - IntPtg must operate with unsigned short. Reading signed short results in incorrect formula calculation + 44296 - Fix for reading slide background images + 44293 - Avoid swapping AreaPtgs from relative to absolute + 44292 - Correctly process the last paragraph in a word file 44254 - Avoid some unread byte warnings, and properly understand DVALRecord Add another formula evaluation method, evaluateFormulaCell(cell), which will re-calculate the value for a formula, without affecting the formula itself. 41726 - Fix how we handle signed cell offsets in relative areas and references diff --git a/src/java/org/apache/poi/hssf/dev/BiffViewer.java b/src/java/org/apache/poi/hssf/dev/BiffViewer.java index 2e343012d..242a85f45 100644 --- a/src/java/org/apache/poi/hssf/dev/BiffViewer.java +++ b/src/java/org/apache/poi/hssf/dev/BiffViewer.java @@ -515,6 +515,9 @@ public class BiffViewer { case FileSharingRecord.sid: retval = new FileSharingRecord( in ); break; + case HyperlinkRecord.sid: + retval = new HyperlinkRecord( in ); + break; default: retval = new UnknownRecord( in ); } diff --git a/src/java/org/apache/poi/hssf/eventusermodel/HSSFEventFactory.java b/src/java/org/apache/poi/hssf/eventusermodel/HSSFEventFactory.java index 70c989c17..f8e64c928 100644 --- a/src/java/org/apache/poi/hssf/eventusermodel/HSSFEventFactory.java +++ b/src/java/org/apache/poi/hssf/eventusermodel/HSSFEventFactory.java @@ -129,109 +129,25 @@ public class HSSFEventFactory protected short genericProcessEvents(HSSFRequest req, RecordInputStream in) throws IOException, HSSFUserException { + boolean going = true; short userCode = 0; - - short sid = 0; - process: - { - - Record rec = null; - Record lastRec = null; - DrawingRecord lastDrawingRecord = new DrawingRecord(); - - while (in.hasNextRecord()) - { - in.nextRecord(); - sid = in.getSid();; - - // - // for some reasons we have to make the workbook to be at least 4096 bytes - // but if we have such workbook we fill the end of it with zeros (many zeros) - // - // it is not good: - // if the length( all zero records ) % 4 = 1 - // e.g.: any zero record would be readed as 4 bytes at once ( 2 - id and 2 - size ). - // And the last 1 byte will be readed WRONG ( the id must be 2 bytes ) - // - // So we should better to check if the sid is zero and not to read more data - // The zero sid shows us that rest of the stream data is a fake to make workbook - // certain size - // - if ( sid == 0 ) - break; - - - if ((rec != null) && (sid != ContinueRecord.sid)) - { - userCode = req.processRecord(rec); - if (userCode != 0) break process; - } - if (sid != ContinueRecord.sid) - { - //System.out.println("creating "+sid); - Record[] recs = RecordFactory.createRecord(in); - - if (recs.length > 1) - { // we know that the multiple - for (int k = 0; k < (recs.length - 1); k++) - { // record situations do not - userCode = req.processRecord( - recs[ k ]); // contain continue records - if (userCode != 0) break process; - } - } - rec = recs[ recs.length - 1 ]; // regardless we'll process - - // the last record as though - // it might be continued - // if there is only one - // records, it will go here too. - } - else { - // Normally, ContinueRecords are handled internally - // However, in a few cases, there is a gap between a record at - // its Continue, so we have to handle them specially - // This logic is much like in RecordFactory.createRecords() - Record[] recs = RecordFactory.createRecord(in); - ContinueRecord crec = (ContinueRecord)recs[0]; - if((lastRec instanceof ObjRecord) || (lastRec instanceof TextObjectRecord)) { - // You can have Obj records between a DrawingRecord - // and its continue! - lastDrawingRecord.processContinueRecord( crec.getData() ); - // Trigger them on the drawing record, now it's complete - rec = lastDrawingRecord; - } - else if((lastRec instanceof DrawingGroupRecord)) { - ((DrawingGroupRecord)lastRec).processContinueRecord(crec.getData()); - // Trigger them on the drawing record, now it's complete - rec = lastRec; - } - else { - if (rec instanceof UnknownRecord) { - ;//silently skip records we don't know about - } else { - throw new RecordFormatException("Records should handle ContinueRecord internally. Should not see this exception"); - } - } - } - - // Update our tracking of the last record - lastRec = rec; - if(rec instanceof DrawingRecord) { - lastDrawingRecord = (DrawingRecord)rec; - } - } - if (rec != null) - { - userCode = req.processRecord(rec); - if (userCode != 0) break process; + Record r = null; + + // Create a new RecordStream and use that + HSSFRecordStream recordStream = new HSSFRecordStream(in); + + // Process each record as they come in + while(going) { + r = recordStream.nextRecord(); + if(r != null) { + userCode = req.processRecord(r); + if (userCode != 0) break; + } else { + going = false; } } + // All done, return our last code return userCode; - - // Record[] retval = new Record[ records.size() ]; - // retval = ( Record [] ) records.toArray(retval); - // return null; } } diff --git a/src/java/org/apache/poi/hssf/eventusermodel/HSSFRecordStream.java b/src/java/org/apache/poi/hssf/eventusermodel/HSSFRecordStream.java new file mode 100644 index 000000000..feb7a36d5 --- /dev/null +++ b/src/java/org/apache/poi/hssf/eventusermodel/HSSFRecordStream.java @@ -0,0 +1,234 @@ +/* ==================================================================== + 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.eventusermodel; + +import java.util.Vector; + +import org.apache.poi.hssf.record.ContinueRecord; +import org.apache.poi.hssf.record.DrawingGroupRecord; +import org.apache.poi.hssf.record.DrawingRecord; +import org.apache.poi.hssf.record.ObjRecord; +import org.apache.poi.hssf.record.Record; +import org.apache.poi.hssf.record.RecordFactory; +import org.apache.poi.hssf.record.RecordFormatException; +import org.apache.poi.hssf.record.RecordInputStream; +import org.apache.poi.hssf.record.TextObjectRecord; +import org.apache.poi.hssf.record.UnknownRecord; + +/** + * A stream based way to get at complete records, with + * as low a memory footprint as possible. + * This handles reading from a RecordInputStream, turning + * the data into full records, processing continue records + * etc. + * Most users should use {@link HSSFEventFactory} / + * {@link HSSFListener} and have new records pushed to + * them, but this does allow for a "pull" style of coding. + */ +public class HSSFRecordStream { + private RecordInputStream in; + + /** Have we run out of records on the stream? */ + private boolean hitEOS = false; + /** Have we returned all the records there are? */ + private boolean complete = false; + + /** + * Sometimes we end up with a bunch of + * records. When we do, these should + * be returned before the next normal + * record processing occurs (i.e. before + * we check for continue records and + * return rec) + */ + private Vector bonusRecords = null; + + /** + * The next record to return, which may need to have its + * continue records passed to it before we do + */ + private Record rec = null; + /** + * The most recent record that we gave to the user + */ + private Record lastRec = null; + /** + * The most recent DrawingRecord seen + */ + private DrawingRecord lastDrawingRecord = new DrawingRecord(); + + public HSSFRecordStream(RecordInputStream inp) { + this.in = inp; + } + + /** + * Returns the next (complete) record from the + * stream, or null if there are no more. + */ + public Record nextRecord() { + Record r = null; + + // Loop until we get something + while(r == null && !complete) { + // Are there any bonus records that we need to + // return? + r = getBonusRecord(); + + // If not, ask for the next real record + if(r == null) { + r = getNextRecord(); + } + } + + // All done + return r; + } + + /** + * If there are any "bonus" records, that should + * be returned before processing new ones, + * grabs the next and returns it. + * If not, returns null; + */ + private Record getBonusRecord() { + if(bonusRecords != null) { + Record r = (Record)bonusRecords.remove(0); + if(bonusRecords.size() == 0) { + bonusRecords = null; + } + return r; + } + return null; + } + + /** + * Returns the next available record, or null if + * this pass didn't return a record that's + * suitable for returning (eg was a continue record). + */ + private Record getNextRecord() { + Record toReturn = null; + + if(in.hasNextRecord()) { + // Grab our next record + in.nextRecord(); + short sid = in.getSid(); + + // + // for some reasons we have to make the workbook to be at least 4096 bytes + // but if we have such workbook we fill the end of it with zeros (many zeros) + // + // it is not good: + // if the length( all zero records ) % 4 = 1 + // e.g.: any zero record would be readed as 4 bytes at once ( 2 - id and 2 - size ). + // And the last 1 byte will be readed WRONG ( the id must be 2 bytes ) + // + // So we should better to check if the sid is zero and not to read more data + // The zero sid shows us that rest of the stream data is a fake to make workbook + // certain size + // + if ( sid == 0 ) + return null; + + + // If we had a last record, and this one + // isn't a continue record, then pass + // it on to the listener + if ((rec != null) && (sid != ContinueRecord.sid)) + { + // This last record ought to be returned + toReturn = rec; + } + + // If this record isn't a continue record, + // then build it up + if (sid != ContinueRecord.sid) + { + //System.out.println("creating "+sid); + Record[] recs = RecordFactory.createRecord(in); + + // We know that the multiple record situations + // don't contain continue records, so just + // pass those on to the listener now + if (recs.length > 1) { + bonusRecords = new Vector(recs.length-1); + for (int k = 0; k < (recs.length - 1); k++) { + bonusRecords.add(recs[k]); + } + } + + // Regardless of the number we created, always hold + // onto the last record to be processed on the next + // loop, in case it has any continue records + rec = recs[ recs.length - 1 ]; + // Don't return it just yet though, as we probably have + // a record from the last round to return + } + else { + // Normally, ContinueRecords are handled internally + // However, in a few cases, there is a gap between a record at + // its Continue, so we have to handle them specially + // This logic is much like in RecordFactory.createRecords() + Record[] recs = RecordFactory.createRecord(in); + ContinueRecord crec = (ContinueRecord)recs[0]; + if((lastRec instanceof ObjRecord) || (lastRec instanceof TextObjectRecord)) { + // You can have Obj records between a DrawingRecord + // and its continue! + lastDrawingRecord.processContinueRecord( crec.getData() ); + // Trigger them on the drawing record, now it's complete + rec = lastDrawingRecord; + } + else if((lastRec instanceof DrawingGroupRecord)) { + ((DrawingGroupRecord)lastRec).processContinueRecord(crec.getData()); + // Trigger them on the drawing record, now it's complete + rec = lastRec; + } + else { + if (rec instanceof UnknownRecord) { + ;//silently skip records we don't know about + } else { + throw new RecordFormatException("Records should handle ContinueRecord internally. Should not see this exception"); + } + } + } + + // Update our tracking of the last record + lastRec = rec; + if(rec instanceof DrawingRecord) { + lastDrawingRecord = (DrawingRecord)rec; + } + } else { + // No more records + hitEOS = true; + } + + // If we've hit the end-of-stream, then + // finish off the last record and be done + if(hitEOS) { + complete = true; + + // Return the last record if there was + // one, otherwise null + if(rec != null) { + toReturn = rec; + rec = null; + } + } + + return toReturn; + } +} \ No newline at end of file diff --git a/src/java/org/apache/poi/hssf/model/Workbook.java b/src/java/org/apache/poi/hssf/model/Workbook.java index 08c236cda..2ba50857c 100644 --- a/src/java/org/apache/poi/hssf/model/Workbook.java +++ b/src/java/org/apache/poi/hssf/model/Workbook.java @@ -93,6 +93,8 @@ 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 protected int numfonts = 0; // hold the number of font records @@ -133,7 +135,8 @@ public class Workbook implements Model Workbook retval = new Workbook(); ArrayList records = new ArrayList(recs.size() / 3); - for (int k = 0; k < recs.size(); k++) { + int k; + for (k = 0; k < recs.size(); k++) { Record rec = ( Record ) recs.get(k); if (rec.getSid() == EOFRecord.sid) { @@ -248,6 +251,17 @@ public class Workbook implements Model // retval.records.supbookpos = retval.records.bspos + 1; // retval.records.namepos = retval.records.supbookpos + 1; // } + + // Look for other interesting values that + // follow the EOFRecord + for ( ; k < recs.size(); k++) { + Record rec = ( Record ) recs.get(k); + switch (rec.getSid()) { + case HyperlinkRecord.sid: + retval.hyperlinks.add(rec); + break; + } + } retval.records.setRecords(records); @@ -2116,6 +2130,11 @@ public class Workbook implements Model return null; } + public List getHyperlinks() + { + return hyperlinks; + } + public List getRecords() { return records.getRecords(); diff --git a/src/java/org/apache/poi/hssf/record/HyperlinkRecord.java b/src/java/org/apache/poi/hssf/record/HyperlinkRecord.java new file mode 100644 index 000000000..0dcd45a72 --- /dev/null +++ b/src/java/org/apache/poi/hssf/record/HyperlinkRecord.java @@ -0,0 +1,370 @@ +/* ==================================================================== + Copyright 2002-2004 Apache Software Foundation + + Licensed 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.IOException; +import java.net.MalformedURLException; +import java.net.URL; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.poi.util.LittleEndian; +import org.apache.poi.util.StringUtil; + +/** + * The HyperlinkRecord wraps an HLINK-record + * from the Excel-97 format. + * Supports only external links for now (eg http://) + * + * @author Mark Hissink Muller (in.remaining()/2) ? (in.remaining()/2) : field_7_url_len; + field_12_url = in.readUnicodeLEString(strlen); + } + + /* (non-Javadoc) + * @see org.apache.poi.hssf.record.Record#getSid() + */ + public short getSid() + { + return HyperlinkRecord.sid; + } + + protected void validateSid(short id) + { + if (id != sid) + { + throw new RecordFormatException("NOT A HYPERLINKRECORD!"); + } + } + + public int serialize(int offset, byte[] data) + { + LittleEndian.putShort(data, 0 + offset, sid); + LittleEndian.putShort(data, 2 + offset, + ( short )(getRecordSize()-4)); + LittleEndian.putShort(data, 4 + offset, field_1_unknown); + LittleEndian.putUShort(data, 6 + offset, field_2_row); + LittleEndian.putShort(data, 8 + offset, field_3_column); + LittleEndian.putShort(data, 10 + offset, field_4_xf_index); + + offset += 12; + for(int i=0; i Short.MAX_VALUE) { - // Need to wrap - value -= (Short.MAX_VALUE+1)*2; - } - field_1_value = (short)value; + if(value < 0 || value > (Short.MAX_VALUE + 1)*2 ) + throw new IllegalArgumentException("Unsigned short is out of range: " + value); + field_1_value = value; } /** * Returns the value as a short, which may have * been wrapped into negative numbers */ - public short getValue() + public int getValue() { return field_1_value; } @@ -102,7 +91,7 @@ public class IntPtg public void writeBytes(byte [] array, int offset) { array[ offset + 0 ] = sid; - LittleEndian.putShort(array, offset + 1, getValue()); + LittleEndian.putUShort(array, offset + 1, getValue()); } public int getSize() diff --git a/src/java/org/apache/poi/hssf/record/formula/Ptg.java b/src/java/org/apache/poi/hssf/record/formula/Ptg.java index cc9a4236b..f006509c2 100644 --- a/src/java/org/apache/poi/hssf/record/formula/Ptg.java +++ b/src/java/org/apache/poi/hssf/record/formula/Ptg.java @@ -137,8 +137,8 @@ public abstract class Ptg break; case DividePtg.sid : // 0x06 - retval = new DividePtg(in); - break; + retval = new DividePtg(in); + break; case PowerPtg.sid : // 0x07 retval = new PowerPtg(in); @@ -208,6 +208,7 @@ public abstract class Ptg break; case AttrPtg.sid : // 0x19 + case 0x1a : retval = new AttrPtg(in); break; @@ -224,8 +225,8 @@ public abstract class Ptg break; case NumberPtg.sid : // 0x1f - retval = new NumberPtg(in); - break; + retval = new NumberPtg(in); + break; case ArrayPtg.sid : // 0x20 retval = new ArrayPtg(in); @@ -350,9 +351,12 @@ public abstract class Ptg case DeletedArea3DPtg.sid + 0x40 : // 0x7d retval = new DeletedArea3DPtg(in); break; - + + case 0x00: + retval = new UnknownPtg(); + break; + default : - //retval = new UnknownPtg(); throw new java.lang.UnsupportedOperationException(" Unknown Ptg in Formula: 0x"+ Integer.toHexString(( int ) id) + " (" + ( int ) id + ")"); diff --git a/src/java/org/apache/poi/hssf/record/formula/RangePtg.java b/src/java/org/apache/poi/hssf/record/formula/RangePtg.java index f0bd8c1c1..51df7844a 100644 --- a/src/java/org/apache/poi/hssf/record/formula/RangePtg.java +++ b/src/java/org/apache/poi/hssf/record/formula/RangePtg.java @@ -25,22 +25,22 @@ import org.apache.poi.hssf.record.RecordInputStream; */ public class RangePtg extends OperationPtg { + public final static int SIZE = 1; public final static byte sid = 0x11; - public RangePtg() { } public RangePtg(RecordInputStream in) { - // doesn't need anything + // No contents } public int getSize() { - return 1; + return SIZE; } public void writeBytes( byte[] array, int offset ) 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 bc5430bb5..510eebb03 100644 --- a/src/java/org/apache/poi/hssf/record/formula/Ref3DPtg.java +++ b/src/java/org/apache/poi/hssf/record/formula/Ref3DPtg.java @@ -157,18 +157,31 @@ public class Ref3DPtg extends Ptg { } - public String toFormulaString(Workbook book) { + // TODO - find a home for this method + // There is already a method on Workbook called getSheetName but it seems to do something different. + static String getSheetName(Workbook book, int externSheetIndex) { + // TODO - there are 3 ways this method can return null. Is each valid? + if (book == null) { + return null; + } + + SheetReferences refs = book.getSheetReferences(); + if (refs == null) { + return null; + } + return refs.getSheetName(externSheetIndex); + } + /** + * @return text representation of this cell reference that can be used in text + * formulas. The sheet name will get properly delimited if required. + */ + public String toFormulaString(Workbook book) + { StringBuffer retval = new StringBuffer(); - SheetReferences refs = book == null ? null : book.getSheetReferences(); - if (refs != null) { - String sheetName =refs.getSheetName((int)this.field_1_index_extern_sheet); - boolean appendQuotes = sheetName.indexOf(" ") >= 0; - if (appendQuotes) - retval.append("'"); - retval.append(sheetName); - if (appendQuotes) - retval.append("'"); - retval.append('!'); + String sheetName = getSheetName(book, field_1_index_extern_sheet); + if(sheetName != null) { + SheetNameFormatter.appendFormat(retval, sheetName); + retval.append( '!' ); } retval.append((new CellReference(getRow(),getColumn(),!isRowRelative(),!isColRelative())).toString()); return retval.toString(); diff --git a/src/java/org/apache/poi/hssf/record/formula/SheetNameFormatter.java b/src/java/org/apache/poi/hssf/record/formula/SheetNameFormatter.java new file mode 100755 index 000000000..ba796db3b --- /dev/null +++ b/src/java/org/apache/poi/hssf/record/formula/SheetNameFormatter.java @@ -0,0 +1,245 @@ +/* ==================================================================== + 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.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Formats sheet names for use in formula expressions. + * + * @author Josh Micich + */ +final class SheetNameFormatter { + + private static final String BIFF8_LAST_COLUMN = "IV"; + private static final int BIFF8_LAST_COLUMN_TEXT_LEN = BIFF8_LAST_COLUMN.length(); + private static final String BIFF8_LAST_ROW = String.valueOf(0x10000); + private static final int BIFF8_LAST_ROW_TEXT_LEN = BIFF8_LAST_ROW.length(); + + private static final char DELIMITER = '\''; + + private static final Pattern CELL_REF_PATTERN = Pattern.compile("([A-Za-z])+[0-9]+"); + + private SheetNameFormatter() { + // no instances of this class + } + /** + * Used to format sheet names as they would appear in cell formula expressions. + * @return the sheet name unchanged if there is no need for delimiting. Otherwise the sheet + * name is enclosed in single quotes ('). Any single quotes which were already present in the + * sheet name will be converted to double single quotes (''). + */ + public static String format(String rawSheetName) { + StringBuffer sb = new StringBuffer(rawSheetName.length() + 2); + appendFormat(sb, rawSheetName); + return sb.toString(); + } + + /** + * Convenience method for when a StringBuffer is already available + * + * @param out - sheet name will be appended here possibly with delimiting quotes + */ + public static void appendFormat(StringBuffer out, String rawSheetName) { + boolean needsQuotes = needsDelimiting(rawSheetName); + if(needsQuotes) { + out.append(DELIMITER); + appendAndEscape(out, rawSheetName); + out.append(DELIMITER); + } else { + out.append(rawSheetName); + } + } + + private static void appendAndEscape(StringBuffer sb, String rawSheetName) { + int len = rawSheetName.length(); + for(int i=0; itrue if the presence of the specified character in a sheet name would + * require the sheet name to be delimited in formulas. This includes every non-alphanumeric + * character besides underscore '_'. + */ + /* package */ static boolean isSpecialChar(char ch) { + // note - Character.isJavaIdentifierPart() would allow dollars '$' + if(Character.isLetterOrDigit(ch)) { + return false; + } + switch(ch) { + case '_': // underscore is ok + return false; + case '\n': + case '\r': + case '\t': + throw new RuntimeException("Illegal character (0x" + + Integer.toHexString(ch) + ") found in sheet name"); + } + return true; + } + + + /** + * Used to decide whether sheet names like 'AB123' need delimiting due to the fact that they + * look like cell references. + *

+ * This code is currently being used for translating formulas represented with Ptg + * tokens into human readable text form. In formula expressions, a sheet name always has a + * trailing '!' so there is little chance for ambiguity. It doesn't matter too much what this + * method returns but it is worth noting the likely consumers of these formula text strings: + *

    + *
  1. POI's own formula parser
  2. + *
  3. Visual reading by human
  4. + *
  5. VBA automation entry into Excel cell contents e.g. ActiveCell.Formula = "=c64!A1"
  6. + *
  7. Manual entry into Excel cell contents
  8. + *
  9. Some third party formula parser
  10. + *
+ * + * At the time of writing, POI's formula parser tolerates cell-like sheet names in formulas + * with or without delimiters. The same goes for Excel(2007), both manual and automated entry. + *

+ * For better or worse this implementation attempts to replicate Excel's formula renderer. + * Excel uses range checking on the apparent 'row' and 'column' components. Note however that + * the maximum sheet size varies across versions: + *

+ *

+ * + * + * + * + *
Version  File Format  Last Column  Last Row
97-2003BIFF8"IV" (2^8)65536 (2^14)
2007BIFF12"XFD" (2^14)1048576 (2^20)
+ * POI currently targets BIFF8 (Excel 97-2003), so the following behaviour can be observed for + * this method: + *
+ * + * + * + * + * + * + * + * + * + * + * + *
Input           Result 
"A1", 1true
"a111", 1true
"A65536", 1true
"A65537", 1false
"iv1", 2true
"IW1", 2false
"AAA1", 3false
"a111", 1true
"Sheet1", 6false
+ */ + /* package */ static boolean cellReferenceIsWithinRange(String rawSheetName, int numberOfLetters) { + + if(numberOfLetters > BIFF8_LAST_COLUMN_TEXT_LEN) { + // "Sheet1" case etc + return false; // that was easy + } + int nDigits = rawSheetName.length() - numberOfLetters; + if(nDigits > BIFF8_LAST_ROW_TEXT_LEN) { + return false; + } + if(numberOfLetters == BIFF8_LAST_COLUMN_TEXT_LEN) { + String colStr = rawSheetName.substring(0, BIFF8_LAST_COLUMN_TEXT_LEN).toUpperCase(); + if(colStr.compareTo(BIFF8_LAST_COLUMN) > 0) { + return false; + } + } else { + // apparent column name has less chars than max + // no need to check range + } + + if(nDigits == BIFF8_LAST_ROW_TEXT_LEN) { + String colStr = rawSheetName.substring(numberOfLetters); + // ASCII comparison is valid if digit count is same + if(colStr.compareTo(BIFF8_LAST_ROW) > 0) { + return false; + } + } else { + // apparent row has less chars than max + // no need to check range + } + + return true; + } + + /** + * Note - this method assumes the specified rawSheetName has only letters and digits. It + * cannot be used to match absolute or range references (using the dollar or colon char). + *

+ * Some notable cases: + *

+ * + * + * + * + * + * + * + * + * + * + *
Input Result Comments
"A1"  true 
"a111"  true 
"AA"  false 
"aa1"  true 
"A1A"  false 
"A1A1"  false 
"A$1:$C$20"  falseNot a plain cell reference
"SALES20080101"  trueStill needs delimiting even though well out of range
+ * + * @return true if there is any possible ambiguity that the specified rawSheetName + * could be interpreted as a valid cell name. + */ + /* package */ static boolean nameLooksLikePlainCellReference(String rawSheetName) { + Matcher matcher = CELL_REF_PATTERN.matcher(rawSheetName); + if(!matcher.matches()) { + return false; + } + + // rawSheetName == "Sheet1" gets this far. + String lettersPrefix = matcher.group(1); + return cellReferenceIsWithinRange(rawSheetName, lettersPrefix.length()); + } + +} diff --git a/src/java/org/apache/poi/hssf/record/formula/UnknownPtg.java b/src/java/org/apache/poi/hssf/record/formula/UnknownPtg.java index 9cc515832..1badf5197 100644 --- a/src/java/org/apache/poi/hssf/record/formula/UnknownPtg.java +++ b/src/java/org/apache/poi/hssf/record/formula/UnknownPtg.java @@ -28,7 +28,7 @@ import org.apache.poi.hssf.record.RecordInputStream; public class UnknownPtg extends Ptg { - private short size; + private short size = 1; /** Creates new UnknownPtg */ diff --git a/src/java/org/apache/poi/hssf/usermodel/HSSFCell.java b/src/java/org/apache/poi/hssf/usermodel/HSSFCell.java index 33417dad7..19ead33f0 100644 --- a/src/java/org/apache/poi/hssf/usermodel/HSSFCell.java +++ b/src/java/org/apache/poi/hssf/usermodel/HSSFCell.java @@ -34,20 +34,7 @@ import java.util.Iterator; import org.apache.poi.hssf.model.FormulaParser; import org.apache.poi.hssf.model.Sheet; import org.apache.poi.hssf.model.Workbook; -import org.apache.poi.hssf.record.BlankRecord; -import org.apache.poi.hssf.record.BoolErrRecord; -import org.apache.poi.hssf.record.CellValueRecordInterface; -import org.apache.poi.hssf.record.CommonObjectDataSubRecord; -import org.apache.poi.hssf.record.ExtendedFormatRecord; -import org.apache.poi.hssf.record.FormulaRecord; -import org.apache.poi.hssf.record.LabelSSTRecord; -import org.apache.poi.hssf.record.NoteRecord; -import org.apache.poi.hssf.record.NumberRecord; -import org.apache.poi.hssf.record.ObjRecord; -import org.apache.poi.hssf.record.Record; -import org.apache.poi.hssf.record.SubRecord; -import org.apache.poi.hssf.record.TextObjectRecord; -import org.apache.poi.hssf.record.UnicodeString; +import org.apache.poi.hssf.record.*; import org.apache.poi.hssf.record.aggregates.FormulaRecordAggregate; import org.apache.poi.hssf.record.formula.Ptg; import org.apache.poi.ss.usermodel.Cell; @@ -1073,4 +1060,31 @@ public class HSSFCell implements Cell } return comment; } + + /** + * Returns hyperlink associated with this cell + * + * @return hyperlink associated with this cell or null if not found + */ + public HSSFHyperlink getHyperlink(){ + for (Iterator it = sheet.getRecords().iterator(); it.hasNext(); ) { + Record rec = ( Record ) it.next(); + if (rec instanceof HyperlinkRecord){ + HyperlinkRecord link = (HyperlinkRecord)rec; + if(link.getColumn() == record.getColumn() && link.getRow() == record.getRow()){ + return new HSSFHyperlink(link); + } + } + } + return null; + } + + /** + * Assign a hypelrink to this cell + * + * @param link hypelrink associated with this cell + */ + public void setHyperlink(HSSFHyperlink link){ + + } } diff --git a/src/java/org/apache/poi/hssf/usermodel/HSSFDataFormat.java b/src/java/org/apache/poi/hssf/usermodel/HSSFDataFormat.java index 547ec8303..fb7ac49f3 100644 --- a/src/java/org/apache/poi/hssf/usermodel/HSSFDataFormat.java +++ b/src/java/org/apache/poi/hssf/usermodel/HSSFDataFormat.java @@ -206,12 +206,12 @@ public class HSSFDataFormat implements DataFormat } /** - * get the format index that matches the given format string. - * Creates a new format if one is not found. Aliases text to the proper format. + * Get the format index that matches the given format + * string, creating a new format entry if required. + * Aliases text to the proper format as required. * @param format string matching a built in format * @return index of format. */ - public short getFormat( String format ) { ListIterator i; diff --git a/src/java/org/apache/poi/hssf/usermodel/HSSFHyperlink.java b/src/java/org/apache/poi/hssf/usermodel/HSSFHyperlink.java new file mode 100755 index 000000000..e1bd28af6 --- /dev/null +++ b/src/java/org/apache/poi/hssf/usermodel/HSSFHyperlink.java @@ -0,0 +1,128 @@ +/* ==================================================================== + 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 org.apache.poi.hssf.record.EscherAggregate; +import org.apache.poi.hssf.record.NoteRecord; +import org.apache.poi.hssf.record.TextObjectRecord; +import org.apache.poi.hssf.record.HyperlinkRecord; +import org.apache.poi.ddf.*; + +import java.util.Map; +import java.util.List; +import java.util.Iterator; + +/** + * Represents a hyperlink. + * + * @author Yegor Kozlov + */ +public class HSSFHyperlink { + + /** + * Link to a existing file or web page + */ + public static final int LINK_URL = 1; + + /** + * Link to a place in this document + */ + public static final int LINK_DOCUMENT = 2; + + /** + * Link to an E-mail address + */ + public static final int LINK_EMAIL = 3; + + /** + * Unknown type + */ + public static final int LINK_UNKNOWN = 4; + + /** + * Low-level record object that stores the actual hyperlink data + */ + private HyperlinkRecord record = null; + + protected HSSFHyperlink( HyperlinkRecord record ) + { + this.record = record; + } + + /** + * Return the row of the cell that contains the hyperlink + * + * @return the 0-based row of the cell that contains the hyperlink + */ + public int getRow(){ + return record.getRow(); + } + + /** + * Set the row of the cell that contains the hyperlink + * + * @param row the 0-based row of the cell that contains the hyperlink + */ + public void setRow(int row){ + record.setRow(row); + } + + /** + * Return the column of the cell that contains the hyperlink + * + * @return the 0-based column of the cell that contains the hyperlink + */ + public short getColumn(){ + return record.getColumn(); + } + + /** + * Set the column of the cell that contains the hyperlink + * + * @param col the 0-based column of the cell that contains the hyperlink + */ + public void setColumn(short col){ + record.setColumn(col); + } + + /** + * Hypelink address. Depending on the hyperlink type it can be URL, e-mail, etc. + * + * @return the address of this hyperlink + */ + public String getAddress(){ + return record.getUrlString(); + } + + /** + * Return text to display for this hyperlink + * + * @return text to display + */ + public String getLabel(){ + return record.getLabel(); + } + + /** + * Return the type of this hyperlink + * + * @return the type of this hyperlink + */ + public int getType(){ + throw new RuntimeException("Not implemented"); + } +} diff --git a/src/java/org/apache/poi/util/CommonsLogger.java b/src/java/org/apache/poi/util/CommonsLogger.java index 4265a88af..16a48f28f 100644 --- a/src/java/org/apache/poi/util/CommonsLogger.java +++ b/src/java/org/apache/poi/util/CommonsLogger.java @@ -22,8 +22,6 @@ package org.apache.poi.util; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import java.util.*; - /** * A logger class that strives to make it as easy as possible for * developers to write log calls, while simultaneously making those @@ -53,7 +51,6 @@ public class CommonsLogger extends POILogger * @param level One of DEBUG, INFO, WARN, ERROR, FATAL * @param obj1 The object to log. */ - public void log(final int level, final Object obj1) { if(level==FATAL) @@ -98,6 +95,78 @@ public class CommonsLogger extends POILogger log.trace(obj1); } } + } + + /** + * Log a message + * + * @param level One of DEBUG, INFO, WARN, ERROR, FATAL + * @param obj1 The object to log. This is converted to a string. + * @param exception An exception to be logged + */ + public void log(final int level, final Object obj1, + final Throwable exception) + { + if(level==FATAL) + { + if(log.isFatalEnabled()) + { + if(obj1 != null) + log.fatal(obj1, exception); + else + log.fatal(exception); + } + } + else if(level==ERROR) + { + if(log.isErrorEnabled()) + { + if(obj1 != null) + log.error(obj1, exception); + else + log.error(exception); + } + } + else if(level==WARN) + { + if(log.isWarnEnabled()) + { + if(obj1 != null) + log.warn(obj1, exception); + else + log.warn(exception); + } + } + else if(level==INFO) + { + if(log.isInfoEnabled()) + { + if(obj1 != null) + log.info(obj1, exception); + else + log.info(exception); + } + } + else if(level==DEBUG) + { + if(log.isDebugEnabled()) + { + if(obj1 != null) + log.debug(obj1, exception); + else + log.debug(exception); + } + } + else + { + if(log.isTraceEnabled()) + { + if(obj1 != null) + log.trace(obj1, exception); + else + log.trace(exception); + } + } } diff --git a/src/java/org/apache/poi/util/POILogger.java b/src/java/org/apache/poi/util/POILogger.java index b2d358d4d..514edf90c 100644 --- a/src/java/org/apache/poi/util/POILogger.java +++ b/src/java/org/apache/poi/util/POILogger.java @@ -51,7 +51,24 @@ public abstract class POILogger abstract public void initialize(final String cat); + /** + * Log a message + * + * @param level One of DEBUG, INFO, WARN, ERROR, FATAL + * @param obj1 The object to log. This is converted to a string. + */ abstract public void log(final int level, final Object obj1); + + /** + * Log a message + * + * @param level One of DEBUG, INFO, WARN, ERROR, FATAL + * @param obj1 The object to log. This is converted to a string. + * @param exception An exception to be logged + */ + abstract public void log(final int level, final Object obj1, + final Throwable exception); + /** * Check if a logger is enabled to log at the specified level @@ -237,17 +254,15 @@ public abstract class POILogger } /** - * Log a message + * Log an exception, without a message * * @param level One of DEBUG, INFO, WARN, ERROR, FATAL - * @param obj1 The object to log. This is converted to a string. * @param exception An exception to be logged */ - public void log(final int level, final Object obj1, - final Throwable exception) + public void log(final int level, final Throwable exception) { - log(level , obj1, exception); + log(level, null, exception); } /** diff --git a/src/java/org/apache/poi/util/SystemOutLogger.java b/src/java/org/apache/poi/util/SystemOutLogger.java index 8b3dc50d9..af678e186 100644 --- a/src/java/org/apache/poi/util/SystemOutLogger.java +++ b/src/java/org/apache/poi/util/SystemOutLogger.java @@ -49,8 +49,24 @@ public class SystemOutLogger extends POILogger public void log(final int level, final Object obj1) { - if (check(level)) + log(level, obj1, null); + } + + /** + * Log a message + * + * @param level One of DEBUG, INFO, WARN, ERROR, FATAL + * @param obj1 The object to log. This is converted to a string. + * @param exception An exception to be logged + */ + public void log(final int level, final Object obj1, + final Throwable exception) { + if (check(level)) { System.out.println("["+cat+"] "+obj1); + if(exception != null) { + exception.printStackTrace(System.out); + } + } } /** diff --git a/src/scratchpad/src/org/apache/poi/hslf/model/Fill.java b/src/scratchpad/src/org/apache/poi/hslf/model/Fill.java index 7eae4edc4..f9cc43a7e 100644 --- a/src/scratchpad/src/org/apache/poi/hslf/model/Fill.java +++ b/src/scratchpad/src/org/apache/poi/hslf/model/Fill.java @@ -23,6 +23,8 @@ import org.apache.poi.hslf.record.*; import org.apache.poi.hslf.usermodel.PictureData; import org.apache.poi.hslf.usermodel.SlideShow; import org.apache.poi.hslf.exceptions.HSLFException; +import org.apache.poi.util.POILogger; +import org.apache.poi.util.POILogFactory; import java.awt.*; import java.util.*; @@ -33,6 +35,9 @@ import java.util.*; * @author Yegor Kozlov */ public class Fill { + // For logging + protected POILogger logger = POILogFactory.getLogger(this.getClass()); + /** * Fill with a solid color */ @@ -208,15 +213,18 @@ public class Fill { java.util.List lst = bstore.getChildRecords(); int idx = p.getPropertyValue(); - EscherBSERecord bse = (EscherBSERecord)lst.get(idx); - for ( int i = 0; i < pict.length; i++ ) { - if (pict[i].getOffset() == bse.getOffset()){ - return pict[i]; + if (idx == 0){ + logger.log(POILogger.ERROR, "no reference to picture data found "); + } else { + EscherBSERecord bse = (EscherBSERecord)lst.get(idx - 1); + for ( int i = 0; i < pict.length; i++ ) { + if (pict[i].getOffset() == bse.getOffset()){ + return pict[i]; + } } } - throw new HSLFException("Picture data not found: \n" + - " bse: " + bse + " at " + bse.getOffset() ); + return null; } /** diff --git a/src/scratchpad/src/org/apache/poi/hslf/model/Picture.java b/src/scratchpad/src/org/apache/poi/hslf/model/Picture.java index 0740e23bc..90efd5f3e 100644 --- a/src/scratchpad/src/org/apache/poi/hslf/model/Picture.java +++ b/src/scratchpad/src/org/apache/poi/hslf/model/Picture.java @@ -109,7 +109,7 @@ public class Picture extends SimpleShape { */ public int getPictureIndex(){ EscherOptRecord opt = (EscherOptRecord)getEscherChild(_escherContainer, EscherOptRecord.RECORD_ID); - EscherSimpleProperty prop = (EscherSimpleProperty)getEscherProperty(opt, EscherProperties.BLIP__BLIPTODISPLAY + 0x4000); + EscherSimpleProperty prop = (EscherSimpleProperty)getEscherProperty(opt, EscherProperties.BLIP__BLIPTODISPLAY); return prop == null ? 0 : prop.getPropertyValue(); } diff --git a/src/scratchpad/src/org/apache/poi/hslf/model/Shape.java b/src/scratchpad/src/org/apache/poi/hslf/model/Shape.java index 56d77764e..5cff81a8d 100644 --- a/src/scratchpad/src/org/apache/poi/hslf/model/Shape.java +++ b/src/scratchpad/src/org/apache/poi/hslf/model/Shape.java @@ -227,7 +227,7 @@ public abstract class Shape { for ( Iterator iterator = opt.getEscherProperties().iterator(); iterator.hasNext(); ) { EscherProperty prop = (EscherProperty) iterator.next(); - if (prop.getId() == propId) + if (prop.getPropertyNumber() == propId) return prop; } return null; diff --git a/src/scratchpad/src/org/apache/poi/hslf/model/Slide.java b/src/scratchpad/src/org/apache/poi/hslf/model/Slide.java index 201a069fc..ea7201751 100644 --- a/src/scratchpad/src/org/apache/poi/hslf/model/Slide.java +++ b/src/scratchpad/src/org/apache/poi/hslf/model/Slide.java @@ -262,4 +262,11 @@ public class Slide extends Sheet SlideAtom sa = getSlideRecord().getSlideAtom(); return sa.getFollowMasterBackground(); } + + public Background getBackground() { + if(getFollowMasterBackground()) + return getMasterSheet().getBackground(); + else + return super.getBackground(); + } } diff --git a/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Columns.java b/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Columns.java index e25fad66e..b75864e72 100644 --- a/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Columns.java +++ b/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Columns.java @@ -14,12 +14,46 @@ * 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 Columns 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.NumberEval; +import org.apache.poi.hssf.record.formula.eval.RefEval; +/** + * Implementation for Excel COLUMNS function. + * + * @author Josh Micich + */ +public final class Columns implements Function { + + public Eval evaluate(Eval[] args, int srcCellRow, short srcCellCol) { + switch(args.length) { + case 1: + // expected + break; + case 0: + // too few arguments + return ErrorEval.VALUE_INVALID; + default: + // too many arguments + return ErrorEval.VALUE_INVALID; + } + Eval firstArg = args[0]; + + int result; + if (firstArg instanceof AreaEval) { + AreaEval ae = (AreaEval) firstArg; + result = ae.getLastColumn() - ae.getFirstColumn() + 1; + } else if (firstArg instanceof RefEval) { + result = 1; + } else { // anything else is not valid argument + return ErrorEval.VALUE_INVALID; + } + return new NumberEval(result); + } } diff --git a/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Counta.java b/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Counta.java index 6c6b30574..9061e77e5 100644 --- a/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Counta.java +++ b/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Counta.java @@ -14,12 +14,107 @@ * 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 Counta extends NotImplementedFunction { +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.RefEval; +import org.apache.poi.hssf.record.formula.eval.StringEval; +import org.apache.poi.hssf.record.formula.eval.ValueEval; +/** + * Counts the number of cells that contain data within the list of arguments. + * + * Excel Syntax + * COUNTA(value1,value2,...) + * Value1, value2, ... are 1 to 30 arguments representing the values or ranges to be counted. + * + * @author Josh Micich + */ +public final class Counta implements Function { + + public Eval evaluate(Eval[] args, int srcCellRow, short srcCellCol) { + int nArgs = args.length; + if (nArgs < 1) { + // too few arguments + return ErrorEval.VALUE_INVALID; + } + + if (nArgs > 30) { + // too many arguments + return ErrorEval.VALUE_INVALID; + } + + int temp = 0; + // Note - observed behavior of Excel: + // Error values like #VALUE!, #REF!, #DIV/0!, #NAME? etc don't cause this COUNTA to return an error + // in fact, they seem to get counted + + for(int i=0; i + * + * Syntax: COUNTIF ( range, criteria ) + * + * + * + *
range   is the range of cells to be counted based on the criteria
criteriais used to determine which cells to count
+ *

+ * + * @author Josh Micich + */ +public final class Countif implements Function { + + /** + * Common interface for the matching criteria. + */ + private interface I_MatchPredicate { + boolean matches(Eval x); + } + + private static final class NumberMatcher implements I_MatchPredicate { + + private final double _value; + + public NumberMatcher(double value) { + _value = value; + } + + public boolean matches(Eval x) { + if(x instanceof StringEval) { + // if the target(x) is a string, but parses as a number + // it may still count as a match + StringEval se = (StringEval)x; + Double val = parseDouble(se.getStringValue()); + if(val == null) { + // x is text that is not a number + return false; + } + return val.doubleValue() == _value; + } + if(!(x instanceof NumberEval)) { + return false; + } + NumberEval ne = (NumberEval) x; + return ne.getNumberValue() == _value; + } + } + private static final class BooleanMatcher implements I_MatchPredicate { + + private final boolean _value; + + public BooleanMatcher(boolean value) { + _value = value; + } + + public boolean matches(Eval x) { + if(x instanceof StringEval) { + StringEval se = (StringEval)x; + Boolean val = parseBoolean(se.getStringValue()); + if(val == null) { + // x is text that is not a boolean + return false; + } + if (true) { // change to false to observe more intuitive behaviour + // Note - Unlike with numbers, it seems that COUNTA never matches + // boolean values when the target(x) is a string + return false; + } + return val.booleanValue() == _value; + } + if(!(x instanceof BoolEval)) { + return false; + } + BoolEval be = (BoolEval) x; + return be.getBooleanValue() == _value; + } + } + private static final class StringMatcher implements I_MatchPredicate { + + private final String _value; + + public StringMatcher(String value) { + _value = value; + } + + public boolean matches(Eval x) { + if(!(x instanceof StringEval)) { + return false; + } + StringEval se = (StringEval) x; + return se.getStringValue() == _value; + } + } + + public Eval evaluate(Eval[] args, int srcCellRow, short srcCellCol) { + switch(args.length) { + case 2: + // expected + break; + default: + // TODO - it doesn't seem to be possible to enter COUNTIF() into Excel with the wrong arg count + // perhaps this should be an exception + return ErrorEval.VALUE_INVALID; + } + + AreaEval range = (AreaEval) args[0]; + Eval criteriaArg = args[1]; + if(criteriaArg instanceof RefEval) { + // criteria is not a literal value, but a cell reference + // for example COUNTIF(B2:D4, E1) + RefEval re = (RefEval)criteriaArg; + criteriaArg = re.getInnerValueEval(); + } else { + // other non literal tokens such as function calls, have been fully evaluated + // for example COUNTIF(B2:D4, COLUMN(E1)) + } + I_MatchPredicate mp = createCriteriaPredicate(criteriaArg); + return countMatchingCellsInArea(range, mp); + } + /** + * @return the number of evaluated cells in the range that match the specified criteria + */ + private Eval countMatchingCellsInArea(AreaEval range, I_MatchPredicate criteriaPredicate) { + ValueEval[] values = range.getValues(); + int result = 0; + for (int i = 0; i < values.length; i++) { + if(criteriaPredicate.matches(values[i])) { + result++; + } + } + return new NumberEval(result); + } + + private static I_MatchPredicate createCriteriaPredicate(Eval evaluatedCriteriaArg) { + if(evaluatedCriteriaArg instanceof NumberEval) { + return new NumberMatcher(((NumberEval)evaluatedCriteriaArg).getNumberValue()); + } + if(evaluatedCriteriaArg instanceof BoolEval) { + return new BooleanMatcher(((BoolEval)evaluatedCriteriaArg).getBooleanValue()); + } + + if(evaluatedCriteriaArg instanceof StringEval) { + return createGeneralMatchPredicate((StringEval)evaluatedCriteriaArg); + } + throw new RuntimeException("Unexpected type for criteria (" + + evaluatedCriteriaArg.getClass().getName() + ")"); + } + + /** + * When the second argument is a string, many things are possible + */ + private static I_MatchPredicate createGeneralMatchPredicate(StringEval stringEval) { + String value = stringEval.getStringValue(); + char firstChar = value.charAt(0); + Boolean booleanVal = parseBoolean(value); + if(booleanVal != null) { + return new BooleanMatcher(booleanVal.booleanValue()); + } + + Double doubleVal = parseDouble(value); + if(doubleVal != null) { + return new NumberMatcher(doubleVal.doubleValue()); + } + switch(firstChar) { + case '>': + case '<': + case '=': + throw new RuntimeException("Incomplete code - criteria expressions such as '" + + value + "' not supported yet"); + } + + //else - just a plain string with no interpretation. + return new StringMatcher(value); + } + + /** + * Under certain circumstances COUNTA will equate a plain number with a string representation of that number + */ + /* package */ static Double parseDouble(String strRep) { + if(!Character.isDigit(strRep.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(strRep); + } catch (NumberFormatException e) { + return null; + } + return new Double(val); + } + /** + * Boolean literals ('TRUE', 'FALSE') treated similarly but NOT same as numbers. + */ + /* package */ static Boolean parseBoolean(String strRep) { + switch(strRep.charAt(0)) { + case 't': + case 'T': + if("TRUE".equalsIgnoreCase(strRep)) { + return Boolean.TRUE; + } + break; + case 'f': + case 'F': + if("FALSE".equalsIgnoreCase(strRep)) { + return Boolean.FALSE; + } + break; + } + return null; + } } diff --git a/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Index.java b/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Index.java index db798ee0f..aebf6aab0 100644 --- a/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Index.java +++ b/src/scratchpad/src/org/apache/poi/hssf/record/formula/functions/Index.java @@ -14,12 +14,95 @@ * 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 Index 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.NumberEval; +import org.apache.poi.hssf.record.formula.eval.RefEval; +/** + * Implementation for the Excel function INDEX

+ * + * Syntax :
+ * INDEX ( reference, row_num[, column_num [, area_num]])
+ * INDEX ( array, row_num[, column_num]) + * + * + * + * + * + * + *
referencetypically an area reference, possibly a union of areas
arraya literal array value (currently not supported)
row_numselects the row within the array or area reference
column_numselects column within the array or area reference. default is 1
area_numused when reference is a union of areas
+ *

+ * + * @author Josh Micich + */ +public final class Index implements Function { + + // TODO - javadoc for interface method + public Eval evaluate(Eval[] args, int srcCellRow, short srcCellCol) { + int nArgs = args.length; + if(nArgs < 2) { + // too few arguments + return ErrorEval.VALUE_INVALID; + } + Eval firstArg = args[0]; + if(firstArg instanceof AreaEval) { + AreaEval reference = (AreaEval) firstArg; + + int rowIx = 0; + int columnIx = 0; + int areaIx = 0; + switch(nArgs) { + case 4: + areaIx = convertIndexArgToZeroBase(args[3]); + throw new RuntimeException("Incomplete code" + + " - don't know how to support the 'area_num' parameter yet)"); + // Excel expression might look like this "INDEX( (A1:B4, C3:D6, D2:E5 ), 1, 2, 3) + // In this example, the 3rd area would be used i.e. D2:E5, and the overall result would be E2 + // Token array might be encoded like this: MemAreaPtg, AreaPtg, AreaPtg, UnionPtg, UnionPtg, ParenthesesPtg + // The formula parser doesn't seem to support this yet. Not sure if the evaluator does either + + case 3: + columnIx = convertIndexArgToZeroBase(args[2]); + case 2: + rowIx = convertIndexArgToZeroBase(args[1]); + break; + default: + // too many arguments + return ErrorEval.VALUE_INVALID; + } + + int nColumns = reference.getLastColumn()-reference.getFirstColumn()+1; + int index = rowIx * nColumns + columnIx; + + return reference.getValues()[index]; + } + + // else the other variation of this function takes an array as the first argument + // it seems like interface 'ArrayEval' does not even exist yet + + throw new RuntimeException("Incomplete code - cannot handle first arg of type (" + + firstArg.getClass().getName() + ")"); + } + + /** + * takes a NumberEval representing a 1-based index and returns the zero-based int value + */ + private static int convertIndexArgToZeroBase(Eval ev) { + NumberEval ne; + if(ev instanceof RefEval) { + // TODO - write junit to justify this + RefEval re = (RefEval) ev; + ne = (NumberEval) re.getInnerValueEval(); + } else { + ne = (NumberEval)ev; + } + + return (int)ne.getNumberValue() - 1; + } } 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 47e4dc870..6a4eb8edb 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 @@ -14,12 +14,46 @@ * 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 Rows 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.NumberEval; +import org.apache.poi.hssf.record.formula.eval.RefEval; +/** + * Implementation for Excel COLUMNS function. + * + * @author Josh Micich + */ +public final class Rows implements Function { + + public Eval evaluate(Eval[] args, int srcCellRow, short srcCellCol) { + switch(args.length) { + case 1: + // expected + break; + case 0: + // too few arguments + return ErrorEval.VALUE_INVALID; + default: + // too many arguments + return ErrorEval.VALUE_INVALID; + } + Eval firstArg = args[0]; + + int result; + if (firstArg instanceof AreaEval) { + AreaEval ae = (AreaEval) firstArg; + result = ae.getLastRow() - ae.getFirstRow() + 1; + } else if (firstArg instanceof RefEval) { + result = 1; + } else { // anything else is not valid argument + return ErrorEval.VALUE_INVALID; + } + return new NumberEval(result); + } } diff --git a/src/scratchpad/src/org/apache/poi/hwpf/usermodel/TableRow.java b/src/scratchpad/src/org/apache/poi/hwpf/usermodel/TableRow.java index ff9cf2b9c..a88e32360 100644 --- a/src/scratchpad/src/org/apache/poi/hwpf/usermodel/TableRow.java +++ b/src/scratchpad/src/org/apache/poi/hwpf/usermodel/TableRow.java @@ -58,7 +58,7 @@ public class TableRow p = getParagraph(end); s = p.text(); } - _cells[cellIndex] = new TableCell(start, end, this, levelNum, + _cells[cellIndex] = new TableCell(start, end+1, this, levelNum, _tprops.getRgtc()[cellIndex], _tprops.getRgdxaCenter()[cellIndex], _tprops.getRgdxaCenter()[cellIndex+1]-_tprops.getRgdxaCenter()[cellIndex]); diff --git a/src/scratchpad/testcases/org/apache/poi/hslf/data/44296.ppt b/src/scratchpad/testcases/org/apache/poi/hslf/data/44296.ppt new file mode 100755 index 000000000..1e0529db1 Binary files /dev/null and b/src/scratchpad/testcases/org/apache/poi/hslf/data/44296.ppt differ diff --git a/src/scratchpad/testcases/org/apache/poi/hslf/usermodel/TestBugs.java b/src/scratchpad/testcases/org/apache/poi/hslf/usermodel/TestBugs.java index 996a733ac..f3f5f8e7e 100644 --- a/src/scratchpad/testcases/org/apache/poi/hslf/usermodel/TestBugs.java +++ b/src/scratchpad/testcases/org/apache/poi/hslf/usermodel/TestBugs.java @@ -330,4 +330,24 @@ public class TestBugs extends TestCase { assertEquals(tr1[i].getText(), tr2[i].getText()); } } + + /** + * Bug 44296: HSLF Not Extracting Slide Background Image + */ + public void test44296 () throws Exception { + FileInputStream is = new FileInputStream(new File(cwd, "44296.ppt")); + SlideShow ppt = new SlideShow(is); + is.close(); + + Slide slide = ppt.getSlides()[0]; + + Background b = slide.getBackground(); + Fill f = b.getFill(); + assertEquals(Fill.FILL_PICTURE, f.getFillType()); + + PictureData pict = f.getPictureData(); + assertNotNull(pict); + assertEquals(Picture.JPEG, pict.getType()); + } + } diff --git a/src/scratchpad/testcases/org/apache/poi/hssf/data/44297.xls b/src/scratchpad/testcases/org/apache/poi/hssf/data/44297.xls new file mode 100755 index 000000000..bc65efd4e Binary files /dev/null and b/src/scratchpad/testcases/org/apache/poi/hssf/data/44297.xls differ 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 new file mode 100755 index 000000000..b5e084367 --- /dev/null +++ b/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/functions/AllIndividualFunctionEvaluationTests.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.hssf.record.formula.functions; + +import junit.framework.Test; +import junit.framework.TestSuite; + +/** + * Direct tests for all implementors of Function. + * + * @author Josh Micich + */ +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(TestCountFuncs.class); + result.addTestSuite(TestDate.class); + result.addTestSuite(TestFinanceLib.class); + result.addTestSuite(TestIndex.class); + result.addTestSuite(TestMathX.class); + result.addTestSuite(TestRowCol.class); + result.addTestSuite(TestStatsLib.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 new file mode 100755 index 000000000..958c48664 --- /dev/null +++ b/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/functions/EvalFactory.java @@ -0,0 +1,63 @@ +/* ==================================================================== + 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.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.ValueEval; + +/** + * Test helper class for creating mock Eval objects + * + * @author Josh Micich + */ +final class EvalFactory { + private static final NumberEval ZERO = new NumberEval(0); + + private EvalFactory() { + // no instances of this class + } + + /** + * Creates a dummy AreaEval (filled with zeros) + *

+ * nCols and nRows could have been derived + */ + public static AreaEval createAreaEval(String areaRefStr, int nCols, int nRows) { + int nValues = nCols * nRows; + ValueEval[] values = new ValueEval[nValues]; + for (int i = 0; i < nValues; i++) { + values[i] = ZERO; + } + + return new Area2DEval(new AreaPtg(areaRefStr), values); + } + + /** + * Creates a single RefEval (with value zero) + */ + public static RefEval createRefEval(String refStr) { + return new Ref2DEval(new ReferencePtg(refStr), ZERO, true); + } +} 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 new file mode 100755 index 000000000..87405a491 --- /dev/null +++ b/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/functions/NumericFunctionInvoker.java @@ -0,0 +1,101 @@ +/* ==================================================================== + 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.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; + +/** + * Test helper class for invoking functions with numeric results. + * + * @author Josh Micich + */ +final class NumericFunctionInvoker { + + private NumericFunctionInvoker() { + // no instances of this class + } + + private static final class NumericEvalEx extends Exception { + public NumericEvalEx(String msg) { + super(msg); + } + } + + /** + * Invokes the specified function with the arguments. + *

+ * Assumes that the cell coordinate parameters of + * Function.evaluate(args, srcCellRow, srcCellCol) + * are not required. + *

+ * 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(Function f, Eval[] args) { + try { + return invokeInternal(f, args, -1, -1); + } 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) + throws NumericEvalEx { + Eval evalResult = f.evaluate(args, srcCellRow, (short)srcCellCol); + if(evalResult == null) { + throw new NumericEvalEx("Result object was null"); + } + if(evalResult instanceof ErrorEval) { + ErrorEval ee = (ErrorEval) evalResult; + throw new NumericEvalEx(formatErrorMessage(ee)); + } + if(!(evalResult instanceof NumericValueEval)) { + throw new NumericEvalEx("Result object type (" + evalResult.getClass().getName() + + ") is invalid. Expected implementor of (" + + NumericValueEval.class.getName() + ")"); + } + + NumericValueEval result = (NumericValueEval) evalResult; + return result.getNumberValue(); + } + private static String formatErrorMessage(ErrorEval ee) { + if(errorCodesAreEqual(ee, ErrorEval.FUNCTION_NOT_IMPLEMENTED)) { + return "Function not implemented"; + } + if(errorCodesAreEqual(ee, ErrorEval.UNKNOWN_ERROR)) { + return "Unknown error"; + } + return "Error code=" + ee.getErrorCode(); + } + private static boolean errorCodesAreEqual(ErrorEval a, ErrorEval b) { + if(a==b) { + return true; + } + return a.getErrorCode() == b.getErrorCode(); + } + +} 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 new file mode 100755 index 000000000..fbaace921 --- /dev/null +++ b/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/functions/TestCountFuncs.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.record.formula.functions; + +import junit.framework.TestCase; + +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.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; + +/** + * Test cases for COUNT(), COUNTA() COUNTIF(), COUNTBLANK() + * + * @author Josh Micich + */ +public final class TestCountFuncs extends TestCase { + + public TestCountFuncs(String testName) { + super(testName); + } + + public void testCountA() { + + Eval[] args; + + args = new Eval[] { + new NumberEval(0), + }; + confirmCountA(1, args); + + args = new Eval[] { + new NumberEval(0), + new NumberEval(0), + new StringEval(""), + }; + confirmCountA(3, args); + + args = new Eval[] { + EvalFactory.createAreaEval("D2:F5", 3, 4), + }; + confirmCountA(12, args); + + args = new Eval[] { + EvalFactory.createAreaEval("D1:F5", 3, 5), // 15 + EvalFactory.createRefEval("A1"), + EvalFactory.createAreaEval("A1:F6", 7, 6), // 42 + new NumberEval(0), + }; + confirmCountA(59, args); + } + + public void testCountIf() { + + AreaEval range; + ValueEval[] values; + + // when criteria is a boolean value + values = new ValueEval[] { + new NumberEval(0), + new StringEval("TRUE"), // note - does not match boolean TRUE + BoolEval.TRUE, + BoolEval.FALSE, + BoolEval.TRUE, + BlankEval.INSTANCE, + }; + range = createAreaEval("A1:B2", values); + confirmCountIf(2, range, BoolEval.TRUE); + + // when criteria is numeric + values = new ValueEval[] { + new NumberEval(0), + new StringEval("2"), + new StringEval("2.001"), + new NumberEval(2), + new NumberEval(2), + BoolEval.TRUE, + BlankEval.INSTANCE, + }; + range = createAreaEval("A1:B2", values); + confirmCountIf(3, range, new NumberEval(2)); + // note - same results when criteria is a string that parses as the number with the same value + confirmCountIf(3, range, new StringEval("2.00")); + + if (false) { // not supported yet: + // when criteria is an expression (starting with a comparison operator) + confirmCountIf(4, range, new StringEval(">1")); + } + } + /** + * special case where the criteria argument is a cell reference + */ + public void testCountIfWithCriteriaReference() { + + ValueEval[] values = { + new NumberEval(22), + new NumberEval(25), + new NumberEval(21), + new NumberEval(25), + new NumberEval(25), + new NumberEval(25), + }; + Area2DEval arg0 = new Area2DEval(new AreaPtg("C1:C6"), values); + + Ref2DEval criteriaArg = new Ref2DEval(new ReferencePtg("A1"), new NumberEval(25), true); + Eval[] args= { arg0, criteriaArg, }; + + double actual = NumericFunctionInvoker.invoke(new Countif(), args); + assertEquals(4, actual, 0D); + } + + + private static AreaEval createAreaEval(String areaRefStr, ValueEval[] values) { + return new Area2DEval(new AreaPtg(areaRefStr), values); + } + + private static void confirmCountA(int expected, Eval[] args) { + double result = NumericFunctionInvoker.invoke(new Counta(), args); + assertEquals(expected, result, 0); + } + private static void confirmCountIf(int expected, AreaEval range, Eval criteria) { + + Eval[] args = { range, criteria, }; + double result = NumericFunctionInvoker.invoke(new Countif(), args); + assertEquals(expected, result, 0); + } +} diff --git a/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/functions/TestIndex.java b/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/functions/TestIndex.java new file mode 100755 index 000000000..902c4122e --- /dev/null +++ b/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/functions/TestIndex.java @@ -0,0 +1,89 @@ +/* ==================================================================== + 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.Eval; +import org.apache.poi.hssf.record.formula.eval.NumberEval; +import org.apache.poi.hssf.record.formula.eval.ValueEval; + +/** + * Tests for the INDEX() function + * + * @author Josh Micich + */ +public final class TestIndex extends TestCase { + + public TestIndex(String testName) { + super(testName); + } + + private static final double[] TEST_VALUES0 = { + 1, 2, + 3, 4, + 5, 6, + 7, 8, + 9, 10, + 11, 12, + 13, // excess array element. TODO - Area2DEval currently has no validation to ensure correct size of values array + }; + + /** + * For the case when the first argument to INDEX() is an area reference + */ + public void testEvaluateAreaReference() { + + double[] values = TEST_VALUES0; + confirmAreaEval("C1:D6", values, 4, 1, 7); + confirmAreaEval("C1:D6", values, 6, 2, 12); + confirmAreaEval("C1:D6", values, 3, -1, 5); + + // now treat same data as 3 columns, 4 rows + confirmAreaEval("C10:E13", values, 2, 2, 5); + confirmAreaEval("C10:E13", values, 4, -1, 10); + } + + /** + * @param areaRefString in Excel notation e.g. 'D2:E97' + * @param dValues array of evaluated values for the area reference + * @param rowNum 1-based + * @param colNum 1-based, pass -1 to signify argument not present + */ + private static void confirmAreaEval(String areaRefString, double[] dValues, + int rowNum, int colNum, double expectedResult) { + ValueEval[] values = new ValueEval[dValues.length]; + for (int i = 0; i < values.length; i++) { + values[i] = new NumberEval(dValues[i]); + } + Area2DEval arg0 = new Area2DEval(new AreaPtg(areaRefString), values); + + Eval[] args; + if (colNum > 0) { + args = new Eval[] { arg0, new NumberEval(rowNum), new NumberEval(colNum), }; + } else { + args = new Eval[] { arg0, new NumberEval(rowNum), }; + } + + double actual = NumericFunctionInvoker.invoke(new Index(), args); + assertEquals(expectedResult, actual, 0D); + } +} diff --git a/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/functions/TestRowCol.java b/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/functions/TestRowCol.java new file mode 100755 index 000000000..4002c30d0 --- /dev/null +++ b/src/scratchpad/testcases/org/apache/poi/hssf/record/formula/functions/TestRowCol.java @@ -0,0 +1,102 @@ +/* ==================================================================== + 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.Eval; + +/** + * Tests for ROW(), ROWS(), COLUMN(), COLUMNS() + * + * @author Josh Micich + */ +public final class TestRowCol extends TestCase { + + public TestRowCol(String testName) { + super(testName); + } + + public void testCol() { + Function target = new Column(); + { + Eval[] args = { EvalFactory.createRefEval("C5"), }; + double actual = NumericFunctionInvoker.invoke(target, args); + assertEquals(3, actual, 0D); + } + { + Eval[] args = { EvalFactory.createAreaEval("E2:H12", 4, 11), }; + double actual = NumericFunctionInvoker.invoke(target, args); + assertEquals(5, actual, 0D); + } + } + + public void testRow() { + Function target = new Row(); + { + Eval[] args = { EvalFactory.createRefEval("C5"), }; + double actual = NumericFunctionInvoker.invoke(target, args); + assertEquals(5, actual, 0D); + } + { + Eval[] args = { EvalFactory.createAreaEval("E2:H12", 4, 11), }; + double actual = NumericFunctionInvoker.invoke(target, args); + assertEquals(2, actual, 0D); + } + } + + public void testColumns() { + + confirmColumnsFunc("A1:F1", 6, 1); + confirmColumnsFunc("A1:C2", 3, 2); + confirmColumnsFunc("A1:B3", 2, 3); + confirmColumnsFunc("A1:A6", 1, 6); + + Eval[] args = { EvalFactory.createRefEval("C5"), }; + double actual = NumericFunctionInvoker.invoke(new Columns(), args); + assertEquals(1, actual, 0D); + } + + public void testRows() { + + confirmRowsFunc("A1:F1", 6, 1); + confirmRowsFunc("A1:C2", 3, 2); + confirmRowsFunc("A1:B3", 2, 3); + confirmRowsFunc("A1:A6", 1, 6); + + Eval[] args = { EvalFactory.createRefEval("C5"), }; + double actual = NumericFunctionInvoker.invoke(new Rows(), args); + assertEquals(1, actual, 0D); + } + + private static void confirmRowsFunc(String areaRefStr, int nCols, int nRows) { + Eval[] args = { EvalFactory.createAreaEval(areaRefStr, nCols, nRows), }; + + double actual = NumericFunctionInvoker.invoke(new Rows(), args); + assertEquals(nRows, actual, 0D); + } + + + private static void confirmColumnsFunc(String areaRefStr, int nCols, int nRows) { + Eval[] args = { EvalFactory.createAreaEval(areaRefStr, nCols, nRows), }; + + double actual = NumericFunctionInvoker.invoke(new Columns(), args); + assertEquals(nCols, actual, 0D); + } +} diff --git a/src/scratchpad/testcases/org/apache/poi/hssf/usermodel/TestBug44297.java b/src/scratchpad/testcases/org/apache/poi/hssf/usermodel/TestBug44297.java new file mode 100755 index 000000000..ce4afd36f --- /dev/null +++ b/src/scratchpad/testcases/org/apache/poi/hssf/usermodel/TestBug44297.java @@ -0,0 +1,103 @@ +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; + +/** + * Bug 44297: 32767+32768 is evaluated to -1 + * Fix: IntPtg must operate with unsigned short. Reading signed short results in incorrect formula calculation + * if a formula has values in the interval [Short.MAX_VALUE, (Short.MAX_VALUE+1)*2] + * + * @author Yegor Kozlov + */ + +public class TestBug44297 extends TestCase { + protected String cwd = System.getProperty("HSSF.testdata.path"); + + public void test44297() throws IOException { + FileInputStream in = new FileInputStream(new File(cwd, "44297.xls")); + HSSFWorkbook wb = new HSSFWorkbook(in); + in.close(); + + HSSFRow row; + HSSFCell cell; + + HSSFSheet sheet = wb.getSheetAt(0); + + HSSFFormulaEvaluator eva = new HSSFFormulaEvaluator(sheet, wb); + + row = (HSSFRow)sheet.getRow(0); + cell = row.getCell((short)0); + assertEquals("31+46", cell.getCellFormula()); + eva.setCurrentRow(row); + assertEquals(77, eva.evaluate(cell).getNumberValue(), 0); + + row = (HSSFRow)sheet.getRow(1); + cell = row.getCell((short)0); + assertEquals("30+53", cell.getCellFormula()); + eva.setCurrentRow(row); + assertEquals(83, eva.evaluate(cell).getNumberValue(), 0); + + row = (HSSFRow)sheet.getRow(2); + cell = row.getCell((short)0); + assertEquals("SUM(A1:A2)", cell.getCellFormula()); + eva.setCurrentRow(row); + assertEquals(160, eva.evaluate(cell).getNumberValue(), 0); + + row = (HSSFRow)sheet.getRow(4); + cell = row.getCell((short)0); + assertEquals("32767+32768", cell.getCellFormula()); + eva.setCurrentRow(row); + assertEquals(65535, eva.evaluate(cell).getNumberValue(), 0); + + row = (HSSFRow)sheet.getRow(7); + cell = row.getCell((short)0); + assertEquals("32744+42333", cell.getCellFormula()); + eva.setCurrentRow(row); + assertEquals(75077, eva.evaluate(cell).getNumberValue(), 0); + + row = (HSSFRow)sheet.getRow(8); + cell = row.getCell((short)0); + assertEquals("327680.0/32768", cell.getCellFormula()); + eva.setCurrentRow(row); + assertEquals(10, eva.evaluate(cell).getNumberValue(), 0); + + row = (HSSFRow)sheet.getRow(9); + cell = row.getCell((short)0); + assertEquals("32767+32769", cell.getCellFormula()); + eva.setCurrentRow(row); + assertEquals(65536, eva.evaluate(cell).getNumberValue(), 0); + + row = (HSSFRow)sheet.getRow(10); + cell = row.getCell((short)0); + assertEquals("35000+36000", cell.getCellFormula()); + eva.setCurrentRow(row); + assertEquals(71000, eva.evaluate(cell).getNumberValue(), 0); + + row = (HSSFRow)sheet.getRow(11); + cell = row.getCell((short)0); + assertEquals("-1000000.0-3000000.0", cell.getCellFormula()); + eva.setCurrentRow(row); + assertEquals(-4000000, eva.evaluate(cell).getNumberValue(), 0); + } + +} diff --git a/src/scratchpad/testcases/org/apache/poi/hssf/usermodel/TestFormulaEvaluatorDocs.java b/src/scratchpad/testcases/org/apache/poi/hssf/usermodel/TestFormulaEvaluatorDocs.java index cd2acc7ea..6c2e3b641 100644 --- a/src/scratchpad/testcases/org/apache/poi/hssf/usermodel/TestFormulaEvaluatorDocs.java +++ b/src/scratchpad/testcases/org/apache/poi/hssf/usermodel/TestFormulaEvaluatorDocs.java @@ -82,7 +82,7 @@ public class TestFormulaEvaluatorDocs extends TestCase { assertEquals(HSSFCell.CELL_TYPE_FORMULA, wb.getSheetAt(0).getRow(1).getCell((short)2).getCellType()); assertEquals(22.3, wb.getSheetAt(1).getRow(0).getCell((short)0).getNumericCellValue(), 0); - assertEquals("S1!A1", wb.getSheetAt(1).getRow(0).getCell((short)0).getCellFormula()); + assertEquals("'S1'!A1", wb.getSheetAt(1).getRow(0).getCell((short)0).getCellFormula()); assertEquals(HSSFCell.CELL_TYPE_FORMULA, wb.getSheetAt(1).getRow(0).getCell((short)0).getCellType()); diff --git a/src/scratchpad/testcases/org/apache/poi/hwpf/data/Bug44292.doc b/src/scratchpad/testcases/org/apache/poi/hwpf/data/Bug44292.doc new file mode 100644 index 000000000..fd7ca6cc3 Binary files /dev/null and b/src/scratchpad/testcases/org/apache/poi/hwpf/data/Bug44292.doc differ diff --git a/src/scratchpad/testcases/org/apache/poi/hwpf/usermodel/TestProblems.java b/src/scratchpad/testcases/org/apache/poi/hwpf/usermodel/TestProblems.java index 8e7f47ed9..e82c4d130 100644 --- a/src/scratchpad/testcases/org/apache/poi/hwpf/usermodel/TestProblems.java +++ b/src/scratchpad/testcases/org/apache/poi/hwpf/usermodel/TestProblems.java @@ -74,4 +74,34 @@ public class TestProblems extends TestCase { } } } + + /** + * Test for TableCell not skipping the last paragraph + */ + public void testTableCellLastParagraph() throws Exception { + HWPFDocument doc = new HWPFDocument(new FileInputStream(dirname + "/Bug44292.doc")); + Range r = doc.getRange(); + + //get the table + Paragraph p = r.getParagraph(0); + Table t = r.getTable(p); + + //get the only row + TableRow row = t.getRow(0); + + //get the first cell + TableCell cell = row.getCell(0); + // First cell should have one paragraph + assertEquals(1, cell.numParagraphs()); + + //get the second + cell = row.getCell(1); + // Second cell should be detected as having two paragraphs + assertEquals(2, cell.numParagraphs()); + + //get the last cell + cell = row.getCell(2); + // Last cell should have one paragraph + assertEquals(1, cell.numParagraphs()); + } } diff --git a/src/testcases/org/apache/poi/hssf/HSSFTests.java b/src/testcases/org/apache/poi/hssf/HSSFTests.java index 1bc9df179..1e0edd682 100644 --- a/src/testcases/org/apache/poi/hssf/HSSFTests.java +++ b/src/testcases/org/apache/poi/hssf/HSSFTests.java @@ -75,13 +75,7 @@ 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.TestAreaErrPtg; -import org.apache.poi.hssf.record.formula.TestErrPtg; -import org.apache.poi.hssf.record.formula.TestFuncPtg; -import org.apache.poi.hssf.record.formula.TestIntersectionPtg; -import org.apache.poi.hssf.record.formula.TestPercentPtg; -import org.apache.poi.hssf.record.formula.TestRangePtg; -import org.apache.poi.hssf.record.formula.TestUnionPtg; +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; @@ -215,13 +209,7 @@ public class HSSFTests suite.addTest(new TestSuite(TestSheetReferences.class)); - suite.addTest(new TestSuite(TestAreaErrPtg.class)); - suite.addTest(new TestSuite(TestErrPtg.class)); - suite.addTest(new TestSuite(TestFuncPtg.class)); - suite.addTest(new TestSuite(TestIntersectionPtg.class)); - suite.addTest(new TestSuite(TestPercentPtg.class)); - suite.addTest(new TestSuite(TestRangePtg.class)); - suite.addTest(new TestSuite(TestUnionPtg.class)); + suite.addTest(AllFormulaTests.suite()); suite.addTest(new TestSuite(TestValueRecordsAggregate.class)); suite.addTest(new TestSuite(TestNameRecord.class)); suite.addTest(new TestSuite(TestEventRecordFactory.class)); diff --git a/src/testcases/org/apache/poi/hssf/data/HyperlinksOnManySheets.xls b/src/testcases/org/apache/poi/hssf/data/HyperlinksOnManySheets.xls new file mode 100755 index 000000000..1884e57c5 Binary files /dev/null and b/src/testcases/org/apache/poi/hssf/data/HyperlinksOnManySheets.xls differ diff --git a/src/testcases/org/apache/poi/hssf/data/SimpleWithChoose.xls b/src/testcases/org/apache/poi/hssf/data/SimpleWithChoose.xls new file mode 100755 index 000000000..96a8e743a Binary files /dev/null and b/src/testcases/org/apache/poi/hssf/data/SimpleWithChoose.xls differ diff --git a/src/testcases/org/apache/poi/hssf/data/TestDataValidation.xls b/src/testcases/org/apache/poi/hssf/data/TestDataValidation.xls index a9460375c..0b2a86948 100644 Binary files a/src/testcases/org/apache/poi/hssf/data/TestDataValidation.xls and b/src/testcases/org/apache/poi/hssf/data/TestDataValidation.xls differ diff --git a/src/testcases/org/apache/poi/hssf/data/WithHyperlink.xls b/src/testcases/org/apache/poi/hssf/data/WithHyperlink.xls new file mode 100644 index 000000000..e136506c2 Binary files /dev/null and b/src/testcases/org/apache/poi/hssf/data/WithHyperlink.xls differ diff --git a/src/testcases/org/apache/poi/hssf/data/WithTwoHyperLinks.xls b/src/testcases/org/apache/poi/hssf/data/WithTwoHyperLinks.xls new file mode 100644 index 000000000..6ee60b535 Binary files /dev/null and b/src/testcases/org/apache/poi/hssf/data/WithTwoHyperLinks.xls differ diff --git a/src/testcases/org/apache/poi/hssf/eventusermodel/TestHSSFEventFactory.java b/src/testcases/org/apache/poi/hssf/eventusermodel/TestHSSFEventFactory.java index bd936a0af..049b43ef9 100644 --- a/src/testcases/org/apache/poi/hssf/eventusermodel/TestHSSFEventFactory.java +++ b/src/testcases/org/apache/poi/hssf/eventusermodel/TestHSSFEventFactory.java @@ -23,8 +23,13 @@ import java.io.File; import java.io.FileInputStream; import java.util.ArrayList; +import org.apache.poi.hssf.record.DVALRecord; +import org.apache.poi.hssf.record.DVRecord; +import org.apache.poi.hssf.record.EOFRecord; import org.apache.poi.hssf.record.Record; import org.apache.poi.hssf.record.ContinueRecord; +import org.apache.poi.hssf.record.SelectionRecord; +import org.apache.poi.hssf.record.WindowTwoRecord; import org.apache.poi.poifs.filesystem.POIFSFileSystem; import junit.framework.TestCase; @@ -48,7 +53,15 @@ public class TestHSSFEventFactory extends TestCase { factory.processWorkbookEvents(req, fs); // Check we got the records + System.out.println("Processed, found " + mockListen.records.size() + " records"); assertTrue( mockListen.records.size() > 100 ); + + // Check that the last few records are as we expect + // (Makes sure we don't accidently skip the end ones) + int numRec = mockListen.records.size(); + assertEquals(WindowTwoRecord.class, mockListen.records.get(numRec-3).getClass()); + assertEquals(SelectionRecord.class, mockListen.records.get(numRec-2).getClass()); + assertEquals(EOFRecord.class, mockListen.records.get(numRec-1).getClass()); } public void testWithCrazyContinueRecords() throws Exception { @@ -66,6 +79,7 @@ public class TestHSSFEventFactory extends TestCase { factory.processWorkbookEvents(req, fs); // Check we got the records + System.out.println("Processed, found " + mockListen.records.size() + " records"); assertTrue( mockListen.records.size() > 100 ); // And none of them are continue ones @@ -74,6 +88,13 @@ public class TestHSSFEventFactory extends TestCase { for(int i=0; i