功能说明
runLengthEncode 把连续重复字符转换为数量加字符的记号,例如 aaabb 变成 3a2b;runLengthDecode 执行逆操作并校验输入格式。实现按 Unicode 码点而不是 UTF-16 码元遍历,因此 emoji 与代理对不会被拆开。适用于简单压缩、棋盘或图像行数据、日志去重以及传输前的体积优化。
关键代码
编码阶段用展开运算符把字符串转成码点数组,外层循环定位每段起点,内层统计连续相同字符的数量,再拼接数量与字符。解码阶段用带 u 标志的正则逐个匹配数字与任意码点,matchAll 提供位置信息,代码据此要求记号首尾相接,任何多余字符或缺失数字都会抛错。数量必须是正的安全整数,避免 repeat 造成异常内存分配。
使用注意
该格式只适合重复率高的数据,随机字符串编码后可能比原文更长,使用前应比较体积。数字与字符直接相连,若原文本身包含数字不会混淆,但解码器只接受严格交替的记号。超长重复串可能触发 repeat 的内存上限,处理不可信输入时应先限制解码后的最大长度。编码结果不包含校验和,损坏检测仅限格式层面。
验证方式
测试空字符串、单字符、全相同字符、交替字符以及包含 emoji 的字符串,断言编码后再解码等于原文。构造 3a2b、12z 等合法输入验证数量解析,测试缺少数字、数量为零、负号、尾随垃圾与空记号等非法输入。再对随机字符串做属性测试,确认往返一致,并验证非字符串参数会抛出类型错误。
代码示例
export function runLengthEncode(input) {
if (typeof input !== "string") throw new TypeError("input must be a string");
const characters = [...input];
let encoded = "";
for (let index = 0; index < characters.length; ) {
let run = 1;
while (index + run < characters.length && characters[index + run] === characters[index]) {
run += 1;
}
encoded += run + characters[index];
index += run;
}
return encoded;
}
export function runLengthDecode(encoded) {
if (typeof encoded !== "string") throw new TypeError("encoded must be a string");
const token = /(\d+)([\s\S])/gu;
let decoded = "";
let consumed = 0;
for (const match of encoded.matchAll(token)) {
if (match.index !== consumed) throw new SyntaxError("invalid run-length token");
const count = Number(match[1]);
if (!Number.isSafeInteger(count) || count <= 0) {
throw new RangeError("run length must be a positive safe integer");
}
decoded += match[2].repeat(count);
consumed = match.index + match[0].length;
}
if (consumed !== encoded.length) throw new SyntaxError("invalid run-length token");
return decoded;
}