packages feed

parse (empty) → 0.1.0.0

raw patch · 9 files changed

+331/−0 lines, 9 filesdep +basedep +parsedep +splitsetup-changed

Dependencies added: base, parse, split, tasty, tasty-hunit, template-haskell

Files

+ ChangeLog.md view
@@ -0,0 +1,4 @@+# Changelog for parse++## 0.1.0.0 - First version+That's it, enjoy.
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright Jacob Lagares Pozo (c) 2021++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++	* Redistributions of source code must retain the above copyright+	  notice, this list of conditions and the following disclaimer.++	* Redistributions in binary form must reproduce the above+	  copyright notice, this list of conditions and the following+	  disclaimer in the documentation and/or other materials provided+	  with the distribution.++	* Neither the name of Jacob Lagares Pozo nor the names of other+	  contributors may be used to endorse or promote products derived+	  from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,44 @@+# parse+Haskell, while notorious for its extremely powerful [parser combinators+library](https://hackage.haskell.org/package/parsec), lacks a quick-and-dirty way to parse+simple-formatted strings in a one-liner. This is especially frustrating for [Advent of+Code](https://adventofcode.com/), where the input to [some+puzzles](https://adventofcode.com/2021/day/5), which is in other languages fairly painless to parse,+requires some overly convoluted, hacky and noncompact solution.++Envious of Python's [parse](https://pypi.org/project/parse/) package, I decided to write my own,+very stripped down version, in Haskell. This package provides a way to create a sort of template for+how strings should look, and then parse them accodringly. While inspired by Python format strings,+the type of each field is not specified in the string itself, and as a matter of fact, if the+compiler can't guess them by their use, they must be specified explicitly (like in the example.)++Examples:+```hs+parse "It's {}, I love it!" "It's spam, I love it!" :: String+ == "spam"++parse "{} + {} = {}" "2.1568743 + 7.196057 = 9.3529313" :: (String, String, String)+ == ("2.1568743","7.196057","9.3529313")+```++A safe variant of `parse` is also provided, that returns `Maybe` instead of throwing an error:+```hs+parse "{} * {} = {}" "2 + 3 = 5" :: (String, String, String)+*** Exception: No candidates for "" in "2 + 3 = 5".++-- Whereas:+parseMaybe "{} * {} = {}" "2 + 3 = 5" :: Maybe (String, String, String)+ == Nothing+```++You can also parse lists quite easily:+```hs+map read $ parseList "{}, {}, {}, {}, {}, {}, {}, {}, {}, {}" "1, 2, 3, 5, 7, 11, 13, 17, 19, 23" :: [Int]+ == [1,2,3,5,7,11,13,17,19,23]+```+(Note: You're still parsing a fixed amount of values, but now you can, as shown in the example, map+stuff to it.)++It should be noted that this package lacks most of the features supplied by+[parse](https://pypi.org/project/parse/), instead providing only basic parsing functionality. This+may or may not change in the future.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ parse.cabal view
@@ -0,0 +1,64 @@+cabal-version: 1.12++-- This file has been generated from package.yaml by hpack version 0.34.4.+--+-- see: https://github.com/sol/hpack++name:           parse+version:        0.1.0.0+synopsis:       Simple way to parse strings with Python-like format strings.+description:    Please refer to the README file that ships with this package.+category:       Parser+homepage:       https://github.com/jlagarespo/parse#readme+bug-reports:    https://github.com/jlagarespo/parse/issues+author:         Jacob Lagares Pozo+maintainer:     jlagarespo@protonmail.com+copyright:      Jacob Lagares Pozo+license:        BSD3+license-file:   LICENSE+build-type:     Simple+extra-source-files:+    README.md+    ChangeLog.md++source-repository head+  type: git+  location: https://github.com/jlagarespo/parse++library+  exposed-modules:+      Parse+      Parse.Internal.Instances+      Parse.Internal.Parse+  other-modules:+      Paths_parse+  hs-source-dirs:+      src+  default-extensions:+      FlexibleInstances+      TemplateHaskell+      LambdaCase+  build-depends:+      base >=4.7 && <5+    , split ==0.2.3.4+    , template-haskell ==2.16.0.0+  default-language: Haskell2010++test-suite parse-test+  type: exitcode-stdio-1.0+  main-is: Main.hs+  other-modules:+      Paths_parse+  hs-source-dirs:+      test+  default-extensions:+      FlexibleInstances+      TemplateHaskell+      LambdaCase+  ghc-options: -threaded -rtsopts -with-rtsopts=-N -Wall+  build-depends:+      base >=4.7 && <5+    , parse+    , tasty ==1.4.2+    , tasty-hunit ==0.10.0.3+  default-language: Haskell2010
+ src/Parse.hs view
@@ -0,0 +1,47 @@+module Parse where++import Data.List.Split (splitOn)++import Parse.Internal.Parse++-- |Parse @str@ according to the @format@, and return a tuple of parsed fields. It can fail with an+-- error. Parsable fields in the format string are denoted with @"{}"@, and they will match anything+parse :: ParseTuple a => String -> String -> a+parse format str =+  case parseTuple format str of+    Left err -> error err+    Right x  -> x++-- |Safe variant of @parse@ which returns a @Maybe@ monad instead of failing with an error.+parseMaybe :: ParseTuple a => String -> String -> Maybe a+parseMaybe format str =+  case parseTuple format str of+    Left _  -> Nothing+    Right x -> Just x++-- |Safe variant of @parse@ which returns an @Either@ monad instead of failing with an error. The+-- @Left@ (error) constructor carries a @String@ that contains information about the error.+parseEither :: ParseTuple a => String -> String -> Either String a+parseEither = parseTuple++-- |Parse @str@ according to the @format@, and return a list of parsed fields. It can fail with an+-- error. Parsable fields in the format string are denoted with @"{}"@, and they will match anything+-- until the next block of the format string matches something in @str@.+parseList :: String -> String -> [String]+parseList format str =+  case parseListEither format str of+    Left err -> error err+    Right x  -> x++-- |Safe variant of @parseList@ which returns a @Maybe@ monad instead of failing with an error.+parseListMaybe :: String -> String -> Maybe [String]+parseListMaybe format str =+  case parseListEither format str of+    Left _  -> Nothing+    Right x -> Just x++-- |Safe variant of @parseList@ which returns an @Either@ monad instead of failing with an+-- error. The @Left@ (error) constructor carries a @String@ that contains information about the+-- error.+parseListEither :: String -> String -> Either String [String]+parseListEither format = parseParts (splitOn "{}" format)
+ src/Parse/Internal/Instances.hs view
@@ -0,0 +1,56 @@+module Parse.Internal.Instances where++import Control.Monad+import Data.List.Split (splitOn)+import Language.Haskell.TH++-- Dear future self,+-- You're looking at this file because the parseTupleInstance function finally broke.+-- It's not fixable. You have to rewrite it.+-- Sincerely, past self.++class ParseTuple a where+  parseTuple :: String -> String -> Either String a++parseTupleInstance :: Int -> Q Dec+parseTupleInstance n = do+  unless (n > 0) $+    fail $ "Non-positive size: " ++ show n++  doParse <- [|parseParts (splitOn "{}" format) str|]+  invalidLengthL <- [p|Right result|]+  invalidLengthR <- [|Left $ "Parsed " ++ show (length result) ++ " values, expected " +++                      show n ++ "."|]+  parseErrorL <- [p|Left x|]+  parseErrorR <- [|Left x|]++  let vars = [mkName ('t' : show n) | n <- [1..n]]+      tupleSignature = foldl (\acc var -> AppT acc (ConT $ mkName "String")) (TupleT n) vars+        -- foldl (\acc var -> AppT acc (VarT var)) (TupleT n) vars+      parseList = ListP $ map VarP vars+      parse = TupE $ map (Just . -- AppE (VarE $ mkName "read")+                          VarE) vars+      -- context = map (AppT (ConT $ mkName "Read") . VarT) vars+      iDecl = InstanceD Nothing [] -- context+              (AppT (ConT $ mkName "ParseTuple") tupleSignature) [parseDecl]+      parseDecl =+        -- `parseTuple` function+        FunD (mkName "parseTuple")+        -- only has one clause+        [Clause+          -- Arguments: format and str+          [VarP $ mkName "format", VarP $ mkName "str"]+          -- Body+          (NormalB+           (CaseE doParse+           [ Match (ConP (mkName "Right") [parseList])+                   (NormalB (AppE (ConE $ mkName "Right") parse)) []+           , Match invalidLengthL (NormalB invalidLengthR) []+           , Match parseErrorL (NormalB parseErrorR) [] ]))+          -- Where+          []]++  pure iDecl++parseTupleInstances :: [Int] -> Q [Dec]+parseTupleInstances = mapM parseTupleInstance
+ src/Parse/Internal/Parse.hs view
@@ -0,0 +1,43 @@+module Parse.Internal.Parse (parseParts, ParseTuple, parseTuple) where++import Data.List (inits, tails)+import Data.List.Split (splitOn)+import Data.Maybe (mapMaybe, catMaybes)++import Parse.Internal.Instances++parseParts :: [String] -> String -> Either String [String]+parseParts (part:parts@(nextPart:_)) str = do+  case dropBy part str of+    Just str' ->+      let candidates = filter (beginEqual nextPart . snd) (splits str')+          (parsed, rest) = head candidates+      in if null candidates+         then Left $ "No candidates for \"" ++ part ++ "\" in \"" ++ str ++ "\"."+         else (parsed:) <$> parseParts parts rest+    Nothing -> Left $ "Expected \"" ++ part ++ "\", instead got \"" ++ str ++ "\"."++  where+    beginEqual :: String -> String -> Bool+    beginEqual "" target = target == ""+    beginEqual str target = take (length str) target == str++    dropBy :: String -> String -> Maybe String+    dropBy str target =+      if take (length str) target == str+      then Just (drop (length str) target)+      else Nothing++    splits :: [a] -> [([a], [a])]+    splits x = zip (inits x) (tails x)++parseParts _ _ = Right []++$(parseTupleInstances [2..62])++instance ParseTuple String where+  parseTuple format str =+    case parseParts (splitOn "{}" format) str of+      Right [x] -> Right x+      Right result -> Left $ "Parsed " ++ show (length result) ++ " values, expected 1."+      Left x -> Left x
+ test/Main.hs view
@@ -0,0 +1,41 @@+module Main where++import Test.Tasty+import Test.Tasty.HUnit++import Parse++spam :: TestTree+spam =+  testGroup "Spam"+  [ -- Success+    testCase "parse" $+    parse "It's {}, I love it!" "It's spam, I love it!" @?= "spam"+  , testCase "parseMaybe" $+    parseMaybe "It's {}, I love it!" "It's spam, I love it!" @?= Just "spam"+  , testCase "parseEither" $+    parseEither "It's {}, I love it!" "It's spam, I love it!" @?= Right "spam"+  , testCase "parseList" $+    parseList "It's {}, I love it!" "It's spam, I love it!" @?= ["spam"]+  , testCase "parseListMaybe" $+    parseListMaybe "It's {}, I love it!" "It's spam, I love it!" @?= Just ["spam"]+  , testCase "parseListEither" $+    parseListEither "It's {}, I love it!" "It's spam, I love it!" @?= Right ["spam"]++    -- Failures+  -- , testCase "bad parse" $+  -- parse "It's {}, I love it!" "It's spam, I hate it!" @?= ???+  , testCase "bad parseMaybe" $+    parseMaybe "It's {}, I love it!" "It's spam, I hate it!" @?= (Nothing :: Maybe String)+  , testCase "bad parseEither" $ parseEither "It's {}, I love it!" "It's spam, I hate it!" @?=+    (Left "No candidates for \"It's \" in \"It's spam, I hate it!\"." :: Either String String)+  -- , testCase "bad parseList" $+  --   parseList "It's {}, I love it!" "It's spam, I hate it!" @?= ???+  , testCase "bad parseListMaybe" $+    parseListMaybe "It's {}, I love it!" "It's spam, I hate it!" @?= Nothing+  , testCase "bad parseListEither" $+    parseListEither "It's {}, I love it!" "It's spam, I hate it!" @?=+    Left "No candidates for \"It's \" in \"It's spam, I hate it!\"." ]++main :: IO ()+main = defaultMain $ testGroup "Tests" [ spam ]