Simple math in DateTime: partitioning an interval

jdavidb on 2007-08-07T19:29:07

I loved Time::Piece (formerly Time::Object) with a passion. After Y2K I sketched out a plan for two Perl libraries that would perform date functions. It sat on my whiteboard for several months, and then suddenly Matt Sergeant released a module that implemented it almost exactly. It worked exactly the way I wanted to think. There was a class to represent moments in time, there was a class to represent intervals of time, and every operator possible was overloaded in meaningful ways. If you subtracted two points in time you got an interval, if you added an interval to a point in time you got a point in time, if you performed multiplication or division on an interval it just worked, and on and on. Intervals were real objects, not just integers representing some unit of time like in Oracle or other systems I've seen.

DateTime came out, and I knew it was the next best thing, and in fact Time::Object was reimplemented to use it internally. But the learning curve was steep. I slowly picked up a bit here and there, wrote myself some utility programs, and then when I got interested in other calendars and some of the more esoteric things that happened to have DateTime-related modules on CPAN I converted and started writing with it.

But DateTime's two classes did not work the way I expected. I tried to perform division on a time interval and got something that was not a time interval. DateTime refused to perform all sorts of conversions because of exception cases that meant that certain things were not always true. Every time I touched it I found myself looking things up ...

... and looking things up didn't help. Time interval objects didn't stringify. They appeared to have redundant methods. There weren't ways to say "Look, don't give me the seconds component of the interval, tell me how many seconds long the interval really is, and since I computed it from two moments in time this is not a meaningless question that can't be answered; tell me, and tell me in a way that I can perform math on." And the documentation had an oh-so-helpful section on "How Datetime Math is done" that really didn't help at all, because it dealt with all sorts of exceptions I didn't care about and didn't actually tell me how to do what I used to just do with Time::Object.

Of course, I couldn't go back for a host of reasons. So I'd puzzle something out, and then when I needed it again I'd hope I'd find it lying around in one of my programs somewhere so I could refer to it again.

So it's time to start recording some of this knowledge here. IMO, DateTime long since should've had a date math section in the documentation that actually shows how to do these things.

So, today's puzzle is: given two DateTimes, divide the interval between them into N equal parts and give me back a series of N + 1 DateTimes, from $start to $end, all evenly spaced:

sub partition_datetime
{
  my($start, $end, $n) = @_;
  my @a;
  my $int = $end - $start;
  for my $i (0 .. $n)
  {
    push @a, $start + $int->clone->multiply($i / $n);
  }
  return @a;
}

Of course, I'm scared to death this somehow doesn't really work right because DateTime is so pathological about saying, "I can't really tell if this interval divides like this or like that because sometimes days have extra hours or seconds or something." Even though I'm doing it all in UTC.

Update: No, it doesn't work. If I divide into too many partitions, DateTime can't handle adding fractional days, hours, minutes, seconds, etc. Look, the interval is three days long. Divide it into 250 equal portions and let me know where those breaks are. I could do it in a snap with Time::Object.


how about...

jtrammell on 2007-08-07T21:12:22

sub partition {
    my ($start, $end, $n) = @_;
    my $s = $start->epoch;
    my $e = $end->epoch;
    my $i = int( ($e - $s) / $n );    # interval
    return map DateTime->from_epoch(epoch => $s + $i * $_), 0 .. $n;
}
?

Re:how about...

jdavidb on 2007-08-08T02:00:52

What's the point of having classes to handle date arithmetic if you have to convert back into numbers in order to do the math?

That works, but one shouldn't have to go to those lengths.

There's always the DateTime FAQ code...

Hasher_Bob on 2007-08-07T22:07:07

http://datetime.perl.org/index.cgi?FAQSampleCalculations

# As a Perl list
my $start_dt = DateTime->new(year => 1998, month  => 4,  day => 7);
my $end_dt   = DateTime->new(year => 1998, month  => 7,  day => 7);

my @list = ();
for (my $dt = $start_dt->clone();
     $dt <= $end_dt;
     $dt->add(weeks => 1) )
{
  push @list, $dt->clone();
}

# As a DateTime::Set.  We use DateTime::Event::Recurrence to easily
# create the sets (see also DateTime::Event::ICal for more
# complicated sets)
use DateTime::Event::Recurrence;
use DateTime::Span;
my $set = DateTime::Event::Recurrence->daily(start    => $start_dt,
                                             interval => 7);

$set = $set->intersection(DateTime::Span->from_datetimes(start => $start_dt, end => $end_dt ));

Re:There's always the DateTime FAQ code...

jdavidb on 2007-08-08T02:05:46

The first of those solutions only works if you know the size of the intervals you want to partition into ahead of time. I want to deal with the general case: say I have two moments in time that are exactly 29 days, 3 hours, 1 minute, and 17 seconds apart, and I want to divide it into 8 equal intervals? (My next journal entry does reveal the solution, but I'm just responding to show that I'm dealing with a harder problem than your solution addressed.)

I think the same is true of the second solution. The crucial issue is how to perform meaningful math on a DateTime::Duration. And the real issue behind it is that I'm always dealing with durations that are the result of subtracting two DateTime objects, and the output of the overloaded subtraction operator is a DateTime::Duration that is basically useless for most of the purposes I want to use it for. The good news is there's a DateTime->subtract_datetime_absolute method that yields what I want and, in my opinion, should be what the subtraction operator overloads to.