7
\$\begingroup\$

In five minutes I made a pretty ugly looking function. Can you help before I have to commit the code into history?

Requirements:

I would like a function that takes an array of numbers, and returns an array of ranges. Ideally remove the duplicate line.

Sample Input:

Array(1,2,3,4,5,6,10,11,12,13,20,24)

Output:

Array
(
    [0] => 1-6
    [1] => 10-13
    [2] => 20
    [3] => 24
)

Winning Entries:

Pseudo code  
PHP

My attempt:

$myArray = array(1,2,3,4,5,6,10,11,12,13,20,24);

$rangeArray = array();
$start = $end = current($myArray);
foreach($myArray as $range){
    if($range - $end > 1){
        $rangeArray[] = ($start == $end)?$start:$start."-".$end;
        $start = $range;
    }

    $end = $range;
}
$rangeArray[] = ($start == $end)?$start:$start."-".$end;

print_r($rangeArray);
\$\endgroup\$
1
  • \$\begingroup\$ I don't see a better way to do it more efficiently in an algorithmic meaning. This kind of task must go straight to the point and that is exactly what you have done. However if you project to build a function, you will need to check the parameters, to define default behaviors and to throw exceptions for inappropriate types of data, that's all. \$\endgroup\$ Feb 14, 2015 at 10:01

4 Answers 4

5
\$\begingroup\$

While these may not be any shorter or more efficient than existing answers, perhaps with a slightly different approach they still bring something to the table and might be considered a little less 'ugly'.

Essentially the GetRanges function I've written does the following:

  1. Take a parameter-supplied array of numbers ($aNumbers), remove any duplicates from the array and then sort them numerically lowest to highest.

  2. We next create a multi-dimensional array ($aGroups) and working sequentially through the array of numbers ($aNumbers): Except for the first number which we just add to $aGroups to get us started, for each consecutive number $i > 0, if the previous number $aNumbers[$i-1] is equal to the current number minus 1 $aNumbers[$i] - 1 then the current number is added to the same group because its consecutive and belongs to that range, otherwise the current number is added to $aGroups as a new sub-array with one entry array( $aNumbers[$i] ).

For options 2 and 3 where the function is expanded upon for a simple array:

  1. A new array ($aRanges) is created and for each $aGroups record, if the first entry of the sub-array is the only entry then a new entry is added to $aRanges for just this number, otherwise a new entry is added to $aRanges specifying the first number in the range followed by a hyphen followed by the last number in the range.

For option 3 where the function is expanded upon to return a string:

  1. The implode function is used to create a comma-separated list of all the entries in the $aRanges array and return this as a string.

Option 1: If you want your function to return a multi-dimensional array:

<?php
function GetRanges( $aNumbers ) {
  $aNumbers = array_unique( $aNumbers );
  sort( $aNumbers );
  $aGroups = array();
  for( $i = 0; $i < count( $aNumbers ); $i++ ) {
    if( $i > 0 && ( $aNumbers[$i-1] == $aNumbers[$i] - 1 ))
      array_push( $aGroups[count($aGroups)-1], $aNumbers[$i] );
    else
      array_push( $aGroups, array( $aNumbers[$i] )); 
  }
  return $aGroups;
}

$aNumbers = array( 1, 2, 3, 4, 5, 6, 10, 11, 12, 13, 20, 24 );
print_r( GetRanges( $aNumbers ));

Output:

Array (
  [0] => Array ( [0] => 1, [1] => 2, [2] => 3, [3] => 4, [4] => 5, [5] => 6 )
  [1] => Array ( [0] => 10, [1] => 11, [2] => 12, [3] => 13 )
  [2] => Array ( [0] => 20 )
  [3] => Array ( [0] => 24 )
)

Option 2: If you want your function to return a simple array:

<?php
function GetRanges( $aNumbers ) {
  $aNumbers = array_unique( $aNumbers );
  sort( $aNumbers );
  $aGroups = array();
  for( $i = 0; $i < count( $aNumbers ); $i++ ) {
    if( $i > 0 && ( $aNumbers[$i-1] == $aNumbers[$i] - 1 ))
      array_push( $aGroups[count($aGroups)-1], $aNumbers[$i] );
    else
      array_push( $aGroups, array( $aNumbers[$i] )); 
  }
  $aRanges = array();
  foreach( $aGroups as $aGroup ) {
    if( count( $aGroup ) == 1 )
      $aRanges[] = $aGroup[0];
    else
      $aRanges[] = $aGroup[0] . '-' . $aGroup[count($aGroup)-1];
  }
  return $aRanges;
}

$aNumbers = array( 1, 2, 3, 4, 5, 6, 10, 11, 12, 13, 20, 24 );
print_r( GetRanges( $aNumbers ));

Output: Array ( [0] => 1-6, [1] => 10-13, [2] => 20, [3] => 24 )

Option 3: If you want your function to return a string:

<?php
function GetRanges( $aNumbers ) {
  $aNumbers = array_unique( $aNumbers );
  sort( $aNumbers );
  $aGroups = array();
  for( $i = 0; $i < count( $aNumbers ); $i++ ) {
    if( $i > 0 && ( $aNumbers[$i-1] == $aNumbers[$i] - 1 ))
      array_push( $aGroups[count($aGroups)-1], $aNumbers[$i] );
    else
      array_push( $aGroups, array( $aNumbers[$i] )); 
  }
  $aRanges = array();
  foreach( $aGroups as $aGroup ) {
    if( count( $aGroup ) == 1 )
      $aRanges[] = $aGroup[0];
    else
      $aRanges[] = $aGroup[0] . '-' . $aGroup[count($aGroup)-1];
  }
  return implode( ',', $aRanges );
}

$aNumbers = array( 1, 2, 3, 4, 5, 6, 10, 11, 12, 13, 20, 24 );
echo( GetRanges( $aNumbers ));

Output: 1-6,10-13,20,24

\$\endgroup\$
3
  • \$\begingroup\$ Please consider editing your answer to explain more about your code. You should try to explain all the suggestions you're making, how they work and what difference they might make. \$\endgroup\$ Sep 4, 2015 at 11:19
  • 1
    \$\begingroup\$ @SuperBiasedMan I've now added detailed explanation to the answer of how the code works. In terms of difference it might make, I suspect absolutely nothing other than perhaps the code is easier to read and understand. \$\endgroup\$ Sep 4, 2015 at 12:03
  • \$\begingroup\$ For the record, readability is a perfectly valid reason to give someone when you review their code (just in case my comment seemed to imply otherwise). \$\endgroup\$ Sep 4, 2015 at 13:15
3
\$\begingroup\$

You could add a null value at the end of your main array and remove last line. This way the null will be dropped, but it probably will be harder to understand if someone comes later to see your code.

Maybe better naming for your vars.

I played a bit with the problem, here's what I got. The idea is the same wrote in a different way.

$myArray = array(1,2,3,4,5,6,10,11,12,13,20,24);

//last value is dropped so add something useless to be dropped
array_push($myArray, null);
$rangeArray = array();

array_walk($myArray, function($val) use (&$rangeArray){
    static $oldVal, $rangeStart;

    if (is_null($rangeStart))
        goto init;

    if ($oldVal+1 == $val) {
        $oldVal = $val;
        return;
    }

    if ($oldVal == $rangeStart) {
        array_push($rangeArray, $rangeStart);
        goto init;
    }

    array_push($rangeArray, $rangeStart . '-' . $oldVal);

    init: {
        $rangeStart = $val;
        $oldVal = $val;
    }
});

print_r($rangeArray);
\$\endgroup\$
2
  • \$\begingroup\$ Thank you for your input. I am not a big fan of the goto statement and therefore usually stay away, with a little bit of refactoring it can be removed. Interesting idea to add a null value to remove the need to duplicate the array_push line. \$\endgroup\$ Feb 11, 2015 at 19:36
  • \$\begingroup\$ Hi @WilliamGeorge, if you like this (and other answers too, as per your other comments), why not reward them with upvotes? \$\endgroup\$
    – janos
    Feb 15, 2015 at 11:45
3
\$\begingroup\$

I've changed the output slightly to make the processing easier:

$numbers = array(1,2,3,4,5,6,10,11,12,13,20,24);

$ranges[] = array($numbers[0],$numbers[0]); // initial value

foreach ($numbers as $number) {
  $range    = array_pop($ranges);
  $extend   = ($range[1] == $number-1);
  $ranges[] = array($range[0],$extend ? $number : $range[1]);
  if (!$extend) $ranges[] = array($number,$number);
}

echo '<pre>'.print_r($ranges,TRUE).'</pre>';

It's now easy to use this output for whatever purpose you need it. It looks like this:

Array
(
    [0] => Array
        (
            [0] => 1
            [1] => 1
        )

    [1] => Array
        (
            [0] => 1
            [1] => 6
        )

    [2] => Array
        (
            [0] => 10
            [1] => 13
        )

    [3] => Array
        (
            [0] => 20
            [1] => 20
        )

    [4] => Array
        (
            [0] => 24
            [1] => 24
        )

)

If you really need to, you could get your output by using this code:

foreach ($ranges as $range) {
  $output[] = ($range[0] == $range[1]) ? $range[0] : $range[0].'-'.$range[1];
}

echo '<pre>'.print_r($output,TRUE).'</pre>';

Which will result in:

Array
(
    [0] => 1
    [1] => 1-6
    [2] => 10-13
    [3] => 20
    [4] => 24
)
\$\endgroup\$
2
\$\begingroup\$

more like an alternative to give some impulse than a review of your code

<?php
$myArray = array(1,2,3,4,5,6,10,11,12,13,20,24);
$rangeArray = array();

class ArrayAggregator implements Iterator {

    private $data;
    private $index = 0;

    public function __construct(array $array) {
        $this->data = $array;
    }

    public function current()
    {
        // save the start value
        $startValue = $this->_current();

        // iterate until gap is bigger than 1
        do {
            // get the "real" current value
            $value = $this->_current();
            // move to the next value
            $this->next();
            // until gap
        } while ($this->_current() == $value +1);

        // @todo more grace
        // set index back because foreach will call "next" on after we return the current value
        $this->index -= 1;

        // return start and end value as aggregate output
        return sprintf('%d - %d', $startValue, $value);

    }

    private function _current() {
        return $this->data[$this->index];
    }

    public function next()
    {
        $this->index += 1;
    }

    public function key()
    {
        return $this->index;
    }

    public function valid()
    {
        return isset($this->data[$this->index]);
    }

    public function rewind()
    {
        $this->index = 0;
    }
}

$aa = new ArrayAggregator($myArray);
foreach ($aa  as $a) {
    echo $a."\n";
}
\$\endgroup\$
1
  • \$\begingroup\$ Wow, thats a fun class. I never would have thought to do that. A little overkill for my specific needs, but I like it :) \$\endgroup\$ Feb 11, 2015 at 20:14

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.