Working with data: arrays, Maps, JSON, dates and money
Most business code takes rows of data, reshapes them and adds them up. This lesson covers the tools for that and the four places where they quietly give wrong answers: sorting, JSON, time zones and decimal money.
By the end you can
- Chain
filter,map,Object.groupBy,reduceandtoSortedinto a per-store summary - Sort numbers correctly, and avoid sorting shared arrays in place
- Say what JSON loses (dates, Maps, BigInt) and when to use a
Mapor aSet - Compute a store's business day in its own time zone on a server that runs in UTC
- Explain why
0.1 + 0.2is not0.3, and calculate tax in integer cents before formatting it as money
The everyday picture
At month end, the accounts team gets a box of paper sale slips from every store. Nobody adds them up in one go. One person removes the voided slips. The next writes each amount in cents on the corner, so nobody has to deal with "$4.55". A third sorts the slips into one tray per store. A fourth adds up each tray, and the last one puts the trays in order, biggest first, for the manager's report.
Each person does one simple job and passes the result on. That is how good data code looks in JavaScript: a pipeline of small steps. And the mistakes in this lesson are the mistakes that team could make: sorting slips by the first digit instead of the amount, writing the wrong date on a slip from 12:30 am, or rounding each slip's tax differently from the till.
Array methods: one job per step
Each array method takes a function and applies it to every item. Learn what shape each one returns, and the pipeline reads itself:
map(fn): a new array with one output per input.filter(fn): a new array with the items wherefnreturned truthy.find(fn): the first matching item, orundefined.findIndexgives its position, or-1.some(fn)/every(fn): a boolean. Is any item a match? Are all items a match?reduce(fn, start): one value built from all the items, such as a total.flatMap(fn): likemap, but each item may produce zero, one or several outputs, which are joined into one flat array.
// methods.js: the everyday array methods on one GRN (goods received note)
const grn = [
{ articleCode: '100234', ordered: 40, received: 40, damaged: 0 },
{ articleCode: '200871', ordered: 10, received: 8, damaged: 1 },
{ articleCode: '310045', ordered: 24, received: 24, damaged: 2 },
];
console.log(grn.map((l) => l.received - l.damaged)); // one output per input
console.log(grn.filter((l) => l.received < l.ordered)); // keep the matches
console.log(grn.find((l) => l.damaged > 0)?.articleCode); // first match, or undefined
console.log(grn.some((l) => l.damaged > 0), grn.every((l) => l.received > 0));
console.log(grn.reduce((sum, l) => sum + l.damaged, 0)); // many values to one
// flatMap: each line can produce zero, one or many outputs
const issues = grn.flatMap((l) => [
...(l.received < l.ordered ? [`${l.articleCode}: short by ${l.ordered - l.received}`] : []),
...(l.damaged > 0 ? [`${l.articleCode}: ${l.damaged} damaged`] : []),
]);
console.log(issues);
console.log([].every((l) => l.received > 0)); // true for an empty list![ 40, 7, 22 ]
[ { articleCode: '200871', ordered: 10, received: 8, damaged: 1 } ]
200871
true true
3
[ '200871: short by 2', '200871: 1 damaged', '310045: 2 damaged' ]
trueTwo traps hide in that output. every on an empty array is true, so "all lines received" is true for a GRN with no lines; check the length too. And always give reduce its start value: [].reduce((sum, l) => sum + l.qty) throws TypeError: Reduce of empty array with no initial value, which is exactly what happens on the first day a new store has no sales.
None of these methods change the original array. That makes them safe to chain and safe on shared data (Objects, arrays and references). sort is different.
Quick check
A store's GRN arrives with zero lines. What does lines.some((l) => l.damaged > 0) return?
some asks "is there at least one match?", and with no items there is none, so it is false. every asks "is there any item that fails?", and with no items there is none either, so it is true. Neither throws; only reduce without a start value does.