this is my object:
{
"name":"fff",
"onlineConsultation":false,
"image":"",
"primaryLocation":{
"locationName":"ggg",
"street":"",
},
"billingAndInsurance":[
],
"categories":[
""
],
"concernsTreated":[
""
],
"education":[
{
"nameOfInstitution":"ffff",
"description":"fff",
}
],
"experience":[
{
"from":"",
"current":"",
}
],
}
What is the algorithm to recursively remove all empty objects, and empty arrays from this?
this is my code:
function rm(obj) {
for (let k in obj) {
const s = JSON.stringify(obj[k]);
if (s === '""' || s === "[]" || s === "{}") {
delete obj[k];
}
if (Array.isArray(obj[k])) {
obj[k] = obj[k].filter((x) => {
const s = JSON.stringify(obj[x]);
return s !== '""' && s !== "[]" && s !== "{}";
});
obj[k] = obj[k].map(x=>{
return rm(x)
})
}
}
return obj
}
I'v tried multiple algorithms, but none worked. the one above should work with a little more completeness. But I'v exhausted all my resources to make it work
One nice thing about keeping around helpful functions is that you can often solve for your new requirements pretty simply. Using some library functions I've written over the years, I was able to write this version:
const removeEmpties = (input) =>
pathEntries (input)
.filter (([k, v]) => v !== '')
.reduce ((a, [k, v]) => assocPath (k, v, a), {})
This uses two function I had around, pathEntries and assocPath, and I'll give their implementations below. It returns the following when given the input you supplied:
{
name: "fff",
onlineConsultation: false,
primaryLocation: {
locationName: "ggg"
},
education: [
{
nameOfInstitution: "ffff",
description: "fff"
}
]
}
This removes empty string, arrays with no values (after the empty strings are removed) and objects with no non-empty values.
We begin by calling pathEntries (which I've used in other answers here, including a fairly recent one.) This collects paths to all the leaf nodes in the input object, along with the values at those leaves. The paths are stored as arrays of strings (for objects) or numbers (for arrays.) And they are embedded in an array with the value. So after that step we get something like
[
[["name"], "fff"],
[["onlineConsultation"], false],
[["image"], ""],
[["primaryLocation", "locationName"], "ggg"],
[["primaryLocation", "street"], ""],
[["categories", 0], ""],
[["concernsTreated", 0], ""],
[["education", 0, "nameOfInstitution"], "ffff"],
[["education", 0, "description"],"fff"],
[["experience", 0, "from"], ""],
[["experience", 0, "current"], ""]
]
This should looks something like the result of Object.entries for an object, except that the key is not a property name but an entire path.
Next we filter to remove any with an empty string value, yielding:
[
[["name"], "fff"],
[["onlineConsultation"], false],
[["primaryLocation", "locationName"], "ggg"],
[["education", 0, "nameOfInstitution"], "ffff"],
[["education", 0, "description"],"fff"],
]
Then by reducing calls to assocPath (another function I've used quite a few times, including in a very interesting question) over this list and an empty object, we hydrate a complete object with just these leaf nodes at their correct paths, and we get the answer we're seeking. assocPath is an extension of another function assoc, which immutably associates a property name with a value in an object. While it's not as simple as this, due to handling of arrays as well as objects, you can think of assoc like (name, val, obj) => ({...obj, [name]: val}) assocPath does something similar for object paths instead of property names.
The point is that I wrote only one new function for this, and otherwise used things I had around.
Often I would prefer to write a recursive function for this, and I did so recently for a similar problem. But that wasn't easily extensible to this issue, where, if I understand correctly, we want to exclude an empty string in an array, and then, if that array itself is now empty, to also exclude it. This technique makes that straightforward. In the implementation below we'll see that pathEntries depends upon a recursive function, and assocPath is itself recursive, so I guess there's still recursion going on!
I also should note that assocPath and the path function used in pathEntries are inspired by Ramda (disclaimer: I'm one of it's authors.) I built my first pass at this in the Ramda REPL and only after it was working did I port it to vanilla JS, using the versions of dependencies I've created for those previous questions. So even though there are a number of functions in the snippet below, it was quite quick to write.
const path = (ps = []) => (obj = {}) =>
ps .reduce ((o, p) => (o || {}) [p], obj)
const assoc = (prop, val, obj) =>
Number .isInteger (prop) && Array .isArray (obj)
? [... obj .slice (0, prop), val, ...obj .slice (prop + 1)]
: {...obj, [prop]: val}
const assocPath = ([p = undefined, ...ps], val, obj) =>
p == undefined
? obj
: ps.length == 0
? assoc(p, val, obj)
: assoc(p, assocPath(ps, val, obj[p] || (obj[p] = Number .isInteger (ps[0]) ? [] : {})), obj)
const getPaths = (obj) =>
Object (obj) === obj
? Object .entries (obj) .flatMap (
([k, v]) => getPaths (v) .map (p => [Array.isArray(obj) ? Number(k) : k, ... p])
)
: [[]]
const pathEntries = (obj) =>
getPaths (obj) .map (p => [p, path (p) (obj)])
const removeEmpties = (input) =>
pathEntries (input)
.filter (([k, v]) => v !== '')
.reduce ((a, [k, v]) => assocPath (k, v, a), {})
const input = {name: "fff", onlineConsultation: false, image: "", primaryLocation: {locationName: "ggg", street:""}, billingAndInsurance: [], categories: [""], concernsTreated: [""], education: [{nameOfInstitution: "ffff", description: "fff"}], experience: [{from: "", current:""}]}
console .log(removeEmpties (input))
At some point, I may choose to go a little further. I see a hydrate function looking to be pulled out:
const hydrate = (entries) =>
entries .reduce ((a, [k, v]) => assocPath2(k, v, a), {})
const removeEmpties = (input) =>
hydrate (pathEntries (input) .filter (([k, v]) => v !== ''))
And I can also see this being written more Ramda-style like this:
const hydrate = reduce ((a, [k, v]) => assocPath(k, v, a), {})
const removeEmpties = pipe (pathEntries, filter(valueNotEmpty), hydrate)
with an appropriate version of valuesNotEmpty.
But all that is for another day.
It's an interesting problem. I think it can be solved elegantly if we write a generic map and filter function that works on both Arrays and Objects -
const map = (t, f) =>
isArray(t)
? t.map(f)
: isObject(t)
? Object.fromEntries(Object.entries(t).map(([k, v]) => [k, f(v, k)]))
: t
const filter = (t, f) =>
isArray(t)
? t.filter(f)
: isObject(t)
? Object.fromEntries(Object.entries(t).filter(([k, v]) => f(v, k)))
: t
We can write your removeEmpties program easily now -
if the input, t, is an object, recursively map over it and keep the non-empty values
(inductive) t is not an object. If t is a non-empty value, return t
(inductive) t is not an object and t is an empty value. Return the empty sentinel
const empty =
Symbol()
const removeEmpties = (t = {}) =>
isObject(t)
? filter(map(t, removeEmpties), nonEmpty) // 1
: nonEmpty(t)
? t // 2
: empty // 3
Now we have to define what it means to be nonEmpty -
const nonEmpty = t =>
isArray(t)
? t.length > 0
: isObject(t)
? Object.keys(t).length > 0
: isString(t)
? t.length > 0
: t !== empty // <- all other t are OK, except for sentinel
To this point we have use is* functions to do dynamic type-checking. We will define those now -
const isArray = t => Array.isArray(t)
const isObject = t => Object(t) === t
const isString = t => String(t) === t
const isNumber = t => Number(t) === t
const isMyType = t => // As many types as you want
Finally we can compute the result of your input -
const input =
{name:"fff",zero:0,onlineConsultation:false,image:"",primaryLocation:{locationName:"ggg",street:""},billingAndInsurance:[],categories:[""],concernsTreated:[""],education:[{nameOfInstitution:"ffff",description:"fff"}],experience:[{from:"",current:""}]}
const result =
removeEmpties(input)
console.log(JSON.stringify(result, null, 2))
{
"name": "fff",
"zero": 0,
"onlineConsultation": false,
"primaryLocation": {
"locationName": "ggg"
},
"education": [
{
"nameOfInstitution": "ffff",
"description": "fff"
}
]
}
Expand the program below to verify the result in your browser -
const map = (t, f) =>
isArray(t)
? t.map(f)
: isObject(t)
? Object.fromEntries(Object.entries(t).map(([k, v]) => [k, f(v, k)]))
: t
const filter = (t, f) =>
isArray(t)
? t.filter(f)
: isObject(t)
? Object.fromEntries(Object.entries(t).filter(([k, v]) => f(v, k)))
: t
const empty =
Symbol()
const removeEmpties = (t = {}) =>
isObject(t)
? filter(map(t, removeEmpties), nonEmpty)
: nonEmpty(t)
? t
: empty
const isArray = t => Array.isArray(t)
const isObject = t => Object(t) === t
const isString = t => String(t) === t
const nonEmpty = t =>
isArray(t)
? t.length > 0
: isObject(t)
? Object.keys(t).length > 0
: isString(t)
? t.length > 0
: t !== empty
const input =
{name:"fff",zero:0,onlineConsultation:false,image:"",primaryLocation:{locationName:"ggg",street:""},billingAndInsurance:[],categories:[""],concernsTreated:[""],education:[{nameOfInstitution:"ffff",description:"fff"}],experience:[{from:"",current:""}]}
const result =
removeEmpties(input)
console.log(JSON.stringify(result, null, 2))
function removeEmpty(obj){
if(obj.__proto__.constructor.name==="Array"){
obj = obj.filter(e=>e.length)
return obj.map((ele,i)=>{
if(obj.__proto__.constructor.name==="Object")return removeEmpty(ele) /* calling the same function*/
else return ele
})
}
if(obj.__proto__.constructor.name==="Object")for(let key in obj){
switch(obj[key].__proto__.constructor.name){
case "String":
if(obj[key].length===0)delete obj[key]
break;
case "Array":
obj[key] = removeEmpty(obj[key]) /* calling the same function*/
if(! obj[key].length)delete obj[key]
break;
case "Object":
obj[key] = removeEmpty(obj[key]) /* calling the same function*/
break;
}
}
return obj;
}
const input = {name: "fff", onlineConsultation: false, image: "", primaryLocation: {locationName: "ggg", street:""}, billingAndInsurance: [], categories: [""], concernsTreated: [""], education: [{nameOfInstitution: "ffff", description: "fff"}], experience: [{from: "", current:""}]}
console .log(removeEmpty(input))
function dropEmptyElements(object) {
switch (typeof object) {
case "object":
const keys = Object.keys(object || {}).filter(key => {
const value = dropEmptyElements(object[key]);
if(value === undefined) {
delete object[key];
}
return value !== undefined;
});
return keys.length === 0 ? undefined : object;
case "string":
return object.length > 0 ? object : undefined;
default:
return object;
}
}
This should do the trick for you ;)
Related
How do I remove all attributes which are undefined or null in a JavaScript object?
(Question is similar to this one for Arrays)
ES10/ES2019 examples
A simple one-liner (returning a new object).
let o = Object.fromEntries(Object.entries(obj).filter(([_, v]) => v != null));
Same as above but written as a function.
function removeEmpty(obj) {
return Object.fromEntries(Object.entries(obj).filter(([_, v]) => v != null));
}
This function uses recursion to remove items from nested objects.
function removeEmpty(obj) {
return Object.fromEntries(
Object.entries(obj)
.filter(([_, v]) => v != null)
.map(([k, v]) => [k, v === Object(v) ? removeEmpty(v) : v])
);
}
ES6/ES2015 examples
A simple one-liner. Warning: This mutates the given object instead of returning a new one.
Object.keys(obj).forEach((k) => obj[k] == null && delete obj[k]);
A single declaration (not mutating the given object).
let o = Object.keys(obj)
.filter((k) => obj[k] != null)
.reduce((a, k) => ({ ...a, [k]: obj[k] }), {});
Same as above but written as a function.
function removeEmpty(obj) {
return Object.entries(obj)
.filter(([_, v]) => v != null)
.reduce((acc, [k, v]) => ({ ...acc, [k]: v }), {});
}
This function uses recursion to remove items from nested objects.
function removeEmpty(obj) {
return Object.entries(obj)
.filter(([_, v]) => v != null)
.reduce(
(acc, [k, v]) => ({ ...acc, [k]: v === Object(v) ? removeEmpty(v) : v }),
{}
);
}
Same as the function above, but written in an imperative (non-functional) style.
function removeEmpty(obj) {
const newObj = {};
Object.entries(obj).forEach(([k, v]) => {
if (v === Object(v)) {
newObj[k] = removeEmpty(v);
} else if (v != null) {
newObj[k] = obj[k];
}
});
return newObj;
}
ES5/ES2009 examples
In the old days things were a lot more verbose.
This is a non recursive version written in a functional style.
function removeEmpty(obj) {
return Object.keys(obj)
.filter(function (k) {
return obj[k] != null;
})
.reduce(function (acc, k) {
acc[k] = obj[k];
return acc;
}, {});
}
This is a non recursive version written in an imperative style.
function removeEmpty(obj) {
const newObj = {};
Object.keys(obj).forEach(function (k) {
if (obj[k] && typeof obj[k] === "object") {
newObj[k] = removeEmpty(obj[k]);
} else if (obj[k] != null) {
newObj[k] = obj[k];
}
});
return newObj;
}
And a recursive version written in a functional style.
function removeEmpty(obj) {
return Object.keys(obj)
.filter(function (k) {
return obj[k] != null;
})
.reduce(function (acc, k) {
acc[k] = typeof obj[k] === "object" ? removeEmpty(obj[k]) : obj[k];
return acc;
}, {});
}
You can loop through the object:
var test = {
test1: null,
test2: 'somestring',
test3: 3,
}
function clean(obj) {
for (var propName in obj) {
if (obj[propName] === null || obj[propName] === undefined) {
delete obj[propName];
}
}
return obj
}
console.log(test);
console.log(clean(test));
If you're concerned about this property removal not running up object's proptype chain, you can also:
function clean(obj) {
var propNames = Object.getOwnPropertyNames(obj);
for (var i = 0; i < propNames.length; i++) {
var propName = propNames[i];
if (obj[propName] === null || obj[propName] === undefined) {
delete obj[propName];
}
}
}
A few notes on null vs undefined:
test.test1 === null; // true
test.test1 == null; // true
test.notaprop === null; // false
test.notaprop == null; // true
test.notaprop === undefined; // true
test.notaprop == undefined; // true
Shortest one liners for ES6+
Filter all falsy values ( "", 0, false, null, undefined )
Object.entries(obj).reduce((a,[k,v]) => (v ? (a[k]=v, a) : a), {})
Filter null and undefined values:
Object.entries(obj).reduce((a,[k,v]) => (v == null ? a : (a[k]=v, a)), {})
Filter ONLY null
Object.entries(obj).reduce((a,[k,v]) => (v === null ? a : (a[k]=v, a)), {})
Filter ONLY undefined
Object.entries(obj).reduce((a,[k,v]) => (v === undefined ? a : (a[k]=v, a)), {})
Recursive Solutions: Filters null and undefined
For Objects:
const cleanEmpty = obj => Object.entries(obj)
.map(([k,v])=>[k,v && typeof v === "object" ? cleanEmpty(v) : v])
.reduce((a,[k,v]) => (v == null ? a : (a[k]=v, a)), {});
For Objects and Arrays:
const cleanEmpty = obj => {
if (Array.isArray(obj)) {
return obj
.map(v => (v && typeof v === 'object') ? cleanEmpty(v) : v)
.filter(v => !(v == null));
} else {
return Object.entries(obj)
.map(([k, v]) => [k, v && typeof v === 'object' ? cleanEmpty(v) : v])
.reduce((a, [k, v]) => (v == null ? a : (a[k]=v, a)), {});
}
}
If you are using lodash or underscore.js, here is a simple solution:
var obj = {name: 'John', age: null};
var compacted = _.pickBy(obj);
This will only work with lodash 4, pre lodash 4 or underscore.js, use _.pick(obj, _.identity);
If somebody needs a recursive version of Owen's (and Eric's) answer, here it is:
/**
* Delete all null (or undefined) properties from an object.
* Set 'recurse' to true if you also want to delete properties in nested objects.
*/
function delete_null_properties(test, recurse) {
for (var i in test) {
if (test[i] === null) {
delete test[i];
} else if (recurse && typeof test[i] === 'object') {
delete_null_properties(test[i], recurse);
}
}
}
JSON.stringify removes the undefined keys.
removeUndefined = function(json){
return JSON.parse(JSON.stringify(json))
}
You can use a combination of JSON.stringify, its replacer parameter, and JSON.parse to turn it back into an object. Using this method also means the replacement is done to all nested keys within nested objects.
Example Object
var exampleObject = {
string: 'value',
emptyString: '',
integer: 0,
nullValue: null,
array: [1, 2, 3],
object: {
string: 'value',
emptyString: '',
integer: 0,
nullValue: null,
array: [1, 2, 3]
},
arrayOfObjects: [
{
string: 'value',
emptyString: '',
integer: 0,
nullValue: null,
array: [1, 2, 3]
},
{
string: 'value',
emptyString: '',
integer: 0,
nullValue: null,
array: [1, 2, 3]
}
]
};
Replacer Function
function replaceUndefinedOrNull(key, value) {
if (value === null || value === undefined) {
return undefined;
}
return value;
}
Clean the Object
exampleObject = JSON.stringify(exampleObject, replaceUndefinedOrNull);
exampleObject = JSON.parse(exampleObject);
CodePen example
Simplest possible Lodash solution to return an object with the null and undefined values filtered out.
_.omitBy(obj, _.isNil)
You can do a recursive removal in one line using json.stringify's replacer argument
const removeEmptyValues = obj => (
JSON.parse(JSON.stringify(obj, (k,v) => v ?? undefined))
)
Usage:
removeEmptyValues({a:{x:1,y:null,z:undefined}}) // Returns {a:{x:1}}
As mentioned in Emmanuel's comment, this technique only worked if your data structure contains only data types that can be put into JSON format (strings, numbers, lists, etc).
(This answer has been updated to use the new Nullish Coalescing operator. depending on browser support needs you may want to use this function instead: (k,v) => v!=null ? v : undefined)
You are probably looking for the delete keyword.
var obj = { };
obj.theProperty = 1;
delete obj.theProperty;
you can do shorter with ! condition
var r = {a: null, b: undefined, c:1};
for(var k in r)
if(!r[k]) delete r[k];
Remember in usage : as #semicolor announce in comments: This would also delete properties if the value is an empty string, false or zero
Remove all the properties with null and undefined
let obj = {
"id": 1,
"firstName": null,
"lastName": null,
"address": undefined,
"role": "customer",
"photo": "fb79fd5d-06c9-4097-8fdc-6cebf73fab26/fc8efe82-2af4-4c81-bde7-8d2f9dd7994a.jpg",
"location": null,
"idNumber": null,
};
let result = Object.entries(obj).reduce((a,[k,v]) => (v == null ? a : (a[k]=v, a)), {});
console.log(result)
I have same scenario in my project and achieved using following method.
It works with all data types, few mentioned above doesn't work with date and empty arrays .
removeEmptyKeysFromObject.js
removeEmptyKeysFromObject(obj) {
Object.keys(obj).forEach(key => {
if (Object.prototype.toString.call(obj[key]) === '[object Date]' && (obj[key].toString().length === 0 || obj[key].toString() === 'Invalid Date')) {
delete obj[key];
} else if (obj[key] && typeof obj[key] === 'object') {
this.removeEmptyKeysFromObject(obj[key]);
} else if (obj[key] == null || obj[key] === '') {
delete obj[key];
}
if (obj[key]
&& typeof obj[key] === 'object'
&& Object.keys(obj[key]).length === 0
&& Object.prototype.toString.call(obj[key]) !== '[object Date]') {
delete obj[key];
}
});
return obj;
}
pass any object to this function removeEmptyKeysFromObject()
Using ramda#pickBy you will remove all null, undefined and false values:
const obj = {a:1, b: undefined, c: null, d: 1}
R.pickBy(R.identity, obj)
As #manroe pointed out, to keep false values use isNil():
const obj = {a:1, b: undefined, c: null, d: 1, e: false}
R.pickBy(v => !R.isNil(v), obj)
Shorter ES6 pure solution, convert it to an array, use the filter function and convert it back to an object.
Would also be easy to make a function...
Btw. with this .length > 0 i check if there is an empty string / array, so it will remove empty keys.
const MY_OBJECT = { f: 'te', a: [] }
Object.keys(MY_OBJECT)
.filter(f => !!MY_OBJECT[f] && MY_OBJECT[f].length > 0)
.reduce((r, i) => { r[i] = MY_OBJECT[i]; return r; }, {});
JS BIN https://jsbin.com/kugoyinora/edit?js,console
Functional and immutable approach, without .filter and without creating more objects than needed
Object.keys(obj).reduce((acc, key) => (obj[key] === undefined ? acc : {...acc, [key]: obj[key]}), {})
Instead of delete the property, you can also create a new object with the keys that are not null.
const removeEmpty = (obj) => {
return Object.keys(obj).filter(key => obj[key]).reduce(
(newObj, key) => {
newObj[key] = obj[key]
return newObj
}, {}
)
}
For a deep search I used the following code, maybe it will be useful for anyone looking at this question (it is not usable for cyclic dependencies ) :
function removeEmptyValues(obj) {
for (var propName in obj) {
if (!obj[propName] || obj[propName].length === 0) {
delete obj[propName];
} else if (typeof obj[propName] === 'object') {
removeEmptyValues(obj[propName]);
}
}
return obj;
}
Here is a comprehensive recursive function (originally based on the one by #chickens) that will:
recursively remove what you tell it to defaults=[undefined, null, '', NaN]
Correctly handle regular objects, arrays and Date objects
const cleanEmpty = function(obj, defaults = [undefined, null, NaN, '']) {
if (!defaults.length) return obj
if (defaults.includes(obj)) return
if (Array.isArray(obj))
return obj
.map(v => v && typeof v === 'object' ? cleanEmpty(v, defaults) : v)
.filter(v => !defaults.includes(v))
return Object.entries(obj).length
? Object.entries(obj)
.map(([k, v]) => ([k, v && typeof v === 'object' ? cleanEmpty(v, defaults) : v]))
.reduce((a, [k, v]) => (defaults.includes(v) ? a : { ...a, [k]: v}), {})
: obj
}
USAGE:
// based off the recursive cleanEmpty function by #chickens.
// This one can also handle Date objects correctly
// and has a defaults list for values you want stripped.
const cleanEmpty = function(obj, defaults = [undefined, null, NaN, '']) {
if (!defaults.length) return obj
if (defaults.includes(obj)) return
if (Array.isArray(obj))
return obj
.map(v => v && typeof v === 'object' ? cleanEmpty(v, defaults) : v)
.filter(v => !defaults.includes(v))
return Object.entries(obj).length
? Object.entries(obj)
.map(([k, v]) => ([k, v && typeof v === 'object' ? cleanEmpty(v, defaults) : v]))
.reduce((a, [k, v]) => (defaults.includes(v) ? a : { ...a, [k]: v}), {})
: obj
}
// testing
console.log('testing: undefined \n', cleanEmpty(undefined))
console.log('testing: null \n',cleanEmpty(null))
console.log('testing: NaN \n',cleanEmpty(NaN))
console.log('testing: empty string \n',cleanEmpty(''))
console.log('testing: empty array \n',cleanEmpty([]))
console.log('testing: date object \n',cleanEmpty(new Date(1589339052 * 1000)))
console.log('testing: nested empty arr \n',cleanEmpty({ 1: { 2 :null, 3: [] }}))
console.log('testing: comprehensive obj \n', cleanEmpty({
a: 5,
b: 0,
c: undefined,
d: {
e: null,
f: [{
a: undefined,
b: new Date(),
c: ''
}]
},
g: NaN,
h: null
}))
console.log('testing: different defaults \n', cleanEmpty({
a: 5,
b: 0,
c: undefined,
d: {
e: null,
f: [{
a: undefined,
b: '',
c: new Date()
}]
},
g: [0, 1, 2, 3, 4],
h: '',
}, [undefined, null]))
Here's an alternative
Typescript:
function objectDefined <T>(obj: T): T {
const acc: Partial<T> = {};
for (const key in obj) {
if (obj[key] !== undefined) acc[key] = obj[key];
}
return acc as T;
}
Javascript:
function objectDefined(obj) {
const acc = {};
for (const key in obj) {
if (obj[key] !== undefined) acc[key] = obj[key];
}
return acc;
}
If you want 4 lines of a pure ES7 solution:
const clean = e => e instanceof Object ? Object.entries(e).reduce((o, [k, v]) => {
if (typeof v === 'boolean' || v) o[k] = clean(v);
return o;
}, e instanceof Array ? [] : {}) : e;
Or if you prefer more readable version:
function filterEmpty(obj, [key, val]) {
if (typeof val === 'boolean' || val) {
obj[key] = clean(val)
};
return obj;
}
function clean(entry) {
if (entry instanceof Object) {
const type = entry instanceof Array ? [] : {};
const entries = Object.entries(entry);
return entries.reduce(filterEmpty, type);
}
return entry;
}
This will preserve boolean values and it will clean arrays too. It also preserves the original object by returning a cleaned copy.
a reduce helper can do the trick (without type checking) -
const cleanObj = Object.entries(objToClean).reduce((acc, [key, value]) => {
if (value) {
acc[key] = value;
}
return acc;
}, {});
If you don't want to mutate in place, but return a clone with the null/undefined removed, you could use the ES6 reduce function.
// Helper to remove undefined or null properties from an object
function removeEmpty(obj) {
// Protect against null/undefined object passed in
return Object.keys(obj || {}).reduce((x, k) => {
// Check for null or undefined
if (obj[k] != null) {
x[k] = obj[k];
}
return x;
}, {});
}
To piggypack on Ben's answer on how to solve this problem using lodash's _.pickBy, you can also solve this problem in the sister library: Underscore.js's _.pick.
var obj = {name: 'John', age: null};
var compacted = _.pick(obj, function(value) {
return value !== null && value !== undefined;
});
See: JSFiddle Example
You can also use ... spread syntax using forEach something like this:
let obj = { a: 1, b: "b", c: undefined, d: null };
let cleanObj = {};
Object.keys(obj).forEach(val => {
const newVal = obj[val];
cleanObj = newVal ? { ...cleanObj, [val]: newVal } : cleanObj;
});
console.info(cleanObj);
If someone needs to remove undefined values from an object with deep search using lodash then here is the code that I'm using. It's quite simple to modify it to remove all empty values (null/undefined).
function omitUndefinedDeep(obj) {
return _.reduce(obj, function(result, value, key) {
if (_.isObject(value)) {
result[key] = omitUndefinedDeep(value);
}
else if (!_.isUndefined(value)) {
result[key] = value;
}
return result;
}, {});
}
With Lodash:
_.omitBy({a: 1, b: null}, (v) => !v)
If you use eslint and want to avoid tripping the the no-param-reassign rule, you can use Object.assign in conjunction with .reduce and a computed property name for a fairly elegant ES6 solution:
const queryParams = { a: 'a', b: 'b', c: 'c', d: undefined, e: null, f: '', g: 0 };
const cleanParams = Object.keys(queryParams)
.filter(key => queryParams[key] != null)
.reduce((acc, key) => Object.assign(acc, { [key]: queryParams[key] }), {});
// { a: 'a', b: 'b', c: 'c', f: '', g: 0 }
Here is a functional way to remove nulls from an Object using ES6 without mutating the object using only reduce:
const stripNulls = (obj) => {
return Object.keys(obj).reduce((acc, current) => {
if (obj[current] !== null) {
return { ...acc, [current]: obj[current] }
}
return acc
}, {})
}
Recursively remove null, undefined, empty objects and empty arrays, returning a copy (ES6 version)
export function skipEmpties(dirty) {
let item;
if (Array.isArray(dirty)) {
item = dirty.map(x => skipEmpties(x)).filter(value => value !== undefined);
return item.length ? item : undefined;
} else if (dirty && typeof dirty === 'object') {
item = {};
Object.keys(dirty).forEach(key => {
const value = skipEmpties(dirty[key]);
if (value !== undefined) {
item[key] = value;
}
});
return Object.keys(item).length ? item : undefined;
} else {
return dirty === null ? undefined : dirty;
}
}
I have the following object:
const modules = {
celebrity: {
actor: {
male: 'male',
female: 'female'
},
director: 'director'
},
movie: 'movie',
user: 'user'
};
In result I want an array of string as the following:
[
"celebrity/actor/male",
"celebrity/actor/female",
"celebrity/director",
"movie",
"user"
]
I create the following function:
function getPathsList(node, path = '') {
let pathsList = [];
const childs = Object.entries(node);
for (const child of childs) {
if (typeof child[1] === 'string') {
path += `/${child[1]}`
pathsList.push(path)
} else {
path += `/${child[0]}`
pathsList = [...pathsList, ...getPathsList(child[1], path, pathsList)]
}
}
return pathsList;
}
But I got:
[
"/celebrity/actor/male",
"/celebrity/actor/male/female",
"/celebrity/actor/director",
"/celebrity/movie",
"/celebrity/movie/user"
]
I know that the path variable should be initialized somewhere, but I can't figure it out.
You could use an appraoch which works without a path, but assembles the path by iterating the nested part object.
function getPathsList(node) {
const pathsList = [];
for (const [key, value] of Object.entries(node)) {
if (value && typeof value === 'object') {
pathsList.push(...getPathsList(value).map(p => `${key}/${p}`))
} else {
pathsList.push(key);
}
}
return pathsList;
}
const modules = {
celebrity: {
actor: {
male: 'male',
female: 'female'
},
director: 'director'
},
movie: 'movie',
user: 'user'
};
console.log(getPathsList(modules));
Another way, using reduce:
const modules = {celebrity:{actor:{male:"male",female:"female"},director:"director"},movie:"movie",user:"user"};
function getPathsList(node, path = '') {
return Object.entries(node)
.reduce(
(res, [k, v]) => res.concat(
typeof v === "string" ? `${path}${v}` : getPathsList(v, `${path}${k}/`
)
), []);
}
console.log(getPathsList(modules));
You may consider a "dfs" like algorithm where you explore every path from root to leaf.
You then join your path with '/'.
Subtlety: don't put the leaf itself into the path (e.g: otherwise you would get movie/movie)
Below an example using flatMap
const modules = {"celebrity":{"actor":{"male":"male","female":"female"},"director":"director"},"movie":"movie","user":"user"}
const flatten = x => {
if (typeof(x) === 'string') { return [[]] }
// each node has for children its entries
// each node returns an array of path
return Object.entries(x).flatMap(([k, v]) => {
return flatten(v).map(path => [k , ...path])
})
}
console.log(flatten(modules).map(path => path.join('/')))
Where is the difficulty ?
const
modules =
{ celebrity:
{ actor: { male: 'male', female: 'female' }
, director: 'director'
}
, movie: 'movie'
, user: 'user'
}
, pathsList = []
;
function getPathsList( obj, path='' )
{
for (let key in obj )
{
if (typeof(obj[key]) === 'object') getPathsList( obj[key], path+'/'+key )
else pathsList.push( (path+'/'+key).substring(1) )
}
}
getPathsList( modules )
console.log( pathsList )
A very simple recursive solution using Object .entries returns an array containing only an empty string for non-objects, and otherwise, for every key-value, combines the key with the results of a recursive call to the value. The only slightly tricky part is to not insert the slash (/) before an empty string. It looks like this:
const getPathsList = (obj) => Object (obj) === obj
? Object .entries (obj) .flatMap (
([k, v]) => getPathsList (v) .map (p => p ? k + '/' + p : k)
)
: ['']
const modules = {celebrity: {actor: {male: 'male', female: 'female'}, director: 'director'}, movie: 'movie', user: 'user'}
console .log (getPathsList (modules))
But I would prefer to do this a slightly different way, building our function atop one that gathers the result into arrays of values (e.g. [['celebrity', 'actor', 'male'], ['celebrity', 'actor', 'female'], ... ['user']]), then simply joining those new arrays together with slashes. It's quite similar:
const getPaths = (obj) => Object (obj) === obj
? Object .entries (obj) .flatMap (
([k, v]) => getPaths (v) .map (p => [k, ...p])
)
: [[]]
const getPathsList = (obj) =>
getPaths (obj) .map (xs => xs .join ('/'))
const modules = {celebrity: {actor: {male: 'male', female: 'female'}, director: 'director'}, movie: 'movie', user: 'user'}
console .log (getPathsList (modules))
I find that intermediate array format much more helpful.
This is a slightly less sophisticated version of getPaths than I generally write. Usually I distinguish between numeric array indices and string object keys, but that's not relevant here, since we're folding them back into strings, so this version is simplified.
I'm new to programming, and I was working a question which is bother me for a while.
I want to recursively create a nested object from another nested object in javascript,
below is the sample data for input, but in real situation, I don't how deep will this object to be.
nums = {
Obj:{
x1:{
x11:43,
x12:4,
x13:612
},
x2:{
x21:4,
x22:7,
},
x3:2,
}}
this is the result I want (see number is even or odd, even=true, odd=false)
res = {
Obj:{
x1:{
x11:false,
x12:true,
x13:true
},
x2:{
x21:true,
x22:false,
},
x3:true,
}}
and this is my code
const nums = {
Obj:{
x1:{
x11:43,
x12:4,
x13:612
},
x2:{
x21:4,
x22:7,
},
x3:2,
}
}
const res ={};
getResult(nums);
console.log(res);
function getResult(x){
Object.keys(x).forEach(element => {
if(isNaN(x[element])){
res[element]=getResult(x[element]);
} else {
let result = (x[element] % 2 < 1)? true:false;
return {[element]: result}; // this is where I don't know what to, I try to return a object,
// but it gives{x1: undefined, x2: undefined, Obj: undefined}
//
// if I change to "return res[element]=result"
// every sub-Object will add under the same level
}
});
}
I will really appreciate if someone can help me on this.
Instead of return {[element]: result}; , overwrite the value, and return the mutated object from the function after the loop :
note that this will mutate the original object, if you want to keep it, make a copy :
const copy = JSON.parse(JSON.stringify(nums));
const nums = {
Obj: {
x1: {
x11: 43,
x12: 4,
x13: 612
},
x2: {
x21: 4,
x22: 7,
},
x3: 2,
}
}
const res = {};
const copy = JSON.parse(JSON.stringify(nums));
getResult(copy);
console.log(res);
function getResult(x) {
Object.keys(x).forEach(element => {
if (isNaN(x[element])) {
res[element] = getResult(x[element]);
} else {
let result = (x[element] % 2 < 1) ? true : false;
x[element] = result; // overwrite the number with true or flse
}
});
return x; // return the mutated object
}
Instead of mutating something make something more functional and return a new object:
const getResults = o => typeof o === "object"
? Object.keys(o).reduce((a, k) => ({ ...a, [k]: getResults(o[k]) }), {})
: o % 2 === 1;
Basically we check if object is an object (using typeof) and go deeper if so. Otherwise we check if it is odd or even.
You can also think of this more generically, writing a function that will apply your transformation to all the leaf nodes of your object, then calling it with an isEven function. Here's one technique:
const mapLeaves = (fn) => (tree) =>
typeof tree == "object"
? Object .fromEntries (Object .entries (tree) .map (
([k, v]) => [k, mapLeaves (fn) (v)]
))
: fn (tree)
const isEven = (n) => n % 2 == 0
const nums = {Obj: {x1: {x11: 43, x12: 4, x13: 612}, x2: {x21: 4, x22: 7}, x3: 2}}
console .log (
mapLeaves (isEven) (nums)
)
And of course mapLeaves (isEven) is a reusable function that you could apply to multiple objects.
This does not handle arrays. It would only be slightly more complex to create a version of mapLeaves that also applied this to entries of an array:
const mapLeaves = (fn) => (tree) =>
Array .isArray (tree)
? tree .map (x => mapLeaves (fn) (x))
: typeof tree == "object"
? Object .fromEntries (Object .entries (tree) .map (
([k, v]) => [k, mapLeaves (fn) (v)]
))
: fn (tree)
I'm trying to sort an array of nested objects. It's working with a static chosen key but I can't figure out how to get it dynamically.
So far I've got this code
sortBy = (isReverse=false) => {
this.setState(prevState => ({
files: prevState.files.sort((a, b) => {
const valueA = (((a || {})['general'] || {})['fileID']) || '';
const valueB = (((b || {})['general'] || {})['fileID']) || '';
if(isReverse) return valueB.localeCompare(valueA);
return valueA.localeCompare(valueB);
})
}));
}
At this point the keys are hardcoded ['general']['orderID'] but I want this part to be dynamic by adding a keys param to the sortBy function:
sortBy = (keys, isReverse=false) => { ...
keys is an array with the nested keys. For the above example, it will be ['general', 'fileID'].
What are the steps that need to be taken to make this dynamic?
Note: child objects can be undefined therefore I'm using a || {}
Note 2: I'm using es6. No external packages.
The currently accepted answer apart from putting bugs in your code is not doing much to help you. Use of a simple function deepProp would mitigate the painful repetition -
const deepProp = (o = {}, props = []) =>
props.reduce((acc = {}, p) => acc[p], o)
Now without so much noise -
sortBy = (keys, isReverse = false) =>
this.setState ({
files: // without mutating the previous state!
[...this.state.files].sort((a,b) => {
const valueA = deepProp(a, keys) || ''
const valueB = deepProp(b, keys) || ''
return isReverse
? valueA.localeCompare(valueB)
: valueB.localeCompare(valueA)
})
})
Still, this does little in terms of actually improving your program. It's riddled with complexity, and worse, this complexity will be duplicated in any component that requires similar functionality. React embraces functional style so this answer approaches the problem from a functional standpoint. In this post, we'll write sortBy as -
sortBy = (comparator = asc) =>
this.setState
( { files:
isort
( contramap
( comparator
, generalFileId
)
, this.state.files
)
}
)
Your question poses us to learn two powerful functional concepts; we'll use these to answer the question -
Monads
Contravariant Functors
Let's not get overwhelmed by terms though and instead focus on gaining an intuition for how things work. At first, it looks like we have a problem checking for nulls. Having to deal with the possibility that some of our inputs may not have the nested properties makes our function messy. If we can generalize this concept of a possible value, we can clean things up a bit.
Your question specifically says you are not using an external package right now, but this is a good time to reach for one. Let's take a brief look at the data.maybe package -
A structure for values that may not be present, or computations that may fail. Maybe(a) explicitly models the effects that implicit in Nullable types, thus has none of the problems associated with using null or undefined — like NullPointerException or TypeError.
Sounds like a good fit. We'll start by writing a function safeProp that accepts an object and a property string as input. Intuitively, safeProp safely returns the property p of object o -
const { Nothing, fromNullable } =
require ('data.maybe')
const safeProp = (o = {}, p = '') =>
// if o is an object
Object (o) === o
// access property p on object o, wrapping the result in a Maybe
? fromNullable (o[p])
// otherwise o is not an object, return Nothing
: Nothing ()
Instead of simply returning o[p] which could be a null or undefined value, we'll get back a Maybe that guides us in handling the result -
const generalFileId = (o = {}) =>
// access the general property
safeProp (o, 'general')
// if it exists, access the fileId property on the child
.chain (child => safeProp (child, 'fileId'))
// get the result if valid, otherwise return empty string
.getOrElse ('')
Now we have a function which can take objects of varying complexity and guarantees the result we're interested in -
console .log
( generalFileId ({ general: { fileId: 'a' } }) // 'a'
, generalFileId ({ general: { fileId: 'b' } }) // 'b'
, generalFileId ({ general: 'x' }) // ''
, generalFileId ({ a: 'x '}) // ''
, generalFileId ({ general: { err: 'x' } }) // ''
, generalFileId ({}) // ''
)
That's half the battle right there. We can now go from our complex object to the precise string value we want to use for comparison purposes.
I'm intentionally avoiding showing you an implementation of Maybe here because this in itself is a valuable lesson. When a module promises capability X, we assume we have capability X, and ignore what happens in the black box of the module. The very point of data abstraction is to hide concerns away so the programmer can think about things at a higher level.
It might help to ask how does Array work? How does it calculate or adjust the length property when an element is added or removed from the array? How does the map or filter function produce a new array? If you never wondered these things before, that's okay! Array is a convenient module because it removes these concerns from the programmer's mind; it just works as advertised.
This applies regardless of whether the module is provided by JavaScript, by a third party such as from npm, or if you wrote the module yourself. If Array didn't exist, we could implement it as our own data structure with equivalent conveniences. Users of our module gain useful functionalities without introducing additional complexity. The a-ha moment comes when you realize that the programmer is his/her own user: when you run into a tough problem, write a module to free yourself from the shackles of complexity. Invent your own convenience!
We'll show a basic implementation of Maybe later in the answer, but for now, we just have to finish the sort ...
We start with two basic comparators, asc for ascending sort, and desc for descending sort -
const asc = (a, b) =>
a .localeCompare (b)
const desc = (a, b) =>
asc (a, b) * -1
In React, we cannot mutate the previous state, instead, we must create new state. So to sort immutably, we must implement isort which will not mutate the input object -
const isort = (compare = asc, xs = []) =>
xs
.slice (0) // clone
.sort (compare) // then sort
And of course a and b are sometimes complex objects, so case we can't directly call asc or desc. Below, contramap will transform our data using one function g, before passing the data to the other function, f -
const contramap = (f, g) =>
(a, b) => f (g (a), g (b))
const files =
[ { general: { fileId: 'e' } }
, { general: { fileId: 'b' } }
, { general: { fileId: 'd' } }
, { general: { fileId: 'c' } }
, { general: { fileId: 'a' } }
]
isort
( contramap (asc, generalFileId) // ascending comparator
, files
)
// [ { general: { fileId: 'a' } }
// , { general: { fileId: 'b' } }
// , { general: { fileId: 'c' } }
// , { general: { fileId: 'd' } }
// , { general: { fileId: 'e' } }
// ]
Using the other comparator desc, we can see sorting work in the other direction -
isort
( contramap (desc, generalFileId) // descending comparator
, files
)
// [ { general: { fileId: 'e' } }
// , { general: { fileId: 'd' } }
// , { general: { fileId: 'c' } }
// , { general: { fileId: 'b' } }
// , { general: { fileId: 'a' } }
// ]
Now to write the method for your React component, sortBy. The method is essentially reduced to this.setState({ files: t (this.state.files) }) where t is an immutable transformation of your program's state. This is good because complexity is kept out of your components where testing is difficult, and instead it resides in generic modules, which are easy to test -
sortBy = (reverse = true) =>
this.setState
( { files:
isort
( contramap
( reverse ? desc : asc
, generalFileId
)
, this.state.files
)
}
)
This uses the boolean switch like in your original question, but since React embraces functional pattern, I think it would be even better as a higher-order function -
sortBy = (comparator = asc) =>
this.setState
( { files:
isort
( contramap
( comparator
, generalFileId
)
, this.state.files
)
}
)
If the nested property you need to access is not guaranteed to be general and fileId, we can make a generic function which accepts a list of properties and can lookup a nested property of any depth -
const deepProp = (o = {}, props = []) =>
props .reduce
( (acc, p) => // for each p, safely lookup p on child
acc .chain (child => safeProp (child, p))
, fromNullable (o) // init with Maybe o
)
const generalFileId = (o = {}) =>
deepProp (o, [ 'general', 'fileId' ]) // using deepProp
.getOrElse ('')
const fooBarQux = (o = {}) =>
deepProp (o, [ 'foo', 'bar', 'qux' ]) // any number of nested props
.getOrElse (0) // customizable default
console.log
( generalFileId ({ general: { fileId: 'a' } } ) // 'a'
, generalFileId ({}) // ''
, fooBarQux ({ foo: { bar: { qux: 1 } } } ) // 1
, fooBarQux ({ foo: { bar: 2 } }) // 0
, fooBarQux ({}) // 0
)
Above, we use the data.maybe package which provides us with the capability to work with potential values. The module exports functions to convert ordinary values to a Maybe, and vice versa, as well as many useful operations that are applicable to potential values. There's nothing forcing you to use this particular implementation, however. The concept is simple enough that you could implement fromNullable, Just and Nothing in a couple dozen lines, which we'll see later in this answer -
Run the complete demo below on repl.it
const { Just, Nothing, fromNullable } =
require ('data.maybe')
const safeProp = (o = {}, p = '') =>
Object (o) === o
? fromNullable (o[p])
: Nothing ()
const generalFileId = (o = {}) =>
safeProp (o, 'general')
.chain (child => safeProp (child, 'fileId'))
.getOrElse ('')
// ----------------------------------------------
const asc = (a, b) =>
a .localeCompare (b)
const desc = (a, b) =>
asc (a, b) * -1
const contramap = (f, g) =>
(a, b) => f (g (a), g (b))
const isort = (compare = asc, xs = []) =>
xs
.slice (0)
.sort (compare)
// ----------------------------------------------
const files =
[ { general: { fileId: 'e' } }
, { general: { fileId: 'b' } }
, { general: { fileId: 'd' } }
, { general: { fileId: 'c' } }
, { general: { fileId: 'a' } }
]
isort
( contramap (asc, generalFileId)
, files
)
// [ { general: { fileId: 'a' } }
// , { general: { fileId: 'b' } }
// , { general: { fileId: 'c' } }
// , { general: { fileId: 'd' } }
// , { general: { fileId: 'e' } }
// ]
The advantages of this approach should be evident. Instead of one big complex function that is difficult to write, read, and test, we've combined several smaller functions that are easier to write, read, and test. The smaller functions have the added advantage of being used in other parts of your program, whereas the big complex function is likely to only be usable in one part.
Lastly, sortBy is implemented as a higher-order function which means we're not limited to only ascending and descending sorts toggled by the reverse boolean; any valid comparator can be used. This means we could even write a specialized comparator that handles tie breaks using custom logic or compares year first, then month, then day, etc; higher-order functions expand your possibilities tremendously.
I don't like making empty promises so I want to show you that it's not difficult to devise your own mechanisms like Maybe. This is also a nice lesson in data abstraction because it shows us how a module has its own set of concerns. The module's exported values are the only way to access the module's functionalities; all other components of the module are private and are free to change or refactor as other requirements dictate -
// Maybe.js
const None =
Symbol ()
class Maybe
{ constructor (v)
{ this.value = v }
chain (f)
{ return this.value == None ? this : f (this.value) }
getOrElse (v)
{ return this.value === None ? v : this.value }
}
const Nothing = () =>
new Maybe (None)
const Just = v =>
new Maybe (v)
const fromNullable = v =>
v == null
? Nothing ()
: Just (v)
module.exports =
{ Just, Nothing, fromNullable } // note the class is hidden from the user
Then we would use it in our module. We only have to change the import (require) but everything else just works as-is because of the public API of our module matches -
const { Just, Nothing, fromNullable } =
require ('./Maybe') // this time, use our own Maybe
const safeProp = (o = {}, p = '') => // nothing changes here
Object (o) === o
? fromNullable (o[p])
: Nothing ()
const deepProp = (o, props) => // nothing changes here
props .reduce
( (acc, p) =>
acc .chain (child => safeProp (child, p))
, fromNullable (o)
)
// ...
For more intuition on how to use contramap, and perhaps some unexpected surprises, please explore the following related answers -
multi-sort using contramap
recursive search using contramap
You can use a loop to extract a nested property path from an object:
const obj = {
a: {
b: {
c: 3
}
}
}
const keys = ['a', 'b', 'c']
let value = obj;
for (const key of keys) {
if (!value) break; // stop once we reach a falsy value. Optionally you can make this a tighter check accounting for objects only
value = value[key];
}
console.log(`c=${value}`);
Then you can wrap the function above into a helper:
function getPath(obj, keys) {
let value = obj;
for (const key of keys) {
if (!value) break; // stop once we reach a falsy value. Optionally you can make this a tighter check accounting for objects only
value = value[key];
}
return value;
}
And use it when obtaining your values:
sortBy = (isReverse = false, keys = []) => {
this.setState(prevState => ({
files: prevState.files.sort((a, b) => {
const valueA = getPath(a, keys) || '';
const valueB = getPath(b, keys) || '';
// ...
})
}));
}
You can loop ovver the keys to get the values and then compare them like
sortBy = (keys, isReverse=false) => {
this.setState(prevState => ({
files: prevState.files.sort((a, b) => {
const clonedKey = [...keys];
let valueA = a;
let valueB = b
while(clonedKey.length > 0) {
const key = clonedKey.shift();
valueA = (valueA || {})[key];
valueB = (valueB || {})[key];
}
valueA = valueA || '';
valueB = valueB || '';
if(isReverse) return valueB.localeCompare(valueA);
return valueA.localeCompare(valueB);
})
}));
}
One way could be using reduce() over the new keys argument, something like this:
sortBy = (keys, isReverse=false) =>
{
this.setState(prevState =>
({
files: prevState.files.slice().sort((a, b) =>
{
const valueA = (keys.reduce((acc, key) => (acc || {})[key], a) || '').toString();
const valueB = (keys.reduce((acc, key) => (acc || {})[key], b) || '').toString();
return (isReverse ? valueB.localeCompare(valueA) : valueA.localeCompare(valueB));
})
}));
}
To work with an arbitrary number of keys, you could create a function that could be reused with .reduce() to traverse deeply into nested objects. I'd also put the keys as the last parameter, so that you can use "rest" and "spread" syntax.
const getKey = (o, k) => (o || {})[k];
const sorter = (isReverse, ...keys) => (a, b) => {
const valueA = keys.reduce(getKey, a) || '';
const valueB = keys.reduce(getKey, b) || '';
if (isReverse) return valueB.localeCompare(valueA);
return valueA.localeCompare(valueB);
};
const sortBy = (isReverse = false, ...keys) => {
this.setState(prevState => ({
files: prevState.files.sort(sorter(isReverse, ...keys))
}));
}
I also moved the sort function out to its own const variable, and made it return a new function that uses the isReverse value.
This also handles the case when the path resolves to a non-string value by converting it to string. Otherwise .localeCompare might fail.
sortBy = (keys, isReverse=false) => {
this.setState(prevState => ({
files: prevState.files.sort((a, b) => {
const valueA = getValueAtPath(a, keys);
const valueB = getValueAtPath(b, keys);
if(isReverse) return valueB.localeCompare(valueA);
return valueA.localeCompare(valueB);
})
}));
}
function getValueAtPath(file, path) {
let value = file;
let keys = [...path]; // preserve the original path array
while(value && keys.length) {
let key = keys.shift();
value = value[key];
}
return (value || '').toString();
}
Compare elements in sort function in following way:
let v= c => keys.reduce((o,k) => o[k]||'',c)
return (isReverse ? -1 : 1) * v(a).localeCompare(v(b));
likte this:
sortBy = (keys, isReverse=false) => {
this.setState(prevState => ({
files: prevState.files.sort((a, b) => {
let v=c=>keys.reduce((o,k) => o[k]||'',c)
return (isReverse ? -1 : 1)*v(a).localeCompare(v(b));
})
}));
}
Here is example how this idea works:
let files = [
{ general: { fileID: "3"}},
{ general: { fileID: "1"}},
{ general: { fileID: "2"}},
{ general: { }}
];
function sortBy(keys, arr, isReverse=false) {
arr.sort((a,b,v=c=>keys.reduce((o,k) => o[k]||'',c)) =>
(isReverse ? -1 : 1)*v(a).localeCompare(v(b)) )
}
sortBy(['general', 'fileID'],files,true);
console.log(files);
i have this type of object which fetched from Redis
{
'username': 'hamet',
'username_Type': 'string',
'meta': 'object',
'meta_Type': 'object',
'meta.avatar': '/avatar.png',
'meta.avatar_Type': 'string',
'meta.active': 'false',
'meta.active_Type': 'boolean',
'meta.someArr': 'array',
'meta.someArr_Type': 'array',
'meta.someArr.0': 'object',
'meta.someArr.0_Type': 'object',
'meta.someArr.0.field': '123',
'meta.someArr.0.field_Type': 'number',
'meta.someArr.1': 'object',
'meta.someArr.1_Type': 'object',
'meta.someArr.1.field': '321',
'meta.someArr.1.field_Type': 'number'
}
all i want is convert this object to valid object like this:
{
username: 'hamet',
meta: {
avatar: '/avatar.png',
active: false,
someArr: [
{ field: 123 },
{ field: 321 }
]
}
}
once i created iterated function, but there was a problem with that. is it possible to convert with Iterated function and how?
You could create object with value types that you will use for creating new instances of different data types and then use reduce() method to build your object.
const data = {"username":"hamet","username_Type":"string","meta":"object","meta_Type":"object","meta.avatar":"/avatar.png","meta.avatar_Type":"string","meta.active":"false","meta.active_Type":"boolean","meta.someArr":"array","meta.someArr_Type":"array","meta.someArr.0":"object","meta.someArr.0_Type":"object","meta.someArr.0.field":"123","meta.someArr.0.field_Type":"number","meta.someArr.1":"object","meta.someArr.1_Type":"object","meta.someArr.1.field":"321","meta.someArr.1.field_Type":"number"}
const result = {}
const create = {'string': String,'number': Number,'boolean': Boolean,'array': Array,'object': Object}
const findType = (key, obj) => obj[key]
Object.keys(data).forEach(key => {
if (!key.includes('Type')) {
key.split('.').reduce((r, e, i, arr) => {
let type = findType(key + '_Type', data);
let value = create[data[key]] || arr[i + 1] ? new create[type] : new create[type](data[key]).valueOf()
if (data[key] == 'false') value = false
r[e] = r[e] || value;
return r[e]
}, result)
}
})
console.log(result)
Get an array of keys with Object.keys(). Filter out the _Type keys. Sort the keys to ensure that parents (shorter) keys are first, since keys` order in an object is not ensured.
Reduce the array of keys, and for each key get it's value by type. If the type is not object/array use the actual key value. Iterate the result object with Array.forEach(), until you get to the leaf. Add the key with the value.
const obj = {"meta.someArr.1.field":"321","username":"hamet","username_Type":"string","meta":"object","meta_Type":"object","meta.avatar":"/avatar.png","meta.avatar_Type":"string","meta.active":"false","meta.active_Type":"boolean","meta.someArr":"array","meta.someArr_Type":"array","meta.someArr.0":"object","meta.someArr.0_Type":"object","meta.someArr.0.field":"123","meta.someArr.0.field_Type":"number","meta.someArr.1":"object","meta.someArr.1_Type":"object","meta.someArr.1.field_Type":"number"};
const byType = {
object: Object,
array: Array
};
const result = Object.keys(obj)
.filter((k) => !k.includes('_Type')) // remove Type keys
.sort((a, b) => a.length - b.length) // ensures that shorter (parent) keys are first
.reduce((r, k) => {
const type = obj[`${k}_Type`];
const valueByType = byType[type] && byType[type]();
const value = valueByType ? valueByType : obj[k];
const keys = k.split('.');
let current = r;
keys.forEach((key, i) => {
if(!(key in current)) current[key] = value;
else current = current[key];
});
return r;
}, {});
console.log(result);
const result = {};
function apply(obj, value, key, ...keys) {
if(keys.length) {
apply(obj[key] || (obj[key] = {}), value ...keys);
} else {
obj[key] = value;
}
}
for(const [key, value] of Object.entries(yourObj))
apply(result, value, ...key.split("."));
You could use a recursive approach to generate the nested structure. I havent included a check if key is a number so that it creates an array, thats your job ;)
If you prefer functional programming:
const apply = (obj, value, ...keys) => keys.slice(1).reduce((obj, key) => obj[key] || (obj[key] = {}), obj)[keys.pop()] = value;