Change property within deeply nested data structure where name matches - javascript

I have some data structure like so:
{
"category": "Fruits",
"department": "ABC123",
"content": {
"subcategory1": [
{
"fruitName": "Apples",
"inStock": false
},
{
"fruitName": "Pears",
"inStock": false
}
],
"subcategory2": [
{
"fruitName": "Oranges",
"inStock": false
},
{
"fruitName": "Lemons",
"inStock": false
}
]
}
}
I'd like to be able to update the "inStock" value based on some input. I have an array like:
["Pears", "Oranges"]
For all instances of the properties in that array, I want to update inStock to be true to end up:
{
"category": "Fruits",
"department": "ABC123",
"content": {
"subcategory1": [
{
"fruitName": "Apples",
"inStock": false
},
{
"fruitName": "Pears",
"inStock": true
}
],
"subcategory2": [
{
"fruitName": "Oranges",
"inStock": true
},
{
"fruitName": "Lemons",
"inStock": false
}
]
}
}
I'd like to write some generic function for that, so that I can do something like const newData = updateStock( oldData );. However I'm not sure how to start writing this. If I could get some pointers for starting (even if not a solution) I would be very thankful.

Please always post your attempt in OP so that we get to know how you are approaching..
You can try below approach using Array.filter and Array.forEach
let data = {
"category": "Fruits",
"department": "ABC123",
"content": {
"subcategory1": [
{
"fruitName": "Apples",
"inStock": false
},
{
"fruitName": "Pears",
"inStock": false
}
],
"subcategory2": [
{
"fruitName": "Oranges",
"inStock": false
},
{
"fruitName": "Lemons",
"inStock": false
}
]
}
}
function updateStock(names) {
Object.values(data.content).flat()
.filter(d => names.includes(d.fruitName))
.forEach(d => d.inStock = true)
}
updateStock(['Oranges', 'Pears'])
console.log(data)

I started writing this before I saw #NitishNarang's answer, so I'll finish and expand on it a bit.
Start by breaking your problem down into smaller pieces.
First, you want a way to update the inStock property on an object. We'll move on to updating multiple objects in a nested data structure later.
I'm going to take a functional approach here. Let's write a higher-order function (a function 'factory', so to speak). When called with a property name and a value, it will return a new function. This new function, when called with an object, will update that object with that property set to the specified value:
const updateProperty = (key, value) => obj => {
obj[key] = value;
return obj;
};
Note that this function can be used for all manner of things. We could make a function to update the key called material with the value chocolate if we wanted:
const renderUseless = updateProperty('material', 'chocolate');
Example:
const teapot = { material: 'china' };
renderUseless(teapot);
Anyway, we can use updateProperty to do more useful things, like create convenience functions for marking an item as in or out of stock:
const setInStock = updateProperty('inStock', true);
const setOutOfStock = updateProperty('inStock', false);
Now, how might we mark a bunch of things as being in stock?
const products = [
{
fruitName: 'Apples',
inStock: false
},
{
fruitName: 'Pears',
inStock: true
}
];
products.forEach(setInStock);
So how might we only run the function for those elements that match a given predicate? Well, we could use Array.filter, which when given an array, returns a new array containing only those items that passed a given predicate.
Let's build a function that accepts an array of fruit names, and returns a predicate that returns true if its input object has a fruitName property that is included in that array, and false otherwise:
const hasFruitName = names => item =>
names.includes(item.fruitName);
Used in conjunction with Array.filter:
const products = [
{
fruitName: 'Apples',
inStock: false
},
{
fruitName: 'Pears',
inStock: true
}
];
const matchingProducts = products.filter(isFruitName(['Apples']));
So we now have a solution for a flat array of items. We just need to deal with your nested data structure. How might we create a flat array of items, given that you have a nested object containing several arrays?
Object.values(data.content) will grab the values from the object data.content, yielding an array of arrays. To flatten this into a single array, we could use Array.flat, but it's not widely-supported yet. Let's make an equivalent portable function:
const flattenNestedArray = arr => [].concat.apply([], arr);
Now let's make another convenience function:
const getItemsFromData = data =>
flattenNestedArray(Object.values(data.content));
Putting it all together we get:
const setFruitInStock = (fruitNames, data) =>
getItemsFromData(data)
.filter(hasFruitName(fruitNames))
.forEach(setInStock);
This is a bit over-engineered, but hopefully it will illustrate the benefits of starting with small, generic, higher-order functions. You end up with a lot of small, reusable helpers, that grow into slightly more specialised building blocks. Everything is named clearly, and by the time you reach the final implementation it's obvious what's happening at a glance.
Finally, I added data as a second parameter so your function is a bit more reusable.
I'll leave refactoring this for purity (i.e. not mutating the input data) as an exercise for the OP :)

Related

How to assert that a JSON contains certain keys?

I have successfully converted an excel sheet's content into a JSON, now I am trying to do some validation to it.
I need to assert that the jsonData below contains the following keys: Breakfast, Lunch, Snack, Dinner. It should also be in this specific order.
To test out an assertion first, I tried this:
const jsonData = [{
"Breakfast": "Cereal",
"Lunch": "Chicken",
"Snack": "Biscuit",
"Dinner": "Pork",
"Drinks": "Water"
}]
expect(jsonData).to.be.an('array').that.contains.keys('Breakfast')
And I got this error:
I would say to.contain.keys can't be applied to the array, but only the first object within the array.
Try splitting out that first item.
const jsonData = [{
"Breakfast": "Cereal",
"Lunch": "Chicken",
"Snack": "Biscuit",
"Dinner": "Pork",
"Drinks": "Water"
}]
expect(jsonData).to.be.an('array')
const firstItem = jsonData[0]
expect(firstItem).to.contain.keys('Breakfast')
Syntax for chai keys assertion
Reading the docs, I'm not sure to.contain.keys is correct syntax either.
Try
expect(firstItem).to.include.any.keys('Breakfast')
Simplest method
You can assert that "Breakfast" exists by comparing it to undefined
First object:
expect(jsonData[0].Breakfast).to.not.eq(undefined)
Every object in the array
jsonData.forEach(item => {
expect(item.Breakfast).to.not.eq(undefined)
}
Everyone's inputs have been such a big help. This is what I have come up with, let me know if there are ways to improve or if there are better ways to do this.
// This points to my testdata.json file
const column = testdata.Meals.column
const key = Object.keys(jsonData[0])
const value = Object.values(jsonData[0])
const jsonData = [{
"Breakfast": "Cereal",
"Lunch": "Chicken",
"Snack": "Biscuit",
"Dinner": "Pork",
"Drinks": "Water"
"Something": "Else"
}]
cy.wrap(column).each(($el, index, $list) => {
expect(jsonData[0]).to.haveOwnProperty(key[index])
// To check the specific order of the key-value pair
if (index == 0) {
expect(key[i]).to.eql(column[index])
expect(value[index]).to.eql(context.mealName)
} else if (index == 2) {
expect(key[index]).to.eql(column[index])
expect(value[index]).to.eql(context.snackName)
} else if (index == 4) {
expect(key[index]).to.eql(column[index])
expect(value[index]).to.eql(context.drinkName)
}
})
The testdata.js file:
{
"Meals": {
"column": [
"Breakfast",
"Lunch",
"Snack",
"Dinner",
"Drinks"
]
}
}

How can I add an element in object at certain position?

I have this object:
var ages = [{
"getasafieldDetail": {
"id": "xxx",
"asaentrySet": [{
"result": "ON",
"buy": {
"username": "Dis"
},
"offerSet": [{
"createdStr": "2001-08-09 at 11:52 pm",
"value": 5.0
}]
}]
}
}];
and i want to add an element and have an output like this:
var ages = [{
"getasafieldDetail": {
"id": "xxx",
"asaentrySet": [{
"result": "ON",
"buy": {
"username": "Dis"
},
"land": "111", // THIS <<<<------------
"offerSet": [{
"createdStr": "2001-08-09 at 11:52 pm",
"value": 5.0
}]
}]
}
}];
i tried using splice but not works...
ages.splice(ages[0]['getasafieldDetail']['asaentrySet'][0]['offerSet'],0,'"land": "111"');
ages.join();
There is the handy syntax of Destructuring assignments which helps with cutting and reassembling objects.
Edit
#FireFuro99 did point to the ES6/ES2015 spec which explicitly states how to preserve/handle an object's key-order at the object's creation time.
Thus one can say ...
Every JS engine which does support Destructuring assignment has to respect too any object's key order from/at this object's creation time.
const ages = [{
getasafieldDetail: {
id: "xxx",
asaentrySet: [{
result: "ON",
buy: {
username: "Dis",
},
offerSet: [{
createdStr: "2001-08-09 at 11:52 pm",
value: 5.0,
}],
}],
},
}];
const { result, buy, ...rest } = ages[0].getasafieldDetail.asaentrySet[0];
ages[0].getasafieldDetail.asaentrySet[0] = {
result,
buy,
land: "111",
...rest,
};
console.log({ ages });
.as-console-wrapper { min-height: 100%!important; top: 0; }
Splice only works on Arrays.
To make this work, convert your Object to an Array using Object.entries(), then use splice, and then convert it back to an object using Object.fromEntries().
const entrySet = Object.entries(ages[0]['getasafieldDetail']['asaentrySet'][0]);
entrySet.splice(2,0, ["land", "111"]);
ages[0]['getasafieldDetail']['asaentrySet'][0] = Object.fromEntries(entrySet);
This will insert the key-value pair at the the specified position.
The advantage this has over the destructuring assignment is, that you can specify the index, whereas destructuring is pretty hardcoded.
ages[0]["getasafieldDetail"]["asaentrySet"][0].land = '111' will create the key land in the first object in asaentrySet and assign the value 111. Key order is not guaranteed
var ages = [{
"getasafieldDetail": {
"id": "xxx",
"asaentrySet": [{
"result": "ON",
"buy": {
"username": "Dis"
},
"offerSet": [{
"createdStr": "2001-08-09 at 11:52 pm",
"value": 5.0
}]
}]
}
}];
ages[0]["getasafieldDetail"]["asaentrySet"][0].land = '111'
console.log(ages)
When it is an array of objects you could simple, add, passing the position that you want by editing the array like the example below:
let land = {land: 1111}
let ages = [{'a':11},'2', 'wd']
let new =[]
new.push(ages[1])
new.push(land)
ages[1] = new
console.log(ages)
output:
(3) [{…}, Array(2), "wd"]
You get what you want from the array, edit it, and put back in the same position, may it can help.

how to create a javascript Set with objects?

I have a json file businessList.json with the following
[
{"availableTimes": [{"type": "time", "time": "06:30"}, {"type": "time", "time": "07:00"}]},
{"availableTimes": [{"type": "time", "time": "08:30"}, {"type": "time", "time": "07:00"}]}
]
In another file, I am trying to create a set out of this, but it won't work, it'll show all of the duplicate values. I'm assuming it's because of how objects are passed by reference. How could I solve this to get to the desired result?
const timesAvailable = new Set()
businessList.map(item => item.availableTimes.map(item => timesAvailable.add(item))) //won't work
Like the first comment by Pointy says, objects are only equal to each other in JavaScript if they refer to the same place in memory. For example,
const object1 = { foo: 'bar' };
const object2 = object1;
object1 === object2 //true;
{ foo: 'bar' } === { foo: 'bar' } //false
There isn't any way around it with Set. One thing I've done in a similar situation is loop through the array and create a dictionary (either with a JavaScript Object or Map), generating a unique key for each item, then iterating through that Object or Map to get the unique times.
For example, in your case, something like:
const availableTimesMap = availableTimes.reduce((acc, timeObject) => {
const key = `${timeObject.type}-${timeObject.time}`;
acc[key] = timeObject;
}, {});
const uniqueTimes = Object.values(availableTimesMap);
Hope this helps. Let me know if you have any questions.
I had a similar issue in my own project. To solve it I ended up having to use strings to be the keys of a Map object. You would need to figure out a standard way to ID a time so that two "identical" times are converted to identical strings. Simply using the time would probably work well enough.
const timesAvailable = new Map()
businessList.forEach(item =>
item.availableTimes.forEach(item =>
timesAvailable.add(item.time, item)
)
)
Alternatively, you could do away with objects and just use the string times.
[
{"availableTimes": ["06:30", "07:00"]},
{"availableTimes": ["08:30", "07:00"]}
]
I am not sure what the structure of the result you want to get. I assume that if you want to get a set of non-duplicate objects and each object looks like this: {"type": "time", "time": "06:30"}.
let businessList = [
{"availableTimes": [{"type": "time", "time": "06:30"}, {"type": "time", "time": "07:00"}]},
{"availableTimes": [{"type": "time", "time": "08:30"}, {"type": "time", "time": "07:00"}]}
]
const timesAvailable = new Set(); // a set of strings representing time, which is used for checking duplicates
const availableBusiness = new Set(); //the result we want
for(let business of businessList) {
for (let availableTime of business.availableTimes) {
if(availableTime.type === "time") {
if(!timesAvailable.has(availableTime.time)){
timesAvailable.add(availableTime.time);
availableBusiness.add(availableTime);
}
}
}
}
console.log(availableBusiness);
console.log(timesAvailable);

Filter out objects in array if one value is the same

I am fetching data from an api that, sometimes, gives me multiple objects with the same values, or very similar values, which I want to remove.
For example, I might get back:
[
{
"Name": "blah",
"Date": "1992-02-18T00:00:00.000Z",
"Language": "English",
},
{
"Name": "blahzay",
"Date": "1998-02-18T00:00:00.000Z",
"Language": "French",
}, {
"Name": "blah", // same name, no problem
"Date": "1999-02-18T00:00:00.000Z", // different date
"Language": "English", // but same language
},
]
So I want to check that no two objects have a key with the same "Language" value (in this case, "English").
I would like to get the general process of filtering out the entire object if it's "Language" value is duplicated, with the extra issue of not having the same number of objects returned each time. So, allowing for dynamic number of objects in the array.
There is an example here:
Unexpeected result when filtering one object array against two other object arrays
but it's assuming that you have a set number of objects in the array and you are only comparing the contents of those same objects each time.
I would be looking for a way to compare
arrayName[eachObject].Language === "English"
and keep one of the objects but any others (an unknown number of objects) should be filtered out, most probably using .filter() method along with .map().
The below snippets stores the languages that have been encountered in an array. If the current objects language is in the array then it is filtered out. It makes the assumption that the first object encountered with the language is stored.
const objs = [
{
"Name": "blah",
"Date": "1992-02-18T00:00:00.000Z",
"Language": "English",
},
{
"Name": "blahzay",
"Date": "1998-02-18T00:00:00.000Z",
"Language": "French",
}, {
"Name": "blah", // same name, no problem
"Date": "1999-02-18T00:00:00.000Z", // different date
"Language": "English", // but same language
},
],
presentLanguages = [];
let languageIsNotPresent;
const objsFilteredByLanguage = objs.filter(function (o) {
languageIsNotPresent = presentLanguages.indexOf(o.Language) == -1;
presentLanguages.push(o.Language);
return languageIsNotPresent;
});
console.log(objsFilteredByLanguage);
You could take a hash table and filter the array by checking Name and Language.
var array = [{ Name: "blah", Date: "1992-02-18T00:00:00.000Z", Language: "English" }, { Name: "blahzay", Date: "1998-02-18T00:00:00.000Z", Language: "French" }, { Name: "blah", Date: "1999-02-18T00:00:00.000Z", Language: "English" }],
hash = {},
result = array.filter(({ Name, Language }) => {
var key = `${Name}|${Language}`;
if (!hash[key]) return hash[key] = true;
});
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }
Using Set makes it easy to remove duplicates for as many keys as you like. I tried to be as verbose as possible so that each step was clear.
var objects = [{ "Name": "blah", "Date": "1992-02-18T00:00:00.000Z", "Language": "English", }, { "Name": "blah", "Date": "1998-02-18T00:00:00.000Z", "Language": "French", }, { "Name": "blah", "Date": "1999-02-18T00:00:00.000Z", "Language": "English" }];
function uniqueKeyVals(objects, key) {
const objVals = objects.map(object => object[key]); // ex. ["English", "French", "English"]
return objects.slice(0, new Set(objVals).size); // ex. { "English", "French" }.size = 2
}
function removeKeyDuplicates(objects, keys) {
keys.forEach(key => objects = uniqueKeyVals(objects, key));
return objects;
}
// can also use uniqueKeyVals(key) directly for just one key
console.log("Unique 'Language': \n", removeKeyDuplicates(objects, ["Language"]));
console.log("Unique ['Language', 'Name']: \n", removeKeyDuplicates(objects, ["Language", "Name"]));
I would use the underscore module for JavaScript and the unique function in this scenario. Here is a sample array of data objects:
let data = [{
name: 'blah',
date: Date.now(),
language: "en"
},
{
name: 'noblah',
date: Date.now(),
language: 'es'
},
{
name: 'blah',
date: Date.now(),
language: 'en'
}];
Then we can use the unique function in the underscore library to only return a copy of the data that has unique values associated with the language key:
const result = _.unique(data, 'language');

Combining two objects from two different arrays

I have two arrays, orders and cartitems
I want to create a singel payload with information of both of arrays combined. to post to my API using Axios.
I've tried mapping one array (since I only need one item from the other array) and then trying to add the objects together like this:
const payload = []
let newArray = []
this.props.cartItems.map((item) => {
let payloadobject = {
productName: item.productname,
amount: item.qty,
price: item.price,
order_id: this.props.cart.id,
}
newArray = appendObjTo(payload, payloadobject);
})
Hoping newArray would hold the combined combined array. But get met with the error:
can't find variable: appendObjTo
How do I combine both objects? That are each in side of their own array
edit
current data structure
catritems
cartItems Array [
Object {
"id": 2,
"price": "6.50",
"productname": "Baco",
"qty": 2,
}
]
orders
orders Array [
Object {
"id": 2,
}
]
desired output
newArray Array [
Object {
"id": 2,
"price": "6.50",
"productname": "Baco",
"qty": 2,
"order_id": 1 (hold id from order object),
}
]
You get the error message since appendObjTo isn't defined. It's not a standard function, and if you've defined it yourself it's probably in another scope.
Instead of appendObjTo you could use the Object.assign function (MDN Reference). It could be used like this:
newArray = Object.assign({}, payload, payloadobject);
However, there's another fault in your code. Right now your assigning each combined object to newArray, and at the en it will hold the last combined object, not an array.
The lambda function you supply to map should return a new object that you want to replace the input object in the new array. When all objects are looped through the map function returns a new array (MDN Reference). In your case it could be used like this:
const payload = [];
let newArray = this.props.cartItems.map((item) => {
let payloadobject = {
productName: item.productname,
amount: item.qty,
price: item.price,
order_id: this.props.cart.id
};
return Object.assign({ }, payload, payloadobject);
});
This will make newArray be an array of object where each object is a combination of the whole payload, and payloadobject.
Hope this helps 😊

Categories