2023-07-31|閱讀時間 ‧ 約 7 分鐘

使用 C# 移除字串中的重音/變音符

    0x00 前情提要

    最近正在嘗試將 WebApi 改用 GraphQL
    並使用了 HotChocolate 套件來自動生成整個 GraphQL 文件
    But!
    原以為一切應該要很順利的
    卻遇到了一個令人困惑的錯誤訊息

    HotChocolate.SchemaException: For more details look at the `Errors` property.
    1. The specified name is not a valid GraphQL name. (Parameter 'value') (HotChocolate.Types.EnumType<MyEnum>)

    起初我以為將 Enum 值轉換成字串只不過是 ToString() 而已
    實在是不解為什麼會發生錯誤?


    0x01 開始抓蟲

    由於原本的 Enum 很大一包
    所以我快速地掃了一遍並沒有發現任何明顯的問題
    尤其整包 Enum 都是品牌名稱
    直覺上不會有問題才對

    後來使用了中斷點的方式抓出造成錯誤的 Enum
    原來有一個 Enum 值的名稱包含了變音符號

    來個例子🌰:

    enum Brand 
    {
    // …
    Hermès,
    // …
    }

    當 Hermès ToString() 轉換為 GraphQL 的 Enum Type 時
    就違反了命名規範因此噴錯


    0x02 殺蟲時間

    其實最簡單暴力的解決方法是建立一個對照表
    將所有帶有變音符號的字母直接替換成相應的字母
    但這方案不是很優雅

    因此持續尋找答案的過程中
    發現 Unicode 有四種不同的正規化形式
    (Normalization Form,簡稱 NFC、NFD、NFKC、NFKD)

    Ref:
    1. .NET NormalizationForm Enum
    2. UNICODE NORMALIZATION FORMS

    對於處理變音符號,我們需要使用 NFC 和 NFD 這兩種形式。

    static void PrintAsBytes(string s, NormalizationForm form)
    {
    Console.WriteLine($"{form}: {string.Join(" ", s.Normalize(form).Select(s => $"{(short)s:X4}"))}");
    }
    PrintAsBytes("è", NormalizationForm.FormC);
    PrintAsBytes("è", NormalizationForm.FormD);

    建立一個方法將不同 NormalizationForm 的字串用 bytes 的方式印出來
    可以得到結果為

    FormC: 00E8
    FormD: 0065 0300

    而 ASCII 中的 e 就是 0x65
    由此可見我們可以利用 FormD 的格式找到無變音符號的字母
    將變音符號去除之後就可以得到符合 ASCII 範圍內的字母了

    根據上面的想法可以寫一段這樣的方法來實作

    static string RemoveDiacritics(string s) {
    return string.Concat(Regex.Matches(s.Normalize(NormalizationForm.FormD), @"[A-Za-z0-9_]").Normalize(NormalizationForm.FormC);
    }

    但這樣的做法閱讀起還有那麼一點彆扭
    後來發現微軟有提供 CharUnicodeInfo.GetUnicodeCategory 的方法
    利用 UnicodeCategory 來識別是否應該去除

    static string RemoveDiacritics(string s) {
    return string.Concat(s.Normalize(NormalizationForm.FormD).Where(c => CharUnicodeInfo.GetUnicodeCategory(c) != UnicodeCategory.NonSpacingMark)).Normalize(NormalizationForm.FormC);
    }

    這樣的程式碼更容易閱讀
    讓意圖更加明確


    0x03 Unicode Ascii Folding Filter

    上面有提到的其中一種做法是用對照表的方式做替換
    而其實在查找的過程中
    發現 Apache 專案中有個 ASCII Folding Filter 的方法
    就是用超大的 switch case 製作的
    網路上也有人把這個邏輯改寫成 C# 版本
    但後來沒有使用這個版本
    因為 ASCII Folding Filter 做的事情不只移除變音符號
    還處理了所有的 Unicode 的類型
    這是擷取 source code 中的註解內容

    This class converts alphabetic, numeric, and symbolic Unicode characters which are not in the first 127 ASCII characters (the “Basic Latin” Unicode block) into their ASCII equivalents, if one exists.

    如果是文章或是使用者輸入的文字
    要轉進只允許 ASCII 的系統會比較合適


    0xFF 後記

    這次的問題出自於在建立這個 Enum 時
    大家都是習慣性的複製貼上
    由於這次提供原始資料的來源是歐洲公司
    因此名稱上就容易出現變音符
    而現在多數的程式語言也都支援 Unicode
    所以貼上時也不會出問題

    這時突然想到一件有趣的事情
    例如 Swift 甚至可以用 emoji 來寫 code

    ref: https://docs.swift.org/swift-book/documentation/the-swift-programming-language/thebasics/#Naming-Constants-and-Variables

    就嘗試了一下看看 C# 的 enum 是不是也可以用 emoji
    經過嘗試之後發現
    中文是可以的
    但 emoji 不行
    或 unicode 的符號 (e.g. ①) 也不行
    查了一下原因找到了一個 GitHub 的討論串
    滿有趣的

    希望這篇文章的小發現你也會覺得有趣


    分享至
    成為作者繼續創作的動力吧!
    © 2024 vocus All rights reserved.