Como posso iterar pelos pontos de código Unicode de uma string Java?

105

Eu sei String#codePointAt(int), mas é indexado pelo chardeslocamento, não pelo deslocamento do ponto de código.

Estou pensando em tentar algo como:

Mas minhas preocupações são

  • Não tenho certeza se os pontos de código que estão naturalmente na faixa de substitutos elevados serão armazenados como dois charvalores ou um
  • esta parece uma maneira terrivelmente cara de iterar pelos personagens
  • alguém deve ter inventado algo melhor.
rampion
fonte

Respostas:

143

Sim, Java usa uma codificação UTF-16-esque para representações internas de Strings, e, sim, ele codifica caracteres fora do Basic Multilingual Plane ( BMP ) usando o esquema de substituição.

Se você sabe que estará lidando com caracteres fora do BMP, esta é a maneira canônica de iterar os caracteres de uma string Java:

final int length = s.length();
for (int offset = 0; offset < length; ) {
   final int codepoint = s.codePointAt(offset);

   // do something with the codepoint

   offset += Character.charCount(codepoint);
}
Jonathan Feinberg
fonte
2
Quanto a ser ou não "caro", bem ... não há outra maneira de construir em Java. Mas se você está lidando apenas com scripts latinos / europeus / cirílicos / gregos / hebraicos / árabes, basta usar s.charAt () o quanto quiser. :)
Jonathan Feinberg
24
Mas você não deveria. Por exemplo, se seu programa gera XML e se alguém fornece algum operador matemático obscuro, de repente seu XML pode ser inválido.
Caracol mecânico de
2
Eu teria usado offset = s.offsetByCodePoints(offset, 1);. Existe algum benefício em usar em offset += Character.charCount(codepoint);vez disso?
Paul Groke de
3
@Mechanicalsnail Não entendi seu comentário. Por que a saída de XML faria com que essa resposta se comportasse mal?
Gili,
3
@Gili, a resposta é boa. Ele estava se referindo ao comentário de @Jonathan Feinberg no qual ele defende o uso do charAt()que é uma má ideia
RecursiveExceptionException
72

Java 8 adicionado, CharSequence#codePointsque retorna um IntStreamcontendo os pontos de código. Você pode usar o stream diretamente para iterar sobre eles:

string.codePoints().forEach(c -> ...);

ou com um loop for coletando o stream em uma matriz:

for(int c : string.codePoints().toArray()){
    ...
}

Essas formas são provavelmente mais caras do que a solução de Jonathan Feinbergs , mas são mais rápidas de ler / gravar e a diferença de desempenho geralmente será insignificante.

Alex - GlassEditor.com
fonte
3
for (int c : (Iterable<Integer>) () -> string.codePoints().iterator())também funciona.
saka1029
2
Versão ligeiramente mais curta do código @ saka1029: s:for (int c : (Iterable<Integer>) string.codePoints()::iterator) ...
Lii
7

Pensei em adicionar um método de solução alternativa que funcione com loops foreach ( ref ), além de poder convertê-lo para o novo método String # codePoints do java 8 facilmente ao mudar para o java 8:

Você pode usá-lo com foreach assim:

 for(int codePoint : codePoints(myString)) {
   ....
 }

Aqui está o método auxiliar:

public static Iterable<Integer> codePoints(final String string) {
  return new Iterable<Integer>() {
    public Iterator<Integer> iterator() {
      return new Iterator<Integer>() {
        int nextIndex = 0;
        public boolean hasNext() {
          return nextIndex < string.length();
        }
        public Integer next() {
          int result = string.codePointAt(nextIndex);
          nextIndex += Character.charCount(result);
          return result;
        }
        public void remove() {
          throw new UnsupportedOperationException();
        }
      };
    }
  };
}

Ou, alternativamente, se você quiser apenas converter uma string em uma matriz de int (que pode usar mais RAM do que a abordagem acima):

 public static List<Integer> stringToCodePoints(String in) {
    if( in == null)
      throw new NullPointerException("got null");
    List<Integer> out = new ArrayList<Integer>();
    final int length = in.length();
    for (int offset = 0; offset < length; ) {
      final int codepoint = in.codePointAt(offset);
      out.add(codepoint);
      offset += Character.charCount(codepoint);
    }
    return out;
  }

Felizmente, usa "codePoints" para lidar com o par substituto do UTF-16 (representação de string interna do java).

rogerdpack
fonte