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.
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
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;
}
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
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);
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 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;