DOBON.NET プログラミング道: .NET Framework, VB.NET, C#, Visual Basic, Visual Studio, インストーラ, ...

サロゲートペアや結合文字が含まれているか調べる

String型の文字列にサロゲートペアや結合文字が含まれていると、非常に厄介です。これらを考慮しないでコードを書くと、文字列にこれらの文字が入っているときに、不具合が生じるかもしれません。ここでは文字列内にサロゲートペアや結合文字が含まれているかを調べる方法を紹介します。

サロゲートペアが含まれているか調べる

通常は、1つのChar値で1つの文字を表現します。ところがサロゲートペア(代用対)は、2つのChar値を使って1つの文字を表現します。サロゲートペアの例としては、魚のホッケ(魚へんに花)(文字コードはU+29E3D)や、牛丼の吉野家の「よし」の字(土に口)(文字コードはU+20BB7)などがあります。ホッケの場合はU+D867とU+DE3D、「よし」の場合はU+D842とU+DFB7の2つのChar値を使って表現します。

サロゲートペアについてより詳しくは、Wikipediaの「サロゲートペア」や「サロゲートペア入門:CodeZine」などが参考になります。

.NET Frameworkでは、指定されたChar値がサロゲートペアであるか調べるのに、Char.IsSurrogatePair、Char.IsSurrogate、Char.IsHighSurrogate、Char.IsLowSurrogateといったメソッドが用意されています。

指定されたChar値が上位サロゲート(1つ目のChar)であるかを調べるにはChar.IsHighSurrogateを、下位サロゲート(2つ目のChar)であるかを調べるにはChar.IsLowSurrogateを使います。Char.IsSurrogateメソッドは、指定された文字が上位または下位のサロゲートかを調べます。

Char.IsSurrogatePairメソッドはこれらとちょっと異なります。Char.IsSurrogatePairメソッドにはパラメータとして2つのChar値を渡す必要があり、1つ目が上位サロゲートで、2つ目が下位サロゲートかを調べます。Char.IsSurrogatePairメソッドの別のオーバーロードでは、パラメータに文字列とその中の位置を指定し、その位置のCharが上位サロゲートで、次の位置のCharが下位サロゲートかを調べます。

以下にこれらのメソッドを使って(使わない例も紹介します)、文字列内にサロゲートペアが含まれているか調べる例を示します。

VB.NET
コードを隠すコードを選択
Dim s As String = "昨日" & ChrW(&HD867) & ChrW(&HDE3D) & "(ホッケ)を食べた"
'.NET Framework 2.0以降であれば、以下のようにもできる
'Dim s As String = "昨日" & Char.ConvertFromUtf32(&H29E3D) & "(ホッケ)を食べた"

'上位または下位サロゲートが含まれているか調べる
'上位または下位サロゲートが単独で含まれていても含まれていると判断する
Dim c As Char
For Each c In s
    If Char.IsSurrogate(c) Then
        Console.WriteLine("サロゲート文字が含まれています。")
        Exit For
    End If
Next

'Char.IsSurrogatePairを使って調べる
Dim i As Integer
For i = 0 To s.Length - 2
    If Char.IsSurrogatePair(s.Chars(i), s.Chars(i + 1)) Then
        Console.WriteLine("サロゲートペアが含まれています。")
        Exit For
    End If
    'または、次のようにもできる
    'If Char.IsSurrogatePair(s, i) Then
    '    Console.WriteLine("サロゲートペアが含まれています。")
    '    Exit For
    'End If
Next

'IsHighSurrogateとIsLowSurrogateを使って調べる
For i = 0 To s.Length - 2
    If Char.IsHighSurrogate(s.Chars(i)) AndAlso _
            Char.IsLowSurrogate(s.Chars(i + 1)) Then
        Console.WriteLine("サロゲートペアが含まれています。")
        Exit For
    End If
Next

'Char構造体のメソッドを使わないで調べる
For i = 0 To s.Length - 2
    Dim c1 As Char = s.Chars(i)
    If ChrW(&HD800) <= c1 AndAlso c1 <= ChrW(&HDBFF) Then
        Dim c2 As Char = s.Chars(i + 1)
        If ChrW(&HDC00) <= c2 AndAlso c2 <= ChrW(&HDFFF) Then
            Console.WriteLine("サロゲートペアが含まれています。")
            Exit For
        End If
    End If
Next
C#
コードを隠すコードを選択
string s = "昨日\uD867\uDE3D(ホッケ)を食べた";
//.NET Framework 2.0以降であれば、以下のようにもできる
//string s = "昨日" + char.ConvertFromUtf32(0x29E3D) + "(ホッケ)を食べた";

//上位または下位サロゲートが含まれているか調べる
//上位または下位サロゲートが単独で含まれていても含まれていると判断する
foreach (char c in s)
{
    if (char.IsSurrogate(c))
    {
        Console.WriteLine("サロゲート文字が含まれています。");
        break;
    }
}

//Char.IsSurrogatePairを使って調べる
for (int i = 0; i < s.Length - 1; i++)
{
    if (char.IsSurrogatePair(s[i], s[i + 1]))
    {
        Console.WriteLine("サロゲートペアが含まれています。");
        break;
    }
    //または、次のようにもできる
    //if (char.IsSurrogatePair(s, i))
    //{
    //    Console.WriteLine("サロゲートペアが含まれています。");
    //    break;
    //}
}

//IsHighSurrogateとIsLowSurrogateを使って調べる
for (int i = 0; i < s.Length - 1; i++)
{
    if (char.IsHighSurrogate(s[i]) && char.IsLowSurrogate(s[i + 1]))
    {
        Console.WriteLine("サロゲートペアが含まれています。");
        break;
    }
}

//Char構造体のメソッドを使わないで調べる
for (int i = 0; i < s.Length - 1; i++)
{
    char c1 = s[i];
    if ('\uD800' <= c1 && c1 <= '\uDBFF')
    {
        char c2 = s[i + 1];
        if ('\uDC00' <= c2 && c2 <= '\uDFFF')
        {
            Console.WriteLine("サロゲートペアが含まれています。");
            break;
        }
    }
}
補足:上のコードではホッケの字を文字列をして表現するのに、C#では「\uD867\uDE3D」、VB.NETでは「ChrW(&HD867) & ChrW(&HDE3D)」と回りくどいことをしていますが、この文字を直接入力できるのであれば、そうしても構いません。以下に紹介する結合文字も場合も同じです。サロゲートペアを入力する方法については、「“つちよし”を入力する方法」が参考になります。

上のコードを使って、指定した文字列にサロゲートペアが含まれているか調べるメソッドを作成した例を以下に示します。

VB.NET
コードを隠すコードを選択
''' <summary>
''' 指定した文字列にサロゲートペアが含まれているかどうかを示します。
''' </summary>
''' <param name="s">文字列</param>
''' <returns>s にサロゲートペアが含まれている場合は true。
''' それ以外は false。</returns>
Public Shared Function ContainSurrogatePair(ByVal s As String) As Boolean
    If s Is Nothing Then
        Throw New ArgumentNullException("s")
    End If

    For i As Integer = 0 To s.Length - 2
        If Char.IsSurrogatePair(s.Chars(i), s.Chars(i + 1)) Then
            Return True
        End If
    Next
    Return False
End Function
C#
コードを隠すコードを選択
/// <summary>
/// 指定した文字列にサロゲートペアが含まれているかどうかを示します。
/// </summary>
/// <param name="s">文字列</param>
/// <returns>s にサロゲートペアが含まれている場合は true。
/// それ以外は false。</returns>
public static bool ContainSurrogatePair(string s)
{
    if (s == null)
    {
        throw new ArgumentNullException("s");
    }

    for (int i = 0; i < s.Length - 1; i++)
    {
        if (char.IsSurrogatePair(s[i], s[i + 1]))
        {
            return true;
        }
    }
    return false;
}

正規表現を使って調べる

次に正規表現を使った方法を紹介します。ただし正規表現についての説明はしませんので、分からないという方は「正規表現の基本」をご覧ください。

上位サロゲートは、U+D800からU+DBFFの範囲にあります。下位サロゲートは、U+DC00からU+DFFFまでの範囲にあります。

また、上位サロゲートには「HighSurrogates」下位サロゲートには「LowSurrogates」というブロック名が付いています。

以下に、正規表現を使って文字列内にサロゲートペアが含まれているか調べる例を示します。

VB.NET
コードを隠すコードを選択
Dim s As String = "昨日" & ChrW(&HD867) & ChrW(&HDE3D) & "(ホッケ)を食べた"


If System.Text.RegularExpressions.Regex.IsMatch( _
        s, "[\uD800-\uDBFF][\uDC00-\uDFFF]") Then
    Console.WriteLine("サロゲートペアが含まれています。")
End If

If System.Text.RegularExpressions.Regex.IsMatch( _
        s, "\p{IsHighSurrogates}\p{IsLowSurrogates}") Then
    Console.WriteLine("サロゲートペアが含まれています。")
End If
C#
コードを隠すコードを選択
string s = "昨日\uD867\uDE3D(ホッケ)を食べた";

if (System.Text.RegularExpressions.Regex.IsMatch(s,
    @"[\uD800-\uDBFF][\uDC00-\uDFFF]"))
{
    Console.WriteLine("サロゲートペアが含まれています。");
}

if (System.Text.RegularExpressions.Regex.IsMatch(s,
    @"\p{IsHighSurrogates}\p{IsLowSurrogates}"))
{
    Console.WriteLine("サロゲートペアが含まれています。");
}

結合文字が含まれているか調べる

結合文字(Combining character)は、他の文字(基底文字)の後ろに付くことによって、その文字を修飾します。結合文字の例としては、濁点(U+3099)、半濁点(U+309A)、アクセント記号、ウムラウトのようなダイアクリティカルマークなどがあります。

また、基底文字と結合文字の連続は「結合文字列」(Combining Character Sequence、組み合わせ文字シーケンス)などと呼ばれています。

あるChar値が結合文字であるかを一発で調べる方法は、サロゲートペアとは違い、.NET Frameworkでは用意されていません。よって、何らかの工夫が必要です。

正規表現を使って調べる

結合文字は、ユニコードのMarks(M)というカテゴリにほとんどが入っているようです。Marksというカテゴリは、次の3つのカテゴリから成ります。

カテゴリ プロパティ 説明 リスト
Marks, nonspacing Mn 通常文字の上下に付き、文字幅に影響を与えない非スペーシング文字 Unicode Characters in the 'Mark, Nonspacing' Category
Marks, spacing combining Ms 通常文字の左右に付き、文字幅に影響を与えるスペーシング文字 Unicode Characters in the 'Mark, Spacing Combining' Category
Marks, enclosing Me 文字を囲む囲み記号文字 Unicode Characters in the 'Mark, Enclosing' Category

ただし、Marksカテゴリにすべての結合文字が含まれているか、そして、結合文字以外の文字が一切含まれていないかについては、はっきりしていません。ご存じの方がいらっしゃいましたら、コメントで教えていただけると助かります。

以下に、Marksカテゴリの文字が含まれているかを正規表現を使って調べる例を示します。

VB.NET
コードを隠すコードを選択
'結合文字が含まれている文字列
Dim s As String = "「か」に点々で「か" & ChrW(&H3099) & "」"

If System.Text.RegularExpressions.Regex.IsMatch(s, "\p{M}") Then
    Console.WriteLine("結合文字が含まれています。")
End If
C#
コードを隠すコードを選択
//結合文字が含まれている文字列
string s = "「か」に点々で「か\u3099」";

if (System.Text.RegularExpressions.Regex.IsMatch(s, @"\p{M}"))
{
    Console.WriteLine("結合文字が含まれています。");
}

上のコードを使って、指定した文字列に結合文字が含まれているか調べるメソッドを作成すると、以下のようになります。

VB.NET
コードを隠すコードを選択
''' <summary>
''' 指定した文字列に結合文字が含まれているかどうかを示します。
''' </summary>
''' <param name="s">文字列</param>
''' <returns>s に結合文字が含まれている場合は true。
''' それ以外は false。</returns>
Public Shared Function ContainCombiningCharacter(ByVal s As String) As Boolean
    Return System.Text.RegularExpressions.Regex.IsMatch(s, "\p{M}")
End Function
C#
コードを隠すコードを選択
/// <summary>
/// 指定した文字列に結合文字が含まれているかどうかを示します。
/// </summary>
/// <param name="s">文字列</param>
/// <returns>s に結合文字が含まれている場合は true。
/// それ以外は false。</returns>
public static bool ContainCombiningCharacter(string s)
{
    return System.Text.RegularExpressions.Regex.IsMatch(s, @"\p{M}");
}

StringInfoクラスを使って、サロゲートペアか結合文字が含まれているか調べる

StringInfoクラスを使うと、指定した文字列内にサロゲートペアや結合文字列が含まれていたとしても、これを1文字として扱うことができます。例えばStringInfo.LengthInTextElementsプロパティを使えば、サロゲートペアや結合文字列を1文字として文字数をカウントできます(詳しくは、「文字列の長さ(文字数)を取得する」)。よって、String.LengthとStringInfo.LengthInTextElementsの値が違っていれば、その文字列にはサロゲートペアか結合文字(またはその両方)が含まれていると判断できます。

VB.NET
コードを隠すコードを選択
'結合文字が含まれている文字列
Dim s As String = "「か」に点々で「か" & ChrW(&H3099) & "」"

'StringInfoオブジェクトを作成
Dim si As New System.Globalization.StringInfo(s)

'String内の文字数とChar数を比較し、
' 異なればサロゲートペアか結合文字が含まれていると判断する
If si.LengthInTextElements < s.Length Then
    Console.WriteLine("サロゲートペアか結合文字が含まれています。")
End If
C#
コードを隠すコードを選択
//結合文字が含まれている文字列
string s = "「か」に点々で「か\u3099」";

//StringInfoオブジェクトを作成
System.Globalization.StringInfo si = new System.Globalization.StringInfo(s);

//String内の文字数とChar数を比較し、
// 異なればサロゲートペアか結合文字が含まれていると判断する
if (si.LengthInTextElements < s.Length)
{
    Console.WriteLine("サロゲートペアか結合文字が含まれています。");
}

また、TextElementEnumeratorを使えば、文字列に含まれている文字を列挙する時、サロゲートペアや結合文字列でも1文字として列挙することができます(詳しくは、「文字列から1文字取得する、文字列内の文字を列挙する」)。もし列挙した文字がサロゲートペアや結合文字列の場合、その文字(Stringオブジェクト)のLengthプロパティは2以上になります。よってこれを利用すれば、サロゲートペアや結合文字が含まれているかだけでなく、含まれているサロゲートペアや結合文字列が何で、どこにあるかを知ることもできます。

VB.NET
コードを隠すコードを選択
Dim s As String = "昨日" & ChrW(&HD867) & ChrW(&HDE3D) & "(ホッケ)を食べた" & _
    "「か」に点々で「か" & ChrW(&H3099) & "」"

'TextElementEnumeratorを作成する
Dim tee As System.Globalization.TextElementEnumerator = _
    System.Globalization.StringInfo.GetTextElementEnumerator(s)
'読み取る位置をテキストの先頭にする
tee.Reset()
'1文字ずつ取得する
While tee.MoveNext()
    '1文字取得する
    Dim te As String = tee.GetTextElement()
    '1文字が2つ以上のCharから成る場合は、サロゲートペアか結合文字列と判断する
    If 1 < te.Length Then
        'サロゲートペアか調べる
        If te.Length = 2 AndAlso Char.IsSurrogatePair(te, 0) Then
            Console.WriteLine("サロゲートペア「{0}」が「{1}」の位置にあります。", _
                              te, tee.ElementIndex)
        Else
            'サロゲートペアでない場合は結合文字列と判断する
            Console.WriteLine("結合文字列「{0}」が「{1}」の位置にあります。", _
                              te, tee.ElementIndex)
        End If
    End If
End While
C#
コードを隠すコードを選択
//サロゲートペアと結合文字が含まれている文字列
string s = "昨日\uD867\uDE3D(ホッケ)を食べた。「か」に点々で「か\u3099」。";

//TextElementEnumeratorを作成する
System.Globalization.TextElementEnumerator tee =
    System.Globalization.StringInfo.GetTextElementEnumerator(s);
//読み取る位置をテキストの先頭にする
tee.Reset();
//1文字ずつ取得する
while (tee.MoveNext())
{
    //1文字取得する
    string te=tee.GetTextElement();
    //1文字が2つ以上のCharから成る場合は、サロゲートペアか結合文字列と判断する
    if (1 < te.Length)
    {
        //サロゲートペアか調べる
        if (te.Length == 2 && char.IsSurrogatePair(te, 0))
        {
            Console.WriteLine("サロゲートペア「{0}」が「{1}」の位置にあります。",
                te, tee.ElementIndex);
        }
        else
        {
            //サロゲートペアでない場合は結合文字列と判断する
            Console.WriteLine("結合文字列「{0}」が「{1}」の位置にあります。",
                te, tee.ElementIndex);
        }
    }
}
  • 履歴:
  • 2013/9/30 StringInfoクラスを使用する方法を追加。(コメントでご指摘頂きました。)

注意:この記事では、基本的な事柄の説明が省略されているかもしれません。初心者の方は、特に以下の点にご注意ください。

  • .NET Tipsをご利用いただく際は、注意事項をお守りください。