Dirk80 has asked for the wisdom of the Perl Monks concerning the following question:

Hello,

I wrote some code to convert a time string to seconds.

use strict; use warnings; my $seconds; # 1) min:sec,frac e.g. "5:29,11" # 2) sec,frac e.g. "29,11" # 3) sec e.g. "29" # 4) ,frac e.g. ",11" # 5) min:sec e.g. "5:29" # 6) someth. else e.g. "abc" my $time = " 5:21,11 "; # remove leading and trailing whitespace characters $time =~ s/^\s+|\s+$//g; if( $time =~ m/[^0-9:,]+/ ) { $seconds = -1; } elsif( $time =~ m/((?<min>\d+):)? ((?<sec>\d+))? (,(?<frac>\d+))?/x ) { $seconds = ($+{'min'} * 60) + $+{'sec'} + ($+{'frac'}/(10**length( +$+{'frac'}))); } print("$seconds\n");

I have one problem. I know that if an optional part of the regular expression is not available then it is set to undef. But I also know that undef means in a numeric case the value zero. This behaviour is exactly what I want. Because of use warnings; I get warnings. Of course I want to use the code in a bigger perl script and so it is not a solution to not use use warnings. What do you think how the code could be rewritten to avoid these warnings?

Then of course it is always interesting to see for me how you monks would solve the same problem.

Thank you very much for your help.

Dirk

Replies are listed 'Best First'.
Re: Convert time string to seconds
by moritz (Cardinal) on Aug 16, 2010 at 09:40 UTC
    What do you think how the code could be rewritten to avoid these warnings?

    Use no warnings 'uninitialized';

    in the inner scope where you do the arithmetics.

    That tells the reader of your code knows exactly that the undef being interpreted as 0 is intentional. The effect of the "no warnings" is limited to the scope it is in, so there's no harm for the program as a whole.

    Perl 6 - links to (nearly) everything that is Perl 6.

      For less typing, tighter "scope" and to avoid suppressing other warnings at the same time, you can simply say $1||0, or in this case ($+{'min'}||0).

        I knew somebody would suggest this method :-)

        It's just that when I add code to surpress a warning, "switch off the warning" feels more direct to me than "avoid the warnings".

        Since it's not an error, but a warning, using undef as 0 is legitime, and I find avoiding the feature at all rather backwards. After all warnings should help me write better code, not force me to write different application logic.

        Perl 6 - links to (nearly) everything that is Perl 6.
Re: Convert time string to seconds
by ikegami (Patriarch) on Aug 16, 2010 at 13:40 UTC
Re: Convert time string to seconds
by ww (Archbishop) on Aug 16, 2010 at 15:03 UTC
    Moritz has the answer to your question.

    I have some concerns about your approach.

    The formats you list,

    min:sec,frac e.g. "5:29,11" sec,frac e.g. "29,11" sec e.g. "29" ,frac e.g. ",11" min:sec e.g. "5:29" someth. else e.g. "abc"

    are not normalized. In consequence, you provide no coverage of possibilities such as 5:29,3, which appears to be at least theoretically possible in your schema. Were you to begin by normalizing the data to mm:ss,frac, writing a reliable conversion to seconds would be both more reliable and easier (and which is left as an exercise for the user).

    That said, and accepting the un-normalized data, suhailck's solution has much to commend it, but also has a couple glitches:

    1. Its initial test of the string won't correctly handle mixed alphanumerics, such as 1abc
    2. Its use of $3/10**2 in calculation of the decimal portion fails to consider the possiobility (again, not in your schema, but, ISTM, distinctly possible) of a single-digit, decimal fraction, such as .3 or a decimal fraction where length > 2, as, for example, .303

    So, in part TIMTOWTDI, and in part, perhaps by way of improvement:

    #!/usr/bin/perl use strict; use warnings; # 855222 # 1) min:sec,frac e.g. "5:29,11" # 2) sec,frac e.g. "29,11" # 3) sec e.g. "29" # 4) ,frac e.g. ",11" # 5) min:sec e.g. "5:29" # 6) someth. else e.g. "abc" my $seconds; my @times=("5:29,11", "29,11", "29:00", "29:00,3", "29:00,303","29:13, +07", ",11", "0:11", "5:29", "abc", "1abc"); for my $time(@times) { no warnings 'uninitialized'; if ($time =~ /[a-z]/i) { # won't catch non-"time-ish" symbols s +uch as ?, #, @, etc # $seconds = -1; # my personal preference is for an explicit er +ror statement print "Data not recognized: $time\n"; next; } else { ($seconds = $time) =~ s<^(?:(\d+):)?(\d+)?(?:,(\d+))?$><$1*60+$2+$ +3/(10**length($3))>e; } print "$time = $seconds \n"; }

    which produces:

    5:29,11 = 329.11 29,11 = 29.11 29:00 = 1740 29:00,3 = 1740.3 29:00,303 = 1740.303 29:13,07 = 1753.07 ,11 = 0.11 0:11 = 11 5:29 = 329 Data not recognized: abc Data not recognized: 1abc

      Thank you all for your answers.

      Now I wrote the following code. It is a bit longer, but more readable I think. And so I'm sure that it really is doing what I want. And I also wrote a function which is doing the way back.

      use strict; use warnings; my @time_str_list = ( "abc", "1abc", ":", ":,", "5:29,11", "29,11", "2 +9:00", "29:00,3", "29:00,303", "9:13,07", ",11", "0:11", "5:29" ); my $seconds; for my $time_str ( @time_str_list ) { &convertTimeStrToSeconds($time_str, \$seconds); next unless defined $seconds; print("$time_str\n"); print("$seconds\n"); &convertSecondsToTimeStr($seconds, \$time_str); next unless defined $time_str; print("$time_str\n"); print("\n"); } sub convertTimeStrToSeconds { my ($time_str, $ref_seconds) = @_; # 1) min:sec,frac e.g. "5:29,11" # 2) sec,frac e.g. "29,11" # 3) sec e.g. "29" # 4) ,frac e.g. ",11" # 5) min:sec e.g. "5:29" # 6) someth. else e.g. "abc" $$ref_seconds = undef; # 6) # remove leading and trailing whitespace characters $time_str =~ s/^\s+|\s+$//g; # valid time string if( ($time_str =~ m/^(?<min>\d+):(?<sec>\d+),(?<frac>\d+)$/) ||# 1 +) ($time_str =~ m/^(?<sec>\d+),(?<frac>\d+)$/) || # 2) ($time_str =~ m/^(?<sec>\d+)$/) || # 3) ($time_str =~ m/^,(?<frac>\d+)$/) || # 4) ($time_str =~ m/^(?<min>\d+):(?<sec>\d+)$/) ) # 5) { no warnings 'uninitialized'; # intention: undef treated as zer +o $$ref_seconds = ($+{'min'} * 60.0) + $+{'sec'} + ($+{'frac'}/( +10**length($+{'frac'}))); } } sub convertSecondsToTimeStr { my ($seconds, $ref_time_str) = @_; $$ref_time_str = undef; if( $seconds > 0 ) { my $min = int($seconds / 60.0); my $sec = $seconds % 60.0; my $frac = int(sprintf("%.2f", ($seconds - int($seconds))*100. +0)); $$ref_time_str = "$min:" if( $min > 0 ); $$ref_time_str .= sprintf("%02d", $sec) if ( ($min > 0) || ($s +ec > 0) ); $$ref_time_str .= ",$frac" if ( $frac > 0 ); } }

      With the error handling you are right. To set $seconds to -1 is not that good. Error handling is in general for me a difficult thing. I'm not sure what is the best way. Now I'm setting the output parameter to undef. Is this good style? Of course I could print a error message. Or I could return undef. Let's assume this function would be part of an official module. What would be the best error handling?

      Thank you and Greetings

      Dirk

        Narrowly re your last two sentences: before publishing, you should probably check that you're not merely duplicating the capabilities already available in some Time::... or Date::... module.

        Then, assuming you do publish, please make the error message clear and specific -- tell precisely what the problem is. The expert won't mind and the occasional coder will be thankful.

Re: Convert time string to seconds
by suhailck (Friar) on Aug 16, 2010 at 09:40 UTC
    perl -le '@times=qw(5:29,11 29,11 29 ,11 5:29 abc); foreach my $time (@times){ if($time!~/[0-9:,]+/) {$seconds=-1} else { ($seconds=$time)=~s{^(?:(\d+):)?(\d+)?(?:,(\d+))?$}{$1*60+$2+$3 +/10**2}e;} print $seconds}' 329.11 29.11 29 0.11 329 -1


    ~suhail