How to create a cfspreadsheet with protected cells - coldfusion

I am creating a spreadsheet object using cfspreadsheet. Would like to make some of the individual cells as protected (read-only). Please let me know if anybody has tried this before.
I did try putting cell format as locked but it did not seems to work. Here is the sample code:
<cfset a = spreadsheetnew()>
<cfset format1 = structNew()>
<cfset format1.locked=true>
<cfset SpreadsheetFormatCell(a,format1,1,1)>
<cfspreadsheet action="write" filename="#expandpath('.')#/test.xls" name="a" overwrite="true">
Thanks.

Locking a cell does nothing unless the sheet is protected ie using cfspreadsheet's password attribute. But doing so has some negative side effects ...
Protecting the sheet locks all cells. That means you essentially have to "unlock" everything else by applying a format. In theory you could just unlock the entire sheet:
<cfset SpreadsheetFormatCellRange (sheet, {locked=false}, 1, 1, maxRow, maxCol)>
However, that has the nasty effect of populating every single cell in the sheet. So if you read the file into a query, the query would contain ~65,536 rows and 256 columns. Even if you only populated a few cells explicitly.
The lock feature is better suited to cases where you want everything to be locked except a few cells (not the reverse). Unless that is what you are doing, I probably would not bother with it, given all the negative side effects.
Side effect example
<cfset testFile = "c:/test.xls">
<cfset sheet = spreadsheetNew()>
<!--- only unlocking 100 rows to demonstrate --->
<cfset SpreadsheetFormatCellRange (sheet, {locked=false}, 1, 1, 100, 10)>
<!--- populate two cells --->
<cfset SpreadsheetSetCellValue(sheet,"LOCKED",1,1)>
<cfset SpreadsheetSetCellValue(sheet,"UNLOCKED",2,1)>
<!--- make one cell locked --->
<cfset SpreadsheetFormatCell(sheet, {locked=true}, 1, 1)>
<cfspreadsheet action="write"
name="sheet"
fileName="#testFile#"
password=""
overwrite="true" >
<!--- now see it is filled with empty cells --->
<cfspreadsheet action="read"
query="sheetData"
src="#testFile#" >
<cfdump var="#sheetData#" label="Lots of empty cells" />

Related

Parse the contents of a Coldfusion variable into separate variables

OK, so I've been banging my head against this one for a while and getting nowhere. I've been attempting to take the contents of a variable and parse the contained string into parts that would then be ingested into 5 separate variables. Seems simple enough right? Well, it has not proven to be simple at all, at least for me.
So I have a variable (PageContent) that contains the trimmed content from a CFHTTP request.
The PageContent variable now contains:
<tdnowrapalign=right>07/18/2020 13:00</td>
<tdalign=right>1002.12</td>
<tdalign=right>2,874,887</td>
<tdalign=right>12,766</td>
<tdalign=right>13,038</td>
It seems like there should be an easy way to write a loop that would loop over the tags in the "PageContent" variable assigning the content of each tag to a different variable. But every way I try to parse the data in the variable I either get an error (Complex object types cannot be converted to simple values.) or I end up with the content that I originally had in the "PageContent" variable repeated within the loop.
For instance, if I had a loop that would run through 5 iterations and could grab the contents of the tags assigning each to a variable then the desired result would be:
DateTime = "07/18/2020 13:00"
Elevation = "1002.12"
Storage = "2,874,887"
Outflow = "12,766"
Inflow = "13,038"
After trying every example I could find here and elsewhere online I'm now on something like my 100th attempt. Now I'm trying to use regular expressions to grab the contents of the tags and assign them to variables but no luck there. What I ended up with was the entire contents of the PageContent variable being stuffed into each one of the variables. The result was not really unexpected since I don't know of a way to differentiate between the 3 identical "tdalign" tags, but still it seems like at least the first variable would have worked since the tag was different "tdnowrapalign".
<cfset i=5/>
<cfloop index = "LoopCount" from = "1" to = #i#>
<cfif i EQ 1>
<cfset dataDateTime = Replace(PageContent, "<[tdnowrapalign][^>]*>(.+?)</[td]>","","ALL")>
<cfelseif i EQ 2>
<cfset elevation = Replace(PageContent, "<[tdalign][^>]*>(.+?)</[td]>","","ALL")>
<cfelseif i EQ 3>
<cfset storage = Replace(PageContent, "<[tdalign][^>]*>(.+?)</[td]>","","ALL")>
<cfelseif i EQ 4>
<cfset outflow = Replace(PageContent, "<[tdalign][^>]*>(.+?)</[td]>","","ALL")>
<cfelseif i EQ 5>
<cfset inflow = Replace(PageContent, "<[tdalign][^>]*>(.+?)</[td]>","","ALL")>
</cfif>
<cfoutput>
<cfif isdefined("dataDateTime")>
dataDateTime = #dataDateTime#<br>
</cfif>
<cfif isdefined("elevation")>
elevation = #elevation#<br>
</cfif>
<cfif isdefined("storage")>
storage = #storage#<br>
</cfif>
<cfif isdefined("outflow")>
outflow = #outflow#<br>
</cfif>
<cfif isdefined("inflow")>
inflow = #inflow#<br>
</cfif>
</cfoutput>
<cfset i = i - 1>
</cfloop>
Does anyone know if there is a way to get to the desired outcome I described where I end up with 5 variables containing the contents of the tags contained within the "PageContent" variable?
One way of doing it would be like this
<cfset PageContent = '<tdnowrapalign=right>07/18/2020 13:00</td>
<tdalign=right>1002.12</td>
<tdalign=right>2,874,887</td>
<tdalign=right>12,766</td>
<tdalign=right>13,038</td>' />
<cfset data = ListToArray(PageContent, '</td>', false, true) />
<cfset DateTime = ListLast(data[1], '>') />
<cfset Elevation = ListLast(data[2], '>') />
<cfset Storage = ListLast(data[3], '>') />
<cfset Outflow = ListLast(data[4], '>') />
<cfset Inflow = ListLast(data[5], '>') />
Demo: https://trycf.com/gist/b4f3b630bd1cbdc505d07a7d79b68ef5/acf?theme=monokai

How to properly format dates with SpreadsheetFormatColumns

Server: CF10
Platform: Windows and Linux (Win in DEV windows in PROD)
So I'm generating an excel file for a client, but having trouble getting date fields to behave properly. Everything else seems to work just fine but when I send the data to excel to be generated, excel treats it as just text. So when the field gets sorted, it is handled as such.
More info: The column I'm trying to configure is called Arrival Date it is arriving formatted as mm/dd/yyyy. (I have tried to format is as m/d/yy, but when it arrives in the sheet that doesn't work as well.)
Code:
<cfset filename = expandPath("./TDYData_(#DateFormat(now(),'mmddyy')#).xls")>
<!--- Make a spreadsheet object --->
<cfset s = spreadsheetNew("TDYData")>
<!--- Add header row --->
<cfset spreadsheetAddRow(s, "TDY Phase,Full Name,Employment Category,Gender,Originating Agency,Agency Comments,Originating Office,Office Comments,Originating Country,TDY Request Received,Mission Office Supported,Type of TDY Support,eCC Submission,eCC Approval,eCC Point of Contact,Date of Departure from Originating Country,Arrival Date,Departure Date,Accomodation Type,Accomodation Comments,Assigned Desk,Local Mobile Number,TDY Comments")>
<!--- format header --->
<cfset spreadsheetFormatRow(s, {bold=true,fgcolor="lemon_chiffon",fontsize=10}, 1)>
<!--- Add query --->
<cfset spreadsheetAddRows(s, myExcel)>
<cfset SpreadSheetAddFreezePane(s,0,1)>
<cfset SpreadsheetFormatColumn(s, {dataformat="m/d/yy"}, 17) />
<cfset SpreadsheetFormatColumn(s, {alignment="right"}, 16) />
<cfheader name="content-disposition" value="attachment; filename=TDY_Data_(#DateFormat(now(),'mmddyy')#).xls">
<cfcontent type="application/msexcel" variable="#spreadsheetReadBinary(s)#" reset="true">
Any ideas?
Thanks
So I have a workable solution:
<cfset therow = 0>
<cfoutput query="myExcel" startrow="1">
<cfset therow = myExcel.currentrow + 1>
<cfif len(eCCSubDate) GT 0>
<cfset SpreadsheetSetCellFormula(s,"DATEVALUE(#Chr(34)##eCCSubDate##Chr(34)#)",therow,13)>
</cfif>
<cfif len(eCCApproved) GT 0>
<cfset SpreadsheetSetCellFormula(s,"DATEVALUE(#Chr(34)##eCCApproved##Chr(34)#)",therow,14)>
</cfif>
<cfif len(DateDepartCntry) GT 0>
<cfset SpreadsheetSetCellFormula(s,"DATEVALUE(#Chr(34)##DateDepartCntry##Chr(34)#)",therow,16)>
</cfif>
<cfif len(DateArrive) GT 0>
<cfset SpreadsheetSetCellFormula(s,"DATEVALUE(#Chr(34)##DateArrive##Chr(34)#)",therow,17)>
</cfif>
<cfif len(DateDepart) GT 0>
<cfset SpreadsheetSetCellFormula(s,"DATEVALUE(#Chr(34)##DateDepart##Chr(34)#)",therow,18)>
</cfif>
</cfoutput>
So I set a variable for a counter then loop back over the query and check to make sure its not an empty cell then apply a formula to the cell the chr(34) are quotes then the data value. By using this, (After the data is written) it will at least behave like a real date field.

SpreadsheetFormatColumn() Right Border

When I run this code:
<cfscript>
flinstones = "fred,wilma,pebbles";
Workbook = Spreadsheetnew("Workbook");
for (i = 1; i lte listlen(flinstones); i ++) {
ThisFlinstone = ListGetAt(Flinstones, i);
if (ThisFlinstone == "wilma")
SpreadSheetAddRow(Workbook, "#ThisFlinstone#,barney");
else
SpreadSheetAddRow(Workbook, ThisFlinstone);
}
Format = {};
format.rightborder = "thin";
SpreadsheetFormatColumn(WorkBook, Format, 2);
MYfile = "d:\dw\dwtest\dan\abc.xls";
writedump(format);
</cfscript>
<cfspreadsheet action="write" filename="#MYFile#" name="Workbook"
sheet=1 sheetname="flinstones" overwrite=true>
I expect to see a worksheet with three rows. The second row will have two columns, with wilma and barney in the cells. So far, I see what I expect. I also expect to see Column B with a right hand border. I actually see cell B2 with a right hand border.
If I change this:
SpreadSheetAddRow(Workbook, ThisFlinstone);
to this
SpreadSheetAddRow(Workbook, "#ThisFlinstone#, ");
I see a right hand border for the first three rows of Column B.
Is there a way to have the right hand border apply to all of Column B?
Update:
As Dan mentioned in the comments, I misread the question. To clarify, adding a border to an entire column was not supported by POI last I checked. The only way to make the border appear on the entire column is to populate every single cell in that column first. Either by setting a value explicitly or using SpreadsheetFormatCellRange(sheet, format, 1, 2, maxRow, 2) with the maximum row number. While this does work, I would not recommend it as it has some undesirable side effects.
Is there a way to have the right hand border apply to all of Column B?
Short answer:
Yes, by doing what you already discovered: give all of the cells ie B1, B2 and B3 a value first. Even if it is just a blank or an empty string. This will create a border on all three cells.
Update 1: If you are using CF 9.0.1, another option is SpreadsheetFormatCellRange. Unlike most spreadsheet functions, it automatically creates non-existent cells first:
<cfset SpreadsheetFormatCellRange(Workbook, Format, 1, 2, 3, 2)>
Longer answer:
To apply a format, you need what POI calls a Cell object. When you create a new worksheet, it is completely blank, with no "cells" whatsoever. In most cases, CF only creates a Cell object when you set a value with one of the various functions ie SpreadSheetSetCellValue, SpreadSheetAddRow, etcetera. (See example below). Your original code only sets a one value in Column B. Hence it only creates a single Cell, ie B2. SpreadsheetFormatColumn only formats existing cells, that is why the border is only visible in B2.
<!--- blank sheet with no CELLS --->
<cfset Workbook = Spreadsheetnew() />
<cfset SpreadSheetWrite(Workbook, "c:/test.xls", true) />
<cfspreadsheet action="read" src="c:/test.xls" query="qValues" />
<cfdump var="#qValues#" label="No cells exist yet" />
<!--- setting one value, creates one CELL --->
<cfset Workbook = Spreadsheetnew() />
<cfset SpreadSheetAddRow(Workbook, "Fred") />
<cfset SpreadSheetWrite(Workbook, "c:/test.xls", true) />
<cfspreadsheet action="read" src="c:/test.xls" query="qValues" />
<cfdump var="#qValues#" label="Creates a single cell" />
<!--- setting two values, creates two CELL's --->
<cfset Workbook = Spreadsheetnew() />
<cfset SpreadSheetAddRow(Workbook, "Fred, ") />
<cfset SpreadSheetWrite(Workbook, "c:/test.xls", true) />
<cfspreadsheet action="read" src="c:/test.xls" query="qValues" />
<cfdump var="#qValues#" label="Creates two cells" />

Comma is invalid when using dataformat

Here is my code to output a query to a spreadsheet.
<cfscript>
//Use an absolute path for the files. --->
theDir=GetDirectoryFromPath(GetCurrentTemplatePath());
theFile=theDir & "getTestInv.xls";
//Create an empty ColdFusion spreadsheet object. --->
theSheet = SpreadsheetNew("invoicesData");
//Populate the object with a query. --->
SpreadsheetAddRows(theSheet,getTestInv);
</cfscript>
<cfset format = StructNew()>
<cfset format.dataformat = "#,###0.00">
<cfset SpreadsheetFormatColumn(theSheet,format,10)
<cfspreadsheet action="write" filename="#theFile#" name="theSheet" sheetname="getTestInv" overwrite=true>
The error I am getting is:
Invalid CFML construct found on line 125 at column 32.
ColdFusion was looking at the following text:
,
The CFML compiler was processing:
An expression beginning with /", on line 125, column 30.This message is usually caused by a problem in the expressions structure.
A cfset tag beginning on line 125, column 4.
A cfset tag beginning on line 125, column 4.
125: <cfset format.dataformat = "#,###0.00">
For some reason, it doesn't like the comma, even though it is valid according to the documentation. If I take the comma out, it works, but I need it for the thousands grouping.
Anyone encountered this?
In ColdFusion, the # is a reserved character. To escape it, you'll have to double them up to escape them:
<cfset format.dataformat = "##,######0.00">
Silly that they didn't account for this either in the documentation or followed ColdFusion's formatting rules using 9s instead of #s.
Here is my full working standalone test code:
<cfset myQuery = QueryNew('number')>
<cfset newRow = QueryAddRow(MyQuery, 2)>
<cfset temp = QuerySetCell(myQuery, "number", "349348394", 1)>
<cfset temp = QuerySetCell(myQuery, "number", "10000000", 2)>
<cfscript>
//Use an absolute path for the files. --->
theDir=GetDirectoryFromPath(GetCurrentTemplatePath());
theFile=theDir & "getTestInv.xls";
//Create an empty ColdFusion spreadsheet object. --->
theSheet = SpreadsheetNew("invoicesData");
//Populate the object with a query. --->
SpreadsheetAddRows(theSheet,myQuery,1,1);
</cfscript>
<cfset format = StructNew()>
<cfset format.dataformat = "##,######0.00">
<cfset SpreadsheetFormatColumn(theSheet,format,1)>
<cfspreadsheet action="write" filename="#theFile#" name="theSheet" sheetname="theSheet" overwrite=true>
it should be like that
<cfset format = StructNew()>
<cfset format.dataformat = "##,####0.00">

ColdFusion , REGEX - Given TEXT, find all items contained in SPANs

I'm looking to learn how to create a REGEX in Coldfusion that will scan through a large item of html text and create a list of items.
The items I want are contained between the following
<span class="findme">The Goods</span>
Thanks for any tips to get this going.
You don't say what version of CF. Since v8 you can use REMatch to get an array
results = REMatch('(?i)<span[^>]+class="findme"[^>]*>(.+?)</span>', text)
Use ArrayToList to turn that into a list.
For older version use REFindNoCase and use Mid() to extract substrings.
EDIT: To answer your follow-up comment the process of using REFind to return all matches is quite involved because the function only returns the FIRST match. This means you actually have to call REFind many times passing a new startpos each time. Ben Forta has written a UDF which does exactly this and will save you some time.
<!---
Returns all the matches of a regular expression within a string.
NOTE: Updated to allow subexpression selection (rather than whole match)
#param regex Regular expression. (Required)
#param text String to search. (Required)
#param subexnum Sub-expression to extract (Optional)
#return Returns a structure.
#author Ben Forta (ben#forta.com)
#version 1, July 15, 2005
--->
<cffunction name="reFindAll" output="true" returnType="struct">
<cfargument name="regex" type="string" required="yes">
<cfargument name="text" type="string" required="yes">
<cfargument name="subexnum" type="numeric" default="1">
<!--- Define local variables --->
<cfset var results=structNew()>
<cfset var pos=1>
<cfset var subex="">
<cfset var done=false>
<!--- Initialize results structure --->
<cfset results.len=arraynew(1)>
<cfset results.pos=arraynew(1)>
<!--- Loop through text --->
<cfloop condition="not done">
<!--- Perform search --->
<cfset subex=reFind(arguments.regex, arguments.text, pos, true)>
<!--- Anything matched? --->
<cfif subex.len[1] is 0>
<!--- Nothing found, outta here --->
<cfset done=true>
<cfelse>
<!--- Got one, add to arrays --->
<cfset arrayappend(results.len, subex.len[arguments.subexnum])>
<cfset arrayappend(results.pos, subex.pos[arguments.subexnum])>
<!--- Reposition start point --->
<cfset pos=subex.pos[1]+subex.len[1]>
</cfif>
</cfloop>
<!--- If no matches, add 0 to both arrays --->
<cfif arraylen(results.len) is 0>
<cfset arrayappend(results.len, 0)>
<cfset arrayappend(results.pos, 0)>
</cfif>
<!--- and return results --->
<cfreturn results>
</cffunction>
This gives you the start (pos) and length of each match so to get each substring use another loop
<cfset text = '<span class="findme">The Goods</span><span class="findme">More Goods</span>' />
<cfset pattern = '(?i)<span[^>]+class="findme"[^>]*>(.+?)</span>' />
<cfset results = reFindAll(pattern, text, 2) />
<cfloop index="i" from="1" to="#ArrayLen(results.pos)#">
<cfoutput>match #i#: #Mid(text, results.pos[i], results.len[i])#<br></cfoutput>
</cfloop>
EDIT: Updated reFindAll with subexnum argument. Setting this to 2 will capture the first subexpression. The default value 1 captures the entire match.
Try looking into the possibility of making your HTML work with a regular DOM Parser and querying it via XPath instead of hammering this trough an regex-based abomination.
to make HTML input usable, pass it through jTidy (see http://jtidy.riaforge.org/)
Once you have well-formed XML/XHTML, build an XML document from it
<cfset dom = XmlParse(scrubbedHtml, true)>
query the XML document using XPath
<cfset result = XmlSearch(dom, "//span[#class='findme']")>
Done.
EDIT: Coldfusion's XmlSearch() doesn't have great XML namespace support. If you end up producing XHTML instead of the more recommendable XML, use the following XPath (note the colon) "//:span[#class='findme']" or "//*:span[#class='findme']". See here and here for more info.
See the jTidy API documentation for a complete overview what jTidy can do.