diff --git a/ChangeLog.md b/ChangeLog.md
new file mode 100644
--- /dev/null
+++ b/ChangeLog.md
@@ -0,0 +1,4 @@
+# Changelog for parse
+
+## 0.1.0.0 - First version
+That's it, enjoy.
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -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.
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -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.
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/parse.cabal b/parse.cabal
new file mode 100644
--- /dev/null
+++ b/parse.cabal
@@ -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
diff --git a/src/Parse.hs b/src/Parse.hs
new file mode 100644
--- /dev/null
+++ b/src/Parse.hs
@@ -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)
diff --git a/src/Parse/Internal/Instances.hs b/src/Parse/Internal/Instances.hs
new file mode 100644
--- /dev/null
+++ b/src/Parse/Internal/Instances.hs
@@ -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
diff --git a/src/Parse/Internal/Parse.hs b/src/Parse/Internal/Parse.hs
new file mode 100644
--- /dev/null
+++ b/src/Parse/Internal/Parse.hs
@@ -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
diff --git a/test/Main.hs b/test/Main.hs
new file mode 100644
--- /dev/null
+++ b/test/Main.hs
@@ -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 ]
