oalders oalders - 2 months ago 14
Perl Question

Adding MM::SS times, dividing them and printing the average

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.

Answer

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 is so specific and so very 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 related core module Time::Seconds.


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.


Note that Class::Date also looks suitable for these particular requirements.