【问题标题】:Detecting and Retrieving codepoints and surrogates from a Delphi String从 Delphi 字符串中检测和检索代码点和代理项
【发布时间】:2015-11-08 07:17:30
【问题描述】:

我试图更好地理解 Delphi 中的代理对和 Unicode 实现。

如果我在 Delphi 中对 Unicode 字符串 S := 'Ĥà̲V̂e' 调用 length(),我会返回 8。

这是因为单个字符 [Ĥ]、[à̲]、[V̂] 和 [e] 的长度分别为 2、3、2 和 1。这是因为 Ĥ 有一个代理项,à̲ 有两个额外的代理项,V̂ 有一个代理项,而 e 没有代理项。

如果我想返回包含所有代理项的字符串中的第二个元素 [à̲],我该怎么做?我知道我需要对各个字节进行某种测试。我使用例程进行了一些测试

function GetFirstCodepointSize(const S: UTF8String): Integer;  

this SO Question 中引用。

但得到了一些不寻常的结果,例如,这里有一些不同代码点的长度和大小。 下面是我如何生成这些表的 sn-p。

...
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 surrogate-pairs


【解决方案1】:

我试图更好地理解 Delphi 中的代理对和 Unicode 实现。

让我们了解一些术语。

由 Unicode 定义的每个“字符”(称为 grapheme)都被分配了一个唯一的 codepoint

采用 Unicode 转换格式 (UTF) 编码 - UTF-7、UTF-8、UTF-16 和 UTF-32 - 每个代码点都被编码为 codeunits。每个代码单元的大小由编码决定 - 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(含)。更高的代码点需要 2 个代码单元,也称为代理对

如果我在 Delphi 中对 Unicode 字符串 S := 'Ĥà̲V̂e' 调用 length(),我会返回 8。

这是因为单个字符 [Ĥ]、[à̲]、[V̂] 和 [e] 的长度分别为 2、3、2 和 1。

这是因为 Ĥ 有一个代理项,à̲ 有两个额外的代理项,V̂ 有一个代理项,而 e 没有代理项。

是的,您的 UTF-16 UnicodeString 中有 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: UTF8String): 整数

仔细查看该函数签名。查看参数类型?它是 UTF-8 字符串,而不是 UTF-16 字符串。这甚至在您从以下获得该功能的答案中有所说明:

这里是一个如何解析 UTF8 字符串的例子

UTF-8 和 UTF-16 是非常不同的编码,因此具有不同的语义。您不能使用 UTF-8 语义来处理 UTF-16 字符串,反之亦然。

Delphi 中是否有可靠的方法来确定 Unicode 字符串中元素的开始和结束位置?

不直接。您必须从头开始解析字符串,根据需要跳过元素,直到到达所需的元素。请记住,每个代码点可以编码为 1 个或 2 个代码单元元素,并且每个逻辑字形都可以使用多个代码点(因此也可以是多个代码单元序列)进行编码。

我知道我使用元素一词的术语可能不正确,但我认为代码点和字符也不正确,特别是考虑到一个元素的代码点大小可能为 3,但长度仅为 1。

1 个字形由 1+ 个代码点组成,每个代码点被编码为 1+ 个代码单元。

有人可以实现以下功能吗?

function GetElementAtIndex(S: String; StrIdx: Integer): String;

试试这样的:

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;

【讨论】:

  • 感谢您提供的所有详细信息。这也清楚地表明,索引 utf16 字符串,例如,S [i] 并不总是按预期工作,因为 char 本身可能有也可能没有组合标记,并且可能不适合宽字符。感谢您帮助我更好地理解这一点。
  • 我相信函数 getFirstCodePointSize 中会发生从 utf16 到 utf8 的自动转换。我会尝试找到参考。再次感谢。
  • 是的,将一种字符串类型分配给另一种时会自动转换。 UTF8StringUnicodeString 是单独的字符串类型。 getFirstCodePointSize()UTF8String 作为输入,因此它将返回与 UTF-8 相关的信息,而不是 UTF-16。在这种情况下,它返回用于对 UTF-8 字符串中的第一个代码点进行编码的 8 位代码单元的数量。 UTF-8 使用 1、2、3 或 4 个 8 位代码单元对代码点进行编码。正如我之前所说,UTF-16 使用 1 或 2 个 16 位代码单元对代码点进行编码。这就是为什么我说你不能使用 UTF-8 语义来处理 UTF-16 字符串。
  • 另一个收获,我希望是真的。是不是我会得到一个 UTF16 字符串中的总字节数,如果我将它的长度乘以 SizeOf(Char),例如,totalBytes = Length(S)*SizeOf(Char),总是会给我确切的字节数在 UTF16 字符串中,无论是否存在代理对或组合标记,即使该字符不在 BMP 上。我想知道,因为大量代码表明我们可以通过将其长度乘以 WideChar 的大小来获得 UTF16 字符串中的字节数。我只是想确保这始终是正确的。再次感谢你。 :)
  • 是的,Length(S)*SizeOf(Char)String 的总字节数。对于 D2009+、String=UnicodeStringChar=WideChar。 RTL 在SysUtils 单元中有一个ByteLength() 函数,可以为您执行该计算。您可以通过将Length() 乘以SizeOf(AnsiChar)(即:1)来对UTF8String(或任何其他AnsiString-based 字符串类型)使用类似的计算。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2017-05-12
  • 1970-01-01
  • 1970-01-01
  • 2012-09-02
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多