かずきのBlog@hatena

すきな言語は C# + XAML の組み合わせ。Azure Functions も好き。最近は Go 言語勉強中。日本マイクロソフトで働いていますが、ここに書いていることは個人的なメモなので会社の公式見解ではありません。

StringInfoのSubstringByTextElementsがWindowsストアアプリでは使えない

がりっちさんに4バイト文字の𩹉(とびうお)を送り付けて、がりっちさんが作ってるTwitterクライアントの文字列処理をばぐらせて遊んでいたら、𩹉(とびうお)問題という名前がつきました。

StringInfoクラス使えば便利メソッドあるから簡単じゃん?と思ったら

SubstringByTextElementsという、今回のがりっちさんの中で重要なメソッドがなくなってたみたいです。自力実装されてたコードを見て自分だったら…というのを書いてみました。

public static class StringExtensions
{
    /// <summary>
    /// 文字数を返す
    /// </summary>
    /// <param name="self">対象の文字列</param>
    /// <returns>文字数</returns>
    public static int LengthInTextElements(this string self)
    {
        return new StringInfo(self).LengthInTextElements;
    }

    /// <summary>
    /// 1文字ずつ返す感じのIEを作る
    /// </summary>
    /// <param name="self">対象の文字列</param>
    /// <param name="index">開始位置</param>
    /// <returns>IE</returns>
    public static IEnumerable<string> GetTextElementEnumerable(this string self, int index = 0)
    {
        var e = StringInfo.GetTextElementEnumerator(self, index);
        while (e.MoveNext())
        {
            yield return (string)e.Current;
        }
    }

    /// <summary>
    /// 指定した範囲の文字列を切り出す
    /// </summary>
    /// <param name="self">対象の文字列</param>
    /// <param name="startIndex">開始位置</param>
    /// <param name="length">長さ</param>
    /// <returns></returns>
    public static string SubstringByTextElements(this string self, int startIndex, int length)
    {
        var sub = self.GetTextElementEnumerable(startIndex).Take(length).ToArray();
        if (sub.Length != length)
        {
            throw new ArgumentOutOfRangeException();
        }

        return string.Concat(sub);
    }
}

とりあえずGetTextElementEnumeratorというメソッドがあるので、それを使ってIEにするメソッドを作ってしまえばSubstringなんて楽勝さ!という発想。以下のテストケースを通してみてグリーンでした。

[TestClass]
public class UnitTest1
{
    [TestMethod]
    public void LengthInTextElements()
    {
        var target = "俺が𩹉だ!";
        Assert.AreEqual(5, target.LengthInTextElements());
    }

    [TestMethod]
    public void GetTextElementEnumerable()
    {
        var target = "俺が𩹉だ!";
        var result = target.GetTextElementEnumerable().ToArray();
        Assert.AreEqual("俺", result[0]);
        Assert.AreEqual("が", result[1]);
        Assert.AreEqual("𩹉", result[2]);
        Assert.AreEqual("だ", result[3]);
        Assert.AreEqual("!", result[4]);
    }

    [TestMethod]
    public void SubString()
    {
        var target = "俺が𩹉だ!";
        var result = target.SubstringByTextElements(2, 1);
        Assert.AreEqual("𩹉", result);
    }

    [TestMethod]
    public void SubString_範囲外()
    {
        try
        {
            var target = "俺が𩹉だ!";
            var result = target.SubstringByTextElements(2, 1000);
            Assert.Fail();
        }
        catch (Exception ex)
        {
            // Lengthがはみ出てると例外が出る
            Assert.IsInstanceOfType(ex, typeof(ArgumentOutOfRangeException));
        }
    }
}