4

How to filter an object given its partial path?

As an example.

let address  = {
  country :{
    name:'Japan',
    city:{
      name:'Tokyo',
      town: {
        name:'korushawa'
      }
    },
    code:'JP'
  },
  nearbyCountry:'Korea'
}

path1: countr.cit

For address, path1 will result in

{
  country :{
    city:{
      name:'Tokyo',
      town: {
        name:'korushawa'
      }
    }
  }
}


path2: countr

For path2, I should get entire address object because countr is present in country and nearbyCountry

{
  country :{
    name:'Japan',
    city:{
      name:'Tokyo',
      town: {
        name:'korushawa'
      }
    }
  },
  nearbyCountry:'Korea'
}

Edit: I have been able to solve this when given an exact path (ex: country.city). But having difficulty with partial paths.

asdasd
  • 6,050
  • 3
  • 14
  • 32

3 Answers3

4
  • You could create a recursive function which takes an object and an array of partial paths as a parameter.
  • Get the latest partial path and filter the keys which include that path
  • If there are no keys, return null
  • Else, create an object from the keys using reduce
    • if there are no more keys left, just add the all the filtered properties of obj to the accumulator
    • else, recursively call the function get another nested object

const address={country:{name:'Japan',city:{name:'Tokyo',town:{name:'korushawa'}},code:'JP'},nearbyCountry:'Korea'};

function filterObject(obj, paths) {
  if (!obj) return null;

  const partial = paths.shift(),
        filteredKeys = Object.keys(obj).filter(k => k.toLowerCase().includes(partial));

  if (!filteredKeys.length) return null; // no keys with the path found
  
  return filteredKeys.reduce((acc, key) => {
    if(!paths.length) return { ...acc, [key]: obj[key] }
    
    const nest = filterObject(obj[key], [...paths]) // filter another level
    return nest ? { ...acc, [key]: nest } : acc
  }, null)
}

let path;
console.log(path = 'countr', ':');
console.log(filterObject(address, path.split('.')))

console.log(path = 'countr.cit', ':');
console.log(filterObject(address, path.split('.')))

console.log(path = 'countr.cit.to', ':');
console.log(filterObject(address, path.split('.')))

console.log(path = 'countr.cit.doesntexist', ':');
console.log(filterObject(address, path.split('.')))
.as-console-wrapper {max-height:100% !important; top:0;}

If you just need the first key that fully or partial matches the keys, you could split the path and use reduce like this. If the key is found, return the object, else find the keys which include the given key (This gives the data for the last key match. Not the entire object tree)

const address={country:{name:'Japan',city:{name:'Tokyo',town:{name:'korushawa'}},code:'JP'},nearbyCountry:'Korea'},
    path = "countr.cit";

const output = path.split('.').reduce((obj, key) => {
  if (key in obj) {
    return obj[key];
  } else {
    let found = Object.keys(obj).find(k => k.includes(key));
    if (found)
      return obj[found]
    else
      return {}
  }
}, address);

console.log(output)
adiga
  • 34,372
  • 9
  • 61
  • 83
  • The problem with this approach is you can only have one subtree in output at a time. Ex: for path 'country', it will not give `nearbyCountry:'Korea'` subtree. – asdasd Apr 11 '19 at 05:22
  • OP said: _"For path2, I should get entire address object because countr is **present in** country and nearbyCountry_. `startsWith` will not work neither `includes()`. You will need a case-insensitive `RegExp`. – Maheer Ali Apr 14 '19 at 18:46
4

Since I don't fully understand your goal (for example: why on your first output example do you keep name property of country object but not the code property), I will give you two approachs that I hope can help you.

First approach:

In this first approach we recursively traverse the main object to filter out the keys that don't match at a particular level. The output will be an object with the properties that match at a specific level:

let address = {
  country: {
    name: 'Japan',
    city: {name: 'Tokyo', town: {name: 'korushawa'}},
    code: 'JP'
  },
  nearbyCountry: {name: 'Korea', code: 'KO'}
};

const myFilter = (obj, keys) =>
{
    if (keys.length <= 0)
        return obj;

    return Object.keys(obj).reduce(
        (nObj, k) => k.toLowerCase().match(keys[0])
            ? ({...nObj, [k]: myFilter(obj[k], keys.slice(1))})
            : nObj,
        {}
    );
}

const customFilter = (o, k) => myFilter(
    JSON.parse(JSON.stringify(o)),
    k.split(".").map(x => x.toLowerCase())
);

console.log("Filter by 'countr':", customFilter(address, "countr"));
console.log("Filter by 'countr.cit':", customFilter(address, "countr.cit"));
console.log("Filter by 'countr.cit.to':", customFilter(address, "countr.cit.to"));
console.log("Filter by 'countr.cit.lala':", customFilter(address, "countr.cit.lala"));
.as-console {background-color:black !important; color:lime;}
.as-console-wrapper {max-height:100% !important; top:0;}

As you can see, when filtering by "countr.cit" this approach keeps the key = nearbyCountry even if there isn't an inner key matching with cit inside of it.

Second approach

In this approach we going to filter out all the first-level keys of the main object that don't have a match on all the sections of the provided path. However, I have to say that this approach is a little strange. I believe this will have more sense if your input is an array of objects, not just one object.

let address = {
  country: {
    name: 'Japan',
    city: {name: 'Tokyo', town: {name: 'korushawa'}},
    code: 'JP'
  },
  nearbyCountry: {name: 'Korea', code: 'KO'}
};

const myFilter = (obj, paths) =>
{
    let newObj = {};

    Object.entries(obj).forEach(([key, val]) =>
    {
        let res = paths.slice(1).reduce((o, cp) => 
        {
            if (o === undefined) return o;
            let found = Object.keys(o).find(k => k.toLowerCase().match(cp));
            return found !== undefined ? o[found] : found;
        }, val);

        if (key.toLowerCase().match(paths[0]) && res !== undefined)
            newObj[key] = val;
    });

    return newObj;
}

const customFilter = (o, k) => myFilter(
    JSON.parse(JSON.stringify(o)),
    k.split(".").map(x => x.toLowerCase())
);

console.log("Filter by 'countr':", customFilter(address, "countr"));
console.log("Filter by 'countr.cit':", customFilter(address, "countr.cit"));
console.log("Filter by 'countr.cit.to':", customFilter(address, "countr.cit.to"));
console.log("Filter by 'countr.cit.lala':", customFilter(address, "countr.cit.lala"));
.as-console {background-color:black !important; color:lime;}
.as-console-wrapper {max-height:100% !important; top:0;}

Finally, the other thing you may want to do is already shown on the answer provided by @adiga.

Community
  • 1
  • 1
Shidersz
  • 16,846
  • 2
  • 23
  • 48
4

This approach rely on filtering the entries and rebuilding a new object by mapping objects with a single property.

function getParts(object, fragments) {
    var [part, ...rest] = fragments.split('.');

    return Object.assign({}, ...Object
        .entries(object)
        .filter(([key]) => key.toLowerCase().includes(part))
        .map(([k, v]) => {
            if (!rest.length) return { [k]: v };
            var parts = v && typeof v === 'object' && getParts(v, rest.join('.'));
            if (parts) return { [k]: parts };
        })
    );
}

let address = { country: { name: 'Japan', city: { name: 'Tokyo', town: { name: 'korushawa' } }, code: 'JP' }, nearbyCountry: 'Korea' };

console.log(getParts(address, 'countr.cit'));
console.log(getParts(address, 'countr'));
.as-console-wrapper { max-height: 100% !important; top: 0; }
Nina Scholz
  • 376,160
  • 25
  • 347
  • 392
  • 1
    Thanks. `([key]) => key.toLowerCase().includes(part)` could be `([key]) => key.toLowerCase().includes(part.toLowerCase())`. – asdasd Apr 20 '19 at 12:53