Я предложил решение, намекаемое при редактировании.
Я не смог найти простой в использовании список допустимых диапазонов в пространстве Unicode; даже официальная база данных символов Unicode будет проходить больше анализа, чем я действительно хотел иметь дело. Поэтому вместо этого я написал быстрый скрипт для циклического перебора всех чисел в диапазоне [0x0, 0x10FFFF], преобразования его в string
с использованием Encoding.UTF32.GetString(BitConverter.GetBytes(code))
и попытки .Normalize()
получения результата. Если возникает исключение, то это значение не является допустимой кодовой точкой.
Из этих результатов я создал следующую функцию:
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);
}
Обратите внимание, что эта функция не обязательно подходит для очистки общего назначения, в зависимости от ваших потребностей. Он не исключает неназначенные или зарезервированные кодовые точки, только те, которые специально обозначены как «нехарактерные» (edit: и некоторые другие, которые Normalize (), кажется, душат, например 0xfffff). Тем не менее, это, кажется, единственные кодовые точки, которые заставят IsNormalized()
и Normalize()
вызвать исключение, так что это хорошо для моих целей.
После этого нужно просто преобразовать строку в UTF-32 и прочесать ее. Так как Encoding.GetBytes()
возвращает байтовый массив и IsValidCodePoint()
ожидает UInt32, я использовал небезопасный блок и некоторую приведение для преодоления разрыва:
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);
}
Производительность хорошая, сравнительно - на несколько порядков быстрее, чем образец, размещенный в вопросе. Предполагалось, что хранение данных в UTF-16 было бы быстрее и более эффективным для использования памяти, но за счет большого количества дополнительного кода для работы с суррогатами. И, конечно, наличие replacement
в качестве char
означает, что заменяющий символ должен быть на BMP.
edit: Вот гораздо более краткая версия IsValidCodePoint ():
private static bool IsValidCodePoint(UInt32 point)
{
return point < 0xfdd0
|| (point >= 0xfdf0
&& ((point & 0xffff) != 0xffff)
&& ((point & 0xfffe) != 0xfffe)
&& point <= 0x10ffff
);
}