skip to Main Content

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.

5

Answers


  1. 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:

    enter image description here

    <script src="https://cdn.jsdelivr.net/npm/[email protected]/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>
    Login or Signup to reply.
  2. Not sure if this is the most efficient way, but I would get the intersections of every "Thing" first, and then remove them from the "Thing"s:

    const originalData = {
      '2020': {
        Thing1: ['ABC', '123'],
        Thing2: ['DEF']
      },
      '2020.5': {
        Thing1: ['ABC', '123', 'XYZ'],
        Thing2: ['DEF']
      },
      '2020.75': {
        Thing1: ['ABC', '123'],
        Thing2: ['XYZ'],
        Thing3: ['AAA']
      }
    };
    
    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
    })
    
    const result = 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))
      }), {})
    }), {})
    
    console.log(result)
    Login or Signup to reply.
  3. If you wish to replace the data of newData, without a new object, a nested for..of Object.keys() and a filter() would be enough:

    const toRemove = [ 'ABC', '123' ];
    
    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'] }
    };
    
    for (var o of Object.keys(newData)) {
        for (var key of Object.keys(newData[o])) {
            newData[o][key] = newData[o][key].filter(e => !toRemove.includes(e));
        }
    }
    
    console.log(newData)
    Login or Signup to reply.
  4. Here is a solution using mainly normal JavaScript. I do use _.uniq, _.intersection and _.difference since they don’t have a simple JavaScipt equivalent.

    This solution works in a few steps.

    1. Collect all the _.uniq "Thing" keys.
    2. Store an _.intersection of each "Thing" key.
    3. Replace the "Thing" collection of each row with a new one. The new value is produced by taking the _.difference between the current value and the stored intersection.
    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'] }
    };
    
    // we don't care aboute the `newData` labels, so take only the values
    const rows = Object.values(newData);
    // collect the different `Thing` keys
    const things = _.uniq(rows.flatMap((row) => (
      Object.keys(row).filter(key => key.match(/^Thing/))
    )));
    
    // build an intersection array for each "Thing", use an empty array if the key is missing
    const intersections = Object.fromEntries(things.map((thing) => (
      [thing, _.intersection(...rows.map(row => row[thing] || []))]
    )));
    console.log("intersections =", intersections);
    
    // subtract the intersection from the row value (difference)
    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]);
      }
    }
    
    console.log("mutated newData =", newData);
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/lodash.min.js"></script>

    Note that this answer mutates the original structure.

    Login or Signup to reply.
    1. Create an object (toRemove) that contains, for each property, all the values that are common between the original objects.
    2. Map the original object’s values, and then map all the propertoes inside each object. Get the difference between each array, and it’s toRemove counterpart property.
    const { mergeWith, cloneDeep, values, intersection, mapValues,  difference } = _;
    
    const newData = {"2020":{"Thing1":["ABC","123"],"Thing2":["DEF"]},"2020.5":{"Thing1":["ABC","123","XYZ"],"Thing2":["DEF"]},"2020.75":{"Thing1":["ABC","123"],"Thing2":["XYZ"],"Thing3":["AAA"]}};
    
    const toRemove = mergeWith(
      ...cloneDeep(values(newData)), 
      (a, b) => intersection(a, b)
    )
    
    const result = mapValues(
      newData, 
      o => mapValues(o, (v, k) => difference(v, toRemove[k]))
    )
    
    console.log(result)
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/lodash.min.js"></script>
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search