雜記 › 技術筆記 › 同一串 HTTPServer 點遍 7 個模式,為什麼 49 對互轉之中 0 對能原樣轉回來

同一串 HTTPServer 點遍 7 個模式,為什麼 49 對互轉之中 0 對能原樣轉回來

同一串 HTTPServer 點遍 7 個模式的 49 對互轉,441 次實測只有 36 次原樣回來,帶內部大寫的源字串恆等 0 對,切詞只認兩種邊界,另有 31 個大寫字母被抹掉。

起因:一個類名轉了一圈回不來

那串 HTTPServer 轉成底線寫法是 httpserver,再點一次 PascalCase 得 Httpserver。四個大寫字母沒了——連號大寫一旦走進底線就再也長不回來。

於是把這一頁的 7 個模式排成 7×7=49 對互轉,拿 9 條輸入逐對跑滿 441 次,恆等的只有 36 次,而 HTTPServer、myVar2Name、iOS_App-Name 三條是 0/49。看著只是換大小寫,中間藏著一刀切詞:切完再拼,有些形狀注定拼不回去。下面的數字都是摳出那段轉換函式逐條量過的。

切詞三連只看兩種邊界

輸入先走 3 步:trim() 去首尾空白,[-_]+ 整段換成一個空格,再在「ASCII 小寫緊跟 ASCII 大寫」處補一個空格,最後按 /\s+/ 切開。能切的邊界只有這兩種:

輸入token 數卡在哪裡
HTTPServer1連號大寫之間不切
HTTPRequestHandler2只有 tH 一處
myVar2Name22N 是數字接大寫
API_KEY_V23兩個 _ 各換一個空格
iOS_App-Name42 段符號再加 iO 一處

點號與帶重音的大寫都不算邊界,a..b、ΩK 整塊留著;Tab、換行、U+3000 卻都算空白。

抹掉內部大寫的那一步

詞切好後,大小寫轉換這一頁第 50–54 行對每個 token 做的都是「首字母定大小,其餘一律 toLowerCase()」。按鈕寫 Title Case(首字母大寫)、camelCase(小駝峰),註釋裡沒提那句「其餘」。24 條實測輸入裡有 9 條帶 token 內部大寫,合計 31 個大寫字母被抹掉。

ABCDEFGHIJ 9 MAX_SIZE 5 HTTPServer 4 API_KEY_V2 4 HTTPRequestHandler 4 iOSAppName 2

圖 1 被抹掉的大寫字母數,柱長按個數等比。

ABCDEFGHIJ 變 Abcdefghij 一次抹 9 個,myVar2Name 只抹 1 個也夠它回不了原形。title 這一支還多挨一刀:iPhone 先切成 2 個 token 變 I Phone,多插 1 個空格;userID 變 User Id。

49 對互轉,只有 36 次原樣回來

往返指按 a 轉一次、結果再按 b 轉一次仍等於原字串。9 條輸入跑滿 49 對共 441 次,恆等只有 36 次。

user_name 10 user-name 10 userName 5 UserName 5 user name 3 API_KEY_V2 3 HTTPServer 0 myVar2Name 0 iOS_App-Name 0

圖 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 保留行數。

📎 相關文章

16 組開關實測 HTML 去標籤:報 19 個標籤,實際刪掉 148 個字元 2026-10-30 SVG 轉 PNG 後外部引用的圖丟了一半:同一份 594 B 檔案預覽有 3 張圖、成品只剩 1 張 2026-10-30 二進位轉 Base64 這一頁:前綴只有 5 檔,換行能解、連字號和底線直接報錯 2026-10-30 替應用做商店介紹圖,點匯出卻拿到 1242×2688:8 檔畫布預設的像素帳、噪點 24.83 倍體積與預設被裁掉 134.93 px 的手機框 2026-10-30 16 個鍵進、4 個欄出:JSON 轉表格的表頭聯集、點號展平與 CSV 引號規則 2026-10-30