2008-09-23 16 views

Respuesta

21

Aquí es un simple bucle que cuenta cuán grande es la representación UTF-8 va a ser, y trunca cuando se excede:

public static String truncateWhenUTF8(String s, int maxBytes) { 
    int b = 0; 
    for (int i = 0; i < s.length(); i++) { 
     char c = s.charAt(i); 

     // ranges from http://en.wikipedia.org/wiki/UTF-8 
     int skip = 0; 
     int more; 
     if (c <= 0x007f) { 
      more = 1; 
     } 
     else if (c <= 0x07FF) { 
      more = 2; 
     } else if (c <= 0xd7ff) { 
      more = 3; 
     } else if (c <= 0xDFFF) { 
      // surrogate area, consume next char as well 
      more = 4; 
      skip = 1; 
     } else { 
      more = 3; 
     } 

     if (b + more > maxBytes) { 
      return s.substring(0, i); 
     } 
     b += more; 
     i += skip; 
    } 
    return s; 
} 

Este hace manejar surrogate pairs que aparecen en la cadena de entrada. El codificador UTF-8 de Java (correctamente) genera parejas sustitutas como una secuencia de 4 bytes en lugar de dos secuencias de 3 bytes, por lo que truncateWhenUTF8() devolverá la cadena truncada más larga que pueda. Si ignora los pares suplentes en la implementación, las cadenas truncadas pueden acortarse de lo necesario.

no he hecho un montón de pruebas en ese código, pero aquí hay algunas pruebas preliminares:

private static void test(String s, int maxBytes, int expectedBytes) { 
    String result = truncateWhenUTF8(s, maxBytes); 
    byte[] utf8 = result.getBytes(Charset.forName("UTF-8")); 
    if (utf8.length > maxBytes) { 
     System.out.println("BAD: our truncation of " + s + " was too big"); 
    } 
    if (utf8.length != expectedBytes) { 
     System.out.println("BAD: expected " + expectedBytes + " got " + utf8.length); 
    } 
    System.out.println(s + " truncated to " + result); 
} 

public static void main(String[] args) { 
    test("abcd", 0, 0); 
    test("abcd", 1, 1); 
    test("abcd", 2, 2); 
    test("abcd", 3, 3); 
    test("abcd", 4, 4); 
    test("abcd", 5, 4); 

    test("a\u0080b", 0, 0); 
    test("a\u0080b", 1, 1); 
    test("a\u0080b", 2, 1); 
    test("a\u0080b", 3, 3); 
    test("a\u0080b", 4, 4); 
    test("a\u0080b", 5, 4); 

    test("a\u0800b", 0, 0); 
    test("a\u0800b", 1, 1); 
    test("a\u0800b", 2, 1); 
    test("a\u0800b", 3, 1); 
    test("a\u0800b", 4, 4); 
    test("a\u0800b", 5, 5); 
    test("a\u0800b", 6, 5); 

    // surrogate pairs 
    test("\uD834\uDD1E", 0, 0); 
    test("\uD834\uDD1E", 1, 0); 
    test("\uD834\uDD1E", 2, 0); 
    test("\uD834\uDD1E", 3, 0); 
    test("\uD834\uDD1E", 4, 4); 
    test("\uD834\uDD1E", 5, 4); 

} 

Actualizado ejemplo de código modificado, que ahora se encarga de los pares suplentes.

+0

UTF-8 puede codificar cualquier UCS2 personaje en 3 bytes o menos. Verifique la página a la que hace referencia. Sin embargo, si desea cumplir con UCS4 o UTF16 (que pueden hacer referencia a todo el conjunto de caracteres), deberá permitir caracteres de hasta 6 bytes en UTF8. – billjamesdev

+0

Proyecto de ley: vea la discusión de CESU-8 en la página de wikipedia. Según entiendo, se supone que UTF-8 codifica parejas sustitutas como una sola secuencia de 4 bytes, no como dos secuencias de 3 bytes. –

+0

No es 2 de tres bytes, es una secuencia de hasta 6 bytes para almacenar UCS4, que es un carácter completo de 31 bits, no 2 "pares" de 16 bits (eso es UTF16). Secuencia de 6 bytes = 1111110C 10CCCCCC 10CCCCCC 10CCCCCC 10CCCCCC 10CCCCCC donde las C son bits de datos. En este momento, solo se utilizan suficientes caracteres para necesitar 4 bytes. – billjamesdev

9

La codificación UTF-8 tiene un rasgo perfecto que le permite ver en qué lugar de un byte se encuentra.

verifique la transmisión con el límite de caracteres que desee.

  • Si su bit alto es 0, es un carácter de byte único, simplemente reemplázalo por 0 y todo irá bien.
  • Si su bit alto es 1 y también lo es el siguiente bit, entonces estás al inicio de un char de varios bytes, así que simplemente establece ese byte en 0 y estás bien.
  • Si el bit alto es 1 pero el siguiente bit es 0, entonces estás en el medio de un personaje, retrocedes por el buffer hasta que alcances un byte que tiene 2 o más 1s en los bits altos, y reemplazas ese byte con 0.

Ejemplo: Si su flujo es: 31 33 31 C1 A3 32 33 00, puede hacer su cadena 1, 2, 3, 5, 6 o 7 bytes, pero no 4 , ya que eso pondría el 0 después de C1, que es el inicio de un char de múltiples bytes.

+0

http://java.sun.com/j2se/1.5.0/docs/api/java/io/DataInput.html#modified-utf-8 explica la codificación UTF-8 modificado utilizado por Java y demuestra por qué esta respuesta es correcta. – Alexander

+1

Por cierto, esta solución (la única factura @Bill James) es mucho más eficiente que la respuesta aceptada actualmente por @Matt Quail, porque la primera requiere que pruebe 3 bytes como máximo, mientras que la segunda requiere que pruebe todos los caracteres en el texto. – Alexander

+1

Alexander: el primero requiere que * primero convierta la cadena a UTF8 *, lo que requiere iterar sobre todos los caracteres del texto. –

19

Debe usar CharsetEncoder, el simple getBytes() + copie todos los que pueda para cortar los charts UTF-8 a la mitad.

Algo como esto:

public static int truncateUtf8(String input, byte[] output) { 

    ByteBuffer outBuf = ByteBuffer.wrap(output); 
    CharBuffer inBuf = CharBuffer.wrap(input.toCharArray()); 

    Charset utf8 = Charset.forName("UTF-8"); 
    utf8.newEncoder().encode(inBuf, outBuf, true); 
    System.out.println("encoded " + inBuf.position() + " chars of " + input.length() + ", result: " + outBuf.position() + " bytes"); 
    return outBuf.position(); 
} 
+2

Esto funcionó muy bien para mí, probablemente menos eficiente, pero mucho más difícil de equivocar, y funciona para cualquier conjunto de caracteres. Funciona muy bien con una 'cadena nueva (salida, 0, output.length - returnValue, CHARSET)' – ojrac

+0

La solución @ sigget es similar y además devuelve la cadena truncada real, en lugar de solo la longitud –

3

Puede calcular el número de bytes sin realizar ninguna conversión.

foreach character in the Java string 
    if 0 <= character <= 0x7f 
    count += 1 
    else if 0x80 <= character <= 0x7ff 
    count += 2 
    else if 0x800 <= character <= 0xd7ff // excluding the surrogate area 
    count += 3 
    else if 0xdc00 <= character <= 0xffff 
    count += 3 
    else { // surrogate, a bit more complicated 
    count += 4 
    skip one extra character in the input stream 
    } 

Usted tendría que detectar pares suplentes (D800-DBFF y U + DC00-U + DFFF) y contar 4 bytes para cada par sustituto válido. Si obtiene el primer valor en el primer rango y el segundo en el segundo, todo está bien, omítelos y agregue 4. Pero si no es así, entonces es un par sustituto no válido. No estoy seguro de cómo Java trata con eso, pero su algoritmo tendrá que hacer un conteo correcto en ese caso (poco probable).

9

Esto es lo que se me ocurrió, utiliza API Java estándar por lo que debe ser seguro y compatible con todas las rarezas Unicode y pares de sustitución, etc.La solución se toma desde http://www.jroller.com/holy/entry/truncating_utf_string_to_the con comprobaciones agregadas para null y para evitar la decodificación cuando la cadena tiene menos bytes que maxBytes.

/** 
* Truncates a string to the number of characters that fit in X bytes avoiding multi byte characters being cut in 
* half at the cut off point. Also handles surrogate pairs where 2 characters in the string is actually one literal 
* character. 
* 
* Based on: http://www.jroller.com/holy/entry/truncating_utf_string_to_the 
*/ 
public static String truncateToFitUtf8ByteLength(String s, int maxBytes) { 
    if (s == null) { 
     return null; 
    } 
    Charset charset = Charset.forName("UTF-8"); 
    CharsetDecoder decoder = charset.newDecoder(); 
    byte[] sba = s.getBytes(charset); 
    if (sba.length <= maxBytes) { 
     return s; 
    } 
    // Ensure truncation by having byte buffer = maxBytes 
    ByteBuffer bb = ByteBuffer.wrap(sba, 0, maxBytes); 
    CharBuffer cb = CharBuffer.allocate(maxBytes); 
    // Ignore an incomplete character 
    decoder.onMalformedInput(CodingErrorAction.IGNORE) 
    decoder.decode(bb, cb, true); 
    decoder.flush(cb); 
    return new String(cb.array(), 0, cb.position()); 
} 
+0

'CharBuffer.allocate (maxBytes)' asigna demasiado. ¿Podría ser 'CharBuffer.allocate (s.length())'? –

Cuestiones relacionadas