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


194

输入:

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

我也尝试过此问题的所有答案,但都无济于事。

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


26
我看不到第二种解决方案的问题。我想念什么?
Pedro Lima

13
因此,这些表情符号实际上实际上是组合表情符号,非常有趣。首先,您拥有女人的脸表情符号,其本身由您的两个字符表示,然后有一个额外的连接字符,即字符代码8205,然后还有另外两个-代表``红头发'',这5个字符在一起的意思是“女人的脸红头发”
TKoL

11
我认为,使用组合的表情符号正确反转字符串会非常复杂。您必须检查每个表情符号后是否都带有字符代码8205,如果是,则必须将其与以前的表情符号结合使用,而不是将其视为自己的字符。非常复杂...
TKoL

19
Javascript使我感到困惑。这是低级和高级语言概念的最奇怪的组合。它的级别是完全抽象内存(没有指针,没有手动内存管理),但是级别低到可以将字符串视为哑代码点而不是扩展字素簇。这真的很令人困惑,这使我从不知道在使用该工具时会发生什么。
亚历山大

12
@ Alexander-ReinstateMonica有没有默认通过字形分割的语言?JS只提供用UTF-16编码的标准字符串。
灯0123年

Answers:


94

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

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

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>


3
您指向的更改日志提到“ v4.9.0-确保_.split可与表情符号一起使用”,我认为4.0可能为时过早。用于拆分字符串的代码中的注释(github.com/lodash/lodash/blob/4.17.15/lodash.js#L261)引用了 2013年的mathiasbynens.be/notes/javascript-unicode。它从那时起它似乎一直在发展,但是它确实很难破解许多Unicode正则表达式。我也看不到他们的代码库中的任何测试以进行unicode拆分。所有这些使我警惕在生产中使用它。
迈克尔·安德森

5
仅需一点点搜索就发现它失败了reverse("뎌쉐") (2个韩国字素),它给出了“ᅰ셔ᄃ”(3个字素)。
迈克尔·安德森

2
似乎没有简单的本机解决方案可以解决此问题。不想仅仅为了解决这个问题而导入一个库,但这确实是目前最可靠/一致的方法。
郝武

1
在Firefox得到这个工作的正确😎逆转书写方向上Windows10荣誉依然是凌晨点点出问题(孩子们在后方结束),所以lodash拍的Windows 10,我想,这可能是一个稍微低预算😅
自耕农

54

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


5
我在想,当您在浏览器上拖动并选择文本时,👩‍👩‍👦‍👦只能整体选择。浏览器如何知道它是一个字符?有内置的方法吗?
郝武

10
@HaoWu这就是“字素簇”上的“ Unicode分段”。您的浏览器(可能使用您的操作系统提供的浏览器)将呈现并允许按每个字素簇进行选择。您可以在此处阅读规范:unicode.org/reports/tr29/#Grapheme_Cluster_Boundaries
lights0123

7
@HaoWu:“浏览器如何知道它是一个字符?” –不是“一个字符”。它是多个字符组合形成的单个字素,呈现为单个字形
约尔格W¯¯米塔格

6
和这里一样; 并非所有合成都使用零宽度连接符。
Holger

6
除了用ZWJ组成的字符外,这不会正确地反转任何东西。请不仅在这里,而且作为一般规则,请使用由知道自己在做什么的人编写的外部库,而不是黑客破解可能适用于一个测试用例的定制解决方案。在其他答案中建议使用符文lodash库(我不能保证两者之一)。
benrg

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转换为ä,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>


8
+1极具创造性地比迪(中- :它的安全关闭覆盖了POP直接格式化字符'\u202e' + text + '\u202c',以免影响下面的文字。
贝尼切尔尼亚夫斯基-帕斯金

2
由于😎这是一个相当哈克把戏和我联系的文章进入了很多详细的解释为什么它的方式更聪明的使用HTML属性,但这种方式我可以只使用字符串连接为我的黑客😂
自耕农

7
顺便说一句。我在这台机器上赢的Firefox(胜利10)不能完全正确,当从右到左书写时孩子们在父母的后面,我想很难用这些非常复杂的表情符号人组修饰符来使书写方向正确。 ..
约瑟曼

2
另一个有趣的例子:用于标志表情符号的区域指示符。如果您使用字符串“🇦🇨”(两个代码点U + 1F1E6,U + 1F1E8,将其升为升天岛的标志)并尝试将其天真地反转,则会得到“🇨🇦”,即加拿大的标志。
亚当·罗森菲尔德

2
@yeoman FYI:“ UTF-16字符”(如您在此处使用的术语)否则称为“ UTF-16代码单元”。“字符”一词往往含糊不清,因为它可以引用很多东西(但在Unicode上下文中,通常是一个代码点)。
暗示

39

我知道!我将使用RegExp。可能出什么问题了?(答案留给读者练习。)

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

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

console.log(reversed);


5
您的答案听起来很抱歉,但是,老实说,我将这个答案称为规范。绝对优于尝试手动执行相同操作的其他答案。基于字符的文本操作是regex的专长并且擅长于此,并且Unicode联盟明确地标准化了必要的regex功能(在这种情况下,ECMAScript恰好正确实现了)。也就是说,它无法处理组合字符(IIRC正则表达式使用.通配符处理)。
康拉德·鲁道夫

14
不适用于非的合成作品U+200D,例如🏳️‍🌈。值得注意的是,在Emijoi世界之外也确实存在着角色扮演的角色
Holger

2
@StevenPenny 🏳️‍🌈包含两种成分,其中一种不使用U+200D。很容易验证🏳️‍🌈不适用于此答案的代码……
Holger

1
@Holger虽然🏳️‍🌈包含不是使用U + 200D构建的合成,但它是一个非常糟糕的示例,因为它还包含使用U + 200D构建的合成。一个更好的例子是像🧑🏻或🏳️
史蒂芬竹篙

3
与此处的其他注释相反,并不是将零宽度连接符的每次使用都视为一个单个字素簇。例如,unicode 13字素测试的最后三行(unicode.org/Public/13.0.0/ucd/auxiliary/GraphemeBreakTest.txt)显示了三种非常相似的情况,其中ZWJ的处理方式不同。
Michael Anderson

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"

3
这是最好的答案。所有其他所有答案都有失败的情况,该库(希望如此)符合所有极端情况。
卡森·格雷厄姆,

1
有趣的是,乍一看这样的“简单问题”并不是一件容易解决的任务。同意Carson-希望随着Emojis的不断发展,图书馆将不断进行更新和更改。
阿尼斯·朱拉加

3
看来这已经3年没有更新了。Unicode 11大约在那个时候发布,但此后发生了变化,后来发布了Unicode 13。13中的扩展字素规则有所变化,因此可能存在一些无法解决的极端情况。(我没有看过代码-但值得注意)
Michael Anderson

2
我同意@MichaelAnderson,该库似乎使用的是天真的或旧的算法。要正确执行此操作,应使用Unicode中指定的字素分割算法
暗示

21

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

将字符串拆分为这些簇非常棘手(例如,请参见这些unicode文档)。我不会自己依靠它来实现,而是使用现有的库。Google将我指向了字素分割器库。该库的文档包含一些不错的示例,这些示例可以实现大多数实现:

使用这个你应该能够写:

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

旁白:对于未来的游客,或愿意生活在前沿的游客:

建议将字素分割器添加到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,那么您可能可以做得更整洁...

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

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


1
看起来该代码大约2年没有更新-因此其表可能不是最新的。因此,您可能需要搜索最新的内容。
Michael Anderson

3
看起来该库的最新分支可以在github.com/flmnt/graphemer上找到
Michael Anderson

4
我很惊讶我不得不向下滚动才能看到实际上是正确的答案。
Lambda Fairy,

1
对于建议示例,您可以执行const graphemes = Array.from(segment_iterator, ({segment}) => segment)
暗示

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


1
好吧,实际上很长一段时间是因为调试信息。我真的很感激
郝昊

1
@AndrewSavinykh不是代码高尔夫球,但正在寻找更优雅的解决方案。也许不像单线疯狂,但容易记住。如正则表达式解决方案是一个非常好的恕我直言。
郝武

0

您可以使用:

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

它应该将您的字符串转换为列表,将其反转然后再次使其成为字符串。


3
你读过这个问题吗?您的代码正是OP在问题中证明是错误的代码。
华盛顿瓜迪斯

-1

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

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

console.log(反向);

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.