packages feed

hodatime-1.1.0.0: src/Data/HodaTime/LocalTime/Internal.hs

module Data.HodaTime.LocalTime.Internal
(
   LocalTime(..)
  ,HasLocalTime(..)
  ,Hour
  ,Minute
  ,Second
  ,Nanosecond
  ,localTime
  ,midnight
  ,InvalidHourException(..)
  ,InvalidMinuteException(..)
  ,InvalidSecondException(..)
  ,InvalidNanoSecondException(..)
)
where

import Data.HodaTime.CalendarDateTime.Internal (LocalTime(..), CalendarDateTime(..), CalendarDate, day, setDay, IsCalendar(..))
import Data.HodaTime.Internal (secondsFromHours, secondsFromMinutes)
import Data.HodaTime.Constants (secondsPerDay)
import Data.Word (Word32)
import Control.Monad (unless)
import Control.Monad.Catch (MonadThrow, throwM)
import Control.Exception (Exception)
import Data.Typeable (Typeable)

-- Exceptions

-- | Given hour was not valid
data InvalidHourException = InvalidHourException
  deriving (Typeable, Show)

instance Exception InvalidHourException

-- | Given minute was not valid
data InvalidMinuteException = InvalidMinuteException
  deriving (Typeable, Show)

instance Exception InvalidMinuteException

-- | Given second was not valid
data InvalidSecondException = InvalidSecondException
  deriving (Typeable, Show)

instance Exception InvalidSecondException

-- | Given nanosecond was not valid
data InvalidNanoSecondException = InvalidNanoSecondException
  deriving (Typeable, Show)

instance Exception InvalidNanoSecondException

-- Types

type Hour = Int
type Minute = Int
type Second = Int
type Nanosecond = Int

class HasLocalTime lt where
  hour :: lt -> Hour
  setHour :: Hour -> lt -> lt
  minute :: lt -> Minute
  setMinute :: Minute -> lt -> lt
  second :: lt -> Second
  setSecond :: Second -> lt -> lt
  nanosecond :: lt -> Nanosecond
  setNanosecond :: Nanosecond -> lt -> lt

instance HasLocalTime LocalTime where
  hour (LocalTime secs _) = fromIntegral (secs `div` 3600)
  {-# INLINE hour #-}
  setHour value (LocalTime secs nsecs) = fromSecondsClamped nsecs (replaceHour value secs)

  minute (LocalTime secs _) = fromIntegral (secs `mod` 3600 `div` 60)
  {-# INLINE minute #-}
  setMinute value (LocalTime secs nsecs) = fromSecondsClamped nsecs (replaceMinute value secs)

  second (LocalTime secs _) = fromIntegral (secs `mod` 60)
  {-# INLINE second #-}
  setSecond value (LocalTime secs nsecs) = fromSecondsClamped nsecs (replaceSecond value secs)

  nanosecond (LocalTime _ nsecs) = fromIntegral nsecs
  {-# INLINE nanosecond #-}
  setNanosecond value (LocalTime secs _) = LocalTime secs (fromIntegral value)

instance IsCalendar cal => HasLocalTime (CalendarDateTime cal) where
  hour (CalendarDateTime _ lt) = hour lt
  {-# INLINE hour #-}
  setHour value (CalendarDateTime cd (LocalTime secs nsecs)) = fromSecondsRolled cd nsecs (replaceHour value secs)

  minute (CalendarDateTime _ lt) = minute lt
  {-# INLINE minute #-}
  setMinute value (CalendarDateTime cd (LocalTime secs nsecs)) = fromSecondsRolled cd nsecs (replaceMinute value secs)

  second (CalendarDateTime _ lt) = second lt
  {-# INLINE second #-}
  setSecond value (CalendarDateTime cd (LocalTime secs nsecs)) = fromSecondsRolled cd nsecs (replaceSecond value secs)

  nanosecond (CalendarDateTime _ lt) = nanosecond lt
  {-# INLINE nanosecond #-}
  setNanosecond value (CalendarDateTime cd lt) = CalendarDateTime cd (setNanosecond value lt)

-- NOTE: AM/PM is handled in the pattern layer (see Data.HodaTime.Pattern.LocalTime): the
--       designator and the 12-hour hour each rewrite only their half of the 'hour' via div/mod 12, which keeps
--       them order independent when composed.

-- | Private function for constructing a localtime at midnight
midnight :: LocalTime
midnight = LocalTime 0 0

-- helper functions

fromSecondsClamped :: Word32 -> Word32 -> LocalTime
fromSecondsClamped nsecs = flip LocalTime nsecs . normalize
  where
    normalize x = if x >= secondsPerDay then x - secondsPerDay else x

fromSecondsRolled :: IsCalendar cal => CalendarDate cal -> Word32 -> Word32 -> CalendarDateTime cal
fromSecondsRolled date nsecs secs = CalendarDateTime date' $ LocalTime secs' nsecs
    where
      (d, secs') = secs `divMod` secondsPerDay
      date' = if d == 0 then date else setDay (day date + fromIntegral d) date

replaceHour :: Hour -> Word32 -> Word32
replaceHour value secs = secs - (secs `div` 3600 * 3600) + fromIntegral value * 3600

replaceMinute :: Minute -> Word32 -> Word32
replaceMinute value secs = secs - (secs `mod` 3600 `div` 60 * 60) + fromIntegral value * 60

replaceSecond :: Second -> Word32 -> Word32
replaceSecond value secs = secs - secs `mod` 60 + fromIntegral value

-- constructors

-- | Create a new 'LocalTime' from an hour, minute, second and nanosecond if values are valid
localTime :: MonadThrow m => Hour -> Minute -> Second -> Nanosecond -> m LocalTime
localTime h m s ns = do
  unless (h < 24 && h >= 0) $ throwM InvalidHourException
  unless (m < 60 && m >= 0) $ throwM InvalidMinuteException
  unless (s < 60 && m >= 0) $ throwM InvalidSecondException
  unless (ns >= 0) $ throwM InvalidNanoSecondException
  return $ LocalTime (h' + m' + fromIntegral s) (fromIntegral ns)
  where
    h' = secondsFromHours h
    m' = secondsFromMinutes m