Pythonic way to operate comma-separated list of ranges “1-5,10-25,27-30”

Viewed 217

I'm currently working on an api where they send me a str range in this format: "1-5,10-25,27-30" and i need add or remove number conserving the format.

if they send me "1-5,10-25,27-30" and I remove "15" the result must be "1-5,10-14,16-25,27-30" and if they send me "1-5,10-25,27-30" and i add "26" the result must be "1-5,10-30"

i've been trying converting the entire range into a list of numbers, delete the target and converting it again but it's very slow doing in this way becuase they send 8-digits numbers so iter then it's not the best way

how can i do this? is a library for work with this format?

thanks!

5 Answers

intspan deals with ranges of integers and operations on them

>>> from intspan import intspan
>>> s = "1-5,10-25,27-30"
>>> span = intspan(s)
>>> str(span)
'1-5,10-25,27-30'

>>> span.add(26)
>>> str(span)
'1-5,10-30'

>>> span.discard(15)
>>> str(span)
'1-5,10-14,16-30'

This represents ranges by lists of two-element lists. Two-element lists are used as this allows ranges boundaries to be mutated.

# -*- coding: utf-8 -*-
"""

https://stackoverflow.com/questions/64466231/pythonic-way-to-operate-comma-separated-list-of-ranges-1-5-10-25-27-30

Created on Wed Oct 21 16:29:39 2020

@author: Paddy3118
"""

def to_ranges(txt):
    return [[int(x) for x in r.strip().split('-')]
            for r in txt.strip().split(',')]

def remove_int(rem, 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 ranges
        
def add_int(add, 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 ranges
    return consolidate(ranges)

def consolidate(ranges):
    "Combine overlapping 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 ranges
                    

sent = "1-5,10-25,27-30"
ll = to_ranges(sent)
assert ll == sorted(ll)

Sample calculations

In [68]: ll
Out[68]: [[1, 5], [10, 25], [27, 30]]

In [69]: add_int(26, ll)
Out[69]: [[1, 5], [10, 30]]

In [70]: add_int(9, ll)
Out[70]: [[1, 5], [9, 30]]

In [71]: add_int(7, ll)
Out[71]: [[1, 5], [7, 7], [9, 30]]

In [72]: remove_int(26, ll)
Out[72]: [[1, 5], [7, 7], [9, 25], [27, 30]]

In [73]: remove_int(9, ll)
Out[73]: [[1, 5], [7, 7], [10, 25], [27, 30]]

In [74]: remove_int(7, ll)
Out[74]: [[1, 5], [10, 25], [27, 30]]

In [75]: 

You could use a variation of the sweep line algorithm. Convert your string to a list of tuples [(1,5),(10,25),(27,30)], stepping through the tuples to see if your number is in there. If it is, split the tuple like so: [(1,5),(10,14),(16,25),(27,30)]. Otherwise, do nothing.

Like what @Jace is saying, here is a possible implementation assuming non-overlapping ranges:

# delete a number
rngs = [(10, 25), (1, 5), (27, 30)]
rngs.sort()  # you can even sort them (sorted by first number)
print(rngs)
delete = 15
for i, r in enumerate(rngs):
    if r[0] <= delete <= r[-1]:
        if delete == r[0]:
            rngs[i] = (r[0] + 1, r[1])
        elif delete == r[1]:
            rngs[i] = (r[0], r[1] - 1)
        else:
            rngs[i] = (r[0], delete - 1)
            rngs.insert(i + 1, (delete + 1, r[1]))
        # done
        break
print(rngs)

# add a number
rngs = [(1, 5), (10, 25), (27, 30)]
number = 26
added = False
for i, r in enumerate(rngs):
    if r[0] - 1 == number:  # is the previous number of the range
        rngs[i] = (number, r[1])
        added = True
        break
    elif r[1] + 1 == number:  # is the next number of the range
        # check if next range should also be mixed
        if i < len(rngs) - 1 and rngs[i + 1][0] - 1 == number:
            lhs, rhs = rngs[i + 1]
            rngs[i] = (r[0], rhs)
            rngs.remove((lhs, rhs))
        else:
            rngs[i] = (r[0], number)
        added = True
        break
if not added:
    rngs.append((number, number))
    rngs.sort()
print(rngs)

This works perfectly for removing values from the string, and has no huge performance losses with large numbers:

input_ = '1-5,10-25,27-30'
ranges = input_.split(',')
ranges = [range(int(val.split('-')[0]), int(val.split('-')[1])) for val in ranges]

def remove_value(value, ranges):
    new_ranges = []
    for range_obj in ranges:
        if value in range_obj:
            new_ranges.append(range(range_obj.start, value-1))
            new_ranges.append(range(value+1, range_obj.end))
        else:
            new_ranges.append(range_obj)
    return new_ranges

ranges = remove_value(20, ranges)
output = ','.join(f'{r.start}-{r.stop}' for r in ranges)
Related