【问题标题】:UTF-16 safe substring in C# .NETC# .NET 中的 UTF-16 安全子字符串
【发布时间】:2015-11-03 07:02:12
【问题描述】:

我想得到一个给定长度的子字符串,比如 150。但是,我想确保我不会切断 unicode 字符之间的字符串。

例如见以下代码:

var str = "Hello???? world!";
var substr = str.Substring(0, 6);

这里substr 是一个无效字符串,因为笑脸字符被切成两半。

相反,我想要一个功能如下:

var str = "Hello???? world!";
var substr = str.UnicodeSafeSubstring(0, 6);

其中substr 包含“Hello????”

作为参考,下面是我在 Objective-C 中使用 rangeOfComposedCharacterSequencesForRange 的方法

NSString* str = @"Hello???? world!";
NSRange range = [message rangeOfComposedCharacterSequencesForRange:NSMakeRange(0, 6)];
NSString* substr = [message substringWithRange:range]];

C#中的等价代码是什么?

【问题讨论】:

  • @Eser UTF-16 字符可以是 2 个甚至 3 个字符。所以是的,你可以把它们切成两半。
  • @Eser read msdn.microsoft.com/en-us/library/… Char 是一个代码点,一个 unicode 字符可以包含多个 Char。例如???是 0xD83D 0xDE03 这是 2 个 16 位字符。
  • 我不明白 substr 函数应该做什么......在"Hello????" 中,???? 是什么?它应该如何与combining characters 一起使用? (因此,例如,您可以拥有 a + ̀,如果将其拆分,您将获得不带变音符号的 a...
  • @KostubDeshmukh 如果你知道哪些 unicode 字符不应该被“剪切”,你可以将它们全部放在一个列表或数组中;然后用 String.IndexOf() 方法获取它的位置,最后使用 Substring() 来获取你想要的。请参阅以下链接:msdnSOSO
  • @Eser 如果你这样做"????".Length,你会得到2。我已经测试过了。

标签: c# .net string unicode xamarin.ios


【解决方案1】:

看起来您要在 graphemes 上拆分字符串,即在单个显示的字符上。

在这种情况下,你有一个方便的方法:StringInfo.SubstringByTextElements:

var str = "Hello? world!";
var substr = new StringInfo(str).SubstringByTextElements(0, 6);

【讨论】:

  • 唯一要记住的重要事情是06 都是文本元素单元,而不是字符...如果str == "??????"(每个字形是2个字符), substr 将是 "??????",所以 substr.Length == 12
  • 正如@xanatos 提到的,当所有前 6 个字素都是 2 个字符时,这并不能解决问题。我仍然希望长度为最后一个字素所需的 6 + 额外代码点,即在 xanatos 提到的情况下返回“???”。
  • 好的,我当时误解了问题陈述。
  • 您的解决方案效果很好,但是对于以后遇到此问题的人来说,需要记住的是,StringInfo API 可能因您使用的 .NET 版本而异。对我来说,SubstringByTexElements 在 .NET Portable 4.5 中不可用
【解决方案2】:

这应该返回从索引startIndex 开始的最大子字符串,并且“完整”字素的长度最长为length...所以初始/最终“拆分”代理对将被删除,初始组合标记将被删除, 缺少组合标记的最终字符将被删除。

请注意,这可能不是您所要求的...您似乎想使用字形作为度量单位(或者您可能希望包含最后一个字形,即使它的长度会超过 length 参数)

public static class StringEx
{
    public static string UnicodeSafeSubstring(this string str, int startIndex, int length)
    {
        if (str == null)
        {
            throw new ArgumentNullException("str");
        }

        if (startIndex < 0 || startIndex > str.Length)
        {
            throw new ArgumentOutOfRangeException("startIndex");
        }

        if (length < 0)
        {
            throw new ArgumentOutOfRangeException("length");
        }

        if (startIndex + length > str.Length)
        {
            throw new ArgumentOutOfRangeException("length");
        }

        if (length == 0)
        {
            return string.Empty;
        }

        var sb = new StringBuilder(length);

        int end = startIndex + length;

        var enumerator = StringInfo.GetTextElementEnumerator(str, startIndex);

        while (enumerator.MoveNext())
        {
            string grapheme = enumerator.GetTextElement();
            startIndex += grapheme.Length;

            if (startIndex > length)
            {
                break;
            }

            // Skip initial Low Surrogates/Combining Marks
            if (sb.Length == 0)
            {
                if (char.IsLowSurrogate(grapheme[0]))
                {
                    continue;
                }

                UnicodeCategory cat = char.GetUnicodeCategory(grapheme, 0);

                if (cat == UnicodeCategory.NonSpacingMark || cat == UnicodeCategory.SpacingCombiningMark || cat == UnicodeCategory.EnclosingMark)
                {
                    continue;
                }
            }

            sb.Append(grapheme);

            if (startIndex == length)
            {
                break;
            }
        }

        return sb.ToString();
    }
}

在子字符串末尾仅包含“额外”字符的变体,如果有必要使整个字素:

public static class StringEx
{
    public static string UnicodeSafeSubstring(this string str, int startIndex, int length)
    {
        if (str == null)
        {
            throw new ArgumentNullException("str");
        }

        if (startIndex < 0 || startIndex > str.Length)
        {
            throw new ArgumentOutOfRangeException("startIndex");
        }

        if (length < 0)
        {
            throw new ArgumentOutOfRangeException("length");
        }

        if (startIndex + length > str.Length)
        {
            throw new ArgumentOutOfRangeException("length");
        }

        if (length == 0)
        {
            return string.Empty;
        }

        var sb = new StringBuilder(length);

        int end = startIndex + length;

        var enumerator = StringInfo.GetTextElementEnumerator(str, startIndex);

        while (enumerator.MoveNext())
        {
            if (startIndex >= length)
            {
                break;
            }

            string grapheme = enumerator.GetTextElement();
            startIndex += grapheme.Length;

            // Skip initial Low Surrogates/Combining Marks
            if (sb.Length == 0)
            {
                if (char.IsLowSurrogate(grapheme[0]))
                {
                    continue;
                }

                UnicodeCategory cat = char.GetUnicodeCategory(grapheme, 0);

                if (cat == UnicodeCategory.NonSpacingMark || cat == UnicodeCategory.SpacingCombiningMark || cat == UnicodeCategory.EnclosingMark)
                {
                    continue;
                }
            }

            sb.Append(grapheme);
        }

        return sb.ToString();
    }
}

这将返回您询问的内容"Hello? world!".UnicodeSafeSubstring(0, 6) == "Hello?"

【讨论】:

  • 这对我来说似乎是合理的,虽然我希望有一个内置函数。
  • @KostubDeshmukh 刚刚添加了一个变体,它将在子字符串的末尾包含额外的字符...
  • 为什么要跳过第一个字符是低位代理的字素?它们是否只是被视为格式错误/无效的字符?
  • @ubarar 因为它们是“不完整的”:代理对由一个高代理和一个低代理组成。因此,如果您从低代理开始,那么它是无效的。组合标记是相似的:它们是例如变音符号放在字符之后(所以想象'a'+'`')......所以组合标记作为第一个字符是无用的(因为那里之前没有什么可以结合的)
【解决方案3】:

这是一个简单实现截断(startIndex = 0):

string truncatedStr = (str.Length > maxLength)
    ? str.Substring(0, maxLength - (char.IsLowSurrogate(str[maxLength]) ? 1 : 0))
    : str;

【讨论】:

  • 这不会返回原始发布者想要的 - 它只返回没有笑脸的“你好”。但是,如果您必须将文本截断到一定限制的 UTF-16 字符而不将 4 字节字符切成两半,这正是这个函数的作用。
猜你喜欢
  • 2020-07-17
  • 2011-08-23
  • 2011-09-06
  • 2010-10-16
  • 2014-02-05
  • 2016-05-31
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多