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が下位サロゲートかを調べます。
以下にこれらのメソッドを使って(使わない例も紹介します)、文字列内にサロゲートペアが含まれているか調べる例を示します。
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
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)」と回りくどいことをしていますが、この文字を直接入力できるのであれば、そうしても構いません。以下に紹介する結合文字も場合も同じです。サロゲートペアを入力する方法については、「“つちよし”を入力する方法」が参考になります。
上のコードを使って、指定した文字列にサロゲートペアが含まれているか調べるメソッドを作成した例を以下に示します。
''' <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
/// <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」というブロック名が付いています。
以下に、正規表現を使って文字列内にサロゲートペアが含まれているか調べる例を示します。
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
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カテゴリの文字が含まれているかを正規表現を使って調べる例を示します。
'結合文字が含まれている文字列 Dim s As String = "「か」に点々で「か" & ChrW(&H3099) & "」" If System.Text.RegularExpressions.Regex.IsMatch(s, "\p{M}") Then Console.WriteLine("結合文字が含まれています。") End If
//結合文字が含まれている文字列 string s = "「か」に点々で「か\u3099」"; if (System.Text.RegularExpressions.Regex.IsMatch(s, @"\p{M}")) { Console.WriteLine("結合文字が含まれています。"); }
上のコードを使って、指定した文字列に結合文字が含まれているか調べるメソッドを作成すると、以下のようになります。
''' <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
/// <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クラスを使うと、指定した文字列内にサロゲートペアや結合文字列が含まれていたとしても、これを1文字として扱うことができます。例えばStringInfo.LengthInTextElementsプロパティを使えば、サロゲートペアや結合文字列を1文字として文字数をカウントできます(詳しくは、「文字列の長さ(文字数)を取得する」)。よって、String.LengthとStringInfo.LengthInTextElementsの値が違っていれば、その文字列にはサロゲートペアか結合文字(またはその両方)が含まれていると判断できます。
'結合文字が含まれている文字列 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
//結合文字が含まれている文字列 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以上になります。よってこれを利用すれば、サロゲートペアや結合文字が含まれているかだけでなく、含まれているサロゲートペアや結合文字列が何で、どこにあるかを知ることもできます。
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
//サロゲートペアと結合文字が含まれている文字列 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); } } }
注意:この記事では、基本的な事柄の説明が省略されているかもしれません。初心者の方は、特に以下の点にご注意ください。