Как изменить строку UTF-8?
Недавно кто-то спросил о алгоритме для изменения строки в C. Большинство предложенных решений имели проблемы при работе с однобайтовыми строками. Итак, мне было интересно, что может быть хорошим алгоритмом для конкретного использования строк utf-8.
Я придумал какой-то код, который я отправляю в качестве ответа, но я был бы рад увидеть другие идеи или предложения других людей. Я предпочел использовать фактический код, поэтому я выбрал С#, поскольку он, кажется, является одним из самых популярных языков на этом сайте, но я не против, если ваш код на другом языке, если он может быть разумным понимаемый кем-либо, кто знаком с императивным языком. И поскольку это предназначено для того, чтобы увидеть, как такой алгоритм может быть реализован на низком уровне (по низкому уровню, я имею в виду только байты), идея состоит в том, чтобы избежать использования библиотек для основного кода.
Примечания:
Меня интересует сам алгоритм, его производительность и как его можно оптимизировать (я имею в виду оптимизацию на уровне алгоритма, не заменяя я ++ на ++ я и т.д., я тоже не очень заинтересован в реальных тестах).
Я не хочу на самом деле использовать его в производственном коде или "изобретать колесо". Это просто из любопытства и как упражнение.
Я использую массивы байтов на С#, поэтому я предполагаю, что вы можете получить длину строки, не запуская строку, пока не найдете NUL.
То есть, я не учитываю сложность нахождения длины строки. Но, если вы используете C, например, вы можете определить это с помощью функции strlen() перед вызовом основного кода.
Edit:
Как указывает Майк Ф, мой код (и код других людей, размещенный здесь) не имеет отношения к составным символам. Некоторая информация о тех здесь. Я не знаком с этой концепцией, но если это означает, что существуют "комбинированные символы", то есть символы/кодовые точки, которые действительны только в сочетании с другими "базовыми" символами/кодовыми точками, смотровая таблица таких символы могут использоваться для сохранения порядка "глобального" символа ( "базовый" + "комбинирование" символов) при обращении.
Ответы
Ответ 1
Я сделал бы один проход, изменяя байты, а затем второй проход, который меняет байты в любых многобайтовых символах (которые легко обнаруживаются в UTF8) обратно в их правильный порядок.
Вы можете определенно справиться с этим в очереди за один проход, но я бы не стал беспокоиться, если бы рутина не стала узким местом.
Ответ 2
Этот код предполагает, что входная строка 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
χαρά → άραχ
και → ιακ
γεια → αιεγ
Если код слишком загадочный, я с радостью уточню.
Ответ 3
Согласитесь, что ваш подход является единственным разумным способом сделать это на месте.
Лично мне не нравится переопределять UTF8 внутри каждой функции, которая с ним связана, и обычно делать только то, что необходимо для предотвращения сбоев; он добавляет намного меньше кода. Dunno much С#, так что вот он в 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 );
}
Ответ 4
Мой первоначальный подход мог бы суммироваться следующим образом:
1) Наименее обратные байты
2) Запустите строку назад и исправьте последовательности utf8 по мере продвижения.
Недействительные последовательности рассматриваются на втором шаге, и на первом этапе мы проверяем, находится ли строка в "sync" (то есть, если она начинается с левого ведущего байта).
EDIT: улучшенная проверка для старшего байта в 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 a trailing char...
// if it starts with 1111 10xx or 1111 110x
// it 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;
}
}
}
Ответ 5
Лучшее решение:
- Преобразовать в широкую строку char
- Обратить новую строку
Никогда, никогда, никогда, никогда не рассматривайте одиночные байты как символы.