从Delphi字符串检测和检索代码点和代理

人气:911 发布:2022-10-16 标签: unicode delphi surrogate-pairs

问题描述

我正在努力更好地理解代理项对和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及更高版本中,StringUnicodeString的别名,CharWideChar的别名。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;

790