packages feed

smh 0.2.0 → 0.2.1

raw patch · 7 files changed

+287/−264 lines, 7 files

Files

CHANGELOG.md view
@@ -26,4 +26,14 @@  * fixed "regex" (it was very broken, lol) -* added escaping support to string literals (only \")+* added escaping support to string literals (only \\")++## 0.2.1 -- 2024-03-15++* rewrote the "if" and "filter" focusers++* added "=", "!=", "<", ">", "<=", ">=", "&&", "||", "all", "any", "not" focusers++* changed the collecting syntax from "<focuser>" to "%focuser"++* added the "literal" focuser
smh.cabal view
@@ -1,6 +1,6 @@ cabal-version:      3.0 name:               smh-version:            0.2.0+version:            0.2.1 synopsis:           String manipulation tool written in haskell description:        String manipulation CLI tool based on optics category:           CLI Tool
src/Common.hs view
@@ -95,33 +95,6 @@         | i < 0 = max (i + len) 0         | otherwise = min i len --data Evaluatable-    = EText !Text-    | ENumber !Rational-    | EFocuser { evalFocuserUnsafe :: !Focuser }--data IfExpr-    = IfAnd ![IfExpr]-    | IfOr ![IfExpr]-    | IfSingle !Comparison--data Comparison = Comparison-    { cmpLHS :: !(Quantor, Evaluatable)-    , cmpOp  :: !Oper-    , cmpRHS :: !(Quantor, Evaluatable)-    }--data Quantor = QAll | QAny--data Oper-    = OpEq-    | OpNe-    | OpLt-    | OpLe-    | OpGt-    | OpGe- ws :: Parser () ws = L.space space1 empty empty 
src/Focusers.hs view
@@ -6,12 +6,11 @@  module Focusers where -import           Common               (Comparison (..), Evaluatable (..),-                                       Focus (..), Focuser (..), IfExpr (..),-                                       Mapping, Oper (..), Parser, Quantor (..),-                                       Range (RangeSingle), _toListUnsafe,-                                       composeFocusers, fromIndexes, getIndexes,-                                       lexeme, makeFilteredText, mapText,+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  ((<|>))@@ -309,70 +308,10 @@   -focusIf :: IfExpr -> Focuser-focusIf ifexpr = FTrav $ \f focus -> if focus `passesIf` ifexpr-    then f focus-    else pure focus-  where-    passesIf :: Focus -> IfExpr -> Bool-    passesIf focus (IfAnd ifexprs) = all (passesIf focus) ifexprs-    passesIf focus (IfOr ifexprs) = any (passesIf focus) ifexprs-    passesIf focus (IfSingle comp) =-        let op = cmpOp comp-            q1 = fst $ cmpLHS comp-            q2 = fst $ cmpRHS comp-            f1s = evaluateEval focus $ snd $ cmpLHS comp-            f2s = evaluateEval focus $ snd $ cmpRHS comp-            results = [[applyOp op f1 f2 | f2 <- f2s] | f1 <- f1s]-        in case (q1, q2) of-            (QAll, QAll) -> all and results-            (QAll, QAny) -> all or results-            (QAny, QAll) -> any and results-            (QAny, QAny) -> any or results--    evaluateEval :: Focus -> Evaluatable -> [Either Rational Focus]-    evaluateEval focus eval = case eval of-        EText s               -> [Right $ FText s]-        ENumber n             -> [Left n]-        EFocuser (FTrav trav) -> Right <$> focus ^.. trav--    applyOp :: Oper -> Either Rational Focus -> Either Rational Focus -> Bool-    applyOp OpEq (Left n1) (Left n2) = n1 == n2-    applyOp OpEq (Right f1) (Right f2) = f1 == f2-    applyOp OpEq (Left n1) (Right (FText s2)) = Just n1 == readMaybeRational s2-    applyOp OpEq (Right (FText s1)) (Left n2) = readMaybeRational s1 == Just n2-    applyOp OpNe (Left n1) (Left n2) = n1 /= n2-    applyOp OpNe (Left n1) (Right (FText s2)) = case readMaybeRational s2 of-        Just n2 -> n1 /= n2-        Nothing -> False-    applyOp OpNe (Right (FText s1)) (Left n2) = case readMaybeRational s1 of-        Just n1 -> n1 /= n2-        Nothing -> False-    applyOp OpNe (Right f1) (Right f2) = f1 /= f2-    applyOp op e1 e2 = case op of-        OpLt -> applyOpOrd (<) e1 e2-        OpGt -> applyOpOrd (>) e1 e2-        OpLe -> applyOpOrd (<=) e1 e2-        OpGe -> applyOpOrd (>=) e1 e2-        _    -> False-      where-        applyOpOrd-            :: (forall a. Ord a => a -> a -> Bool)-            -> Either Rational Focus-            -> Either Rational Focus-            -> Bool-        applyOpOrd f (Left n1) (Left n2) = f n1 n2-        applyOpOrd f (Left n1) (Right (FText s2)) = case readMaybeRational s2 of-            Just n2 -> f n1 n2-            Nothing -> False-        applyOpOrd f (Right (FText s1)) (Left n2) = case readMaybeRational s1 of-            Just n1 -> f n1 n2-            Nothing -> False-        applyOpOrd f (Right (FText s1)) (Right (FText s2)) =-            case (readMaybeRational s1, readMaybeRational s2) of-                (Just n1, Just n2) -> f n1 n2-                _                  -> f s1 s2-        applyOpOrd _ _ _ = False+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@@ -411,6 +350,13 @@     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 ->@@ -420,8 +366,8 @@         in  FText . T.concat . interleave nonMatches <$> newMatches     _ -> pure focus -focusFilter :: IfExpr -> Focuser-focusFilter pred = focusCollect $ focusEach `composeFocusers` focusIf pred+focusFilter :: Focuser -> Focuser+focusFilter ftrav = focusCollect $ focusEach `composeFocusers` focusIf ftrav  focusContains :: Text -> Focuser focusContains text = FTrav $ lens contains const@@ -608,8 +554,63 @@  focusAtKey :: Text -> Focuser focusAtKey key = focusKV-    `composeFocusers` focusIf (IfSingle $ Comparison (QAll, EFocuser focusKey) OpEq (QAll, EText key))+    `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)
src/Mappings.hs view
@@ -3,17 +3,16 @@  module Mappings where -import           Common          (Evaluatable (..), Focus (FList, FText),-                                  Focuser (..), Mapping, Range, getIndexes,-                                  makeFilteredText, mapText,-                                  showRational, toTextUnsafe, readMaybeRational)-import           Control.Lens    ((^..))-import           Data.Char       (toLower, toUpper)-import           Data.Function   (on)-import           Data.List       (sortBy)-import           Data.Text       (Text)-import qualified Data.Text       as T-import           Text.Read       (readMaybe)+import           Common        (Focus (FList, FText), Focuser (..), Mapping,+                                Range, getIndexes, makeFilteredText, mapText,+                                readMaybeRational, showRational, toTextUnsafe)+import           Control.Lens  ((^..))+import           Data.Char     (toLower, toUpper)+import           Data.Function (on)+import           Data.List     (sortBy)+import           Data.Text     (Text)+import qualified Data.Text     as T+import           Text.Read     (readMaybe)  mappingReverse :: Mapping mappingReverse (FList lst) = FList (reverse lst)@@ -28,18 +27,14 @@ mappingMap mapping (FText str) = FText $ T.concat $ mapText     (toTextUnsafe . mapping . FText . T.singleton) str -mappingAppend :: Evaluatable -> Mapping-mappingAppend (EText str') (FText str) = FText $ T.append str str'-mappingAppend (ENumber n) (FText str) = FText $ T.append str (showRational n)-mappingAppend (EFocuser (FTrav trav)) fstr@(FText str) = case fstr ^.. trav of+mappingAppend :: Focuser -> Mapping+mappingAppend (FTrav trav) fstr@(FText str) = case fstr ^.. trav of     [FText s] -> FText $ T.append str s     _         -> fstr mappingAppend _ flist            = flist -mappingPrepend :: Evaluatable -> Mapping-mappingPrepend (EText str') (FText str) = FText $ T.append str' str-mappingPrepend (ENumber n) (FText str) = FText $ T.append (showRational n) str-mappingPrepend (EFocuser (FTrav trav)) fstr@(FText str) = case fstr ^.. trav of+mappingPrepend :: Focuser -> Mapping+mappingPrepend (FTrav trav) fstr@(FText str) = case fstr ^.. trav of     [FText s] -> FText $ T.append s str     _         -> fstr mappingPrepend _ flist            = flist
src/Parsers.hs view
@@ -2,12 +2,11 @@ {-# LANGUAGE OverloadedStrings #-}  module Parsers where-import           Common               (Comparison (..), Evaluatable (..),-                                       Focuser (..), IfExpr (..), Mapping,-                                       Oper (..), Parser, Quantor (..),+import           Common               (Focuser (..), Mapping, Parser,                                        Range (..), composeFocusers, focusTo,                                        foldFocusers, foldMappings, integer,-                                       lexeme, mappingTo, rational, symbol)+                                       lexeme, mappingTo, rational,+                                       showRational, symbol) import           Data.Char            (isAlphaNum) import           Data.Functor         (($>)) import           Data.Maybe           (fromMaybe)@@ -15,15 +14,18 @@ import qualified Data.Text            as T import           Focusers             (escaping, focusAtIdx, focusAtKey,                                        focusAverage, focusCollect, focusCols,+                                       focusCompEq, focusCompOrd, focusConst,                                        focusContains, focusEach, focusEl,                                        focusEndsWith, focusFilter, focusId,                                        focusIf, focusIndex, focusIsAlpha,                                        focusIsAlphaNum, focusIsDigit,-                                       focusIsLower, focusIsSpace, focusIsUpper,-                                       focusKV, focusKey, focusLength,-                                       focusLines, focusMaxBy, focusMaxLexBy,-                                       focusMinBy, focusMinLexBy, focusProduct,-                                       focusRegex, focusSlice, focusSortedBy,+                                       focusIsLower, focusIsNumber,+                                       focusIsSpace, focusIsUpper, focusKV,+                                       focusKey, focusLength, focusLines,+                                       focusLogic2, focusLogicMany, focusMaxBy,+                                       focusMaxLexBy, focusMinBy, focusMinLexBy,+                                       focusNot, focusProduct, focusRegex,+                                       focusSlice, focusSortedBy,                                        focusSortedLexBy, focusSpace,                                        focusStartsWith, focusSum, focusVal,                                        focusWords)@@ -48,7 +50,6 @@ parseFocuser = label "valid focuser" $ choice     [ symbol "id" $> focusId     , symbol "each" $> focusEach-    , parseFocusCollect     , symbol "words" $> focusWords     , symbol "lines" $> focusLines     , symbol "ws" $> focusSpace@@ -87,6 +88,7 @@     , symbol "isAlphaNum" $> focusIsAlphaNum     , symbol "isAlpha" $> focusIsAlpha     , symbol "isSpace" $> focusIsSpace+    , symbol "isNumber" $> focusIsNumber     , parseFocusRegex     , parseFocusFilter     , parseFocusContains@@ -98,6 +100,18 @@     , symbol "val" $> focusVal     , parseFocusAtKey     , parseFocusAtIdx+    , parseFocusAll+    , parseFocusAny+    , symbol "not" $> focusNot+    , parseFocusOr+    , parseFocusAnd+    , parseFocusEq+    , parseFocusNeq+    , parseFocusLeq+    , parseFocusGeq+    , try parseFocusLt <|> parseFocusCollect+    , parseFocusGt+    , parseFocusLit     ]  parseFocusers :: Parser [Focuser]@@ -105,11 +119,8 @@  parseFocusCollect :: Parser Focuser parseFocusCollect = do-    symbol "<"-    focusers <- parseFocusers-    symbol ">"-    let focuser = foldFocusers focusers-    return $ focusCollect focuser+    symbol "%"+    focusCollect <$> parseFocuser  parseFocusSlice :: Parser Focuser parseFocusSlice = do@@ -200,54 +211,7 @@ parseFocusIf :: Parser Focuser parseFocusIf = do     lexeme $ string "if" >> notFollowedBy (satisfy isAlphaNum)-    focusIf <$> parseIfExpr--parseIfExpr :: Parser IfExpr-parseIfExpr = label "one or more blocks separated by '||'" $ do-    andBlocks <- parseAndBlock `sepBy1` symbol "||"-    case andBlocks of-        []      -> empty-        [block] -> return block-        _       -> return $ IfOr andBlocks--parseAndBlock :: Parser IfExpr-parseAndBlock = label "one or more blocks separated by '&&'" $ do-    atoms <- parseAtom `sepBy1` symbol "&&"-    case atoms of-        []     -> empty-        [atom] -> return atom-        _      -> return $ IfAnd atoms--parseAtom :: Parser IfExpr-parseAtom = between (symbol "(") (symbol ")") parseIfExpr <|> try parseComp <|> parseIfExprShort--parseComp :: Parser IfExpr-parseComp = do-    q1 <- fromMaybe QAll <$> optional parseQuantor-    lhs <- fromMaybe (EFocuser focusId) <$> optional parseEvaluatableLong-    comp <- parseCompOp-    q2 <- fromMaybe QAll <$> optional parseQuantor-    rhs <- parseEvaluatableLong-    return $ IfSingle $ Comparison (q1, lhs) comp (q2, rhs)--parseQuantor :: Parser Quantor-parseQuantor = symbol "all " $> QAll <|> symbol "any " $> QAny--parseCompOp :: Parser Oper-parseCompOp = choice-    [ symbol "=" $> OpEq-    , symbol "!=" $> OpNe-    , symbol "<=" $> OpLe-    , symbol "<"  $> OpLt-    , symbol ">=" $> OpGe-    , symbol ">"  $> OpGt-    ]--parseIfExprShort :: Parser IfExpr-parseIfExprShort = do-    q <- fromMaybe QAll <$> optional parseQuantor-    e <- EFocuser <$> parseFocuser-    return $ IfSingle $ Comparison (q, e) OpEq (QAny, EText "1")+    focusIf <$> parseFocuser  parseFocusRegex :: Parser Focuser parseFocusRegex = do@@ -257,7 +221,7 @@ parseFocusFilter :: Parser Focuser parseFocusFilter = do     lexeme $ string "filter" >> notFollowedBy (satisfy isAlphaNum)-    focusFilter <$> parseIfExpr+    focusFilter <$> parseFocuser  parseFocusContains :: Parser Focuser parseFocusContains = do@@ -290,6 +254,90 @@     symbol "atIdx "     focusAtIdx <$> integer +parseFocusAll :: Parser Focuser+parseFocusAll = do+    lexeme $ string "all" >> notFollowedBy (satisfy isAlphaNum)+    focusLogicMany and <$> parseFocuser++parseFocusAny :: Parser Focuser+parseFocusAny = do+    lexeme $ string "any" >> notFollowedBy (satisfy isAlphaNum)+    focusLogicMany or <$> parseFocuser++parseFocusAnd :: Parser Focuser+parseFocusAnd = do+    symbol "&&"+    p1 <- parseFocuser+    p2 <- parseFocuser+    return $ focusLogic2 (&&) p1 p2++parseFocusOr :: Parser Focuser+parseFocusOr = do+    symbol "||"+    p1 <- parseFocuser+    p2 <- parseFocuser+    return $ focusLogic2 (||) p1 p2++parseFocusEq :: Parser Focuser+parseFocusEq = do+    symbol "="+    p1 <- parseFocuser+    mp2 <- optional parseFocuser+    return $ case mp2 of+        Just p2 -> focusCompEq (==) p1 p2+        Nothing -> focusCompEq (==) focusId p1++parseFocusNeq :: Parser Focuser+parseFocusNeq = do+    symbol "!="+    p1 <- parseFocuser+    mp2 <- optional parseFocuser+    return $ case mp2 of+        Just p2 -> focusCompEq (/=) p1 p2+        Nothing -> focusCompEq (/=) focusId p1++parseFocusLt :: Parser Focuser+parseFocusLt = do+    symbol "<"+    p1 <- parseFocuser+    mp2 <- optional parseFocuser+    return $ case mp2 of+        Just p2 -> focusCompOrd (<) p1 p2+        Nothing -> focusCompOrd (<) focusId p1++parseFocusGt :: Parser Focuser+parseFocusGt = do+    symbol ">"+    p1 <- parseFocuser+    mp2 <- optional parseFocuser+    return $ case mp2 of+        Just p2 -> focusCompOrd (>) p1 p2+        Nothing -> focusCompOrd (>) focusId p1++parseFocusLeq :: Parser Focuser+parseFocusLeq = do+    symbol "<="+    p1 <- parseFocuser+    mp2 <- optional parseFocuser+    return $ case mp2 of+        Just p2 -> focusCompOrd (<=) p1 p2+        Nothing -> focusCompOrd (<=) focusId p1++parseFocusGeq :: Parser Focuser+parseFocusGeq = do+    symbol ">="+    p1 <- parseFocuser+    mp2 <- optional parseFocuser+    return $ case mp2 of+        Just p2 -> focusCompOrd (>=) p1 p2+        Nothing -> focusCompOrd (>=) focusId p1++parseFocusLit :: Parser Focuser+parseFocusLit = parseFocusString <|> parseFocusNumber+  where+    parseFocusString = focusConst <$> stringLiteral+    parseFocusNumber = focusConst . showRational <$> rational+ -- mapping parsers  parseMapping :: Parser Mapping@@ -326,21 +374,9 @@     lexeme $ string "map" >> notFollowedBy (satisfy isAlphaNum)     mappingMap <$> parseMapping -parseEvaluatable :: Parser Evaluatable-parseEvaluatable =-    EText <$> stringLiteral <|>-    ENumber <$> rational <|>-    EFocuser <$> parseFocuser--parseEvaluatableLong :: Parser Evaluatable-parseEvaluatableLong =-    EText <$> stringLiteral <|>-    ENumber <$> rational <|>-    EFocuser . foldFocusers <$> parseFocusers- stringLiteral :: Parser Text stringLiteral = T.pack <$> label "string literal" (do-    between (char '"') (char '"') $ many $ choice+    between (char '"') (symbol "\"") $ many $ choice         [ char '\\' >> anySingle         , anySingleBut '"'         ])@@ -348,12 +384,12 @@ parseMappingAppend :: Parser Mapping parseMappingAppend = do     lexeme $ string "append" >> notFollowedBy (satisfy isAlphaNum)-    mappingAppend <$> parseEvaluatableLong+    mappingAppend . foldFocusers <$> parseFocusers  parseMappingPrepend :: Parser Mapping parseMappingPrepend = do     lexeme $ string "prepend" >> notFollowedBy (satisfy isAlphaNum)-    mappingPrepend <$> parseEvaluatableLong+    mappingPrepend . foldFocusers <$> parseFocusers  parseMappingAdd :: Parser Mapping parseMappingAdd = do
test/Main.hs view
@@ -5,9 +5,10 @@ import           Data.Char        (isAlpha, isAlphaNum, isDigit, isLower,                                    isSpace, isUpper, toLower, toUpper) import           Data.Function    (on)-import           Data.List        (groupBy, transpose)+import           Data.List        (groupBy, isInfixOf, isPrefixOf, isSuffixOf,+                                   transpose) import           Data.List.Extra  (dropEnd, groupBy, sort, takeEnd, transpose)-import           Data.Maybe       (fromMaybe, mapMaybe, fromJust)+import           Data.Maybe       (fromJust, fromMaybe, mapMaybe) import qualified Data.Text        as T import           Focusers         (interleave, myWords) import           System.IO.Unsafe (unsafePerformIO)@@ -26,7 +27,7 @@         ]     , testGroup "each"         [ "each|get" $= concatMap (: "\n") input-        , "<words>.each|get" $=$ "words|get"+        , "%words.each|get" $=$ "words|get"         ]     , testGroup "words"         [ "words|get-tree" $= show (words input)@@ -61,28 +62,28 @@         , "{:}|get-tree" $= show [input]         ]     , testGroup "sortLexBy"-        [ "<words>.sortedLexBy id.each|get-tree" $= show (sort $ words input)-        , "<words>.sortedLexBy id.each|over id" $= input+        [ "%words.sortedLexBy id.each|get-tree" $= show (sort $ words input)+        , "%words.sortedLexBy id.each|over id" $= input         ]     , testGroup "minLexBy/maxLexBy/minLex/maxLex"-        [ "<words>.minLexBy id|get-tree" $=$ "<words>.sortedLexBy id.[0]|get-tree"-        , "<words>.maxLexBy id|get-tree" $=$ "<words>.sortedLexBy id.[-1]|get-tree"-        , "<words>.minLex|get-tree" $=$ "<words>.minLexBy id|get-tree"-        , "<words>.maxLex|get-tree" $=$ "<words>.maxLexBy id|get-tree"+        [ "%words.minLexBy id|get-tree" $=$ "%words.sortedLexBy id.[0]|get-tree"+        , "%words.maxLexBy id|get-tree" $=$ "%words.sortedLexBy id.[-1]|get-tree"+        , "%words.minLex|get-tree" $=$ "%words.minLexBy id|get-tree"+        , "%words.maxLex|get-tree" $=$ "%words.maxLexBy id|get-tree"         ]     , testGroup "sortedBy"-        [ "<words.if isDigit>.sortedBy id.each|get-tree" $=+        [ "%(words.if isDigit).sortedBy id.each|get-tree" $=             show (map showRational $ sort $ map (fromJust . readMaybeRational . T.pack) $ filter (all isDigit) $ words input)-        , "<words.if isAlpha>.sortedBy id.each|over id" $=$ "id|over id"+        , "%(words.if isAlpha).sortedBy id.each|over id" $=$ "id|over id"         ]     , testGroup "sorted"-        [ "<words>.sorted|get-tree" $=$ "<words>.sortedBy id|get-tree"+        [ "%words.sorted|get-tree" $=$ "%words.sortedBy id|get-tree"         ]     , testGroup "minBy/maxBy/min/max"-        [ "<words>.minBy id|get-tree" $=$ "<words>.sortedBy id.[0]|get-tree"-        , "<words>.maxBy id|get-tree" $=$ "<words>.sortedBy id.[-1]|get-tree"-        , "<words>.min|get-tree" $=$ "<words>.minBy id|get-tree"-        , "<words>.max|get-tree" $=$ "<words>.maxBy id|get-tree"+        [ "%words.minBy id|get-tree" $=$ "%words.sortedBy id.[0]|get-tree"+        , "%words.maxBy id|get-tree" $=$ "%words.sortedBy id.[-1]|get-tree"+        , "%words.min|get-tree" $=$ "%words.minBy id|get-tree"+        , "%words.max|get-tree" $=$ "%words.maxBy id|get-tree"         ]     , testGroup "index"         [ "[3]|get-tree" $= show [take 1 $ drop 3 input]@@ -105,73 +106,71 @@         , "(words.len)|get-tree" $=$ "words.len|get-tree"         ]     , testGroup "sum"-        [ "<words>.sum|get-tree" $= show [showRational (sum $ inputNums input)]+        [ "%words.sum|get-tree" $= show [showRational (sum $ inputNums input)]         , "words.sum|get-tree" $=             show (map (showRational . sum . mapMaybe (readMaybeRational . T.pack . (:[]))) $ words input)         ]     , testGroup "product"-        [ "<words>.product|get-tree" $= show [showRational (product $ inputNums input)]+        [ "%words.product|get-tree" $= show [showRational (product $ inputNums input)]         , "words.product|get-tree" $=             show (map (showRational . product . mapMaybe (readMaybeRational . T.pack . (:[]))) $ words input)         ]     , testGroup "average"-        [ "<words>.average|get-tree" $= show [showRational (average $ inputNums input)]+        [ "%words.average|get-tree" $= show [showRational (average $ inputNums input)]         , "words.average|get-tree" $=             show (map (showRational . average . mapMaybe (readMaybeRational . T.pack . (:[]))) $ words input)         ]     , testGroup "add"-        [ "<words.add 1>.sum|get-tree" $= show [showRational (sum $ map (+1) $ inputNums input)]+        [ "%(words.add 1).sum|get-tree" $= show [showRational (sum $ map (+1) $ inputNums input)]         ]     , testGroup "sub"-        [ "<words.sub 1>.sum|get-tree" $= show [showRational (sum $ map (subtract 1) $ inputNums input)]+        [ "%(words.sub 1).sum|get-tree" $= show [showRational (sum $ map (subtract 1) $ inputNums input)]         ]     , testGroup "mult"-        [ "<words.mult 2>.sum|get-tree" $= show [showRational (sum $ map (*2) $ inputNums input)]+        [ "%(words.mult 2).sum|get-tree" $= show [showRational (sum $ map (*2) $ inputNums input)]         ]     , testGroup "div"-        [ "<words.div 2>.sum|get-tree" $= show [showRational (sum $ map (/ 2) $ inputNums input)]+        [ "%(words.div 2).sum|get-tree" $= show [showRational (sum $ map (/ 2) $ inputNums input)]         ]     , testGroup "pow"-        [ "<words.pow 2>.sum|get-tree" $= show [showRational (sum $ map (^^ 2) $ inputNums input)]+        [ "%(words.pow 2).sum|get-tree" $= show [showRational (sum $ map (^^ 2) $ inputNums input)]         ]     , testGroup "abs"-        [ "<words.abs>.sum|get-tree" $= show [showRational (sum $ map abs $ inputNums input)]+        [ "%(words.abs).sum|get-tree" $= show [showRational (sum $ map abs $ inputNums input)]         ]     , testGroup "sign"-        [ "<words.sign>.sum|get-tree" $= show [showRational (sum $ map signum $ inputNums input)]+        [ "%(words.sign).sum|get-tree" $= show [showRational (sum $ map signum $ inputNums input)]         ]     , testGroup "if"-        [ "words.if 1=1|get-tree" $= show (words input)-        , "words.if 1=2|get-tree" $= "[]"-        , "words.if \"1\"=\"1\"|get-tree" $= show (words input)-        , "words.if \"1\"=\"2\"|get-tree" $= "[]"-        , "words.if 1=\"1\"|get-tree" $= show (words input)-        , "words.if 1=\"2\"|get-tree" $= "[]"-        , "words.if \"1\"=1|get-tree" $= show (words input)-        , "words.if \"1\"=2|get-tree" $= "[]"-        , "words.if id<=id|get-tree" $= show (words input)-        , "words.if id=len|get-tree" $= show (filter (\w -> w == show (length w)) $ words input)-        , "words.if len=2|get-tree" $= show (filter (\w -> length w == 2) $ words input)-        , "words.if len=\"3\"|get-tree" $= show (filter (\w -> length w == 3) $ words input)-        , "<words>.if id<=id|get-tree" $= "[]"-        , "<words>.if id<id|get-tree" $= "[]"-        , "<words>.if id>id|get-tree" $= "[]"-        , "<words>.if id>=id|get-tree" $= "[]"-        , "words.if len<3 && len>1|get-tree" $= show (filter (\w -> length w < 3 && length w > 1) $ words input)-        , "words.if len<2 || len>2|get-tree" $= show (filter (\w -> length w < 2 || length w > 2) $ words input)-        , "words.if len>1 && len<3 || [0].isUpper=1|get-tree" $=+        [ "words.if =1 1|get-tree" $= show (words input)+        , "words.if =2 1|get-tree" $= "[]"+        , "words.if =\"1\" \"1\"|get-tree" $= show (words input)+        , "words.if =\"2\" \"1\"|get-tree" $= "[]"+        , "words.if =\"1\" 1|get-tree" $= show (words input)+        , "words.if =\"2\" 1|get-tree" $= "[]"+        , "words.if =1 \"1\"|get-tree" $= show (words input)+        , "words.if =2 \"1\"|get-tree" $= "[]"+        , "words.if <=id id|get-tree" $= show (words input)+        , "words.if =len id|get-tree" $= show (filter (\w -> w == show (length w)) $ words input)+        , "words.if =len 2|get-tree" $= show (filter (\w -> length w == 2) $ words input)+        , "words.if =len \"3\"|get-tree" $= show (filter (\w -> length w == 3) $ words input)+        , "%words.if <=id id|get-tree" $= "[]"+        , "%words.if <id id|get-tree" $= "[]"+        , "%words.if >id id|get-tree" $= "[]"+        , "%words.if >=id id|get-tree" $= "[]"+        , "words.if && < len 3 > len 1|get-tree" $= show (filter (\w -> length w < 3 && length w > 1) $ words input)+        , "words.if || < len 2 > len 2|get-tree" $= show (filter (\w -> length w < 2 || length w > 2) $ words input)+        , "words.if || && > len 1 < len 3 ([0].isUpper)|get-tree" $=             show (filter (\w -> length w > 1 && length w < 3 || isUpper (head w)) $ words input)-        , "words.if [0].isUpper=1 || len>1 && len<3|get-tree" $=+        , "words.if || ([0].isUpper) && > len 1 < len 3|get-tree" $=             show (filter (\w -> isUpper (head w) || length w > 1 && length w < 3) $ words input)-        , "words.if ([0].isUpper=1 || len>1) && len<3|get-tree" $=+        , "words.if && || ([0].isUpper) > len 1 < len 3|get-tree" $=             show (filter (\w -> (isUpper (head w) || length w > 1) && length w < 3) $ words input)         , "words.if len|get-tree" $= show (filter (\w -> length w == 1) $ words input)         , "words.if all (each.isUpper)|get-tree" $= show (filter (all isUpper) $ words input)         , "words.if any (each.isUpper)|get-tree" $= show (filter (any isUpper) $ words input)-        , "words.if 1=all each.isUpper|get-tree" $= show (filter (all isUpper) $ words input)-        , "words.if 1=any each.isUpper|get-tree" $= show (filter (any isUpper) $ words input)-        , "words.if each=each|get-tree" $= show (filter allEqual $ words input)-        , "words.if =\"ee\"|get-tree" $=$ "words.if id=\"ee\"|get-tree"+        , "words.if all (each.isUpper)|get-tree" $= show (filter (all isUpper) $ words input)+        , "words.if any (each.isUpper)|get-tree" $= show (filter (any isUpper) $ words input)         ]     , testGroup "isUpper"         [ "words.if isUpper|get-tree" $= show (filter (all isUpper) $ words input)@@ -192,10 +191,10 @@         [ "words.if isDigit|get-tree" $= show (filter (all isDigit) $ words input)         ]     , testGroup "collect"-        [ "<words>|get-tree" $= show [words input]+        [ "%words|get-tree" $= show [words input]         ]     , testGroup "filter"-        [ "<words>.filter len<3.each|get-tree" $=$ "words.if len<3|get-tree" ]+        [ "%words.filter < len 3.each|get-tree" $=$ "words.if < len 3|get-tree" ]     , testGroup "regex"         [ "regex \"([a-z]|[A-Z]|[0-9]|_)+\"|get" $=$ "words|get"         , "regex \"([a-z]|[A-Z]|[0-9]|_)+\"|over id" $=$ "id|over id"@@ -214,14 +213,23 @@             , "kv.val|get" $$=$ "kv.[1]|get"             ]         , testGroup "atKey"-            [ "atKey \"quiz\"|get" $$=$ "kv.if key=\"quiz\".val|get"+            [ "atKey \"quiz\"|get" $$=$ "kv.if = key \"quiz\".val|get"             ]         , testGroup "atIdx"             [ "atKey \"quiz\".atKey \"sport\".val.atKey \"options\".atIdx 3|get" $$= "\"Huston Rocket\"\n"             ]         , testGroup "el"-            [ "atKey \"quiz\".atKey \"sport\".val.atKey \"options\".<el>.[3]|get" $$= "\"Huston Rocket\"\n" ]+            [ "atKey \"quiz\".atKey \"sport\".val.atKey \"options\".%el.[3]|get" $$= "\"Huston Rocket\"\n" ]         ]+    , testGroup "startsWith"+        [ "words.if startsWith \"a\"|get-tree" $= show (filter (isPrefixOf "a") $ words input)+        ]+    , testGroup "endsWith"+        [ "words.if endsWith \"a\"|get-tree" $= show (filter (isSuffixOf "a") $ words input)+        ]+    , testGroup "contains"+        [ "words.if contains \"a\"|get-tree" $= show (filter (isInfixOf "a") $ words input)+        ]     ]  mappingTests :: TestTree@@ -234,7 +242,7 @@         [ "id|over len" $= show (length input)         ]     , testGroup "map"-        [ "<words>|over map len" $= "1 2 3 3 1 1 1 1\n1 2 3 3 1 1 1 1  1\n1 2 3 3 1 1 1 1  1\n\n"+        [ "%words|over map len" $= "1 2 3 3 1 1 1 1\n1 2 3 3 1 1 1 1  1\n1 2 3 3 1 1 1 1  1\n\n"         , "words|over map upper" $=$ "words|over upper"         , "id|over id" $= input         ]@@ -247,7 +255,7 @@         , "id|over prepend \"hello\"" $= ("hello" ++ input)         , "id|over append len" $= (input ++ show (length input))         , "id|over prepend len" $= (show (length input) ++ input)-        , "id|over prepend <words>" $= input+        , "id|over prepend %words" $= input         ]     , testGroup "upper/lower"         [ "id|over upper" $= map toUpper input@@ -278,21 +286,21 @@         , "id|over {:}" $= input         ]     , testGroup "sortLexBy"-        [ "<words>|over sortLexBy id" $= "1 1 2 2 3 3 Ccc Hhh\nMmm _ _ _ _ _ a bb  dd1\ne f gg ii2 j k ll nn3  o\n\n"-        , "<words.<each>>|over sortLexBy id" $= input+        [ "%words|over sortLexBy id" $= "1 1 2 2 3 3 Ccc Hhh\nMmm _ _ _ _ _ a bb  dd1\ne f gg ii2 j k ll nn3  o\n\n"+        , "%(words.%each)|over sortLexBy id" $= input         ]     , testGroup "sortLex"-        [ "<words>|over sortLexBy id" $=$ "<words>|over sortLex"+        [ "%words|over sortLexBy id" $=$ "%words|over sortLex"         ]     , testGroup "sortBy"-        [ "<words>|over sortBy id" $= "a bb Ccc dd1 e 1 1 3\n_ f gg Hhh ii2 j 2 2  _\n_ k ll Mmm nn3 o 3 _  _\n\n"-        , "<words.<each>>|over sortBy id" $= input+        [ "%words|over sortBy id" $= "a bb Ccc dd1 e 1 1 3\n_ f gg Hhh ii2 j 2 2  _\n_ k ll Mmm nn3 o 3 _  _\n\n"+        , "%(words.%each)|over sortBy id" $= input         ]     , testGroup "sort"-        [ "<words>|over sortBy id" $=$ "<words>|over sort"+        [ "%words|over sortBy id" $=$ "%words|over sort"         ]     , testGroup "id"-        [ "<words>|over id" $= input+        [ "%words|over id" $= input         ]     , testGroup "to"         [ "words|over to len" $=$ "words|over len" ]