packages feed

smh-0.2.1: src/Focusers.hs

{-# OPTIONS_GHC -Wno-unused-do-bind #-}
{-# OPTIONS_GHC -Wno-name-shadowing #-}
{-# LANGUAGE LambdaCase        #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes        #-}

module Focusers where

import           Common               (Focus (..), Focuser (..), Mapping,
                                       Parser, Range (RangeSingle),
                                       _toListUnsafe, composeFocusers,
                                       fromIndexes, getIndexes, lexeme,
                                       makeFilteredText, mapText,
                                       readMaybeRational, showRational, symbol,
                                       toListUnsafe, toTextUnsafe, unsort, ws)
import           Control.Applicative  ((<|>))
import           Control.Lens         (lens, partsOf, (^..))
import           Control.Monad        (void, when, zipWithM)
import           Data.Char            (isAlpha, isAlphaNum, isDigit, isLower,
                                       isSpace, isUpper)
import           Data.Data.Lens       (biplate)
import           Data.Function        (on)
import           Data.Functor         ((<&>))
import           Data.List            (sortBy, transpose)
import           Data.Maybe           (mapMaybe)
import           Data.Ord             (comparing)
import           Data.Ratio           (denominator)
import           Data.Text            (Text)
import qualified Data.Text            as T
import           Text.Megaparsec      (anySingle, anySingleBut, between, choice,
                                       empty, getOffset, many, optional,
                                       parseMaybe, satisfy, sepBy, some, try)
import           Text.Megaparsec.Char (char)
import           Text.Read            (readMaybe)
import           Text.Regex.PCRE      (AllMatches (getAllMatches), (=~))
import           Text.Regex.PCRE.Text ()

focusId :: Focuser
focusId = FTrav id

focusEach :: Focuser
focusEach = FTrav traverseFocus

traverseFocus :: Applicative f => (Focus -> f Focus) -> (Focus -> f Focus)
traverseFocus f focus = case focus of
    FText str -> FText . T.concat . map toTextUnsafe <$> traverse f (mapText (FText . T.singleton) str)
    FList lst -> FList <$> traverse f lst

focusCollect :: Focuser -> Focuser
focusCollect (FTrav innerTrav) = FTrav $ partsOf innerTrav . _toListUnsafe

focusWords :: Focuser
focusWords = FTrav wordsTrav

wordsTrav :: Applicative f => (Focus -> f Focus) -> (Focus -> f Focus)
wordsTrav _ flst@(FList _) = pure flst
wordsTrav f (FText str) =
    let (str_ws, str_words) = myWords str
        new_words = map toTextUnsafe <$> traverse (f . FText) str_words
        new_str = T.concat . interleave str_ws <$> new_words
    in  FText <$> new_str

myWords :: Text -> ([Text], [Text])
myWords "" = ([], [])
myWords str =
    let (ws, str') = T.span isSpace str
        (word, str'') = T.break isSpace str'
        (str_ws, str_words) = myWords str''
    in  (ws : str_ws, if not (T.null word) then word : str_words else str_words)

focusSpace :: Focuser
focusSpace = FTrav spaceTrav

spaceTrav :: Applicative f => (Focus -> f Focus) -> (Focus -> f Focus)
spaceTrav _ flst@(FList _) = pure flst
spaceTrav f (FText str) =
    let (str_nonspace, str_space) = mySpace str
        new_space = map toTextUnsafe <$> traverse (f . FText) str_space
        new_str = T.concat . interleave str_nonspace <$> new_space
    in  FText <$> new_str

mySpace :: Text -> ([Text], [Text])
mySpace "" = ([], [])
mySpace str =
    let (nonspace, str') = T.break isSpace str
        (space, str'') = T.span isSpace str'
        (str_nonspace, str_space) = mySpace str''
    in  (nonspace : str_nonspace, if not (T.null space) then space : str_space else str_space)

interleave :: [a] -> [a] -> [a]
interleave [] a2s                = a2s
interleave a1s []                = a1s
interleave (a1 : a1s) (a2 : a2s) = a1 : a2 : interleave a1s a2s

focusLines :: Focuser
focusLines = FTrav linesTrav

linesTrav :: Applicative f => (Focus -> f Focus) -> (Focus -> f Focus)
linesTrav _ flst@(FList _) = pure flst
linesTrav f (FText str) = FText . T.concat . map ((`T.append` "\n") . toTextUnsafe)
    <$> traverse (f . FText) (T.lines str)

transposeTravUnsafe :: Applicative f => (Focus -> f Focus) -> (Focus -> f Focus)
transposeTravUnsafe f flist = transposeFListUnsafe <$> f (transposeFListUnsafe flist)

transposeFListUnsafe :: Focus -> Focus
transposeFListUnsafe (FList lst) = FList . map FList $ transpose (toListUnsafe <$> lst)
transposeFListUnsafe _ =
    error "smh: transposeFListUnsafe called on a non-FList. Please, report this bug."

focusCols :: Focuser
focusCols = focusCollect (focusLines `composeFocusers` focusCollect focusWords)
    `composeFocusers` FTrav transposeTravUnsafe
    `composeFocusers` focusEach

focusSlice :: [Range] -> Focuser
focusSlice ranges = FTrav $ \f focus -> case focus of
    FText str -> FText <$> new_str
      where
        str_length = T.length str
        is = getIndexes ranges str_length
        filtered_str = makeFilteredText str_length is str
        new_filtered_str = toTextUnsafe <$> (f . FText $ filtered_str)
        new_str = updateText str is <$> new_filtered_str

    FList lst -> FList <$> new_lst
      where
        is = getIndexes ranges (length lst)
        filtered_lst = makeFilteredList is 0 lst
        new_filtered_list = toListUnsafe <$> (f . FList $ filtered_lst)
        new_lst = updateList lst . zip is <$> new_filtered_list
  where
    makeFilteredList [] _ _ = []
    makeFilteredList _ _ [] = []
    makeFilteredList (i : is) idx (c : str)
        | idx == i = c : makeFilteredList is (idx + 1) str
        | otherwise = makeFilteredList (i : is) (idx + 1) str

    updateList :: [a] -> [(Int, a)] -> [a]
    updateList as updates = aux (zip [0..] as) updates
      where
        aux old [] = map snd old
        aux [] _ = []
        aux ((i, a) : old) ((j, a') : updates)
            | i == j = a' : aux old updates
            | otherwise = a : aux old ((j, a') : updates)

    updateText :: Text -> [Int] -> Text -> Text
    updateText old is new = T.unfoldrN (oldLen + newLen) builder (0, 0, is)
      where
        newLen = T.length new
        oldLen = T.length old

        builder :: (Int, Int, [Int]) -> Maybe (Char, (Int, Int, [Int]))
        builder (oldI, newI, [])
            | newI < newLen = Just (T.index new newI, (oldI, newI + 1, []))
            | oldI < oldLen = Just (T.index old oldI, (oldI + 1, newI, []))
            | otherwise = Nothing
        builder (oldI, newI, i : is)
            | oldI == i = if newI < newLen
                then Just (T.index new newI, (oldI + 1, newI + 1, is))
                else builder (oldI + 1, newI + 1, is)
            | otherwise = Just (T.index old oldI, (oldI + 1, newI, i : is))

focusSortedBy :: Focuser -> Focuser
focusSortedBy (FTrav trav) = FTrav $ \f focus -> case focus of
    FText str ->
        let str_length = T.length str
            (is, sorted_str) = unzip $ sortBy (cmp `on` (FText . T.singleton . snd)) $
                zip [0..] $ T.unpack str
            new_sorted_str = toTextUnsafe <$> (f . FText) ( T.pack sorted_str)
            unsort_is = unsort is str_length
            new_str = unsortText unsort_is str_length <$> new_sorted_str
        in  FText <$> new_str
    FList lst ->
        let (is, sorted_lst) = unzip $ sortBy (cmp `on` snd) $ zip [0..] lst
            new_sorted_lst = toListUnsafe <$> (f . FList) sorted_lst
            new_lst = map snd . sortBy (comparing fst) . zip is <$> new_sorted_lst
        in  FList <$> new_lst
  where
    cmp f1 f2 =
        let f1' = f1 ^.. trav
            f2' = f2 ^.. trav
        in case (f1', f2') of
            ([FText s1], [FText s2]) -> case (readMDouble s1, readMDouble s2) of
                (Just n1, Just n2) -> compare n1 n2
                _                  -> EQ
            _ -> EQ

    unsortText :: [Int] -> Int -> Text -> Text
    unsortText is strLen str = T.unfoldrN strLen builder is
      where
        builder :: [Int] -> Maybe (Char, [Int])
        builder []       = Nothing
        builder (i : is) = Just (T.index str i, is)


    readMDouble :: Text -> Maybe Double
    readMDouble = readMaybe . T.unpack

focusIndex :: Int -> Focuser
focusIndex n_ = FTrav $ \f focus -> case focus of
    FText str -> if n < 0 || n >= T.length str then pure focus else
        (f . FText . T.singleton) (T.index str n) <&> \new_str ->
            case toTextUnsafe new_str of
                ""   -> FText str
                text -> FText $ updateTextAt str_length str n (T.head text)
      where
        str_length = T.length str
        n = if n_ < 0 then str_length + n_ else n_
    FList lst -> if n < 0 || n >= length lst then pure focus else
        let new_focus = f (lst !! n)
            in FList . updateListAt lst n <$> new_focus
      where
        n = if n_ < 0 then length lst + n_ else n_
  where
    updateListAt :: [a] -> Int -> a -> [a]
    updateListAt [] _ _         = []
    updateListAt (_ : olds) 0 a = a : olds
    updateListAt (o : olds) n a = o : updateListAt olds (n - 1) a

    updateTextAt :: Int -> Text -> Int -> Char -> Text
    updateTextAt strLen str i newC = T.unfoldrN strLen builder 0
      where
        builder :: Int -> Maybe (Char, Int)
        builder n
            | n >= strLen = Nothing
            | n == i    = Just (newC, n + 1)
            | otherwise = Just (T.index str n, n + 1)

focusMinBy :: Focuser -> Focuser
focusMinBy f = focusSortedBy f `composeFocusers` focusIndex 0

focusMaxBy :: Focuser -> Focuser
focusMaxBy f = focusSortedBy f `composeFocusers` focusIndex (-1)

focusSortedLexBy :: Focuser -> Focuser
focusSortedLexBy (FTrav trav) = FTrav $ \f focus -> case focus of
    FText str ->
        let (is, sorted_str) = unzip $ sortBy (cmp `on` (FText . T.singleton . snd)) $
                zip [0..] $ T.unpack str
            str_length = T.length str
            new_sorted_str = toTextUnsafe <$> (f . FText . T.pack) sorted_str
            unsort_is = unsort is str_length
            new_str = unsortText unsort_is str_length <$> new_sorted_str
        in  FText <$> new_str
    FList lst ->
        let (is, sorted_lst) = unzip $ sortBy (cmp `on` snd) $ zip [0..] lst
            new_sorted_lst = toListUnsafe <$> (f . FList) sorted_lst
            new_lst = map snd . sortBy (comparing fst) . zip is <$> new_sorted_lst
        in  FList <$> new_lst
  where
    cmp f1 f2 =
        let f1' = f1 ^.. trav
            f2' = f2 ^.. trav
        in case (f1', f2') of
            ([FText s1], [FText s2]) -> compare s1 s2
            _                        -> EQ

    unsortText :: [Int] -> Int -> Text -> Text
    unsortText is strLen str = T.unfoldrN strLen builder is
      where
        builder :: [Int] -> Maybe (Char, [Int])
        builder []       = Nothing
        builder (i : is) = Just (T.index str i, is)

focusMinLexBy :: Focuser -> Focuser
focusMinLexBy f = focusSortedLexBy f `composeFocusers` focusIndex 0

focusMaxLexBy :: Focuser -> Focuser
focusMaxLexBy f = focusSortedLexBy f `composeFocusers` focusIndex (-1)

focusSum :: Focuser
focusSum = FTrav $ lens getSum const

getSum :: Focus -> Focus
getSum focus = case focus of
    FList _ -> FText $ showRational $ sum $
        mapMaybe readMaybeRational $ focus ^.. biplate
    FText s -> FText $ showRational $ sum $
        mapMaybe (readMaybeRational . T.singleton) $ T.unpack s

focusProduct :: Focuser
focusProduct = FTrav $ lens getProduct const

getProduct :: Focus -> Focus
getProduct focus = case focus of
    FList _ -> FText $ showRational $ product $
        mapMaybe readMaybeRational $ focus ^.. biplate
    FText s -> FText $ showRational $ product $
        mapMaybe (readMaybeRational . T.singleton) $ T.unpack s

focusAverage :: Rational -> Focuser
focusAverage n = FTrav $ lens (getAverage n) const

getAverage :: Rational -> Focus -> Focus
getAverage n focus = case focus of
    FList _ -> FText $ showRational $ average n $
        mapMaybe readMaybeRational $ focus ^.. biplate
    FText s -> FText $ showRational $ average n $
        mapMaybe (readMaybeRational . T.singleton) $ T.unpack s

average :: Rational -> [Rational] -> Rational
average n [] = n
average _ xs = sum xs / fromIntegral (length xs)





focusIf :: Focuser -> Focuser
focusIf (FTrav trav) = FTrav $ \f focus -> case focus ^.. trav of
    [FText "1"] -> f focus
    _           -> pure focus

logicFocuser :: (Focus -> Bool) -> Focuser
logicFocuser pred = FTrav $ lens
    (\focus -> if pred focus
        then FText "1"
        else FText "0")
    const

focusIsUpper :: Focuser
focusIsUpper = logicFocuser (\case
    FText s -> T.all isUpper s
    _         -> False)

focusIsLower :: Focuser
focusIsLower = logicFocuser (\case
    FText s -> T.all isLower s
    _         -> False)

focusIsAlpha :: Focuser
focusIsAlpha = logicFocuser (\case
    FText s -> T.all isAlpha s
    _         -> False)

focusIsAlphaNum :: Focuser
focusIsAlphaNum = logicFocuser (\case
    FText s -> T.all isAlphaNum s
    _         -> False)

focusIsDigit :: Focuser
focusIsDigit = logicFocuser (\case
    FText s -> T.all isDigit s
    _         -> False)

focusIsSpace :: Focuser
focusIsSpace = logicFocuser (\case
    FText s -> T.all isSpace s
    _         -> False)

focusIsNumber :: Focuser
focusIsNumber = logicFocuser (\case
    FText s -> case readMaybeRational s of
        Just _  -> True
        Nothing -> False
    _         -> False)

focusRegex :: Text -> Focuser
focusRegex regex = FTrav $ \f focus -> case focus of
    FText s ->
        let matchIdxs = getAllMatches (s =~ regex)
            (nonMatches, matches) = fromIndexes 0 s matchIdxs
            newMatches = map toTextUnsafe <$> traverse (f . FText) matches
        in  FText . T.concat . interleave nonMatches <$> newMatches
    _ -> pure focus

focusFilter :: Focuser -> Focuser
focusFilter ftrav = focusCollect $ focusEach `composeFocusers` focusIf ftrav

focusContains :: Text -> Focuser
focusContains text = FTrav $ lens contains const
  where
    contains focus = case focus of
        FText s   -> FText $ if T.isInfixOf text s then "1" else "0"
        FList lst -> FText $ if any check lst then "1" else "0"
    check focus = case focus of
        FText s -> text == s
        _       -> False

focusStartsWith :: Text -> Focuser
focusStartsWith text = FTrav $ lens starts const
  where
    starts focus = case focus of
        FText s -> FText $ if T.isPrefixOf text s then "1" else "0"
        _       -> FText "0"

focusEndsWith :: Text -> Focuser
focusEndsWith text = FTrav $ lens ends const
  where
    ends focus = case focus of
        FText s -> FText $ if T.isSuffixOf text s then "1" else "0"
        _       -> FText "0"

focusLength :: Focuser
focusLength = FTrav $ \f focus -> case focus of
    fs@(FText s)     -> fs <$ f (FText . T.pack . show . T.length $ s)
    flst@(FList lst) -> flst <$ f (FText . T.pack . show . length $ lst)

parseListElemIdxs :: Parser [(Int, Int)]
parseListElemIdxs = do
    symbol "["
    idxs <- parseElemIdxs `sepBy` symbol ","
    symbol "]"
    pure idxs

parseElemIdxs :: Parser (Int, Int)
parseElemIdxs = lexeme $ do
    idx1 <- getOffset
    skipListElem
    idx2 <- getOffset
    pure (idx1, idx2 - idx1)

skipListElem :: Parser ()
skipListElem = choice
    [ inQuotes
    , inDoubleQuotes
    , inSquareBraces
    , inParens
    , inCurlyBraces
    , escapingCommaSquareBrace]
    >> void (optional (try $ ws >> skipListElem))

inQuotes = char '\'' >> escaping '\'' '\'' 1
inDoubleQuotes = char '"' >> escaping '"' '"' 1
inSquareBraces = char '[' >> escaping '[' ']' 1
inParens = char '(' >> escaping '(' ')' 1
inCurlyBraces = char '{' >> escaping '{' '}' 1
escapingCommaSquareBrace = void $ some $ satisfy (\c -> c /= ',' && c /= ']' && not (isSpace c))

escaping :: Char -> Char -> Int -> Parser ()
escaping start end depth = choice
    [ char end >> if depth == 1 then return () else void (optional $ escaping start end (depth - 1))
    , char start >> void (optional $ escaping start end (depth + 1))
    , char '\\' >> anySingle >> void (optional $ escaping start end depth)
    , anySingle >> void (optional $ escaping start end depth)
    ]

focusEl :: Focuser
focusEl = FTrav $ \f focus -> case focus of
    FText s -> case parseMaybe parseListElemIdxs s of
        Just idxs ->
            let (nonMatches, matches) = fromIndexes 0 s idxs
                newMatches = map toTextUnsafe <$> traverse (f . FText) matches
            in  FText . T.concat . interleave nonMatches <$> newMatches
        Nothing -> pure focus
    FList _ -> pure focus

parseObjKVIdxs :: Parser [((Int, Int), (Int, Int))]
parseObjKVIdxs = do
    symbol "{"
    idxs <- parseKVIdxs `sepBy` symbol ","
    symbol "}"
    pure idxs

parseKVIdxs :: Parser ((Int, Int), (Int, Int))
parseKVIdxs = do
    keyIdxs <- parseKeyIdxs
    symbol ":"
    valIdxs <- parseValIdxs
    pure (keyIdxs, valIdxs)

parseKeyIdxs :: Parser (Int, Int)
parseKeyIdxs = lexeme $ do
    idx1 <- getOffset
    skipKey
    idx2 <- getOffset
    pure (idx1, idx2 - idx1)

skipKey :: Parser ()
skipKey = choice
    [ inQuotes
    , inDoubleQuotes
    , inSquareBraces
    , inParens
    , inCurlyBraces
    , escapingColonCurlyBrace]
    >> void (optional (try $ ws >> skipKey))

escapingColonCurlyBrace = void $ some $ satisfy (\c -> c /= ':' && c /= '}' && not (isSpace c))

parseValIdxs :: Parser (Int, Int)
parseValIdxs = lexeme $ do
    idx1 <- getOffset
    skipVal
    idx2 <- getOffset
    pure (idx1, idx2 - idx1)

skipVal :: Parser ()
skipVal = choice
    [ inQuotes
    , inDoubleQuotes
    , inSquareBraces
    , inParens
    , inCurlyBraces
    , escapingCommaCurlyBrace]
    >> void (optional (try $ ws >> skipVal))

escapingCommaCurlyBrace = void $ some $ satisfy (\c -> c /= ',' && c /= '}' && not (isSpace c))

focusKV :: Focuser
focusKV = FTrav $ \f focus -> case focus of
    FText s -> case parseMaybe parseObjKVIdxs s of
        Just idxs ->
            let idxs_ = concatMap (\(a, b) -> [a, b]) idxs
                (nonMatches, matches) = fromIndexes 0 s idxs_
                matches_ = pairUp $ map FText matches
                newMatches_ = map toListUnsafe <$> traverse (f . FList) matches_
                newMatches = map toTextUnsafe . concat <$> newMatches_
            in  FText . T.concat . interleave nonMatches <$> newMatches
        Nothing -> pure focus
    FList _ -> pure focus
  where
    pairUp :: [a] -> [[a]]
    pairUp []             = []
    pairUp (a1 : a2 : as) = [a1, a2] : pairUp as
    pairUp _              = error "pairUp: list too short"

data KeyType
    = InQuotes Text
    | InDoubleQuotes Text
    | Default

focusKey :: Focuser
focusKey = FTrav $ \f focus -> case focus of
    FList [FText key, FText val] -> case stripKey key of
        InQuotes key_ -> setKey val . (\k -> "'" <> k <> "'") . toTextUnsafe <$> f (FText key_)
        InDoubleQuotes key_ -> setKey val . (\k -> "\"" <> k <> "\"") . toTextUnsafe <$> f (FText key_)
        Default -> setKey val . toTextUnsafe <$> f (FText key)
    FText _ ->
        let FTrav trav = focusKV `composeFocusers` focusKey
        in  trav f focus

stripKey :: Text -> KeyType
stripKey s
    | T.index s 0 == '"' && T.index s (T.length s - 1) == '"' =
        InDoubleQuotes $ T.drop 1 $ T.dropEnd 1 s
    | T.index s 0 == '\'' && T.index s (T.length s - 1) == '\''=
        InQuotes $ T.drop 1 $ T.dropEnd 1 s
    | otherwise = Default

setKey :: Text -> Text -> Focus
setKey val key = FList [FText key, FText val]

focusVal :: Focuser
focusVal = FTrav $ \f focus -> case focus of
    FList _ ->
        let FTrav trav = focusIndex 1
        in  trav f focus
    FText _ ->
        let FTrav trav = focusKV `composeFocusers` focusVal
        in  trav f focus

focusAtKey :: Text -> Focuser
focusAtKey key = focusKV
    `composeFocusers` focusIf (focusCompEq (==) focusKey (focusConst key))
    `composeFocusers` focusVal

focusAtIdx :: Int -> Focuser
focusAtIdx i = focusCollect focusEl `composeFocusers` focusIndex i

textToBool :: Text -> Bool
textToBool = \case
    "1" -> True
    _   -> False

boolToText :: Bool -> Text
boolToText = \case
    True  -> "1"
    False -> "0"

focusLogic2 :: (Bool -> Bool -> Bool) -> Focuser -> Focuser -> Focuser
focusLogic2 op (FTrav t1) (FTrav t2) = FTrav $ \f focus ->
    case (focus ^.. t1, focus ^.. t2) of
    ([FText s1], [FText s2]) ->
        let b1 = textToBool s1
            b2 = textToBool s2
        in  focus <$ (f . FText . boolToText $ op b1 b2)

focusToMaybeBool :: Focus -> Maybe Bool
focusToMaybeBool = \case
    FText s -> Just $ textToBool s
    FList _ -> Nothing

focusLogicMany :: ([Bool] -> Bool) -> Focuser -> Focuser
focusLogicMany op (FTrav t) = FTrav $ \f focus ->
    case traverse focusToMaybeBool (focus ^.. t) of
        Just bs -> focus <$ (f . FText . boolToText $ op bs)
        Nothing -> pure focus

focusNot :: Focuser
focusNot = FTrav $ \f focus ->
    case focusToMaybeBool focus of
        Just b  -> focus <$ (f . FText . boolToText $ not b)
        Nothing -> pure focus

focusConst :: Text -> Focuser
focusConst s = FTrav $ lens (const $ FText s) const

focusCompOrd :: (forall a . (Ord a, Eq a) => a -> a -> Bool) -> Focuser -> Focuser -> Focuser
focusCompOrd op (FTrav t1) (FTrav t2) = FTrav $ \f focus ->
    case (focus ^.. t1, focus ^.. t2) of
    ([FText s1], [FText s2]) -> case (readMaybeRational s1, readMaybeRational s2) of
        (Just r1, Just r2) -> focus <$ (f . FText . boolToText $ op r1 r2)
        _                  -> focus <$ (f . FText . boolToText $ op s1 s2)
    _ -> pure focus

focusCompEq :: (forall a . Eq a => a -> a -> Bool) -> Focuser -> Focuser -> Focuser
focusCompEq op (FTrav t1) (FTrav t2) = FTrav $ \f focus ->
    case (focus ^.. t1, focus ^.. t2) of
    ([FText s1], [FText s2]) -> case (readMaybeRational s1, readMaybeRational s2) of
        (Just r1, Just r2) -> focus <$ (f . FText . boolToText $ op r1 r2)
        _                  -> focus <$ (f . FText . boolToText $ op s1 s2)
    ([FList lst1], [FList lst2]) ->
        focus <$ (f . FText . boolToText $ all (uncurry op) $ zip lst1 lst2)