Skip to content

Commit

Permalink
Make Nanosecond represent time < 1 Millisecond (#11)
Browse files Browse the repository at this point in the history
* Make Nanosecond represent time < 1 Millisecond

* Refactor padding function
  • Loading branch information
paulyoung authored Mar 9, 2018
1 parent cf28620 commit 31165bf
Show file tree
Hide file tree
Showing 3 changed files with 98 additions and 72 deletions.
13 changes: 8 additions & 5 deletions src/Data/PreciseDate/Component.purs
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,13 @@ import Data.Enum (class BoundedEnum, class Enum, Cardinality(..), fromEnum, toEn
import Data.Maybe (Maybe(..))
import Data.Newtype (class Newtype)

-- | An nanosecond component for a time value.
-- | A nanosecond component for a time value.
-- |
-- | Must be used in conjunction with a millisecond component to represent the
-- | total amount of time less than 1 second.
-- |
-- | The constructor is private as values for the type are restricted to the
-- | range 0 to 999999999, inclusive. The `toEnum` function can be used to
-- | range 0 to 999999, inclusive. The `toEnum` function can be used to
-- | safely acquire an `Nanosecond` value from an integer. Correspondingly, a
-- | `Nanosecond` can be lowered to a plain integer with the `fromEnum`
-- | function.
Expand All @@ -23,16 +26,16 @@ derive newtype instance ordNanosecond :: Ord Nanosecond

instance boundedNanosecond :: Bounded Nanosecond where
bottom = Nanosecond 0
top = Nanosecond 999999999
top = Nanosecond 999999

instance enumNanosecond :: Enum Nanosecond where
succ = toEnum <<< (_ + 1) <<< fromEnum
pred = toEnum <<< (_ - 1) <<< fromEnum

instance boundedEnumNanosecond :: BoundedEnum Nanosecond where
cardinality = Cardinality 1000000000
cardinality = Cardinality 1000000
toEnum n
| n >= 0 && n <= 999999999 = Just (Nanosecond n)
| n >= 0 && n <= 999999 = Just (Nanosecond n)
| otherwise = Nothing
fromEnum (Nanosecond n) = n

Expand Down
78 changes: 50 additions & 28 deletions src/Data/PreciseDateTime.purs
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ import Prelude
import Control.Alt ((<|>))
import Data.Array ((!!))
import Data.Char.Unicode (isDigit)
import Data.DateTime (DateTime)
import Data.DateTime (DateTime, millisecond, time)
import Data.DateTime as DateTime
import Data.Decimal (Decimal, pow, modulo, truncated)
import Data.Decimal (Decimal, pow, truncated)
import Data.Decimal as Decimal
import Data.Enum (toEnum)
import Data.Enum (fromEnum, toEnum)
import Data.Formatter.DateTime (format)
import Data.Int (decimal)
import Data.Int as Int
Expand All @@ -26,7 +26,7 @@ import Data.PreciseDate.Component (Nanosecond)
import Data.PreciseDateTime.Internal (dateTimeFormatISO)
import Data.RFC3339String (RFC3339String(..), trim)
import Data.RFC3339String as RFC3339String
import Data.String (Pattern(Pattern), drop, length, split, takeWhile)
import Data.String (Pattern(Pattern), drop, length, split, take, takeWhile)
import Data.Time.Duration as Duration
import Data.Time.PreciseDuration (PreciseDuration(..), toMilliseconds, toNanoseconds, unPreciseDuration)

Expand All @@ -42,32 +42,55 @@ instance boundedPreciseDateTime :: Bounded PreciseDateTime where
instance showPreciseDateTime :: Show PreciseDateTime where
show (PreciseDateTime dateTime ns) = "PreciseDateTime (" <> show dateTime <> ") " <> show ns

nanoStringPadding = "000000000" :: String
milliStringPadding = "000" :: String
nanoStringPadding = "000000" :: String
subsecondStringPadding = "000000000" :: String

padString :: String -> (String -> String -> String) -> String -> String
padString padding fn string = fn string $ drop (length string) padding

padMilliString :: (String -> String -> String) -> String -> String
padMilliString = padString milliStringPadding

padNanoString :: (String -> String -> String) -> String -> String
padNanoString fn string =
let padding = drop (length string) nanoStringPadding
in fn string padding
padNanoString = padString nanoStringPadding

padSubsecondString :: (String -> String -> String) -> String -> String
padSubsecondString = padString subsecondStringPadding

leftPadMilliString :: String -> String
leftPadMilliString = padMilliString (flip append)

rightPadMilliString :: String -> String
rightPadMilliString = padMilliString append

leftPadNanoString :: String -> String
leftPadNanoString = padNanoString (flip append)

rightPadNanoString :: String -> String
rightPadNanoString = padNanoString append

parseSubseconds :: RFC3339String -> Maybe Int
leftPadSubsecondString :: String -> String
leftPadSubsecondString = padSubsecondString (flip append)

rightPadSubsecondString :: String -> String
rightPadSubsecondString = padSubsecondString append

parseSubseconds :: RFC3339String -> Maybe String
parseSubseconds (RFC3339String s) = do
let parts = split (Pattern ".") s
afterDot <- parts !! 1
let digits = takeWhile isDigit afterDot
Int.fromString <<< rightPadNanoString $ digits
pure $ rightPadSubsecondString $ take 9 digits

-- | Convert to `Nanosecond` separately so that a failure to parse subseconds
-- | results in `Just (Nanosecond 0)` instead of `Nothing`. This accounts for
-- | when subseconds were omitted from the timestamp, and allows us to
-- | differentiate between invalid `Int`s and invalid `Nanosecond`s.
nanosecond :: RFC3339String -> Maybe Nanosecond
nanosecond rfcString = parseSubseconds rfcString <|> Just 0 >>= toEnum
nanosecond rfcString = nanoseconds rfcString <|> Just 0 >>= toEnum
where
nanoseconds = parseSubseconds >=> drop 3 >>> Int.fromString

fromRFC3339String :: RFC3339String -> Maybe PreciseDateTime
fromRFC3339String rfcString = do
Expand All @@ -79,10 +102,12 @@ toRFC3339String :: PreciseDateTime -> RFC3339String
toRFC3339String (PreciseDateTime dateTime ns) =
let
beforeDot = format dateTimeFormatISO dateTime
nanos = Int.toStringAs decimal (unwrap ns)
leftPadded = leftPadNanoString nanos
millis = Int.toStringAs decimal $ fromEnum $ millisecond $ time dateTime
nanos = Int.toStringAs decimal $ unwrap ns
leftPaddedMs = leftPadMilliString millis
leftPaddedNs = leftPadNanoString nanos
in
trim <<< RFC3339String $ beforeDot <> "." <> leftPadded <> "Z"
trim <<< RFC3339String $ beforeDot <> "." <> leftPaddedMs <> leftPaddedNs <> "Z"

-- | Adjusts a date/time value with a duration offset. `Nothing` is returned
-- | if the resulting date would be outside of the range of valid dates.
Expand All @@ -97,15 +122,13 @@ adjust pd (PreciseDateTime dt ns) = do
roundTripDurInt = unPreciseDuration <<< toNanoseconds $ Milliseconds msPrecDurDec

negative = nsPrecDurInt < zero
msModTen = msPrecDurDec `modulo` ten
nsDiff = nsPrecDurInt - roundTripDurInt

-- If the duration is negative, the duration in milliseconds is a multiple
-- of 10, and the conversion from nanoseconds to milliseconds and back is
-- not lossless, then we need to round up the lost nanoseconds to 1
-- millisecond and adjust the duration.
-- If the duration is negative and the conversion from nanoseconds to
-- milliseconds and back is not lossless then we need to round up the lost
-- nanoseconds to 1 millisecond and adjust the duration.
adjustment =
if negative && msModTen == zero && nsDiff < zero
if negative && nsDiff < zero
then 1
else 0

Expand All @@ -117,23 +140,23 @@ adjust pd (PreciseDateTime dt ns) = do
adjustedDateTime <- DateTime.adjust msDur dt

-- If the duration is larger than can be represented in a `Nanosecond`
-- component, take the last 9 digits.
-- component, take the last 6 digits.
let
unsigned = Decimal.abs nsPrecDurInt
nsString = Decimal.toString unsigned

lastNine =
lastSix =
if unsigned > maxNano
then drop (length nsString - 9) nsString
then drop (length nsString - 6) nsString
else nsString

adjustedNsPrecDurInt <- Decimal.fromString lastNine
adjustedNsPrecDurInt <- Decimal.fromString lastSix
let adjustedNsInt = Decimal.fromInt (unwrap ns) + adjustedNsPrecDurInt

let
inverted =
if negative && adjustedNsInt <= maxNano && adjustedNsPrecDurInt /= zero
then tenPowNine - adjustedNsInt
then tenPowSix - adjustedNsInt
else adjustedNsInt

adjustedNs <- toInt inverted >>= toEnum
Expand All @@ -142,9 +165,8 @@ adjust pd (PreciseDateTime dt ns) = do
where
zero = Decimal.fromInt 0
ten = Decimal.fromInt 10
tenPowNine = ten `pow` Decimal.fromInt 9
maxNano = tenPowNine - Decimal.fromInt 1
maxm = ten `pow` Decimal.fromInt 22
tenPowSix = ten `pow` Decimal.fromInt 6
maxNano = tenPowSix - Decimal.fromInt 1

-- | Coerce a `Data.Decimal` to an Int, preserving precision.
toInt :: Decimal -> Maybe Int
Expand Down
Loading

0 comments on commit 31165bf

Please sign in to comment.