Improve performance of searching JSON object with jQuery - javascript

Please forgive me if this is answered on SO somewhere already. I've searched, and it seems as though this is a fairly specific case.
Here's an example of the JSON (NOTE: this is very stripped down - this is dynamically loaded, and currently there are 126 records):
var layout = {
"2":[{"id":"40","attribute_id":"2","option_id":null,"design_attribute_id":"4","design_option_id":"131","width":"10","height":"10",
"repeat":"0","top":"0","left":"0","bottom":"0","right":"0","use_right":"0","use_bottom":"0","apply_to_options":"0"},
{"id":"41","attribute_id":"2","option_id":"115","design_attribute_id":"4","design_option_id":"131","width":"2","height":"1",
"repeat":"0","top":"0","left":"0","bottom":"4","right":"2","use_right":"0","use_bottom":"0","apply_to_options":"0"},
{"id":"44","attribute_id":"2","option_id":"118","design_attribute_id":"4","design_option_id":"131","width":"10","height":"10",
"repeat":"0","top":"0","left":"0","bottom":"0","right":"0","use_right":"0","use_bottom":"0","apply_to_options":"0"}],
"5":[{"id":"326","attribute_id":"5","option_id":null,"design_attribute_id":"4","design_option_id":"154","width":"5","height":"5",
"repeat":"0","top":"0","left":"0","bottom":"0","right":"0","use_right":"0","use_bottom":"0","apply_to_options":"0"}]
};
I need to match the right combination of values. Here's the function I currently use:
function drawOption(attid, optid) {
var attlayout = layout[attid];
$.each(attlayout, function(k, v) {
// d_opt_id and d_opt_id are global scoped variable set elsewhere
if (v.design_attribute_id == d_att_id
&& v.design_option_id == d_opt_id
&& v.attribute_id == attid
&& ((v.apply_to_options == 1 || (v.option_id === optid)))) {
// Do stuff here
}
});
}
The issue is that I might iterate through 10-15 layouts (unique attid's), and any given layout (attid) might have as many as 50 possibilities, which means that this loop is being run A LOT.
Given the multiple criteria that have to be matched, would an AJAX call work better? (This JSON is dynamically created via PHP, so I could craft a PHP function that could possibly do this more efficently),
or am I completely missing something about how to find items in a JSON object?
As always, any suggestions for improving the code are welcome!
EDIT:
I apologize for not making this clear, but the purpose of this question is to find a way to improve the performance. The page has a lot of javascript, and this is a location where I know that performance is lower than it could be.

First and foremost you should measure and only act if there is a real performance concern. You need exact numbers like 200ms or 80% time is spent there. "It runs a lot" doesn't mean anything. The browser can loop very fast.
You can improve constant factors as others mentioned, like using a native for loop instead of jQuery.each. Chaching global variables won't help you too much in this case.
If you really want to improve efficency you should find a better algorithm than O(n). Assuming that you only use this data for finding elements matching a certain criteria you can use JS objects as hashes to achive a O(1) performance.
Just an example for you specific case:
var layout = {
"2": { "2,,4,131,10,0": ["40", "93"], "2,115,4,131,0": ["41"] },
"4": { ... },
...
};
It's fairly easy to generate this output in php, and then you just use a lookup to find ids matching your particular criteria.

IMHO, a simple hashmap index will probably work best. This does require you to loop over the data ahead of time, but the index can be easily appended to and cached.
Once the index is generated, this should be O(1) for lookups, and will handle multiple entries per key.
var layout = {
"2":[[...], ...],
"5":[[...], ...]
};
var index = {};
function makeKey(data) {
return data.join('_');
}
for(var l in layout) {
var cur = layout[l];
for(var i in cur) {
var item = cur[i];
var key = makeKey([item.p1, item.p2, ...]);
index[key] = index[key] || [];
index[key].push(item);
}
}
function find(attid, optid) {
var key = makeKey([attid, optid, 1, d_att_id, ...]);
return index[key]; //this is an array of results
}

My first suggestion would be to stop using $.each if you want to squeeze out every bit of performance you can. jQuery.each does a bit more than a traditional loop. Take a look at this jsFiddle with your browser's debugger running (i.e. Safari's/Chrome's web developer tools) and step through the fiddle until execution fully returns from jQuery.
For reference, the fiddle's code is:
var myArr = [{"foo":"bar"},{"answer":42}];
debugger;
$.each(myArr, function(k, v) {
console.log(k + ': ');
console.dir(v);
});​
Now run through the second version:
var myArr = [{"foo":"bar"},{"answer":42}],
i, j;
debugger;
for (i = 0, j = myArr.length; i < j; i += 1) {
console.log('myArr[' + i + ']: ');
console.dir(myArr[i]);
}​
Notice that there are far fewer operations being executed in the second version. So that's a tiny bit of performance gain.
Second, eliminate as many lookups outside of the local scope as you can. Even if you cache a reference to your global variables (boo!) then you can save a lot of time given that this loop will be executed possibly hundreds of times. So instead of:
function foo(a, b) {
if (a === globalA && b === globalB) {
// Do stuff
}
}
You'd do:
function foo(a, b) {
var gA = globalA,
gB = globalB;
if (a === gA && b === gB) {
// Do stuff
}
}
As for pairing down the conditional based on the object members, I'm not seeing much else that could be improved. The object properties you are checking are top level, and you're looking at local instances of each object (so the scope chain lookups are short).
Without knowing more about how this is actually supposed to work, those are the best recommendations I can make. However, I can make the guess that your idea of starting with simpler JSON data would be a big improvement. If you know what the layout, and its constraints, is, then requesting the specific details from the server would mean you don't have to check so many conditions. You could simply ask the server for the details that you actually need to implement and loop through those (to update the DOM or whatever).

I see that the you are searching by
5 fields: v.design_attribute_id,v.design_option_id,v.attribute_id,v.apply_to_options,v.option_id.
What you could do is add an extra field to the objects called "key" that is a composite of the values in those fields.
Here's an example
{
"key": "4_131_2_0_0" //i picked 0 to represent null, but you can pick any number
"id": "40",
"attribute_id": "2",
"option_id": null,
"design_attribute_id": "4",
"design_option_id": "131",
"width": "10",
"height": "10",
"repeat": "0",
"top": "0",
"left": "0",
"bottom": "0",
"right": "0",
"use_right": "0",
"use_bottom": "0",
"apply_to_options": "0"
}
Note though that you must normalize the length of each value.
Meaning that if one objects optionId is 1 and another object optionID is 566 you must represent
the first optionId as 001 in the key string.
With this field you can then sort the array on the server side before returning it to the client.
Then you can use a binary search to find the values on the client.
Using the binary search implementation located here
http://www.nczonline.net/blog/2009/09/01/computer-science-in-javascript-binary-search/
Your search function would look something like
function drawOption(attid, optid) {
var attlayout = layout[attid];
var needle = d_att_id + "_" + d_opt_id + "_" + attid + "_" + optid; //remember to normalize length if you have to
var idx = binarySearch(attlayout,needle);
var item;
if(idx !== -1){
item = attlayout[idx];
//do something
}
}
Another method you can try using this composite key idea is to have the server return
the layout objects in one big object mapped by
attid,v.design_attribute_id,v.design_option_id,v.attribute_id,v.apply_to_options,v.option_id
Then you can look up in O(1) time.
It would look something like
function drawOption(attid, optid) {
var needle = attid + "_" + d_att_id + "_" + d_opt_id + "_" + attid + "_" + optid; //remember to normalize length if you have to
var item = layout[needle];
if(typeof item !== "undefined"){
//do something
}
}

When trying to improve your code, it is always better to check which functions are taking time using firebug profiling. You can either profile by clicking on profile button in firebug's console panel and then run your code or using firebug's profiling commands in your code
From the code that you have given, only a few improvement points can be given.
$.each is slow compared to native looping solutions. For the best
looping solutions, check out this JsPref test
It would be better to change the JSON to use arrays instead of object literals. It is said to be more faster to retrieve values.

I have experience to such issue before, my js array of objects consist of 8 thousands record and more.
My experience is not about the performance, but the readability, maintainability, scalable of the codes.
hence I developed an JS Object Query Library 2 years ago: JSOQL
http://code.google.com/p/jsoql/
It works like SQL to allow you query your js array of objects with syntax similar to SQL.
The example usage is something like this, I cant really remember, but you can download the example usage in the download tab.
new JSQOL().Select([field1, field2, field3 ...]).From({ ... }) .Where(fn) .Offset(int) .Limit(int) .Get();
Note:
{...} is your array of objects, or an object it self.
Hope it helps, you can send me message if you need more information.

It's not going to work everywhere but your problem sounds like something that can be done with webworkers
Another thing I would look at if you dont have webworkers is trying not to block the ui to long. If you can chunk it into bits of about 40ms and then setTimeout the next chunck for just a few ms later the user will have a more pleasant experience. This needs a little fiddling but users will start to notice stuff when something takes longer than somewhere between 50 and 100ms

Have you considered using the jQuery grep function?
jQuery grep
And jquery grep on json object array for an example.

Here is one technique that will probably yield better performance at the expense of using a bit more memory.
I'll leave my code examples simple just to illustrate the concept.
First, you'll want to pre-process your JSON data into some additional arrays that act as indexes. Here is an example of what the final arrays might look like after pre-processing:
var layouts_by_attribute = {
// attribute_id => array(layouts)
2: [40, 41, 44],
5: [326]
};
var layouts_by_design_attribute_id = {
// design_attribute_id => array(layouts)
4: [40, 41, 44, 326]
};
Finding a layout by attribute is now very quick:
function findByAttribute(attribute_id) {
return layouts = layouts_by_attribute[attribute_id];
}
function findByDesignAttribute(design_attribute_id) {
return layouts = layouts_by_design_attribute[design_attribute_id];
}

Related

Weirdness when modifying massive json file/array in Node.js

I am working with a massive json file (almost 60 MB) that i am trying to remove all entries in it where volume = 0. The format of the array is
{
"date": 1424373000,
"high": 0.33,
"low": 225,
"open": 0.33,
"close": 225,
"volume": 0.999999,
"quoteVolume": 0.00444444,
"weightedAverage": 225
}
To do this I am using this code.
fs.readFile('JSONFiles/poloniexBTCDataFeb19|2015-July2|2018.json', function read(err, data) {
if (err) {
throw err;
}
rawdata = JSON.parse(data);
rawdata.forEach(function(val, index, array) {
if (rawdata[index].volume == 0) {
rawdata.splice(index, 1)
}
})
});
The problem with this is that it only deletes about half of the entries with this characteristic (60k/108k). The way I fixed this was to use a for loop which runs the code 9 times, which deletes them all, but this causes the code to take significantly longer cause the whole json file has about 360k entries and it has to check each one with that if statement. I was wondering if there was any way to do this where it actually deletes them all without having to use a for loop in this manner?
EDIT: I have realized that I do not need this code in the first place so nevermind but thanks for all the answers. I hope this helps someone else when they hit a simmilar issue.
The problem is, you are mutating the array rawdata. Let's take an example array [e1, e2, e3, e4] and the code,
var arr = ['e1', 'e2', 'e3', 'e4']
arr.forEach(function(elem, idx){
console.log('checking elem', elem);
if (elem === 'e2'){
arr.splice(idx, 1)
}
});
console.log('\nAfter iteration', arr);
As you can see, I am removing e2 when I encounter it. This affects the actual array and the element which is getting replaced for it, will not be inspected (as forEach iteration already visited element at that index). In the above code, e3 did not get checked. So, it is advised to not mutate array in forEach iteration.
You can do like,
rawdata.slice().forEach(function (val, index, array) {
if (rawdata[index].volume == 0) {
rawdata.splice(index, 1)
}
});
Here slice() will make a new array and mutating your original rawdata will not affect the iteration.
You are splicing the records, that might be taking time.
Instead of the forEach, try this:
var filteredData = rawdata.filter(function (val) {
return val.volume != 0
})
Your code is buggy and the error is quite common (iterating over an array while mutating it). The code is also very inefficient because for each element to delete will move all other elements by one place (the fact that you use splice doesn't mean the a loop is not done... there is still a loop behind the scenes to implement that function).
If you need to remove elements from an array in-place (i.e. you don't want to get a copy) a simple approach is using what I normally call a read-skip-write loop:
let wp = 0; // the "write pointer"
for (let x of data) {
if (keep(x)) data[wp++] = x;
}
data.length = wp; // trim unused space
PS: as a side note try to change your mindset about programming. If your first thought is that node is buggy then you're not going to get very far into coding. The reality is that bug is 99.99% of the times in your code... looking somewhere else won't make you a better programmer.

Programming optional ignorance

In Javascript what is the best way to handle scenarios when you have a set of arrays to perform tasks on sets of data and sometimes you do not want to include all of the arrays but instead a combination.
My arrays are labeled in this small snippet L,C,H,V,B,A,S and to put things into perspective the code is around 2500 lines like this. (I have removed code notes from this post)
if(C[0].length>0){
L=L[1].concat(+(MIN.apply(this,L[0])).toFixed(7));
C=C[1].concat(C[0][0]);
H=H[1].concat(+(MAX.apply(this,H[0])).toFixed(7));
V=V[1].concat((V[0].reduce(function(a,b){return a+b}))/(V[0].length));
B=B[1].concat((MAX.apply(this,B[0])-MIN.apply(this,B[0]))/2);
A=A[1].concat((MAX.apply(this,A[0])-MIN.apply(this,A[0]))/2);
D=D[1].concat((D[0].reduce(function(a,b){return a+b}))/(D[0].length));
S=S[1].concat((S[0].reduce(function(a,b){return a+b}))/(S[0].length));
}
It would seem counter-productive in this case to litter the code with tones of bool conditions asking on each loop or code section if an array was included in the task and even more silly to ask inside each loop iteration with say an inline condition as these would also slow down the processing and also make the code look like a maze or rabbit hole.
Is there a logical method / library to ignore instruction or skip if an option was set to false
All I have come up with so far is kind of pointless inline thing
var op=[0,1,1,0,0,0,0,0]; //options
var L=[],C=[],H=[],V=[],B=[],A=[],D=[],S=[];
op[0]&&[L[0]=1];
op[1]&&[C[0]=1,console.log('test, do more than one thing')];
op[2]&&[H[0]=1];
op[3]&&[V[0]=1];
op[4]&&[B[0]=1];
op[5]&&[A[0]=1];
op[6]&&[A[0]=1];
It works in that it sets only C[0] and H[0] to 1 as the options require, but it fails as it needs to ask seven questions per iteration of a loop as it may be done inside a loop. Rather than make seven versions of the the loop or code section, and rather than asking questions inside each loop is there another style / method?
I have also noticed that if I create an array then at some point make it equal to NaN rather than undefined or null the console does not complain
var L=[],C=[],H=[],V=[],B=[],A=[],D=[],S=[];
L=NaN;
L[0]=1;
//1
console.log(L); //NaN
L=undefined;
L[0]=1
//TypeError: Cannot set property '0' of undefined
L=null
L[0]=1
//TypeError: Cannot set property '0' of null
Am I getting warmer? I would assume that if I performed some math on L[0] when isNaN(L)===true that the math is being done but not stored so the line isn't being ignored really..
If I understand what you want I would do something like this.
var op = [...],
opchoice = {
//these can return nothing, no operation, or a new value.
'true': function(val){ /*operation do if true*/ },
'false': function(val){ /*operation do if false*/ },
//add more operations here.
//keys must be strings, or transformed into strings with operation method.
operation: function(val){
//make the boolean a string key.
return this[''+(val == 'something')](val);
}
};
var endop = [];//need this to prevent infinite recursion(loop).
var val;
while(val = op.shift()){
//a queue operation.
endop.push(opchoice.operation(val));
}
I'm sure this is not exactly what you want, but it's close to fulfilling the want of not having a ton of conditions every where.
Your other option is on every line do this.
A = isNaN(A) ? A.concat(...) : A;
Personally I prefer the other method.
It looks like you repeat many of the operations. These operations should be functions so at least you do not redefine the same function over and over again (it is also an optimization to do so).
function get_min(x)
{
return +(MIN.apply(this, a[0])).toFixed(7);
}
function get_max(x)
{
return +(MAX.apply(this, a[0])).toFixed(7);
}
function get_average(x)
{
return (x[0].reduce(function(a, b) {return a + b})) / (x[0].length);
}
function get_mean(x)
{
return (MAX.apply(this, x[0]) - MIN.apply(this, x[0])) / 2;
}
if(C[0].length > 0)
{
L = L[1].concat(get_min(L));
C = C[1].concat(C[0][0]);
H = H[1].concat(get_max(H));
V = V[1].concat(get_average(V));
B = B[1].concat(get_mean(B));
A = A[1].concat(get_mean(A);
D = D[1].concat(get_average(D));
S = S[1].concat(get_average(S));
}
You could also define an object with prototype functions, but it is not clear whether it would be useful (outside of putting those functions in a namespace).
In regard to the idea/concept of having a test, what you've found is probably the best way in JavaScript.
op[0] && S = S[1].concat(get_average(S));
And if you want to apply multiple operators when op[0] is true, use parenthesis and commas:
op[3] && (V = V[1].concat(get_average(V)),
B = B[1].concat(get_mean(B)),
A = A[1].concat(get_mean(A));
op[0] && (D = D[1].concat(get_average(D)),
S = S[1].concat(get_average(S)));
However, this is not any clearer, to a programmer, than an if() block as shown in your question. (Actually, many programmers may have to read it 2 or 3 times before getting it.)
Yet, there is another solution which is to use another function layer. In that last example, you would do something like this:
function VBA()
{
V = V[1].concat(get_average(V));
B = B[1].concat(get_mean(B));
A = A[1].concat(get_mean(A));
}
function DS()
{
D = D[1].concat(get_average(D));
S = S[1].concat(get_average(S));
}
op = [DS,null,null,VBA,null,null,...];
for(key in op)
{
// optional: if(op[key].hasOwnProperty(key)) ... -- verify that we defined that key
if(op[key])
{
op[key](); // call function
}
}
So in other words you have an array of functions and can use a for() loop to go through the various items and if defined, call the function.
All of that will very much depend on the number of combinations you have. You mentioned 2,500 lines of code, but the number of permutations may be such that writing it one way or the other will possibly not reduce the total number of lines, but it will make it easier to maintain because many lines are moved to much smaller code snippet making the overall program easier to understand.
P.S. To make it easier to read and debug later, I strongly suggest you put more spaces everywhere, as shown above. If you want to save space, use a compressor (minimizer), Google or Yahoo! both have one that do a really good job. No need to write your code pre-compressed.

MapReduce or Normal Queries? (Several emits per Map)

I have some different Map/Reduces functions that I use in my project. But one is a lot different than the others since it requires a loop in the map functionality. And for each count in the loop, I send an emit.
What I have is this scenario (in the user collection):
"channels" : [
"Channel 1",
"Channel 2",
],
What I want to do is to count how many users each channel has. So for that I could use db.users.find({channels: "Channel 1"}).count() but unfortunately channels are dynamic which means I don't know all the possible channel names and it may well change in the future.
So I thought that a Map/Reduce job would sit just perfect. But the problem is that the first Reduce job I wrote calculated wrong. And the other where I used a query for each emit, would come to take forever (more than 3 hours before the ssh session shut down).
So now I'm stuck and I need help, preferably I would want to have a Map/Reduce job since it's more nice than a bunch of queries which is kind of slow to run in real time.
This is the latest Map and Reduce functions I wrote:
var map = function() {
if(this.channels) {
for(var i = 0, imax = this.channels.length; i<imax; i++) {
emit(this.channels[i], 1);
}
}
}
var reduce = function (key, values) {
var result = 0;
values.forEach(function (value) {
// had this before: result += 1;
result = db.users.find({'channels' : key}).count();
});
return result;
}
I knew that the reduce function was horrific but I just tried the best I could think of. I think my logic may seem wrong but I can't find a good solution. Now I'm thinking of just doing a bunch of queries on every page load, but it will be slow as hell.
Please help! :)
In your scenario the reduce function should look like this:
var reduce = function (key, values) {
var result = 0;
values.forEach(function (value) {
result += value;
});
return result;
}
Let me know if it is still not working and if it does please give an example of input and (incorrect) output.
MR is sometimes a bit slow. So you might want to check out the new aggregation framework coming with 2.2 (which i think is s currently in release phase).
See: http://docs.mongodb.org/manual/applications/aggregation/
Additionally you might need to speed up the queries via using proper indices. Or adding a user count to the channels and increasing/decreasing when the user joining/leaving a channel. Depends on your app's use case of course.

Delete certain properties from an object without a loop

I'm trying to find the most efficient way of deleting properties from an object whose properties of commentCount and likeCount are both equal to 0. In the following example, Activity.3 would be removed. I don't want loop over them with a $.each() as that seems like it would take more time than necessary.
Activity = {
0 : {
'commentCount' : 10,
'likeCount' : 20
},
1 : {
'commentCount' : 0,
'likeCount' : 20
},
2 : {
'commentCount' : 10,
'likeCount' : 0
},
3 : {
'commentCount' : 0,
'likeCount' : 0
}
}
UPDATE
The circumstances of the creation of this object have come into question. To clarify, the Activity object can have up to 3 million properties inside of it. It's generated server side as an AJAX JSON response which is saved into memory. It includes more than just commentCount and likeCount that are used elsewhere, so I can't just not have the server not respond with things that have a 0 for both commentCount and likeCount.
Ah, the smell of premature optimization ^_^
How many of these objects do you have? How many do you need to clean them? If the answers are "less than 1 million" and "once or rarely", it's probably not worth to bother.
If you need a quick and optimal way, here is an idea: Create a new data structure and setters for the properties. Every time they are set, check whether they are both 0 and put them into a "kill" list.
That way, you just have to iterate over the kill list.
[EDIT] With several million objects and the need for a quick cleanup, a kill list is the way to go, especially when the condition is rare (just a few objects match).
Just write a function that updates these properties and make sure all code goes through it to update them. Then, you can manage the kill list in there.
Or you can simply delete the object as soon as the function is called to set both or the second property to 0.
I'm adding a second answer, because this solution comes from a completely different angle. In this solution, I attempt to find the fastest way to remove unwanted entries. I'm not aware of any way of doing this without loops, but I can think of several ways to do it with loops using jQuery as well as just raw javascript.
This jsperf shows all of the test cases side-by-side.
I'll explain each test and the caveats associated with each.
Raw JS: Slowest option. It looks like jQuery knows what they are doing with their $.each and $.map loops.
var obj;
for (var field in Activity) {
if (Activity.hasOwnProperty(field)) {
obj = Activity[field];
if (obj.commentCount === 0 && obj.likeCount === 0) {
delete Activity[field];
}
}
}
$.each: Tied for 2nd place. Cleaner syntax and faster than raw js loop above.
$.each(Activity, function(key, val){
if (val.commentCount === 0 && val.likeCount === 0) {
delete Activity[key];
}
});
$.map (Object version): Tied for 2nd place. Caveat: only supported in jQuery >= 1.6.
Activity = $.map(Activity, function(val, key){
if (val.commentCount === 0 && val.likeCount === 0) {
return null;
}
});
$.map (Array version): Fastest option. Caveat: You must use the $.makeArray function to convert your object to an array. I'm not sure if this is suitable for your needs.
var arrActivity = $.makeArray(Activity);
Activity = $.map(arrActivity, function(val, key){
if (val.commentCount === 0 && val.likeCount === 0) {
return null;
}
});
Conclusion
It looks like $.map is the fastest if you convert your object to an array using $.makeArray first.
This is just a starting-point, but how about something like this? Basically, it puts the Activities into buckets based on the sum of their likeCount and commentCount. It makes it easy to kill all of the Activities with no likes or comments, but I would assume there is a trade-off. I'm not sure how you are inserting these things and reading them. So, you'll have to decide if this is worth it.
var ActivityMgr = function(){
if(!(this instanceof ActivityMgr)){
return new ActivityMgr();
}
this.activities = {};
};
ActivityMgr.prototype.add = function(activity){
var bucket = parseInt(activity.commentCount, 10) + parseInt(activity.likeCount, 10);
if (this.activities[bucket] === undefined) {
this.activities[bucket] = [activity];
}
else {
this.activities[bucket].push(activity);
}
this.cleanse();
};
ActivityMgr.prototype.cleanse = function(){
this.activities[0] = [];
};
//Usage:
var activityMgr = new ActivityMgr();
activityMgr.add({
likeCount: 0,
commentCount: 10
});
EDIT:
After posting this, it becomes incredibly apparent that if you are adding items in this manner, you could just not add them if they have no likes or comments. My guess is that things aren't that simple, so please provide some detail as to how things are added and updated.

Iterate Through Nested JavaScript Objects - Dirty?

I have some JavaScript that I wrote in a pinch, but I think it could be optimized greatly by someone smarter than me. This code runs on relatively small objects, but it runs a fair amount of times, so its worth getting right:
/**
* Determine the maximum quantity we can show (ever) for these size/color combos
*
* #return int=settings.limitedStockThreshold
*/
function getMaxDefaultQuantity() {
var max_default_quantity = 1;
if (inventory && inventory.sizes) {
sizecolor_combo_loop:
for (var key in inventory.sizes) {
if (inventory.sizes[key].combos) {
for (var key2 in inventory.sizes[key].combos) {
var sizecolor_combo = inventory.sizes[key].combos[key2];
if (isBackorderable(sizecolor_combo)) {
//if even one is backorderable, we can break out
max_default_quantity = settings.limitedStockThreshold;
break sizecolor_combo_loop;
} else {
//not backorderable, get largest quantity (sizecolor_combo or max_default_quantity)
var qoh = parseInt(sizecolor_combo.quantityOnHand || 1);
if (qoh > max_default_quantity) {
max_default_quantity = qoh;
};
};
};
};
};
};
return Math.min(max_default_quantity, settings.limitedStockThreshold);
};
First, inventory is a object returned via JSON. It has a property inventory.sizes that contain all of the available sizes for a product. Each size has a property inventory.sizes.combos which maps to all of the available colors for a size. Each combo also has a property quantityOnHand that tells the quantity available for that specific combo. (the JSON structure returned cannot be modified)
What the code does is loop through each size, then each size's combos. It then checks if the size-color combo is backorderable (via another method). If it any combo is backorderable, we can stop because the default quantity is defined elsewhere. If the combo isn't backorderable, the max_default_quantity is the largest quantityOnHand we find (with a maximum of settings.limitedStockThreshold).
I really don't like the nested for loops and my handling of the math and default values feels overly complicated.
Also, this whole function is wrapped in a much larger jQuery object if that helps clean it up.
Have you considered using map-reduce? See a live example of a functional approach.
This particular example uses underscore.js so we can keep it on a elegant level without having to implement the details.
function doStuff(inventory) {
var max = settings.limitedStockThreshold;
if (!(inventory && inventory.sizes)) return;
var quantity = _(inventory.sizes).chain()
.filter(function(value) {
return value.combos;
})
.map(function(value) {
return _(value.combos).chain()
.map(function(value) {
return isBackorderable(value) ? max : value.quantityOnHand;
})
.max().value();
})
.max().value();
return Math.min(quantity, max);
}
As for an explanation:
We take the inventory.sizes set and remove any that don't contain combos. We then map each size to the maximum quantity of it's colour. We do this mapping each combo to either its quantity or the maximum quantity if backordable. We then take a max of that set.
Finally we take a max of set of maxQuantities per size.
We're still effectily doing a double for loop since we take two .max on the set but it doesn't look as dirty.
There are also a couple of if checks that you had in place that are still there.
[Edit]
I'm pretty sure the above code can be optimized a lot more. but it's a different way of looking at it.
Unfortunately, JavaScript doesn't have much in the way of elegant collection processing capabilities if you have to support older browsers, so without the help of additional libraries, a nested loop like the one you've written is the way to go. You could consider having the values precomputed server-side instead, perhaps cached, and including it in the JSON to avoid having to run the same computations again and again.

Categories

Resources