对如何计数所见内容做出了适度的贡献,而不是组成的内容
tldr
要产生与观察到的不同图形符号相对应的Unicode字符串计数,需要在原始字符串上执行几个还原操作。
我们需要在适当的情况下删除替代和变体编码以及修饰符。我们还需要考虑零宽木连接器(ZWJ)连接器。最终结果可以在index.ts文件或本文档的底部看到。
我们计算我们看到的
但JavaScript不
只要其 ivem>份量的总和。
我们期望我们识别是一个单元 - 字母,标点符号或其他不同的图形符号 - 也应视为不可分割的和计数,一一 /em>,直到我们到达结束。
直觉,这似乎很清楚。就像Hello
一词具有5个不同的字母一样,以下每个表情符号:ð©,€,€,ðð»,ðü½ð»或𧧧被视为独立的单独单位。因此,计算字符串Hello 👋🏻
的各个部分应等于7。
length
除非这不是JavaScript中的方式。
"👋🏻".length; // => Expected 1, got 4.
"👨👩👧👧".length; // => Expected 1, got 11.
"🤽🏿♀️".length; // => Expected 1, got 7.
"Hello 👋🏻".length; // => Expected 7, got 10.
"Family 👨👩👧👧".length; // => Expected 8, got 18.
为什么会发生这种差异?
从编码到外观
许多是一个
上面的原始长度结果实际上代表了对产生观察到的符号的所需Unicode字符组合的正确评估。 length
操作并不计算我们立即期望的 - 最终的视觉单元结果 - 但 所有的零件都合并在一起以构成最终外观:一个苍白的手,一个家庭,一个家庭,一个女人玩水。
实际上是由更原始的符号组成的简单符号应该是非常熟悉的想法。当我们学会写作时,我们看到每个字母都是由不同的线组成的。单个字母I
是用一行绘制的,单个字母H
由三个不同的行组合组合。
输出其他符号,例如表情符号,也可能需要组成。挥舞的手表情符号可以具有肤色修饰符。可以将水上球员的性别(女人)具有中等深色的肤色。心脏可以具有红色变体。等等...
为了帮助我们了解如何计算我们所看到的和感知的,已经做出了不同的解释,建议和策略,并具有不同程度的成功和灵活性。 14 SUP>本文试图以此为基础并提供相对紧凑的功能,该功能将允许计算许多不同的Unicode字符串的长度,尤其是当它们包含表情符号字符时。 2 sup>
显然,它不是完美的
显然,欢迎任何建议ð
计数规则
忽略不会看到的
在我们的 - 公认的轶事 - 测试中,我们一直观察到字符串计数至少与我们期望见的符号数量一样长,但是它们有时可以超出示波。我们没有观察到比最终符号的数量低的计数。
额外的计数是由于修改或 connect 字符的字符所致,这些字符对最终外观有影响,但并未作为单独的符号。<<<<<<<<<<<<<<<<< /p>
因此,有两组主要的规则将指导我们的代码结构:
通常应忽略修饰符
- 替代对(两个字符的组合生成一个单个符号以扩展Unicode空间的目的) 3 ,应忽略,因为这对表示单个视觉实体。
- 变体编码(例如,针对红色心脏表情符号的编码)应与它们所代表的变体的角色融合在一起,并且不应具有视觉含义。
- 同样,肤色选择器增强了另一个身体部位表情符号,并与该外观融合在一起。
- 例外:对于具有自己图形表示的修饰符(例如肤色修饰符),如果自己使用,则应该算作不同的。
连接器序列应暂停计数
- 零宽度木匠(ZWJ)表示以前的和后续的独立符号应视为一个单元。
- 连接器序列由ZWJ连接的单个图形链定义。
- 例如,家庭表情符号的一种变体(𩧧)由四个独立符号组成('ð由ZWJ连接。所有这些不同的元素由于与ZWJ的联系而以视觉上的视觉和计数组合在一起。
代码实现
零件
-
删除替代配对:将字符串散布到阵列中(
[...str]
)将删除任何替代对(臭名昭著的"💩".length
等于2问题)。 -
删除变体选择器:差异不会删除变体编码(使Emoji的编码使表情符号变成红色€€s符号),因此,这些仍然返回计数为了解决这个问题,我们 split 在捕获这些编码(
/[\u{fe00}-\u{fe0f}]/gu
)的正则表达式(REGEX)上的字符串。将字符串分开然后再次加入后,将删除变体(str.split(regex).join("")
)。 -
删除修饰符:相同的分裂方法,扭曲。我们仍然想计算修饰符,如果它们仅表示自己并因此出现 - 并且不会修改其他任何内容。因此,我们的分离器是一个综合:
-
修饰符捕获:在这里,我们将自己限制在皮肤修饰符上,但很容易推断出其他情况:
[\u{1f3fb}-\u{1f3ff}]
。 -
否定的lookbehind :我们前提是修饰符是在它修改的事物之后出现的。因此,不应先于一个空间,也不应放置在线的开头。我们还为修饰符没有修改普通脚本字母的前提。因此,无论是捕获修饰符是否被捕获的情况,这种情况是:
(?<!(\p{L}|^|\s|\p{Punctuation}))
-
最终正则:
/(?<!(\p{L}|^|\s|\p{Punctuation}))[\u{1f3fb}-\u{1f3ff}]/gu
-
修饰符捕获:在这里,我们将自己限制在皮肤修饰符上,但很容易推断出其他情况:
-
ZWJ的解释:
- 删除了替代物,变体和修饰符后,我们最后在ZWJ Capture Regex上拆分字符串:
/\u{200d}/gu
- 如果拆分长度为1,我们没有ZWJ,可以安全地加入过滤的字符串,张开并计数其长度。
- 否则,我们通过以下方式减少数组来计算数组的长度:
- 对于第一个元素,我们占用它的长度。
- 对于后续元素,我们添加其长度,然后减去1 以调整当前元素通过ZWJ形成单个单元的事实。
- 删除了替代物,变体和修饰符后,我们最后在ZWJ Capture Regex上拆分字符串:
整个交易
export const characterCount = (str: string) => {
// Not strictly needed for the count, but why not normalize, if we can 😀
const normalized = str.normalize();
// Define regex selectors
const variantsSelector = /[\u{fe00}-\u{fe0f}]/gu;
const skinModifiers = /(?<!(\p{L}|^|\s|\p{Punctuation}))[\u{1f3fb}-\u{1f3ff}]/gu;
const zeroJoinRegEx = /\u{200d}/gu;
// Remove variants and modifiers.
const purifiedStr = normalized
.split(variantsSelector)
.join("")
.split(skinModifiers)
.join("");
//
const splitWithZero = purifiedStr.split(zeroJoinRegEx);
if (splitWithZero.length === 1) {
return [...splitWithZero.join("")].length;
}
// Because an emoji that contains ZWJ can contain other text left and right from it
// we need to count the entire text length from each part, then subtract one.
// For example: "A 👩❤️👨 is two people and a heart" splits into [ 'A 👩', '❤️', '👨 is two people and a heart' ]
const total = splitWithZero.reduce((sum, curr, currIndex) => {
if (currIndex === 0) return (sum += [...curr].length);
sum += [...curr].length - 1;
return sum;
}, 0);
return total;
};
-
例如,众所周知的"💩".length === 2网页在解释试图计算Unicode编码文本的长度的不同特殊性方面做得很棒。 ↩
但是,在解决ZWJ问题时,每当检查的字符串具有多个字符时,它都不会正确计数。在这种情况下,它甚至将返回分数值!对于使用修饰符的情况,例如皮肤修饰符。
也将失败。要观察这些差异,请查看示例test suite。
-
有许多涉及Unicode和JavaScript的交集的重要来源。除了已经引用过的"💩".length === 2,您还邀请您看看The Absolute Minimum Every Software Developer Absolutely, Positively Must Know About Unicode and Character Sets (No Excuses!),What every JavaScript developer should know about Unicode和JavaScript has a Unicode problem。 ↩