Недавно кто-то спросил о алгоритм для перестановки строки на месте в C. Большинство предлагаемых решений имели проблемы при работе со строками, отличными от однобайтовых. Итак, мне было интересно, какой может быть хороший алгоритм для работы со строками utf-8.
Я придумал код, который публикую как ответ, но я был бы рад увидеть идеи или предложения других людей. Я предпочел использовать реальный код, поэтому я выбрал C#, поскольку он кажется одним из самых популярных языков на этом сайте, но я не возражаю, если ваш код будет на другом языке, если это может быть разумно понимает любой, кто знаком с императивным языком. И, поскольку это предназначено для того, чтобы увидеть, как такой алгоритм может быть реализован на низком уровне (под низким уровнем я имею в виду только работу с байтами), идея состоит в том, чтобы избежать использования библиотек для основного кода.
Заметки:
Меня интересует сам алгоритм, его производительность и как его можно оптимизировать (я имею в виду оптимизацию на уровне алгоритма, а не замену i ++ на ++ i и т. д.; Меня тоже не интересуют реальные тесты).
Я не имею в виду использовать его в производственном коде или «изобретать колесо». Это просто из любопытства и в качестве упражнения.
Я использую байтовые массивы C#, поэтому предполагаю, что вы можете получить длину строки, не просматривая строку, пока не найдете NUL. То есть я не беру в расчет сложность определения длины строки. Но если вы, например, используете C, вы можете учесть это, используя strlen () перед вызовом основного кода.
Редактировать:
Как указывает Майк Ф, мой код (и код других людей, размещенный здесь) не имеет дело с составными символами. Немного информации о тех здесь. Я не знаком с этой концепцией, но если это означает, что существуют «комбинирующие символы», т. Е. Символы / кодовые точки, которые действительны только в сочетании с другими «базовыми» символами / кодовыми точками, таблица поиска таких символы могут использоваться для сохранения порядка «глобального» символа («базовый» + «комбинирующий» символы) при реверсировании.
Спасибо за внимание. Я не знал составных персонажей. Я сначала поищу это.

Я бы сделал один проход, меняющий байты, затем второй проход, который меняет байты в любых многобайтовых символах (которые легко обнаруживаются в UTF8) обратно в их правильный порядок.
Вы определенно можете справиться с этим в очереди за один проход, но я бы не стал беспокоиться, если рутина не станет узким местом.
К сожалению, это не решение для каждого языка. Например, во время второго прохода, когда вы пытаетесь использовать DecodeRune, вы получаете неправильное количество байтов для каждого multibyte characters. Конечно, это легко исправить, просто поменяйте порядок обратных вызовов методов. Сначала переверните байты в многобайтовых символах, а затем в массив байтов.
Мой первоначальный подход можно резюмировать следующим образом:
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;
}
}
}
Лучшее решение:
Никогда, никогда, никогда, никогда не относитесь к отдельным байтам как к символам.
Я согласен, что это, вероятно, лучшее решение в «реальном» коде (с использованием приличной библиотеки). Но меня интересует, как бы вы это сделали, если бы вам пришлось делать это на месте.
Это не работает по многим причинам. Даже ради этой надуманной проблемы UTF-8 может представлять символы, длина которых в UTF-16 превышает два байта.
Джим: найдите <a href="kerneltrap.org/man/linux/man0p/stddef.h.0p">man stddef.h</a> - в этом комментарии нет места для определения wchar_t, но я прочитал это так, что если среда компиляции поддерживает кодировку, например, с 6-байтовой кодировкой, wchar_t должен быть> = 6 байт.
gnud, вероятно, Джим прав, но в любом случае это не значит, что мне нужно писать код для реальной задачи, я просто хотел бы увидеть, как можно лучше всего решить эту проблему с учетом этих ограничений. Это просто самообразование.
Да, я понял твой ангел - в конце концов =). Но приравнивать UTF-16 к wchar_t так же неправильно, как приравнивать символ к 8 битам. В моей системе wchar_t составляет 32 бита. И поскольку я читал стандарт, каждый wchar_t гарантированно содержит один символ.
Согласитесь, что ваш подход - единственный разумный способ сделать это на месте.
Лично мне не нравится повторная проверка 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 & указателям, но идею я понял. Спасибо.
Отлично сделано, MikeF. Кстати: вы, наверное, забыли char *start= string; в начале reverse_string.
Соотечественник, но вырос в Великобритании. Кстати, я только что заметил, что правильное реверсирование строки Unicode на самом деле включает в себя также сохранение порядка составных символов: p
Определенно! Особенно для UTF-16, поскольку я никогда не видел закодированной строки UTF-32 с суррогатными символами. (Я поддерживаю базу данных байтовых строк / кодировок / языка, используемых для определения кодировки и языка входных строк; подумайте о файлах субтитров).
Этот код предполагает, что входная строка 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
χαρά → άραχ
και → ιακ
γεια → αιεγ
Если код слишком загадочный, я с радостью поясню.
Аккуратный! Но как работает регистр трехбайтовых символов? Кроме того, я думаю, что это будет проще, если вы сначала перевернете отдельные символы.
Трехбайтовый символ работает с одной заменой (байты [0] и [2]), [1] не требует замены. Прошу прощения за загадочный код, в течение многих лет я кодирую на Python, и весь C-код, который я пишу, предназначен для сред с ограниченным объемом памяти с не очень умными компиляторами, поэтому я стараюсь сильно оптимизировать размер кода.
Да, ваш метод намного проще; в моем коде, если я переворачиваю строку в конце (пропуская вызов strlen), тогда мой процесс реверсирования символов требует рефакторинга.
Ну да, я на мгновение почесал в затылке, пытаясь выяснить аргумент переключателя (scanr - scanl) и то, что вы делали в каждом случае, но теперь я понял. Спасибо.
Это забавный вопрос, но для того, чтобы полезно перевернул строку Unicode (UTF8 или иначе), вам нужно позаботиться о сохранении порядка составных символов, а также о манипулировании байтами.