Comment puis-je tronquer une chaîne de caractères java pour s'adapter à un nombre donné d'octets, une fois encodé UTF-8?

Comment puis-je tronquer un java String de sorte que je sais qu'il s'adaptera dans un nombre donné d'octets de stockage une fois qu'il est encodé UTF-8?

27
demandé sur hippietrail 2008-09-23 10:03:21

5 réponses

Voici une boucle simple qui compte quelle sera la taille de la représentation UTF-8 et qui se tronque lorsqu'elle est dépassée:

public static String truncateWhenUTF8(String s, int maxBytes) {
    int b = 0;
    for (int i = 0; i < s.length(); i++) {
        char c = s.charAt(i);

        // ranges from http://en.wikipedia.org/wiki/UTF-8
        int skip = 0;
        int more;
        if (c <= 0x007f) {
            more = 1;
        }
        else if (c <= 0x07FF) {
            more = 2;
        } else if (c <= 0xd7ff) {
            more = 3;
        } else if (c <= 0xDFFF) {
            // surrogate area, consume next char as well
            more = 4;
            skip = 1;
        } else {
            more = 3;
        }

        if (b + more > maxBytes) {
            return s.substring(0, i);
        }
        b += more;
        i += skip;
    }
    return s;
}

Ce ne poignée les paires de substitution qui apparaissent dans la chaîne d'entrée. L'encodeur UTF-8 de Java (correctement) produit des paires de substitution comme une seule séquence de 4 octets au lieu de deux séquences de 3 octets, donc truncateWhenUTF8() retournera la plus longue chaîne tronquée qu'il puisse. Si vous ignorez les couples de substitution dans l'implémentation alors les chaînes tronquées peuvent être court-circuitées qu'elles devaient être.

je n'ai pas fait beaucoup de tests sur le code, mais voici quelques tests préliminaires:

private static void test(String s, int maxBytes, int expectedBytes) {
    String result = truncateWhenUTF8(s, maxBytes);
    byte[] utf8 = result.getBytes(Charset.forName("UTF-8"));
    if (utf8.length > maxBytes) {
        System.out.println("BAD: our truncation of " + s + " was too big");
    }
    if (utf8.length != expectedBytes) {
        System.out.println("BAD: expected " + expectedBytes + " got " + utf8.length);
    }
    System.out.println(s + " truncated to " + result);
}

public static void main(String[] args) {
    test("abcd", 0, 0);
    test("abcd", 1, 1);
    test("abcd", 2, 2);
    test("abcd", 3, 3);
    test("abcd", 4, 4);
    test("abcd", 5, 4);

    test("a\u0080b", 0, 0);
    test("a\u0080b", 1, 1);
    test("a\u0080b", 2, 1);
    test("a\u0080b", 3, 3);
    test("a\u0080b", 4, 4);
    test("a\u0080b", 5, 4);

    test("a\u0800b", 0, 0);
    test("a\u0800b", 1, 1);
    test("a\u0800b", 2, 1);
    test("a\u0800b", 3, 1);
    test("a\u0800b", 4, 4);
    test("a\u0800b", 5, 5);
    test("a\u0800b", 6, 5);

    // surrogate pairs
    test("\uD834\uDD1E", 0, 0);
    test("\uD834\uDD1E", 1, 0);
    test("\uD834\uDD1E", 2, 0);
    test("\uD834\uDD1E", 3, 0);
    test("\uD834\uDD1E", 4, 4);
    test("\uD834\uDD1E", 5, 4);

}

mise à jour exemple de code modifié, il traite maintenant des paires de substituts.

23
répondu Matt Quail 2008-09-23 14:11:51

vous devez utiliser CharsetEncoder , le simple getBytes() + copier autant que vous pouvez couper les charcteurs UTF-8 en deux.

quelque chose comme ça:

public static int truncateUtf8(String input, byte[] output) {

    ByteBuffer outBuf = ByteBuffer.wrap(output);
    CharBuffer inBuf = CharBuffer.wrap(input.toCharArray());

    Charset utf8 = Charset.forName("UTF-8");
    utf8.newEncoder().encode(inBuf, outBuf, true);
    System.out.println("encoded " + inBuf.position() + " chars of " + input.length() + ", result: " + outBuf.position() + " bytes");
    return outBuf.position();
}
21
répondu mitchnull 2011-11-09 08:28:52

voici ce que j'ai inventé, il utilise des API Java standard donc devrait être sûr et compatible avec toutes les bizarreries unicode et les paires de substituts, etc. La solution est tirée de http://www.jroller.com/holy/entry/truncating_utf_string_to_the avec contrôles ajoutés pour null et pour éviter le décodage lorsque la chaîne est moins d'octets que maxBytes .

/**
 * Truncates a string to the number of characters that fit in X bytes avoiding multi byte characters being cut in
 * half at the cut off point. Also handles surrogate pairs where 2 characters in the string is actually one literal
 * character.
 *
 * Based on: http://www.jroller.com/holy/entry/truncating_utf_string_to_the
 */
public static String truncateToFitUtf8ByteLength(String s, int maxBytes) {
    if (s == null) {
        return null;
    }
    Charset charset = Charset.forName("UTF-8");
    CharsetDecoder decoder = charset.newDecoder();
    byte[] sba = s.getBytes(charset);
    if (sba.length <= maxBytes) {
        return s;
    }
    // Ensure truncation by having byte buffer = maxBytes
    ByteBuffer bb = ByteBuffer.wrap(sba, 0, maxBytes);
    CharBuffer cb = CharBuffer.allocate(maxBytes);
    // Ignore an incomplete character
    decoder.onMalformedInput(CodingErrorAction.IGNORE)
    decoder.decode(bb, cb, true);
    decoder.flush(cb);
    return new String(cb.array(), 0, cb.position());
}
11
répondu sigget 2017-08-01 15:00:11

encodage UTF-8 a un trait soigné qui vous permet de voir où dans un byte-set Vous êtes.

vérifiez le flux à la limite de caractères que vous voulez.

  • si son high bit est 0, c'est un char à un seul octet, il suffit de le remplacer par 0 et vous êtes très bien.
  • si son high bit est 1 et que c'est le prochain bit, alors vous êtes au début d'un char multi-octets, donc il suffit de mettre ce byte à 0 et vous êtes bon.
  • si le bit de haut est 1 mais le bit suivant est 0, alors vous êtes au milieu d'un personnage, voyagez le long de la mémoire tampon jusqu'à ce que vous frappez un octet qui a 2 ou plus 1s dans les bits de haut, et remplacez ce octet par 0.

exemple: si votre flux est: 31 33 31 C1 A3 32 33 00, Vous pouvez faire votre chaîne 1, 2, 3, 5, 6, ou 7 octets de long, mais pas 4, comme cela placerait le 0 après C1, qui est le début d'un char multi-octets.

9
répondu billjamesdev 2008-09-28 02:42:10

Vous pouvez calculer le nombre d'octets, sans faire de conversion.

foreach character in the Java string
  if 0 <= character <= 0x7f
     count += 1
  else if 0x80 <= character <= 0x7ff
     count += 2
  else if 0x800 <= character <= 0xd7ff // excluding the surrogate area
     count += 3
  else if 0xdc00 <= character <= 0xffff
     count += 3
  else { // surrogate, a bit more complicated
     count += 4
     skip one extra character in the input stream
  }

vous devriez détecter des paires de substituts (D800-DBFF et U+DC00–U+DFFF) et compter 4 octets pour chaque paire de substituts valide. Si vous obtenez la première valeur de la première plage et le deuxième dans la deuxième plage, tout est ok, passez-les et ajoutez-4. Mais si non, alors c'est une défaillance de la paire de substitution. Je ne sais pas comment Java gère ça, mais votre algorithme devra faire le bon choix. comptant à ce que (peu probable).

3
répondu user19050 2008-09-23 08:11:54