hodatime-0.2.1.1: src/Data/HodaTime/Calendar/Gregorian/Internal.hs
{-# LANGUAGE TypeFamilies #-}
module Data.HodaTime.Calendar.Gregorian.Internal
(
daysToYearMonthDay
,fromWeekDate
,Gregorian
,Month(..)
,DayOfWeek(..)
,invalidDayThresh
,epochDayOfWeek
,maxDaysInMonth
,yearMonthDayToDays
,nthDayToDayOfMonth
,dayOfWeekFromDays
,instantToYearMonthDay
)
where
import Data.HodaTime.Constants (daysPerCycle, daysPerCentury, daysPerFourYears)
import Data.HodaTime.CalendarDateTime.Internal (IsCalendar(..), CalendarDate(..), IsCalendarDateTime(..), DayOfMonth, Year, WeekNumber, CalendarDateTime(..), LocalTime(..))
import Data.HodaTime.Calendar.Gregorian.CacheTable (DTCacheTable(..), decodeMonth, decodeYear, decodeDay, cacheTable)
import Data.HodaTime.Calendar.Constants (daysPerStandardYear)
import Data.HodaTime.Instant.Internal (Instant(..))
import Control.Arrow ((>>>), (&&&), (***), first)
import Data.Maybe (fromJust)
import Data.List (findIndex)
import Data.Int (Int32, Int8)
import Data.Word (Word8, Word32)
import Control.Monad (guard)
-- Constants
invalidDayThresh :: Integral a => a
invalidDayThresh = -152445 -- NOTE: 14.Oct.1582, one day before Gregorian calendar came into effect
firstGregDayTuple :: (Integral a, Integral b, Integral c) => (a, b, c)
firstGregDayTuple = (1582, 9, 15)
epochDayOfWeek :: DayOfWeek Gregorian
epochDayOfWeek = Wednesday
monthDayOffsets :: Num a => [a]
monthDayOffsets = 0 : rest
where
rest = zipWith (+) daysPerMonth (0:rest)
daysPerMonth = [31, 30, 31, 30, 31, 31, 30, 31, 30, 31, 31, 28] -- NOTE: rotated (TODO BUG: Why do we need Feb? That will be past end of year, thus impossible)
-- types
data Gregorian
instance IsCalendar Gregorian where
type Date Gregorian = CalendarDate Gregorian
data DayOfWeek Gregorian = Sunday | Monday | Tuesday | Wednesday | Thursday | Friday | Saturday
deriving (Show, Eq, Ord, Enum, Bounded)
data Month Gregorian = January | February | March | April | May | June | July | August | September | October | November | December
deriving (Show, Eq, Ord, Enum, Bounded)
day' f (CalendarDate _ d m y) = mkcd . (rest+) <$> f (fromIntegral d)
where
rest = pred $ yearMonthDayToDays (fromIntegral y) (toEnum . fromIntegral $ m) 1
mkcd days =
let
days' = fromIntegral $ if days > invalidDayThresh then days else invalidDayThresh + 1
(y', m', d') = daysToYearMonthDay days'
in CalendarDate days' d' m' y'
{-# INLINE day' #-}
month' (CalendarDate _ _ m _) = toEnum . fromIntegral $ m
monthl' f (CalendarDate _ d m y) = mkcd <$> f (fromEnum m)
where
mkcd months = CalendarDate (fromIntegral days) d'' (fromIntegral m') (fromIntegral y'')
where
(y', months') = flip divMod 12 >>> first (+ fromIntegral y) $ months
(y'', m', d') = if (y', months', d) < firstGregDayTuple then firstGregDayTuple else (y', months', d)
mdim = fromIntegral $ maxDaysInMonth (toEnum m') y'
d'' = if d' > mdim then mdim else d'
days = yearMonthDayToDays y'' (toEnum m') (fromIntegral d'')
{-# INLINE monthl' #-}
year' f (CalendarDate _ d m y) = mkcd <$> f (fromIntegral y)
where
mkcd y' = CalendarDate days d'' m' (fromIntegral y'')
where
(y'', m', d') = if (y', m, d) < firstGregDayTuple then firstGregDayTuple else (y', m, d)
m'' = toEnum . fromIntegral $ m'
mdim = fromIntegral $ maxDaysInMonth m'' y''
d'' = if d' > mdim then mdim else d'
days = fromIntegral $ yearMonthDayToDays y'' m'' (fromIntegral d'')
{-# INLINE year' #-}
dayOfWeek' (CalendarDate days _ _ _) = toEnum . dayOfWeekFromDays . fromIntegral $ days
next' n dow (CalendarDate days _ _ _) = moveByDow n dow (-) (+) (fromIntegral days)
previous' n dow (CalendarDate days _ _ _) = moveByDow n dow subtract (-) (fromIntegral days) -- NOTE: subtract is (-) with the arguments flipped
instance IsCalendarDateTime Gregorian where
fromAdjustedInstant (Instant days secs nsecs) = CalendarDateTime cd lt
where
cd = CalendarDate days d m y
(y, m, d) = daysToYearMonthDay days
lt = LocalTime secs nsecs
toUnadjustedInstant (CalendarDateTime (CalendarDate days _ _ _) (LocalTime secs nsecs)) = Instant days secs nsecs
-- constructors
fromWeekDate :: Int -> DayOfWeek Gregorian -> WeekNumber -> DayOfWeek Gregorian -> Year -> Maybe (Date Gregorian)
fromWeekDate minWeekDays wkStartDoW weekNum dow y = do
guard $ days > invalidDayThresh
return $ CalendarDate days d m y'
where
soyDays = yearMonthDayToDays y January minWeekDays
soyDoW = dayOfWeekFromDays soyDays
startDoWDistance = fromEnum soyDoW - fromEnum wkStartDoW
dowDistance = fromEnum dow - fromEnum wkStartDoW
dowDistance' = if dowDistance < 0 then dowDistance + 7 else dowDistance
startDays = soyDays - startDoWDistance
weekNum' = pred weekNum
days = fromIntegral $ startDays + weekNum' * 7 + dowDistance'
(y', m, d) = daysToYearMonthDay days
-- helper functions
nthDayToDayOfMonth :: Int -> Int -> Month Gregorian -> Int -> Int
nthDayToDayOfMonth nth day month y = dom + d' + 7 * nth
where
mdm = maxDaysInMonth month y
dom = if nth < 0 then mdm else 1
m = fromEnum month
dow = (dom + (13 * m' - 1) `div` 5 + yrhs + (yrhs `div` 4) + (ylhs `div` 4) - 2 * ylhs) `mod` 7
d = day - dow
d' = if d < 0 then d + 7 else d
(m', y') = if m < 2 then (m + 11, y - 1) else (m - 1, y)
yrhs = y' `mod` 100
ylhs = y' `div` 100
dayOfWeekFromDays :: Int -> Int
dayOfWeekFromDays = normalize . (fromEnum epochDayOfWeek +) . flip mod 7
where
normalize n = if n >= 7 then n - 7 else n
moveByDow :: Int -> DayOfWeek Gregorian -> (Int -> Int -> Int) -> (Int -> Int -> Int) -> Int -> CalendarDate Gregorian
moveByDow n dow distanceF adjust days = CalendarDate days' d m y
where
currentDoW = dayOfWeekFromDays days
targetDow = fromIntegral . fromEnum $ dow
distance = distanceF targetDow currentDoW
days' = fromIntegral $ fromIntegral days `adjust` (7 * n) `adjust` distance
(y, m, d) = daysToYearMonthDay days'
maxDaysInMonth :: Month Gregorian -> Year -> Int
maxDaysInMonth February y
| isLeap = 29
| otherwise = 28
where
isLeap
| 0 == y `mod` 100 = 0 == y `mod` 400
| otherwise = 0 == y `mod` 4
maxDaysInMonth m _
| m == April || m == June || m == September || m == November = 30
| otherwise = 31
-- NOTE: Epoch is March 1 2000 because that has nicest properties that is near our current time.
-- TODO: The addition of leap days below will add from the previous year. We need to determine if this is a bug
-- TODO: and if it is not, why isn't it
yearMonthDayToDays :: Year -> Month Gregorian -> DayOfMonth -> Int
yearMonthDayToDays y m d = days
where
m' = if m > February then fromEnum m - 2 else fromEnum m + 10
years = if m < March then y - 2001 else y - 2000
yearDays = years * daysPerStandardYear + years `div` 4 + years `div` 400 - years `div` 100
days = yearDays + monthDayOffsets !! m' + d - 1
-- | The issue is that 4 * daysPerCentury will be one less than daysPerCycle. The reason for this is that the Gregorian calendar adds one more day per 400 year cycle
-- and this day is missing from adding up 4 individual centuries. We have the same issue again with 4 years (i.e. 365*4 is daysPerFourYears - 1)
-- so we use this function to check if this has occurred so we can add the missing day back in.
borders :: (Num a, Eq a) => a -> a -> Bool
borders c x = x == c - 1
-- | Count up centuries, plus remaining days and determine if this is a special extra cycle day. NOTE: This
-- function would be more accurate if it only took absolute values, but it does end up coming up with the correct answer even on negatives. It just
-- ends up doing extra calculations with negatives (e.g. year comes back as -100 and entry is +100, which ends up being right but it could have been 0 and the +0 entry)
calculateCenturyDays :: Int32 -> (Int32, Int32, Bool)
calculateCenturyDays days = (y, centuryDays, isExtraCycleDay)
where
(cycleYears, (cycleDays, isExtraCycleDay)) = flip divMod daysPerCycle >>> (* 400) *** id &&& borders daysPerCycle $ days
(centuryYears, centuryDays) = flip divMod daysPerCentury >>> first (* 100) $ cycleDays
y = cycleYears + centuryYears
daysToYearMonthDay :: Int32 -> (Word32, Word8, Word8)
daysToYearMonthDay days = (fromIntegral y, fromIntegral m'', fromIntegral d')
where
(centuryYears, centuryDays, isExtraCycleDay) = calculateCenturyDays days
(fourYears, (remaining, isLeapDay)) = flip divMod daysPerFourYears >>> (* 4) *** id &&& borders daysPerFourYears $ centuryDays
(oneYears, yearDays) = remaining `divMod` daysPerStandardYear
m = pred . fromJust . findIndex (\mo -> yearDays < mo) $ monthDayOffsets
(m', startDate) = if m >= 10 then (m - 10, 2001) else (m + 2, 2000)
d = yearDays - monthDayOffsets !! m + 1
(m'', d') = if isExtraCycleDay || isLeapDay then (1, 29) else (m', d)
y = startDate + centuryYears + fourYears + oneYears
-- TODO: At some point we should see how much a difference the caching makes
_daysToYearMonthDay' :: Int32 -> (Int32, Int8, Int8)
_daysToYearMonthDay' days = (y',m'', fromIntegral d')
where
(centuryYears, centuryDays, isExtraCycleDay) = calculateCenturyDays days
decodeEntry (DTCacheTable xs _ _) = (\x -> (decodeYear x, decodeMonth x, decodeDay x)) . (!!) xs
(y,m,d) = decodeEntry cacheTable . fromIntegral $ centuryDays
(m',d') = if isExtraCycleDay then (1,29) else (m,d)
(y',m'') = (2000 + centuryYears + fromIntegral y, fromIntegral $ m')
-- here to avoid circular dependancy between Instant and Gregorian
instantToYearMonthDay :: Instant -> (Word32, Word8, Word8)
instantToYearMonthDay (Instant days _ _) = daysToYearMonthDay days