I have this:
{ 'Payment' : {
'Referenced' : 'referenced payment',
'Conciliate' : 'conciliate payment',
'Multiple' : 'multiple payment'
}
}
but can change in all moment for random nodes or add more, like:
{ 'Payment' : {
'Referenced' : 'referenced payment',
'Conciliate' : 'conciliate payment',
'Multiple' : {
'mult1' : 'example1',
'mult2' : 'example1'
},
'Inventory' : {
'datastorage' : 'dt1'
}
}
All nodes can be asigned randomly, and I need to search by value, I can pass:
referenced payment
and need:
Payment/Referenced
or I send:
example1
and I need:
Payment/Multiple/mult1
I don't know if exist something like that.
// Function
const findPath = (obj, query) => {
const makeArray = (obj, path = []) => {
const pairs = Object.entries(obj);
return pairs.map(([key, value]) =>
typeof value === "object"
? makeArray(value, [...path, key])
: { path: [...path, key], value }
);
};
return (
makeArray(obj)
.flat(Infinity)
.find(({ path, value }) => value === query)?.path.join("/") ?? null
);
};
// Usage
const path1 = findPath(
{
Payment: {
Referenced: "referenced payment",
Conciliate: "conciliate payment",
Multiple: {
mult1: "example1",
mult2: {
test: 123,
},
},
Inventory: {
datastorage: "dt1",
},
},
},
123
);
const path2 = findPath(
{
Payment: {
Referenced: "referenced payment",
Conciliate: "conciliate payment",
Multiple: {
mult1: "example1",
},
Inventory: {
datastorage: "dt1",
},
},
},
"referenced payment"
);
console.log("123: " + path1);
console.log("referenced payment: " + path2);
Explanation
The first step is converting the object into a linear array of the object tree and its paths. This is done recursively. Then, the array is flattened in order to be iterated through with Array.prototype.find, if the value matches the query, the path is returned, if no match was found it returns null.
Credits
Thanks to #Bravo for suggesting path array instead of template literal
Related
I got a schema object that looks like this:
const schema = {
social: {
facebook: 'someValue',
twitter: {
department: {
departmentImage: {
editable: 'someValue'
}
}
}
}
};
The editable property indicates a value that I want to edit, and may appear in several nested locations in the object.
My approach to edit it is to recursively create a new object who is an exact copy of the original, and populate a new value where I encounter editable.
Like this:
const formatSchema = (schema, data, formattedSchema = {}) => {
for (const schemaKey in schema) {
const firstKey = Object.keys(schema[schemaKey])[0];
if (schema[schemaKey] instanceof Object) {
formattedSchema[schemaKey] = schema[schemaKey];
formatschema(schema[schemaKey], data, formattedSchema[schemaKey]);
}
if (schema[schemaKey] instanceof Object && firstKey === 'editable') {
*replacing data logic*
formattedSchema[schemaKey] = ...*replacingData*;
formatschema(schema[schemaKey], data, formattedSchema[schemaKey]);
} else {
formattedSchema[schemaKey] = schema[schemaKey];
}
}
return formattedSchema;
};
But I feel this solution may be inefficient as I create every single bit of the object from scratch and this would happen thousands of times a day.
Is there a way to do it better?
Here's a recursive immutable update that works for any native input type. Don't worry about performance here as it's plenty fast, even if your object has thousands of fields. Let me know how this suits you and I can make a change if it's needed -
function update(t, func) {
switch (t?.constructor) {
case Object:
return Object.fromEntries(
Object.entries(t).map(([k,v]) =>
[k, func([k, update(v, func)])]
)
)
case Array:
return t.map((v, k) => func([k, update(v, func)]))
default:
return func([null, t])
}
}
const schema = {
social: {
facebook: 'someValue',
twitter: {
department: {
departmentImage: {
editable: 'someValue'
}
},
someArr: [{ editable: 1 }, { editable: 2 }, { hello: "world" }]
},
}
}
console.log(update(schema, ([k,v]) =>
k == "editable" ? "✅" : v
))
.as-console-wrapper {min-height: 100% !important; top: 0}
{
"social": {
"facebook": "someValue",
"twitter": {
"department": {
"departmentImage": {
"editable": "✅"
}
},
"someArr": [
{
"editable": "✅"
},
{
"editable": "✅"
},
{
"hello": "world"
}
]
}
}
}
I got this type of object:
const obj = {
group: {
data: {
data: [
{
id: null,
value: 'someValue',
data: 'someData'
}
]
}
}
};
I need to edit this object so whenever null is in the property value,
it would be replaced with some string.
Meaning if the replacement string will be 'someId',
the expected outcome is:
const obj = {
group: {
data: {
data: [
{
id: 'someId',
value: 'someValue',
data: 'someData'
}
]
}
}
};
Closest I found were this and this but didn't manage to manipulate the solutions there to what i need.
How should I do it?
Probably running into issues with the array values. Pass in the index of the array to modify. In this case [0]
obj.group.data.data[0].id = "someId"
EDIT
This will update all null values of id inside the data array:
obj.group.data.data.forEach(o => {
if (o.id === null) {
o.id = "someId"
}
})
Another EDIT
Here is an algorithm to recursively check all deeply nested values in an object. It will compile an array of object paths where null values live. There is an included helper method to find and update the value of the object at the given path in the array. There is a demonstration of the program in the console.
const object = {
group: {
data: {
data: [
{
id: null,
value: "foo",
data: [null, "bar", [null, { stuff: null }]]
},
{
id: null,
value: null,
data: {
bar: [null]
}
},
{
id: null,
value: "foo",
data: null
},
{
id: 4,
value: "foo",
data: "bar"
},
{
id: 4,
value: "stuff",
data: null
}
]
},
attributes: null,
errors: ["stuff", null]
}
}
const inspectProperty = (key, obj, path = "") => {
if (typeof obj[key] === "object") {
if (obj[key] instanceof Array) {
return analyzeArray(obj[key], `${path ? path + "." : ""}${key}`);
}
return analyzeObj(obj[key], `${path ? path + "." : ""}${key}`);
}
return [];
};
const analyzeKey = (obj, key, path = "") => {
if (obj[key] === null) return [`${path ? path + "." : ""}${key}`];
return inspectProperty(key, obj, path).reduce((a, k) => [...a, ...k], []);
};
const analyzeObj = (obj, path = "") => {
return Object.keys(obj).map((item) => analyzeKey(obj, item, path));
};
const analyzeArray = (array, path) => {
return array.map((item, i) => analyzeKey(array, i, path));
};
const updateNullValue = (path, value) => {
let p = path.split(".");
p.reduce((accum, iter, i) => {
if (i === p.length - 1) {
accum[iter] = value;
return object;
}
return accum[iter];
}, object);
};
let nullValues = analyzeObj(object)[0]
console.log(nullValues)
nullValues.forEach((nullVal, i) => {
updateNullValue(nullVal, "hello-" + i)
})
console.log(object)
I have a simple array and I want to update this array with the value order:"asc" and want to delete all other order key only if type == "user" and key == "country"
const items = [
{
type: "user",
values: [
{order:"asc", key:"first_name"},
{key:"last_name"},
{key:"address"},
{key:"country"},
]
},
]
My expected result is
const items = [
{
type: "user",
values: [
{key:"first_name"},
{key:"last_name"},
{key:"address"},
{order:"asc", key:"country"},
]
},
]
I'm able to do this with map inside map. Is it possible without looping twice?
items.map(
x => { if (x.type == "user") {
x.values = x.values.map(y => {
if (y.key.includes("country")) {
y.order = "asc"
} else if (JSON.stringify(x.values).includes("country")) {
delete y.order
}
return y
})
}
return [x]
});
I think that you can do that only with double loop. I write this script but it similar to yours.
var newItems = items.map(el => {
if(el.type === "user"){
el.values = el.values.map(value => {
if(value.key === "country"){
value["order"] = "asc"
}else if(value["order"] != undefined){
delete value["order"]
}
return value
})
return el
}
})
Logic
Loop through items array.
Find nodes with type "type"
From that nodes loop through values array.
Clear "order" of nodes where key is not "country"
Add "order" as "asc" where key is "country"
Working Fiddle
const items = [
{
type: "user",
values: [
{ order: "asc", key: "first_name" },
{ key: "last_name" },
{ key: "address" },
{ key: "country" },
]
},
];
items.filter(item => item.type === "user").forEach(item => {
item.values.filter(value => value.order === "asc").forEach(value => value.order && value.key !== "country" ? delete value.order : {});
item.values.filter(value => value.key === "country").forEach(value => value.order = "asc");
});
console.log(items);
items.filter(itm => itm.type == "user")
.map(u => {u.values.map(v => {
delete v.order
if (u.values.key == "country")
u.values.order = "asc"
})
}
);
If you don't want to delete order:"asc" if there is no country in the values array then instead of JSON.stringify(x.values).includes("country") upon every object in values you could use .find() and then only loop and delete if there is an object with key:"country":
const items = [ { type: "user", values: [ {order:"asc", key:"first_name"}, {key:"last_name"}, {key:"address"}, {key:"country"}, ] }, ]
const result = items.map(obj => {
if (obj.type === 'user') {
const country = obj.values.find(o => o.key === 'country')
if (country) {
obj.values.forEach(value => delete value.order)
country.order = 'asc'
}
}
return obj
})
console.log(result)
.as-console-wrapper{min-height:100%}
I have next array of objects:
const fields = [
{ givenName: 'firstName' },
{ familyName: 'lastName' },
{ 'custom:data': 'blabla' },
{ 'custom:data2': '' },
{ 'custom:data3': null },
];
What I need is to filter out elements which is empty, null or undefined and convert it to one object pameters:
{
givenName: 'firstName',
familyName: 'lastName',
'custom:data': 'blabla'
}
You could filter the array by looking to the values. This approach assumes, that only one key/value pair is available.
const
fields = [{ givenName: 'firstName' }, { familyName: 'lastName' }, { 'custom:data': 'blabla' }, { 'custom:data2': '' }, { 'custom:data3': null }],
result = Object.assign({}, ...fields.filter(o => {
const [v] = Object.values(o);
return v || v === 0 || v === false;
}));
console.log(result);
How to check whether a value is empty?
Most people would go about this with a truthy check:
const empty = x => x ? false : true;
empty(null); //=> true
empty(undefined); //=> true
empty(''); //=> true
But that's always going to exclude things you perhaps didn't intend to exclude:
empty(0); //=> true
empty(false); //=> true
empty(NaN); //=> true
Admittedly NaN could be seen as the "empty" value of its type but for the sake of your question and educational purpose we'll say it's not.
The "workaround" is often something like that:
const empty = x => (x || x === 0 || x === false || Number.isNaN(x)) ? false : true;
However this doesn't need to be more complicated than this:
const empty = x => x == null || x === '' ? true : false;
Checking for either undefined or null is one example where not using triple equality makes sense:
null == undefined;
// true
null === undefined;
// false
See Google JavaScript Style Guide.
If you need to exclude null, undefined and '' please don't rely on clever shorthand tricks and just be explicit about it. Type checking should be a straightforward job (YMMV) and not a show-off contest. Future you and your team mates will thank you.
As for your question, I'd suggest this:
Merge everything you've got with Object.assign:
Object.assign({}, {a:1}, {b:2}, {c:2});
// {a: 1, b: 2, c: 3}
Then deconstruct it into pairs, exclude those whose value is empty, then reconstruct the object from what's left:
const merge = xs =>
Object.fromEntries(
Object.entries(
Object.assign({}, ...xs))
.filter(([_, v]) =>
v != null && v !== ''));
console.log(merge(fields));
<script>
const fields = [
{ givenName: 'firstName' },
{ familyName: 'lastName' },
{ 'custom:data': 'blabla' },
{ 'custom:data2': '' },
{ 'custom:data3': null },
];
</script>
const fields = [
{ givenName: 'firstName' },
{ familyName: 'lastName' },
{ 'custom:data': 'blabla' },
{ 'custom:data2': '' },
{ 'custom:data3': null },
];
res = fields.reduce((acc, cur) => {
if (cur[Object.keys(cur)[0]]) {
acc = { ...acc, ...cur }
}
return acc
}, {})
console.log(res)
const fields = [
{ givenName: 'firstName' },
{ familyName: 'lastName' },
{ 'custom:data': 'blabla' },
{ 'custom:data2': '' },
{ 'custom:data3': null },
];
const result = fields.reduce( ( acc, field ) => {
Object.keys( field ).forEach( ( key ) => {
if( field[key] ) {
acc[key] = field[key];
}
} )
return acc;
}, {} )
console.log(result)
You could use reduce and forEach and check if value of each property is falsy or not.
const fields = [
{ givenName: 'firstName' },
{ familyName: 'lastName' },
{ 'custom:data': 'blabla' },
{ 'custom:data2': '' },
{ 'custom:data3': null },
];
const result = fields.reduce((r, e) => {
Object.entries(e).forEach(([k, v]) => {
if (v || [false, 0].includes(v)) r[k] = v
})
return r
}, {})
console.log(result)
Use Array.prototype.filter() method to filter out the empty, null or undefined objects. Then using Array.prototype.map() make a key-value pair array. At last, use Object.fromEntries() method to transform it to a single object.
const fields = [
{ givenName: 'firstName' },
{ familyName: 'lastName' },
{ 'custom:data': 'blabla' },
{ 'custom:data2': '' },
{ 'custom:data3': null },
];
const ret = Object.fromEntries(
fields
.filter((x) => {
const value = Object.values(x)[0];
return value || value === false || value === 0 || Object.is(value, NaN);
})
.map((x) => [Object.keys(x)[0], Object.values(x)[0]])
);
console.log(ret);
It might help you.
const fields = [
{ givenName: 'firstName' },
{ familyName: 'lastName' },
{ 'custom:data': 'blabla' },
{ 'custom:data2': '' },
{ 'custom:data3': null },
];
let item = {};
for ( let i = 0; i < fields.length; i++ ){
for (const [key, value] of Object.entries(fields[i])) {
if ( value !== null && value !== '' )
item [key] = value;
}
}
console.log(item);
Works with one key, simple modification can work on n keys.
const fields = [
{ givenName: "firstName" },
{ familyName: "lastName" },
{ "custom:data": "blabla" },
{ "custom:data2": "" },
{ "custom:data3": null },
];
const reduced = fields
.filter((f) => {
const key = Object.keys(f)[0];
return f[key] === "" || f[key] === null || f[key] === undefined
? false
: true;
})
.reduce((acc, curr) => {
const key = Object.keys(curr)[0];
acc[key] = curr[key];
return acc;
}, {});
console.log(reduced);
I want to create objects with properties in an array by string. I'm extracting "name" from string and "data" without round brackets and trying to create objects in the array with the property "name"
and property "data". But actual result differs from expected, please help to solve
const names = ["name1 /2 (data1)", "name1 /2 (data2)", "name2 /1 (data1)"]
const flag = true
names.forEach(name => {
console.log(getStructuredDataFromNames(name))
})
function getStructuredDataFromNames(name) {
const names = {}
// extract name from string and conver to valid format - 'name1'
const formattedName = name.substr(0, name.indexOf("/")).replace(/\s+/g, "")
// extract data from string and conver to valid format - 'data1'
const formattedData = name.match(/\((.*)\)/).pop()
const reference = {
formattedData,
flag
}
// if no name then create a new property by this name
if (!names[name]) {
names[name] = [{
data: [reference]
}]
} else {
// if object have name but name have more then 1 data then push this data to array
names[name].push(reference)
}
const result = Object.keys(names)
return result.map(el => ({
name: el,
data: names[el]
}))
}
Expected result
[{
name: "name1",
data: [{
flag: true,
formattedData: "data1"
},
{
flag: true,
formattedData: "data2"
}
]
},
{
name: "name2",
data: [{
flag: true,
formattedData: "data1"
}]
}
]
Although the code is not that clean, it can do the job.
My flow is first to remove the spaces, and then split the name and the data, remove the number before the data and remove (). If "formattedNames" already have the same "name" object, push the data to the "name" object.
const names = ["name1 /2 (data1)", "name1 /2 (data2)", "name2 /1 (data1)"]
const formattedNames = []
names.forEach(value =>{
const processedName = value.replace(/ /g,'').split("/")
const formattedName = formattedNames.find((object)=>{ return object.name === processedName[0]})
const formattedData = processedName[1].split("(")[1].replace(")","")
if (!formattedName) {
formattedNames.push({name: processedName[0],data: [{flag: true, formattedData}]})
} else {
formattedName.data.push({flag: true, formattedData})
}
})
console.log(formattedNames)
Here is a really inefficient way to do it
var names2 = ["name1 /2 (data1)", "name1 /2 (data2)", "name2 /1 (data1)"]
var flag2 = true
var result2 = names2.map(x =>
{
return {
name: x.split(" ")[0],
data:
[
{
flag: flag2,
formattedData: x.match(/\((.*)\)/).pop()
}
]
}
})
result2.forEach((el, index, arr) =>
{
let el2 = arr.filter(x => x.name == el.name)[0];
if(el2 == el)
return;
el2.data = el2.data.concat(el.data);
delete arr[index];
})
result2 = result2.filter(x => true);
console.log(result2);