packages feed

hodatime-1.1.0.0: src/Data/HodaTime/Calendar/Internal.hs

{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE FlexibleContexts #-}
module Data.HodaTime.Calendar.Internal
(
  mkCommonDaySetter
  ,mkCommonMonthSetter
  ,mkYearSetter
  ,mkFromNthDay
  ,mkFromWeekDate
  ,moveByDow
  ,dayOfWeekFromDays
  ,commonMonthDayOffsets
  ,borders
  ,daysPerStandardYear
  ,daysPerFourYears
  ,daysPerCentury
)
where

import Data.HodaTime.CalendarDateTime.Internal (Year, DayOfMonth, DayNth, WeekNumber)
import Data.Int (Int32)
import Data.Word (Word8)
import Control.Arrow ((>>>), first)
import Control.Monad (guard)

-- Constants

daysPerStandardYear :: Num a => a
daysPerStandardYear = 365

daysPerFourYears :: Num a => a
daysPerFourYears = 1461

daysPerCentury :: Num a => a
daysPerCentury = 36524

-- helper functions
--
-- NOTE: These are representation-agnostic: each calendar passes its own @fromDays@ (build a date from a flat
-- epoch-relative day count) and @toYmd@ (decode a date to year\/month\/day) so the same setter logic works whether
-- the calendar stores a flat day count, a packed cycle, or anything else.

mkCommonDaySetter :: Enum mon =>
     Int
  -> (Year -> mon -> DayOfMonth -> Int)
  -> (Int32 -> d)
  -> (d -> (Int32, Word8, Word8))
  -> DayOfMonth
  -> d
  -> d
mkCommonDaySetter preStartDay yearMonthDayToDays fromDays toYmd newDay date = mkcd (rest + newDay)
    where
      (y, m, _) = toYmd date
      rest = pred $ yearMonthDayToDays (fromIntegral y) (toEnum . fromIntegral $ m) 1
      mkcd days = fromDays days'
        where days' = fromIntegral $ if days > preStartDay then days else preStartDay + 1
{-# INLINE mkCommonDaySetter #-}

mkCommonMonthSetter :: Enum mon =>
     Int
  -> (Int, Int, Word8)
  -> (mon -> Year -> Int)
  -> (Year -> mon -> DayOfMonth -> Int)
  -> (d -> (Int32, Word8, Word8))
  -> (Int32 -> d)
  -> Int
  -> d
  -> d
mkCommonMonthSetter monthsPerYear firstDayTuple maxDaysInMonth yearMonthDayToDays toYmd fromDays newMonth date = mkcd newMonth
    where
      (y, _, d) = toYmd date
      mkcd months = fromDays (fromIntegral days)
        where
          (y', months') = flip divMod monthsPerYear >>> first (+ fromIntegral y) $ months
          (y'', m', d') = if (y', months', d) < firstDayTuple then firstDayTuple 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 mkCommonMonthSetter #-}

mkYearSetter :: Enum mon =>
     (Int, Word8, Word8)
  -> (mon -> Year -> Int)
  -> (Year -> mon -> DayOfMonth -> Int)
  -> (d -> (Int32, Word8, Word8))
  -> (Int32 -> d)
  -> Int
  -> d
  -> d
mkYearSetter firstDayTuple maxDaysInMonth yearMonthDayToDays toYmd fromDays newYear date = mkcd newYear
    where
      (_, m, d) = toYmd date
      mkcd y' = fromDays days
        where
          (y'', m', d') = if (y', m, d) < firstDayTuple then firstDayTuple 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 mkYearSetter #-}

-- | Build a date from the nth (or nth-from-last) weekday within a month (e.g. \"the third Monday\").  This is the
--   calendar-agnostic core of a per-calendar @fromNthDay@: it reads the weekday of the anchor day (the 1st, or the
--   last day of the month for a \"from last\" request) via the calendar's own day count, so it needs no per-calendar
--   weekday formula.
mkFromNthDay :: (Enum mon, Enum dow) =>
     Int                                   -- ^ invalid-day threshold (dates on or before this are rejected)
  -> dow                                   -- ^ epoch day of week
  -> (Year -> mon -> DayOfMonth -> Int)    -- ^ yearMonthDayToDays
  -> (mon -> Year -> Int)                  -- ^ maxDaysInMonth
  -> (Int32 -> d)                          -- ^ fromDays
  -> DayNth -> dow -> mon -> Year -> Maybe d
mkFromNthDay invalidDayThresh epochDayOfWeek yearMonthDayToDays maxDaysInMonth fromDays nth dow m y = do
  guard $ d > 0 && d <= mdim
  guard $ days > invalidDayThresh
  return $ fromDays (fromIntegral days)
    where
      nth' = fromEnum nth - 4
      mdim = maxDaysInMonth m y
      target = fromEnum dow
      -- forward (nth' >= 0) counts from the first of the month; \"from last\" (nth' < 0) counts back from the last day.
      -- Using the backward distance for the from-last case is what makes \"the last Friday\" land on the final Friday
      -- even when the month ends exactly on that weekday (where a naive forward offset would be a week short).
      d | nth' < 0  = mdim - backwardDist + 7 * (nth' + 1)
        | otherwise = 1    + forwardDist  + 7 * nth'
      forwardDist  = (target - dowOf 1)    `mod` 7
      backwardDist = (dowOf mdim - target) `mod` 7
      dowOf dom = dayOfWeekFromDays epochDayOfWeek (yearMonthDayToDays y m dom)
      days = yearMonthDayToDays y m d
{-# INLINE mkFromNthDay #-}

-- | Build a date from a week-numbering rule.  @minWeekDays@ and @weekStart@ define the rule (e.g. @1 Sunday@ for the
--   simple rule where week 1 is the first week with any day in the new year, or @4 Monday@ for ISO-8601).  This is the
--   calendar-agnostic core of a per-calendar @fromWeekDate@.
mkFromWeekDate :: (Enum mon, Enum dow) =>
     Int                                   -- ^ invalid-day threshold
  -> dow                                   -- ^ epoch day of week
  -> (Year -> mon -> DayOfMonth -> Int)    -- ^ yearMonthDayToDays
  -> (Int32 -> d)                          -- ^ fromDays
  -> Int                                   -- ^ minimum days of the new year that fall in week 1
  -> dow                                   -- ^ first day of the week
  -> WeekNumber -> dow -> Year -> Maybe d
mkFromWeekDate invalidDayThresh epochDayOfWeek yearMonthDayToDays fromDays minWeekDays wkStartDoW weekNum dow y = do
  guard $ days > invalidDayThresh
  return $ fromDays (fromIntegral days)
    where
      soyDays = yearMonthDayToDays y (toEnum 0) minWeekDays
      soyDoW = dayOfWeekFromDays epochDayOfWeek soyDays
      startDoWDistance = soyDoW - fromEnum wkStartDoW
      dowDistance = fromEnum dow - fromEnum wkStartDoW
      dowDistance' = if dowDistance < 0 then dowDistance + 7 else dowDistance
      startDays = soyDays - startDoWDistance
      days = startDays + pred weekNum * 7 + dowDistance'
{-# INLINE mkFromWeekDate #-}

moveByDow :: Enum dow =>
     (Int32 -> d)
  -> dow
  -> Int
  -> dow
  -> (Int -> Int -> Int)
  -> (Int -> Int -> Int)
  -> (Int -> Int -> Bool)
  -> Int
  -> d
moveByDow fromDays epochDayOfWeek n dow distanceF adjust cmp days = fromDays days'
  where
    n' = if targetDow `cmp` currentDoW then n-1 else n
    currentDoW = dayOfWeekFromDays epochDayOfWeek days
    targetDow = fromIntegral . fromEnum $ dow
    distance = distanceF targetDow currentDoW
    days' = fromIntegral $ fromIntegral days `adjust` (7 * n') `adjust` distance

dayOfWeekFromDays :: Enum dow => dow -> Int -> Int
dayOfWeekFromDays epochDayOfWeek = normalize . (fromEnum epochDayOfWeek +) . flip mod 7
  where
    normalize n = if n >= 7 then n - 7 else n

commonMonthDayOffsets :: Num a => [a]
commonMonthDayOffsets = 0 : rest
  where
    rest = zipWith (+) daysPerMonth (0:rest)
    daysPerMonth = [31, 30, 31, 30, 31, 31, 30, 31, 30, 31, 31]

-- | 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