Jak odwrócić ciąg znaków zawierający skomplikowane emotikony?


194

Wejście:

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

Pożądane wyjście:

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

Próbowałem kilku podejść, ale żadna nie dała mi poprawnej odpowiedzi.

To się nie udało:

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

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

console.log(reversed);

Ten rodzaj działa, ale dzieli się 👩‍👩‍👦‍👦na 4 różne emoji:

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

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

console.log(reversed);

Spróbowałem też każdej odpowiedzi na to pytanie, ale żadna z nich nie działa.

Czy istnieje sposób na uzyskanie pożądanego wyniku?


26
Nie widzę problemu z drugim rozwiązaniem. czego mi brakuje?
Pedro Lima,

13
Więc te emotikony są w jakiś sposób kombinatorycznymi emoji, to całkiem interesujące. Najpierw masz emoji z twarzą kobiety, które samo w sobie jest reprezentowane przez dwie postacie, a następnie jest dodatkowy znak łączący, którym jest kod 8205, a następnie są kolejne dwa , które reprezentują „rude włosy” i te 5 znaków razem oznacza „
kobiecą

11
Myślę, że prawidłowe odwrócenie ciągu z połączonymi emoji byłoby dość skomplikowane. Musisz sprawdzić, czy po każdym emoji występuje kod 8205, a jeśli tak, musisz połączyć go z poprzednim emoji, zamiast traktować go jako własną postać. Dość skomplikowane ...
TKoL

19
Javascript wprawia mnie w zakłopotanie. To najdziwniejsza mieszanka koncepcji językowych niskiego i wysokiego poziomu. Jest na poziomie, ponieważ w pełni abstrahuje pamięć (bez wskaźników, ręczne zarządzanie pamięcią), ale jest tak niski, że traktuje łańcuchy jako głupie punkty kodowe, a nie rozszerzone klastry grafemowe. To naprawdę zagmatwane i sprawia, że ​​nigdy nie wiem, czego się spodziewać podczas pracy z tym urządzeniem.
Alexander,

12
@ Alexander-ReinstateMonica Czy jest jakiś język, który domyślnie dokonuje podziału przez podział grafemów? JS dostarcza tylko standardowe łańcuchy zakodowane w UTF-16.
światła0123

Odpowiedzi:


94

Jeśli możesz, użyj _.split()funkcji dostarczonej przez lodash . Począwszy od wersji 4.0 , _.split()jest w stanie rozdzielić emoji Unicode.

Używanie natywnego .reverse().join('')do odwracania `` znaków '' powinno działać dobrze z emoji zawierającymi łączniki o zerowej szerokości

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
W dziennikach zmian, na które wskazałeś, "v4.9.0 - Zapewniony _.split działa z emoji", myślę, że 4.0 może być za wcześnie. Komentarze w kodzie używanym do dzielenia ciągów ( github.com/lodash/lodash/blob/4.17.15/lodash.js#L261 ) odnoszą się do mathiasbynens.be/notes/javascript-unicode, który pochodzi z 2013 roku. wygląda na to, że od tamtego czasu się zmienił, ale używa dość trudnych do rozszyfrowania wielu wyrażeń regularnych Unicode. Nie widzę też żadnych testów w ich bazie kodów dla dzielenia Unicode. Wszystko to sprawiłoby, że nie chciałbym używać go w produkcji.
Michael Anderson

5
Wystarczyło trochę poszukiwań, aby stwierdzić, że to się nie udaje reverse("뎌쉐") (2 koreańskie grafemy), co daje „ᅰ셔 ᄃ” (3 grafemy).
Michael Anderson

2
Wygląda na to, że nie ma łatwego natywnego rozwiązania tego problemu. Nie wolałbym importować biblioteki tylko po to, aby rozwiązać ten problem, ale jest to rzeczywiście najbardziej niezawodny / spójny sposób, aby to zrobić w tym momencie.
Hao Wu,

1
Brawa za poprawne działanie tego programu Odwrócenie kierunku pisania w Firefoksie na Windows10 wciąż jest trochę glitchy (dzieci kończą z tyłu), więc lodash pokonał Windows 10, jak sądzę, co prawdopodobnie jest nieco niższym budżetem 😅
yeoman

54

Wziąłem pomysł TKoL na użycie \u200dpostaci i użyłem go do stworzenia mniejszego skryptu.

Uwaga: nie wszystkie kompozycje używają łącznika o zerowej szerokości, więc będzie on zawierał błędy z innymi znakami kompozycji.

Używa tradycyjnej forpętli, ponieważ pomijamy niektóre iteracje w przypadku znalezienia połączonych emotikonów. Wewnątrz forpętli znajduje się whilepętla sprawdzająca, czy jest następny \u200dznak. Dopóki istnieje jeden, dodajemy również kolejne 2 znaki i przekazujemy forpętlę 2 iteracjami, aby połączone emotikony nie zostały odwrócone.

Aby łatwo użyć go na dowolnym łańcuchu, stworzyłem go jako nową funkcję prototypową w obiekcie 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());


5
Myślałem, że kiedy przeciągasz i zaznaczasz tekst w przeglądarkach, 👩‍👩‍👦‍👦można go zaznaczyć tylko jako całość. Skąd przeglądarki wiedzą, że to jeden znak? Czy jest na to wbudowany sposób?
Hao Wu,

10
@HaoWu jest to tzw. „Segmentacja Unicode” w „Grapheme Clusters”. Twoja przeglądarka (która może korzystać z tej dostarczonej przez system operacyjny) będzie renderować i zezwalać na wybór według klastra grafemów. Specyfikację możesz przeczytać tutaj: unicode.org/reports/tr29/#Grapheme_Cluster_Boundaries
lights0123

7
@HaoWu: "Skąd przeglądarki wiedzą, że to jeden znak?" - To nie jest „jedna postać”. To wiele znaków łączących się w jedną grupę grafemów , renderowaną jako pojedynczy glif .
Jörg W Mittag

6
Tak samo jak tutaj ; nie wszystkie kompozycje używają łącznika o zerowej szerokości.
Holger

6
To właściwie nie odwraca niczego poza znakami skomponowanymi za pomocą ZWJ. Proszę, nie tylko tutaj, ale z zasady korzystaj z zewnętrznych bibliotek napisanych przez ludzi, którzy wiedzą, co robią, zamiast hakować rozwiązania na zamówienie, które działają dla jednego przypadku testowego. Biblioteki run i lodash zostały polecone w innych odpowiedziach (za które nie mogę ręczyć).
benrg

47

Odwracanie tekstu Unicode jest trudne z wielu powodów.

Po pierwsze, w zależności od języka programowania, łańcuchy są reprezentowane na różne sposoby, albo jako lista bajtów, lista jednostek kodu UTF-16 (szerokość 16 bitów, często nazywane „znakami” w API) lub jako punkty kodowe ucs4 (Szerokość 4 bajty).

Po drugie, różne interfejsy API odzwierciedlają tę wewnętrzną reprezentację w różnym stopniu. Niektórzy pracują nad abstrakcją bajtów, inni na znakach UTF-16, inni na punktach kodowych. Gdy reprezentacja używa bajtów lub znaków UTF-16, zwykle istnieją części interfejsu API, które zapewniają dostęp do elementów tej reprezentacji, a także części, które wykonują niezbędną logikę, aby uzyskać z bajtów (przez UTF-8) lub z UTF-16 znaków do rzeczywistych punktów kodowych.

Często części API wykonujące tę logikę, a tym samym dające dostęp do punktów kodowych, były dodawane później, ponieważ najpierw było 7-bitowe ascii, a nieco później wszyscy myśleli, że wystarczy 8 bitów, używając różnych stron kodowych, a nawet później, że 16 bitów wystarczyło dla Unicode. Pojęcie punktów kodowych jako liczb całkowitych bez ustalonej górnej granicy było historycznie dodawane jako czwarta wspólna długość znaku do logicznego kodowania tekstu.

Korzystanie z interfejsu API, który zapewnia dostęp do rzeczywistych punktów kodowych, wydaje się być tym. Ale...

Po trzecie, istnieje wiele modyfikatorów punktów kodowych wpływających na następny punkt kodowy lub kolejne punkty kodowe. Np. Istnieje modyfikator diakrytyczny zamieniający następujące po a w ä, e na ë i c. Obróć punkty kodowe, a aë stanie się eä, złożone z różnych liter. Istnieje bezpośrednia reprezentacja np. Ä jako własnego punktu kodowego, ale użycie modyfikatora jest równie ważne.

Po czwarte, wszystko jest w ciągłym ruchu. Wśród emoji jest również wiele modyfikatorów, jak użyto w przykładzie, a co roku jest ich więcej. Dlatego jeśli interfejs API zapewnia dostęp do informacji, czy punkt kodowy jest modyfikatorem, wersja interfejsu API określi, czy zna już określony nowy modyfikator.

Unicode zapewnia jednak sprytną sztuczkę, gdy chodzi tylko o wygląd:

Istnieją modyfikatory kierunku pisania. W przykładzie zastosowano kierunek pisania od lewej do prawej. Po prostu dodaj modyfikator kierunku pisania od prawej do lewej na początku tekstu iw zależności od wersji API / przeglądarki będzie wyglądał poprawnie odwrócony 😎

„\ u202e” nazywa się przesłonięciem od prawej do lewej, jest to najsilniejsza wersja znacznika od prawej do lewej.

Zobacz to wyjaśnienie 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 bardzo kreatywne użycie BiDi (-: Bezpieczniej jest zamknąć nadpisanie znakiem POP DIRECTIONAL FORMATTING, '\u202e' + text + '\u202c'aby uniknąć wpływu na następujący tekst.
Beni Cherniavsky-Paskin

2
Dzięki 😎 To dość hakerska sztuczka, a artykuł, do którego utworzyłem łącze, zawiera wiele szczegółów wyjaśniających, dlaczego lepiej jest używać atrybutów html, ale w ten sposób mogę po prostu użyć konkatenacji ciągów do mojego hackowania 😂
yeoman

7
Przy okazji. mój firefox na tej maszynie (wygrana 10) nie działa całkowicie poprawnie, dzieci są za rodzicami podczas pisania od prawej do lewej, myślę, że trudno jest uzyskać właściwy kierunek pisania z tymi masowo złożonymi modyfikatorami grup emoji-ludzi. ..
yeoman

2
Kolejny zabawny przypadek: regionalne symbole wskaźników używane do emoji flagowych. Jeśli weźmiesz ciąg „🇦🇨” (dwa punkty kodowe U + 1F1E6, U + 1F1E8, tworząc flagę Wyspy Wniebowstąpienia) i spróbujesz naiwnie go odwrócić, otrzymasz „🇨🇦”, flagę Kanady.
Adam Rosenfield

2
@yeoman FYI: „Znaki UTF-16” (jak używasz tego terminu) są inaczej znane jako „ jednostki kodu UTF-16 ”. Termin „znak” wydaje się być zbyt niejednoznaczny, ponieważ może odnosić się do wielu rzeczy (ale w kontekście Unicode zwykle jest to punkt kodowy).
Inkling

39

Wiem! Użyję RegExp. Co mogłoby pójść źle? (Odpowiedź pozostawiona jako ćwiczenie dla czytelnika).

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

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

console.log(reversed);


5
Twoja odpowiedź brzmi przepraszająco, ale szczerze, nazwałbym tę odpowiedź bliską kanonicznej. Jest zdecydowanie lepszy od innych odpowiedzi, które próbują zrobić to samo ręcznie. Manipulacja tekstem oparta na znakach jest tym, do czego regex jest przeznaczony i w czym wyróżnia się, a konsorcjum Unicode wyraźnie standaryzuje niezbędne funkcje wyrażeń regularnych (które w tym przypadku ECMAScript implementuje poprawnie). To powiedziawszy, nie obsługuje łączenia znaków (które wyrażenie regularne IIRC powinno obsługiwać z użyciem .symboli wieloznacznych).
Konrad Rudolph

14
Nie działa z kompozycjami niezbudowanymi U+200Dnp 🏳️‍🌈. Warto zauważyć, że skomponowane postacie istnieją również poza światem Emijoi…
Holger

2
@StevenPenny 🏳️‍🌈 zawiera dwie kompozycje, a jedna z nich nie używa U+200D. Łatwo sprawdzić, czy 🏳️‍🌈 nie działa z kodem tej odpowiedzi…
Holger

1
@Holger, chociaż prawdą jest, że 🏳️‍🌈 zawiera kompozycję niezbudowaną za pomocą U + 200D, jest to całkiem zły przykład, ponieważ zawiera również kompozycję z U + 200D. Lepszym przykładem byłoby coś takiego jak 🧑🏻 lub 🏳️
Steven Penny

3
Odwrotnie do innych komentarzy tutaj, nie każde użycie łącznika o zerowej szerokości powinno być traktowane jako pojedynczy klaster grafemowy. Na przykład ostatnie trzy wiersze testu grafemowego Unicode 13 ( unicode.org/Public/13.0.0/ucd/auxiliary/GraphemeBreakTest.txt ) pokazują trzy bardzo podobne przypadki, w których ZWJ jest traktowany inaczej.
Michael Anderson

32

Alternatywnym rozwiązaniem byłoby użycie runesbiblioteki, małego, ale skutecznego rozwiązania:

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
To najlepsza odpowiedź do ustalenia. Wszystkie te inne odpowiedzi mają przypadki, w których zawodzą, ta biblioteka (miejmy nadzieję) spełnia wszystkie przypadki skrajne.
Carson Graham

1
To zabawne, że takie „proste pytanie” na pierwszy rzut oka okazało się zadaniem trudnym do rozwiązania. Zgadzam się z Carsonem - miejmy nadzieję, że biblioteka będzie się rozwijać wraz z aktualizacjami i zmianami w miarę rozwoju emotikonów.
Arnis Juraga

3
Wygląda na to, że nie było aktualizowane od około 3 lat. Unicode 11 został wydany mniej więcej w tym czasie, ale od tego czasu wszystko się zmieniło, a Unicode 13 został wydany później. Były pewne zmiany w rozszerzonych regułach grafemów w 13. Mogą więc istnieć pewne skrajne przypadki, w których to nie rozwiązuje. (Nie przejrzałem kodu - ale warto z nim uważać)
Michael Anderson,

2
Zgadzam się z @MichaelAnderson, ta biblioteka wydaje się używać naiwnego lub starego algorytmu. Aby to zrobić poprawnie, powinien użyć algorytmu segmentacji grafemów określonego w Unicode .
Inkling

21

Masz problem nie tylko z emoji, ale także z innymi łączącymi się postaciami. Te rzeczy, które wyglądają jak pojedyncze litery, ale w rzeczywistości są jednym lub więcej znakami Unicode, nazywane są „rozszerzonymi klastrami grafemów”.

Podział łańcucha na te klastry jest trudny (na przykład zobacz te dokumenty unicode ). Nie polegałbym na samodzielnym wdrożeniu, ale korzystałbym z istniejącej biblioteki. Google wskazał mi bibliotekę rozdzielaczy grafemów . Dokumentacja tej biblioteki zawiera kilka fajnych przykładów, które mogą potykać się z większością implementacji:

Używając tego powinieneś być w stanie napisać:

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

POZA STRONĄ: Dla gości z przyszłości lub tych, którzy chcą żyć na krawędzi:

Istnieje propozycja dodania programu do segmentacji grafemów do standardu javascript. (W rzeczywistości zapewnia również inne opcje segmentacji). Obecnie znajduje się na 3 etapie przeglądu do akceptacji i jest obecnie wdrażany w JSC i V8 (patrz https://github.com/tc39/proposal-intl-segmenter/issues/114 ).

Używając tego kod wyglądałby następująco:

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

Prawdopodobnie możesz zrobić to schludniej, jeśli znasz bardziej nowoczesny javascript niż ja ...

Jest tu implementacja - ale nie wiem, czego ona wymaga.

Uwaga: wskazuje to na zabawny problem, którego inne odpowiedzi jeszcze nie rozwiązały. Segmentacja może zależeć od używanego ustawienia narodowego - nie tylko od znaków w ciągu.


1
Wygląda na to, że kod nie był aktualizowany od około 2 lat - więc jego tabele mogą być nieaktualne. Dlatego może być konieczne wyszukanie czegoś nowszego.
Michael Anderson

3
Wygląda na to, że nowsza wersja tej biblioteki jest dostępna pod adresem github.com/flmnt/graphemer
Michael Anderson

4
Dziwię się, że musiałem przewinąć tak daleko w dół, aby zobaczyć poprawną odpowiedź.
Lambda Fairy

1
Na przykład propozycji, którą możesz zrobić const graphemes = Array.from(segment_iterator, ({segment}) => segment).
Inkling

17

Po prostu postanowiłem zrobić to dla przyjemności, było to dobre wyzwanie. Nie jestem pewien, czy jest poprawny we wszystkich przypadkach, więc używaj na własne ryzyko, ale oto jest:

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
Cóż, właściwie to jest długie, ponieważ informacje o debugowaniu. Naprawdę to doceniam
Hao Wu

1
@AndrewSavinykh Nie był to kod-golf, ale szukał bardziej eleganckiego rozwiązania. Może nie jak szalona jedna linijka, ale łatwa do zapamiętania. Takie jak rozwiązanie regex jest naprawdę dobre, imho.
Hao Wu

0

Możesz użyć:

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

Powinien przekształcić Twój ciąg w listę, odwrócić go, a następnie ponownie przekształcić w ciąg.


3
Czy przeczytałeś pytanie? Twój kod jest dokładnie tym kodem, który OP okazał się błędny w pytaniu.
Washington Guedes

-1

const text = 'Witaj, świecie👩‍🦰👩‍👩‍👦‍👦';

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

console.log (odwrócona);

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.