Усечение строк по байтам
Я создаю следующее Для усечения строки в 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;
EDIT: я просто удалил ссылку UTF8, потому что я бы предпочел будьте в состоянии сделать это для различных типов хранения, а также.
12 ответов
почему бы не преобразовать в байты и не идти вперед-повинуясь границам символов 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
более разумное решение использует декодер:
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();
Я думаю, что решение Рекса Керра имеет 2 ошибки.
- во-первых, он будет усекать, чтобы ограничить+1, Если символ не ASCII находится непосредственно перед пределом. Усечение "123456789á1" приведет к "123456789á", который представлен в 11 символах в UTF-8. 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 байт они выполняют довольно почти то же самое если вы снова создаете строку из массива байтов.
обратите внимание, что оба метода предполагают допустимый ввод utf-8, который является допустимым предположением после использования функции getBytes() Java.
используйте кодировщик UTF-8 и кодируйте, пока выходной ByteBuffer не содержит столько байтов, сколько вы готовы взять, ища CoderResult.ПЕРЕПОЛНЕНИЕ.
второй подход здесь работает хорошо http://www.jroller.com/holy/entry/truncating_utf_string_to_the
Как уже отмечалось, решение Питера Лоури имеет большой недостаток производительности (~3,500 msc для 10,000 раз), Rex Kerr был намного лучше (~500msc для 10,000 раз), но результат не был точным - он вырезал намного больше, чем ему нужно (вместо оставшихся 4000 байт он остается 3500 для некоторых примеров). прикрепленное здесь мое решение (~250msc для 10,000 раз), предполагая, что максимальная длина UTF-8 в байтах равна 4 (Спасибо Википедии):
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;
}
вы можете преобразовать строку в байты и преобразовать только эти байты обратно в строку.
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();
}
используя регулярное выражение ниже, вы также можете удалить начальное и конечное пробелы двухбайтового символа.
stringtoConvert = stringtoConvert.replaceAll("^[\s ]*", "").replaceAll("[\s ]*$", "");
Это мое :
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;
}
}
Это не может быть более эффективным решением, но работает
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;
}
я улучшил решение Питера Лоури, чтобы точно обрабатывать суррогатные пары. Кроме того, я оптимизирован исходя из того, что максимальное количество байтов на 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;
}