Creating and referencing an array of objects in Javascript - javascript

I'm trying to create a generic To Do list that has tasks and subtasks. Any given To Do list will have one or more tasks, and each task will have one or more subtasks. For each subtask, I'm displaying card that presents the task and subtask, as well as some other relevant information about the user. Thus, the number of cards presented for any given list equals the number of subtasks, and can be categorized by task.
The tricky part for me is that I need to be able to reference these cards (as I will be making an API call to get task and subtask data -- both of which are arrays, incidentally -- from different types of lists), and I'm not sure the best way to reference something that effectively has 2 indexes (task and subtask).
My thinking has been focused on creating a toDoList array of task:subtask objects [{task 1, subtask 1}, {task 1, subtask 2}, {task 2, subtask 1), etc}] -- but when I start doing that I'm getting stuck in two places:
in programmatically creating the key-value pairs (I'm using a nest loop to iterate through tasks and subtasks)
how to reference them later, as I need to associate these task:subtask pairs with specific DOM objects to do perform functions like hiding them, for example.
I have some code but it feels really far off the mark so not even including here. Basically, I feel like there's a simple solution of less than a dozen lines of code that a better programmer than I will see immediately, and I'd appreciate the help. Many thanks!

Not sure how much control you have over writing the data structure, but maybe something like this would work.
The idea here is that you give all subtasks an ID, then keep track of a task's subtasks through an array of subtask IDs.
var tasks = [
{name: 'clean apartment', subtasks: [1, 2, 3]},
{name: 'do the dishes', subtasks: [6]},
{name: 'eat lunch', subtasks: [4, 5]}
];
var subtasks = [
{name: 'vaccuum', id: 1},
{name: 'dust', id: 2},
{name: 'wipe down', id: 3},
{name: 'make sandwich', id: 4},
{name: 'drink a beer', id: 5},
{name: 'soak pans', id: 6},
];
var i, j, l = tasks.length;
for(i; i < l; i++) {
var task = tasks[i],
ll = task.subtasks.length;
for(j=0; j < ll; j++) {
// Now you know the parent task and all its subtasks
}
}
Additionally, if you didn't mind using the help of a library like underscore, you could do it the opposite way. This way you give all tasks an ID, then give subtasks a task field, which maps them to a task. Using underscore's findWhere you can easily lookup the task when iterating over every subtask.
var tasks = [
{name: 'clean apartment', id: 1},
{name: 'do the dishes', id: 2},
{name: 'eat lunch', id: 3}
];
var subtasks = [
{name: 'vaccuum', task: 1},
{name: 'dust', task: 1},
{name: 'wipe down', task: 1},
{name: 'make sandwich', task: 3},
{name: 'drink a beer', task: 2},
{name: 'soak pans', task: 2},
];
for(var i = 0; i < subtasks.length; i++) {
var subtask = subtasks[i],
task = _.findWhere(tasks, {id: subtask.task})
// Now you know the subtask and its task
}
From the comments on my post, I also created a fiddle to give an example of creating subtask cards: http://jsfiddle.net/grammar/ynw0eb25/

Could a structure like this be Ok for the job?
[
{taskId: '1',
subtasks:[{subtaskId:'1', otherAttributes:'...'},
{subtaskId:'2', otherAttributes:'...'},
...
]
},
{taskId: '2',
subtasks:[{subtaskId:'1', otherAttributes:'...'},
...
]
}
....
]

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')
})
)

How to transfer MAP.values into Arrays efficiently?

In the example below, you see some noisy but straightforward implementation.
Initial Situation
We have an initial Array with repeating information.
const listArray = [
{ songBy: 'George Michael', uid: 'A', whatEver: 12},
{ songBy: 'George Michael', uid: 'A', whatEver: 13},
{ songBy: 'George Michael', uid: 'A', whatEver: 14},
{ songBy: 'Michael Jackson', uid: 'B', whatEver: 12},
{ songBy: 'Michael Jackson', uid: 'B', whatEver: 16},
]
STEP 1
We create a new Map because we distinctly want to save artist names, which means → no artist-name-repetitions (and we also want to get rid of the third column, by the way).
We need to use songBy as key, for some reason. We change it within the value into name.
const listMap = new Map();
listArray.forEach(
row => listMap.set(
row.songBy, {name: row.songBy, uid: row.uid}
)
);
STEP 2
Finally, we need an array with its values:
const distinctListArray: Array<any> = [];
listMap.forEach(value => distinctListArray.push(value));
So we achieve a result as an Array of distinct objects in the form of:
[
name: string
uid: string
]
Question:
To my mind, this implementation is too noisy and not so elegant. There are too many steps and too many variables. (This example here is a simplified version of a real code I cannot share).
Is there a way to simplify that code and make it more efficient?
EDIT: See online: TypeScript Playground
Convert the list to [key, value] pairs using Array.map(), and then create the map from the list. Convert the Map back to an array by applying Array.from() to the Map.values() iterator (TS playground):
const listArray = [{"songBy":"George Michael","uid":"A","whatEver":12},{"songBy":"George Michael","uid":"A","whatEver":13},{"songBy":"George Michael","uid":"A","whatEver":14},{"songBy":"Michael Jackson","uid":"B","whatEver":12},{"songBy":"Michael Jackson","uid":"B","whatEver":16}]
const distinctListArray = Array.from(
new Map(listArray.map(({ songBy, uid }) =>
[songBy, { name: songBy, uid }]
)).values()
)
console.log(distinctListArray)

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.

Multi Level mappings in Parent/Child hierarchy in D3 Possible?

I've a flat file which is of format
"id":1,"depends_on":2
"id":1,"depends_on":4
"id":1,"depends_on":5
"id":2,"depends_on":3
"id":4,"depends_on":5
Normally if I don't have
"id":1,"depends_on":5
I can plot the D3 Layout as follows
When I include "id":1,"depends_on":5
Graph that's being plot will be
Ideally I should have a line between 1 & 5 too, along with other mappings.
1)How can I achieve this?
2)How should the data-structure should be?
Does it really need to have duplicate entries (objects) in various parts of main data-structure ( to obtain in the format D3 needed (parent,children[])
Using d3.layout.force produces
Check out this example, which uses d3.layout.force().
A force layout's data structure involves 2 arrays:
an array of nodes
[{id: 1}, {id: 2}, {id: 3}, {id: 4}]`
and array of links
[
{ source: {id: 1}, target: {id: 2} },
{ source: {id: 1}, target: {id: 3} },
{ source: {id: 2}, target: {id: 4} }
]
The objects used in the nodes array and links array should be the same objects. I.e, in the example above, nodes[0] == links.source[0] should be true.

angular filter a scope down to 1 key type to prepare for send

So I have a scope that has a large chunk of key value pairs, so that I can work with it and make it do a bunch of stuff before I send it.
The problem I am having is I want to change that scope into just a small array of the keys of id with inside of it before I send it
so I have the scope which has the obect that is like this :
$scope.myScope = {name: name 1, id: 1, order: 1}, {name: name 2, id: 2, order: 2}
And I want to turn it into
$scope.FilteredScope = {1,2};
of just the id's. I'm wondering if you can filter this scope inside of the controller before send? I've only ever used filters in stuff like repeats. so inside of the controller, it would filter out the ids and place them in maybe a new scope for sending just the ids. Is this possible?
Thanks!
You could just use array.map (may need to polyfill for older browsers):
angular.module('MyModule', [])
.controller('MyController', function( $scope ) {
$scope.myScope = [
{name: 'name1', id: 1, order: 1},
{name: 'name2', id: 2, order: 2}
];
$scope.ids = $scope.myScope.map( function(obj){
return obj.id;
});
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app='MyModule' ng-controller='MyController'>
ids: {{ids | json}}
</div>
Firstly for the $scope.myScope to be a correct Javascript Object ( in this case it is an array of map Objects ), it should be of the form:
$scope.myScope = [{name: "name 1", id: 1, order: 1}, {name: "name 2", id: 2, order: 2}]
Secondly, if assume you want your $scope.filteredScope to be an array of all the id values from the $scope.myScope object.
So it can easily be done like this
$scope.filteredScope = []
for(entry of $scope.myScope) $scope.filteredScope.push(entry.id)
This will lead to an output array like [1,2] which you can then send it.
Please let me know if my assumptions were right.If not please edit your question with more info and I will be happy to help

Categories