This is an alternative site for discovering Elm packages. You may be looking for the official Elm package site instead.

Date.Extra.Duration

A Duration is a length of time that may vary with calendar date and time. It can be used to modify a date.

Represents a period of time expressed in human chronological terms in terms of a calendar which may have varying components based upon the dates involved in the math.

When modify dates using Durations (Day | Month | Week | Year) this module compensates for day light saving hour variations to minimise the scenarios that cause the Hour field in the result to be different to the input date. It can't completely avoid the hour changing as some hours are not a real world date and hence will modify the hour more than the Duration modified.

This behaviour is modelled on momentjs so any observed behaviour that is not the same as momentjs should be raised as an issue.

Note adding or subtracting 24 * Hour units from a date may produce a different answer to adding or subtracting a Day if day light saving transitions occur as part of the date change.

Warning

Be careful if you add Duration Delta to a Date as Duration contains months and Years which are not fixed elapsed times like Period Delta, however if you really need a relative number of months or years then it may meet your needs.

add : Duration -> Int -> Date -> Date

Add duration * count to date.

type Duration = Millisecond | Second | Minute | Hour | Day | Week | Month | Year | Delta DeltaRecord

A Duration is time period that may vary with with calendar and time.

Using Duration adding 24 hours can produce different result to adding 1 day.

type alias DeltaRecord = { year : Int , month : Int , day : Int , hour : Int , minute : Int , second : Int , millisecond : Int }

A multi granularity duration delta.

This does not contain week like Period.DeltaRecord. It does contain month and year.

zeroDelta : DeltaRecord

All zero delta. Useful as a starting point if you want to set a few fields only.

diff : Date -> Date -> DeltaRecord

Return a Period representing date difference. date1 - date2.

If you add the result of this function to date2 with addend of 1 will not always return date1, this is because this module supports human calendar concepts like Day Light Saving, Months with varying number of days dependent on the month and leap years. So the difference between two dates is dependent on when those dates are.

Differences to Period.diff

  • Duration DeltaRecord excludes week field
  • Duration DeltaRecord includes month field
  • Duration DeltaRecord includes year field
  • Day is number of days difference between months.

When adding a Duration DeltaRecord to a date. The larger granularity fields are added before lower granularity fields so Years are added before Months before Days etc.

  • Very different behaviour to Period diff
    • If date1 > date2 then all fields in DeltaRecord will be positive or zero.
    • If date1 < date2 then all fields in DeltaRecord will be negative or zero.
  • Because it deals with non fixed length periods of time

Example 1. days in 2016/05 (May) = 31 days in 2016/04 (Apr) = 30 days in 2016/03 (Mar) = 31

days in 2015/03 (Mar) = 31

diff of "2016/05/15" "2015/03/20" result naive field diff. year 1, month 2, day -5

days "2015/03/20" to "2015/04/01" (31 - 20) = 11 days (12). still in march with 11. days "2015/04/01" to "2016/04/15" (15 - 1) = 14 days months "2016/04/15" to "2016/05/15" 1 months result field diff year 1, month 1, day 26

This logic applies all the way down to milliseconds.

diffDays : Date -> Date -> Int

Returns date1 - date2 as number of days to add to date1 to get to day date2 is on. date1 - date2 in days.

Only calculates days difference and ignores any field smaller than day in calculation.

Copyright (c) 2016-2018 Robin Luiten

module Date.Extra.Duration
    exposing
        ( DeltaRecord
        , Duration(..)
        , add
        , diff
        , diffDays
        , zeroDelta
        )

{-| A Duration is a length of time that may vary with calendar date
and time. It can be used to modify a date.

Represents a period of time expressed in human chronological terms
in terms of a calendar which may have varying components based upon
the dates involved in the math.

When modify dates using Durations (Day | Month | Week | Year) this module
compensates for day light saving hour variations to minimise the scenarios
that cause the Hour field in the result to be different to the input date.
It can't completely avoid the hour changing as some hours are not a real
world date and hence will modify the hour more than the Duration modified.

This behaviour is modelled on momentjs so any observed behaviour that is
not the same as momentjs should be raised as an issue.

Note adding or subtracting 24 * Hour units from a date may produce a
different answer to adding or subtracting a Day if day light saving
transitions occur as part of the date change.

**Warning**

Be careful if you add Duration Delta to a Date as Duration contains months
and Years which are not fixed elapsed times like Period Delta, however if
you really need a relative number of months or years then it may meet
your needs.

@docs add
@docs Duration
@docs DeltaRecord
@docs zeroDelta
@docs diff
@docs diffDays

Copyright (c) 2016-2018 Robin Luiten

-}

-- import Date.Extra.Calendar as Calendar

import Date exposing (Date, Month)
import Date.Extra.Compare as Compare
import Date.Extra.Core as Core
import Date.Extra.Create as Create
import Date.Extra.Internal as Internal
import Date.Extra.Period as Period


{-| A Duration is time period that may vary with with calendar and time.

Using `Duration` adding 24 hours can produce different result to adding 1 day.

-}
type Duration
    = Millisecond
    | Second
    | Minute
    | Hour
    | Day
    | Week
    | Month
    | Year
    | Delta DeltaRecord


{-| A multi granularity duration delta.

This does not contain week like Period.DeltaRecord.
It does contain month and year.

-}
type alias DeltaRecord =
    { year : Int
    , month : Int
    , day : Int
    , hour : Int
    , minute : Int
    , second : Int
    , millisecond : Int
    }


{-| All zero delta.
Useful as a starting point if you want to set a few fields only.
-}
zeroDelta : DeltaRecord
zeroDelta =
    { year = 0
    , month = 0
    , day = 0
    , hour = 0
    , minute = 0
    , second = 0
    , millisecond = 0
    }



{- Return true if this Duration unit compensates for crossing daylight saving
   boundaries.
   TODO this may need to compensate for day light saving for all fields as all of them
   can cause the date to change the zone offset.
-}


requireDaylightCompensateInAdd : Duration -> Bool
requireDaylightCompensateInAdd duration =
    case duration of
        Millisecond ->
            False

        Second ->
            False

        Minute ->
            False

        Hour ->
            False

        Day ->
            True

        Week ->
            True

        Month ->
            True

        Year ->
            True

        --  If day,month,year is non zero in Delta then compensate.
        Delta delta ->
            delta.day /= 0 || delta.month /= 0 || delta.year /= 0


{-| Add duration * count to date.
-}
add : Duration -> Int -> Date -> Date
add duration addend date =
    let
        -- _ = Debug.log "add" (duration, addend)
        outputDate =
            doAdd duration addend date
    in
        if requireDaylightCompensateInAdd duration then
            daylightOffsetCompensate date outputDate
        else
            outputDate


doAdd : Duration -> Int -> Date -> Date
doAdd duration addend date =
    case duration of
        Millisecond ->
            Period.add Period.Millisecond addend date

        Second ->
            Period.add Period.Second addend date

        Minute ->
            Period.add Period.Minute addend date

        Hour ->
            Period.add Period.Hour addend date

        Day ->
            Period.add Period.Day addend date

        Week ->
            Period.add Period.Week addend date

        Month ->
            addMonth addend date

        Year ->
            addYear addend date

        Delta delta ->
            doAdd Year delta.year date
                |> doAdd Month delta.month
                |> Period.add
                    (Period.Delta
                        { week = 0
                        , day = delta.day
                        , hour = delta.hour
                        , minute = delta.minute
                        , second = delta.second
                        , millisecond = delta.millisecond
                        }
                    )
                    addend


daylightOffsetCompensate : Date -> Date -> Date
daylightOffsetCompensate dateBefore dateAfter =
    let
        offsetBefore =
            Create.getTimezoneOffset dateBefore

        offsetAfter =
            Create.getTimezoneOffset dateAfter
    in
        -- this 'fix' can only happen if the date isnt allready shifted ?
        if offsetBefore /= offsetAfter then
            let
                adjustedDate =
                    Period.add
                        Period.Millisecond
                        ((offsetAfter - offsetBefore) * Core.ticksAMinute)
                        dateAfter

                adjustedOffset =
                    Create.getTimezoneOffset adjustedDate
            in
                -- our timezone difference compensation caused us to leave the
                -- the after time zone this indicates we are falling in a place
                -- that is shifted by daylight saving so do not compensate
                if adjustedOffset /= offsetAfter then
                    dateAfter
                else
                    adjustedDate
        else
            dateAfter



{- Return a date with month count added to date.

   New version leveraging daysFromCivil does not loop
   over months so faster and only compensates at outer
   level for DST.

   Expects input in local time zone.
   Return is in local time zone.
-}


addMonth : Int -> Date -> Date
addMonth monthCount date =
    let
        year =
            Date.year date

        monthInt =
            Core.monthToInt (Date.month date)

        day =
            Date.day date

        inputCivil =
            Internal.daysFromCivil year monthInt day

        newMonthInt =
            monthInt + monthCount

        targetMonthInt =
            newMonthInt % 12

        yearOffset =
            if newMonthInt < 0 && targetMonthInt /= 0 then
                (newMonthInt // 12) - 1
                -- one extra year than the negative modulus
            else
                newMonthInt // 12

        newYear =
            year + yearOffset

        newDay =
            min (Core.daysInMonth newYear (Core.intToMonth newMonthInt)) day

        -- _ = Debug.log "addMonth a" ((year, monthInt, day)
        --     , "yearOffset", yearOffset, newMonthInt
        --     , "a", (newYear, targetMonthInt, newDay))
        newCivil =
            Internal.daysFromCivil newYear targetMonthInt newDay

        daysDifferent =
            newCivil - inputCivil

        -- _ = Debug.log "addMonth b" (newCivil, inputCivil, newCivil - inputCivil)
    in
        Period.add Period.Day daysDifferent date



{- Return a date with year count added to date. -}


addYear : Int -> Date -> Date
addYear yearCount date =
    addMonth (12 * yearCount) date


{-| Return a Period representing date difference. date1 - date2.

If you add the result of this function to date2 with addend of 1
will not always return date1, this is because this module supports
human calendar concepts like Day Light Saving, Months with varying
number of days dependent on the month and leap years. So the difference
between two dates is dependent on when those dates are.

**Differences to Period.diff**

  - Duration DeltaRecord excludes week field
  - Duration DeltaRecord includes month field
  - Duration DeltaRecord includes year field
  - Day is number of days difference between months.

When adding a Duration DeltaRecord to a date.
The larger granularity fields are added before lower granularity fields
so Years are added before Months before Days etc.

  - Very different behaviour to Period diff
      - If date1 > date2 then all fields in DeltaRecord will be positive or zero.
      - If date1 < date2 then all fields in DeltaRecord will be negative or zero.
  - Because it deals with non fixed length periods of time

Example 1.
days in 2016/05 (May) = 31
days in 2016/04 (Apr) = 30
days in 2016/03 (Mar) = 31

days in 2015/03 (Mar) = 31

diff of "2016/05/15" "2015/03/20"
result naive field diff.
year 1, month 2, day -5

days "2015/03/20" to "2015/04/01" (31 - 20) = 11 days (12). still in march with 11.
days "2015/04/01" to "2016/04/15" (15 - 1) = 14 days
months "2016/04/15" to "2016/05/15" 1 months
result field diff
year 1, month 1, day 26

This logic applies all the way down to milliseconds.

-}
diff : Date -> Date -> DeltaRecord
diff date1 date2 =
    if Compare.is Compare.After date1 date2 then
        positiveDiff date1 date2 1
    else
        positiveDiff date2 date1 -1


{-| Return diff between dates.

It returns date1 - date2 in a DeltaRecord.

Precondition for this function is date1 must be after date2.
Input multiplier is used to multiply output fields as needed for caller,
this is used to conditionally negate them in initial use case.

-}
positiveDiff : Date -> Date -> Int -> DeltaRecord
positiveDiff date1 date2 multiplier =
    let
        year1 =
            Date.year date1

        year2 =
            Date.year date2

        month1Mon =
            Date.month date1

        month2Mon =
            Date.month date2

        month1 =
            Core.monthToInt month1Mon

        month2 =
            Core.monthToInt month2Mon

        day1 =
            Date.day date1

        day2 =
            Date.day date2

        hour1 =
            Date.hour date1

        hour2 =
            Date.hour date2

        minute1 =
            Date.minute date1

        minute2 =
            Date.minute date2

        second1 =
            Date.second date1

        second2 =
            Date.second date2

        msec1 =
            Date.millisecond date1

        msec2 =
            Date.millisecond date2

        -- _ =
        --     Debug.log "diff>>" ( ( year1, year2 ), ( month1, month2 ), ( day1, day2 ) )
        accumulatedDiff acc v1 v2 maxV2 =
            if v1 < v2 then
                ( acc - 1, maxV2 + v1 - v2 )
            else
                ( acc, v1 - v2 )

        daysInDate1Month =
            Core.daysInMonth year1 month1Mon

        daysInDate2Month =
            Core.daysInMonth year2 month2Mon

        -- _ =
        --     Debug.log "daysInDate2Month" ( year2, month2Mon, daysInDate2Month )
        ( yearDiff, monthDiffA ) =
            accumulatedDiff (year1 - year2) month1 month2 12

        -- _ =
        --     Debug.log "diff year" ( (year1 - year2), ( yearDiff, monthDiffA ) )
        ( monthDiff, dayDiffA ) =
            accumulatedDiff monthDiffA day1 day2 daysInDate2Month

        -- _ =
        --     Debug.log "diff month" ( monthDiffA, monthDiff, daysInDate2Month )
        ( dayDiff, hourDiffA ) =
            accumulatedDiff dayDiffA hour1 hour2 24

        -- _ =
        --     Debug.log "diff day" ( dayDiffA, dayDiff )
        ( hourDiff, minuteDiffA ) =
            accumulatedDiff hourDiffA minute1 minute2 60

        ( minuteDiff, secondDiffA ) =
            accumulatedDiff minuteDiffA second1 second2 60

        ( secondDiff, msecDiff ) =
            accumulatedDiff secondDiffA msec1 msec2 1000

        -- Need to carry negative differences to next higher unit
        -- to make all differences output positive.
        propogateCarry current carry maxVal =
            let
                adjusted =
                    current + carry
            in
                if adjusted < 0 then
                    ( maxVal + adjusted, -1 )
                else
                    ( adjusted, 0 )

        ( msecX, secondCarry ) =
            propogateCarry msecDiff 0 1000

        ( secondX, minuteCarry ) =
            propogateCarry secondDiff secondCarry 60

        ( minuteX, hourCarry ) =
            propogateCarry minuteDiff minuteCarry 60

        ( hourX, dayCarry ) =
            propogateCarry hourDiff hourCarry 60

        -- if dayDiff + dayCarry is negative
        --    then add days in date1 month to make dayDiff positive
        --    and carry Month negatve
        ( dayX, monthCarry ) =
            propogateCarry dayDiff dayCarry daysInDate1Month

        ( monthX, yearCarry ) =
            propogateCarry monthDiff monthCarry 12

        -- maxVal parameter no effect for year
        ( yearX, _ ) =
            propogateCarry yearDiff yearCarry 0
    in
        { year = yearX * multiplier
        , month = monthX * multiplier
        , day = dayX * multiplier
        , hour = hourX * multiplier
        , minute = minuteX * multiplier
        , second = secondX * multiplier
        , millisecond = msecX * multiplier
        }


{-| Returns date1 - date2 as number of days to add to date1 to get to day date2 is on.
`date1 - date2 in days`.

Only calculates days difference and ignores any field smaller than day in calculation.

-}
diffDays : Date -> Date -> Int
diffDays date1 date2 =
    if Compare.is Compare.After date1 date2 then
        positiveDiffDays date1 date2 1
    else
        positiveDiffDays date2 date1 -1


{-| Return number of days added to date1 to produce date2
-}
positiveDiffDays : Date -> Date -> Int -> Int
positiveDiffDays date1 date2 multiplier =
    let
        date1DaysFromCivil =
            Internal.daysFromCivil
                (Date.year date1)
                (Core.monthToInt (Date.month date1))
                (Date.day date1)

        date2DaysFromCivil =
            Internal.daysFromCivil
                (Date.year date2)
                (Core.monthToInt (Date.month date2))
                (Date.day date2)
    in
        (date1DaysFromCivil - date2DaysFromCivil) * multiplier