http://qs321.pair.com?node_id=1033231

eibwen has asked for the wisdom of the Perl Monks concerning the following question:

Objective: Determine if a given day is an occurrence of a bi-weekly event.

Approach: Given the complexities of dates, opted to use a date module, hence DateTime::Set.

Code:

#!/usr/bin/perl -w use strict; use diagnostics; use DateTime; use DateTime::Set; use Test::More; my $biweekly = DateTime::Set->from_recurrence( 'recurrence' => sub { return $_[0]->truncate('to' => 'day')->add('days' => 1 +4) }, 'start' => DateTime->today() ); print 'FIRST 5 ELEMENTS OF SET', $/; my $iterator = $biweekly->iterator; for (0..4) { my $date = $iterator->next; print $date->datetime, $/; } print $/; print 'TESTING whether Date is in set...', $/; my $date = DateTime->today(); is($biweekly->contains($date), 1, $date->ymd . ' *IS* in the bi-weekly + set'); $date->add('days' => 7); is($biweekly->contains($date), 0, $date->ymd . ' is *NOT* the bi-weekl +y set'); $date->add('days' => 7); is($biweekly->contains($date), 1, $date->ymd . ' *IS* the bi-weekly se +t'); $date->add('days' => 7); is($biweekly->contains($date), 0, $date->ymd . ' is *NOT* the bi-weekl +y set'); $date->add('days' => 7); is($biweekly->contains($date), 1, $date->ymd . ' *IS* the bi-weekly se +t'); # ... done_testing();

Output:

FIRST 5 ELEMENTS OF SET 2013-05-12T00:00:00 2013-05-26T00:00:00 2013-06-09T00:00:00 2013-06-23T00:00:00 2013-07-07T00:00:00 TESTING whether Date is in set... ok 1 - 2013-05-12 *IS* in the bi-weekly set not ok 2 - 2013-05-19 is *NOT* the bi-weekly set # Failed test '2013-05-19 is *NOT* the bi-weekly set' # at ./off-friday line 28. # got: '1' # expected: '0' ok 3 - 2013-05-26 *IS* the bi-weekly set not ok 4 - 2013-06-02 is *NOT* the bi-weekly set # Failed test '2013-06-02 is *NOT* the bi-weekly set' # at ./off-friday line 32. # got: '1' # expected: '0' ok 5 - 2013-06-09 *IS* the bi-weekly set 1..5 # Looks like you failed 2 tests of 5.

NOTE: get the same results with closest, contains, current, intersects methods.

CLARIFICATION UPDATE: I have working code using a different method. The question here is not how to accomplish the objective, but rather why didn't this particular method work as expected?

EXPLICITLY:
(a) WHY DID THE TESTS FAIL?
(b) HOW TO DO THIS PROPERLY WITH DateTime::Set?

Replies are listed 'Best First'.
Re: Determine if a given DateTime is a member of a DateTime::Set
by hdb (Monsignor) on May 13, 2013 at 08:48 UTC

    After testing a few alternatives, it seems to be that the issue is with DateTime::Set->from_recurrence. Manually populating a set seems to work.

    </#!/usr/bin/perl -w use strict; use diagnostics; use DateTime; use DateTime::Set; my $date = DateTime->today(); my $date2 = $date->clone(); $date2->add('days' => 14 ); my $set1 = DateTime::Set->from_datetimes( dates => [ $date, $date2 ]); my $iter = $set1->iterator; while( my $d = $iter->next ) { print $d->datetime, "\n"; } my $date3 = $date->clone(); $date3->add('days' => 7); print $set1->contains( $date3 ),"\n"; # gives 0 correct! my $biweekly = DateTime::Set->from_recurrence( 'recurrence' => sub { return $_[0]->truncate('to' => 'day')->add('days' => 1 +4) }, 'after' => $date ); my $set4 = DateTime::Set->from_datetimes( dates => [ $date3 ] ); print $biweekly->contains( $set4 ), "\n"; # gives 1 wrong! print $set4->contains( $biweekly ), "\n"; # gives a warning

    You said you had a solution already, but you could create the recurring dates yourself and then it should (hopefully) work.

Re: Determine if a given DateTime is a member of a DateTime::Set
by jakeease (Friar) on May 13, 2013 at 07:24 UTC

    Maybe I am misunderstanding the problem; your iterator works, adding 14 days each time. But in your test code, you are adding just 7 days and the "in-between" weeks properly fail.

      the "in-between" weeks properly fail.

      No, the second call to Test::More::is:

      is($biweekly->contains($date), 0, $date->ymd . ' is *NOT* the bi-weekl +y set');

      succeeds only if the call to contains returns 0, i.e., if the DateTime::Set object $biweekly does not contain the specified date (which it shouldn’t). But the call to contains is actually (and incorrectly) returning 1, thereby indicating that the object does contain the given date. So, the test fails because the call to contains returns the wrong answer in this case.

      To the OP: I’ve been playing with the code, but haven’t found any way of making it work correctly. So, either the module is buggy, or, at the least, it is poorly documented.

      I did notice that you are calling the contains method with a scalar, whereas the module documentation says it takes a list. But putting the argument into an array and passing that to the function makes no difference to the result of the tests. :-(

      Athanasius <°(((><contra mundum Iustus alius egestas vitae, eros Piratica,

        I agree, the documentation for that module is murky and confusing. I also played around with it for a while without success. DateTime::Event::ICal has a cleaner API for creating the DateTime::Set object. This replacement makes it work, leaving the rest of the original code alone.

        use DateTime::Event::ICal; my $biweekly = DateTime::Event::ICal->recur( dtstart => DateTime->today(), freq => 'weekly', interval => 2, );

        Thanks; I did read that too fleetingly.

        I dare come again out of the wood...

        It seems that it is important that a DateTime::Set has both start and end point.

        Here is my today's attempt with and without an end point and the output below is different (correct if end point is defined):

        #!/usr/bin/perl use strict; use warnings; use DateTime; use DateTime::Set; my $start = DateTime->today(); my $end = DateTime->today()->add('days' => 90); my $biweekly = DateTime::Set->from_recurrence( 'recurrence' => sub { return $_[0]->add('days' => 14)->truncate('to' => 'day') }, 'start' => $start, 'end' => $end, # !!! This is critical. ); my $iter = $biweekly->iterator; print "This is the set with defined 'end': ", $/; while ( my $dt = $iter->next ) { print $dt->datetime, $/; }; print $/; print 'TESTING whether Date is in set...', $/; my $date = DateTime->today(); print $biweekly->contains($date), ' ', $date->ymd, $/; $date->add('days' => 7); print $biweekly->contains($date), ' ', $date->ymd, $/; $date->add('days' => 7); print $biweekly->contains($date), ' ', $date->ymd, $/; $date->add('days' => 7); print $biweekly->contains($date), ' ', $date->ymd, $/; $date->add('days' => 7); print $biweekly->contains($date), ' ', $date->ymd, $/; $date->add('days' => 7); print $biweekly->contains($date), ' ', $date->ymd, $/; print $/; print "This is the begin of the set with 'end' commented out: ", $/; $biweekly = DateTime::Set->from_recurrence( 'recurrence' => sub { return $_[0]->add('days' => 14)->truncate('to' => 'day') }, 'start' => $start, # 'end' => $end, # !!! This is critical. ); $iter = $biweekly->iterator; while ( my $dt = $iter->next ) { last if $dt > $end; # to avoid the infinite loop. print $dt->datetime, $/; }; print $/; print 'TESTING whether Date is in set...', $/; $date = DateTime->today(); print $biweekly->contains($date), ' ', $date->ymd, $/; $date->add('days' => 7); print $biweekly->contains($date), ' ', $date->ymd, $/; $date->add('days' => 7); print $biweekly->contains($date), ' ', $date->ymd, $/; $date->add('days' => 7); print $biweekly->contains($date), ' ', $date->ymd, $/; $date->add('days' => 7); print $biweekly->contains($date), ' ', $date->ymd, $/; $date->add('days' => 7); print $biweekly->contains($date), ' ', $date->ymd, $/;

        The output:

        C:\Perl\bin>perl N:\Perle\Learn\DateTime\pm_1033231_orig_analyse_003_h +.pl This is the set with defined 'end': 2013-05-14T00:00:00 2013-05-28T00:00:00 2013-06-11T00:00:00 2013-06-25T00:00:00 2013-07-09T00:00:00 2013-07-23T00:00:00 2013-08-06T00:00:00 TESTING whether Date is in set... 1 2013-05-14 0 2013-05-21 1 2013-05-28 0 2013-06-04 1 2013-06-11 0 2013-06-18 This is the begin of the set with 'end' commented out: 2013-05-14T00:00:00 2013-05-28T00:00:00 2013-06-11T00:00:00 2013-06-25T00:00:00 2013-07-09T00:00:00 2013-07-23T00:00:00 2013-08-06T00:00:00 TESTING whether Date is in set... 1 2013-05-14 1 2013-05-21 1 2013-05-28 1 2013-06-04 1 2013-06-11 1 2013-06-18
Re: Determine if a given DateTime is a member of a DateTime::Set
by vagabonding electron (Curate) on May 13, 2013 at 13:34 UTC
Re: Determine if a given DateTime is a member of a DateTime::Set
by fglock (Vicar) on May 15, 2013 at 09:27 UTC

    The problem is in the truncate('to' => 'day') step. What you need is to truncate to "14th day", which is not that easy.

    Do you think you can do with DateTime::Event::Recurrence or DateTime::Event::ICal? These modules provide a large set of recurrence modes.

    #!/usr/bin/perl use strict; use warnings; use DateTime::Event::Recurrence; print "DateTime::Event::Recurrence: ", $/; my $start = DateTime->today(); my $biweekly = DateTime::Event::Recurrence->daily( interval => 14, start => $start, ); print 'TESTING whether Date is in set...', $/; my $date = DateTime->today(); print $biweekly->contains($date), ' ', $date->ymd, $/; $date->add('days' => 7); print $biweekly->contains($date), ' ', $date->ymd, $/; $date->add('days' => 7); print $biweekly->contains($date), ' ', $date->ymd, $/; $date->add('days' => 7); print $biweekly->contains($date), ' ', $date->ymd, $/; $date->add('days' => 7); print $biweekly->contains($date), ' ', $date->ymd, $/; $date->add('days' => 7); print $biweekly->contains($date), ' ', $date->ymd, $/;
    output:
    DateTime::Event::Recurrence: TESTING whether Date is in set... 1 2013-05-15 0 2013-05-22 1 2013-05-29 0 2013-06-05 1 2013-06-12 0 2013-06-19

        DateTime::Set is fine to use, but calendar math has these mysterious details, and recurrence specifications are particularly hard.

        The specialised "DateTime::Event" subclasses can make life easier. DateTime::Event::Sunrise and DateTime::Event::Cron are other nice examples.

        About the endpoint that makes it work: when both endpoints of the set are known, it triggers an optimisation that pre-calculates the whole set. The internal representation then changes to a list. This "fixes" the problem, because no further recurrence math is involved.