Continué con la solución insinuada en la edición.
No pude encontrar una lista fácil de usar de rangos válidos en el espacio Unicode; incluso la base de datos oficial de caracteres Unicode iba a tomar más análisis de los que realmente quería tratar. Así que en su lugar escribí un guión rápido para recorrer cada número en el rango [0x0, 0x10FFFF], convertirlo a string
usando Encoding.UTF32.GetString(BitConverter.GetBytes(code))
, y probar .Normalize()
buscando el resultado. Si se genera una excepción, ese valor no es un punto de código válido.
A partir de esos resultados, he creado la siguiente función:
bool IsValidCodePoint(UInt32 point)
{
return (point >= 0x0 && point <= 0xfdcf)
|| (point >= 0xfdf0 && point <= 0xfffd)
|| (point >= 0x10000 && point <= 0x1fffd)
|| (point >= 0x20000 && point <= 0x2fffd)
|| (point >= 0x30000 && point <= 0x3fffd)
|| (point >= 0x40000 && point <= 0x4fffd)
|| (point >= 0x50000 && point <= 0x5fffd)
|| (point >= 0x60000 && point <= 0x6fffd)
|| (point >= 0x70000 && point <= 0x7fffd)
|| (point >= 0x80000 && point <= 0x8fffd)
|| (point >= 0x90000 && point <= 0x9fffd)
|| (point >= 0xa0000 && point <= 0xafffd)
|| (point >= 0xb0000 && point <= 0xbfffd)
|| (point >= 0xc0000 && point <= 0xcfffd)
|| (point >= 0xd0000 && point <= 0xdfffd)
|| (point >= 0xe0000 && point <= 0xefffd)
|| (point >= 0xf0000 && point <= 0xffffd)
|| (point >= 0x100000 && point <= 0x10fffd);
}
Tenga en cuenta que esta función no es necesariamente grande para la limpieza de uso general, dependiendo de sus necesidades. No excluye los puntos de código no asignados o reservados, solo los que están específicamente designados como 'noncharacter' (edit: y algunos otros que Normalize() parece ahogarse, como 0xfffff). Sin embargo, estos parecen ser los únicos puntos de código que provocarán que IsNormalized()
y Normalize()
presenten una excepción, por lo que está bien para mis propósitos.
Después de eso, solo se trata de convertir la cuerda en UTF-32 y peinarla. Desde Encoding.GetBytes()
devuelve una matriz de bytes y IsValidCodePoint()
espera un UInt32, he utilizado un bloque insegura y algo de colada para reducir la brecha:
unsafe string ReplaceInvalidCodePoints(string aString, char replacement)
{
if (char.IsHighSurrogate(replacement) || char.IsLowSurrogate(replacement))
throw new ArgumentException("Replacement cannot be a surrogate", "replacement");
byte[] utf32String = Encoding.UTF32.GetBytes(aString);
fixed (byte* d = utf32String)
fixed (byte* s = Encoding.UTF32.GetBytes(new[] { replacement }))
{
var data = (UInt32*)d;
var substitute = *(UInt32*)s;
for(var p = data; p < data + ((utf32String.Length)/sizeof(UInt32)); p++)
{
if (!(IsValidCodePoint(*p))) *p = substitute;
}
}
return Encoding.UTF32.GetString(utf32String);
}
El rendimiento es bueno, comparativamente hablando - varios órdenes de magnitud más rápido que la muestra publicado en el pregunta. Dejando los datos en UTF-16 presumiblemente habría sido más rápido y más eficiente en la memoria, pero a costa de un montón de código adicional para tratar con los sustitutos. Y, por supuesto, tener replacement
ser un char
significa que el personaje de reemplazo debe estar en el BMP.
edición: Aquí es una versión mucho más concisa de IsValidCodePoint():
private static bool IsValidCodePoint(UInt32 point)
{
return point < 0xfdd0
|| (point >= 0xfdf0
&& ((point & 0xffff) != 0xffff)
&& ((point & 0xfffe) != 0xfffe)
&& point <= 0x10ffff
);
}
Tenga en cuenta que, a causa de los pares suplentes, no será posible mirar simplemente en un 'DWORD' arbitrario y diga si es un punto de código válido. –
UTF-32 no usa pares suplentes. –
¿Cómo está recibiendo esta información incorrecta? Si lo está leyendo con la clase 'Encoding', estos caracteres deberían eliminarse por defecto. – porges