grouping similar models in a Backbone collection - javascript

I have a Backbone collection, where I am iterating through and looking up mdl.get('group'). This returns:
[undefined, undefined, group, group, group, undefined, group, group, group, undefined, group, group]
What I would like it to return is arrays (or collections) of those group models:
[undefined, undefined, [group], undefined, [group], undefined, [group]
I am trying to think of the best way to:
iterate through the object
return 'undefined' values without changing them
find siblings with similar values
collapse them into an array, within the object
Much like the _.groupBy method, but I need to retain the correct order of the models within the object.
I would love some ideas on how to approach this. So far I'm stumbling on how to tackle this one correctly.
thanks!

After some tinkering, came up with this solution. I extended Array.prototype so that you can easily drop it in. You can make an Underscore extension or a Collection.prototype method out of it:
Array.prototype.pack = function(field) {
var result = [], target, lastItem;
while(this.length > 0) {
var item = this.shift();
if(item === undefined) {
target = result;
} else {
if(!lastItem || item[field] != lastItem[field]) {
target = [];
result.push(target);
}
}
target.push(item);
lastItem = item;
}
return result;
}
Note it wasn't heavy tested, and for sure it can be refined, but should give you an idea.
You can use it like this:
models.pack("name")
where models is a plain array like [undefined, {field:value}].
Tried to be as concise as possible. A working demo here: http://jsfiddle.net/YZQ6v/

You can use collection.groupBy() to do this. First groupBy "group" to create a hash of grouped models then use collection.map to map the siblings onto the models.
Here is a fiddle:
http://jsfiddle.net/puleos/w7drB/
var models = [
{name: "Scott", group: "alpha"},
{name: "Rose", group: "alpha"},
{name: "Charles", group: "alpha"},
{name: "Stan"},
{name: "John"},
{name: "Mary", group: "beta"},
{name: "Dan", group: "beta"},
{name: "Paul", group: "beta"},
{name: "Grace"},
{name: "Sarah", group: "omega"}
];
var SourceCollection = Backbone.Collection.extend({});
var sourceCollection = new SourceCollection(models);
var grouped = sourceCollection.groupBy('group');
sourceCollection.map(function(model) {
if(model.has("group")) {
model.set({siblings: grouped[model.get("group")]});
}
});
// sourceCollection with siblings
console.log(sourceCollection);

Related

Lodash merging and unioning of nested array / object structure

I have two arrays that need merging in Javascript. They are arranged as follows:
arrayA = [town1A, town2A, town3A];
arrayB = [town3B, town5B];
Each town is an object with a townName: 'town1' (matching the object variable name). Each town also has an array of occupants: [{}, {}] which each have their own personName, and a status: 'dead' or 'alive'.
My goal, is that after merging, the new array will contain every unique town according to townName (town3B and town3A both have townName : 'town3').
arrayC = [town1, town2, town3, town5]
Any new towns in arrayB (i.e., town5) should be added directly to the list. Any towns with the same name (i.e., town3) should combine their lists of occupants, but remove any "dead" people. ArrayB has priority over ArrayA when determining status, as it is "overwriting" the old data. For example:
arrayA.town3.occupants = [{name: 'Bob', status: 'alive'}, {name: 'Joe', status: 'alive'}];
arrayB.town3.occupants = [{name: 'Bob', status: 'dead'}, {name: 'Alice', status: 'alive'}];
arrayC.town3.occupants = [{name: 'Joe', status: 'alive'}, {name: 'Alice', status: 'alive'}];
I'm just struggling with the logic sequence process here and need a nudge to figure out what tools to use. Currently I'm trying to work with Lodash's _.merge and _.union in some combination. It seems I can use _.mergeWith or _.unionBy to "nest" the merging steps without resorting to manually looping over the arrays, but their usage is going over my head. If a solution exists that uses one of those, I would like to see an example to learn better how they work.
Edit: I was asked for the entire contents of an example arrayA and arrayB:
arrayA = [
{
townName: 'town1',
occupants: [
{name: 'Charlie', status: 'alive'},
{name: 'Jim', status: 'dead'}
]
},
{
townName: 'town2',
occupants: [
{name: 'Rachel', status: 'alive'},
]
},
{
townName: 'town3',
occupants: [
{name: 'Bob', status: 'alive'},
{name: 'Joe', status: 'alive'}
]
}
];
arrayB = [
{
townName: 'town3',
occupants: [
{name: 'Bob', status: 'dead'},
{name: 'Alice', status: 'alive'}
]
},
{
townName: 'town5',
occupants: [
{name: 'Sam', status: 'dead'},
{name: 'Ray', status: 'alive'},
{name: 'Bob', status: 'alive'},
]
}
];
The output I expect is:
arrayC = [
{
townName: 'town1',
occupants: [
{name: 'Charlie', status: 'alive'},
]
},
{
townName: 'town2',
occupants: [
{name: 'Rachel', status: 'alive'},
]
},
{
townName: 'town3',
occupants: [
{name: 'Joe', status: 'alive'},
{name: 'Alice', status: 'alive'}
]
},
{
townName: 'town5',
occupants: [
{name: 'Ray', status: 'alive'},
{name: 'Bob', status: 'alive'},
]
}
];
I managed to find a consistent way to do this (thanks to #Enlico for some hints). Since _.mergeWith() is recursive, you can watch for a specific nested object property and handle each property differently if needed.
// Turn each array into an Object, using "townName" as the key
var objA = _.keyBy(arrayA, 'townName');
var objB = _.keyBy(arrayB, 'townName');
// Custom handler for _.merge()
function customizer(valueA, valueB, key) {
if(key == "occupants"){
//merge occupants by 'name'. _.union prioritizes first instance (so swap A and B)
return _.unionBy(valueB, valueA, 'name');
//Else, perform normal _.merge
}
}
// Merge arrays, then turn back into array
var merged = _.values(_.mergeWith(objA, objB, customizer));
// Remove dead bodies
var filtered = _.map(merged, town => {
town.occupants = _.filter(town.occupants, person => {return person.status == "alive";});
return town;
});
The complexity with this problem is that you want to merge on 2 different layers:
you want to merge two arrays of towns, so you need to decide what to do with towns common to the two arrays;
when handling two towns with common name, you want to merge their occupants.
Now, both _.merge and _.mergeWith are good candidates to accomplish the task, except that they are for operating on objects (or associative maps, if you like), whereas you have vectors of pairs (well, not really pairs, but objects with two elements with fixed keys; name/status and townName/occupants are fundamentally key/value) at both layers mentioned above.
One function that can be useful in this case is one that turns an array of pairs into an object. Here's such a utility:
arrOfPairs2Obj = (k, v) => (arr) => _.zipObject(..._.unzip(_.map(arr, _.over([k, v]))));
Try executing the following
townArr2townMap = arrOfPairs2Obj('townName', 'occupants');
mapA = townArr2townMap(arrayA);
mapB = townArr2townMap(arrayB);
to see what it does.
Now you can merge mapA and mapB more easily…
_.mergeWith(mapA, mapB, (a, b) => {
// … well, not that easily
})
Again, a and b are arrays of "pairs" name/status, so we can reuse the abstraction I showed above, defining
personArr2personMap = arrOfPairs2Obj('name', 'status');
and using it on a and b.
But still, there are some problems. I thought that the (a, b) => { … } I wrote above would be called by _.mergeWith only for elements which have the same key across mapA and mapB, but that doesn't seem to be the case, as you can verify by running this line
_.mergeWith({a: 1, b: 3}, {b:2, c:4, d: 6}, (x, y) => [x, y])
which results in
{
a: 1
b: [3, 2]
c: [undefined, 4]
d: [undefined, 6]
}
revealing that the working lambda is called for the "clashing" keys (in the case above just b), and also for the keys which are absent in the first object (in the case above c and d), but not for those absent in the second object (in the case above a).
This is a bit unfortunate, because, while you could filter dead people out of towns which are only in arrayB, and you could also filter out those people which are dead in arrayB while alive in arrayA, you'd still have no place to filter dead people out of towns which are only in arrayA.
But let's see how far we can get. _.merge doc reads
Source objects are applied from left to right. Subsequent sources overwrite property assignments of previous sources.
So we can at least handle the merging of towns common across the array in a more straightforward way. Using _.merge means that if a person is common in the two arrays, we'll always pick the one from arrayB, whether that's (still) alive or (just) dead.
Indeed, a strategy like this doesn't give you the precise solution you want, but not even one too far from it,
notSoGoodResult = _.mergeWith(mapA, mapB, (a, b) => {
return _.merge(personArr2personMap(a), personArr2personMap(b));
})
its result being the following
{
town1: [
{name: "Charlie", status: "alive"},
{name: "Jim", status: "dead"}
],
town2: [
{name: "Rachel", status: "alive"}
],
town3:
Alice: "alive",
Bob: "dead",
Joe: "alive"
},
town5: {
Bob: "alive",
Ray: "alive",
Sam: "dead"
}
}
As you can see
Bob in town3 is correctly dead,
we've not forgotten Alice in town3,
nor have we forogtten about Joe in town3.
What is left to do is
"reshaping" town3 and town5 to look like town1 and town2 (or alternatively doing the opposite),
filtering away all dead people (there's no more people appearing with both the dead and alive status, so you don't risk zombies).
Now I don't have time to finish up this, but I guess the above should help you in the right direction.
The bottom line, however, in my opinion, is that JavaScript, even with the power of Lodash, is not exactly the best tool for functional programming. _.mergeWith disappointed me, for the reason explained above.
Also, I want to mention that there a module named lodash/fp that
promotes a more functional programming (FP) friendly style by exporting an instance of lodash with its methods wrapped to produce immutable auto-curried iteratee-first data-last methods.
This shuould slightly help you be less verbose. With reference to your self answer, and assuming you wanted to write the lambda
person => {return person.status == "alive";}
in a more functional style, with "normal" Lodash you'd write
_.flowRight([_.curry(_.isEqual)('alive'), _.iteratee('status')])
whereas with lodash/fp you'd write
_.compose(_.isEqual('alive'), _.get('status'))
You can define a function for merging arrays with a mapper like this:
const union = (a1, a2, id, merge) => {
const dict = _.fromPairs(a1.map((v, p) => [id(v), p]))
return a2.reduce((a1, v) => {
const i = dict[id(v)]
if (i === undefined) return [...a1, v]
return Object.assign([...a1], { [i]: merge(a1[i], v) })
}, a1)
}
and use it like this:
union(
arrayA,
arrayB,
town => town.townName,
(town1, town2) => ({
...town1,
occupants: union(
town1.occupants,
town2.occupants,
occupant => occupant.name,
(occupant1, occupant2) => occupant1.status === 'alive' ? occupant1 : occupant2
).filter(occupant => occupant.status === 'alive')
})
)

Loop through an array of objects and get where object.field equals value

I'm currently working on a small application where I have to loop through an enormous array of objects. What would be the most efficient method to perform this?
var array = [
{
id: "1",
name: "Alpha"
},
{
id: "2",
name: "Beta"
},
...
];
I'd like to get each object where name equals "Alpha". I'm currently using a simple if statement to filter the objects with a different name value out, but I wonder if there's a more efficient way to do this, performance-wise.
It's worth to mention that I'll push the matching results into a new array.
No, there is no more efficient way.
The alternative is to build and maintain some kind of internal data structure which allows you to find the desired elements faster. As usual, the trade off is between the work involved in maintaining such a structure vs the time it saves you.
I don't have any way about which I would know it's more effective.
But if you had your objects ordered by name you could stop your search imideatly upon reaching an object whose name is not equal to "Alpha".
To find the first object you're looking for you can use binary search and from this Object you go up and down until at both ends you reach an object which isn't named "Alpha" or the end of array.
This is only a way of optimizing and will require time to sort the array and also will take more time when adding an element.
There's a JavaScript function exactly for this kind of task. Filter
From the Docs
The filter() method creates a new array with all elements that pass the test implemented by the provided function.
Here is a small example by code for getting all element from array which has a certain 'name' field:
const arr = [
{name: 'Abc'},
{name: 'Xyz'},
{name: 'Lmn'},
{name: 'Xyz'},
{name: 'Xyz'}
];
let response = findByName('Xyz');
console.log(response);
function findByName (name) {
return arr.filter((element) => {
return element.name = name;
});
}
If you need more than one time a collection with a given name, you could use an object with the names as hashes and have instantly access to the items.
var array = [{ id: "1", name: "Alpha" }, { id: "2", name: "Beta" }, { id: "3", name: "Beta" }, { id: "4", name: "Gamma" }, { id: "5", name: "Beta" }, { id: "2", name: "Alpha" }],
hash = Object.create(null);
array.forEach(function (a) {
if (!hash[a.name]) {
hash[a.name] = [];
}
hash[a.name].push(a);
});
console.log(hash);
.as-console-wrapper { max-height: 100% !important; top: 0; }

flatten and uneven array into an object

I have this Array and Object representing the same data:
arrayExample = [
{name: "max", age: 21},
{name: "max.David", age: 27},
{name: "max.Sylvia"},
{name: "max.David.Jeff"},
{name: "max.Sylvia.Anna", age: 20},
{name: "max.David.Buffy"},
{name: "max.Sylvia.Craig"},
{name: "max.Sylvia.Robin"}
];
ObjectExample = {
name: "max",
age: 21,
children: [
{
name: "Sylvia",
children: [
{name: "Craig"},
{name: "Robin"},
{name: "Anna", age: 20}
]
},
{
name: "David",
age: 27,
children: [
{name: "Jeff"},
{name: "Buffy"}
]
}
]
};
my objective is to extend the Array class to have 2 functions flatten which transform the objectExample into the arrayExample and uneven which do the opposite, I'm thinking maybe lodash would help here but I still didn't find the correct way to do this here's where I'm now:
to flatten from objectExample to arrayExample first the objectExample structure must be specific meaning the parents must share a property with all their children sure the parents and children could have other property that should be ported to the proper item in the new arrayExample, also for the uneven function it should create an object that all the parents share the same property with their children and other property should be copied respectively.
To give my use case for this I'm trying to make a d3js tree layout of angular ui router in my application that will be generated from the routes JSON file since I make the routes in a JSON file.
update:
my specific problem is that I need to create a d3js tree layout for angular-ui-router configurations states object which I can extract into a json file as I said before, the structure for the ui-router is like the arrayExample, and the required structure for the d3js tree layout is like the objectExample, one way to go about this is to manually rewrite it and it wont take too much time but that solution is not what I want I need to make a build task for this for generic routes that will always have the name attribute in their config object that could be used to find children of each route or state, for more information check ui-router for routes config object and this d3 videos for d3 tre layout:
part 1.
part 2.
correction: extending the Object class with a flatten function to flatten an object into an array and the Array class with unEven function to unEven an array into an object not like I wrote before:
my objective is to extend the Array class to have 2 functions.
update 2:
To make this more clear, both flatten and unEven are like the map function except flatten is for an object not an array and it return an array, and the unEven function is for an array but return an object.
Here's a function that will produce the flattened output:
Working demo: http://jsfiddle.net/jfriend00/w134L7c6/
var ObjectExample = {
name: "max",
age: 35,
status: "single",
hometown: "Scottsdale",
children: [
{
name: "Sylvia",
children: [
{name: "Craig", age: 16},
{name: "Robin"},
{name: "Anna"}
]
},
{
name: "David",
age: 54,
children: [
{name: "Jeff"},
{name: "Buffy"}
]
}
]
};
// call this on an object with a name property
// and an optional children property (which would be an array of objects)
function flatten(obj, key, outputArray, rootName) {
var name, item;
outputArray = outputArray || [];
rootName = rootName || "";
if (rootName) {
rootName += ".";
}
if (obj.hasOwnProperty(key)) {
name = rootName + obj[key];
item = {};
item[key] = name;
for (var prop in obj) {
if (obj.hasOwnProperty(prop) && prop !== "children") {
item[prop] = obj[prop];
}
}
outputArray.push(item)
if (obj.children) {
for (var i = 0; i < obj.children.length; i++) {
flatten(obj.children[i], key, outputArray, name);
}
}
}
return outputArray;
}
var result = flatten(ObjectExample, "name");
Produces this output:
[{"name":"max","age":35,"status":"single","hometown":"Scottsdale"},
{"name":"max.Sylvia"},
{"name":"max.Sylvia.Craig","age":16},
{"name":"max.Sylvia.Robin"},
{"name":"max.Sylvia.Anna"},
{"name":"max.David","age":54},
{"name":"max.David.Jeff"},
{"name":"max.David.Buffy"}]
You could adapt this function to be a method on the Array prototype if you really want to (not something I would recommend, particularly since the input isn't even an array).
I do not know what you mean when you say "the rootName could have more then one". ObjectExample is an object and thus cannot have more than one name at the top level. If you started with an array of ObjectExample like structures, then you could just loop over the array calling flatten() on each object in the top level array and it would accumulate the results.

Given an array of objects, find the object with a particular key

Given an array of objects, what is the best way to find the object with a particular key in JS?
Using jQuery and underscoreJS is fine. I'm just looking for the easiest / least code answer.
Example:
An array of objects, where each object has a "name". Find the object with a particular "name".
var people = [{name: "A"}, {name: "B"}, {name: "C"}];
My current solution:
Pass in the array, the key (e.g. "name"), and the value (e.g. "C").
function getObject(myArray, searchKey, searchValue) {
myArray.forEach(function(element){
if (element[searchKey] == searchValue) {
return element;
}
});
}
You can use Underscore.js's _.where function, like this
console.log(_.where(people, {
"name": "C"
}));
# [ { name: 'C' } ]
This returns all the objects in the array, which exactly matches the object we pass as the second argument.
You should go with grep of jQuery
var people = [{name: "A"}, {name: "B"}, {name: "C"}];
var obj1= jQuery.grep(people,function(n,i){return (n.name=='A')})
Using _.filter:
var people = [{name: "A"}, {name: "B"}, {name: "C"}];
var filteredPeople = _.filter(people, function(obj){
return obj['name'] == 'C';
});
console.log(JSON.stringify(filteredPeople)) // [{"name":"C"}]
If you want an array without the matched object(s), use _.reject:
var filteredPeople = _.reject(people, function(obj){
return obj['name'] == 'C';
});
console.log(JSON.stringify(filteredPeople)) // [{name: "A"}, {name: "B"}]
Without any custom library you can also do this. Please take a look at the following code
var people = [{name: "A"}, {name: "B"}, {name: "C"}],
match=function(element){
return element.name==="A";
};
console.log(people.filter(match));
But it is kind of static code . I don't know how to pass the key and value to the match() function.
I'm surprised not to find the obvious answer here, so: To do that with Underscore, you'd use _.find:
function getObject(myArray, searchKey, searchValue) {
return _.find(myArray, function(entry) { return entry[seachKey] === searchValue; });
}
You don't need Underscore for this, though; JavaScript's array type has find (as of ES5):
function getObject(myArray, searchKey, searchValue) {
return myArray.find(function(entry) { return entry[searchKey] === searchValue; });
}
As of ES2015+, you can make it more concise with an arrow function and destructuring:
function getObject(myArray, searchKey, searchValue) {
return myArray.find(({[searchKey]: value}) => value === searchValue);
}
Live example of that last one:
function getObject(myArray, searchKey, searchValue) {
return myArray.find(({[searchKey]: value}) => value === searchValue);
}
const people = [{name: "A"}, {name: "B"}, {name: "C"}];
console.log(getObject(people, "name", "C"));

How can I iterate through an object array and add matching values while pushing new values

So I have two data sets. The mainData being the data I want to push to and display as the main data set.
// data set 1
var mainData = [{name: "david", views: 2}, {name: "andrew", views: 2}];
// data set 2
var newData = [{name: "andrew", views: 4}, {name: "david", views: 4}, {name: "chris", views: 2}];
The second data set is new or incoming data which I want to do one of two things with. I want to search through the main data set and see if any keys match, if so I just want to add views to the object with the same name. If the name key doesn't match any object in the mainData I want to then push that object to mainData.
My final mainData should look like this:
[{name: "david", views: 6}, {name: "andrew", views: 6}, {name: "chris", views: 2}]
Notice how david and andrew now have values of 6 while chris did not match any objects and was simply pushed. What is the most effiecient way of acheiving this in pure Javascript?
If you want to empty newData during processing do:
while (newData.length) {
var nd = newData.shift(), nam = nd.name, vie = nd.view;
if (!mainData.some(function(md) {
if (md.name === nam) {md.view += vie; return true;}
})) mainData.push(nd);
}
else if the objects in newData shall stay there do
newData.forEach(function(nd) {
var nam = nd.nam, vie = nd.view;
if (!mainData.some(function(md) {
if (md.name === nam) {md.view += vie; return true;}
})) mainData.push(nd);
});
In both cases mainData.some() iterates over mainData. Its callback function checks at each step whether the name-properties are identic. If so, the view-properties are added, the callback returns true (= "matching names found") and iteration is stopped. Otherwise the callback returns nothing and iteration goes on. Since some() is inside an negated condition, mainData.push() happens only if some() does not find a match.
Array.forEach() and some() are very fast. If they are not available, you have to use for-loops with less efficiency.

Categories