packages feed

fb-util-0.1.0.1: Util/TimeSec.hs

{-
  Copyright (c) Meta Platforms, Inc. and affiliates.
  All rights reserved.

  This source code is licensed under the BSD-style license found in the
  LICENSE file in the root directory of this source tree.
-}

-- | Utilities for dealing with time in seconds. This is mostly for
-- interacting with external systems that store or communicate using
-- time in seconds. For higher-resolution times, see "Util.Timing".
module Util.TimeSec
  ( Time(..)
  , TimeSpan(..)
  , TimeRange(..)
  , fromUTCTime
  , toUTCTime
  , addTime, subTime, timeDiff
  , addTimeSpan
  , now, timeAgo
  , minutes, hours, days, weeks
  , minute, hour, day, week
  , toSeconds, toMinutes, toHours, toDays
  , trBetween
  , trLength
  , trLast
  , ppUTCTime, ppDate, ppTime, ppTimeSpan, ppTimeSpanWithGranularity
  , PPTimeSpanGranularity(..)
  , showNominalDiffTime
  ) where

import Data.Aeson
import Data.Text (Text)
import qualified Data.Text as Text
import Data.Time.Clock
import Data.Time.Clock.POSIX
import Data.Time.Format
import Data.Maybe (catMaybes)
import TextShow

import GHC.Generics
import Control.DeepSeq
import Data.Hashable

-- | Unix timestamp. Time in seconds
newtype Time = Time { timeInSeconds :: Int }
  deriving (Eq, Ord, Show, Generic, FromJSON, ToJSON, TextShow, Num, Enum)

instance NFData   Time
instance Hashable Time

-- | 'TimeSpan' means a time difference, duration in seconds,
-- as opposed to 'Time', which is a specific point in time
-- (a unix timestamp).
newtype TimeSpan = TimeSpan { timeSpanInSeconds :: Int }
                 deriving (Show, Eq, Ord)

data TimeRange = TimeRange
  { start :: Time
  , end :: Time
  } deriving (Show, Eq)

fromUTCTime :: UTCTime -> Time
fromUTCTime = Time . truncate . utcTimeToPOSIXSeconds

toUTCTime :: Time -> UTCTime
toUTCTime (Time t) = posixSecondsToUTCTime $ fromIntegral t

addTime :: Time -> TimeSpan -> Time
addTime (Time t) (TimeSpan d) = Time (t + d)

subTime :: Time -> TimeSpan -> Time
subTime (Time t) (TimeSpan d) = Time (t - d)

timeDiff :: Time -> Time -> TimeSpan
timeDiff (Time e) (Time s) = TimeSpan (e - s)

negateTimeSpan :: TimeSpan -> TimeSpan
negateTimeSpan TimeSpan{..} = TimeSpan (- timeSpanInSeconds)

addTimeSpan :: TimeSpan -> TimeSpan -> TimeSpan
addTimeSpan t1 t2 = TimeSpan $ timeSpanInSeconds t1 + timeSpanInSeconds t2

now :: IO Time
now = fromUTCTime <$> getCurrentTime

timeAgo :: TimeSpan -> IO Time
timeAgo d = (`subTime` d) <$> now

minutes, hours, days, weeks :: Int -> TimeSpan
minutes = TimeSpan . (60 *)
hours = TimeSpan . (3600 *)
days = TimeSpan . (86400 *)
weeks = TimeSpan . (604800 *)

minute, hour, day, week :: TimeSpan
minute = minutes 1
hour = hours 1
day = days 1
week = weeks 1

trLength :: TimeRange -> TimeSpan
trLength (TimeRange s e) = timeDiff e s

trLast :: TimeSpan -> IO TimeRange
trLast d = trBetween d (TimeSpan 0)

trBetween :: TimeSpan -> TimeSpan -> IO TimeRange
trBetween startDiff endDiff = do
  t <- now
  return TimeRange
    { start = t `subTime` startDiff
    , end = t `subTime` endDiff
    }

-- Some generally useful pretty printing functions -----------------------------

ppUTCTime :: FormatTime t => t -> Text
ppUTCTime t = ppDate t <> " at " <> ppTime t

ppDate :: FormatTime t => t -> Text
ppDate = Text.pack . formatTime defaultTimeLocale "%F"  -- e.g. 2017-07-20

ppTime :: FormatTime t => t -> Text
ppTime = Text.pack . formatTime defaultTimeLocale "%T UTC"  -- e.g. 14:18:45

data PPTimeSpanGranularity = Day | Hour | Minute | Second

secondsInOneDay :: Int
secondsInOneDay = 3600 * 24

toDays, toHours, toMinutes, toSeconds :: TimeSpan -> Int
toDays TimeSpan{..}  = timeSpanInSeconds `div` secondsInOneDay
toHours TimeSpan{..}  = (timeSpanInSeconds `mod` secondsInOneDay) `div` 3600
toMinutes TimeSpan{..} = (timeSpanInSeconds `mod` 3600) `div` 60
toSeconds TimeSpan{..} = timeSpanInSeconds `mod` 60

plural :: Int -> Text -> Maybe Text
plural 0 _       = Nothing
plural 1 unitStr = Just $ "1 " <> unitStr
plural n unitStr = Just $ showt n <> " " <> unitStr <> "s"

joinTimeSectionString :: Text -> [Maybe Text] -> Text
joinTimeSectionString unitStr maybeSecs = case catMaybes maybeSecs of
  [] -> "0" <> unitStr <> "s"
  s  -> Text.intercalate ", " s

ppTimeSpanWithGranularity :: PPTimeSpanGranularity -> TimeSpan -> Text
ppTimeSpanWithGranularity g ts | timeSpanInSeconds ts < 0 =
  "-" <> ppTimeSpanWithGranularity g (negateTimeSpan ts)
ppTimeSpanWithGranularity Day ts
  | timeSpanInSeconds ts < secondsInOneDay = ppTimeSpanWithGranularity Hour ts
  | otherwise = joinTimeSectionString "day" [ plural (toDays ts) "day" ]
ppTimeSpanWithGranularity Hour ts
  | timeSpanInSeconds ts < 3600 = ppTimeSpanWithGranularity Minute ts
  | otherwise = joinTimeSectionString "hour"
    [ plural (toDays ts) "day"
    , plural (toHours ts) "hour"
    ]
ppTimeSpanWithGranularity Minute ts
  | timeSpanInSeconds ts < 60 = ppTimeSpanWithGranularity Second ts
  | otherwise = joinTimeSectionString "minute"
    [ plural (toDays ts) "day"
    , plural (toHours ts) "hour"
    , plural (toMinutes ts) "minute"
    ]
ppTimeSpanWithGranularity Second ts =
  joinTimeSectionString "second"
    [ plural (toDays ts) "day"
    , plural (toHours ts) "hour"
    , plural (toMinutes ts) "minute"
    , plural (toSeconds ts) "second"
    ]

ppTimeSpan :: TimeSpan -> Text
ppTimeSpan = ppTimeSpanWithGranularity Second

showNominalDiffTime :: NominalDiffTime -> String
showNominalDiffTime t = Text.unpack (ppTimeSpan (TimeSpan (round t)))