Sum up dynamic fields in list - list

I have a list like:
<cfset list ="group 1:1; group 2:4; group a:7; group 1:3; group a:1;">
What I want to do is count (sum up) how many people are in group 1, group 2, group a, etc.. Now these fields are dynamic, so I don't know the exact name of the groups.
So the end result in this example is:
group 1: 4
group 2: 4
group a: 8
So the list is just an example, in reality it's much bigger with more groups (dynamic names). I'm using Coldfusion/Lucee. Can someone help me with this?

Just one of numerous of possible alternative ways of doing this. Starting from using a query instead of a list as the starting value.
What I am doing is to loop the list with ; as delimiter and adding the values to a structure. I later use that structure to loop and list the final total.
<cfset list ="group 1:1; group 2:4; group a:7; group 1:3; group a:1;">
<cfset totalStruct = {}>
<cfloop list="#list#" item="group" delimiters=';'>
<cfset groupName = listFirst(group, ':')>
<cfset groupNameKey = replace(groupName, ' ', '', 'all')>
<cfset groupValue = val(listLast(group, ':'))>
<cfif !structKeyExists(totalStruct, groupNameKey)>
<cfset totalStruct[groupNameKey] = {name:groupName, total=groupValue}>
<cfelse>
<cfset totalStruct[groupNameKey].total += groupValue>
</cfif>
</cfloop>
<cfoutput>
<ul>
<cfloop collection="#totalStruct#" item="group">
<li>#totalStruct[group].name# : #totalStruct[group].total#</li>
</cfloop>
</ul>
</cfoutput>
DEMO

Another alternative is to use the map and/or reduce closure functions to parse your list.
NOTE 1: I generally find cfscript to be much easier for most things for parsing text or "looping".
NOTE 2: I'm not a huge fan of loops, especially around large text values. Closure functions will likely be much more performant.
<cfset lst ="group 1:1; group 2:4; group a:7; group 1:3; group a:1;">
First, with a map() inside of a function:
<cfscript>
public Struct function parseLst (required String lst) {
var retval = {} ; //// Default return variable.
//// https://docs.lucee.org/reference/functions/listmap.html
arguments.lst.listmap(
function(el) {
var k = el.listFirst(":").ltrim() ; /// Get the "key" and strip extra leading space.
var v = el.listLast(":") ; /// Get the "value".
var p = retval["#k#"]?:0 ; /// Use Elvis to default to 0 if no struct Key exists.
retval["#k#"] = v + p ; /// Set the value of the key. NOTE: A struck key with the same name will generally overwrite itself. We want to add it.
}
,";" /// Specify the delimiter of the list.
) ;
return retval ;
}
writeDump(parseLst(lst));
</cfscript>
Then with a reduce() without being inside a function.
<cfscript>
//// https://docs.lucee.org/reference/functions/listreduce.html
r = listReduce(lst,
function(prev,nxt){
k = nxt.listFirst(":").ltrim() ; /// Get the "key" and strip extra leading space.
/// To shorten it, I just skipped setting the value beforehand and just did it while setting the struct value. Same method as above.
prev["#k#"] = (nxt.listLast(":"))+(prev["#k#"]?:0) ;
return prev ;
}
,
{} // Initial value
,";" // Delimiter
) ;
writedump(r) ;
</cfscript>
Both could (and probably should) be inside a function, and then you could just send your list variable to it.
And if possible, it might be a lot easier to fix the original list to be simpler to work with.
https://trycf.com/gist/dda51d88504a625fce5548142d73edb3/lucee5?theme=monokai
=======================================================
EDIT:
An alternative to using listFirst/Last() functions would be to just convert the "list" to an array and then use those pieces to get your "key" and "value".
<cfscript>
//// https://docs.lucee.org/reference/functions/listreduce.html
s = listReduce(lst,
function(prev,nxt){
var elem = nxt.listToArray(":") ;
prev["#elem[1].ltrim()#"] = elem[2] + (prev["#elem[1].ltrim()#"]?:0) ;
return prev ;
}
,
{} // Initial value
,";" // Delimiter
) ;
writedump(s) ;
</cfscript>

Related

Using CGI Scope to Create a working URL [duplicate]

I have a url.LoginID, and I'd like to remove it from the address bar when the user clicks on the link to login. It has to be a bookmark, it can't be a form submit.
Q: How do I remove ?LoginID from Index.cfm?LoginID=XYZ&AssignmentID=123
It's probably something along the lines of:
<cflocation url="#cgi.SCRIPT_NAME#?#cgi.QUERY_STRING#" addtoken="no">
Looks like you are on the right track.
If loginID is the only thing in the query string, you can simply cflocation to the destination page without the query string.
If there is other data in the query string, then you can do something like this:
<cfset q = reReplaceNoCase(cgi.query_string, "LOGINID=[^&]+&?", "")>
<cflocation url="#cgi.SCRIPT_NAME#?#q#">
This essentially removes loginid and everything until either the en of the string or the next URL variable.
As usual, there's already a UDF that someone has written available on CFLIB: queryStringDeleteVar
You can then do like so
<cflocation
url="#cgi.SCRIPT_NAME#?#queryStringDeleteVar("LoginID",cgi.QUERY_STRING)#"
addtoken="no"
>
CGI.QUERY_STRING is actually the default for the second arg, so this will work just as well
<cflocation
url="#cgi.SCRIPT_NAME#?#queryStringDeleteVar("LoginID")#"
addtoken="no"
>
Here's the code for queryStringDeleteVar:
<cfscript>
/**
* Deletes a var from a query string.
* Idea for multiple args from Michael Stephenson (michael.stephenson#adtran.com)
*
* #param variable A variable, or a list of variables, to delete from the query string.
* #param qs Query string to modify. Defaults to CGI.QUERY_STRING.
* #return Returns a string.
* #author Nathan Dintenfass (michael.stephenson#adtran.comnathan#changemedia.com)
* #version 1.1, February 24, 2002
*/
function queryStringDeleteVar(variable){
//var to hold the final string
var string = "";
//vars for use in the loop, so we don't have to evaluate lists and arrays more than once
var ii = 1;
var thisVar = "";
var thisIndex = "";
var array = "";
//if there is a second argument, use that as the query string, otherwise default to cgi.query_string
var qs = cgi.query_string;
if(arrayLen(arguments) GT 1)
qs = arguments[2];
//put the query string into an array for easier looping
array = listToArray(qs,"&");
//now, loop over the array and rebuild the string
for(ii = 1; ii lte arrayLen(array); ii = ii + 1){
thisIndex = array[ii];
thisVar = listFirst(thisIndex,"=");
//if this is the var, edit it to the value, otherwise, just append
if(not listFind(variable,thisVar))
string = listAppend(string,thisIndex,"&");
}
//return the string
return string;
}
</cfscript>
There are number of ways to do this, here is one way using a list loop to read through your existing parameters and check for the one you want to ignore:
<cfset newParams = "" />
<cfloop list="#cgi.query_string#" delimiters="&" index="i">
<cfif listFirst(i, "=") neq "loginID">
<cfset newParams = listAppend(newParams, i, "&") />
</cfif>
</cfloop>
<cflocation url="#cgi.script_name#?#newParams#" addtoken="no">
Hope that helps!
Suppose you don't really want to remove the ? to keep the URL valid, so simple regex should work:
QUERY_STRING = ReReplaceNoCase(cgi.QUERY_STRING, "LoginID=.+\&", "");
BTW, I'm not sure why do you keep LoginID in URL at all, it may be insecure approach. Using sessions sounds like a better idea.
Edit: Ben's regex is better, because my version is so simple that will "eat" all key=value pairs before last one.
Insert famous Zawinski two problem regex quote and solve differently:
<cfset copy = duplicate(url)>
<cfset structDelete(copy, "loginid")>
<cfset entries = []>
<cfloop collection="#copy#" item="key">
<cfset arrayAppend(entries, "#key#=#copy[key]#")>
</cfloop>
<cfoutput>#arrayToList(entries, "&")#</cfoutput>

using multiple characters as delimiters in Coldfusion list

I am trying to use multiple characters as the delimeter in ColdFusion list like ,( comma and blank) but it ignores the blank.
I then tried to use:
<cfset title = listappend( title, a[ idx ].title, "#Chr(44)##Chr(32)#" ) />
But it also ignores the blank and without blanks the list items to diffucult to read.
Any ideas?
With ListAppend you can only use one delimiter. As the docs say for the delimiters parameter:
If this parameter contains more than one character, ColdFusion uses only the first character.
I'm not sure what a[ idx ].title contains or exactly what the expected result is (would be better if you gave a complete example), but I think something like this will do what you want or at least get you started:
<cfscript>
a = [
{"title"="One"},
{"title"="Two"},
{"title"="Three"}
];
result = "";
for (el in a) {
result &= el.title & ", ";
}
writeDump(result);
</cfscript>
I think there's a fundamental flaw in your approach here. The list delimiter is part of the structure of the data, whereas you are also trying to use it for "decoration" when you come to output the data from the list. Whilst often conveniently this'll work, it's kinda conflating two ideas.
What you should do is eschew the use of lists as a data structure completely, as they're a bit crap. Use an array for storing the data, and then deal with rendering it as a separate issue: write a render function which puts whatever separator you want in your display between each element.
function displayArrayAsList(array, separator){
var list = "";
for (var element in array){
list &= (len(list) ? separator : "");
list &= element;
}
return list;
}
writeOutput(displayAsList(["tahi", "rua", "toru", "wha"], ", "));
tahi, rua, toru, wha
Use a two step process. Step 1 - create your comma delimited list. Step 2
yourList = replace(yourList, ",", ", ", "all");

Remove characters in <cfoutput> with ReplaceNoCase() ColdFusion

I need to display an output from a data record that is formatted similar to this: XXXX:12345 (Xxxxxxxxx)
However, the only data I want to output is the "12345" and with two preceding zeros, i.e. the output should look like "0012345". The "12345" in the record is example only, each record has a unique number assigned. An example record looks like this: CAST:98765 (RPOS1234-XY)
Can I use the ReplaceNoCase() to pull only that data out of the record? If so, how would I write the code to remove the unwanted characters?
You can do this in one line of code using a few functions.
str = 'CAST:98765 (RPOS1234-XY)';
projectCode = '00' & listLast( listFirst( str, ' ' ), ':' );
writeDump( projectCode );
To explain this code from the inner most function going out.
ListFirst() gets the first element in an a list based on the delimiter you specify, in this case the delimiter is ' ' - a space - yes, you can use a space as a delimiter.
ListLast() gets the last element in a list based on the delimiter you specify, in this case the delimiter is ':'
The first part simplt appends '00' to the result of the above function calls.
If I had to use reReplaceNoCase or reFindNoCase this is how I would do it.
function parseTokenUsingReFindNoCase(token) {
var local = {};
// use regex to locate position of number (see only set of parentheses in regex pattern)
local.positions = reFindNoCase("^.+:(\d+).+$", arguments.token, 1, true);
// obtain the token substring and ensure at least 7 digits with preceding 0's
local.result = numberFormat( mid(arguments.token, local.positions.pos[2], local.positions.len[2]), repeatString(0, 7));
return local.result;
}
function parseTokenUsingReReplaceNoCase(token) {
var local = {};
// use regex to strip away text before and after the token
local.result = reReplaceNoCase(arguments.token, "(^\D+|\s.+$)", "", "all");
// ensure at least 7 digits with preceding 0's
local.result = numberFormat(local.result, repeatString(0, 7));
return local.result;
}
<h1>ParseToken</h1>
<h2>Using ReFindNoCase</h2>
<cfdump var="#parseTokenUsingReFindNoCase("CAST:98765 (RPOS1234-XY)")#" /><br>
<cfdump var="#parseTokenUsingReFindNoCase("CAST:591498 (FUBAR56-XE)")#" /><br>
<cfdump var="#parseTokenUsingReFindNoCase("CAST:784 (RFP4542-LL)")#" /><br>
<h2>Using ReReplaceNoCase</h2>
<cfdump var="#parseTokenUsingReReplaceNoCase("CAST:98765 (RPOS1234-XY)")#" /><br>
<cfdump var="#parseTokenUsingReReplaceNoCase("CAST:591498 (FUBAR56-XE)")#" /><br>
<cfdump var="#parseTokenUsingReReplaceNoCase("CAST:784 (RFP4542-LL)")#" /><br>
ParseToken
Using ReFindNoCase
0098765
0591498
0000784
Using ReReplaceNoCase
0098765
0591498
0000784
It doesn't use replaceNoCase, but based on your comments this will work:
<cfset castTicket = projectCode>
<!--- strip the first 5 characters, since it is always "CAST " --->
<cfset castTicket = removechars(castTicket, 1,5)>
<!--- now return the leftmost characters, up to the space --->
<cfset castTicket = left(castTicket, find(" ", castTicket) )>
<!--- format the number so it has 7 digits (2 leading zeros in this case) --->
<cfset castTicket = NumberFormat(castTicket, 0000000)>
<cfoutput>#castTicket#</cfoutput>
Returns:
0012345

Testing if value is not blank using IsNull vs Len() vs NEQ ""

From a best-practice or performance standpoint which option is best to use for testing if a FORM value is not blank?
<cfif NOT isNull(FORM.Forename)>
OR
<cfif Len(Trim(FORM.Forename)) GT 0>
OR
<cfif FORM.Forename NEQ "">
I don't want the value to be valid if it has something silly like 4 blank spaces in it. I guess 4 blank spaces is not technically a NULL value?
The second
<cfif Len(Trim(FORM.Forename)) GT 0>
The first will not be null. Cf will receive an empty string or no form element.
The third is covered by the second.
You may need to wrap the form element with an isdefined depending on the form element type.
The problem with all of these examples is that the variable needs to exist or it errors (hence Kobby's "isdefined").
To get around missing FORM variables, attributes, etc. (and not have to isDefined test for them all of the time) I constantly use these custom "NULL" functions...
/* "NULL" functions are to assist with setting and returning variable values of the type you want,
whether they already exist or not ... for easy coding.
<cfif NULL("message") EQ "Optional message value" />
#message# now exists <cfif message EQ "Other value" />
<cfif boolNULL("form.optionalVariableName") />
#form.optionalVariableName# now exists as a boolean <cfif form.optionalVariableName />
<cfif valNULL("attributes.expectedNumericVariableName") />
#attributes.expectedNumericVariableName# now exists as a numeric <cfif attributes.expectedNumericVariableName GT 5 />
<cfif lenNULL("query.expectedNonBlankQueryOrStructureVariableName") />
*/
// List not last is all elements except the last
function listNotLast(list) {
// 2nd param is optional list delimiter
var delimiter = arrayLen(arguments) GT 1 ? arguments[2] : ",";
// If more than 1 element return the list without the last one
return listLen(list, delimiter) GT 1 ? listDeleteAt(list, listLen(list, delimiter), delimiter) : list;
}
// Sets and returns a string even if not defined (sets the variable to "" or a query as query.column[1] = "").
function NULL(v) {
// Override the default blank string with optional 2nd param
var NA = arrayLen(arguments) GT 1 ? arguments[2] : "";
// Variable v does not exist. We'll make it an empty string
if (!isDefined(v)) {
// Query vars are treated differently. Can fail if we're a struct. It looks like we want a query variable query.column_name
if (listLen(v, ".") GT 1 AND !listFindNoCase("caller,attributes,request,client,session,variables,cookie,cgi", listFirst(v, "."))) {
// If the query doesn't exist at all make a blank one (NA)
if (!isDefined(listFirst(v, "."))) {
setVariable(listFirst(v, "."), queryNew("NULL"));
}
// Now add the column name we want as a blank array (NA)
queryAddColumn(evaluate(listFirst(v, ".")), listLast(v, "."), [ NA ]);
}
// Non-query variables created here as a blank string (NA)
else setVariable(v, NA);
}
// Originally defined or not we just return our string value
return evaluate(v);
}
// Returns a numeric value even if not defined (sets the variable to 0). Override the set and return in second var. Uses the NULL and listNotLast functions.
function valNULL(v) {
if (!isDefined("NULL") OR !isDefined("listNotLast")) throw(type = "Missing function", message = "The NULL and listNotLast functions are required for the valNULL function.");
// Return a zero if non defined or non-numeric. Override here with 2nd param
var NA = arrayLen(arguments) GT 1 ? arguments[2] : 0;
var LE = listNotLast(v, ".")
// Use the NULL routine to ensure we're already created, test numericy
if (!isNumeric(NULL(v))) {
// We're non-numeric, check if its a query var (NULL will have created if need be)
if (isDefined(LE) AND isQuery(evaluate(LE))) {
// The query may need a row
if (!evaluate(LE & ".recordCount")) queryAddRow(evaluate(LE), 1);
// NULL already returned us a blank query with our column name if it didn't exist, just set our cell's NA (0) value here
querySetCell(evaluate(LE), listLast(v, "."), NA, 1);
// Non-query variables just set NA (0) value
} else setVariable(v, NA);
}
// Originally defined or not we just return our numeric value
return evaluate(v);
}
// Returns a boolean value even if not defined (sets the variable to false). Uses the NULL function. No overrides.
function boolNULL(v) {
if (!isDefined("NULL")) throw(type = "Missing function", message = "The NULL function is required for the boolNULL function.");
// Use the NULL function to return true for "1" values
if (NULL(v) EQ 1) setVariable(v, true);
// If its not boolean then its false. eg. 0, [blank], false, 'fasle' all set and return false
if (NOT isBoolean(NULL(v))) setVariable(v, false);
// Boolean only values returned here
return evaluate(v);
}
// Returns a string length even if not defined (sets the variable to "" and returns 0 length). Uses the NULL function. No overrides.
function lenNULL(v) {
if (!isDefined("NULL")) throw(type = "Missing function", message = "The NULL function is required for the lenNULL function.");
return len(NULL(v));
}

How to strip out a url variable

I have a url.LoginID, and I'd like to remove it from the address bar when the user clicks on the link to login. It has to be a bookmark, it can't be a form submit.
Q: How do I remove ?LoginID from Index.cfm?LoginID=XYZ&AssignmentID=123
It's probably something along the lines of:
<cflocation url="#cgi.SCRIPT_NAME#?#cgi.QUERY_STRING#" addtoken="no">
Looks like you are on the right track.
If loginID is the only thing in the query string, you can simply cflocation to the destination page without the query string.
If there is other data in the query string, then you can do something like this:
<cfset q = reReplaceNoCase(cgi.query_string, "LOGINID=[^&]+&?", "")>
<cflocation url="#cgi.SCRIPT_NAME#?#q#">
This essentially removes loginid and everything until either the en of the string or the next URL variable.
As usual, there's already a UDF that someone has written available on CFLIB: queryStringDeleteVar
You can then do like so
<cflocation
url="#cgi.SCRIPT_NAME#?#queryStringDeleteVar("LoginID",cgi.QUERY_STRING)#"
addtoken="no"
>
CGI.QUERY_STRING is actually the default for the second arg, so this will work just as well
<cflocation
url="#cgi.SCRIPT_NAME#?#queryStringDeleteVar("LoginID")#"
addtoken="no"
>
Here's the code for queryStringDeleteVar:
<cfscript>
/**
* Deletes a var from a query string.
* Idea for multiple args from Michael Stephenson (michael.stephenson#adtran.com)
*
* #param variable A variable, or a list of variables, to delete from the query string.
* #param qs Query string to modify. Defaults to CGI.QUERY_STRING.
* #return Returns a string.
* #author Nathan Dintenfass (michael.stephenson#adtran.comnathan#changemedia.com)
* #version 1.1, February 24, 2002
*/
function queryStringDeleteVar(variable){
//var to hold the final string
var string = "";
//vars for use in the loop, so we don't have to evaluate lists and arrays more than once
var ii = 1;
var thisVar = "";
var thisIndex = "";
var array = "";
//if there is a second argument, use that as the query string, otherwise default to cgi.query_string
var qs = cgi.query_string;
if(arrayLen(arguments) GT 1)
qs = arguments[2];
//put the query string into an array for easier looping
array = listToArray(qs,"&");
//now, loop over the array and rebuild the string
for(ii = 1; ii lte arrayLen(array); ii = ii + 1){
thisIndex = array[ii];
thisVar = listFirst(thisIndex,"=");
//if this is the var, edit it to the value, otherwise, just append
if(not listFind(variable,thisVar))
string = listAppend(string,thisIndex,"&");
}
//return the string
return string;
}
</cfscript>
There are number of ways to do this, here is one way using a list loop to read through your existing parameters and check for the one you want to ignore:
<cfset newParams = "" />
<cfloop list="#cgi.query_string#" delimiters="&" index="i">
<cfif listFirst(i, "=") neq "loginID">
<cfset newParams = listAppend(newParams, i, "&") />
</cfif>
</cfloop>
<cflocation url="#cgi.script_name#?#newParams#" addtoken="no">
Hope that helps!
Suppose you don't really want to remove the ? to keep the URL valid, so simple regex should work:
QUERY_STRING = ReReplaceNoCase(cgi.QUERY_STRING, "LoginID=.+\&", "");
BTW, I'm not sure why do you keep LoginID in URL at all, it may be insecure approach. Using sessions sounds like a better idea.
Edit: Ben's regex is better, because my version is so simple that will "eat" all key=value pairs before last one.
Insert famous Zawinski two problem regex quote and solve differently:
<cfset copy = duplicate(url)>
<cfset structDelete(copy, "loginid")>
<cfset entries = []>
<cfloop collection="#copy#" item="key">
<cfset arrayAppend(entries, "#key#=#copy[key]#")>
</cfloop>
<cfoutput>#arrayToList(entries, "&")#</cfoutput>