2010-08-26 2 views
8

Я создаю следующее для обрезания строки в java для новой строки с заданным количеством байтов.Усечение строк по байтам

 String truncatedValue = ""; 
     String currentValue = string; 
     int pivotIndex = (int) Math.round(((double) string.length())/2); 
     while(!truncatedValue.equals(currentValue)){ 
      currentValue = string.substring(0,pivotIndex); 
      byte[] bytes = null; 
      bytes = currentValue.getBytes(encoding); 
      if(bytes==null){ 
       return string; 
      } 
      int byteLength = bytes.length; 
      int newIndex = (int) Math.round(((double) pivotIndex)/2); 
      if(byteLength > maxBytesLength){ 
       pivotIndex = newIndex; 
      } else if(byteLength < maxBytesLength){ 
       pivotIndex = pivotIndex + 1; 
      } else { 
       truncatedValue = currentValue; 
      } 
     } 
     return truncatedValue; 

Это первое, что мне пришло в голову, и я знаю, что смогу улучшить его. Я видел еще одно сообщение, которое задавало аналогичный вопрос, но они усекали строки, используя байты вместо String.substring. Думаю, я предпочел бы использовать String.substring в моем случае.

EDIT: Я просто удалил ссылку UTF8, потому что я предпочел бы сделать это для разных типов хранения.

+0

Я бы перефразировать вашу проблему. Вы пытаетесь поместить строку в массив байтов, который не может превышать maxUTF8BytesLength. Вы хотите использовать UTF-8 для кодирования. Вы хотите скопировать как можно больше символов. Верный? – gawi

+0

Правильно, я бы сказал, что это правильно. Я также хотел бы сделать это эффективно. – stevebot

+0

Я только что редактировал вопрос, чтобы не ссылаться на UTF-8. Извините, что это вводит в заблуждение. – stevebot

ответ

11

Почему бы не преобразовать в байты и идти вперед - подчиняясь границам символов UTF8, как вы это делаете, до тех пор, пока вы не получите максимальное число, а затем переместите эти байты обратно в строку?

Или вы можете просто вырезать исходную строку, если вы отслеживать, где должно происходить срез:

// Assuming that Java will always produce valid UTF8 from a string, so no error checking! 
// (Is this always true, I wonder?) 
public class UTF8Cutter { 
    public static String cut(String s, int n) { 
    byte[] utf8 = s.getBytes(); 
    if (utf8.length < n) n = utf8.length; 
    int n16 = 0; 
    int advance = 1; 
    int i = 0; 
    while (i < n) { 
     advance = 1; 
     if ((utf8[i] & 0x80) == 0) i += 1; 
     else if ((utf8[i] & 0xE0) == 0xC0) i += 2; 
     else if ((utf8[i] & 0xF0) == 0xE0) i += 3; 
     else { i += 4; advance = 2; } 
     if (i <= n) n16 += advance; 
    } 
    return s.substring(0,n16); 
    } 
} 

Примечание: отредактированный для исправления ошибок на 2014-08-25

+1

Я определенно мог бы это сделать. Есть ли причина, почему использование String.substring хуже? Кажется, что делать это так, как вы описываете, придется учитывать все кодовые точки, что не очень весело. (в зависимости от вашего определения удовольствия :)). – stevebot

+0

@stevebot - Чтобы быть эффективными, вам нужно использовать известную структуру данных. Если вы не заботитесь об эффективности и хотите, чтобы это было легко, или вы хотите поддерживать все возможные кодировки Java, не зная, что это такое, ваш метод кажется достаточно разумным. –

1

вы могли бы преобразовать строку в байты и преобразовать только эти байты в строку.

public static String substring(String text, int maxBytes) { 
    StringBuilder ret = new StringBuilder(); 
    for(int i = 0;i < text.length(); i++) { 
     // works out how many bytes a character takes, 
     // and removes these from the total allowed. 
     if((maxBytes -= text.substring(i, i+1).getBytes().length) < 0) break; 
     ret.append(text.charAt(i)); 
    } 
    return ret.toString(); 
} 
+0

Проверьте один за другим символ может быть не очень хорошо для производительности – NguyenDat

+2

@nguyendat, есть много причин, по которым это не очень хорошо. Главным было бы создание объекта для substring() и getBytes() Однако вы были бы удивлены, сколько вы можете сделать за миллисекунду, и этого обычно достаточно. –

+1

Этот метод не обрабатывает суррогатные пары должным образом, например. substring ("\ uD800 \ uDF30 \ uD800 \ uDF30", 4) .getBytes ("UTF-8"). length вернет 8, а не 4. Половина суррогатной пары представлена ​​как однобайтная "?" по String.getBytes ("UTF-8"). –

3

Используйте UTF-8 CharsetEncoder и закодировать до выхода ByteBuffer содержит столько байт, сколько вы готовы взять на себя, ища CoderResult.OVERFLOW.

2

Как уже отмечалось, решение Peter Lawrey имеет большую производительность недостаток (~ 3,500msc для 10000 раз), Рекс Керр был намного лучше (~ 500msc 10000 раз), но результат не был точным - он разрезал намного больше, чем нужно (вместо оставшегося 4000 байт он остался 3500 для некоторого примера). прилагается здесь мое решение (~ 250msc 10000 раз), предполагая, что UTF-8 Максимальная длина полукокса в байтах составляет 4 (спасибо WikiPedia):

public static String cutWord (String word, int dbLimit) throws UnsupportedEncodingException{ 
    double MAX_UTF8_CHAR_LENGTH = 4.0; 
    if(word.length()>dbLimit){ 
     word = word.substring(0, dbLimit); 
    } 
    if(word.length() > dbLimit/MAX_UTF8_CHAR_LENGTH){ 
     int residual=word.getBytes("UTF-8").length-dbLimit; 
     if(residual>0){ 
      int tempResidual = residual,start, end = word.length(); 
      while(tempResidual > 0){ 
       start = end-((int) Math.ceil((double)tempResidual/MAX_UTF8_CHAR_LENGTH)); 
       tempResidual = tempResidual - word.substring(start,end).getBytes("UTF-8").length; 
       end=start; 
      } 
      word = word.substring(0, end); 
     } 
    } 
    return word; 
} 
+0

Не похоже, что это решение предотвращает пару с половиной суррогатной пары? Во-вторых, в случае getBytes() длина будет применяться к обеим половинам суррогатной пары отдельно (не сразу очевидная для меня, она никогда не будет), она также недооценила бы размер представления UTF-8 пары в целом, предполагая, что «байтовый массив замены» представляет собой один байт. В-третьих, для 4-байтовых кодовых точек UTF-8 всем требуется суррогатная пара с двумя символами в Java, так что максимум max составляет всего 3 байта на Java-символ. –

0

s = new String(s.getBytes("UTF-8"), 0, MAX_LENGTH - 2, "UTF-8");

5

Я думаю, что решение Rex Керра имеет 2 ошибки.

  • Во-первых, он усекает ограничение + 1, если символ не ASCII находится непосредственно перед лимитом. Усечение «1234567891» приведет к «123456789», который представлен в 11 символах в UTF-8.
  • Во-вторых, я думаю, что он неверно истолковал стандарт UTF. https://en.wikipedia.org/wiki/UTF-8#Description показывает, что 110xxxxx в начале последовательности UTF сообщает нам, что представление имеет длину 2 символа (в отличие от 3). Именно по этой причине его реализация обычно не использует все свободное пространство (как отметил Ниссим Абитан).

Пожалуйста, найдите свою исправленную версию ниже:

public String cut(String s, int charLimit) throws UnsupportedEncodingException { 
    byte[] utf8 = s.getBytes("UTF-8"); 
    if (utf8.length <= charLimit) { 
     return s; 
    } 
    int n16 = 0; 
    boolean extraLong = false; 
    int i = 0; 
    while (i < charLimit) { 
     // Unicode characters above U+FFFF need 2 words in utf16 
     extraLong = ((utf8[i] & 0xF0) == 0xF0); 
     if ((utf8[i] & 0x80) == 0) { 
      i += 1; 
     } else { 
      int b = utf8[i]; 
      while ((b & 0x80) > 0) { 
       ++i; 
       b = b << 1; 
      } 
     } 
     if (i <= charLimit) { 
      n16 += (extraLong) ? 2 : 1; 
     } 
    } 
    return s.substring(0, n16); 
} 

я до сих пор думал, что это далеко не эффективным.Так что если вы на самом деле не нужны строковое представление результата и массив будет делать, вы можете использовать это:

private byte[] cutToBytes(String s, int charLimit) throws UnsupportedEncodingException { 
    byte[] utf8 = s.getBytes("UTF-8"); 
    if (utf8.length <= charLimit) { 
     return utf8; 
    } 
    if ((utf8[charLimit] & 0x80) == 0) { 
     // the limit doesn't cut an UTF-8 sequence 
     return Arrays.copyOf(utf8, charLimit); 
    } 
    int i = 0; 
    while ((utf8[charLimit-i-1] & 0x80) > 0 && (utf8[charLimit-i-1] & 0x40) == 0) { 
     ++i; 
    } 
    if ((utf8[charLimit-i-1] & 0x80) > 0) { 
     // we have to skip the starter UTF-8 byte 
     return Arrays.copyOf(utf8, charLimit-i-1); 
    } else { 
     // we passed all UTF-8 bytes 
     return Arrays.copyOf(utf8, charLimit-i); 
    } 
} 

Забавно, что с реалистическим пределом 20-500 байт они выполняют в значительной степени тот же IF вы снова создаете строку из массива байтов.

Обратите внимание, что оба метода предполагают допустимый ввод utf-8, который является допустимым предположением после использования функции getBytes() Java.

+0

Вы также должны поймать UnsupportedEncodingException в s.getBytes («UTF-8») – asalamon74

+0

Я не вижу, как getBytes бросает что-нибудь. Хотя http://docs.oracle.com/javase/7/docs/api/java/lang/String.html#getBytes%28java.lang.String%29 говорит «Поведение этого метода, когда эта строка не может быть закодирована в данной кодировке не указывается ». –

+1

Связанная вами страница показывает, что она выбрасывает UnsupportedEncodingException: «public byte [] getBytes (String charsetName) throws UnsupportedEncodingException« – asalamon74

0

Это мой:

private static final int FIELD_MAX = 2000; 
private static final Charset CHARSET = Charset.forName("UTF-8"); 

public String trancStatus(String status) { 

    if (status != null && (status.getBytes(CHARSET).length > FIELD_MAX)) { 
     int maxLength = FIELD_MAX; 

     int left = 0, right = status.length(); 
     int index = 0, bytes = 0, sizeNextChar = 0; 

     while (bytes != maxLength && (bytes > maxLength || (bytes + sizeNextChar < maxLength))) { 

      index = left + (right - left)/2; 

      bytes = status.substring(0, index).getBytes(CHARSET).length; 
      sizeNextChar = String.valueOf(status.charAt(index + 1)).getBytes(CHARSET).length; 

      if (bytes < maxLength) { 
       left = index - 1; 
      } else { 
       right = index + 1; 
      } 
     } 

     return status.substring(0, index); 

    } else { 
     return status; 
    } 
} 
0

При использовании ниже регулярных выражений, также вы можете удалить ведущие и завершающие пробелы двойного символа байта.

stringtoConvert = stringtoConvert.replaceAll("^[\\s ]*", "").replaceAll("[\\s ]*$", ""); 
0

Это не может быть более эффективным решением, но работает

public static String substring(String s, int byteLimit) { 
    if (s.getBytes().length <= byteLimit) { 
     return s; 
    } 

    int n = Math.min(byteLimit-1, s.length()-1); 
    do { 
     s = s.substring(0, n--); 
    } while (s.getBytes().length > byteLimit); 

    return s; 
} 
5

Более здравомыслящий решение использует декодер:

final Charset CHARSET = Charset.forName("UTF-8"); // or any other charset 
final byte[] bytes = inputString.getBytes(CHARSET); 
final CharsetDecoder decoder = CHARSET.newDecoder(); 
decoder.onMalformedInput(CodingErrorAction.IGNORE); 
decoder.reset(); 
final CharBuffer decoded = decoder.decode(ByteBuffer.wrap(bytes, 0, limit)); 
final String outputString = decoded.toString(); 
0

я улучшил решение Питера Lawrey к точно обрабатывать суррогатные пары. Кроме того, я оптимизированной основан на том, что максимальное количество байтов в char в UTF-8 кодировке составляет 3.

public static String substring(String text, int maxBytes) { 
    for (int i = 0, len = text.length(); (len - i) * 3 > maxBytes;) { 
     int j = text.offsetByCodePoints(i, 1); 
     if ((maxBytes -= text.substring(i, j).getBytes(StandardCharsets.UTF_8).length) < 0) 
      return text.substring(0, i); 
     i = j; 
    } 
    return text; 
} 
Смежные вопросы