Преобразование UTF-16 в ASCII в Java

Игнорируя это все это время, я в настоящее время заставляю себя больше узнать о юникоде в Java. Мне нужно выполнить упражнение по преобразованию строки UTF-16 в 8-битный ASCII. Может кто-нибудь просветить меня, как это сделать на Java? Я понимаю, что вы не можете представить все возможные значения Unicode в ASCII, поэтому в этом случае я хочу, чтобы код, превышающий 0xFF, просто добавлялся в любом случае (неверные данные также должны просто добавляться молча).

Спасибо!


person His    schedule 29.09.2009    source источник
comment
добавили??? Вы имеете ввиду выбросили? Отброшено?   -  person Stephen C    schedule 29.09.2009
comment
Извините за неясность в первую очередь. На самом деле, я сам не слишком ясен. Упражнение в книге, которую я читал, говорит только о том, что код, превышающий 0xFF, должен быть просто приведен к байту и добавлен в любом случае (плохие данные должны добавляться молча).   -  person His    schedule 29.09.2009
comment
0xFF не является допустимым значением для символа ASCII. ASCII является 7-битным, поэтому максимально допустимое значение — 0x7F.   -  person Joachim Sauer    schedule 29.09.2009


Ответы (5)


Как насчет этого:

String input = ... // my UTF-16 string
StringBuilder sb = new StringBuilder(input.length());
for (int i = 0; i < input.length(); i++) {
    char ch = input.charAt(i);
    if (ch <= 0xFF) {
        sb.append(ch);
    }
}

byte[] ascii = sb.toString().getBytes("ISO-8859-1"); // aka LATIN-1

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

Кстати, строго говоря, нет такого набора символов, как 8-битный ASCII. ASCII — это 7-битный набор символов. LATIN-1 ближе всего к «8-битному набору символов ASCII» (и блок 0 Unicode эквивалентен LATIN-1), поэтому я предполагаю, что вы имеете в виду именно это.

РЕДАКТИРОВАТЬ: в свете обновления вопроса решение еще проще:

String input = ... // my UTF-16 string
byte[] ascii = new byte[input.length()];
for (int i = 0; i < input.length(); i++) {
    ascii[i] = (byte) input.charAt(i);
}

Это решение является более эффективным. Поскольку теперь мы знаем, сколько байтов ожидать, мы можем предварительно выделить массив байтов и скопировать (усеченные) символы без использования StringBuilder в качестве промежуточного буфера.

Однако я не уверен, что такое обращение с неверными данными разумно.

РЕДАКТИРОВАТЬ 2: с этим есть еще одна неясная ошибка. Unicode фактически определяет кодовые точки (символы) как «примерно 21-битные» значения ... от 0x000000 до 0x10FFFF ... и использует суррогаты для представления кодов> 0x00FFFF. Другими словами, кодовая точка Unicode> 0x00FFFF фактически представлена ​​​​в UTF-16 как два «символа». Ни мой ответ, ни какой-либо другой не учитывают этот (по общему признанию эзотерический) момент. На самом деле, работать с кодовыми точками > 0x00FFFF в Java довольно сложно. Это связано с тем, что 'char' является 16-битным типом, а String определяется в терминах 'char'.

РЕДАКТИРОВАТЬ 3: возможно, более разумным решением для работы с неожиданными символами, которые не преобразуются в ASCII, является замена их стандартным символом замены:

String input = ... // my UTF-16 string
byte[] ascii = new byte[input.length()];
for (int i = 0; i < input.length(); i++) {
    char ch = input.charAt(i);
    ascii[i] = (ch <= 0xFF) ? (byte) ch : (byte) '?';
}
person Stephen C    schedule 29.09.2009
comment
В свете Edit 2 выше, можем ли мы не отметить это как решение? Это не решение, поэтому его не следует помечать как таковое. - person rplankenhorn; 17.12.2012
comment
@rplankenhorn - На самом деле, поскольку проблема действительно заключается в принудительном преобразовании Unicode в ASCII, любая версия преобразования является адекватным решением даже перед лицом суррогатов. В первой версии любая кодовая единица ›= FF будет удалена. Во второй версии любая кодовая единица ›= FF будет добавлена ​​в любом случае ... это то, о чем явно просил ОП. (Не то чтобы я думаю, что это разумный подход.) - person Stephen C; 19.10.2016

Вы можете использовать java.nio для простого решения:

// first encode the utf-16 string as a ByteBuffer
ByteBuffer bb = Charset.forName("utf-16").encode(CharBuffer.wrap(utf16str));
// then decode those bytes as US-ASCII
CharBuffer ascii = Charset.forName("US-ASCII").decode(bb);
person Gunslinger47    schedule 29.09.2009

Java внутренне представляет строки в UTF-16. Если вы начинаете с объекта String, вы можете кодировать с помощью String.getBytes(Charset c), где вы можете указать US-ASCII (который может отображать кодовые точки 0x00-0x7f) или ISO-8859-1 ( который может отображать кодовые точки 0x00-0xff и может быть тем, что вы подразумеваете под «8-битным ASCII»).

Что касается добавления "плохих данных"... Строки ASCII или ISO-8859-1 просто не могут представлять значения за пределами определенного диапазона. Я считаю, что getBytes просто отбросит символы, которые он не может представить в целевом наборе символов.

person Phil    schedule 29.09.2009
comment
Я считаю, что getBytes просто отбросит символы, которые он не может представить в целевом наборе символов. Это зависит от массива байтов замены по умолчанию для набора символов... согласно Javadoc. - person Stephen C; 29.09.2009
comment
Я также столкнулся с этим в Javadoc, но я не смог найти ничего о том, как реализованы объекты Charset по умолчанию. Знаете ли вы, что на самом деле происходит, когда вы вызываете, скажем, Charset.forName(US-ASCII)? - person Phil; 29.09.2009

Поскольку это упражнение, похоже, вам нужно реализовать это вручную. Вы можете думать о кодировке (например, UTF-16 или ASCII) как о таблице поиска, которая сопоставляет последовательность байтов с логическим символом (кодовой точкой).

Java использует строки UTF-16, что означает, что любой код может быть представлен одной или двумя переменными char. Хотите ли вы обрабатывать суррогатные пары two-char, зависит от того, насколько вероятно, по вашему мнению, ваше приложение столкнется с ними (см. класс символов для их обнаружения). ASCII использует только первые 7 бит октета (байта), поэтому допустимый диапазон значений от 0 до 127. UTF-16 использует идентичные значения для этого диапазона (они просто шире). Это можно подтвердить с помощью этого кода:

Charset ascii = Charset.forName("US-ASCII");
byte[] buffer = new byte[1];
char[] cbuf = new char[1];
for (int i = 0; i <= 127; i++) {
  buffer[0] = (byte) i;
  cbuf[0] = (char) i;
  String decoded = new String(buffer, ascii);
  String utf16String = new String(cbuf);
  if (!utf16String.equals(decoded)) {
    throw new IllegalStateException();
  }
  System.out.print(utf16String);
}
System.out.println("\nOK");

Поэтому вы можете преобразовать UTF-16 в ASCII, приведя char к byte.

Подробнее о кодировке символов Java можно прочитать здесь< /а>.

person McDowell    schedule 29.09.2009

Просто чтобы оптимизировать принятый ответ и не платить штраф, если строка уже содержит все символы ascii, вот оптимизированная версия. Спасибо @stephen-c

public static String toAscii(String input) {
  final int length = input.length();
  int ignoredChars = 0;
  byte[] ascii = null;
  for (int i = 0; i < length; i++) {
    char ch = input.charAt(i);
    if (ch > 0xFF) {
      //-- ignore this non-ascii character
      ignoredChars++;
      if (ascii == null) {
        //-- first non-ascii character. Create a new ascii array with all ascii characters
        ascii = new byte[input.length() - 1];  //-- we know, the length will be at less by at least 1
        for (int j = 0; j < i-1; j++) {
          ascii[j] = (byte) input.charAt(j);
        }
      }
    } else if (ascii != null) {
      ascii[i - ignoredChars] = (byte) ch;
    }
  }
  //-- (ignoredChars == 0) is the same as (ascii == null) i.e. no non-ascii characters found
  return ignoredChars == 0 ? input : new String(Arrays.copyOf(ascii, length - ignoredChars));
}
person Ari Singh    schedule 11.06.2021