複雑な絵文字を含む文字列を逆にする方法は?


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
2番目の解決策では問題がわかりません。何が足りないのですか?
ペドロ・リマ

13
ですから、これらの絵文字は実際にはどういうわけか組み合わせ絵文字であり、非常に興味深いものです。まず、女性の顔の絵文字があり、それ自体が2つのキャラクターで表されています。次に、追加の接続キャラクターである文字コード8205があり、次に「赤い髪」を表す別の2つのキャラクターがあり、これらの5つのキャラクターが一緒になっています。平均「赤い髪と梨花の顔」
TKoL

11
絵文字を組み合わせて文字列を適切に反転させるのはかなり複雑だと思います。各絵文字の後に文字コード8205が続くかどうかを確認する必要があります。そうである場合は、それを独自の文字として扱うのではなく、前の絵文字と組み合わせる必要があります。かなり複雑な...
TKoL

19
Javascriptは私を混乱させます。これは、低水準言語と高水準言語の概念の最も奇妙な組み合わせです。これは、メモリを完全に抽象化するという点でレベルですが(ポインタなし、手動のメモリ管理)、文字列を拡張書記素クラスターではなくダムコードポイントとして扱うほど低レベルです。それは本当に紛らわしいです、そしてそれは私がこのことで働くとき何を期待するかを決して知らないようにします。
アレクサンダー

12
@ Alexander-ReinstateMonicaは、デフォルトで書記素分割による分割を行う言語はありますか?JSは、UTF-16でエンコードされた標準の文字列を提供するだけです。
Lights01 2320年

回答:


94

可能であれば_.split()lodashが提供する機能を使用してくださいバージョン4.0以降、_.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分割のテストがコードベースに表示されません。これはすべて、本番環境での使用に慎重になるでしょう。
マイケル・アンダーソン

5
これが失敗しreverse("뎌쉐") (韓国語の書記素2つ)、「ᅰ셔ᄃ」(3つの書記素)が得られることを見つけるのに少しの検索しかかかりませんでした。
マイケル・アンダーソン

2
この問題に対する簡単なネイティブソリューションはないようです。これを解決するためだけにライブラリをインポートすることは好みませんが、現時点で最も信頼性が高く一貫性のある方法です。
HaoWu20年

1
Windows10上のFirefoxで筆記方向を😎正しく仕事にこれを取得反転させるための賞賛は、まだWindowsの10、私は推測、おそらく若干低い予算😅おしっこTADのグリッチ(子供が後部で終わる)、そうlodashビートです
ヨーマン

54

私はこの\u200dキャラクターを使用するというTKoLのアイデアを採用し、それを使用してより小さなスクリプトを作成しようとしました。

注:すべてのコンポジションがゼロ幅ジョイナーを使用しているわけではないため、他のコンポジションキャラクターではバグが発生します。

for結合された絵文字が見つかった場合にいくつかの反復をスキップするため、従来のループを使用します。forループ内whileには、次の\u200d文字があるかどうかを確認するためのループがあります。1つある限り、次の2文字も追加し、for2回の反復でループを転送して、結合された絵文字が反転しないようにします。

任意の文字列で簡単に使用できるように、文字列オブジェクトの新しいプロトタイプ関数として作成しました。

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
ブラウザでテキストをドラッグして選択する👩‍👩‍👦‍👦と、全体としてしか選択できないと思っていました。ブラウザはそれが1文字であることをどのように認識しますか?それを行うための組み込みの方法はありますか?
HaoWu20年

10
@HaoWuこれは、「書記素クラスター」の「Unicodeセグメンテーション」として知られているものです。お使いのブラウザ(OSが提供するものを使用する場合があります)がレンダリングされ、書記素クラスターごとに選択できるようになります。ここで仕様を読むことができます:unicode.org/reports/tr29/#Grapheme_Cluster_Boundaries
Lights0123

7
@HaoWu:「ブラウザはそれが1文字であることをどうやって知るのですか?」–それ「1文字」ではありません。複数の文字を組み合わせて単一の書記素クラスターを形成し、単一のグリフとしてレンダリングします
イェルクWミッターク

6
ここ同じ; すべてのコンポジションがゼロ幅接合子を使用しているわけではありません。
ホルガー

6
これは、ZWJで構成された文字以外は正しく反転しません。ここだけでなく、原則として、1つのテストケースで機能する特注のソリューションをハッキングするのではなく、自分が何をしているかを知っている人が作成した外部ライブラリを使用してください。ルーン文字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ビットで十分でした。固定された上限のない整数としてのコードポイントの概念は、テキストを論理的にエンコードするための4番目の一般的な文字長として歴史的に追加されました。

実際のコードポイントへのアクセスを提供するAPIを使用することは、それだけのようです。だが...

第三に、次のコードポイントまたは次のコードポイントに影響を与える修飾子コードポイントがたくさんあります。たとえば、次のaをä、eからë、&cに変換するdiacritic修飾子があります。コードポイントを逆にすると、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 DIRECTIONALの書式文字でオーバーライドを閉じるために安全です'\u202e' + text + '\u202c'。次のテキストに影響を与えることを避けるために
紅Cherniavsky-Paskin

2
おかげ😎これはかなりハックトリックと私はリンク先の記事だが賢く、それの方法は、HTML属性を使用するが、この方法で、私はちょうど私のハック😂のための文字列の連結を使用した理由を説明する詳細の多くに入る
ヨーマン

7
ところで。このマシン(win 10)のFirefoxは完全に正しくありません。右から左に書くとき、子供たちは親の後ろにいます。これらの非常に複雑な絵文字グループの修飾子を使用して、正しい方向を書くのは難しいと思います。 ..
ヨーマン

2
もう1つの楽しいエッジケース:旗の絵文字に使用される地域インジケーターシンボル。文字列「🇦🇨」(2つのコードポイントU + 1F1E6、U + 1F1E8、アセンション島の旗を作成)を取り、それを素朴に反転しようとすると、カナダの旗である「🇨🇦」が得られます。
AdamRosenfield20年

2
@yeoman参考:「UTF-16文字」(ここでこの用語を使用している場合)は、「UTF-16コードユニット」とも呼ばれます。「文字」は、多くのことを参照できるため、用語があいまいになる傾向があります(ただし、Unicodeのコンテキストでは、通常はコードポイントです)。
インクリング

39

知っている!RegExpを使用します。何がうまくいかない可能性がありますか?(読者のための演習として残された回答。)

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

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

console.log(reversed);


5
あなたの答えはお詫びに聞こえますが、正直なところ、私はこの答えを標準に近いと呼びます。同じことを手動で行おうとする他の回答よりも間違いなく優れています。文字ベースのテキスト操作は、正規表現が設計され、優れているものであり、ユニコードコンソーシアムは必要な正規表現機能(この場合、ECMAScriptが正しく実装する)を明示的に標準化します。とはいえ、結合文字(IIRC正規表現.ワイルドカードで処理する必要があります)の処理に失敗します。
KonradRudolph20年

14
で構築されていない組成物では動作しませんU+200D例えば、🏳️‍🌈。作曲されたキャラクターがエミジョイの世界の外にも存在することは注目に値します…
Holger

2
@ StevenPenny🏳️‍🌈には2つのコンポジションが含まれており、そのうちの1つはを使用していませんU+200D。🏳️‍🌈がこの回答のコードで機能しないことを確認するのは簡単です…
Holger

1
@Holgerは、🏳️‍🌈にU + 200Dで構築されていないコンポジションが含まれていることは事実ですが、U + 200Dで構築されていないコンポジションも含まれているため、かなり悪い例です。より良い例は、🧑🏻または🏳️のようなものです
Steven Penny

3
ここでの他のコメントとは逆に、ゼロ幅接合子のすべての使用が単一の書記素クラスターとして扱われるべきではありません。たとえば、Unicode 13の書記素テストの最後の3行(unicode.org/Public/13.0.0/ucd/auxiliary/GraphemeBreakTest.txt)は、ZWJの処理が異なる3つの非常によく似たケースを示しています。
マイケル・アンダーソン

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
このような「単純な質問」が一見簡単に解決できないようになったのはおかしいです。カーソンに同意します-絵文字が進化し続けるにつれて、ライブラリは更新と変更を進めてくれることを願っています。
ArnisJuraga20年

3
これは約3年間更新されていないようです。Unicode 11はその頃にリリースされましたが、その後状況が変わり、Unicode13が後でリリースされました。13では、拡張書記素ルールにいくつかの変更がありました。したがって、これが処理できないエッジケースがいくつかある可能性があります。(私は、コードを見ていませんでした-しかし、それは価値を持つように注意している)
マイケル・アンダーソン

2
@MichaelAndersonに同意します。このライブラリは、ナイーブまたは古いアルゴリズムを使用しているようです。これを適切に行うには、Unicodeで指定されている書記素セグメンテーションアルゴリズムを使用する必要があります
インクリング

21

絵文字だけでなく、他の結合文字にも問題があります。個々の文字のように感じますが、実際には1つ以上のUnicode文字であるこれらのものは、「拡張書記素クラスター」と呼ばれます。

文字列をこれらのクラスターに分割するのは注意が必要です(たとえば、これらのUnicodeドキュメントを参照してください)。私はそれを自分で実装することに依存せず、既存のライブラリを使用します。グーグルは私に書記素スプリッターライブラリを指さした。このライブラリのドキュメントには、ほとんどの実装をトリップさせるいくつかの優れた例が含まれています。

これを使用すると、次のように書くことができます。

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

ASIDE:未来からの訪問者、または最先端に住むことをいとわない訪問者のために:

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年間更新されていないようです。そのため、テーブルが最新ではない可能性があります。したがって、より最近のものを検索する必要があるかもしれません。
マイケル・アンダーソン

3
このライブラリのより最近のフォークのように見えるがで入手できますgithub.com/flmnt/graphemer
マイケル・アンダーソン

4
実際に正しい答えを見つけるために、これまで下にスクロールしなければならなかったことに驚いています。
ラムダフェアリー

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
まあ、実際にはデバッグ情報のために長いです。本当に感謝しています
HaoWu20年

1
@AndrewSavinykhコードゴルフではありませんが、よりエレガントなソリューションを探していました。ワンライナークレイジーではないかもしれませんが、覚えやすいです。以下のような正規表現ソリューション本当に良いものの私見です。
HaoWu20年

0

次を使用できます。

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

文字列をリストに変換し、逆にしてから再び文字列にする必要があります。


3
質問を読みましたか?あなたのコードはまさにOPが質問で間違っていると証明したコードです。
ワシントンゲデス

-1

const text = 'Helloworld👩‍🦰👩‍👩‍👦‍👦';

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

console.log(reversed);

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.