// ───────────────────────────────────────────────────────────────────────────── // JS STRINGS — immutable; every method returns a NEW string // ───────────────────────────────────────────────────────────────────────────── // INDEX : s[i], s.at(-1), s.charAt(i), s.charCodeAt(i), String.fromCharCode(n) // SLICE : s.slice(start, end) — negatives OK. Prefer over substring/substr. // SPLIT : s.split(sep) keeps empties; [...s] for chars; s.split(/\s+/) regex // TRAP : join() default separator is ","; replace() replaces FIRST only; // '.' in a regex is a wildcard; < > compare by char code ('B' < 'a') // SAY : "immutable", "code unit vs code point", "lexicographic", "case-fold" // ───────────────────────────────────────────────────────────────────────────── 'use strict'; const assert = require('node:assert/strict'); // ── 1. Basics const s = 'Hello, World'; assert.equal(s.length, 12); assert.equal(s[0], 'H'); assert.equal(s.at(-1), 'd'); assert.equal(s.slice(7), 'World'); assert.equal(s.slice(-5, -1), 'Worl'); assert.equal(s.indexOf('o'), 4); assert.equal(s.lastIndexOf('o'), 8); assert.equal(s.indexOf('zzz'), -1); assert.equal(s.includes('World'), true); assert.equal(s.startsWith('He') && s.endsWith('ld'), true); assert.equal(s.toLowerCase(), 'hello, world'); assert.equal(' x '.trim(), 'x'); // trimStart / trimEnd too assert.equal('ab'.repeat(3), 'ababab'); assert.equal('7'.padStart(3, '0'), '007'); assert.equal('7'.padEnd(3, '.'), '7..'); assert.equal('a-b'.concat('-c'), 'a-b-c'); // just use + or template literal // ── 2. Split / join / chars assert.deepEqual('a,b,,c'.split(','), ['a', 'b', '', 'c']); // keeps empties assert.deepEqual('a b c'.split(/\s+/), ['a', 'b', 'c']); assert.deepEqual('a,b,c'.split(',', 2), ['a', 'b']); // limit assert.deepEqual([...'abc'], ['a', 'b', 'c']); // unicode-safe chars assert.deepEqual('abc'.split(''), ['a', 'b', 'c']); // NOT unicode-safe (splits surrogate pairs) assert.equal(['a', 'b'].join(), 'a,b'); // default "," assert.equal(['a', 'b'].join(''), 'ab'); assert.equal('a b c'.split(' ').reverse().join(' '), 'c b a'); // ── 3. Build / reverse assert.equal([...'abc'].reverse().join(''), 'cba'); let out = ''; for (const ch of 'abc') out = ch + out; assert.equal(out, 'cba'); // Building in a loop with += is fine in V8 (ropes). Array+join is also fine. // ── 4. Char codes — letter math assert.equal('a'.charCodeAt(0), 97); assert.equal('A'.charCodeAt(0), 65); assert.equal('0'.charCodeAt(0), 48); assert.equal(String.fromCharCode(97 + 2), 'c'); const shift = (ch, k) => String.fromCharCode(((ch.charCodeAt(0) - 97 + k) % 26 + 26) % 26 + 97); // Caesar assert.equal(shift('y', 3), 'b'); const isDigit = ch => ch >= '0' && ch <= '9'; const isLetter = ch => /^[a-z]$/i.test(ch); const isAlnum = ch => /^[a-z0-9]$/i.test(ch); assert.equal(isDigit('5') && isLetter('q') && !isAlnum('!'), true); const freq26 = str => { const f = new Array(26).fill(0); for (const c of str) f[c.charCodeAt(0) - 97]++; return f; }; assert.equal(freq26('abca')[0], 2); // ── 5. Replace assert.equal('a.b.c'.replace('.', '-'), 'a-b.c'); // first only assert.equal('a.b.c'.replaceAll('.', '-'), 'a-b-c'); // Node 15+ assert.equal('a.b.c'.replace(/\./g, '-'), 'a-b-c'); // escape the dot assert.equal('john smith'.replace(/\b\w/g, c => c.toUpperCase()), 'John Smith'); // function replacer assert.equal('2024-01-31'.replace(/(\d+)-(\d+)-(\d+)/, '$3/$2/$1'), '31/01/2024'); // group refs // ── 6. Compare assert.equal('B' < 'a', true); // char code assert.equal('a'.localeCompare('B') < 0, true); // locale-aware assert.equal('abc' === 'abc', true); assert.equal('Straße'.toLowerCase() === 'strasse', false); // case-folding is not toLowerCase; fine for ASCII // ── 7. Template literals const n = 3, name = 'x'; assert.equal(`${name}: ${n} item${n === 1 ? '' : 's'}`, 'x: 3 items'); assert.equal(`multi line`.split('\n').length, 2); // ── 8. Number <-> string assert.equal(String(42), '42'); assert.equal((42).toString(2), '101010'); assert.equal(Number('42'), 42); assert.equal(parseInt('101', 2), 5); assert.equal((3.14159).toFixed(2), '3.14'); // STRING assert.equal(`${1}${2}`, '12'); // concatenation, not addition // ── 9. Classic patterns const isPalindrome = str => { const t = str.toLowerCase().replace(/[^a-z0-9]/g, ''); return t === [...t].reverse().join(''); }; assert.equal(isPalindrome('A man, a plan, a canal: Panama'), true); const isAnagram = (a, b) => a.length === b.length && [...a].sort().join('') === [...b].sort().join(''); assert.equal(isAnagram('listen', 'silent'), true); const countVowels = str => (str.match(/[aeiou]/gi) ?? []).length; // match returns null on no match assert.equal(countVowels('rhythm'), 0); const capitalize = str => str.charAt(0).toUpperCase() + str.slice(1); assert.equal(capitalize('abc'), 'Abc'); const words = 'the quick brown'.trim().split(/\s+/); assert.deepEqual(words, ['the', 'quick', 'brown']); const compress = str => str.replace(/(.)\1*/g, (run, ch) => ch + (run.length > 1 ? run.length : '')); assert.equal(compress('aaabccdddd'), 'a3bc2d4'); // ── 10. Unicode notes (only if the problem mentions emoji/unicode) assert.equal('😀'.length, 2); // UTF-16 code units assert.equal([...'😀'].length, 1); // code points assert.equal('é'.normalize('NFD').length, 2); // combining marks; normalize('NFC') to compare console.log('ok: javascript/strings.js');