A Date with Perl
Dave Rolsky
autarch@urth.org
IRC: autarch
Copyright © David Rolsky 2012-2017
A Date with Perl by David Rolsky is licensed under a Creative Commons Attribution-Share Alike 3.0 United States License.
Dates and Times are Insane
- Calendars
- Time Zones
- Daylight Saving Time
- Leap Seconds!
Do Not Write Your Own Date and Time Manipulation Code!
- Do Not Write Your Own Date and Time Manipulation Code!
- Do Not Write Your Own Date and Time Manipulation Code!
- Do Not Write Your Own Date and Time Manipulation Code!
Seriously
Gregorian Calendar
- Based only on earth's revolution around the sun
- Current world standard
- DateTime.pm == Gregorian
Gregorian Calendar for Dummies
- 365 days in a regular year
- 366 in a leap year
- Begins on January 1, year 1 (0001-01-01)
Gregorian Calendar for Dummies
- Year 0 == 1 BC(E)
- May need to tweak the leap year algorithm around year 3000
- Earth's revolution is slowing down
Simple Dates
use DateTime;
my $dt = DateTime->new(
year => 2013,
month => 6,
day => 5,
);
say $dt->date(); # 2013-06-05
say $dt->month_name(); # June
Les Dates Simples
use DateTime;
my $dt = DateTime->new(
year => 2013,
month => 6,
day => 5,
locale => 'fr',
);
say $dt->date(); # 2013-06-05
say $dt->month_name(); # Juin
Other Calendars
use DateTime;
use DateTime::Calendar::Chinese;
my $dt = DateTime->new(
year => 2013,
month => 6,
day => 5,
);
my $chdt = DateTime::Calendar::Chinese->from_object( object => $dt );
say $chdt->cycle(); # 78
say $chdt->zodiac_animal(); # snake
say $chdt->celestial_stem(), $chdt->terrestrial_branch(); # 癸巳
Time for Dummies
- Day length == 1 rotation of the Earth around its axis
- 1 day == 24 hours
- First hour is hour 0
- Last hour is hour 23
- 1 hour == 60 minutes
- 1 minute == 60 seconds (almost all of the time)
- 1 day == 86,400 seconds (more or less)
Atomic Clocks
- Ties length of second to physicsy stuff
- Length of second never changes
- TAI is the international atomic time standard
Leap Seconds
- AKA "The Devil", "A Really, Really Bad Idea"
- The earth's rotation is slowing down
- The length of a second is not
- We need to resync midnight
- Bam, leap second announced!
UTC
- Coordinated Universal Time
- Temps Universel Coordineé
- UTC = TAI (atomic time) + leap seconds to date (37)
- Current world standard
- Based on time in Greenwich, England
- Time zones are based on an offset from UTC
Time Zone Warning
- Time zones are political
- Change all the time for dumb reasons
- Last US change was pointless
Time Zone Standards
- IANA (née Olson) time zone database is the standard
- http://www.iana.org/time-zones
- An open source data/software project
- Microsoft does their own thing (of course)
Time Zone 101
- An offset in minutes and hours from UTC
- Washington, DC is currently at -04:00
- Also has a name
- Names are (mostly) continent or ocean + major city
- America/Chicago
- Asia/Taipei
- Pacific/Fiji
- America/Argentina/San_Juan
Time Zone 102
- A named zone is a collection of rules
- Rules define historical and future DST changes
- Also define short names like CDT
- Short names are not unique!
- Only use short names for display
Picking Time Zones
- The Olson database includes many historical zones
- America/Chicago == America/Menominee
- Menominee moved from Eastern to Central in 1973
- No API for finding current time zones (yet?)
What Time is it There?
use DateTime;
my $dt = DateTime->new(
year => 2013,
month => 6,
day => 5,
hour => 9,
minute => 30,
time_zone => 'America/Chicago',
);
say $dt->datetime(); # 2013-06-05T09:30:00
$dt->set_time_zone('Asia/Taipei');
say $dt->datetime(); # 2013-06-05T22:30:00
The Floating Time Zone
use DateTime;
my $dt = DateTime->now(
time_zone => 'floating',
);
- No time zone at all
- No offset conversion when set to a real zone
- No leap seconds
The Floating Time Zone
use DateTime;
my $dt = DateTime->new(
year => 2013,
month => 6,
day => 5,
hour => 9,
minute => 30,
time_zone => 'floating',
);
say $dt->datetime(); # 2013-06-05T09:30:00
$dt->set_time_zone('Asia/Taipei');
say $dt->datetime(); # 2013-06-05T09:30:00
Epochs and the Unix Epoch
- An epoch is a reference point for a calendar's start date & time
- The Unix epoch == 1970-01-01T00:00:00 UTC
- Unix epoch is counted in seconds
- Not really UTC since POSIX says we skip leap seconds
- But not really TAI cause NTP is UTC-based
Calculating the Epoch
use DateTime;
my $dt = DateTime->new(
year => 2013,
month => 6,
day => 5,
hour => 9,
minute => 30,
time_zone => 'America/Chicago',
);
say $dt->epoch(); # 1370442600
The y2.038k problem
- The epoch will no longer fit in a 32-bit int
- 2038-01-19T03:14:07 UTC
- A 30-year mortgage in 2013 ends in 2043
- As of Perl 5.12, Perl always uses a large-enough epoch
DateTime::* Ecosystem
- Formatter/parsers
- Other calendars
- Event and recurrence modules
- DateTimeX modules
Recommendations and Gotchas
There's no DateTime::Date Class
- If I could do it all over again ...
- Use the floating time zone
- Use the
delta_md()
and delta_days
methods for math
Calculating the Difference Between Two Dates
my $dt1 = DateTime->new(
year => 2013,
month => 6,
day => 5,
time_zone => 'floating',
);
my $dt2 = DateTime->new(
year => 1973,
month => 12,
day => 6,
time_zone => 'floating',
);
my $duration = $dt1->delta_days($dt2);
say $duration->in_units('days'); # 14426
DateTime::Duration Has a Terrible API
- What moron created this?
- Internally it stores months, days, minutes, seconds, and nanoseconds
- Externally has
years()
, months()
, weeks()
, days()
, etc. methods
- When you call
$duration->days()
you get days but not weeks
- Use
$duration->in_units('days')
instead
Say What, DateTime::Duration?
my $dt1 = DateTime->new(
year => 2013,
month => 6,
day => 5,
time_zone => 'floating',
);
my $dt2 = DateTime->new(
year => 1973,
month => 12,
day => 6,
time_zone => 'floating',
);
my $duration = $dt1->delta_days($dt2);
say $duration->days(); # 6 ... WTF?
say $duration->weeks(); # 2060
say $duration->in_units('days'); # 14426
DateTime Math is Hard
Let's Go Shopping
How Long is a Month?
my $dt = ...; # 2009-02-01
$dt->add( days => 28 );
say $dt; # 2009-03-01
$dt->add( days => 28 );
say $dt; # 2009-03-29
How Long is a Month??
my $dt = ...; # 2009-01-30
$dt->add( months => 1 );
say $dt; # 2009-03-02
$dt->add( months => 1 );
say $dt; # 2009-04-02
How Long is a Month??!
my $dt = ...; # 2009-01-30
$dt->add( months => 1, end_of_month => 'limit' );
say $dt; # 2009-02-28
$dt->add( months => 1 );
say $dt; # 2009-03-28
- End of month modes
- wrap - default for adding months
- preserve - default for subtracting months
- limit
How Long is a Day?
my $dt = DateTime->new(
year => 2012,
month => 11,
day => 4,
hour => 0,
time_zone => 'America/Chicago',
);
say $dt; # 2012-11-04T00:00:00
$dt->add( hours => 1 );
say $dt; # 2012-11-04T01:00:00
$dt->add( hours => 1 );
say $dt; # 2012-11-04T01:00:00
How Long is a Day?
my $dt = DateTime->new(
year => 2012,
month => 3,
day => 11,
hour => 0,
time_zone => 'America/Chicago',
);
say $dt; # 2012-03-11T00:00:00
$dt->add( hours => 1 );
say $dt; # 2012-03-11T01:00:00
$dt->add( hours => 1 );
say $dt; # 2012-03-11T03:00:00
No 02:00 for You!
my $dt = DateTime->new(
year => 2012,
month => 3,
day => 10,
hour => 2,
time_zone => 'America/Chicago',
);
say $dt; # 2012-03-11T00:02:00
$dt->add( days => 1 ); # Throws an exception ...
# Invalid local time for date in time zone: America/Chicago
# But this works
$dt->add( hours => 24 );
say $dt; # 2012-03-11T03:00:00
Math Order Matters
my $dt = DateTime->new(
year => 2011,
month => 2,
day => 28,
);
$dt->add( months => 1, days => 1 );
say $dt; # 2011-04-01, not 2011-03-29
Want control? Make separate calls:
$dt->add( months => 1 )->add( days => 1, );
say $dt; # 2011-03-29
More math gotchas
- Math is not always reversible
- $dt1 - $dt2 = $dur
- $dt2 + $dur != $dt1
- Math across DST changes is confusing
- $dst_date - $non_dst_date = ?
- Does the duration include the DST change's hour?
- Leap years
- Leap seconds
How to Do Math Safely
- Always use
add()
, delta_days()
, subtract()
, etc.
- Never write something like this:
$dt->set( day => $dt->day() + 1 )
- Use the floating time zone if you can
- Use UTC if you can - UTC has no DST changes
Ambiguous Local Times
my $dt = DateTime->new(
year => 2003,
month => 10,
day => 26,
hour => 1,
minute => 30,
second => 0,
time_zone => 'America/Chicago',
);
- Is this standard or DST time?
- There is a DST change on 2003-10-26 from 01:59:59 (DST) to 01:00:00 (standard)
- DateTime.pm always picks the latest UTC time
Storage and Presentation
- Store datetimes as floating or UTC whenever possible
- Or store them as a datetime + time zone (Pgs's
TIMESTAMP WITH TZ
type)
- Also store the named time zone if the database only stores an offset
- Don't store an epoch, store a datetime
- Use time zones for presentation to users
- Never just store datetimes in the machine's current local time zone
- What happens when you move?
(Stupid?) Performance tricks
My Rules of Optimization
- Don't optimize
- Don't optimize, I'm serious
- Don't optimize without benchmarking first
- Don't benchmark without profiling first
- See rule #1
Cache the time zone object
my $dt = DateTime->new(
year => 2013,
month => 6,
day => 5,
hour => 9,
minute => 30,
time_zone => 'America/Chicago',
);
my $tz = DateTime::TimeZone->new( name => 'America/Chicago' );
my $dt = DateTime->new(
year => 2013,
month => 6,
day => 5,
hour => 9,
minute => 30,
time_zone => $tz,
);
Don't Use a Parser
- If your data only comes in one flavor
my $dt = DateTime::Format::Foo->parse_datetime($string);
my ( $y, $m, $d ) = $string =~ /^(\d{4})-(\d{2})-(\d{2})/;
my $dt = DateTime->new(
year => $y,
month => $m,
day => $d,
);
Thank You