{-# LANGUAGE CPP #-}
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE MultiWayIf #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
module Text.Period
( Period, PeriodFmt(..), parsePeriod, parsePeriodMay, parsePeriodEither
, formatPeriod, collapsePeriod
)where
import Control.Applicative hiding ((<|>))
import Control.Arrow
import Control.Monad
import Data.Char
import Data.Monoid ((<>))
import qualified Data.Text as T
#if MIN_VERSION_time(1, 5, 0)
import Data.Time (defaultTimeLocale)
#else
import System.Locale (defaultTimeLocale)
#endif
import Data.Time.Calendar
import Data.Time (formatTime)
import Prelude
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
, perDateSep :: T.Text
}
data ParseState
= StateYear Integer
| StateMonth Integer Int
| StateDay Integer Int Int
| StateNone
number :: (Read 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 ())
parsePeriod :: T.Text -> Period
parsePeriod = either (error . show) id . parse period "parsePeriod"
parsePeriodMay :: T.Text -> Maybe Period
parsePeriodMay = either (const Nothing) Just . parse period ""
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)
collapsePeriod :: PeriodFmt -> Period -> T.Text
collapsePeriod (PeriodFmt fieldSep sep) (start, end) = if
| start == end -> format yyyymmdd start
| all1 [m1, d1, m2', d2'] -> if
| y1 == y2 -> showt y1
| otherwise -> showt y1 <> sep <> showt y2
| all1 [d1, d2'] -> if
| y1 == y2, m1 == m2 -> format yyyymm start
| y1 == y2, m1 `mod` 3 == 1, m2 - m1 == 2
-> showt y1 <> "Q" <> showt ((m1 - 1) `div` 3 + 1)
| otherwise
-> format yyyymm start <> sep <> if y1 == y2 then showt0 m2 else format yyyymm end
| otherwise -> format yyyymmdd start <> sep <> format yyyymmdd end
where
all1 = all (== 1)
format f = T.pack . formatTime defaultTimeLocale f
showt0 n = let t = showt n in if T.length t == 1 then "0" <> t else t
(y1, m1, d1) = toGregorian start
(y2, m2, _d2) = toGregorian end
(_y2', m2', d2') = toGregorian (succ end)
yyyymm = T.unpack $ "%Y" <> fieldSep <> "%m"
yyyymmdd = T.unpack $ "%Y" <> fieldSep <> "%m" <> fieldSep <> "%d"
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"