The sum of a struct within an array - struct

I am in the process of putting together a shopping cart, I am holding the cart data in an array, inside the array is a struct which holds the individual product information. I need get the sum of the totalprice column within the struct, please see my dump below, I have tried
<cfset carTotal = ArraySum(session.mycart[ "totalPrice" ])> but this creates an error
the value totalprice cannot be converted to a number
Is this because I am using arraysum in a struct?
Any help would be appreciated, thank you.

If mycart was a Query object, it'd be a simple ArraySum(mycart.totalPrice)
Since it's an array of struct, you've got to loop it yourself, which is still rather easy:
<cfset sum = 0>
<cfloop array="#session.mycart#" index="item">
<cfset sum += item.totalPrice>
</cfloop>
<cfdump var="#sum#">
Don't forget to use PrecisionEvaluate() when you need full precision.

As it's an array you'll have to loop yourself, keeping track of the sum.
<cfset cartTotal = 0 />
<cfloop array=#session.mycart# index="i">
<cfset cartTotal += i.totalPrice />
</cfloop>
<cfdump var="#cartTotal#" />

In Coldfusion 10 or Railo 4, you could use the reduce function of Underscore.cfc:
_ = new Underscore();
cartTotal = _.reduce(session.mycart, function(total, item){
return total + item.TotalPrice;
}, 0);
reduce is a common higher-order function that "reduces" a set of values to a single value. In this case, we're "reducing" the price of all items in a collection to a single sum.
Using a well-understood function likereduce, rather than a custom solution, yields more readable code.
Note: I wrote Underscore.cfc

Additional to the answer by Russ, in Coldfusion 11 or Lucee you can simply do arrayReduce() as well. No explicit need for underscore anymore.
cartTotal = arrayReduce(session.mycart, function(total, item){
return total + item.TotalPrice;
}, 0);

Related

Using dynamic variables with reReplaceNoCase in a cfloop

I'm using a cfloop to insert dynamic form values into a session struct. I use reReplaceNoCase to sanitize the input and am running into an issue trying to use dynamic form field names in the reReplaceNoCase method. I've tried different naming schemes but can't nail down the syntax. In the posted code, FORM.RTchoice[r] is what is failing and shows the error: Element RTCHOICE is undefined in a Java object of type class [Ljava.lang.String;.. How do I get the form fields to increment by "r" correctly?
<cfloop index="r" from="1" to="#APPLICATION.theCount#">
<cfset a = StructInsert(SESSION.USER_OBJECT, "RTchoice#r#", "#reReplaceNoCase(FORM.RTchoice[r], "[^a-zA-Z0-9.,(\s)-]", "", "all")#", 1)>
</cfloop>
You could use
<cfset a = StructInsert(SESSION.USER_OBJECT, "RTchoice#r#", reReplaceNoCase(FORM['RTchoice#r#'], "[^a-zA-Z0-9.,(\s)-]", "", "all"), 1)>
Or
<cfset a = StructInsert(SESSION.USER_OBJECT, "RTchoice#r#", reReplaceNoCase(FORM['RTchoice'&r], "[^a-zA-Z0-9.,(\s)-]", "", "all"), 1)>
Eg: https://cffiddle.org/app/file?filepath=cafebd5c-f4b5-4fc7-93bf-ff81ca97c234/00f6a79c-7f5f-42b2-b567-8a8a371fa8aa/3c7d3229-f65f-4afe-8538-306d98adf25f.cfm

getDocumentByKey with a number vector doesn't find the document

I have a 2 column sorted view and try to get a document the following code:
var searchArr = new java.util.Vector();
searchArr.addElement(10000310);
searchArr.addElement(45);
var customerdoc:NotesDocument = viw.getDocumentByKey(searchArr,true);
but the result is null.
If I use only the first element for the key (10000310), then I get (the first) doc with that key. But with the 2-element-vector the lookup returns null.
the same in LotusScript works fine:
Dim searchkey(1) As Double
searchkey(0) = 10000307
searchkey(1) = 45
Set doc = luview.Getdocumentbykey(searchkey, true)
gives me the document I need.
Confusing, for me ....
Uwe
This is a known bug, hopefully to be fixed in 9.0.2. See this question getDocumentByKey with view category separated by "\\" in XPages
Your LS example uses an array, not a Vector. I am not even sure if it is intended to work with a Vector - never did that. So just use an array here, too, as the key.

CFspreadsheet looping

I have a question regarding cfspreadsheet....So I'm using cfspreadshseet to create excel spreadsheets for reporting purposes. My page allows a user to select whatever columns from the database to include in the report. So here is an example:
The spreadsheet could look like this:
First Name---Last Name---Organization---Address---City---State---Zip---Concern
Joe Smith Sample 12 main denver co 80513 concerns go here
My question is this, if Joe has more than 1 concern I get multiple rows with joe's info...is there a way I can loop the concerns and only have 1 row for joe?
Thanks,
Steve
Using the "group" feature of cfoutput is perfectly fine for this task. Just to throw out another possibility, you could also generate the list within your database query.
For example, MySQL has the GROUP_CONCAT function. I do not know the structure of your tables, but say you have two tables User and UserConcern, you could use GROUP_CONCAT like so to concatenate the "Concern" values into a single string:
SQLFiddle
SELECT
u.UserID
, u.FirstName
, ... other columns
, GROUP_CONCAT( uc.Concern ) AS ConcernList
FROM UserTable u INNER JOIN UserConcern uc
ON uc.UserID = u.UserID
GROUP BY
u.UserID
, u.FirstName
, ... other columns
For SQL Server, a standard trick is to use XML Path:
SQLFiddle
SELECT
u.UserID
, u.FirstName
, ... other columns
, STUFF( ( SELECT ',' + uc.Concern
FROM UserConcern uc
WHERE uc.UserID = u.UserID
ORDER BY uc.Concern
FOR XML PATH('')
)
, 1, 1, ''
) AS ConcernList
FROM UserTable u
GROUP BY
u.UserID
, u.FirstName
, ... other columns
Then simply generate your spreadsheet as usual.
You need a unique row ID to do this safest, the outer group working with lastname, or something, could cause conflict. UserID is a placeholder variable. Make sure to replace it with an accurate ID name. Of course, a few of these variable names are just guessed.
<cfoutput query ="thequery" group="UserID">
<cfset cList="">
<cfoutput group="concern">
<cfset cList=ListAppend(cList,Concern)>
</cfoutput>
<cfset temp = spreadsheetAddRow(my_spreadsheet,"'#fn#','#ln#',...,'#cList#'">
</cfoutput>
Assuming you want separate lines for each comment, something like this would work:
<cfset current_id = "">
<cfloop query = "my_query">
<cfset next_id = user_id>
<!--- or whatever else forms the primary key --->
<cfif next_id neq current_id>
<cfset current_id = next_id>
<cfset SpreadsheetAddRow(my_spreadsheet, "#first_name#,#last_name#,etc, #comment#">
<cfelse>
<cfset SpreadsheetAddRow(my_spreadsheet, ",,#comment#">
</cfif>
</cfloop>
This is based on the information provided. If you have a unique ID the group attribute would work better.

How to maintain counters with LinqToObjects?

I have the following c# code:
private XElement BuildXmlBlob(string id, Part part, out int counter)
{
// return some unique xml particular to the parameters passed
// remember to increment the counter also before returning.
}
Which is called by:
var counter = 0;
result.AddRange(from rec in listOfRecordings
from par in rec.Parts
let id = GetId("mods", rec.CKey + par.UniqueId)
select BuildXmlBlob(id, par, counter));
Above code samples are symbolic of what I am trying to achieve.
According to the Eric Lippert, the out keyword and linq does not mix. OK fair enough but can someone help me refactor the above so it does work? A colleague at work mentioned accumulator and aggregate functions but I am novice to Linq and my google searches were bearing any real fruit so I thought I would ask here :).
To Clarify:
I am counting the number of parts I might have which could be any number of them each time the code is called. So every time the BuildXmlBlob() method is called, the resulting xml produced will have a unique element in there denoting the 'partNumber'.
So if the counter is currently on 7, that means we are processing 7th part so far!! That means XML returned from BuildXmlBlob() will have the counter value embedded in there somewhere. That's why I need it somehow to be passed and incremented every time the BuildXmlBlob() is called per run through.
If you want to keep this purely in LINQ and you need to maintain a running count for use within your queries, the cleanest way to do so would be to make use of the Select() overloads that includes the index in the query to get the current index.
In this case, it would be cleaner to do a query which collects the inputs first, then use the overload to do the projection.
var inputs =
from recording in listOfRecordings
from part in recording.Parts
select new
{
Id = GetId("mods", recording.CKey + part.UniqueId),
Part = part,
};
result.AddRange(inputs.Select((x, i) => BuildXmlBlob(x.Id, x.Part, i)));
Then you wouldn't need to use the out/ref parameter.
XElement BuildXmlBlob(string id, Part part, int counter)
{
// implementation
}
Below is what I managed to figure out on my own:.
result.AddRange(listOfRecordings.SelectMany(rec => rec.Parts, (rec, par) => new {rec, par})
.Select(#t => new
{
#t,
Id = GetStructMapItemId("mods", #t.rec.CKey + #t.par.UniqueId)
})
.Select((#t, i) => BuildPartsDmdSec(#t.Id, #t.#t.par, i)));
I used resharper to convert it into a method chain which constructed the basics for what I needed and then i simply tacked on the select statement right at the end.

How can I compare two lists and find the differences between them?

The following function compares a new list of items to an old one and finds the differences:
Items that have been deleted from the old list
Items that were added to the new list (not present in the original list).
I wrote two loops to achieve this, and they produced the following output:
oldItems = "an, old, list" ---> Items To Delete: 'an,old'
newItems = "a, new, list" ---> Items To Create: 'new'
The first issue is a should show up in the items to create, but I believe because it's a substring of an it's not getting picked up.
The second issue(?) is I doing two loops seems inefficient. Can the code be refactored?
public function testList() hint="Compares two lists to find the differences."
{
local.oldItems = "a, new, list";
local.newItems = "an, old, list";
local.toDelete = "";
local.toCreate = "";
// Loop over newItems to find items that do not exist in oldItems
for (local.i = 1; local.i LTE ListLen(local.newItems, ", "); local.i++)
{
if (! ListContains(local.oldItems, ListGetAt(local.newItems, local.i, ", ")))
{
local.toCreate = ListAppend(local.toCreate, ListGetAt(local.newItems, local.i, ", "));
}
}
// Loop over old items to find items that do not exist in newItems
for (local.i = 1; local.i LTE ListLen(local.oldItems, ", "); local.i++)
{
if (! ListContains(local.newItems, ListGetAt(local.oldItems, local.i, ", ")))
{
local.toDelete = ListAppend(local.toDelete, ListGetAt(local.oldItems, local.i, ", "));
}
}
writeDump(var="Items To Delete: '" & local.toDelete & "'");
writeDump(var="Items To Create: '" & local.toCreate & "'", abort=true);
}
Yes, I believe you can refactor your code.
I prefer to use the array functions as it does an exact match (including case). This method ensures that "a" is picked up as a difference between the lists.
Hope this helps:
<cfscript>
oldList = "an, old, list";
newList = "a, new, list";
result = compareLists(oldList, newList);
writeDump(result);
// -------------------------------------------------------------------------
public struct function compareLists (
required string listA,
required string listB
){
local.a = listToArray(arguments.listA, ',');
local.b = listToArray(arguments.listB, ',');
local.itemsNotInListB = [];
local.itemsNewInListB = [];
// Compare all items in 'list A' to 'list B'
for (local.item in local.a) {
if (!arrayContains(local.b, local.item))
{
arrayAppend(local.itemsNotInListB, local.item);
}
}
// Compare all items in 'list B' to 'list A'
for (local.item in local.b) {
if (!arrayContains(local.a, local.item))
{
arrayAppend(local.itemsNewInListB, local.item);
}
}
return {
newItems = local.itemsNewInListB
,deletedItems = local.itemsNotInListB
};
}
</cfscript>
The answer to getting the correct matches is to use the ListFind() function with a delimiter instead of ListContains():
if (! ListFind(local.oldItems, ListGetAt(local.newItems, local.i, ", "), ", ")) {}
The delimiter is necessary otherwise the function will try and match the whole string.
No answer for the refactoring, though.
There are 3 list UDFs on cflib.org:
List Compare - http://cflib.org/udf/listCompare - Compares one list against another to find the elements in the first list that don't exist in the second list. Performs the same funciton as the custom tag of the same name.
List Diff - http://cflib.org/udf/ListDiff - Compares two lists and returns the elements that do not appear in both lists.
List Diff Dup - http://cflib.org/udf/ListDiffDup - This function compares two lists and will return a new list containing the difference between the two input lists. This function is different from ListDiff as it treats duplicate elements within the lists as distinct elements.
Seriously, don't reinvent the wheel. Use Java Lists or Sets, ColdFusion runs atop a JVM:
<cfscript>
oldItems = "an, old, list"; //Items To Delete: 'an,old'
newItems = "a, new, list"; //Items To Create: 'a,new'
// ArrayList could be HashSet if items in both lists are expected to be unique
oldItems = createObject("java", "java.util.ArrayList").init(listToArray(oldItems, ", "));
newItems = createObject("java", "java.util.ArrayList").init(listToArray(newItems, ", "));
itemsToDelete = createObject("java", "java.util.HashSet").init(oldItems);
itemsToDelete.removeAll(newItems);
itemsToCreate = createObject("java", "java.util.HashSet").init(newItems);
itemsToCreate.removeAll(oldItems);
</cfscript>
<cfoutput>
itemsToDelete: #listSort(arrayToList(itemsToDelete.toArray()),"textNoCase")#<br /><!--- an,old --->
itemsToCreate: #listSort(arrayToList(itemsToCreate.toArray()),"textNoCase")#<!--- a,new --->
</cfoutput>
As a bonus here's a link to Java code I drew my example from.
Did you try searching CFLib.org? There are multiple list functions including one to find differences. As for your issues with substrings, read the documentation on ListContains. This is how it is supposed to work. Try ListFind or ListFindNoCase instead.
<cfset strListTupla = "SKU,CANTIDAD,VENTA">
<cfset strListSend = "CANTIDAD,SKU">
<cfset strListSend = ListSort(strListSend, "textnocase", "asc")>
<cfset strListTupla = ListSort(strListTupla, "textnocase", "asc")>
<cfset strListTupla = ListToArray (strListTupla)>
<cfloop index="j" from="1" to="#Arraylen(strListTupla)#">
<cfoutput>
<cfif NOT ListFind(strListSend, strListTupla[j])>
Not Found #strListTupla[j]#
</cfif><br/>
</cfoutput>
</cfloop>
With that I can search for the item to see if it is in the list or not and likewise show that element is missing.
The 2 loops practically does the same thing, the only difference is that the lists have swapped place. Totally unneccessary. You only need to check one way; Old -> New. NOT the otherway around too (Old <- New).
Create an instance of the 2 lists and send them through one loop where you check if the old list contains an item equal to any of the new items.
I don't know how good a for-loop is for this purpose as local.i in old list can be index[7] and the exact same item you need to check on in the new can be index[3] after possible changes.

Resources