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:  


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:


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:


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.


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:


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]} #{}"

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


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

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:


def secs_to_new_year(now = DateTime::now())
new_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

puts secs_to_new_year()


| now |
now := DateAndTime now.
now secondsUntil:
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.
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)))

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())

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.


AJAX × Date × Time × Time zones - best practices

ecmanaut offers a great rant!


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: 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() :
lblOffset.Text =
lblUTC.Text =

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;

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')


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.
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:
dateSeparator: $
dateAndTimeSeparator: $
timeZoneSeparator: $
timeZoneElementSeparator: nil
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])
[: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... uploaded to wrong directory

When I uploaded the new 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):

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.


NYSE Full-Day Closures--1885 to 2006

Soon, I'll be releasing a new version of Chronos. One of the new features in the new version will be knowledge of all the dates from 1885 to the present that the New York Stock Exhange was closed for the whole day. This information is useful when performing certain kinds of technical analysis--especially when computing the number of trading days between dates for Fibonacci ratio analysis.

In doing the research for this, I discovered some interesting facts:

1. In 1984, Election Day was removed from the list of regularly-scheduled NYSE holidays.
2. The NYSE was closed on Monday, July 21, 1969 in honor of the first lunar landing.
3. The NYSE was closed every Wednesday for about a six-month period in 1968 due to the 'Paper Crisis' (google it.)
4. In 1953, Lincoln's Birthday, Flag Day and Columbus Day were removed from the list of regularly-scheduled holidays.
5. From 1873 through 1952, there was a 2-hour trading session (10am to Noon) on Saturdays (an excellent trivia question!)
6. The NYSE was closed for several months in 1914 on account of WWI. (!!!)
7. Flag Day was added in 1916.
8. Columbus Day was added in 1909.
9. New Year's Day is not observed when it occurs on a Saturday. 2005 is the most recent example.

Anyway, the list (in reverse order) of all NYSE trading holidays from 1885 to 2006 is presented below. The list only includes full day closures, other than Sundays (or Saturdays after 1952.) This information is not easy to find.

You can view the complete list here: NYSE Full-Day Closures--1885 to 2006.


Recommended Article: Why is Business Semantics the New Hot Topic?

Why is Business Semantics the New Hot Topic?


Q: Can we talk more about ontologies? What is an ontology and why is it useful?

McComb: The most commonly quoted definition (from Tom Gruber of Stanford) of an ontology is "a specification of a conceptualization." Or, to put that slightly differently, an ontology is a formal way to organize knowledge and terms. Typically ontologies are represented as graphical relationships or networks, as opposed to taxonomies which are usually represented hierarchically.

As to why they are useful? Well, in any system (be it computer-based or in the world at large), we need to have agreement on the meaning or terms and their interrelationships in order to share understanding. In the IT world, software and software agents act upon these meanings, so there need to be specific "commitments" to the meanings in the system. Paraphrasing Gruber again, we build software and agents that commit to ontologies, and we design ontologies so we can share knowledge among the agents. The value (to answer the original question) then comes from the ability to share and re-use knowledge between agents in the system.

Ontologies are like UML class diagrams, only much more general. An ontology is used to define concepts, not to define data structures or encapuslations of state and behavior. There is a difference of purpose. Is there an analogy to classes? Certainly--but there are also important differences.

In an ontology, one not only formally defines concepts, one also formally defines the properties of each concept. Properties are first-class values--both at the base level (instances) and at the metalevel.

One can define both "monthOrdinal" and "monthCardinal" as properties, binding "monthOrdinal" to the "ordinal number" metaproperty, and binding "monthCardinal" to the "cardinal number" metaproperty.

One may also define declarative logic translation rules that apply globally within the knowledge base. For example, one such rule might say that "monthCardinal" should always be translated to "monthOrdinal," and another rule might say that when translating a cardinal number to an ordinal number, the value of the destination property should be 1 greater than the value of the source property.


Start Making Sense: Get From Data To Semantic Integration

In Start Making Sense: Get From Data To Semantic Integration, author Neil Raden (founder and president of consulting firm Hired Brains) makes the following points:

What if we had a different kind of metadata that would let more users go further on their own with BI [Business Intelligence] and productive data analysis? Semantic integration technology, also known as "ontology," is the next step in rationalizing information integration in and beyond organizations. Semantic integration is the application of Semantic Web concepts developed by Tim Berners-Lee, director of the World Wide Web Consortium (W3C). Berners-Lee and the W3C define the Semantic Web as a framework that "allows data to be shared and reused across application, enterprise and community boundaries." Semantic integration raises the level of abstraction so people and systems can focus on meaning and relationships.


A rich semantic ontology can serve as the abstraction layer, not just between BI tools and the data warehouse, but between everything. For an enterprise, semantic integration can become the orchestration point, resource broker and domain adviser. Physical location and data structures become irrelevant; data stewards are free to tune and modify their resources as they see fit. Operational and analytic processes are freed of the burden of tracking data.

With composite application development based on service-oriented architecture (SOA) on the rise, consider one last critical reason to do something about semantic integration. The composite idea is to assemble and reassemble, in something close to real time, applications based on business needs from standards-based services. In other words, we'll be relying on machines to make inferences about information because there won't be time to program everything.

Also see the companion article, Content: The Other Half of the Integration Problem.

Semantic mediation using ontologies is what I now do for a living. Chronos is just my hobby. I see ontolgoy-based semantic mediation as a paradigm shift that is even more important than OOP (and may I remind you that I started in Smalltalk in 1985.) The time to get on board is now.

Ignoring Time Zone Issues Can Have Serious Consequences

According to the bug report in DateTime Difference Bug Makes PayPal_Framework Impractical for Real-Time Transactions, the fact that different components of a particular software system reside in different time zones--and use their own local time for timestamps--resulted in a critical error.

Timestamps should be recorded in UT (Universal Time, which most people incorrectly refer to as either GMT or UTC,) especially in situations where system components may reside in different time zones.

Even when all server and client machines will be in the same time zone, recording timestamps in local time can cause bugs.

For example, in most of the United States, whether 2005-10-30T01:50:42 is greater than or less than 2005-10-30T01:10:17 (both local time) is not well defined, because both timestamps occur during the ambigous hour T01:00:00/T02:00:00 (a left closed, right open interval) which occurs twice in local time on days where a transition from DST to standard time occurs. Most of the United States transitioned from DST to standard time at T02:00:00 local time on 30 October 2005--which resulted in local time in San Franciso (for example) jumping from 2005-10-30T01:59:59-07:00 to 2005-10-30T01:00:00-08:00.

Such discontinuities and ambiguities do not occur when timestamps are recorded in UT. For example, the UT timestamp that corresponds to 2005-10-30T01:59:59-07:00 is 2005-10-30T08:59:59Z, and the UT timestamp that corresponds to 2005-10-30T01:00:00-08:00 is 2005-10-30T09:00:00Z.

Don't add to the bug list.


Finding the Signal-to-Noise Ratio in the Never-Ending Language Debate

Exceprt from a blog entry by Reg Braithwaite:

Speaking of Rails, I'm going to conclude with my take on one reason why Rails is taking off and Seaside is not. Rails allows programmers to express the idioms they already know (relational databases, web-backed MVC, stateless event handling plus a global session store) in fewer bits.

Seaside provides a whole new idiom, continuations, that IMO is more powerful. I think you end up with an even higher signal-to-noise ratio with a Seaside app than with a Rails app. Why? Because continuations afford you a much higher degree of controller reuse.

Now, here's the catch: if you try to imagine your current application running on both Rails and on Seaside, you probably won't see much difference between the two (although they'll both be an order of magnitude better than ASP.NET). They will look the same because you designed your application with idioms that both Rails and Seaside support.

To get a big win, you'd have to rethink your application's flow and logic. You'd have to "think in Seaside." And you're not going to do that. So you pick Rails, like so many others have picked it, because it looks just like your ASP app, only all the noise has gone away. It's all signal, baby.

Now, do you see the thing here? Ruby has been around for a while, but nobody switched. Why? Because they couldn't see how its new idioms would help them. Every time a programmer considered switching from Blub to Ruby, she was presented with all these new idioms, like dynamic types. None of this meant anything to her, because they didn't appear to make the idioms she already knew more compact. No win.

But now she looks and she sees her existing web idioms, and she sees them expressed with way fewer bits, and she is suddenly prepared to learn this Ruby thing if it will let her say:

class Language < ActiveRecord::Base

has_and_belongs_to_many :idioms


My take: Eventually, the world realized and accepted the fact that Arabic number notation provided a compelling advantage over the use of Roman Numerals (ever try to do long division using Roman Numerals?) But the acceptance/adoption process took decades--or even centuries.

To paraphrase Lord Keynes: "The herd can remain irrational far longer than you can remain solvent."

Ruby On Rails is a "killer app." I'm thinking it's time to learn Ruby. I also think that vendors/purveyors of Smalltalk systems should provide Ruby compilers that generate code that runs on Smalltalk VMs.

I also wonder whether anyone would be interested in porting Chronos to Ruby. Hmmm....

New personal computer design wins Microsoft competition

New personal computer design wins Microsoft competition:

Two Purdue University industrial designers won a grand prize at an international competition co-sponsored by Microsoft Corp. for a new personal computer design that may change the way people watch movies, listen to music, play games and read magazines.

The Bookshelf is a personal computer that physically resembles a bookshelf and functions like a bookshelf as books and magazines – or in this case, hardware – are placed on it. The foundation of the computer is its central processing unit, which is a 7-inch cube. The Bookshelf operates with add-on hard drive attachments that are supplied by digital service providers. (Photo provided by Sungho "Oho" Son)

This image shows how the Bookshelf can be pieced together with hard drive attachments that contain multiple movies, games or magazines. As attachments are added, the Bookshelf becomes its own multimedia library custom built by its owner. (Photo provided by Sungho "Oho" Son)


External Repository for VW Core.TimeZones--all 500+ Olson Time Zones

I have implemented a package that adds support to VisualWorks' Core.TimeZone class for retrieving TimeZone instances from (and storing them into) an external repository. I have also added code to Chronos that generates an external repository of serialized Core.TimeZone instances.

The name of the package is TimeZone-External Repository-Olson TZDB. It's available from the Chronos web site (direct download link: -- URL changed as of 2006-02-20)

The download file includes the installation instructions and the current version of the VisualWorks TimeZone Repository. The repository contains serialized instances of Core.TimeZone for all 500+ time zones in the Olson Time Zone Database.

Other than the use of Chronos to generate the external repository of Core.TimeZones, there is no other dependency or connection between Chronos and the TimeZone-External Repository-Olson TZDB package.

With the TimeZone-External Repository-Olson TZDB package installed (and whether or not Chronos is also installed,) one may retrieve instances of Core.TimeZone (with correct rules for the current year) as in the examples below. All the examples retrieve a Core.TimeZone instance from the external repository using one of the Olson keys, and then make that instance be the 'reference' (default) TimeZone for VisualWorks:

(TimeZone at: 'Australia/Sydney') beReference.
(TimeZone at: 'Asia/Tokyo') beReference.
(TimeZone at: 'Australia/Perth') beReference.
(TimeZone at: 'Asia/Calcutta') beReference.
(TimeZone at: 'Europe/Athens') beReference.
(TimeZone at: 'Europe/Berlin') beReference.
(TimeZone at: 'Europe/London') beReference.
(TimeZone at: 'America/New_York') beReference.
(TimeZone at: 'America/Indiana/Indianapolis') beReference.
(TimeZone at: 'America/Chicago') beReference.
(TimeZone at: 'America/Denver') beReference.
(TimeZone at: 'America/Boise') beReference.
(TimeZone at: 'America/Phoenix') beReference.
(TimeZone at: 'America/Los_Angeles') beReference.
(TimeZone at: 'Pacific/Honolulu') beReference.

US Federal Holidays for 2006

2006-01-01->#(#NewYearsDay #Sunday)
2006-01-02->#(#NewYearsDay #Monday)
2006-01-16->#(#MartinLutherKingDay #Monday)
2006-02-20->#(#PresidentsDay #Monday)
2006-05-29->#(#MemorialDay #Monday)
2006-07-04->#(#IndependenceDay #Tuesday)
2006-09-04->#(#LaborDay #Monday)
2006-10-09->#(#ColumbusDay #Monday)
2006-11-10->#(#VeteransDay #Friday)
2006-11-11->#(#VeteransDay #Saturday)
2006-11-23->#(#Thanksgiving #Thursday)
2006-12-25->#(#Christmas #Monday)

Note that both the official day of the holiday, and the day it is observed by the US Government, are both listed (if different.)

The Chronos code used to generate the list is given below:

| holidays |
holidays := SortedCollection sortBlock: [:a :b | a key < b key].
(Timeperiod yearStartingAt: (YearMonthDay year: 2006 month: January day: 1)) every: 1 daysDo: [:day |
| date semanticKey |
date := day start.
annualDateSemanticKeyFrom: SemanticDatePolicy unitedStates
[:key |
semanticKey := key.
(SemanticDatePolicy usFederalHolidaySemanticKeys includes: semanticKey)])
ifTrue: [holidays add: date->(Array with: semanticKey with: date dayOfWeekName)]].
Transcript cr.
holidays do: [:holiday | Transcript cr; show: holiday printString]


Book Recommendation: Marking Time: The Epic Quest to Invent the Perfect Calendar

From the article at the link Marking Time: The Epic Quest to Invent the Perfect Calendar:

A provocative history lesson and a unique, entertaining read rolled into one, Marking Time will leave you with a sense of awe at the random, hit-or-miss nature of our calendar s development a quality that parallels the growth of civilization itself. What results is a truthful, and, above all, very human view of the calendar as we know it. After reading Marking Time, you will never look at the calendar the same way again.

What are the origins of the years, months, and days that give our lives their familiar rhythm?

In Marking Time: The Epic Quest to Invent the Perfect Calendar, astronomer and acclaimed author Duncan Steel marches through human history to deliver a fascinating, milestone-by-milestone look at how the modern-day calendar came to be. From the definition of the lunar month by Meton of Athens in 432 b.c., through present-day proposals to reform our calendar, Steel captures the often-flawed but always fascinating story of the calendar s evolution.

Here, you will discover fun facts and surprising anecdotes as the author visits with some of the seminal figures of the past Julius Caesar, William the Conqueror, and Benjamin Franklin among them as well as some lesser-known names, all of whom left an indelible mark on how we record time. You will also gain an in-depth look at the role science, astronomy, religion, politics, and even war played in various calendrical systems, including the one hanging on your wall. Open up a copy of Marking Time and, as the author puts it, "read, puzzle, and enjoy."


Version 2005r of The Olson Time Zone Database Published

The 18th version of 2005 (2005r) of the Olson Time Zone Database was published on 2005-12-27. That's an average of a new version every 2.9 weeks. That's more than double the number of versions published during 2004. The nations of the world appear to be in the mood to tinker with their time zone rules.

I was not at all expecting another version in 2005 after version 2005q, so I didn't even check until today.

I have generated a new version of the Chronos Time Zone Repository, and published it on the Chronos web site. Here's the direct link to download the Chronos Time Zone Repository.

The Chronos Installation Instructions explain how to do all the following yourself: 1) Obtain the latest version of the Olson Time Zone Database, and 2) Generate a new version of the Chronos Time Zone Database based on an updated version of Olson.

Here are the instructions:

The source files from which the Chronos time zone rulesets, localization policies and UTC leap second schedule are generated reside in the Chronos Time Zone Repository at the location <path-prefix>/time-zones/Olson-zoneinfo-sources. Arthur David Olson publishes updated versions of the source data about once every month (some countries decide de novo every year what their daylight saving policy will be, and of course there’s always some set of countries every year that decide to change their rules.)

To generate the Chronos Time Zone Repository de novo, or to ensure you are using the latest version of the Olson Time Zone Database, fetch the Olson source files from, extract/install them into <path-prefix>/time-zones/Olson-zoneinfo-sources, and then run the Chronos Time Zone Compiler by evaluating "OlsonTZDBRulesetCompiler processTimeZoneRules", thereby generating a new version of the Chronos Time Zone Repository. This procedure can also be used to generate a “de novo” version of the Chronos Time Zone Repository, which may be necessary when you do not already have a version of the Chronos Time Zone Repository, and are unable to access the latest version from the Chronos web site [].


  • You must install the Chronos-Utilities bundle in order to run the Chronos Time Zone Compiler

  • The Olson time zone source files are packaged as a zipped tar file in the folder. The name of the file has the form tzdataYYYYv.tar.gz, where YYYY is the 4 digits of the year and v is a lower-case letter of the English alphabet that indicates the version index within the year (so that the first version in the year 2006 would be 2006a, the second version would be 2006b, and so on.) The last version of the file for 2005 has the name tzdata2005r.tar.gz, and contains the version of the database commonly referred to as 2005r.

  • The value of <path-prefix> currently used by a Chronos image can be discovered by evaluating “ChronosSystemFacade current resourcePathPrefix.” If the value is the empty string or nil, then <path-prefix> is the current working directory (the absolute path for which can be discovered by evaluating “ResourcePath defaultDirectory.”)

See the installation instructions for more information about how to set the <path-prefix>.

VW 7.4 Synchronizes Its Clock

Cincom didn't publicize it, but VW 7.4 includes a fix that may be quite useful in some situations.

In previsous versions, once an image had started running, the count of microseconds returned by primitive 351 (or the count of milliseconds returned by primitive 350, in versions of VW prior to VW7) was unaffected by any adjustments made to the host platform's system clock. So, if an NTP service was running that kept the host system's clock synchronized with a time server, any running VW image would receive no benefit. Clock adjustments from any source (including the user) were invisible to a running VW image.

Most computer clocks are not all that accurate. They can diverge from accurate time by minutes per month. Such inaccuracy may be acceptable to many, but to some it is not. There are technical, scientific, legal and even business use cases that must have a clock with much greater accuracy. Having "legally traceable time" may be the difference between winning and losing a lawsuit.

But as of VW 7.4, VisualWorks will be useable in use cases where time must be accurately kept within subsecond tolerances over long periods of time, without requiring an expensive, non-standard clock device.


Event Times in Multiple Time Zones--A Chronos Example

A "Squeak Chat" is a discussion about Squeak Smalltalk hosted on an IRC channel. It's hosted on Freenode with the channel ID #squeak.

Squeak Chats are scheduled every 100 hours, so that the time-of-day at which they occur in any given time zone varies by +4 hours each time the event occurs (assuming no DST transitions intervene.)

The next Squeak Chat is scheduled for 2006-01-14T04:00:00+0000 (UT: Universal Time). What time is that where you live?

Using Chronos, the answer can be obtained by doing a "print it" of the following expression:

DateAndTime utYear: 2006 month: January day: 14 hour: 4 minute: 0 second: 0

The expression above creates an instance of DateAndTime (actually, Timepoint--DateAndTime is simply a global alias for Timepoint) that represents the same point-in-time as 2006-01-14T04:00:00+0000, but whose local time is set to be the same as Chronos' current default time zone (which would normally be your local time zone.) When a DateAndTime (Timepoint) instance prints itself, it displays the local time in the time zone to which it was bound when it was created. So, if your local time zone offset is -8 hours, then the result of doing a "print it" of the Timepoint created by the foregoing expression would be 2006-01-13T20:00:00-08:00 (13 Jan 2006, 8pm.)

When will Squeak Chats occur during the next year? When will they occur in a representative sample of world time zones? The code to compute the schedule of Squeak Chats for the next year in selected world time zones is given below:

| base interval timePeriod stream timeZones printPolicy |
interval := 100 hours.
base := DateAndTime year: 2006 month: January day: 14 hour: 4 minute: 0 second: 0 timeZone: 'Universal'.
timePeriod := Timeperiod yearStartingAt: base.
timeZones := #(UT 'Pacific/Auckland' 'Australia/Sydney' 'Asia/Tokyo' 'Asia/Hong_Kong' 'Asia/Calcutta' 'Europe/Moscow' 'Asia/Jerusalem' 'Europe/Amsterdam' 'Europe/London' 'America/Sao_Paulo' 'America/Argentina/Buenos_Aires' 'America/New_York' 'America/Chicago' 'America/Denver' 'America/Los_Angeles' 'Pacific/Honolulu').
printPolicy :=
     ChronosPrintPolicy applying:
         dateAndTimeSeparator: ' '
         timeZoneSeparator: $
         timeZoneElementSeparator: nil).
[stream := (ResourcePath fromString: 'schedule.txt') newWriteStream.
timePeriod every: interval do: [:tp |
    stream nextPutAll: '== Squeak Chat Times in selected time zones =============================='; cr.
    timeZones do: [:tz |
        | timepoint |
        timepoint := tp start in: tz.
        stream cr; tab.
        timepoint printOn: stream using: printPolicy].
        stream cr; cr]] ensure: [stream close].

The ouptut shows that the next two Squeak Chats will occur at the following times in the selected set of time zones:

== Squeak Chat Times in selected time zones ==============================

Sat, 2006-Jan-14 04:00:00 am +0000 (UT: Universal Time)
Sat, 2006-Jan-14 05:00:00 pm +1300 (NZDT: Pacific/Auckland)
Sat, 2006-Jan-14 03:00:00 pm +1100 (EST: Australia/Sydney)
Sat, 2006-Jan-14 01:00:00 pm +0900 (JST: Asia/Tokyo)
Sat, 2006-Jan-14 12:00:00 pm +0800 (HKT: Asia/Hong_Kong)
Sat, 2006-Jan-14 09:30:00 am +0530 (IST: Asia/Calcutta)
Sat, 2006-Jan-14 07:00:00 am +0300 (MSK: Europe/Moscow)
Sat, 2006-Jan-14 06:00:00 am +0200 (IST: Asia/Jerusalem)
Sat, 2006-Jan-14 05:00:00 am +0100 (CET: Europe/Amsterdam)
Sat, 2006-Jan-14 04:00:00 am +0000 (GMT: Europe/London)
Sat, 2006-Jan-14 02:00:00 am -0200 (BRST: America/Sao_Paulo)
Sat, 2006-Jan-14 01:00:00 am -0300 (ART: America/Argentina/Buenos_Aires)
Fri, 2006-Jan-13 11:00:00 pm -0500 (EST: America/New_York)
Fri, 2006-Jan-13 10:00:00 pm -0600 (CST: America/Chicago)
Fri, 2006-Jan-13 09:00:00 pm -0700 (MST: America/Denver)
Fri, 2006-Jan-13 08:00:00 pm -0800 (PST: America/Los_Angeles)
Fri, 2006-Jan-13 06:00:00 pm -1000 (HST: Pacific/Honolulu)

== Squeak Chat Times in selected time zones ==============================

Wed, 2006-Jan-18 08:00:00 am +0000 (UT: Universal Time)
Wed, 2006-Jan-18 09:00:00 pm +1300 (NZDT: Pacific/Auckland)
Wed, 2006-Jan-18 07:00:00 pm +1100 (EST: Australia/Sydney)
Wed, 2006-Jan-18 05:00:00 pm +0900 (JST: Asia/Tokyo)
Wed, 2006-Jan-18 04:00:00 pm +0800 (HKT: Asia/Hong_Kong)
Wed, 2006-Jan-18 01:30:00 pm +0530 (IST: Asia/Calcutta)
Wed, 2006-Jan-18 11:00:00 am +0300 (MSK: Europe/Moscow)
Wed, 2006-Jan-18 10:00:00 am +0200 (IST: Asia/Jerusalem)
Wed, 2006-Jan-18 09:00:00 am +0100 (CET: Europe/Amsterdam)
Wed, 2006-Jan-18 08:00:00 am +0000 (GMT: Europe/London)
Wed, 2006-Jan-18 06:00:00 am -0200 (BRST: America/Sao_Paulo)
Wed, 2006-Jan-18 05:00:00 am -0300 (ART: America/Argentina/Buenos_Aires)
Wed, 2006-Jan-18 03:00:00 am -0500 (EST: America/New_York)
Wed, 2006-Jan-18 02:00:00 am -0600 (CST: America/Chicago)
Wed, 2006-Jan-18 01:00:00 am -0700 (MST: America/Denver)
Wed, 2006-Jan-18 12:00:00 am -0800 (PST: America/Los_Angeles)
Tue, 2006-Jan-17 10:00:00 pm -1000 (HST: Pacific/Honolulu)

Chronos: A Few Comparative Examples

The home page for the Joda Date/Time Library presents some examples intended to demonstrate the "look and feel" of using Joda-Time. To see the Joda examples, click the link in the previous sentence.

Here are the equivalent examples using Chronos:

daysToNewYear: fromDate [Alternative 1]
    ^CalendarDuration days: (fromDate daysInYear - fromDate dayOfYear + 1)

daysToNewYear: fromDate [Alternative 2]
    ^CalendarDuration days: (fromDate daysUntil: (fromDate nextYear withDayOfYear: 1))

isRentalOverdue: datetimeRented [Alternative 1]
    "Answer whether a rental made on <datetimeRented> is now overdue."
    ^datetimeRented + 2.5 days < DateAndTime now

isRentalOverdue: datetimeRented [Alternative 2]
    "Answer whether a rental made on <datetimeRented> is now overdue."
    ^datetimeRented + (CivilDuration days: 2 hours: 12) < DateAndTime now

joinedInLastThreeMonths: datetimeJoined
    "Answer whether the <datetimeJoined> is within the past three months."
    ^(DateAndTime now withDuration: -3 months) includes: datetimeJoined

getBirthMonthText: dateOfBirth forLocale: localeKey
    "Answer the name of the month of <dateOfBirth> according to the locale identified by the <localeKey>."
            forLocale: localeKey
                [:monthName | monthName]
                [dateOfBirth monthName]


Design, Notation and Relative Utility: A Matter of Perspective

Very early Friday morning (2006-01-06,) I released a new version of Chronos and announced the new version on the Chronos blog--including an explanation of the reason the new version was necessary, wherein I said the following:

"So I have reluctantly changed WeekOfMonthDayOfWeek so that it can gracefully handle weekly periods that overflow the number of days in a month, even though that's contrary to my preferred philosophy."

Ever since that post, I have found myself reconsidering whether the change I made to WeekOfMonthDayOfWeek actually is against my design philosophy--and if it is, whether or not my design philosophy is wrong in this case.

I think a discussion of the relevant issues involved in this matter, and the conclusions to which I have come, may be of wider interest. Hence this post.

As currently implemented, instances of the WeekOfMonthDayOfWeek class represent a partial date of a particular type in a particular way. The type of partial date represented by a WeekOfMonthDayOfWeek instance is what I call an AnnualDate, which is a date that does not specify any particular year. The way that a WeekOfMonthDayOfWeek instance specifies an AnnualDate is as the first occurrence of a specified day-of-the-week on or after a particular day of a particular month.

For example, evaluating "WeekOfMonthDayOfWeek month: 11 minDayOfMonth: 22 dayOfWeek: 5" creates an instance of WeekOfMonthDayOfWeek that represents the first occurrence of a Thursday on or after 22 November (which, by the way, is the rule for Thanksgiving Day in the US.) "The first Thursday on or after the 22nd" is equivalent to "the fourth Thursday of the month."

More generally, the first Thursday on or after the 1st of a month is equivalent to the first Thursday of the month, the first Thursday on or after the 8th of the month is equivalent to the second Thursday of the month, the first Thursday on or after the 15th of the month is equivalent to the third Thursday of the month, and the first Thursday on or after the 22nd of the month is equivalent to the fourth Thursday of the month. In the case of a month with 30 days (such as November,) the first Thursday on or after the 24th of the month is equivalent to the last Thursday of the month.

However, the current implementation of WeekOfMonthDayOfWeek is more general than it was as I originally implemented it. Originally, it could represent dates as the Nth specified day-of-week of the specified month, or else as the last specified day-of-week of the specified month, but could not represent "the first specified day-of-week on or after a specified day-of-month of a specified month" (as it does now.)

I made the implementation of WeekOfMonthDayOfWeek more general because such was necessary in order to handle the generality with which time zone transition rules are specified by the Olson Time Zone Database. However, in spite of this more generalized reimplementation, I never changed my conceptual model of WeekOfMonthDayOfWeek from what it was originally. And according to that conceptual model, a WeekOfMonthDayOfWeek should only represent dates within the month its rule specifies, and not dates in other months.

Of course, the first Friday on or after the 26th of March may be the first day of April--and April is obviously not March. And from the perspective of my original conceptual model of WeekOfMonthDayOfWeek, which was that a WeekOfMonthDayOfWeek should specify a date within a specified month as a function of a specified day-of-week and specified week-of-the-month, such a specification of a date in some other month is improper.

However, it should be noted that WeekOfMonthDayOfWeek has always included an offset number of days from the nominally specified date. By default, the offset is zero days. However, a WeekOfMonthDayOfWeek whose offset is set to +3 days represents the date 3 days later than (for example) "the last Sunday of the specified month." The offset may have the effect of making a WeekOfMonthDayOfWeek instance specify a date not in the same month as the one specified in its rule. So even before I generalized the implementation, it was possible to create an instance of WeekOfMonthDayOfWeek that specified a date in a month other than the one specified in the instance's rule.

A very analogous issue must be confronted when implementing any date creation message, such as "DateAndTime class>>year:month:day:". Should "DateAndTime year: 2006 month: 2 day: 29" be treated as an error, or should it instead be interpreted as the date 29 days later than 2006-01-31--in other words, as 2006-03-01?

The issue can be generalized as follows: Should instance creation methods (or even more generally, mutational methods) treat as errors any parameter sequences that would not be legal/valid according to the notational rules for representing a value of that type, or is it acceptable for such methods to interpret their input parameters according to a more general, mathematical model--a representational model different than the standard one? Is the fact that there is no such date as 2006-02-29 sufficient reason, in and of itself, to forcibly prevent the creation of a DateAndTime by using a parameter sequence that is interpreted to be a reference to the date 29 days later than 2006-01-31?

Consider the Smalltalk expression "6/8." If one prints this value, the text that will be printed is "3/4." This establishes the precedent that the canonical notation to represent a value need not be the same as the parameters used to create the value. If it's considered kosher to autoconvert "6/8" to "3/4," what's wrong with converting "DateAndTime year: 2006 month: 2 day: 29" to 2006-03-01?

One objection would be that "6/8" has a well-defined meaning, and is legal acording to the notational rules of arithmetic, whereas "2006-02-29" is either a date that does not exist, or else is not a formally-defined notational synonym for "2006-03-01." But this objection rests on two assumptions that may be untrue. Specifically, the objection assumes that the same constraints that apply to the standard notational representation should also apply to the arguments accepted by an instance creation and/or mutational message, and it assumes that there can only ever be one canonical notational scheme for representing the values of a type, and that there will never be any need or justification for alternatives.

A better objection would be that it is far more likely that a "non-canonical" date designation is an error than it is that anyone would actually need and/or want to specify a date using a notational scheme such as "799 days after subtracting 200 months from 2000-01-01" (i.e., "DateAndTime year: 2000 month: -200 day: 800.") This objection appears to me to be quite valid. Apparently, the ANSI Smalltalk Committee came to the same conclusion, because the ANSI Smalltalk Standard requires that an Exception be raised in such a case--which happens to be what Chronos does.

But there is a problem with the "no one is likely to need or intentionally use it" objection: it applies well enough to the specific case we have been discussing, but it may not apply to all (or even most) other cases. In fact, the proper generalization of the objection would be to apply a "relative utility" test to each case. In other words, we should consider the relative utility of whatever alternative (and perhaps more general) representation scheme might be supported by any instance creation and/or mutational methods versus the relative utility of detecting any unintended errors in notation.

So now we return to the case which started this dicussion.

Firstly, we note that there is no canonical, standard or well-known notational scheme for representing "the first specified day-of-week on or after the specified day-of-month of the specified month."

Secondly, we note that there is no compelling reason to require that the date specified by a WeekOfMonthDayOfWeek instance be in the same month as the month referenced by its rule. It's true that it may be a tad surprising when the specified date happens to fall in a different month, but whether or not one is surprised depends upon how one views the function and purpose of a WeekOfMonthDayOfWeek instance. It's a matter of perspective.

Thirdly, we note that there are compelling reasons to allow the date specified by a WeekOfMonthDayOfWeek instance to fall in a month other than the one specified in its rule. One is that the Olson Time Zone Database apparently specifies such annually-recurring dates using that level of generality. Another is that "the first Sunday on or after the 31st of May" is a perfectly well-defined concept, and could in fact be an example of a rule that might be adopted in some law, regulation or policy promulgated by some legislature, agency or other organization.

In view of the foregoing, I have decided that what was wrong wasn't my design philosophy, but rather my mental model of the function and purpose of WeekOfMonthDayOfWeek. Or to put it another way, my new conceptualization of WeekOfMonthDayOfWeek has greater relative utility than my old one.

I have, however, clarified and refined my design philosophy as a result of having thought through these issues. I hope that those who have read this article will derive some benefit from having considered these matters along with me.


Chronos Version B1.4 Published

While checking out the new version of VisualWorks (7.4, a.k.a. the Winter Edition,) I discovered a problem that has actually been in the code since it was originally written, but which remained hidden from my tests until now. It turns out it was waiting for 2006 to arrive before revealing itself.

The root cause of the problem is that the latest version of the Olson TimeZone Database (2005q, and perhaps earlier versions, I haven't checked because it doesn't really matter) includes the following rule for the 'Asia/Jerusalem' time zone:

"Rule Zion 2006 2010 - Mar Fri>=26 2:00 1:00 D".

Such a rule is probably an error, since it specifies that the DST transition occurs on Friday on or after 26 March--a date which may not exist, since there are of course only 31 days in March, not 32. This problem was not discovered previously, because the rules for the years prior to 2006 don't specify a week with days beyond the end of the month.

As originally written, the WeekOfMonthDayOfWeek class will fail (noisily) in some circumstances when such an invalid week-of-month is specified. Personally, I would prefer to leave it that way, except for the fact that the data containing this invalid week-of-month specification is the Olson Time Zone rules. So I have reluctantly changed WeekOfMonthDayOfWeek so that it can gracefully handle weekly periods that overflow the number of days in a month, even though that's contrary to my preferred philosophy. I can't force the Olson Time Zone Database to live by my rules, nor can I expect that its authors will never make errors of this sort.

I found no issues with VW 7.4, so both new and old versions of Chronos should be compatible with any version of VW, subject to the caveats mentioned in the porting document on the Chronos website.