module Data.Time.Lens ( -- * Time -- $time HasTime(..) , hours , minutes , seconds -- * Date -- $date , HasDate(..) , year , month , day , gregorian -- * Time zone -- $zone , HasTimeZone(..) -- * Re-exports from "Data.Time" , T.Day , T.TimeOfDay , T.LocalTime , T.ZonedTime , T.getZonedTime -- * Re-exports from "Data.Lens.Light" , Lens , getL , modL , setL ) where import Control.Category import Prelude hiding ((.), id) import Data.Lens.Light import Data.Fixed import qualified Data.Time as T import Data.Time (TimeOfDay(..), LocalTime(..), fromGregorian) -- $time -- The semantics of 'getL' for time lenses ('time','hours','minutes','seconds') -- is straightforward. -- -- The semantics of 'setL' is to «normalize» the time before setting. Hence -- @'modL' 'minutes' (+5)@ will correctly add 5 minutes to the time, e.g. -- -- >>> modL minutes (+5) (TimeOfDay 16 57 13) -- 17:02:13 -- -- If this means crossing a day boundary, the semantics varies for different -- structures. For structures that have a date component (i.e. for instances of -- 'HasDate') the date is adjusted appropriately. -- -- >>> modL hours (+10) (LocalTime (fromGregorian 2012 05 23) (TimeOfDay 16 57 13)) -- 2012-05-24 02:57:13 -- >>> modL seconds (subtract 1) (LocalTime (fromGregorian 2012 05 23) (TimeOfDay 0 0 0)) -- 2012-05-22 23:59:59 -- -- If there's no date, the time is simply wrapped around. -- -- >>> modL seconds (subtract 1) (TimeOfDay 0 0 0) -- 23:59:59 class HasTime a where time :: Lens a T.TimeOfDay hours :: HasTime a => Lens a Int hours = (lens T.todHour $ \x t -> t { T.todHour = x }) . time minutes :: HasTime a => Lens a Int minutes = (lens T.todMin $ \x t -> t { T.todMin = x }) . time seconds :: HasTime a => Lens a Pico seconds = (lens T.todSec $ \x t -> t { T.todSec = x }) . time instance HasTime T.TimeOfDay where time = ntime $ iso id id localTimeOfZonedTime :: Lens T.ZonedTime T.LocalTime localTimeOfZonedTime = lens T.zonedTimeToLocalTime $ \x t -> t { T.zonedTimeToLocalTime = x } instance HasTime T.LocalTime where time = ntimeAdjustDay $ lens T.localTimeOfDay $ \x t -> t { T.localTimeOfDay = x } instance HasTime T.ZonedTime where time = time . localTimeOfZonedTime instance HasTime T.UTCTime where time = time . iso (T.utcToLocalTime T.utc) (T.localTimeToUTC T.utc) -- $date -- In contrast to 'time', the 'date' lens is a simple accessor (it doesn't make -- sense to «normalize» a 'T.Day'). -- -- Instead, setters for 'year', 'month' and 'day' have special semantics -- described below. -- Getters are always straightforward. class HasDate a where date :: Lens a T.Day -- | The semantics of 'gregorian' corresponds to that of 'T.toGregorian' and -- 'T.fromGregorian' gregorian :: HasDate a => Lens a (Integer,Int,Int) gregorian = iso T.toGregorian (uncurry3 T.fromGregorian) . date -- | @'modL' 'year' (+n)@ adds @n@ years, matching month and day, with Feb 29th -- rolled over to Mar 1st if necessary (like 'T.addGregorianYearsRollOver') year :: HasDate a => Lens a Integer year = lens getYear setYear . date where getYear date = case T.toGregorian date of (year,_,_) -> year setYear year date = case T.toGregorian date of (origYear,_,_) -> T.addGregorianYearsRollOver (fromIntegral $ year - origYear) date -- | @'modL' 'month' (+n)@ adds @n@ months, with days past the last day of the -- month rolling over to the next month (like 'T.addGregorianMonthsRollOver') month :: HasDate a => Lens a Int month = lens getMonth setMonth . date where getMonth date = case T.toGregorian date of (_,month,_) -> month setMonth month date = case T.toGregorian date of (_,origMonth,_) -> T.addGregorianMonthsRollOver (fromIntegral $ month - origMonth) date -- | @'modL' 'day' (+n)@ computes the date @n@ days after the original date -- (like 'T.addDays') day :: HasDate a => Lens a Int day = lens getDay setDay . date where getDay date = case T.toGregorian date of (_,_,day) -> day setDay day date = case T.toGregorian date of (_,_,origDay) -> T.addDays (fromIntegral $ day - origDay) date uncurry3 :: (a -> b -> c -> d) -> (a, b, c) -> d uncurry3 f (a, b, c) = f a b c instance HasDate T.Day where date = iso id id instance HasDate T.LocalTime where date = lens T.localDay (\d s -> s { T.localDay = d }) instance HasDate T.ZonedTime where date = date . localTimeOfZonedTime instance HasDate T.UTCTime where date = date . iso (T.utcToLocalTime T.utc) (T.localTimeToUTC T.utc) -- $zone -- Getting 'timeZone' is straightforward. Setting 'TimeZone' changes both -- 'timeZone' and 'time' (and 'date', if present) in such a way that the new -- zoned time corresponds to the same UTC time as the original zoned time. class HasTime a => HasTimeZone a where timeZone :: Lens a T.TimeZone instance HasTimeZone T.ZonedTime where timeZone = lens T.zonedTimeZone setTimeZone where setTimeZone newz (T.ZonedTime oldt oldz) = T.ZonedTime newt newz where newt = modL minutes (+ tdiff) oldt tdiff = T.timeZoneMinutes newz - T.timeZoneMinutes oldz -- -- Auxiliary functions -- normalizeTime :: T.TimeOfDay -> (T.TimeOfDay, Integer) normalizeTime = timeToTimeOfDay . timeOfDayToTime -- Can't rely on a HasTime instance here because this function will be used to -- define one ntime :: Lens a T.TimeOfDay -> Lens a T.TimeOfDay ntime time = iso id (fst . normalizeTime) . time ntimeAdjustDay :: (HasDate a) => Lens a T.TimeOfDay -> Lens a T.TimeOfDay ntimeAdjustDay time = lens (getL time) $ \t -> case normalizeTime t of (t', days) -> setL time t' . modL date (T.addDays days) -- We don't use T.timeToTimeOfDay and T.timeOfDayToTime here for the following -- reasons: -- * T.timeOfDayToTime could potentially perform bounds checking (although its -- current implementation doesn't) -- * T.timeToTimeOfDay converts excess time to leap seconds timeOfDayToTime :: T.TimeOfDay -> T.DiffTime timeOfDayToTime (T.TimeOfDay h m s) = ((fromIntegral h) * 60 + (fromIntegral m)) * 60 + (realToFrac s) timeToTimeOfDay :: T.DiffTime -> (T.TimeOfDay, Integer) timeToTimeOfDay dt = (T.TimeOfDay (fromInteger h) (fromInteger m) s, d) where s' = realToFrac dt s = mod' s' 60 m' = div' s' 60 m = mod' m' 60 h' = div' m' 60 h = mod' h' 24 d = div' h' 24