Posted on 2008-06-07 16:30:54-07 by belrick
Bug in Date::ICal.pm subtract subroutine

I believe I found a bug in the subtract subroutine of the Date/ICal.pm module. It is possible for the julian date difference to be of opposite sign from the julsec which by itself makes sense, however, the new() subroutine of the Date/ICal/Duration.pm module has an explicit comment that it assumes that if one of its julian & julsec args then both are negative:

# If one of the attributes is negative, then they all must be # negative. Otherwise, we're not sure what this means.

I've developed the following patch to ICal.pm that fixes it for my testcase script. The patch assumes that, in the subtract() subroutine, julsec is constrained between -86399 and +86399 and that using julian and julsec protects us from all daylight savings oddities.

Below you will find:
1) the patch,
2) the testcase,
3) the results of the testcase using the original ICal.pm, and
4) the results of the testcase using the patched ICal.pm

I've sent and email to Rich Bowen, but since these modules haven't changed in years (or at least that's how I interpret the release dates) I'm not sure if he is still maintaining the module, thus the post here.

Thanks,
Bruce

1) the patch

root@gandalf:/usr/share/perl5/Date# diff -U 7 ICal.pm.orig ICal.pm --- ICal.pm.orig 2008-06-07 10:20:02.000000000 -0600 +++ ICal.pm 2008-06-07 10:34:31.000000000 -0600 @@ -800,14 +800,32 @@ } else { # If $date2 is a Date::ICal object, or some class thereof, we should # subtract and get a duration my $days = $date1->{julian} - $date2->{julian}; my $secs = $date1->{julsec} - $date2->{julsec}; + if ($days * $secs < 0) { + # days and secs are opposite signs + # Date::ICal::Duration can't handle that, so massage values. + # The following assumes that julian and julsec make us immune + # to funkiness like daylight savings hours. + if ($secs < 0) { + $secs += 86400; + $days -= 1; + } else { + $secs -= 86400; + $days += 1; + } + if ($days * $secs < 0) { + # If we get here, then the logic used to build the above is + # flawed. The user should not rely on this value. + warn "Logic error in Date::ICal::subtract()"; + } + } return Date::ICal::Duration->new( days => $days, seconds => $secs ); } } elsif ( ref $date1 && ( $dur = Date::ICal::Duration->new( ical => $date2 ) )

2) the testcase,

#!/usr/bin/perl use Date::ICal; use Data::Dumper::Simple; $dt1 = Date::ICal->new( ical => 'TZID=America/Denver:20080606T175958'); $dt2 = Date::ICal->new( ical => 'TZID=America/Denver:20080606T175959'); $dt3 = Date::ICal->new( ical => 'TZID=America/Denver:20080606T180000'); $dt4 = Date::ICal->new( ical => 'TZID=America/Denver:20080606T180001'); $delta21 = $dt2 - $dt1; $delta32 = $dt3 - $dt2; $delta43 = $dt4 - $dt3; $delta31 = $dt3 - $dt1; $delta42 = $dt4 - $dt2; $delta41 = $dt4 - $dt1; $delta12 = $dt1 - $dt2; $delta23 = $dt2 - $dt3; $delta34 = $dt3 - $dt4; $delta13 = $dt1 - $dt3; $delta24 = $dt2 - $dt4; $delta14 = $dt1 - $dt4; $s21 = $delta21->as_seconds(); $s32 = $delta32->as_seconds(); $s43 = $delta43->as_seconds(); $s31 = $delta31->as_seconds(); $s42 = $delta42->as_seconds(); $s41 = $delta41->as_seconds(); $s12 = $delta12->as_seconds(); $s23 = $delta23->as_seconds(); $s34 = $delta34->as_seconds(); $s13 = $delta13->as_seconds(); $s24 = $delta24->as_seconds(); $s14 = $delta14->as_seconds(); print $dt1->ical(), "\n"; print $dt2->ical(), "\n"; print $dt3->ical(), "\n"; print $dt4->ical(), "\n"; printf "s21 (expect 2-1=1) %d\n", $s21; printf "s32 (expect 3-2=1) %d\n", $s32; printf "s43 (expect 4-3=1) %d\n", $s43; printf "s31 (expect 3-1=2) %d\n", $s31; printf "s42 (expect 4-2=2) %d\n", $s42; printf "s41 (expect 4-1=3) %d\n", $s41; printf "s12 (expect 1-2=-1) %d\n", $s12; printf "s23 (expect 2-3=-1) %d\n", $s23; printf "s34 (expect 3-4=-1) %d\n", $s34; printf "s13 (expect 1-3=-2) %d\n", $s13; printf "s24 (expect 2-4=-2) %d\n", $s24; printf "s14 (expect 1-4=-3) %d\n", $s14; print Dumper($dt3, $dt2, $delta32, $delta23);

3) the results of the testcase using the original ICal.pm,

20080606T235958Z 20080606T235959Z 20080607Z 20080607T000001Z s21 (expect 2-1=1) 1 s32 (expect 3-2=1) -172799 s43 (expect 4-3=1) 1 s31 (expect 3-1=2) -172798 s42 (expect 4-2=2) -172798 s41 (expect 4-1=3) -172797 s12 (expect 1-2=-1) -1 s23 (expect 2-3=-1) -172799 s34 (expect 3-4=-1) -1 s13 (expect 1-3=-2) -172798 s24 (expect 2-4=-2) -172798 s14 (expect 1-4=-3) -172797 $dt3 = bless( { 'julsec' => 0, 'julian' => 733200, 'offset' => -21600 }, 'Date::ICal' ); $dt2 = bless( { 'julsec' => 86399, 'julian' => 733199, 'offset' => -21600 }, 'Date::ICal' ); $delta32 = bless( { 'nsecs' => 86399, 'ndays' => 1, 'sign' => -1 }, 'Date::ICal::Duration' ); $delta23 = bless( { 'nsecs' => 86399, 'ndays' => 1, 'sign' => -1 }, 'Date::ICal::Duration' );

4) and the results of the testcase using the patched ICal.pm

20080606T235958Z 20080606T235959Z 20080607Z 20080607T000001Z s21 (expect 2-1=1) 1 s32 (expect 3-2=1) 1 s43 (expect 4-3=1) 1 s31 (expect 3-1=2) 2 s42 (expect 4-2=2) 2 s41 (expect 4-1=3) 3 s12 (expect 1-2=-1) -1 s23 (expect 2-3=-1) -1 s34 (expect 3-4=-1) -1 s13 (expect 1-3=-2) -2 s24 (expect 2-4=-2) -2 s14 (expect 1-4=-3) -3 $dt3 = bless( { 'julsec' => 0, 'julian' => 733200, 'offset' => -21600 }, 'Date::ICal' ); $dt2 = bless( { 'julsec' => 86399, 'julian' => 733199, 'offset' => -21600 }, 'Date::ICal' ); $delta32 = bless( { 'nsecs' => 1, 'ndays' => 0, 'sign' => 1 }, 'Date::ICal::Duration' ); $delta23 = bless( { 'nsecs' => 1, 'ndays' => 0, 'sign' => -1 }, 'Date::ICal::Duration' );
Direct Responses: Write a response
Perl Weekly newsletter
A free weekly newsletter for people who are busy to read all the blogs. click here to check it out.