packages feed

hodatime-1.0.0.0: src/Data/HodaTime/Pattern/Duration.hs

-----------------------------------------------------------------------------
-- |
-- Module      :  Data.HodaTime.Pattern.Duration
-- Copyright   :  (C) 2017 Jason Johnson
-- License     :  BSD-style (see the file LICENSE)
-- Maintainer  :  Jason Johnson <jason.johnson.081@gmail.com>
-- Stability   :  experimental
-- Portability :  POSIX, Windows
--
-- Patterns for a 'Data.HodaTime.Duration.Duration', rendered as @[-]D:HH:mm:ss@; @pDurationNano@ additionally carries
-- the fractional second down to the nanosecond.
----------------------------------------------------------------------------
module Data.HodaTime.Pattern.Duration
(
  -- * Standard Patterns
   pDuration
  ,pDurationNano
)
where

import Data.HodaTime.Pattern.Internal (Pattern(..), p_sixty)
import Data.HodaTime.Duration.Internal (Duration(..))
import qualified Data.HodaTime.Duration as Dur (fromSeconds, fromNanoseconds, add)
import Data.HodaTime.Instant.Internal (Instant(..))
import Data.HodaTime.Constants (secondsPerDay, nsecsPerSecond)
import Formatting (Format, later)
import qualified Data.Text as T
import qualified Data.Text.Lazy.Builder as TLB
import Text.Parsec (Parsec, many1, count, digit, option, (<?>))
import qualified Text.Parsec as P (char)

-- d1 = fromStandardDays 1 `Dur.add` fromSeconds 30
-- format pDuration d1        -- "1:00:00:30"
-- parse pDuration "-0:00:00:30"

type Parser a = Parsec String () a

-- DESIGN NOTE: like 'Data.HodaTime.Offset.Offset', a 'Duration' is a /signed/ quantity whose sign belongs to the
-- whole value, not to any one component — and it is stored floor-style (a signed day count plus /non-negative/
-- normalized sub-day seconds and nanoseconds, so e.g. -30s is stored as -1 day + 86370s).  So we do not decompose it
-- with independent field lenses; instead both directions treat it as a whole: the signed total is computed once (in
-- 'Integer', to interpret the floor representation correctly and to avoid overflow), then formatting emits the sign
-- followed by the magnitude, and parsing reads the sign and all components and rebuilds via 'Dur.fromSeconds' /
-- 'Dur.fromNanoseconds' (which re-normalize).  Hence the parser's setter is 'const': the text fully determines the
-- value.

-- | The duration pattern @[-]D:HH:mm:ss@ (days, hours, minutes, seconds; a leading @-@ only for negative durations),
--   e.g. @1:00:00:30@ or @-0:00:00:30@.
pDuration :: Pattern (Duration -> Duration) (Duration -> String) String
pDuration = Pattern (const <$> durationParser False <?> "duration: [-]D:HH:mm:ss") (durationFormat False)

-- | Like 'pDuration' but with a nanosecond fraction, @[-]D:HH:mm:ss.fffffffff@.
pDurationNano :: Pattern (Duration -> Duration) (Duration -> String) String
pDurationNano = Pattern (const <$> durationParser True <?> "duration: [-]D:HH:mm:ss.fffffffff") (durationFormat True)

-- helpers

-- | Decompose into @(isNegative, days, hours, minutes, seconds, nanoseconds)@ as non-negative magnitudes.
durComponents :: Duration -> (Bool, Integer, Integer, Integer, Integer, Integer)
durComponents (Duration (Instant days secs nsecs)) = (total < 0, d, h, m, s, ns)
  where
    nsPerSec = nsecsPerSecond :: Integer
    nsPerDay = (secondsPerDay :: Integer) * nsPerSec
    total = fromIntegral days * nsPerDay + fromIntegral secs * nsPerSec + fromIntegral nsecs
    a = abs total
    (totalSec, ns) = a `divMod` nsPerSec
    (totalMin, s)  = totalSec `divMod` 60
    (totalHr, m)   = totalMin `divMod` 60
    (d, h)         = totalHr `divMod` 24

durationFormat :: Bool -> Format String (Duration -> String)
durationFormat withFrac = later (TLB.fromText . T.pack . render)
  where
    render dur = sign ++ show d ++ ":" ++ pad2 h ++ ":" ++ pad2 m ++ ":" ++ pad2 s ++ fracPart
      where
        (neg, d, h, m, s, ns) = durComponents dur
        sign = if neg then "-" else ""
        fracPart = if withFrac then "." ++ padN 9 ns else ""
    pad2 = padN 2
    padN n x = let str = show x in replicate (n - length str) '0' ++ str

durationParser :: Bool -> Parser Duration
durationParser withFrac = do
  neg <- option False (True <$ P.char '-')
  d <- readInt <$> many1 digit
  _ <- P.char ':'
  h <- twoDigit
  _ <- P.char ':'
  m <- p_sixty
  _ <- P.char ':'
  s <- p_sixty
  ns <- if withFrac then P.char '.' *> (readInt <$> count 9 digit) else pure 0
  let sign = if neg then -1 else 1
      totalSec = sign * (((d * 24 + h) * 60 + m) * 60 + s)
      signedNs = sign * ns
  return $ Dur.fromSeconds totalSec `Dur.add` Dur.fromNanoseconds signedNs
  where
    twoDigit = readInt <$> count 2 digit
    readInt = read :: String -> Int