1#!/usr/bin/perl -w
2
3use strict;
4
5use Test::More tests => 127;
6
7use DateTime;
8
9# These tests should be the final word on dt subtraction involving a
10# DST-changing time zone
11
12{
13    my $dt1 = DateTime->new( year => 2003, month => 5, day => 6,
14                             time_zone => 'America/Chicago',
15                           );
16
17    my $dt2 = DateTime->new( year => 2003, month => 11, day => 6,
18                             time_zone => 'America/Chicago',
19                           );
20
21    my $dur1 = $dt2->subtract_datetime($dt1);
22    my %deltas1 = $dur1->deltas;
23    is( $deltas1{months}, 6, 'delta_months is 6' );
24    is( $deltas1{days}, 0, 'delta_days is 0' );
25    is( $deltas1{minutes}, 0, 'delta_minutes is 0' );
26    is( $deltas1{seconds}, 0, 'delta_seconds is 0' );
27
28    is( DateTime->compare( $dt1->clone->add_duration($dur1), $dt2 ), 0,
29        'subtract_datetime is reversible from start point' );
30    is( DateTime->compare( $dt2->clone->subtract_duration($dur1), $dt1 ), 0,
31        'subtract_datetime is reversible from end point' );
32    is( $deltas1{nanoseconds}, 0, 'delta_nanoseconds is 0' );
33
34    my $dur2 = $dt1->subtract_datetime($dt2);
35    my %deltas2 = $dur2->deltas;
36    is( $deltas2{months}, -6, 'delta_months is -6' );
37    is( $deltas2{days}, 0, 'delta_days is 0' );
38    is( $deltas2{minutes}, 0, 'delta_minutes is 0' );
39    is( $deltas2{seconds}, 0, 'delta_seconds is 0' );
40    is( $deltas2{nanoseconds}, 0, 'delta_nanoseconds is 0' );
41
42    my $dur3 = $dt2->delta_md($dt1);
43    my %deltas3 = $dur3->deltas;
44    is( $deltas3{months}, 6, 'delta_months is 6' );
45    is( $deltas3{days}, 0, 'delta_days is 0' );
46    is( $deltas3{minutes}, 0, 'delta_minutes is 0' );
47    is( $deltas3{seconds}, 0, 'delta_seconds is 0' );
48    is( $deltas3{nanoseconds}, 0, 'delta_nanoseconds is 0' );
49
50    is( DateTime->compare( $dt1->clone->add_duration($dur3), $dt2 ), 0,
51        'delta_md is reversible from start point' );
52    is( DateTime->compare( $dt2->clone->subtract_duration($dur3), $dt1 ), 0,
53        'delta_md is reversible from end point' );
54
55    my $dur4 = $dt2->delta_days($dt1);
56    my %deltas4 = $dur4->deltas;
57    is( $deltas4{months}, 0, 'delta_months is 0' );
58    is( $deltas4{days}, 184, 'delta_days is 184' );
59    is( $deltas4{minutes}, 0, 'delta_minutes is 0' );
60    is( $deltas4{seconds}, 0, 'delta_seconds is 0' );
61    is( $deltas4{nanoseconds}, 0, 'delta_nanoseconds is 0' );
62
63    is( DateTime->compare( $dt1->clone->add_duration($dur3), $dt2 ), 0,
64        'delta_days is reversible from start point' );
65    is( DateTime->compare( $dt2->clone->subtract_duration($dur4), $dt1 ), 0,
66        'delta_days is reversible from end point' );
67}
68
69# same as above, but now the UTC hour of the earlier datetime is
70# _greater_ than that of the later one.  this checks that overflows
71# are handled correctly.
72{
73    my $dt1 = DateTime->new( year => 2003, month => 5, day => 6, hour => 18,
74                             time_zone => 'America/Chicago',
75                           );
76
77    my $dt2 = DateTime->new( year => 2003, month => 11, day => 6, hour => 18,
78                             time_zone => 'America/Chicago',
79                           );
80
81    my $dur1 = $dt2->subtract_datetime($dt1);
82    my %deltas1 = $dur1->deltas;
83    is( $deltas1{months}, 6, 'delta_months is 6' );
84    is( $deltas1{days}, 0, 'delta_days is 0' );
85    is( $deltas1{minutes}, 0, 'delta_minutes is 0' );
86    is( $deltas1{seconds}, 0, 'delta_seconds is 0' );
87    is( $deltas1{nanoseconds}, 0, 'delta_nanoseconds is 0' );
88}
89
90# make sure delta_md and delta_days work in the face of DST change
91# where we lose an hour
92{
93    my $dt1 = DateTime->new( year => 2003, month => 11, day => 6,
94                             time_zone => 'America/Chicago',
95                           );
96
97    my $dt2 = DateTime->new( year => 2004, month => 5, day => 6,
98                             time_zone => 'America/Chicago',
99                           );
100
101    my $dur1 = $dt2->delta_md($dt1);
102    my %deltas1 = $dur1->deltas;
103    is( $deltas1{months}, 6, 'delta_months is 6' );
104    is( $deltas1{days}, 0, 'delta_days is 0' );
105    is( $deltas1{minutes}, 0, 'delta_minutes is 0' );
106    is( $deltas1{seconds}, 0, 'delta_seconds is 0' );
107    is( $deltas1{nanoseconds}, 0, 'delta_nanoseconds is 0' );
108
109    my $dur2 = $dt2->delta_days($dt1);
110    my %deltas2 = $dur2->deltas;
111    is( $deltas2{months}, 0, 'delta_months is 0' );
112    is( $deltas2{days}, 182, 'delta_days is 182' );
113    is( $deltas2{minutes}, 0, 'delta_minutes is 0' );
114    is( $deltas2{seconds}, 0, 'delta_seconds is 0' );
115    is( $deltas2{nanoseconds}, 0, 'delta_nanoseconds is 0' );
116
117}
118
119# the docs say use UTC to guarantee reversibility
120{
121    my $dt1 = DateTime->new( year => 2003, month => 5, day => 6,
122                             time_zone => 'America/Chicago',
123                           );
124
125    my $dt2 = DateTime->new( year => 2003, month => 11, day => 6,
126                             time_zone => 'America/Chicago',
127                           );
128
129    $dt1->set_time_zone('UTC');
130    $dt2->set_time_zone('UTC');
131
132    my $dur = $dt2->subtract_datetime($dt1);
133
134    is( DateTime->compare( $dt1->add_duration($dur), $dt2 ), 0,
135        'subtraction is reversible from start point with UTC' );
136    is( DateTime->compare( $dt2->subtract_duration($dur), $dt2 ), 0,
137        'subtraction is reversible from start point with UTC' );
138}
139
140# The important thing here is that after a subtraction, we can use the
141# duration to get from one date to the other, regardless of the type
142# of subtraction done.
143{
144    my $dt1 = DateTime->new( year => 2003, month => 5, day => 6,
145                             time_zone => 'America/Chicago',
146                           );
147
148    my $dt2 = DateTime->new( year => 2003, month => 11, day => 6,
149                             time_zone => 'America/Chicago',
150                           );
151
152    my $dur1 = $dt2->subtract_datetime_absolute($dt1);
153
154    my %deltas1 = $dur1->deltas;
155    is( $deltas1{months}, 0, 'delta_months is 0' );
156    is( $deltas1{days}, 0, 'delta_days is 0' );
157    is( $deltas1{minutes}, 0, 'delta_minutes is 0' );
158    is( $deltas1{seconds}, 15901200, 'delta_seconds is 15901200' );
159    is( $deltas1{nanoseconds}, 0, 'delta_nanoseconds is 0' );
160
161    is( DateTime->compare( $dt1->clone->add_duration($dur1), $dt2 ), 0,
162        'subtraction is reversible' );
163    is( DateTime->compare( $dt2->clone->subtract_duration($dur1), $dt1 ), 0,
164        'subtraction is doubly reversible' );
165
166    my $dur2 = $dt1->subtract_datetime_absolute($dt2);
167
168    my %deltas2 = $dur2->deltas;
169    is( $deltas2{months}, 0, 'delta_months is 0' );
170    is( $deltas2{days}, 0, 'delta_days is 0' );
171    is( $deltas2{minutes}, 0, 'delta_minutes is 0' );
172    is( $deltas2{seconds}, -15901200, 'delta_seconds is -15901200' );
173    is( $deltas2{nanoseconds}, 0, 'delta_nanoseconds is 0' );
174
175    is( DateTime->compare( $dt2->clone->add_duration($dur2), $dt1 ), 0,
176        'subtraction is reversible' );
177    is( DateTime->compare( $dt1->clone->subtract_duration($dur2), $dt2 ), 0,
178        'subtraction is doubly reversible' );
179}
180
181{
182    my $dt1 = DateTime->new( year => 2003, month => 4, day => 6,
183                             hour => 1, minute => 58,
184                             time_zone => 'America/Chicago',
185                           );
186
187    my $dt2 = DateTime->new( year => 2003, month => 4, day => 6,
188                             hour => 3, minute => 1,
189                             time_zone => 'America/Chicago',
190                           );
191
192    my $dur = $dt2->subtract_datetime($dt1);
193
194    my %deltas = $dur->deltas;
195    is( $deltas{months}, 0, 'delta_months is 0' );
196    is( $deltas{days}, 0, 'delta_days is 0' );
197    is( $deltas{minutes}, 3, 'delta_minutes is 3' );
198    is( $deltas{seconds}, 0, 'delta_seconds is 0' );
199    is( $deltas{nanoseconds}, 0, 'delta_nanoseconds is 0' );
200
201    is( DateTime->compare( $dt1->clone->add_duration($dur), $dt2 ), 0,
202        'subtraction is reversible' );
203    is( DateTime->compare( $dt2->clone->subtract_duration($dur), $dt1), 0,
204        'subtraction is doubly reversible' );
205}
206
207{
208    my $dt1 = DateTime->new( year => 2003, month => 4, day => 5,
209                             hour => 1, minute => 58,
210                             time_zone => 'America/Chicago',
211                           );
212
213    my $dt2 = DateTime->new( year => 2003, month => 4, day => 6,
214                             hour => 3, minute => 1,
215                             time_zone => 'America/Chicago',
216                           );
217
218    my $dur = $dt2->subtract_datetime($dt1);
219
220    my %deltas = $dur->deltas;
221    is( $deltas{months}, 0, 'delta_months is 0' );
222    is( $deltas{days}, 1, 'delta_days is 1' );
223    is( $deltas{minutes}, 3, 'delta_minutes is 3' );
224    is( $deltas{seconds}, 0, 'delta_seconds is 0' );
225    is( $deltas{nanoseconds}, 0, 'delta_nanoseconds is 0' );
226
227    is( DateTime->compare( $dt1->clone->add_duration($dur), $dt2 ), 0,
228        'dt1 + dur = dt2' );
229    # this are two examples from the docs
230    is( DateTime->compare( $dt2->clone->subtract_duration($dur),
231                           $dt1->clone->add( hours => 1 ) ),
232        0,
233        'dt2 - dur != dt1 (not reversible)' );
234    is( DateTime->compare( $dt2->clone->subtract_duration( $dur->clock_duration )
235                               ->subtract_duration( $dur->calendar_duration ),
236                           $dt1 ),
237        0,
238        'dt2 - dur->clock - dur->cal = dt1 (reversible when componentized)' );
239
240    my $dur2 = $dt1->subtract_datetime($dt2);
241    my %deltas2 = $dur2->deltas;
242    is( $deltas2{months}, 0, 'delta_months is 0' );
243    is( $deltas2{days}, -1, 'delta_days is 1' );
244    is( $deltas2{minutes}, -3, 'delta_minutes is 3' );
245    is( $deltas2{seconds}, 0, 'delta_seconds is 0' );
246    is( $deltas2{nanoseconds}, 0, 'delta_nanoseconds is 0' );
247    is( $dt2->clone->add_duration($dur2)->datetime, '2003-04-05T02:58:00', 'dt2 + dur2 != dt1' );
248    is( DateTime->compare( $dt2->clone->add_duration( $dur2->clock_duration )
249                               ->add_duration( $dur2->calendar_duration ),
250                           $dt1 ),
251        0,
252        'dt2 + dur2->clock + dur2->cal = dt1' );
253    is( DateTime->compare( $dt1->clone->subtract_duration($dur2), $dt2 ), 0,
254        'dt1 - dur2 = dt2' );
255
256}
257
258# These tests makes sure that days with DST changes are "normal" when
259# they're the smaller operand
260{
261    my $dt1 = DateTime->new( year => 2003, month => 4, day => 6,
262                             hour => 3, minute => 1,
263                             time_zone => 'America/Chicago',
264                           );
265
266    my $dt2 = DateTime->new( year => 2003, month => 4, day => 7,
267                             hour => 3, minute => 2,
268                             time_zone => 'America/Chicago',
269                           );
270
271    my $dur = $dt2->subtract_datetime($dt1);
272
273    my %deltas = $dur->deltas;
274    is( $deltas{months}, 0, 'delta_months is 0' );
275    is( $deltas{days}, 1, 'delta_days is 1' );
276    is( $deltas{minutes}, 1, 'delta_minutes is 1' );
277    is( $deltas{seconds}, 0, 'delta_seconds is 0' );
278    is( $deltas{nanoseconds}, 0, 'delta_nanoseconds is 0' );
279
280    my $dur2 = $dt1->subtract_datetime($dt2);
281
282    my %deltas2 = $dur2->deltas;
283    is( $deltas2{months}, 0, 'delta_months is 0' );
284    is( $deltas2{days}, -1, 'delta_days is -1' );
285    is( $deltas2{minutes}, -1, 'delta_minutes is -1' );
286    is( $deltas2{seconds}, 0, 'delta_seconds is 0' );
287    is( $deltas2{nanoseconds}, 0, 'delta_nanoseconds is 0' );
288
289}
290
291{
292    my $dt1 = DateTime->new( year => 2003, month => 4, day => 5,
293                             hour => 1, minute => 58,
294                             time_zone => 'America/Chicago',
295                           );
296
297    my $dt2 = DateTime->new( year => 2003, month => 4, day => 7,
298                             hour => 2, minute => 1,
299                             time_zone => 'America/Chicago',
300                           );
301
302    my $dur = $dt2->subtract_datetime($dt1);
303
304    my %deltas = $dur->deltas;
305    is( $deltas{months}, 0, 'delta_months is 0' );
306    is( $deltas{days}, 2, 'delta_days is 2' );
307    is( $deltas{minutes}, 3, 'delta_minutes is 3' );
308    is( $deltas{seconds}, 0, 'delta_seconds is 0' );
309    is( $deltas{nanoseconds}, 0, 'delta_nanoseconds is 0' );
310
311    is( DateTime->compare( $dt1->clone->add_duration($dur), $dt2 ), 0,
312        'subtraction is reversible' );
313    is( DateTime->compare( $dt2->clone->subtract_duration($dur), $dt1 ), 0,
314        'subtraction is doubly reversible' );
315}
316
317# from example in docs
318{
319    my $dt1 = DateTime->new( year => 2003, month => 5, day => 6,
320                             time_zone => 'America/Chicago',
321                           );
322
323    my $dt2 = DateTime->new( year => 2003, month => 11, day => 6,
324                             time_zone => 'America/Chicago',
325                           );
326
327    $dt1->set_time_zone('floating');
328    $dt2->set_time_zone('floating');
329
330    my $dur = $dt2->subtract_datetime($dt1);
331    my %deltas = $dur->deltas;
332    is( $deltas{months}, 6, 'delta_months is 6' );
333    is( $deltas{days}, 0, 'delta_days is 0' );
334    is( $deltas{minutes}, 0, 'delta_minutes is 0' );
335    is( $deltas{seconds}, 0, 'delta_seconds is 0' );
336    is( $deltas{nanoseconds}, 0, 'delta_nanoseconds is 0' );
337
338    is( DateTime->compare( $dt1->clone->add_duration($dur), $dt2 ), 0,
339        'subtraction is reversible from start point' );
340    is( DateTime->compare( $dt2->clone->subtract_duration($dur), $dt1 ), 0,
341        'subtraction is reversible from end point' );
342}
343
344{
345    my $dt1 = DateTime->new( year => 2005, month => 8,
346                             time_zone => 'Europe/London',
347                           );
348
349    my $dt2 = DateTime->new( year => 2005, month => 11,
350                             time_zone => 'Europe/London',
351                           );
352
353    my $dur = $dt2->subtract_datetime($dt1);
354    my %deltas = $dur->deltas;
355    is( $deltas{months}, 3, '3 months between two local times over DST change' );
356    is( $deltas{days}, 0, '0 days between two local times over DST change' );
357    is( $deltas{minutes}, 0, '0 minutes between two local times over DST change' );
358}
359
360# same as previous but without hours overflow
361{
362    my $dt1 = DateTime->new( year => 2005, month => 8, hour => 12,
363                             time_zone => 'Europe/London',
364                           );
365
366    my $dt2 = DateTime->new( year => 2005, month => 11, hour => 12,
367                             time_zone => 'Europe/London',
368                           );
369
370    my $dur = $dt2->subtract_datetime($dt1);
371    my %deltas = $dur->deltas;
372    is( $deltas{months}, 3, '3 months between two local times over DST change' );
373    is( $deltas{days}, 0, '0 days between two local times over DST change' );
374    is( $deltas{minutes}, 0, '0 minutes between two local times over DST change' );
375}
376
377# another docs example
378{
379    my $dt2 = DateTime->new( year => 2003, month => 10, day => 26,
380                             hour => 1,
381                             time_zone => 'America/Chicago',
382                           );
383
384    my $dt1 = $dt2->clone->subtract( hours => 1 );
385
386    my $dur = $dt2->subtract_datetime($dt1);
387
388    my %deltas = $dur->deltas;
389    is( $deltas{months}, 0, '0 months between two local times over DST change' );
390    is( $deltas{days}, 0, '0 days between two local times over DST change' );
391    is( $deltas{minutes}, 60, '60 minutes between two local times over DST change' );
392
393    is( DateTime->compare( $dt1->clone->add_duration($dur), $dt2 ), 0,
394        'subtraction is reversible' );
395    is( DateTime->compare( $dt2->clone->subtract_duration($dur), $dt1 ), 0,
396        'subtraction is doubly reversible' );
397}
398
399{
400    my $dt1 = DateTime->new( year => 2003, month => 5, day => 6,
401                             time_zone => 'America/New_York',
402                           );
403
404    my $dt2 = DateTime->new( year => 2003, month => 5, day => 6,
405                             time_zone => 'America/Chicago',
406                           );
407
408    my $dur = $dt2->subtract_datetime($dt1);
409
410    my %deltas = $dur->deltas;
411    is( $deltas{months}, 0, '0 months between two local times over DST change' );
412    is( $deltas{days}, 0, '0 days between two local times over DST change' );
413    is( $deltas{minutes}, 60, '60 minutes between two local times over DST change' );
414
415    is( DateTime->compare( $dt1->clone->add_duration($dur), $dt2 ), 0,
416        'subtraction is reversible' );
417    is( DateTime->compare( $dt2->clone->subtract_duration($dur), $dt1 ), 0,
418        'subtraction is doubly reversible' );
419}
420
421# Fix a bug that occurred when the local time zone had DST and the two
422# datetime objects were on the same day
423{
424    my $dt1 = DateTime->new( year => 2005, month => 4, day => 3,
425                             hour => 7, minute => 0,
426                             time_zone => 'America/New_York' );
427
428    my $dt2 = DateTime->new( year => 2005, month => 4, day => 3,
429                             hour => 8, minute => 0,
430                             time_zone => 'America/New_York' );
431
432    my $dur = $dt2->subtract_datetime($dt1);
433    my ( $minutes, $seconds ) = $dur->in_units( 'minutes','seconds' );
434
435    is( $minutes, 60, 'subtraction of two dates on a DST change date, minutes == 60' );
436    is( $seconds, 0, 'subtraction of two dates on a DST change date, seconds == 0' );
437
438    $dur = $dt1->subtract_datetime($dt1);
439    ok( $dur->is_zero, 'dst change date (no dst) - itself, duration is zero' );
440}
441
442{
443    my $dt1 = DateTime->new( year => 2005, month => 4, day => 3,
444                             hour => 1, minute => 0,
445                             time_zone => 'America/New_York' );
446
447    my $dur = $dt1->subtract_datetime($dt1);
448    ok( $dur->is_zero, 'dst change date (with dst) - itself, duration is zero' );
449}
450
451# This tests a bug where one of the datetimes is changing DST, and the
452# other is not. In this case, no "adjustments" (aka hacks) are made in
453# subtract_datetime, and it just gives the "UTC difference".
454{
455    # This is UTC-4
456    my $dt1 = DateTime->new( year => 2009, month => 3, day => 9,
457                             time_zone => 'America/New_York' );
458    # This is UTC+8
459    my $dt2 = DateTime->new( year => 2009, month => 3, day => 9,
460                             time_zone => 'Asia/Hong_Kong' );
461
462    my $dur = $dt1->subtract_datetime($dt2);
463
464    is( $dur->delta_minutes, 720,
465        'subtraction the day after a DST change in one zone, where the other datetime is in a different zone' );
466}
467
468{
469    # This is UTC-5
470    my $dt1 = DateTime->new( year => 2009, month => 3, day => 8,
471                             hour => 1,
472                             time_zone => 'America/New_York' );
473    # This is UTC+8
474    my $dt2 = DateTime->new( year => 2009, month => 3, day => 8,
475                             hour => 1,
476                             time_zone => 'Asia/Hong_Kong' );
477
478    my $dur = $dt1->subtract_datetime($dt2);
479
480    is( $dur->delta_minutes, 780,
481        'subtraction the day of a DST change in one zone (before the change),'
482        . ' where the other datetime is in a different zone' );
483}
484
485
486{
487    # This is UTC-4
488    my $dt1 = DateTime->new( year => 2009, month => 3, day => 8,
489                             hour => 4,
490                             time_zone => 'America/New_York' );
491    # This is UTC+8
492    my $dt2 = DateTime->new( year => 2009, month => 3, day => 8,
493                             hour => 4,
494                             time_zone => 'Asia/Hong_Kong' );
495
496    my $dur = $dt1->subtract_datetime($dt2);
497
498    is( $dur->delta_minutes, 720,
499        'subtraction the day of a DST change in one zone (after the change),'
500        . ' where the other datetime is in a different zone' );
501}
502
503