repa-array-4.0.0.1: Data/Repa/Bits/Date32.hs
module Data.Repa.Bits.Date32
( Date32
, pack, unpack
, next
, range
, readYYYYsMMsDD)
where
import Data.Repa.Array.Material.Foreign as A
import Data.Repa.Array.Material.Unboxed as A
import Data.Repa.Array as A
import Data.Repa.Eval.Array as A
import Data.Word
import Data.Bits
import GHC.Exts
import GHC.Word
import Prelude as P
-- | A date packed into a 32-bit word.
--
-- The bitwise format is:
--
-- @
-- 32 16 8 0
-- | year | month | day |
-- @
--
-- Pros: Packing and unpacking a Date32 is simpler than using other formats
-- that represent dates as a number of days from some epoch. We can also
-- avoid worrying about what the epoch should be, and the representation
-- will not overflow until year 65536.
--
-- Cons: Computing a range of dates is slower than with representations
-- using an epoch, as we cannot simply add one to get to the next valid date.
--
type Date32
= Word32
-- | Pack a year, month and day into a `Word32`.
--
-- If any components of the date are out-of-range then they will be bit-wise
-- truncated so they fit in their destination fields.
--
pack :: (Word, Word, Word) -> Date32
pack (yy, mm, dd)
= ((fromIntegral yy .&. 0x0ffff) `shiftL` 16)
.|. ((fromIntegral mm .&. 0x0ff) `shiftL` 8)
.|. (fromIntegral dd .&. 0x0ff)
{-# INLINE pack #-}
-- | Inverse of `pack`.
--
-- This function does a simple bit-wise unpacking of the given `Word32`,
-- and does not guarantee that the returned fields are within a valid
-- range for the given calendar date.
--
unpack :: Date32 -> (Word, Word, Word)
unpack date
= ( fromIntegral $ (date `shiftR` 16) .&. 0x0ffff
, fromIntegral $ (date `shiftR` 8) .&. 0x0ff
, fromIntegral $ date .&. 0x0ff)
{-# INLINE unpack #-}
-- | Yield the next date in the series.
--
-- This assumes leap years occur every four years,
-- which is valid after year 1900 and before year 2100.
--
next :: Date32 -> Date32
next (W32# date)
= W32# (next' date)
{-# INLINE next #-}
next' :: Word# -> Word#
next' !date
| (yy, mm, dd) <- unpack (W32# date)
, (yy', mm', dd')
<- case mm of
1 -> if dd >= 31 then (yy, 2, 1) else (yy, mm, dd + 1) -- Jan
2 -> if yy `mod` 4 == 0 -- Feb
then if dd >= 29
then (yy, 3, 1)
else (yy, mm, dd + 1)
else if dd >= 28
then (yy, 3, 1)
else (yy, mm, dd + 1)
3 -> if dd >= 31 then (yy, 4, 1) else (yy, mm, dd + 1) -- Mar
4 -> if dd >= 30 then (yy, 5, 1) else (yy, mm, dd + 1) -- Apr
5 -> if dd >= 31 then (yy, 6, 1) else (yy, mm, dd + 1) -- May
6 -> if dd >= 30 then (yy, 7, 1) else (yy, mm, dd + 1) -- Jun
7 -> if dd >= 31 then (yy, 8, 1) else (yy, mm, dd + 1) -- Jul
8 -> if dd >= 31 then (yy, 9, 1) else (yy, mm, dd + 1) -- Aug
9 -> if dd >= 30 then (yy, 10, 1) else (yy, mm, dd + 1) -- Sep
10 -> if dd >= 31 then (yy, 11, 1) else (yy, mm, dd + 1) -- Oct
11 -> if dd >= 30 then (yy, 12, 1) else (yy, mm, dd + 1) -- Nov
12 -> if dd >= 31 then (yy + 1, 1, 1) else (yy, mm, dd + 1) -- Dec
_ -> (0, 0, 0)
= case pack (yy', mm', dd') of
W32# w -> w
{-# NOINLINE next' #-}
-- | Yield an array containing a range of dates, inclusive of the end points.
---
-- TODO: avoid going via lists.
--
range :: TargetI l Date32
=> Name l -> Date32 -> Date32 -> Array l Date32
range n from to
| to < from = A.fromList n []
| otherwise = A.fromList n $ go [] from
where
go !acc !d
| d > to = P.reverse acc
| otherwise = go (d : acc) (next d)
{-# NOINLINE range #-}
-- | Read a `Date32` in ASCII YYYYsMMsDD format, using the given separator
-- character 's'.
---
-- TODO: avoid going via lists.
--
readYYYYsMMsDD
:: BulkI l Char
=> Char -> Array l Char -> Maybe Date32
readYYYYsMMsDD sep arr
= case words
$ A.toList
$ A.mapS U (\c -> if c == sep then ' ' else c) arr of
[yy, mm, dd] -> Just $ pack (read yy, read mm, read dd)
_ -> Nothing
{-# INLINE readYYYYsMMsDD #-}