Creo que podemos hacerlo mejor que contando ingenuamente la longitud total de una cadena con cada adición.LINQ es genial, pero puede alentar accidentalmente el código ineficiente. ¿Qué pasa si quiero los primeros 80,000 bytes de una cadena UTF gigante? Eso es un lote de conteo innecesario. "Tengo 1 byte. Ahora tengo 2. Ahora tengo 13 ... Ahora tengo 52,384 ..."
Eso es una tontería. La mayoría de las veces, al menos en l'anglais, podemos cortar exactamente en ese nth
byte. Incluso en otro idioma, estamos a menos de 6 bytes de un buen punto de corte.
Así que voy a comenzar con la sugerencia de @ Oren, que es quitar la punta de un bit de UTF8. Comencemos cortando a la derecha en el byte n+1th
, y usemos el truco de Oren para averiguar si necesitamos cortar algunos bytes antes.
tres posibilidades
Si el primer byte después del corte tiene una 0
en el bit inicial, sé que estoy cortando con precisión antes de un solo byte (ASCII convencional) carácter, y puede cortar limpiamente.
Si tengo un 11
tras el corte, el siguiente byte después del corte es el inicio de un carácter multi-byte, por lo que es un buen lugar para cortar también!
Si tengo un 10
, sin embargo, sé que estoy en medio de un carácter de varios bytes, y necesito volver a comprobar para ver dónde comienza realmente.
Es decir, aunque quiero cortar la cadena después del n-ésimo byte, si ese byte n + 1 viene en medio de un carácter de múltiples bytes, cortar crearía un valor UTF8 no válido. Necesito hacer una copia de seguridad hasta que llegue a uno que comience con 11
y corte justo antes.
Código
Notas: estoy usando cosas como Convert.ToByte("11000000", 2)
para que sea más fácil decir lo que estoy bits de enmascaramiento (un poco más sobre el enmascaramiento poco here). En pocas palabras, estoy &
para devolver lo que está en los primeros dos bits del byte y devolver 0
s para el resto. Luego verifico el XX
desde XX000000
para ver si es 10
o 11
, según corresponda.
me enteré hoy que C# 6.0 might actually support binary representations, que es fresca, pero vamos a seguir utilizando este kludge por ahora para ilustrar lo que está pasando.
El PadLeft
es solo porque soy demasiado OCD sobre la salida a la consola.
Así que aquí hay una función que lo reducirá a una cadena que es n
bytes de longitud o la mayor cantidad menor que n
que termina con un carácter "completo" UTF8.
public static string CutToUTF8Length(string str, int byteLength)
{
byte[] byteArray = Encoding.UTF8.GetBytes(str);
string returnValue = string.Empty;
if (byteArray.Length > byteLength)
{
int bytePointer = byteLength;
// Check high bit to see if we're [potentially] in the middle of a multi-byte char
if (bytePointer >= 0
&& (byteArray[bytePointer] & Convert.ToByte("10000000", 2)) > 0)
{
// If so, keep walking back until we have a byte starting with `11`,
// which means the first byte of a multi-byte UTF8 character.
while (bytePointer >= 0
&& Convert.ToByte("11000000", 2) != (byteArray[bytePointer] & Convert.ToByte("11000000", 2)))
{
bytePointer--;
}
}
// See if we had 1s in the high bit all the way back. If so, we're toast. Return empty string.
if (0 != bytePointer)
{
returnValue = Encoding.UTF8.GetString(byteArray, 0, bytePointer); // hat tip to @NealEhardt! Well played. ;^)
}
}
else
{
returnValue = str;
}
return returnValue;
}
Inicialmente escribí esto como una extensión de cadena. Simplemente vuelva a agregar this
antes de string str
para volver a ponerlo en el formato de extensión, por supuesto. Eliminé el this
para que pudiéramos cambiar el método en Program.cs
en una aplicación de consola simple para demostrarlo.
Prueba y salida esperada
He aquí un buen caso de prueba, con la salida se crean a continuación, escrito esperando a ser el método de Main
en Program.cs
una aplicación de consola sencilla.
static void Main(string[] args)
{
string testValue = "12345“”67890”";
for (int i = 0; i < 15; i++)
{
string cutValue = Program.CutToUTF8Length(testValue, i);
Console.WriteLine(i.ToString().PadLeft(2) +
": " + Encoding.UTF8.GetByteCount(cutValue).ToString().PadLeft(2) +
":: " + cutValue);
}
Console.WriteLine();
Console.WriteLine();
foreach (byte b in Encoding.UTF8.GetBytes(testValue))
{
Console.WriteLine(b.ToString().PadLeft(3) + " " + (char)b);
}
Console.WriteLine("Return to end.");
Console.ReadLine();
}
Salida a continuación. Tenga en cuenta que las "comillas inteligentes" en testValue
tienen tres bytes de longitud en UTF8 (aunque cuando escribimos los caracteres en la consola en ASCII, se emiten comillas). También tenga en cuenta la salida de ?
para el segundo y tercer byte de cada cita inteligente en la salida.
Los primeros cinco caracteres de nuestro testValue
son bytes individuales en UTF8, por lo que los valores de 0-5 bytes deben tener entre 0 y 5 caracteres. Luego tenemos una cita inteligente de tres bytes, que no se puede incluir en su totalidad hasta 5 + 3 bytes. Efectivamente, vemos que el pop a cabo en la convocatoria de 8
.Our siguiente cita inteligente se sale a las 8 + 3 = 11, y luego estamos de vuelta a través de caracteres de un solo byte 14.
0: 0::
1: 1:: 1
2: 2:: 12
3: 3:: 123
4: 4:: 1234
5: 5:: 12345
6: 5:: 12345
7: 5:: 12345
8: 8:: 12345"
9: 8:: 12345"
10: 8:: 12345"
11: 11:: 12345""
12: 12:: 12345""6
13: 13:: 12345""67
14: 14:: 12345""678
49 1
50 2
51 3
52 4
53 5
226 â
128 ?
156 ?
226 â
128 ?
157 ?
54 6
55 7
56 8
57 9
48 0
226 â
128 ?
157 ?
Return to end.
Así que es una especie de diversión, y estoy justo antes del quinto aniversario de la pregunta. Aunque la descripción de Oren de los bits tuvo un pequeño error, eso es exactamente el truco que desea usar. Gracias por la pregunta; ordenado.
P.S. Incluí la introducción en caso de que alguien busque en Google mi mensaje de error de oráculo en el futuro. Con suerte, esto les ahorrará algo de tiempo. –