module Model.Offset
( Offset(..)
, offsetMillis
, diffTimeOffset
, offsetDiffTime
) where
import Data.Char (isDigit, digitToInt)
import Data.Fixed (Fixed(..), HasResolution(..), Milli, Pico)
import qualified Data.Scientific as Sci
import qualified Data.Text as T
import Data.Time (DiffTime)
import Data.Typeable (Typeable)
import Database.PostgreSQL.Typed.Types (PGParameter(..), PGColumn(..))
import Numeric (showSigned, showFFloat, readSigned, readDec)
import qualified Text.ParserCombinators.ReadP as RP
import qualified Text.ParserCombinators.ReadPrec as RP (lift)
import Text.Read (readMaybe, Read(readPrec))
import qualified Web.Route.Invertible as R
import qualified JSON
newtype Offset = Offset { offsetMilli :: Milli } deriving (Eq, Ord, Num, Real, Fractional, RealFrac, Typeable)
fixedToFixed :: (HasResolution a, HasResolution b) => Fixed a -> Fixed b
fixedToFixed x@(MkFixed xv) = y where
yv = xv * yr `div` xr
y = MkFixed yv
xr = resolution x
yr = resolution y
diffTimeOffset :: DiffTime -> Offset
diffTimeOffset = Offset . fixedToFixed . (realToFrac :: DiffTime -> Pico)
offsetDiffTime :: Offset -> DiffTime
offsetDiffTime = (realToFrac :: Pico -> DiffTime) . fixedToFixed . offsetMilli
offsetMillis :: Offset -> Integer
offsetMillis (Offset (MkFixed t)) = t
instance PGParameter "interval" Offset where
pgEncode t = pgEncode t . offsetDiffTime
pgEncodeValue e t = pgEncodeValue e t . offsetDiffTime
pgLiteral t = pgLiteral t . offsetDiffTime
instance PGColumn "interval" Offset where
pgDecode t = diffTimeOffset . pgDecode t
pgDecodeValue e t = diffTimeOffset . pgDecodeValue e t
instance Show Offset where
showsPrec p (Offset t) = showSigned ss p t where
ss a =
(if h /= 0 then shows (h :: Integer) . (':' :) else id)
. pads m' . shows m' . (':' :)
. pads s' . showFFloat Nothing (fromIntegral s' + realToFrac f :: Double)
where
(s, f) = properFraction a
(m, s') = divMod s 60
(h, m') = divMod m 60
pads x
| x < 10 = ('0' :)
| otherwise = id
instance Read Offset where
readPrec = RP.lift $ rm RP.<++ rc where
rm = do
m <- RP.readS_to_P (readSigned readDec)
r <- RP.look
case r of
(':':_) -> RP.pfail
('.':_) -> RP.pfail
_ -> return $ Offset (MkFixed m)
rc = do
pm <- RP.option '+' $ RP.satisfy (`elem` "-+")
c <- RP.sepBy1 (RP.readS_to_P readDec) (RP.char ':')
ms <- RP.option 0 (do
_ <- RP.char '.'
foldr (\d m -> 100*digitToInt d + m `div` 10) 0 <$> RP.many (RP.satisfy isDigit))
Offset . MkFixed . (if pm == '-' then negate else id) . (toInteger ms +) . (1000 *) <$> case c of
[s] -> return s
[m, s] -> return (60*m + s)
[h, m, s] -> return (60*(60*h + m) + s)
[d, h, m, s] -> return (60*(60*(24*d + h) + m) + s)
_ -> RP.pfail
instance JSON.ToJSON Offset where
toJSON = JSON.Number . fromInteger . offsetMillis
instance JSON.FromJSON Offset where
parseJSON (JSON.Number ms) | Sci.base10Exponent ms < 10 = return $ Offset $ MkFixed $ floor ms
parseJSON (JSON.String s) = maybe (fail "Invalid offset string") return $ readMaybe $ T.unpack s
parseJSON (JSON.Bool False) = return 0
parseJSON _ = fail "Invalid offset"
instance R.Parameter T.Text Offset where
renderParameter = T.pack . show . offsetMillis