$ foo="1,2,3,6,7,8,11,13,14,15,16,17"
In shell, how to group the numbers in $foo
as 1-3,6-8,11,13-17
$ foo="1,2,3,6,7,8,11,13,14,15,16,17"
In shell, how to group the numbers in $foo
as 1-3,6-8,11,13-17
Given the following function:
build_range() {
local range_start= range_end=
local -a result
end_range() {
: range_start="$range_start" range_end="$range_end"
[[ $range_start ]] || return
if (( range_end == range_start )); then
# single number; just add it directly
result+=( "$range_start" )
elif (( range_end == (range_start + 1) )); then
# emit 6,7 instead of 6-7
result+=( "$range_start" "$range_end" )
else
# larger span than 2; emit as start-end
result+=( "$range_start-$range_end" )
fi
range_start= range_end=
}
# use the first number to initialize both values
range_start= range_end=
result=( )
for number; do
: number="$number"
if ! [[ $range_start ]]; then
range_start=$number
range_end=$number
continue
elif (( number == (range_end + 1) )); then
(( range_end += 1 ))
continue
else
end_range
range_start=$number
range_end=$number
fi
done
end_range
(IFS=,; printf '%s\n' "${result[*]}")
}
...called as follows:
# convert your string into an array
IFS=, read -r -a numbers <<<"$foo"
build_range "${numbers[@]}"
...we get the output:
1-3,6-8,11,13-17
awk solution for an extended sample:
foo="1,2,3,6,7,8,11,13,14,15,16,17,19,20,33,34,35"
awk -F',' '{
r = nxt = 0;
for (i=1; i<=NF; i++)
if ($i+1 == $(i+1)){ if (!r) r = $i"-"; nxt = $(i+1) }
else { printf "%s%s", (r)? r nxt : $i, (i == NF)? ORS : FS; r = 0 }
}' <<<"$foo"
The output:
1-3,6-8,11,13-17,19-20,33-35
As an alternative, you can use this awk command:
cat series.awk
function prnt(delim) {
printf "%s%s", s, (p > s ? "-" p : "") delim
}
BEGIN {
RS=","
}
NR==1 {
s = $1
}
p < $1-1 {
prnt(RS)
s = $1
}
{
p = $1
}
END {
prnt(ORS)
}
Now run it as:
$> foo="1,2,3,6,7,8,11,13,14,15,16,17"
$> awk -f series.awk <<< "$foo"
1-3,6-8,11,13-17
$> foo="1,3,6,7,8,11,13,14,15,16,17"
$> awk -f series.awk <<< "$foo"
1,3,6-8,11,13-17
$> foo="1,3,6,7,8,11,13,14,15,16,17,20"
$> awk -f series.awk <<< "$foo"
1,3,6-8,11,13-17,20
Here is an one-liner for doing the same:
awk 'function prnt(delim){printf "%s%s", s, (p > s ? "-" p : "") delim}
BEGIN{RS=","} NR==1{s = $1} p < $1-1{prnt(RS); s = $1} {p = $1}END {prnt(ORS)}' <<< "$foo"
In this awk command we keep 2 variables:
p
for storing previous line's numbers
for storing start of the range that need to be printedHow it works:
NR==1
we set s
to first line's numberp
is less than (current_number -1) or $1-1
that indicates we have a break in sequence and we need to print the range. prnt
for doing the printing that accepts only one argument that is end delimiter. When prnt
is called from p < $1-1 { ...}
block then we pass RS
or comma as end delimiter and when it gets called from END{...}
block then we pass ORS
or newline as delimiter.p < $1-1 { ...}
we reset s
(start range) to $1
$1
in variable p
.prnt
uses printf
for formatted output. It always prints starting number s
first. Then it checks if p > s
and prints hyphen followed by p
if that is the case.