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 --version
为3.1.100
。
这倒不重要,毕竟没用到什么新特性。
参考
- UTF-16 - Wikipedia:其中有关代理对的内容可以看看。
- C# Regular Expressions with \Uxxxxxxxx characters in the pattern:提到了变通实现可以用的pattern。
实践
项目作成
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([email protected]"(?:\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, [email protected]"[\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的库在这里。只是体量很大,资料也没找到太多,暂且放放。