Reputation: 5279
Let's say I just ran for 10 km and I now have 10 1km split times in the form of MM::SS. I want a simple way to add up an arbitrary number of the split times and average them. For instance, maybe I want to see how much faster (or slower) the last 5 km were when compared with the first 5 km.
I can do this myself by parsing the times, dividing them into seconds and then converting them back to MM::SS. The math isn't hard, but I was wondering if something on CPAN already does this in a simple way. My first attempt was using DateTime, but it doesn't convert from seconds to minutes, because of leap seconds.
To be clear, I don't care about leap seconds in this context, but I'm curious as to whether a library exists. As an example, here is what I have tried.
#!/usr/bin/env perl
use strict;
use warnings;
use feature qw( say );
use DateTime::Format::Duration;
my $formatter = DateTime::Format::Duration->new( pattern => '%M:%S' );
my @splits = @ARGV;
my $split_number = @splits;
my $total = $formatter->parse_duration( shift @splits );
foreach my $split (@splits) {
$total->add_duration(
$formatter->parse_duration($split) );
}
say 'Total time: ' . join ':', $total->minutes, $total->seconds;
$total->multiply( 1 / $split_number );
say 'Average time: ' . join ':', $total->minutes, $total->seconds;
say 'Using DateTime::Format::Duration: ' . $formatter->format_duration( $total );
And a sample run:
$ perl script/add-splits.pl 1:30 2:30
Total time: 3:60
Average time: 1.5:30
Using DateTime::Format::Duration: 01:30
So, you can see that the duration object itself, gives me a correct answer, but not something that a human wants to decipher.
DateTime::Format::Duration tries to be helpful, but tosses out 30 seconds in the process.
I'm not looking for the raw code to do this. I am interested in whether this already exists on CPAN.
Upvotes: 2
Views: 104
Reputation: 66901
The problem with finding this exact functionality on CPAN is that you want to manipulate strings that are time intervals. Most modules are concerned with the context of such strings, working with them as date and time. So it's hard to find something that simply adds mm:ss
format. Since this quest is rather specific, and simple to write, why not wrap it in your own package?
Having said that, see whether the snippet below fits what you are looking for.
This is a simple solution with the core module Time::Piece. It does go to seconds to do the math, but it can be wrapped in a few subs that are then also easily extended for other calculations.
use warnings 'all';
use strict;
use feature 'say';
use Time::Piece;
use POSIX 'strftime';
use List::Util 'sum';
my @times = @ARGV;
my $fmt = '%M:%S';
my $tot_sec = sum map { Time::Piece->strptime($_, $fmt)->epoch } @times;
my $ave_sec = sprintf("%.0f", $tot_sec/@times); # round the average
my ($tot, $ave) = map { strftime $fmt, gmtime $_ } ($tot_sec, $ave_sec);
say "Total time: $tot";
say "Average time: $ave";
For manip_times.pl 2:30 1:30
this prints
Total time: 04:00 Average time: 02:00
We use strptime
from Time::Piece
to get the object, and then its epoch
method returns seconds, which are added and averaged. This is converted back to mm:ss
using strftime
from POSIX. The Time::Piece
also has strftime
but to use it we'd have to have an object.
Note that Time::Piece
does subtract its objects directly, $t1 - $t2
, but it cannot add them. One can add an integer (seconds) to an object though, $t1 + 120
. Also see the core Time::Seconds, a part of the distribution.
Comments on the method used in the question
The DateTime::Duration objects that are used cannot convert between different units
See the How DateTime Math Works section of the
DateTime.pm
documentation for more details. The short course: One cannot in general convert between seconds, minutes, days, and months, so this class will never do so.
From a bit further down the page, we see what conversions can be done and the related ones are only "hours <=> minutes" and "seconds <=> nanoseconds". The reasons have to do with leap seconds, DST, and such. Thus the calculation has to produce results such as 1.5
minutes.
The Class::Date also looks suitable for these particular requirements.
Upvotes: 3
Reputation: 6602
Here is a version using the DateTime
object. Since it doesn't handle date parsing, we have to split the strings ourselves...
add_splits.pl
#!/usr/bin/env perl
use strict;
use warnings;
use feature qw( say );
use DateTime;
my @splits = @ARGV;
my $split_number = @splits;
my $fmt = "%M:%S";
my $total = DateTime->from_epoch( epoch => 0 );
foreach my $split (@splits) {
my ($split_min, $split_sec) = split /:/, $split;
$total->add( minutes => $split_min, seconds => $split_sec );
}
say 'Total time: ' . $total->strftime($fmt);
my $avg_seconds = $total->epoch / $split_number;
my $avg = DateTime->from_epoch( epoch => 0 )->add( seconds => $avg_seconds );
say 'Average time: ' . $avg->strftime($fmt);
Output
$ perl add_splits.pl 1:30 2:30
Total time: 04:00
Average time: 02:00
Upvotes: 1