Senyaak
Senyaak

Reputation: 55

Lodash pick from deep array

I have an complex Object

{
  "a": 1,
  "b": {"test": {
    "b1": 'b1'
  }},
  "c": {
    "d": [{foo: 1}, {foo: 2}, {foo: 3, bar: 1}, {bar: 12}]
  },
}

And I have list of keys:

[
  "a", 
  "b.test.b1",
  "c.d[].foo"
]

What I want to do - is to pick all values I have keys for. The problem is - I do not sure how to handle arrays ("c.d[].foo"). I do not know how long the array is and which elements do or do not have foo

The result should be

{
  "a": 1,
  "b": {"test": {
    "b1": 'b1'
  }},
  "c": {
    "d": [{foo: 1}, {foo: 2}, {foo: 3}]
  },
}

UPD If someone interested, here is my implementation of this function:

const deepPick = (input, paths) => {

    return paths.reduce((result, path) => {
      if(path.indexOf('[]') !== -1) {
        if(path.match(/\[\]/g).length !== 1) {
          throw new Error(`Multiplie [] is not supported!`);
        }
        const [head, tail] = path.split('[]');

        const array = (get(input, head) || []).reduce((result, item) => {
          // if tail is an empty string, we have to return the head value;
          if(tail === '') {
            return get(input, head);
          }
          const value = get(item, tail);

          if(!isNil(value)) {
            result.push(set({} , tail, value));
          } else {
            result.push(undefined);
          }
          return result;
        }, []);

        const existingArray = get(result, head);

        if((existingArray || []).length > 0) {
          existingArray.forEach((_, i) => {
            if(!isNil(get(array[i], tail))) {
              set(existingArray, `${i}.${tail}`, get(array[i], tail));
            }
          });
        } else if(array.length > 0) {
          set(result, head, array);
        }
      } else {
        set(result, path, get(input, path));
      }
      return result;
    }, {});
}

and here a sandbox to play with

Upvotes: 2

Views: 931

Answers (3)

th3n3wguy
th3n3wguy

Reputation: 3737

I updated this answer to include a special function that I wrote that solves the problem. I haven't tested it against every possible scenario, but I know with 100% certainty that it runs for your cases.

_.mixin({
  "pickSpecial": function pickSpecial(obj, key) {
    if (!_.includes(key, '[]')) {
      return _.pick(obj, key);
    }
    else {
        const keys = _.split(key, /(\[\]\.|\[\])/);
        const pickFromArray = _.chain(obj)
            .get(_.first(keys))
            .map((nextArrayElement) => pickSpecial(nextArrayElement, _.reduce(_.slice(keys, 2), (curr, next) => `${curr}${next}`, '')))
            .compact()
            .reject((elem) => (_.isObject(elem) || _.isArray(elem)) && _.isEmpty(elem))
            .value();

        return _.chain(obj)
            .pick(_.first(keys))
            .set(_.first(keys), pickFromArray)
            .value();
    }
  }
});

const initialData = {
  "a": 1,
  "b": {"test": {
    "b1": 'b1'
  }},
  "c": {
    "d": [{foo: 1}, {foo: 2}, {foo: 3, bar: 1}, {bar: 12}]
  },
};

const keys = [
  "a", 
  "b.test.b1",
  "c.d[].foo"
];

/* Expected result
{
  "a": 1,
  "b": {"test": {
    "b1": 'b1'
  }},
  "c": {
    "d": [{foo: 1}, {foo: 2}, {foo: 3}]
  },
}
*/

const output = _.chain(keys)
  .map((key) => _.pickSpecial(initialData, key))
  .reduce((obj, next) => _.merge({}, obj, next), {})
  .value();
  
console.log(output);
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.15/lodash.min.js"></script>

Upvotes: 0

Ashish Modi
Ashish Modi

Reputation: 7770

map-factory might help to do this task in elegant way. see here for more details: https://www.npmjs.com/package/map-factory

code will looks like this

const mapper = require("map-factory")();
mapper
  .map("a")
  .map("b.test.b1")
  .map("c.d[].foo");

const input = {
  a: 1,
  b: {
    test: {
      b1: "b1"
    }
  },
  c: {
    d: [{ foo: 1 }, { foo: 2 }, { foo: 3, bar: 1 }, { bar: 12 }]
  }
};

console.log(JSON.stringify(mapper.execute(input)));

Upvotes: 3

Orelsanpls
Orelsanpls

Reputation: 23515

Loadash alternative

Idk about loadash, but I would simply remove the [] from your string keys and use a simple function to retrieve what you are looking for.

const obj = {
  a: 1,
  b: {
    test: {
      b1: 'b1',
    },
  },
  c: {
    d: [{
      foo: 1,
    }, {
      foo: 2,
    }, {
      foo: 3,
      bar: 1,
    }, {
      bar: 12,
    }],
  },
};

const myKeys = [
  'a',
  'b.test.b1',
  'c.d[].foo',
].map(x => x.replace(/\[\]/, ''));

function deepSearch(key, obj) {
  // We split the keys so we can loop on them
  const splittedKeys = key.split('.');

  return splittedKeys.reduce((tmp, x, xi) => {
    if (tmp === void 0) {
      return tmp;
    }

    if (tmp instanceof Array) {
      const dataIndex = tmp.findIndex(y => y[x] !== void 0);

      // If the data we are looking for doesn't exists
      if (dataIndex === -1) {
        return void 0;
      }

      const data = tmp[dataIndex];
      const ptr = data[x];

      // Remove the data only if it's the last key we were looking for
      if (splittedKeys.length === xi + 1) {
        delete data[x];

        // If the array element we removed the key from is now empty
        // remove it
        if (Object.keys(data).length === 0) {
          tmp.splice(dataIndex, 1);
        }
      }

      return ptr;
    }

    const ptr = tmp[x];

    // Remove the data only if it's the last key we were looking for
    if (splittedKeys.length === xi + 1) {
      delete tmp[x];
    }

    return ptr;
  }, obj);
}

console.log('Results', myKeys.map(x => deepSearch(x, obj)));

console.log('Final object', obj);

Upvotes: 0

Related Questions