packages feed

text-builder-time-0.1: library/TextBuilderTime/Iso8601.hs

module TextBuilderTime.Iso8601
  ( day,
    utcTime,
  )
where

import Data.Time
import TextBuilder
import Prelude

-- |
-- Convert 'Day' to ISO-8601 format.
--
-- >>> day (fromGregorian 2022 6 16)
-- "2022-06-16"
{-# INLINE day #-}
day :: Day -> TextBuilder
day (toGregorian -> (year, month, day)) =
  mconcat
    [ fixedLengthDecimal 4 year,
      "-",
      fixedLengthDecimal 2 month,
      "-",
      fixedLengthDecimal 2 day
    ]

-- |
-- Convert 'UTCTime' to ISO-8601 format.
--
-- >>> utcTime (UTCTime (fromGregorian 2022 6 16) 0)
-- "2022-06-16T00:00:00Z"
--
-- >>> utcTime (UTCTime (fromGregorian 2022 6 16) 0.000001)
-- "2022-06-16T00:00:00.000001Z"
--
-- >>> utcTime (UTCTime (fromGregorian 2022 6 16) 0.000000001)
-- "2022-06-16T00:00:00.000000001Z"
--
-- >>> utcTime (UTCTime (fromGregorian 2022 6 16) (3 * 60 * 60 + 4 * 60 + 5))
-- "2022-06-16T03:04:05Z"
{-# INLINE utcTime #-}
utcTime :: UTCTime -> TextBuilder
utcTime UTCTime {..} =
  let picoseconds = diffTimeToPicoseconds utctDayTime
      (seconds, picosecond) = divMod picoseconds 1_000_000_000_000
      seconds' = fromInteger seconds :: Int
      (dayMinutes, second) = divMod seconds' 60
      (hour, minute) = divMod dayMinutes 60
   in mconcat
        [ day utctDay,
          "T",
          fixedLengthDecimal 2 hour,
          ":",
          fixedLengthDecimal 2 minute,
          ":",
          fixedLengthDecimal 2 second,
          picosecondsSubsecondsComponent (fromIntegral picosecond),
          "Z"
        ]

-- |
-- Subseconds component of the ISO-8601 format compiled from picoseconds.
--
-- >>> picosecondsSubsecondsComponent 000_000_000_001
-- ".000000000001"
--
-- >>> picosecondsSubsecondsComponent 000_000_000_010
-- ".00000000001"
--
-- >>> picosecondsSubsecondsComponent 100_000_000_000
-- ".1"
--
-- >>> picosecondsSubsecondsComponent 0
-- ""
{-# INLINE picosecondsSubsecondsComponent #-}
picosecondsSubsecondsComponent ::
  -- | Picoseconds.
  Int ->
  TextBuilder
picosecondsSubsecondsComponent =
  skipTrail 12
  where
    skipTrail pos val =
      if val == 0
        then
          mempty
        else case divMod val 10 of
          (quotient, remainder) ->
            if remainder == 0
              then
                skipTrail (pred pos) quotient
              else
                "." <> fixedLengthDecimal pos val