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

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


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

  addMonthsTruncated = addTimeUnitTruncate gregorianMonths

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:

  add with truncation
  add with rolling over
  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:
> what about year 0?

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

I'm not so sure about this one. It introduces a new class that means 
"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 

Ashley Yakeley, Seattle WA

More information about the Libraries mailing list