non-negative-time-diff: type safe diffUTCTime

[ bsd3, library, system ] [ Propose Tags ] [ Report a vulnerability ]

Both arguments of diffUTCTime function from time package have the same type. It is easy to mix them.

f = do
  started <- getCurrentTime
  threadDelay 10_000_000
  ended <- getCurrentTime
  pure $ started `diffUTCTime` ended

This package provides a stricter diffUTCTime that significantly reduces possibility of mixing its arguments by an accident.

import Data.Time.Clock.NonNegativeTimeDiff
f = do
  started <- getCurrentTime
  threadDelay 10_000_000
  ended <- getTimeAfter started
  pure $ ended `diffUTCTime` started

STM use case

The STM package is shipped without a function to get current time. Let’s consider a situtation like this:

data Ctx
  = Ctx { m :: Map Int UTCTime
        , s :: TVar NominalDiffTime
        , q :: TQueue Int
        }

f (c :: Ctx) = do
  now <- getCurrentTime
  atomically $ do
    i <- readTQueue q
    lookup i c.m >>= \case
      Nothing -> pure ()
      Just t -> modifyTVar' c.s (+ diffUTCTime now t)

now might be less than t because the queue might be empty by the time f is invoked. The package API can correct the above snippet as follows:

data Ctx
  = Ctx { m :: Map Int UtcBox
        , s :: TVar NominalDiffTime
        , q :: TQueue Int
        }

f (c :: Ctx) = do
  atomically $ do
    i <- readTQueue q
    lookup i c.m >>= \case
      Nothing -> pure ()
      Just t ->
        doAfter tb \t -> do
          now <- getTimeAfter t
          modifyTVar' c.s (+ diffUTCTime now t)

File access time

Another popular usecase where original diffUTCTime might be misused.

isFileOlderThan :: FilePath -> NominalDiffTime -> IO Bool
isFileOlderThan fp maxAge = do
  now <- getCurrentTime
  mt <- getModificationTime fp
  when (mt `diffUTCTime` now > maxAge) $ do
    removeFile fp

File age is always negative in the above example - this eventually would cause a space leak on disk.

Corrected version:

isFileOlderThan :: FilePath -> NominalDiffTime -> IO Bool
isFileOlderThan fp maxAge =
  getModificationTime fp >>= (`doAfter` \mt -> do
    now <- getTimeAfter mt
    when (now `diffUTCTime` mt > maxAge) $ do
      removeFile fp)

Requirements

Unboxing UtcBox values requires a GHC natnormalise plugin:

{-# GHC_OPTIONS -fplugin GHC.TypeLits.Normalise #-}

Downloads

Maintainer's Corner

Package maintainers

For package maintainers and hackage trustees

Candidates

  • No Candidates
Versions [RSS] 0.0.1
Change log changelog.md
Dependencies aeson (<3), base (>=4.7 && <5), deepseq (<2), directory (<2), ghc-typelits-natnormalise (<1), mtl (<3), safecopy (<1), time (<2) [details]
Tested with ghc ==9.12.2
License BSD-3-Clause
Copyright Daniil Iaitkov 2026
Author Daniil Iaitskov
Maintainer dyaitskov@gmail.com
Uploaded by DaniilIaitskov at 2026-03-09T13:54:35Z
Category System
Home page http://github.com/yaitskov/non-negative-time-diff
Bug tracker https://github.com/yaitskov/non-negative-time-diff/issues
Source repo head: git clone https://github.com/yaitskov/non-negative-time-diff.git
Distributions
Downloads 1 total (1 in the last 30 days)
Rating (no votes yet) [estimated by Bayesian average]
Your Rating
  • λ
  • λ
  • λ
Status Docs uploaded by user
Build status unknown [no reports yet]