Skip Menu |

Preferred bug tracker

Please visit the preferred bug tracker to report your issue.

This queue is for tickets about the DateTime CPAN distribution.

Report information
The Basics
Id: 79845
Status: resolved
Priority: 0/
Queue: DateTime

People
Owner: Nobody in particular
Requestors: mhasch-cpanbugs [...] cozap.com
Cc: datetime [...] perl.org
AdminCc:

Bug Information
Severity: Normal
Broken in:
  • 0.67
  • 0.68
  • 0.69
  • 0.70
  • 0.71
  • 0.72
  • 0.73
  • 0.74
  • 0.75
  • 0.76
Fixed in: 0.78



CC: datetime [...] perl.org
Subject: [PATCH] sub-second precision and rounding
Maintainers of DateTime, DateTime goes to some lengths to be consistent when doing "mathematics". This is very helpful. At one point, however, I see it breaking a pattern for no good reason. The pattern I am referring to is that dates and durations are split into units and sub-units (of various sizes) where units are *truncated* to whole numbers, and sub-units represent the finer-grained remainder if there is any. For example, a time "shortly before noon" is modeled with an hour value of 11, not 12, as the fractional part of the hour will be stored in (non-negative) minutes, seconds etc. Starting with DateTime-0.67, this pattern is broken where fractions of seconds are involved. $dt->millisecond gives a *rounded* value for the 1000th part of a second, which is not documented but checked in the test suite, and $dt->microsecond gives a *rounded* value for the millionth part of a second, which is documented but not less strange. It seems that this weirdness was introduced in an attempt to fix a rounding error in from_epoch, pointed out by Michael R. Davis in RT #66744. The root cause of this error made a second appearance in RT #67736 (reported by Zefram) and was properly taken care of soon after in DateTime-0.68. I would like to encourage you to revert the rounding behaviour of milli- and microseconds back to truncating (patch enclosed). It is funny that DateTime math should achieve consistency dealing with the most messy details of our calendar and fail to do so with its only metric component. Rectifying this also relieves DateTime from a non-core dependency. To demonstrate that the current rounding behaviour is not quite thought through, try: use DateTime 0.76; my $d = DateTime->new( year => 2012, month => 9, day => 25, hour => 12, minute => 39, second => 59, nanosecond => 999876000, time_zone => 'Europe/Berlin', ); print $d->strftime('%H:%M:%S.%3N'), "\n"; This will print "12:39:59.1000" rather than "12:39:59.999". And no, please don't make it print "12:40:00.000" either. Rounding is just not the right thing to do by default. -Martin [Cc to the mailing list as this might call for some discussion]
And here is the patch. -Martin
Subject: DateTime-0.76-MHASCH-01.patch
diff -rup DateTime-0.76.orig/Build.PL DateTime-0.76/Build.PL --- DateTime-0.76.orig/Build.PL 2012-07-01 23:55:52.000000000 +0200 +++ DateTime-0.76/Build.PL 2012-09-25 12:34:19.000000000 +0200 @@ -31,7 +31,6 @@ my %module_build_args = ( "Carp" => 0, "DateTime::Locale" => "0.41", "DateTime::TimeZone" => "1.09", - "Math::Round" => 0, "Params::Validate" => "0.76", "Scalar::Util" => 0, "XSLoader" => 0, diff -rup DateTime-0.76.orig/lib/DateTime.pm DateTime-0.76/lib/DateTime.pm --- DateTime-0.76.orig/lib/DateTime.pm 2012-07-01 23:55:52.000000000 +0200 +++ DateTime-0.76/lib/DateTime.pm 2012-09-25 12:30:47.000000000 +0200 @@ -44,7 +44,7 @@ use DateTime::Duration; use DateTime::Helpers; use DateTime::Locale 0.41; use DateTime::TimeZone 1.09; -use Math::Round qw( nearest round ); +use POSIX qw(floor); use Params::Validate 0.76 qw( validate validate_pos UNDEF SCALAR BOOLEAN HASHREF OBJECT ); @@ -824,9 +824,9 @@ sub nanosecond { return $_[0]->{rd_nanosecs}; } -sub millisecond { round( $_[0]->{rd_nanosecs} / 1000000 ) } +sub millisecond { floor( $_[0]->{rd_nanosecs} / 1000000 ) } -sub microsecond { round( $_[0]->{rd_nanosecs} / 1000 ) } +sub microsecond { floor( $_[0]->{rd_nanosecs} / 1000 ) } sub leap_seconds { my $self = shift; @@ -1304,7 +1304,7 @@ sub _format_nanosecs { return sprintf( '%0' . $precision . 'u', - round( $self->{rd_nanosecs} / $divide_by ) + floor( $self->{rd_nanosecs} / $divide_by ) ); } @@ -2647,10 +2647,9 @@ Half a second is 500 milliseconds. =item * $dt->microsecond() Returns the fractional part of the second as microseconds (1E-6 -seconds). This value will be rounded to an integer. +seconds). -Half a second is 500_000 microseconds. This value will be rounded to -an integer. +Half a second is 500_000 microseconds. =item * $dt->nanosecond() diff -rup DateTime-0.76.orig/t/03components.t DateTime-0.76/t/03components.t --- DateTime-0.76.orig/t/03components.t 2012-07-01 23:55:52.000000000 +0200 +++ DateTime-0.76/t/03components.t 2012-09-25 09:36:49.000000000 +0200 @@ -260,14 +260,14 @@ is( $monday->day_of_week, 1, "Monday is $dt->set( nanosecond => 500_000_500 ); is( $dt->nanosecond, 500_000_500, 'nanosecond is 500,000,500' ); - is( $dt->microsecond, 500_001, 'microsecond is 500,001' ); + is( $dt->microsecond, 500_000, 'microsecond is 500,001' ); is( $dt->millisecond, 500, 'millisecond is 500' ); $dt->set( nanosecond => 499_999_999 ); is( $dt->nanosecond, 499_999_999, 'nanosecond is 499,999,999' ); - is( $dt->microsecond, 500_000, 'microsecond is 500,000' ); - is( $dt->millisecond, 500, 'millisecond is 500' ); + is( $dt->microsecond, 499_999, 'microsecond is 499,999' ); + is( $dt->millisecond, 499, 'millisecond is 499' ); $dt->set( nanosecond => 450_000_001 ); @@ -279,7 +279,7 @@ is( $monday->day_of_week, 1, "Monday is is( $dt->nanosecond, 450_500_000, 'nanosecond is 450,500,000' ); is( $dt->microsecond, 450_500, 'microsecond is 450,500' ); - is( $dt->millisecond, 451, 'millisecond is 451' ); + is( $dt->millisecond, 450, 'millisecond is 450' ); } { diff -rup DateTime-0.76.orig/t/13strftime.t DateTime-0.76/t/13strftime.t --- DateTime-0.76.orig/t/13strftime.t 2012-07-01 23:55:52.000000000 +0200 +++ DateTime-0.76/t/13strftime.t 2012-09-25 09:41:22.000000000 +0200 @@ -209,7 +209,7 @@ year => 1999, month => 9, day => 7, hour %M => '02' %N => '123456789' %3N => '123' -%6N => '123457' +%6N => '123456' %10N => '1234567890' %p => 'PM' %r => '01:02:42 PM' @@ -314,5 +314,5 @@ year => 2012, month => 1, day => 10 year => 1999, month => 9, day => 7, hour => 13, minute => 2, second => 42, nanosecond => 00012345678 %N => '012345678' %3N => '012' -%6N => '012346' +%6N => '012345' %10N => '0123456780'
On Tue Sep 25 07:27:53 2012, MHASCH wrote: Show quoted text
> To demonstrate that the current rounding behaviour is not > quite thought through, try: > > use DateTime 0.76; > my $d = DateTime->new( > year => 2012, > month => 9, > day => 25, > hour => 12, > minute => 39, > second => 59, > nanosecond => 999876000, > time_zone => 'Europe/Berlin', > ); > print $d->strftime('%H:%M:%S.%3N'), "\n"; > > This will print "12:39:59.1000" rather than "12:39:59.999". > And no, please don't make it print "12:40:00.000" either. > Rounding is just not the right thing to do by default.
There were a few RT tickets about this and I've thought about it a fair bit. I think your solution is right. This gets really messy with a datetime like 2011-12-31T23:59:59.999 To round properly we'd have to round every single unit, including the year! There's really no way to do this in strftime and format_cldr anyway, since you can only dictate the formatting _per unit_, so you might not even be printing out all the units which need rounding.