Converting array elements into range in php

2019-02-09 19:34发布

问题:

I’m working on an array of numeric values.

I have a array of numeric values as the following in PHP

11,12,15,16,17,18,22,23,24

And I’m trying to convert it into range for e.g in above case it would be:

11-12,15-18,22-24

I don’t have any idea how to convert it into range.

回答1:

You have to code it yourself ;-)

The algorithm is quite simple:

  • Iterate over the items.
  • Remember the previous item and the start of the range.
  • For each item (except the first one) check:
    • If currentItem = prevItem + 1 then you haven't found a new range. Continue.
    • Otherwise your range has ended. Write down the range. You have remembered the start of the range. The end is the previous item. The new range starts with the current item.
    • The first item always starts a new range. Remember this one as start of the range.
  • Don't forget to write down the current range when leaving the loop.


回答2:

I have used this one before, it does the trick.

Takes as input a comma separated string of numbers. Call to sort could be ignored if numbers are guaranteed to be sorted already.

function range_string($csv)
{
    // split string using the , character
    $number_array = array_map('intval', explode(',', $csv));
    sort($number_array);

    // Loop through array and build range string
    $previous_number = intval(array_shift($number_array)); 
    $range = false;
    $range_string = "" . $previous_number; 
    foreach ($number_array as $number) {
      $number = intval($number);
      if ($number == $previous_number + 1) {
        $range = true;
      }
      else {
        if ($range) {
          $range_string .= "-$previous_number";
          $range = false;
        }
        $range_string .= ",$number";
      }
      $previous_number = $number;
    }
    if ($range) {
      $range_string .= "-$previous_number";
    }

    return $range_string;
}

$csv_string = "11,16,12,17,18,15,22,23,24";
print range_string($csv_string); // 11-12,15-18,22-24


回答3:

Just adding my copy that is slightly different and supports a few extra things. I came here to compare it against other implementations. Here is test code to check the capability/correctness of my code:

$tests = [
    '1, 3, 5, 7, 9, 11, 13-15' => [1, 3, 5, 7, 9, 11, 13, 14, 15],
    '1-5'                      => [1, 2, 3, 4, 5],
    '7-10'                     => [7, 8, 9, 10],
    '1-3'                      => [1, 2, 3],
    '1-5, 10-12'               => [1, 2, 3, 4, 5, 10, 11, 12],
    '1-5, 7'                   => [1, 2, 3, 4, 5, 7],
    '10, 12-15'                => [10, 12, 13, 14, 15],
    '10, 12-15, 101'           => [10, 12, 13, 14, 15, 101],
    '1-5, 7, 10-12'            => [1, 2, 3, 4, 5, 7, 10, 11, 12],
    '1-5, 7, 10-12, 101'       => [1, 2, 3, 4, 5, 7, 10, 11, 12, 101],
    '1-5, 7, 10, 12, 14'       => [1, 2, 3, 4, 5, 7, 10, 12, 14],
    '1-4, 7, 10-12, 101'       => '1,2,3,4,7,10,11,12,101',
    '1-3, 5.5, 7, 10-12, 101'  => '1,2,3,5.5,7,10,11,12,101',
];

foreach($tests as $expectedResult => $array) {
    $funcResult = Utility::rangeToStr($array);
    if($funcResult != $expectedResult) {
        echo "Failed: result '$funcResult' != test check '$expectedResult'\n";
    } else {
        echo "Passed!: '$funcResult' == '$expectedResult'\n";
    }
}

The meat and potatoes, this is meant to be called statically within a class howver simply remove "static public" to use as a normal procedural function:

/**
 * Converts either a array of integers or string of comma-separated integers to a natural english range, such as "1,2,3,5" to "1-3, 5".  It also supports
 * floating point numbers, however with some perhaps unexpected / undefined behaviour if used within a range.
 *
 * @param string|array $items    Either an array (in any order, see $sort) or a comma-separated list of individual numbers.
 * @param string       $itemSep  The string that separates sequential range groups.  Defaults to ', '.
 * @param string       $rangeSep The string that separates ranges.  Defaults to '-'.  A plausible example otherwise would be ' to '.
 * @param bool|true    $sort     Sort the array prior to iterating?  You'll likely always want to sort, but if not, you can set this to false.
 *
 * @return string
 */
static public function rangeToStr($items, $itemSep = ', ', $rangeSep = '-', $sort = true) {
    if(!is_array($items)) {
        $items = explode(',', $items);
    }
    if($sort) {
        sort($items);
    }
    $point = null;
    $range = false;
    $str = '';
    foreach($items as $i) {
        if($point === null) {
            $str .= $i;
        } elseif(($point + 1) == $i) {
            $range = true;
        } else {
            if($range) {
                $str .= $rangeSep . $point;
                $range = false;
            }
            $str .= $itemSep . $i;
        }
        $point = $i;
    }
    if($range) {
        $str .= $rangeSep . $point;
    }

    return $str;
}


回答4:

If we have previous item and current item is not next number in sequence, then we put previous range (start-prev) in output array and current item will be start of next range, if we don't have previous item, then this item is the first item and as mentioned before - first item starts a new range. newItem function returns range or sigle number if there is no range. If you have unsorted array with repeating numbers, use sort() and array_unique() functions.

$arr = array(1,2,3,4,5,7,9,10,11,12,15,16);

function newItem($start, $prev)
{
    if ($start == $prev)
    {
        $result = $start;
    }
    else
    {
        $result = $start . '-' . $prev;
    }

    return $result;
}

foreach($arr as $item)
{
    if ($prev)
    {
        if ($item != $prev + 1)
        {
            $newarr[] = newItem($start, $prev);
            $start = $item;
        }
    }
    else
    {
        $start = $item;
    }
    $prev = $item;
}

$newarr[] = newItem($start, $prev);

echo implode(',', $newarr);

1-5,7,9-12,15-16



回答5:

http://ideone.com/lmd7SY

This is my version folks. The algorithm is quite evident from the way I've coded it up, with comments at regular intervals. The approach was to divide the problem into sub-parts, and here's pseudo-code.

Pass through the sorted array using a loop to detect break points. If the next and previous numbers are in a consequent progression of n+1, do nothing, else, note that a break point has now been detected. Track that break point by pushing the key of break into a new array. Using the data from the break point array, use the key information where the break points occur to loop through the initial array to build range values. Ensure a check for last iteration.

$array = array(11,12,15,16,17,18,22,23,24);
$break_start = array();

//range finder
for ($i=0; $i<sizeof($array); $i++) {
    $current = $array[$i]; 
    $previous = $array[$i-1];
    if ($current==($previous+1)) { 
        //no break points are found 
    } else { 
        //return break points with keys intact
        array_push($break_start, $i);
    }

}

for ($i=0; $i<sizeof($break_start); $i++) {
    $key = $break_start[$i];
    $next_key = $break_start[$i+1];

    //if last iteration
    if ($i==sizeof($break_start)-1) { 
        echo "Range: ".$array[$key]." - ".$array[count($array)-1]." \n"; 
        } 
    else { 
        echo "Range: ".$array[$key]." - ".$array[$next_key-1]." \n";    
        }
}

Pretty solid and hand coded in around 10 minutes.

Works for larger sets as well, tried and test:

$array = array(11,12,15,16,17,18,22,23,24,26,27,28,56,57,58);



回答6:

The answer provided by Ali Gajani kept giving me "Illegal offset" warnings. So, because I figured somebody might want to use it as badly as I do, I'm posting my fixes here - though note that my fixes might be deemed silly by an advanced programmer - it does seem to work now with no issues.

I replaced/tweaked two parts of the code. Below you will see what I added (marked in bold) and what was removed, which I commented (//).

As near as I can tell, it was having trouble on the first pass because there was no "previous pass" to refer to (hence, it was balking at "$previous = $array[$i-1];") - and on the last pass for a similar reason. In that second instance, I simply moved "$next_key = $break_start[$i+1];" below the last iteration check.

$break_start = array();

//range finder
for ($i=0; $i<sizeof($array); $i++) {
    $current = $array[$i]; 
**if($i>0) {
        $previous = $array[$i-1];
}
else {
        $previous = $current;
}**
  //  $previous = $array[$i-1];
    if ($current==($previous+1)) { 
        //no break points are found 
    } else { 
        //return break points with keys intact
        array_push($break_start, $i);
    }

}

for ($i=0; $i<sizeof($break_start); $i++) {
    $key = $break_start[$i];
//    $next_key = $break_start[$i+1];

    //if last iteration
    if ($i==sizeof($break_start)-1) { 
        echo "Range: ".$array[$key]." - ".$array[count($array)-1]." \n"; 
        } 
    else { 
    **$next_key = $break_start[$i+1];**
        echo "Range: ".$array[$key]." - ".$array[$next_key-1]." \n";    
        }
}

If there are smarter ways to get this done, please advise. But I searched high and low for this - and figured someone else might also benefit. Another tip of the hat to Ali Gajani for his initial help.



回答7:

You could do it like that:

$numbers = [11,12,15,16,17,18,22,23,24];
$ranges  = [];
$start   = $end = current($numbers);

foreach($numbers as $range){
    if($range - $end > 1){
        $ranges[] = ($start == $end) ? $start : $start . "-" . $end;
        $start    = $range;
    }
    $end = $range;
}
$ranges[] = ($start == $end) ? $start : $start . "-" . $end;


标签: php arrays range