packages feed

hodatime-1.1.0.0: src/Data/HodaTime/Period.hs

{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE RoleAnnotations #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE UndecidableInstances #-}

-- |
-- Module      : Data.HodaTime.Period
-- Description : Calendar-relative amounts of time.
--
-- A 'Period' is indexed by the type it can be applied to. Unit constructors
-- constrain that target, so combining date and time units with '<>' infers a
-- target that supports both sets of fields.
--
-- For example, a mixed period can be applied to a 'CalendarDateTime', while
-- either half can also be used independently with a date or time:
--
-- @
-- applyPeriod (months 5 <> hours 2) calendarDateTime
-- applyPeriod (months 5) calendarDate
-- applyPeriod (hours 2) localTime
-- @
--
-- A reusable top-level binding needs either a target annotation or
-- @NoMonomorphismRestriction@. With the latter, GHC generalizes
-- @months 5 <> hours 2@ to a period requiring both 'HasDate' and
-- 'HasLocalTime'.
module Data.HodaTime.Period
(
   Period
  ,years
  ,months
  ,weeks
  ,days
  ,hours
  ,minutes
  ,seconds
  ,nanoseconds
  ,negatePeriod
  ,scalePeriod
  ,ApplyPeriod(..)
)
where

import Data.HodaTime.CalendarDateTime.Internal
  (CalendarDateTime(..), Date, HasDate(..), IsCalendar(..), LocalTime(..))
import Data.HodaTime.LocalTime.Internal (HasLocalTime)

-- | A calendar-relative amount applicable to @target@.
--
-- The constructor is hidden so the constraints introduced by the unit
-- constructors cannot be bypassed.
data Period target = Period
  { periodYears :: !Int
  , periodMonths :: !Int
  , periodWeeks :: !Int
  , periodDays :: !Int
  , periodHours :: !Int
  , periodMinutes :: !Int
  , periodSeconds :: !Int
  , periodNanoseconds :: !Int
  }
  deriving (Eq, Show)

type role Period nominal

instance Semigroup (Period target) where
  Period y1 mo1 w1 d1 h1 mi1 s1 ns1 <> Period y2 mo2 w2 d2 h2 mi2 s2 ns2 =
    Period (y1 + y2) (mo1 + mo2) (w1 + w2) (d1 + d2)
      (h1 + h2) (mi1 + mi2) (s1 + s2) (ns1 + ns2)

instance Monoid (Period target) where
  mempty = Period 0 0 0 0 0 0 0 0

-- | Construct a period measured in calendar years.
years, months, weeks, days :: HasDate target => Int -> Period target
years value = mempty { periodYears = value }
-- | Construct a period measured in calendar months.
months value = mempty { periodMonths = value }
-- | Construct a period measured in seven-day calendar weeks.
weeks value = mempty { periodWeeks = value }
-- | Construct a period measured in calendar days.
days value = mempty { periodDays = value }

-- | Construct a period measured in hours.
hours, minutes, seconds, nanoseconds :: HasLocalTime target => Int -> Period target
hours value = mempty { periodHours = value }
-- | Construct a period measured in minutes.
minutes value = mempty { periodMinutes = value }
-- | Construct a period measured in seconds.
seconds value = mempty { periodSeconds = value }
-- | Construct a period measured in nanoseconds.
nanoseconds value = mempty { periodNanoseconds = value }

-- | Negate every component of a period.
negatePeriod :: Period target -> Period target
negatePeriod = scalePeriod (-1)

-- | Multiply every component of a period by an integer.
scalePeriod :: Int -> Period target -> Period target
scalePeriod factor (Period y mo w d h mi s ns) =
  Period (factor * y) (factor * mo) (factor * w) (factor * d)
    (factor * h) (factor * mi) (factor * s) (factor * ns)

-- | Types to which periods can be applied.
class ApplyPeriod target where
  applyPeriod :: Period target -> target -> target

instance ApplyPeriod LocalTime where
  applyPeriod period = snd . applyTimePeriod period

instance (IsCalendar cal, Enum (Month cal)) => ApplyPeriod (Date cal) where
  applyPeriod = applyDatePeriod

instance (IsCalendar cal, Enum (Month cal)) => ApplyPeriod (CalendarDateTime cal) where
  applyPeriod period (CalendarDateTime date time) =
    CalendarDateTime (shiftDateByDays carry (applyDatePeriod period date)) time'
    where
      (carry, time') = applyTimePeriod period time

-- Noda Time applies period fields from largest to smallest. Keeping year and
-- month as separate operations preserves that behavior when either clamps an
-- end-of-month date.
applyDatePeriod :: (HasDate target, Enum (MoY target)) => Period periodTarget -> target -> target
applyDatePeriod period =
    shiftDateByDays (periodDays period)
  . shiftDateByDays (7 * periodWeeks period)
  . adjustDate (fromEnum . month) setMonthIndex (periodMonths period)
  . adjustDate year setYear (periodYears period)

shiftDateByDays :: HasDate target => Int -> target -> target
shiftDateByDays = adjustDate day setDay

adjustDate
  :: (target -> Int)
  -> (Int -> target -> target)
  -> Int
  -> target
  -> target
adjustDate getter setter amount target = setter (getter target + amount) target

applyTimePeriod :: Period target -> LocalTime -> (Int, LocalTime)
applyTimePeriod period (LocalTime currentSeconds currentNanoseconds) =
  (fromInteger carry, LocalTime (fromInteger secondsOfDay) (fromInteger nanos))
  where
    nanosPerSecond = 1000000000 :: Integer
    nanosPerDay = 86400 * nanosPerSecond
    current =
      (toInteger currentSeconds * nanosPerSecond) + toInteger currentNanoseconds
    delta =
      (((toInteger (periodHours period) * 60
        + toInteger (periodMinutes period)) * 60
        + toInteger (periodSeconds period)) * nanosPerSecond)
        + toInteger (periodNanoseconds period)
    (carry, withinDay) = (current + delta) `divMod` nanosPerDay
    (secondsOfDay, nanos) = withinDay `divMod` nanosPerSecond