I have a list of numbers not in order 8,9,10,25,47,48,49,50,51,52,53,54,55,102,107,111,201,202,203,204,205
, and it is a very important to me to make this list of numbers in ranges, like this : 8-10,25,47-55,102,107,111,201-205
,
I'm familiar with shell scripting, but i couldn't find any solution.
CodePudding user response:
Find the minimum and maximum in the list. Store all the numbers as keys in an associative array. Then, iterate all the numbers from the minimum to the maximum, if the number - 1 is not in the associative array, a new range starts. If it's there, the range continues, if the number 1 is not in there, the range ends here.
#! /bin/bash
list2ranges () {
local -a arr
IFS=, arr=($1)
local min=${arr[0]}
local max=${arr[0]}
local -A seen
local e
for e in "${arr[@]}" ; do
(( e < min )) && min=e
(( e > max )) && max=e
seen[$e]=1
done
local -a out
local i
for ((i=min; i<=max; i)) ; do
if [[ ${seen[$i]} ]] ; then
if [[ ${seen[$((i-1))]} ]] ; then
if [[ ${out[${#out[@]}-1]} != *- ]] ; then
out[${#out[@]}-1] =-
fi
if [[ ! ${seen[$((i 1))]} ]] ; then
out[${#out[@]}-1] =$i
fi
else
out =($i)
fi
fi
done
IFS=, echo "${out[*]}"
}
list=8,9,10,25,47,48,49,50,51,52,53,54,55,102,107,111,201,202,203,204,205
diff <(list2ranges "$list") <(echo 8-10,25,47-55,102,107,111,201-205)
If the difference between the minimum and the maximum is large, this will take a long time. Consider using Perl with Set::IntSpan instead.
list=8,9,10,25,47,48,49,50,51,52,53,54,55,102,107,111,201,202,203,204,205
perl -MSet::IntSpan -wE 'say Set::IntSpan->new(shift)' -- "$list"
CodePudding user response:
As only shell
and bash
are tagged, here's a bash solution based on Parameter Expansion:
This iterates over the list of numbers, and with each new number adjusts the final part of the target string accordingly.
list=
for n in 8 9 10 25 47 48 49 50 51 52 53 54 55 102 107 111 201 202 203 204 205; do
a=${list##*,} b=${a#*-} c=${a%-*} list=${list::-${#a}}
if ((n > b 1)); then list =$a,$n
elif ((n > c)); then list =$c-$n
else list =$n; fi
done
echo ${list:1}
8-10,25,47-55,102,107,111,201-205
- If your numbers are stored in an array, loop over that instead, as in
for n in ${arrayvar[@]}; do
. - If your input is a string of comma-separated numbers (similar to the output), either turn it into an array using
arrayvar=(${strvar//,/ })
, or iterate directly on the conversionfor n in ${strvar//,/ }; do
. - If you want to pipe in a file with each number on its own line, change the
for
loop intowhile read -r n; do
.
- If you need to sort the file's contents first,
sort -n
is the easiest way. - If you wish to sort it while being an array, either use
mapfile -t arrayvar < <(printf '%s\n' "${arrayvar[@]}" | sort -n)
, or follow the solution from here:IFS=$'\n' arrayvar=($(sort -n <<< "${arrayvar[*]}")); unset IFS
.
CodePudding user response:
Alright, this is a fun little issue, even though you lack showing your Minimal, Complete, and Verifiable Example (MCVE), it's worth adding another approach.
You can make this task easier by keeping a couple of flag (or state) variables. Simple variables that are either 1
or 0
(true or false) that keep track of whether you have written your first value yet, and whether you are in a sequence of numbers or between ranges of sequential numbers. When you loop over the values, if you keep the last value from the prior iteration, the task become fairly easy.
For example you can do:
#!/bin/bash
list=8,9,10,25,47,48,49,50,51,52,53,54,55,102,107,111,201,202,203,204,205
first=0 ## first value written flag
inseq=0 ## in sequence flag
while read -r n; do ## read each of the sorted values
## if first not written, write, set last to current and set first flag
[ "$first" -eq 0 ] && { printf "%s" "$n"; last="$n"; first=1; continue; }
## if current minus last is 1 -- in sequence, set inseq flag
if ((n - last == 1)); then
inseq=1
elif [ "$inseq" -eq 1 ]; then ## if not sequential
printf -- "-%s,%s" "$last" "$n" ## finish with last, output next
inseq=0 ## unset inseq flag
else
printf ",%s" "$n" ## individual number, print
fi
last="$n" ## set last to current
## process substitution sorts list with printf trick
done < <(IFS=$','; printf "%s\n" $list | sort -n)
## output final term either ending sequence or as individual number
if [ "$inseq" -gt 0 ]; then
printf -- "-%s" "$last"
else
printf ",%s" "$last"
fi
echo "" ## tidy up with a newline
You state your numbers are not in order, so the while()
loop is fed by a process substitution that sorts the values with sort -n
before the values are read. (the unquoted $list
in the process substitution is intentional)
Example Use/Output
$ bash ranges.sh
8-10,25,47-55,102,107,111,201-205