There are several main functions: GetNewSheetStruct() This returns a structure that defines the Excel sheet object

. Write functions expect either an array of these types of object (one per sheet) or a single instance (defining a one-sheet document). ReadExcel() This reads an Excel file into an array of structures that contains the Excel file information OR if a specific sheet index is passed in, only that sheet object is returned. ReadExcelSheet() This takes a given WorkBook instance and reads the given sheet into a Sheet structure. It can be access by the public, but is meant to be used primarily by the ReadExcel() method. WriteExcel() This takes an array of Sheet structure objects and writes each of them to a tab in the resulting Excel file OR it takes a single Sheet object and writes that to the first tab of the resulting Excel file. WriteExcelSheet() This takes a workbook and writes the given sheet data to the Sheet of an Excel file. This can be used by the public but is meant to be used primarily by WriteExcel() method. WriteSingleExcel() This allows the user to write a single query to an Excel file without having to create an intermediary Sheet object. This is just a convenient short hand that allows you to bypass the intermediary "Sheet" structure - underneath, it just packages the data and, in-turn, calls the WriteExcel() method. Here is the code for the beta POI utiltiy ColdFusion component, POIUtility.cfc: Launch code in new window » Download code as text file »
y y y y y y

<cfcomponent displayname="POIUtility" output="false" hint="Handles the reading and writing of Microsoft Excel files using POI and ColdFusion.">

y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y

<cffunction name="Init" access="public" returntype="POIUtility" output="false" hint="Returns an initialized POI Utility instance."> <!--- Return This reference. ---> <cfreturn THIS /> </cffunction>

<cffunction name="GetNewSheetStruct" access="public" returntype="struct" output="false" hint="Returns a default structure of what this Component is expecting for a sheet definition when WRITING Excel files."> <!--- Define the local scope. ---> <cfset var LOCAL = StructNew() /> <cfscript> // This is the query that will hold the data. LOCAL.Query = ""; // THis is the list of columns (in a given order) that will be // used to output data. LOCAL.ColumnList = ""; // These are the names of the columns used when creating a header // row in the Excel file. LOCAL.ColumnNames = ""; // This is the name of the sheet as it appears in the bottom Excel tab. LOCAL.SheetName = ""; // Return the local structure containing the sheet info. return( LOCAL ); </cfscript> </cffunction>

<cffunction name="ReadExcel" access="public" returntype="any" output="false" hint="Reads an Excel file into an array of strutures that contains the Excel file information OR if a specific sheet index is passed in, only that sheet object is returned."> <!--- Define arguments. ---> <cfargument name="FilePath"

y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y

type="string" required="true" hint="The expanded file path of the Excel file." /> <cfargument name="HasHeaderRow" type="boolean" required="false" default="false" hint="Flags the Excel files has using the first data row a header column. If so, this column will be excluded from the resultant query." /> <cfargument name="SheetIndex" type="numeric" required="false" default="-1" hint="If passed in, only that sheet object will be returned (not an array of sheet objects)." /> <cfscript> // Define the local scope. var LOCAL = StructNew(); // Create the Excel file system object. This object is responsible // for reading in the given Excel file. LOCAL.ExcelFileSystem = CreateObject( "java", "org.apache.poi.poifs.filesystem.POIFSFileSystem" ).Init( // Create the file input stream. CreateObject( "java", "java.io.FileInputStream" ).Init( ARGUMENTS.FilePath ) );

y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y

// Get the workbook from the Excel file system. LOCAL.WorkBook = CreateObject( "java", "org.apache.poi.hssf.usermodel.HSSFWorkbook" ).Init( LOCAL.ExcelFileSystem );

// Check to see if we are returning an array of sheets OR just // a given sheet. if (ARGUMENTS.SheetIndex GTE 0){ // We just want a given sheet, so return that. return( ReadExcelSheet( LOCAL.WorkBook, ARGUMENTS.SheetIndex, ARGUMENTS.HasHeaderRow ) ); } else { // No specific sheet was requested. We are going to return an array // of sheets within the Excel document. // Create an array to return. LOCAL.Sheets = ArrayNew( 1 ); // Loop over the sheets in the documnet. for ( LOCAL.SheetIndex = 0 ; LOCAL.SheetIndex LT LOCAL.WorkBook.GetNumberOfSheets() ; LOCAL.SheetIndex = (LOCAL.SheetIndex + 1) ){ // Add the sheet information. ArrayAppend( LOCAL.Sheets, ReadExcelSheet( LOCAL.WorkBook, LOCAL.SheetIndex, ARGUMENTS.HasHeaderRow ) );

y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y

} // Return the array of sheets. return( LOCAL.Sheets ); } </cfscript> </cffunction>

<cffunction name="ReadExcelSheet" access="public" returntype="struct" output="false" hint="Takes an Excel workbook and reads the given sheet (by index) into a structure."> <!--- Define arguments. ---> <cfargument name="WorkBook" type="any" required="true" hint="This is a workbook object created by the POI API." /> <cfargument name="SheetIndex" type="numeric" required="false" default="0" hint="This is the index of the sheet within the passed in workbook. This is a ZERO-based index (coming from a Java object)." /> <cfargument name="HasHeaderRow" type="boolean" required="false" default="false" hint="This flags the sheet as having a header row or not (if so, it will NOT be read into the query)." /> <cfscript> // Define the local scope. var LOCAL = StructNew();

y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y

// Set up the default return structure. LOCAL.SheetData = StructNew(); // This is the index of the sheet within the workbook. LOCAL.SheetData.Index = ARGUMENTS.SheetIndex; // This is the name of the sheet tab. LOCAL.SheetData.Name = ARGUMENTS.WorkBook.GetSheetName( JavaCast( "int", ARGUMENTS.SheetIndex ) ); // This is the query created from the sheet. LOCAL.SheetData.Query = ""; // This is a flag for the header row. LOCAL.SheetData.HasHeaderRow = ARGUMENTS.HasHeaderRow; // An array of header columns names. LOCAL.SheetData.ColumnNames = ArrayNew( 1 ); // This keeps track of the min number of data columns. LOCAL.SheetData.MinColumnCount = 0; // This keeps track of the max number of data columns. LOCAL.SheetData.MaxColumnCount = 0;

// Get the sheet object at this index of the // workbook. This is based on the passed in data. LOCAL.Sheet = ARGUMENTS.WorkBook.GetSheetAt( JavaCast( "int", ARGUMENTS.SheetIndex ) );

// Loop over the rows in the Excel sheet. For each // row, we simply want to capture the number of // physical columns we are working with that are NOT // blank. We will then use that data to figure out // how many columns we should be using in our query. for ( LOCAL.RowIndex = 0 ; LOCAL.RowIndex LT LOCAL.Sheet.GetPhysicalNumberOfRows() ; LOCAL.RowIndex = (LOCAL.RowIndex + 1) ){ // Get a reference to the current row.

y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y

LOCAL.Row = LOCAL.Sheet.GetRow( JavaCast( "int", LOCAL.RowIndex ) ); // Get the number of physical cells in this row. While I think that // this can possibly change from row to row, for the purposes of // simplicity, I am going to assume that all rows are uniform and // that this row is a model of how the rest of the data will be // displayed. LOCAL.ColumnCount = LOCAL.Row.GetPhysicalNumberOfCells(); // Check to see if the query variable we have it actually a query. // If we have not done anything to it yet, then it should still // just be a string value (Yahoo for dynamic typing!!!). If that // is the case, then let's use this first data row to set up the // query object. if (NOT IsQuery( LOCAL.SheetData.Query )){ // Create an empty query. Doing it this way creates a query // with neither column nor row values. LOCAL.SheetData.Query = QueryNew( "" ); // Now that we have an empty query, we are going to loop over // the cells COUNT for this data row and for each cell, we are // going to create a query column of type VARCHAR. I understand // that cells are going to have different data types, but I am // chosing to store everything as a string to make it easier. for ( LOCAL.ColumnIndex = 0 ; LOCAL.ColumnIndex LT LOCAL.ColumnCount ; LOCAL.ColumnIndex = (LOCAL.ColumnIndex + 1) ){ // Add the column. Notice that the name of the column is // the text "column" plus the column index. I am starting // my column indexes at ONE rather than ZERO to get it back // into a more ColdFusion standard notation. QueryAddColumn( LOCAL.SheetData.Query, "column#(LOCAL.ColumnIndex + 1)#", "CF_SQL_VARCHAR", ArrayNew( 1 ) );

// Check to see if we are using a header row. If so, we

y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y

// want to capture the header row values into an array // of header column names. if (ARGUMENTS.HasHeaderRow){ // Try to get a header column name (it might throw // an error). try { ArrayAppend( LOCAL.SheetData.ColumnNames, LOCAL.Row.GetCell( JavaCast( "int", LOCAL.ColumnIndex ) ).GetStringCellValue() ); } catch (any ErrorHeader){ // There was an error grabbing the text of the header // column type. Just add an empty string to make up // for it. ArrayAppend( LOCAL.SheetData.ColumnNames, "" ); } } } // Set the default min and max column count based on this first row. LOCAL.SheetData.MinColumnCount = LOCAL.ColumnCount; LOCAL.SheetData.MaxColumnCount = LOCAL.ColumnCount; }

// ASSERT: Whether we are on our first Excel data row or // our Nth data row, at this point, we have a ColdFusion // query object that has the proper columns defined.

// Update the running min column count. LOCAL.SheetData.MinColumnCount = Min( LOCAL.SheetData.MinColumnCount,

y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y

LOCAL.ColumnCount ); // Update the running max column count. LOCAL.SheetData.MaxColumnCount = Max( LOCAL.SheetData.MaxColumnCount, LOCAL.ColumnCount );

// Add a row to the query so that we can store this row's // data values. QueryAddRow( LOCAL.SheetData.Query );

// Loop over the cells in this row to find values. for ( LOCAL.ColumnIndex = 0 ; LOCAL.ColumnIndex LT LOCAL.ColumnCount ; LOCAL.ColumnIndex = (LOCAL.ColumnIndex + 1) ){ // When getting the value of a cell, it is important to know // what type of cell value we are dealing with. If you try // to grab the wrong value type, an error might be thrown. // For that reason, we must check to see what type of cell // we are working with. These are the cell types and they // are constants of the cell object itself: // // 0 - CELL_TYPE_NUMERIC // 1 - CELL_TYPE_STRING // 2 - CELL_TYPE_FORMULA // 3 - CELL_TYPE_BLANK // 4 - CELL_TYPE_BOOLEAN // 5 - CELL_TYPE_ERROR // Get the cell from the row object. LOCAL.Cell = LOCAL.Row.GetCell( JavaCast( "int", LOCAL.ColumnIndex ) ); // Get the type of data in this cell. LOCAL.CellType = LOCAL.Cell.GetCellType(); // Get teh value of the cell based on the data type. The thing // to worry about here is cell forumlas and cell dates. Formulas

y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y

// can be strange and dates are stored as numeric types. For // this demo, I am not going to worry about that at all. I will // just grab dates as floats and formulas I will try to grab as // numeric values. if (LOCAL.CellType EQ LOCAL.Cell.CELL_TYPE_NUMERIC) { // Get numeric cell data. This could be a standard number, // could also be a date value. I am going to leave it up to // the calling program to decide. LOCAL.CellValue = LOCAL.Cell.GetNumericCellValue(); } else if (LOCAL.CellType EQ LOCAL.Cell.CELL_TYPE_STRING){ LOCAL.CellValue = LOCAL.Cell.GetStringCellValue(); } else if (LOCAL.CellType EQ LOCAL.Cell.CELL_TYPE_FORMULA){ // Since most forumlas deal with numbers, I am going to try // to grab the value as a number. If that throws an error, I // will just grab it as a string value. try { LOCAL.CellValue = LOCAL.Cell.GetNumericCellValue(); } catch (any Error1){ // The numeric grab failed. Try to get the value as a // string. If this fails, just force the empty string. try { LOCAL.CellValue = LOCAL.Cell.GetStringCellValue(); } catch (any Error2){ // Force empty string. LOCAL.CellValue = ""; } } } else if (LOCAL.CellType EQ LOCAL.Cell.CELL_TYPE_BLANK){ LOCAL.CellValue = ""; } else if (LOCAL.CellType EQ LOCAL.Cell.CELL_TYPE_BOOLEAN){

y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y

LOCAL.CellValue = LOCAL.Cell.GetBooleanCellValue(); } else { // If all else fails, get empty string. LOCAL.CellValue = ""; }

// ASSERT: At this point, we either got the cell value out of the // Excel data cell or we have thrown an error or didn't get a // matching type and just have the empty string by default. // No matter what, the object LOCAL.CellValue is defined and // has some sort of SIMPLE ColdFusion value in it.

// Now that we have a value, store it as a string in the ColdFusion // query object. Remember again that my query names are ONE based // for ColdFusion standards. That is why I am adding 1 to the // cell index. LOCAL.SheetData.Query[ "column#(LOCAL.ColumnIndex + 1)#" ][ LOCAL.SheetData.Query.RecordCount ] = JavaCast( "string", LOCAL.CellValue ); } }

// At this point we should have a full query of data. However, if // we were using a header row, then the header row was included in // the final query. We do NOT want this. If we are using a header // row, delete the first row of the query. if ( ARGUMENTS.HasHeaderRow AND LOCAL.SheetData.Query.RecordCount ){ // Delete the first row which is the header row. LOCAL.SheetData.Query.RemoveRows( JavaCast( "int", 0 ), JavaCast( "int", 1 ) ); }

y y y y y y y y y y y y y y y y y y y y y y y y y y

// Return the sheet object that contains all the Excel data. return( LOCAL.SheetData ); </cfscript> </cffunction>

<cffunction name="WriteExcel" access="public" returntype="void" output="false" hint="Takes an array of 'Sheet' structure objects and writes each of them to a tab in the Excel file."> <!--- Define arguments. ---> <cfargument name="FilePath" type="string" required="true" hint="This is the expanded path of the Excel file." /> <cfargument name="Sheets" type="any" required="true" hint="This is an array of the data that is needed for each sheet of the excel OR it is a single Sheet object. Each 'Sheet' will be a structure containing the Query, ColumnList, ColumnNames, and SheetName." /> <cfargument name="Delimiters" type="string" required="false" default="," hint="The list of delimiters used for the column list and column name arguments." /> <cfscript> // Set up local scope. var LOCAL = StructNew(); // Create Excel workbook. LOCAL.WorkBook = CreateObject(

y y y y y y y y y y y y y y y y y

y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y

"java", "org.apache.poi.hssf.usermodel.HSSFWorkbook" ).Init();

// Check to see if we are dealing with an array of sheets or if we were // passed in a single sheet. if (IsArray( ARGUMENTS.Sheets )){ // This is an array of sheets. We are going to write each one of them // as a tab to the Excel file. Loop over the sheet array to create each // sheet for the already created workbook. for ( LOCAL.SheetIndex = 1 ; LOCAL.SheetIndex LTE ArrayLen( ARGUMENTS.Sheets ) ; LOCAL.SheetIndex = (LOCAL.SheetIndex + 1) ){

// Create sheet for the given query information.. WriteExcelSheet( WorkBook = LOCAL.WorkBook, Query = ARGUMENTS.Sheets[ LOCAL.SheetIndex ].Query, ColumnList = ARGUMENTS.Sheets[ LOCAL.SheetIndex ].ColumnList, ColumnNames = ARGUMENTS.Sheets[ LOCAL.SheetIndex ].ColumnNames, SheetName = ARGUMENTS.Sheets[ LOCAL.SheetIndex ].SheetName, Delimiters = ARGUMENTS.Delimiters ); } } else { // We were passed in a single sheet object. Write this sheet as the // first and only sheet in the already created workbook. WriteExcelSheet( WorkBook = LOCAL.WorkBook, Query = ARGUMENTS.Sheets.Query, ColumnList = ARGUMENTS.Sheets.ColumnList, ColumnNames = ARGUMENTS.Sheets.ColumnNames, SheetName = ARGUMENTS.Sheets.SheetName, Delimiters = ARGUMENTS.Delimiters ); }

y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y

// ASSERT: At this point, either we were passed a single Sheet object // or we were passed an array of sheets. Either way, we now have all // of sheets written to the WorkBook object.

// Create a file based on the path that was passed in. We will stream // the work data to the file via a file output stream. LOCAL.FileOutputStream = CreateObject( "java", "java.io.FileOutputStream" ).Init( JavaCast( "string", ARGUMENTS.FilePath ) ); // Write the workout data to the file stream. LOCAL.WorkBook.Write( LOCAL.FileOutputStream ); // Close the file output stream. This will release any locks on // the file and finalize the process. LOCAL.FileOutputStream.Close(); // Return out. return; </cfscript> </cffunction>

<cffunction name="WriteExcelSheet" access="public" returntype="void" output="false" hint="Writes the given 'Sheet' structure to the given workbook."> <!--- Define arguments. ---> <cfargument name="WorkBook" type="any" required="true" hint="This is the Excel workbook that will create the sheets." />

y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y

<cfargument name="Query" type="any" required="true" hint="This is the query from which we will get the data." /> <cfargument name="ColumnList" type="string" required="false" default="#ARGUMENTS.Query.ColumnList#" hint="This is list of columns provided in custom-ordered." /> <cfargument name="ColumnNames" type="string" required="false" default="" hint="This the the list of optional header-row column names. If this is not provided, no header row is used." /> <cfargument name="SheetName" type="string" required="false" default="Sheet #(ARGUMENTS.WorkBook.GetNumberOfSheets() + 1)#" hint="This is the optional name that appears in this sheet's tab." /> <cfargument name="Delimiters" type="string" required="false" default="," hint="The list of delimiters used for the column list and column name arguments." /> <cfscript> // Set up local scope. var LOCAL = StructNew();

y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y

// Set up data type map so that we can map each column name to // the type of data contained. LOCAL.DataMap = StructNew(); // Get the meta data of the query to help us create the data mappings. LOCAL.MetaData = GetMetaData( ARGUMENTS.Query ); // Loop over meta data values to set up the data mapping. for ( LOCAL.MetaIndex = 1 ; LOCAL.MetaIndex LTE ArrayLen( LOCAL.MetaData ) ; LOCAL.MetaIndex = (LOCAL.MetaIndex + 1) ){ // Map the column name to the data type. LOCAL.DataMap[ LOCAL.MetaData[ LOCAL.MetaIndex ].Name ] = LOCAL.MetaData[ LOCAL.MetaIndex ].TypeName; }

// Create the sheet in the workbook. LOCAL.Sheet = ARGUMENTS.WorkBook.CreateSheet( JavaCast( "string", ARGUMENTS.SheetName ) ); // Set a default row offset so that we can keep add the header // column without worrying about it later. LOCAL.RowOffset = -1; // Check to see if we have any column names. If we do, then we // are going to create a header row with these names in order // based on the passed in delimiter. if (Len( ARGUMENTS.ColumnNames )){ // Convert the column names to an array for easier // indexing and faster access. LOCAL.ColumnNames = ListToArray( ARGUMENTS.ColumnNames, ARGUMENTS.Delimiters ); // Create a header row. LOCAL.Row = LOCAL.Sheet.CreateRow(

y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y

JavaCast( "int", 0 ) ); // Loop over the column names. for ( LOCAL.ColumnIndex = 1 ; LOCAL.ColumnIndex LTE ArrayLen( LOCAL.ColumnNames ) ; LOCAL.ColumnIndex = (LOCAL.ColumnIndex + 1) ){ // Create a cell for this column header. LOCAL.Cell = LOCAL.Row.CreateCell( JavaCast( "int", (LOCAL.ColumnIndex - 1) ) ); // Set the cell value. LOCAL.Cell.SetCellValue( JavaCast( "string", LOCAL.ColumnNames[ LOCAL.ColumnIndex ] ) ); } // Set the row offset to zero since this will take care of // the zero-based index for the rest of the query records. LOCAL.RowOffset = 0; } // Convert the list of columns to the an array for easier // indexing and faster access. LOCAL.Columns = ListToArray( ARGUMENTS.ColumnList, ARGUMENTS.Delimiters ); // Loop over the query records to add each one to the // current sheet. for ( LOCAL.RowIndex = 1 ; LOCAL.RowIndex LTE ARGUMENTS.Query.RecordCount ; LOCAL.RowIndex = (LOCAL.RowIndex + 1) ){ // Create a row for this query record.

y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y

LOCAL.Row = LOCAL.Sheet.CreateRow( JavaCast( "int", (LOCAL.RowIndex + LOCAL.RowOffset) ) ); // Loop over the columns to create the individual data cells // and set the values. for ( LOCAL.ColumnIndex = 1 ; LOCAL.ColumnIndex LTE ArrayLen( LOCAL.Columns ) ; LOCAL.ColumnIndex = (LOCAL.ColumnIndex + 1) ){ // Create a cell for this query cell. LOCAL.Cell = LOCAL.Row.CreateCell( JavaCast( "int", (LOCAL.ColumnIndex - 1) ) ); // Get the generic cell value (short hand). LOCAL.CellValue = ARGUMENTS.Query[ LOCAL.Columns[ LOCAL.ColumnIndex ] ][ LOCAL.RowIndex ]; // Check to see how we want to set the value. Meaning, what // kind of data mapping do we want to apply? Get the data // mapping value. LOCAL.DataMapValue = LOCAL.DataMap[ LOCAL.Columns[ LOCAL.ColumnIndex ] ]; // Check to see what value type we are working with. I am // not sure what the set of values are, so trying to keep // it general. if (REFindNoCase( "int", LOCAL.DataMapValue )){ LOCAL.DataMapCast = "int"; } else if (REFindNoCase( "long", LOCAL.DataMapValue )){ LOCAL.DataMapCast = "long"; } else if (REFindNoCase( "double", LOCAL.DataMapValue )){ LOCAL.DataMapCast = "double";

y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y

} else if (REFindNoCase( "float|decimal|real|date|time", LOCAL.DataMapValue )){ LOCAL.DataMapCast = "float"; } else if (REFindNoCase( "bit", LOCAL.DataMapValue )){ LOCAL.DataMapCast = "boolean"; } else if (REFindNoCase( "char|text|memo", LOCAL.DataMapValue )){ LOCAL.DataMapCast = "string"; } else if (IsNumeric( LOCAL.CellValue )){ LOCAL.DataMapCast = "float"; } else { LOCAL.DataMapCast = "string"; } // Cet the cell value using the data map casting that we // just determined and the value that we previously grabbed // (for short hand). LOCAL.Cell.SetCellValue( JavaCast( LOCAL.DataMapCast, LOCAL.CellValue ) ); } } // Return out. return; </cfscript> </cffunction>

<cffunction name="WriteSingleExcel" access="public" returntype="void" output="false" hint="Write the given query to an Excel file.">

y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y

<!--- Define arguments. ---> <cfargument name="FilePath" type="string" required="true" hint="This is the expanded path of the Excel file." /> <cfargument name="Query" type="query" required="true" hint="This is the query from which we will get the data for the Excel file." /> <cfargument name="ColumnList" type="string" required="false" default="#ARGUMENTS.Query.ColumnList#" hint="This is list of columns provided in custom-order." /> <cfargument name="ColumnNames" type="string" required="false" default="" hint="This the the list of optional header-row column names. If this is not provided, no header row is used." /> <cfargument name="SheetName" type="string" required="false" default="Sheet 1" hint="This is the optional name that appears in the first (and only) workbook tab." /> <cfargument name="Delimiters" type="string" required="false" default="," hint="The list of delimiters used for the column list and column name arguments."

y y y y y y y y y y y y y y y y y y y y y y y y y y y y y y

/> <cfscript> // Set up local scope. var LOCAL = StructNew(); // Get a new sheet object. LOCAL.Sheet = GetNewSheetStruct(); // Set the sheet properties. LOCAL.Sheet.Query = ARGUMENTS.Query; LOCAL.Sheet.ColumnList = ARGUMENTS.ColumnList; LOCAL.Sheet.ColumnNames = ARGUMENTS.ColumnNames; LOCAL.Sheet.SheetName = ARGUMENTS.SheetName; // Write this sheet to an Excel file. WriteExcel( FilePath = ARGUMENTS.FilePath, Sheets = LOCAL.Sheet, Delimiters = ARGUMENTS.Delimiters ); // Return out. return; </cfscript> </cffunction> </cfcomponent>

To test this, I created a multi page Excel file that has some food information for different imaginary meals:

To read the Excel file above, I would do this: Launch code in new window » Download code as text file »
y y y y y y y y y y y y y y y y

<!--- Create a new instance of the POI utility. ---> <cfset objPOIUtility = CreateObject( "component", "POIUtility" ).Init() /> <!--- Get the path to our Excel document. ---> <cfset strFilePath = ExpandPath( "./meals.xls" ) /> <!--Read the Excel document into an array of Sheet objects. Each sheet object will contain the data in the Excel sheet as well as some other property-type information. ---> <cfset arrExcel = objPOIUtility.ReadExcel(

y y y

FilePath = strFilePath, HasHeaderRow = true ) />

Dumping out the arrExcel array, we get to take a look at the data that gets returned:

Notice that when reading in the Excel file, I only passed in the FilePath and the HasHeaderRow flag. This will precipitate all sheets to be read in. If, however, I passed in the optional argument, SheetIndex, only the given sheet would be read in and the return structure would be a single Sheet objects as follows: Launch code in new window » Download code as text file »
y y y y y y y y y y y

<!--Read in only the Lunch sheet. This is the seocnd Sheet of the Excel file, but since Java is ZERO-based, we are going to request the first sheet. This will return a Sheet object rather than an array of Sheet objects. ---> <cfset objSheet = objPOIUtility.ReadExcel( FilePath = strFilePath, HasHeaderRow = true, SheetIndex = 1 ) />

Notice that the only difference in this example is that we passed in the SheetIndex. CFDumping out the objSheet, we get:

Now, what you can't see is that all values from the Excel are stored in the resultant ColdFusion queries as CF_SQL_VARCHAR values. I figure this is the easiest way to deal with the data. I don't mind leaving it up to the ColdFusion programmer to figure out how to use this data. This might be fixed going forward, but so far I am fine with handling it that way. Now, that covers reading in the Excel files, which is an arguably easier task. Writing Excel files is a bit more complicated. To simplify things, especially while I am learning how to use POI with ColdFusion, I am not giving any formatting options. You can set up header rows, but other than that, data is written to the Excel sheet based on the SQL column type and nothing else. No additionally formatting is applied. One step at a time, please! Writing works in a similar way to the reading of Excel files; you can write an array of query "objects" to multiple tabs or you can write a single query to a file. Let' start off writing the meals.xls query data that we read in before (since we already have those ColdFusion queries in memory). We can't just send those objects back into the Write methods as the required structures are not quite the same. Let's create an array of new Sheet objects and then write those to the Excel: Launch code in new window » Download code as text file »
y y y y y y y y y y y y y y y y y y y y y y y y y

<!--Create an array to define the sheets that we want to pass in. We are going to use the queries that we read in previously. ---> <cfset arrSheets = ArrayNew( 1 ) /> <!--Set up a sheet for the Breakfast meal. We can get a default structure from the POI utility (as below) or we could just create our own struct of the same type (but this is a nice short hand and easy to debug). ---> <cfset arrSheets[ 1 ] = objPOIUtility.GetNewSheetStruct() /> <cfset arrSheets[ 1 ].Query = objSheet[ 1 ].Query /> <cfset arrSheets[ 1 ].SheetName = "NEW Breakfast" /> <cfset arrSheets[ 1 ].ColumnList = "column1,column2,column3" /> <cfset arrSheets[ 1 ].ColumnNames = "Food,Quantity,Tastiness" /> <!--- Set up a sheet for the Lunch meal. ---> <cfset arrSheets[ 2 ] = objPOIUtility.GetNewSheetStruct() /> <cfset arrSheets[ 2 ].Query = objSheet[ 2 ].Query /> <cfset arrSheets[ 2 ].SheetName = "NEW Lunch" /> <cfset arrSheets[ 2 ].ColumnList = "column1,column2,column3" /> <cfset arrSheets[ 2 ].ColumnNames = "Food,Quantity,Tastiness" />

y y y y y y y y y y y y y y y y y

<!--- Set up a sheet for the Dinner meal. ---> <cfset arrSheets[ 3 ] = objPOIUtility.GetNewSheetStruct() /> <cfset arrSheets[ 3 ].Query = objSheet[ 3 ].Query /> <cfset arrSheets[ 3 ].SheetName = "NEW Dinner" /> <cfset arrSheets[ 3 ].ColumnList = "column1,column2,column3" /> <cfset arrSheets[ 3 ].ColumnNames = "Food,Quantity,Tastiness" />

<!--Now that we have our array of Sheet objects, we can write them to a new Excel file. ---> <cfset objPOIUtility.WriteExcel( FilePath = ExpandPath( "./new_meals.xls" ), Sheets = arrSheets ) />

Opening the resultant new_meals.xls file, you will see that it is a duplicate of the original XLS file with new Tab names:

You may notice that in the original Excel file, the Quantity column had numeric values and that in the new Excel file, the quantity column has numbers stored as string values. This is because when the Excel file gets read in, all values get stored as numbers. Then, when writing the queries back to Excel, the POIUtility.cfc ColdFusion component sees that the query column has VARCHAR values and writes them back to the Excel file as strings. This is a byproduct of the demo (reading and writing the same file), not of an error in the Write methods. The Sheets object that gets passed in was an array, but this could have been a single Sheet object as well that would have written a single-tab Excel file. If you are interested in writing just a single-tab file without creating the intermediary Struct, you could use the WriteSingleExcel() method: Launch code in new window » Download code as text file »

y y y y y y y y y y y

<!--When writing a single file, just grab the breakfast meal from the previous read. ---> <cfset objPOIUtility.WriteSingleExcel( FilePath = ExpandPath( "./single_meal.xls" ), Query = objSheet[ 1 ].Query, ColumnList = "column1,column2,column3", ColumnNames = "Food,Quantity,Tastiness", SheetName = "SINGLE Breakfast" ) />

When we open up the resultant Excel file, you will see that we have a single tab with the NEW tab name:

That about sums it up. Like I said before, I wrote this this morning so it has not been field testing. But, I have used it to generate some sweet-ass multi-tab reports so far and I am loving it. Hope this can help some people.

Formulas
NOTE: This page is no longer updated. Most of the topics here are now covered on other pages, or have pages of their own. However, I will leave this page intact and available. See the Topics page for a complete list of topics covered on my web site.

Array Formulas
Many of the formulas described here are Array Formulas, which are a special type of formula in Excel. If you are not familiar with Array Formulas, click here.

Array To Column
Sometimes it is useful to convert an MxN array into a single column of data, for example for charting (a data series must be a single row or column). Click here for more details.

Averaging Values In A Range
You can use Excel's built in =AVERAGE function to average a range of values. By using it with other functions, you can extend its functionality. For the formulas given below, assume that our data is in the range A1:A60.

Averaging Values Between Two Numbers
Use the array formula =AVERAGE(IF((A1:A60>=Low)*(A1:A60<=High),A1:A60)) Where Low and High are the values between which you want to average.

Averaging The Highest N Numbers In A Range
To average the N largest numbers in a range, use the array formula =AVERAGE(LARGE(A1:A60,ROW(INDIRECT("1:10"))))

Change "1:10" to "1:N" where N is the number of values to average.

Averaging The Lowest N Numbers In A Range
To average the N smallest numbers in a range, use the array formula =AVERAGE(SMALL(A1:A60,ROW(INDIRECT("1:10")))) Change "1:10" to "1:N" where N is the number of values to average. In all of the formulas above, you can use =SUM instead of =AVERAGE to sum, rather than average, the numbers.

Counting Values Between Two Numbers
If you need to count the values in a range that are between two numbers, for example between 5 and 10, use the following array formula: =SUM((A1:A10>=5)*(A1:A10<=10)) To sum the same numbers, use the following array formula: =SUM((A1:A10>=5)*(A1:A10<=10)*A1:A10)

Counting Characters In A String
The following formula will count the number of "B"s, both upper and lower case, in the string in B1. =LEN(B1)-LEN(SUBSTITUTE(SUBSTITUTE(B1,"B",""),"b",""))

Date And Time Formulas
A variety of formulas useful when working with dates and times are described on the DateTime page. Other Date Related Procedures are described on the following pages.

Adding Months And Years The DATEDIF Function Date Intervals Dates And Times Date And Time Entry Holidays Julian Dates

Duplicate And Unique Values In A Range
The task of finding duplicate or unique values in a range of data requires some complicated formulas. These procedures are described in Duplicates.

Dynamic Ranges
You can define a name to refer to a range whose size varies depending on its contents. For example, you may want a range name that refers only to the portion of a list of numbers that are not blank. such as only the first N non-blank cells in A2:A20. Define a name called MyRange, and set the Refers To property to: =OFFSET(Sheet1!$A$2,0,0,COUNTA($A$2:$A$20),1) Be sure to use absolute cell references in the formula. Also see then Named Ranges page for more information about dynamic ranges.

Finding The Used Part Of A Range
Suppose we've got a range of data called DataRange2, defined as H7:I25, and that cells H7:I17 actually contain values. The rest are blank. We can find various properties of the range, as follows:

To find the range that contains data, use the following array formula: =ADDRESS(ROW(DataRange2),COLUMN(DataRange2),4)&":"& ADDRESS(MAX((DataRange2<>"")*ROW(DataRange2)),COLUMN(DataRange2) + COLUMNS(DataRange2)-1,4) This will return the range H7:I17. If you need the worksheet name in the returned range, use the following array formula: =ADDRESS(ROW(DataRange2),COLUMN(DataRange2),4,,"MySheet")&":"& ADDRESS(MAX((DataRange2<>"")*ROW(DataRange2)),COLUMN(DataRange2) + COLUMNS(DataRange2)-1,4) This will return MySheet!H7:I17. To find the number of rows that contain data, use the following array formula: =(MAX((DataRange2<>"")*ROW(DataRange2)))-ROW(DataRange2)+1 This will return the number 11, indicating that the first 11 rows of DataRange2 contain data. To find the last entry in the first column of DataRange2, use the following array formula: =INDIRECT(ADDRESS(MAX((DataRange2<>"")*ROW(DataRange2)), COLUMN(DataRange2),4)) To find the last entry in the second column of DataRange2, use the following array formula: =INDIRECT(ADDRESS(MAX((DataRange2<>"")*ROW(DataRange2)), COLUMN(DataRange2)+1,4))

First And Last Names
Suppose you've got a range of data consisting of people's first and last names. There are several formulas that will break the names apart into first and last names separately. Suppose cell A2 contains the name "John A Smith".

To return the last name, use =RIGHT(A2,LEN(A2)-FIND("*",SUBSTITUTE(A2," ","*",LEN(A2)LEN(SUBSTITUTE(A2," ",""))))) To return the first name, including the middle name (if present), use =LEFT(A2,FIND("*",SUBSTITUTE(A2," ","*",LEN(A2)LEN(SUBSTITUTE(A2," ",""))))-1) To return the first name, without the middle name (if present), use =LEFT(B2,FIND(" ",B2,1)) We can extend these ideas to the following. Suppose A1 contains the string "First Second Third Last".

Returning First Word In A String
=LEFT(A1,FIND(" ",A1,1)) This will return the word "First".

Returning Last Word In A String
=RIGHT(A1,LEN(A1)-MAX(ROW(INDIRECT("1:"&LEN(A1))) *(MID(A1,ROW(INDIRECT("1:"&LEN(A1))),1)=" "))) This formula in as array formula. (This formula comes from Laurent Longre). This will return the word "Last"

Returning All But First Word In A String
=RIGHT(A1,LEN(A1)-FIND(" ",A1,1)) This will return the words "Second Third Last"

Returning Any Word Or Words In A String
The following two array formulas come compliments of Laurent Longre. To return any single word from a single-spaced string of words, use the following array formula: =MID(A10,SMALL(IF(MID(" ("1:"&LEN(A10)+1)),1)=" B10),SUM(SMALL(IF(MID(" ("1:"&LEN(A10)+2)),1)=" B10+{0,1})*{-1,1})-1) "&A10,ROW(INDIRECT ",ROW(INDIRECT("1:"&LEN(A10)+1))), "&A10&" ",ROW(INDIRECT ",ROW(INDIRECT("1:"&LEN(A10)+2))),

Where A10 is the cell containing the text, and B10 is the number of the word you want to get. This formula can be extended to get any set of words in the string. To get the words from M for N words (e.g., the 5th word for 3, or the 5th, 6th, and 7th words), use the following array formula: =MID(A10,SMALL(IF(MID(" "&A10,ROW(INDIRECT ("1:"&LEN(A10)+1)),1)=" ",ROW(INDIRECT("1:"&LEN(A10)+1))), B10),SUM(SMALL(IF(MID(" "&A10&" ",ROW(INDIRECT ("1:"&LEN(A10)+2)),1)=" ",ROW(INDIRECT("1:"&LEN(A10)+2))), B10+C10*{0,1})*{-1,1})-1) Where A10 is the cell containg the text, B10 is the number of the word to get, and C10 is the number of words, starting at B10, to get. Note that in the above array formulas, the {0,1} and {-1,1} are enclosed in array braces (curly brackets {} ) not parentheses. Download a workbook illustrating these formulas.

Grades
A frequent question is how to assign a letter grade to a numeric value. This is simple. First create a define name called "Grades" which refers to the array: ={0,"F";60,"D";70,"C";80,"B";90,"A"} Then, use VLOOKUP to convert the number to the grade: =VLOOKUP(A1,Grades,2) where A1 is the cell contains the numeric value. You can add entries to the Grades array for other grades like C- and C+. Just make sure the numeric values in the array are in increasing order.

High And Low Values
You can use Excel's Circular Reference tool to have a cell that contains the highest ever reached value. For example, suppose you have a worksheet used to track team scores. You can set up a cell that will contain the highest score ever reached, even if

that score is deleted from the list. Suppose the score are in A1:A10. First, go to the Tools->Options dialog, click on the Calculation tab, and check the Interations check box. Then, enter the following formula in cell B1: =MAX(A1:A10,B1) Cell B1 will contian the highest value that has ever been present in A1:A10, even if that value is deleted from the range. Use the =MIN function to get the lowest ever value. Another method to do this, without using circular references, is provided by Laurent Longre, and uses the CALL function to access the Excel4 macro function library. Click here for details.

Left Lookups
The easiest way do table lookups is with the =VLOOKUP function. However, =VLOOKUP requires that the value returned be to the right of the value you're looking up. For example, if you're looking up a value in column B, you cannot retrieve values in column A. If you need to retrieve a value in a column to the left of the column containing the lookup value, use either of the following formulas: =INDIRECT(ADDRESS(ROW(Rng)+MATCH(C1,Rng,0)-1,COLUMN(Rng)ColsToLeft)) Or =INDIRECT(ADDRESS(ROW(Rng)+MATCH(C1,Rng,0)-1,COLUMN(A:A) )) Where Rng is the range containing the lookup values, and ColsToLeft is the number of columns to the left of Rng that the retrieval values are. In the second syntax, replace "A:A" with the column containing the retrieval data. In both examples, C1 is the value you want to look up. See the Lookups page for many more examples of lookup formulas.

Minimum And Maximum Values In A Range

Of course you can use the =MIN and =MAX functions to return the minimum and maximum values of a range. Suppose we've got a range of numeric values called NumRange. NumRange may contain duplicate values. The formulas below use the following example:

Address Of First Minimum In A Range
To return the address of the cell containing the first (or only) instance of the minimum of a list, use the following array formula: =ADDRESS(MIN(IF(NumRange=MIN(NumRange),ROW(NumRange))),COLUMN(Nu mRange),4) This function returns B2, the address of the first '1' in the range.

Address Of The Last Minimum In A Range
To return the address of the cell containing the last (or only) instance of the minimum of a list, use the following array formula: =ADDRESS(MAX(IF(NumRange=MIN(NumRange),ROW(NumRange)*(NumRange<> ""))), COLUMN(NumRange),4) This function returns B4, the address of the last '1' in the range.

Address Of First Maximum In A Range
To return the address of the cell containing the first instance of the maximum of a list, use the following array formula: =ADDRESS(MIN(IF(NumRange=MAX(NumRange),ROW(NumRange))),COLUMN(Nu mRange),4) This function returns B1, the address of the first '5' in the range.

Address Of The Last Maximum In A Range
To return the address of the cell containing the last instance of the maximum of a list, use the following array formula: =ADDRESS(MAX(IF(NumRange=MAX(NumRange),ROW(NumRange)*(NumRange<> ""))), COLUMN(NumRange),4) This function returns B5, the address of the last '5' in the range. Download a workbook illustrating these formulas.

Most Common String In A Range
The following array formula will return the most frequently used entry in a range: =INDEX(Rng,MATCH(MAX(COUNTIF(Rng,Rng)),COUNTIF(Rng,Rng),0)) Where Rng is the range containing the data.

Ranking Numbers
Often, it is useful to be able to return the N highest or lowest values from a range of data. Suppose we have a range of numeric data called RankRng. Create a range next to RankRng (starting in the same row, with the same number of rows) called TopRng. Also, create a named cell called TopN, and enter into it the number of values you want to return (e.g., 5 for the top 5 values in RankRng). Enter the following formula in the first cell in TopRng, and use Fill Down to fill out the range: =IF(ROW()-ROW(TopRng)+1>TopN,"",LARGE(RankRng,ROW()ROW(TopRng)+1)) To return the TopN smallest values of RankRng, use =IF(ROW()-ROW(TopRng)+1>TopN,"",SMALL(RankRng,ROW()ROW(TopRng)+1))

The list of numbers returned by these functions will automatically change as you change the contents of RankRng or TopN. Download a workbook illustrating these formulas. See the Ranking page for much more information about ranking numbers in Excel.

Removing Blank Cells In A Range
The procedures for creating a new list consisting of only those entries in another list, excluding blank cells, are described in NoBlanks.

Summing Every Nth Value
You can easily sum (or average) every Nth cell in a column range. For example, suppose you want to sum every 3rd cell. Suppose your data is in A1:A20, and N = 3 is in D1. The following array formula will sum the values in A3, A6, A9, etc. =SUM(IF(MOD(ROW($A$1:$A$20),$D$1)=0,$A$1:$A$20,0)) If you want to sum the values in A1, A4, A7, etc., use the following array formula: =SUM(IF(MOD(ROW($A$1:$A$20)-1,$D$1)=0,$A$1:$A$20,0)) If your data ranges does not begin in row 1, the formulas are slightly more complicated. Suppose our data is in B3:B22, and N = 3 is in D1. To sum the values in rows 5, 8, 11, etc, use the following array formula: =SUM(IF(MOD(ROW($B$3:$B$22)-ROW($B$3)+1,$D$1)=0,$B$3:B$22,0)) If you want to sum the values in rows 3, 6, 9, etc, use the following array formula: =SUM(IF(MOD(ROW($B$3:$B$22)-ROW($B$3),$D$1)=0,$B$3:B$22,0)) Download a workbook illustrating these formulas.

Miscellaneous
Sheet Name
Suppose our active sheet is named "MySheet" in the file C:\Files\MyBook.Xls. To return the full sheet name (including the file path) to a cell, use =CELL("filename",A1) Note that the argument to the =CELL function is the word "filename" in quotes, not your actual filename. This will return "C:\Files\[MyBook.xls]MySheet" To return the sheet name, without the path, use =MID(CELL("filename",A1),FIND("]",CELL("filename",A1))+1, LEN(CELL("filename",A1))-FIND("]",CELL("filename",A1))) This will return "MySheet"

File Name
Suppose our active sheet is named "MySheet" in the file C:\Files\MyBook.Xls. To return the file name without the path, use =MID(CELL("filename",A1),FIND("[",CELL("filename",A1))+1,FIND("] ", CELL("filename",A1))-FIND("[",CELL("filename",A1))-1) This will return "MyBook.xls" To return the file name with the path, use either =LEFT(CELL("filename",A1),FIND("]",CELL("filename",A1))) Or =SUBSTITUTE(SUBSTITUTE(LEFT(CELL("filename",A1),FIND("]", CELL("filename",A1))),"[",""),"]","") The first syntax will return "C:\Files\[MyBook.xls]"

The second syntax will return "C:\Files\MyBook.xls" In all of the examples above, the A1 argument to the =CELL function forces Excel to get the sheet name from the sheet containing the formula. Without it, and Excel calculates the =CELL function when another sheet is active, the cell would contain the name of the active sheet, not the sheet actually containing the formula. Download a workbook illustrating these formulas.