Text up close
Problem
A new string on every +=
You already know that title.toUpperCase(); does nothing on its own: a string is immutable, and the method returns a new string. This has a cost, and you can see it in a loop.
The online store's program builds the daily report line by line:
let report = "";for (let i = 1; i <= 3; i++) { report += `Order ${i}: €12.50\n`;}process.stdout.write(report);Order 1: €12.50Order 2: €12.50Order 3: €12.50process.stdout.write prints the text without adding a new line at the end: the report already ends with \n.
By the rules of the language, each report += ... does not add to the old string. It makes a new string: the whole report so far plus the new line. If the engine really copied everything on every iteration, then near the end of 50,000 orders every iteration would copy almost a million characters. That is what happens in Java (about 2 seconds) and in Python (about 1 second).
In Node the same loop with 50,000 orders takes about 8 milliseconds. We measured it on the machine where we check the code of the lessons. V8, the engine that runs JavaScript, cheats: on += it does not copy. It remembers the two pieces side by side and glues them all at once when you need the text. This is a trick of the engine, not a rule of the language. The string is still immutable.
The idea
An array of pieces and join
JavaScript has no string that changes in place. So you collect the pieces in an array, which does change in place. At the end, join glues them into one string and copies each character only once:
const parts = [];for (let i = 1; i <= 3; i++) { parts.push(`Order ${i}: €12.50`);}const report = parts.join("\n");console.log(report);Order 1: €12.50Order 2: €12.50Order 3: €12.50const parts = []is an empty array.pushadds a piece to the end without touching the others.parts.join("\n")makes one string from all the pieces, with the separator"\n"between them. To glue with no separator, you writeparts.join("").- The new line between the pieces comes from the separator, so here the pieces have no
\nat the end.
In Node both ways are fast: with 50,000 orders join takes about 5 milliseconds and += about 8. An array and join are the usual way when there are many pieces or you need a separator between them, and they do not depend on the engine's trick.
Here is what the rules of the language say about each approach. With += V8 puts off the copying, but the new string is still a new string:
let report = "";for (let i = 1; i <= 3; i++) { report += `Order ${i}: €12.50\n`;}const parts = [];for (let i = 1; i <= 3; i++) { parts.push(`Order ${i}: €12.50\n`);}const joined = parts.join("");console.log(report.length, joined.length);console.log(report === joined);Step by step, in words
- report points to an empty string.
- The first += makes a new string with one line: 16 characters.
- By the rules of the language, the second += gives one more string: the old line (16 characters) and the new one. Nobody needs the old string anymore.
- The third gives a string with 32 old characters and the new line. Java and Python copy them every time. V8 puts off the copying: it keeps the pieces and glues them once.
- parts is an empty array: the pieces will go in it.
- push puts each piece in the array in place. The old pieces are not copied, and the array stays the same.
- join glues the pieces once: 48 characters. The text is the same. By the rules, += copies another 48 old characters, and join not a single one.
More precisely, if you are curious
- V8 calls the glued pieces a "rope". When you read a character from such a string, the engine glues it into one string, once.
- A single template literal like
`Order ${i}: ${price}`is one new string. How much+=copies in a loop depends on the engine, whilejoinis predictable everywhere.
See it
Changing with slice
A string does not change in place, so every change is a new string, built from parts of the old one. Here is a card number for printing on a receipt:
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([..."stressed"].reverse().join(""));4532 0151 1283 0366**** **** **** 03660888123456dessertscard.slice(0, 12) is the characters from the start up to index 12, without it. card.slice(12) is from index 12 to the end.
| You want to | You write |
|---|---|
insert x before index i |
s.slice(0, i) + x + s.slice(i) |
replace the characters from a to b (without b) with x |
s.slice(0, a) + x + s.slice(b) |
remove the character at index i |
s.slice(0, i) + s.slice(i + 1) |
remove the characters from a to b (without b) |
s.slice(0, a) + s.slice(b) |
| reverse the text, back to front | [...s].reverse().join("") |
| get the length, a character | s.length, s[i] |
[...s] makes an array of the characters of the string. The array has reverse(), and join("") glues it back.
The spaces in the card number go in from the back: first at 12, then at 8, then at 4. If you start at 4, the inserted space shifts everything after it, and 8 is no longer the place you want. The dashes in the phone number are removed from the back first for the same reason.
Try it
Two names, one object
What will the program print?
const note = ["Table 4"];const copy = note;copy.push(", 2 coffees");console.log(note.join(""));
const text = "Table 4";let other = text;other += ", 2 coffees";console.log(text);copy = note copies the arrow, both variables point to the same array, and push changes it in place. A string is different: other += ... makes a new string and moves only other to it. text still points to the old one.The idea
In alphabetical order: localeCompare
=== only tells you "are they the same". To sort, you need more: which string comes before the other. That is what localeCompare tells you:
console.log("Andreev".localeCompare("Borisov", "en"));console.log("Borisov".localeCompare("Borisov", "en"));console.log("Andreev" < "Borisov");console.log("ivanova" < "Ivanova");-10truefalse- a negative number: the first one comes before the second;
- 0: they are the same;
- a positive number: the first one comes after the second.
"en" says to compare by the rules of the English language. You can also compare strings with < and >, but then it goes by the code of each character: capital letters come before lowercase ones, so "ivanova" < "Ivanova" is false.
The sign matters, not the number itself. With it you sort a list of names, for example with the bubble sort from the lesson on arrays:
const names = ["Petrova", "Andreev", "Ivanova", "Borisov"];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], "en") > 0) { const tmp = names[j]; names[j] = names[j + 1]; names[j + 1] = tmp; } }}console.log(names.join(", "));Andreev, Borisov, Ivanova, Petrovanames.join(", ") glues the elements of the array with a separator between them.
More precisely, if you are curious
localeComparewith the"en"rules puts the lowercase letter before the capital one:"ivanova".localeCompare("Ivanova", "en")is -1. With{ sensitivity: "base" }it ignores upper and lower case and gives 0.- With
{ numeric: true }it compares the numbers in the text as numbers: then"INV-10"comes after"INV-9". names.sort()sorts the array without the bubble sort, but by the codes of the characters, like<. For these names the result is the same.
console.log("ivanova".localeCompare("Ivanova", "en"));console.log("ivanova".localeCompare("Ivanova", "en", { sensitivity: "base" }));console.log("INV-10".localeCompare("INV-9", "en", { numeric: true }));const names = ["Petrova", "Andreev", "Ivanova", "Borisov"];names.sort();console.log(names.join(", "));-101Andreev, Borisov, Ivanova, PetrovaTry it
Invoice 10 or 9?
The invoice numbers are text. Which one is earlier?
What will the program print?
const a = "INV-10";const b = "INV-9";console.log(a < b);console.log(Number(a.slice(4)) < Number(b.slice(4)));Text is compared character by character. The first four characters are the same, then come "1" and "9", and "1" comes before "9". So "INV-10" comes before "INV-9", just as "ba" comes before "c" in a dictionary. The numbers 10 and 9 are compared as numbers only after Number(). If you sort numbers as text, pad them with zeros: INV-009, INV-010.
The idea
Search, cut, put together
Here is a line from a contacts file: name, email and city, separated with " ; ", with extra spaces at the ends:
const line = " Elena Koleva ; elena.koleva@company.com ; Plovdiv ";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.kolevacompany.com20Elena Koleva | elena.koleva@company.com | Plovdiv====================trim()removes the spaces at the front and at the back, and also the "wide" spaces from other languages.indexOf("@")gives the index of the first occurrence, andindexOf(".", at)starts searching only from indexaton.lastIndexOfsearches from the back.email.slice(0, at)is from the start up toat, without it.email.slice(at + 1)is fromat + 1to the end.joinis the opposite ofsplit."=".repeat(20)repeats the text 20 times.
Empty and "blank" are not the same thing:
const empty = "";const spaces = " ";console.log(empty.length === 0, spaces.length === 0);console.log(empty.trim() === "", spaces.trim() === "");true falsetrue truelength === 0 is true only for empty text. trim() === "" is also true when there are only spaces. To check a field someone filled in, you usually want the second one.
The idea
Numbers and text
Numbers go into text and come out of text:
const code = Number("0042");const price = Number("2.40");const label = String(code) + "-" + String(price);console.log(label);42-2.4You already know Number(). String() is the opposite: it makes text from a number or a boolean.
For a table with columns you need a width for each column. Here is a receipt from a café:
const names = ["Coffee", "Croissant", "Mineral water"];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(`${"Total".padEnd(29)}€${total.toFixed(2).padStart(5)}`);Coffee 2 x 2.40 = 4.80Croissant 1 x 3.10 = 3.10Mineral water 3 x 1.50 = 4.50-----------------------------------Total €12.40padEnd(16)pads the text with spaces on the right up to 16 characters: it is aligned left.padStart(5)pads on the left: it is aligned right.toFixed(2)makes text from the number, with 2 digits after the point.String(qty[i])is needed becausepadStartis a method of a string, not of a number.
More precisely, if you are curious
a.concat(b)is the same asa + b. You will see it in older code.padEndandpadStartdo not cut longer text: it pushes the column to the right.Number("2,40")does not stop the program. It givesNaN("not a number"), and the program carries on with it.toFixedalways writes a point, whiletoLocaleString("de")writes by the German rules, with a comma.
console.log(Number("2,40"));console.log((2.4).toFixed(2));console.log((2.4).toLocaleString("de", { minimumFractionDigits: 2 }));console.log("Coffee".concat(" and ", "croissant"));NaN2.402,40Coffee and croissantTry it
Find the bug
"Kayak" reads the same backwards, but the program says "kayak is not a palindrome". Which line has the bug?
Pick the line with the bug.
[...word].reverse() is an array of characters, and word is a string. === between a string and an array is false, whatever is inside. The fix is [...word].reverse().join(""): that turns the array into a string.Exercise
Text you put together
Solve the tasks in your editor. Do not print prompts.
1. Palindrome. Read a line and say whether it is a palindrome, ignoring capital letters, spaces, commas and dashes. This time reverse the text with [...text].reverse().join("") instead of a loop.
Never odd or evenPalindromeNever odd, never evenNot a palindrome2. A receipt with columns. The first line has the number of items. Each line after it has a name, a price and a quantity, separated with ;. Print a receipt with aligned columns and the total at the end.
3Bread;1.20;2Cheese;9.80;1Eggs;0.35;10Bread 2 x 1.20 = 2.40Cheese 1 x 9.80 = 9.80Eggs 10 x 0.35 = 3.50------------------------------------Total 15.703. Words in reverse. Read a sentence and print its words in reverse order. There may be more than one space between the words, and the result must have exactly one. Put it together with an array and join.
Tomorrow at 9 there is a meetingmeeting a is there 9 at TomorrowMore tasks
- Initials. Read a full name ("Maria Ivanova Petrova") and print the initials with dots: "M. I. P." Put them together with an array and
join. - Hidden email. From
elena.koleva@company.commakee***********@company.com: the first letter, then as many asterisks as there are other characters before the@. UseindexOfandrepeat. - A tour of strings. Create one string with quotes (
"Practice text") and another withnew String("Practice text"). Compare them with===and with==, takeslice(0, 8), split the first one on the space and print the parts, turn"100"into a number and print it with a template literal`Value: ${...}`, and finally add" and more"to an array with the first string and glue them withjoin. Predict every line before you run the program. (new Stringmakes an object instead of a string. Real code does not use it.) - Sort ignoring case. Change the bubble sort from the
localeComparecard to pass{ sensitivity: "base" }, and try it with "ivan", "Boris" and "ana".
Test