Nested foreach/forloops, looping through JSON to extract values - javascript

I'm going to pre-face this with saying i'm not sure this is the best approach so other approaches are greatly appreciated
End Goal: To store a list of products and the toppings purchased by calling the woocommerce API and using the response data
I'm calling the woocommerce REST api that provides me a good chunk of JSON data back. In the JSON are line_items. These are the products purchased. Nested in line_items are meta_data, this is the toppings for example tomato or sauce.
Attached an image of the JSON
So what i'm trying to do is create something like this
var testOrderItems =
[{
title: "Fried Chicken Burger",
meta: [
"Lettuce",
"cheese slice",
"kethcup"
]
},
{
title: "Beef Burger",
meta: [
"Lettuce",
"cheese slice",
"kethcup"
]
}
]
which will follow my schema for oder items
var orderItems = new Schema({
title: {type: String, required: true},
meta: [{type: String}]
});
So to do this, i figured I would just do a forloop or foreach through the JSON to get all the product names and their meta. Getting actual values is easy. The hard part is creating the array or JSON object that I can then store, i'm just not sure how to create it whilst in the loop. Below are a few things I tried
let fullData = JSON.parse(result)
//parsed response from woocommerce API call
fullData.line_items.forEach((product, index) => {
//for each line item get me the product
orderItems.push(product.name)
//var namey =
//push the product name to the orderItemsArray
product.meta_data.forEach(function(meta) {
//checks for string as one of the plug-ins fills the meta with more nested information and we only want the top level string
if (typeof meta.value === 'string' || meta.value instanceof String)
// it's a string
orderItems.push(meta.value)
//Onbviously won't nest the meta with the product name just on new lines
})
});
The I thought I could do it in for loops by storing an ID ref as "i" and being able to re-reference this later in the nested loop to add the meta, i got a little lost with this
var length = fullData.line_items.length
for (let i = 0; i < length; i++) {
// console.log(i);
console.log(fullData.line_items[i].name)
for (let j = 0; j < fullData.line_items[i].meta_data.length; j++) {
var metaValue = fullData.line_items[i].meta_data[j].value
if (typeof metaValue === 'string' || metaValue instanceof String) {
console.log(fullData.line_items[i].meta_data[j].value);
stringMeta = fullData.line_items[i].meta_data[j].value
//this works but has drawbacks
//1 obviously just overwrites itself each time
//2 will stop at the end of meta so won't add items without meta
finalOrderItems = {
id: i,
name: fullData.line_items[i].name,
meta: [stringMeta]
}
}
}
}
and thats where I am, feels like this should be incredibly easy but can't quite grasp it at the moment.

You could simply create the object that represents your schema first, then return it from a map of your json Object. So, it would look like the following:
let testOrderItems = fullData.line_items.map((product)=>{
let obj = { name: product.name };
obj.meta = product.meta_data.map((meta)=>{
if (typeof meta.value === 'string' || meta.value instanceof String)
return meta.value;
}).filter((value)=>!!value);
return obj;
})
console.log(testOrderItems);
Although, the if statement seems a little redundant, since the woocommerce api will simply either have meta or not. However, you may have some plugin or something which is adding more information to the meta area so i've kept it in my example.

This looks like a job for map and reduce not forEach. map will map each object of line_items into a new object and reduce will group and organize the metas by key for each object:
var orderItems = fullData.line_items.map(function(product) { // map each product in line_items
return { // into a new object
title: product.name, // with title equals to the current product's name
meta: product.meta_data.reduce(function(acc, meta) { // and metas accumulated from each meta object in the current product's meta_data array
acc[meta.key] = acc[meta.key] || []; // first, check if there is an array for the current meta's key in the group object 'acc', if not create one
acc[meta.key].push(meta.value); // add the current meta's value to that array
return acc;
}, {})
}
});
Shorter using arrow functions:
var orderItems = fullData.line_items.map(product => ({
title: product.name,
meta: product.meta_data.reduce((acc, meta) => {
acc[meta.key] = acc[meta.key] || [];
acc[meta.key].push(meta.value);
return acc;
}, {})
}));

Related

pushing new object to array - cant get push() to work - using LocalStorage

I've tried a few stack examples. an i am still just rewriting what i was hoping to be adding to ... mainly we have a list of check box items each time you check it i would like existing items to turn into a list of those object so we can use them on a 'comparing' page
const handleChange = () => {
localStorage.setItem("item", JSON.stringify(products)); <-- works as expected
var existingItems: any[] = [];
existingItems?.push({ products });
localStorage.setItem("compareItems", JSON.stringify(existingItems));
};
existingItems is always = to products i want existing items to = [{item}, {item},] and so on
detail of what products would be:
products (object of key and values frm api) = {name: "Product 1", description: "Product 1", isActiveForEntry: true, displayRank: 0, sku: "ABC123"}
You have to get the existing items first before defaulting to an empty array:
const existingItems: any[] = JSON.parse(localStorage.getItem("compareItems")) ?? [];
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
existingItems.push({ products });
localStorage.setItem("compareItems", JSON.stringify(existingItems));
See nullish coalescing operator for what the ?? means.
At each method invocation, you are initializing existingItems object then storing its value with one item being the products hence the stored object will only hold one item at a time.
You should retrieve the compareItems stored value being your existingItems then append the new products objet to it:
const handleChange = () => {
localStorage.setItem("item", JSON.stringify(products));
const compareItems = JSON.parse(localStorage.getItem("compareItems"));
var existingItems: any[] = compareItems ? compareItems : [];
existingItems.push({ products });
localStorage.setItem("compareItems", JSON.stringify(existingItems));
};

Improve loop over nested properties - javascript

Trying to filter students according to a nested property value.
Basically each student object has property of type array : studentSuperSubject
which in turn has array as a property as well : studentSubSubjectOrSkill
subjects is array of objects in the following form:
{superSubject: "some super subject name", subSubject: "some sub subject name"}.
each subSubject inside studentSubSubjectOrSkill has the property studentSubSubjectScore which is the one I'm looking for.
I want to loop over the students array for each subject in subjects and create and object i then store to another array - grades=[]
I manage to get it done with what looks like a Hugh bawl of spaghetti code:
for (let subject of subjects) {
var grades = [];
this.data.students.filter(student => {
let name = student.studentName;
// loop over each student > superSubject > subSubject
for (let superSubject of student.studentSuperSubject) {
if (superSubject.superSubjectName === subject.superSubject) {
superSubject.studentSubSubjectOrSkill.filter(sub => {
if (sub.subSubjectName === subject.subSubject) {
grades.push({
name: name,
pass: sub.studentSubSubjectScore > 58
});
return;
}
});
}
}
});
subject["grades"] = grades;
}
Any idea how can I improve that to make life easier for the future developers which are going to use this code?
as a side note, lodash is available in this project
thanks.
1/ You have improper use of filter that should be used when you want to return subset of the array, you should use map when you want to execute an action for each element of the array
2/ for... of ... is for iterating over the values, for... in... over the properties
3/ I think it is more readable like this:
for (let subject of subjects) {
var grades = [];
this.data.students.map(student => {
let name = student.studentName;
student.studentSuperSubject
.filter(superSubject => superSubject.superSubjectName === subject.superSubject)
.map(superSubject=> {
superSubject.studentSubSubjectOrSkill
.filter(sub => sub.subSubjectName === subject.subSubject)
.map(sub=>{
grades.push({
name: name,
pass: sub.studentSubSubjectScore > 58
});
});
});
});
subject["grades"] = grades;
}
For readability, I would check one thing at a time and add some comments.
// Iterates for all subjects.
subjects.forEach(subject => {
// Subject grades.
subject.grades = [];
// Iterates for all the students.
students.forEach(student => {
// Finds and validates the student super subject.
var studentSuperSubject = student.studentSuperSubject.find(superSubject => superSubject.superSubjectName === subject.superSubject);
if (!studentSuperSubject) return;
// Finds and validates the student sub subject.
var studentSubSubject = studentSuperSubject.studentSubSubjectOrSkill.find(subSubject => subSubject.subSubjectName == subject.subSubject);
if (!studentSubSubject) return;
// Saves the student grade.
subject.grades.push({
name: student.studentName,
pass: studentSubSubject.studentSubSubjectScore > 58
});
});
});

How to declare a Hash/Dictionary of Array

I have a program that pushes values into one data structure like this:
if(symbolType == "C" || symbolType == "P") // The calls and puts
stocks.push({
symbol: symbol,
undsymbol: undSymbol,
open: 0,
type: symbolType,
expiry: expiry,
days: days,
strike: strike
});
}
else // The stock
{
stocks.push({
symbol: symbol,
open: 0,
type: symbolType
});
}
So this is the key: NOT A STRING!
{
symbol: symbol,
open: 0,
type: symbolType
}
And the values of which are many look like this:
{
symbol: symbol,
undsymbol: undSymbol,
open: 0,
type: symbolType,
expiry: expiry,
days: days,
strike: strike
}
The problem is that stocks and calls and puts are being put into one collection. Instead, I want to add the the stocks and their corresponding calls and puts into a dictionary/map, where the stocks are the keys, and the calls and puts get pushed into an array indexed by it's stock.
At the end, I want to be able to iterate and get the keys and values.
How do I declare this object
Index into it to see if the key[stock] already exists, if it doesn't add it with an empty array.
If I get a "C" or "P", I want to get the corresponding array that holds the Calls/Puts for this key [stock] and push the call/put into the array.
Initially I thought the declaration was something like this:
var stockCallsPutDict = {[]}
stockCallsPutDict[stock] = [];
stockCallsPut[stock].push(call);
// Pretty print the dict of keys and its options =
stockCallsPutDict.forEach(function kvp) {
...
}
If ES6 is an option, you can either build an object yourself or use a Map.
Here's some quick code I came up with:
const stocks = {};
const addCallAndPut = callAndPut => {
const symbol = callAndPut.symbol;
if (!stocks[symbol]) {
stocks[symbol] = [];
}
stocks[symbol].push(callAndPut);
}
const showStuff = () => {
for (const symbol in stocks) {
// output stuff using stocks[symbol]
}
}
OR WITH A MAP
const stocks = new Map();
// basic implementation
const addCallAndPut = callAndPut => {
const stockCallsAndPuts = stocks.get(callAndPut.symbol) || [];
stockCallsAndPuts.push(callAndPut);
stock.set(callAndPut.symbol, stockCallsAndPuts);
}
There are a few ways to go about this, and the best depends on how the data needs to be processed later, but from your description I'd go with something along the lines of
var stocks = {};
var stockCallsPut = {};
// loop over stocks and actions
if (!(symbol in stocks)) {
stocks[symbol] = [];
}
if (!(symbol in stockCallsPut)) {
stockCallsPut[symbol] = {};
}
if (!(symbolType in stockCallsPut[symbol])) {
stockCallsPut[symbol][symbolType] = [];
}
// accumulated stock json items here
stocks[symbol].push(new_stock_item);
// accumulated push/call json items of stock here
stockCallsPut[symbol][symbolType].push(new_action);
I'm still not sure I actually understood what your data looks like, but sounds kind of like this to me:
// Not sure if data is an object or array
var data = {
'one': {
'name': 'one-somename',
'number': 'one-somenumber',
'symbol': 'C'
},
'two': {
'name': 'two-somename',
'number': 'two-somenumber',
'symbol': 'P'
},
'three': {
'name': 'three-somename',
'number': 'three-somenumber',
'symbol': 'C'
}
};
var stocks = {};
for (var name in data) {
// It sounded like you wanted a call/put array for each object but I'm not sure if that's true since it wouldn't be possible... if so can just divide this part up into it's appropriate place in the if statement below
// Checking that the property is set on the object, if it is, it uses itself, otherwise it adds it with the call/put arrays created
stocks[name] = stocks[name] ? stocks[name] : {'calls': [], 'puts': []};
var type;
if (data[name]['symbol'] === 'C') {
type = 'calls';
} else if (data[name]['symbol'] === 'P') {
type = 'puts';
}
stocks[name][type].push(data[name]);
}

AngularJS: Merge object by ID, i.e. replace old entry when IDs are identical

I am using Ionic with AngularJS and I am using a localForage database and AJAX via $http. My app has a news stream that contains data like this:
{
"feed":[
{
"id":"3",
"title":"Ein Hund",
"comments:"1"
},
{
"id":"2",
"title":"Eine Katze",
"comments":"2"
}
],
"ts":"20150907171943"
}
ts stands for Timestamp. My app saves the feed locally via localForage.
When the app starts it first loads the locally saved items:
$localForage.getItem("feed").then(function(val) { vm.feed = val; })
Then, it loads the new or updated items (ts < current timestamp) and merges both the old and new data:
angular.extend(vm.feed, response.data.feed);
Updated items look like this:
{
"feed":[
{
"id":"2",
"title":"Eine Katze",
"comments":"4"
}
],
"ts":"20150907171944"
}
That is, the comments count on feed item 2 has changed from 2 to 4. When I merge the old and new data, vm.feed has two items with id = 2.
Does angularjs has a built-in "merge by id" function, i. e. copy from source to destination (if it is a new element), or otherwise replace the old element? In case angularjs does not have such a function, what's the best way to implement this?
Thanks in advance!
angular.merge(vm.feed, response.data.feed);
// EDIT
Probably, it will not merge correctly, so you have to update all properties manually. Update ts property and then find your object with id and replace it.
There is no builtin, I usually write my own merge function:
(function(){
function itemsToArray(items) {
var result = [];
if (items) {
// items can be a Map, so don't use angular.forEach here
items.forEach(function(item) {
result.push(item);
});
}
return result;
}
function idOf(obj) {
return obj.id;
}
function defaultMerge(newItem, oldItem) {
return angular.merge(oldItem, newItem);
}
function mergeById(oldItems, newItems, idSelector, mergeItem) {
if (mergeItem === undefined) mergeItem = defaultMerge;
if (idSelector === undefined) idSelector = idOf;
// Map retains insertion order
var mapping = new Map();
angular.forEach(oldItems, function(oldItem) {
var key = idSelector(oldItem);
mapping.set(key, oldItem);
});
angular.forEach(newItems, function(newItem) {
var key = idSelector(newItem);
if (mapping.has(key)) {
var oldItem = mapping.get(key);
mapping.set(key, mergeItem(newItem, oldItem));
} else {
// new items are simply added, will be at
// the end of the result list, in order
mapping.set(key, newItem);
}
});
return itemsToArray(mapping);
}
var olds = [
{ id: 1, name: 'old1' },
{ id: 2, name: 'old2' }
];
var news = [
{ id: 3, name: 'new3' },
{ id: 2, name: 'new2' }
];
var merged = mergeById(olds, news);
console.log(merged);
/* Prints
[
{ id: 1, name: 'old1' },
{ id: 2, name: 'new2' },
{ id: 3, name: 'new3' }
];
*/
})();
This builds a Map from the old items by id, merges in the new items, and converts the map back to list. Fortunately the Map object will iterate on the entries in insertion order, according to the specification. You can provide your idSelector and mergeItem functions.
Thanks hege_hegedus. Based on your code, I've written my own and tried to use less loops to speed things up a bit:
function updateCollection(localCollection, fetchedCollection) {
angular.forEach(fetchedCollection, function(item) {
var append = true;
for (var i = 0; i < localCollection.length; i++) {
if (localCollection[i].id == item.id) {
// Replace item
localCollection[i] = item;
append = false;
break;
} else if (localCollection[i].id > item.id) {
// Add new element at the right position, if IDs are descending check for "< item.id" instead
localCollection.splice(i, 0, item);
append = false;
break;
}
}
if (append) {
// Add new element with a higher ID at the end
localCollection.push(item);
// When IDs are descending use .unshift(item) instead
}
});
}
There is still room for improvements, i. e. the iteration through all the objects should use binary search since all items are sorted by id.

Best way to store this data (array, object, etc)

I need to store (many) objects or arrays of data, which need to have the following criteria:
I need to be able to add a new set of data into the existing data easily
I need to be able to sort the data by date/ time added easily (array in order of when entries were pushed to it)
I need to be able to grab an entry easily using a reference, either integer or string. This is important, at the moment I have to do an $.each() to loop through my data until I find the entry I want.
I have tried using a structure like:
saved_info = {
1001: {//all my data for ref 1001},
1002: {//all my data for ref 1002}
}
which gave me what wanted of being able to grab the info easily given a reference:
info = saved_info[1001];
however, the reference numbers I use aren't in order - I use a reference given to me (its a unique identifier), therefore the object isn't in order of when items were added/saved/pushed.
You can use two objects:
One that stores the data by key
Another that stores the sort order
This way you can (i) lookup an element by key (ii) loop over elements in the order they were inserted. Rough outline of the structure:
var DataObject = {
data: {},
sort: []
};
Here is how you add data to this structure:
DataObject.data[1004] = {name: "Test 4"};
DataObject.sort.push(1004);
DataObject.data[1001] = {name: "Test 1"};
DataObject.sort.push(1001);
DataObject.data[1003] = {name: "Test 3"};
DataObject.sort.push(1003);
DataObject.data[1002] = {name: "Test 2"};
DataObject.sort.push(1002);
Here is how you perform a random access:
console.log(DataObject.data[1001].name);
console.log(DataObject.data[1003].name);
And here is how you iterate over all elements in the order they were added:
var i;
for (i = 0; i < DataObject.sort.length; i++) {
console.log(DataObject.data[DataObject.sort[i]].name);
}
It is possible to wrap the entire logic inside a class:
function DataObject() {
this.data = {};
this.sort = [];
this.setItem = function (k, v) {
this.data[k] = v;
this.sort.push(k);
};
this.getItemByKey = function (k) {
return this.data[k];
};
this.getItemByPos = function (i) {
return this.data[this.sort[i]];
};
this.getAllItems = function () {
var i, r = [];
for (i = 0; i < this.sort.length; i++) {
r.push(this.data[this.sort[i]]);
}
return r;
};
}
var t = new DataObject();
t.setItem(1001, {name: "Test 1"});
t.setItem(1002, {name: "Test 2"});
t.setItem(1003, {name: "Test 3"});
t.setItem(1004, {name: "Test 4"});
console.log(t.getItemByKey(1001));
console.log(t.getItemByPos(0));
console.log(t.getAllItems());
Try to build a Json like this,
var xJson = {
"1001":{//all my data for ref 1001},
"1002":{//all my data for ref 1002}
};
and you can fetch the records as per your wish using the bracket notation, since we are using a numeric value as a key.
var xData = xJson["1001"];

Categories