packages feed

praxo-core-0.3.0: src/Praxo/Pattern.hs

module Praxo.Pattern (matchPattern) where

import Data.Char (isAscii, isDigit, isSpace)
import qualified Data.IntSet as Set
import qualified Data.IntMap.Strict as Map

type Token = (Char -> Bool, Char)

matchPattern :: String -> String -> Either String Bool
matchPattern patternText input
    | length patternText > 256 || (toInteger (length patternText) + 1) * (toInteger (length input) + 1) ^ (2 :: Int) > 2000000 = Left "SCHEMA_PATTERN_CAPACITY"
    | not (all isAscii patternText) = Left "SCHEMA_PATTERN_UNSUPPORTED"
    | otherwise = do
        let (atStart, body) = case patternText of '^':rest -> (True, rest); _ -> (False, patternText)
        (tokens, atEnd) <- compile body
        let initial = if atStart then Set.singleton 0 else Set.fromList [0..length input]
            result = foldl transition initial tokens
        pure (if atEnd then Set.member inputSize result else not (Set.null result))
  where
    inputSize = length input
    characters = Map.fromList (zip [0..] input)
    transition positions (predicate, quantifier) = Set.fromList
        [end | start <- Set.toList positions, end <- consume predicate quantifier start]
    consume predicate quantifier start =
        let ends = takeWhile (\end -> end <= inputSize && (end == start || predicate (characters Map.! (end - 1)))) [start..]
        in case quantifier of
            '*' -> ends
            '+' -> drop 1 ends
            '?' -> take 2 ends
            _ -> take 1 (drop 1 ends)

compile :: String -> Either String ([Token], Bool)
compile [] = Right ([], False)
compile "$" = Right ([], True)
compile text = do
    (predicate, rest) <- atom text
    let (quantifier, remaining) = case rest of
            first:suffix | first `elem` "*+?" -> (first, suffix)
            _ -> ('1', rest)
    (tokens, atEnd) <- compile remaining
    pure ((predicate, quantifier) : tokens, atEnd)

atom :: String -> Either String (Char -> Bool, String)
atom ('.':rest) = Right ((/= '\n'), rest)
atom ('\\':code:rest) = do
    predicate <- escaped code
    pure (predicate, rest)
atom ('[':rest) = do
    let (negative, body) = case rest of '^':suffix -> (True, suffix); _ -> (False, rest)
    (predicates, remaining) <- characterClass body
    if null predicates then Left "SCHEMA_PATTERN_UNSUPPORTED" else
        Right ((\c -> any ($ c) predicates /= negative), remaining)
atom (first:rest)
    | first `elem` "^$[](){}|*+?\\" = Left "SCHEMA_PATTERN_UNSUPPORTED"
    | otherwise = Right ((== first), rest)
atom [] = Left "SCHEMA_PATTERN_UNSUPPORTED"

escaped :: Char -> Either String (Char -> Bool)
escaped 'd' = Right isDigit
escaped 'w' = Right (\c -> isAscii c && (c `elem` ['a'..'z'] || c `elem` ['A'..'Z'] || isDigit c || c == '_'))
escaped 's' = Right (\c -> isAscii c && isSpace c)
escaped code
    | code `elem` ".^$[](){}|*+?\\-" = Right (== code)
    | otherwise = Left "SCHEMA_PATTERN_UNSUPPORTED"

characterClass :: String -> Either String ([Char -> Bool], String)
characterClass (']':rest) = Right ([], rest)
characterClass ('\\':code:rest) = do
    predicate <- escaped code
    (others, remaining) <- characterClass rest
    pure (predicate : others, remaining)
characterClass (low:'-':high:rest) | high /= ']' = do
    if low > high || low == '\\' || high == '\\' then Left "SCHEMA_PATTERN_UNSUPPORTED" else do
        (others, remaining) <- characterClass rest
        pure ((\c -> c >= low && c <= high) : others, remaining)
characterClass (first:rest) = do
    (others, remaining) <- characterClass rest
    pure ((== first) : others, remaining)
characterClass [] = Left "SCHEMA_PATTERN_UNSUPPORTED"