続:ạに関する諸問題

Windows の標準ベトナム語入力からạを入力すると二文字になるというのは、前回の記事。

ですが、Wikipedia上では、一文字の「合成済み」が標準として使われているようです。(Wikipediaの規約などを調べたわけではないので推測です。)

なので、ベトナム語版Wikipedia で「ホーチミン」のページを開くと、URLは下図のようになります。(Firefox 10 の場合。)

URLをWord に貼り付けると、こうなります。

http://vi.wikipedia.org/wiki/H%E1%BB%93_Ch%C3%AD_Minh

 

さて、このURLをWindowsから手入力してみます。” Hồ_Chí_Minh”を上記URLの/wiki/ の後に貼り付けます

URLを同様にWordに貼り付けると、こうなります。

http://vi.wikipedia.org/wiki/H%C3%B4%CC%80_Chi%CC%81_Minh

 

二つのURLは厳密には違うところに注意してください。

しかしながら、Wikipediaは両者を「同じもの」とみなして、同じ内容を表示しています。

 

ここまで来るともう少し意地悪してみたくなります。

これら二つの「ホーチミン」を検索エンジンに投入するとどうなるのか?

さっそくBingとGoogleでやってみました。(Firefoxの検索ボックスから投入しただけという、実に手抜きなテストです。)

それぞれ、上段が合成文字、下段が合成済み文字です。

Bing

Google

 

なかなか見事な結果がとれました。

Bingはこれら二つを「別のもの」として扱っています。そのため、検索結果は大きく割れて、合成文字で入力した場合には欲しい情報が得られない可能性があります。

Googleはこれら二つを「同一のもの」として扱っています。結果はほぼ同じです(1億超を全部比較したわけではないので断定はしません)。

ạ に関する諸問題

日本人があまり目にすることはない字ですが、確かに世の中に存在します。
ベトナム語で使用される文字です。”a”に、声調記号の.が付加された文字です。
(豆:thanh nặnh という声調を表す記号です。)

で、この文字を、Windows から標準ベトナム語入力を使用して入力すると、
U+0061 (Latin Small Letter A)
U+0323 (Combining Dot Below)
の二文字になります。二文字ですが、aの真下にドットが描画されるのが正しい動きです。考慮されていないフォントの場合には、aの次に文字化け記号が表示されることがあります。

ところが、同じ文字を iOS から入力してみたところ、
U+1EA1 (Latin Small Letter A With Dot Below)
という文字になりました。

前回の記事 続:「が」と「が」は等しいか? の問題が「何から入力したか」によっても発生するわけです。

どちらが正しいとかそういう話ではなく、混乱の元になりそうな話ではあります。
何も考えずに単に「文字」として比較すると酷い目にあえそうな、そんな感じです。

続:「が」と「が」は等しいか?

前回は「が」の話でしたが、今度は「ệ」です。
以前「今更な文字列長の話」で取り上げた文字です。

これをWindows 7 + 標準のベトナム語入力メソッド で入力すると、合成文字になります。
ê : U+00EA に、声調記号の下ドット U+0323 (Combining Dot Below) が結合しています。

ですが、「が」の時と同じように、全く同じ文字が別のコードに割り当てられています。

「ラテン拡張追加」の U+1EC7 に “Latin Small Letter E With Circumflex and Dot Below” として が割り当てられています。たとえば Wikipedia 上では、こちらの U+1EC7 を使うのが一般的なようです。

そこで簡単な実験。Wikipedia で「ベトナム」のページを開きます。画面右にベトナム語での国号が書かれています。ブラウザ内検索 [Ctrl] + [F] で「ê」を検索します。
結果は、「ヒットしない」です。(IE9 と Firefox 9.0.1 で検証)
(まぁこのページで実験していただいてもよいのですが…)

確かに文字コードとしては別物ですが、ユーザーはそうは考えないかもしれません。
例えば、なぜ画面上に表示されている「か」を検索できないのかを義母に説明するとか、そういうことを考えるとちょっと恐ろしくなります。

「が」と「が」は等しいか?

間違い探しクイズのような感じですが、この二つの「が」は厳密には違います。

「が」 – U+304C

「が」 – U+304B U+3099

と、異なる文字で記述しています。見た目は(フォントなどによりますが)一緒です。話がややこしくなるので、1文字の方を「」、2文字の方をが」とします。
(U+3099 は、”Combining Katakana-Hiragana Voiced Sound Mark” という名前がついています。結合文字用の濁点です。)

なお、手元の環境 (Win7 + ATOK) で「が」を入力すると1文字の方の」でした。

ということは、String.Length をとると…

」 = 1

が」 = 2

となります。さらに、これらを入れたStringを比較すると…

Dim strOne As String = "が"  '---一文字の方です

Dim strTwo As String = "が"  '---二文字の方です

MsgBox(strOne = strTwo) '---FALSE が表示される

となります。確かにString オブジェクトの中身という観点では正しい挙動なのですが、見た目が同じなのでかなりややこしい問題の元になりそうです。

これをこうしてみます。

Thread.CurrentThread.CurrentCulture = New Globalization.CultureInfo("ja-JP")

MsgBox(String.Equals(strOne, strTwo)) '---FALSEが返却されます

MsgBox(String.Equals(strOne, strTwo, StringComparison.CurrentCulture)) '---TRUEが返却されます

後者のコードは、明示的に”ja-JP”カルチャーで比較するようにしたものです。

このあたりについては、MSDN : .NET Framework で文字列を使用するためのベスト プラクティス に詳しいのですが、やたらと話が入り組んでいてわかりにくいのが難点です。

書評:Cooking for Geeks

Cooking for Geeks – 料理の科学と実践レシピ

発行:オライリー・ジャパン

書店のオライリーの棚に料理本という「?」な状況に思わず買ってしまいました。まぁ、趣味としての料理も嫌いではないので。

さて、”for Geeks”と銘打っているだけあって、書き方はいろいろとそそります。食材の「入力」、時間と温度の「変数」など。
タンパク質の温度による変性、フライパンの材質による熱伝導と反応速度、材料加熱・解凍における熱勾配など、科学的に解説されていて、実に興味をそそりますし、勉強になります。

しかしながら、良くも悪くも米国の書籍の翻訳です。本文中に出てくる各単位系がヤード・ポンド系なのは実に残念です-例えばグラフの軸は華氏ではなく摂氏にすべきではなかったのでしょうか。
同様に、どうやっても米国風の肉中心・デザートが中心で、魚はあまり登場しません。さらに、米国食品医薬品局の基準やニューヨーク州の食品衛生基準など、別にどうでもよろしいという気もします。日本の家庭には普通存在しない(設置も難しい)機材の使い方を懇切丁寧に説かれても如何かと思います。(液体窒素なぞ常人には手に入りません-が、そこはネタの章なので。)

まとめ:
立ち読みで十分です。その上で、料理の化学に興味があるのでしたらどうぞ。

今更な文字列長の話

相当に今更感があるのですが、文字列長の話です。
普通に考えれば、string.Lenth か、Len(String) 取ればいいんじゃね?と思うのですが、残念ながら…というお話です。

よーくMSDNを読んでみて下さい。
まずは、String.Length の解説
「プロパティ値」に「現在の文字列の文字数。」と書いてありますが、その下の「解説」の冒頭に、「Length プロパティは、このインスタンス内の Char オブジェクトの数を返します。Unicode 文字の数ではありません。」と書いてあります。

Char の数 ≠ 文字の数

ということです。これがどういうコトかは、後ほど改めて説明します。

お次は Len です。
こちらも「文字列内の文字数または変数を格納するために必要な公称バイト数を表す整数を返します。」と書かれています。この「または」以下がくせ者です-まるで法律の但し書き条項のようにくせ者です。
解説欄をよく読むと、まるでVB6以前のファイルの扱い方の解説を読んでいるような気分になります-FilePutなんて何時の話だと…。

どちらも何故こんな回りくどい言い回しをしているのか?何故素直に「文字列長」と言えないのか。
そもそも、VB.NETに移行することによって、 Len と LenB の混乱から逃れたのではなかったのか。

残念ながら、そうではないのです。
理由は大きく分けて二つあります。
一つは「サロゲートペア(代用対)」問題。Unicodeの16bitから溢れて、32bit(=Char 二つ分)で一文字を表す文字が存在することです。更に悪いことに、日本語にはそれにが居るということです。
もう一つは、他の言語で使われる「合成文字」です。こちらもコードとしては2byte以上(合成するだけ)必要ですが、表現する文字としては「1文字」です。濁点・半濁点を考えてもらえれば良いでしょう(実際、一部の体系ではそうですし)。

これらの問題を解決した上で「文字列」の長さをきちんと数えるには、System.Globalization.StringInfo を使用しなければいけないのです。

論より証拠。実際にやってみましょう。


こんなモノを用意してみます。一番上のテキストボックスに入れたTextの文字数を表示するというモノです。

コードはこんな感じです。ここでは実験が目的なので、何も考えずにコードビハインドに書きます。

    Private Sub cmdExec_Click(sender As System.Object, e As System.Windows.RoutedEventArgs) Handles cmdExec.Click
        Dim strTarget = Me.txtTest.Text
        '一つめのテキストボックスには、単純にString.Length
        Me.txtLength.Text = strTarget.Length.ToString()
        '二つ目はLEN
        Me.txtLength2.Text = Len(strTarget).ToString()
        '三つめは国際化対応
        Dim g As New System.Globalization.StringInfo(strTarget)
        Me.txtLength3.Text = g.LengthInTextElements.ToString
    End Sub

では、実行結果。

まずはとりあえず「あ」でテストしてみます。

どれも一文字と表示しています。

次はサロゲートペア(代用対)の問題です。
よく知られているのは「𠮟る」ですが、その辺りの一覧はコチラにあります。

見事に割れました。見ての通り、「文字数」としての正解は「1文字」なので、正しく値を返しているのは、System.Globalization.StringInfo のみということになります。

このサロゲートペア(代用対)は、確かに高頻度に現れる問題ではないとはいえ、ハマるとデカいという落とし穴になっています。コードを書く側もテストする側も、「お約束のヒトネタ」として押さえておくと良いでしょう。

#おまけ:[アクセサリ]→[システムツール]→[文字コード表]でも、Unicodeの直接入力が4文字まで(!)です。𠮟(U+20B9F)はどうするんだと…
#もう一つ。wordpress を使用しているのですが、素のママで𠮟(U+20B9F)を投入したら、そこで投稿が打ち切られました(内部でなんか落ちたのかなと…)。これで小一時間悩みました…。こうなりますと言うことで。

最後に、外国語の合成文字の問題です。
日本語の濁点・半濁点付きの文字も「一文字」として割り振られています。例えば、「が」は U+304C です。が、そうではない言語もあります。ここではベトナム語を取り上げます。(現代の)ベトナム語は、アルファベットと声調記号の合成で表現されます。例えば「ベトナム」のことは”Việt Nam”と書きますが、これの”ệ”は、実は合成文字で二文字です。が、実際上は1文字として認識されます。(「が」を二文字と数える日本人がどれだけ居ますか?というのと同じです。)

こちらもズレました
バイト数で切ると、文や語の途中で打ち切られる可能性があります。

と言う訳で、文字列長とバイト長は使い分けましょうというお話でした。

VB.NET で Razor記法を試す

ASP.NET MVC3 といえば Razor が重要なポイントですが、それをVB.NETでやろうとするとどうなるのか。

基本的には@の使い方なので、C#とさしたる違いはなさそうに見えます。が、そうも行かない微妙な差異があります。
C# だと .cshtml だったのが .vbhtml になるというファイルの拡張子の話もありますが、まぁそれは置いておきます。

まず、いきなり一行目から違います。

@ModelType ...[モデル名]

同じコードをC#で書くと…

@model ...[モデル名]

となります。Visual Studio からViewの追加を行った場合には自動的に追加してくれるのですが、サンプルをコピって…という場合には一瞬困ります。

次はこちらです。

@Code
    ViewData("Title") = "ホーム ページ"
End Code

これをC#にすると

@{
    ViewData["Title"] = "ホーム ページ"
}

となります。この”Code” “End Code” がなんともかんとも…という感じです。

この冒頭部を乗り越えてしまえば、.vbhtml内はC#とほとんど…と言いたいところですがそうは行きません。
たとえば、ループ処理内で詳細ページへのリンクと国名・国コードを出すようなコードはこんな感じになります。
(AdventureWorks の CountryRegion, CountryRegionCurrency を使って書いた例です。)

    @For Each country In Model
        @<tr>
            <td>@Html.ActionLink("詳細", "Detail",New With{.CountryRegionCode = country.CountryRegionCode}) </td>
            <td>@country.Name [@country.CountryRegionCurrencies.Count]

            <td>@country.CountryRegionCode</td>
        @</tr>
    Next

Forループの書式もですが、匿名オブジェクトの作成方法と初期化方法にも注意が必要です。
匿名オブジェクトはMVC3では頻出です。New と With に気をつけてサンプルから持ってきても”.”を忘れがちです。

さらに、詳細ページでは…

        <td>@Html.DisplayFor(Function(x) x.CountryRegionCode)</td>
        <td>@Html.DisplayFor(Function(x) x.CurrencyCode)</td>

Html.DisplayFor など、引数にLinqの式をとる構文の場合、このような冗長な感じになってしまいます。
この例では一行で済んでいますが、複数行で”End Function”まで書く羽目になった日にはかなり涙目です。

やはり、VB.NETでのラムダ式は、もう少し簡素な方式を望みたいところであります。

Razorコードの色

Visual Studio のコードの色について。

他と同じように、[ツール(T)]→[オプション(O)] から[環境]→[フォントおよび色] で他と同じように変更できるのですが、「Razor コード」という設定項目があるので、それだろうと思うと、ちょっと残念なことになります。

「Razor コード」だけ設定すると、こんな感じになってしまいます。

中の”ViewData…”という部分が”Razorコード”で、”@Code”, “End Code” の部分は”HTML サーバーサイドスクリプト”だからです。

黄色い部分が”HTMLサーバーサイドスクリプト” にあたります。

なので、

この部分も忘れずに変更する必要があります。

VBで書くラムダ式

LINQはうまく使えば非常に便利なのですが、残念ながらVBで書くと非常にゴチャゴチャします。C# なら => で済む所が、Function やら Sub やら長ったらしく、しかも使い分ける必要があるのが悩みどころです。どういう感じになるか、ひとつやってみます。

お題:小田急線の列車種別・線別の停車駅をクエリして、コンソールに出力します。「正解」はコチラです。

Step1: 駅名を保持するクラスを用意します。列車種別と線名にFlags属性が付いているところに注意してください。

    Private Class Station

        <Flags()>
        Friend Enum TrainKind
            各停 = 1
            区間準急 = 2
            準急 = 4
            多摩急行 = 8
            急行 = 16
            快速急行 = 32
            特急 = 64
        End Enum

        <Flags()>
        Friend Enum Line
            小田原線 = 1
            多摩線 = 2
            江ノ島線 = 4
        End Enum

        Property 駅名 As String
        Property 線 As Line
        Property 停車 As TrainKind

    End Class

Step2:上で作ったクラスに初期値を入れておきます。 Sub main の冒頭でやっておけば十分です。(全駅を書くと大変なので、一部だけにします。)

        Dim colStations As New List(Of Station)
        With colStations
            .Add(New Station With {.駅名 = "代々木上原", .線 = Station.Line.小田原線, .停車 = Station.TrainKind.多摩急行 Or Station.TrainKind.快速急行 Or Station.TrainKind.急行 Or Station.TrainKind.準急 Or Station.TrainKind.区間準急 Or Station.TrainKind.各停})
            .Add(New Station With {.駅名 = "豪徳寺", .線 = Station.Line.小田原線, .停車 = Station.TrainKind.区間準急 Or Station.TrainKind.各停})
            .Add(New Station With {.駅名 = "登戸", .線 = Station.Line.小田原線, .停車 = Station.TrainKind.多摩急行 Or Station.TrainKind.急行 Or Station.TrainKind.準急 Or Station.TrainKind.区間準急 Or Station.TrainKind.各停})
            .Add(New Station With {.駅名 = "向ヶ丘遊園", .線 = Station.Line.小田原線, .停車 = Station.TrainKind.特急 Or Station.TrainKind.急行 Or Station.TrainKind.準急 Or Station.TrainKind.区間準急 Or Station.TrainKind.各停})
            .Add(New Station With {.駅名 = "栗平", .線 = Station.Line.多摩線, .停車 = Station.TrainKind.急行 Or Station.TrainKind.多摩急行 Or Station.TrainKind.区間準急 Or Station.TrainKind.各停})
            .Add(New Station With {.駅名 = "南林間", .線 = Station.Line.江ノ島線, .停車 = Station.TrainKind.急行 Or Station.TrainKind.各停})
        End With

Step3:早速クエリを作ってみます。条件は「小田原線内の多摩急行停車駅」です。まずは、一行で条件を書いてみます。

        Dim queryInLine = colStations.Where(Function(x) (x.停車 And Station.TrainKind.多摩急行) AndAlso (x.線 And Station.Line.小田原線)) _
                            .Select(Function(y) New With {.result = y.駅名 & "[" & y.線.ToString() & "]"})

        For Each stationName In queryInLine
            Console.WriteLine(stationName.result)
        Next

一行で書こうとするとやはりゴチャゴチャします。
このうちwhereの部分だけををC# で書くとすると、

var query = colStations.Where(x => (x.停車.HasFlag(Station.TrainKind.多摩急行)) && (x.線.HasFlag(Station.Line.小田原線)));

と、かなりすっきりするのですが、ないものねだりをしても仕方がありません。

また、Selectで匿名クラスを作っています。匿名クラスのメンバーであろうと、インテリセンスは補完してくれます。

Step4:同じ条件で、複数行で書いてみます。VS2010からは「暗黙の行継続文字」という機能がついたので、狂ったように”_”を付けなくていいところが救いです。「暗黙の行継続文字」が適用される条件については、コチラをご覧下さい。

        Dim queryWithLines = colStations.Where(Function(x)
                                                   Return (x.停車 And Station.TrainKind.多摩急行) _
                                                            AndAlso _
                                                          (x.線 And Station.Line.小田原線)
                                                   End Function
                                ).ToArray()

        Array.ForEach(queryWithLines, Sub(station As Station)
                                      Console.WriteLine(String.Format("{0} [{1}]", station.駅名, station.線))
                                  End Sub)

確かに冗長ですが、見通しはかなり良くなりました。

なお、Array.ForEachの中で直接Console.WriteLineを呼んでで見ましたが、ここは”Sub”であるところに注意してください。つまり、値を返さないので、”Function”ではないのです。この使い分けが若干面倒ではあります。

Step5:最後に、デリゲートを使ってみます。

                Dim fncFilter = Function(x As Station)
                            Return (
                                (x.停車 And Station.TrainKind.多摩急行) _
                                AndAlso _
                                (x.線 And Station.Line.小田原線))
                            End Function

        Dim fncWriteLine = Sub(x As Station)
                               Console.WriteLine(String.Format("{0} [{1}]", x.駅名, x.線))
                           End Sub

        Dim queryUseDeligate = colStations.Where(Function(x) fncFilter(x))
        Array.ForEach(queryUseDeligate.ToArray(), Sub(x As Station) fncWriteLine(x))

C#の「匿名メソッド」のようなものです(コチラによると厳密には違うそうですが)。ここでは細かいことは気にせず、「こういうやり方もありますよ」ということで。
デリゲートの変数名とか書く場所に気をつけてあげれば、見通しの良いコードになりそうな気がします。

文字列の数値変換

受け取った文字列を数値にしなければいけない時、ざっと3通りの方法が用意されています。

  • Integer.Parse(String)
  • Convert.ToInt32(String)
  • CInt(String) または CType(String, typename)  VBのみ

これらがどう違うのか、実験コードを書いてみます。

Sub Main()

    Console.WriteLine("Input number...")
    Dim strOriginal As String = Console.ReadLine()

     Try
        Dim intCastResult As Integer = Integer.Parse(strOriginal)
        Console.WriteLine("int.Parse : {0}", intCastResult)
    Catch ex As Exception
        Console.WriteLine("int.Parse failed.")
    End Try

    Try
        Dim intConvert As Integer = Convert.ToInt32(strOriginal)
        Console.WriteLine("Convert.ToInt32 : {0}", intConvert)
    Catch ex As Exception
        Console.WriteLine("Convert.ToInt32 failed.")
    End Try

    Try
        Dim intCtype As Integer = CType(strOriginal, Integer)
        Console.WriteLine("Ctype : {0}", intCtype)
    Catch ex As Exception
        Console.WriteLine("Ctype failed.")
    End Try

    Console.WriteLine("Hit any key...")
    Console.ReadKey()
End Sub

これを実行してみると、以下のようになります。
“123,456″ (カンマ区切りされた数値)
Input number…
123,456
int.Parse failed.
Convert.ToInt32 failed.
Ctype : 123456
Hit any key…

“\123456″ (通貨記号をつけてみた)
Input number…
\123456
int.Parse failed.
Convert.ToInt32 failed.
Ctype : 123456
Hit any key…

“$123456″ (ではドル記号で)
Input number…
$123456
int.Parse failed.
Convert.ToInt32 failed.
Ctype failed.
Hit any key…

“123.456,45″ (海外の3桁区切り)
Input number…
123.456,45
int.Parse failed.
Convert.ToInt32 failed.
Ctype failed.
Hit any key…

“&hFE” (では16進で)
Input number…
&hFE
int.Parse failed.
Convert.ToInt32 failed.
Ctype : 254
Hit any key…

CTypeは地域ロケールに依存しつつも、出来る限り変換しようと試みるようです。これが良い場合もあれば悪い場合もあるでしょう。

さらに、もう一歩突っ込んで、Convert.ToInt32 と int.Parse の違いを見てみましょう。
Convert の方は、IFormatProvider を引数に取ることができます。
Parse も IFormatProviderを引数に取ることができますが、NumberStyles列挙体も引数に取ることができます。例えば円記号とカンマ区切りを許容したいのであれば、

Dim intCastResult As Integer = Integer.Parse(strOriginal, Globalization.NumberStyles.AllowThousands Or Globalization.NumberStyles.AllowCurrencySymbol)

と書けることになります。
が、やはりこれも地域ロケールに依存します。
ロケールに依存させたくないというのであれば、最後はIFormatProviderということになるでしょう。

参考:
Int.Parse (MSDN)
Convert.ToInt32 (MSDN)
CType 関数 (MSDN)