起因:一個類名轉了一圈回不來
那串 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 保留行數。