Download as docx, pdf, or txt
Download as docx, pdf, or txt
You are on page 1of 41

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 »

 <cfcomponent
 displayname="POIUtility"
 output="false"
 hint="Handles the reading and writing of Microsoft Excel files using POI and
ColdFusion.">
  
  
 <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"
 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
  
 )
 );
  
  
 // 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
 )
 );
  
 }
  
 // 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();
  
 // 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.
 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
 // 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,
 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
 // 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){
  
 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 )
 );
  
 }
  
  
 // 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(
 "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
 );
  
 }
  
  
 // 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."
 />
  
 <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();
  
 // 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(
 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.
 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";
  
 } 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.">
  
 <!--- 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."
 />
  
 <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 »

 <!--- 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(
 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 »

 <!---
 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 »

 <!---
 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" />
  
 <!--- 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 »

 <!---
 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(" "&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+{0,1})*{-1,1})-1)

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(Num
Range),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(Num
Range),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.

You might also like