Bill
Bill

Reputation: 1247

How can I get array of Sundays between two dates?

I'm starting out with two dates.

my $date1 = 01/01/2016;
my $date2 = 05/15/2016;

I need to put all the dates of the Sundays between two dates.

Any idea where I should start?

Upvotes: 2

Views: 343

Answers (4)

ikegami
ikegami

Reputation: 385764

You should start by picking a module. I'm partial to DateTime, using DateTime::Format::Strptime for the parsing.

use DateTime                   qw( );
use DateTime::Format::Strptime qw( );

my $start_date = "01/01/2016";
my $end_date   = "05/15/2016";

my $format = DateTime::Format::Strptime->new(
  pattern   => '%m/%d/%Y',
  time_zone => 'floating',  # Best not to use "local" for dates with no times.
  on_error  => 'croak',
);

my $start_dt = $format->parse_datetime($start_date)->set_formatter($format);
my $end_dt   = $format->parse_datetime($end_date  )->set_formatter($format);

my $sunday_dt = $start_dt->clone->add( days => 7 - $start_dt->day_of_week );
while ($sunday_dt <= $end_dt) {
  print "$sunday_dt\n";
  $sunday_dt->add( days => 7 );
}

Note: You really shouldn't use DateTime->new as Bill used and Schwern endorsed. It's not the recommended use of DateTime because it creates code that's far more complicated and error-prone. As you can see, using a formatter cut the code size in half.


Note: Schwern is advocating the use of an iterator, replacing the last four lines of my answer with something 4 times longer (all the code in his answer). There's no reason for that high level complexity! He goes into length saying how much memory the iterator is saving, but it doesn't save any at all.

Upvotes: 4

Schwern
Schwern

Reputation: 164809

Your solution is good, but it potentially consumes a lot of memory creating an array of Sundays that might never be used. DateTime objects are not small nor cheap.

An alternative is an iterator, a function which every time it's called generates the next Sunday. It generates each Sunday on demand rather than calculating them all beforehand. This saves memory and supports potentially infinite Sundays.

use strict;
use warnings;
use v5.10;

sub sundays_iterator {
    my($start, $end) = @_;

    # Since we're going to modify it, copy it.
    $start = $start->clone;

    # Move to Sunday, if necessary.
    $start->add( days => 7 - $start->day_of_week );

    # Create an iterator using a closure.
    # This will remember the values of $start and $end as
    # they were when the function was returned.
    return sub {
        # Clone the date to return it without modifications.
        # We always start on a Sunday.
        my $date = $start->clone;

        # Move to the next Sunday.
        # Do this after cloning because we always start on Sunday.
        $start->add( days => 7 );

        # Return Sundays until we're past the end date
        return $date <= $endDate ? $date : ();
    };
}

That returns a closure, an anonymous subroutine which remembers the lexical variables it was created with. Sort of like an inside out object, a function with data attached. You can then call it like any subroutine reference.

my $sundays = sundays_iterator($startDate, $endDate);
while( my $sunday = $sundays->() ) {
    say $sunday;
}

The upside is it saves a lot of memory, this can be especially important if you're taking the dates as user input: a malicious attacker can ask you for an enormous range consuming a lot of your server's memory.

It also allows you to separate generating the list from using the list. Now you have a generic way of generating Sundays within a date range (or, with a slight tweak, any day of the week).

The downside is it's likely to be a bit slower than building an array in a loop... but probably not noticeably so. Function calls are relatively slow in Perl, so making one function call for each Sunday will be slower than looping, but calling those DateTime methods (which call other methods which call other methods) will swamp that cost. Compared to using DateTime, calling the iterator function is a drop in the bucket.

Upvotes: 7

ysth
ysth

Reputation: 98398

DateTime::Set makes constructing an iterator easy:

use DateTime::Format::Strptime ();
use DateTime::Set ();

my $start_date = "01/01/2016";
my $end_date   = "05/15/2016";

my $format = DateTime::Format::Strptime->new(
  pattern   => '%m/%d/%Y',
  time_zone => 'local',
  on_error  => 'croak',
);

my $iterator = DateTime::Set->from_recurrence(
  start => $format->parse_datetime($start_date)->set_formatter($format),
  end => $format->parse_datetime($end_date)->set_formatter($format),
  recurrence => sub { $_[0]->add( days => 7 - $_[0]->day_of_week || 7 ) }, # next Sunday after $_[0]
);

while ( my $date = $iterator->next ) {
  say $date;
}

Upvotes: 3

Bill
Bill

Reputation: 1247

This is what I came up with but please let me know if there is a better way.

use DateTime;

my $date1 = "1/1/2016";
my $date2 = "5/15/2016";

my ($startMonth, $startDay, $startYear) = split(/\//, $date1);
my ($endMonth, $endDay, $endYear) = split(/\//, $date2);

my $startDate = DateTime->new(
     year  => $startYear,
     month => $startMonth,
     day   => $startDay
);

my $endDate = DateTime->new(
     year  => $endYear,
     month => $endMonth,
     day   => $endDay
);

my @sundays;

do {
  my $date = DateTime->new(
     year  => $startDate->year,
     month => $startDate->month,
     day   => $startDate->day
  );

  push @sundays, $date if ($date->day_of_week == 7);
  $startDate->add(days => 1);

} while ($startDate <= $endDate);

foreach my $sunday (@sundays) {
  print $sunday->strftime("%m/%d/%Y");
}

Upvotes: 0

Related Questions