# Explicit range beween values

Is there any clean way of calculating the range between elements of the second column of a text file using some bash magic? (I'm currently doing this using Python).

Input: File 1

``````A   1-5
A   17-19
B   1-5
B   4-6
``````

Expected output: File 2

``````A   1,2,3,4,5,17,18,19
B   1,2,3,4,5,6
``````

EDIT @Anthon: to cumulate elements I'm using something like this (then calculate the ranges using a for loop)

``````d_pos= {}
for row in open('File.txt'):
x, y = [ value.strip() for value in row.split('\t')]
if x in d_pos:
d_pos[x].append(y)
else:
d_pos[x] = [y]
``````
-
I would argue that using Python is the clean way. Are you using a dict of sets to accumulate the elements? – Anthon Jul 16 '14 at 7:50
Please refer to my above edited question for the cumulation code. – dovah Jul 16 '14 at 8:04

Straight bash, since you asked (though note I'm using associative arrays, requiring bash 4.0).

The trick is in the brace sequence expansion expression `{x..y}` which, for integers x any y expands to the inclusive range of values (i.e. [x,y]) as a textual list. We need to throw in an `eval` too, since brace expansion happens before variable expansion.

``````declare -A data seen  # explicit associative arrays
data[\$col]="\${data[\$col]} \$(eval echo {\${range/-/..}})"
done <<DATA
A   1-5
A   17-19
B   1-5
B   4-6
DATA

# dump array
#declare -p data

for ii in \${!data[@]}; do
seen=();  datum=""
# build list of unique values
for dd in \${data[\$ii]}; do
(( \${seen[\$dd]:-0} )) || datum="\$datum \$dd"
let seen[\$dd]++
done

datum=\${datum# }     # drop leading space
datum=\${datum// /,}  # spaces to commas
printf "%-4s %s\n" "\$ii" "\$datum"
done
``````

A variation on the sequence expansion is `a{x..y}b` where "a" is prepended and "b" is appended to each term of the expansion: you can use this to append a "," and alter the datum variable fiddling if desired. Sequence expansion handles increments of 1, or -1 if x > y

You may also need to sort the output: iterating the keys of an associative array has no well-defined order, and you haven't stated if the input ranges are pre-sorted (so I didn't over-complicate the code).

-
oops, not quite right yet, I need to fix overlap... – mr.spuratic Jul 16 '14 at 9:08
updated to output unique values only. – mr.spuratic Jul 16 '14 at 9:30

Your Python code comes close, but e.g. cannot deal with the overlap of 4 and 5 for item B.

The following handles that correctly using a `set()` to prevent the overlap, setdefault to eliminate the explicit test it the key already exists in `d_pos` and `.split()` on the input line to be less dependent on the `\t` character and eliminating the explicit `.strip()`:

``````d_pos= {}
for row in open('File.txt'):
x, y = [ value for value in row.split()]
y1, y2 = map(int, y.split('-'))
d_pos.setdefault(x, set()).update(range (y1, y2+1))
for x in sorted(d_pos):
print '{}\t{}'.format(x, ','.join(map(str, d_pos[x])))
``````
-
Thanks for the tips! ^^ – dovah Jul 16 '14 at 8:52
@Dovah Your welcome, please be aware that this starts to come close to a generic programming question that would be more appropriate on Stack Overflow – Anthon Jul 16 '14 at 8:57

If you can use `perl`:

``````\$ perl -MList::MoreUtils=uniq -anle '
(\$s,\$e) = split "-", \$F[1];
push @{\$h{\$F[0]}}, \$s..\$e;
END {
\$" = ",";
print "\$_   @{[uniq@{\$h{\$_}}]}" for keys %h;
}
' file
A   1,2,3,4,5,17,18,19
B   1,2,3,4,5,6
``````

If you don't want to use `List::MoreUtils`, since when it's not in core, you can do:

``````\$ perl -anle '
(\$s,\$e) = split "-", \$F[1];
push @{\$h{\$F[0]}}, \$s..\$e;
END {
\$" = ",";
for \$k (keys %h) {
%u=();
print "\$k   @{[grep {!\$u{\$_}++} @{\$h{\$k}}]}";
}
}
' file
``````
-