Calendar Modules
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.
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:
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.
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.
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.
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.
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.
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.
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.
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.
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.
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 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.