diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,30 @@
+Copyright (c) 2015, Alexey Karakulov
+
+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 Alexey Karakulov 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/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/exec/period.hs b/exec/period.hs
new file mode 100644
--- /dev/null
+++ b/exec/period.hs
@@ -0,0 +1,43 @@
+{-# LANGUAGE OverloadedStrings #-}
+module Main where
+
+import Text.Period
+import Options.Applicative
+import qualified Data.Text as T
+import qualified Data.Text.IO as T
+
+main :: IO ()
+main = execParser (info (helper <*> optParser) $ progDesc "period") >>= runCommand
+
+data Command
+  = Collapse PeriodFmt T.Text
+  | Expand PeriodFmt T.Text
+
+periodArg :: Parser T.Text
+periodArg = argument (T.pack <$> str) (metavar "PERIOD")
+
+parseFmt :: Parser PeriodFmt
+parseFmt = PeriodFmt <$>
+  option (T.pack <$> str) (
+    short 'f' <> long "field-sep" <> value "-" <>
+    help "Field separator, by default '-' as in 'yyyy-mm-dd'") <*>
+  option (T.pack <$> str) (
+    short 'd' <> long "date-sep" <> value "," <>
+    help "Field separator, by default ',' as in 'yyyy-mm-dd,yyyy-mm-dd'")
+
+
+mkCommand :: String -> String -> Parser Command -> Mod CommandFields Command
+mkCommand cmd desc parser =
+  command cmd $ info (helper <*> parser) (progDesc desc)
+
+optParser :: Parser Command
+optParser = subparser $ mconcat $
+  [ mkCommand "collapse" "Find shortest representation for time period" $
+    Collapse <$> parseFmt <*> periodArg
+  , mkCommand "expand" "Display start and end points of time period" $
+    Expand <$> parseFmt <*> periodArg
+  ]
+
+runCommand :: Command -> IO ()
+runCommand (Collapse fmt per) = T.putStrLn $ collapsePeriod fmt $ parsePeriod per
+runCommand (Expand fmt per) = T.putStrLn $ formatPeriod fmt $ parsePeriod per
diff --git a/period.cabal b/period.cabal
new file mode 100644
--- /dev/null
+++ b/period.cabal
@@ -0,0 +1,54 @@
+name:                period
+version:             0.1.0.0
+synopsis:            Parse and format date periods, collapse and expand their text representations.
+-- description:         
+homepage:            https://github.com/w3rs/period
+license:             BSD3
+license-file:        LICENSE
+author:              Alexey Karakulov
+maintainer:          ankarakulov@gmail.com
+-- copyright:           
+category:            Text
+build-type:          Simple
+-- extra-source-files:  
+cabal-version:       >=1.10
+
+library
+  exposed-modules:     Text.Period
+  build-depends:
+                base >=4.6 && < 5,
+                time >=1.4,
+                text >= 1.2,
+                text-show >= 2,
+                parsec >= 3.1
+  hs-source-dirs:      src
+  default-language:    Haskell2010
+  GHC-options: -Wall
+
+test-suite test
+  main-is:             Spec.hs
+  type:                exitcode-stdio-1.0
+  hs-source-dirs:      test
+  default-language:    Haskell2010
+  build-depends:
+                base,
+                period,
+                text,
+                hspec,
+                HUnit,
+                time
+
+executable period
+  main-is: period.hs
+  hs-source-dirs: exec/
+  default-language: Haskell2010
+  build-depends:
+                period,
+                base,
+                text,
+                optparse-applicative
+  GHC-options: -Wall -O2
+
+source-repository head
+  type: git
+  location: git://github.com/w3rs/period.git
diff --git a/src/Text/Period.hs b/src/Text/Period.hs
new file mode 100644
--- /dev/null
+++ b/src/Text/Period.hs
@@ -0,0 +1,159 @@
+{-# LANGUAGE LambdaCase   #-}
+{-# LANGUAGE MultiWayIf   #-}
+{-# LANGUAGE ViewPatterns #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE ConstraintKinds #-}
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE OverloadedStrings #-}
+
+module Text.Period
+ ( Period, PeriodFmt(..), parsePeriod, parsePeriodMay, parsePeriodEither
+ , formatPeriod, collapsePeriod
+ )where
+
+import           Control.Arrow
+import           Control.Monad
+import           Data.Char
+import           Data.Monoid ((<>))
+import qualified Data.Text as T
+import           Data.Time
+import           TextShow (showt)
+
+import           Text.Parsec.Char
+import           Text.Parsec.Combinator
+import           Text.Parsec.Prim
+import           Text.Parsec.Text
+
+
+type Period = (Day, Day)
+
+data PeriodFmt = PeriodFmt
+  { perFieldSep :: T.Text -- ^ Separator between year, month and day
+  , perDateSep :: T.Text -- ^ Separator between dates in the range, e.g. comma
+                         -- in @yyyy-mm-dd,yyyy-mm-dd@
+  }
+
+data ParseState
+  = StateYear Integer
+  | StateMonth Integer Int
+  | StateDay Integer Int Int
+  | StateNone
+
+number :: (Read a, Integral a) => Int -> Parser a
+number n = read <$> count n digit
+
+skipFieldSep :: Stream s m Char => Bool -> T.Text -> ParsecT s u m ()
+skipFieldSep b sep = when b (string (T.unpack sep)>> return ())
+
+-- | Parse period from text representation, which can be either simple date
+-- range (@yyyy-mm-dd,yyyy-mm-dd@) or something shorter like @yyyy,yyyy@, or even
+-- something like @yyyy-01-23,03-21@ which corresponds to
+-- @yyyy-01-23,yyyy-03-21@. The list of possible combinations can be found in
+-- @test/Spec.hs@.
+--
+-- 'parsePeriod' understands dash as field separator (like @yyyy-mm-dd@), or
+-- accepts short date format (@yyyymmdd@). It
+-- understands comma and underscore as separators between
+-- dashed dates (@yyyy-mm-dd@) and dash as separator between short dates (@yyyymmdd@).
+--
+-- 'parsePeriod' produces error on unparsable input.
+parsePeriod :: T.Text -> Period
+parsePeriod = either (error . show) id . parse period "parsePeriod"
+
+-- | Safe analogue of 'parsePeriod' using Maybe
+parsePeriodMay :: T.Text -> Maybe Period
+parsePeriodMay = either (const Nothing) Just . parse period ""
+
+-- | Safe analogue of 'parsePeriod' using Either
+parsePeriodEither :: T.Text -> Either String Period
+parsePeriodEither = either (Left . show) Right . parse period "parsePeriod"
+
+period :: Parser Period
+period =
+  try (rangePeriod '-' "") <|>
+  try (rangePeriod '_' "-") <|>
+  try (rangePeriod ',' "-") <|>
+  try (primPeriod "" <* eof) <|>
+  try (primPeriod "-" <* eof) <|>
+  (quarter <* eof)
+
+rangePeriod :: Char -> T.Text -> Parser Period
+rangePeriod sep fmt = do
+  s1 <- prim StateNone True
+  _ <- char sep
+  s2 <- foldr (<|>) (prim StateNone True <* eof) $
+    [try (prim s False <* eof) | s <- states s1]
+  return (startDay s1, endDay s2)
+  where
+  prim = primPeriod' fmt
+  states (StateMonth y _) = [StateYear y]
+  states (StateDay y m _) = [StateMonth y m, StateYear y]
+  states _ = []
+
+startDay :: ParseState -> Day
+startDay StateNone = error "startDay StateNone"
+startDay (StateYear y) = fromGregorian y 1 1
+startDay (StateMonth y m) = fromGregorian y m 1
+startDay (StateDay y m d) = fromGregorian y m d
+
+endDay :: ParseState -> Day
+endDay StateNone = error "endDay StateNone"
+endDay (StateYear y) = fromGregorian y 12 31
+endDay (StateMonth y m) = fromGregorian y m 31
+endDay (StateDay y m d) = fromGregorian y m d
+
+primPeriod :: T.Text -> Parser Period
+primPeriod fmt = (startDay &&& endDay) <$> primPeriod' fmt StateNone True
+
+primPeriod' :: T.Text -> ParseState -> Bool -> Parser ParseState
+primPeriod' fmt StateNone _ = do
+  s <- StateYear <$> number 4
+  primPeriod' fmt s True <|> return s
+primPeriod' fmt (StateYear y) skip = do
+  skipFieldSep skip fmt
+  s <- StateMonth y <$> number 2
+  primPeriod' fmt s True <|> return s
+primPeriod' fmt (StateMonth y m) skip = do
+  skipFieldSep skip fmt
+  StateDay y m <$> number 2
+primPeriod' _ (StateDay _ _ _) _ = unexpected "primPeriod': StateDay"
+
+quarter :: Parser Period
+quarter = do
+  y <- number 4
+  _ <- char 'Q'
+  q <- digitToInt <$> digit
+  return (fromGregorian y (q * 3 - 2) 1, fromGregorian y (q * 3) 31)
+
+-- | Format a period in the shortest fashion, e.g. collapse @yyyy-01-01,yyyy-01-31@
+-- to @yyyy-01@
+collapsePeriod :: PeriodFmt -> Period -> T.Text
+collapsePeriod (PeriodFmt fieldSep sep) (start, end) = if
+  | m1 == 1, d1 == 1, m2 == 12, d2 == 31 -> if
+      | y1 == y2 -> showt y1
+      | otherwise -> showt y1 <> sep <> showt y2
+  | d1 == 1, end == monthEnd start -> format yyyymm start
+  | d1 == 1, end == quarterEnd start -> showt y1 <> "Q" <> showt q
+  | start == end -> format yyyymmdd start
+  | otherwise -> format yyyymmdd start <> sep <> format yyyymmdd end
+  where
+  format f = T.pack . formatTime defaultTimeLocale f
+  (y1, m1, d1) = toGregorian start
+  (y2, m2, d2) = toGregorian end
+  q = (m1 - 1) `div` 3 + 1
+  yyyymm = T.unpack $ "%Y" <> fieldSep <> "%m"
+  yyyymmdd = T.unpack $ "%Y" <> fieldSep <> "%m" <> fieldSep <> "%d"
+
+-- | Format a period as a simple date range, e.g. @yyyy-mm-dd,yyyy-mm-dd@
+formatPeriod :: PeriodFmt -> Period -> T.Text
+formatPeriod (PeriodFmt fieldSep sep) (start, end) =
+  format start <> sep <> format end
+  where
+  format = T.pack . formatTime defaultTimeLocale yyyymmdd
+  yyyymmdd = T.unpack $ "%Y" <> fieldSep <> "%m" <> fieldSep <> "%d"
+
+monthEnd :: Day -> Day
+monthEnd = pred . addGregorianMonthsClip 1
+
+quarterEnd :: Day -> Day
+quarterEnd =  pred . addGregorianMonthsClip 3
diff --git a/test/Spec.hs b/test/Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Spec.hs
@@ -0,0 +1,46 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module Main where
+
+import Data.Time
+import Test.Hspec
+import Test.Hspec.QuickCheck
+import Test.HUnit ((@?=))
+import qualified Data.Text as T
+import Text.Period
+
+main :: IO ()
+main = hspec $ do
+    describe "Primitive" $ do
+      parse "2015" (2015,01,01) (2015,12,31)
+      parse "201501" (2015,01,01) (2015,01,31)
+      parse "20150131" (2015,01,31) (2015,01,31)
+      parse "2015-01" (2015,01,01) (2015,01,31)
+      parse "2015-01-31" (2015,01,31) (2015,01,31)
+    describe "Quarters" $ do
+      parse "2015Q1" (2015,01,01) (2015,03,31)
+      parse "2015Q2" (2015,04,01) (2015,06,31)
+      parse "2015Q3" (2015,07,01) (2015,09,31)
+      parse "2015Q4" (2015,10,01) (2015,12,31)
+    describe "Simple ranges" $ do
+      parse "2014_2015" (2014,01,01) (2015,12,31)
+      parse "2014-2015" (2014,01,01) (2015,12,31)
+      parse "201406-201502" (2014,06,01) (2015,02,28)
+      parse "2014-06_2015-02" (2014,06,01) (2015,02,28)
+      parse "20140601-20150314" (2014,06,01) (2015,03,14)
+      parse "2014-06-01_2015-03-14" (2014,06,01) (2015,03,14)
+    describe "Ranges within same year" $ do
+      parse "2015-02_05" (2015,02,01) (2015,05,31)
+      parse "2015-02-22_05-11" (2015,02,22) (2015,05,11)
+    describe "Ranges within same month" $ do
+      parse "2015-01-22_31" (2015,01,22) (2015,01,31)
+    describe "Unparsable" $ do
+      failOn "201501_201502"
+      failOn "20140601_20150314"
+
+  where
+  parse per (y1,m1,d1) (y2,m2,d2) = specify per $
+    parsePeriod (T.pack per) @?=(fromGregorian y1 m1 d1, fromGregorian y2 m2 d2)
+  failOn per = specify per $ parsePeriodMay (T.pack per) @?= Nothing
+
+
