変換表について。

タグの編集
投稿者 YUU  (社会人) 投稿日時 2015/12/22 12:47:51
現在、指定の文字列のみ変換したい対象の文字列として変換し、出力するといったPGを作成しております。

過去、IFによる分岐やSelect Caseのような条件分岐を多用してきましたが読みにくく、かつ条件が増えれば増えるほど保守の観点からも大変です。

以前、お伺いした西暦から和暦への変換時にいただいた返信の中に変換表を利用した手法を提示していただいてからいろいろ試しております。
現在のコード↓

        Dim dic As Dictionary(Of Regex, String) = New Dictionary(Of Regex, String)() From {
                                {New Regex("山田"), "太郎"}, {New Regex("田中"), "太郎"},
                                {New Regex("佐藤"), "太郎"}, {New Regex("藤井"), "太郎"}}

        '変換表から、条件に合致する値のペアを取得する
        Dim text As String = "佐々木"
        Dim matchedValue As String = If(dic.FirstOrDefault(Function(item) item.Key.Match(text).Success).Value, "")

        Console.WriteLine("一致した値= {0}", matchedValue) '今回はマッチさせず。

上記の手法でもうまくいっているのですが、コード内に変換表が散らばりあまり見栄えとしてはよくありません。
変換表が多い、少ないの問題もあるのですが一部変換対象が異なる等でもDictionaryを新たに作成しているため見栄えが悪いです。

汎用的な手法があるのかわかりませんが、他になにか手法がございますでしょうか。


投稿者 shu  (社会人) 投稿日時 2015/12/22 15:43:02
ケースバイケースだと思うので、パターンをもう少し詳しく書かれた方がよいかと思います。

投稿者 るきお  (社会人) 投稿日時 2015/12/23 12:49:07
仕様がよくわからないので、想像してみました。
変換内容をプログラム中に埋め込むのだったら、私は次のように書くと思います。

この例ではReplaceを使った単純置換ですが、Regexを使うこともできます。
Public Class Form1
    Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click

        Dim source As String
        source = "アメリカとイギリスが戦った独立戦争にはフランス貴族の有志も参戦した。"

        Dim result As String

        Dim converter As New NameConverter
        result = converter.Convert(source)

        '結果 
        '亜米利加と英吉利が戦った独立戦争には仏蘭西貴族の有志も参戦した。 

    End Sub
End Class

Public Class NameConverter

    ''' <summary>変換元辞書。同じ順番にある変換先辞書の項目に変換します。</summary> 
    Protected Sources As String() =
        {
        "アメリカ""イギリス""ドイツ",
        "フランス""オランダ""スウェーデン"
        }

    ''' <summary>変換先辞書。同じ順番にある変換元辞書の項目から変換されます。</summary> 
    Protected Targets As String() =
        {
        "亜米利加""英吉利""独逸",
        "仏蘭西""和蘭""瑞典"
        }

    ''' <summary>変換元辞書の項目を変換先辞書の項目で置換します。</summary> 
    Public Overridable Function Convert(source As StringAs String

        Dim result As String = source

        For i As Integer = 0 To Sources.Length - 1
            result = Replace(result, Sources(i), Targets(i))
        Next

        Return result

    End Function

End Class


変換テーブルの量や内容によっては耐えられほど遅い処理になる可能性があります。
100個や200個のReplaceくらいなら余裕でこなすと思います。

YUUさんは、
>指定の文字列のみ変換したい対象の文字列として変換し、出力するといったPGを作成しております。
とのことですが、
定義されている「現在のコード」は何も変換していないように見えます。

DictionaryのキーにRegexのような機能的なものを格納するのは個人的な感覚としてはちょっと違和感があります。
投稿者 YUU  (社会人) 投稿日時 2015/12/23 16:52:13
返信ありがとうございます。

勢いで質問したためこちらのお伺いしたい内容に誤りがありました。

正確には条件に該当した値のペアを取得するです。

変換テーブルとは呼ばないのかな。

であるため条件の文字列が一致すればペアの値がほしいため疑似テーブルを想定しました。

>定義されている「現在のコード」は何も変換していないように見えます

失礼しました。変換ではなく別の値を取得ですね。
投稿者 YUU  (社会人) 投稿日時 2015/12/23 16:55:09
追記

例として単位があげられるかと思います。

センチメートル → ㎝

メートル → m

グラム → ㌘

など。こちらのイメージとしてはこのような感じで条件にあった値を取得したいです。
投稿者 るきお  (社会人) 投稿日時 2015/12/23 17:08:13
もうちょっと具体的に教えてください。
私の読解力では厳しいです。
インプットで何で、アウトプットは何なのでしょうか?

>正確には条件に該当した値のペアを取得するです。

>センチメートル → ㎝
>メートル → m
>グラム → ㌘

条件はどれで値のペアはどれですか?
投稿者 魔界の仮面弁士  (社会人) 投稿日時 2015/12/24 10:47:57
> センチメートル → ㎝
> メートル → m
> グラム → ㌘

例示として、あえて表記を揺らしているのだとは思いますが、
  U+3322『㌢』「SQUARE SENTI」
  U+334D『㍍』「SQUARE MEETORU」
  U+3318『㌘』「SQUARE GURAMU」
で統一するわけでは無いのですね。

このあたりも候補に上がりそう
  U+0063『c』「LATIN SMALL LETTER C」
  U+006D『m』「LATIN SMALL LETTER M」
  U+0067『g』「LATIN SMALL LETTER G」
U+210A『ℊ』「SCRIPT SMALL G」


それはさておき:


> こちらのイメージとしてはこのような感じで条件にあった値を取得したいです。 

完全一致なのか部分一致なのか、質問文からは読み取れませんでした。

完全一致で取得するのなら、Dictionary(Of String, String) で十分であり、
判定も ContainsKey メソッドだけで済みますから、Regex の出番すら
なさそうですので…部分一致なのでしょうか?

仮に部分一致だったとすると、たとえば
「100グラム当たり98円」を「100㌘当たり98円」に置き換える処理を
連想してみたのですが、この場合の変換ルールが良く分かりません。

たとえば、「おセンチセンチメートル」という文章には
「センチメートル」と「メートル」という語が両方含まれますが、
複数の条件が同時に当てはまるような場合の変換優先度が
質問内容からは読み取れませんでした。



> コード内に変換表が散らばりあまり見栄えとしてはよくありません。
であれば、コード外に変換表を配置するとか。(データベース、リソース、App.Config、XML ファイル等々)
投稿者 YUU  (社会人) 投稿日時 2015/12/24 15:30:05
返信遅くなりましてすみません。

>例示として、あえて表記を揺らしているのだとは思いますが、
あくまで例題として挙げさせていただきましたが不十分でした。

>完全一致なのか部分一致なのか、質問文からは読み取れませんでした。
これも不十分ですみません。自分のコードからサンプル用に出力したため一部不明瞭な点がございます。
今回の例としては完全一致が正であるためRegexは不正ですね。

>コード外に変換表を配置するとか。(データベース、リソース、App.Config、XML ファイル等々)
データベースもしくはリソースファイルあたりでしょうか。クラス化程度に考えておりました。

今回提示させていただくサンプルも適切ではないかと思いますが上記の点を踏まえ修正してみました。

    Private Sub Button1_Click(ByVal sender As Object, ByVal e As EventArgs) Handles Button1.Click

        Dim ct = New Table
        Dim text As String = "センチメートル"
        Dim matchedValue As String = If(ct.dic.FirstOrDefault(Function(k) k.Key = text).Value, "")

        Console.WriteLine("Found value= {0}", matchedValue)

'Found value= ㎝(出力結果)

    End Sub

Public Class Table

    Public dic As Dictionary(Of String, String) = New Dictionary(Of String, String)() From {
                                                {New String("ナノメートル"), "nm"},
                                                {New String("センチメートル"), "㎝"},
                                                {New String("マイクロメートル"), "μm"},
                                                {New String("ミリメートル"), "mm"}}


End Class

不十分な点、修正点等ございましたらご指摘いただけると幸いです。

よろしくお願い致します。
投稿者 YuO  (社会人) 投稿日時 2015/12/24 16:29:51
ぱっと見,
・Dictionaryの初期化で用いているNew String(" ... ")は不要 (というか無駄) なので,そのまま" ... "と書く
・Dictionaryに対してFirstOrDefault拡張メソッドでなめるよりも,TryGetValueメソッド使った方がよい
あたりでしょうか。

後者に関して,Dictionaryの初期化時にIEqualityComparerを渡せるため,
これを渡しているとFirstOrDefaultとTryGetValueで比較のされ方が異なってきます。
# あと,FirstOrDefaultはOption Compareが効くけれどもTryGetValueは効かない
投稿者 (削除されました)  () 投稿日時 2015/12/24 17:29:27
(削除されました)
投稿者 魔界の仮面弁士  (社会人) 投稿日時 2015/12/24 17:32:42
> Private Sub Button1_Click(ByVal sender As Object, ByVal e As EventArgs) Handles Button1.Click
>    Dim ct = New Table
Button1_Click 内で、Dictionary を毎回 New するのは得策ではありません。

New Dictionary するのは、変換表一つにつき一度だけにし、
アプリケーション内では同じインスタンスを使いまわす方が良いでしょう。


> Dim matchedValue As String = If(ct.dic.FirstOrDefault(Function(k) k.Key = text).Value, "")

FirstOrDefault を使うのは、データ件数が多い場合に不利になります。
ContainsKey を使うほうがお奨めです。
Dim matchedValue As String = If(dic.ContainsKey(text), dic(text), "")

YuO さんが紹介された、TryGetValue を用いる手もありますね。


> データベースもしくはリソースファイルあたりでしょうか。
App.config で設定する例を紹介しておきます。

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <configSections>
    <sectionGroup name="変換辞書">
      <section name="単位" type="System.Configuration.DictionarySectionHandler" />
    </sectionGroup>
  </configSections>
  <変換辞書>
    <単位>
      <add key="ナノメートル" value="㎚" />
      <add key="ミリメートル" value="㎜" />
      <add key="センチメートル" value="㎝" />
      <add key="平方センチメートル" value="㎠" />
      <add key="平方メートル" value="㎡" />
      <add key="平方キロメートル" value="㎢" />
      <add key="立方ミリメートル" value="㎣" />
      <add key="立方キロメートル" value="㎦" />
      <add key="バール" value="㍴" />
      <add key="ミリバール" value="㏔" />
      <add key="ヘクトパスカル" value="㍱" />
      <add key="キロバイト" value="㎅" />
      <add key="メガバイト" value="㎆" />
      <add key="ギガバイト" value="㎇" />
      <add key="カロリー" value="㎈" />
      <add key="キロカロリー" value="㎉" />
    </単位>
  </変換辞書>
</configuration>


上記を Dictionary として利用するためのコードはこんな感じ。
参照設定に System.Configuration.DLL を指定しておいて下さい。

※要VB2008以降

Public Shared Function 単位変換表のロード() As Dictionary(Of StringString)
    Return ReadDictionary("変換辞書/単位")
End Function

Public Shared Function ReadDictionary(sectionName As StringAs Dictionary(Of StringString)
    Return DirectCast(System.Configuration.ConfigurationManager.GetSection(sectionName), System.Collections.Hashtable).Cast(Of System.Collections.DictionaryEntry).ToDictionary(Function(x) x.Key.ToString(), Function(x) x.Value.ToString())
End Function
投稿者 YUU  (社会人) 投稿日時 2015/12/25 00:12:53
YuO様、魔界の仮面弁士様、返信ありがとうございます。

>Dictionaryの初期化で用いているNew String(" ... ")
確かに無駄ですね。削除しました。

>TryGetValueメソッド
検索回数が少なくスピードがでる?Dictionaryを使用する上で注意する点でしょうか。
使いかたが今一つつかめません。FirstOrDefaultやContainsKey等とはどのように異なるのでしょうか。
理解が遅くすみません。

>Button1_Click 内で、Dictionary を毎回 New するのは得策
テストとはいえ適切な書き方でないですね。ご指摘ありがとうございます。

>FirstOrDefault を使うのは、データ件数が多い場合に不利
これは上記のTryGetValueにつながる話ですね。件数が少ないため考えておりませんでした。
ContainsKeyも視野に検討し直してみます。

>App.config で設定する例を紹介
リソースファイルでのサンプルを頂けるとは。丁寧な解説ありがとうございます。今回はクラスでの処理を検討しておりますが
勉強になります。





投稿者 shu  (社会人) 投稿日時 2015/12/25 09:55:14
> 一部変換対象が異なる等でもDictionaryを新たに作成しているため見栄えが悪いです。

これが何なのかわかりません。
単純に1対1で文字列を変換するだけなら既出の回答で済むかと思いますが
懸念されている残りの問題はなんなのでしょう?
投稿者 魔界の仮面弁士  (社会人) 投稿日時 2015/12/25 10:37:33
> FirstOrDefaultやContainsKey等とはどのように異なるのでしょうか。
既にご存知かもしれませんが、FirstOrDefault を用いた
Dim matchedValue As String = If(ct.dic.FirstOrDefault(Function(k) k.Key = text).Value, "")
というコードは、要するに、
Dim matchedValue As String = ""    'Default Value 
For Each k In dic
    If k.Key = text Then
        matchedValue = k.Value
    End If
Next
の処理にあたります。

検索対象のデータが、もしも最後の一件に登録されていた場合、
探索のために全件ループが発生することになります。
そのため、データ量が多くなるにつれて、探索時間が伸びます。

一方、ContainsKey や TryGetValue の場合は、ハッシュという仕組みを
使って探索するようになっているため、キーを素早くみつけることができます。


そして、その ContainsKey や TryGetValue は、以下のように使います。
下記の 2 つのコードは、いずれも同じ意味です。

'ContainsKey を使った場合 
Dim matchedValue As String
If dic.ContainsKey(text) Then   'キーの存在チェック 
    matchedValue = dic(text)    '見つかったのでインデクサで取り出す 
    Debug.Print("発見!")
Else
    matchedValue = Nothing
    Debug.Print("見つからない")
End If


'TryGetValue を使った場合 
Dim matchedValue As String = Nothing
If dic.TryGetValue(text, matchedValue) Then  '存在チェック&値取得 
    Debug.Print("発見!")
Else
    Debug.Print("見つからない")
End If



> 今回はクラスでの処理を検討しておりますが
「外部リソースを読み取って管理するクラス」を作れば良いとおもいます。
My.Settings や My.Resources も、そうしたクラスですよね。

固定的データなら、内部リソースにするのも手ですが、
その変換表を今後もメンテナンスする予定があるのなら、
管理上は外部ファイルやデータベースの方が楽だと思います。


> リソースファイルでのサンプル
ちなみに app.config の各セクションは、別ファイルにすることも出来ます。

各ファイルは、EXE と同じ場所に配置しておく必要がありますが、
ソリューション エクスプローラーを右クリックし、各ファイルのプロパティで
「出力ディレクトリにコピー」を変更しておけば OK。
(ただし、App.config 本体だけはコピーしない設定にしておく)


=== App.config ===
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <configSections>
    <sectionGroup name="applicationSettings" type="System.Configuration.ApplicationSettingsGroup" >
      <section name="ConsoleApplication1.My.MySettings" type="System.Configuration.ClientSettingsSection" />
      <section name="WindowsApplication1.My.MySettings" type="System.Configuration.ClientSettingsSection" />
    </sectionGroup>
    <sectionGroup name="変換辞書">
      <section name="長さ単位" type="System.Configuration.DictionarySectionHandler" />
      <section name="面積単位" type="System.Configuration.DictionarySectionHandler" />
    </sectionGroup>
  </configSections>
  <applicationSettings>
    <WindowsApplication1.My.MySettings configSource="App.WindowsApplication1.config" />
    <ConsoleApplication1.My.MySettings configSource="App.ConsoleApplication1.config" />
  </applicationSettings>
  <変換辞書>
    <長さ単位 configSource="App.長さ単位.config" />
    <面積単位 configSource="App.面積単位.config" />
  </変換辞書>
</configuration>



=== App.長さ単位.config ===
<?xml version="1.0" encoding="utf-8" ?>
<長さ単位>
  <add key="ミリメートル" value="㎜" />
  <add key="センチメートル" value="㎝" />
</長さ単位>



=== App.面積単位.config ===
<?xml version="1.0" encoding="utf-8" ?>
<面積単位>
  <add key="平方ミリメートル" value="㎟" />
  <add key="平方センチメートル" value="㎠" />
</面積単位>



=== App.WindowsApplication1.config ===
<?xml version="1.0" encoding="utf-8" ?>
<WindowsApplication1.My.MySettings>
  <setting name="Poster" serializeAs="String">
    <value>YUU</value>
  </setting>
  <setting name="ThreadId" serializeAs="String">
    <value>25977</value>
  </setting>
  <setting name="Title" serializeAs="String">
    <value>変換表について。</value>
  </setting>
</WindowsApplication1.My.MySettings>



=== App.ConsoleApplication1.config ===
<?xml version="1.0" encoding="utf-8" ?>
<ConsoleApplication1.My.MySettings>
  <setting name="Poster" serializeAs="String">
    <value>魔界の仮面弁士</value>
  </setting>
  <setting name="CommentId" serializeAs="String">
    <value>69614</value>
  </setting>
</ConsoleApplication1.My.MySettings>
投稿者 YUU  (社会人) 投稿日時 2015/12/25 18:38:22
返信ありがとうございます。

>これが何なのかわかりません。
ご指摘ありがとうございます。
正確にはDataTableをLinqでGroupByしたものをDictionaryと比較し上記の処理としています。

>単純に1対1で文字列を変換するだけなら既出の回答
その通りなのですが複数のロジックの中で利用する場合、一部そのペアを取得する際に不都合が生じる処理が有ります。
その為、いくつかDictionaryを用意しているのですがそれも冗長的で・・・。

上記例だと

    Public dic As Dictionary(Of String, String) = New Dictionary(Of String, String)() From {
                                                {"ナノメートル", "nm"},
                                                {"センチメートル", "㎝"},
                                                {"マイクロメートル", "μm"},
                                                {"ミリメートル", "mm"}}

    'これを利用するロジックではミリメートルはスルーしたい。。
    Public dic2 As Dictionary(Of String, String) = New Dictionary(Of String, String)() From {
                                                {"ナノメートル", "nm"},
                                                {"センチメートル", "㎝"},
                                                {"マイクロメートル", "μm"}}

    'ペアが異なる場合
    Public dic3 As Dictionary(Of String, String) = New Dictionary(Of String, String)() From {
                                                {"ナノメートル", "nm"},
                                                {"センチメートル", "㎝"},
                                                {"マイクロメートル", "?"}'別の何かになる場合。}

サンプルが適切ではありませんがこのようなパターンの場合条件分岐ないし何かしら手がないかというニュアンスでした。
分かり難く申し訳ありませんでした。


>そして、その ContainsKey や TryGetValue は、以下のように使います。

分かり易い解説、サンプルありがとうございます。分かった気になっている部分が多く助かりました。
確かに自身のコードでは探索回数が多く最善な処理かと問われると難しいかと。


>管理上は外部ファイルやデータベースの方が楽だと思います。
閲覧、管理共にDBの方がしやすいので検討はしているのですが諸事情により困難な状況です。
それこそ上記サンプルの単位の様に変更の頻度が少なく、更新の必要がないものは決め打ちでもよいかもしれませんね。

>ちなみに app.config の各セクションは、別ファイルにすることも出来ます。
app.configはあまり使い慣れていないのでこれを機会に利用することも検討していきます。
投稿者 魔界の仮面弁士  (社会人) 投稿日時 2015/12/25 23:32:49
> 'これを利用するロジックではミリメートルはスルーしたい

その「ロジック」の具体的なところが分からないと判断しにくいのですが、
複合条件があるのなら、判断条件に用いるパラメータ(仮にT型として)も含めて
  As Dictionary(Of String, Func(Of T, String))
あるいは、
  As Dictionary(Of 条件値, Dictionary(String, String))
のような二段階変換にしてしまうのはどうでしょう。
投稿者 YUU  (社会人) 投稿日時 2015/12/27 11:03:50
返信ありがとうございます。

>その「ロジック」の具体的なところが分からないと判断

その通りだと思います。ただ詳細をこちらの都合ではありますがお伝えできないため不出来なサンプルに頼らざる得ない状況です。

>複合条件
一番しっくりくるのが連携したいシステムのverによる仕様変更です。
今作成中のアプリは同システムではあるのですがverがいくつか異なるシステムと連携する必要があります。

その為、上記のようなペア違いが発生しコードにばらつきが目立ちます。

テーブルをver毎に持たせるのも冗長的で悩んでいるのが今回の主題です。

ver1.0用のDictionaryを用意し各ver用の引数を渡すないし、分岐させ変更等の手法が最善なのか?。

>パラメータ(仮にT型として)
私の理解では As Dictionary(Of String, Func(Of T, String))でうまく実現する方法が思いつきません。

どのような手法があるのでしょうか。
投稿者 魔界の仮面弁士  (社会人) 投稿日時 2015/12/28 01:11:49
>>パラメータ(仮にT型として)
> 私の理解では As Dictionary(Of String, Func(Of T, String))でうまく実現する方法が思いつきません。
たとえば T が「バージョン番号 As Integer」だとして、
YUU さん提示の dic, dic2, dic3 をまとめる場合の例。

Public dic0 As New Dictionary(Of String, Func(Of IntegerString))() From {
      {"ナノメートル"Function(version) "nm"},
      {"センチメートル"Function(version) "㎝"},
      {"マイクロメートル"Function(version) If(version = 3, "?""μm")},
      {"ミリメートル"Function(version) If(version = 1, "mm"Nothing)}
    }


ナノメートルとセンチメートルは、dic, dic2, dic3 で共通なので、T の値に関わらず変換
マイクロメートルは、dic3 のみ「別の何か」なので If 演算子等で分岐
ミリメートルは、dic のみの変換なので、T が 2 や 3 のときはNothing を返却
――というイメージです。



まぁ、実際の実装の是非は、要件次第で変わってくるとは思いますが、
バージョンごとの差が大きいなら、テーブルをver毎に持たせるのもよいでしょうし、
ごく一部だけの差異ならば、共有テーブルとバージョン別テーブルを持たせるのもよいでしょう。

たとえば:
Imports Map = System.Collections.Generic.Dictionary(Of StringString)
Module Module1
    '共通変換テーブル 
    Private dicStandard As New Map() From {
        {"ナノメートル""nm"},
        {"センチメートル""㎝"},
        {"マイクロメートル""μm"},
        {"ミリメートル""mm"}
    }

    'バージョン別差分 
    Private dicDiff As New Dictionary(Of Integer, Map) From {
        {1, New Map() From {}},
        {2, New Map() From {{"ミリメートル", Nothing}}},
        {3, New Map() From {
                {"ミリメートル", Nothing},
                {"マイクロメートル""?"}
            }
        }
    }

    Public Function Convert(version As Integer, source As StringAs String
        Dim result As String = Nothing
        If dicDiff(version).ContainsKey(source) Then
            'バージョン別差分を優先的に返す 
            Return dicDiff(version)(source)
        Else
            'バージョン別差分に情報が無ければ共通テーブルを使う 
            dicStandard.TryGetValue(source, result)
        End If
        Return result
    End Function
End Module
投稿者 YUU  (社会人) 投稿日時 2015/12/28 23:54:17
返信ありがとうございます。

いつも丁寧なサンプルありがとうございます。

>まぁ、実際の実装の是非は、要件次第で変わってくるとは思いますが、
その通りだと思います。今回は仕様もさほど変更点がなかったためこのような形で落ち着くかと思いますが、
大きな変更である場合は新規のテーブルや、PGにすべきだと思いました。

実際のところver変動の激しいシステムとの連携はどのような実装を心がけるのが最善なのか難しいところですね。

古い部分を完全に切り捨てることができるのであればまだしも、並行運用はバグやエラーのもとになりかねないですし。

今回は長期間にわたり、質問や疑問に返信していただきありがとうございました。

他の機会がありましたらお力お貸ししていただけると幸いです。