Как мне перевернуть строку UTF-8 на месте?

Недавно кто-то спросил о алгоритм для перестановки строки на месте в C. Большинство предлагаемых решений имели проблемы при работе со строками, отличными от однобайтовых. Итак, мне было интересно, какой может быть хороший алгоритм для работы со строками utf-8.

Я придумал код, который публикую как ответ, но я был бы рад увидеть идеи или предложения других людей. Я предпочел использовать реальный код, поэтому я выбрал C#, поскольку он кажется одним из самых популярных языков на этом сайте, но я не возражаю, если ваш код будет на другом языке, если это может быть разумно понимает любой, кто знаком с императивным языком. И, поскольку это предназначено для того, чтобы увидеть, как такой алгоритм может быть реализован на низком уровне (под низким уровнем я имею в виду только работу с байтами), идея состоит в том, чтобы избежать использования библиотек для основного кода.

Заметки:

Меня интересует сам алгоритм, его производительность и как его можно оптимизировать (я имею в виду оптимизацию на уровне алгоритма, а не замену i ++ на ++ i и т. д.; Меня тоже не интересуют реальные тесты).

Я не имею в виду использовать его в производственном коде или «изобретать колесо». Это просто из любопытства и в качестве упражнения.

Я использую байтовые массивы C#, поэтому предполагаю, что вы можете получить длину строки, не просматривая строку, пока не найдете NUL. То есть я не беру в расчет сложность определения длины строки. Но если вы, например, используете C, вы можете учесть это, используя strlen () перед вызовом основного кода.

Редактировать:

Как указывает Майк Ф, мой код (и код других людей, размещенный здесь) не имеет дело с составными символами. Немного информации о тех здесь. Я не знаком с этой концепцией, но если это означает, что существуют «комбинирующие символы», т. Е. Символы / кодовые точки, которые действительны только в сочетании с другими «базовыми» символами / кодовыми точками, таблица поиска таких символы могут использоваться для сохранения порядка «глобального» символа («базовый» + «комбинирующий» символы) при реверсировании.

Это забавный вопрос, но для того, чтобы полезно перевернул строку Unicode (UTF8 или иначе), вам нужно позаботиться о сохранении порядка составных символов, а также о манипулировании байтами.

Mike F 14.10.2008 04:17

Спасибо за внимание. Я не знал составных персонажей. Я сначала поищу это.

Juan Pablo Califano 14.10.2008 04:30
Включение UTF-8 в jsPDF с помощью Angular
Включение UTF-8 в jsPDF с помощью Angular
Привет, разработчики, я предполагаю, что вы уже знаете, как экспортировать pdf через jsPDF. Если ответ отрицательный, то вы можете ознакомиться с моей...
13
2
9 893
5
Перейти к ответу Данный вопрос помечен как решенный

Ответы 5

Ответ принят как подходящий

Я бы сделал один проход, меняющий байты, затем второй проход, который меняет байты в любых многобайтовых символах (которые легко обнаруживаются в UTF8) обратно в их правильный порядок.

Вы определенно можете справиться с этим в очереди за один проход, но я бы не стал беспокоиться, если рутина не станет узким местом.

К сожалению, это не решение для каждого языка. Например, во время второго прохода, когда вы пытаетесь использовать DecodeRune, вы получаете неправильное количество байтов для каждого multibyte characters. Конечно, это легко исправить, просто поменяйте порядок обратных вызовов методов. Сначала переверните байты в многобайтовых символах, а затем в массив байтов.

s7anley 23.08.2016 07:22

Мой первоначальный подход можно резюмировать следующим образом:

1) Обратить байты наивно

2) Запустите строку в обратном направлении и исправьте последовательности utf8 по мере продвижения.

На втором этапе рассматриваются недопустимые последовательности, а на первом этапе мы проверяем, является ли строка «синхронизированной» (то есть начинается ли она с допустимого ведущего байта).

Обновлено: улучшенная проверка для ведущего байта в Reverse ()

class UTF8Utils {


    public static void Reverse(byte[] str) {
        int len = str.Length;
        int i   = 0;
        int j   = len - 1;

        //  first, check if the string is "synced", i.e., it starts
        //  with a valid leading character. Will check for illegal 
        //  sequences thru the whole string later.
        byte leadChar = str[0];

        //  if it starts with 10xx xxx, it's a trailing char...
        //  if it starts with 1111 10xx or 1111 110x 
        //  it's out of the 4 bytes range.
    //  EDIT: added validation for 7 bytes seq and 0xff
        if ( (leadChar & 0xc0) == 0x80 ||
            (leadChar & 0xfc) == 0xf8 ||
            (leadChar & 0xfe) == 0xfc ||
        (leadChar & 0xff) == 0xfe ||
        leadChar == 0xff) {

            throw new Exception("Illegal UTF-8 sequence");

        }

        //  reverse bytes in-place naïvely
        while(i < j) {
            byte tmp = str[i];
            str[i]  = str[j];
            str[j]  = tmp;
            i++;
            j--;
        }
        //  now, run the string again to fix the multibyte sequences
        UTF8Utils.ReverseMbSequences(str);

    }

    private static void ReverseMbSequences(byte[] str) {
        int i = str.Length - 1;
        byte leadChar = 0;
        int nBytes  = 0;

        //  loop backwards thru the reversed buffer
        while(i >= 0) {
            //  since the first byte in the unreversed buffer is assumed to be
            //  the leading char of that byte, it seems safe to assume that the  
            //  last byte is now the leading char. (Given that the string is
            //  not out of sync -- we checked that out already)
            leadChar = str[i];

            //  check how many bytes this sequence takes and validate against
            //  illegal sequences
            if (leadChar < 0x80) {
                nBytes = 1;
            } else if ((leadChar & 0xe0) == 0xc0) {
                if ((str[i-1] & 0xc0) != 0x80) {
                    throw new Exception("Illegal UTF-8 sequence");
                }
                nBytes = 2;
            } else if ((leadChar & 0xf0) == 0xe0) {
                if ((str[i-1] & 0xc0) != 0x80 ||
                    (str[i-2] & 0xc0) != 0x80 ) {
                    throw new Exception("Illegal UTF-8 sequence");
                }
                nBytes = 3;
            } else if ((leadChar & 0xf8) == 0xf0) {
                if ((str[i-1] & 0xc0) != 0x80 ||
                    (str[i-2] & 0xc0) != 0x80 ||
                    (str[i-3] & 0xc0) != 0x80  ) {
                    throw new Exception("Illegal UTF-8 sequence");
                }
                nBytes = 4;
            } else {
                throw new Exception("Illegal UTF-8 sequence");
            }

            //  now, reverse the current sequence and then continue
            //  whith the next one
            int back    = i;
            int front   = back - nBytes + 1;

            while(front < back) {
                byte tmp = str[front];
                str[front] = str[back];
                str[back] = tmp;
                front++;
                back--;
            }
            i -= nBytes;
        }
    }
} 

Лучшее решение:

  1. Преобразовать в широкую строку символов
  2. Переверните новую строку

Никогда, никогда, никогда, никогда не относитесь к отдельным байтам как к символам.

Я согласен, что это, вероятно, лучшее решение в «реальном» коде (с использованием приличной библиотеки). Но меня интересует, как бы вы это сделали, если бы вам пришлось делать это на месте.

Juan Pablo Califano 14.10.2008 02:40

Это не работает по многим причинам. Даже ради этой надуманной проблемы UTF-8 может представлять символы, длина которых в UTF-16 превышает два байта.

Jim Puls 14.10.2008 02:49

Джим: найдите <a href="kerneltrap.org/man/linux/man0p/stddef.h.0p">man stddef.h</a> - в этом комментарии нет места для определения wchar_t, но я прочитал это так, что если среда компиляции поддерживает кодировку, например, с 6-байтовой кодировкой, wchar_t должен быть> = 6 байт.

gnud 14.10.2008 03:01

gnud, вероятно, Джим прав, но в любом случае это не значит, что мне нужно писать код для реальной задачи, я просто хотел бы увидеть, как можно лучше всего решить эту проблему с учетом этих ограничений. Это просто самообразование.

Juan Pablo Califano 14.10.2008 03:12

Да, я понял твой ангел - в конце концов =). Но приравнивать UTF-16 к wchar_t так же неправильно, как приравнивать символ к 8 битам. В моей системе wchar_t составляет 32 бита. И поскольку я читал стандарт, каждый wchar_t гарантированно содержит один символ.

gnud 14.10.2008 13:28

Согласитесь, что ваш подход - единственный разумный способ сделать это на месте.

Лично мне не нравится повторная проверка UTF8 внутри каждой функции, которая с ним работает, и обычно делаю только то, что необходимо, чтобы избежать сбоев; это дает намного меньше кода. Не знаю много C#, так что вот он на C:

(отредактировано для устранения strlen)

void reverse( char *start, char *end )
{
    while( start < end )
    {
        char c = *start;
        *start++ = *end;
        *end-- = c;
    }
}

char *reverse_char( char *start )
{
    char *end = start;
    while( (end[1] & 0xC0) == 0x80 ) end++;
    reverse( start, end );
    return( end+1 );
}

void reverse_string( char *string )
{
    char *end = string;
    while( *end ) end = reverse_char( end );
    reverse( string, end-1 );
}

Что ж, отсутствие проверки - это нормально, если вы делаете это заранее в другом месте. Я просто добавил туда проверку, так как не предполагал, что это будет действительная строка, и все равно проверял ведущие байты, поэтому добавлял несколько условий. Не эксперт по C & указателям, но идею я понял. Спасибо.

Juan Pablo Califano 14.10.2008 03:24

Отлично сделано, MikeF. Кстати: вы, наверное, забыли char *start= string; в начале reverse_string.

tzot 14.10.2008 03:50

Соотечественник, но вырос в Великобритании. Кстати, я только что заметил, что правильное реверсирование строки Unicode на самом деле включает в себя также сохранение порядка составных символов: p

Mike F 14.10.2008 04:15

Определенно! Особенно для UTF-16, поскольку я никогда не видел закодированной строки UTF-32 с суррогатными символами. (Я поддерживаю базу данных байтовых строк / кодировок / языка, используемых для определения кодировки и языка входных строк; подумайте о файлах субтитров).

tzot 14.10.2008 04:30

Этот код предполагает, что входная строка UTF-8 действительна и правильно сформирована (т.е. не более 4 байтов на многобайтовый символ):

#include "string.h"

void utf8rev(char *str)
{
    /* this assumes that str is valid UTF-8 */
    char    *scanl, *scanr, *scanr2, c;

    /* first reverse the string */
    for (scanl= str, scanr= str + strlen(str); scanl < scanr;)
        c= *scanl, *scanl++= *--scanr, *scanr= c;

    /* then scan all bytes and reverse each multibyte character */
    for (scanl= scanr= str; c= *scanr++;) {
        if ( (c & 0x80) == 0) // ASCII char
            scanl= scanr;
        else if ( (c & 0xc0) == 0xc0 ) { // start of multibyte
            scanr2= scanr;
            switch (scanr - scanl) {
                case 4: c= *scanl, *scanl++= *--scanr, *scanr= c; // fallthrough
                case 3: // fallthrough
                case 2: c= *scanl, *scanl++= *--scanr, *scanr= c;
            }
            scanr= scanl= scanr2;
        }
    }
}

// quick and dirty main for testing purposes
#include "stdio.h"

int main(int argc, char* argv[])
{
    char buffer[256];
    buffer[sizeof(buffer)-1]= '\0';

    while (--argc > 0) {
        strncpy(buffer, argv[argc], sizeof(buffer)-1); // don't overwrite final null
        printf("%s → ", buffer);
        utf8rev(buffer);
        printf("%s\n", buffer);
    }
    return 0;
}

Если вы скомпилируете эту программу (пример имени: so199260.c) и запустите ее в среде UTF-8 (в данном случае это установка Linux):

$ so199260 γεια και χαρά français АДЖИ a♠♡♢♣b
a♠♡♢♣b → b♣♢♡♠a
АДЖИ → ИЖДА
français → siaçnarf
χαρά → άραχ
και → ιακ
γεια → αιεγ

Если код слишком загадочный, я с радостью поясню.

Аккуратный! Но как работает регистр трехбайтовых символов? Кроме того, я думаю, что это будет проще, если вы сначала перевернете отдельные символы.

Mike F 14.10.2008 03:57

Трехбайтовый символ работает с одной заменой (байты [0] и [2]), [1] не требует замены. Прошу прощения за загадочный код, в течение многих лет я кодирую на Python, и весь C-код, который я пишу, предназначен для сред с ограниченным объемом памяти с не очень умными компиляторами, поэтому я стараюсь сильно оптимизировать размер кода.

tzot 14.10.2008 03:58

Да, ваш метод намного проще; в моем коде, если я переворачиваю строку в конце (пропуская вызов strlen), тогда мой процесс реверсирования символов требует рефакторинга.

tzot 14.10.2008 04:06

Ну да, я на мгновение почесал в затылке, пытаясь выяснить аргумент переключателя (scanr - scanl) и то, что вы делали в каждом случае, но теперь я понял. Спасибо.

Juan Pablo Califano 14.10.2008 04:15

Другие вопросы по теме