time-machine-0.1.0: src/Control/Monad/TimeMachine/Engine.hs
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE StandaloneDeriving #-}
module Control.Monad.TimeMachine.Engine (
-- * MonadTime Class
MonadTime(..)
, getTimeZone
, getCurrentTimeZone
, utcToLocalZonedTime
, getZonedTime
, loadLocalTZ
-- * Functions
, departFor
, travelTo
, backTo
, jumpTo
, accelerate
, halt
-- * Configurations
, TimeScale(..)
, Destination(..)
, TimeInterval(..)
, TimeZoneName
, Acceleration(..)
-- * Monad Transformer
, TimeMachineT
) where
import Control.Exception ( IOException, catch )
import Control.Monad.Reader ( ReaderT, ask, runReaderT )
import Control.Monad.Trans ( MonadIO, liftIO, MonadTrans )
import Data.Maybe ( fromMaybe )
import qualified Data.Time as T
import qualified Data.Time.Zones as TZ
-- | A data type to represents the speed of time.
-- It corresponds how many seconds are in the real second,
-- i.e. @TimeScale 1@ is equivalent to the real speed of time.
newtype TimeScale = TimeScale { unTimeScale :: T.NominalDiffTime }
deriving ( Eq, Show, Ord, Num )
-- | A class of monads in which you can obrain the mocked current time
-- and relevant information.
class (Monad m) => MonadTime m where
getCurrentTime :: m T.UTCTime -- ^ foo
getCurrentTZ :: m TZ.TZ
getCurrentTimeScale :: m TimeScale
-- | Returns the mocked time zone at the given point of time.
getTimeZone :: (MonadTime m) => T.UTCTime -> m T.TimeZone
getTimeZone ut = do
tz <- getCurrentTZ
return $ TZ.timeZoneForUTCTime tz ut
-- | Returns the mocked time zone at the mocked current time.
getCurrentTimeZone :: (MonadTime m) => m T.TimeZone
getCurrentTimeZone = getCurrentTime >>= getTimeZone
-- | Returns the mocked local time at the given point of time.
utcToLocalZonedTime :: (MonadTime m) => T.UTCTime -> m T.ZonedTime
utcToLocalZonedTime ut = do
tz <- getCurrentTZ
zone <- getTimeZone ut
return $ T.ZonedTime (TZ.utcToLocalTimeTZ tz ut) zone
-- | Returns the mocked local time at the mocked current time.
getZonedTime :: (MonadTime m) => m T.ZonedTime
getZonedTime = getCurrentTime >>= utcToLocalZonedTime
-- | An alias of 'getCurrentTZ'.
loadLocalTZ :: (MonadTime m) => m TZ.TZ
loadLocalTZ = getCurrentTZ
instance MonadTime IO where
getCurrentTime = T.getCurrentTime
getCurrentTZ = TZ.loadLocalTZ
getCurrentTimeScale = return $ TimeScale 1
data Spacetime = Spacetime {
stSimulatedOrigin :: T.UTCTime
, stRealOrigin :: T.UTCTime
, stTZ :: TZ.TZ
, stTimeScale :: TimeScale
} deriving ( Eq, Show )
-- | A monad transformer to stack the 'MonadTime' contexts.
newtype TimeMachineT m a =
TimeMachineT { timeMachineT :: ReaderT Spacetime m a }
deriving instance (Functor m) => Functor (TimeMachineT m)
deriving instance (Applicative m) => Applicative (TimeMachineT m)
deriving instance (Monad m) => Monad (TimeMachineT m)
deriving instance (MonadIO m) => MonadIO (TimeMachineT m)
deriving instance MonadTrans TimeMachineT
instance (MonadIO m) => MonadTime (TimeMachineT m) where
getCurrentTime = TimeMachineT $ do
realCurr <- liftIO T.getCurrentTime
Spacetime simOrigin realOrigin _ scale <- ask
let diff = scaledDiffUTCTime scale realCurr realOrigin
return $ T.addUTCTime diff simOrigin
getCurrentTZ = TimeMachineT $ ask >>= return . stTZ
getCurrentTimeScale = TimeMachineT $ ask >>= return . stTimeScale
scaledDiffUTCTime :: TimeScale -> T.UTCTime -> T.UTCTime -> T.NominalDiffTime
scaledDiffUTCTime scale t1 t0 = (unTimeScale scale) * T.diffUTCTime t1 t0
runTimeMachineT :: TimeMachineT m a -> Spacetime -> m a
runTimeMachineT = runReaderT . timeMachineT
-- | A data type to represent a point of time for mocking.
data Destination =
None -- ^ Nothing to mock.
| Absolute T.UTCTime -- ^ An absolute point in UTC.
| Zoned T.LocalTime -- ^ A local time in the mocked current time zone.
| Relative TimeInterval -- ^ An interval from the mocked current time.
deriving ( Eq, Show )
-- | A data type to represent intervals for constructing a 'Destination'.
data TimeInterval =
Minutes Integer
| Hours Integer
| Days Integer
| Weeks Integer
| Months Integer
| Years Integer
deriving ( Show )
instance Eq TimeInterval where
x == y = case (normarizeToMinutes x, normarizeToMinutes y) of
(Minutes n1, Minutes n2) -> n1 == n2
(Months n1, Months n2) -> n1 == n2
(Years n1, Years n2) -> n1 == n2
(_ , _ ) -> False
normarizeToMinutes :: TimeInterval -> TimeInterval
normarizeToMinutes (Hours n) = Minutes $ n * 60
normarizeToMinutes (Days n) = Minutes $ n * 60 * 24
normarizeToMinutes (Weeks n) = Minutes $ n * 60 * 24 * 7
normarizeToMinutes ti = ti
-- | A data type to represent how to change the mocked speed of time.
data Acceleration =
Keep -- ^ Nothing to change.
| Velocity TimeScale -- ^ Sets the speed to the given scale.
| Factor TimeScale -- ^ Sets the speed acccording to the current speed.
deriving ( Eq, Show )
-- | Names of time zones, e.g. @"Asia/Tokyo"@ or @"Europe/Paris"@.
type TimeZoneName = String
-- | Switches the 'MonadTime' contexts.
-- You can specify all of the point of time, the time zone and
-- the setting of speed for mocking at once.
departFor :: (MonadIO m, MonadTime m)
=> Destination -> TimeZoneName -> Acceleration
-> TimeMachineT m a -> m a
departFor dest zoneName acc act = do
realCurr <- liftIO T.getCurrentTime
simCurr <- getCurrentTime
tz <- getCurrentTZ
scale <- getCurrentTimeScale
mTZ <- liftIO $ safeLoadTZFromDB zoneName
let newTZ = fromMaybe tz mTZ
newSim = calcSimulatedOrigin dest newTZ simCurr
newScale = calcTimeScale acc scale
newST = Spacetime newSim realCurr newTZ newScale
runTimeMachineT act newST
safeLoadTZFromDB :: TimeZoneName -> IO (Maybe TZ.TZ)
safeLoadTZFromDB zoneName =
(do
tz <- liftIO $ TZ.loadTZFromDB zoneName
return $ Just tz
) `catch` (\(e :: IOException) -> do
return Nothing
)
calcSimulatedOrigin :: Destination -> TZ.TZ -> T.UTCTime -> T.UTCTime
calcSimulatedOrigin None _ t0 = t0
calcSimulatedOrigin (Absolute t) _ _ = t
calcSimulatedOrigin (Zoned lt) tz _ = TZ.localTimeToUTCTZ tz lt
calcSimulatedOrigin (Relative ti) tz t0 = addTimeInterval ti tz t0
addTimeInterval :: TimeInterval -> TZ.TZ -> T.UTCTime -> T.UTCTime
addTimeInterval (Minutes n) _ = T.addUTCTime $ fromIntegral (60 * n)
addTimeInterval (Hours n) _ = T.addUTCTime $ fromIntegral (3600 * n)
addTimeInterval (Days n) _ = T.addUTCTime $ fromIntegral (86400 * n)
addTimeInterval (Weeks n) _ = T.addUTCTime $ fromIntegral (604800 * n)
addTimeInterval (Months n) tz = calcTargetDay (T.addGregorianMonthsClip n) tz
addTimeInterval (Years n) tz = calcTargetDay (T.addGregorianYearsClip n) tz
calcTargetDay :: (T.Day -> T.Day) -> TZ.TZ -> T.UTCTime -> T.UTCTime
calcTargetDay f tz t0 =
let T.LocalTime d tod = TZ.utcToLocalTimeTZ tz t0
in TZ.localTimeToUTCTZ tz $ T.LocalTime (f d) tod
calcTimeScale :: Acceleration -> TimeScale -> TimeScale
calcTimeScale Keep s0 = s0
calcTimeScale (Velocity v) s0 = v
calcTimeScale (Factor f) s0 = f * s0
-- | Switches the mocked current time in the context.
travelTo :: (MonadIO m, MonadTime m) => Destination -> TimeMachineT m a -> m a
travelTo dest = departFor dest "" Keep
-- | An alias of 'travelTo'.
backTo :: (MonadIO m, MonadTime m) => Destination -> TimeMachineT m a -> m a
backTo = travelTo
-- | Switches the mocked current time zone in the context.
jumpTo :: (MonadIO m, MonadTime m) => TimeZoneName -> TimeMachineT m a -> m a
jumpTo zoneName = departFor None zoneName Keep
-- | Changes the mocked speed of time in the context.
accelerate :: (MonadIO m, MonadTime m)
=> Acceleration -> TimeMachineT m a -> m a
accelerate acc = departFor None "" acc
-- | Stops the time to advence in the context.
halt :: (MonadIO m, MonadTime m) => TimeMachineT m a -> m a
halt = accelerate (Velocity 0)