问题描述
我正在努力更好地理解代理项对和Delphi中的Unicode实现。
如果我在Delphi中对unicode字符串S:=‘Ĥà̲V̂e’调用Long(),我将返回,8。
这是因为各个字符[Ĥ]、[à̲]、[V̂]和[e]的长度分别为2、3、2和1。这是因为Ĥ有一个代理,一个̲̀有两个额外的代理,V̂有一个代理,而e没有代理。如果我想返回字符串中的第二个元素,包括所有代理,[à̲],我该如何做呢?我知道我需要对各个字节进行某种类型的测试。我使用例程运行了一些测试
function GetFirstCodepointSize(const S: UTF8String): Integer;
在this SO Question中引用。
但得到了一些不寻常的结果,例如,这里有一些不同码点的长度和大小。下面是我如何生成这些表的一段代码。
...
UTFCRUDResultStrings.add('INPUT: '+#9#9+ DATA +#9#9+ 'GetFirstCodePointSize = ' +intToStr(GetFirstCodepointSize(DATA))
+#9#9+ 'Length =' + intToStr(length(DATA)));
...
第一组:这对我来说很有意义,每个码位的大小都翻了一番,但每个码位都是一个字符,Delphi给我的长度只有1,非常完美。
INPUT: ď GetFirstCodePointSize = 2 Length =1
INPUT: ơ GetFirstCodePointSize = 2 Length =1
INPUT: ǥ GetFirstCodePointSize = 2 Length =1
第二套:在我看来,最初看起来长度和代码点是颠倒的?我猜这是因为字符+代理被单独处理,因此第一个码点大小是‘H’的,即1,但长度返回的是‘H’加‘^’的长度。
INPUT: Ĥ GetFirstCodePointSize = 1 Length =2
INPUT: à̲ GetFirstCodePointSize = 1 Length =3
INPUT: V̂ GetFirstCodePointSize = 1 Length =2
INPUT: e GetFirstCodePointSize = 1 Length =1
一些附加测试...
INPUT: ¼ GetFirstCodePointSize = 2 Length =1
INPUT: ₧ GetFirstCodePointSize = 3 Length =1
INPUT: GetFirstCodePointSize = 4 Length =2
INPUT: ß GetFirstCodePointSize = 2 Length =1
INPUT: GetFirstCodePointSize = 4 Length =2
在Delphi中是否有可靠的方法来确定Unicode字符串中的元素的开始和结束位置?
我知道我使用单词元素的术语可能不正确,但我也不认为码点和字符是正确的,特别是考虑到一个元素的码点大小可能是3,但长度只有1。
推荐答案
我正在努力更好地理解代理项对和Delphi中的Unicode实现。
让我们省去一些术语。
由Unicode定义的每个"字符"(称为字素)都分配有唯一的代码点。
在Unicode转换格式(UTF)编码(UTF-7、UTF-8、UTF-16和UTF-32)中,每个码点都编码为代码单元序列。每个编码单元的大小由编码决定--UTF-7为7位,UTF-8为8位,UTF-16为16位,UTF-32为32位(因此而得名)。
在Delphi 2009及更高版本中,String
是UnicodeString
的别名,Char
是WideChar
的别名。WideChar
为16位。UnicodeString
包含UTF-16编码字符串(在Delphi的早期版本中,等效的字符串类型为WideString
),并且每个WideChar
都是UTF-16编码单元。
在UTF-16中,码点可以使用1个或2个码元进行编码。1个码元可以对基本多语言平面(BMP)范围内的码点值进行编码--$0000到$FFFF,包括$0000和$FFFF。更高的代码点需要2个代码单元,也称为代理项对。
如果我在Delphi中对unicode字符串S:=‘Ĥà̲V̂e’调用Long(),我将返回,8。
这是因为单个字符[Ĥ]、[à̲]、[V̂]和[e]的长度分别为2、3、2和1。
这是因为Ĥ有一个代理,̲̀有两个额外的代理,V̂有一个代理,而e没有代理。
是的,您的UTF-16中有8个WideChar
元素(代码单元)。你们所说的"代理人"实际上被称为"组合标记"。每个组合标记是其自己的唯一码点,因此是其自己的码元序列。
如果我想返回字符串中的第二个元素,包括所有代理,[à̲],我该如何做?
您必须从UnicodeString
的开头开始分析每个WideChar
,直到找到一个不是附加到前一个WideChar
的组合标记。在Windows上,最简单的方法是使用CharNextW()
函数,例如:
var
S: String;
P: PChar;
begin
S := 'Ĥà̲V̂e';
P := CharNext(PChar(S)); // returns a pointer to à̲
end;
Delphi RTL没有等价的函数。您可以手动编写一个,或者使用第三方库。RTL确实有StrNextChar()
函数,但它只处理UTF-16代理,而不处理组合标记(CharNext()
同时处理两者)。因此,您可以使用StrNextChar()
扫描UnicodeString
中的每个码点,但您必须遍历每个码点才能知道它是否是组合标记,例如:
uses
Character;
function MyCharNext(P: PChar): PChar;
begin
if (P <> nil) and (P^ <> #0) then
begin
Result := StrNextChar(P);
while GetUnicodeCategory(Result^) = ucCombiningMark do
Result := StrNextChar(Result);
end else begin
Result := nil;
end;
end;
var
S: String;
P: PChar;
begin
S := 'Ĥà̲V̂e';
P := MyCharNext(PChar(S)); // should return a pointer to à̲
end;
我知道我需要对各个字节进行某种类型的测试。
不是字节,而是它们在解码时表示的代码点。
我使用例程运行了一些测试
函数GetFirstCodepointSize(const S:UTF8字符串):整数
仔细查看函数签名。看到参数类型了吗?它是UTF-8字符串,而不是UTF-16字符串。这一点甚至在你得到该函数的答案中也有说明:这里是一个如何解析UTF8字符串的示例
UTF-8和UTF-16是非常不同的编码,因此具有不同的语义。不能使用UTF-8语义处理UTF-16字符串,反之亦然。在Delphi中是否有可靠的方法来确定Unicode字符串中元素的开始和结束位置?
不是直接的。您必须从头开始解析字符串,根据需要跳过元素,直到到达所需的元素。请记住,每个码点可以编码为1个或2个码元元素,并且每个逻辑字形可以使用多个码点(并因此使用多个码元序列)进行编码。
我知道我使用单词元素的术语可能不正确,但我也不认为码点和字符是正确的,特别是考虑到一个元素的码点大小可能是3,但长度只有1。
1个字形由1+个码点组成,每个码点编码为1+个码元。
有人能实现以下功能吗?
函数GetElementAtIndex(S:字符串;StrIdx:整数):字符串;
尝试这样的操作:
uses
SysUtils, Character;
function MyCharNext(P: PChar): PChar;
begin
Result := P;
if Result <> nil then
begin
Result := StrNextChar(Result);
while GetUnicodeCategory(Result^) = ucCombiningMark do
Result := StrNextChar(Result);
end;
end;
function GetElementAtIndex(S: String; StrIdx : Integer): String;
var
pStart, pEnd: PChar;
begin
Result := '';
if (S = '') or (StrIdx < 0) then Exit;
pStart := PChar(S);
while StrIdx > 1 do
begin
pStart := MyCharNext(pStart);
if pStart^ = #0 then Exit;
Dec(StrIdx);
end;
pEnd := MyCharNext(pStart);
{$POINTERMATH ON}
SetString(Result, pStart, pEnd-pStart);
end;