2

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

2
  • Do you want to delete all properties that are blank ? even those that are inside arrays ? Commented Sep 17, 2020 at 23:38
  • "remove all empty objects, and empty arrays": your code seems to want to remove empty strings as well. Can you clarify? Commented Sep 18, 2020 at 5:56

4 Answers 4

3

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.

Sign up to request clarification or add additional context in comments.

Comments

1

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 -

  1. if the input, t, is an object, recursively map over it and keep the non-empty values
  2. (inductive) t is not an object. If t is a non-empty value, return t
  3. (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))

3 Comments

So much simpler than mine. I don't know why I didn't see this as I work usually with map and filter functions that work this way.
I'm just reading over your answer now and I was tripping up a bit on why filter was happening first, but it makes sense now. It may be more complex, but the flattening and rebuilding of the tree is still a neat computational process to imagine.
Yes, I've been finding all sorts of uses for nesting a function :: [(k, v)] -> [(k, v)] between Object.entries and Object.fromEntries. I think there are similar benefits to nesting :: [(p, v)] -> [(p, v)] between pathEntries and hydrate. I'm sure I'll investigate further.
0

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))

1 Comment

education: [{…}] is not empty so I guess it shouldt be deleted.
0
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 ;)

2 Comments

you did not account for the case of object being null. JS throws an error
welcome to SO. this is considered a low-quality answer because it offers no remarks on the asker's original question and no explanation of the solution provided. this leads to copy/paste development and is the source of many headaches for individual programmers and developer teams alike. consider explaining your thought process or addressing why the OP's code is failing and i will eagerly remove my down-vote.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.