Home > other >  How to collect a list of random numbers in ranges?
How to collect a list of random numbers in ranges?

Time:02-10

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 conversion for n in ${strvar//,/ }; do.
  • If you want to pipe in a file with each number on its own line, change the for loop into while 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
  • Related