dotNET Core 3.1实现BMP以外的正则范围判断

C#那边System.Text.RegularExpressions的正则没提供诸如[\U00020000-\U0002A6DF]这样的语法,BMP以外的判断就变得非常棘手了,幸好可以用(?:[\uD840-\uD868][\uDC00-\uDFFF])|(?:\uD869[\uDC00-\uDEDF])这样的办法变相实现(匹配CJK Unified Ideographs Extension B区块内的字符)。总之尝试一下看看吧。

啊,BMP是什么?它是Basic Multilingual Plane,是Unicode的Plane 0。在UTF-16编码中,BMP中的每一个字符都在char的表示范围之内。然而non-BMP,也就是说BMP以外的情况,UTF-16这边就只好求助于代理对(surrogate pair),就是说用上下两个代理字符才能表示non-BMP的一个字符。可能讲得不太清楚,有些术语查一下或许更好懂。

环境

操作系统Win10 1909,dotnet --version3.1.100

这倒不重要,毕竟没用到什么新特性。

参考

实践

项目作成

Set-Location D:\prj
dotnet new console -o Surrogates
Set-Location .\Surrogates
New-Item SurrogatePair.cs

只是处于熟悉才建立console项目。其中既存的Program.cs留作测试使用,接下来的实现在SurrogatePair.cs里。

SurrogatePair类作成

.\SurrogatePair.cs

using System.Text;
using System.Text.RegularExpressions;

namespace Surrogates
{
    class SurrogatePair
    {
        public int highSurrogate { get; }
        public int lowSurrogate { get; }
        public SurrogatePair(int highSurrogate, int lowSurrogate)
        {
            this.highSurrogate = highSurrogate;
            this.lowSurrogate = lowSurrogate;
        }
        public SurrogatePair(int codePoint)
        {
            var uv = codePoint - 0x10000;
            this.highSurrogate = (uv >> 10) + 0xD800;
            this.lowSurrogate = uv % 0x400 + 0xDC00;
        }

        public SurrogatePair(string s)
            : this(codePoint: char.ConvertToUtf32(s, 0)) { }

        public override string ToString()
        {
            return string.Format(@"\u{0:X}\u{1:X}", highSurrogate, lowSurrogate);
        }

        public static string CreateRegexPart(SurrogatePair begin, SurrogatePair end)
        {
            var sb = new StringBuilder();
            int lowSurrogateLeft, lowSurrogateRight;
            for (int high = begin.highSurrogate; high <= end.highSurrogate; high++)
            {
                lowSurrogateLeft = high == begin.highSurrogate ? begin.lowSurrogate : 0xDC00;
                lowSurrogateRight = high == end.highSurrogate ? end.lowSurrogate : 0xDFFF;
                sb.Append($@"(?:\u{high:X}[\u{lowSurrogateLeft:X}-\u{lowSurrogateRight:X}])|");
            }
            sb.Length -= 1;
            var matches = Regex.Matches(sb.ToString(), @"[0-9A-Fa-f]+(?=\[\\uDC00\-\\uDFFF\])");
            if (matches.Count > 1)
            {
                sb.Remove(startIndex: matches[0].Index,
                          length: matches[matches.Count - 1].Index - matches[0].Index);
                sb.Insert(matches[0].Index + 4, "]");
                sb.Insert(matches[0].Index - 2, $@"[\u{matches[0].Value}-");
            }
            return Regex.Replace(sb.ToString(), @"\[(\\u[0-9A-Fa-f]+)\-\1\]", "$1");
        }

        public static string CreateRegexPart(int beginCodePoint, int endCodePoint)
        {
            return CreateRegexPart(new SurrogatePair(beginCodePoint),
                                   new SurrogatePair(endCodePoint));
        }
        public static string CreateRegexPart(string beginString, string endString)
        {
            return CreateRegexPart(new SurrogatePair(beginString),
                                   new SurrogatePair(endString));
        }
    }
}

就很初步。

测试与使用

现在用Promgram.cs试用也没什么大不了。主要是调用SurrogatePair.CreateRegexPart,别的没什么。

比如说CreateRegexPart(0x20000, 0x2A6DF),这是CJK Unified Ideographs Extension B(中日韩统一表意文字扩展区B,CJK統合漢字拡張B,反正快乐就完事儿了)区块的范围(并不是实际割当分配字符的范围,有9个code point预留了)。结果是(?:[\uD840-\uD868][\uDC00-\uDFFF])|(?:\uD869[\uDC00-\uDEDF]),还算壮观。

只是现在并不清楚程序是否足够完善,还没有详细测试。

那个,单纯只是作为参考w

补充

这个SurrogatePair.CreateRegexPart,前面部分其实只是根据每一个high surrogate来分成几个诸如(?:\uD86E[\uDC20-\uDFFF])这样的东西,再用|拼在一起。这样确实可以,但表达式非常冗长。

至于说解决冗长的办法,之前想了很久,暴力连环判断什么的也有做过,但实在难以维护就作罢。后来还是要用正则来处理,清理一下,爽快得多。

至于说这些东西好不好用,咱也不知道w

再就是StringBuilder的实例长度减一,可以裁掉最后多余的|符号。由于本例中该实例长度(正常使用情况下)在裁剪之前不会为零,所以不用担心。

后记

这问题困扰了很久。没办法,有些东西是设计使然,要是.NET相关开发组再为此实现,不清楚是否会影响到效率。当然我说这些也没用就是w

另外也可以用ICU的办法,.NET的库在这里。只是体量很大,资料也没找到太多,暂且放放。

InSb

InSb

只是跟工作和生活相关的记录