アルパカ三銃士

〜アルパカに酔いしれる獣たちへ捧げる〜

Go で AND, OR 条件付きのテキストマッチを行う

AND, OR 条件付きでテキストがマッチしてるか確認する関数を作る。

PerlPHP 等の PCRE をサポートしてる正規表現を使う場合は、こんな感じでいい。

/(?=.*(hello|world))(?=.*(foo|bar))/

regex101.com

こうすると

hello, foo
hello, bar
world, foo
world, bar

といったそれぞれの文字列にマッチする。「hello または worldかつfoo または bar」にマッチするかどうかという関数を作りたい。

それで、この正規表現を組み立てるようなコードを書いてみたけど、Go ではこの (?=re) といった肯定先読みの表現がサポートされておらず、困ってしまった。

そこで、自力でロジックを書くことにしたが...

全然ロジックが思いつかず、ついにはご飯を食べてしまった。そしたら思いついた。

まずはこんな感じのスライスのスライスを用意する。

dicts := [][]string{
    []string{
        "hello",
        "world",
    },
    []string{
        "foo",
        "bar",
    },
}

これで表現しているのは 「hello または worldかつfoo または bar」である。
この辞書を基に単語がマッチするか確認する関数がこんな感じ。

func ContainsWord(dicts [][]string, field string) bool {
    matchCount := len(dicts)
    if matchCount == 0 {
        return false
    }
    for _, dict := range dicts {
        for _, v := range dict {
            if strings.Contains(field, v) {
                matchCount--
                break
            }
        }
    }
    return matchCount == 0
}

https://play.golang.org/p/rEPO-xykZ9_L

この関数がやってることは

  1. マッチすべき個数(スライスの個数。上では 2 個)を取得する
  2. 辞書が空っぽであれば false を返す
  3. 「A または B」がマッチすれば、カウンタを 1 つ減らし、マッチすべき個数へ近づけていく。次の「C または D」を確認する...というのを繰り返す
  4. マッチすべき個数、マッチされてれば true を返す

とてもシンプル。最初は難しく考えすぎていて、AND, OR の構文木を struct で表現しようかと思ってた...

追記

sters さんからこうするともっと早くなると教えてもらった。

for _, dict := range dicts {
    found := false
    for _, v := range dict {
        if strings.Contains(field, v) {
            found = true
            break
        }
    }
    if !found {
        return false
    }
}
return true

私が書いたコードだと AND 条件をすべて探索するが、"A AND B" の A にマッチしない場合は即座に false を返すようになっている。