起因:一个类名转了一圈回不来
那串 HTTPServer 转成下划线写法是 httpserver,再点一次 PascalCase 得 Httpserver。四个大写字母没了——连号大写一旦走进下划线就再也长不回来。
于是把这一页的 7 个模式排成 7×7=49 对互转,拿 9 条输入逐对跑满 441 次,恒等的只有 36 次,而 HTTPServer、myVar2Name、iOS_App-Name 三条是 0/49。看着只是换大小写,中间藏着一刀切词:切完再拼,有些形状注定拼不回去。下面的数字都是抠出那段转换函数逐条量过的。
切词三连只看两种边界
输入先走 3 步:trim() 去首尾空白,[-_]+ 整段换成一个空格,再在「ASCII 小写紧跟 ASCII 大写」处补一个空格,最后按 /\s+/ 切开。能切的边界只有这两种:
| 输入 | token 数 | 卡在哪 |
|---|---|---|
HTTPServer | 1 | 连号大写之间不切 |
HTTPRequestHandler | 2 | 只有 tH 一处 |
myVar2Name | 2 | 2N 是数字接大写 |
API_KEY_V2 | 3 | 两个 _ 各换一个空格 |
iOS_App-Name | 4 | 2 段符号再加 iO 一处 |
点号与带重音的大写都不算边界,a..b、ΩK 整块留着;Tab、换行、U+3000 却都算空白。
抹掉内部大写的那一步
词切好后,大小写转换这一页第 50–54 行对每个 token 做的都是「首字母定大小,其余一律 toLowerCase()」。按钮写 Title Case(首字母大写)、camelCase(小驼峰),注释里没提那句「其余」。24 条实测输入里有 9 条带 token 内部大写,合计 31 个大写字母被抹掉。
图 1 被抹掉的大写字母数,柱长按个数等比。
ABCDEFGHIJ 变 Abcdefghij 一次抹 9 个,myVar2Name 只抹 1 个也够它回不了原形。title 这一支还多挨一刀:iPhone 先切成 2 个 token 变 I Phone,多插 1 个空格;userID 变 User Id。
49 对互转,只有 36 次原样回来
往返指按 a 转一次、结果再按 b 转一次仍等于原串。9 条输入跑满 49 对共 441 次,恒等只有 36 次。
图 2 同一条输入在 49 对互转里能原样回来的对数。
源串全小写时最宽容(图里前两条各 10 对),带大写的 API_KEY_V2 那 3 对只能以 upper 收尾。同一语义的 5 种写法跑满 49 对共 245 次,恒等实例只有 33 次;49 对里 20 对在任何写法上都不恒等——第二步是 upper 或 title 的全回不来,只有 2 对拿到 3/5。
同一个模式连点两次也不一定一样
24 条输入 × 7 模式 = 168 次二次转换,9 次真实不幂等,另有 3 次被守卫拦回。upper、lower、snake、kebab 都是 24/24,title 23/24,camel 20/24,pascal 只有 17/24。iPhone 点 pascal 得 IPhone,再点变 Iphone;line1Line2Line3 第二次切不出 token,掉成 line1line2line3:第二次已没有「小写紧跟大写」可切。
输出长度是有公式的,破口 3 次
输出长度等于各 token 字符数之和再加 n_tok - 1(title、snake、kebab 各插 1 个分隔符),camel、pascal 加 0。24 条输入 × 5 个模式 = 120 次预测命中 117 次,3 次破口全在 İstanbul:İ(U+0130)小写后是 i 加 U+0307,camel、snake、kebab 从 8 码元变 9 码元,走 charAt(0).toUpperCase() 的 title、pascal 才保住 8。
upper 与 lower 根本不走切词
第 48–49 行直接对原始串动手,不切词也不 trim。输入 ' hello world '(17 字节,首尾各 2 个空格),upper 仍是 17 字节、中间 3 个空格没合并,camel、pascal 只剩 10 字节,title、snake、kebab 是 11 字节;那 1 个 3 字节的 U+3000 同理,在 title、snake、kebab 下换成 ASCII 空格变 19 字节,在 camel、pascal 下消失变 18 字节。
守卫拦下 6 类,纯符号串却漏了
入口只有一句 if (!text.trim())。空串、3 个空格、2 个 Tab、单个换行、BOM、全角空格共 6 类被判空,只弹「请输入文本」,写输出的 3 行一行不执行,输出框旧值与 display:none 全不变(哨兵 <<UNSET>> 跑完仍在)。但纯符号串 ---___--- 过得去,合成 2 个空 token 后 camel、pascal 得 0 字节,结果复制回输入再点,触发 3 次守卫回弹。
前导空 token 与单次处理
首字符是 - 或 _ 时,切完会留一个空 token,第 51 行的 i === 0 就落在空串上:_private_var 转 camel 得 PrivateVar,小驼峰的位置吐出 PascalCase;-_-_a 只剩 A。
模板 application/index/view/tools/case-convert.html 一共 67 行、3254 字节,1 个输入框、转换函数里 0 处循环,没有多条记录的概念,也没有长度上限:200000 字符 11 ms 切出 40001 个 token。该选哪一种不在这篇给结论,早前那篇大小写转换的命名选型讲过,这篇只量丢了什么。
常见问答
Q:HTTPServer 转成下划线再转回来,为什么不是原名?
切词只在「ASCII 小写紧跟 ASCII 大写」处补空格,HTTPServer 整串是 1 个 token,snake 之后变 httpserver,大写信息当场没了;再点 pascal 只得 Httpserver。这类源串在 49 对里恒等 0 对。
Q:想保住缩写的大写怎么办?
转换器没有「缩写」概念,只能人工写成能切开的形状,或转完手工补回大写。HTTPRequestHandler 只剩 tH 一处边界,前面 4 个大写仍然粘着,最后仍被抹掉。
Q:多行内容能一次粘进去转吗?
能,整段一次处理:200000 字符 11 ms 出 40001 个 token。但 camel、pascal 会把换行吃光,500 行变 1 行;只有 upper、lower 保留行数。