packages feed

replace-megaparsec 1.0.1.0 → 1.1.0.0

raw patch · 7 files changed

+379/−134 lines, 7 filesdep +criterionPVP ok

version bump matches the API change (PVP)

Dependencies added: criterion

API changes (from Hackage documentation)

Files

README.md view
@@ -8,7 +8,7 @@ replacing the found patterns. This activity is traditionally done with regular expressions, but __replace-megaparsec__ uses-[__Megaparsec__](http://hackage.haskell.org/package/megaparsec)+[__megaparsec__](http://hackage.haskell.org/package/megaparsec) parsers instead for the pattern matching.  __replace-megaparsec__ can be used in the same sort of “pattern capture”@@ -57,7 +57,7 @@   [regular](https://en.wikipedia.org/wiki/Chomsky_hierarchy#The_hierarchy)   grammers.   Parsers are able pattern-match with context-free grammers, and-  even context-sensitive or Turing grammers, if needed. See below for+  even context-sensitive grammers, if needed. See below for   an example of lifting a `Parser` into a `State` monad for context-sensitive   pattern-matching. @@ -88,7 +88,7 @@ and deconstruct the string of text by separating it into sections which match the pattern, and sections which don't match. -#### Pattern match, capture only the parsed result+#### Pattern match, capture only the parsed result with `sepCap`  Separate the input string into sections which can be parsed as a hexadecimal number with a prefix `"0x"`, and sections which can't.@@ -101,7 +101,7 @@ [Right 10,Left " 000 ",Right 65535] ``` -#### Pattern match, capture only the matched text+#### Pattern match, capture only the matched text with `findAll`  Just get the strings sections which match the hexadecimal parser, throw away the parsed number.@@ -114,7 +114,7 @@ [Right "0xA",Left " 000 ",Right "0xFFFF"] ``` -#### Pattern match, capture the matched text and the parsed result+#### Pattern match, capture the matched text and the parsed result with `findAllCap`  Capture the parsed hexadecimal number, as well as the string section which parses as a hexadecimal number.@@ -227,10 +227,43 @@  <http://hackage.haskell.org/package/template> -## Motivation+<http://hackage.haskell.org/package/regex-applicative> -I wanted to scan a Markdown document and find tokens inside backticks that-look like a Haskell identifier, then look up the identifier in Hoogle to-see if it has a definition in __base__, and if so, insert a Hackage link-for the identifier into the Markdown. I couldn't find a simple and-obvious way to do that with any existing technology.+## Hypothetically Asked Questions++1. *Is it fast?*++   lol not really. `sepCap` is fundamentally about consuming the stream one+   token at a time while we try and fail to run a parser and then+   backtrack each time. That's+   [a slow activity](https://markkarpov.com/megaparsec/megaparsec.html#writing-efficient-parsers).++   Consider a 1 megabyte file that consists of `"foo"` every ten bytes:++   ```+          foo       foo       foo       foo       foo       foo ...+   ```++   We want to replace all the `"foo"` with `"bar"`. We would expect `sed`+   to be about at the upper bound of speed for this task, so here+   are the `perf` results when we compare `sed`+   to __replace-megaparsec__ with some different stream types.++   | Method           | `perf task-clock` |+   | :---              |    ---: |+   | `sed s/foo/bar/g` | 39 msec |+   | `streamEdit String` | 793 msec |+   | `streamEdit ByteString` | 513 msec |+   | `streamEdit Text`       | 428 msec |++2. *Could we write this library for __parsec__?*++   No, because the+   [`match`](https://hackage.haskell.org/package/megaparsec/docs/Text-Megaparsec.html#v:match)+   combinator doesn't exist for __parsec__. (I can't find it anywhere.+   [Can it be written?](http://www.serpentine.com/blog/2014/05/31/attoparsec/#from-strings-to-buffers-and-cursors))++3. *Could we write this library for __attoparsec__?*++   I think so, but I wouldn't expect much of a speed improvement, because+   again, `sepCap` is a fundamentally slow activity.
+ bench/BenchUnit.hs view
@@ -0,0 +1,156 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE BangPatterns #-}++import Text.Megaparsec+import Replace.Megaparsec+import Criterion.Main+import Criterion.Types+import qualified Data.ByteString as B+import qualified Data.ByteString.Lazy as BL+import qualified Data.ByteString.Builder as B.Builder+import qualified Data.Text as T+import qualified Data.Text.Lazy as TL+import Data.Void++fooStringM :: String+fooStringM = take 1000000 $ cycle "       foo" -- a million bytes of foos+fooString10K :: String+fooString10K = take 10000 fooStringM+fooString100K :: String+fooString100K = take 100000 fooStringM++fooByteStringM :: B.ByteString+fooByteStringM = BL.toStrict fooByteStringLM+fooByteString10K :: B.ByteString+fooByteString10K = B.take 10000 fooByteStringM+fooByteString100K :: B.ByteString+fooByteString100K = B.take 100000 fooByteStringM++fooByteStringLM :: BL.ByteString+fooByteStringLM = B.Builder.toLazyByteString $ B.Builder.string8 fooStringM+fooByteStringL10K :: BL.ByteString+fooByteStringL10K = BL.take 10000 fooByteStringLM+fooByteStringL100K :: BL.ByteString+fooByteStringL100K = BL.take 100000 fooByteStringLM++fooTextM :: T.Text+fooTextM = T.pack fooStringM+fooText10K :: T.Text+fooText10K = T.take 10000 fooTextM+fooText100K :: T.Text+fooText100K = T.take 100000 fooTextM++fooTextLM :: TL.Text+fooTextLM = TL.pack fooStringM+fooTextL10K :: TL.Text+fooTextL10K = TL.take 10000 fooTextLM+fooTextL100K :: TL.Text+fooTextL100K = TL.take 100000 fooTextLM++main :: IO ()+main = defaultMainWith (defaultConfig+            { reportFile = Just "criterion-report.html"+            , resamples = 100+            })+    [ bgroup "String"+        [ bench "sepCap 10,000" $ whnf+            (parseMaybe (sepCap (chunk "foo" :: Parsec Void String String)))+            fooString10K+        , bench "streamEdit 10,000" $ whnf+            (streamEdit (chunk "foo" :: Parsec Void String String) (const "bar"))+            fooString10K+        , bench "sepCap 100,000" $ whnf+            (parseMaybe (sepCap (chunk "foo" :: Parsec Void String String)))+            fooString100K+        , bench "streamEdit 100,000" $ whnf+            (streamEdit (chunk "foo" :: Parsec Void String String) (const "bar"))+            fooString100K+        , bench "sepCap 1,000,000" $ whnf+            (parseMaybe (sepCap (chunk "foo" :: Parsec Void String String)))+            fooStringM+        , bench "streamEdit 1,000,000" $ whnf+            (streamEdit (chunk "foo" :: Parsec Void String String) (const "bar"))+            fooStringM+        ]+    , bgroup "ByteString.Strict"+        [ bench "sepCap 10,000" $ whnf+            (parseMaybe (sepCap (chunk "foo" :: Parsec Void B.ByteString B.ByteString)))+            fooByteString10K+        , bench "streamEdit 10,000" $ whnf+            (streamEdit (chunk "foo" :: Parsec Void B.ByteString B.ByteString) (const "bar"))+            fooByteString10K+        , bench "sepCap 100,000" $ whnf+            (parseMaybe (sepCap (chunk "foo" :: Parsec Void B.ByteString B.ByteString)))+            fooByteString100K+        , bench "streamEdit 100,000" $ whnf+            (streamEdit (chunk "foo" :: Parsec Void B.ByteString B.ByteString) (const "bar"))+            fooByteString100K+        , bench "sepCap 1,000,000" $ whnf+            (parseMaybe (sepCap (chunk "foo" :: Parsec Void B.ByteString B.ByteString)))+            fooByteStringM+        , bench "streamEdit 1,000,000" $ whnf+            (streamEdit (chunk "foo" :: Parsec Void B.ByteString B.ByteString) (const "bar"))+            fooByteStringM+        ]+    , bgroup "ByteString.Lazy"+        [ bench "sepCap 10,000" $ whnf+            (parseMaybe (sepCap (chunk "foo" :: Parsec Void BL.ByteString BL.ByteString)))+            fooByteStringL10K+        , bench "streamEdit 10,000" $ whnf+            (streamEdit (chunk "foo" :: Parsec Void BL.ByteString BL.ByteString) (const "bar"))+            fooByteStringL10K+        , bench "sepCap 100,000" $ whnf+            (parseMaybe (sepCap (chunk "foo" :: Parsec Void BL.ByteString BL.ByteString)))+            fooByteStringL100K+        , bench "streamEdit 100,000" $ whnf+            (streamEdit (chunk "foo" :: Parsec Void BL.ByteString BL.ByteString) (const "bar"))+            fooByteStringL100K+        , bench "sepCap 1,000,000" $ whnf+            (parseMaybe (sepCap (chunk "foo" :: Parsec Void BL.ByteString BL.ByteString)))+            fooByteStringLM+        , bench "streamEdit 1,000,000" $ whnf+            (streamEdit (chunk "foo" :: Parsec Void BL.ByteString BL.ByteString) (const "bar"))+            fooByteStringLM+        ]+    , bgroup "Text.Strict"+        [ bench "sepCap 10,000" $ whnf+            (parseMaybe (sepCap (chunk "foo" :: Parsec Void T.Text T.Text)))+            fooText10K+        , bench "streamEdit 10,000" $ whnf+            (streamEdit (chunk "foo" :: Parsec Void T.Text T.Text) (const "bar"))+            fooText10K+        , bench "sepCap 100,000" $ whnf+            (parseMaybe (sepCap (chunk "foo" :: Parsec Void T.Text T.Text)))+            fooText100K+        , bench "streamEdit 100,000" $ whnf+            (streamEdit (chunk "foo" :: Parsec Void T.Text T.Text) (const "bar"))+            fooText100K+        , bench "sepCap 1,000,000" $ whnf+            (parseMaybe (sepCap (chunk "foo" :: Parsec Void T.Text T.Text)))+            fooTextM+        , bench "streamEdit 1,000,000" $ whnf+            (streamEdit (chunk "foo" :: Parsec Void T.Text T.Text) (const "bar"))+            fooTextM+        ]+    , bgroup "Text.Lazy"+        [ bench "sepCap 10,000" $ whnf+            (parseMaybe (sepCap (chunk "foo" :: Parsec Void TL.Text TL.Text)))+            fooTextL10K+        , bench "streamEdit 10,000" $ whnf+            (streamEdit (chunk "foo" :: Parsec Void TL.Text TL.Text) (const "bar"))+            fooTextL10K+        , bench "sepCap 100,000" $ whnf+            (parseMaybe (sepCap (chunk "foo" :: Parsec Void TL.Text TL.Text)))+            fooTextL100K+        , bench "streamEdit 100,000" $ whnf+            (streamEdit (chunk "foo" :: Parsec Void TL.Text TL.Text) (const "bar"))+            fooTextL100K+        , bench "sepCap 1,000,000" $ whnf+            (parseMaybe (sepCap (chunk "foo" :: Parsec Void TL.Text TL.Text)))+            fooTextLM+        , bench "streamEdit 1,000,000" $ whnf+            (streamEdit (chunk "foo" :: Parsec Void TL.Text TL.Text) (const "bar"))+            fooTextLM+        ]+    ]+
replace-megaparsec.cabal view
@@ -1,5 +1,5 @@ name:                replace-megaparsec-version:             1.0.1.0+version:             1.1.0.0 cabal-version:       1.18 synopsis:            Stream editing with parsers homepage:            https://github.com/jamesdbrock/replace-megaparsec@@ -24,7 +24,6 @@  library   hs-source-dirs:      src-  -- rely on megaparsec for version bounds   build-depends:       base >= 4.0 && < 5.0                      , megaparsec   default-language:    Haskell2010@@ -32,33 +31,83 @@   ghc-options:         -O2 -Wall  test-suite test-string-  type:               detailed-0.9-  test-module:        TestString-  hs-source-dirs:     tests-  default-language:     Haskell2010-  build-depends:        base >= 4.0 && < 5.0-                      , replace-megaparsec-                      , megaparsec-                      , Cabal+  type:                detailed-0.9+  test-module:         TestString+  hs-source-dirs:      tests+  default-language:    Haskell2010+  build-depends:       base >= 4.0 && < 5.0+                     , replace-megaparsec+                     , megaparsec+                     , Cabal+  ghc-options:         -Wall  test-suite test-text-  type:               detailed-0.9-  test-module:        TestText-  hs-source-dirs:     tests-  default-language:     Haskell2010-  build-depends:        base >= 4.0 && < 5.0-                      , replace-megaparsec-                      , megaparsec-                      , Cabal-                      , text+  type:                detailed-0.9+  test-module:         TestText+  hs-source-dirs:      tests+  default-language:    Haskell2010+  build-depends:       base >= 4.0 && < 5.0+                     , replace-megaparsec+                     , megaparsec+                     , Cabal+                     , text+  ghc-options:         -Wall  test-suite test-bytestring-  type:               detailed-0.9-  test-module:        TestByteString-  hs-source-dirs:     tests-  default-language:     Haskell2010-  build-depends:        base >= 4.0 && < 5.0-                      , replace-megaparsec-                      , megaparsec-                      , Cabal-                      , bytestring+  type:                detailed-0.9+  test-module:         TestByteString+  hs-source-dirs:      tests+  default-language:    Haskell2010+  build-depends:       base >= 4.0 && < 5.0+                     , replace-megaparsec+                     , megaparsec+                     , Cabal+                     , bytestring+  ghc-options:         -Wall+++-- Disable these executables for now, they clutter up+-- the dependency list and `cabal v2-build`.+--+-- executable bench-string+--   main-is:             BenchString.hs+--   hs-source-dirs:      bench+--   default-language:    Haskell2010+--   build-depends:       base >= 4.0 && < 5.0+--                      , megaparsec+--                      , replace-megaparsec+--   ghc-options:         -O2 -Wall+--+-- executable bench-text+--   main-is:             BenchText.hs+--   hs-source-dirs:      bench+--   default-language:    Haskell2010+--   build-depends:       base >= 4.0 && < 5.0+--                      , megaparsec+--                      , replace-megaparsec+--                      , text+--   ghc-options:         -O2 -Wall+--+-- executable bench-bytestring+--   main-is:             BenchByteString.hs+--   hs-source-dirs:      bench+--   default-language:    Haskell2010+--   build-depends:       base >= 4.0 && < 5.0+--                      , megaparsec+--                      , replace-megaparsec+--                      , bytestring+--   ghc-options:         -O2 -Wall++benchmark bench-unit+  main-is:             BenchUnit.hs+  hs-source-dirs:      bench+  type:                exitcode-stdio-1.0+  default-language:    Haskell2010+  build-depends:       base >= 4.0 && < 5.0+                     , megaparsec+                     , replace-megaparsec+                     , text+                     , bytestring+                     , criterion+  ghc-options:         -O2 -Wall+
src/Replace/Megaparsec.hs view
@@ -41,6 +41,8 @@   , findAllCap      -- * Running parser+    --+    -- Ways to run a parser   , streamEditT   , streamEdit   )@@ -51,11 +53,9 @@ import Data.Bifunctor import Data.Functor.Identity import Data.Proxy-import Data.Foldable import Control.Exception (throw) import Data.Typeable import Control.Monad- import Text.Megaparsec  -- |@@ -90,6 +90,7 @@ -- but, importantly, it returns the parsed result of the @sep@ parser instead -- of throwing it away. --+{-# INLINABLE sepCap #-} sepCap     :: forall e s m a. (MonadParsec e s m)     => m a -- ^ The pattern matching parser @sep@@@ -126,6 +127,7 @@ -- @ --     findAllCap sep = 'sepCap' ('Text.Megaparsec.match' sep) -- @+{-# INLINABLE findAllCap #-} findAllCap     :: MonadParsec e s m     => m a -- ^ The pattern matching parser @sep@@@ -144,6 +146,7 @@ -- @ --     findAll sep = (fmap.fmap) ('Data.Bifunctor.second' fst) $ 'sepCap' ('Text.Megaparsec.match' sep) -- @+{-# INLINABLE findAll #-} findAll     :: MonadParsec e s m     => m a -- ^ The pattern matching parser @sep@@@ -154,7 +157,7 @@ -- | -- == Stream editor ----- Also can be considered “find-and-replace”. Finds all+-- Also known as “find-and-replace”, or “match-and-substitute”. Finds all -- of the sections of the stream which match the pattern @sep@, and replaces -- them with the result of the @editor@ function. --@@ -162,18 +165,26 @@ -- a “way to run a parser”, like 'Text.Megaparsec.parse' -- or 'Text.Megaparsec.runParserT'. ----- === Access the matched section of text in the `editor`+-- === Access the matched section of text in the @editor@ -- -- If you want access to the matched string in the @editor@ function, -- then combine the pattern parser @sep@ with 'Text.Megaparsec.match'.--- This will effectively change the type of the `editor`--- to `(s,a) -> m s`, and then we can write `editor` like:+-- This will effectively change the type of the @editor@+-- to @(s,a) -> m s@. --+-- This allows us to write an @editor@ function which can choose to not+-- edit the match and just leave it as it is. We can write an+-- @editorId@ function such that @streamEditT@ changes nothing.+-- -- @---     let editor (matchString,parseResult) = return matchString+--     editorId (matchString, parseResult) = return matchString+-- @ -----     streamEditT ('Text.Megaparsec.match' sep) editor inputString+-- implies that+-- -- @+--     streamEditT ('Text.Megaparsec.match' sep) editorId ≡ 'Control.Monad.return'+-- @ -- -- === Type constraints --@@ -188,7 +199,7 @@ -- "Data.Bytestring.Lazy", -- and "Data.String". ----- We need the @Monoid s@ instance so that we can @mappend@ the output+-- We need the @Monoid s@ instance so that we can @mconcat@ the output -- stream. -- -- We need @Typeable s@ and @Show s@ for 'Control.Exception.throw'. In theory@@ -206,6 +217,7 @@ -- -- If you want the @editor@ function or the parser @sep@ to remember some state, -- then run this in a stateful monad.+{-# INLINABLE streamEditT #-} streamEditT     :: forall s m a. (Stream s, Monad m, Monoid s, Tokens s ~ s, Show s, Show (Token s), Typeable s)     => ParsecT Void s m a@@ -219,14 +231,13 @@ streamEditT sep editor input = do     runParserT (sepCap sep) "" input >>= \case         (Left err) -> throw err -- sepCap can never fail, but if it does, throw-        (Right r) -> fmap fold $ traverse (either return editor) r--+        (Right r) -> fmap mconcat $ traverse (either return editor) r  -- | -- == Pure stream editor -- -- Pure version of 'streamEditT'.+{-# INLINABLE streamEdit #-} streamEdit     :: forall s a. (Stream s, Monoid s, Tokens s ~ s, Show s, Show (Token s), Typeable s)     => Parsec Void s a
tests/TestByteString.hs view
@@ -9,24 +9,23 @@ import Text.Megaparsec.Byte import Text.Megaparsec.Byte.Lexer import Data.Void-import qualified Data.ByteString as BS-import Data.ByteString (ByteString)+import qualified Data.ByteString as B import Data.ByteString.Internal (c2w) import GHC.Word -type Parser = Parsec Void ByteString+type Parser = Parsec Void B.ByteString  tests :: IO [Test] tests = return     [ Test $ runParserTest "findAll upperChar"         (findAllCap (upperChar :: Parser Word8))-        ("aBcD" :: ByteString)+        ("aBcD" :: B.ByteString)         [Left "a", Right ("B", c2w 'B'), Left "c", Right ("D", c2w 'D')]     -- check that sepCap can progress even when parser consumes nothing     -- and succeeds.     , Test $ runParserTest "zero-consumption parser"         (sepCap (many (upperChar :: Parser Word8)))-        ("aBcD" :: ByteString)+        ("aBcD" :: B.ByteString)         [Left "a", Right [c2w 'B'], Left "c", Right [c2w 'D']]     , Test $ runParserTest "scinum"         (sepCap scinum)@@ -45,33 +44,33 @@         ("a")         ([Left "a"])     ]--runParserTest name p input expected = TestInstance-        { run = do-            case runParser p "" input of-                Left e -> return (Finished $ Fail $ show e)-                Right output ->-                    if (output == expected)-                        then return (Finished Pass)-                        else return (Finished $ Fail-                                    $ show output ++ " ≠ " ++ show expected)-        , name = name-        , tags = []-        , options = []-        , setOption = \_ _ -> Left "no options supported"-        }+  where+    runParserTest nam p input expected = TestInstance+            { run = do+                case runParser p "" input of+                    Left e -> return (Finished $ Fail $ show e)+                    Right output ->+                        if (output == expected)+                            then return (Finished Pass)+                            else return (Finished $ Fail+                                        $ show output ++ " ≠ " ++ show expected)+            , name = nam+            , tags = []+            , options = []+            , setOption = \_ _ -> Left "no options supported"+            } -scinum :: Parser (Double, Integer)-scinum = do-    -- This won't parse mantissas that contain a decimal point,-    -- but if we use the Text.Megaparsec.Byte.Lexer.float, then it consumes-    -- the "E" and the exponent. Whatever, doesn't really matter for this test.-    m <- fromIntegral <$> decimal-    chunk "E"-    e <- decimal-    return (m, e)+    scinum :: Parser (Double, Integer)+    scinum = do+        -- This won't parse mantissas that contain a decimal point,+        -- but if we use the Text.Megaparsec.Byte.Lexer.float, then it consumes+        -- the "E" and the exponent. Whatever, doesn't really matter for this test.+        m <- (fromIntegral :: Integer -> Double) <$> decimal+        _ <- chunk "E"+        e <- decimal+        return (m, e)  -offsetA :: Parser Int-offsetA = getOffset <* chunk "A"+    offsetA :: Parser Int+    offsetA = getOffset <* chunk "A" 
tests/TestString.hs view
@@ -6,7 +6,6 @@ import Replace.Megaparsec import Text.Megaparsec import Text.Megaparsec.Char-import Text.Megaparsec.Char.Lexer import Data.Void  type Parser = Parsec Void String@@ -40,30 +39,30 @@         ("a")         ([Left "a"])     ]--runParserTest name p input expected = TestInstance-        { run = do-            case runParser p "" input of-                Left e -> return (Finished $ Fail $ show e)-                Right output ->-                    if (output == expected)-                        then return (Finished Pass)-                        else return (Finished $ Fail-                                    $ show output ++ " ≠ " ++ show expected)-        , name = name-        , tags = []-        , options = []-        , setOption = \_ _ -> Left "no options supported"-        }+  where+    runParserTest nam p input expected = TestInstance+            { run = do+                case runParser p "" input of+                    Left e -> return (Finished $ Fail $ show e)+                    Right output ->+                        if (output == expected)+                            then return (Finished Pass)+                            else return (Finished $ Fail+                                        $ show output ++ " ≠ " ++ show expected)+            , name = nam+            , tags = []+            , options = []+            , setOption = \_ _ -> Left "no options supported"+            } -scinum :: Parser (Double, Integer)-scinum = do-    m <- some digitChar-    chunk "E"-    e <- some digitChar-    return (read m, read e)+    scinum :: Parser (Double, Integer)+    scinum = do+        m <- some digitChar+        _ <- chunk "E"+        e <- some digitChar+        return (read m, read e)  -offsetA :: Parser Int-offsetA = getOffset <* chunk "A"+    offsetA :: Parser Int+    offsetA = getOffset <* chunk "A" 
tests/TestText.hs view
@@ -7,24 +7,22 @@ import Replace.Megaparsec import Text.Megaparsec import Text.Megaparsec.Char-import Text.Megaparsec.Char.Lexer import Data.Void import qualified Data.Text as T-import Data.Text (Text) -type Parser = Parsec Void Text+type Parser = Parsec Void T.Text  tests :: IO [Test] tests = return     [ Test $ runParserTest "findAll upperChar"         (findAllCap (upperChar :: Parser Char))-        ("aBcD" :: Text)+        ("aBcD" :: T.Text)         [Left "a", Right ("B", 'B'), Left "c", Right ("D", 'D')]     -- check that sepCap can progress even when parser consumes nothing     -- and succeeds.     , Test $ runParserTest "zero-consumption parser"         (sepCap (many (upperChar :: Parser Char)))-        ("aBcD" :: Text)+        ("aBcD" :: T.Text)         [Left "a", Right "B", Left "c", Right "D"]     , Test $ runParserTest "scinum"         (sepCap scinum)@@ -43,30 +41,30 @@         ("a")         ([Left "a"])     ]--runParserTest name p input expected = TestInstance-        { run = do-            case runParser p "" input of-                Left e -> return (Finished $ Fail $ show e)-                Right output ->-                    if (output == expected)-                        then return (Finished Pass)-                        else return (Finished $ Fail-                                    $ show output ++ " ≠ " ++ show expected)-        , name = name-        , tags = []-        , options = []-        , setOption = \_ _ -> Left "no options supported"-        }+  where+    runParserTest nam p input expected = TestInstance+            { run = do+                case runParser p "" input of+                    Left e -> return (Finished $ Fail $ show e)+                    Right output ->+                        if (output == expected)+                            then return (Finished Pass)+                            else return (Finished $ Fail+                                        $ show output ++ " ≠ " ++ show expected)+            , name = nam+            , tags = []+            , options = []+            , setOption = \_ _ -> Left "no options supported"+            } -scinum :: Parser (Double, Integer)-scinum = do-    m <- some digitChar-    chunk "E"-    e <- some digitChar-    return (read m, read e)+    scinum :: Parser (Double, Integer)+    scinum = do+        m <- some digitChar+        _ <- chunk "E"+        e <- some digitChar+        return (read m, read e)  -offsetA :: Parser Int-offsetA = getOffset <* chunk "A"+    offsetA :: Parser Int+    offsetA = getOffset <* chunk "A"