2008-09-23 4 views

ответ

21

Вот простой цикл, который подсчитывает как большой представление UTF-8 будет, и обрезает при превышении:

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

Это Ручка surrogate pairs, которые отображаются во входной строке. Кодировщик UTF-8 Java (правильно) выводит суррогатные пары как одну 4-байтную последовательность вместо двух 3-байтовых последовательностей, поэтому truncateWhenUTF8() вернет самую длинную усеченную строку, которую он может. Если вы игнорируете суррогатные пары в реализации, то усеченные строки могут быть закорочены, чем нужно.

Я не сделал много испытаний на этот код, но вот некоторые предварительные испытания:

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); 

} 

Обновлено Модифицированный пример кода, теперь он обрабатывает суррогатных пар.

9

Кодирование UTF-8 имеет аккуратную характеристику, которая позволяет вам видеть, где находится в байтовом наборе.

Проверьте поток в соответствии с лимитом, который вы хотите.

  • Если его старший бит равен 0, это однобайтовый символ, просто замените его на 0, и вы в порядке.
  • Если его старший бит равен 1, а значит, следующий бит, то вы в начале многобайтового символа, поэтому просто установите этот байт в 0 и вы добры.
  • Если старший бит равен 1, а следующий бит равен 0, то вы находитесь в середине символа, пройдите назад по буфере, пока не нажмете байт, у которого есть два или более 1 с в высоких битах, и замените этот байт с 0.

Пример: Если ваш поток: 31 33 31 C1 A3 32 33 00, вы можете сделать свою строку длиной 1, 2, 3, 5, 6 или 7 байт, но не 4 , поскольку это положило бы 0 после C1, что является началом многобайтового символа.

+0

http://java.sun.com/j2se/1.5.0/docs/api/java/io/DataInput.html#modified-utf-8 объясняет модифицированный UTF-8 кодировку, используемую Java и демонстрирует, почему этот ответ правильный. – Alexander 2008-09-23 11:08:20

+1

Кстати, это решение (один счет @Bill James) намного эффективнее, чем принятый в настоящее время ответ by @Matt Quail, потому что первое требует, чтобы вы тестировали максимум 3 байта, тогда как последнее требует, чтобы вы проверили все символы в текст. – Alexander 2008-09-23 17:32:22

+1

Александр: первый требует, чтобы вы сначала конвертировали строку в UTF8 *, которая требует итерации по всем символам в тексте. – 2008-09-24 00:02:54

19

Вы должны использовать CharsetEncoder, простую копию getBytes() +, так как вы можете вырезать UTF-8 charcters пополам.

Что-то вроде этого:

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(); 
} 
3

Вы можете вычислить количество байтов без какого-либо преобразования.

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 
    } 

Вы должны обнаружить суррогатные пары (D800-DBFF и U + DC00-U + DFFF) и считать 4 байта для каждой действительной суррогатной пары. Если вы получите первое значение в первом диапазоне, а второе во втором диапазоне, все в порядке, пропустите их и добавьте 4. Но если нет, то это недействительная суррогатная пара. Я не уверен, как Java справляется с этим, но ваш алгоритм должен будет делать правильный подсчет в этом (маловероятном) случае.

9

Вот что я придумал, он использует стандартные Java API, поэтому должен быть безопасным и совместимым со всеми странностями юникода и суррогатными парами и т. Д.Решение взято из http://www.jroller.com/holy/entry/truncating_utf_string_to_the с проверками, добавленными для null, и для избежания декодирования, когда строка меньше байтов, чем 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()); 
}