如何反转包含复杂表情符号的字符串?

Hao*_* Wu 197 javascript string emoji

输入:

Hello world????
Run Code Online (Sandbox Code Playgroud)

期望输出:

????dlrow olleH
Run Code Online (Sandbox Code Playgroud)

我尝试了几种方法,但没有一个给我正确的答案。

这惨败:

这有点有效,但它???分为 4 个不同的表情符号:

我也尝试了这个问题中的所有答案,但没有一个有效。

有没有办法获得所需的输出?

0st*_*ne0 96

如果可以,请使用lodash_.split()提供的功能。从4.0 版开始,能够拆分 unicode 表情符号。_.split()

使用原.reverse().join('')生来反转“字符”应该可以很好地处理包含零宽度连接符的表情符号

function reverse(txt) { return _.split(txt, '').reverse().join(''); }

const text = 'Hello world????';
console.log(reverse(text));
Run Code Online (Sandbox Code Playgroud)
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.20/lodash.min.js" integrity="sha512-90vH1Z83AJY9DmlWa8WkjkV79yfS2n2Oxhsi2dZbIv0nC4E6m5AbH8Nh156kkM7JePmqD6tcZsfad1ueoaovww==" crossorigin="anonymous"></script>
Run Code Online (Sandbox Code Playgroud)

  • 只需一点搜索就发现这失败了`reverse("뎌쉐")`(2个韩语字素),它给出了“ᅰ셔ᄃ”(3个字素)。 (6认同)
  • 您指出的变更日志提到“v4.9.0 - 确保 _.split 适用于表情符号”,我认为 4.0 可能还为时过早。代码中用于分割字符串的注释(https://github.com/lodash/lodash/blob/4.17.15/lodash.js#L261)参考https://mathiasbynens.be/notes/javascript -unicode 是 2013 年的。看起来从那时起它就一直在发展,但它确实使用了相当难以破译的大量 unicode 正则表达式。我在他们的代码库中也看不到任何有关 unicode 分割的测试。所有这些都会让我对在生产中使用它持谨慎态度。 (3认同)
  • 似乎这个问题没有简单的本地解决方案。不希望仅仅为了解决这个问题而导入库,但这确实是目前最可靠/一致的方法。 (2认同)

Mar*_*ens 55

我采纳了 TkoL 使用\u200d角色的想法,并用它来尝试创建一个较小的脚本。

注意:并非所有合成都使用零宽度连接器,因此其他合成字符会出现问题。

它使用传统的for循环,因为我们会跳过一些迭代,以防我们找到组合的表情符号。在for循环内有一个while循环来检查是否有后续\u200d字符。只要有一个,我们也添加接下来的 2 个字符,并for用 2 次迭代转发循环,这样组合的表情符号就不会反转。

为了在任何字符串上轻松使用它,我将它作为字符串对象上的新原型函数。

String.prototype.reverse = function() {
  let textArray = [...this];
  let reverseString = "";

  for (let i = 0; i < textArray.length; i++) {
    let char = textArray[i];
    while (textArray[i + 1] === '\u200d') {
      char += textArray[i + 1] + textArray[i + 2];
      i = i + 2;
    }
    reverseString = char + reverseString;
  }
  return reverseString;
}

const text = "Hello world????";

console.log(text.reverse());

//Fun fact, you can chain them to double reverse :)
//console.log(text.reverse().reverse());
Run Code Online (Sandbox Code Playgroud)

  • @HaoWu,这就是所谓的“字素簇”上的“Unicode 分段”。您的浏览器(可能使用操作系统提供的浏览器)将呈现并允许对每个字素簇进行选择。您可以在此处阅读规范:http://www.unicode.org/reports/tr29/#Grapheme_Cluster_Boundaries (10认同)
  • @HaoWu:“浏览器如何知道它是一个字符?” – 它*不是*“一个字符”。它是多个字符组合形成单个*字素簇*,呈现为单个*字形*。 (8认同)
  • [与此处相同](/sf/ask/4489564901/​​ontains-complicated-emojis#comment113445329_64146120); 并非所有组合都使用零宽度连接符。 (6认同)
  • 除了用 ZWJ 组成的字符之外,这不会正确反转任何内容。请不仅在这里,而且作为一般规则,请使用由知道自己在做什么的人编写的外部库,而不是破解恰好适用于一个测试用例的定制解决方案。其他答案中推荐了 [runes](/a/64151057/5670773) 和 [lodash](/a/64138318/5670773) 库(我不能保证)。 (6认同)
  • 我在想,当你在浏览器上拖动选择文本时,`‍‍‍`只能被整体选中。浏览器如何知道它是一个字符?有内置的方法可以做到这一点吗? (5认同)
  • 例如,这将因“Aदे”(U+0926)而失败 (3认同)
  • @trlkly - 如果您导入使用模块并使用现代编译器编写的库,那么我希望 [tree shake](https://webpack.js.org/guides/tree-shaking/) 能够解决死亡问题给你的代码。 (3认同)

yeo*_*man 47

由于很多原因,反转 Unicode 文本很棘手。

首先,根据编程语言,字符串以不同的方式表示,可以是字节列表、UTF-16 代码单元列表(16 位宽,在 API 中通常称为“字符”)或 ucs4 代码点(4 个字节宽)。

其次,不同的 API 在不同程度上反映了这种内部表示。有些研究字节的抽象,有些研究 UTF-16 字符,有些研究代码点。当表示使用字节或 UTF-16 字符时,API 的某些部分通常可让您访问此表示的元素,以及执行必要逻辑以从字节(通过 UTF-8)或从UTF-16 字符到实际代码点。

通常,API 的部分执行该逻辑并因此使您可以访问代码点是后来添加的,因为最初有 7 位 ascii,然后稍后每个人都认为 8 位就足够了,使用不同的代码页,甚至后来 16 位对于 unicode 就足够了。代码点作为没有固定上限的整数的概念在历史上被添加为逻辑编码文本的第四个常见字符长度。

使用可让您访问实际代码点的 API 似乎就是这样。但...

第三,有很多修饰符代码点会影响下一个代码点或后续代码点。例如,有一个变音符号修饰符将 a 后面的 a 变成 ä、e 到 ë、&c。把代码点反过来,aë变成了eä,由不同的字母组成。例如 ä 可以直接表示为它自己的代码点,但使用修饰符同样有效。

第四,一切都在不断变化。表情符号中也有很多修饰符,如示例中所用,并且每年都会添加更多。因此,如果 API 允许您访问代码点是否为修饰符的信息,则 API 的版本将确定它是否已经知道特定的新修饰符。

不过,Unicode 提供了一个技巧,当它仅与视觉外观有关时:

有书写方向修饰符。在示例的情况下,使用从左到右的书写方向。只需在文本开头添加一个从右到左的书写方向修饰符,根据 API / 浏览器的版本,它看起来会正确反转

'\u202e' 被称为从右到左覆盖,它是从右到左标记的最强版本。

请参阅w3.org 的解释

const text = 'Hello world????'
console.log('\u202e' + text)
Run Code Online (Sandbox Code Playgroud)

const text = 'Hello world????'
console.log('\u202e' + text)
Run Code Online (Sandbox Code Playgroud)
const text = 'Hello world????'
let original = document.getElementById('original')
original.appendChild(document.createTextNode(text))
let result = document.getElementById('result')
result.appendChild(document.createTextNode('\u202e' + text))
Run Code Online (Sandbox Code Playgroud)
body {
  font-family: sans-serif
}
Run Code Online (Sandbox Code Playgroud)

  • +1 非常有创意的 bidi 使用(-:使用 POP DIRECTIONAL FORMATTING char `'\u202e' + text + '\u202c'` 关闭覆盖更安全,以避免影响后续文本。 (8认同)
  • 顺便提一句。我在这台机器上的 firefox(win 10)不能完全正确,当从右向左书写时,孩子们在父母后面,我想用这些极其复杂的表情符号群体修饰符很难获得正确的书写方向。 .. (7认同)
  • 谢谢,这是一个相当棘手的技巧,我链接到的文章详细解释了为什么使用 html 属性更聪明,但这样我就可以使用字符串连接来进行我的黑客攻击 (2认同)
  • 另一个有趣的边缘情况:用于旗帜表情符号的区域指示符号。如果您采用字符串“”(两个代码点U+1F1E6、U+1F1E8,制作阿森松岛的旗帜)并尝试天真地反转它,您会得到“”,即加拿大的旗帜。 (2认同)
  • @yeoman FYI:“UTF-16 字符”(正如您在此处使用的术语)也称为“UTF-16 代码*单位*”。“字符”这个术语往往过于模糊,因为它可以指很多东西(但在 Unicode 上下文中通常是一个代码点)。 (2认同)

Nei*_*eil 39

我知道!我将使用正则表达式。什么可能出错?(答案留给读者作为练习。)

const text = 'Hello world????';

const reversed = text.match(/.(\u200d.)*/gu).reverse().join('');

console.log(reversed);
Run Code Online (Sandbox Code Playgroud)

  • 不适用于不是使用“U+200D”构建的作品,例如“️‍”。值得注意的是,在 Emijoi 世界之外,也确实存在着沉稳的角色…… (14认同)
  • 你的回答听起来很抱歉,但说实话,我认为这个答案接近规范。它绝对优于尝试手动执行相同操作的其他答案。基于字符的文本操作是正则表达式的设计目的和擅长之处,Unicode 联盟明确标准化了必要的正则表达式功能(在本例中,ECMAScript 恰好正确实现了这些功能)。也就是说,它无法处理组合字符(IIRC 正则表达式*应该*使用“.”通配符处理)。 (6认同)
  • @StevenPenny ️‍ 包含两个作品,其中之一不使用 `U+200D`。很容易验证️‍不适用于此答案的代码...... (3认同)
  • 与此处的其他评论相反,并非所有零宽度连接器的使用都应被视为单个字素簇。例如,unicode 13 grapheme 测试 (http://www.unicode.org/Public/13.0.0/ucd/auxiliary/GraphemeBreakTest.txt) 的最后三行显示了三种非常相似的情况,其中 ZWJ 的处理方式不同。 (3认同)

Arn*_*aga 32

替代解决方案是使用runes库,小而有效的解决方案:

https://github.com/dotcypress/runes

const runes = require('runes')

// String.substring
'???a'.substring(1) => '????a'

// Runes
runes.substr('???a', 1) => 'a'

runes('12???3?').reverse().join(); 
// results in: "?3???21"
Run Code Online (Sandbox Code Playgroud)

  • 这是最好的答案。所有这些其他答案都有失败的情况,这个库(希望)满足所有边缘情况。 (3认同)
  • 貌似已经有3年没有更新了。Unicode 11 大约在那时发布,但此后情况发生了变化,Unicode 13 稍后发布。13 中的扩展字素规则发生了一些变化。因此可能存在一些无法处理的边缘情况。(我没有仔细查看代码 - 但值得小心) (3认同)
  • 我同意@MichaelAnderson,这个库似乎使用了一种幼稚或旧的算法。要正确执行此操作,应使用 [Unicode 中指定的字素分割算法](https://unicode.org/reports/tr29/)。 (2认同)

Mic*_*son 22

您不仅会遇到表情符号的问题,还会遇到其他组合字符的问题。这些感觉像是单个字母但实际上是一个或多个 unicode 字符的东西被称为“扩展字素簇”。

将字符串分成这些簇是很棘手的(例如,请参阅这些unicode docs)。我不会依赖自己实现它,而是使用现有的库。谷歌指给我看grapheme-splitter库。这个库的文档包含一些很好的例子,这些例子会绊倒大多数实现:

使用这个你应该能够写:

var splitter = new GraphemeSplitter();
var graphemes = splitter.splitGraphemes(string);
var reversed = graphemes.reverse().join('');
Run Code Online (Sandbox Code Playgroud)

旁白:对于来自未来的游客,或那些愿意生活在最前沿的人:

有一项建议将字素分段器添加到 javascript 标准中。(它实际上也提供了其他分割选项)。目前处于第 3 阶段接受审查,目前在 JSC 和 V8 中实现(参见https://github.com/tc39/proposal-intl-segmenter/issues/114)。

使用这个代码看起来像:

var segmenter = new Intl.Segmenter("en", {granularity: "grapheme"})
var segment_iterator = segmenter.segment(string)
var graphemes = []
for (let {segment} of segment_iterator) {
    graphemes.push(segment)
}
var reversed = graphemes.reverse().join('');
Run Code Online (Sandbox Code Playgroud)

如果您比我了解更多现代 javascript,您可能会使它更整洁...

这里有一个实现- 但我不知道它需要什么。

注意:这指出了其他答案尚未解决的有趣问题。分段可能取决于您使用的语言环境 - 而不仅仅是字符串中的字符。

  • 看起来这个库的最新分支可以在 https://github.com/flmnt/graphemer 上找到 (4认同)
  • 我很惊讶我必须向下滚动这么远才能看到实际上正确的答案。 (4认同)
  • 对于提案示例,您可以执行 `const graphemes = Array.from(segment_iterator, ({segment}) =&gt; segment)`。 (2认同)

TKo*_*KoL 17

我只是为了好玩才决定这样做,这是一个很好的挑战。不确定它在所有情况下都是正确的,所以使用风险自负,但这里是:

function run() {
    const text = 'Hello world????';
    const newText = reverseText(text);
    console.log(newText);
}

function reverseText(text) {
    // first, create an array of characters
    let textArray = [...text];
    let lastCharConnector = false;
    textArray = textArray.reduce((acc, char, index) => {
        if (char.charCodeAt(0) === 8205) {
            const lastChar = acc[acc.length-1];
            if (Array.isArray(lastChar)) {
                lastChar.push(char);
            } else {
                acc[acc.length-1] = [lastChar, char];
            }
            lastCharConnector = true;
        } else if (lastCharConnector) {
            acc[acc.length-1].push(char);
            lastCharConnector = false;
        } else {
            acc.push(char);
            lastCharConnector = false;
        }
        return acc;
    }, []);
    
    console.log('initial text array', textArray);
    textArray = textArray.reverse();
    console.log('reversed text array', textArray);

    textArray = textArray.map((item) => {
        if (Array.isArray(item)) {
            return item.join('');
        } else {
            return item;
        }
    });

    return textArray.join('');
}

run();
Run Code Online (Sandbox Code Playgroud)