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 #-}

Static linking

In case of static linking define macro STATIC to disable natnormalise GHC plugin that is not available in such setup. UtcBox version for the static build is less strict, because it does not have existential variable.

cabal build --ghc-option=-optP=-DSTATIC

Downloads

Maintainer's Corner

Package maintainers

For package maintainers and hackage trustees

Candidates

  • No Candidates
Versions [RSS] 0.0.1, 0.0.2
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-14T12:57:42Z
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 3 total (3 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]