# Time Library Date Normalisation and Arithmetic

Ashley Yakeley ashley at semantic.org
Mon Jul 11 05:53:25 EDT 2005

```In article <b0aab04e050710104040c930f7 at mail.gmail.com>,
Brian Smith <brianlsmith at gmail.com> wrote:

> Usually people want normalization to happen automatically when doing date
> arithmetic and I/O. What is the use case for having a representation for
> invalid dates? I think it should not even be possible to have a date
> 2005/12/32 or 2005/01/-4. Why not make GregorianDay, ISOWeek, YearDay
> abstract, and then provide explicit construction functions that normalize
> and/or check validity automatically?

A normalising construction function is a good idea. But if we hide the
GregorianDay constructor, two of your use cases become slightly harder:

"How do I truncate a date to the first of the month?"

"How do I truncate a date to the first day of the year it occurred in?"

Also bear in mind that all the instances of DayEncoding are isomorphic
(considering only normalised values). So if GregorianDay is abstract, we
might as well use ModJulianDay or somesuch (but a newtype rather than a
synonym of Integer):

newtype Day = ModJulianDay Integer
gregorianYear :: Day -> Integer
gregorianMonth :: Day -> Int
gregorianDay :: Day -> Int
gregorianDayOfYear :: Day -> Int
gregorian :: Day -> (Integer,Int,Int)
showGregorian :: Day -> String -- probably 'show' also
makeGregorianTruncate :: Integer -> Int -> Int -> Day
makeGregorianCheck :: Integer -> Int -> Int -> Maybe Day
isoWeekYear :: Day -> Integer
isoWeekNumber :: Day -> Int
etc.

This is actually quite appealing, though it's a rather radical change.
The answers to the use-cases above become

d' = makeGregorianTruncate (gregorianYear d) (gregorianMonth d) 1

d' = makeGregorianTruncate (gregorianYear d) 1 1

Opinions?

> Secondly, does date arithmetic really need to be this complicated? I have
> managed with the following two date arithmetic functions for quite a while:

> > addMonthsTruncated :: Int -> GregorianDay -> GregorianDay

> > addDays :: Int -> GregorianDay -> GregorianDay

> > d' = addTimeUnitTruncate gregorianMonths 3 d
>
>
> I think that 'addMonthsTruncated 3' is a lot clearer.

Mine is just one symbol longer:

I want to reduce the number of exposed symbols. The time-units to deal
with are:

days & weeks
Gregorian months & years
ISO numbered-week years
(units of other calendars)

For each we want to:

find the number in difference between two dates

Is it better to have simple functions for each combination (your scheme)
or selector functions (my original scheme)? I don't know. Perhaps I
could shorten the name to "addTruncate" or somesuch.

> > module System.Calendar.Gregorian
> > ( Date -- abstract
> > , DateTime -- synonym
> >
> > --* Constructing a Date
> > , fromYMD
> > , normalizedFromYMD
> >
> > --* Deconstruction and arithmetic
> > , Gregorian
> >
> > --* Misc
> > , lastDayOfMonth
> > )
> > import System.Time(DayEncoding,DayAndTime)

I like fromYMD, normalizedFromYMD, and lastDayOfMonth, though we might
also consider this:

gregorianMonthLength :: Integer -> Int -> Int

> Dates with positive
> years are A.D., and dates with negative years are B.C. TODO:

ISO 8601 has year 0 for 1 BCE, year -1 for 2 BCE, and so on, so we can
just stick to that. The extension of the Gregorian calendar to before
its adoption is known as the "Proleptic Gregorian calendar".

> Deconstruction and arithmetic on Gregorian dates are defined
> for Date, DateTime, and Zoned DateTime.
> Examples:
>
> > class Gregorian d

"contains a GregorianDay". I think doing the transformation in
constructors in simpler. I would prefer this:

addDays :: (DayEncoding d) => Integer -> d -> d
diffDays :: (DayEncoding d) => d -> d -> Integer
addGregorianMonthsTruncate :: Integer -> GregorianDay -> GregorianDay
addGregorianMonthsRollover :: Integer -> GregorianDay -> GregorianDay
diffGregorianMonths :: GregorianDay -> GregorianDay -> Integer

My current approach to ease of use is to replicate some of this
functionality with the CalendarTime type.

I also wonder if I shouldn't put the modules in Data.Time instead of
System.Time.

--
Ashley Yakeley, Seattle WA

```