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.
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.
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
Related
Every time I tried to find the difference of these date strings, there is an error. I wonder if you could help me this.
my $datecreated = '2021-09-06 04:52:38';
my $dateresolved = '2021-09-06 04:52:48';
my $time_elapsed= $dateresolved - $datecreated;
print $time_elapsed;
And I want to convert the result into minutes and hours.
These two timestamps are mere strings. In order to get the duration between these two moments in time ("subtract" them) one needs to build date-time objects from them, in a library that knows how to then find duration between them. One good choice is DateTime
use warnings;
use strict;
use feature 'say';
use DateTime;
use DateTime::Format::Strptime;
my ($ts1, $ts2) = (#ARGV == 2)
? #ARGV : ('2021-09-05 04:52:38', '2021-09-01 04:52:48');
my $strp = DateTime::Format::Strptime->new(
pattern => '%F %T', time_zone => 'floating', on_error => 'croak'
);
my ($dt1, $dt2) = map { $strp->parse_datetime($_) } $ts1, $ts2;
# Get difference in hours and minutes (seconds discarded per question)
my ($hrs, $min) = delta_hm($dt1, $dt2);
say "$hrs hours and $min minutes";
# Or (time-stamp hh:mm in scalar context)
my $ts_hm = delta_hm($dt1, $dt2);
say $ts_hm;
# To get wanted units (hours+minutes here) best use a delta_X
sub delta_hm {
my ($dt1, $dt2) = #_;
my ($min, $sec) = $dt1->delta_ms($dt2)->in_units('minutes', 'seconds');
my $hrs = int( $min / 60 );
$min = $min % ($hrs*60) if $hrs;
return (wantarray) # discard seconds
? ($hrs, $min)
: join ':', map { sprintf "%02d", $_ } $hrs, $min;
}
The hard-coded input time-stamps here are different than the ones in the question; those would make an hour+minute difference a zero, since they differ only in seconds! (Is that intended?) One can also submit two time-stamp strings as input to this program.
Note that a generic duration object makes it harder to convert to any particular desired units
One cannot in general convert between seconds, minutes, days, and months, so this class will never do so. Instead, create the duration with the desired units to begin with, for example by calling the appropriate subtraction/delta method on a DateTime.pm object.
So above I use delta_ms since minutes are easily converted to hours+minutes. Seconds are discarded as the question implies (if that is in fact unintended add them in the routine).
For more general uses one can do
use DateTime::Duration;
my $dur = $dt1->subtract_datetime($dt2);
# Easy to extract parts (components) of the duration
say "Hours: ", $dur->hours, " and minutes: ", $dur->minutes; # NOT conversion
Can do this with the core Time::Piece as well
use warnings;
use strict;
use feature 'say';
use Time::Piece;
my ($ts1, $ts2) = (#ARGV)
? #ARGV : ('2021-09-05 04:52:38', '2021-09-01 04:52:48');
my ($dt1, $dt2) = map { Time::Piece->strptime($_, "%Y-%m-%d %T") } $ts1, $ts2;
# In older module versions the format specifier `%F` (`%Y-%m-%d`) may fail
# so I spell it out here; the %T (for %H:%M:%S) should always be good
# For local times (not UTC) better use Time::Piece::localtime->strptime
my $delta = $dt1 - $dt2;
# say $delta->pretty;
my $hrs = int( $delta->hours );
my $min = int($delta->minutes) - ($hrs//=0)*60;
say "$hrs:$min";
This is much simpler, but watch out for occasional tricky (error-inducing) API of Time::Piece.
Note, while Time::Piece is core, succinct, and much lighter (and correct!), the DateTime is far more rounded and powerful, also with an ecosystem of extensions.
Use Time::Piece which is a standard part of the Perl library since 2007.
#!/usr/bin/perl
use strict;
use warnings;
use Time::Piece;
# Define the format of your inputs
my $format = '%Y-%m-%d %H:%M:%S';
# Convert your date strings into Time::Piece objects
my $datecreated = Time::Piece->strptime('2021-09-06 04:52:38', $format);
my $dateresolved = Time::Piece->strptime('2021-09-06 04:52:48', $format);
# Time::Piece objects can be subtracted from each other.
# This gives the elapsed time in seconds.
my $time_elapsed = $dateresolved - $datecreated;
# Do the calculations to displace the elapsed time in hours,
# minutes and seconds.
printf "%02dh:%02dm:%02ds\n",
$time_elapsed->hours,
$time_elapsed->minutes % 60,
$time_elapsed->seconds % 60;
I am trying to convert a date from epoch to year month day and get the correct date.
my $day = 18322;
my ($y, $m, $d) = (gmtime 86400*$day)[5,4,3];
The epoch date is 1583020800 The conversion is as follows $y is 120 $m is 2 $d is 1
I guess I have to add $y = $y+1900 I get the correct year, I can add 1 to $m to get the correct month the day $d I don't have to add anything to. Is this correct. I am taking over code for someone but I have no idea what [5,4,3] does.
Epoch time 1583020800 is Sun Mar 1 00:00:00 2020.
You can use gmtime, but it's awkward. It returns an array of values and they need to be converted. The year is the number of years since 1900 and the month starts at 0. This is because it is a thin wrapper around struct tm from the C programming language Perl is written in.
my($y,$m,$d) = (gmtime(1583020800))[5,4,3];
$y += 1900;
$m += 1;
printf "%04d-%02d-%02d\n", $y, $m, $d;
Instead, use the built in Time::Piece.
use v5.10;
use Time::Piece;
my $time = Time::Piece->gmtime(1583020800);
say $time->ymd;
Or the more powerful DateTime.
use v5.10;
use DateTime;
my $dt = DateTime->from_epoch(epoch => 1583020800);
say $dt->ymd;
The (...)[5,4,3] is a literal list slice. The thing inside the parens creates a list, but this selects only elements 5, 4, and 3.
The gmtime docs point to localtime, which shows you the position of each thing in its list:
localtime
Converts a time as returned by the time function to a 9-element
list with the time analyzed for the local time zone. Typically
used as follows:
# 0 1 2 3 4 5 6 7 8
my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) =
localtime(time);
I would use Time::Piece as in Schwern's answer. But just to cover all bases, you can use the strftime() function from POSIX.pm as well.
use feature 'say';
use POSIX qw[strftime];
say strftime('%Y-%m-%d', gmtime(1583020800));
Output:
2020-03-01
You can pass different format strings to strftime().
I want to subtract two timestamps in Perl. I converted them to unix-time via the function below and convert the unix timestamp back to how it was. In the example below the result is 01:20:00 instead of 00:20:00
(I think it has sth to do with the start of the unix timestamp 1.1.1970 01:00:00 but not sure how to resolve it)
Any idea? Many thanks for your help in advance.
use POSIX qw( strftime );
use Time::Local qw( timelocal );
sub to_epoch {
$_ = shift;
my #a = split /\W+/, $_;
my $b = timelocal($a[5],$a[4],$a[3],$a[2],$a[1],$a[0]);
return $b;
}
my $h_end = "2018.11.12 00:50:00";
my $h_start = "2018.11.12 00:30:00";
my $duration = to_epoch($h_end) - to_epoch($h_start);
my $convert_back = POSIX::strftime("%H:%M:%S", localtime($duration));
print $convert_back , "\n";
Ouptut: 01:20:00
It works for me. But I think that's because I'm in GMT and you're in CET (GMT+1).
The flaw is in your final step. You are confusing two concepts - a point in time and a duration.
You correctly convert your two points in time to Unix epoch numbers and then you subtract those numbers to get the number of seconds between them. That number is a duration. And you want to convert that duration into a human-readable format. Using localtime() and POSIX::strtime() is not the way to do that. POSIX::strftime() and localtime() deal with points in time, not durations.
The number you get is 1,200. By passing that to localtime() you are saying "what is the epoch number 1,200 when converted to a date and time in my local timezone?" 1,200 is 20 past midnight on Jan 1st 1970 GMT. But in your local, Frankfurt, timezone, it's 20 past 1am. Which is why you're getting 1:20 and I'm getting 0:20.
There are a couple of ways to fix this. You can do the conversion manually.
my $duration = 1_200;
my $mins = int($duration/60);
my $secs = $duration % 60;
Or you can use a proper date/time handling module like DateTime (along with its associated module DateTime::Duration).
It might work if you use timegm() and gmtime() in place of timelocal() and localtime() - but I really don't recommend this approach as it perpetuates the confusion between points in time and durations.
Update: A version using DateTime.
#/usr/bin/perl
use strict;
use warnings;
use DateTime;
use DateTime::Format::Strptime;
my $h_end = '2018.11.12 00:50:00';
my $h_start = '2018.11.12 00:30:00';
my $date_p = DateTime::Format::Strptime->new(
pattern => '%Y.%m.%d %H:%M:%S'
);
my $duration = $date_p->parse_datetime($h_end)
- $date_p->parse_datetime($h_start);
printf '%02d:%02d:%02d', $duration->in_units('hours', 'minutes', 'seconds');
1200, the value of $duration, signifies the following when treated as a epoch timestamp
1970-01-01T01:20:00+01:00
^^^^^^^^
The solution is to replace
strftime("%H:%M:%S", localtime($duration));
with
strftime("%H:%M:%S", gmtime($duration));
This gives
1970-01-01T00:20:00Z
^^^^^^^^
Of course, this is still a hack. You're not suppose to be passing a duration to gmtime. Use an appropriate module instead.
use DateTime::Format::Strptime qw( );
my $format = DateTime::Format::Strptime->new(
pattern => '%Y.%m.%d %H:%M:%S',
on_error => 'croak',
);
my $h_end = $format->parse_datetime('2018.11.12 00:50:00');
my $h_start = $format->parse_datetime('2018.11.12 00:30:00');
my $dur = $h_end - $h_start;
printf "%02d:%02d:%02d\n", $dur->in_units(qw( hours minutes seconds ));
By the way,
timelocal($a[5],$a[4],$a[3],$a[2],$a[1],$a[0])
should be
timelocal($a[5],$a[4],$a[3],$a[2],$a[1]-1,$a[0])
#!/usr/bin/perl
sub parkingcharge {
sub exittime
{
($sec, $min, $hour) = localtime();
print "exit time:$hour:$min:$sec\n";
}
my $exit = exittime();
my $entry = "9:10:8";
print "\nvehicle entry time is :$entry\n";
print "\nvehicle exit time is :$exit\n";
my $parkingCharge = ($entry - $exit);
print "\ntotal parking charge is : $parkingCharge\n";
}
parkingcharge();
The output appears like this
exit time:5:46:57
vehicle entry time is :9:10:8
vehicle exit time is :1
total parking charge is : 8
I want to find the parking charge in a Perl vehicle management program. The rate is $2 per hour, so I want to find the difference between entry time and exit time in hours and multiply by 2. The code I have written produces the wrong result.
How to take the difference between times in hours?
You can use Time::Piece, which is included with Perl. It gives you a convenient way to parse dates into Time::Piece objects, which are essentially epoch timestamps with syntactic sugar. The nice thing about them is that you can use them in math and you'll get seconds.
Because you only have times, we need to have the same date for both the entry and the exit time. One way to do that would be to check today's date and use it in both variables. But it's easier to just leave it out. Time::Piece will assume it's 1970-01-01, which is fine, because we don't care. It's only important that both timestamps have the same date as long as you cannot park over night.
use strict;
use warnings;
use Time::Piece;
my $entry = Time::Piece->strptime( '9:10:8', '%H:%M:%S' );
We use the strptime method to parse the entry time. The second argument is a pattern of placeholders. %H is hours in 24 hour notation, %M is minutes and %S is seconds. This also works without the leading zeroes.
We now have an entry date of 1970-01-01 09:10:08, or Thu Jan 1 09:10:08 1970 if you just print $entry.
Next we need to get the exit time.
my ( $sec, $min, $hour ) = localtime;
my $exit = Time::Piece->strptime( "$hour:$min:$sec", '%H:%M:%S' );
Because just using localtime in scalar context would give us today's date, we have to do an extra step. Your code already got the seconds, minutes and hours of this moment. We just use that as a string in the right format and feed it into strptime the same way we did for $entry. Now we have the exit timestamp, which is Thu Jan 1 14:46:56 1970 while I write this.
Getting the duration is a simple matter of subtraction. Converting it to hours is just a division by 60 for minutes and by 60 for hours.
my $duration = $exit - $entry;
my $duration_in_hours = $duration / 60 / 60;
The $duration_in_hours is 5.61333333333333 for me right now. If you want people to pay for every started hour, you'd have to round up.
my $fee_started_hours = int( $duration_in_hours + 1 ) * $hourly_fee;
I prefer to only pay for full hours of parking, so I'd like rounding down more.
my $fee_full_hours = int( $duration_in_hours ) * $hourly_fee;
I have following date in string format and I need to convert it to milliseconds using a Perl script. I have tried to convert it using DateTime::Format::Strptime and it returns 0 after convert to millisecond.
Date String : 01-13-15:14.16
#!/usr/bin/perl
use DateTime::Format::Strptime;
my $strp = DateTime::Format::Strptime->new(
pattern => '%m-%d-%y:%H.%M',
on_error => 'croak',
);
my $dt = $strp->parse_datetime("01-13-15:14.16");
print $dt->millisecond."\n";
How can I convert datetime to milliseconds
I think it's likely that what you want is not milliseconds but epoch, which is the number of seconds since January 1 1970. Your date-time format has neither seconds nor milliseconds, so both of these fields will be reported as zero, and if you really want milliseconds then you can simply multiply the epoch by 1000.
It looks like this
use strict;
use warnings;
use 5.010;
use DateTime::Format::Strptime;
my $strp = DateTime::Format::Strptime->new(
pattern => '%m-%d-%y:%H.%M',
on_error => 'croak',
);
my $dt = $strp->parse_datetime('01-13-15:14.16');
say $dt->epoch;
output
1421158560
However, the DateTime module is enormous and slow, and there is a better solution in the core module Time::Piece, which would look like this
use strict;
use warnings;
use 5.010;
use Time::Piece;
my $dt = Time::Piece->strptime('01-13-15:14.16', '%m-%d-%y:%H.%M');
say $dt->epoch;
output
1421158560
It is impossible to convert a date to milliseconds. A date is a point in time. Milliseconds measure a duration.
The only way your question makes sense is if we assume you mean "how many milliseconds are there between my given date/time and some other fixed date/time". Because of the way that Unix measures time, let's assume that the fixed date/time you're measuring from is the Unix Epoch (1970-01-01 00:00:00).
You get the number of seconds since the epoch from a DateTime object with the epoch() method.
say $dt->epoch;
To get the number of milliseconds, just multiply that by a thousand.
say 1000 * $dt->epoch;
Your current timestamp doesn't include milliseconds. If you change to parsing strings that include them, then you can add that value on too.
say $dt->milliseconds + (1000 * $dt->epoch);
Adding my comment as an answer:
If you want to convert the datetime value into seconds since the epoch, try one of these:
print sprintf("%f", $dt->epoch + ($dt->millisecond/1000));
print sprintf("%s.%s", $dt->epoch, $dt->millisecond);
(My original comment did not account for millisecond being an integer)
Similar for the values in milliseconds, rather than seconds.millis:
print sprintf("%d", ($dt->epoch * 1000) + $dt->millisecond);
print sprintf("%d%04d", $dt->epoch, $dt->millisecond);
Remember, that if you don't supply DateTime with a value containing milliseconds (or higher resolution), the value will simply be zero.