0

I've rewritten this into a simplified form to demonstrate, I have an array of pickers who have an array of time entries, I'm using reduce to summarise time entries by type on the pickers & then a second reduce to show global entries across both pickers.

The first reduce per picker works as expected. The second reduce on global time entries works as expected but somehow changes the entries for the first picker ( Sam ).

Sam & John pick the same amount.

Apples 2h, Peaches 2h, Lemons 1h

Is there a better way to write this? Is there a concept I've failed to understand?

function testBug() {
  // Reducer Function
  function entryReducer(summary, entry) {
    // find an index if the types of fruit are the same
    let index = summary.findIndex((item) => {
      return item.type.id === entry.type.id;
    });

    if (index === -1) {
      summary.push(entry);
    } else {
      summary[index].hours = summary[index].hours + entry.hours;
    }
    return summary;
  }

  let pickers = [
    {
      id: 1,
      identifier: "Sam Smith",
      timeEntries: [
        {
          type: {
            id: 1,
            name: "Apples",
          },
          hours: 1,
        },
        {
          type: {
            id: 2,
            name: "Peaches",
          },
          hours: 1,
        },
        {
          type: {
            id: 3,
            name: "Lemons",
          },
          hours: 1,
        },
        {
          type: {
            id: 1,
            name: "Apples",
          },
          hours: 1,
        },
        {
          type: {
            id: 2,
            name: "Peaches",
          },
          hours: 1,
        },
      ],
    },
    {
      id: 2,
      identifier: "John Snow",
      timeEntries: [
        {
          type: {
            id: 1,
            name: "Apples",
          },
          hours: 1,
        },
        {
          type: {
            id: 2,
            name: "Peaches",
          },
          hours: 1,
        },
        {
          type: {
            id: 3,
            name: "Lemons",
          },
          hours: 1,
        },
        {
          type: {
            id: 1,
            name: "Apples",
          },
          hours: 1,
        },
        {
          type: {
            id: 2,
            name: "Peaches",
          },
          hours: 1,
        },
      ],
    },
  ];

  let pickersSummary = [];
  let timeEntriesSummary = [];

  for (const picker of pickers) {
    if (picker.timeEntries.length > 0) {
      // reduce time entries into an array of similar types
      picker.timeEntries = picker.timeEntries.reduce(entryReducer, []);
      // push to pickers summary arr
      pickersSummary.push(picker);
      // push time entries to a summary array for later reduce
      picker.timeEntries.map((entry) => timeEntriesSummary.push(entry));
    }
  }

  // Reduce time entries for all pickers
  // Sam & John pick the same amount
  // Apples 2h
  // Peaches 2h
  // Lemons 1h

  // **** If I run this Sam's entries are overwritten with the global time entries ***
  timeEntriesSummary = timeEntriesSummary.reduce(entryReducer, []);

  const results = { pickersSummary, timeEntriesSummary };

  console.log(results);
}
testBug();

module.exports = testBug;
Brad C
  • 1
  • 1

2 Answers2

0

Even though with each reducer you pass a new array [], the actual objects contained by these arrays could be shared. This means when you edit one of the objects in array "A", the objects could also change in array "B".

You know how some languages let you pass variables by value or by reference and how this fundamentally changes how values are handled? JavaScript technically uses call-by-sharing. I suggest reading this other answer: Is JavaScript a pass-by-reference or pass-by-value language?

Dylan Landry
  • 1,150
  • 11
  • 27
  • Hmmm.. I have tried writing a separate reducer function for the 2nd reducer with different variable names but still no luck.. surely though once an element in an array is pushed into a different array it is separate in memory? – Brad C Oct 20 '20 at 00:17
0

once an element in an array is pushed into a different array it is separate in memory?

No, it isn't. In JavaScript you will always remember when you made an individual copy of an object (or at least wanted to), because that needs some effort, see What is the most efficient way to deep clone an object in JavaScript? or How do I correctly clone a JavaScript object?

So, just like when you use a=b, push(a) into an array refers the original object. See this example where there is a single object accessible via two variables (x and y), and via both elements of array z. So modifying it as z[1] affects all the others:

let x={a:5};
let y=x;
let z=[x];
z.push(y);
z[1].a=4;
console.log(x);
console.log(y);
console.log(z[0]);
console.log(z[1]);

As your objects are value-like ones and do not have anything what JSON would not support (like member functions), JSON-based cloning can work on them:

function testBug() {
  // Reducer Function
  function entryReducer(summary, entry) {
    // find an index if the types of fruit are the same
    let index = summary.findIndex((item) => {
      return item.type.id === entry.type.id;
    });

    if (index === -1) {
      //summary.push(entry);
      summary.push(JSON.parse(JSON.stringify(entry))); // <--- the only change
    } else {
      summary[index].hours = summary[index].hours + entry.hours;
    }
    return summary;
  }

  let pickers = [
    {id: 1, identifier: "Sam Smith", timeEntries: [
      {type: {id: 1, name: "Apples",}, hours: 1,},
      {type: {id: 2, name: "Peaches",}, hours: 1,},
      {type: {id: 3, name: "Lemons",}, hours: 1,},
      {type: {id: 1, name: "Apples",}, hours: 1,},
      {type: {id: 2, name: "Peaches",}, hours: 1,},],},
    {id: 2, identifier: "John Snow", timeEntries: [
      {type: {id: 1, name: "Apples",}, hours: 1,},
      {type: {id: 2, name: "Peaches",}, hours: 1,},
      {type: {id: 3, name: "Lemons",}, hours: 1,},
      {type: {id: 1, name: "Apples",}, hours: 1,},
      {type: {id: 2, name: "Peaches",}, hours: 1,},],},];

  let pickersSummary = [];
  let timeEntriesSummary = [];

  for (const picker of pickers) {
    if (picker.timeEntries.length > 0) {
      // reduce time entries into an array of similar types
      picker.timeEntries = picker.timeEntries.reduce(entryReducer, []);
      // push to pickers summary arr
      pickersSummary.push(picker);
      // push time entries to a summary array for later reduce
      picker.timeEntries.map((entry) => timeEntriesSummary.push(entry));
    }
  }

  // Reduce time entries for all pickers
  // Sam & John pick the same amount
  // Apples 2h
  // Peaches 2h
  // Lemons 1h

  // **** If I run this Sam's entries are overwritten with the global time entries ***
  timeEntriesSummary = timeEntriesSummary.reduce(entryReducer, []);

  const results = { pickersSummary, timeEntriesSummary };

  console.log(results);
}
testBug();

Now it probably displays what you expected, but in the background it still alters the pickers themselves, you have that picker.timeEntries = ... line running after all. It may be worth mentioning that const something = xy; means that you can not write something = yz; later, something will stick with a given entity. But, if that entity is an object, its internals can still be changed, that happens with picker.timeEntries above (while writing picker = 123; would fail).

tevemadar
  • 12,389
  • 3
  • 21
  • 49