Loop through an object-tree by array - javascript

I have a question about walk down an object dynamically by an given array object.
Tried with some static code but this is not flexible in a situation where there are more or less levels
// value = 10
// field = ["data", "input", "level", "0"]
item[field[0]][field[1]][field[2]][field[3]] = value
I have no clue where to start with a function doing this with a for loop. Can anybody give me some advice to get started.

You could reduce the fields and take an object and it's properties. At the end assign the value with the last key.
const
setValue = (object, [...path], value) => {
var last = path.pop();
path.reduce((o, k) => o[k] = o[k] || {}, object)[last] = value;
},
object = {},
value = 10,
fields = ["data", "input", "level", "0"];
setValue(object, fields, value);
console.log(object);

There's a built-in method on Lodash that does just this - _.set.
_.set(item, field, value)
let item = {
data: {
input: {
level: [5]
}
}
};
const field = ["data", "input", "level", "0"];
const value = 10;
_.set(item, field, value);
console.log(item);
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.js"></script>

You can use a recursive function to navigate through your object :
var value = 10;
var field = ["data", "input", "level", "0"];
var obj =
{
data:
{
input:
{
level: [42]
}
}
};
function SetValue(field, obj, value, index = 0)
{
var memberName = field[index];
// not at last item ?
if (index < field.length - 1)
{
if (obj.hasOwnProperty(memberName))
{
SetValue(field, obj[memberName], value, index + 1);
}
}
else
{
obj[memberName] = value;
}
}
console.log("Before");
console.log(obj);
console.log("---------------------");
SetValue(field, obj, value);
console.log("After");
console.log(obj);
console.log("---------------------");

I already use a function in production that literally does what you want:
/**
* Replace an item in datasource with specified path with new value.
* Will _create_ an item if the path does not currently exist.
* #param {object} o Datasource
* #param {array} k Array of keys used to access item (usually getPath())
* #param {*} v New value of specified item
*/
const replaceItem = (o, k, v) => k.reduce((r, e, i, a) => {
if (!a[i + 1]) r[e] = v;
else return r[e]
}, o)
const dataSource = {
data: {
input: {
level: [1]
}
}
}
const path = ["data", "input", "level", "0"]
replaceItem(dataSource, path, 5)
console.log(dataSource)

another version, with a twist. It differentiates wether the object it creates should be an Object or an Array
const setValue = (object, path, value) => {
if (!object || !path || !path.length) return;
var key = path[0],
i = 1,
prev = key;
while (i < path.length) {
key = path[i++];
object = object[prev] || (object[prev] = +key === (key >>> 0) ? [] : {});
prev = key;
}
object[key] = value;
};
const object = {};
setValue(object, ["data", "input", "level", "0"], 10);
console.log(object);

You could keep a current variable which represents the current object you're on in your object and then set the value at the end once your loop is complete:
const item = {
"data": {
"input": {
"level": {
0: -1
}
}
}
}
let value = 10;
let field = ["data", "input", "level", "0"];
let current = item[field[0]];
for (let i = 1; i < field.length - 1; i++) {
current = current[field[i]];
}
current[field.pop()] = value;
console.log(item);
The above can also be achieved recursively (use nf === undefined instead of !nf if falsy field values exist):
const item = {
"data": {
"input": {
"level": {
0: -1
}
}
}
}
let value = 10;
let field = ["data", "input", "level", "0"];
const set_val = (val, obj, [f, nf, ...rest]) =>
!nf ? obj[f] = val : set_val(val, obj[f], [nf, ...rest]);
set_val(value, item, field);
console.log(item);
If you wish to build the item object from scratch, you could also do something like so:
const value = 10;
const field = ["data", "input", "level", "0"];
let item = {};
let curr = item;
for (let i = 0; i < field.length - 1; i++) {
curr[field[i]] = {};
curr = curr[field[i]];
}
curr[field.pop()] = value;
console.log(item);
This build can also be done recursively like so:
const value = 10;
const field = ["data", "input", "level", "0"];
let item = {};
let curr = item;
const set_val = (val, [f, nf, ...rest], item = {}) => {
if (!nf) {
item[f] = val;
return item;
} else {
item[f] = set_val(val, [nf, ...rest], item[f]);
return item;
}
}
console.log(set_val(value, field));

Related

Javascript, getting past values for an array of objects

I have a JavaScript array of objects which looks like
var myarr = [
{'xx':'2023-01-01,,1'},
{'ss':'2023-01-01,2,1.2'},
{'dd':'2023-01-01,4,'},
{'rr':'2023-01-01,,'},
{'ff':'2023-01-01,,'},
{'gg':'2023-01-01,,'}
];
The array is actually much bigger than that, but I have cut it down for testing purposes, some of my arrays are thousands of lines long
Each object contains a date and two comma-separated values, although I have some rows which contain 3 or 4 comma separate values
What I need to do, is if any blank comma-separated value is found on any row then get the previous comma separated value from that position to a maximum of 2 times going back, although I may need to change that to a bigger number in the future
So with my example, I would get the following output
var myarr = [
{'xx':'2023-01-01,,1.6'},
{'ss':'2023-01-01,2,1.2'},
{'dd':'2023-01-01,4,1.2'},
{'rr':'2023-01-01,4,1.2'},
{'ff':'2023-01-01,4,'},
{'gg':'2023-01-01,,'}
];
I have tried to solve this with
var myarr = [
{'xx':'2023-01-01,,1'},
{'ss':'2023-01-01,2,1.2'},
{'dd':'2023-01-01,4,'},
{'rr':'2023-01-01,,'},
{'ff':'2023-01-01,,'},
{'gg':'2023-01-01,,'}
];
var maxAttempts = 3;
for (var i = 0; i < myarr.length; i++) {
var obj = myarr[i];
var values = Object.values(obj)[0].split(",");
var date = values[0];
var value1 = values[1];
var value2 = values[2];
for (var j = 1; j <= maxAttempts; j++) {
if (!value1) {
value1 = (myarr[i-j] && Object.values(myarr[i-j])[0].split(",")[1]) || " ";
}
if (!value2) {
value2 = (myarr[i-j] && Object.values(myarr[i-j])[0].split(",")[2]) || " ";
}
if (value1 && value2) {
break;
}
}
console.log(date, value1, value2);
for (var k = 3; k < values.length; k++) {
var value = values[k];
console.log(value);
}
}
but it doesn't seem to provide the expected output.
Can someone help me with what might be wrong?
Maybe you can use something like this.
const myarr = [
{ "xx": "2023-01-01,,1" },
{ "ss": "2023-01-01,2,1.2" },
{ "dd": "2023-01-01,4," },
{ "rr": "2023-01-01,," },
{ "ff": "2023-01-01,," },
{ "gg": "2023-01-01,," }
]
function fillInBlanks(arr, maxLookBack) {
return arr.map((obj, index) => {
const key = Object.keys(obj)[0]
const value = Object.values(obj)[0]
.split(",")
.map((x, n) => {
if (x === "" && index > 0) {
for (let i = index - 1; i >= Math.max(0, index - maxLookBack); --i) {
const prev = Object.values(arr[i])[0].split(",")
if (prev[n] !== "") return prev[n]
}
} else return x
})
return Object.fromEntries([
[key, value.join(",")]
])
})
}
fillInBlanks(myarr, 2).forEach(x => console.log(x))
Here's my attempt. This will also work with any number of values per row.
const maxAttempts = 2;
myarr.reduce((modifiedAccumulation, currentObject, index) => {
const [key, csv] = Object.entries(currentObject)[0];
const splitCsv = csv.split(",");
const modifiedCsv = splitCsv
.reduce((fixedArray, currentElement, csvPos) => {
let numberToUse =
currentElement === ""
? myarr
.slice(Math.max(index - maxAttempts, 0), index)
.reduceRight((proposedNum, currentPastObj) => {
if (proposedNum !== "") return proposedNum;
let candidate =
Object.entries(currentPastObj)[0][1].split(",")[csvPos];
return candidate !== "" ? candidate : "";
}, "")
: currentElement;
return [...fixedArray, numberToUse];
}, [])
.join(",");
return [...modifiedAccumulation, { [key]: modifiedCsv }];
}, []);
This approach creates a 'window' array containing the last few entries, which is used to look up prior column values.
const myarr = [{"xx":"2023-01-01,,1"},{"ss":"2023-01-01,2,1.2"},{"dd":"2023-01-01,4,"},{"rr":"2023-01-01,,"},{"ff":"2023-01-01,,"},{"gg":"2023-01-01,,"}]
const windowSize = 2
const w = [], r =
myarr.map(e=>Object.entries(e).flatMap(([k,v])=>[k,...v.split(',')]))
.map(a=>(
w.unshift(a) > windowSize+1 && w.pop(),
a.map((_,i)=>w.find(x=>x[i])?.[i])
)).map(([k,...v])=>[k,v.join()]
).map(i=>Object.fromEntries([i]))
console.log(r)

Transform array into object with custom properties

I have this array
myarr = [
'=title1',
'longText0...',
'longtText1...',
'=title2',
'longTextA...',
'longtTextB...',
'longtTextC...'
];
symbol = indicates that is is a property, next to that is a list of items that belongs to that property
I want to transform that array into object
myObj = {
title1: [
'longText0...',
'longtText1...',
],
title2: [
'longTextA...',
'longtTextB...',
'longtTextC...'
]
}
I come up with this code so far:
const arrayToObject = (array) =>
array.reduce((obj, item) => {
if(item.startsWith('=')) {
const itemName = item.replace('=', '')
obj[itemName] = itemName;
} else {
//add the rest....
}
return obj
}, {})
console.log(arrayToObject(myarr))
My challenges so far is that I am not sure how to turn obj[itemName] so I can assign the items to it. Any ideas how to do that?
A reduce based approach which does not depend on outer scope references for keeping track of the currently to be built/aggregated property makes this information part of the reducer function's first parameter, the previousValue which serves as an accumulator/collector object.
Thus, as for the OP's task, this collector would feature two properties, the currentKey and the result, where the former holds the state of the currently processed property name and the latter being the programmatically built result.
// - reducer function which aggregates entries at time,
// either by creating a new property or by pushing a
// value into the currently processed property value.
// - keeps the state of the currently processed property
// by the accumulator`s/collector's `currentKey` property
// whereas the result programmatically gets build as
// the accumulator`s/collector's `result` property.
function aggregateEntry({ currentKey = null, result = {} }, item) {
const key = (item.startsWith('=') && item.slice(1));
if (
(key !== false) &&
(key !== currentKey)
) {
// keep track of the currently processed property name.
currentKey = key;
// create a new entry (key value pair).
result[currentKey] = [];
} else {
// push value into the currently processed property value.
result[currentKey].push(item);
}
return { currentKey, result };
}
console.log([
'=title1',
'longText0...',
'longtText1...',
'=title2',
'longTextA...',
'longtTextB...',
'longtTextC...',
].reduce(aggregateEntry, { result: {} }).result);
.as-console-wrapper { min-height: 100%!important; top: 0; }
I wouldn't do that with reduce but with a simple for loop, because you have to carry the itemname over multiple iterations
let o = {}, n = '';
for (let k of arr) {
if (k.startsWith('=')) {
n = k.substring(1);
o[n] = []
} else {
o[n].push(k);
}
}
You can of course also do it with reduce, but you have to put the declaration of itemname outside of the callback
let n = '';
let o = arr.reduce((a, c) => {
if (c.startsWith('=')) {
n = c.substring(1);
a[n] = [];
} else {
a[n].push(c);
}
return a;
}, {});
Please be aware, there is no error handling, ie the code assumes your array is well structured and the first element in the array must start with =
The following function will give you the desired results
function arrayToObject(arr)
{
let returnObj={};
for(let i =0; i <arr.length; i++)
{
if(arr[i].startsWith('='))
{
let itemName = arr[i].replace('=','');
returnObj[itemName]=[];
for(let j=i+1; j <arr.length;j++)
{
if(arr[j].startsWith('='))
{
break;
}
else
{
let value = arr[j];
returnObj[itemName].push(value) ;
}
}
}
}
return returnObj;
}
Here a version with reduce
const myarr = [
'=title1',
'longText0...',
'longtText1...',
'=title2',
'longTextA...',
'longtTextB...',
'longtTextC...'
];
const obj = myarr.reduce((res, el) => {
if(el.startsWith('=')){
const key = el.substring(1)
return {
data: {
...res.data,
[key]: [],
},
key
}
}
return {
...res,
data:{
...res.data,
[res.key]: [...res.data[res.key], el]
}
}
}, {
data: {},
key: ''
}).data
console.log(obj)
You don't need to keep the key somewhere separate for a reduce method:
const myarr = ['=title1', 'longText0...', 'longtText1...', '=title2', 'longTextA...', 'longtTextB...', 'longtTextC...'];
const res = Object.fromEntries(
myarr.reduce((acc, item) => {
if(item.startsWith('='))
acc.push([item.substring(1), []]);
else
acc[acc.length - 1]?.[1].push(item);
return acc;
}, [])
);
console.log(JSON.stringify( res ));

Find max element and its key in Object - React native

I First must tell that i browsed the site here to find an answer but i couldn't.
I have an object which holds a key(service Name) and a value(counter), i want to extract from this object only the Max and Min value, and their keys.
an example:
Object {
"Learn JavaScript": 3, //Max
"Learn React": 2, //Ignore
"manicure": 1, //Min
}
and then i would like to create an array of objects which will hold them in desecending order
code i used to display the upper outcome:
const arrayofServices = services; //services => state the holding the services
const servicesCounter = arrayofServices.reduce((counterObj, service) => {
if (counterObj.hasOwnProperty(service)) {
counterObj[service] += 1;
return counterObj;
}
return {
...counterObj,
[service]: 1
};
}, {});
console.log("Service Counter in =>" ,servicesCounter);
any suggestions?
One more try :)
const obj = {"Learn JavaScript": 3, "Learn React": 2, "manicure": 1};
function MinMaxFromObj(obj) {
const min = Math.min(...Object.values(obj));
const max = Math.max(...Object.values(obj));
const minKey = Object.entries(obj).find(([key, value]) =>
value === min ? key : null);
const maxKey = Object.entries(obj).find(([key, value]) =>
value === max ? key : null);
return [Object.fromEntries([minKey]), Object.fromEntries([maxKey])];
}
console.log(MinMaxFromObj(obj));
Here is an approach which can help you to solve your problem:
const minMaxEntry = (object) => {
let minKey;
let minValue = Infinity;
let maxKey;
let maxValue = -Infinity;
for (const key in object) {
if (object[key] < minValue) {
minKey = key;
minValue = object[key];
}
if (object[key] > maxValue) {
maxKey = key;
maxValue = object[key];
}
}
const minEntry = { [minKey]: minValue };
const maxEntry = { [maxKey]: maxValue };
return [minEntry, maxEntry];
};
// --- //
const object = {
"Learn JavaScript": 3,
"Learn React": 2,
"manicure": 1,
};
const [minEntry, maxEntry] = minMaxEntry(object);
console.log('Min Entry =>', minEntry);
// { "manicure": 1 }
console.log('Max Entry =>', maxEntry);
// { "Learn JavaScript": 3 }

best way to convert from string to javascript object

I have this string:
periodRows.soccer.on:1,periodRows.soccer.periods:1,periodRows.soccer.prematchPeriods=1,periodRows.soccer.label:1st Half
What is the best way to convert it to this object?
periodRows: {
soccer: {
on: 1,
periods: 1,
prematchPeriods: 1,
label: '1st Half',
}
}
Note that I do not control the string, therefore I can not change it.
Thank you
Functionally, a bit shorter.
const f = (obj, keyPath, value) => {
if (keyPath.length === 0) {
return Number.isNaN(Number(value)) ? value : Number(value);
}
const key = keyPath[0];
if (!obj[key]) {
obj[key] = {};
}
obj[key] = f(obj[key], keyPath.slice(1), value);
return obj;
};
const str = "periodRows.soccer.on:1,periodRows.soccer.periods:1,periodRows.soccer.prematchPeriods=1,periodRows.soccer.label:1st Half";
str.split(",")
.map(token => token.split(/[:=]/))
.map(record => ({keyPath: record[0].split("."), value: record[1]}))
.reduce((obj, item) => f(obj, item.keyPath, item.value), {});
Improved recursive solution
const rec = (tokens, index, target) => {
const prop = tokens[index];
if (tokens.length - 1 === index) {
const [lastProp, value] = prop.split(/[:=]/);
target[lastProp] = value;
return target[lastProp];
}
if (prop && !target[prop]) {
target[prop] = {};
}
return target[prop];
}
"periodRows.soccer.on:1,periodRows.soccer.periods:1,periodRows.soccer.prematchPeriods=1,periodRows.soccer.label:1st Half".split(',').reduce((acc, val) => {
const tokens = val.split('.');
let target = acc;
for (let i = 0; i < tokens.length; i++) {
target = rec(tokens, i, target);
}
return acc;
}, {});
By default JS cannot recognize numbers inside of strings. You have to cast it explicitly. You can update code of rec function with this piece.
if (tokens.length - 1 === index) {
const [lastProp, stringValue] = prop.split(/[:=]/);
const parsedValue = +stringValue;
const value = Number.isNaN(parsedValue) ? stringValue: parsedValue;
target[lastProp] = value;
return target[lastProp];
}
The string format you have above is in bad format, the only way to convert it is by first converting it into a string in json-like format, something like the following (notice a json-like string should always be enclosed by {}):
var periodRows = '{"soccer":{"on":1,"periods":1,"prematchPeriods":1,"label":"1st Half"}}';
Then you'd be able to perform the conversion:
//String to Json
const str = JSON.parse(periodRows);
console.log (str);
//Json to string
var periodRows = {
soccer: {
on: 1,
periods: 1,
prematchPeriods: 1,
label: '1st Half',
}
}
var myStr = JSON.stringify(periodRows);
console.log (myStr);

Create nested object from query string in Javascript

I have the following query string:
student.name.firstname=Foo&student.name.lastname=Bar&student.address=My%20Street
How to convert to nested object like this:
{
student:{
name:{
firstname: "Foo",
lastname: "Bar"
},
address: "My Street"
}
}
I have tried the following code but something is wrong:
function convertQueryToMap(query) {
var params = {};
var vars = query.split('&');
for (var i = 0; i < vars.length; i++) {
var pair = vars[i].split('=');
var subpairs;
if (pair[0].includes('.')) {
subpairs = pair[0].split('.');
var object = {};
subpairs.reduce(function(o, s, i) {
if (i === subpairs.length-1) {
return o[s] = decodeURIComponent(pair[1]);
} else {
return o[s] = {};
}
}, object);
}
}
return params;
}
Do you know a solution?
You can use reduce method to create nested structure and split method to split the query first on parts based on & and also to get key and value from each part.
const query = 'student.name.firstname=Foo&student.name.lastname=Bar&student.address=My%20Street'
const toObject = string => {
return string.split('&').reduce((r, s) => {
const [key, val] = s.split('=');
key.split('.').reduce((a, e, i, ar) => {
return a[e] || (a[e] = (ar[i + 1] ? {} : val.replace(/%20/g, ' ')))
}, r);
return r;
}, {})
}
const result = toObject(query);
console.log(result)
You could decode the string, split for parts, then for keys and value and assign the value to the nested object.
function setValue(object, keys, value) {
var last = keys.pop();
keys.reduce((o, k) => o[k] = o[k] || {}, object)[last] = value;
}
var string = 'student.name.firstname=Foo&student.name.lastname=Bar&user.address=My%20Street',
result = {};
decodeURI(string).split('&').forEach(s => {
var [key, value] = s.split('=');
setValue(result, key.split('.'), value);
});
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }

Categories

Resources