cfspreadsheet escape comma in comma separated row insert - list

The function for adding a row to a coldfusion spreadsheet is SpreadsheetAddrow which accepts data as "A comma delimited list of cell entries, one per column."
Some of my data has commas within it. How do I escape the commas within my data without escaping the commas in the list?
I am currently creating an array with the contents of the row, then converting it to a list to add to the spreadsheet:
<cfset row = ArrayNew(1)>
<cfloop list="#structKeyList(setRecord.columns)#" index="key">
<cfset ArrayAppend(row, "#Evaluate(key)#")>
</cfloop>
<cfset spreadsheetAddRow(xlsObj, "#ArrayToList(row)#")>

Looks like the ability to specify a different delimiter is not supported yet. Since you are already looping, you may as well skip the array and use SpreadsheetSetCellValue instead. You should be able to eliminate the evaluate() as well.
<cfset cols = structKeyArray(yourStruct) >
<cfloop from="1" to="#arrayLen(cols)#" index="c">
<cfset SpreadsheetSetCellValue(xlsObj, yourStruct[ cols[c] ], lastRow, c)>
</cfloop>
<cfset lastRow++>
...
Update: However, if the base object is a query, not a structure, then it is more efficient to use CfSimplicity's suggestion of SpreadSheetAddRows.

If the data you want to add to the sheet is in a query object (recordset) then the simplest solution is to use SpreadSheetAddRows(), (as opposed to SpreadSheetAddRow - singular).
<cfset SpreadSheetAddRows( xlsObj,query )>
The sheet columns are mapped from the query columns, so commas in the data won't matter.
Even if the data is in another format or you are only adding a single row, converting it to a query object is an effective way of getting round the issue, see http://cfsimplicity.com/30/workaround-for-spreadsheetaddrow-limitation-when-column-values-contain-commas

You can replace the commas with the character "#130;" (see the source: https://www.petefreitag.com/cheatsheets/ascii-codes/), which looks the same as comma but doesn't create any problems.

Related

cfspreadsheet cannot add rows to object created using query result

'excelFileQuery' contains a query.
<cfset spreadsheetAddRows(s, excelFileQuery)>
<cfset spreadsheetAddRows(s,"#volunteeralias# Export Report (#dateformat(now(),'mmmm d, yyyy')# at #timeformat(now(),'h:mm tt')#)",1,1)>
<cfheader name="content-disposition" value="attachment; filename=myexcel.xlsx">
<cfcontent type="application/msexcel" variable="#spreadsheetReadBinary(s)#" reset="true">
But when the file is generated, on the query rows are showing up, not the second row. Am I doing it wrong?
I am not quite sure about your goal, but spreadSheetAddRows expects a query or an array. So the second statement should pass in an array, not a string. If you prefer to use a string, you must use the spreadsheetSetCellValue function instead.
If your goal is to append that last string after the query results, then convert the string to an array first. Also, remove the row/column number so the data is appended to the first empty row after the query results.
<cfset spreadsheetAddRows(s, excelFileQuery)>
<cfset spreadsheetAddRows(s, ["#volunteeralias# Export Report (#dateformat(now(),'mmmm d, yyyy')# at #timeformat(now(),'h:mm tt')#)"] )>
...
If you want to prepend it (as a header), do the same thing, but obviously you swap the two statements.

ArrayMin on query column with a null value

CF8
I was using this line to get the MIN value of a query column. I just noticed a null value in a recordset causes an error. Is there a simply way to tell ArrayMin to skip nulls w/o having to loop the column and load an array with all non-null values?
<cfset temp_data_min = #round(ArrayMin(query_x["some_field"]))#>
thanks!
Building off of what Al said with using the query-of-queries, just adding the Min() call into the query.
<cfquery name="query_x_fixed" dbtype="query">
SELECT Min(some_field) as some_field
FROM query_x
WHERE some_field IS NOT NULL
</cfquery>
<cfset temp_data_min = round(query_x_fixed.some_field)>
Tested to work in CF9
You could loop through your array and create a new array that doesn't contain any null values. Then apply the ArrayMin function to the new array.
<cfset newArray = []>
<cfloop index="x" array="#query_x["some_field"]#">
<cfif x NEQ 'null'>
<cfset arrayAppend(newArray, x)>
</cfif>
</cfloop>
<cfset temp_data_min = round(ArrayMin(newArray))>
NOT TESTED
Simplest way is probably to do a query-of-queries with just that column and remove the nulls.
<cfquery name="query_x_fixed" dbtype="query">
SELECT some_field
FROM query_x
WHERE some_field IS NOT NULL
</cfquery>
<cfset temp_data_min= round(ArrayMin(query_x_fixed["some_field"]))>
(not tested)
You can keep the solution at one line by converting the column to a list then into an array. ListToArray defaults to ignoring empty list items, which is what the null values will be.
<cfset temp_data_min = Round(ArrayMin(ListToArray(ValueList(query_x.some_field, Chr(7)), Chr(7))))>
This should be faster than any of the other proposed solutions and is less code.
I've specified the delimiter for locales that use the comma as the decimal separator in numbers. If you're in the US or another locale that uses the "." then you can remove the delimiter arguments.
Additional functions used:
ListToArray
ValueList
Chr

How to access query column with multiple words?

I'm using a cfspreadsheet read to read a sheet into a query object.
<cfspreadsheet action="read" src="TestExcel.xls" sheet="1" query="spreadsheetData" headerrow="1" excludeHeaderRow="true">
The problem is, some of the headers contain more than one word. So I end up with a query a bit like this:
ID Name Start Date End Date
3 Test 1/1/2009 1/1/2013
17 Test 2 11/11/2010 11/11/2012
If I try to access one of the columns that have a space in the column name, I get an error.
<cfoutput query="spreadsheetData">
#start date#
</cfoutput>
I've tried #[start date]# as well, but that didn't work. I cannot control the format of the excel sheet that I receive. Is there any way to access the multiple-worded-header columns?
When using bracket notation the contents must end up as a string, so:
<cfoutput query="spreadsheetData">
#spreadsheetData['start date'][CurrentRow]#
</cfoutput>
If you don't use quotes, you are passing in a variable, which is done like so:
<cfset ColumnName = 'start date' />
<cfoutput query="spreadsheetData">
#spreadsheetData[ColumnName][CurrentRow]#
</cfoutput>
Note that you must use the query name before the brackets - if you simply write [ColumnName] then this is inline array creation notation, not accessing the variable.
Also, if using this outside of a query loop (i.e. not within cfoutput/cfloop with query attribute), you also need to scope the CurrentRow variable, i.e.
spreadsheetData[ColumnName][spreadsheetData.CurrentRow]
(or provide your own explicit number/variable).
As Leigh notes below, for cfspreadsheet-specific behaviour, you can also specify the columnnames attribute, to rename the column to something directly accessible, e.g.
<cfspreadsheet query=".." columnNames="Foo,Bar,StartDate,Etcetera" ..>

coldfusion query loop not cooperating

I'm trying to create a function to create csv files from queries. After I run the query, I'm looping through it and appending each row's fields to a StringBuffer object. To that end, I'm putting the column names into an array:
<cfset indexes = #ListToArray(Arguments.header)# />
where the argument is currently a string like:
"col1, col2, col3...."
I've verified that both the query and the array are what they should be by dumping.
The trouble comes when looping through the query results. Given:
<cfset indexes_length = #ArrayLen(indexes)# />
<cfloop query="query_for_csv">
<cfloop index="i" from="1" to="#indexes_length#">
<cfset attr = #indexes[i]# />
<cfset sbOutput.Append(
"#query_for_csv[attr][query_for_csv.CurrentRow]#") />
</cfloop>
</cfloop>
Only the first value of the first row is output before I get the error message:
[Table (rows 10 columns col1, col2, col3):
[col1: coldfusion.sql.QueryColumn#6f731eba]
[col2: coldfusion.sql.QueryColumn#6ee67e7f]
[col3: coldfusion.sql.QueryColumn#5c6647cb]
is not indexable by col2
If I replace the variable #attr# with the literal "col2":
#query_for_csv['col2'][query_for_csv.CurrentRow]#
then the loop sails through with no problem, and spits out all the values indexed by 'col2'. Any ideas?
I would guess it's the spaces in your header list that is the problem, so probably this would work:
<cfset attr = trim(indexes[i]) />
However, since you're not using them, you probably don't need that and can just do this...
<cfloop query="QueryName">
<cfloop index="CurCol" list=#QueryName.ColumnList# >
<cfset sbOutput.Append(
QueryName[CurCol][QueryName.CurrentRow]
)/>
</cfloop>
</cfloop>
p.s.
You'll note here that there's only one pair of hashes - there only needs to be one pair in your original code snippets too (in the to attribute) - the rest are unnecessary noise.
As has already been said before, try to avoid spaces before or after a list element.
In case you want to compare notes, check out the approach Ben Nadel chose to implement such a Query2CSV converter: http://www.bennadel.com/blog/1239-Updated-Converting-A-ColdFusion-Query-To-CSV-Using-QueryToCSV-.htm

Combining query rows in a loop

I have the following ColdFusion 9 code:
<cfloop from="1" to="#arrayLen(tagArray)#" index="i">
<cfquery name="qryGetSPFAQs" datasource="#application.datasource#">
EXEC searchFAQ '#tagArray[i]#'
</cfquery>
</cfloop>
The EXEC executes a stored procedure on the database server, which returns rows of data, depending on what the parameter is. What I am trying to do is combine the queries into one query object. In other words, if it loops 3 times and each loop returns 4 rows, I want a query object that has all 12 rows in one object. How do I acheive this?
You might want to take a different approach (modify your stored procedure to accept multiple arguments or use a list and fnSplit) and return the dataset all at once. However, to directly answer your question, this is how you could combine the queries as you're asking to:
You can use UNION in a Query of Queries to combine all of the datasets.
<cfloop from="1" to="#arrayLen(tagArray)#" index="i">
<cfquery name="qryGetSPFAQs#i#" datasource="#application.datasource#">
EXEC searchFAQ '#tagArray[i]#'
</cfquery>
</cfloop>
<cfquery name="combined" dbtype="query">
<cfloop from="1" to="#arrayLen(tagArray)#" index="i">
select * from qryGetSPFAQs#i#
<cfif i lt arrayLen(tagArray)>UNION</cfif>
</cfloop>
</cfquery>
A more direct way might be something like this:
<cfset bigQ = queryNew("column")>
<cfloop from="1" to="#arrayLen(tagArray)#" index="i">
<cfquery name="qryGetSPFAQs" datasource="#application.datasource#">
EXEC searchFAQ '#tagArray[i]#'
</cfquery>
<cfset queryAddRow(bigQ)>
<cfset querySetCell(bigQ, "column". qryGetSPFAQs)>
</cfloop>
You will need a querySetCell() assignment for each column. Check out the query functions in the live docs for more information.
Here is an out of the box solution, abandoning the StoredProc for a SQL View (I'll explain).
Disclaimer: without seeing the SP source code, I can't tell if my solution fits. I'm assuming that the SP is fairly basic, and I admit I usually prefer the compiled execution of an SP over a view, but the one-time execution of a SQL View should outperform the looping of the SP x times.
First make a view that looks like the SELECT statement in the SP (minus the parameterization, of course -- you'll cover that in a WHERE clause within the CFQUERY of your new view.
Second, set up your loop to do no more than build a data set we're going to use for the WHERE clause. You'll need to use ArrayToList and a little bit of string manipulation to tidy it up, with the end product being a string stored in a single CF variable looking like this:
('ValueOfArrayElement1','ValueOfArrayElement2','Value_And_So_On')
Building the string is pretty easy, using the delimeter attribute of ArrayToList, and after the loop is complete, append a Left Parenthesis & Single Quote to the Left most position of the string; and append a Single Quote & Right Parenthesis to the Right most position in the string.
Now, write the CFQUERY statement to SELECT the columns you need from your view (instead of executing your SP). And instead of passing a parameter to the SP, you're going to put a WHERE clause in the CFQUERY.
Oh, BTW, I am stating you need a SQL View, but the entire SELECT could be built in CFQUERY. Personally, when I have a multi-table JOIN, I like to define that in a SQL View where it's executed more quickly than a JOIN in CFQUERY. Ultimately a StoredProc is even faster, but our WHERE clause is much friendlier to code and read like this than it would be to send into StoredProc without looping in and out of SQL.
It's a good goal to make only one trip out to the database and back if possible. That's why we looped through the array to write a string equating to all the values in the dataset. This way, we'll only execute one query, one time.
SELECT Col1, Col2, Col_etc
FROM SQL_View_Name
WHERE ColumnName in #BigStringWeMadeFromArrayToList#
when our CFQUERY is rendered, the clause will look just like this in SQL:
WHERE ColumnName in
('ValueOfArrayElement1','ValueOfArrayElement2','Value_And_So_On')
So there you have it. Like I said, this is nice because it makes only one trip to the DB, and since we are building a view, the performance will still be pretty good -- better than running a StoredProc 4+ times. (no offense)
I'll must repeat... without having seen the SP code, I'm not sure if this is do-able. Plus, it's kind of odd to abandon a StoredProc for a SQL View, a "lesser" entity in the RDBMS, but I'm sure we will achieve greater performance and I think it's pretty readable, too.