Текстът отблизо
Проблем
Нов текст при всяко +=
Вече знаеш, че 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);Стъпка по стъпка, с думи
- report сочи празен текст.
- Първото += прави нов текст с един ред: 19 знака.
- Второто += по правилата на езика дава още един текст: стария ред (19 знака) и новия. Старият текст вече не трябва на никого.
- Третото дава текст с 38 стари знака и новия ред. Java и Python ги копират всеки път. V8 отлага копирането: пази парчетата и ги слепва веднъж.
- parts е празен масив: в него ще стоят парчетата.
- push слага всяко парче в масива на място. Старите парчета не се копират, масивът е същият.
- 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**** **** **** 03660888123456ефак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);copy = note копира стрелката, двете променливи сочат един и същ масив, а push го променя на място. При текста е различно: other += ... прави нов текст и мести само other към него. text си сочи стария.Идеята
По азбучен ред: localeCompare
=== казва само „еднакви ли са“. За подреждане трябва повече: кой текст е преди другия. Това казва localeCompare:
console.log("Андреев".localeCompare("Борисов", "bg"));console.log("Борисов".localeCompare("Борисов", "bg"));console.log("Андреев" < "Борисов");console.log("иванова" < "Иванова");-10truefalse- отрицателно число: първият е преди втория;
- 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(", "));-101Андреев, Борисов, Иванова, ПетроваПробвай
Фактура 10 или 9?
Номерата на фактурите са текст. Коя е по-ранна?
Какво ще отпечата програмата?
const a = "INV-10";const b = "INV-9";console.log(a < b);console.log(Number(a.slice(4)) < Number(b.slice(4)));Текстът се сравнява символ по символ. Първите четири символа са еднакви, после идват „1“ и „9“, а „1“ е преди „9“. Затова "INV-10" е преди "INV-9", точно както в речника „ба“ е преди „в“. Числата 10 и 9 се сравняват като числа едва след Number(). Ако подреждаш номера като текст, допълвай с нули: INV-009, INV-010.
Идеята
Търсене, рязане, сглобяване
Ред от файл с контакти: име, имейл и град, разделени с „ ; “, с излишни интервали по краищата:
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.kolevafirma.bg18Елена Колева | 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 falsetrue truelength === 0 е вярно само за празен текст. trim() === "" е вярно и когато има само интервали. За проверка на въведено поле обикновено искаш второто.
Идеята
Числа и текст
Числата влизат в текст и излизат от текст:
const code = Number("0042");const price = Number("2.40");const label = String(code) + "-" + String(price);console.log(label);42-2.4Number() вече познаваш. 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(" и ", "кроасан"));NaN2.402,40Кафе и кроасанПробвай
Намери грешката
„Капак“ се чете еднакво и отзад напред, а програмата казва „капак не е палиндром“. На кой ред е грешката?
Избери реда с грешката.
[...word].reverse() е масив от символи, а word е текст. === между текст и масив е false, каквото и да има вътре. Поправката е [...word].reverse().join(""): така масивът става текст.Упражнение
Текст, който сглобяваш
Реши задачите в редактора си. Не печатай въпроси.
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.703. Думите наобратно. Прочети изречение и отпечатай думите му в обратен ред. Между думите може да има повече от един интервал, а в резултата трябва да има точно един. Сглоби го с масив и 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" }и пробвай с „иван“, „Борис“ и „ана“.
Тест