To ease my befuddlement, I wrote a test for the Closures library. The use case for the below example is summing a column that contains non-numeric values. You can recreate the test by following these steps:
Create a table containing the three columns.
- InvoiceID = Primary Key, autonumbered
- CreateDate = Date
- Amount = varchar 50
Now run the following code to populate the table:
<cfset tDate = dateFormat( DateAdd( "d", -x, now()) ) >
<cfset tAmount = randRange( 100, 10000) />
<cfif x mod 5 IS 0 >
<cfset tAmount = 'Not Applicable' />
<cfquery name="setTest" datasource="test">
INSERT INTO Invoice
( CreateDate, Amount )
( <cfqueryparam value="#tDate#" cfsqltype="cf_sql_date">,
<cfqueryparam value="#tAmount#" cfsqltype="cf_sql_varchar">
You now have 100 rows of data and every 5th row contains the string 'Not Applicable'.
The goal of this is to sum the column 'Amount' and output the result. The closure has an internal scope and can keep the running total. The function body used to create the closure is rather simplistic and takes two arguments, Mode and valueToTotal. The logic of the function simply adds the valueToTotal to the running total. Passing a mode of 'Display' will output the current total.
<cfset cf = createObject("component","org.corfield.closure.ClosureFactory") />
<!--- Create the runningTotal function --->
<cffunction name="runningTotal" >
<cfargument name="mode" type="string" required="true"/>
<cfargument name="valueToTotal" type="string" default="" />
<!--- We need an internal scope to hold the total so lets make sure one exists before we access it --->
<cfif NOT structKeyExists( variables, "total") >
<cfset variables.total = 0 />
<cfif arguments.mode IS "Display"> <!--- Are we trying to display the running total amount --->
<cfreturn val( variables.total ) />
<cfelse><!--- No, we are passing in a value to total --->
<!--- add the value and account for strings by converting to 0 --->
<cfset variables.total = variables.total + val( arguments.valueToTotal ) />
<!--- pass back the original value --->
<cfreturn arguments.valueToTotal />
<!--- add the closure to a new variable called total. We use total to reference the closure --->
<cfset total = cf.new(runningTotal) />
Now, we write all the records from the Invoice table to the screen. Note the use of the closure to output the amount. We also call the same closure method with a different mode to output the Grand Total.
<cfquery name="getInvoice" datasource="test">
SELECT InvoiceID, CreateDate, Amount
<!--- output the date and the amount using the closure call --->
#DateFormat(CreateDate, "mm/dd/yyyy")# #total.call(mode='sum',valueToTotal=Amount)#<br />
<!--- Now output the value of the running total --->
<strong>Grand Total: #total.call(mode='Display')#</strong><br />
Below is the result
12/13/06 Not Applicable
........ snip out 95 more rows ....
Grand Total: 384272
The grand total was calculated using the formula in the closure. It would be trivial to bind additional values to the function if we wished to complicate the formula, perhaps to add a markup value or a discount value.
This code works, you can see that. I still have plenty to learn about closures so please comment if you have thoughts or criticisms.
Sean recommends another method to create the closure which is much more succinct, using more features of the ClosureLibrary. To use:
Remove the runningTotal function definition. Then replace the <cfset total = cf.new(runningTotal) > with the following snippet:
Finally, since both arguments are now required and the Closure library uses the argumentcollection, add an argument for valueToTest to the grandtotal section as follows:
When you run the code, the total is calculated as before. Note, The arguments are defined in the cf.new function along with binding the initial value of 'total'.