Calendar Modules

Dave Rolsky edited this page Jan 30, 2017 · 2 revisions

A module belongs under the DateTime::Calendar namespace if it implements a calendar, such as the Julian calendar, Chinese calendar, Islamic calendar, etc. For reference, the DateTime.pm module implements the Gregorian calendar.

API

Because calendars can differ from each other quite a bit, there are few set standards for a calendar class's API. If the calendar has things like days, months, and years, then inasmuch as is reasonable, the API for the class should look like the DateTime.pm API, particularly in regards to accessors.

It is recommended that module authors base their API style on that used by DateTime.pm, in order to encourage consistency among modules in the DateTime::* namespace. Some things to note are:

Parameters

All methods which take parameters either take one positional parameter, or named parameters. If there is a question about which to use, it's probably better to go with named parameters, as this provides the most flexibility for future changes.

Accessors and Mutators

All mutator methods return the object itself, so that methods may be chained. For example:

$datetime->set_time_zone('America/Chicago')->add( days => 5 );

This does rule out having combined accessor/mutator methods, but that's not necessarily a bad thing.

clone() method

It is recommended that all calendars which implement any sort of mutator methods also implement a clone() method. Any calendar that supports time zones must implement such a method. This method is expected to return a new object which represents exactly the same information as the object upon which clone() is called.

Converting Between Calendars

The only absolute requirement for calendar modules is that they implement the methods needed to allow conversion between different calendar classes. There are two methods needed to do this. The first is a constructor called from_object(). This constructor is expected to take a set of named parameters, at least one of which must be called "object". This constructor then uses the object it was given to create an object of the new class.

This leads directly to the second required method, utcrdvalues(). This is an accessor expected to return a three element array. The first element is the Rata Die day for the datetime in question. For classes that have time zones, this is the UTC value, not the local value. The second value is UTC time represented as seconds. The third value provides sub-second resolution expressed in nanoseconds. This value must be less then 1,000,000,000 nanoseconds (1 second).

Rata Die is the epoch used in Calendrical Calculations by Edward M. Reingold and Nachum Dershowitz. Rata Die day 1 is midnight of January 1, 1 in the Gregorian calendar. For those familiar with the Julian Day system, Rata Die is JD + 1,721,424.5. If you're familiar with Modified Julian Day system, Rata Die is MJD - 678,576.

Given these two methods, it is possible to transparently convert between any two classes which implement both of them. As an example, this is the DateTime.pm implementation of from_object():

sub from_object {
    my $class = shift;
    my %p     = validate(
        @_,
        {
            object => {
                type => OBJECT,
                can  => 'utc_rd_values',
            },
            locale   => { type => SCALAR | OBJECT, optional => 1 },
            language => { type => SCALAR | OBJECT, optional => 1 },
        },
    );

    my $object = delete $p{object};

    my ( $rd_days, $rd_secs, $rd_nanosecs ) = $object->utc_rd_values;

    # A kludge because until all calendars are updated to return all
    # three values, $rd_nanosecs could be undef
    $rd_nanosecs ||= 0;

    my %args;
    @args{qw( year month day )} = $class->_rd2ymd($rd_days);
    @args{qw( hour minute second )}
        = $class->_seconds_as_components($rd_secs);

    $args{nanosecond} = $rd_nanosecs;

    my $new = $class->new( %p, %args, time_zone => 'UTC' );

    $new->set_time_zone( $object->time_zone )
        if $object->can('time_zone');

    return $new;
}

Its implementation of utc_rd_values() is trivial, since it uses these values internally as well:

sub utc_rd_values {
    @{ $_[0] }{ 'utc_rd_days', 'utc_rd_secs', 'rd_nanosecs' };
}

DateTime::Calendar modules are not required to use Rata Die internally, though anyone implementing a calendar documented in Calendrical Calculations will probably find that this is the easiest way to go about the implementation.

Round trips

The following code should work, no matter what calendar classes are involved.

my $dt       = DateTime->today;
my $original = $dt->clone;

foreach my $class (@many_calendar_classes) {
    $dt = $class->from_object( object => $dt );
}

my $dt_again = DateTime->from_object( object => $dt );

print "The same as we started with.\n" if $original == $dt_again;

In other words, it should be possible to convert from one calendar to another, back and forth or cyclically, and always end up with an object representing the exact same datetime as the original object.

Calendars without time components

Some calendars may not deal with the time components at all, and so have no use for the "UTC RD seconds" or "RD nanoseconds" values. In that case, they should store these values internally so that precision is not lost on a round trip between a calendar that does deal with time and one that doesn't.

Time zones

It is possible that neither, both, or just one of the calendars involved in a conversion support time zones. The "source" class is the class of the object passed to the from_object() method, and the "destination" class is the class on which from_object() was called.

Source supports time zones, destination does not

The destination class's from_object() method should include something like the following code before calling utc_rd_values():

$object = $object->clone->set_time_zone('floating')
    if $object->can('set_time_zone');

As mentioned above, all calendar classes which support time zones are expected to implement a clone() method.

The net effect of the above code is to do the conversion based on the object's local datetime, as opposed to its UTC datetime. This should be documented in the docs for the destination class's from_object() method.

Destination supports time zones, source does not

The object returned from the from_object() method should have its time zone set to "floating". This should be documented in the docs for the destination class's from_object() method.

Doing this allows users to easily set the new object to any time zone they like.

Both classes support time zones

After creating a new object, the destination class should set the newly created object to the same time zone as the object passed to the from_object() method.

Rata Die

Rata Die always follows the UTC timescale. This means that even in calendars in which the day changes at sunset, the RD day still changes at midnight.