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

输入:

Hello world👩‍🦰👩‍👩‍👦‍👦

期望的输出:

👩‍👩‍👦‍👦👩‍🦰dlrow olleH

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

这不幸地失败了:

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

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

是否有一种方法可以获得所需的输出?

14761 次浏览

我只是觉得好玩,是个很好的挑战。不确定在所有情况下都是正确的,所以请自担风险,但下面是:

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字符的想法,并使用它来尝试创建一个更小的脚本。

不是所有的组合都使用零宽度的joiner,所以它会与其他组合字符有bug。

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

为了方便地在任何字符串上使用它,我将其作为string对象的新原型函数。

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

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

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

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>

我知道!我将使用RegExp。会出什么问题呢?(答案留作读者练习。)

const text = 'Hello world👩‍🦰👩‍👩‍👦‍👦';


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


console.log(reversed);

你不仅在表情符号上有问题,在其他组合字符上也有问题。 这些看起来像单个字母但实际上是一个或多个unicode字符的东西被称为“扩展字形集群”。

将字符串分解到这些集群中是很棘手的(例如,请参阅这些unicode文档)。我不会依赖于自己实现它,而是使用现有的库。谷歌指向grapheme-splitter库。这个库的文档包含一些不错的例子,这将阻碍大多数实现:

使用这个你应该能够写:

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

旁白:对于来自未来的游客,或者那些愿意生活在流血边缘的人:

有一个< >强建议< / >强来添加一个字素分段到javascript标准。(它实际上还提供了其他细分选项)。 目前它正处于第三阶段的验收审核,目前在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,你可能会把它写得更整洁……

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

注意:这指出了一个有趣的问题,其他答案还没有解决。分段可以取决于你所使用的语言环境,而不仅仅是字符串中的字符。

另一个解决方案是使用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位就足够了,甚至后来unicode的16位就足够了。编码点作为没有固定上限的整数的概念在历史上被添加为逻辑编码文本的第四个常用字符长度。

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

第三,有很多修饰码点影响下一个码点或后面的码点。例如,a后面有一个变音符修饰语,把a变成ä, e变成ë, c。将代码点转过来,aë就变成了eä,由不同的字母组成。有一个直接的表示,例如ä作为它自己的代码点,但使用修饰符也是有效的。

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

不过,当Unicode只涉及视觉外观时,它提供了一个简单的技巧:

有写入方向修饰符。在本例中,使用从左到右的书写方向。只需在文本开头添加一个从右向左的书写方向修饰符,根据API /浏览器的版本,它将正确地反向显示😎

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

看到本文由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 text = 'Hello world👩’;

Const reversed = text.split(").reverse().join(");

console.log(逆转);