Случайный символ из строки - с многобайтовыми символами


Аналогичный вопросзадавался ранее, но это отличается тем, что строка, из которой я пытаюсь извлечь случайные символы, может содержать многобайтовые символы. Я в основном делаю генератор псевдо - "лит", который берет строку и изменяет все символы в произвольно выбранные символы из расширенного Юникода, которые выглядят похожими, чтобы придать ему вид типа "хакер". (Это для игры, и один раздел должен использовать этот стиль. Не судите меня.) Так что у меня есть продление. метод:

private static Random rand = new Random();
public static char random(this string str)
{
    return str[rand.Next(str.Length)];
}

И как это работает, я смотрю на каждый символ в строке, и это называется так:

public static string leetify(this string str)
{
    StringBuilder sb = new StringBuilder();

    foreach (char c in str)
    {
        switch (char.ToLower(c))
        {
            case 'a':
                sb.Append("4ÀÁÂÃÄÅàáâãäåĀāĂ㥹ǎǍǺǻȀȁȂȃȦȧȺɐɑɒªΆѦѧᴀᾼ₳".random());
                break;
                ...  //More of the same for each letter

                //Okay, the letter 's' definitely has a failure case,
                //not the only one, but needed an example
            case 's':
                sb.Append("ŚśŜŝŞşŠšƧƨȘșȿʂϨϩЅѕᵴṠṡṢṣṤṥṦṧṨṩ$§".random());
                break;
                ...
            default:
                sb.Append(c);
                break;
        }
    }
    return sb.toString();
}

С аналогичным кодом для остальных букв, конечно. Последняя строка затем отображается в текстовом поле и, возможно, в различных других элементах управления. Теперь я проверил, и все символы, которые я выбрал, прекрасно могут отображаться в текстовом поле с выбранным шрифтом - я могу скопировать / вставить их туда, и это работает. Но когда я запускаю это, я получаю много ошибок символы, появляющиеся в строках. Я считаю, что моя функция random не понимает, что строка содержит многобайтовые символы. Есть ли какой-то способ изменить его так, чтобы он это делал?

Edit : добавлен набор 's', который определенно приводит к сбою.

Edit 2 : альтернативно, если бы был какой-то способ легко определить, какие символы в моей строке были многобайтовыми, я мог бы просто удалить их и иметь меньший выбор символов на выбор. Я не использую персонажей по их прямому назначению, очевидно, поэтому я бы не возражал пожертвовать немного разнообразия для простоты.

2 2

2 ответа:

Проблема может заключаться в одном из других наборов букв, и комбинация символов-это то, что вызывает вашу проблему. Например, я могу вызвать провал теста @Harrison, включив в строку комбинирующую диакритическую метку, такую как \u0301. Поэтому, не видя другие наборы и входной тестовый случай, который вы используете, трудно сказать.

Игнорируя все это, правильный способ сделать это, если у вас есть комбинирующие символы или суррогатные пары, - использовать StringInfo.GetTextElementEnumerator для перебора строк логических символов. Вот плохо выполняющийся пример, который заменит вашу текущую случайную реализацию.

public static class Extensions
{
    private static Random rand = new Random(1);

    public static string Random(this string str)
    {
        var chars = new List<string>();
        var strElements = StringInfo.GetTextElementEnumerator(str);
        while (strElements.MoveNext())
        {
            chars.Add(strElements.GetTextElement());
        }
        return chars[rand.Next(chars.Count)];
    }
}
Это будет охватывать все случаи, например, буква "ś" может быть определена ее литералом и имеет длину 1 или с комбинирующим символом над s "s\u0301", который имеет длину 2. Они оба представляют один и тот же глиф при рендеринге.

В вашей функции нет ошибки. Следующий тест проходит, который использует все 31 букву в вашей строке s.

public static class Extensions
{
    private static Random rand = new Random(1);

    public static char Random(this string str)
    {
        return str[rand.Next(str.Length)];
    }
}

[TestClass]
public class StackOverflow
{
    [TestMethod]
    public void MyTestMethod()
    {
        string s = "ŚśŜŝŞşŠšƧƨȘșȿʂϨϩЅѕᵴṠṡṢṣṤṥṦṧṨṩ$§";
        HashSet<char> expected = new HashSet<char>();
        HashSet<char> actual = new HashSet<char>();

        foreach (char c in s)
        {
            expected.Add(c);
        }

        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < 1000; i++)
        {
            sb.Append(s.Random());
        }

        string str = sb.ToString();

        foreach (char c in str)
        {
            actual.Add(c);
        }

        Assert.AreEqual(1000, str.Length);
        CollectionAssert.AreEquivalent(expected.ToList(), actual.ToList());
    }
}