diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,11 @@
+Copyright 2020 Mitchell Vitez
+
+Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+
+2. 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.
+
+3. Neither the name of the copyright holder nor the names of its 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 HOLDER 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,25 @@
+# rrule
+
+## Recurrence rule parser and formatter
+
+`fromText` parses from a recurrence rule `Text` to the `RRule` type
+
+```haskell
+> let rule = fromJust $ fromText "RRULE:FREQ=MONTHLY;INTERVAL=1;BYMONTHDAY=1"
+> rule
+RRule {prefix = True, frequency = Just Monthly, byMonthDay = Just (1 :| []), ... }
+```
+
+`toText` formats an `RRule` to a recurrence rule `Text`
+
+```haskell
+> toText rule
+"RRULE:FREQ=MONTHLY;INTERVAL=1;BYMONTHDAY=1"
+```
+
+`description` gives a description of what an `RRule` means, in English
+
+```haskell
+> description rule
+"every month on the 1st day of the month"
+```
diff --git a/changelog.md b/changelog.md
new file mode 100644
--- /dev/null
+++ b/changelog.md
@@ -0,0 +1,5 @@
+# 0.1 - March 2, 2020
+
+- `fromText` parses from `Text` to the `RRule` type
+- `toText` formats the `RRule` type to the RFC 5545 recurrence rule format
+- `description` provides a description of what an `RRule` means, in English
diff --git a/rrule.cabal b/rrule.cabal
new file mode 100644
--- /dev/null
+++ b/rrule.cabal
@@ -0,0 +1,58 @@
+cabal-version: 1.12
+
+name:           rrule
+version:        0.1.0.0
+synopsis:       Recurrence rule parser and formatter
+description:    Parser for recurrence rules including formatting back to RFC 5545 recurrence
+                rule strings as well as providing English descriptions
+homepage:       https://github.com/mitchellvitez/rrule#readme
+bug-reports:    https://github.com/mitchellvitez/rrule/issues
+author:         Mitchell Vitez
+maintainer:     mitchell@vitez.me
+copyright:      2020 Mitchell Vitez
+license:        BSD3
+license-file:   LICENSE
+build-type:     Simple
+category:       Time
+extra-source-files:
+    README.md
+    changelog.md
+
+source-repository head
+  type: git
+  location: https://github.com/mitchellvitez/rrule
+
+library
+  exposed-modules:
+      Data.Time.RRule
+  other-modules:
+      Data.Time.RRule.Parse
+      Data.Time.RRule.Types
+  hs-source-dirs:
+      src
+  build-depends:
+      base               >= 4.7   && < 5
+    , megaparsec         >= 8.0.0 && < 8.1
+    , text               >= 1.2.3 && < 1.3
+    , parser-combinators >= 1.2.1 && < 1.3
+    , time               >= 1.8.0 && < 1.9
+  default-extensions:
+      OverloadedStrings
+    , LambdaCase
+    , RecordWildCards
+  default-language: Haskell2010
+
+test-suite rrule-test
+  type: exitcode-stdio-1.0
+  main-is: Spec.hs
+  hs-source-dirs:
+      test
+  ghc-options: -threaded -rtsopts -with-rtsopts=-N -Wall
+  build-depends:
+      base
+    , hspec              >= 2.7.1 && < 2.8
+    , rrule
+    , text
+  default-extensions:
+      OverloadedStrings
+  default-language: Haskell2010
diff --git a/src/Data/Time/RRule.hs b/src/Data/Time/RRule.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/Time/RRule.hs
@@ -0,0 +1,153 @@
+module Data.Time.RRule
+  ( fromText
+  , toText
+  , defaultRRule
+  , description
+  , RRule(..)
+  , Day(..)
+  , Frequency(..)
+  )
+where
+
+import Data.Maybe (catMaybes, isJust)
+import Data.Text (Text, intercalate, pack, unpack)
+import Data.Time.Clock (UTCTime)
+import Data.Time.Format (formatTime, defaultTimeLocale)
+import Data.Time.RRule.Parse (parseRRule)
+import Data.Time.RRule.Types as Ty
+  ( defaultRRule
+  , RRule(..)
+  , Day(..)
+  , Frequency(..)
+  , ToRRule(toRRule)
+  )
+import Text.Megaparsec (parseMaybe)
+import qualified Data.List.NonEmpty as NE (NonEmpty(..), toList)
+
+-- | Parses RFC 5545 recurrence rule text into an RRule
+fromText :: Text -> Maybe RRule
+fromText = parseMaybe parseRRule
+
+-- | Formats RRule as RFC 5545 recurrence rule text
+toText :: RRule -> Text
+toText RRule{..} =
+  (if prefix then "RRULE:" else "") <>
+  (intercalate ";" $ catMaybes
+    [ labelWith "WKST"       weekStart
+    , labelWith "FREQ"       frequency
+    , labelWith "COUNT"      count
+    , labelWith "UNTIL"      until
+    , labelWith "INTERVAL"   interval
+    , labelWith "BYSECOND"   bySecond
+    , labelWith "BYMINUTE"   byMinute
+    , labelWith "BYHOUR"     byHour
+    , labelWith "BYDAY"      byDay
+    , labelWith "BYWEEKNO"   byWeekNo
+    , labelWith "BYMONTH"    byMonth
+    , labelWith "BYMONTHDAY" byMonthDay
+    , labelWith "BYYEARDAY"  byYearDay
+    , labelWith "BYSETPOS"   bySetPos
+    ])
+
+labelWith :: ToRRule a => Text -> Maybe a -> Maybe Text
+labelWith _ Nothing = Nothing
+labelWith label (Just x) = Just $ label <> "=" <> toRRule x
+
+-- | Describes what an RRule means, in English
+description :: RRule -> Text
+description RRule{..} = intercalate " " $ catMaybes
+  [ byDescription "the" ordinal "instance of" bySetPos
+  , if isJust frequency then Just "every" else Nothing
+  , intervalDescription =<< interval
+  , frequencyDescription <$> frequency
+  , byUsualDescription "second" bySecond
+  , byUsualDescription "minute" byMinute
+  , byUsualDescription "hour" byHour
+  , byDescription "on" ordinalDay "" byDay
+  , byUsualDescription "week of the year" byWeekNo
+  , byDescription "in" monthDescription "" byMonth
+  , byUsualDescription "day of the month" byMonthDay
+  , byUsualDescription "day of the year" byYearDay
+  , countDescription <$> count
+  , untilDescription <$> until
+  , weekStartDescription <$> weekStart
+  ]
+
+ordinal :: Int -> Text
+ordinal n
+  | n == -1 = "last"
+  | n < 0 = ordinal (abs n) <> " from last"
+  | lastDigits n == 11 = showText n <> "th"
+  | lastDigits n == 12 = showText n <> "th"
+  | lastDigits n == 13 = showText n <> "th"
+  | lastDigit  n ==  1 = showText n <> "st"
+  | lastDigit  n ==  2 = showText n <> "nd"
+  | lastDigit  n ==  3 = showText n <> "rd"
+  | otherwise = showText n <> "th"
+  where lastDigit n = n `mod` 10
+        lastDigits n = n `mod` 100
+
+ordinalDay :: (Int, Day) -> Text
+ordinalDay (0, d) = showText d
+ordinalDay (n, d) = "the " <> ordinal n <> " " <> showText d
+
+byUsualDescription :: Text -> Maybe (NE.NonEmpty Int) -> Maybe Text
+byUsualDescription t = byDescription "on the" ordinal t
+
+byDescription :: Text -> (a -> Text) -> Text -> Maybe (NE.NonEmpty a) -> Maybe Text
+byDescription _ _ _ Nothing = Nothing
+byDescription inOrOn toOrdinal t (Just ns) =
+  Just $ inOrOn <> " " <> andedList <> timePeriod
+  where andedList = intercalateAnd . map toOrdinal $ NE.toList ns
+        timePeriod = if t == "" then "" else " " <> t
+
+intercalateAnd :: [Text] -> Text
+intercalateAnd [t1, t2, t3] = t1 <> ", " <> t2 <> ", and " <> t3
+intercalateAnd [t1, t2] = t1 <> " and " <> t2
+intercalateAnd [t] = t
+intercalateAnd [] = ""
+intercalateAnd (t:ts) = t <> ", " <> intercalateAnd ts
+
+monthDescription :: Int -> Text
+monthDescription = \case
+  1  -> "January"
+  2  -> "February"
+  3  -> "March"
+  4  -> "April"
+  5  -> "May"
+  6  -> "June"
+  7  -> "July"
+  8  -> "August"
+  9  -> "September"
+  10 -> "October"
+  11 -> "November"
+  12 -> "December"
+
+showText :: Show a => a -> Text
+showText = pack . show
+
+intervalDescription :: Int -> Maybe Text
+intervalDescription n = case n of
+  0 -> Nothing
+  1 -> Nothing
+  2 -> Just "other"
+  n -> Just $ ordinal n
+
+frequencyDescription :: Frequency -> Text
+frequencyDescription freq = case freq of
+  Secondly -> "second"
+  Minutely -> "minute"
+  Hourly   -> "hour"
+  Daily    -> "day"
+  Weekly   -> "week"
+  Monthly  -> "month"
+  Yearly   -> "year"
+
+countDescription :: Int -> Text
+countDescription n = "for " <> showText n <> " occurrences"
+
+untilDescription :: UTCTime -> Text
+untilDescription t = "until " <> (pack $ formatTime defaultTimeLocale "%B %d, %Y at %H:%M:%S" t)
+
+weekStartDescription :: Day -> Text
+weekStartDescription d = "with weeks starting on " <> showText d
diff --git a/src/Data/Time/RRule/Parse.hs b/src/Data/Time/RRule/Parse.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/Time/RRule/Parse.hs
@@ -0,0 +1,110 @@
+module Data.Time.RRule.Parse
+  ( parseRRule
+  )
+where
+
+import Prelude hiding (until)
+import Control.Monad (msum)
+import qualified Control.Monad.Combinators.NonEmpty as NE
+import Data.List.NonEmpty (NonEmpty(..))
+import Data.Maybe (fromMaybe, catMaybes, isJust)
+import Data.Text (Text, intercalate, pack, unpack)
+import Data.Time.Clock (UTCTime)
+import Data.Time.Format (parseTimeM, defaultTimeLocale)
+import Data.Time.RRule.Types (defaultRRule, RRule(..), Day(..), Frequency(..), ToRRule)
+import Text.Megaparsec hiding (count)
+import Text.Megaparsec.Char.Lexer
+
+type Parser = Parsec () Text
+
+parseRRule :: Parser RRule
+parseRRule = do
+  prefixText <- try . optional $ chunk "RRULE:"
+  rules <- parseVariable `sepBy` single ';'
+  let allRules = foldr combineRules defaultRRule rules
+  return allRules { prefix = isJust prefixText }
+
+combineRules :: RRule -> RRule -> RRule
+combineRules r s = RRule
+  { prefix     = prefix r || prefix s
+  , weekStart  = combine weekStart
+  , frequency  = combine frequency
+  , count      = combine count
+  , until      = combine until
+  , interval   = combine interval
+  , bySecond   = combine bySecond
+  , byMinute   = combine byMinute
+  , byHour     = combine byHour
+  , byDay      = combine byDay
+  , byWeekNo   = combine byWeekNo
+  , byMonth    = combine byMonth
+  , byMonthDay = combine byMonthDay
+  , byYearDay  = combine byYearDay
+  , bySetPos   = combine bySetPos
+  }
+  where combine f = msum [f r, f s]
+
+parseVariable :: Parser RRule
+parseVariable = do
+  let prefix = False
+  weekStart  <- parseVar "WKST"       parseDay
+  frequency  <- parseVar "FREQ"       parseFrequency
+  count      <- parseVar "COUNT"      decimal
+  until      <- parseVar "UNTIL"      parseUtcTime
+  interval   <- parseVar "INTERVAL"   decimal
+  bySecond   <- parseVar "BYSECOND"   parseSomeInt
+  byMinute   <- parseVar "BYMINUTE"   parseSomeInt
+  byHour     <- parseVar "BYHOUR"     parseSomeInt
+  byDay      <- parseVar "BYDAY"      parseSomeDay
+  byWeekNo   <- parseVar "BYWEEKNO"   parseSomeInt
+  byMonthDay <- parseVar "BYMONTHDAY" parseSomeInt
+  byMonth    <- parseVar "BYMONTH"    parseSomeInt
+  byYearDay  <- parseVar "BYYEARDAY"  parseSomeInt
+  bySetPos   <- parseVar "BYSETPOS"   parseSomeInt
+  return RRule{..}
+
+parseVar :: Text -> Parser a -> Parser (Maybe a)
+parseVar label parse = try . optional $ chunk label >> single '=' >> parse
+
+parseSomeInt :: Parser (NonEmpty Int)
+parseSomeInt = parseInt `NE.sepBy1` single ','
+
+parseSomeDay :: Parser (NonEmpty (Int, Day))
+parseSomeDay = parseIntDay `NE.sepBy1` single ','
+
+parseInt :: Parser Int
+parseInt = do
+  sign <- try . optional $ single '-'
+  d <- decimal
+  return $ if isJust sign then (negate d) else d
+
+parseIntDay :: Parser (Int, Day)
+parseIntDay = do
+  n <- try . optional $ parseInt
+  d <- parseDay
+  return (fromMaybe 0 n, d)
+
+parseUtcTime :: Parser UTCTime
+parseUtcTime = do
+  d <- manyTill anySingle (single 'Z')
+  parseTimeM False defaultTimeLocale "%Y%m%dT%H%M%S" d
+
+parseFrequency :: Parser Frequency
+parseFrequency =
+  Secondly <$ chunk "SECONDLY" <|>
+  Minutely <$ chunk "MINUTELY" <|>
+  Hourly   <$ chunk "HOURLY"   <|>
+  Daily    <$ chunk "DAILY"    <|>
+  Weekly   <$ chunk "WEEKLY"   <|>
+  Monthly  <$ chunk "MONTHLY"  <|>
+  Yearly   <$ chunk "YEARLY"
+
+parseDay :: Parser Day
+parseDay =
+  Sunday    <$ chunk "SU" <|>
+  Monday    <$ chunk "MO" <|>
+  Tuesday   <$ chunk "TU" <|>
+  Wednesday <$ chunk "WE" <|>
+  Thursday  <$ chunk "TH" <|>
+  Friday    <$ chunk "FR" <|>
+  Saturday  <$ chunk "SA"
diff --git a/src/Data/Time/RRule/Types.hs b/src/Data/Time/RRule/Types.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/Time/RRule/Types.hs
@@ -0,0 +1,104 @@
+module Data.Time.RRule.Types
+  ( defaultRRule
+  , RRule(..)
+  , Day(..)
+  , Frequency(..)
+  , ToRRule(toRRule)
+  )
+where
+
+import Prelude hiding (until)
+import Data.List.NonEmpty (NonEmpty(..))
+import Data.Text (Text, intercalate, pack, unpack)
+import Data.Time.Format (formatTime, defaultTimeLocale)
+import Data.Time.Clock (UTCTime)
+
+class Show a => ToRRule a where
+  toRRule :: a -> Text
+  toRRule = pack . show
+
+instance ToRRule Int
+
+instance ToRRule a => ToRRule (NonEmpty a) where
+  toRRule (x :| xs) = intercalate "," $ toRRule x : map toRRule xs
+
+instance (Show a, Integral a, ToRRule b) => ToRRule (a, b) where
+  toRRule (a, b) = (if a == 0 then "" else pack $ show a) <> toRRule b
+
+instance ToRRule UTCTime where
+  toRRule = pack . formatTime defaultTimeLocale "%Y%m%dT%H%M%SZ"
+
+instance ToRRule a => ToRRule (Maybe a) where
+  toRRule Nothing = ""
+  toRRule (Just a) = toRRule a
+
+data Frequency
+  = Secondly
+  | Minutely
+  | Hourly
+  | Daily
+  | Weekly
+  | Monthly
+  | Yearly
+  deriving (Eq, Show)
+
+instance ToRRule Frequency where
+  toRRule = \case
+    Secondly -> "SECONDLY"
+    Minutely -> "MINUTELY"
+    Hourly   -> "HOURLY"
+    Daily    -> "DAILY"
+    Weekly   -> "WEEKLY"
+    Monthly  -> "MONTHLY"
+    Yearly   -> "YEARLY"
+
+data Day = Sunday | Monday | Tuesday | Wednesday | Thursday | Friday | Saturday
+  deriving (Eq, Show)
+
+instance ToRRule Day where
+  toRRule = \case
+    Sunday    -> "SU"
+    Monday    -> "MO"
+    Tuesday   -> "TU"
+    Wednesday -> "WE"
+    Thursday  -> "TH"
+    Friday    -> "FR"
+    Saturday  -> "SA"
+
+data RRule = RRule
+  { prefix     :: Bool                        -- ^ whether this rule has the "RRULE:" prefix
+  , weekStart  :: Maybe Day                   -- ^ starting day of the week
+  , frequency  :: Maybe Frequency             -- ^ how often to recur
+  , count      :: Maybe Int                   -- ^ how many times to recur
+  , until      :: Maybe UTCTime               -- ^ what UTCTime to stop recurring after
+  , interval   :: Maybe Int                   -- ^ number of units to wait before recurring
+  , bySecond   :: Maybe (NonEmpty Int)        -- ^ which second(s) to recur on
+  , byMinute   :: Maybe (NonEmpty Int)        -- ^ which minute(s) to recur on
+  , byHour     :: Maybe (NonEmpty Int)        -- ^ which hour(s) to recur on
+  , byDay      :: Maybe (NonEmpty (Int, Day)) -- ^ which days(s) to recur on
+  , byWeekNo   :: Maybe (NonEmpty Int)        -- ^ which week number(s) to recur on
+  , byMonth    :: Maybe (NonEmpty Int)        -- ^ which month(s) to recur on
+  , byMonthDay :: Maybe (NonEmpty Int)        -- ^ which day(s) of the month to recur on
+  , byYearDay  :: Maybe (NonEmpty Int)        -- ^ which day(s) of the year to recur on
+  , bySetPos   :: Maybe (NonEmpty Int)        -- ^ which occurrence of the rule inside the frequency period
+  }
+  deriving (Eq, Show)
+
+defaultRRule :: RRule
+defaultRRule = RRule
+  { prefix     = False
+  , weekStart  = Nothing
+  , frequency  = Nothing
+  , count      = Nothing
+  , until      = Nothing
+  , interval   = Nothing
+  , bySecond   = Nothing
+  , byMinute   = Nothing
+  , byHour     = Nothing
+  , byDay      = Nothing
+  , byWeekNo   = Nothing
+  , byMonth    = Nothing
+  , byMonthDay = Nothing
+  , byYearDay  = Nothing
+  , bySetPos   = Nothing
+  }
diff --git a/test/Spec.hs b/test/Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Spec.hs
@@ -0,0 +1,336 @@
+import Test.Hspec
+import Data.Text
+import Data.Time.RRule
+
+-- many of these test cases were taken from
+-- https://icalendar.org/iCalendar-RFC-5545/3-8-5-3-recurrence-rule.html
+
+roundTrip :: Text -> Expectation
+roundTrip rruleText = do
+  fromText <$> toText <$> fromText rruleText `shouldBe` fromText <$> Just rruleText
+
+main :: IO ()
+main = hspec $ do
+  describe "roundtrips" $ do
+    it "empty" $ do
+      roundTrip ""
+
+    it "empty with prefix" $ do
+      roundTrip "RRULE:"
+
+    it "daily" $ do
+      roundTrip "FREQ=DAILY"
+
+    it "daily with prefix" $ do
+      roundTrip "RRULE:FREQ=DAILY"
+
+    it "10 times daily" $ do
+      roundTrip "FREQ=DAILY;COUNT=10"
+
+    it "daily until 2050" $ do
+      roundTrip "FREQ=DAILY;UNTIL=20500101T000000Z"
+
+    it "first of every month" $ do
+      roundTrip "INTERVAL=1;FREQ=MONTHLY;BYMONTHDAY=1"
+
+    it "every other day" $ do
+      roundTrip "RRULE:FREQ=DAILY;INTERVAL=2"
+
+    it "every 10 days for 5 times" $ do
+      roundTrip "FREQ=DAILY;INTERVAL=10;COUNT=5"
+
+    it "every day in january for 3 years by days" $ do
+      roundTrip "FREQ=YEARLY;COUNT=3;BYMONTH=1;BYDAY=SU,MO,TU,WE,TH,FR,SA"
+
+    it "every day in january for 3 years" $ do
+      roundTrip "FREQ=DAILY;COUNT=3;BYMONTH=1"
+
+    it "weekly for 10 occurences" $ do
+      roundTrip "RRULE:FREQ=WEEKLY;COUNT=10"
+
+    it "weekly until December 24 1997" $ do
+      roundTrip "FREQ=WEEKLY;UNTIL=19971224T000000Z"
+
+    it "every other week starting sunday" $ do
+      roundTrip "RRULE:FREQ=WEEKLY;INTERVAL=2;WKST=SU"
+
+    it "every other week starting friday" $ do
+      roundTrip "RRULE:FREQ=WEEKLY;INTERVAL=2;WKST=FR"
+
+    it "weekly on tuesday and thursday for five weeks" $ do
+      roundTrip "RRULE:FREQ=WEEKLY;COUNT=10;WKST=SU;BYDAY=TU,TH"
+
+    it "every other week on MWF" $ do
+      roundTrip "FREQ=WEEKLY;INTERVAL=2;UNTIL=20251224T000000Z;WKST=SU;BYDAY=MO,WE,FR"
+
+    it "every other week on Tuesday and Thursday, for 8 occurrences" $ do
+      roundTrip "FREQ=WEEKLY;INTERVAL=2;COUNT=8;WKST=SU;BYDAY=TU,TH"
+
+    it "monthly on the first Friday for 10 occurrences" $ do
+      roundTrip "FREQ=MONTHLY;COUNT=10;BYDAY=1FR"
+
+    it "monthly on the first Friday until December 24, 1997" $ do
+      roundTrip "FREQ=MONTHLY;UNTIL=19971224T000000Z;BYDAY=1FR"
+
+    it "every other month on the first and last Sunday of the month for 10 occurrences" $ do
+      roundTrip "FREQ=MONTHLY;INTERVAL=2;COUNT=10;BYDAY=1SU,-1SU"
+
+    it "monthly on the second-to-last Monday of the month for 6 months" $ do
+      roundTrip "FREQ=MONTHLY;COUNT=6;BYDAY=-2MO"
+
+    it "monthly on the third-to-the-last day of the month, forever" $ do
+      roundTrip "FREQ=MONTHLY;BYMONTHDAY=-3"
+
+    it "monthly on the 2nd and 15th of the month for 10 occurrences" $ do
+      roundTrip "FREQ=MONTHLY;COUNT=10;BYMONTHDAY=2,15"
+
+    it "monthly on the first and last day of the month for 10 occurrences" $ do
+      roundTrip "FREQ=MONTHLY;COUNT=10;BYMONTHDAY=1,-1"
+
+    it "every 18 months on the 10th thru 15th of the month for 10 occurrences" $ do
+      roundTrip "FREQ=MONTHLY;INTERVAL=18;COUNT=10;BYMONTHDAY=10,11,12,13,14,15"
+
+    it "every Tuesday, every other month" $ do
+      roundTrip "FREQ=MONTHLY;INTERVAL=2;BYDAY=TU"
+
+    it "yearly in June and July for 10 occurrences" $ do
+      roundTrip "FREQ=YEARLY;COUNT=10;BYMONTH=6,7"
+
+    it "every other year on January, February, and March for 10 occurrences" $ do
+      roundTrip  "FREQ=YEARLY;INTERVAL=2;COUNT=10;BYMONTH=1,2,3"
+
+    it "every third year on the 1st, 100th, and 200th day for 10 occurrences" $ do
+      roundTrip "FREQ=YEARLY;INTERVAL=3;COUNT=10;BYYEARDAY=1,100,200"
+
+    it "every 20th Monday of the year, forever" $ do
+      roundTrip "FREQ=YEARLY;BYDAY=20MO"
+
+    it "Monday of week number 20 (where the default start of the week is Monday), forever" $ do
+      roundTrip "FREQ=YEARLY;BYWEEKNO=20;BYDAY=MO"
+
+    it "every Thursday in March, forever" $ do
+      roundTrip "FREQ=YEARLY;BYMONTH=3;BYDAY=TH"
+
+    it "every Thursday, but only during June, July, and August, forever" $ do
+      roundTrip "FREQ=YEARLY;BYDAY=TH;BYMONTH=6,7,8"
+
+    it "every Friday the 13th" $ do
+      roundTrip "FREQ=MONTHLY;BYDAY=FR;BYMONTHDAY=13"
+
+    it "first Saturday that follows the first Sunday of the month" $ do
+      roundTrip "FREQ=MONTHLY;BYDAY=SA;BYMONTHDAY=7,8,9,10,11,12,13"
+
+    it "Every 4 years, the first Tuesday after a Monday in November, forever (U.S. Presidential Election day)" $ do
+      roundTrip "FREQ=YEARLY;INTERVAL=4;BYMONTH=11;BYDAY=TU;BYMONTHDAY=2,3,4,5,6,7,8"
+
+    it "third instance into the month of one of Tuesday, Wednesday, or Thursday, for the next 3 months" $ do
+      roundTrip "FREQ=MONTHLY;COUNT=3;BYDAY=TU,WE,TH;BYSETPOS=3"
+
+    it "second-to-last weekday of the month" $ do
+      roundTrip "FREQ=MONTHLY;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=-2"
+
+    it "every 3 hours until 5:00 PM on a specific day" $ do
+      roundTrip "FREQ=HOURLY;INTERVAL=3;UNTIL=19970902T170000Z"
+
+    it "every 15 minutes for 6 occurrences" $ do
+      roundTrip "FREQ=MINUTELY;INTERVAL=15;COUNT=6"
+
+    it "every hour and a half for 4 occurrences" $ do
+      roundTrip "FREQ=MINUTELY;INTERVAL=90;COUNT=4"
+
+    it "every 20 minutes from 9:00 AM to 4:40 PM every day" $ do
+      roundTrip "FREQ=DAILY;BYHOUR=9,10,11,12,13,14,15,16;BYMINUTE=0,20,40"
+
+    it "weekstart Monday" $ do
+      roundTrip "FREQ=WEEKLY;INTERVAL=2;COUNT=4;BYDAY=TU,SU;WKST=MO"
+
+    it "weekstart Sunday" $ do
+      roundTrip "FREQ=WEEKLY;INTERVAL=2;COUNT=4;BYDAY=TU,SU;WKST=SU"
+
+    it "seconds" $ do
+      roundTrip "FREQ=SECONDLY;BYSECOND=-10,90"
+
+  describe "describes" $ do
+    it "empty" $ do
+      description <$> fromText "" `shouldBe`
+        Just ""
+
+    it "empty with prefix" $ do
+      description <$> fromText "RRULE:" `shouldBe`
+        Just ""
+
+    it "daily" $ do
+      description <$> fromText "FREQ=DAILY" `shouldBe`
+        Just "every day"
+
+    it "daily with prefix" $ do
+      description <$> fromText "RRULE:FREQ=DAILY" `shouldBe`
+        Just "every day"
+
+    it "10 times daily" $ do
+      description <$> fromText "FREQ=DAILY;COUNT=10" `shouldBe`
+        Just "every day for 10 occurrences"
+
+    it "daily until 2050" $ do
+      description <$> fromText "FREQ=DAILY;UNTIL=20500101T000000Z" `shouldBe`
+        Just "every day until January 01, 2050 at 00:00:00"
+
+    it "first of every month" $ do
+      description <$> fromText "INTERVAL=1;FREQ=MONTHLY;BYMONTHDAY=1" `shouldBe`
+        Just "every month on the 1st day of the month"
+
+    it "every other day" $ do
+      description <$> fromText "RRULE:FREQ=DAILY;INTERVAL=2" `shouldBe`
+        Just "every other day"
+
+    it "every 10 days for 5 times" $ do
+      description <$> fromText "FREQ=DAILY;INTERVAL=10;COUNT=5" `shouldBe`
+        Just "every 10th day for 5 occurrences"
+
+    it "every day in january for 3 years by days" $ do
+      description <$> fromText "FREQ=YEARLY;COUNT=3;BYMONTH=1;BYDAY=SU,MO,TU,WE,TH,FR,SA" `shouldBe`
+        Just "every year on Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, and Saturday in January for 3 occurrences"
+
+    it "every day in january for 3 years" $ do
+      description <$> fromText "FREQ=DAILY;COUNT=3;BYMONTH=1" `shouldBe`
+        Just "every day in January for 3 occurrences"
+
+    it "weekly for 10 occurences" $ do
+      description <$> fromText "RRULE:FREQ=WEEKLY;COUNT=10" `shouldBe`
+        Just "every week for 10 occurrences"
+
+    it "weekly until December 24 1997" $ do
+      description <$> fromText "FREQ=WEEKLY;UNTIL=19971224T000000Z" `shouldBe`
+        Just "every week until December 24, 1997 at 00:00:00"
+
+    it "every other week starting sunday" $ do
+      description <$> fromText "RRULE:FREQ=WEEKLY;INTERVAL=2;WKST=SU" `shouldBe`
+        Just "every other week with weeks starting on Sunday"
+
+    it "every other week starting friday" $ do
+      description <$> fromText "RRULE:FREQ=WEEKLY;INTERVAL=2;WKST=FR" `shouldBe`
+        Just "every other week with weeks starting on Friday"
+
+    it "weekly on tuesday and thursday for five weeks" $ do
+      description <$> fromText "RRULE:FREQ=WEEKLY;COUNT=10;WKST=SU;BYDAY=TU,TH" `shouldBe`
+        Just "every week on Tuesday and Thursday for 10 occurrences with weeks starting on Sunday"
+
+    it "every other week on MWF" $ do
+      description <$> fromText "FREQ=WEEKLY;INTERVAL=2;UNTIL=20251224T000000Z;WKST=SU;BYDAY=MO,WE,FR" `shouldBe`
+        Just "every other week on Monday, Wednesday, and Friday until December 24, 2025 at 00:00:00 with weeks starting on Sunday"
+
+    it "every other week on Tuesday and Thursday, for 8 occurrences" $ do
+      description <$> fromText "FREQ=WEEKLY;INTERVAL=2;COUNT=8;WKST=SU;BYDAY=TU,TH" `shouldBe`
+        Just "every other week on Tuesday and Thursday for 8 occurrences with weeks starting on Sunday"
+
+    it "monthly on the first Friday for 10 occurrences" $ do
+      description <$> fromText "FREQ=MONTHLY;COUNT=10;BYDAY=1FR" `shouldBe`
+        Just "every month on the 1st Friday for 10 occurrences"
+
+    it "monthly on the first Friday until December 24, 1997" $ do
+      description <$> fromText "FREQ=MONTHLY;UNTIL=19971224T000000Z;BYDAY=1FR" `shouldBe`
+        Just "every month on the 1st Friday until December 24, 1997 at 00:00:00"
+
+    it "every other month on the first and last Sunday of the month for 10 occurrences" $ do
+      description <$> fromText "FREQ=MONTHLY;INTERVAL=2;COUNT=10;BYDAY=1SU,-1SU" `shouldBe`
+        Just "every other month on the 1st Sunday and the last Sunday for 10 occurrences"
+
+    it "monthly on the second-to-last Monday of the month for 6 months" $ do
+      description <$> fromText "FREQ=MONTHLY;COUNT=6;BYDAY=-2MO" `shouldBe`
+        Just "every month on the 2nd from last Monday for 6 occurrences"
+
+    it "monthly on the third-to-the-last day of the month, forever" $ do
+      description <$> fromText "FREQ=MONTHLY;BYMONTHDAY=-3" `shouldBe`
+        Just "every month on the 3rd from last day of the month"
+
+    it "monthly on the 2nd and 15th of the month for 10 occurrences" $ do
+      description <$> fromText "FREQ=MONTHLY;COUNT=10;BYMONTHDAY=2,15" `shouldBe`
+        Just "every month on the 2nd and 15th day of the month for 10 occurrences"
+
+    it "monthly on the first and last day of the month for 10 occurrences" $ do
+      description <$> fromText "FREQ=MONTHLY;COUNT=10;BYMONTHDAY=1,-1" `shouldBe`
+        Just "every month on the 1st and last day of the month for 10 occurrences"
+
+    it "every 18 months on the 10th thru 15th of the month for 10 occurrences" $ do
+      description <$> fromText "FREQ=MONTHLY;INTERVAL=18;COUNT=10;BYMONTHDAY=10,11,12,13,14,15" `shouldBe`
+        Just "every 18th month on the 10th, 11th, 12th, 13th, 14th, and 15th day of the month for 10 occurrences"
+
+    it "every Tuesday, every other month" $ do
+      description <$> fromText "FREQ=MONTHLY;INTERVAL=2;BYDAY=TU" `shouldBe`
+        Just "every other month on Tuesday"
+
+    it "yearly in June and July for 10 occurrences" $ do
+      description <$> fromText "FREQ=YEARLY;COUNT=10;BYMONTH=6,7" `shouldBe`
+        Just "every year in June and July for 10 occurrences"
+
+    it "every other year on January, February, and March for 10 occurrences" $ do
+      description <$> fromText  "FREQ=YEARLY;INTERVAL=2;COUNT=10;BYMONTH=1,2,3" `shouldBe`
+        Just "every other year in January, February, and March for 10 occurrences"
+
+    it "every third year on the 1st, 100th, and 200th day for 10 occurrences" $ do
+      description <$> fromText "FREQ=YEARLY;INTERVAL=3;COUNT=10;BYYEARDAY=1,100,200" `shouldBe`
+        Just "every 3rd year on the 1st, 100th, and 200th day of the year for 10 occurrences"
+
+    it "every 20th Monday of the year, forever" $ do
+      description <$> fromText "FREQ=YEARLY;BYDAY=20MO" `shouldBe`
+        Just "every year on the 20th Monday"
+
+    it "Monday of week number 20 where the default start of the week is Monday, forever" $ do
+      description <$> fromText "FREQ=YEARLY;BYWEEKNO=20;BYDAY=MO" `shouldBe`
+        Just "every year on Monday on the 20th week of the year"
+
+    it "every Thursday in March, forever" $ do
+      description <$> fromText "FREQ=YEARLY;BYMONTH=3;BYDAY=TH" `shouldBe`
+        Just "every year on Thursday in March"
+
+    it "every Thursday, but only during June, July, and August, forever" $ do
+      description <$> fromText "FREQ=YEARLY;BYDAY=TH;BYMONTH=6,7,8" `shouldBe`
+        Just "every year on Thursday in June, July, and August"
+
+    it "every Friday the 13th" $ do
+      description <$> fromText "FREQ=MONTHLY;BYDAY=FR;BYMONTHDAY=13" `shouldBe`
+        Just "every month on Friday on the 13th day of the month"
+
+    it "first Saturday that follows the first Sunday of the month" $ do
+      description <$> fromText "FREQ=MONTHLY;BYDAY=SA;BYMONTHDAY=7,8,9,10,11,12,13" `shouldBe`
+        Just "every month on Saturday on the 7th, 8th, 9th, 10th, 11th, 12th, and 13th day of the month"
+
+    it "Every 4 years, the first Tuesday after a Monday in November, forever U.S. Presidential Election day" $ do
+      description <$> fromText "FREQ=YEARLY;INTERVAL=4;BYMONTH=11;BYDAY=TU;BYMONTHDAY=2,3,4,5,6,7,8" `shouldBe`
+        Just "every 4th year on Tuesday in November on the 2nd, 3rd, 4th, 5th, 6th, 7th, and 8th day of the month"
+
+    it "third instance into the month of one of Tuesday, Wednesday, or Thursday, for the next 3 months" $ do
+      description <$> fromText "FREQ=MONTHLY;COUNT=3;BYDAY=TU,WE,TH;BYSETPOS=3" `shouldBe`
+        Just "the 3rd instance of every month on Tuesday, Wednesday, and Thursday for 3 occurrences"
+
+    it "second-to-last weekday of the month" $ do
+      description <$> fromText "FREQ=MONTHLY;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=-2" `shouldBe`
+        Just "the 2nd from last instance of every month on Monday, Tuesday, Wednesday, Thursday, and Friday"
+
+    it "every 3 hours until 5:00 PM on a specific day" $ do
+      description <$> fromText "FREQ=HOURLY;INTERVAL=3;UNTIL=19970902T170000Z" `shouldBe`
+        Just "every 3rd hour until September 02, 1997 at 17:00:00"
+
+    it "every 15 minutes for 6 occurrences" $ do
+      description <$> fromText "FREQ=MINUTELY;INTERVAL=15;COUNT=6" `shouldBe`
+        Just "every 15th minute for 6 occurrences"
+
+    it "every hour and a half for 4 occurrences" $ do
+      description <$> fromText "FREQ=MINUTELY;INTERVAL=90;COUNT=4" `shouldBe`
+        Just "every 90th minute for 4 occurrences"
+
+    it "every 20 minutes from 9:00 AM to 4:40 PM every day" $ do
+      description <$> fromText "FREQ=DAILY;BYHOUR=9,10,11,12,13,14,15,16;BYMINUTE=0,20,40" `shouldBe`
+        Just "every day on the 0th, 20th, and 40th minute on the 9th, 10th, 11th, 12th, 13th, 14th, 15th, and 16th hour"
+
+    it "weekstart Monday" $ do
+      description <$> fromText "FREQ=WEEKLY;INTERVAL=2;COUNT=4;BYDAY=TU,SU;WKST=MO" `shouldBe`
+        Just "every other week on Tuesday and Sunday for 4 occurrences with weeks starting on Monday"
+
+    it "weekstart Sunday" $ do
+      description <$> fromText "FREQ=WEEKLY;INTERVAL=2;COUNT=4;BYDAY=TU,SU;WKST=SU" `shouldBe`
+        Just "every other week on Tuesday and Sunday for 4 occurrences with weeks starting on Sunday"
+
+    it "seconds" $ do
+      description <$> fromText "FREQ=SECONDLY;BYSECOND=-10,90" `shouldBe`
+        Just "every second on the 10th from last and 90th second"
