mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-08 08:44:37 +08:00
- Index walks (slice, substring, substr, startsWith/endsWith with a position, lastIndexOf results) on a non-ASCII receiver of 64 KiB or more: once they would have paid for one pass over it, the receiver's index entry keeps every unit's byte offset (a 32-bit base per 64 units and one byte per unit), and indexes map with two loads. Before that, a walk within 64 units of the hot cursor starts from it, without the index search. The index entry stays 128 bytes on 64-bit targets. - lastIndexOf on non-ASCII receivers tests the needle's first byte before comparing, as the ASCII path does. - normalize: a quick check (UAX #15) returns the receiver when every code point is inert for the form: unchanged when normalized alone, combining class 0, and, in the ranges checked, never the second element of a composition. Other text normalizes segment by segment, each starting at a starter, so runs of inert code points copy as they are and a lone precomposed letter's result is kept per form. The per-code-point facts come from unicode_normalize, 256 code points at a time.
103 lines
3.6 KiB
TypeScript
103 lines
3.6 KiB
TypeScript
// UTF-16 index operations on long non-ASCII strings: slices and substrings
|
|
// walking forward and backward, interleaved on two strings, at astral
|
|
// characters and their halves, with negative and out-of-range indexes;
|
|
// lastIndexOf, startsWith and endsWith with positions; and normalize on
|
|
// text already in each form, composed and decomposed text, Hangul,
|
|
// compatibility characters and combining marks.
|
|
|
|
const unit = "Ünïcödé tëxt with ß and 日本語 and émoji 😀 mixed in, ";
|
|
const big = unit.repeat(3000);
|
|
const other = "ascii words then 中文 and 🎉🎉 and ü, ".repeat(4000);
|
|
|
|
function digest(text: string): number {
|
|
let h = 0;
|
|
for (let i = 0; i < text.length; i++) h = (h * 31 + text.charCodeAt(i)) >>> 0;
|
|
return h;
|
|
}
|
|
|
|
let forward = 0;
|
|
for (let i = 0; i + 30 < big.length; i += 7) {
|
|
forward = (forward * 33 + digest(big.slice(i, i + 20)) + digest(big.substring(i + 3, i + 9))) >>> 0;
|
|
}
|
|
let backward = 0;
|
|
for (let i = big.length - 1; i > 30; i -= 11) {
|
|
backward = (backward * 33 + digest(big.slice(i - 25, i)) + big.substring(i, i - 5).length) >>> 0;
|
|
}
|
|
let interleaved = 0;
|
|
for (let i = 0; i + 40 < other.length && i + 40 < big.length; i += 13) {
|
|
interleaved = (interleaved * 33 + digest(big.slice(i, i + 9)) + digest(other.slice(i + 1, i + 40))) >>> 0;
|
|
}
|
|
console.log(big.length, forward, backward, interleaved);
|
|
|
|
// Indexes at astral characters and their halves.
|
|
const emojiAt = big.indexOf("😀", 100000);
|
|
console.log(
|
|
emojiAt,
|
|
JSON.stringify(big.slice(emojiAt, emojiAt + 1)),
|
|
JSON.stringify(big.slice(emojiAt + 1, emojiAt + 3)),
|
|
JSON.stringify(big.substring(emojiAt - 1, emojiAt + 1)),
|
|
big.charCodeAt(emojiAt + 1),
|
|
JSON.stringify(big.at(emojiAt + 1)),
|
|
JSON.stringify(big.charAt(emojiAt)),
|
|
);
|
|
console.log(
|
|
JSON.stringify(big.slice(-5)),
|
|
JSON.stringify(big.slice(-30, -20)),
|
|
big.slice(big.length - 3, big.length + 10).length,
|
|
JSON.stringify(big.substring(-10, 4)),
|
|
big.slice(5, 2).length,
|
|
JSON.stringify(big.slice(NaN, 3)),
|
|
);
|
|
for (const at of [0, 1, 52, 99999, 150000, big.length - 1, big.length, -1, -2, -big.length]) {
|
|
console.log(at, JSON.stringify(big.at(at)), JSON.stringify(big.charAt(at)), big.charCodeAt(at));
|
|
}
|
|
|
|
// lastIndexOf, startsWith and endsWith with positions.
|
|
console.log(
|
|
big.lastIndexOf("日本"),
|
|
big.lastIndexOf("日本", 100000),
|
|
big.lastIndexOf("😀", 50),
|
|
big.lastIndexOf("absent"),
|
|
big.lastIndexOf("é", 0),
|
|
big.lastIndexOf("Ü"),
|
|
big.lastIndexOf(""),
|
|
big.lastIndexOf("", 10),
|
|
);
|
|
let starts = 0;
|
|
let ends = 0;
|
|
for (let i = 0; i < big.length; i += 17) {
|
|
if (big.startsWith("tëxt", i)) starts++;
|
|
if (big.endsWith("😀", i)) ends++;
|
|
}
|
|
console.log(starts, ends, big.startsWith("Ü"), big.endsWith("in, "), big.startsWith("ünï"), big.endsWith("in, ", big.length - 1));
|
|
|
|
// normalize.
|
|
const samples = [
|
|
"plain ascii",
|
|
"précomposé café naïve",
|
|
"décomposé café",
|
|
"日本語のテキスト 한국어 문장",
|
|
"각 각",
|
|
"fullwidth カタカナ fi ① ℌ ²",
|
|
"ậ ậ ͅ",
|
|
"Ω Å Å Ω K",
|
|
"が が か゚",
|
|
"emoji 😀 and \ud800 lone",
|
|
unit,
|
|
];
|
|
for (const sample of samples) {
|
|
const forms = ["NFC", "NFD", "NFKC", "NFKD"].map((form) => sample.normalize(form));
|
|
console.log(
|
|
forms.map((text) => `${text.length}:${digest(text)}`).join(" "),
|
|
forms.map((text) => text === sample).join(","),
|
|
);
|
|
}
|
|
const longNfc = big.normalize();
|
|
const longNfd = big.normalize("NFD");
|
|
console.log(longNfc === big, longNfc.length, longNfd.length, digest(longNfd.slice(0, 100)), longNfd.normalize("NFC") === big);
|
|
try {
|
|
"x".normalize("NFX");
|
|
} catch (error) {
|
|
console.log((error as Error).name, (error as Error).message);
|
|
}
|