Усечение строк в байтах - PullRequest
       9

Усечение строк в байтах

8 голосов
/ 26 августа 2010

Я создаю следующее для усечения строки в 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 в моем случае.

РЕДАКТИРОВАТЬ: Я только что удалил ссылку на UTF8, потому что я бы предпочел сделать это для различных типов хранения.

Ответы [ 12 ]

13 голосов
/ 26 августа 2010

Почему бы не преобразовать в байты и идти вперед - соблюдая границы символов 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

5 голосов
/ 05 августа 2015

Более разумное решение использует декодер:

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();
5 голосов
/ 27 июля 2013

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

  • Во-первых, он усекает до предела + 1, если не-ASCII символ находится непосредственно перед пределом. Усечение «123456789» 1 приведет к «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);
}

Я все еще думал, что это далеко не эффективно. Поэтому, если вам действительно не нужно String представление результата и байтовый массив подойдет, вы можете использовать это:

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 байт они выполняют почти одинаково ЕСЛИ вы снова создаете строку из массива байтов.

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

3 голосов
/ 02 декабря 2011

Второй подход здесь работает хорошо http://www.jroller.com/holy/entry/truncating_utf_string_to_the

3 голосов
/ 25 апреля 2011

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

2 голосов
/ 23 мая 2013

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

2 голосов
/ 31 января 2013

Как уже отмечалось, решение Питера Лори имеет существенный недостаток в производительности (~ 3500 мсек для 10 000 раз), Rex Kerr был намного лучше (~ 500 мсек для 10 000 раз), но результат не был точным - он сократил намного больше, чем нужно (вместо этого из оставшихся 4000 байтов он остаётся 3500 для некоторого примера). приложил мое решение (~ 250 мсек для 10 000 раз), предполагая, что максимальная длина символа 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;
}
1 голос
/ 28 августа 2010

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

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 голосов
/ 10 декабря 2016

Я улучшил решение Питера Лори для точной обработки суррогатных пар.Кроме того, я оптимизировал, основываясь на том факте, что максимальное число байтов на 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;
}
0 голосов
/ 06 февраля 2015

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

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