Як змінити рядок, що містить складні смайли?


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
Я не бачу проблеми з другим рішенням. Чого мені не вистачає?
Педро Ліма,

13
Тож ці смайли насправді є якось комбінаторними смайликами, це досить цікаво. По-перше, у вас є емодзі із жіночим обличчям, яке саме представлене двома вашими персонажами, а потім є додатковий сполучний символ, який є кодом 8205, а потім є ще два , що представляють "руде волосся", і ці 5 символів разом маю на увазі "жіноче обличчя з рудим волоссям"
TKoL

11
Як мені здається, правильно змінити рядок із комбінованими смайликами було б досить складно. Вам доведеться перевірити, чи не супроводжується кожен смайлик charcode 8205, і якщо він є, вам доведеться поєднувати його з попереднім смайликом, замість того, щоб розглядати його як власний персонаж. Досить складно ...
TKoL

19
Javascript мене бентежить. Це найдивніше поєднання мовних понять низького та високого рівня. Цей рівень полягає в тому, що він повністю абстрагує пам’ять (відсутність покажчиків, ручне управління пам’яттю), але настільки низький рівень, що розглядає рядки як німі кодові точки, а не розширені кластери графем. Це справді заплутано, і це змушує мене ніколи не знати, чого чекати при роботі з цією штукою.
Олександр

12
@ Alexander-ReinstateMonica чи існує якась мова, яка виконує розбиття за допомогою графемного розбиття за замовчуванням? JS просто забезпечує стандартні рядки, закодовані в UTF-16.
lights0123

Відповіді:


94

Якщо у вас є можливість, скористайтеся _.split()функцією, наданою lodash . Починаючи з версії 4.0 , _.split()він здатний розділяти смайлики Unicode.

Використання рідного .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 ), стосуються mathiasbynens.be/notes/javascript-unicode, який починається з 2013 року. схоже, що з тих пір він просунувся, але він досить важко розшифровує безліч регулярних виразів Unicode. Я також не бачу жодного тесту в їхній кодовій базі для розділення Unicode. Все це змусило б мене насторожитися використовувати це у виробництві.
Майкл Андерсон,

5
Потрібно було лише трохи пошукати, щоб виявити, що це не вдається reverse("뎌쉐") (2 корейські графеми), що дає "ᅰ셔 ᄃ" (3 графеми).
Michael Anderson,

2
Здається, немає простого рідного рішення цієї проблеми. Не віддав би перевагу імпортувати бібліотеку лише для вирішення цієї проблеми, але це справді найбільш надійний / послідовний спосіб зробити це на даний момент.
Хао Ву

1
Похвала за те, щоб це працювало коректно. 😎 Змінення напрямку написання у Firefox на Windows10 все ще залишається незнайомим (діти потрапляють в тил), тому, здається, лодаш обіграв Windows 10, що, ймовірно, дещо нижчий бюджет 😅
yeoman

54

Я взяв ідею TKoL про використання \u200dперсонажа і використав його для спроби створити менший сценарій.

Примітка: Не всі композиції використовують столяр нульової ширини, тому він буде глючити з іншими символами композиції.

Він використовує традиційний 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());


5
Я думав, коли ви перетягуєте та виділяєте текст у браузерах, 👩‍👩‍👦‍👦можна вибрати лише ціле. Звідки браузери знають, що це один символ? Чи є вбудований спосіб це зробити?
Hao Wu

10
@HaoWu це те, що відоме як "Сегментація Unicode" у "Кластерах графем". Ваш браузер (який може використовувати той, який надає ваша ОС) збирається відтворити та дозволить виділення для кластера графем. Ви можете прочитати специфікацію тут: unicode.org/reports/tr29/#Grapheme_Cluster_Boundaries
lights0123

7
@HaoWu: "Звідки браузери знають, що це один символ?" - Це не "один персонаж". Це кілька символів, що об’єднуються, утворюючи єдиний кластер графем , переданий як єдиний гліф .
Jörg W Mittag

6
Те саме, що тут ; не всі композиції використовують столяр нульової ширини.
Holger

6
Це не коректно нічого, крім символів, складених за допомогою ZWJ. Будь ласка, не лише тут, але, як правило, використовуйте зовнішні бібліотеки, написані людьми, які знають, що вони роблять, замість того, щоб зламати замовлені рішення, які працюють для одного тестового випадку. Бібліотеки рун і лодаш були рекомендовані в інших відповідях (я не можу поручитися ні за одного).
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 на ë тощо. Поверніть кодові точки навколо, і 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 дуже креативне використання bidi (-: Безпечніше закрити заміну символом POP DIRECTIONAL FORMATING, '\u202e' + text + '\u202c'щоб не впливати на наступний текст.
Бені Чернявський-Паскін,

2
Дякую 😎 Це досить хитрий трюк, і стаття, на яку я посилався, вкладає багато деталей, пояснюючи, чому набагато розумніше використовувати атрибути html, але таким чином я міг би просто використовувати конкатенацію рядків для мого злому 😂
yeoman

7
До речі. мій firefox на цій машині (виграй 10) не розуміє цього цілком правильно, діти відстають від батьків, коли пишуть справа наліво, я думаю, важко отримати правильний напрямок написання за допомогою цих дуже складних модифікаторів груп смайлів. ..
yeoman

2
Ще один цікавий випадок: регіональні індикаторні символи, що використовуються для прапорців смайлів. Якщо взяти рядок "🇦🇨" (дві кодові точки U + 1F1E6, U + 1F1E8, роблячи прапор острова Вознесіння) і спробувати наївно змінити його, ви отримаєте "🇨🇦", прапор Канади.
Адам Розенфілд,

2
@yeoman FYI: "символи UTF-16" (як ви тут використовуєте цей термін) інакше називаються " одиницями коду UTF-16 ". "Символ", як правило, є занадто неоднозначним щодо терміна, оскільки він може стосуватися багатьох речей (але в контексті Unicode, як правило, кодової точки).
Inkling

39

Я знаю! Я буду використовувати RegExp. Що може піти не так? (Відповідь залишена як вправа для читача.)

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

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

console.log(reversed);


5
Ваша відповідь звучить вибачально, але, чесно кажучи, я б назвав цю відповідь близькою до канонічної. Це, безумовно, перевершує інші відповіді, які намагаються зробити те саме вручну. Маніпуляції з текстом на основі символів - це те, для чого призначений регулярний вираз, і він перевершує його, а консорціум Unicode чітко стандартизує необхідні функції регулярного виразу (які ECMAScript, як правило, застосовуються правильно в даному випадку). Тим не менш, він не може обробляти поєднання символів (з якими регулярний вираз IIRC повинен обробляти .символи підстановки).
Конрад Рудольф,

14
Не працює з композиціями, не побудованими U+200D, наприклад 🏳️‍🌈. Варто зазначити, що складені персонажі існують і за межами світу Emijoi ...
Холгер,

2
@StevenPenny 🏳️‍🌈 містить дві композиції, і одна з них не використовує U+200D. Неважко переконатись, що work не працює з кодом цієї відповіді ...
Холгер

1
@Holger, хоча правда, що 🏳️‍🌈 містить композицію, не побудовану з U + 200D, є досить поганим прикладом, оскільки вона також містить композицію з U + 200D. Кращим прикладом може бути щось на зразок 🧑🏻 або 🏳️
Стівен Пенні

3
Навпаки іншим коментарям тут, не кожне використання з’єднувача нульової ширини слід розглядати як єдиний кластер графем. Наприклад, останні три рядки тесту на графему Unicode 13 ( unicode.org/Public/13.0.0/ucd/auxiliary/GraphemeBreakTest.txt ) показують три дуже схожі випадки, коли ZWJ обробляється по-різному.
Майкл Андерсон,

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
Це найкраща відповідь tbh. Всі ці інші відповіді мають випадки, коли вони не вдаються, ця бібліотека (сподіваємось) відповідає всім крайовим випадкам.
Карсон Грем

1
Забавно, що таке "просте питання" на перший погляд виявилось непростим завданням для вирішення. Погодьтеся з Карсоном - сподіваємось, бібліотека рухатиметься вперед із оновленнями та змінами, оскільки Emojis постійно розвиватиметься.
Арніс Юрага

3
Схоже, це не оновлювалося близько 3 років. Приблизно в той час вийшов Unicode 11, але з того часу все змінилося, пізніше вийшов Unicode 13. Були деякі зміни в правилах розширеної графеми у 13. Тож можуть бути деякі крайні випадки, які це не обробляє. (Я не переглядав код - але з ним варто бути обережним)
Майкл Андерсон,

2
Я погоджуюсь з @MichaelAnderson, здається, ця бібліотека використовує наївний або старий алгоритм. Щоб зробити це належним чином, він повинен використовувати алгоритм сегментації графем, зазначений у Unicode .
Inkling

21

У вас виникають проблеми не тільки із смайликами, але й з іншими поєднуючими персонажами. Ці речі, які схожі на окремі літери, але насправді є одним або декількома символами унікоду, називаються "розширеними кластерами графем".

Розбивати рядок на ці кластери досить складно (наприклад, див. Ці документи Unicode ). Я б не покладався на його реалізацію сам, а використовував існуючу бібліотеку. Google вказав мені на бібліотеку роздільника графем . Документи для цієї бібліотеки містять кілька прикладних прикладів, які допоможуть втілити більшість реалізацій:

Використовуючи це, ви повинні мати можливість писати:

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

НАБОК: Для відвідувачів з майбутнього або тих, хто хоче жити на кровоточивому краю:

Існує пропозиція додати сегментатор графем до стандарту javascript. (Це насправді також надає інші варіанти сегментування). На даний момент він перебуває на етапі 3 перегляду для прийняття і в даний час впроваджений в АТ та 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,


4
Я здивований, що мені довелося прокрутити так далеко, щоб побачити відповідь, яка насправді є правильною.
Лямбда-фея

1
Для прикладу пропозиції ви можете зробити const graphemes = Array.from(segment_iterator, ({segment}) => segment).
Inkling

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
Ну, насправді це довго, тому що інформація про налагодження. Я дуже ціную це
Hao Wu

1
@AndrewSavinykh Не був кодом-гольфом, але шукав більш елегантного рішення. Можливо, не подобається божевільний однокласник, але його легко запам’ятати. Такі, як рішення регулярних виразів, є дійсно хорошим рішенням imho.
Hao Wu

0

Ви можете використовувати:

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

Він повинен перетворити ваш рядок у список, змінити його, а потім знову зробити рядком.


3
Ви читали питання? Ваш код - це саме той код, який OP виявив помилковим у питанні.
Washington Guedes,

-1

const text = 'Привіт світ👩‍🦰👩‍👩‍👦‍👦';

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

console.log (сторно);

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.