Related
I thought I understood how to loop through a dictionary, but my loop is wrong. I try to access the name of each sub item but my code does not work.
Here is what I did:
list = [
{
title: 'Groceries',
items: [
{
id: 4,
title: 'Food',
cost: 540 ,
},
{
id: 5,
title: 'Hygiene',
cost: 235,
},
{
id: 6,
title: 'Other',
cost: 20,
},
],
}];
function calculateCost(){
let total = 0;
Object.keys(list).forEach((k) => { for (i in k.items) { total += i.data; } });
console.log(total);
return total;
}
Your list is an array includes 1 object and this object has two properties title and items the items here is an array of objects each one of these objects has property cost so to calculate the total cost you need to loop through items array, here is how you do it:
let list = [
{
title: 'Groceries',
items: [
{
id: 4,
title: 'Food',
cost: 540 ,
},
{
id: 5,
title: 'Hygiene',
cost: 235,
},
{
id: 6,
title: 'Other',
cost: 20,
},
],
}];
function calculateCost(){
let total = 0;
list[0].items.forEach(el => {
total += el.cost;
})
console.log(total)
return total;
}
calculateCost();
Your list is an Array, not an Object.
Instead of Object.keys() use Array.prototype.reduce:
const calculateCost = (arr) => arr.reduce((tot, ob) =>
ob.items.reduce((sum, item) => sum + item.cost, tot), 0);
const list = [
{
title: 'Groceries',
items: [
{id: 4, title: 'Food', cost: 10},
{id: 5, title: 'Hygiene', cost: 20},
{id: 6, title: 'Other', cost: 30}
]
}, {
title: 'Other',
items: [
{id: 8, title: 'Scuba gear', cost: 39}
],
}
];
console.log(calculateCost(list)); // 99
Expanding on #Roko's and #mmh4all's answers, the following code adds several verification statements to handle cases where a deeply nested property in your data is not what you expect it to be.
const calculateCost = (orders) => {
let listOfCosts = [];
// For each 'order' object in the 'orders' array,
// add the value of the 'cost' property of each item
// in the order to 'listOfCosts' array.
orders.forEach(order => {
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/isArray
if (!Array.isArray(order.items)) { return; }
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isNaN
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/parseFloat
const orderCostArr = order.items.map(item =>
isNaN(item.cost) ? 0 : parseFloat(item.cost, 10));
if (orderCostArr.length === 0) { return; }
// Concatenate 'orderCostArr' to the 'listOfCosts' array
//listOfCosts = listOfCosts.concat(orderCostArry);
// Alternate approach is to use the spread syntax (...) to
// push the items in the array returned by 'order.items.map()'
// into the 'listOfCosts' array.
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax
listOfCosts.push(...orderCostArr);
});
// Use the 'reduce' method on the 'listOfCosts' array
// to get the total cost.
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce
const totalCost = listOfCosts.reduce(
(accumulator, currentValue) => accumulator + currentValue, 0);
return totalCost;
};
const list = [
{
title: 'Groceries',
items: [
{ id: 4, title: 'Food', cost: 10 },
{ id: 3, title: 'Baked goods', cost: 20 },
{ id: 5, title: 'Hygiene', cost: 0 },
{ id: 6, title: 'Other' }
]
}, {
title: 'Gear',
items: {},
}, {
title: 'Accessories',
items: [],
}, {
title: 'Bags',
}, {
title: 'Other',
items: [
{ id: 10, title: 'Scuba gear', cost: "5" },
{ id: 8, title: 'Scuba gear', cost: "err" },
{ id: 9, title: 'Scuba gear', cost: 59 }
],
}
];
console.log(calculateCost(list)); // 94
I have an array of objects which presents tasks. These tasks are categorized (primary / secondary category).
let tasks = [
{
id: 1,
name: 'Cleanup desk',
primary_category: {
id: 1,
name: 'Indoor'
},
secondary_category: {
id: 2,
name: 'Surfaces'
}
},
{
id: 2,
name: 'Cleanup office floors',
primary_category: {
id: 1,
name: 'Indoor'
},
secondary_category: {
id: 3,
name: 'Ground'
}
},
{
id: 3,
name: 'Water plants',
primary_category: {
id: 2,
name: 'Outdoor'
},
secondary_category: {
id: 3,
name: 'Irrigation'
}
}
];
I now try to create a categories accordion in my frontend and therefore need to group my array differently. The structure should look like:
1) primary category
> secondary category
> tasks
> secondary category
> tasks
2) primary category
> secondary category
> tasks
Therefore I'm trying to achieve a structure similar to this:
let tasks_categorized = [
{
id: 1,
name: 'Indoor',
secondary_categories: [
{
id: 2,
name: 'Surfaces',
tasks: [
{
id: 1,
name: 'Cleanup desk'
}
]
},
{
id: 3,
name: 'Ground',
tasks: [
{
id: 2,
name: 'Cleanup office floors'
}
]
}
]
},
{
id: 2,
name: 'Outdoor',
secondary_categories: [
{
id: 3,
name: 'Irrigation',
tasks: [
{
id: 3,
name: 'Water plants'
}
]
}
]
}
];
I tried using groupBy by lodash but this does not allow grouping by multiple nested key-value pairs. Does anybody know an approach to solve this?
Thank you in advance!
The following provided approach is going to achieve the expected result within a single reduce cycle without any further nested loops.
It does so by implementing a reducer function which creates and/or aggregates at time a prioritized category task while iterating another task array. But most importantly it keeps track of a task item's related primary and secondary categories via a Map based lookup. This lookup reference together with a result array are properties of this function's return value which has to be partly provided as the reduce method's initial value as follows ... { result: [] }.
function createAndAggregatePrioritizedCategoryTask(
{ lookup = new Map, result }, item
) {
const { primary_category, secondary_category, ...taskRest } = item;
const { id: primaryId, name: primaryName } = primary_category;
const { id: secondaryId, name: secondaryName } = secondary_category;
const primaryKey = [primaryId, primaryName].join('###');
const secondaryKey = [primaryKey, secondaryId, secondaryName].join('###');
let primaryCategory = lookup.get(primaryKey);
if (!primaryCategory) {
// create new primary category item.
primaryCategory = {
id: primaryId,
name: primaryName,
secondary_categories: [],
};
// store newly created primary category reference in `lookup`.
lookup.set(primaryKey, primaryCategory);
// push newly created primary category reference to `result`.
result.push(primaryCategory);
}
let secondaryCategory = lookup.get(secondaryKey);
if (!secondaryCategory) {
// create new secondary category item.
secondaryCategory = {
id: secondaryId,
name: secondaryName,
tasks: [],
};
// store newly created secondary category reference in `lookup`.
lookup.set(secondaryKey, secondaryCategory);
// push newly created secondary category reference into the
// `secondary_categories` array of its related primary category.
primaryCategory
.secondary_categories
.push(secondaryCategory);
}
// push the currently processed task-item's rest-data as
// item into the related secondary category's `task` array.
secondaryCategory
.tasks
.push(taskRest);
return { lookup, result };
}
let tasks = [{
id: 1,
name: 'Cleanup desk',
primary_category: { id: 1, name: 'Indoor' },
secondary_category: { id: 2, name: 'Surfaces' },
}, {
id: 2,
name: 'Cleanup office floors',
primary_category: { id: 1, name: 'Indoor' },
secondary_category: { id: 3, name: 'Ground' },
}, {
id: 3,
name: 'Water plants',
primary_category: { id: 2, name: 'Outdoor' },
secondary_category: { id: 3, name: 'Irrigation' },
}];
const { result: tasks_categorized } = tasks
.reduce(createAndAggregatePrioritizedCategoryTask, { result: [] });
console.log({ tasks_categorized });
.as-console-wrapper { min-height: 100%!important; top: 0; }
You could take a dynamic approach with an array of arrays with functions and keys for the nested arrays.
const
tasks = [{ id: 1, name: 'Cleanup desk', primary_category: { id: 1, name: 'Indoor' }, secondary_category: { id: 2, name: 'Surfaces' } }, { id: 2, name: 'Cleanup office floors', primary_category: { id: 1, name: 'Indoor' }, secondary_category: { id: 3, name: 'Ground' } }, { id: 3, name: 'Water plants', primary_category: { id: 2, name: 'Outdoor' }, secondary_category: { id: 3, name: 'Irrigation' } }],
groups = [
[o => o, 'primary category'],
[o => o.primary_category, 'secondary category'],
[o => o.secondary_category, 'tasks']
],
result = tasks.reduce((r, o) => {
groups.reduce((parent, [fn, children]) => {
const { id, name } = fn(o);
let item = (parent[children] ??= []).find(q => q.id === id)
if (!item) parent[children].push(item = { id, name });
return item;
}, r);
return r;
}, {})[groups[0][1]];
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }
I have an object looking like this
const item = {
id: 123,
type: 'book',
sections: [{
type: 'section',
id: '456',
index: 1,
lessons: [{
type: 'lesson',
id: 789,
index: 1
},
{
type: 'lesson',
id: 999,
index: 2
}
]
}, {
type: 'section',
index: 2,
id: 321,
lessons: [{
type: 'lesson',
id: 444,
index: 1
},
{
type: 'lesson',
id: 555,
index: 2
}
]
}]
}
It should be assumed that there are more objects in sections and lessons array. I want to create a new object like this
result = [{
section: 456,
lessons: [789, 999]
}, {
section: 321,
lessons: [444, 555]
}]
I tried this loop but this just pushes indexes and not lesson's ids
let obj = {};
let sectionWithLessons = [];
let lessons = []
for (const i in item.sections) {
obj = {
sectionId: item.sections[i].id,
lessonIds: item.sections[i].lessons.map((lesson) => {
return lessons.push(lesson.id)
}),
};
sectionWithLessons.push(obj);
}
console.log(sectionWithLessons);
How can i do this correctly and preferably with good performance in consideration?
I believe the best/shortest thing is to use the map function, like:
const result2 = item.sections.map(({id, lessons}) => ({
id,
lessons: lessons.map(({id: lessionId}) => lessionId)
}))
I would suggest using Array.map() to convert the item sections to the desired result.
We'd convert each section into an object with a section value and lessons array.
To create the lessons array, we again use Array.map() to map each lesson to a lesson id.
const item = { id: 123, type: 'book', sections: [{ type: 'section', id: '456', index: 1, lessons: [{ type: 'lesson', id: 789, index: 1 }, { type: 'lesson', id: 999, index: 2 } ] }, { type: 'section', index: 2, id: 321, lessons: [{ type: 'lesson', id: 444, index: 1 }, { type: 'lesson', id: 555, index: 2 } ] }] }
const result = item.sections.map(({ id, lessons }) => {
return ({ section: +id, lessons: lessons.map(({ id }) => id) })
});
console.log('Result:', result);
.as-console-wrapper { max-height: 100% !important; }
I have a javascript object as follows :
let hogwartsHeirarchy = {
Headmaster: [
{
name: "Professor Dumbledore",
key: 1,
Headmistress: [
{
name: "Minerva McGonagall",
key: 2,
StandByProfessor: [
{
name: "Rubeus Hagrid",
subject: "Potions Master",
key: 3,
Professor: [
{ name: "Horace Slughorn", key: 4 },
{ name: "Severus Snape", key: 4 },
],
},
{
name: "Alastor Moody",
subject: "Defense Against the Dark Arts",
key: 3,
Professor: [
{ name: "Remus Lupin", key: 4 },
{ name: "Gilderoy Lockhart", key: 4 },
],
},
],
},
],
},
],
};
I want to print/get each of the node value [headmaster, headmastress,..] and their corresponding child values. I tried various methods like looping through the array using a for loop, recurse etc, but unfortunately I am not able to get any value out of the nodes. Please help.
e.g : I used this :
printArray(hogwartsHeirarchy);
function printArray(arr){
for(var i = 0; i < arr.length; i++){
if(arr[i] instanceof Array){
console.log("true: ");
console.log("intermediate one : ",arr[i]);
printArray(arr[i]);
}else{
console.log("final one : ",arr[i]);
}
}
}
The values can be shown in this format:
Headmaster - name : Professor Dumbledore, key : 1
.
.
StandByProfessor - name : Robeus Hagrid, subject : Potion Master, key : 3
StandByProfessor - name : Alastor Moody, subject : Defence against the dark arts, key : 3
.
.
Professor - ...
Professor - ...
Professor - ...
Professor - ...
I would suggest restructuring so that the subordinates are always accessed with the same key, and thus can be visited very simply. I also made it so every node is a person, there's no object at the top that isn't a person. I left the variable name, but it now refers to Dumbledore directly.
let hogwartsHeirarchy =
{
name: "Professor Dumbledore",
role: "Headmaster",
key: 1,
subordinates: [
{
name: "Minerva McGonagall",
role: "Headmistress",
key: 2,
subordinates: [
{
name: "Rubeus Hagrid",
role: "StandByProfessor",
subject: "Potions Master",
key: 3,
subordinates: [
{ name: "Horace Slughorn", key: 4, role: "Professor" },
{ name: "Severus Snape", key: 4, role: "Professor" },
],
},
{
name: "Alastor Moody",
role: "StandByProfessor",
subject: "Defense Against the Dark Arts",
key: 3,
subordinates: [
{ name: "Remus Lupin", key: 4, role: "Professor" },
{ name: "Gilderoy Lockhart", key: 4, role: "Professor" },
],
},
],
},
],
};
function visitStaff(staffMember) {
if (staffMember.subordinates) {
for (const subordinate of staffMember.subordinates) {
visitStaff(subordinate);
}
}
console.log("Staff member:", staffMember);
}
visitStaff(hogwartsHeirarchy);
When setting up a data structure, it's important to think about how it will be accessed, and what are its defining parts. In this case, there are people, which are the nodes, and (subordinate) relationships, which are the edges of the graph.
In your original code, you had an object { Headmaster: [...] } — what does it represent? is it a person? no; is it a relationship? kind of, but no. It defines something about Dumbledoor, that he's the Headmaster, but not who or what he's the Headmaster of. So it's really just describing the role/job title of Dumbledoor, so it makes more sense as a property of the person. It's redundant as an object.
It helps to align your objects so they all represent something. You should be able to describe what each object and array is.
Given the data structure:
a) I assume that there will be only one array in each type of "title", and
b) that array will contain a list of objects of a similar structure as it's parent
It's possible ...
to use for..of in order to
iterate through each key on an object, and add them to a string. Because there are arrays that contains objects,
I can loop through them, and
do a recursive loop, by having the method calling itself.
const hogwartsHierarchy = { Headmaster: [{ name: "Professor Dumbledore", key: 1, Headmistress: [{ name: "Minerva McGonagall", key: 2, StandByProfessor: [{ name: "Rubeus Hagrid", subject: "Potions Master", key: 3, Professor: [{ name: "Horace Slughorn", key: 4 }, { name: "Severus Snape", key: 4 }] }, { name: "Alastor Moody", subject: "Defense Against the Dark Arts", key: 3, Professor: [{ name: "Remus Lupin", key: 4 }, { name: "Gilderoy Lockhart", key: 4 }] }] }] }] };
function printAllWithChilds(obj, prevProp) {
let listItem = (prevProp) ? ' -- ' + prevProp : '';
for (const property in obj) { // 1
if (obj[property] instanceof Array) {
obj[property].forEach((child_obj) => { // 3
listItem += printAllWithChilds(child_obj, property); // 4
});
} else {
listItem += `, ${property}: ${obj[property]}`; // 2
}
}
return listItem;
}
let listStr = printAllWithChilds(hogwartsHierarchy);
console.log(listStr);
I would honestly split up hogwartsHierarchy into smaller bits, following a kind of database structure, where primary_key is unique for each individual. These arrays doesn't make much sense, until you look at the variable professors and how their respectively belongs_to key corresponds to the standbyprofessors, where you can see that "Horace Slughorn" belongs to "Rubeus Hagrid".
const headermaster = {
name: "Professor Dumbledore",
primary_key: 1
};
const headmistress = {
name: "Minerva McGonagall",
primary_key: 2,
belongs_to: 1
};
const standbyprofessors = [{
name: "Rubeus Hagrid",
subject: "Potions Master",
primary_key: 3,
belongs_to: 2
},
{
name: "Alastor Moody",
subject: "Defense Against the Dark Arts",
primary_key: 4,
belongs_to: 2
}
];
const professors = [{
name: "Horace Slughorn",
primary_key: 5,
belongs_to: 3
},
{
name: "Severus Snape",
primary_key: 6,
belongs_to: 3
},
{
name: "Remus Lupin",
primary_key: 7,
belongs_to: 4
},
{
name: "Gilderoy Lockhart",
primary_key: 8,
belongs_to: 4
},
];
You could remove known keys from the object and get the type hierarchy then iterate the property and return the tupel of type, name, subject and key only if type exists.
const
getValues = (object, type) => [
...(type ? [`${type} - name : ${object.name}, ${object.subject ? `subject : ${object.subject}, ` : ''}key : ${object.key}`] : []),
...Object
.keys(object)
.filter(k => !['name', 'subject', 'key'].includes(k))
.flatMap(k => object[k].flatMap(o => getValues(o, k)))
],
hogwartsHierarchy = { Headmaster: [{ name: "Professor Dumbledore", key: 1, Headmistress: [{ name: "Minerva McGonagall", key: 2, StandByProfessor: [{ name: "Rubeus Hagrid", subject: "Potions Master", key: 3, Professor: [{ name: "Horace Slughorn", key: 4 }, { name: "Severus Snape", key: 4 }] }, { name: "Alastor Moody", subject: "Defense Against the Dark Arts", key: 3, Professor: [{ name: "Remus Lupin", key: 4 }, { name: "Gilderoy Lockhart", key: 4 }] }] }] }] };
console.log(getValues(hogwartsHierarchy));
.as-console-wrapper { max-height: 100% !important; top: 0; }
Based on a 1j01 object model:
const hogwartsHeirarchy =
{ name: "Professor Dumbledore", role: "Headmaster", key: 1,
subordinates: [{ name: "Minerva McGonagall", role: "Headmistress", key: 2,
subordinates: [
{ name: "Rubeus Hagrid", role: "StandByProfessor", subject: "Potions Master", key: 3,
subordinates: [
{ name: "Horace Slughorn", key: 4, role: "Professor" },
{ name: "Severus Snape", key: 4, role: "Professor" }]},
{ name: "Alastor Moody", role: "StandByProfessor", subject: "Defense Against the Dark Arts", key: 3,
subordinates: [
{ name: "Remus Lupin", key: 4, role: "Professor" },
{ name: "Gilderoy Lockhart", key: 4, role: "Professor" }]}]}]};
const visitStaff = (staffMember) => {
const iter = (obj) => {
const { name, role, key, subordinates } = obj;
const subject = obj.subject ? `, subject : ${obj.subject}` : '';
const line = `${role} - name : ${name}${subject}, key : ${key}`;
const sublines = (Array.isArray(subordinates)) ? subordinates.flatMap(iter) : [];
return [line, ...sublines];
}
return iter(staffMember).join('\n');
}
console.log(visitStaff(hogwartsHeirarchy));
.as-console-wrapper { max-height: 100% !important; top: 0; }
I have two streams of objects, the accounts and balances.
I need to merge (join) the two streams according to the id and account_id
var accounts = Rx.Observable.from([
{ id: 1, name: 'account 1' },
{ id: 2, name: 'account 2' },
{ id: 3, name: 'account 3' },
]);
var balances = Rx.Observable.from([
{ account_id: 1, balance: 100 },
{ account_id: 2, balance: 200 },
{ account_id: 3, balance: 300 },
]);
What is expected:
var results = [
{ id: 1, name: 'account 1', balance: 100},
{ id: 2, name: 'account 2', balance: 200},
{ id: 3, name: 'account 3', balance: 300},
];
Is this feasible with RxJs ?
To be clear I know how to do this with plain js/lodash or something similar. In my case I am getting these streams from Angular Http Module, so I am asking If I could get benefit of RxJs in this case
Accoring to one of your comment, your example is to simulate a stream from an Angular Http call.
So instead of :
var accounts = Rx.Observable.from([
{ id: 1, name: 'account 1' },
{ id: 2, name: 'account 2' },
{ id: 3, name: 'account 3' },
]);
var balances = Rx.Observable.from([
{ account_id: 1, balance: 100 },
{ account_id: 2, balance: 200 },
{ account_id: 3, balance: 300 },
]);
I'd rather say that it is :
var accounts = Rx.Observable.of([
{ id: 1, name: 'account 1' },
{ id: 2, name: 'account 2' },
{ id: 3, name: 'account 3' },
]);
var balances = Rx.Observable.of([
{ account_id: 1, balance: 100 },
{ account_id: 2, balance: 200 },
{ account_id: 3, balance: 300 },
]);
Why : from will emit every item one by one, of will emit the entire array and I guess your http response is the whole array.
That said, what you probably want to achieve is :
const { Observable } = Rx;
// simulate HTTP requests
const accounts$ = Rx.Observable.of([
{ id: 1, name: 'account 1' },
{ id: 2, name: 'account 2' },
{ id: 3, name: 'account 3' }
]);
const balances$ = Rx.Observable.of([
{ account_id: 1, balance: 100 },
{ account_id: 2, balance: 200 },
{ account_id: 3, balance: 300 }
]);
// utils
const joinArrays = (accounts, balances) =>
accounts
.map(account => Object.assign({}, account, { balance: findBalanceByAccountId(balances, account.id).balance }));
const findBalanceByAccountId = (balances, id) =>
balances.find(balance => balance.account_id === id) || { balance: 0 };
const print = (obj) => JSON.stringify(obj, null, 2)
// use forkJoin to start both observables at the same time and not wait between every request
Observable
.forkJoin(accounts$, balances$)
.map(([accounts, balances]) => joinArrays(accounts, balances))
.do(rslt => console.log(print(rslt)))
.subscribe();
Output :
[
{
"id": 1,
"name": "account 1",
"balance": 100
},
{
"id": 2,
"name": "account 2",
"balance": 200
},
{
"id": 3,
"name": "account 3",
"balance": 300
}
]
Here's a working Plunkr : https://plnkr.co/edit/bc0YHrISu3FT45ftIFwz?p=preview
EDIT 1 :
Working on an array to compose your result is probably not the best idea for performance and instead of returning an array, maybe try to return an object which have as key, the ID of the account. This way you might simply remove the findBalanceByAccountId function and have a faster app (only modified code here) :
const balances$ = Rx.Observable.of({
1: { account_id: 1, balance: 100 },
2: { account_id: 2, balance: 200 },
3: { account_id: 3, balance: 300 }
});
// utils
const joinArrays = (accounts, balances) =>
accounts
.map(account => Object.assign(
{},
account,
{ balance: balances[account.id].balance }
));
If you have truly 2 observables that emit the results as Observable<{}> in random order, this is a way you can combine them. If the order is not random, or if they always come in 'pairs', more efficient way's exists to combine them.
import { from, merge } from 'rxjs';
import { map, scan, tap } from 'rxjs/operators';
const accounts = from([
{ id: 1, name: 'account 1' },
{ id: 2, name: 'account 2' },
{ id: 3, name: 'account 3' }
]);
const balances = from([
{ account_id: 1, balance: 100 },
{ account_id: 2, balance: 200 },
{ account_id: 3, balance: 300 }
]);
interface Outcome {
id: number;
name?: string;
balance?: number;
}
merge<Outcome>(
accounts,
balances.pipe(map(a => ({ id: a.account_id, balance: a.balance })))
)
.pipe(
scan<Outcome>((result: Outcome[], incomming) => {
const found = result.find(row => row.id === incomming.id);
if (found) {
Object.assign(found, incomming);
} else {
result.push(incomming);
}
return result;
}, []),
tap(r => console.log(r))
)
.subscribe();
Please note that the result is a hot observable. If you only want to emit a single result and complete when all results are in, replace the scan operator with the reduce operator.
The source is based on RXjs version 6. your imports might differ a bit on older versions.