Convert "1d2h3m" to ["day" => 1, ”hour” => 2,"minutes"=>3]
Asked Answered
K

18

11

I am trying to parse a time expression string into an associative array with full-word keys.

My input:

$time = "1d2h3m";

My desired output:

array(
    "day" => 1,
    "hour" => 2,
    "minutes" => 3
)

I have tried to extract the numbers with explode().

$time = "1d2h3m";
$day = explode("d", $time);
var_dump($day); // 0 => string '1' (length=1)
                // 1 => string '2h3m' (length=4)

How can I convert my strictly formatted string into the desired associative array?

Keppel answered 22/9, 2015 at 4:5 Comment(2)
Where is the $ol defined?Interested
explode the resulting array twice, each with h and m, do it inside a loop and array_push into another array.Edmond
O
25

A simple sscanf will parse this into an array. Then you array_combine it with the list of keys you want.

Example:

$time = "1d2h3m";

$result = array_combine(
    ['day', 'hour', 'minutes'],
    sscanf($time, '%dd%dh%dm')
);

print_r($result);

Output:

Array
(
    [day] => 1
    [hour] => 2
    [minutes] => 3
)

To break down the sscanf format used:

  • %d - reads a (signed) decimal number, outputs an integer
  • d - matches the literal character "d"
  • %d - (as the first)
  • h - matches the literal character "h"
  • %d - (as the first)
  • m - matches the literal character "m" (optional, as it comes after everything you want to grab)

It'll also work fine with multiple digits and negative values:

$time = "42d-5h3m";
Array
(
    [day] => 42
    [hour] => -5
    [minutes] => 3
)
Outspoken answered 16/10, 2015 at 7:38 Comment(0)
Y
16

You should go with regex for this case

<?php
 $time = "1d2h3m";
 if(preg_match("/([0-9]+)d([0-9]+)h([0-9]+)m/i",$time,$regx_time)){
    $day = (int) $regx_time[1];
    $hour = (int) $regx_time[2];
    $minute = (int) $regx_time[3];
    var_dump($day);
 }
?>

Explaination :
[0-9] : Match any digits between 0 and 9
[0-9]+ : Means, match digits at least one character
([0-9]+) : Means, match digits at least one character and capture the results
/........../i : Set the case insenstive for the pattern of regex you've setted

Regex is the way better to be a lexer and parse string lexically. And it's good to learn regex. Almost all programming language use regex

Yevetteyew answered 22/9, 2015 at 4:17 Comment(1)
Just compare this basic RegEx with the rest. I have no idea why college/university don't teach RegEx.Dryad
N
13

Customizable function, and the most important thing, that you can catch an exception if input is not properly formed

/**
  * @param $inputs string : date time on this format 1d2h3m
  * @return array on this format                 "day"      => 1,
  *                                              "hour"     => 2,
  *                                              "minutes"  => 3        
  * @throws Exception
  *
  */
function dateTimeConverter($inputs) {
    // here you can customize how the function interprets input data and how it should return the result
    // example : you can add "y"    => "year"
    //                       "s"    => "seconds"
    //                       "u"    => "microsecond"
    // key, can be also a string
    // example                "us"  => "microsecond"
    $dateTimeIndex  = array("d" => "day",
                               "h" => "hour",
                               "m" => "minutes");

    $pattern        = "#(([0-9]+)([a-z]+))#";
    $r              = preg_match_all($pattern, $inputs, $matches);
    if ($r === FALSE) {
        throw new Exception("can not parse input data");
    }
    if (count($matches) != 4) {
        throw new Exception("something wrong with input data");
    }
    $datei      = $matches[2]; // contains number
    $dates      = $matches[3]; // contains char or string
    $result    = array();
    for ($i=0 ; $i<count ($dates) ; $i++) {
        if(!array_key_exists($dates[$i], $dateTimeIndex)) {
            throw new Exception ("dateTimeIndex is not configured properly, please add this index : [" . $dates[$i] . "]");
        }
        $result[$dateTimeIndex[$dates[$i]]] = (int)$datei[$i];
    }
    return $result;
}
Nudibranch answered 15/10, 2015 at 9:37 Comment(2)
@Sorin in this case you should adapt $dateTimeIndex in this way to avoid ambiguity $dateTimeIndex = array( "mon" => "month", "d" => "day", "h" => "hour", "min" => "minutes"); And, for sure, the input string must obey the rule :)Nudibranch
There is no benefit in wrapping the whole pattern in capturing parentheses. This is unnecessarily bloating the matches array.Parous
G
5

Another regex solution

$subject = '1d2h3m';
if(preg_match('/(?P<day>\d+)d(?P<hour>\d+)h(?P<minute>\d+)m/',$subject,$matches))
{
  $result = array_map('intval',array_intersect_key($matches,array_flip(array_filter(array_keys($matches),'is_string'))));
  var_dump($result);
}

Returns

array (size=3)
  'day' => int 1
  'hour' => int 2
  'minute' => int 3
Gush answered 22/9, 2015 at 4:17 Comment(2)
Sorry, I post a comment by mistake. It was mean for another (not your) post.Epithalamium
This answer is missing its educational explanation. I don't think I recommend this answer, since this task can be accomplished in just one native function call versus ...I don't know how many -- I lost count, is it 10 function calls?Parous
E
5

Use this simple implementation with string replace and array combine.

<?php
   $time = "1d2h3m";
   $time=str_replace(array("d","h","m")," " ,$time);
   $time=array_combine(array("day","hour","minute"),explode(" ",$time,3));

   print_r($time);
?>
Ecklund answered 19/10, 2015 at 6:19 Comment(0)
T
4

I think for such a small string, and if the format will always be the same, you could use array_push and substr to get extract the numbers out of the string and put them in an array.

<?php
$time = "1d2h3m";
$array = array();
array_push($array, (int) substr($time, 0, 1));
array_push($array, (int) substr($time, 2, 1));
array_push($array, (int) substr($time, 4, 1));
var_dump($array);
?>
Taffy answered 22/9, 2015 at 4:18 Comment(1)
I don't think it is a very attractive solution to use 6 function calls when one function call can do this job.Parous
P
4

This approach uses a regular expression to extract the values and an array to map the letters to the words.

<?php
   // Initialize target array as empty
   $values = [];

   // Use $units to map the letters to the words
   $units = ['d'=>'days','h'=>'hours','m'=>'minutes'];

   // Test value
   $time = '1d2h3m';

   // Extract out all the digits and letters
   $data = preg_match_all('/(\d+)([dhm])/',$time,$matches);

   // The second (index 1) array has the values (digits)
   $unitValues = $matches[1];

   // The third (index 2) array has the keys (letters)
   $unitKeys = $matches[2];

   // Loop through all the values and use the key as an index into the keys
   foreach ($unitValues as $k => $v) {
       $values[$units[$unitKeys[$k]]] = $v;
   }
Persinger answered 19/10, 2015 at 1:29 Comment(3)
array_combine(array_values(array_intersect_key($units,array_fill_keys($matches[2],1))),$matches[1])) ?Ferruginous
@Ferruginous - That's a nice solution. The reason I used the foreach is that it is only three elements and I like the simplicity of the loop.Persinger
I understand, I was mostly joking, I started writing it thinking it would be cleaner, but by the end it was clear it's harder to follow ;) foreach is fine php isn't really a functional languageFerruginous
S
3

preg_split returns an array of values split by pattern(#[dhm]#).

list() sets the value for each array element.

$d = [];
list($d['day'],$d['hour'],$d['minutes']) = preg_split('#[dhm]#',"1d2h3m");
Shir answered 21/10, 2015 at 0:0 Comment(0)
O
0

Can't you just explode it 3 times...

// Define an array
$day = explode("d", $time);
// add it in array with key as "day" and first element as value
$hour= explode("h", <use second value from above explode>);
// add it in array with key as "hour" and first element as value
$minute= explode("m", <use second value from above explode>);
// add it in array with key as "minute" and first element as value

I don't have any working example right now but I think it will work.

Olwen answered 15/10, 2015 at 9:51 Comment(1)
I don't think I recommend this 4th-of-July technique. This task can be resolved with a single function call.Parous
E
0

We can also achieve this using str_replace() and explode() function.

$time = "1d2h3m"; 
$time = str_replace(array("d","h","m"), "*", $time);
$exp_time =  explode("*", $time); 
$my_array =  array(  "day"=>(int)$exp_time[0],
                     "hour"=>(int)$exp_time[1],
                     "minutes"=>(int)$exp_time[2] ); 
var_dump($my_array);
Ezekiel answered 19/10, 2015 at 9:18 Comment(0)
K
0
$time = "1d2h3m";
$split = preg_split("/[dhm]/",$time);
$day = array(
    "day"     => $split[0]
    "hour"    => $split[1]
    "minutes" => $split[2]
);
Kierakieran answered 19/10, 2015 at 13:4 Comment(1)
This answer is missing its educational explanation.Parous
K
0

You could use one line solution,

$time = "1d2h3m";
$day = array_combine(
           array("day","hour","months")   , 
           preg_split("/[dhm]/", substr($time,0,-1)  )    
       );
Kierakieran answered 19/10, 2015 at 13:16 Comment(1)
Why do you split on m if you unconditionally remove m from the string before you split it? This answer is missing its educational explanation.Parous
F
0

There are a lot of great answers here, but I want to add one more, to show a more generic approach.

function parseUnits($input, $units = array('d'=>'days','h'=>'hours','m' => 'minutes')) {
    $offset = 0;
    $idx = 0;

    $result = array();
    while(preg_match('/(\d+)(\D+)/', $input,$match, PREG_OFFSET_CAPTURE, $offset)) {
        $offset = $match[2][1];

        //ignore spaces
        $unit = trim($match[2][0]);
        if (array_key_exists($unit,$units)) { 
            // Check if the unit was allready found
            if (isset($result[$units[$unit]])) {  
                throw new Exception("duplicate unit $unit");
            }

            // Check for corect order of units
            $new_idx = array_search($unit,array_keys($units));
            if ($new_idx < $idx) {
                throw new Exception("unit $unit out of order");             
            } else {
                $idx = $new_idx;
            }
            $result[$units[trim($match[2][0])]] = $match[1][0];

        } else {
            throw new Exception("unknown unit $unit");
        }
    }
    // add missing units
    foreach (array_keys(array_diff_key(array_flip($units),$result)) as $key) {
        $result[$key] = 0;
    }
    return $result;
}
print_r(parseUnits('1d3m'));
print_r(parseUnits('8h9m'));
print_r(parseUnits('2d8h'));
print_r(parseUnits("3'4\"", array("'" => 'feet', '"' => 'inches')));
print_r(parseUnits("3'", array("'" => 'feet', '"' => 'inches')));
print_r(parseUnits("3m 5 d 5h 1M 10s", array('y' => 'years', 
           'm' => 'months', 'd' =>'days', 'h' => 'hours', 
           'M' => 'minutes', "'" => 'minutes', 's' => 'seconds' )));
print_r(parseUnits("3m 5 d 5h 1' 10s", array('y' => 'years', 
           'm' => 'months', 'd' =>'days', 'h' => 'hours',
           'M' => 'minutes', "'" => 'minutes', 's' => 'seconds' )));
Ferruginous answered 20/10, 2015 at 20:27 Comment(0)
S
0

You can use this fn to get the formatted array which check for the string validation as well :

function getFormatTm($str)
{
    $tm=preg_split("/[a-zA-z]/", $str);

    if(count($tm)!=4) //if not a valid string
       return "not valid string<br>";
    else
       return array("day"=>$tm[0],"hour"=>$tm[1],"minutes"=>$tm[2]);
}
$t=getFormatTm("1d2h3m");
var_dump($t);
Sacrilegious answered 21/10, 2015 at 5:6 Comment(0)
E
0

This answer is not 100% relevant to particular input string format from the question.

But it based on the PHP date parsing mechanism (is not my own date parsing bicycle).

PHP >=5.3.0 has DateInterval class.

You could create DateInterval objects from stings in two formats:

$i = new DateInterval('P1DT12H'); // ISO8601 format
$i = createFromDateString('1 day + 12 hours'); // human format

PHP official docs: http://php.net/manual/en/dateinterval.createfromdatestring.php

In ISO8601 format P stands for "period". Format supports three forms of the periods:

  • PnYnMnDTnHnMnS
  • PnW
  • PT

Capital letters represents the following:

  • P is the duration designator (historically called "period") placed at the start of the duration representation.
  • Y is the year designator that follows the value for the number of years.
  • M is the month designator that follows the value for the number of months.
  • W is the week designator that follows the value for the number of weeks.
  • D is the day designator that follows the value for the number of days.
  • T is the time designator that precedes the time components of the representation.
  • H is the hour designator that follows the value for the number of hours.
  • M is the minute designator that follows the value for the number of minutes.
  • S is the second designator that follows the value for the number of seconds.

For example, "P3Y6M4DT12H30M5S" represents a duration of "three years, six months, four days, twelve hours, thirty minutes, and five seconds".

See https://en.wikipedia.org/wiki/ISO_8601#Durations for details.

Epithalamium answered 21/10, 2015 at 9:14 Comment(0)
B
0

One line code:

$result = array_combine(array("day", "hour", "minutes"), preg_split('/[dhm]/', "1d2h3m", -1, PREG_SPLIT_NO_EMPTY));
Ballinger answered 21/10, 2015 at 12:28 Comment(0)
P
0

sscanf() is certainly an ideal tool for this job. Unlike preg_match(), this native text parsing function avoids creating a useless fullstring match. The numeric values that are extracted can be cast as integers and directly assigned to their respective keys in the result array.

Code (creates reference variables): (Demo)

$time = "1d2h3m";
sscanf($time, '%dd%dh%dm', $result['day'], $result['hour'], $result['minutes']);
var_export($result);

or if you are processing multiple strings in a loop and want to avoid creating reference variables (or find it ugly to unset() the variables at the end of each iteration), then you can "destructure" the return array into the desired associative structure.

Code: (Demo)

$time = "1d2h3m";
[$result['day'], $result['hour'], $result['minutes']] = sscanf($time, '%dd%dh%dm');
var_export($result);

Output:

array (
  'day' => 1,
  'hour' => 2,
  'minutes' => 3,
)
Parous answered 26/8, 2021 at 0:47 Comment(0)
B
-2

You can do using this code.

<?php
$str = "1d2h3m";
list($arr['day'],$arr['day'],$arr['hour'],$arr['hour'],$arr['minute'],$arr['minute']) = $str;
print_r($arr);
?>

OUTPUT

Array ( 
   [minute] => 3
   [hour] => 2
   [day] => 1
)

DEMO

Bombardier answered 22/9, 2015 at 4:57 Comment(1)
You solution does not work with two-digits numbers of days, hours and minutes.Epithalamium

© 2022 - 2024 — McMap. All rights reserved.