Skip to content
wannacode

Array methods

Problem

A report in a loop

Orders now have a city too:

Order
class Order {
constructor(customer, city, total) {
this.customer = customer;
this.city = city;
this.total = total;
}
}
const orders = [
new Order("Maria", "Sofia", 75.5),
new Order("Georgi", "Varna", 12.9),
new Order("Elena", "Sofia", 40),
new Order("Dimitar", "Plovdiv", 120),
new Order("Ana", "Varna", 64),
];

You want the customers with orders of at least €50, in alphabetical order:

const result = [];
for (const o of orders) {
if (o.total >= 50) {
result.push(o.customer);
}
}
result.sort();
console.log(result);
Output
[ 'Ana', 'Dimitar', 'Maria' ]

Eight lines, and the question is just one. The code says how: make an empty list, loop, check, add, sort. To understand what it does, you have to run it in your head. Add "only the first three" or "no duplicates", and the loop will grow.

The idea

A chain of steps

The same report, written as steps:

const result = orders
.filter((o) => o.total >= 50)
.map((o) => o.customer)
.toSorted();
console.log(result);
Output
[ 'Ana', 'Dimitar', 'Maria' ]

You read it from top to bottom: take the orders, keep the ones of at least €50, take the customer, sort. Each step is an array method and takes an arrow function from the previous lesson:

  • filter(condition) keeps only the elements for which the condition is true;
  • map(function) turns each element into another one: it makes a string from an Order;
  • toSorted() sorts alphabetically, and toSorted(compare function) by your rule.

Each method returns a new array, and the next one is called on it. That is why the steps line up in a chain. The array orders does not change.

More precisely, if you are curious
  • The old sort sorts the array itself and returns it. toSorted is the newer one and returns a new array. You will see why the difference matters in the card "Find the bug".
  • Each step goes through the whole array before the next one starts. A conveyor belt that the elements go along one at a time is what an iterator gives you. The next card is about it.

See it

What gets through the filter

Here the steps are not on the array but on the iterator that values() gives. The iterator has the same filter and map, but it is a conveyor belt: it gives the values one at a time, when something asks for them. toArray() collects them into an array.

const prices = [12, 75, 40, 130, 58];
const labels = prices
.values()
.filter((p) => p >= 50)
.map((p) => `€${p}`)
.toArray();
console.log(labels);
Step by step, in words
  1. Five prices. values() gives an iterator: a path that nothing has gone through yet.
  2. 12 does not pass p >= 50 and drops out. It never gets to map.
  3. 75 passes filter, map turns it into the text "€75", toArray() collects it.
  4. 40 drops out too. Each element goes the whole way before the next one starts.
  5. 130 and 58 pass. The array prices does not change.
  6. The result is a new array with three strings.

The five prices go one at a time. 12 gets to filter and drops out. 75 passes, map turns it into the text "€75", and toArray() collects it. Then 40 drops out, and 130 and 58 pass.

Each element goes the whole way before the next one starts. And nothing starts until toArray() asks for the values.

More precisely, if you are curious

This is called lazy evaluation. The iterator methods only record what needs to be done. See it with a print in every step:

const check = (p) => {
console.log("filter " + p);
return p >= 50;
};
const double = (p) => {
console.log("map " + p);
return p * 2;
};
console.log([12, 75, 40].filter(check).map(double));
const stream = [12, 75, 40].values().filter(check).map(double);
console.log("The iterator is ready, nothing has gone through it yet.");
console.log(stream.toArray());
Output
filter 12
filter 75
filter 40
map 75
[ 150 ]
The iterator is ready, nothing has gone through it yet.
filter 12
filter 75
map 75
filter 40
[ 150 ]
  • The array (the first console.log) does everything right away and step by step: filter goes through all three prices, and only then does map go through the one that is left.
  • The iterator is ready before a single element has gone through it. Then map 75 comes right after filter 75: the elements do not wait for all of them to go through filter.
  • That is why you can take just the first few values from the iterator with take, without checking the rest:
const check = (p) => {
console.log("filter " + p);
return p >= 50;
};
console.log([75, 130, 58, 12, 99].values().filter(check).take(2).toArray());
Output
filter 75
filter 130
[ 75, 130 ]

For everyday tasks the array is enough and is easier to read. The iterator is for a lot of data, or when you need only the first few.

The idea

Sum, count, average

The result can be a number instead of a list. The sum of the even numbers:

const numbers = [1, 2, 3, 4, 5];
const sum = numbers.filter((n) => n % 2 === 0).reduce((total, n) => total + n, 0);
console.log(`Sum of the even numbers: ${sum}`);
Output
Sum of the even numbers: 6
reduce(function, start) reduces the array to one value. It starts from 0, and the function gets the sum so far and the next element and returns the new sum.

The same for the orders:

const big = orders.filter((o) => o.total >= 50).length;
const revenue = orders.reduce((sum, o) => sum + o.total, 0);
const average = orders.length > 0 ? revenue / orders.length : 0;
const maxTotal = Math.max(...orders.map((o) => o.total));
console.log(`Orders over €50: ${big}`);
console.log(`Revenue: €${revenue.toFixed(2)}`);
console.log(`Average: €${average.toFixed(2)}`);
console.log(`Largest: €${maxTotal.toFixed(2)}`);
Output
Orders over €50: 3
Revenue: €312.40
Average: €62.48
Largest: €120.00
  • The length of the filtered array is the count.
  • The average is the sum divided by the count. If there are no orders, 0 / 0 is NaN ("not a number"), hence the check orders.length > 0.
  • Math.max(...array) gives the largest number. ... spreads the array into separate parameters: Math.max(75.5, 12.9, ...). If the array is empty, the result is -Infinity. What to do with a result that may be missing is the next idea.
More precisely, if you are curious

With an empty array:

const prices = [];
console.log(prices.reduce((sum, p) => sum + p, 0));
console.log(Math.max(...prices));
console.log(0 / prices.length);
Output
0
-Infinity
NaN

Without a starting value, reduce on an empty array stops with TypeError: Reduce of empty array with no initial value. So always write the starting value.

The idea

Sort, cut, no duplicates

const ratings = [5, 3, 5, 4, 2, 4, 5];
console.log([...new Set(ratings)].toSorted((a, b) => a - b));
const top = orders
.toSorted((a, b) => b.total - a.total)
.slice(0, 3)
.map((o) => o.customer);
console.log(top);
Array.from({ length: 3 }, (_, i) => i + 1).forEach((i) => console.log(`Table ${i}`));
Output
[ 2, 3, 4, 5 ]
[ 'Dimitar', 'Maria', 'Ana' ]
Table 1
Table 2
Table 3
  • new Set(ratings) removes the duplicates: in a set each value is there once. [...set] turns it back into an array.
  • toSorted((a, b) => b.total - a.total) sorts from the largest order, with a compare function from the previous lesson.
  • slice(0, 3) takes the elements from index 0 up to index 3, without it: the first three. Together with the sort, that is a "top 3".
  • JavaScript has no range. Array.from({ length: 3 }, (_, i) => i + 1) makes an array with 3 elements, calling the function for each index i: 1, 2, 3. _ is the name of a parameter we do not need.
More precisely, if you are curious

A Set keeps the values in the order they were added, so without sorting you get the order of first appearance:

const ratings = [5, 3, 5, 4, 2, 4, 5];
console.log([...new Set(ratings)]);
Output
[ 5, 3, 4, 2 ]

Try it

What gets to the end?

What will the program print?

const drinks = ["coffee", "tea", "coffee", "water", "tea", "juice"];
const result = [...new Set(drinks)]
.toSorted()
.slice(0, 3)
.map((d) => d.toUpperCase());
console.log(result);
Answer

The idea

undefined: a result that may be missing

Which order is the largest? If the array is empty, there is no answer. Then the result is undefined: "no value". But the caller has to remember to check. If they forget, the program stops with a TypeError somewhere later.

Java has Optional for this: a box that either holds a value or is empty. JavaScript has no such box, but it has two operators that make the check short:

const biggest = orders.toSorted((a, b) => b.total - a.total)[0];
if (biggest !== undefined) {
console.log(`Largest: ${biggest.customer}`);
}
const huge = orders.find((o) => o.total > 500);
console.log(huge !== undefined);
console.log(huge?.customer ?? "no such order");
Output
Largest: Dimitar
false
no such order
  • [0] of an empty array is undefined.
  • find(condition) gives the first element for which the condition is true, or undefined.
  • huge?.customer: if huge is undefined or null, the whole expression is undefined, instead of the program stopping. This is called optional chaining.
  • x ?? y gives y if x is undefined or null, and x otherwise.

Your own functions usually return null when they have no answer: "missing on purpose". ?? works with it too:

function nickname(user) {
// Returns the nickname, or null if the person has none.
return user === "maria" ? "Mimi" : null;
}
const maria = nickname("maria");
if (maria !== null) {
console.log(`Nickname: ${maria}`);
}
const georgi = nickname("georgi");
console.log(georgi ?? "no nickname");
Output
Nickname: Mimi
no nickname
More precisely, if you are curious
  • Without ?.:
const huge = orders.find((o) => o.total > 500);
console.log(huge.customer);
Error
TypeError: Cannot read properties of undefined (reading 'customer')
  • x || y also gives a fallback, but it treats an empty string and 0 as missing too. ?? does not (the third line is an empty string):
console.log(null ?? "no nickname");
console.log("" || "no nickname");
console.log("" ?? "no nickname");
console.log(0 ?? 5);
Output
no nickname
no nickname
0
  • some and every answer with true or false whether any or all of the elements match the condition:
console.log(orders.some((o) => o.total > 100));
console.log(orders.every((o) => o.total >= 10));
Output
true
true

Try it

Find the bug

The program should print the most expensive price and the first price entered, 12. The second one comes out wrong. Which line is the bug on?

Pick the line with the bug.

The idea

Grouping and joining

How many orders are there from each city, and what is the revenue?

const byCity = Map.groupBy(orders, (o) => o.city);
const cities = [...byCity.keys()].toSorted();
const countByCity = Object.fromEntries(cities.map((city) => [city, byCity.get(city).length]));
console.log(countByCity);
for (const city of cities) {
const sum = byCity.get(city).reduce((total, o) => total + o.total, 0);
console.log(`${city}: €${sum.toFixed(2)}`);
}
const customers = orders
.map((o) => o.customer)
.toSorted()
.join(", ");
console.log(customers);
Output
{ Plovdiv: 1, Sofia: 2, Varna: 2 }
Plovdiv: €120.00
Sofia: €115.50
Varna: €76.90
Ana, Dimitar, Elena, Georgi, Maria
  • Map.groupBy(array, function) gives a Map: for each city, an array of its orders.
  • [...byCity.keys()] makes an array of the keys, and toSorted() sorts them alphabetically.
  • Object.fromEntries(pairs) makes an object from [key, value] pairs. Here the key is the city, and the value is the number of its orders (length).
  • join(", ") joins the strings into one, with a comma between them.

A Map keeps the keys in the order they were added: Sofia, Varna, Plovdiv. That is why the cities are sorted explicitly.

More precisely, if you are curious
  • For each city, an array of the names. Here the cities are in the order they were added:
const namesByCity = new Map();
for (const [city, list] of Map.groupBy(orders, (o) => o.city)) {
namesByCity.set(city, list.map((o) => o.customer));
}
console.log(namesByCity);
Output
Map(3) {
'Sofia' => [ 'Maria', 'Elena' ],
'Varna' => [ 'Georgi', 'Ana' ],
'Plovdiv' => [ 'Dimitar' ]
}
  • If a key repeats in the pairs for Object.fromEntries, the last value wins, with no error (in Java toMap throws an exception):
const lastByCity = Object.fromEntries(orders.map((o) => [o.city, o.customer]));
console.log(lastByCity);
Output
{ Sofia: 'Elena', Varna: 'Ana', Plovdiv: 'Dimitar' }

Exercise

Reports with array methods

Solve the tasks in your editor. Read the input with ask, as before.

1. Sales by category. A class Sale with a product, a category, a quantity and a unit price, and a method revenue() (quantity times price). Group by category with Map.groupBy, add up each group with reduce, print the categories alphabetically and the total revenue. The sales: coffee beans, drinks, 12 at €18.50; tea, drinks, 30 at €4.20; chocolate, sweets, 40 at €2.80; biscuits, sweets, 25 at €3.10; honey, food, 8 at €12; coffee beans again, 5 at €18.50.

Output
Drinks: €440.50
Food: €96.00
Sweets: €189.50
Total: €726.00

2. Top 3 products. With the same sales, find the three products with the highest revenue. The coffee is in two sales, so first group by product and add up, then sort the map's entries by value. Hint: [...byProduct].toSorted((a, b) => b[1] - a[1]).slice(0, 3): each element of a Map is a pair [product, revenue].

Output
Coffee beans: €314.50
Tea: €126.00
Chocolate: €112.00

3. Average grade. Read one line of grades separated by spaces. Print the average, the highest and how many are excellent (5.50 and up). Hint: (await ask("")).split(" ").map(Number).

Input
4.50 5.75 6.00 3.00 5.50
Output
Average: 4.95
Highest: 6.00
Excellent: 3

4. The words in a text. Read a line of text and print the words in it with no duplicates, in alphabetical order, in lowercase, separated by commas. Split on everything that is not a letter: split(/[^\p{L}]+/u), and remove the empty strings.

Input
Coffee with milk, coffee without sugar. Tea with honey!
Output
coffee, honey, milk, sugar, tea, with, without

More tasks

  • The longest word. With reduce, find the longest word in a sentence. What will happen for an empty line without a starting value?
  • Average by class. Students with a name, a class and a grade: group them with Map.groupBy and print the average grade for each class.
  • Big and small orders. With some, check whether there is an order over €1000, and with every whether all of them are at least €10.

Test

Check yourself

1. What happens to the array orders after orders.filter((o) => o.total > 50)?
2. Which of these methods changes the array itself?
3. orders.find((o) => o.total > 500) does not find an order. What does it return?
4. What does huge?.customer ?? "no such order" give if huge is undefined?
5. orders.reduce((sum, o) => sum + o.total, 0). What is the 0?

Made by Ivo Gergov as part of Evo Tech's campaign for accessible programming education ·