2008-10-13 16 views
13

Recientemente, alguien preguntó acerca de algorithm for reversing a string in place in C. La mayoría de las soluciones propuestas tenían problemas cuando se trata de cadenas que no son de un solo byte. Entonces, me preguntaba qué podría ser un buen algoritmo para tratar específicamente con cadenas de utf-8.¿Cómo invierto una cadena UTF-8 en su lugar?

Se me ocurrió algo de código, que estoy publicando como respuesta, pero estaría encantado de ver las ideas o sugerencias de otras personas. Preferí usar el código actual, así que elegí C#, ya que parece ser uno de los idiomas más populares en este sitio, pero no me importa si tu código está en otro idioma, siempre que pueda ser razonable. entendido por cualquiera que esté familiarizado con un lenguaje imperativo. Y, como esto pretende ver cómo un algoritmo de este tipo podría implementarse a bajo nivel (por bajo nivel, me refiero a tratar con bytes), la idea es evitar el uso de bibliotecas para el código central.

Notas:

Estoy interesado en el propio algoritmo, su rendimiento y como no podía ser optimizado (me refiero a la optimización a nivel de algoritmo, no reemplazar i ++ ++ con I y tal, yo soy tampoco está realmente interesado en los puntos de referencia reales).

No me refiero a usarlo realmente en el código de producción o "reinventar la rueda". Esto es solo por curiosidad y como ejercicio.

Estoy usando matrices de bytes C, así que supongo que puedes obtener la longitud de la cadena sin ejecutar la cadena hasta que encuentres una NUL. Es decir, no estoy explicando la complejidad de encontrar la longitud de la cadena. Pero si estás usando C, por ejemplo, puedes factorizarlo usando strlen() antes de llamar al código central.

Editar:

Como Mike F señala, mi código (y el código de otras personas publicado aquí) no está tratando con caracteres compuestos. Algo de información sobre esos here. No estoy familiarizado con el concepto, pero si eso significa que hay "caracteres combinados", es decir, caracteres/puntos de código que solo son válidos en combinación con otros caracteres/puntos de código "base", una tabla de consulta de tales los caracteres se pueden usar para conservar el orden del carácter "global" ("base" + "combinación" de caracteres) al invertir.

+2

Esta es una pregunta divertida, pero para invertir * de manera útil * una cadena Unicode (UTF8 u otra), debe preocuparse de conservar el orden de los caracteres compuestos, así como de hacer malabarear con los bytes. –

+0

Gracias por el aviso. No estaba al tanto de los caracteres compuestos. Buscaré eso primero. –

Respuesta

12

Haría una pasada al invertir los bytes, luego una segunda pasada que invierte los bytes en cualquier carácter multibyte (que se detectan fácilmente en UTF8) de nuevo a su orden correcto.

Definitivamente puede manejar esto en línea en una sola pasada, pero no me molestaría a menos que la rutina se convirtiera en un cuello de botella.

+0

Sí, eso es lo que pensé. Gracias. –

+0

Lamentablemente, no es la solución para todos los idiomas. Por ejemplo, en go, en el segundo paso, cuando intenta 'DecodeRune' obtiene un número incorrecto de bytes para cada 'caracteres multibyte'. Por supuesto, hay una solución fácil para él, simplemente cambie el orden de las llamadas al método inverso. En primer lugar, invierta los bytes en caracteres multibyte y luego en la matriz de bytes enteros. – s7anley

5

Mi planteamiento inicial podría resumirse de esta manera por:

1) bytes inversas ingenuamente

2) Ejecutar la cadena hacia atrás y corregir las secuencias utf8 a medida que avanza.

Las secuencias ilegales se tratan en el segundo paso y en el primer paso, verificamos si la cadena está en "sincronización" (es decir, si comienza con un byte líder legal).

EDIT: mejora de la validación de dirigir byte en reversa()

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; 
     } 
    } 
} 
-2

La mejor solución:

  1. Convertir a una amplia cadena de carbón
  2. Invertir la nueva cadena

Nunca, nunca, nunca, nunca trate bytes únicos como caracteres.

+0

Estoy de acuerdo en que es probablemente la mejor solución en código "real" (eso o usando una biblioteca decente). Pero estoy interesado en cómo lo harías si tuvieras que hacerlo en su lugar. –

+0

Eso no funciona por muchas razones. Incluso por el bien de este problema inventado, UTF-8 puede representar caracteres que terminan siendo más de dos bytes en UTF-16. –

+0

Jim: busque man stddef.h - no hay espacio para la definición de wchar_t en este comentario, pero lo leí en el sentido de que si el entorno de compilación admite un juego de caracteres con, por ejemplo, Codificación de 6 bytes, wchar_t debe ser> = 6 bytes. – gnud

6

De acuerdo en que su enfoque es la única manera sensata de hacerlo en el lugar.

Personalmente, no me gusta revalidar UTF8 dentro de cada función que se ocupa de él, y generalmente solo hago lo que se necesita para evitar bloqueos; agrega mucho menos código. Dunno mucho C# así que aquí está en C:

(editado para eliminar 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); 
} 
+0

Bueno, no validar que está bien si lo haces por adelantado en otro lugar. Acabo de agregar la validación allí, ya que no asumí que sería una cadena válida y estaba comprobando los bytes principales de todas formas, así que estaba agregando algunas condiciones. No soy un experto en C & punteros, pero entiendo la idea.Gracias. –

+0

Bien hecho, MikeF. Por cierto: es probable que hayas olvidado un 'char * start = string;' al comienzo de 'reverse_string'. – tzot

+0

Ουπς ... ευχαριστώ. –

7

Este código se supone que la entrada de UTF-8 cadena es válida y bien formadas (es decir, en la mayoría de 4 bytes por varios bytes de caracteres):

#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; 
} 

Si compila este programa (ejemplo nombre: so199260.c) y ejecutarlo en un entorno UTF-8 (una instalación de Linux en este caso):

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

Si el código es demasiado críptico, con mucho gusto lo aclararé.

+0

¡Aseado! Pero, ¿cómo funciona el caso de caracteres de 3 bytes? Además, creo que resulta más sencillo si primero inviertes los caracteres individuales. –

+1

El carácter de tres bytes funciona con un solo intercambio (bytes [0] y [2]), [1] no necesita intercambio. Lamento el código críptico, durante años codifiqué en Python y todo el código C que escribo es para entornos con memoria limitada con compiladores no tan inteligentes, por lo que tiendo a optimizar mucho el tamaño del código. – tzot

+0

Sí, su método es mucho más simple; en mi código, si invierto la cadena al final (omitiendo la llamada de strlen), entonces mi proceso de inversión de caracteres necesita una refactorización. – tzot

Cuestiones relacionadas