Chronos is one of the many Smalltalk-related blogs syndicated on Planet Smalltalk
χρόνος

Discussion of the Essence# programming language, and related issues and technologies.

Blog Timezone: America/Los_Angeles [Winter: -0800 hhmm | Summer: -0700 hhmm] 
Your local time:  

2006-01-31

Version 2006a of the Chronos Time Zone Repository Published

Version 2006a of the Olson Time Zone Database has been published, and so version 2006a of the Chronos Time Zone Repository has been generated and published. It's available from the Chronos Web Site.

Direct download link: http://chronos-st.org/images/time-zones.zip.



2006-01-30

DateAndTime Construction--And Nominal Time Invariance

At the cost of adding a few new (small) methods (in the latest development version of Chronos, B1.20) I've made it much easier to compose a DateAndTime by combining a TimeOfDay with a Date (or with an AnnualDate, or with a DateAndTime.) The new instance methods (of TemporalCoordinate and CalendricalCoordinate, respectively) are #on: and #atTimeOfDay: The new class methods (of Timepoint) are #todayAt: and #todayAt:in:.

Here are some example usages:


DateAndTime todayAt: TimeOfDay now
DateAndTime todayAt: TimeOfDay now in: (Duration hours: 5.5)
TimeOfDay now on: YearMonthDay today
TimeOfDay now on: Timepoint today
(TimeOfDay hour: 17 minute: 30 second: 0) on: (YearMonthDay year: 2006 month: June day: 14)
(TimeOfDay hour: 17 minute: 30 second: 0) on: (DateAndTime year: 2006 month: June day: 14)
(TimeOfDay hour: 9 minute: 20 second: 0) on: GregorianEaster canonical
YearMonthDay today atTimeOfDay: TimeOfDay now
DateAndTime today atTimeOfDay: TimeOfDay now
GregorianEaster canonical atTimeOfDay: (TimeOfDay hour: 12 minute: 0 second: 0)


Which produce the following results when evaluated:

2006-01-30T22:15:33.465363-08:00
2006-01-30T22:15:44.920133+05:30
2006-01-30T22:15:53.108186
2006-01-30T22:16:03.176193-08:00
2006-06-14T17:30:00
2006-06-14T17:30:00-07:00
2006-04-16T09:20:00
2006-01-30T22:16:58.624536
2006-01-30T22:17:12.008788-08:00
2006-04-16T12:00:00


You might notice that some of the Timepoints that result from the examples above show a time zone offset, and some don't. Those that show a time zone offset are invariant to Universal Time. Those that don't are invariant to nominal time. A VisualWorks Timestamp has nominal time invariance. A DateAndTime instance that conforms to the ANSI-Smalltalk Standard has Universal Time invariance. Instances of either java.util.Calendar or of java.util.Date also have Universal Time invariance.

If "(DateAndTime year: 2006 month: 1 day: 1 offset: 0 hours) = (DateAndTime year: 2006 month: 1 day: 1 hour: 8 minute: 0 second: 0 offset: 8 hours)" evaluates to true, then the two DateAndTime instances have Universal Time invariance. For Universal-Time invariant time values, the point in Universal Time they designate is their invariant for the purpose of defining their meaning (semantics)--such as whether or not they are equal to some other point-in-time value.

A time value that is invariant to Universal Time will still designate the same point in Universal Time when it is translated or converted to a different time zone.

A time value that is invariant to nominal time always designates the same nominal time regardless of time zone (in other words, it acts just like a VisualWorks Timestamp.) It uses the nominal time it designates as its semantic invariant. It compares as equal to any time value that designates the same nominal time. When bound to a time zone (converted into a point-in-time that is invariant to Universal Time) its local time in the time to which it has become bound is the same as its former nominal time (because its nominal time is its semantic invariant.)

If you create a Core.Timestamp (or a nominal-time invariant Chronos.Timepoint) in an image running in San Francisco, save the image, take it with you on a plane flight to Bangalore, then restart the image, that Core.Timestamp instance would still designate the same nominal time--because there is no "binding" between a nominal-time-invariant time value and any particular time zone.

I know that most of you have been trying to use Core.Timestamps as though they had Universal Time invariance--because that's usually what you need. But Core.Timestamps do not have Universal Time invariance--which is one of the reasons they sometimes don't work for you the way you'd like.

If you need to deal with time values from multiple time zones, you need point-in-time objects with Universal Time invariance. The ANSI-Smalltalk Standard requires them. Chronos provides them. The VisualWorks Core library does not.

Which is not to say that nominal time invariance doesn't have its uses. It certainly does. The canonical example is dates that are literal values in business rules (such as the rules that define which days are holidays.) And trying to use Universal Time invariant time values in situations where they are not appropriate can lead to even worse problems than using nominal time invariant time values when Universal Time invariance is that's needed. Just ask those who've had to use java.util.Calendar for certain use cases.

Note: The latest "relatively stable" development version of Chronos is always available from the Cincom Public StORE Repository.


2006-01-29

Comparative Examples: Chronos/Smalltalk vs. Ruby

The documentation of the Ruby "Date.rb" module contains some example code for the usage of Ruby's Date and DateTime classes.

The Ruby examples, and the Chronos code to do the same things, are presented below:

Print out the date of every Sunday between two dates:

Ruby:


def print_sundays(d1, d2)
d1 +=1 while (d1.wday != 0)
d1.step(d2, 7) do |date| // It's not clear whether this means [d1, d2] or [d1, d2)
puts "#{Date::MONTHNAMES[date.mon]} #{date.day}"
end
end

print_sundays(Date::civil(2003, 4, 8), Date::civil(2003, 5, 23))


Chronos/Smalltalk:

DateAndTimeUtility class>>printSundaysFrom: startDate through: endDate
"Left-closed, right-closed interval: [startDate, endDate]"
| nextSunday |
nextSunday := startDate nextDayOfWeek: Sunday.
nextSunday
through: endDate
every: 1
weeksDo: [:eachSunday |
Transcript
cr;
show: eachSunday localePrintString]
############

DateAndTimeUtility
printSundaysFrom: (YearMonthDay year: 2003 month: April day: 8)
through: (YearMonthDay year: 2003 month: May day: 23)


Note that instead of "startDate nextDayOfWeek: Sunday" one could also do exactly what the Ruby code does: "[date dayOfWeek = Sunday] whileFalse: [date := date tomorrow]."

The Chronos/Smalltalk output:


April 13, 2003
April 20, 2003
April 27, 2003
May 4, 2003
May 11, 2003
May 18, 2003


Calculate how many seconds to go till midnight on New Year’s Day:

Ruby:


def secs_to_new_year(now = DateTime::now())
new_year = DateTime.new(now.year + 1, 1, 1)
dif = new_year - now
hours, mins, secs, ignore_fractions = Date::day_fraction_to_time(dif)
return hours * 60 * 60 + mins * 60 + secs
end

puts secs_to_new_year()


Chronos/Smalltalk:

| now |
now := DateAndTime now.
now secondsUntil:
(now
subtractingYears: 0
months: 0
days: now daysSinceStartOfYear - now daysInYear
seconds: now secondsSinceStartOfDay
nanoseconds: now nanosecondsSinceSecond).


It should be noted that, once Chronos implements leap seconds, the Chronos code will correctly answer the number of UTC seconds--including any leap seconds. The Ruby example wouldn't, even if Date.rb ever implements leap second support. If we didn't care about leap seconds, we could instead use the following Chronos code:


| now |
now := DateAndTime now.
(Duration
days: now daysInYear - now daysSinceStartOfYear
hours: 0
minutes: 0
seconds: now fractionalSecondsSinceStartOfDay negated) asSeconds


And there are yet other ways to do this in Chronos. A few examples:

  • In one line of code:

    | now | (now := DateAndTime now) nextYear atFirstDayOfYear atStartOfDay secondsSince: now

  • Efficient, with logic that's easy to follow:

    | now |
    now := DateAndTime now.
    (YearMonthDay year: now year + 1 day: 1) secondsSince: now


I should also point out that the Ruby code in the example above (as well as the last Chronos variation) would be subject to a possible bug, in the following situation: a) the current time is in the year before the year 1, and b) the calendar system in use forbids the year zero. But that's not a situation most of you would ever have to worry about. I, on the other hand, do have to worry about such issues when writing the logic internal to Chronos, since Chronos deals with multiple calendars--and new calendrical systems can easily be defined and added.

Ever want to design and implement your own Calendar?


Chronos Version B1.18 Published

Version B1.18 includes some new functionality, some design/code improvements, and improved/corrected class/method comments. It also includes a few bug fixes for some rather obscure bugs--things I happened to find while browsing the code and thinking up new things to test. The new version is available from the Chronos web site.

I also spent some time refining the site's aesthetics--but more about that below.

Much of the new functionality involves improved flexibility in date/time fomratting:


  • The ability to suppress the printing of a date's year;

  • The ability to print the time-of-day before the date;

  • The ability to use a ChronosPritingPolicy specification array literal wherever a ChronosPrintPolicy instance or key is usable (e.g., "Timepoint now printStringUsing: #(hideSubsecondFraction)" instead of "Timepoint now printStringUsing: (ChronosPrintPolicy applying: #(hideSubsecondFraction))";

  • The ability to override the default print format for a specific instance, without affecting the global default. Example: "Timepoint now withDefaultPrintPolicy: #(hideSubsecondFraction)." (The ANSI-Smalltalk Standard strictly requires specific default formats for DateAndTime and Duration instances--the required default formats are based on the ISO 8601 Standard.)


Also, in support of the use of JavaScript to enable web pages to show timestamps in the local time of each viewer (see AJAX × Date × Time × Time zones - best practices by Johan Sundstrom (a.k.a "ecmanaut,")) I added the class methods #unixEpoch, #javaEpoch, #st80Epoch and #msWindowsNTEpoch to ChronosSystemClock, making it easy to write code such as the following:

[ :timepoint |
| args utTimepoint generatedString |
utTimepoint := timepoint asUT.
args := Array
with: (utTimepoint withDefaultPrintPolicy: #rfc2822)
with: (utTimepoint millisecondsSince: ChronosSystemClock javaEpoch).
generatedString := '
%<noscript>%<p>Page published <1p> (UT: Universal Time)%</p>%</noscript>
%<script type="text/javascript">
document.write(''Page published ''+formatDateAndTimeAsRFC2822(new Date(<2p>)))
%</script>' expandMacrosWithArguments: args]
value: Timepoint now


The above code produces the following html/JavaScript code, suitiable for inclusion in a web page:


<noscript><p>Page published Sun, 29 Jan 2006 12:10:41 +0000 (UT: Universal Time)</p></noscript>
<script type="text/javascript">
document.write('Page published '+formatDateAndTimeAsRFC2822(new Date(1138536641146)))
</script>


Of course, one must have a JavaScript libray included into the web page that defines the function "formatDateAndTimeAsRFC2822()" (or the equivalent.) The idea is that the source code on the web page specifies the timestamp as a count of milliseconds since the Java epoch (1970-01-01T00:00:00Z,) and relies on the user's operating system, browser and JavaScript execution environment to correctly convert that milliseconds-since-the-Java-epoch count (which is relative to Universal Time) into the user's local date and time.

Here's the JavaScript code I developed for use on the Chronos web site, which ultimately derives from the code published by ecmanaut:


<script type="text/javascript">
function toInteger(n){
return (n < 0 ? - 1 : + 1) * Math.floor(Math.abs(n) + 0.5);
}
function zeropad(n) {
return n>9 ? n : '0'+n;
}
function formatTimeZoneOffset(time, elementSeparator) {
var minutesWestOfUT=time.getTimezoneOffset()
var absOffsetInMinutes=Math.abs(minutesWestOfUT)
var offsetInHours=toInteger(absOffsetInMinutes / 60)
var offsetMinutes = absOffsetInMinutes % 60
var isWestOfUT = minutesWestOfUT > 0
return (isWestOfUT ? '-' : '+')+zeropad(offsetInHours)+elementSeparator+zeropad(offsetMinutes)
}
function formatTimeOfDay(time) {
var hour=time.getHours()
var minute=time.getMinutes()
var second=time.getSeconds()
return zeropad(hour)+':'+zeropad(minute)+':'+zeropad(second)
}
function abbreviationOfMonth(time) {
return ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'][time.getMonth()]
}
function formatDateAsYMD(time, elementSeparator) {
var dayOfMonth=time.getDate()
var month=time.getMonth() + 1
var year=time.getFullYear()
return year+elementSeparator+zeropad(month)+elementSeparator+zeropad(dayOfMonth)
}
function formatDateAsMDY(time, mdSeparator, dySeparator) {
var dayOfMonth=time.getDate()
var year=time.getFullYear()
return abbreviationOfMonth(time)+mdSeparator+zeropad(dayOfMonth)+dySeparator+year
}
function formatDateAsDMY(time, dmSeparator, mySeparator) {
var dayOfMonth=time.getDate()
var year=time.getFullYear()
return zeropad(dayOfMonth)+dmSeparator+abbreviationOfMonth(time)+mySeparator+year
}
function abbreviationOfDayOfWeek(time) {
return ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'][time.getDay()]
}
function formatDateAsRFC2822(time) {
return abbreviationOfDayOfWeek(time)+', '+formatDateAsDMY(time, ' ', ' ')
}

function formatDateAndTimeAsISO8601(time) {
return formatDateAsYMD(time,'-')+'T'+formatTimeOfDay(time)+formatTimeZoneOffset(time, ':')
}
function formatDateAndTimeAsRFC2822(time) {
return formatDateAsRFC2822(time)+' '+formatTimeOfDay(time)+' '+formatTimeZoneOffset(time, '')
}
function startClock()
{
document.getElementById('clock').innerHTML=formatDateAndTimeAsRFC2822(new Date())
t=setTimeout('startClock()',500)
}
</script>


Now that the Chronos web site uses the above JavaScript functions, it is able to display time to the viewer in his or her local time. If JavaScript is disabled, I have code that generates a <noscript> section which presents the timestamp in Uiversal Time.


2006-01-27

AJAX × Date × Time × Time zones - best practices

ecmanaut offers a great rant!

Quote:


Are you developing a site or application which relies on javascript, which somewhere on some page or in some view shows some time, or a date, or even a weekday to the visitor? Registration time, for instance. Or an advance notice of your next scheduled downtime? Anything at all! Do you also let visitors from other cities, and maybe even from other countries, visit your web site?

Great! Then this article is for you.



Comparative Examples: Chronos/Smalltalk vs. MS Dot Net

David McNamee, in his Fun With TimeZone blog entry, explains why one should store time stamps relative to Universal Time, and not relative to local time:


...you have to remember that local time is ambiguous and should really only be used for display purposes. It should never be relied upon for logic or stored in a database. What do I mean by ambiguous? What happens when we switch from Standard Time to Daylight Saving Time? At 1:59AM, we are still on Standard Time. Sixty seconds later, it's three o'clock in the morning. An hour disappears. Did the hour really disappear? No. We just change the clocks for the summer months because Ben Franklin thought we were wasting daylight.

If local time isn't reliable, then how do we record time? What we should really store is Coordinated Universal Time (UTC), which is also known as Greenwich Mean Time (GMT). UTC/GMT never changes. There aren't any bizarre rules to follow. If we store that, we can have reliable calculations based on accurate data. If we like, we can also record the offset from UTC where the data was collected. That would allow us to reconstruct collection patterns during data analysis. That information can be collected on the device and sent back to the server.


Yes. And there's an even worse situation when transitioning from "Daylight Saving Time" (a.k.a. "Summer Time" in most of the world) back to "Standard Time": The hour from 1am to 2am (a left-closed, right open interval; local time; assuming the transition time-of-day is 2am local time, which is not at all the case everywhere) is repeated twice!

Another major issue is timestamps collected from one time zone but stored or processed in another. And then there's the fact that time zone offsets can change for other reasons than DST transitions. Hawaii (which doesn't observe DST) changed its "Standard Time" offset from -10:30 to -10:00 in 1946. Parts of Indiana have switched between Eastern and Central Time. Such things can and do happen.

David also provides some example .Net code for storing timestamps in Universal Time, and for also including the key information needed to reconstruct the original local time from the recorded UT timestamp:

DateTime rightNow = DateTime.Now;
TimeZone timeZone = TimeZone.CurrentTimeZone;

lblCurrentTime.Text = rightNow.ToString();
lblTimeZone.Text =
timeZone.IsDaylightSavingTime(rightNow) ?
timeZone.DaylightName.ToString() :
timeZone.StandardName.ToString();
lblOffset.Text =
timeZone.GetUtcOffset(rightNow).TotalHours.ToString();
lblUTC.Text =
rightNow.ToUniversalTime().ToString();


Although Chronos provides an even better approach for applications that use it, David's approach has the advantage or making life easier for applications that don't or can't use Chronos, when such applications also need to use the timestamp information. So here's the Chronos code that implements David's approach:

| rightNow localTimeText universalTimeText timeZoneKey timeZoneAbbreviation timeZoneStatus offsetText |
rightNow := Timepoint now.
localTimeText := rightNow localePrintString. "Human readable format based on current Locale."
universalTimeText := rightNow asUT printString. "defaults to ISO 8601 format."
timeZoneKey := rightNow timeZone key. "e.g., 'America/New_York'"
timeZoneAbbreviation := rightNow timeZone commonAbbreviation. "e.g., 'EDT'"
timeZoneStatus := rightNow timeZone isStandardTime
ifTrue: [#StandardTime]
ifFalse: [#DaylightSavingTime].
offsetText := rightNow timeZone zuluNotation.
(Dictionary new
at: #timestamp put: rightNow;
at: #localTimeText put: localTimeText;
at: #universalTimeText put: universalTimeText;
at: #timeZoneKey put: timeZoneKey;
at: #timeZoneAbbreviation put: timeZoneAbbreviation;
at: #timeZoneStatus put: timeZoneStatus;
at: #timeZoneOffsetText put: offsetText;
yourself)


And here's the output (line breaks and other formatting added by hand):

Dictionary (
#timestamp-> 2006-01-27T14:19:25.022803-08:00
#localTimeText-> 'January 27, 2006 2:19:25 pm PST'
#universalTimeText-> '2006-01-27T22:19:25.022803+00:00'
#timeZoneKey-> #'America/Los_Angeles'
#timeZoneAbbreviation-> #PST
#timeZoneStatus-> #StandardTime
#timeZoneOffsetText-> 'Z-08:00')



2006-01-26

Comparative Examples: Chronos (and Smalltalk) vs. Java's Calendar Class

David Herron blogs about his attempt to use Java to show the local date and time of a widespread set of team members in Santa Clara CA, Beijing China, Bangalore India, Hyderabad India, St. Petersburg Russia and Dublin Ireland. His situation is becoming much more common, especially among IT professionals.

To see his Java code, and the resulting output, click here.

Here's the equivalent code using Chronos (and Smalltalk):


| universalNow printPolicy |
printPolicy := ConfigurableChronosPrintPolicy applying: #(showTimeZoneVerbosely).
universalNow := Timepoint utNow.
#('America/Los_Angeles' 'Asia/Calcutta' 'Europe/Moscow' 'Europe/Prague' 'Europe/Dublin'
'Europe/London' 'Asia/Tokyo' 'Asia/Novosibirsk' 'Asia/Hong_Kong' 'Asia/Shanghai'
'Asia/Seoul' 'America/Denver' 'America/New_York')
do: [:tzKey |
| localTimeNow |
localTimeNow := universalNow >> tzKey.
Transcript
cr;
show: (localTimeNow % printPolicy)]

The code above produces the following output:

2006-01-26T19:51:43.248535-08:00 (PST:America/Los_Angeles)
2006-01-27T09:21:43.248535+05:30 (IST:Asia/Calcutta)
2006-01-27T06:51:43.248535+03:00 (MSK:Europe/Moscow)
2006-01-27T04:51:43.248535+01:00 (CET:Europe/Prague)
2006-01-27T03:51:43.248535+00:00 (GMT:Europe/Dublin)
2006-01-27T03:51:43.248535+00:00 (GMT:Europe/London)
2006-01-27T12:51:43.248535+09:00 (JST:Asia/Tokyo)
2006-01-27T09:51:43.248535+06:00 (NOVT:Asia/Novosibirsk)
2006-01-27T11:51:43.248535+08:00 (HKT:Asia/Hong_Kong)
2006-01-27T11:51:43.248535+08:00 (CST:Asia/Shanghai)
2006-01-27T12:51:43.248535+09:00 (KST:Asia/Seoul)
2006-01-26T20:51:43.248535-07:00 (MST:America/Denver)
2006-01-26T22:51:43.248535-05:00 (EST:America/New_York)


To get output formatted as RFC 2822, just replace "% printPolicy" with "% #rfc2822". To get output formatted according to the user's default preferences, replace "% printPolicy" with "localePrintString".

With the following improvements, we can sort the output by time zone offset, and get much more readable output:


| universalNow printPolicy |
printPolicy := ConfigurableChronosPrintPolicy applying:
#(showDayOfWeekAbbreviation
useDayMonthYearOrder
useMonthAbbreviation
dateSeparator: $
dateAndTimeSeparator: $
hideSubsecondFraction
timeZoneSeparator: $
timeZoneElementSeparator: nil
showTimeZoneVerbosely).
universalNow := Timepoint utNow.
#('America/Los_Angeles' 'Asia/Calcutta' 'Europe/Moscow' 'Europe/Prague' 'Europe/Dublin'
'Europe/London' 'Asia/Tokyo' 'Asia/Novosibirsk' 'Asia/Hong_Kong' 'Asia/Shanghai'
'Asia/Seoul' 'America/Denver' 'America/New_York')
collect: [:tzKey | universalNow >> tzKey])
asSortedCollection:
[:localTimeA :localTimeB | localTimeA offset > localTimeB offset])
do: [:localTime | Transcript cr; show: localTime % printPolicy]


The improved code produces the following output:

Fri, 27 Jan 2006 14:29:43 +0900 (KST: Asia/Seoul)
Fri, 27 Jan 2006 14:29:43 +0900 (JST: Asia/Tokyo)
Fri, 27 Jan 2006 13:29:43 +0800 (CST: Asia/Shanghai)
Fri, 27 Jan 2006 13:29:43 +0800 (HKT: Asia/Hong_Kong)
Fri, 27 Jan 2006 11:29:43 +0600 (NOVT: Asia/Novosibirsk)
Fri, 27 Jan 2006 10:59:43 +0530 (IST: Asia/Calcutta)
Fri, 27 Jan 2006 08:29:43 +0300 (MSK: Europe/Moscow)
Fri, 27 Jan 2006 06:29:43 +0100 (CET: Europe/Prague)
Fri, 27 Jan 2006 05:29:43 +0000 (GMT: Europe/London)
Fri, 27 Jan 2006 05:29:43 +0000 (GMT: Europe/Dublin)
Fri, 27 Jan 2006 00:29:43 -0500 (EST: America/New_York)
Thu, 26 Jan 2006 22:29:43 -0700 (MST: America/Denver)
Thu, 26 Jan 2006 21:29:43 -0800 (PST: America/Los_Angeles)


Perhaps David should have considered using Joda Time, if he's determined to use Java.


Ooops... Chronos.zip uploaded to wrong directory

When I uploaded the new Chronos.zip with version B1.12 of Chronos earlier today, I uploaded it to the wrong directory. So those of you who fetched the zip file from the Chronos web site, instead of getting the latest version from the Cincom Public Store Repository, got version B1.11, not version B1.12.

Sorry about that. It's fixed now.


Chronos Version B1.12 Published--API Changes, New Functionality

Firstly, there have been some significant API changes to CalendarDuration, Timeperiod, SemanticDatePolicy and SemanticAnnualDateRule. Some of the "example do its" in the old version of the "executable examples" will no longer work in this version (so there's also a new version of the "executable examples.") Further details are listed below.

Secondly, the functionality of SemanticDatePolicy and SemanticAnnualDateRule have been enhanced. They are now able to correctly detect all the non-trading days on the New York Stock Exchange, from 1885 to the present--including the fact that from 1873 through 1952, most (but not all) Saturdays were trading days (actually, they only had 2-hour sessions from 10am to Noon (America/New_York time, of course.)

SemanticDatePolicy has two new "example" class methods: #usFederalBusinessDayCountFrom:through:, and #nyseTradingDayCountFrom:through:. The following statement will compute the number of trading days (on the NYSE) from the all-time DJIA high on 14 Jan 2000 to the recent rally high on 11 Jan 2006 (over 300 times/second on my machine):


SemanticDatePolicy
nyseTradingDayCountFrom: (YearMonthDay year: 2000 month: January day: 14)
through: (YearMonthDay year: 2006 month: January day: 11)


Thirdly, CalendarDuration was refactored into two classes, CalendarDuration and CivilDuration. CalendarDuration itself only represents years, months and days, and does not represent hours, minutes, seconds or fractions of a second. Calendar durations with sub-day temporal extents are now handled by CivilDuration, which inherits from CalendarDuration. CivilDuration should be type-compatible with the old CalendarDuration, and a CalendarDuration will usually auto-convert into a CivilDuration instance when sent any messages that require the bevavior of a CivilDuration (the exceptions will be mentioned below.)

By the way, the reason for the redesign of CalendarDuration was so that when leap seconds are implemented, CivilDurations and CalendarDurations will have the correct behavior. And that probably requires some explanation.

On a normal day, there are 86400 seconds, which are equivalent to 1440 minutes or 24 hours. Each minute of the day has 60 seconds, and each hour has 3600 seconds. Not so on a day with a leap second. A day with a leap second has 86401 seconds. Worse, the minute in which a leap second occurs has 61 seconds, and the hour in which a leap second occurs has 3601 seconds (so that there are still exactly 1440 minutes in the day, and exactly 24 hours.)

So, if one wants to measure time as a scientist would, the easiest approach is to reduce all times (both durations and points in time) to seconds and fractions of a second. Leap seconds (and differences between calendars, for that matter) then disappear. The point-in-time 5000 seconds later than another is a simple matter of adding 5000 to a count of seconds since an epoch. The problems with this approach only arise when one wants to convert from seconds into minutes, hours, days, months and years.

Conversely, if one represents time as years, months, days, hours and minutes, and never deals with seconds, then leap seconds are also invisible, since leap seconds don't change the number of minutes in an hour, the number of hours in a day, the number of days in a month, nor the number of months in a year.

Unfortunately, neither discarding all time units other than seconds, nor ignoring seconds, is a viable option for most people. There's always some reason that a conversion to and/or from seconds and higher-level time units is needed. And leap seconds make that surprisingly tricky.

The old implementation of CalendarDuration stored sub-day time extents internally as just seconds and nanoseconds. That's a reasonable design for a point-in-time value, but in the case of a durational object meant to have civil (business, legal) time semantics, it just doesn't work if leap seconds are supported.

If a duration object is supposed to represent "1 minute, civil time," then when it is added to 2005-12-31T23:59:00Z, the result should be 2006-01-01T00:00:00Z. But if its internal representation of "1 minute" is in fact "60 seconds," then the actual result will be 2005-12-31T23:59:60Z--assuming the point-in-time value correctly implements leap seconds (the final second of 2005 was a leap second.)

The new CivilDuration subclass of CalendarDuration has separate instance variables for hours, minutes, seconds and nanoseconds. So it should also be able to correctly handle "leap hours" or "leap minutes," should those ever be mandated (don't laugh, there's a serious proposal to do just that being hotly debated right now.)

Other New Functionality


  • TemporalCoordinate now implements instance methods such as #addingMinutes:, #addingHours:, #addingDays:, #addingMonths: and #addingYears:, #minutesSince:, #hoursUntil:, #microsecondsSince: and #nanosecondsUntil: (and other similar methods following the same pattern.) In the case of #addingMonths:, #addingYears: and #addingDays:, this just means that TimeOfDay instances will now also understand such messages (and will respond to them as though they were points-in-time of the current day.)

  • TemporalCoordinate now implements instance methods such as #to:every:do:, #through:every:do:, #to:every:daysDo:, #through:every:monthsDo:, and other methods following the same pattern. The "through:" methods enumerate over a closed interval, the "to:" methods enumerate over a left-closed, right-open interval.

  • CalendricalCoordinate now implements instance methods such as #daysSince:, #monthsUntil: and #yearsSince: (and other similar methods following the same pattern.)

  • Core.TimeZone now understands #asChronosValue. It responds with a ChronosTimezone equivalent to itself. Similarly, a ChronosTimezone now understands #asNative. It responds with a Core.TimeZone as congruent to itself as possible (using the rules for the current year.)

  • Core.Time now understands #asChronosValue. It responds with a TimeOfDay equivalent to itself. TimeOfDay now understands #asNative. It responds with a Core.Time representing the same time of day (down to the second, since Core.Time doesn't deal with sub-second times.)



API Changes

  1. The following expression will now result in an MNU: "CalendarDuration days: 0.5." The reason is because the instance of CalendarDuration that is created will now no longer understand the initialization messages needed to set sub-day time periods. Use CivilDuration instead. (In contrast, "CalendarDuration seconds: 5" will still work--but it returns a CivilDuraiton, not a CalendarDuration.) Preventing this problem would have required that the class methods check the input arguments for fractional values--and I really don't want to do that.

  2. The Timeperiod instance methods #seconds, #minutes, #hours, #days, #weeks, #months, #quarters and #years have all been renamed to #secondPeriods, #minutePeriods, #hourPeriods, #dayPeriods, #weekPeriods, #monthPeriods, #quarterPeriods and #yearPeriods (respectively.) Timeperiod>>monthPeriods (for example) answers a collection of the month-long Timeperiods contained by the receiving Timeperiod. Messages named #months (for example) were ambiguous, and conflicted semantically with messages of the same name in other classes.

  3. The CalendricalCoordinate instance methods #calendarDurationSince: and #calendarDurationUntil: still answer instances of CalendarDuration--but that means those methods no longer consider the sub-day temporal extents between the two points in time. The messages #civilDurationSince: and #civilDurationUntil: have been added, and have the same semantics as the first two methods used to have.

  4. The CalendricalCoordinate instance methods #annualDateSemanticKeyFrom:ifNone: and #annualDateSemanticKeyIfNone: have been renamed to #annualDateSemanticKeysFrom:ifNone: and #annualDateSemanticKeysIfNone: (respectively.) There can be more than one "annualDateSemanticKey" associated with the same date.