Last week I posted an entry on the spreadsheet support planned for ColdFusion Centaur. That post (unsurprisingly) generated lots of questions and comments, and so I thought it worthwhile to post a bigger and more sophisticated example, this time building a new spreadsheet from scratch.
We'll start with a simple Application.cfc to define the datasource needed for this app:
<cfcomponent>
<cfset this.datasource="cfartgallery">
</cfcomponent>
Now to create a spreadsheet using a database query:
<!--- Get data --->
<cfquery name="ArtOrders">
SELECT orderid, customerfirstname, customerlastname, total
FROM orders
ORDER BY orderid
</cfquery>
<!--- Save it --->
<cfspreadsheet action="write"
query="ArtOrders"
filename="Orders.xls"
overwrite="true">
That's all it takes. <cfspreadsheet> creates a spreadsheet populated with the retrieved data, using the query column names as column headers in the first spreadsheet row, and the data starting in the second row. Nice, huh?
But what if you want more control? What if you want to explicitly control data placement? What if you needed to provide row or column or cell level formatting? And what if you needed a total cell containing a formula to SUM the total column?
Here's a complete example:
<!--- Get data --->
<cfquery name="ArtOrders">
SELECT orderid, customerfirstname, customerlastname, total
FROM orders
ORDER BY orderid
</cfquery>
<!--- Create new spreadsheet --->
<cfset sObj=SpreadsheetNew()>
<!--- Create header row --->
<cfset SpreadsheetAddRow(sObj, "Order,First Name,Last Name,Amount")>
<cfset SpreadsheetFormatRow(sObj, {bold=TRUE, alignment="center"}, 1)>
<!--- Add orders from query --->
<cfset SpreadsheetAddRows(sObj, ArtOrders)>
<!--- Figure out row for formula, 2 after data --->
<cfset rowDataStart=2>
<cfset rowDataEnd=ArtOrders.recordCount+1>
<cfset rowTotal=rowDataEnd+2>
<cfset totalFormula="SUM(D#rowDataStart#:D#rowDataEnd#)">
<!--- Add total formula --->
<cfset SpreadsheetSetCellValue(sObj, "TOTAL:", rowTotal, 3)>
<cfset spreadsheetSetCellFormula(sObj, totalFormula, rowTotal, 4)>
<!--- Format amount column as currency --->
<cfset SpreadsheetFormatColumn(sObj, {dataformat="$00000.00"}, 4)>
<!--- Save it --->
<cfspreadsheet action="write"
name="sObj"
filename="Orders.xls"
overwrite="true">
Once again, we start with a database query. Then, SpreadsheetNew() is used to create a new spreadsheet object.
The code then creates the header row. SpreadsheetAddRow() is used to add a specific row, the column headers (as no row number was specified, SpreadsheetAddRow() adds the row to the next free row, the first). SpreadsheetFormatObject() is then used to format row 1, this function accepts a structure (which here is implicitly created inline).
Next comes the data. SpreadsheetAddRows() adds the entire query to the spreadsheet (again, as no row was specified the data gets added to the next free row).
Now for the formula which will total all orders. So as to not hard code the row, a few <cfset> statements are used to calculate the first and last data row, and the row for the total (2nd row after the end of the data, so leaving one empty row in between data and total). The formula too can't be hard coded, so instead of SUM(D2:D24), the row variables are used to build the formula string dynamically. SpreadsheetSetCellValue() is used to add a title, and SpreadsheetSetCellFormula() inserts the formula.
Next, the entire fourth column, containing the order amounts and the calculated total, is formatted to display as currency using SpreadsheetFormatColumn().
All that is left to do then is to save the file using <cfspreadsheet>.
<cfspreadsheet> and its 30+ supporting functions can do lots more, but this should give you a taste of just what's possible using this innovative new feature.
Oh, I also added a line to the code, a SpreadsheetFormatColumn() call to format the amounts column as currency.
--- Ben
P.S.
MORE BOLT INFO! :D
--- Ben
excel through coldfusion?
Is it possible to do presentation type stuff, such as Graphing in the spreadsheet ?
Can an existing SS be opened and the contents of specified SS cell values read and edited ?
Eddie, no, creating new charts is not supported yet. But updating values that existing charts are dependent on works.
Bryn, sheets are supported, charts are not, and as for updates, sure, see the examples in the first post.
--- Ben
I've tested some complex Excel reports and the <cfspreadsheet action="read" > worked great but is it also possible to extract images from a worksheet?
1. Use cfchart to generate the chart. Insert the chart using spreadsheetaddimage:
Example:
<cfchart format="png" scalefrom="-100" scaleTo="100" gridlines="5" name="test">
<cfchartseries type="line">
<cfchartdata item="Point1" value="-50">
<cfchartdata item="Point2" value="-25">
<cfchartdata item="Point3" value="1">
<cfchartdata item="Point4" value="25">
<cfchartdata item="Point5" value="50">
<cfchartdata item="Point6" value="75">
<cfchartdata item="Point7" value="99">
</cfchartseries>
</cfchart>
<cfset spreadsheet=spreadsheetNew()>
<cfset spreadsheetAddImage(spreadsheet,test,"png","5,5,15,15")>
<cfspreadsheet action="write" filename="#dirname#SingleSheet1.xls" name="excelVar" sheetname="chart" overwrite="true">
No its is currently not possible to extract images through coldfusion. Is there an use case you will have? We can raise an ER for this.
- Vinu
Eddie
We have a client which stores vessel information in an Excel report. (For example: information like the paint / coating condition of a vessel).
The ''photo worksheet" includes several photos of the condition of the vessel (For example: a photo before a treatment and after a treatment). After the client is done, they upload the report in a webapplication.
TikiHopwu
I tried using the Excel functionality of CF9. It works great with formulas too. However I couldn't make the excel financial formulas (like IRR and NPV etc.) work with it. Is that me or are those formulas not supported yet. Do you have any information on that?
Cheers!
Here is a dump of my query:
CACHED: false
EXECUTIONTIME: 16
SQL: SELECT P0FIELD2,P1FIELD5,P2FIELD3,P3FIELD1
FROM
dataQuery
RESULTSET:
[Record # 1]
P0FIELD2: {ts '2009-11-08 13:32:27'}
P1FIELD5: 1
P2FIELD3: 2
P3FIELD1: STATUS2
[Record # 2]
P0FIELD2: {ts '2009-11-06 13:32:27'}
P1FIELD5: 1
P2FIELD3: 0
P3FIELD1: STATUS0
[Record # 3]
P0FIELD2: {ts '2009-11-07 13:32:27'}
P1FIELD5: 1
P2FIELD3: 1
P3FIELD1: STATUS1
This how it looks in the Excel:
2 true STATUS2 {ts '2009-11-08 13:32:27'}
0 true STATUS0 {ts '2009-11-06 13:32:27'}
1 true STATUS1 {ts '2009-11-07 13:32:27'}
3 true STATUS3 {ts '2009-11-09 13:32:27'}
Which is pretty messed up!
What is wrong here?
I was getting a Array from Flex. I use an ArrayToQuery function and then pass the Query to the SpreadsheetAddRow() function. In that case, CF doesnt care at all about how the query looks, it just does something.... bad bad bad!
So, I send some MetaData from Flex now, with a List which has the right order of fields to use in a QueryOfQuery Statement. So, I now QueryOfQuery my Object sent from Flex and then pass it to the SpreadsheetAddRow() function. Finallly it works correct this way.
I consider this a bug, because why should CF care about a SQL statement?? Why not just use the QueryObject I pass in the correct way?
I have logged the issue 80615. This looks like a bug. We will investigate more on this.
Thanks
Kunal Saini
Adobe ColdFusion Team
I believe I have the same question/issue as Neil.
I am reading an existing Excel SS and would like fields containing formulas to be read as VALUES. So instead of "SUM(A1:A20)" I would just like to see "394.22" or whatever the sum is.
I am using SpreadSheetGetCellValue() to retrieve the cell but am still getting the formula.
Help is very much appreciated!
It seems that ColdFusion cannot process external links to other Excel sheets. In these cases, CF9 just returns the cell's formula, not the value.
I turned off the "Update Links" prompt in Excel and that didn't help.
Can anyone address this?
I am having similar issues, though SpreadsheetGetCellValue() does actually return the value when a formula references another cell, but it barfs when the cell (or the cell that the cell references) contains some specific functions.
For instance, it had no problem evaluating a cell which had a formula "=E18*Assumptions!$D$19" which is a ref to another cell in another sheet. However, if a cell contains any of the following (non-exhustive) list of functions, it will return the function and not the value (this includes any cells that reference cells containing these functions): "NPV(), IIR()". So what I am going to try to do is replace the functions in those cells with something else that IS compatible.
To reiterate, if I call upon spreadsheetGetCellValue() to evaluate a cell containing =IRR(E34:H34,500%) it will fail (and return the function), yet if i change that function to =SUM(E34:H34,500%), then it works fine.
I don't know why CF would care what function is used, since I figured that would be evaluated in the Excel engine and not CF, but who knows. A solution would be appreciated!
I am having similar issues, though SpreadsheetGetCellValue() does actually return the value when a formula references another cell, but it barfs when the cell (or the cell that the cell references) contains some specific functions.
For instance, it had no problem evaluating a cell which had a formula "=E18*Assumptions!$D$19" which is a ref to another cell in another sheet. However, if a cell contains any of the following (non-exhustive) list of functions, it will return the function and not the value (this includes any cells that reference cells containing these functions): "NPV(), IIR()". So what I am going to try to do is replace the functions in those cells with something else that IS compatible.
To reiterate, if I call upon spreadsheetGetCellValue() to evaluate a cell containing =IRR(E34:H34,500%) it will fail (and return the function), yet if i change that function to =SUM(E34:H34,500%), then it works fine.
I don't know why CF would care what function is used, since I figured that would be evaluated in the Excel engine and not CF, but who knows. A solution would be appreciated!
Edit:
I ended up writing a custom java class to house my own NPV and IRR functions. What I do is I "correct" my sheet by performing my own calculations and overwriting the cell values (IRR and NPV functions) with the calculated values. Then I can let the rest of the sheet logic do it's magic. The code is here. If anyone needs help, you can reach me at spam2 AT splicedigital DOT com.
Just compile this class with javac irrNPV.java, and move the class file info c:\coldfusion9\lib\ Restart CF and you can instance it by <cfset L.IrrNPV = CreateObject("java", "IrrNPV")>. Again this solution will not work for everyone, and I was lucky enough to have a pretty simple sheet where it came to IRR and NPV.
public class IrrNPV {
/*
public static void main(String[] args) {
double npv_disc = 0.1;
double[] the_arr = new double[4];
the_arr[0] = -200320;
the_arr[1] = 5060430;
the_arr[2] = 5060430;
the_arr[3] = 5060430;
System.out.println("" + IRR(the_arr, 5) );
//System.out.println("" + npvOld(npv_disc, the_arr[0], the_arr[1], the_arr[2], the_arr[3]) );
//System.out.println("" + npv(npv_disc, the_arr, 0) );
}
*/
public double npv(double r, double[] the_arr, int index) {
double i = the_arr[index];
if(the_arr.length-1 == index)
{
return i / java.lang.Math.pow(1+r, index);
}
else
{
return (i / java.lang.Math.pow(1+r, index)) + npv(r, the_arr, index+1);
}
}
private final double MINDIF=.0000001;
public double IRR(double[] list, double guess) {
int i;
/* Testing */
/* for (i=0;i<list.length;i++) {
System.out.println("The array cell " + i + " = " + list[i]);
} */
double irr=0;
/* System.out.println("The array length is " + list.length); */
if (list[0]==0)
{
/* System.out.println("Returning NaN because first element in list was 0"); */
return Double.NaN;
}
int ineg=0;
int ipos=0;
for (i=0;i<list.length;i++) {
irr+=list[ i ];
if (list[ i ]>0) ipos++;
else if (list[ i ]<0) ineg++;
}
if (ineg==0 || ipos==0)
{
/* System.out.println("Returning NaN because ineg= " + ineg + " ipos= " + ipos); */
return Double.NaN;
}
double a1=list[0];
if (guess<=0) guess=.5;
if (irr<0) irr=-guess; else irr=guess;
boolean was_hi=false;
double a3;
for (int iter=0;iter<=50;iter++) {
a3=a1;
int j=1;
for (i=1;i<list.length;i++) {
a3+=list[ i ]/Math.pow(1.0+irr,j);
j++;
}
if (Math.abs(a3)<.01)
{ /* System.out.println("Returning a "+ irr); */
return irr;
}
if (a3>0) {
if (was_hi) guess/=2;
irr+=guess;
if (was_hi) {
guess-=MINDIF;
was_hi=false;
}
} else {
guess/=2;
irr-=guess;
was_hi=true;
}
if (guess<=MINDIF)
{ /* System.out.println("Returning b "+ irr); */
return irr;
}
}
/* System.out.println("Returning NaN becase at end of routine"); */
return Double.NaN;
}
}
9 -> $9
1234 -> $1,234