diff --git a/ChangeLog.md b/ChangeLog.md
new file mode 100644
--- /dev/null
+++ b/ChangeLog.md
@@ -0,0 +1,3 @@
+# Changelog for fuzzy-dates
+
+## Unreleased changes
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,30 @@
+Copyright Reed Oei (c) 2018
+
+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 Reed Oei 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,18 @@
+# fuzzy-dates
+
+[![Build Status](https://travis-ci.org/ReedOei/fuzzy-dates.svg?branch=master)](https://travis-ci.org/ReedOei/fuzzy-dates)
+
+`fuzzy-dates` is a Haskell library for parsing dates when you don't know/care to specify the format of the dates beforehand.
+
+It is heavily based off of <https://gitlab.com/doshitan/hourglass-fuzzy-parsing>, which had not been updated for over 2 years at the time of writing, so I created this library, and added numerous new date formats to it.
+
+# Quickstart
+
+Import the main module, then call one of the extract dates functions, like so:
+
+```
+>>> import Data.Dates.Parsing
+>>> extractDatesY 2018 "The party will be on 6/9"
+[Date 2018 June 9]
+```
+
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/app/Main.hs b/app/Main.hs
new file mode 100644
--- /dev/null
+++ b/app/Main.hs
@@ -0,0 +1,5 @@
+module Main where
+
+main :: IO ()
+main = pure ()
+
diff --git a/fuzzy-dates.cabal b/fuzzy-dates.cabal
new file mode 100644
--- /dev/null
+++ b/fuzzy-dates.cabal
@@ -0,0 +1,75 @@
+-- This file has been generated from package.yaml by hpack version 0.20.0.
+--
+-- see: https://github.com/sol/hpack
+--
+-- hash: a9ebf0d34504c67f2b28393aa560365b2e0f7c4bcdfc12512e614700f1c14e36
+
+name:           fuzzy-dates
+version:        0.1.0.0
+description:    Please see the README on GitHub at <https://github.com/ReedOei/fuzzy-dates#readme>
+homepage:       https://github.com/ReedOei/fuzzy-dates#readme
+bug-reports:    https://github.com/ReedOei/fuzzy-dates/issues
+author:         Reed Oei
+maintainer:     oei.reed@gmail.com
+copyright:      2018 Reed Oei
+license:        BSD3
+license-file:   LICENSE
+build-type:     Simple
+cabal-version:  >= 1.10
+
+extra-source-files:
+    ChangeLog.md
+    README.md
+
+source-repository head
+  type: git
+  location: https://github.com/ReedOei/fuzzy-dates
+
+library
+  hs-source-dirs:
+      src
+  build-depends:
+      base >=4.7 && <5
+    , hourglass
+    , hspec
+    , lens
+    , parsec
+  exposed-modules:
+      Data.Dates.Parsing
+      Data.Dates.Parsing.Internal
+  other-modules:
+      Paths_fuzzy_dates
+  default-language: Haskell2010
+
+executable fuzzy-dates-exe
+  main-is: Main.hs
+  hs-source-dirs:
+      app
+  ghc-options: -threaded -rtsopts -with-rtsopts=-N
+  build-depends:
+      base >=4.7 && <5
+    , fuzzy-dates
+    , hourglass
+    , hspec
+    , lens
+    , parsec
+  other-modules:
+      Paths_fuzzy_dates
+  default-language: Haskell2010
+
+test-suite fuzzy-dates-test
+  type: exitcode-stdio-1.0
+  main-is: Spec.hs
+  hs-source-dirs:
+      test
+  ghc-options: -threaded -rtsopts -with-rtsopts=-N
+  build-depends:
+      base >=4.7 && <5
+    , fuzzy-dates
+    , hourglass
+    , hspec
+    , lens
+    , parsec
+  other-modules:
+      Paths_fuzzy_dates
+  default-language: Haskell2010
diff --git a/src/Data/Dates/Parsing.hs b/src/Data/Dates/Parsing.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/Dates/Parsing.hs
@@ -0,0 +1,397 @@
+{-# LANGUAGE DeriveDataTypeable #-}
+{-# LANGUAGE FlexibleContexts   #-}
+{-# LANGUAGE TemplateHaskell    #-}
+
+-- | Parse strings that aren't so precise
+module Data.Dates.Parsing
+  (
+    Config (..)
+  , DateTime (..)
+  , DateInterval (..)
+  , Time (..)
+  , defaultConfig
+  , defaultConfigIO
+  , parseDate
+  , parseDateTime
+  , pAbsDateTime
+  , pAbsDate
+  , pDate
+  , pDateTime
+  , pTime
+  , pDateInterval
+  , weekdayToInterval
+  , dateWeekDay
+  , getStartOfThisWeek
+  , getStartOfNextWeek
+  , lastDate
+  , nextDate
+  , addInterval
+  , negateInterval
+  , minusInterval
+  , dateInFormat
+  , extractDates, extractDatesY, extract
+  ) where
+
+import Control.Lens
+import Control.Monad
+
+import Data.Char                            (toLower)
+import Data.Data                            (Data, Typeable)
+import Data.Hourglass
+import Data.List                            (intercalate, find)
+import Text.Parsec
+import Text.Read (readMaybe)
+
+import Time.System (dateCurrent)
+
+import Data.Dates.Parsing.Internal
+
+data DateInterval = Days Int
+                  | Weeks Int
+                  | Months Int
+                  | Years Int
+  deriving (Eq,Show,Data,Typeable)
+
+data Config = Config
+    { _now            :: DateTime -- ^ "Current" date/time, to use as base for relative dates
+    , _startOfWeekDay :: WeekDay} -- ^ Which day of the week to consider the start day
+
+makeLenses ''Config
+
+defaultConfig :: DateTime -> Config
+defaultConfig now' = Config
+  {
+    _now = now'
+  , _startOfWeekDay = Monday
+  }
+
+defaultConfigIO :: IO Config
+defaultConfigIO = defaultConfig <$> dateCurrent
+
+-- | Weekday as interval from the configure start of the week
+weekdayToInterval :: Config -> WeekDay -> DateInterval
+weekdayToInterval c wd =
+  Days (fromIntegral $ fromEnum wd - fromEnum (c^.startOfWeekDay))
+
+getStartOfThisWeek :: Config -> DateTime
+getStartOfThisWeek c = (c^.now) `minusInterval` weekdayToInterval c (dateWeekDay (c^.now))
+
+getStartOfNextWeek :: Config -> DateTime
+getStartOfNextWeek c = getStartOfThisWeek c `addInterval` Weeks 1
+
+-- | Get weekday of given date.
+dateWeekDay :: DateTime -> WeekDay
+dateWeekDay = getWeekDay . timeGetDate
+
+lookupMonth :: String -> Either [Month] Month
+lookupMonth = uniqFuzzyMatch
+
+time :: Stream s m Char => Hours -> ParsecT s st m TimeOfDay
+time hMax = do
+  h <- number 2 hMax
+  char ':'
+  m <- number 2 59
+  x <- optionMaybe $ char ':'
+  case x of
+    Nothing -> return $ TimeOfDay h m 0 0
+    Just _ -> do
+      s <- number 2 59
+      notFollowedBy letter
+      return $ TimeOfDay h m s 0
+
+time24 :: Stream s m Char => ParsecT s st m TimeOfDay
+time24 = time 23
+time12 :: Stream s m Char => ParsecT s st m TimeOfDay
+time12 = time 12
+
+ampm :: Stream s m Char => ParsecT s st m Int
+ampm = do
+  s <- many1 letter
+  case uppercase s of
+    "AM" -> return 0
+    "PM" -> return 12
+    _ -> fail "AM/PM expected"
+
+
+pTime :: Stream s m Char => ParsecT s st m TimeOfDay
+pTime = choice $ map try [time12, time24]
+
+newtype DateFormat = DateFormat [(DatePart, String)]
+data DatePart = D | M | Y
+data DatePartVal = DV Int | MV Month | YV Int
+
+datePart :: Stream s m Char => DatePart -> ParsecT s st m DatePartVal
+datePart M = MV <$> pMonth
+datePart D = DV <$> pDay
+datePart Y = YV <$> pYear
+
+isYV (YV _) = True
+isYV _ = False
+
+isMV (MV _) = True
+isMV _ = False
+
+isDV (DV _) = True
+isDV _ = False
+
+monthPart :: [DatePartVal] -> Month
+monthPart = maybe January (\(MV m) -> m) . find isMV
+
+dayPart :: [DatePartVal] -> Int
+dayPart = maybe 1 (\(DV d) -> d) . find isDV
+
+yearPart :: Int -> [DatePartVal] -> Int
+yearPart year = maybe year (\(YV y) -> y) . find isYV
+
+makeFormat :: String -> [DatePart] -> DateFormat
+makeFormat sep parts = DateFormat $ zip parts $ repeat sep
+
+dateInFormat year (DateFormat parts) = do
+    partVals <- zipWithM go [1..] parts
+
+    pure $ Date (yearPart year partVals) (monthPart partVals) (dayPart partVals)
+    where
+        go i (p, sep)
+            -- The last one doesn't need to have a separator.
+            | i == length parts = datePart p
+            | otherwise = do
+                v <- datePart p
+                string sep
+                pure v
+
+euroNumDate = makeFormat "." [D, M, Y]
+writtenDate = DateFormat [(M, " "), (D, ","), (Y, "")]
+americanDate = makeFormat "/" [M, D, Y]
+dashDate = makeFormat "-" [Y, M, D]
+strDate = makeFormat " " [D, M, Y]
+spaceDate = makeFormat " " [D, M]
+spaceDateMD = makeFormat " " [M, D]
+
+dotDateMonth = makeFormat "." [D, M]
+dashDateMonth = makeFormat "-" [M, D]
+slashDateMonth = makeFormat "/" [M, D]
+
+pAbsDateTime :: Stream s m Char => Int -> ParsecT s st m DateTime
+pAbsDateTime year = do
+  date <- pAbsDate year
+  optional $ char ','
+  s <- optionMaybe space
+  case s of
+    Nothing -> return $ DateTime date (TimeOfDay 0 0 0 0)
+    Just _ -> do
+      t <- pTime
+      return $ DateTime date t
+
+pAbsDate :: Stream s m Char => Int -> ParsecT s st m Date
+pAbsDate year = choice $ map (try . dateInFormat year)
+    [euroNumDate, americanDate, strDate, writtenDate, dashDate,
+     dotDateMonth, dashDateMonth, slashDateMonth, spaceDate, spaceDateMD]
+
+intervalToPeriod :: DateInterval -> Period
+intervalToPeriod (Days ds)   = mempty { periodDays   = ds}
+intervalToPeriod (Weeks ws)  = mempty { periodDays   = ws*7 }
+intervalToPeriod (Months ms) = mempty { periodMonths = ms }
+intervalToPeriod (Years ys)  = mempty { periodYears  = ys }
+
+-- | Add date interval to DateTime
+addInterval :: DateTime -> DateInterval -> DateTime
+addInterval dt@DateTime {dtDate = date} interval =
+  dt { dtDate = date `dateAddPeriod` intervalToPeriod interval }
+
+-- | Negate DateInterval value: Days 3 -> Days (-3).
+negateInterval :: DateInterval -> DateInterval
+negateInterval (Days n)   = Days (negate n)
+negateInterval (Weeks n)  = Weeks (negate n)
+negateInterval (Months n) = Months (negate n)
+negateInterval (Years n)  = Years (negate n)
+
+-- | Subtract DateInterval from DateTime.
+minusInterval :: DateTime -> DateInterval -> DateTime
+minusInterval date int = date `addInterval` negateInterval int
+
+maybePlural :: Stream s m Char => String -> ParsecT s st m String
+maybePlural str = do
+  r <- string str
+  optional $ char 's'
+  return r
+
+pDateIntervalType :: Stream s m Char => ParsecT s st m (Int -> DateInterval)
+pDateIntervalType = do
+  s <- choice $ map maybePlural ["day", "week", "month", "year"]
+  case toLower (head s) of
+    'd' -> return Days
+    'w' -> return Weeks
+    'm' -> return Months
+    'y' -> return Years
+    _ -> fail $ "Unknown date interval type: " ++ s
+
+pDateInterval :: Stream s m Char => ParsecT s st m DateInterval
+pDateInterval = do
+  maybeN <- readMaybe <$> many1 digit
+
+  case maybeN of
+    Nothing -> fail "Noperino."
+    Just n -> do
+      spaces
+      tp <- pDateIntervalType
+      pure $ tp n
+
+pRelDate :: Stream s m Char => Config -> ParsecT s st m DateTime
+pRelDate c = do
+  offs <- try futureDate
+     <|> try passDate
+     <|> try today
+     <|> try tomorrow
+     <|> yesterday
+  return $ (c^.now) `addInterval` offs
+
+lastDate :: Stream s m Char => Config -> ParsecT s st m DateTime
+lastDate c = do
+    string "last"
+    spaces
+    try byweek <|> try bymonth <|> byyear
+  where
+    startOfWeekDay' = c^.startOfWeekDay
+    now' = c^.now
+    byweek = do
+      wd <- try (string "week" >> return startOfWeekDay') <|> pWeekDay
+      let lastWeekStart = getStartOfThisWeek c `minusInterval` Weeks 1
+      return $ lastWeekStart `addInterval` weekdayToInterval c wd
+
+    bymonth = do
+      string "month"
+      let lastMonth = now' `minusInterval` Months 1
+      return $ lastMonth { dtDate = (dtDate lastMonth) { dateDay = 1 } }
+
+    byyear = do
+      string "year"
+      let lastYear = now' `minusInterval` Years 1
+      return $ lastYear { dtDate = (dtDate lastYear) { dateMonth = January, dateDay = 1 } }
+
+nextDate :: Stream s m Char => Config -> ParsecT s st m DateTime
+nextDate c = do
+    string "next"
+    spaces
+    try byweek <|> try bymonth <|> byyear
+  where
+    startOfWeekDay' = c^.startOfWeekDay
+    now' = c^.now
+    byweek = do
+      wd <- try (string "week" >> return startOfWeekDay') <|> pWeekDay
+      let nextWeekStart = getStartOfNextWeek c
+      return $ nextWeekStart `addInterval` weekdayToInterval c wd
+
+    bymonth = do
+      string "month"
+      let nextMonth = now' `addInterval` Months 1
+      return nextMonth { dtDate = (dtDate nextMonth) { dateDay = 1 } }
+
+    byyear = do
+      string "year"
+      let nextYear = now' `addInterval` Years 1
+      return nextYear { dtDate = (dtDate nextYear) { dateMonth = January, dateDay = 1 } }
+
+pWeekDay :: Stream s m Char => ParsecT s st m WeekDay
+pWeekDay = do
+  w <- many1 (oneOf "mondaytueswnhrfi")
+  case uniqFuzzyMatch w :: Either [WeekDay] WeekDay of
+    Left ds -> fail $ if null ds
+                         then "unknown weekday: " ++ w
+                         else "ambiguous weekday '" ++ w ++ "' could mean: " ++ intercalate " or " (map show ds)
+    Right d -> return d
+
+futureDate :: Stream s m Char => ParsecT s st m DateInterval
+futureDate = do
+  string "in "
+  maybeN <- readMaybe <$> many1 digit
+
+  case maybeN of
+    Nothing -> fail "Noperino."
+    Just n -> do
+      char ' '
+      tp <- pDateIntervalType
+      pure $ tp n
+
+passDate :: Stream s m Char => ParsecT s st m DateInterval
+passDate = do
+  maybeN <- readMaybe <$> many1 digit
+
+  case maybeN of
+    Nothing -> fail "Noperino."
+    Just n -> do
+      char ' '
+      tp <- pDateIntervalType
+      string " ago"
+      pure $ tp $ negate n
+
+today :: Stream s m Char => ParsecT s st m DateInterval
+today = do
+  string "today" <|> string "now"
+  return $ Days 0
+
+tomorrow :: Stream s m Char => ParsecT s st m DateInterval
+tomorrow = do
+  string "tomorrow"
+  return $ Days 1
+
+yesterday :: Stream s m Char => ParsecT s st m DateInterval
+yesterday = do
+  string "yesterday"
+  return $ Days (-1)
+
+pByWeek :: Stream s m Char => Config -> ParsecT s st m DateTime
+pByWeek c =
+  try (lastDate c) <|> nextDate c
+
+-- | Parsec parser for DateTime.
+pDateTime :: Stream s m Char
+          => Config
+          -> ParsecT s st m DateTime
+pDateTime c =
+      try (pRelDate c)
+  <|> try (pByWeek c)
+  <|> try (pAbsDateTime (dateYear (timeGetDate (c^.now))))
+
+-- | Parsec parser for Date only.
+pDate :: Stream s m Char
+      => Config
+      -> ParsecT s st m Date
+pDate c =
+      try (timeGetDate <$> pRelDate c)
+  <|> try (timeGetDate <$> pByWeek c)
+  <|> try (pAbsDate $ dateYear (timeGetDate (c^.now)))
+
+-- | Parse date/time
+parseDate :: Config
+          -> String -- ^ String to parse
+          -> Either ParseError Date
+parseDate c = runParser (pDate c) () ""
+
+-- | Parse date/time
+parseDateTime :: Config
+              -> String -- ^ String to parse
+              -> Either ParseError DateTime
+parseDateTime c = runParser (pDateTime c) () ""
+
+-- | Same as extractDatesY, but will get the current year from the system, so you don't have to provide it.
+extractDates :: String -> IO [Date]
+extractDates str = do
+    c <- defaultConfigIO
+
+    pure $ extractDatesY (dateYear (timeGetDate (c ^. now))) str
+
+-- | Extract dates from a string, with the first argument being the current year (used for things like "Jan 18").
+--
+-- >>> extractDatesY 2018 "The party will be on 6/9"
+-- [Date 2018 June 9]
+extractDatesY :: Int -> String -> [Date]
+extractDatesY y str =
+    case parse (extract (pAbsDate y)) "" str of
+        Left err -> error $ show err
+        Right dates -> dates
+
+extract :: Stream s m Char => ParsecT s st m a -> ParsecT s st m [a]
+extract parser = Text.Parsec.many loop
+    where
+        loop = try parser <|> (anyChar >> loop)
+
diff --git a/src/Data/Dates/Parsing/Internal.hs b/src/Data/Dates/Parsing/Internal.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/Dates/Parsing/Internal.hs
@@ -0,0 +1,152 @@
+{-# LANGUAGE FlexibleContexts #-}
+
+module Data.Dates.Parsing.Internal where
+
+import Data.Char           (digitToInt, isDigit, toUpper, toLower, toUpper)
+import Data.Hourglass
+import Data.List           (isPrefixOf, lookup)
+import Data.Maybe          (fromJust, catMaybes)
+import Text.Parsec
+import Text.Read (readMaybe)
+
+-- | Parsers the parser at least once, but no more than n times.
+takeN1 :: Stream s m Char => Int -> ParsecT s st m a -> ParsecT s st m [a]
+takeN1 0 _ = error "n must not be 0!"
+takeN1 1 parser = (:[]) <$> parser
+takeN1 n parser = do
+    v <- parser
+    rest <- takeN1 (n - 1) parser <|> pure []
+    pure $ v : rest
+
+-- | Parse natural number of N digits which is not greater than M
+number :: (Stream s m Char, Read a, Num a, Ord a)
+       => Int -- ^ Number of digits
+       -> a   -- ^ Maximum value
+       -> ParsecT s st m a
+number n m = do
+  maybeT <- readMaybe <$> takeN1 n digit
+  case maybeT of
+    Just t | t <= m -> pure t
+    _ -> parserZero
+
+pYear :: Stream s m Char => ParsecT s st m Int
+pYear = do
+    n <- try pYearNormal <|> pYearAny
+        -- Assume two digit years are after 2000.
+        -- TODO: Update in 82 years (2018-05-03).
+    pure $ if n < 2000 && n < 100 && n >= 10 then n + 2000 else n
+
+pYearNormal :: Stream s m Char => ParsecT s st m Int
+pYearNormal = do
+    n <- read <$> many1 digit
+
+    notFollowedBy (try (spaces >> yearAbbreviations) <|> (digit >> pure ""))
+
+    pure n
+
+readNum :: (Num a, Stream s m Char) => ParsecT s st m a
+readNum = do
+    isNegative <- optionMaybe $ char '-'
+    digits <- many1 digit
+
+    let sign = maybe 1 (const (-1)) isNegative
+
+    pure $ sign * fromInteger (read digits)
+
+yearAbbreviations :: Stream s m Char => ParsecT s st m String
+yearAbbreviations = choice $ map (try . abbParser) ["BCE", "AD", "CE", "BC"]
+    where
+        abbParser :: Stream s m Char => String -> ParsecT s st m String
+        abbParser abbr = parseAs (concatMap casings $ makeAbbr abbr) abbr
+
+makeAbbr :: String -> [String]
+makeAbbr abb = [abb, foldl (\cur n -> cur ++ [n] ++ ".") "" abb]
+
+pYearAny :: Stream s m Char => ParsecT s st m Int
+pYearAny = do
+    spaces
+    -- Allow abbreviations before or after.
+    prefix <- optionMaybe yearAbbreviations
+
+    spaces
+    n <- readNum
+    spaces
+
+    suffix <- optionMaybe yearAbbreviations
+
+    let isBC = case catMaybes [prefix, suffix] of
+                (abb:_) -> abb == "BC" || abb == "BCE"
+                [] -> False
+
+    pure $ if isBC then -n else n
+
+monthAssoc :: [(String, Month)]
+monthAssoc = [("january", January), ("jan", January), ("february", February), ("feb", February),
+              ("march", March), ("mar", March), ("april", April), ("apr", April),
+              ("may", May), ("june", June), ("jun", June), ("july", July), ("july", July),
+              ("august", August), ("aug", August), ("september", September), ("sept", September),
+              ("october", October), ("oct", October), ("november", November), ("nov", November),
+              ("december", December), ("dec", December)]
+
+casings :: String -> [String]
+casings [] = []
+casings str@(f:rest) = [str, map toLower str, map toUpper str, toUpper f : rest]
+
+-- | Parse various capitalizations of the given string, but always return the same string on success.
+parseAs :: Stream s m Char => [String] -> String -> ParsecT s st m String
+parseAs options str = do
+    _ <- choice $ map (try . string) options
+
+    optional $ char '.'
+
+    pure str
+
+pMonthName :: Stream s m Char => ParsecT s st m Month
+pMonthName = do
+    monthName <- choice $ map (\(name,_) -> try $ parseAs (casings name) name) monthAssoc
+
+    -- Safe because month names come from monthAssoc
+    return $ fromJust $ lookup monthName monthAssoc
+
+pMonth :: Stream s m Char => ParsecT s st m Month
+pMonth = try (toEnum . pred <$> number 2 12) <|>
+         pMonthName
+
+pDay :: Stream s m Char => ParsecT s st m Int
+pDay = number 2 31
+
+uppercase :: String -> String
+uppercase = map toUpper
+
+-- | Case-insensitive version of 'isPrefixOf'
+isPrefixOfI :: String -> String -> Bool
+p `isPrefixOfI` s = uppercase p `isPrefixOf` uppercase s
+
+-- | Use a data type's Bounded, Enum and Show instances to determine if the
+-- given string uniquely matches a constructor. The comparison is
+-- case-insensitive and starts from the beginning of the strings (so a partial
+-- constructor name can still match if there are enough characters for a
+-- unique match)
+--
+-- For example:
+--
+-- @
+--  data Things = Foo | Bar | Baz deriving (Bounded, Enum, Show)
+--
+--  -- Right Foo
+--  uniqFuzzyMatch "f" :: Either [Things] Things
+--
+--  -- Left [Bar, Baz]
+--  uniqFuzzyMatch "ba" :: Either [Things] Things
+-- @
+uniqFuzzyMatch :: (Bounded a, Enum a, Show a)
+               => String
+               -> Either [a] a -- ^ Either collection of matches or the unique match
+uniqFuzzyMatch n =
+    case matches of
+        [match] -> Right match
+        _ -> Left matches
+  where
+    possibilities = [minBound..maxBound]
+    matches = filter (isPrefixOfI n . show) possibilities
+
diff --git a/test/Spec.hs b/test/Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Spec.hs
@@ -0,0 +1,129 @@
+module Main where
+
+import Data.Hourglass
+import Data.Dates.Parsing
+import Test.Hspec
+import Text.Parsec                 (parse)
+
+testConfig :: Config
+testConfig = defaultConfig testDateTime
+
+testDate :: Date
+testDate = Date 2015 March 14
+
+testTimeOfDay :: TimeOfDay
+testTimeOfDay = TimeOfDay 3 1 4 0
+
+testDateTime :: DateTime
+testDateTime = DateTime testDate testTimeOfDay
+
+main :: IO ()
+main = hspec spec
+
+spec :: Spec
+spec = do
+  describe "weekdayToInterval" $
+    it "should consider the configured day as the start of the week" $ do
+      weekdayToInterval testConfig { _startOfWeekDay = Monday } Monday `shouldBe` Days 0
+      weekdayToInterval testConfig { _startOfWeekDay = Sunday } Monday `shouldBe` Days 1
+
+  describe "getStartOfThisWeek" $ do
+    it "gives the preceding Monday when Monday is the week start" $
+      getStartOfThisWeek testConfig
+        `shouldBe` testDateTime { dtDate = Date 2015 March 9 }
+
+    it "gives the preceding Sunday when Sunday is the week start" $
+      getStartOfThisWeek testConfig { _startOfWeekDay = Sunday }
+        `shouldBe` testDateTime { dtDate = Date 2015 March 8 }
+
+  describe "getStartOfNextWeek" $ do
+    it "gives the next Monday when Monday is the week start" $
+      getStartOfNextWeek testConfig
+        `shouldBe` testDateTime { dtDate = Date 2015 March 16 }
+
+    it "gives the next Sunday when Sunday is the week start" $
+      getStartOfNextWeek testConfig { _startOfWeekDay = Sunday }
+        `shouldBe` testDateTime { dtDate = Date 2015 March 15 }
+
+  describe "lastDate" $ do
+    mapM_
+      (\(str, ans) ->
+        it ("understands '" ++ str ++ "'") (parse (lastDate testConfig) "" str `shouldBe` Right ans)
+      )
+      [
+        ("last week", testDateTime { dtDate = (dtDate testDateTime) { dateDay = 2 } })
+      , ("last month", testDateTime { dtDate = (dtDate testDateTime) { dateMonth = February, dateDay = 1 } })
+      , ("last year", testDateTime { dtDate = (dtDate testDateTime) { dateYear = 2014, dateMonth = January, dateDay = 1 } })
+      ]
+
+    it "understands 'last week' using configured week start" $
+      parse
+        (lastDate testConfig { _startOfWeekDay = Sunday })
+        ""
+        "last week"
+      `shouldBe` Right testDateTime { dtDate = (dtDate testDateTime) { dateDay = 1 } }
+
+    it "understands 'last thursday' as last week's Thursday when 'now' is Friday" $
+      parse
+        (lastDate testConfig { _now = testDateTime { dtDate = (dtDate testDateTime) { dateDay = 13 } } })
+        ""
+        "last thursday"
+      `shouldBe` Right testDateTime { dtDate = (dtDate testDateTime) { dateDay = 5 } }
+
+  describe "nextDate" $ do
+    mapM_
+      (\(str, ans) ->
+        it ("understands '" ++ str ++ "'") (parse (nextDate testConfig) "" str `shouldBe` Right ans)
+      )
+      [
+        ("next week", testDateTime { dtDate = (dtDate testDateTime) { dateDay = 16 } })
+      , ("next month", testDateTime { dtDate = (dtDate testDateTime) { dateMonth = April, dateDay = 1 } })
+      , ("next year", testDateTime { dtDate = (dtDate testDateTime) { dateYear = 2016, dateMonth = January, dateDay = 1 } })
+      ]
+
+    it "understands 'next week' using configured week start" $
+      parse
+        (nextDate testConfig { _startOfWeekDay = Sunday })
+        ""
+        "next week"
+      `shouldBe` Right testDateTime { dtDate = (dtDate testDateTime) { dateDay = 15 } }
+
+    it "understands 'next thursday' as next week's Thursday when 'now' is Monday" $
+      parse
+        (nextDate testConfig { _now = testDateTime { dtDate = (dtDate testDateTime) { dateDay = 9 } } })
+        ""
+        "next thursday"
+      `shouldBe` Right testDateTime { dtDate = (dtDate testDateTime) { dateDay = 19 } }
+
+  describe "parseDate" $
+    mapM_
+      (\(str, ans) ->
+        it ("understands '" ++ str ++ "'") (parseDate testConfig str `shouldBe` Right ans)
+      )
+      [
+        ("today", testDate)
+      , ("tomorrow", testDate { dateDay = 15 })
+      , ("yesterday", testDate { dateDay = 13 })
+      , ("in 2 days", testDate { dateDay = 16 })
+      , ("in 3 weeks", Date 2015 April 4)
+      , ("1 week ago", testDate { dateDay = 7 })
+      , ("next friday", testDate { dateDay = 20 })
+      , ("next year", Date 2016 January 1)
+      , ("12 September 2012", Date 2012 September 12)
+      , ("12 September 12", Date 2012 September 12)
+      , ("12.09.2012", Date 2012 September 12)
+      , ("January 1, 2017", Date 2017 January 1)
+      , ("apr. 8, 15", Date 2015 April 8)
+      , ("july 30, BC 82", Date (-82) July 30)
+      , ("aug 15, 7 AD", Date 7 August 15)
+      , ("2017-08-09", Date 2017 August 9)
+      , ("DECEMBER 8, -9", Date (-9) December 8)
+      ]
+
+  describe "extractDatesY" $
+    mapM_ (\(str, ans) -> it ("understands '" ++ str ++ "'") (extractDatesY 2018 str `shouldBe` ans))
+        [ ("2017-08-9", [Date 2017 August 9])
+        , ("Jan 19", [Date 2018 January 19])
+        , ("The party will be on 6/9", [Date 2018 June 9])
+        , ("Start: November 27, 1993\nEnd: December 5, 1993", [Date 1993 November 27, Date 1993 December 5])]
+
