Search code examples
javascriptarraysobjectlodash

Filter out value from arrays within multiple objects using javascript and lodash


In theory this sounds reasonably easy, but a certain AI bot keeps giving me incorrect info. I have data that looks like this:

let newData = {
  '2020': { Thing1: ['ABC', '123'], Thing2: ['DEF'] },
  '2020.5': { Thing1: ['ABC', '123', 'XYZ'], Thing2: ['DEF'] },
  '2020.75': { Thing1: ['ABC', '123'], Thing2: ['XYZ'], Thing3: ['AAA'] }
};

All I want to do is remove values from the “Thing” arrays that are the same across all objects and specific array. In the case above remove ‘ABC’ and ‘123’ from the Thing1 arrays. The returned data looking like this:

{
  '2020': { Thing1: [], Thing2: ['DEF'] },
  '2020.5': { Thing1: ['XYZ'], Thing2: ['DEF'] },
  '2020.75': { Thing1: [], Thing2: ['XYZ'], Thing3: ['AAA'] }
}

This was the answer I got from the aforementioned AI bot:

const numObjects = Object.keys(newData).length;

_.forEach(newData, (value, key) => {
    _.forEach(value, (arr, thing) => {
        newData[key][thing] = _.filter(arr, (value) => {
            const count = _.countBy(newData, (obj) => obj[thing] && obj[thing].includes(value));
            return Object.keys(count).length !== numObjects;
        });
    });
});

console.log(newData);

But this just returns everything. I don’t think it’s iterating through the actual values in the Thing arrays.

Any help would be most appreciated.


Solution

  • The author's code mutates the original object.
    First we get counts of values, then we clean the arrays.

    A pure solution by Damzaky is slower (but could be useful if we need to go pure).

    You don't need the lodash here. The ES6+ allows to do the same things. I've added 2 lodash solutions to the benchmark but they are the slowest:

    ` Chrome/121
    ---------------------------------------------------------------------------
    Nenashev's in-place solution            1.00x | x100000 124 127 132 133 136
    Nenashev's pure solution                1.25x | x100000 155 160 161 162 167
    Damzaky's pure solution                 2.23x | x100000 277 281 288 289 290
    in-place lodash solution by 3limin4t0r  3.02x | x100000 374 376 387 394 402
    lodash pure solution by Ori Drori       4.61x | x100000 572 580 589 590 599
    ---------------------------------------------------------------------------
    https://github.com/silentmantra/benchmark `
    

    <script src="https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js"></script>
    <script name="benchmark" data-count="100000">
    
    const getData = () => ({
        '2020': { Thing1: ['ABC', '123'], Thing2: ['DEF'] },
        '2020.5': { Thing1: ['ABC', '123', 'XYZ'], Thing2: ['DEF'] },
        '2020.75': { Thing1: ['ABC', '123'], Thing2: ['XYZ'], Thing3: ['AAA'] }
    });
    
    // @benchmark Nenashev's in-place solution
    {
    const newData = getData();
    
    const values = Object.values(newData);
    
    const mapped = values
        .reduce((map, item) =>
            Object.entries(item).forEach(
                ([key, arr]) => arr.forEach(value => {
                    const values = map[key] ??= {};
                    if (!values[value]) {
                        values[value] = 0;
                    }
                    values[value]++;
                })) || map, {});
    
    values.forEach(item => {
    
        for (const key in item) {
    
            const arr = item[key];
            for (let i = 0; i < arr.length; i++) {
                mapped[key][arr[i]] === values.length &&
                    arr.splice(i--, 1);
            }
    
        }
    
    });
    
    newData;
    
    }
    // @benchmark Nenashev's pure solution
    
    {
        const newData = getData();
    
        const values = Object.values(newData);
    
        const mapped = values
            .reduce((map, item) =>
                Object.entries(item).forEach(
                    ([key, arr]) => arr.forEach(value => {
                        const values = map[key] ??= {};
                        if (!values[value]) {
                            values[value] = 0;
                        }
                        values[value]++;
                    })) || map, {});
    
    
        const result = {};
        for (const dataKey in newData) {
    
            const newItem = result[dataKey] = {};
            item = newData[dataKey];
    
            for (const key in item) {
                newItem[key] = item[key].filter(val=>mapped[key][val] !== values.length);
            }
        }
    
        result;
    
    }
    
    // @benchmark Damzaky's pure solution
    
    {
    const originalData = getData();
    
    const mustRemove = Object.values(originalData).reduce((acc, val) => {
      let newAcc = { ...acc
      }
      Object.entries(val).forEach(([key, value]) => {
        newAcc[key] = key in newAcc ? value.filter(v => newAcc[key].includes(v)) : []
      })
      return newAcc
    })
    
    Object.entries(originalData).reduce((acc, [key, val]) => ({
      ...acc,
      [key]: Object.entries(val).reduce((cacc, [ckey, cval]) => ({
        ...cacc,
        [ckey]: cval.filter(c => !mustRemove[ckey].includes(c))
      }), {})
    }), {})
    }
    
    // @benchmark in-place lodash solution by 3limin4t0r
    {
    let newData = getData();
    
    const rows = Object.values(newData);
    const things = _.uniq(rows.flatMap((row) => (
      Object.keys(row).filter(key => key.match(/^Thing/))
    )));
    
    const intersections = Object.fromEntries(things.map((thing) => (
      [thing, _.intersection(...rows.map(row => row[thing] || []))]
    )));
    
    for (const row of rows) {
      for (const thing of things) {
        if (!(thing in row)) continue; // skip if key not present
        row[thing] = _.difference(row[thing], intersections[thing]);
      }
    }
    
    newData;
    
    }
    
    // @benchmark lodash pure solution by Ori Drori
    {
    
    const { mergeWith, cloneDeep, values, intersection, mapValues,  difference } = _;
    
    const newData = getData();
    
    const toRemove = mergeWith(
      ...cloneDeep(values(newData)), 
      (a, b) => intersection(a, b)
    )
    
    const result = mapValues(
      newData, 
      o => mapValues(o, (v, k) => difference(v, toRemove[k]))
    )
    
    result;
    
    
    }
    
    </script>
    <script src="https://cdn.jsdelivr.net/gh/silentmantra/benchmark/loader.js"></script>