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 +4/−0
- LICENSE +30/−0
- README.md +44/−0
- Setup.hs +2/−0
- parse.cabal +64/−0
- src/Parse.hs +47/−0
- src/Parse/Internal/Instances.hs +56/−0
- src/Parse/Internal/Parse.hs +43/−0
- test/Main.hs +41/−0
+ 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 ]