JavaScript - Properly Extract Deep Object Properties and Construct New Object - javascript

Suppose the following array of objects is returned from an API:
const data = [
{ // first item
meta: {
stems: [
"serpentine",
"serpentinely"
]
},
hwi: {
hw: "sep*pen*tine",
prs: [
{
mw: "ˈsər-pən-ˌtēn",
sound: {
audio: "serpen02"
}
},
]
},
shortdef: [
"of or resembling a serpent (as in form or movement)",
"subtly wily or tempting",
"winding or turning one way and another"
]
},
{ // second item
meta: {
stems: [
"moribund",
"moribundities",
"moribundity"
]
},
hwi: {
hw: "mor*i*bund",
},
fl: "adjective"
}
]
I want to create a function that will generate a new array of objects. The objects in this new array will consist of data from the old objects, just rearranged. This is how I expect a new array to look, for example:
[
{
word: 'serpentine',
definitions: [
'of or resembling a serpent (as in form or movement)',
'subtly wily or tempting',
'winding or turning one way and another'
]
},
{
word: 'moribund',
definitions: [
'being in the state of dying : approaching death',
'being in a state of inactivity or obsolescence'
],
partOfSpeech: 'adjective'
}
]
I do this with the following function:
const buildNewData = arr => {
const newData = []
arr.forEach(item => {
newData.push({
...item.meta.stems[0] && { word: item.meta.stems[0]},
...item.shortdef && { definitions: item.shortdef },
...item.fl && { partOfSpeech: item.fl },
...item.hwi.prs[0].mw && { pronunciation: item.hwi.prs[0].mw}
})
})
return newData
}
buildNewData(data)
You may be curious as to why I use ...item.meta.stems[0] && { word: item.meta.stems[0]} in the creation of the new objects. This is to check if the property exists in the original object. If it doesn't exist, the expression will evaluate to false and therefore not be added to the new object. The first object in the original array does not have the fl property, so it evaluates to false when the new object is being constructed.
But this doesn't work when looking up a property that is an array. The code above fails with the error: TypeError: Cannot read property '0' of undefined. That's because the second item does not have a prs array under the hwi property, so the lookup fails.
Since I cannot control what data is returned from the API, how do I write a function that successfully creates a new array of objects in the format I've specified, without causing an error? I already have a solution to not add particular properties if they do not exist, but how do I take into account arrays?
More generally, I'm curious if there is a standardized way of extracting data from objects programmatically that prevents errors like this from occurring. Is there a better way to do this?

You need an additional guard so:
...item.hwi.prs[0].mw && { pronunciation: item.hwi.prs[0].mw}
becomes
...(Array.isArray(item.hwi.prs) && item.hwi.prs[0].mw) && { pronunciation: item.hwi.prs[0].mw}
which can be shortened to:
...(item.hwi.prs && item.hwi.prs[0].mw) && { pronunciation: item.hwi.prs[0].mw}
if you are confident that if item.hwi.prs exists its value will be an array that has a 0 value that can be spread.
const data = [
{ // first item
meta: {
stems: [
"serpentine",
"serpentinely"
]
},
hwi: {
hw: "sep*pen*tine",
prs: [
{
mw: "ˈsər-pən-ˌtēn",
sound: {
audio: "serpen02"
}
},
]
},
shortdef: [
"of or resembling a serpent (as in form or movement)",
"subtly wily or tempting",
"winding or turning one way and another"
]
},
{ // second item
meta: {
stems: [
"moribund",
"moribundities",
"moribundity"
]
},
hwi: {
hw: "mor*i*bund",
},
fl: "adjective"
}
];
const buildNewData = arr => {
const newData = []
arr.forEach(item => {
newData.push({
...item.meta.stems[0] && { word: item.meta.stems[0]},
...item.shortdef && { definitions: item.shortdef },
...item.fl && { partOfSpeech: item.fl },
...(Array.isArray(item.hwi.prs) && item.hwi.prs[0].mw) && { pronunciation: item.hwi.prs[0].mw}
})
})
return newData
}
let newData = buildNewData(data);
console.log(newData);

As you need to check existence of properties in an Object:
Use Optionnal chaining: https://javascript.info/optional-chaining
It returns a type undefined if the prop doesn't exist (but not string "undefined" ;) )
For desired order in new array, add numbers before the names of props.
let newData = [];
for (let i = 0; i < data.length; i++) {
newData[i] = {};
if (data[i]?.meta?.stems[i] != undefined)
newData[i].word = data[i].meta.stems[i];
if (data[i]?.shortdef != undefined) {
newData[i].definitions = data[i].shortdef.join(', ') + '.';
newData[i].definitions = newData[i].definitions.charAt(0).toUpperCase() + newData[i].definitions.substring(1); // Capitalize first letter
}
if (data[i]?.fl != undefined)
newData[i].partOfSpeech = data[i].fl;
}
console.log(...newData);

Related

How can I return a dynamic data using methods in JavaScript?

I am having a variable that needs to return data according to the array of objects.
There are 3 dummies which are dynamic in such a way -->
Example:
dummy1_A, dummy1_B, dummy1_C, ...
dummy2_A, dummy2_B, dummy2_C, ...
dummy3_A, dummy3_B, dummy3_C, ...
I want to set 'fieldName' and 'text' while returning.
And this is my code. I have to use JSON.stringify to show the data I need
Also, I am using map method
let a1 = [
{
dummy1_A: 0.5526714707565221,
dummy2_A: 0.5526714707565223,
dummy3_A: 0.5526714707565224,
dummy1_B: 0.5028423429150607,
dummy2_B: 0.5028423429150605,
dummy3_B: 0.5028423429150604,
},
{
dummy1_A: 0.542947572819916,
dummy2_A: 0.4965857885945633,
dummy3_A: 0.4965857885945677,
dummy1_B: 0.4470431086251489,
dummy2_B: 0.3785646261205342,
dummy3_B: 0.3785646261205345,
},
];
let a2 = a1.map((x, i) => {
let seqStr = String(a1.entries); // I need some help here
return {
text: seqStr,
fieldName: seqStr,
};
});
// output
[
{ text: 'dummy1_A', fieldName: 'A' },
{ text: 'dummy2_A', fieldName: 'A' },
{ text: 'dummy1_B', fieldName: 'B' },
{ text: 'dummy2_B', fieldName: 'B' },
];
We can also use forEach but this also need more logic
a1.forEach(obj => {
const key = Object.keys(obj)[0];
const newKey = key[key.length - 1];
obj[newKey] = obj[key];
delete obj[key];
});
I have used console.log(JSON.stringify(a2)); Though I was using map but not got such result
let newHeaders1 = a1.map( i => {
return {
text: i,
fieldName: 'dummy1_'+i,
width: 110
};
});
Use this and create 2 more arrays with 2 and 3 respectively.
Then consolidated all the variable in an array using concat or spread operator

How to check if two elements are in an object

I am trying to make a function that checks if a key ( a number ) exists in an object, and then if that element has a specific sub element. this is what I am doing
const blacklistedCities = {
[NATION_ID_USA]: [
'New York',
],
[NATION_ID_JAPAN]: [
'Tokio',
'Kyoto',
]
}
export function isBlacklistedCity(city, nationId) {
if(blacklistedCities.indexOf(nationId) !== -1 && blacklistedCities. ??WHAT SHOULD I PUT HERE??) {
return false;
}
return true;
}
NATION_ID_USA and NATION_ID_JAPAN are constants imported from another file
this function should return false when the element is found because I am using it from a filter() function somewhere else but I am open to any suggestion
thanks
I'd think of the input object as describing a much simpler array of scalar values that will be queried.
// Convert the input to a set containing [ 'nationId-cityId', ...]
//
const blacklistedCities = {
NATION_ID_USA: [
'New York',
],
NATION_ID_JAPAN: [
'Tokio',
'Kyoto',
]
}
const keys = Object.entries(blacklistedCities).reduce((acc, [nationId, cityIdArray]) => {
let nationIds = cityIdArray.map(cityId => `${nationId}-${cityId}`)
acc = acc.concat(nationIds)
return acc
}, [])
// keep this set around for queries
const queryMe = new Set(keys)
// and query with nation, key pair
function queryFor(nationId, cityId) {
return queryMe.has(`${nationId}-${cityId}`)
}
console.log(queryFor('NATION_ID_USA', 'New York')) // -> true
console.log(queryFor('NATION_ID_FRANCE', 'Paris')) // -> false
You can use the nation name as a property name to index directly into the object, and then use .includes() on the array (if present):
export function isBlacklistedCity(city, nationId) {
return (nationId in blacklistedCitites) && blacklistedCities[nationId].includes(city);
}
You can do something like this
const blacklistedCities = {
NATION_ID_USA: [
'New York',
],
NATION_ID_JAPAN: [
'Tokio',
'Kyoto',
]
}
function isBlacklistedCity(city, nationId) {
if(blacklistedCities[nationId] && blacklistedCities[nationId].length > 0 && blacklistedCities[nationId].find((cityInObj) => cityInObj==city)) {
return true;
} else {
return false;
}
}
console.log(isBlacklistedCity("Tokio", "NATION_ID_JAPAN"));

Using if statement in array.forEach function statement to choose sub-object in array of objects

I cannot get an if statement to work inside a function called by a forEach array loop.
I have an array with objects, (with an object)
arrofobj = [
{"thing_id":"1a", "val": 1, "Type": "Switch","ValType":{"0":"Open","1":"Closed"}},
{"thing_id":"1b", "val": 72, "Type": "Sensor","ValType":{"0":"%"}}]
I would like to test if the Type is a switch, in order to write info in a new field of the objects of the array CatX:
- when it is, I want to use the val value to determine which ValType element to use in a new variable of array arrofobj.
- if not, I want to use the arrofobj.ValType.0 value
const getCat = function(){
if(arrofobj.Type !== 'Switch')
arrofobj.ValType'.0'
} else {
arrofobj.ValType.(arrofobj.val)
};
arrofobj.forEach(p => p.CatX = getCat() );
I am not getting the lint to accept the code, so cannot test.
1) You have to use bracket notation to access properties as strings.
2) You have to close the brackets on if/else correctly.
3) You have to return something from inside getCat to have something to assign to p.CatX
4) You have to actually send the object to getCat inside the loop.
const arrofobj = [
{"thing_id":"1a", "val": 1, "Type": "Switch","ValType":{"0":"Open","1":"Closed"}},
{"thing_id":"1b", "val": 72, "Type": "Sensor","ValType":{"0":"%"}}
];
const getCat = function( obj ){
if(obj.Type !== 'Switch') {
return obj.ValType[ '0' ]
} else {
return obj.ValType[ obj.val ];
}
};
arrofobj.forEach(p => {
p.CatX = getCat(p);
});
console.log( arrofobj );
Just to add to Shilly's answer:
1) In the long-run, if this is data you're creating yourself and not something from a 3rd-party endpoint, you'll find standardising the format of your object property key names (in camelCase) to be easier to work with. It won't introduce as many bugs to your code if they're identically formatted.
2) You can use object destructuring assignment and a ternary operator to shorten the code footprint a little.
const arrofobj = [
{ id: '1a', val: 1, type: 'Switch', valType: { '0': 'Open', '1': 'Closed' } },
{ id: '1b', val: 72, type: 'Sensor', valType: { '0': '%' } }
];
function getCat(obj) {
// Deconstruct the properties from obj
const { type, valType, val } = obj;
// Use a ternary operator to decide what to return
return type === 'Switch' ? valType[val] : valType['0'];
}
arrofobj.forEach(obj => {
obj.catX = getCat(obj);
});
console.log(arrofobj);

JavaScript - Filter object based on multiple values

I need to filter some data based on multiple values. Language, title and slug
[
{
de: "4567uy55",
en: "654321",
lang: [
{
id: "654321",
language: "English",
title: "Title1"
},
{
id: "4567uy55",
language: "German",
title: "Title2"
}
],
slug: 'some-slug'
},
...
]
What I have now returns all objects which have one or part of the filters(in case title is This is a title, the word this should match), but I need to return objects which have all of them.
I used an object flattner just to get all properties and values in one object, but I can't get it to filter the way I need it.
multiFilter = (arr, filters) => {
console.log(filters)
console.log(arr)
let newArray = []
for (let c of arr) {
let flatCourse = flatten(c)
for (let k in flatCourse) {
const keyArr = k.split('/')
const filterKeys = Object.keys(filters)
Object.keys(filters).map((key) => {
if (keyArr.includes(key)) {
const flatVal = flatCourse[k].toString().toLowerCase()
const filterVal = filters[key].toString().toLowerCase()
console.log(flatVal)
console.log(filterVal)
if (flatVal.includes(filterVal)) {
arr = []
arr.push(c)
newArray.push(c)
}
}
})
}
}
return newArray
}
Filters look like this:
[
language:["English"],
title: ["Some title"],
slug:["some slug"]
]
Instead of mixing for loops and functional chaining you could just go with one of them:
multiFilter = (arr, filters) =>
arr.map(flatten).filter(el => // filter out elements from arr
Object.entries(filters).every(([fKey, fValues]) => // ensure that every key is included in the object
Object.entries(el).some(([oKey, oValue]) =>
oKey.split("/").includes(fKey) && fValues.includes(oValue)// make sure that at least one of the values equals the elements value
)
)
);
arr.filter(course => {
// Returns an array of booleans corresponding to whether or not each filter condition is satisfied
return Object.keys(filters).map(key => {
return filters[key].map(filter => {
// Special case here because lang is an array
if (key == 'language' && course.lang != undefined) {
return course.lang.some(lang => lang[key].includes(filter))
}
if (course[key] == undefined) {
return false
}
return course[key].includes(filter)
}).every(result => result == true)
}).every(result => result == true)
})

Parsing a value from JSON response that keeps changing order

Im trying to parse JSON response from my home automation system in javscript.
The response is
available here
That is only a small part of the response, also, the order of keys changes on every reboot for reasons i do not know, su using the the number index wont really work
i need to be able to store the state value of sensor.out, sensor.in, sensor.door into variables for tasker on andorid
i tried to select using the entity.id, but for some reason the code never finished ( i believe i just didnt know what i was doing)
With ES6 you can use the Array#find method:
response.find(o => o.entity_id == 'sensor.out').state
See snippet:
var response = [ { "attributes":{ "friendly_name":"door" }, "entity_id":"sensor.door", "last_changed":"2016-12-31T11:15:59.395808+00:00", "last_updated":"2016-12-31T11:15:59.395808+00:00", "state":"closed" }, { "attributes":{ "friendly_name":"In", "unit_of_measurement":"\u00b0C" }, "entity_id":"sensor.in", "last_changed":"2016-12-31T11:20:02.179821+00:00", "last_updated":"2016-12-31T11:20:02.179821+00:00", "state":"20.7" }, { "attributes":{ "changed_by":null, "code_format":".+", "friendly_name":"panel" }, "entity_id":"alarm_control_panel.panel", "last_changed":"2016-12-31T11:14:56.471966+00:00", "last_updated":"2016-12-31T11:14:56.471966+00:00", "state":"disarmed" }, { "attributes":{ "friendly_name":"Out", "unit_of_measurement":"\u00b0C" }, "entity_id":"sensor.out", "last_changed":"2016-12-31T11:14:58.452345+00:00", "last_updated":"2016-12-31T11:14:58.452345+00:00", "state":"7.1" }];
var state = response.find(o => o.entity_id == 'sensor.out').state;
console.log('sensor.out state is', state);
Alternatively, you could convert the response to an object with the entity id values as keys, so you can access it like response['session.out'].state:
response = Object.assign({}, ...response.map( o => ({[o.entity_id]: o}) ));
See snippet:
var response = [ { "attributes":{ "friendly_name":"door" }, "entity_id":"sensor.door", "last_changed":"2016-12-31T11:15:59.395808+00:00", "last_updated":"2016-12-31T11:15:59.395808+00:00", "state":"closed" }, { "attributes":{ "friendly_name":"In", "unit_of_measurement":"\u00b0C" }, "entity_id":"sensor.in", "last_changed":"2016-12-31T11:20:02.179821+00:00", "last_updated":"2016-12-31T11:20:02.179821+00:00", "state":"20.7" }, { "attributes":{ "changed_by":null, "code_format":".+", "friendly_name":"panel" }, "entity_id":"alarm_control_panel.panel", "last_changed":"2016-12-31T11:14:56.471966+00:00", "last_updated":"2016-12-31T11:14:56.471966+00:00", "state":"disarmed" }, { "attributes":{ "friendly_name":"Out", "unit_of_measurement":"\u00b0C" }, "entity_id":"sensor.out", "last_changed":"2016-12-31T11:14:58.452345+00:00", "last_updated":"2016-12-31T11:14:58.452345+00:00", "state":"7.1" }];
response = Object.assign({}, ...response.map( o => ({[o.entity_id]: o}) ));
console.log('sensor.out state is', response['sensor.out'].state);
If you're trying to uses indexes to select properties from objects, you shouldn't be, unless there is a very specific reason to do so.
Fortunately that's fine, you don't need to know the order. I took two of the objects from your JSON array, scrambled up the properties, and wrote a function that returns any object that contains the key/val you specify.
Your question is a little hard to follow, but I think this will give you the idea.
<script type="text/javascript">
let arr = [
{
"attributes":{
"friendly_name":"door"
},
"entity_id":"sensor.frontdoor",
"last_changed":"2016-12-31T11:15:59.395808+00:00",
"last_updated":"2016-12-31T11:15:59.395808+00:00",
"state":"closed"
},
{
"last_changed":"2016-12-31T11:15:59.395808+00:00",
"state":"closed",
"attributes":{
"friendly_name":"door"
},
"entity_id":"sensor.backdoor",
"last_updated":"2016-12-31T11:15:59.395808+00:00"
}
];
function findKey ( theKey, theVal ) {
let reduced = arr.filter ( d => {
return d [ theKey ] === theVal;
});
return reduced;
}
let targets = findKey ( 'entity_id', 'sensor.backdoor' );
targets.forEach ( d => {
// This check is a little naive, but should give you the idea
if ( 'state' in d ) {
console.log ( d.state );
}
} );
</script>
What everyone says is correct: The order of keys in the response doesn't matter. Use the (string) key, not a numerical index.
var arr = [
{
"entity_id":"sensor.door",
"state":"closed"
}]; // other attributes chopped out for brevity
var entity_id_interested_in = 'sensor.door';
var state = '[entity_id not found in response]';
for (var i = 0; i < arr.length; i++) {
console.log(arr[i].entity_id + ' state:' + arr[i].state);
if (arr[i].entity_id == entity_id_interested_in)
{
state = arr[i].state;
break;
}
}
console.log (state);

Categories

Resources