A Date with CPAN, Update #2: A Little Piece of Date::Piece

[This is an addendum post to a series.  You may want to begin at the beginning.  The last update was update #1.

IMPORTANT NOTE!  When I provide you links to code on GitHub, I’m giving you links to particular commits.  This allows me to show you the code as it was at the time the blog post was written and insures that the code references will make sense in the context of this post.  Just be aware that the latest version of the code may be very different.]


This year is only the second time since 2011 that I’ve been unable to attend YAPC::NA.  Since I couldn’t make it out to hang with my Perl peeps in person, I thought the least I could do is offer up a long-awaited update to Date::Easy.

This latest version (available now-ish on CPAN as 0.03_01, and to be upgraded to 0.04 within the next few days assuming CPAN Testers approves) contains a few small updates, and one big one.  First, the miscellaneous bits:

  • I fixed a bug with one of my last update’s new features, add_months.  Turns out it was returning a Time::Piece object (i.e. the underlying implementation) instead of a Date::Easy::Date (or Date::Easy::Datetime), and Date::Easy objects compare to other objects so transparently that my unit tests didn’t catch it.  So, fixed that, and added more isa_ok checks to avoid making that mistake again.
  • There’s now a day_of_year method.  As is the custom with Date::Easy, the name of the method, as well as its return values (i.e. the fact that Jan 1st is 1, not 0) is drawn from DateTime rather than Time::Piece.1
  • I updated the docs a bit, including discussing language under “Limitations” and giving credit to all the modules that together inpsired this one.
  • You can now split a Date::Easy object into its component integers: 3 parts for a date, 6 parts for a datetime.  The number and order of the parts lines up perfectly with what Date::Calc expects, for easier integration with that most useful module.
Let’s expand on that last point briefly.
use Date::Easy;
my $from = date("1/1/2016");
my $to   = $from->add_months(6);
 
use Date::Calc qw< Delta_Days >;
my $diff = Delta_Days( $from->split, $to->split );
 
use Date::Gregorian::Business;
use Date::Gregorian qw< :weekdays >;
# ignore holidays for now
my $date = Date::Gregorian::Business->new([ [ SATURDAY, SUNDAY ], [] ]);
$date->set_ymd( $from->split );
my $diff = $date->get_businessdays_until(Date::Gregorian->new->set_ymd( $to->split ));

Now, that first example (the call to Delta_Days) will be integrated directly into Date::Easy at some point in the future,2 but I’m not sure if the second one (i.e. get_businessdays_until) ever will.3  And, even if it is someday, Date::Easy will never be the solution to all date problems, so having simple interoperability with the plethora of Perl’s other date modules will always be a worthy goal.

Of course split works with datetimes too:
use Date::Calc qw< Add_Delta_DHMS >;
my $some_time_later = Date::Easy::Datetime->new(
        Add_Delta_DHMS(now->split, $days, $hrs, $mins, $secs)
);
But the really big news is that I finally got around to stealing the bits of Date::Piece that I threatened to crib way back in part 2 of my (long-ass, wandering) design series.  Specifically, the concept of “units” objects.  The idea is that, instead of having to do this:
    my $next_quarter = today->add_months(3);
you should be able to do this:
    my $next_quarter = today + 3*months;
Which is pretty nifty looking and intuitive, at least to me.  This is the best part of Date::Piece in my opinion, even better than the date and today functions (which I also totally stole).  It’s super-clever4 and the implementation is pretty simple.  In Eric’s original code, each unit type (“seconds,” “hours,” “weeks,” etc) was its own class, but I decided that I’d keep things simple and just have a single class which kept track of its unit type as well as its quantity.  As for the meat of the class, it’s surprisingly simple.  The constructor takes the units and the quantity (defaulting to 1), checks to make sure quantity is an integer, and returns the blessed hash.  Multiplying just takes the existing quantity, multiplies it by the operand, and returns a new units object.  Addition calls add_$unit on the other operand, and subtraction calls subtract_$unit.  Toss in some super-simple stringification, and you’re basically done.  Oh, sure: there’s a bit of trickiness with the order of operands—a datetime plus a units object is going to call the overloaded + operator for the datetime, not the units—but that’s just bookkeeping. Of course, the real guts of the matter is to create all those methods for dates and datetimes: add_seconds and add_minutes and so forth.  But that’s pretty easy too:
sub add_seconds         { shift->_add_seconds      (@_) }
sub add_minutes         { shift->_add_seconds      ($_[0] * 60)            }
sub add_hours           { shift->_add_seconds      ($_[0] * 60 * 60)       }
and so forth up through weeks, with correlating subtractions.  Adding months requires its own method, of course, but I already made that last time.  Adding years wasn’t there, but it’s trivial to add, because all we’re doing is passing it through to Time::Piece (which is exactly what we did for add_months:
sub add_years           { ref($_[0])->new( shift->{impl}->add_years (@_) ) }
And of course subtracting those final two is also trivial:
sub subtract_months     { shift->add_months($_[0] * -1)                    }
sub subtract_years      { shift->add_years ($_[0] * -1)                    }

The only real issue I ran across was that, since I was overloading addition (and subtraction) on dates differently from how I’m doing it on datetimes, I would have to repeat a bunch of code to deal with the units objects (the “bookkeeping” I mentioned above).  I didn’t like this, so I decided to take a different approach: addition will always call a method named _add_integer.  In the (base) datetime class, this just calls add_seconds.  In the (derived) date class, it instead calls add_days.  Easy peasy.  Now, practically every decision we make in computer programming involves a trade-off, and this one is no different.  There’s a downside, and there’s an upside.

The principal downside is that I’ve added more layers—specifically more method calls—to basic addition and subtraction.  If you’re a micro-optimizer sort of person, you will immediately turn your nose up at this.  Method calls in Perl are “slow,” meaning that they suck when you’re doing direct benchmark comparison of millions of empty method calls vs doing the same thing in another language, like C.  But that’s the kind of “on paper” slow that I personally don’t get too chuffed about.  If you’re really going to be adding millions of integers to dates, just call the relevant add_ method directly.  Or, better yet: don’t use Date::Easy at all!  Christian Hansen’s excellent Time::Moment exists for just that sort of need, and it’s exactly what I myself would use in that situation.  The units interface is designed for convenience, not speed ... although I think it will hardly ever be noticeably slower to the point where you need to worry about it.

Now, the upside—apart from saving the repeated code, as I mentioned before—is that it makes inheriting from Date::Easy::Datetime much simpler.  Say you wanted to create a Week class, or maybe a Decade class.  In order to get all your overloaded math ops for free, all you really need to do is override _add_integer to call the appropriate addition method (and of course _subtract_integer as well), and you’re set.5  All the math stuff Just Works(tm).

So now overloaded addition (in both classes!) is just mapped to this:
sub _dispatch_add
{
    if ( blessed $_[1] && $_[1]->isa('Date::Easy::Units') )
    {
        $_[1]->_add_to($_[0]);
    }
    else
    {
        # this should DTRT for whichever class we are
        $_[0]->_add_integer($_[1]);
    }
}
And likewise for _dispatch_subtract, except there’s a little extra housekeeping for dealing with the fact that subtraction isn’t transitive.  Note how I don’t “unpack” @_ in these; that helps keep the extra method call overhead down.6

Finally, I wanted to just mention in passing a thing I wanted to fix, but wasn’t sure how to.  The problem is quite simply demonstrated:
[cibola:~/proj/date-easy] perl -MDate::Easy -E 'say datetime("2/1/17")'
Wed Feb  1 00:00:00 2017
[cibola:~/proj/date-easy] perl -MDate::Easy -E 'say date("2/1/17")'
Thu Feb  1 00:00:00 1917
Obviously this is not ... ideal.  After some basic digging, I was able to figure out what was going on.  When I copied a chunk of code to replicate the guts of Date::Parse,7 I took advantage of the fact I had to copy-paste anyway to make a slight change.  This change fixed an outstanding bug with Date::Parse:
[absalom:~/proj/date-easy] perl -MDate::Parse -MDate::Format -E 'say time2str("%Y-%m-%d", str2time("1/1/1967"))'
2067-01-01
[absalom:~/proj/date-easy] perl -Ilib -MDate::Easy -E 'say date("1/1/1967")'
Sun Jan  1 00:00:00 1967

Unfortunately, it broke the ability of my slightly customized version of str2time to deal with 2-digit years, as you see above.  There isn’t a particularly easy solution to this without just reverting back to having the original bug, and I sort of feel like the original bug is worse: at least, this way, as long as you stick to 4-digit years (which, you know, you really should), everything works out all right.

Well, it works out all right for dates.  For datetimes, I’m just calling the original str2time, so I’m still stuck with the original bug:
[absalom:~/proj/date-easy] perl -Ilib -MDate::Easy -E 'say datetime("1/1/1967")'
Sat Jan  1 00:00:00 2067
Which is pretty awful.  But I can’t figure out quite what to do with this, either from my side or even how I might fix it in a pull request for Graham Barr, even assuming he were willing to entertain such a thing.  Brilliant suggestions from my readers are certainly welcome; for now, I’m just documenting this behavior and setting the issue aside for further pondering.

The full code for Date::Easy so far is here, and there’s also a new developer’s version.  Of special note:

Hopefully, with this update, Date::Easy will start being actually useful for a certain class of problems.  If you have a quick script that needs to do some basic date working, give it a shot!  Your feedback (here, in GitHub issues, or in CPAN RT tickets) is always greatly appreciated.



__________

1 For a Time::Piece object, you’d call yday and get 0 for Jan 1st.  Although I love the tight implementation of Time::Piece, when it comes to interface issues, I usually find DateTime to be more sane.


2 In fact, it’s one of the few remaining bits before I consider the Date::Easy interface mostly done.


3 But maybe.  I find the Date::Gregorian::Business interface pretty clunky, so I’d love to come up with a better one.  Although the whole holidays part is very tricky (which is why I cleverly ducked it altogether in my example).


4 So big ups to Eric Wilhelm, who originated it, as far as I know.


5 Well, also don’t forget to override all the units that are “lower” than your base to throw exceptions: you don’t want people calling add_hours on your Decade object.


6 It’s not much help, granted, but why not do every little bit you can?


7 Which I talked briefly about towards the end of part 4 of this series.  You can even follow the links I provide to look at a comparison of my version with the original.


Leave a comment

About Buddy Burden

user-pic 9 years in California, 20 years in Perl, 29 years in computers, 50 years in bare feet.