{-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE DerivingStrategies #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ViewPatterns #-} module Shpadoinkle.Widgets.Types.Search where import Data.Aeson (FromJSON, ToJSON) import Data.Foldable as F (Foldable (foldl')) import Data.List (sort) import Data.Maybe (mapMaybe) import Data.String (IsString) import Data.Text (Text, isInfixOf, splitOn, strip, toLower, unpack) import GHC.Generics (Generic) import Text.EditDistance (defaultEditCosts, levenshteinDistance) newtype Search = Search { unSearch :: Text } deriving newtype (Eq, Ord, Show, Read, IsString, Semigroup, Monoid, ToJSON, FromJSON) deriving stock Generic newtype EditDistance = EditDistance { unEditDistance :: Int } deriving newtype (Eq, Ord, Show, Read, ToJSON, FromJSON) deriving stock Generic data Levenshtiened a = Levenshtiened { _distance :: !EditDistance, _unLevenshtiened :: a } deriving Eq instance Eq a => Ord (Levenshtiened a) where compare (Levenshtiened x _) (Levenshtiened y _) = unEditDistance x `compare` unEditDistance y mkLevenshtiened :: Text -> Search -> a -> Levenshtiened a mkLevenshtiened t (Search s) = Levenshtiened . EditDistance $ levenshteinDistance defaultEditCosts (prep s) (prep t) where prep = unpack . strip forgivingly :: Search -> Text -> Bool forgivingly (Search (strip -> "")) _ = True forgivingly (Search s) haystack = Prelude.all test . splitOn " " $ strip s where test "" = False test needle = forgive needle `isInfixOf` forgive haystack forgive = toLower . strip concatFuzzy :: [a -> Text] -> a -> Text concatFuzzy = F.foldl' (\f g a -> f a <> " " <> g a) (const "") fuzzySearch :: Ord a => [a -> Text] -> Search -> [a] -> [a] fuzzySearch toChunks s = fmap _unLevenshtiened . sort . mapMaybe (\x -> let hay = concatFuzzy toChunks x in if forgivingly s hay then Just $ mkLevenshtiened hay s x else Nothing )