Generating unique values and filtering data object

Viewed 80

I'm trying to filter this data by type of ferment, then for each ferment type, combine the unique values of the use and year as arrays. For the "made" I want to add all the values up by year.

ferments = [
  {"ferment": "cabbage", "use": "sauerkraut", "year": "2018", "made": 2},
  {"ferment": "cabbage", "use": "pickled", "year": "2018", "made": 1},
  {"ferment": "cabbage", "use": "sauerkraut", "year": "2019", "made": 2},
  {"ferment": "cabbage", "use": "pickled", "year": "2019", "made": 1},
  {"ferment": "cabbage", "use": "pickled", "year": "2020", "made": 3},
  {"ferment": "beets", "use": "pickled", "year": "2018", "made": 3},
  {"ferment": "beets", "use": "fermented", "year": "2018", "made": 2},
  {"ferment": "beets", "use": "pickled", "year": "2019", "made": 4},
  {"ferment": "beets", "use": "fermented", "year": "2019", "made": 2},
  {"ferment": "beets", "use": "pickled", "year": "2020", "made": 4},
  {"ferment": "beets", "use": "fermented", "year": "2020", "made": 1}
]

I've tried many variations, and this is the closest I've gotten (without adding up the "made" values). This gives me an array of two objects, but I would like an object so that I can call it like so and get the relevant information for each key: ferments[cabbage]

const groupedByFerment = _(data)
.groupBy("ferment")
.map(d => ({
    // ferment: d[0].ferment,
    use: _(d).flatMap("use").uniq().value(),
    salt: _(d).flatMap("made").uniq().value(),
    year: _(d).flatMap("year").uniq().value(),
}))
.value()

Here's my desired output:

ferments = {
  cabbage: {
    use: [sauerkraut, pickled],
    year: [2018, 2019, 2020],
    made: [3, 3, 3],
   },
  beets: {
    use: [fermented, pickled],
    year: [2018, 2019, 2020],
    made: [5, 6, 5],
    } 
  }
5 Answers

You can do this with a single reduce() call, grouping by ferment, checking for year and incrementing made accordingly, and finally adding use if it doesn't already exist.

const byFerment = ferments.reduce((a, o) => {
    let { use, year, made } = (a[o.ferment] ??= { use: [], year: [], made: [] });
    // check for year, if it exists access the same index in made and increment.
    let yearIndex = year.indexOf(o.year);
    if (yearIndex !== -1) {
      made[yearIndex] += o.made;
    } else {
      year.push(o.year);
      made.push(o.made);
    }
    // add use if it doesn't already exist.
    if (!use.includes(o.use)) use.push(o.use);
    return a;
  }, {});

console.log(byFerment);
<script>
const ferments = [
  { "ferment": "cabbage", "use": "sauerkraut", "year": "2018", "made": 2 },
  { "ferment": "cabbage", "use": "pickled", "year": "2018", "made": 1 },
  { "ferment": "cabbage", "use": "sauerkraut", "year": "2019", "made": 2 },
  { "ferment": "cabbage", "use": "pickled", "year": "2019", "made": 1 },
  { "ferment": "cabbage", "use": "pickled", "year": "2020", "made": 3 },
  { "ferment": "beets", "use": "pickled", "year": "2018", "made": 3 },
  { "ferment": "beets", "use": "fermented", "year": "2018", "made": 2 },
  { "ferment": "beets", "use": "pickled", "year": "2019", "made": 4 },
  { "ferment": "beets", "use": "fermented", "year": "2019", "made": 2 },
  { "ferment": "beets", "use": "pickled", "year": "2020", "made": 4 },
  { "ferment": "beets", "use": "fermented", "year": "2020", "made": 1 }
];
</script>

UPDATE: Implementation without logical nullish assignment (??=)

As noted in the comments the logical nullish assignment operator isn't yet fully supported. To implement the above without it is simply a matter of adding a line to retrieve/assign the property in the accumulator before proceeding.

This can be done either with an explicit check using hasOwnProperty()

a[o.ferment] = a.hasOwnProperty(o.ferment) ? a[o.ferment] : { use: [], year: [], made: [] };

or using short circuit assignment ideally with ?? otherwise ||.

a[o.ferment] = a[o.ferment] || { use: [], year: [], made: [] };

const byFerment = ferments.reduce((a, o) => {
//a[o.ferment] = a.hasOwnProperty(o.ferment) ? a[o.ferment] : { use: [], year: [], made: [] };
    //or
//a[o.ferment] = a[o.ferment] ?? { use: [], year: [], made: [] };
    // or
  a[o.ferment] = a[o.ferment] || { use: [], year: [], made: [] };
  
  let { use, year, made } = a[o.ferment];
  // check for year, if it exists access the same index in made and increment.
  let yearIndex = year.indexOf(o.year);
  if (yearIndex !== -1) {
    made[yearIndex] += o.made;
  } else {
    year.push(o.year);
    made.push(o.made);
  }
  // add use if it doesn't already exist.
  if (!use.includes(o.use)) use.push(o.use);
  return a;
}, {});

console.log(byFerment);
<script>
const ferments = [   {"ferment": "cabbage", "use": "sauerkraut", "year": "2018", "made": 2 },   {"ferment": "cabbage", "use": "pickled", "year": "2018", "made": 1 },   {"ferment": "cabbage", "use": "sauerkraut", "year": "2019", "made": 2 },   {"ferment": "cabbage", "use": "pickled", "year": "2019", "made": 1 },   {"ferment": "cabbage", "use": "pickled", "year": "2020", "made": 3 },   {"ferment": "beets", "use": "pickled", "year": "2018", "made": 3 },   {"ferment": "beets", "use": "fermented", "year": "2018", "made": 2 },   {"ferment": "beets", "use": "pickled", "year": "2019", "made": 4 },   {"ferment": "beets", "use": "fermented", "year": "2019", "made": 2 },   {"ferment": "beets", "use": "pickled", "year": "2020", "made": 4 },   {"ferment": "beets", "use": "fermented", "year": "2020", "made": 1 } ];
</script>


A more robust structure would be to use an object to track years/made so as not to rely on parallel arrays. One could map this as two arrays using Object.values() for made, and Object.keys() for year.

{
  cabbage: {
    use: [ 'sauerkraut', 'pickled' ],
    years: { '2018': 3, '2019': 3, '2020': 3 }
  },
  beets: {
    use: [ 'pickled', 'fermented' ],
    years: { '2018': 5, '2019': 6, '2020': 5 }
  }
}

Which has the added benefit of simplifying the reduce().

ferments.reduce((a, o) => {
  let { use, years } = (a[o.ferment] ??= { use: [], years: {} });
  if (!use.includes(o.use)) use.push(o.use);
  years[o.year] = (years[o.year] ?? 0) + o.made;
  return a;
}, {});

The first step would be to group by the ferment using .reduce. Then, you can iterate over the resulting object using .forOwn to modify the year-made values and get unique use of each item:

const ferments = [
  {"ferment": "cabbage", "use": "sauerkraut", "year": "2018", "made": 2},
  {"ferment": "cabbage", "use": "pickled", "year": "2018", "made": 1},
  {"ferment": "cabbage", "use": "sauerkraut", "year": "2019", "made": 2},
  {"ferment": "cabbage", "use": "pickled", "year": "2019", "made": 1},
  {"ferment": "cabbage", "use": "pickled", "year": "2020", "made": 3},
  {"ferment": "beets", "use": "pickled", "year": "2018", "made": 3},
  {"ferment": "beets", "use": "fermented", "year": "2018", "made": 2},
  {"ferment": "beets", "use": "pickled", "year": "2019", "made": 4},
  {"ferment": "beets", "use": "fermented", "year": "2019", "made": 2},
  {"ferment": "beets", "use": "pickled", "year": "2020", "made": 4},
  {"ferment": "beets", "use": "fermented", "year": "2020", "made": 1}
];

const grouped = _.reduce(ferments, (acc,item) => {
  const { ferment, use, year, made } = item;
  const prev = acc[ferment];
  acc[ferment] = prev
    ? { use:[...prev.use, use], year:[...prev.year, year], made:[...prev.made, made] }
    : { use:[use], year:[year], made:[made] };
  return acc;
}, {});

_.forOwn(grouped, (value,key) => {
  value.use = _.uniq(value.use);
  const { year:yearList, made:madeList } = value;
  const yearMadeMap = _.reduce(yearList, (acc,year,index) => {
     acc[year] = madeList[index] + (acc[year] || 0)
     return acc;
  }, {});
  value.year = _.keys(yearMadeMap);
  value.made = _.values(yearMadeMap);
});

console.log(grouped);
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.20/lodash.min.js" integrity="sha512-90vH1Z83AJY9DmlWa8WkjkV79yfS2n2Oxhsi2dZbIv0nC4E6m5AbH8Nh156kkM7JePmqD6tcZsfad1ueoaovww==" crossorigin="anonymous"></script>

const ferments = [
  { ferment: "cabbage", use: "sauerkraut", year: "2018", made: 2 },
  { ferment: "cabbage", use: "pickled", year: "2018", made: 1 },
  { ferment: "cabbage", use: "sauerkraut", year: "2019", made: 2 },
  { ferment: "cabbage", use: "pickled", year: "2019", made: 1 },
  { ferment: "cabbage", use: "pickled", year: "2020", made: 3 },
  { ferment: "beets", use: "pickled", year: "2018", made: 3 },
  { ferment: "beets", use: "fermented", year: "2018", made: 2 },
  { ferment: "beets", use: "pickled", year: "2019", made: 4 },
  { ferment: "beets", use: "fermented", year: "2019", made: 2 },
  { ferment: "beets", use: "pickled", year: "2020", made: 4 },
  { ferment: "beets", use: "fermented", year: "2020", made: 1 },
];

const data = {};

ferments.forEach((item) => {
  if (data[item.ferment] === undefined) {
    data[item.ferment] = { use: {}, year: {} };
  }
  data[item.ferment]["use"] = {
    ...data[item.ferment]["use"],
    [item.use]: true,
  };
  data[item.ferment]["year"][item.year] =
    data[item.ferment]["year"][item.year] === undefined ? item.made : data[item.ferment]["year"][item.year] + item.made;
});

const result = {};
Object.keys(data).forEach((key) => {
  result[key] = {
    use: Object.keys(data[key].use),
    year: Object.keys(data[key].year),
    made: Object.values(data[key].year),
  };
});

console.log(result);

Group the array's items by the ferment value. Use _.mapValues() to iterate the group. Merge each groups, and add or concat the items according to the merged key. To concat use _.union() which concats and maintains uniqueness. Use _.omit() to remove the ferment key from the merged group object.

const fn = arr => {
  // create groups by the ferment value
  const groups = _.groupBy(arr, 'ferment');
  
  return _.mapValues(groups, 
    group => _.omit( // remove ferment from the merged group
      // merge each group
      _.mergeWith({}, ...group, (o1, o2, key) => {
        // add made values
        if(key === 'made') return (o1 || 0) + o2;
        // concat use or year values while maintaining uniqueness
        if(key === 'use' || key === 'year') return _.union((o1 || []), [o2]);
    }), 'ferment'));
}

const ferments = [{"ferment":"cabbage","use":"sauerkraut","year":"2018","made":2},{"ferment":"cabbage","use":"pickled","year":"2018","made":1},{"ferment":"cabbage","use":"sauerkraut","year":"2019","made":2},{"ferment":"cabbage","use":"pickled","year":"2019","made":1},{"ferment":"cabbage","use":"pickled","year":"2020","made":3},{"ferment":"beets","use":"pickled","year":"2018","made":3},{"ferment":"beets","use":"fermented","year":"2018","made":2},{"ferment":"beets","use":"pickled","year":"2019","made":4},{"ferment":"beets","use":"fermented","year":"2019","made":2},{"ferment":"beets","use":"pickled","year":"2020","made":4},{"ferment":"beets","use":"fermented","year":"2020","made":1}];

const result = fn(ferments);

console.log(result);
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.20/lodash.min.js" integrity="sha512-90vH1Z83AJY9DmlWa8WkjkV79yfS2n2Oxhsi2dZbIv0nC4E6m5AbH8Nh156kkM7JePmqD6tcZsfad1ueoaovww==" crossorigin="anonymous"></script>

const data = [
  {"ferment": "cabbage", "use": "sauerkraut", "year": "2018", "made": 2},
  {"ferment": "cabbage", "use": "pickled", "year": "2018", "made": 1},
  {"ferment": "cabbage", "use": "sauerkraut", "year": "2019", "made": 2},
  {"ferment": "cabbage", "use": "pickled", "year": "2019", "made": 1},
  {"ferment": "cabbage", "use": "pickled", "year": "2020", "made": 3},
  {"ferment": "beets", "use": "pickled", "year": "2018", "made": 3},
  {"ferment": "beets", "use": "fermented", "year": "2018", "made": 2},
  {"ferment": "beets", "use": "pickled", "year": "2019", "made": 4},
  {"ferment": "beets", "use": "fermented", "year": "2019", "made": 2},
  {"ferment": "beets", "use": "pickled", "year": "2020", "made": 4},
  {"ferment": "beets", "use": "fermented", "year": "2020", "made": 1}
];

const groupedByFerment = _(data)
  .groupBy("ferment")
  .mapValues(objectsWithTheSameFerment => ({
    use: _(objectsWithTheSameFerment)
      .flatMap("use")
      .uniq(),
    year: _(objectsWithTheSameFerment)
      .flatMap("year")
      .uniq(),
    made: _(objectsWithTheSameFerment)
      .groupBy("year")
      .map(objectsWithTheSameYear => _(objectsWithTheSameYear).sumBy("made")),
  }));
  
console.log(groupedByFerment);
<script src="https://cdn.jsdelivr.net/npm/lodash@4.17.20/lodash.min.js"></script>

Related