为什么 PHP mb_convert_case() 和 mb_strtoupper() 将 μ (U+00B5 MICRO SIGN) 转换为“μ”?

Why does PHP mb_convert_case() and mb_strtoupper() convert µ (U+00B5 MICRO SIGN) to "Μ"?

我正在尝试编写自己的 mb_ucwords() 函数来提供 mb_convert_case 的快速包装器,以便它可以处理多字节字符串,因为基础 ucwords() 函数不能。

我 运行 遇到一个问题,即传入的以 µ 字符 (U+00B5 MICRO SIGN) 开头的字符串返回为“μ”(U+039C 希腊大写字母 MU)像我认为应该发生的那样被忽略。

我写了一个快速测试脚本来验证一些信息:

        function testUtf8($letter) {
            echo "CHAR: " . $letter . "\n";
            echo "Detected Encoding: " . mb_detect_encoding($letter) . "\n";
            echo "IS VALID UTF-8? " . (mb_check_encoding($letter, 'UTF-8') ? 'YES' : 'NO') . "\n";
            $lower = mb_strtolower($letter);
            $upper = mb_strtoupper($letter);
            $conv = mb_convert_case($letter, MB_CASE_TITLE, 'UTF-8');
            echo "mb_strtolower(): " . $lower . "(" . mb_ord($lower) . ")\n";
            echo "mb_strtoupper(): " . $upper . "(" . mb_ord($upper) . ")\n";
            echo "mb_convert_case(): " . $conv . "(" . mb_ord($conv) . ")\n";
            echo "\n";
            echo "Matches RegEx /\p{L}/u: " . (preg_match('/\p{L}/u', $letter) ? 'YES' : 'NO') . "\n";
            echo "Matches RegEx /\p{N}/u: " . (preg_match('/\p{N}/u', $letter) ? 'YES' : 'NO') . "\n";
            echo "Matches RegEx /\p{Xan}/u: " . (preg_match('/\p{Xan}/u', $letter) ? 'YES' : 'NO') . "\n";
        }

        testUtf8('µ');

我得到的输出是:

CHAR: µ
Detected Encoding: UTF-8
IS VALID UTF-8? YES
mb_strtolower(): µ(181)
mb_strtoupper(): Μ(924)
mb_convert_case(): Μ(924)

Matches RegEx /\p{L}/u: YES
Matches RegEx /\p{N}/u: NO
Matches RegEx /\p{Xan}/u: YES

谁能给我解释一下为什么 PHP 认为 µ 是一个“字母”,为什么 MB 的大写版本是“μ”?我打算通过测试每个单词的第一个字母并在 运行 转换之前验证它是一个有效的 unicode“字母”来解决这个问题,但是正如你所看到的那样,这个字符不会工作,因为 /\p {L}/u 匹配那个字符 :(

知道如何解决这个问题吗?

这是我的函数的草稿:

    /**
     * @param string $string The string to convert
     * @param string $encoding Default is UTF-8
     * @param string $delim_pattern Pattern used to break $string into words
     * @return string
     */
    public static function mb_ucwords(
        string $string,
        string $encoding = 'UTF-8',
        string $delim_pattern = '/([\/\-\s\v"\'\\]+)/u'
    ): string {
        $words = preg_split($delim_pattern, $string, -1, PREG_SPLIT_DELIM_CAPTURE);
        $output = "";
        foreach($words as $word) {
            $output .= mb_convert_case($word, MB_CASE_TITLE, $encoding);
        }
        return $output;
    }

目前正在针对 PHP7.4

测试此代码

编辑:

显然这是一个希腊字母,也是micro的符号,M是该希腊字母的大写形式。我不确定如何处理这个...

你可以这么简单

function mb_ucfirst($string)
{
    $main_encoding = "cp1250"; 
    $inner_encoding = "utf-8";
    $string = iconv($main_encoding, $inner_encoding, $string);
    $strlen = mb_strlen($string);
    $firstChar = mb_substr($string, 0, 1, $inner_encoding);
    $then = mb_substr($string, 1, $strlen - 1, $inner_encoding);
    return iconv($inner_encoding, $main_encoding , mb_strtoupper($firstChar, $inner_encoding) . $then );
}

在我测试时保留 µ

在 Unicode 2 中,µ (U+00B5 MICRO SIGN) was changed to have a compatibility decomposition of μ (U+03BC GREEK SMALL LETTER MU)。同时,其类别从符号变为字母,以匹配 μ(U+03BC GREEK SMALL LETTER MU)。这意味着 U+00B5 不应在新文本中使用;它仅用于与 non-Unicode 字符集兼容。在某些规范化形式下,这些被认为是相同的字符。

在 Unicode 3.0 中,它已更新为具有 M (U+039C GREEK CAPITAL LETTER MU) 作为其大写映射,给出您现在看到的结果。

不幸的是,由于 µ (U+00B5 MICRO SIGN) 基本上已被弃用,如果您使用它,您只能靠自己了。在调用 mb_convert_case 之前,您可以将字符串的第一个字符与 µ (U+00B5 MICRO SIGN) 进行比较。但是,无法保证某些系统不会自动将其转换为 μ(U+03BC 希腊小写字母 MU),例如,如果它规范化字符串。如果您永远不会使用 μ(U+03BC 希腊小写字母 MU),您也可以 special-case 该字符。

在不破坏对希腊文本支持的情况下处理此问题的 fail-safe 方法是使用某种标记语言或富文本来指示该字符用作符号而不是字母,然后在执行大小写转换时解析它。但这显然是一项更大的任务。