Time library design proposal

I read the discussion of Time Library organization, but did not feel like it arrived anywhere. The current design seems like it was expedient for its time, but does not feel particularly robust. It does not handle typical usecases well and provides little protection from user error. So I'm going to take a shot at a proposal: ---- The Y2K problem --------------- It would be nice if the library was explicit about whether year was 2 or 4 digits. Why not define year like this: newtype Year = Year Int -- don't export constructor mkYear century centyear = Year (100*century+centyear) mkBigYear millenium cent centyear = ... Allow Dates without Times ------------------- There are lots of contexts in which you want a date and not a time e.g. storing birthdates. Is there any reason the Time library shouldn't have a standard Date type e.g. data Date = {month::Month,day::Day,year::Year} Don't allow imposible weekdays ------------------------------ Weekday and yearday seem like they should be functions on this Date type and not part of the datastructure (you shouldn't be able to produce weekdays that don't match dates). weekDay::Date->WeekDay Don't allow impossible times ---------------------------- The type of hour minute and second should protect the user from 28 o'clock and making appointments for 31:101 PM So we probably want: data Hour = H0 | H1 | H2 ... H23 data Minute = M0 | M1 | M2 ... M59 data Second = S0 | S1 | S2 ... S59 data AMPM = AM | PM Times Types ----------- There are also lots of contexts where you want a time but not a date e.g. an alarm clock.... data Time = {hour::Hour ,minute::Minute ,second::Second ,millis::Int ,isDST::Bool, ,timeZone::TimeZone } If you have a leap second time that doesn't work in this model, round it! The 99.9999% case does not need to be sacrificed for the occasional physicist who need to know that the time is is 24:00:03. Re millis, yes it seems like it should be Rational, but this is human meaninful time. And humans don't care about millis. We can then produce a DateTime as data DateTime = DateTime {date::Date,time::Time} Epochal High Precision Time -------------------------- If you are doing physicist calculations, then use: data EpochTime a = EpochTime {seconds::Integer ,fraction::Ratio a ,precision::Integer } precision specifies the maximum size of the denominator of the fraction. epochTimeToDateTime::Epoch->DateTime The existing TimeDiff is totally bizarre. If tdSec == 3600 is that an error or is that the same as tdSec == 0 and tdHour == 1? How many months are a difference in months? It all seems too application specific to belong in a library. I don't have a particular good idea here. I think EpochTime does what you need in a library. The last major element we need is: getEpochTime::IO EpochTime Analogous to the old getClockTime but includes the resolution of the system clock. -Alex- ______________________________________________________________ S. Alexander Jacobson tel:917-770-6565 http://alexjacobson.com

"S. Alexander Jacobson"
The Y2K problem --------------- It would be nice if the library was explicit about whether year was 2 or 4 digits. Why not define year like this:
newtype Year = Year Int -- don't export constructor mkYear century centyear = Year (100*century+centyear) mkBigYear millenium cent centyear = ...
Because it's easier to use the actual year number.
The type of hour minute and second should protect the user from 28 o'clock and making appointments for 31:101 PM So we probably want:
data Hour = H0 | H1 | H2 ... H23 data Minute = M0 | M1 | M2 ... M59 data Second = S0 | S1 | S2 ... S59 data AMPM = AM | PM
Again, it's simpler to use actual numbers. These values almost never appear as literals in a program, so they would have to be converted to/from numbers anyway, which is unnecessary and no other language does this.
There are also lots of contexts where you want a time but not a date e.g. an alarm clock....
And there are contexts where you want a weekday and hour/minute but not a month or second (a weekly schedule), contexts where you want month/day but not a year (a holiday with a fixed date) etc. Where to stop? -- __("< Marcin Kowalczyk \__/ qrczak@knm.org.pl ^^ http://qrnik.knm.org.pl/~qrczak/

In article <87k6prrgb2.fsf@qrnik.zagroda>,
Marcin 'Qrczak' Kowalczyk
There are also lots of contexts where you want a time but not a date e.g. an alarm clock....
And there are contexts where you want a weekday and hour/minute but not a month or second (a weekly schedule), contexts where you want month/day but not a year (a holiday with a fixed date) etc. Where to stop?
Perl's DateTime has an "Incomplete" class for representing incompletely specified times. Someone might do something similiar in Haskell like this: data IncompleteDateTime = MkIncompleteDateTime incYear :: Maybe Int incMonth :: Maybe Int incDay :: Maybe Int incHour :: Maybe Int ... An interesting idea, but probably not useful enough to put in a standard library. -- Ashley Yakeley, Seattle WA

On 2005 February 01 Tuesday 22:15, Ashley Yakeley wrote:
Marcin 'Qrczak' Kowalczyk
wrote: And there are contexts where you want a weekday and hour/minute but not a month or second (a weekly schedule), contexts where you want month/day but not a year (a holiday with a fixed date) etc. Where to stop?
Perl's DateTime has an "Incomplete" class for representing incompletely specified times.
newtype AbsPicoseconds = AbsPicoseconds Integer newtype AbsSeconds = AbsSeconds Integer newtype AbsDays = AbsDays Integer and more newtype AbsHours = AbsHours Integer newtype AbsMonths = AbsMonths Integer newtype AbsYears = AbsYears Integer
If there were types for various intervals of time, as Ben Rudiak-Gould wrote: then an incomplete time would amount to a function from a less precise time interval to a more precise time interval. Various types of incomplete time fall right out. This wouldn't handle the conversion between "local time", and time with a specified zone.

The overall goal here is to make sure that when you have a function that takes a Date or Time type, the values are consistent with the intended invariants of the type! On Wed, 2 Feb 2005, Marcin 'Qrczak' Kowalczyk wrote:
or 4 digits. Why not define year like this:
newtype Year = Year Int -- don't export constructor mkYear century centyear = Year (100*century+centyear) mkBigYear millenium cent centyear = ...
Because it's easier to use the actual year number.
Ok, but we have the common enough case of 2 digit vs 4 digit year that we might as well handle it. The Y2K problem is an existence proof Perhaps mkYear returns an error if the argument is not a four digit year. The point is to make it difficult to make appointments for times that occured during the Roman Empire.
The type of hour minute and second should protect the user from 28 o'clock and making appointments for 31:101 PM So we probably want:
data Hour = H0 | H1 | H2 ... H23 data Minute = M0 | M1 | M2 ... M59 data Second = S0 | S1 | S2 ... S59
Again, it's simpler to use actual numbers. These values almost never appear as literals in a program, so they would have to be converted to/from numbers anyway, which is unnecessary and no other language does this.
Its only simpler to use actual numbers if the library does not provide the services to make it easy to use these values. And we should certainly prefer to produce errors at constructor time rather than at destructor time. It should not be possible to construct a Time like 43:84:00.
There are also lots of contexts where you want a time but not a date e.g. an alarm clock....
And there are contexts where you want a weekday and hour/minute but not a month or second (a weekly schedule), contexts where you want month/day but not a year (a holiday with a fixed date) etc. Where to stop?
Are you seriously saying that you never have use for a Date without a Time? On what date were you born? In my particular case, I am storing a lot of dates and don't also want to consumer memory for Time data that I won't use. Note: I would grant that Time without Date is a lot less useful but it seems like the way to go once you have a Date type. Perl's incomplete type misses the point I am making here. -Alex- ______________________________________________________________ S. Alexander Jacobson tel:917-770-6565 http://alexjacobson.com

"S. Alexander Jacobson"
newtype Year = Year Int -- don't export constructor mkYear century centyear = Year (100*century+centyear) mkBigYear millenium cent centyear = ...
Because it's easier to use the actual year number.
Ok, but we have the common enough case of 2 digit vs 4 digit year that we might as well handle it. The Y2K problem is an existence proof
And your proposal actually makes this worse. It encourages working on a year-of-the-century and century separately, which was the cause of the Y2K problem. It's better to encourage working on the year number as a whole, so all rollovers happen automatically.
Perhaps mkYear returns an error if the argument is not a four digit year. The point is to make it difficult to make appointments for times that occured during the Roman Empire.
If you want to prevent constructing dates with such years, it can be done when the Date object is constructed. I'm not convinced that it would be a good idea though (perhaps the program uses 1.01.0001 as a reference point to simplify calculations? or as minus infinity in a time span, since our library doesn't support infinities?).
And we should certainly prefer to produce errors at constructor time rather than at destructor time. It should not be possible to construct a Time like 43:84:00.
You can prevent it by checking the range in the constructor function. You must do this anyway for the day of the month, so why not for the minute of the hour? BTW, I would prefer that out of range values cause adjustments of higher units, as in most date calculation libraries in various languages. This simplifies computing things like "the beginning of the next month" which work in December too.
Are you seriously saying that you never have use for a Date without a Time? On what date were you born? In my particular case, I am storing a lot of dates and don't also want to consumer memory for Time data that I won't use.
If we cared that much about memory, we would pack the whole date + time in a few bytes or something. I would just use the 0:00:00 time. -- __("< Marcin Kowalczyk \__/ qrczak@knm.org.pl ^^ http://qrnik.knm.org.pl/~qrczak/

In article
Are you seriously saying that you never have use for a Date without a Time? On what date were you born?
There's something to be said for representing date and time of day separately in calendar time, something like this: data CalendarDate = CalendarDate { cdYear :: Int, cdMonth :: Int, cdDay :: Int } data TimeOfDay = TimeOfDay { todHour :: Int, todMin :: Int, todSec :: Int, todPicosec :: Integer -- or nanosec } data CalendarTime = CalendarTime { ctDate :: CalendarDate, ctTime :: TimeOfDay } getDateGregorian :: JulianDay -> CalendarDate -- much less useful, but easy to write getDateJulian :: JulianDay -> CalendarDate I can see a number of applications for calendar dates by themselves and perhaps times of day also. This also might be more internationalisation-friendly, as users can create their own calendar types and still use TimeOfDay. On the other hand, it adds two levels of construction to CalendarTime. -- Ashley Yakeley, Seattle WA

getDateGregorian :: JulianDay -> CalendarDate
-- much less useful, but easy to write getDateJulian :: JulianDay -> CalendarDate
I can see a number of applications for calendar dates by themselves and perhaps times of day also. This also might be more internationalisation-friendly, as users can create their own calendar types and still use TimeOfDay.
On the other hand, it adds two levels of construction to CalendarTime.
Don't forget decimal time (100 seconds = 1 minute, 100 minutes = 1 hour, 10 hours = 1 day)... I still feel using a simple single unit counter (picoseconds?) and then defining a class allowing different representations is the most flexible way to do this... Ignoring the TAI/UTC issue, this would be how I would define the interface: type TAI = Word128 getTAI :: IO TAI -- or Integer or Word64? class Calendar c where fromTAI :: TAI -> c toTAI :: c -> TAI data Gregorian = Gregorian { second :: Int, minute :: Int, hour :: Int, dayOfMonth :: Int, dayOfWeek :: Int, weekOfYear :: Int, monthOfYear :: Int, Year :: Int } -- enumerations may make more sense for dayOfWeek/monthOfYear This allows users to implement alternative arrangements and convert to and from TAI. Keean.

On Wednesday 02 February 2005 05:42, S. Alexander Jacobson wrote:
The overall goal here is to make sure that when you have a function that takes a Date or Time type, the values are consistent with the intended invariants of the type!
Right. But see below.
On Wed, 2 Feb 2005, Marcin 'Qrczak' Kowalczyk wrote:
or 4 digits. Why not define year like this:
newtype Year = Year Int -- don't export constructor mkYear century centyear = Year (100*century+centyear) mkBigYear millenium cent centyear = ...
Because it's easier to use the actual year number.
Ok, but we have the common enough case of 2 digit vs 4 digit year that we might as well handle it. The Y2K problem is an existence proof
I support most of your suggestions, but this one makes no sense at all. Just use twoDigitYear year = year `mod` 100 if you really want only the last two digits.
The type of hour minute and second should protect the user from 28 o'clock and making appointments for 31:101 PM So we probably want:
data Hour = H0 | H1 | H2 ... H23 data Minute = M0 | M1 | M2 ... M59 data Second = S0 | S1 | S2 ... S59
Again, it's simpler to use actual numbers. These values almost never appear as literals in a program, so they would have to be converted to/from numbers anyway, which is unnecessary and no other language does this.
Its only simpler to use actual numbers if the library does not provide the services to make it easy to use these values.
Let us not talk so much about data representations. Most important is the interface. We can have Data as an ADT and use constructors that check validity of month & day values and throw exceptions if check fails. Same with hours, minutes, etc..
And we should certainly prefer to produce errors at constructor time rather than at destructor time. It should not be possible to construct a Time like 43:84:00.
Yes.
There are also lots of contexts where you want a time but not a date e.g. an alarm clock....
And there are contexts where you want a weekday and hour/minute but not a month or second (a weekly schedule), contexts where you want month/day but not a year (a holiday with a fixed date) etc. Where to stop?
Are you seriously saying that you never have use for a Date without a Time? On what date were you born? In my particular case, I am storing a lot of dates and don't also want to consumer memory for Time data that I won't use.
I don't buy the memory wasting argument, but still aggree, that a (Calendar.Gregorian.) Date should only contain year, month, day. The other thing should be TimeOfDay. And then we would have Time = (Date, TimeOfDay) (or a data type instead of tuple). My argument: A thing should be what its name says it is. When I talk about date I mean date and not date plus time of day. Ben -- "The CIA is Wallstreet. Wallstreet is the CIA." (Mike Ruppert)

Marcin 'Qrczak' Kowalczyk
"S. Alexander Jacobson"
writes:
newtype Year = Year Int -- don't export constructor mkYear century centyear = Year (100*century+centyear)
Surely .... = Year (100*(century-1)+centyear) ?
mkBigYear millenium cent centyear = ...
The type of hour minute and second should protect the user from 28 o'clock and making appointments for 31:101 PM So we probably want:
This depends on which calendar you are using. UTC sometimes has 61 seconds. The Ethiopian calendar has 13 months.
There are also lots of contexts where you want a time but not a date e.g. an alarm clock....
And there are contexts where you want a weekday and hour/minute but not a month or second (a weekly schedule), contexts where you want month/day but not a year (a holiday with a fixed date) etc. Where to stop?
This is hard to do if you insist of a calendar being represented as a single record representing points in time, with a one-to-one correspondence to a system clock. I rather think a calendar should let the user specify units in a more natural, flexible way. So I don't particularly like the "incomplete" proposal, filling out the record with Maybes. I'd rather have a (set of) calendar(s) that let me specify precisely which day, minute, or hour I mean, in a way that is natural to me. Perhaps something like: import Calendar.Gregorian easter :: Year -> Day easter yr = first (filter isSunday (daysFrom (springEquinox yr))) easter05 :: Year -> Day easter05 = easter 2005 easter_lunch :: Hour easter_lunch = hours easter05 !! 12 vacation :: [Day] vacation = daysFromTo (june 2005 !! 20) (august 2005 !! 25) -- I wish The coupling between a calendar (as in dates and hours) and time (as in a continous flow measured in seconds) should be loose, but one should of course be able to find out which day, hour, nanosecond any point in time is. -kzm -- If I haven't seen further, it is by standing in the footprints of giants

Ketil Malde
easter :: Year -> Day easter yr = first (filter isSunday (daysFrom (springEquinox yr)))
Thanks to everybody who pointed out that we also need to incorporate isFullMoon or similar in the equation. This omission was, as many of you guessed, of course intentional, and shows in a nice way how one in such a scheme can fix all Easter date mistakes in one's programs in one fell swoop. Try *that* with picoseconds! :-) -kzm -- If I haven't seen further, it is by standing in the footprints of giants
participants (8)
-
Ashley Yakeley
-
Benjamin Franksen
-
Keean Schupke
-
Ketil Malde
-
Marcin 'Qrczak' Kowalczyk
-
S. Alexander Jacobson
-
S. Alexander Jacobson
-
Scott Turner