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

Meir
  • 516
  • 4
  • 19
  • Do you want to delete all properties that are blank ? even those that are inside arrays ? – Carlos1232 Sep 17 '20 at 23:38
  • *"remove all empty objects, and empty arrays"*: your code seems to want to remove empty strings as well. Can you clarify? – trincot Sep 18 '20 at 05:56

4 Answers4

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.

Scott Sauyet
  • 49,207
  • 4
  • 49
  • 103
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))
Mulan
  • 129,518
  • 31
  • 228
  • 259
  • 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. – Scott Sauyet Sep 18 '20 at 18:11
  • 1
    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. – Mulan Sep 18 '20 at 18:15
  • 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. – Scott Sauyet Sep 18 '20 at 21:09
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 ;)

Mulan
  • 129,518
  • 31
  • 228
  • 259
WolverinDEV
  • 1,494
  • 9
  • 19
  • you did not account for the case of `object` being `null`. JS throws an error – Meir Sep 22 '20 at 17:42
  • 1
    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. – Mulan Dec 31 '20 at 17:43
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))
Scott Sauyet
  • 49,207
  • 4
  • 49
  • 103