冬休みの自由研究として、C#/.NETでひらがな判定・ひらがな→カタカナ変換などをやってみました。
スマホでよみがなが入力されたらインクリメンタルサーチを実行するという動作を行うのに、入力された文字がすべてひらがなかどうかの判定が必要だったというのが動機です。具体的にはコミケWebカタログのアプリ連携でサクッとサークル検索したいなーというやつ。
変換方法がいくつかあったので、BenchmarkDotNetを使ってパフォーマンス比較をしてみました。
ソースコードはここに置いてあります。
https://github.com/anseketamen/KanaConvertStudy
1. ひらがな判定
C#/.NETでは文字の表現にUTF-16を使っていて、ひらがなは大体U+3041(ぁ)~U+3094(ゔ)にあります。あと長音(U+30FC)、読点(U+3001)、句点(U+3002)などもありますが、ひらがな判定に含めるかはケースバイケースで。
private static bool IsHiragana(char letter) => (letter >= 0x3041 && letter <= 0x3094) || letter == 0x30FC;
詳しい表はWikipediaにあるのでそちらをどうぞ。ゖとかヺとか使いどころのわからない文字も並んでいて、見てるだけで楽しいです。
https://ja.wikipedia.org/wiki/Unicode%E4%B8%80%E8%A6%A7_3000-3FFF
1.1 正規表現で判定
textがすべてひらがなかどうか、こんな感じで判定できます。どっかからコピペしてきたので合ってるかは知りませんが、それっぽく動作はします。何文字だろうとforとかに頼らないのはステキ。
using System.Text.RegularExpressions; var hiraganaRegex = new Regex("^([ぁ-ゔ]|ー)*$"); return hiraganaRegex.IsMatch(text);
1.2 forで回す
さて、ここからはさっきのひらがな判定のコードをforなどでぶんまわす実装です。
とりあえず愚直にforで回してみます。forの条件をLengthでアクセスすることで、ループごとの境界値チェックが省かれてちょっと早くなるとかなんとか。
for (var i = 0; i < text.Length; i++) { if (!IsHiragana(text[i])) { return false; } } return true;
1.3 foreachで回す
foreachでも回してみます。こういうのだとコードの最適化で上のforと同一コードになるらしいです。
foreach (var t in text) { if (!IsHiragana(t)) { return false; } } return true;
1.4 LINQで回す
LINQでもやってみます。このシンプルさが最高。
return text.All(x => IsHiragana(x));
1.5 for(unsafe)で回す
unsafeコードを使ってstringを強制的にchar[]にしてforで回してみます。
fixed (char* p = text) { for (var i = 0; i < text.Length; i++) { if (!IsHiragana(p[i])) { return false; } } } return true;
1.6 速度比較まとめ
Method | Mean | Error | StdDev | Gen 0 | Allocated |
---|---|---|---|---|---|
正規表現 | 1,198.05 ns | 40.564 ns | 2.223 ns | - | - |
for | 37.91 ns | 0.399 ns | 0.022 ns | - | - |
foreach | 36.37 ns | 0.760 ns | 0.042 ns | - | - |
LINQ | 175.60 ns | 7.325 ns | 0.402 ns | 0.0229 | 96 B |
unsafe for | 39.84 ns | 2.449 ns | 0.134 ns | - | - |
項目の意味は以下です。英語力と.NET力が不足しているので正しいかは知りません。
実行時間は for、foreach、unsafe for < LINQ <<< 正規表現 です。
for、foreach、unsafe forはほぼ拮抗しています。unsafe forはLengthアクセスが悪さしてるのかそれ以外の何かなのかわかりませんが、誤差レベルでちょっと遅いです。
そして、LINQだけアロケーションが発生しています。本来は正規表現もアロケーション発生していますが、hiraganaRegexをreadonly staticなフィールドで保持して使いまわしているのでメソッドを呼ぶだけではnewされず、0 Byteとなっています。
まあ無難にforeachで回すのがよいでしょう。
2. ひらがな→カタカナ変換
上の方のWikipediaのUnicode一覧表を見ればわかりますが、ひらがなに0x60を足すとカタカナになります。わーお簡単。
注意する点として、上で書いたひらがな判定のように長音もひらがなと判定していると、長音のカタカナ変換をしようとしておかしくなります。よってU+3041(ぁ)~U+3094(ゔ)のみをカタカナ変換するようにします。
ちなみに、なぜひらがな→カタカナ変換を調べることになったかというと、Circle.msから送られてくるよみがなデータがカタカナっぽいからです。変換しないとインクリメンタルサーチできないのです。
2.1 VB.NETのStrConv関数を使う
Windowsでしか使えないため.NET MAUIでスマホアプリを作るような場合には不向きですが、一応比較のためにやってみます。
//Shift_JIS関連のおまじない System.Text.Encoding.RegisterProvider(System.Text.CodePagesEncodingProvider.Instance); return Strings.StrConv(text, VbStrConv.Katakana, 0x411);
さて、残りはforなどでぶん回す実装です。
2.2 forで回す
var textArr = text.ToCharArray(); for (var i = 0; i < textArr .Length; i++) { if (IsHiragana(textArr[i])) { text[i] = (char)(textArr[i] + 0x60); } } return new string(textArr);
2.3 LINQで回す
サクッと書けるやつ。new string
がダサすぎるのでchar[]
かIEnumerable<char>
の拡張メソッドを生やしたくなります。
return new string(text .Select(x => IsHiragana(x) ? (char)(x + 0x60) : x) .ToArray());
2.4 for(unsafe)で回す
forやLINQの実装を見てるとToCharArrayとかnew stringとかアロケーションが発生してそうで嫌ですよね。そんなあなたにunsafe。
fixed (char* p = text) { for (var i = 0; i < text.Length; i++) { if (IsHiragana(p[i])) { p[i] = (char)(p[i] + 0x60); } } } return text;
2.5 速度比較まとめ
Method | Mean | Error | StdDev | Gen 0 | Allocated |
---|---|---|---|---|---|
VB.NET StrConv | 3,473.99 ns | 1,011.636 ns | 55.451 ns | 0.3853 | 1,616 B |
for | 182.38 ns | 51.919 ns | 2.846 ns | 0.0610 | 256 B |
LINQ | 820.91 ns | 189.860 ns | 10.407 ns | 0.1659 | 696 B |
unsafe for | 80.56 ns | 9.147 ns | 0.501 ns | - | - |
実行時間は unsafe for < for < LINQ << VB.NET です。
VB.NETはだめですね。Shift_JISのおまじないも必要だしWindowsでしか動かないしパフォーマンスも最低です。
LINQ、for、unsafe forの順にコードが汚くなり、パフォーマンスが向上しています。
サクッと書くならLINQで、パフォーマンス重視ならunsafe forがいいんじゃないかなと思います。forで書くくらいならunsafe forにしたほうがよさそう。
3. 結論
ひらがな判定はforeachで文字ごとに比較し、ひらがな→カタカナ判定はunsafeを使ってごにょごにょするのがよいのではないかという結論になりました。
ひらがな判定本体(IsHiragana)に関しては、短絡評価を考慮して、入力されるであろう文字列(アルファベットなのかカタカナなのか漢字なのか)によって不等式の向きや順番を変えてあげると実用上のパフォーマンスが若干変わってくるかもしれません。めんどくさいのでここではしませんが。