I'm working on modernizing Rosetta Code's infrastructure. Starting with communications. Please accept this time-limited open invite to RC's Slack.. --Michael Mol (talk) 20:59, 30 May 2020 (UTC)

Range modifications

From Rosetta Code
Range modifications is a draft programming task. It is not yet considered ready to be promoted as a complete task, for reasons that should be found in its talk page.

The increasing range of all integers from a lower to an upper bound, including each boundary, is shown by the lower-boundary separated from the higher-boundary by a single dash. So 64-1234 is the range of all integers from 64 to 1234, (including both 64 and 1234). 10-10 is the range covering the single integer 10.

A sequence of ranges is shown by successive integer ranges that do not overlap, and which have increasing lower bounds. Each range is separated by a single comma. So 13-14,22-22,100999999-101000001 is a sequence of ranges that contain the integers 13 14 22 100999999 101000000 and 101000001.
Empty ranges are removed. An empty sequence has an empty string representation.

Note: There are NO internal spaces in the sequence format.

Task

Given an initial sequence of ranges write programs to add or remove an integer from the sequence and display the resultant sequence.
Note:

  • The initial sequence may be empty.
  • Adding an int that is already covered should not change the sequence.
  • removing an int that is not in the sequence should not change the sequence.
  • The add and remove operations should print their result in the standard form mentioned.
  • Solutions must work by modifying range boundaries, splitting and joining, as well as creating and deleting ranges.
    Do not use algorithms that create and modify arrays of all the integer values within ranges.

Show the results, (including intermediate results), of performing the following steps

Ex0
   Start with ""
       add 77
       add 79
       add 78
       remove 77
       remove 78
       remove 79
       
Ex1
   Start with "1-3,5-5"
       add 1
       remove 4
       add 7
       add 8
       add 6
       remove 7
Ex2
   Start with "1-5,10-25,27-30"
       add 26
       add 9
       add 7
       remove 26
       remove 9
       remove 7

C++[edit]

#include <algorithm>
#include <iomanip>
#include <iostream>
#include <list>
 
struct range {
range(int lo, int hi) : low(lo), high(hi) {}
int low;
int high;
};
 
std::ostream& operator<<(std::ostream& out, const range& r) {
return out << r.low << '-' << r.high;
}
 
class ranges {
public:
ranges() {}
explicit ranges(std::initializer_list<range> init) : ranges_(init) {}
void add(int n);
void remove(int n);
bool empty() const { return ranges_.empty(); }
private:
friend std::ostream& operator<<(std::ostream& out, const ranges& r);
std::list<range> ranges_;
};
 
void ranges::add(int n) {
for (auto i = ranges_.begin(); i != ranges_.end(); ++i) {
if (n + 1 < i->low) {
ranges_.emplace(i, n, n);
return;
}
if (n > i->high + 1)
continue;
if (n + 1 == i->low)
i->low = n;
else if (n == i->high + 1)
i->high = n;
else
return;
if (i != ranges_.begin()) {
auto prev = std::prev(i);
if (prev->high + 1 == i->low) {
i->low = prev->low;
ranges_.erase(prev);
}
}
auto next = std::next(i);
if (next != ranges_.end() && next->low - 1 == i->high) {
i->high = next->high;
ranges_.erase(next);
}
return;
}
ranges_.emplace_back(n, n);
}
 
void ranges::remove(int n) {
for (auto i = ranges_.begin(); i != ranges_.end(); ++i) {
if (n < i->low)
return;
if (n == i->low) {
if (++i->low > i->high)
ranges_.erase(i);
return;
}
if (n == i->high) {
if (--i->high < i->low)
ranges_.erase(i);
return;
}
if (n > i->low & n < i->high) {
int low = i->low;
i->low = n + 1;
ranges_.emplace(i, low, n - 1);
return;
}
}
}
 
std::ostream& operator<<(std::ostream& out, const ranges& r) {
if (!r.empty()) {
auto i = r.ranges_.begin();
out << *i++;
for (; i != r.ranges_.end(); ++i)
out << ',' << *i;
}
return out;
}
 
void test_add(ranges& r, int n) {
r.add(n);
std::cout << " add " << std::setw(2) << n << " => " << r << '\n';
}
 
void test_remove(ranges& r, int n) {
r.remove(n);
std::cout << " remove " << std::setw(2) << n << " => " << r << '\n';
}
 
void test1() {
ranges r;
std::cout << "Start: \"" << r << "\"\n";
test_add(r, 77);
test_add(r, 79);
test_add(r, 78);
test_remove(r, 77);
test_remove(r, 78);
test_remove(r, 79);
}
 
void test2() {
ranges r{{1,3}, {5,5}};
std::cout << "Start: \"" << r << "\"\n";
test_add(r, 1);
test_remove(r, 4);
test_add(r, 7);
test_add(r, 8);
test_add(r, 6);
test_remove(r, 7);
}
 
void test3() {
ranges r{{1,5}, {10,25}, {27,30}};
std::cout << "Start: \"" << r << "\"\n";
test_add(r, 26);
test_add(r, 9);
test_add(r, 7);
test_remove(r, 26);
test_remove(r, 9);
test_remove(r, 7);
}
 
int main() {
test1();
std::cout << '\n';
test2();
std::cout << '\n';
test3();
return 0;
}
Output:
Start: ""
       add 77 => 77-77
       add 79 => 77-77,79-79
       add 78 => 77-79
    remove 77 => 78-79
    remove 78 => 79-79
    remove 79 => 

Start: "1-3,5-5"
       add  1 => 1-3,5-5
    remove  4 => 1-3,5-5
       add  7 => 1-3,5-5,7-7
       add  8 => 1-3,5-5,7-8
       add  6 => 1-3,5-8
    remove  7 => 1-3,5-6,8-8

Start: "1-5,10-25,27-30"
       add 26 => 1-5,10-30
       add  9 => 1-5,9-30
       add  7 => 1-5,7-7,9-30
    remove 26 => 1-5,7-7,9-25,27-30
    remove  9 => 1-5,7-7,10-25,27-30
    remove  7 => 1-5,10-25,27-30

Go[edit]

Translation of: Wren
package main
 
import (
"fmt"
"strings"
)
 
type rng struct{ from, to int }
 
type fn func(rngs *[]rng, n int)
 
func (r rng) String() string { return fmt.Sprintf("%d-%d", r.from, r.to) }
 
func rangesAdd(rngs []rng, n int) []rng {
if len(rngs) == 0 {
rngs = append(rngs, rng{n, n})
return rngs
}
for i, r := range rngs {
if n < r.from-1 {
rngs = append(rngs, rng{})
copy(rngs[i+1:], rngs[i:])
rngs[i] = rng{n, n}
return rngs
} else if n == r.from-1 {
rngs[i] = rng{n, r.to}
return rngs
} else if n <= r.to {
return rngs
} else if n == r.to+1 {
rngs[i] = rng{r.from, n}
if i < len(rngs)-1 && (n == rngs[i+1].from || n+1 == rngs[i+1].from) {
rngs[i] = rng{r.from, rngs[i+1].to}
copy(rngs[i+1:], rngs[i+2:])
rngs[len(rngs)-1] = rng{}
rngs = rngs[:len(rngs)-1]
}
return rngs
} else if i == len(rngs)-1 {
rngs = append(rngs, rng{n, n})
return rngs
}
}
return rngs
}
 
func rangesRemove(rngs []rng, n int) []rng {
if len(rngs) == 0 {
return rngs
}
for i, r := range rngs {
if n <= r.from-1 {
return rngs
} else if n == r.from && n == r.to {
copy(rngs[i:], rngs[i+1:])
rngs[len(rngs)-1] = rng{}
rngs = rngs[:len(rngs)-1]
return rngs
} else if n == r.from {
rngs[i] = rng{n + 1, r.to}
return rngs
} else if n < r.to {
rngs[i] = rng{r.from, n - 1}
rngs = append(rngs, rng{})
copy(rngs[i+2:], rngs[i+1:])
rngs[i+1] = rng{n + 1, r.to}
return rngs
} else if n == r.to {
rngs[i] = rng{r.from, n - 1}
return rngs
}
}
return rngs
}
 
func standard(rngs []rng) string {
if len(rngs) == 0 {
return ""
}
var sb strings.Builder
for _, r := range rngs {
sb.WriteString(fmt.Sprintf("%s,", r))
}
s := sb.String()
return s[:len(s)-1]
}
 
func main() {
const add = 0
const remove = 1
fns := []fn{
func(prngs *[]rng, n int) {
*prngs = rangesAdd(*prngs, n)
fmt.Printf(" add %2d => %s\n", n, standard(*prngs))
},
func(prngs *[]rng, n int) {
*prngs = rangesRemove(*prngs, n)
fmt.Printf(" remove %2d => %s\n", n, standard(*prngs))
},
}
 
var rngs []rng
ops := [][2]int{{add, 77}, {add, 79}, {add, 78}, {remove, 77}, {remove, 78}, {remove, 79}}
fmt.Printf("Start: %q\n", standard(rngs))
for _, op := range ops {
fns[op[0]](&rngs, op[1])
}
 
rngs = []rng{{1, 3}, {5, 5}}
ops = [][2]int{{add, 1}, {remove, 4}, {add, 7}, {add, 8}, {add, 6}, {remove, 7}}
fmt.Printf("\nStart: %q\n", standard(rngs))
for _, op := range ops {
fns[op[0]](&rngs, op[1])
}
 
rngs = []rng{{1, 5}, {10, 25}, {27, 30}}
ops = [][2]int{{add, 26}, {add, 9}, {add, 7}, {remove, 26}, {remove, 9}, {remove, 7}}
fmt.Printf("\nStart: %q\n", standard(rngs))
for _, op := range ops {
fns[op[0]](&rngs, op[1])
}
}
Output:
Start: ""
       add 77 => 77-77
       add 79 => 77-77,79-79
       add 78 => 77-79
    remove 77 => 78-79
    remove 78 => 79-79
    remove 79 => 

Start: "1-3,5-5"
       add  1 => 1-3,5-5
    remove  4 => 1-3,5-5
       add  7 => 1-3,5-5,7-7
       add  8 => 1-3,5-5,7-8
       add  6 => 1-3,5-8
    remove  7 => 1-3,5-6,8-8

Start: "1-5,10-25,27-30"
       add 26 => 1-5,10-30
       add  9 => 1-5,9-30
       add  7 => 1-5,7-7,9-30
    remove 26 => 1-5,7-7,9-25,27-30
    remove  9 => 1-5,7-7,10-25,27-30
    remove  7 => 1-5,10-25,27-30

Julia[edit]

Julia has iterator classes called a type of Range, such as integer UnitRanges, that are like the "10-10" of the task but are stated as 10:10, with a colon not a minus sign. This implementation uses Julia's UnitRange class internally.

import Base.parse, Base.print, Base.reduce
 
const RangeSequence = Array{UnitRange, 1}
 
function combine!(seq::RangeSequence, r::UnitRange)
isempty(seq) && return push!(seq, r)
if r.start < seq[end].start
reduce!(push!(seq, r))
elseif r.stop > seq[end].stop
if r.start <= seq[end].stop + 1
seq[end] = seq[end].start:r.stop
else
push!(seq, r)
end
end
return seq
end
 
function parse(::Type{RangeSequence}, s)
seq = UnitRange[]
entries = sort!(split(s, r"\s*,\s*"))
for e in entries
startstop = split(e, r"\:|\-")
if length(startstop) == 2
start, stop = tryparse(Int, startstop[1]), tryparse(Int, startstop[2])
start, stop = start <= stop ? (start, stop) : (stop, start)
start != nothing && stop != nothing && push!(seq, start:stop)
elseif (n = tryparse(Int, startstop[1])) != nothing
push!(seq, n:n)
end
end
return reduce!(seq)
end
 
reduce!(a::RangeSequence) = (s = sort(a); empty!(a); for r in s combine!(a, r) end; a)
reduce(a::RangeSequence) = (seq = UnitRange[]; for r in sort(a) combine!(seq, r) end; seq)
 
insertinteger!(seq::RangeSequence, n::Integer) = begin push!(seq, n:n); reduce!(seq) end
 
insertintegerprint!(seq, n) = println(" added $n => ", insertinteger!(seq, n))
removeintegerprint!(seq, n) = println(" removed $n => ", removeinteger!(seq, n))
 
function removeinteger!(seq::RangeSequence, n::Integer)
for (pos, r) in enumerate(seq)
if n in r
start, stop = r.start, r.stop
if start == stop == n
deleteat!(seq, pos:pos)
elseif stop == n
seq[pos] = start:stop-1
elseif start == n
seq[pos] = start+1:stop
elseif start < n < stop
seq[pos] = n+1:stop
insert!(seq, pos, start:n-1)
end
break
end
end
return seq
end
 
function print(io::IO, seq::RangeSequence)
return print(io, "\"" * join(map(r -> "$(r.start)-$(r.stop)", reduce(seq)), ",") * "\"")
end
 
const seq = parse(RangeSequence, "")
println("Start: $seq")
insertintegerprint!(seq, 77)
insertintegerprint!(seq, 79)
insertintegerprint!(seq, 78)
removeintegerprint!(seq, 77)
removeintegerprint!(seq, 78)
removeintegerprint!(seq, 79)
 
const seq2 = parse(RangeSequence, "1-3, 5-5")
println("Start: $seq2")
insertintegerprint!(seq2, 1)
removeintegerprint!(seq2, 4)
insertintegerprint!(seq2, 7)
insertintegerprint!(seq2, 8)
insertintegerprint!(seq2, 6)
removeintegerprint!(seq2, 7)
 
const seq3 = parse(RangeSequence, "1-5, 10-25, 27-30")
println("Start: $seq3")
insertintegerprint!(seq3, 26)
insertintegerprint!(seq3, 9)
insertintegerprint!(seq3, 7)
removeintegerprint!(seq3, 26)
removeintegerprint!(seq3, 9)
removeintegerprint!(seq3, 7)
 
println("Parse \"10-25, 1-5, 27-30\" => ", parse(RangeSequence, "10-25, 1-5, 27-30"))
println("Parse \"3-1,15-5,25-10,30-27\" => ", parse(RangeSequence, "3-1,15-5,25-10,30-27"))
 
Output:
Start: ""
    added 77 => 77-77
    added 79 => 77-77,79-79
    added 78 => 77-79
    removed 77 => 78-79
    removed 78 => 79-79
    removed 79 =>

Start: 1-3,5-5
    added 1 => 1-3,5-5
    removed 4 => 1-3,5-5
    added 7 => 1-3,5-5,7-7
    added 8 => 1-3,5-5,7-8
    added 6 => 1-3,5-8
    removed 7 => 1-3,5-6,8-8

Start: 1-5,10-25,27-30
    added 26 => 1-5,10-30
    added 9 => 1-5,9-30
    added 7 => 1-5,7-7,9-30
    removed 26 => 1-5,7-7,9-25,27-30
    removed 9 => 1-5,7-7,10-25,27-30
    removed 7 => 1-5,10-25,27-30

Parse "10-25, 1-5, 27-30" => 1-5,10-25,27-30
Parse "3-1,15-5,25-10,30-27" => 1-3,5-25,27-30

Phix[edit]

requires("0.8.2")  -- (uses latest apply() functionality)
 
function add(sequence ranges, atom v)
--
-- eg {} + 9 --> {{9,9}} -- [1]
-- {{3,5}} + 9 --> {{3,5},{9,9}} -- [1]
-- {{3,5}} + 4 --> as-is -- [2]
-- {{3,5}} + 2 --> {{2,5}} -- [3]
-- {{3,5},{7,9}} + 6 --> {{3,9}} -- [4]
-- {{3,5},{8,9}} + 6 --> {{3,6},{8,9}} -- [5]
-- {{3,5},{8,9}} + 7 --> {{3,5},{7,9}} -- [3]
-- {{3,5}} + 6 --> {{3,6}} -- [6]
-- {{3,5}} + 1 --> {{1,1},{3,5}} -- [7]
--
integer l = length(ranges)
for i=1 to l+1 do
if i>l then ranges &= {{v,v}} exit -- [1]
end if
atom nl,{lo,hi} = ranges[i]
if v>=lo and v<=hi then exit -- [2]
elsif v=lo-1 then ranges[i][1] = v exit -- [3]
elsif v=hi+1 then
if i<l then
{nl,hi} = ranges[i+1]
if nl=v+1 then
ranges[i..i+1] = {{lo,hi}} exit -- [4]
else
ranges[i][2] = v exit -- [5]
end if
else
ranges[i][2] = v exit -- [6]
end if
elsif v<lo then ranges[i..i-1] = {{v,v}} exit -- [7]
end if
end for
return ranges
end function
 
function del(sequence ranges, atom v)
--
-- eg {{1,2}} - 1 --> {{2,2}} -- [1]
-- {{2,2}} - 2 --> {} -- [2]
-- {{1,2}} - 2 --> {{1,1}} -- [3]
-- {{1,3}} - 2 --> {{1,1},{3,3}} -- [4]
-- {{2,3}} - 1 --> as-is -- [5]
--
for i=1 to length(ranges) do
atom {lo,hi} = ranges[i]
if v>=lo and v<=hi then
if v=lo then
if v<hi then ranges[i][1] = lo+1 -- [1]
else ranges[i..i] = {} end if -- [2]
elsif v==hi then ranges[i][2] = hi-1 -- [3]
else ranges[i..i] = {{lo,v-1},{v+1,hi}} -- [4]
end if exit
elsif v<hi then exit end if -- [5]
end for
return ranges
end function
 
constant tests = split("""
Start with ""
add 77
add 79
add 78
remove 77
remove 78
remove 79
Start with "1-3,5-5"
add 1
remove 4
add 7
add 8
add 6
remove 7
Start with "1-5,10-25,27-30"
add 26
add 9
add 7
remove 26
remove 9
remove 7
Start with "13-14,22-22,100000999999-100001000000,100001000003-999999999999"
remove 22
remove 100000999999
remove 100001000000
add 100001000001
add 100001000002
remove 100001000002
remove 100001000001
""","\n",no_empty:=true)
 
string range = ""
sequence ranges -- range internal form
for i=1 to length(tests) do
string ti = tests[i]
if match("Start with",ti) then
{{range}} = scanf(trim(ti),"Start with \"%s\"")
ranges = split(range,",")
ranges = vslice(apply(true,scanf,{ranges,{"%d-%d"}}),1)
printf(1,"\n Start with: \"%s\"\n",{range})
-- ^^ ^^
else
{{string op, atom v}} = scanf(trim(ti),"%s %d")
integer rid = routine_id(substitute(op,"remove","del"))
ranges = rid(ranges,v)
range = join(apply(true,sprintf,{{"%d-%d"},ranges}),",")
printf(1," %9s %-12d -> \"%s\"\n",{op,v,range})
-- ^^ ^^
end if
end for
Output:

(Note that all double-quotes in the output were deliberately added in the last two printf() statements, mainly to prove there are no unnecessary spaces, etc, and are (see ^^) obviously trivial to remove.)

               Start with: ""
       add 77           -> "77-77"
       add 79           -> "77-77,79-79"
       add 78           -> "77-79"
    remove 77           -> "78-79"
    remove 78           -> "79-79"
    remove 79           -> ""

               Start with: "1-3,5-5"
       add 1            -> "1-3,5-5"
    remove 4            -> "1-3,5-5"
       add 7            -> "1-3,5-5,7-7"
       add 8            -> "1-3,5-5,7-8"
       add 6            -> "1-3,5-8"
    remove 7            -> "1-3,5-6,8-8"

               Start with: "1-5,10-25,27-30"
       add 26           -> "1-5,10-30"
       add 9            -> "1-5,9-30"
       add 7            -> "1-5,7-7,9-30"
    remove 26           -> "1-5,7-7,9-25,27-30"
    remove 9            -> "1-5,7-7,10-25,27-30"
    remove 7            -> "1-5,10-25,27-30"

               Start with: "13-14,22-22,100000999999-100001000000,100001000003-999999999999"
    remove 22           -> "13-14,100000999999-100001000000,100001000003-999999999999"
    remove 100000999999 -> "13-14,100001000000-100001000000,100001000003-999999999999"
    remove 100001000000 -> "13-14,100001000003-999999999999"
       add 100001000001 -> "13-14,100001000001-100001000001,100001000003-999999999999"
       add 100001000002 -> "13-14,100001000001-999999999999"
    remove 100001000002 -> "13-14,100001000001-100001000001,100001000003-999999999999"
    remove 100001000001 -> "13-14,100001000003-999999999999"

Python[edit]

class Sequence():
 
def __init__(self, sequence_string):
self.ranges = self.to_ranges(sequence_string)
assert self.ranges == sorted(self.ranges), "Sequence order error"
 
def to_ranges(self, txt):
return [[int(x) for x in r.strip().split('-')]
for r in txt.strip().split(',') if r]
 
def remove(self, rem):
ranges = self.ranges
for i, r in enumerate(ranges):
if r[0] <= rem <= r[1]:
if r[0] == rem: # range min
if r[1] > rem:
r[0] += 1
else:
del ranges[i]
elif r[1] == rem: # range max
if r[0] < rem:
r[1] -= 1
else:
del ranges[i]
else: # inside, range extremes.
r[1], splitrange = rem - 1, [rem + 1, r[1]]
ranges.insert(i + 1, splitrange)
break
if r[0] > rem: # Not in sorted list
break
return self
 
def add(self, add):
ranges = self.ranges
for i, r in enumerate(ranges):
if r[0] <= add <= r[1]: # already included
break
elif r[0] - 1 == add: # rough extend to here
r[0] = add
break
elif r[1] + 1 == add: # rough extend to here
r[1] = add
break
elif r[0] > add: # rough insert here
ranges.insert(i, [add, add])
break
else:
ranges.append([add, add])
return self
return self.consolidate()
 
def consolidate(self):
"Combine overlapping ranges"
ranges = self.ranges
for this, that in zip(ranges, ranges[1:]):
if this[1] + 1 >= that[0]: # Ranges interract
if this[1] >= that[1]: # this covers that
this[:], that[:] = [], this
else: # that extends this
this[:], that[:] = [], [this[0], that[1]]
ranges[:] = [r for r in ranges if r]
return self
def __repr__(self):
rr = self.ranges
return ",".join(f"{r[0]}-{r[1]}" for r in rr)
 
def demo(opp_txt):
by_line = opp_txt.strip().split('\n')
start = by_line.pop(0)
ex = Sequence(start.strip().split()[-1][1:-1]) # Sequence("1-3,5-5")
lines = [line.strip().split() for line in by_line]
opps = [((ex.add if word[0] == "add" else ex.remove), int(word[1]))
for word in lines]
print(f"Start: \"{ex}\"")
for op, val in opps:
print(f" {op.__name__:>6} {val:2} => {op(val)}")
print()
 
if __name__ == '__main__':
demo("""
Start with ""
add 77
add 79
add 78
remove 77
remove 78
remove 79
"""
)
demo("""
Start with "1-3,5-5"
add 1
remove 4
add 7
add 8
add 6
remove 7
"""
)
demo("""
Start with "1-5,10-25,27-30"
add 26
add 9
add 7
remove 26
remove 9
remove 7
"""
)
 
Output:
Start: ""
       add 77 => 77-77
       add 79 => 77-77,79-79
       add 78 => 77-79
    remove 77 => 78-79
    remove 78 => 79-79
    remove 79 => 

Start: "1-3,5-5"
       add  1 => 1-3,5-5
    remove  4 => 1-3,5-5
       add  7 => 1-3,5-5,7-7
       add  8 => 1-3,5-5,7-8
       add  6 => 1-3,5-8
    remove  7 => 1-3,5-6,8-8

Start: "1-5,10-25,27-30"
       add 26 => 1-5,10-30
       add  9 => 1-5,9-30
       add  7 => 1-5,7-7,9-30
    remove 26 => 1-5,7-7,9-25,27-30
    remove  9 => 1-5,7-7,10-25,27-30
    remove  7 => 1-5,10-25,27-30

Raku[edit]

Works with: Rakudo version 2020.09

Quite a bit of this is just transforming syntax back and forth. Raku already has Ranges and Sequences as first class objects, though the syntax is different.

Demonstrate the task required examples: adding and removing integers, as well as an example adding and removing ranges to / from the sequence, in both native Raku syntax and the "Stringy" syntax required by the task.

Demo with some extremely large values / ranges. Capable of working with infinite ranges by default.

Won't handle negative numbers as written, mostly due the need to work around the syntax requirements for output.

my @seq;
 
-> $op, $string { printf "%20s -> %s\n", $op, $string } for
'Start', to-string( @seq = canonicalize "" ),
'add 77', to-string( @seq .= &add(77) ),
'add 79', to-string( @seq .= &add(79) ),
'add 78', to-string( @seq .= &add(78) ),
'remove 77', to-string( @seq .= &remove(77) ),
'remove 78', to-string( @seq .= &remove(78) ),
'remove 79', to-string( @seq .= &remove(79) );
 
say '';
-> $op, $string { printf "%20s -> %s\n", $op, $string } for
'Start', to-string( @seq = canonicalize "1-3,5-5" ),
'add 1', to-string( @seq .= &add(1) ),
'remove 4', to-string( @seq .= &remove(4) ),
'add 7', to-string( @seq .= &add(7) ),
'add 8', to-string( @seq .= &add(8) ),
'add 6', to-string( @seq .= &add(6) ),
'remove 7', to-string( @seq .= &remove(7) );
 
say '';
-> $op, $string { printf "%20s -> %s\n", $op, $string } for
'Start', to-string( @seq = canonicalize "1-5,10-25,27-30" ),
'add 26', to-string( @seq .= &add(26) ),
'add 9', to-string( @seq .= &add(9) ),
'add 7', to-string( @seq .= &add(7) ),
'remove 26', to-string( @seq .= &remove(26) ),
'remove 9', to-string( @seq .= &remove(9) ),
'remove 7', to-string( @seq .= &remove(7) );
 
say '';
-> $op, $string { printf "%30s -> %s\n", $op, $string } for
'Start', to-string( @seq = canonicalize "6-57,160-251,2700-7000000" ),
'add "2502-2698"', to-string( @seq .= &add("2502-2698") ),
'add 41..69', to-string( @seq .= &add(41..69) ),
'remove 17..30', to-string( @seq .= &remove(17..30) ),
'remove 4391..6527', to-string( @seq .= &remove("4391-6527") ),
'add 2699', to-string( @seq .= &add(2699) ),
'add 76', to-string( @seq .= &add(76) ),
'add 78', to-string( @seq .= &add(78) ),
'remove "70-165"', to-string( @seq .= &remove("70-165") ),
'remove 16..31', to-string( @seq .= &remove(16..31) ),
'add 1.417e16 .. 3.2e21', to-string( @seq .= &add(1.417e16.Int .. 3.2e21.Int) ),
'remove "4001-Inf"', to-string( @seq .= &remove("4001-Inf") );
 
 
sub canonicalize (Str $ranges) { sort consolidate |sort parse-range $ranges }
 
sub parse-range (Str $_) { .comb(/\d+|'Inf'/).map: { +$^α .. +$^ω } }
 
sub to-string (@ranges) { qq|"{ @ranges».minmax».join('-').join(',') }"| }
 
multi add (@ranges, Int $i) { samewith @ranges, $i .. $i }
multi add (@ranges, Str $s) { samewith @ranges, |parse-range($s) }
multi add (@ranges, Range $r) { @ranges > 0 ?? (sort consolidate |sort |@ranges, $r) !! $r }
 
multi remove (@ranges, Int $i) { samewith @ranges, $i .. $i }
multi remove (@ranges, Str $s) { samewith @ranges, |parse-range($s) }
multi remove (@ranges, Range $r) {
gather for |@ranges -> $this {
if $r.min <= $this.min {
if $r.max >= $this.min and $r.max < $this.max {
take $r.max + 1 .. $this.max
}
elsif $r.max < $this.min {
take $this
}
}
else {
if $r.max >= $this.max and $r.min <= $this.max {
take $this.min .. $r.min - 1
}
elsif $r.max < $this.max and $r.min > $this.min {
take $this.min .. $r.min - 1;
take $r.max + 1 .. $this.max
}
else {
take $this
}
}
}
}
 
multi consolidate() { () }
 
multi consolidate($this is copy, **@those) {
sub infix:<> (Range $a, Range $b) { Range.new($a.min,max($a.max,$b.max)) }
 
sub infix:<> (Range $a, Range $b) { so $a.max >= $b.min }
 
my @ranges = sort gather {
for consolidate |@those -> $that {
next unless $that;
if $this$that { $this= $that }
else { take $that }
}
take $this;
}
for reverse ^(@ranges - 1) {
if @ranges[$_].max == @ranges[$_ + 1].min - 1 {
@ranges[$_] = @ranges[$_].min .. @ranges[$_ + 1].max;
@ranges[$_ + 1]:delete
}
}
@ranges
}
Output:
               Start -> ""
              add 77 -> "77-77"
              add 79 -> "77-77,79-79"
              add 78 -> "77-79"
           remove 77 -> "78-79"
           remove 78 -> "79-79"
           remove 79 -> ""

               Start -> "1-3,5-5"
               add 1 -> "1-3,5-5"
            remove 4 -> "1-3,5-5"
               add 7 -> "1-3,5-5,7-7"
               add 8 -> "1-3,5-5,7-8"
               add 6 -> "1-3,5-8"
            remove 7 -> "1-3,5-6,8-8"

               Start -> "1-5,10-25,27-30"
              add 26 -> "1-5,10-30"
               add 9 -> "1-5,9-30"
               add 7 -> "1-5,7-7,9-30"
           remove 26 -> "1-5,7-7,9-25,27-30"
            remove 9 -> "1-5,7-7,10-25,27-30"
            remove 7 -> "1-5,10-25,27-30"

                         Start -> "6-57,160-251,2700-7000000"
               add "2502-2698" -> "6-57,160-251,2502-2698,2700-7000000"
                    add 41..69 -> "6-69,160-251,2502-2698,2700-7000000"
                 remove 17..30 -> "6-16,31-69,160-251,2502-2698,2700-7000000"
             remove 4391..6527 -> "6-16,31-69,160-251,2502-2698,2700-4390,6528-7000000"
                      add 2699 -> "6-16,31-69,160-251,2502-4390,6528-7000000"
                        add 76 -> "6-16,31-69,76-76,160-251,2502-4390,6528-7000000"
                        add 78 -> "6-16,31-69,76-76,78-78,160-251,2502-4390,6528-7000000"
               remove "70-165" -> "6-16,31-69,166-251,2502-4390,6528-7000000"
                 remove 16..31 -> "6-15,32-69,166-251,2502-4390,6528-7000000"
        add 1.417e16 .. 3.2e21 -> "6-15,32-69,166-251,2502-4390,6528-7000000,14170000000000000-3200000000000000000000"
             remove "4001-Inf" -> "6-15,32-69,166-251,2502-4000"

Wren[edit]

Library: Wren-fmt
import "/fmt" for Fmt
 
var rangesAdd = Fn.new { |ranges, n|
if (ranges.count == 0) {
ranges.add(n..n)
return
}
for (i in 0...ranges.count) {
var r = ranges[i]
if (n < r.from - 1) {
ranges.insert(i, n..n)
return
} else if (n == r.from-1) {
ranges[i] = n..r.to
return
} else if (n <= r.to) {
return
} else if (n == r.to+1) {
ranges[i] = r.from..n
if (i < ranges.count - 1 && (n == ranges[i+1].from || n + 1 == ranges[i+1].from)) {
ranges[i] = r.from..ranges[i+1].to
ranges.removeAt(i+1)
}
return
} else if (i == ranges.count - 1) {
ranges.add(n..n)
return
}
}
}
 
var rangesRemove = Fn.new { |ranges, n|
if (ranges.count == 0) return
for (i in 0...ranges.count) {
var r = ranges[i]
if (n <= r.from - 1) {
return
} else if (n == r.from && n == r.to) {
ranges.removeAt(i)
return
} else if (n == r.from) {
ranges[i] = n+1..r.to
return
} else if (n < r.to) {
ranges[i] = r.from..n-1
ranges.insert(i+1, n+1..r.to)
return
} else if (n == r.to) {
ranges[i] = r.from..n-1
return
}
}
}
 
var standard = Fn.new { |ranges| Fmt.v("s", 0, ranges, 0, ",", "").replace("..", "-") }
 
var add = 0
var remove = 1
var fns = [
Fn.new { |ranges, n|
rangesAdd.call(ranges, n)
Fmt.print(" add $2d => $n", n, standard.call(ranges))
},
Fn.new { |ranges, n|
rangesRemove.call(ranges, n)
Fmt.print(" remove $2d => $n", n, standard.call(ranges))
}
]
 
var ranges = []
var ops = [ [add, 77], [add, 79], [add, 78], [remove, 77], [remove, 78], [remove, 79] ]
Fmt.print("Start: $q", standard.call(ranges))
for (op in ops) fns[op[0]].call(ranges, op[1])
 
ranges = [1..3, 5..5]
ops = [ [add, 1], [remove, 4], [add, 7], [add, 8], [add, 6], [remove, 7] ]
Fmt.print("\nStart: $q", standard.call(ranges))
for (op in ops) fns[op[0]].call(ranges, op[1])
 
ranges = [1..5, 10..25, 27..30]
ops = [ [add, 26], [add, 9], [add, 7], [remove, 26], [remove, 9], [remove, 7] ]
Fmt.print("\nStart: $q", standard.call(ranges))
for (op in ops) fns[op[0]].call(ranges, op[1])
 
Output:
Start: ""
       add 77 => 77-77
       add 79 => 77-77,79-79
       add 78 => 77-79
    remove 77 => 78-79
    remove 78 => 79-79
    remove 79 =>  

Start: "1-3,5-5"
       add  1 => 1-3,5-5
    remove  4 => 1-3,5-5
       add  7 => 1-3,5-5,7-7
       add  8 => 1-3,5-5,7-8
       add  6 => 1-3,5-8
    remove  7 => 1-3,5-6,8-8

Start: "1-5,10-25,27-30"
       add 26 => 1-5,10-30
       add  9 => 1-5,9-30
       add  7 => 1-5,7-7,9-30
    remove 26 => 1-5,7-7,9-25,27-30
    remove  9 => 1-5,7-7,10-25,27-30
    remove  7 => 1-5,10-25,27-30