packages feed

hodatime-1.0.0.0: src/Data/HodaTime/Calendar/Gregorian/Internal.hs

{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE FlexibleInstances #-}
module Data.HodaTime.Calendar.Gregorian.Internal
(
   daysToYearMonthDay
  ,fromWeekDate
  ,Gregorian
  ,Month(..)
  ,DayOfWeek(..)
  ,invalidDayThresh
  ,epochDayOfWeek
  ,maxDaysInMonth
  ,yearMonthDayToDays
  ,nthDayToDayOfMonth
  ,dayOfWeekFromDays
  ,instantToYearMonthDay
  ,yearMonthDayToCycleCenturyDays
  ,gregorianFromYmd
  ,gregorianToDays
  ,daysToGregorian
  ,gregorianToYearMonthDay
)
where

import Data.HodaTime.CalendarDateTime.Internal (IsCalendar(..), IsCalendarDateTime(..), DayOfMonth, Year, WeekNumber, CalendarDateTime(..), LocalTime(..), Date)
import Data.HodaTime.Calendar.Gregorian.CacheTable (DTCacheTable(..), decodeMonth, decodeYear, decodeDay, cacheTable)
import Data.HodaTime.Calendar.Internal (mkCommonMonthLens, mkYearLens, mkFromWeekDate, dayOfWeekFromDays, commonMonthDayOffsets, borders, daysPerStandardYear, daysPerCentury)
import Data.HodaTime.Instant.Internal (Instant(..))
import Control.Arrow ((>>>), (&&&), (***), first)
import Data.Int (Int32, Int8)
import Data.Word (Word8, Word32)
import Data.Array.Unboxed ((!))
import Control.DeepSeq (NFData(..))
import Data.Hashable (Hashable(..))

-- Constants

yearsPerCycle :: Num a => a
yearsPerCycle = 400

daysPerCycle :: Num a => a      -- NOTE: A "cycle" is 400 years
daysPerCycle = 146097

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

-- types
    
data Gregorian
    
instance IsCalendar Gregorian where
  data Date Gregorian = GregorianDate {-# UNPACK #-} !Int8 {-# UNPACK #-} !Word8 {-# UNPACK #-} !Word32
    deriving (Eq, Ord)

  data DayOfWeek Gregorian = Sunday | Monday | Tuesday | Wednesday | Thursday | Friday | Saturday
    deriving (Show, Read, Eq, Ord, Enum, Bounded)

  data Month Gregorian = January | February | March | April | May | June | July | August | September | October | November | December
    deriving (Show, Read, Eq, Ord, Enum, Bounded)

  fromDays = daysToGregorian
  toDays = gregorianToDays
  toYmd = gregorianToYearMonthDay
  calendarName _ = "Gregorian"

  -- Fast path: shift only the day-in-century, leaving cycle\/century untouched when we stay in-century.
  day' f gd = mkgd <$> f (fromIntegral d)
    where
      (_, _, d) = gregorianToYearMonthDay gd
      mkgd d' = shiftDaysWith clampToValid (d' - fromIntegral d) gd

  month' gd = toEnum . fromIntegral $ m
    where (_, m, _) = gregorianToYearMonthDay gd

  monthl' = mkCommonMonthLens 12 firstGregDayTuple maxDaysInMonth yearMonthDayToDays gregorianToYearMonthDay daysToGregorian
  {-# INLINE monthl' #-}

  year' = mkYearLens firstGregDayTuple maxDaysInMonth yearMonthDayToDays gregorianToYearMonthDay daysToGregorian
  {-# INLINE year' #-}

  dayOfWeek' (GregorianDate _ century dic) = toEnum . dayOfWeekFromDays epochDayOfWeek $ 5 * fromIntegral century + fromIntegral dic

  next' n dow gd@(GregorianDate _ century dic) = shiftDaysWith id (7 * n' + targetDow - currentDoW) gd
    where
      currentDoW = dayOfWeekFromDays epochDayOfWeek $ 5 * fromIntegral century + fromIntegral dic
      targetDow = fromEnum dow
      n' = if targetDow > currentDoW then n - 1 else n

  previous' n dow gd@(GregorianDate _ century dic) = shiftDaysWith id (negate $ 7 * n' + currentDoW - targetDow) gd
    where
      currentDoW = dayOfWeekFromDays epochDayOfWeek $ 5 * fromIntegral century + fromIntegral dic
      targetDow = fromEnum dow
      n' = if targetDow < currentDoW then n - 1 else n

instance NFData (Date Gregorian) where
  rnf (GregorianDate cyc century dic) = rnf cyc `seq` rnf century `seq` rnf dic

instance Hashable (Date Gregorian) where
  hashWithSalt s (GregorianDate cyc century dic) = s `hashWithSalt` cyc `hashWithSalt` century `hashWithSalt` dic

instance NFData (Month Gregorian) where
  rnf m = m `seq` ()

instance Hashable (Month Gregorian) where
  hashWithSalt s = hashWithSalt s . fromEnum

instance NFData (DayOfWeek Gregorian) where
  rnf d = d `seq` ()

instance Hashable (DayOfWeek Gregorian) where
  hashWithSalt s = hashWithSalt s . fromEnum

instance IsCalendarDateTime Gregorian where
  fromAdjustedInstant (Instant days secs nsecs) = CalendarDateTime (daysToGregorian days) (LocalTime secs nsecs)
  toUnadjustedInstant (CalendarDateTime gd (LocalTime secs nsecs)) = Instant (gregorianToDays gd) secs nsecs

-- constructors

fromWeekDate :: Int -> DayOfWeek Gregorian -> WeekNumber -> DayOfWeek Gregorian -> Year -> Maybe (Date Gregorian)
fromWeekDate = mkFromWeekDate invalidDayThresh epochDayOfWeek yearMonthDayToDays daysToGregorian

-- helper functions

nthDayToDayOfMonth :: Int -> Int -> Month Gregorian -> Int -> Int
nthDayToDayOfMonth nth day month y
  | nth < 0   = mdm - backwardDist + 7 * (nth + 1)      -- NOTE: "from last" (POSIX week 5 -> nth -1) counts back from the last day, so the final weekday is not missed when the month ends exactly on it
  | otherwise = 1   + forwardDist  + 7 * nth
  where
    mdm = maxDaysInMonth month y
    forwardDist  = (day - dowOf 1)   `mod` 7
    backwardDist = (dowOf mdm - day) `mod` 7
    dowOf dom = (dom + (13 * m' - 1) `div` 5 + yrhs + (yrhs `div` 4) + (ylhs `div` 4) - 2 * ylhs) `mod` 7
    m = fromEnum month
    (m', y') = if m < 2 then (m + 11, y - 1) else (m - 1, y)
    yrhs = y' `mod` 100
    ylhs = y' `div` 100

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

-- | Construct the (cycle, century, day-in-century) triple directly from a year\/month\/day, without first
--   computing the flat day count and dividing it back down.  Within a cycle each century is exactly 36524 days
--   (4*36524 = 146097 - 1, the missing day being the cycle's extra leap day), and within a century the leap rule
--   reduces to a plain \/4 (the \/100 and \/400 corrections vanish for year-offsets 0..99).  This naturally yields
--   representation (ii): 'century' is always in [0,3] and the extra leap day falls out as day 36524 of the last century.
yearMonthDayToCycleCenturyDays :: Year -> Month Gregorian -> DayOfMonth -> (Int, Int, Int)
yearMonthDayToCycleCenturyDays y m d = (cyc, century, dic)
  where
    years = if m < March then y - 2001 else y - 2000
    (cyc, yearInCycle) = years `divMod` yearsPerCycle
    (century, yoc) = yearInCycle `divMod` 100
    m' = if m > February then fromEnum m - 2 else fromEnum m + 10
    dic = yoc * daysPerStandardYear + yoc `div` 4 + commonMonthDayOffsets !! m' + d - 1

-- | Build a 'Date' 'Gregorian' directly from a year\/month\/day via 'yearMonthDayToCycleCenturyDays' (keeping the
--   'GregorianDate' constructor internal to this module).  No validity checking is performed here.
gregorianFromYmd :: Year -> Month Gregorian -> DayOfMonth -> Date Gregorian
gregorianFromYmd y m d = GregorianDate (fromIntegral cyc) (fromIntegral century) (fromIntegral dic)
  where (cyc, century, dic) = yearMonthDayToCycleCenturyDays y m d

-- NOTE: Epoch is March 1 2000 because that has nicest properties that is near our current time.  Because the year is
-- NOTE: shifted to start in March, January and February belong to the /previous/ shifted year (years = y - 2001), so
-- NOTE: the leap-day terms (div 4 \/ 100 \/ 400) naturally count Feb 29 only once it has actually occurred.  Verified
-- NOTE: against proleptic Gregorian arithmetic for every date in years 1-9999 (all century boundaries and negatives).
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 + commonMonthDayOffsets !! m' + d - 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 -> (Int32, Word8, Word8)
daysToYearMonthDay days = (fromIntegral 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 -> (Int32, Word8, Word8)
instantToYearMonthDay (Instant days _ _) = daysToYearMonthDay days

-- Date Gregorian bridge functions (cycle\/century\/day-in-century representation)

-- | Reconstruct the flat (epoch-relative) day count from a 'Date' 'Gregorian'.  Inverse of 'daysToGregorian'; must
--   agree with 'yearMonthDayToDays' so the cycle representation round-trips against the flat day count.
gregorianToDays :: Date Gregorian -> Int32
gregorianToDays (GregorianDate cyc century days) = fromIntegral $ cyc' * daysPerCycle + century' * daysPerCentury + days'
  where
    cyc' = fromIntegral cyc :: Int
    century' = fromIntegral century :: Int
    days' = fromIntegral days :: Int

-- | Decompose a flat (epoch-relative) day count into the cycle\/century\/day-in-century representation.  Uses floored
--   'divMod' so remainders are non-negative.  Representation (ii): 'century' is always in [0,3]; the single extra leap
--   day per cycle (which floored division would place at century 4, day 0) is folded back to day 36524 of century 3.
daysToGregorian :: Int32 -> Date Gregorian
daysToGregorian days = GregorianDate (fromIntegral cycles) (fromIntegral century) (fromIntegral dic)
  where
    (cycles, cycleDays) = (fromIntegral days :: Int) `divMod` daysPerCycle
    (century0, dic0) = cycleDays `divMod` daysPerCentury
    (century, dic) = if century0 == (4 :: Int) then (3, dic0 + daysPerCentury) else (century0, dic0)

-- | Decode a 'Date' 'Gregorian' directly to (year, zero-based month, day) from its stored fields: the cycle\/century
--   split is already present, so month and day come from a single cache-table lookup on the day-in-century.
gregorianToYearMonthDay :: Date Gregorian -> (Int32, Word8, Word8)
gregorianToYearMonthDay (GregorianDate cyc century dic)
  | dic == daysPerCentury = (fromIntegral extraYear, 1, 29)   -- extra-cycle-day: 29 Feb (month 1 = February, 0-based)
  | otherwise             = (fromIntegral yr, fromIntegral m, fromIntegral d)
  where
    cycleYear = fromIntegral cyc * (400 :: Int)
    extraYear = 2000 + cycleYear + 400
    yr = 2000 + cycleYear + fromIntegral century * 100 + fromIntegral y
    (y, m, d) = decodeEntry cacheTable . fromIntegral $ dic
    decodeEntry (DTCacheTable xs _) = (\x -> (decodeYear x, decodeMonth x, decodeDay x)) . (!) xs

-- | Shift a date by 'delta' days.  Fast path: when the shift stays within the current century (and we are safely
--   past the pre-Gregorian threshold, so cyc >= -1) only the day-in-century changes and the cycle\/century are
--   untouched.  Otherwise fall back to reconstructing the flat day count, applying 'onFlat' (e.g. the validity clamp),
--   and re-decomposing.  The extra-cycle-day (day-in-century == 36524) always fails the in-century bound.
shiftDaysWith :: (Int32 -> Int32) -> Int -> Date Gregorian -> Date Gregorian
shiftDaysWith onFlat delta gd@(GregorianDate cyc century dic)
  | cyc >= -1 && dic' >= 0 && dic' < daysPerCentury = GregorianDate cyc century (fromIntegral dic')
  | otherwise                                       = daysToGregorian . onFlat $ gregorianToDays gd + fromIntegral delta
  where dic' = fromIntegral dic + delta :: Int

-- | Clamp a flat day count so it never precedes the first valid Gregorian date (15 Oct 1582).
clampToValid :: Int32 -> Int32
clampToValid days = if days > invalidDayThresh then days else invalidDayThresh + 1