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

Текстът отблизо

Проблем

Нов текст при всяко +=

Вече знаеш, че title.toUpperCase(); сам не прави нищо: текстът е неизменим и методът връща нов текст. Това има цена, която се вижда в цикъл.

Програмата на онлайн магазина сглобява дневния отчет ред по ред:

let report = "";
for (let i = 1; i <= 3; i++) {
report += `Поръчка ${i}: 12.50 €\n`;
}
process.stdout.write(report);
Изход
Поръчка 1: 12.50 €
Поръчка 2: 12.50 €
Поръчка 3: 12.50 €

process.stdout.write печата текста, без да добавя нов ред накрая: отчетът вече завършва с \n.

По правилата на езика всяко report += ... не добавя към стария текст, а прави нов текст: целия отчет досега и новия ред. Ако машината наистина копираше всичко при всяко завъртане, към края на 50 000 поръчки всяко завъртане би копирало над милион символа. Така става в Java (около 2,5 секунди) и в Python (около 6,5 секунди).

В Node същият цикъл с 50 000 поръчки отнема около 8 милисекунди. Измерихме го на машината, на която проверяваме кода на уроците. V8, машината, която изпълнява JavaScript, хитрува: при += не копира, а запомня двете парчета едно до друго и ги слепва наведнъж, когато текстът ти потрябва. Това е хитрина на машината, а не правило на езика. Текстът пак е неизменим.

Идеята

Масив от парчета и join

В JavaScript няма текст, който се променя на място. Затова парчетата се събират в масив, а той се променя на място. Накрая join ги слепва в един текст, като копира всеки символ само веднъж:

const parts = [];
for (let i = 1; i <= 3; i++) {
parts.push(`Поръчка ${i}: 12.50 €`);
}
const report = parts.join("\n");
console.log(report);
Изход
Поръчка 1: 12.50 €
Поръчка 2: 12.50 €
Поръчка 3: 12.50 €
  • const parts = [] е празен масив. push добавя парче накрая, без да пипа другите.
  • parts.join("\n") прави един текст от всички парчета, с разделителя "\n" между тях. За слепване без разделител пишеш parts.join("").
  • Новият ред между парчетата идва от разделителя, затова тук парчетата нямат \n накрая.

В Node и двата начина са бързи: със 50 000 поръчки join отнема около 5 милисекунди, а += около 8. Масивът и join са обичайният начин, когато парчетата са много или трябва разделител между тях, и не зависят от хитрината на машината.

Ето какво казват правилата на езика за двата начина. При += V8 отлага копирането, но новият текст е нов и тук:

let report = "";
for (let i = 1; i <= 3; i++) {
report += `Поръчка ${i}: 12.50 €\n`;
}
const parts = [];
for (let i = 1; i <= 3; i++) {
parts.push(`Поръчка ${i}: 12.50 €\n`);
}
const joined = parts.join("");
console.log(report.length, joined.length);
console.log(report === joined);
Стъпка по стъпка, с думи
  1. report сочи празен текст.
  2. Първото += прави нов текст с един ред: 19 знака.
  3. Второто += по правилата на езика дава още един текст: стария ред (19 знака) и новия. Старият текст вече не трябва на никого.
  4. Третото дава текст с 38 стари знака и новия ред. Java и Python ги копират всеки път. V8 отлага копирането: пази парчетата и ги слепва веднъж.
  5. parts е празен масив: в него ще стоят парчетата.
  6. push слага всяко парче в масива на място. Старите парчета не се копират, масивът е същият.
  7. join слепва парчетата веднъж: 57 знака. Текстът е един и същ. По правилата += копира още 57 стари знака, а join нито един.
По-точно, за любопитните
  • Слепените парчета V8 нарича „въже“ (rope). Когато прочетеш символ от такъв текст, машината го слепва в един низ, веднъж.
  • Един шаблонен низ като `Поръчка ${i}: ${price}` е един нов текст. Колко копира += в цикъл, зависи от машината, а join е предвидим навсякъде.

Виж го

Промяна със slice

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

let card = "4532015112830366";
card = card.slice(0, 12) + " " + card.slice(12);
card = card.slice(0, 8) + " " + card.slice(8);
card = card.slice(0, 4) + " " + card.slice(4);
console.log(card);
card = "**** **** ****" + card.slice(14);
console.log(card);
let phone = "0888-123-456";
phone = phone.slice(0, 8) + phone.slice(9);
phone = phone.slice(0, 4) + phone.slice(5);
console.log(phone);
console.log([..."кафе"].reverse().join(""));
Изход
4532 0151 1283 0366
**** **** **** 0366
0888123456
ефак

card.slice(0, 12) са символите от началото до индекс 12, без него. card.slice(12) е от индекс 12 до края.

Искаш Пишеш
вмъкни x преди индекс i s.slice(0, i) + x + s.slice(i)
замени символите от a до b (без b) с x s.slice(0, a) + x + s.slice(b)
махни символа с индекс i s.slice(0, i) + s.slice(i + 1)
махни символите от a до b (без b) s.slice(0, a) + s.slice(b)
обърни текста отзад напред [...s].reverse().join("")
дължина, символ s.length, s[i]

[...s] прави масив от символите на текста. Масивът има reverse(), а join("") го слепва обратно.

Интервалите в номера на картата се вмъкват отзад напред: първо на 12, после на 8, после на 4. Ако започнеш от 4, вмъкнатият интервал измества всичко след него и 8 вече не е мястото, което искаш. Тиретата в телефона се махат по същата причина първо отзад.

Пробвай

Две имена, един обект

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

const note = ["Маса 4"];
const copy = note;
copy.push(", 2 кафета");
console.log(note.join(""));
const text = "Маса 4";
let other = text;
other += ", 2 кафета";
console.log(text);
Отговор

Идеята

По азбучен ред: localeCompare

=== казва само „еднакви ли са“. За подреждане трябва повече: кой текст е преди другия. Това казва localeCompare:

console.log("Андреев".localeCompare("Борисов", "bg"));
console.log("Борисов".localeCompare("Борисов", "bg"));
console.log("Андреев" < "Борисов");
console.log("иванова" < "Иванова");
Изход
-1
0
true
false
  • отрицателно число: първият е преди втория;
  • 0: еднакви са;
  • положително: първият е след втория.

"bg" казва да сравнява по правилата на българския език. Текстовете се сравняват и с < и >, но тогава по кода на всеки символ: главните букви са преди малките, затова "иванова" < "Иванова" е false.

Знакът е важен, не самото число. С него подреждаш списък с имена, например с метода на мехурчето от урока за масивите:

const names = ["Петрова", "Андреев", "Иванова", "Борисов"];
for (let i = 0; i < names.length - 1; i++) {
for (let j = 0; j < names.length - 1 - i; j++) {
if (names[j].localeCompare(names[j + 1], "bg") > 0) {
const tmp = names[j];
names[j] = names[j + 1];
names[j + 1] = tmp;
}
}
}
console.log(names.join(", "));
Изход
Андреев, Борисов, Иванова, Петрова

names.join(", ") долепва елементите на масива с разделител между тях.

По-точно, за любопитните
  • localeCompare по правилата на "bg" слага малката буква преди главната: "иванова".localeCompare("Иванова", "bg") е -1. С { sensitivity: "base" } не гледа главни и малки и дава 0.
  • С { numeric: true } сравнява числата в текста като числа: тогава "INV-10" е след "INV-9".
  • names.sort() подрежда масива без мехурчето, но по кодовете на символите, като <. За тези имена резултатът е същият.
console.log("иванова".localeCompare("Иванова", "bg"));
console.log("иванова".localeCompare("Иванова", "bg", { sensitivity: "base" }));
console.log("INV-10".localeCompare("INV-9", "bg", { numeric: true }));
const names = ["Петрова", "Андреев", "Иванова", "Борисов"];
names.sort();
console.log(names.join(", "));
Изход
-1
0
1
Андреев, Борисов, Иванова, Петрова

Пробвай

Фактура 10 или 9?

Номерата на фактурите са текст. Коя е по-ранна?

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

const a = "INV-10";
const b = "INV-9";
console.log(a < b);
console.log(Number(a.slice(4)) < Number(b.slice(4)));
Отговор

Идеята

Търсене, рязане, сглобяване

Ред от файл с контакти: име, имейл и град, разделени с „ ; “, с излишни интервали по краищата:

const line = " Елена Колева ; elena.koleva@firma.bg ; Пловдив ";
const parts = line.trim().split(" ; ");
const email = parts[1];
const at = email.indexOf("@");
console.log(email.slice(0, at));
console.log(email.slice(at + 1));
console.log(email.indexOf(".", at));
console.log(parts.join(" | "));
console.log("=".repeat(20));
Изход
elena.koleva
firma.bg
18
Елена Колева | elena.koleva@firma.bg | Пловдив
====================
  • trim() маха интервалите отпред и отзад, и „широките“ интервали от другите езици.
  • indexOf("@") дава индекса на първата поява, а indexOf(".", at) търси чак от индекс at нататък. lastIndexOf търси отзад напред.
  • email.slice(0, at) е от началото до at, без него. email.slice(at + 1) е от at + 1 до края. join е обратното на split.
  • "=".repeat(20) повтаря текста 20 пъти.

Празно и „празно“ не е едно и също:

const empty = "";
const spaces = " ";
console.log(empty.length === 0, spaces.length === 0);
console.log(empty.trim() === "", spaces.trim() === "");
Изход
true false
true true

length === 0 е вярно само за празен текст. trim() === "" е вярно и когато има само интервали. За проверка на въведено поле обикновено искаш второто.

Идеята

Числа и текст

Числата влизат в текст и излизат от текст:

const code = Number("0042");
const price = Number("2.40");
const label = String(code) + "-" + String(price);
console.log(label);
Изход
42-2.4

Number() вече познаваш. String() е обратното: прави текст от число или boolean.

За таблица с колони ти трябва ширина на всяка колона. Бележка от кафене:

const names = ["Кафе", "Кроасан", "Минерална вода"];
const prices = [2.4, 3.1, 1.5];
const qty = [2, 1, 3];
let total = 0;
for (let i = 0; i < names.length; i++) {
const amount = prices[i] * qty[i];
total += amount;
console.log(`${names[i].padEnd(16)}${String(qty[i]).padStart(2)} x ${prices[i].toFixed(2).padStart(5)} = ${amount.toFixed(2).padStart(6)}`);
}
console.log("-".repeat(35));
console.log(`${"Общо".padEnd(26)}${total.toFixed(2).padStart(9)} €`);
Изход
Кафе 2 x 2.40 = 4.80
Кроасан 1 x 3.10 = 3.10
Минерална вода 3 x 1.50 = 4.50
-----------------------------------
Общо 12.40 €
  • padEnd(16) допълва текста с интервали отдясно до 16 знака: подравнен е вляво. padStart(5) допълва отляво: подравнен е вдясно.
  • toFixed(2) прави от числото текст с 2 цифри след точката.
  • String(qty[i]) е нужно, защото padStart е метод на текста, не на числото.
По-точно, за любопитните
  • a.concat(b) е същото като a + b. Ще го срещнеш в по-стар код.
  • padEnd и padStart не режат по-дълъг текст: той избутва колоната вдясно.
  • Number("2,40") не спира програмата, а дава NaN („не е число“), и програмата продължава с него. toFixed винаги пише точка, а toLocaleString("bg") пише по българските правила, със запетая.
console.log(Number("2,40"));
console.log((2.4).toFixed(2));
console.log((2.4).toLocaleString("bg", { minimumFractionDigits: 2 }));
console.log("Кафе".concat(" и ", "кроасан"));
Изход
NaN
2.40
2,40
Кафе и кроасан

Пробвай

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

„Капак“ се чете еднакво и отзад напред, а програмата казва „капак не е палиндром“. На кой ред е грешката?

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

Упражнение

Текст, който сглобяваш

Реши задачите в редактора си. Не печатай въпроси.

1. Палиндром. Прочети ред и кажи дали е палиндром, без значение от главните букви, интервалите, запетаите и тиретата. Този път обърни текста с [...text].reverse().join("") вместо с цикъл.

Вход
Дебел лебед
Изход
Палиндром
Вход
Дебел гълъб
Изход
Не е палиндром

2. Бележка с колони. На първия ред е броят на артикулите. На всеки следващ ред има име, цена и брой, разделени с ;. Отпечатай бележка с подравнени колони и сумата накрая.

Вход
3
Хляб;1.20;2
Сирене;9.80;1
Яйца;0.35;10
Изход
Хляб 2 x 1.20 = 2.40
Сирене 1 x 9.80 = 9.80
Яйца 10 x 0.35 = 3.50
------------------------------------
Общо 15.70

3. Думите наобратно. Прочети изречение и отпечатай думите му в обратен ред. Между думите може да има повече от един интервал, а в резултата трябва да има точно един. Сглоби го с масив и join.

Вход
Утре в 9 ще има среща
Изход
среща има ще 9 в Утре

Още задачи

  • Инициали. Прочети пълно име („Мария Иванова Петрова“) и отпечатай инициалите с точки: „М. И. П.“ Сглоби ги с масив и join.
  • Скрит имейл. От elena.koleva@firma.bg направи e***********@firma.bg: първата буква, после толкова звездички, колкото са останалите символи преди @. Ползвай indexOf и repeat.
  • Обиколка на текста. Създай текст с кавички ("Примерен текст") и друг с new String("Примерен текст"). Сравни ги с === и с ==, вземи slice(0, 8), раздели първия по интервала и отпечатай частите, превърни "100" в число и го отпечатай с шаблонен низ `Стойност: ${...}`, и накрая добави " и още" към масив с първия текст и ги слепи с join. Предвиди всеки ред, преди да пуснеш програмата. (new String прави обект вместо текст. В истински код не се ползва.)
  • Подреди без значение от главните букви. Промени мехурчето от картата за localeCompare да подава { sensitivity: "base" } и пробвай с „иван“, „Борис“ и „ана“.

Тест

Провери се

1. Защо в Node report += line в цикъл с 50 000 завъртания не е бавно?
2. Какво връща "Борисов".localeCompare("Андреев", "bg")?
3. const a = ["12"]; const b = a; b.push("3"); Какво дава a.join("")?
4. parts е масив от текстове. Как правиш от него един текст без разделители?
5. Какво дава (2.4).toFixed(2).padStart(6)?

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