Simple timeValidator() function, exists?

Hi, I’m writing a nice front end to a calendar, and want the user to be able to just type in start and end times (not dates) for an event.

I want this to be as forgiving as I can, so they can just type 19 for 7pm if they want - whilst not leaving myself a hole for attacks of course.

I want this to be a PHP only solution.

Below, $times is a test array, and where validateTimes should return “19:00” in every case… but then I thought, maybe there is already a similar time validator stashed away somewhere, and maybe someone on SP knows where it is?

$times = array( '19','19.00','19:00',' 19 ',' 19 00 ', '1900' );

$b = array_map (  'validateTimes', $times ) ;


function validateTimes( $time ){


// target to return string 19:00
return '19:00' ; 
}

var_dump( $b );

Other use cases which should fail should probably include
29
25:00

I’m also going to trap events starting and ending at nutty times like 5am so need to really test the final result against a programmable range.

Any code, pointers, ideas or other test cases greatly appreciated, if it leads to a solution I’ll post it here.

Edit:

I suppose one question I should be asking too is, are there any native PHP time checking functions that would check a string like “19:00” is a valid time?

Yeah, OK, I should know how it works, I post some code then you improve it…


2 test arrays, each with 5 passes, 5 fails

/**/  
$times = array( 
'19 30','0915','19.00','19:00',' 19 5 ',
' 19 99 ', '19: 001', '75', 'aa' , '12:0a' );
/**/
$times = array( '9','0915', '9.00','09:00',' 09 ',
' 9 90 ', '09: 001', '45:09', '9am:06' ,'-9-09');
/**/

var_dump( $times ) ;


$b = array_map (  'validateTime', $times ) ;


function validateTime( $time = '' ){

$time = trim( $time );

// reject ambiguous 3 digit only format
if( preg_match('/^(\\d){3}$/', $time) ) return false ;
 
// inject colon into obvious 4 digit entry
if( preg_match('/^(\\d){4}$/', $time) ) 
    $time = substr($time, 0, 2) .':'. substr( $time, 2, 2 ) ;

// inject colon where user attempts to distinguish hh:mm
$time = str_replace( array(' ',';',',','.','-'), ":", $time ) ;

$len = strlen( $time ) ;

 if( $len > 5 ) return false ;

    // deal with hours entry only
 if ( $len === 1 ) $time  = '0'. $time . ':00';
 if ( $len === 2 ) $time .= ':00';

 $parts = explode( ':', $time );
 $time = sprintf("%02s:%02s",   $parts[0] , $parts[1] );

 if( ! preg_match('/^([0-1][0-9]|[2][0-3]):([0-5][0-9])$/', $time) ) return false ;

return $time ;
}

var_dump( $b ) ;

All works as I want it to ( ie returns false, which will equate to “Sorry cannot work out what time you mean” ) except for the very last test entry “-0-09” which returns 00:09,

This is for a data entry screen and will not get much use at all.

Hi Cups,

I had almost exactly same problem 3/4 months before but I was also unable to find a solid solution for this and had written following for me which is just a rough validation not the robust one:


$times = array( '19','19.00','19:00',' 19 ',' 19 00 ', '1900' );
foreach($times as $time){
    $time = trim($time);
    $time = str_replace(array(".", " "), ":", $time);
    $time = str_replace(".", ":", $time);
    if(strlen($time) == 4){
        $time = substr($time, 0, 2) . ":" . substr($time, -2);
    }
    $time = "$time:00";
    echo $time . '=' . date('H:i', strtotime($time)) . '<br />';
}

Edit:
You wrote yourself far better than mine :slight_smile:

Rajug, thanks for replying, mine looked just like that - KISS etc and I kept saying to myself, that’ll do … but then couldn’t help coming back to it over and again, then I kept swapping functionality between them and a regex … and - well then ended up with that oh so familiar feeling - why did I not use TDD on this from the start?

I hate how often I have to relearn that lesson.

Speaking of testing, I wonder if you would be better off having a constant testable format returned from your functions/methods.

If a confobulator can process a given string and transform it into a time, it will return it in the format of 00:00 (24h), otherwise false.

Much like you have already, but I’d probably have an particular object responsible for converting a particular string into said time format.

You can then just chain them together to add more functionality, or tolerance if you will, to your application.

My example is pretty poor, but it seems to be more explicit, testable and less verbose.

To me anyway. :wink:



$confobulator = new TimeConfobulatorCollection();
echo $confobulator->addConfobulator(
    new FooTimeConfobulator()
)->addConfobulator(
    new BarTimeConfobulator()
)->accepts('6pm'); #18:00

interface TimeConfobulator
{
    /**
    * @return false or string on success
    */
    public function accepts($subject);
}

class FooTimeConfobulator implements TimeConfobulator
{
    public function accepts($subject){
        preg_match_all('~((0|1)?[0-9])((a|p)m)~i', $subject, $parts);
        if(5 !== count($parts)){
            return false;
        }
        return sprintf(
            '&#37;02s:00',
            ('pm' === strtolower($parts[3][0])) ? $parts[1][0] + 12 : $parts[1][0]
        );
    }
}

class TimeConfobulatorCollection implements TimeConfobulator
{
    protected
        $confobulators = array();
    
    public function addConfobulator(TimeConfobulator $confobulator){
        array_push($this->confobulators, $confobulator);
        return $this;
    }
    
    public function accepts($subject){
        foreach($this->confobulators as $confobulator){
            if(false !== ($processed = $confobulator->accepts($subject))){
                return $processed;
            }
        }
        return false;
    }
}

Thanks, I wish I’d waited till I got these replies now.

My own solution is there and works ( 2 more tweaks need to plug holes I later found ) but I don’t like the lack of structure - though I am sure I am going to be using in 2 other places, I bet if I assembled it as you suggest then I could use it far more.

I like the 6pm idea, though I didn’t mind stipulating on the gui that all times have to be in the 24hr format.

A particular user I have in mind likes to pick dates (JS popup) but not times - finds that too pedantic, now I’ve used it to enter bits of test data, I can say I really like it, just enter ‘12’ and ‘13’ for a 1 hour event.

I might get my simpleTestFoo on and start again and build outwards in the manner you described.

Is that some form of Decorator ?

Pondering this whilst eating pasta, I came up with the thoughts that:

My function above returns a mysql-ready time ‘nn:nn’ , I might want it in formatted in other ways from other places. Hmmmm… separate out that which changes.

(going back and forth in my mind was the idea, should I change plan and try and separate hours from minutes from the very start? With the above idea, yes, that might have been a good move).

The other angle I thought about was how to handle bad and ambiguous values, such as 143 (is that 13:43 or 14:30.

In my head today, all I want is a correct value or false.

What if in the future I want more fine-grained feedback?

Or then again that might all end up yagni.

What I came up with below, but I think requiring a format for entry would be safest. That is, if you want to know whether 140 is 01:40 or a typo for 14:00, you have to ask the user.

Accepts pretty much anything like 9, 930, 9pm, 9::20am, 2330, 07:30pm etc (not exhaustively tested).

function parseTime($time)
{
    $pm = false;
    
    // check for typo like om instead of pm as anything not pm is am by default
    if(preg_match('~[a-z]+~i', $time))
    {
        if(!stripos($time, 'am') && !stripos($time, 'pm')) { return false; }
    }
    
    // check for pm, remove all non digits
    if(stripos($time, 'pm')) { $pm = true; }
    $t = preg_replace('~\\D~', '', $time); 
    
    // too long or short
    $len = strlen($t);
    if(!$t || $len > 4) { return false; }
    if(1 == $len || 2 == $len) { $t = $t.'00'; } 
    
    // split into h:m
    $m = substr($t, -2);
    $h = str_pad(str_replace($m, '', $t), 2, '0', STR_PAD_LEFT);
    
    // take care of pm
    if($pm && $h!=12) $h+=12;
    elseif(!$pm && 12==$h) $h = '00';
    
    // validate and format
    if($h>23 || $m>59) return false;
    return $h.':'.$m;
}

@hash, thats neato!

That even handled test cases I thought would fail: ‘9am:06’ and ‘-9-09’.


//2 test arrays

/**/  
$times = array( 
'19 30','0915','19.00','19:00',' 19 5 ',
' 19 99 ', '19: 001', '75', 'aa' , '12:0a' );
/** /
$times = array( '9','0915', '9.00','09:00',' 09 ',
' 9 90 ', '09: 001', '45:09', '9am:06' ,'-9-09');
/**/

var_dump( $times ) ;

$b = array_map (  'parseTime', $times ) ;

var_dump( $b );

Re the 3 straight digits, yes, I agree that is exactly what I wanted - the failure is fed back using ajax anyway and together with a short visual gui explanation I am happy it’ll do the trick

Interestingly you took the route of just making a 4 digit number, then just assembling its parts rather than look for evidence of an array of values.

Also, if anyone is interested, you can then go on and format the result at runtime like so:


$t = parseTime( "1730" ) ;

if( $t ) {
  echo date( "g.ia", strtotime( $t ) )  ; 

}else{
  echo 'bad time, try again';

}
// outputs the readable 5.30pm

[edit]I found one line I had to alter;
From:
elseif(!$pm && 12==$h) $h = ‘00’;

Tp:
elseif($pm && 12==$h){ $h = ‘23’; $m = ‘59’ ;}

As in my case each calendar entry can only be a single day, a 2 day event would be 2 entries hence 00:00 is understood to be the start of that day.

Also I found that 12:15 midday was returning 00:15

[/edit]

Also I found that 12:15 midday was returning 00:15

Believe that’s correct?? 12:15pm = 12:15, 12:15 = 12:15am = 00:15
Anyway, like I said, not fully tested, but glad it helped :slight_smile:

Although I now already have 2 working functions I thought I’d get on and really grok Anthony’s idea - he keeps putting this my way, and while I understand how it works, its not till you start playing with things you really deeply “grok it to its fullness”, anyhow I came up with this:

Use the interface and the collection from his code, but change the concrete classes to these:


class AmPmParser implements TimeConfobulator
{
    public function accepts($subject){

        preg_match_all('~((0|1)?[0-9])((a|p)m)~i', $subject, $parts);

        if(5 !== count($parts) || empty( $parts[0] ) ){
            return false;
        }
        return sprintf(
            '%02s:00',
            ('pm' === strtolower($parts[3][0])) ? $parts[1][0] + 12 : $parts[1][0]
        );
    }
}

class DigitsOnlyParser implements TimeConfobulator
{
    public function accepts($subject){

       $subject = preg_replace('~\\D~', '', $subject); 

        preg_match_all('~^((0|1|2)?[0-9])([0-5][0-9])?$~i', $subject, $parts);

        if( 4 !== count($parts) ||  empty( $parts[0] ) ){
         return false;
        }

        $mins = $parts[3][0] ? $parts[3][0] : "00" ;

        return sprintf(
            '%02s:%02s' , $parts[1][0] , $mins  
        );
    }
}

Then run the same tests on it like so;


$confobulator = new TimeConfobulatorCollection();

//2 test arrays

/** /  
$times = array( 
'19 30','0915','19.00','19:00',' 19 5 ', '12.15',
' 19 99 ', '19: 001', '75', 'aa' , '12pm' );
/**/
$times = array( '9','0915', '9.00','09:00',' 09 ',
' 9 90 ', '09: 001', '45:09', '9am:06' ,'-9-09');
/**/

var_dump( $times );

foreach( $times as $time ){

var_dump( $confobulator->addConfobulator( 
 new DigitsOnlyParser() )->addConfobulator( 
 new AmPmParser() )->accepts( 
$time ) 
);  

}


Heres the results from $times (second array)


Original dump array
  0 => string '9' (length=1)
  1 => string '0915' (length=4)
  2 => string '9.00' (length=4)
  3 => string '09:00' (length=5)
  4 => string ' 09 ' (length=4)
  5 => string ' 9 90 ' (length=6)  // ambiguous, fail
  6 => string '09: 001' (length=7) // too long, fail
  7 => string '45:09' (length=5) // out of bounds fail
  8 => string '9am:06' (length=6)
  9 => string '-9-09' (length=5)

Results
string '09:00' (length=5)
string '09:15' (length=5)
string '09:00' (length=5)
string '09:00' (length=5)
string '09:00' (length=5)
boolean false
boolean false
boolean false
string '09:06' (length=5)
string '09:09' (length=5)

Like he says “You can then just chain them together to add more functionality, or tolerance if you will, to your application.”

If one object cannot handle the problem, it passes the ball to the next one, and so on. (what pattern is that?)

So you could have a range of time parsers, exactly what they would be rather leaves me wondering at the moment, but I am sure there are more.

The question of formatting as I said can be done in a number of ways, based on using the native strtotime().

Thanks for the time and discussion on this thread, and hoping it helps someone else to make their input fields just a teeny bit nicer to use.

The traditional way to deal with this problem of picking times is to give the user a number of pick boxes, which are OK if you were prompting a visitor to add one or two events a year, but for an adminner, adding maybe one or two events per day (an maybe adding dates and times in other applications too) then this routine of "leave the keyboard, get the mouse, pick on hours, scroll down to 21:00 from a list, go back to keyboard " starts to become tiresome.

Just in case anyone else finds this function useful:

In hash’s solution parseTime() I found a bug when submitting times where the hour was the same as minutes eg “2020”.

Replace this line:


    $h = str_pad(str_replace($m, '', $t), 2, '0', STR_PAD_LEFT);

with:


    $h = str_pad( substr($t, 0, -2), 2, '0', STR_PAD_LEFT);

Thank god I had unit tests for this! Made nailing it feel more like fun.

Otherwise this function is working really nicely, thx again hash.