Към съдържанието
wannacode

Методите на масива

Проблем

Отчет в цикъл

Поръчките вече имат и град:

Order
class Order {
constructor(customer, city, total) {
this.customer = customer;
this.city = city;
this.total = total;
}
}
const orders = [
new Order("Мария", "София", 75.5),
new Order("Георги", "Варна", 12.9),
new Order("Елена", "София", 40),
new Order("Димитър", "Пловдив", 120),
new Order("Ана", "Варна", 64),
];

Искаш клиентите с поръчки поне 50 €, подредени по азбучен ред:

const result = [];
for (const o of orders) {
if (o.total >= 50) {
result.push(o.customer);
}
}
result.sort();
console.log(result);
Изход
[ 'Ана', 'Димитър', 'Мария' ]

Осем реда, а въпросът е един. Кодът казва как: направи празен списък, обиколи, провери, добави, подреди. За да разбереш какво прави, трябва да го изпълниш наум. Добавиш ли „само първите трима“ или „без повторения“, цикълът ще расте.

Идеята

Верига от стъпки

Същият отчет, написан като стъпки:

const result = orders
.filter((o) => o.total >= 50)
.map((o) => o.customer)
.toSorted();
console.log(result);
Изход
[ 'Ана', 'Димитър', 'Мария' ]

Чете се отгоре надолу: вземи поръчките, остави тези поне за 50 €, вземи клиента, подреди. Всяка стъпка е метод на масива и приема стрелка от предишния урок:

  • filter(условие) оставя само елементите, за които условието е вярно;
  • map(функция) превръща всеки елемент в друг: от Order прави текст;
  • toSorted() подрежда по азбука, а toSorted(функция за сравнение) по твоето правило.

Всеки метод връща нов масив, а следващият се вика върху него. Затова стъпките се нареждат във верига. Масивът orders не се променя.

По-точно, за любопитните
  • Старият sort подрежда самия масив и връща него. toSorted е по-новият и връща нов масив. Защо разликата е важна, ще видиш в картата „Намери грешката“.
  • Всяка стъпка минава целия масив, преди да започне следващата. Конвейер, по който елементите минават един по един, има при итератора. За него е следващата карта.

Виж го

Какво минава през филтъра

Тук стъпките не са на масива, а на итератора, който дава values(). Итераторът има същите filter и map, но е конвейер: дава стойностите една по една, когато някой ги поиска. toArray() ги събира в масив.

const prices = [12, 75, 40, 130, 58];
const labels = prices
.values()
.filter((p) => p >= 50)
.map((p) => `${p} €`)
.toArray();
console.log(labels);
Стъпка по стъпка, с думи
  1. Пет цени. values() дава итератор: път, по който още нищо не е минало.
  2. 12 не минава p >= 50 и отпада. До map не стига.
  3. 75 минава filter, map го превръща в текста „75 €“, toArray() го събира.
  4. 40 също отпада. Всеки елемент минава целия път, преди да тръгне следващият.
  5. 130 и 58 минават. Масивът prices не се променя.
  6. Резултатът е нов масив с три текста.

Петте цени тръгват една по една. 12 стига до filter и отпада. 75 минава, map го прави на текста „75 €“, и toArray() го събира. После 40 отпада, а 130 и 58 минават.

Всеки елемент изминава целия път, преди да тръгне следващият. И нищо не тръгва, докато toArray() не поиска стойностите.

По-точно, за любопитните

Това се казва мързеливо изпълнение. Методите на итератора само записват какво трябва да се направи. Виж го с отпечатване във всяка стъпка:

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("Итераторът е готов, нищо не е минало през него.");
console.log(stream.toArray());
Изход
filter 12
filter 75
filter 40
map 75
[ 150 ]
Итераторът е готов, нищо не е минало през него.
filter 12
filter 75
map 75
filter 40
[ 150 ]
  • Масивът (първият console.log) прави всичко веднага и стъпка по стъпка: filter минава трите цени, чак после map минава тази, която е останала.
  • Итераторът е готов, преди през него да е минал и един елемент. После map 75 е веднага след filter 75: елементите не чакат всички да минат през filter.
  • Затова от итератора може да се вземат само първите няколко стойности с take, без да се проверяват останалите:
const check = (p) => {
console.log("filter " + p);
return p >= 50;
};
console.log([75, 130, 58, 12, 99].values().filter(check).take(2).toArray());
Изход
filter 75
filter 130
[ 75, 130 ]

За обикновените задачи масивът е достатъчен и се чете по-лесно. Итераторът е за много данни или когато трябват само първите няколко.

Идеята

Сбор, брой, средно

Резултатът може да е число вместо списък. Сборът на четните числа:

const numbers = [1, 2, 3, 4, 5];
const sum = numbers.filter((n) => n % 2 === 0).reduce((total, n) => total + n, 0);
console.log(`Сума на четните числа: ${sum}`);
Изход
Сума на четните числа: 6
reduce(функция, начало) свежда масива до една стойност. Започва от 0, а функцията получава сбора досега и следващия елемент и връща новия сбор.

Същото за поръчките:

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(`Поръчки над 50 €: ${big}`);
console.log(`Оборот: ${revenue.toFixed(2)} €`);
console.log(`Средно: ${average.toFixed(2)} €`);
console.log(`Най-голяма: ${maxTotal.toFixed(2)} €`);
Изход
Поръчки над 50 €: 3
Оборот: 312.40 €
Средно: 62.48 €
Най-голяма: 120.00 €
  • length на филтрирания масив е броят.
  • Средното е сборът, делено на броя. Ако поръчки няма, 0 / 0 е NaN („не е число“), затова проверката orders.length > 0.
  • Math.max(...масив) дава най-голямото число. ... разгъва масива на отделни параметри: Math.max(75.5, 12.9, ...). Ако масивът е празен, резултатът е -Infinity. Какво да правиш с резултат, който може да липсва, е следващата идея.
По-точно, за любопитните

С празен масив:

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

Без начална стойност reduce на празен масив спира с TypeError: Reduce of empty array with no initial value. Затова началната стойност се пише винаги.

Идеята

Подреди, отрежи, без повторения

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(`Маса ${i}`));
Изход
[ 2, 3, 4, 5 ]
[ 'Димитър', 'Мария', 'Ана' ]
Маса 1
Маса 2
Маса 3
  • new Set(ratings) маха повторенията: в множеството всяка стойност е веднъж. [...множество] го превръща обратно в масив.
  • toSorted((a, b) => b.total - a.total) подрежда от най-голямата поръчка, с функция за сравнение от предишния урок.
  • slice(0, 3) взима елементите от номер 0 до номер 3, без него: първите три. Заедно с подреждането това е „топ 3“.
  • range в JavaScript няма. Array.from({ length: 3 }, (_, i) => i + 1) прави масив с 3 елемента, като за всеки номер i вика функцията: 1, 2, 3. _ е името на параметър, който не ни трябва.
По-точно, за любопитните

Set пази стойностите в реда на добавяне, затова без подреждане остава редът на първата поява:

const ratings = [5, 3, 5, 4, 2, 4, 5];
console.log([...new Set(ratings)]);
Изход
[ 5, 3, 4, 2 ]

Пробвай

Кое стига до края?

Какво ще отпечата програмата?

const drinks = ["кафе", "чай", "кафе", "вода", "чай", "сок"];
const result = [...new Set(drinks)]
.toSorted()
.slice(0, 3)
.map((d) => d.toUpperCase());
console.log(result);
Отговор

Идеята

undefined: резултат, който може да липсва

Коя е най-голямата поръчка? Ако масивът е празен, отговор няма. Тогава резултатът е undefined: „няма стойност“. Но викащият трябва да помни да провери. Забрави ли, програмата спира с TypeError някъде по-късно.

Java за това има Optional: кутия, в която има стойност или е празна. В JavaScript такава кутия няма, но има два оператора, с които проверката е кратка:

const biggest = orders.toSorted((a, b) => b.total - a.total)[0];
if (biggest !== undefined) {
console.log(`Най-голяма: ${biggest.customer}`);
}
const huge = orders.find((o) => o.total > 500);
console.log(huge !== undefined);
console.log(huge?.customer ?? "няма такава");
Изход
Най-голяма: Димитър
false
няма такава
  • [0] на празен масив е undefined.
  • find(условие) дава първия елемент, за който условието е вярно, или undefined.
  • huge?.customer: ако huge е undefined или null, целият израз е undefined, вместо програмата да спре. Това се казва незадължителна верига (optional chaining).
  • x ?? y дава y, ако x е undefined или null, иначе x.

Твоите функции обикновено връщат null, когато нямат отговор: „нарочно няма“. ?? работи и с него:

function nickname(user) {
// Връща прякора или null, ако човекът няма.
return user === "maria" ? "Мими" : null;
}
const maria = nickname("maria");
if (maria !== null) {
console.log(`Прякор: ${maria}`);
}
const georgi = nickname("georgi");
console.log(georgi ?? "без прякор");
Изход
Прякор: Мими
без прякор
По-точно, за любопитните
  • Без ?.:
const huge = orders.find((o) => o.total > 500);
console.log(huge.customer);
Грешка
TypeError: Cannot read properties of undefined (reading 'customer')
  • x || y също дава резерва, но смята за липса и празния текст, и 0. ?? не ги смята (третият ред е празен текст):
console.log(null ?? "без прякор");
console.log("" || "без прякор");
console.log("" ?? "без прякор");
console.log(0 ?? 5);
Изход
без прякор
без прякор
0
  • some и every отговарят с true или false дали някой или всички елементи отговарят на условието:
console.log(orders.some((o) => o.total > 100));
console.log(orders.every((o) => o.total >= 10));
Изход
true
true

Пробвай

Намери грешката

Програмата трябва да отпечата най-скъпата цена и първата въведена цена, 12. Втората излиза грешна. На кой ред е грешката?

Избери реда с грешката.

Идеята

Групиране и съединяване

Колко поръчки има от всеки град и какъв е оборотът?

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);
Изход
{ 'Варна': 2, 'Пловдив': 1, 'София': 2 }
Варна: 76.90 €
Пловдив: 120.00 €
София: 115.50 €
Ана, Георги, Димитър, Елена, Мария
  • Map.groupBy(масив, функция) дава Map: за всеки град масив с поръчките му.
  • [...byCity.keys()] прави масив от ключовете, а toSorted() ги подрежда по азбука.
  • Object.fromEntries(двойки) прави обект от двойки [ключ, стойност]. Тук ключът е градът, а стойността е броят на поръчките му (length).
  • join(", ") съединява текстовете в един, със запетая между тях.

Map пази ключовете в реда на добавяне: София, Варна, Пловдив. Затова градовете се подреждат изрично.

По-точно, за любопитните
  • За всеки град масив с имената. Тук градовете са в реда на добавяне:
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);
Изход
Map(3) {
'София' => [ 'Мария', 'Елена' ],
'Варна' => [ 'Георги', 'Ана' ],
'Пловдив' => [ 'Димитър' ]
}
  • Ако в двойките за Object.fromEntries ключ се повтори, последната стойност печели, без грешка (в Java toMap хвърля изключение):
const lastByCity = Object.fromEntries(orders.map((o) => [o.city, o.customer]));
console.log(lastByCity);
Изход
{ 'София': 'Елена', 'Варна': 'Ана', 'Пловдив': 'Димитър' }

Упражнение

Отчети с методите на масива

Реши задачите в редактора си. Входът се чете с ask, както досега.

1. Продажби по категория. Клас Sale с продукт, категория, брой и единична цена и метод revenue() (брой по цена). Групирай по категория с Map.groupBy, сумирай всяка група с reduce, отпечатай категориите по азбука и общия оборот. Продажбите: кафе на зърна, напитки, 12 бр. по 18,50 €; чай, напитки, 30 по 4,20 €; шоколад, сладки, 40 по 2,80 €; бисквити, сладки, 25 по 3,10 €; мед, храни, 8 по 12 €; пак кафе на зърна, 5 по 18,50 €.

Изход
Напитки: 440.50 €
Сладки: 189.50 €
Храни: 96.00 €
Общо: 726.00 €

2. Топ 3 продукта. Със същите продажби намери трите продукта с най-голям оборот. Кафето е в две продажби, затова първо групирай по продукт и сумирай, после подреди записите на речника по стойност. Подсказка: [...byProduct].toSorted((a, b) => b[1] - a[1]).slice(0, 3): всеки елемент на Map е двойка [продукт, оборот].

Изход
Кафе на зърна: 314.50 €
Чай: 126.00 €
Шоколад: 112.00 €

3. Средна оценка. Прочети един ред с оценки, разделени с интервал. Отпечатай средната, най-високата и колко са отличните (5,50 и нагоре). Подсказка: (await ask("")).split(" ").map(Number).

Вход
4.50 5.75 6.00 3.00 5.50
Изход
Средно: 4.95
Най-висока: 6.00
Отлични: 3

4. Думите в текст. Прочети ред текст и отпечатай думите в него без повторения, по азбучен ред, с малки букви, разделени със запетая. Раздели по всичко, което не е буква: split(/[^\p{L}]+/u), и махни празните низове.

Вход
Кафе с мляко, кафе без захар. Чай с мед!
Изход
без, захар, кафе, мед, мляко, с, чай

Още задачи

  • Най-дългата дума. С reduce намери най-дългата дума в изречение. Какво ще стане за празен ред без начална стойност?
  • Средно по клас. Ученици с име, клас и оценка: групирай ги с Map.groupBy и отпечатай средната оценка за всеки клас.
  • Големи и малки поръчки. С some провери дали има поръчка над 1000 €, а с every дали всички са поне 10 €.

Тест

Провери се

1. Какво става с масива orders след orders.filter((o) => o.total > 50)?
2. Кой от тези методи променя самия масив?
3. orders.find((o) => o.total > 500) не намира поръчка. Какво връща?
4. Какво дава huge?.customer ?? "няма такава", ако huge е undefined?
5. orders.reduce((sum, o) => sum + o.total, 0). Какво е 0?

Създаден от Иво Гергов като част от кампанията на Evo Tech за достъпно обучение по програмиране ·