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 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
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
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;
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.
You have to code it yourself ;-)
The algorithm is quite simple:
currentItem = prevItem + 1
then you haven't found a new range. Continue.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;
}