こちらの投稿でLIKE演算子相当のものをPowerQueryで作りましたが、"%"や"_"の対応ができていなかったので、改めて作ってみました。
学び
- @をつけると再帰呼び出しで関数が呼び出せるということ。
LikeText関数
MatchRecursive関数がLIKE演算子の本体。
LikeText
(input as any, pattern as any, escapeChar as nullable text) as logical =>
let
safeInput = ToText(input, ""),
safePattern = ToText(pattern, ""),
actualEscapeChar = if escapeChar = null then "\" else escapeChar,
result = MatchRecursive(Text.ToList(safeInput), TokenizePattern(safePattern, actualEscapeChar), actualEscapeChar)
in
result
MatchRecursive関数
設計の考え方として、
- iList(入力値)とpList(パターン)を1文字ずつ比較して同じならtrue、違うならfalseを返す。文字列の長さが異なる場合もfalse。
- "_"はiListの先頭と一致したとみなして、iListの残りの文字を比較する。
- "%"はiListの全要素と、pListの残りで、どれか1つでも一致すればtrue、違うならfalse。
という考え方です。
特筆すべきところは、@MatchRecursive
のところ。 @をつけると再帰呼び出しで関数が呼び出せるということ。
MatchRecursive
(iList as list, pList as list, escapeChar as text) as logical =>
if List.IsEmpty(pList) then
List.IsEmpty(iList)
else
let
p = List.First(pList),
restP = List.Skip(pList, 1)
in
// ゼロ文字以上のワイルドカード
if p = "%" then
List.AnyTrue(
List.Transform(
{0..List.Count(iList)}, each @MatchRecursive(List.Skip(iList, _), restP, escapeChar)
)
)
// 1文字以上のワイルドカード
else if p = "_" then
if not(List.IsEmpty(iList)) then
@MatchRecursive(List.Skip(iList, 1), restP, escapeChar)
else
false
// エスケープ文字対応
else if Text.StartsWith(p, escapeChar) then
let
literal = Text.End(p, Text.Length(p) -1)
in
if not(List.IsEmpty(iList)) and List.First(iList)=literal then
@MatchRecursive(restP, List.Skip(iList, 1), escapeChar)
else
false
// 通常の文字
else
if not(List.IsEmpty(iList)) and p = List.First(iList) then
@MatchRecursive(List.Skip(iList, 1), restP, escapeChar)
else
false
ToText関数
ToText
(expr as any, fallback as nullable text) as text =>
let
result = try Text.From(if expr = null or expr = "" then fallback else expr) otherwise null
in
result
TokenizePattern関数
エスケープ文字列処理用のラッパー関数。本体は_Tokenize関数。
TokenizePattern
(pattern as text, escapeChar as nullable text) as list =>
let
// 指定が無かったらデフォルト値として"\"を使う
actualEscapeChar = if escapeChar = null then "\" else escapeChar,
// 文字列を文字のリストに変換
chars = Text.ToList(pattern)
in
_Tokenize(chars, {}, actualEscapeChar)
_Tokenize関数
_Tokenize
(lst as list, acc as list, escapeChar as text) as list =>
if List.IsEmpty(lst) then acc
else
let
head = List.First(lst),
rest = List.Skip(lst, 1)
in
// エスケープ文字の次の文字を&で結合して1つのトークンにする
if head = escapeChar and not List.IsEmpty(rest) then
let
nextHead = List.First(rest),
restRest = List.Skip(rest, 1)
in
@_Tokenize(restRest, acc & {escapeChar & nextHead}, escapeChar)
// エスケープ文字で終わってしまったら、不完全なエスケープなのでそれまでの累積文字列を返す
else if head = escapeChar and List.IsEmpty(rest) then
acc
else
// それ以外の文字は連続文字として&でつなげる
@_Tokenize(rest, acc & {head}, escapeChar)