Skip to main content

[探索 3 分鐘] 判斷字串內容是否是全數字或全英文, 效率型做法 (c#)

Question : [密碼欄位檢查] 內容不可全數字或全英文, 範例輸出入如下 (input → output)。
  • 123213123123 → false (因為全數字)
  • adfdsbrr → false (因為全英文)
  • abc123 → true
看似簡單, 但條件後來加了一個, 不可用 regex。以及有個小陷阱, 就是 : 不是去檢查英文或數字, 而是不能清一色英文或數字; 這個有了解嗎 ? 如果單純跑迴圈去檢查英文或數字, 那就誤會了哦。一開始沒想太多也不知道好用的函式, 但覺得 ASCII 滿適合上陣的, 先丟兩個版本。
public static bool ASCIIIsStrongPassword(string s)
{
    s = s.ToLower();
    long i;
    if (long.TryParse(s, out i)) return false; //全數字NG
    if (s.Any(c => c < 'a' || c > 'z')) return true; //擁有一個非英文就pass

    return false;
}

public static bool XORIsStrongPassword(string s)
{
    s = s.ToLower();
    bool isDigit = false;
    bool isLetter = false;
    for (var i = 0; i < s.Length; ++i)
    {
        if (s[i] >= '0' && s[i] <= '9') isDigit = true;
        else if (s[i] >= 'a' && s[i] <= 'z') isLetter = true;
        if (isDigit && isLetter) return true;
    }

    return !isDigit && !isLetter;
}
這兩個算創意版本, 有點囉嗦而且還要把字串 ToLower() 。
  1. 第一個版本針對數字做 TryParse(), 如果回傳 True 表示內容都是數字, 當然就是很弱的密碼; 若第一個判斷過了, 再追打內容是否都是英文, 用到 LINQ 以及 ASCII 值判斷, 相當容易混淆的實作。
  2. 第二個版本, 就更囉嗦了, 訪問每個字元, 打上是否為英文或數字的記號。若其中一個為 True, 另一個為 False, 那代表是清一色的英文或數字; 兩者皆有或皆沒有, 就是稍微 strong的 password。
雖然這樣可通過考試, 但若要追求簡潔或效能的極致, 我們來看還有什麼其他寫法。
public static bool RegexIsStrongPassword(string s)
{
    return !Regex.IsMatch(s, @"^[0-9]+$") && !Regex.IsMatch(s, @"^[a-zA-Z]+$");
}

public static bool LINQIsStrongPassword(string s)
{
    return !s.All(Char.IsDigit) && !s.All(Char.IsLetter);
}
是不是一兩行就搞定了 ! 當然 regex 是不符合命題的版本, 但看到簡潔的代碼還是會想筆記下來; 而另一個 LINQ 語法版本直接訪問字串中每個字元, 用 Char 類的 IsDigit()、IsLetter() 來操作, 非常精簡易讀。以上累計 4 個版本, 猜猜隨手版效能是否還不錯, 雖然囉嗦點 ? 跑個 100 萬次來試試。

> Input a string: 123

> Input a string: abc

> Input a string: abc123
答案揭曉, LINQ + Char 操作版本是最快的, 而 Regex 版本慢非常多, 兩者差異有 10 倍以上。隨手版 XORIsStrongPassword() 速度還不錯, 但只能當玩具 (千萬別放到團隊代碼中, 會被殺的)。說實在, regex 可以考慮放在一些小型的應用程式或檢核上, 如果搭配 config 字串動態判斷 regex 的條件, 還是滿靈活的。

介紹到這, 有點不太甘願, 隨手版有沒有優化空間 ? 如果把 String.ToLower() 操作拿掉呢 ?
public static bool XNORIsStrongPassword(string s)
{
    bool isDigit = false;
    bool isLetter = false;
    for (var i = 0; i < s.Length; ++i)
    {
        if (s[i] >= '0' && s[i] <= '9') isDigit = true;
        else if ((s[i] >= 'a' && s[i] <= 'z') || (s[i] >= 'A' && s[i] <= 'Z')) isLetter = true;
        if (isDigit && isLetter) return true;
    }

    return !(isDigit ^ isLetter); // (1,0), (0,1) -> false; (0,0), (1,1) -> True
}
沒錯, 更醜陋了, 但大體還是看得懂吧 ? (不是屍體...), 趕緊回頭再跟第一名相互 PK, 一樣, 100 萬次。
看到了嗎 ? XNORIsStrongPassword() 版本自我進化快 1 倍以上了, 就只是一行不起眼的 ToLower() 拿掉而已。是否效能跟可讀性這時候分家了呢 ? 是這樣的, 如果應用程式或服務沒有 1 秒鐘跑個上百萬次的需求, 建議選擇高可讀性, 也就是 LINQ 的版本, 然後是 Regex 的版本, 畢竟代碼只有一行, 讓後續接手的人可以關注在此, 自我解釋性也很不錯; 但有高頻處理的需求 , 就可以考慮邏輯閘的運算版本。需求不同, 實作就會有差異, 各取所需囉。

補充, Char 類別其實還有 IsLetterOrDigit() 方便的函式, 如果有需要可以直接取用。文末來欣賞 Char.IsDigit()、Char.IsLetter() 原始碼 內部有沒有什麼有趣的實作。
[Pure]
        public static bool IsLetterOrDigit(char c)
        {
            if (IsLatin1(c))
            {
                return (CheckLetterOrDigit(GetLatin1UnicodeCategory(c)));
            }
            return (CheckLetterOrDigit(CharUnicodeInfo.GetUnicodeCategory(c)));
        }

[Pure]
public static bool IsDigit(char c)
{
    if (IsLatin1(c))
    {
        return (c >= '0' && c <= '9');
    }
    return (CharUnicodeInfo.GetUnicodeCategory(c) == UnicodeCategory.DecimalDigitNumber);
}

[Pure]
public static bool IsLetter(char c)
{
    if (IsLatin1(c))
    {
        if (IsAscii(c))
        {
            c |= (char)0x20;
            return ((c >= 'a' && c <= 'z'));
        }
        return (CheckLetter(GetLatin1UnicodeCategory(c)));
    }
    return (CheckLetter(CharUnicodeInfo.GetUnicodeCategory(c)));
}   
也是有用到 ASCII 判斷哦。你還有更好的寫法嗎 ? 歡迎留言分享 ~

參考資料

  • https://github.com/Microsoft/referencesource/blob/master/mscorlib/system/char.cs
  • https://github.com/Microsoft/referencesource/blob/master/mscorlib/system/string.cs
  • https://stackoverflow.com/questions/1181419/verifying-that-a-string-contains-only-letters-in-c-sharp

Comments