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

How to reverse a string that contains complicated emojis?

输入:

Hello world‍‍‍‍

期望的输出:

‍‍‍‍dlrow olleH

我尝试了几种方法,但 none 给了我正确的答案。

这次惨败:

const text = 'Hello world‍‍‍‍';

const reversed = text.split('').reverse().join('');

console.log(reversed);

这有点管用,但它将 ‍‍‍ 分成 4 个不同的表情符号:

const text = 'Hello world‍‍‍‍';

const reversed = [...text].reverse().join('');

console.log(reversed);

我也尝试了 this question 中的每个答案,但其中 none 有效。

有没有办法得到想要的输出?

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

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();

我接受了 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());

如果可以,请使用_.split() function provided by lodash. From version 4.0以后,_.split()可以拆分unicode表情符号。

使用本机 .reverse().join('') 反转 'characters' 应该可以很好地处理包含 zero-width joiners

的表情符号

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

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

我知道!我将使用正则表达式。会出什么问题? (留下答案作为 reader 的练习。)

const text = 'Hello world‍‍‍‍';

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

console.log(reversed);

您不仅对表情符号有问题,对其他组合字符也有问题。 这些感觉像单个字母但实际上是 one-or-more unicode 字符的东西被称为“扩展字素簇”。

将一个字符串分成这些簇是很棘手的(例如,请参阅这些 unicode docs). I would not rely on implementing it myself but use an existing library. Google pointed me at the grapheme-splitter library. The docs for this library contain some nice examples 会导致大多数实现失败:

使用这个你应该能够写:

var splitter = new GraphemeSplitter();
var graphemes = splitter.splitGraphemes(string);
var reversed = graphemes.reverse().join('');

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

有一个 proposal 可以将字素分割器添加到 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('');

如果你比我更了解现代javascript,你或许可以使这个更整洁...

有一个 implementation here - 但我不知道它需要什么。

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

替代解决方案是使用 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"

由于很多原因,反转 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 变成 ä,e 变成 ë,&c。将代码点调过来,aë 变成 eä,由不同的字母组成。有一个直接表示例如ä 作为它自己的代码点,但使用修饰符同样有效。

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

Unicode 提供了一个 hacky 技巧,但是,当它只与视觉外观有关时:

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

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

this explanation by w3.org

const text = 'Hello world‍‍‍‍'
console.log('\u202e' + text)

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))
body {
  font-family: sans-serif
}
<p id="original"></p>
<p id="result"></p>

您可以使用:

yourstring.split('').reverse().join('')

它应该把你的字符串变成一个列表,反转它然后再把它变成一个字符串。

const 文本 = 'Hello world‍‍‍‍';

const reversed = text.split('').reverse().join('');

console.log(反转);