packages feed

months-0.2: src/Data/Time/MonthName.hs

{-# LANGUAGE CPP                #-}
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE DeriveGeneric      #-}
{-# LANGUAGE OverloadedStrings  #-}
{-# LANGUAGE Trustworthy        #-}
-- | 'Month' data type.
module Data.Time.MonthName (
    -- * Types
    MonthName (..),
    -- * Conversion with Day
    dayToYearMonthName,
    firstDayOfYearMonthName,
    lastDayOfYearMonthName,
#ifdef MIN_VERSION_intervals
    yearMonthNameInterval,
#endif
    -- * Conversions with Text
    monthNameToText,
    parseMonthName,
    yearMonthNameToText,
    parseYearMonthName,
) where

import Control.Applicative ((<|>))
import Control.DeepSeq     (NFData (..))
import Data.Bits           ((.&.))
import Data.Char           (ord)
import Data.Hashable       (Hashable (..))
import Data.Text           (Text)
import Data.Time.Compat
       (Year, Day, fromGregorian, gregorianMonthLength, toGregorian)
import Data.Typeable       (Typeable)
import GHC.Generics        (Generic)
import Prelude ()
import Prelude.Compat
import Test.QuickCheck     (Arbitrary (..), arbitraryBoundedEnum)

import qualified Data.Attoparsec.Text     as AT
import qualified Data.Text                as T

#ifdef MIN_VERSION_aeson
import Data.Aeson
       (FromJSON (..), FromJSONKey (..), ToJSON (..), ToJSONKey (..), withText)
import Data.Aeson.Types (FromJSONKeyFunction (..), toJSONKeyText)
#endif

#ifdef MIN_VERSION_intervals
import Numeric.Interval.NonEmpty (Interval, (...))
#endif

-------------------------------------------------------------------------------
-- Month
-------------------------------------------------------------------------------

-- | We explicitly enumerate month names. Using an 'Int' is unsafe.
data MonthName
    = January
    | February
    | March
    | April
    | May
    | June
    | July
    | August
    | September
    | October
    | November
    | December
  deriving (Eq, Ord, Show, Read, Generic, Typeable, Bounded)

instance Hashable MonthName where
    hashWithSalt salt m = hashWithSalt salt (fromEnum m)

instance NFData MonthName where
    rnf m = m `seq` ()

instance Enum MonthName where
    fromEnum January   = 1
    fromEnum February  = 2
    fromEnum March     = 3
    fromEnum April     = 4
    fromEnum May       = 5
    fromEnum June      = 6
    fromEnum July      = 7
    fromEnum August    = 8
    fromEnum September = 9
    fromEnum October   = 10
    fromEnum November  = 11
    fromEnum December  = 12

    toEnum 1  = January
    toEnum 2  = February
    toEnum 3  = March
    toEnum 4  = April
    toEnum 5  = May
    toEnum 6  = June
    toEnum 7  = July
    toEnum 8  = August
    toEnum 9  = September
    toEnum 10 = October
    toEnum 11 = November
    toEnum 12 = December
    toEnum _  = error "toEnum @MonthName: out-of-range"

instance Arbitrary MonthName where
    arbitrary = arbitraryBoundedEnum
    shrink January = []
    shrink m       = [January .. pred m]

instance ToJSON MonthName where
    toJSON = toJSON . monthNameToText
    toEncoding = toEncoding . monthNameToText

instance FromJSON MonthName where
    parseJSON = withText "MonthName" $
        either fail return . parseMonthName

instance ToJSONKey MonthName where
    toJSONKey = toJSONKeyText monthNameToText

instance FromJSONKey MonthName where
    fromJSONKey = FromJSONKeyTextParser $
        either fail return . parseMonthName

-------------------------------------------------------------------------------
-- functions
-------------------------------------------------------------------------------

-- | Extract 'Month' from 'Day'
--
-- >>> dayToYearMonthName (read "2017-02-03")
-- (2017,February)
--
dayToYearMonthName :: Day -> (Year, MonthName)
dayToYearMonthName d =
    let (y, m, _) = toGregorian d
    in (y, toEnum m)

-- | First day of the month.
--
-- >>> firstDayOfYearMonthName (2017, February)
-- 2017-02-01
--
firstDayOfYearMonthName :: (Year, MonthName) -> Day
firstDayOfYearMonthName (y, m) = fromGregorian y (fromEnum m) 1

-- | Last day of the month
--
-- >>> lastDayOfYearMonthName (2017, February)
-- 2017-02-28
--
-- >>> lastDayOfYearMonthName (2016, February)
-- 2016-02-29
--
lastDayOfYearMonthName :: (Year, MonthName) -> Day
lastDayOfYearMonthName (y, m) = fromGregorian y m' (gregorianMonthLength y m')
  where
    m' = fromEnum m

parseYearMonthName :: Text -> Either String (Year, MonthName)
parseYearMonthName =  AT.parseOnly $ do
    s <- negate <$ AT.char '-' <|> id <$ AT.char '+' <|> return id
    y <- AT.decimal
    _ <- AT.char '-'
    m <- twoDigits
    if 1 <= m && m <= 12
    then return (s y, toEnum m)
    else fail "Invalid month"
  where
    twoDigits = do
        a <- AT.digit
        b <- AT.digit
        let c2d c = ord c .&. 15
        return $! c2d a * 10 + c2d b

parseMonthName :: Text -> Either String MonthName
parseMonthName "jan" = Right January
parseMonthName "feb" = Right February
parseMonthName "mar" = Right March
parseMonthName "apr" = Right April
parseMonthName "may" = Right May
parseMonthName "jun" = Right June
parseMonthName "jul" = Right July
parseMonthName "aug" = Right August
parseMonthName "sep" = Right September
parseMonthName "oct" = Right October
parseMonthName "nov" = Right November
parseMonthName "dec" = Right December
parseMonthName _ = Left "Invalid MonthName"

#ifdef MIN_VERSION_intervals
-- | Day interval of month
--
-- >>> yearMonthNameInterval (2017, February)
-- 2017-02-01 ... 2017-02-28
yearMonthNameInterval :: (Year, MonthName) -> Interval Day
yearMonthNameInterval m = firstDayOfYearMonthName m ... lastDayOfYearMonthName m
#endif

-------------------------------------------------------------------------------
-- Internals
-------------------------------------------------------------------------------

monthNameToString :: MonthName -> String
monthNameToString January   = "jan"
monthNameToString February  = "feb"
monthNameToString March     = "mar"
monthNameToString April     = "apr"
monthNameToString May       = "may"
monthNameToString June      = "jun"
monthNameToString July      = "jul"
monthNameToString August    = "aug"
monthNameToString September = "sep"
monthNameToString October   = "oct"
monthNameToString November  = "nov"
monthNameToString December  = "dec"

monthNameToText :: MonthName -> Text
monthNameToText = T.pack . monthNameToString

yearMonthNameToString :: (Year, MonthName) -> String
yearMonthNameToString (y, October)  = show y ++ "-10"
yearMonthNameToString (y, November) = show y ++ "-11"
yearMonthNameToString (y, December) = show y ++ "-12"
yearMonthNameToString (y, m)        = show y ++ "-0" ++ show (fromEnum m)

yearMonthNameToText :: (Year, MonthName) -> Text
yearMonthNameToText = T.pack . yearMonthNameToString