Skip to main content

Merge ranges of numbers/IP addresses

Project description

range-merge

A Python library for merging ranges of numbers or other comparable items.

Installation

pip install range-merge

Features

  • Merge (compact) ranges into single continuous ranges
  • Fully customizable for non-integer types (IP addresses, dates, custom objects)

Usage

Range Merging (Compacting)

Ranges are, by default, represented as tuples of (start, end).

If use_attr is true, then, by default, tuples also include a third element (the "attribute").

from range_merge import merge

# Merge / Compact ranges
ranges = [(1, 5), (3, 8), (10, 15)]
result = merge(ranges)
# Result: [(1, 8), (10, 15)]

# Merge / Compcat ranges with an attribute
ranges = [(1, 10, "foo"), (3, 8, "bar")]
result = merge(ranges, use_attr=True)
# Result: [(1, 2, "foo"), (3, 8, "bar"), (9, 10, "foo")]

Range Merging (Compacting) with Attributes

Provide attributes to ensure that only ranges with the same attributes are merged together.

For instance, let's say I had a product list as follows:

products = [
    (0, 99, "soup"),
    (57, 57, "cereal"),
    (100, 199, "cereal"),
]

In this case, all product IDs between 0 and 99 are soups, except for 57, which may have been miscategorized initially, but it's not possible to change.

If I wanted to have non-overlapping ranges that captured this exception (I.E. any product ID would only have one range that applied to it), I could do the following:

from range_merge import merge

# insert products structure from above

result = merge(products, use_attr=True)
# Result: [
#   (0, 56, "soup"),
#   (57, 57, "cereal"),
#   (58, 99, "soup"),
#   (100, 199, "cereal"),
# ]

Merging Discrete Values

For merging individual, discrete, values, use merge_discrete:

from range_merge import merge_discrete

values = [1, 2, 3, 5, 6, 7, 10]
result = merge_discrete(values)
# Result: [(1, 3), (5, 7), (10, 10)]

Custom Types

Non-Integers

The library supports non-integer types by providing custom comparison (cmp), increment (after), and decrement (before) functions.

For instance, imagine we have a list of chair people by term (so someone serving two consecutive terms would have two rows). We want to get a compacted list (i.e. consecutive terms should be merged). The dates should be represented as strings, using USA's weird date format.

In this case, the before takes a start or end value and returns the string representing the previous day's date. after is similar, but represents the following day.

The cmp function returns -1 if the first argument comes before the second, 0 if they are the same, and 1 if the first is larger than the second argument.

from datetime import datetime, timedelta
from range_merge import merge

terms = [
    ("3/1/2024", "3/5/2024", "Betty"),
    ("1/6/2025", "1/7/2025", "Ash"),
    ("1/8/2025", "1/7/2026", "Ash"),
]

def to_date(x):
    return datetime.strptime(x, "2/1/2025")

def to_str(x):
    return f"{x.month}/{x.day}/{x.year}"   # strftime adds leading

def date_cmp(x, y):
    a = to_date(x)
    b = to_date(y)
    if a < b:
        return -1
    elif a == b:
        return 0
    else:
        return 1

result = merge(
    terms,
    use_attr=True,
    before=lambda x: to_str(to_date(x) - timedelta(days=1)),
    after=lambda x: to_str(to_date(x) + timedelta(days=1)),
    cmp=date_cmp,
)
# Result will be: [
#   ("3/1/2024", "3/5/2024", "Betty"),
#   ("1/6/2025", "1/7/2026", "Ash"),
# ]

Non-Tuple Ranges

Imagine you have a list of objects representing products (from the "Range Merging (Compacting) with Attributes" example above, but you want to represent them as a custom ProductGroup class:

In this case, three callables are used:

  • start: This is the accessor for the start value of the custom object
  • end: This is the accessor for the end value of the custom object
  • new: This creates a new object, and takes three parameters (start, end, and attribute).

Note that we don't have to specify use_attr=True since we are providing a custom attr callable.

from dataclasses import dataclass
from range_merge import merge

@dataclass
class ProductGroup:
    low: int
    high: int
    group: str

products = [
    ProductGroup(low=0, high=99, group="soup"),
    ProductGroup(low=57, high=57, group="cereal"),
    ProductGroup(low=100, high=199, group="cereal"),
]

result = merge(
    products,
    start=lambda p: p.low,
    end=lambda p: p.high,
    attr=lambda p: p.group,
    new=lambda s, e, attr: ProductGroup(low=s, high=e, group=attr),
)
# Result: [
#   ProductGroup(0, 56, "soup"),
#   ProductGroup(57, 57, "cereal"),
#   ProductGroup(58, 99, "soup"),
#   ProductGroup(100, 199, "cereal")
]

Merging IP Ranges

If you want to merge a list of IP ranges, you can use merge_ip_ranges. See the next section for CIDR ranges rather than IP ranges.

There is no option to merge IP ranges without attributes (attributes must match to merge), as Python's ipaddress.collapse_addresses() handles this functionality.

from ipaddress import IPv4Address, IPv6Address
from range_merge import merge_ip_ranges

src = [
    ("1.0.0.0", "1.255.240.0", "foo"),
    ("1.255.240.1", "2.0.255.255", "foo"),
    ("2000::", "2fff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", "foo"),
    ("3000::", "3fff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", "foo"),
]

result = merge_ip_ranges(src)
# Result: [
#   (IPv4Address("1.0.0.0"), IPv4Address("2.0.255.255"), "foo"),
#   (IPv6Address("2000::"), IPv6Address("3fff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"), "foo"),
# ]

Merging CIDR Ranges

If you want to merge a list of CIDR ranges, you can use merge_cidr_ranges.

There is no option to merge CIDR ranges without attributes (attributes must match to merge), as Python's ipaddress.collapse_addresses() handles this functionality.

from ipaddress import IPv4Network, IPv6Network
from range_merge import merge_cidr_ranges

src = [
    ("1.0.0.0/8", "foo"),
    ("1.255.240.0/24", "bar"),
    ("2000::/4", "foo"),
    ("3000::/4", "foo"),
]

result = merge_cidr_ranges(src)
# Result: [
#   (IPv4Network("1.0.0.0/9"), "foo"),
#   (IPv4Network("1.128.0.0/10"), "foo"),
#   (IPv4Network("1.192.0.0/11"), "foo"),
#   (IPv4Network("1.224.0.0/12"), "foo"),
#   (IPv4Network("1.240.0.0/13"), "foo"),
#   (IPv4Network("1.248.0.0/14"), "foo"),
#   (IPv4Network("1.252.0.0/15"), "foo"),
#   (IPv4Network("1.254.0.0/16"), "foo"),
#   (IPv4Network("1.255.0.0/17"), "foo"),
#   (IPv4Network("1.255.128.0/18"), "foo"),
#   (IPv4Network("1.255.192.0/19"), "foo"),
#   (IPv4Network("1.255.224.0/20"), "foo"),
#   (IPv4Network("1.255.240.0/24"), "bar"),
#   (IPv4Network("1.255.241.0/24"), "foo"),
#   (IPv4Network("1.255.242.0/23"), "foo"),
#   (IPv4Network("1.255.244.0/22"), "foo"),
#   (IPv4Network("1.255.248.0/21"), "foo"),
#   (IPv6Network("2000::/3"), "foo"),
# ]

API Reference

merge(ranges, **options)

Merge a sequence of ranges.

Parameters:

Parameter Type Default Description
ranges Sequence required The ranges to merge
start Callable lambda r: r[0] Extract start value from a range
end Callable lambda r: r[1] Extract end value from a range
before Callable lambda x: x - 1 Return the value before x
after Callable lambda x: x + 1 Return the value after x
new Callable Creates range object Create a new range from (start, end, attr)
attr Callable lambda r: r[2] Extract attribute from a range
use_attr bool False Whether to use attributes (if no attr is provided when calling)
cmp Callable Default comparator Custom comparison function

The start and end callables each take a single argument, the range object being used.

The before and after callables also take a single argument, but this is a discrete value, not a range.

The new callable takes three parameters (start, end, attr). The third parameter is passed as None if attributes aren't being used.

Returns: list of merged ranges.

merge_discrete(values, **options)

Merge a sequence of discrete values into ranges.

Parameters:

Parameter Type Default Description
values Sequence required The discrete values to merge
before Callable lambda x: x - 1 Return the value before x
after Callable lambda x: x + 1 Return the value after x
cmp Callable Default comparator Custom comparison function

For details on before, after, and cmp, see the merge() section.

Returns: list of (start, end) tuples.

merge_ip_ranges(values, **options)

Merge a sequence of ip range values into consolidated ranges.

Parameters:

Parameter Type Default Description
ranges Sequence required The IP ranges to merge
start Callable lambda r: r[0] Return the starting IP address in the range
end Callable lambda r: r[1] Return the ending IP address in the range
new Callable Creates range object Create a new range from (start, end, attr)
attr Callable lambda r: r[2] Extract attribute from a range
cmp Callable Default comparator Custom comparison function

For details on start, end, attr, and cmp, see the merge() section.

Returns: list of IP ranges. All addresses are converted to either an IPv4Address or an IPv6Address

merge_cidr_ranges(values, **options)

Merge a sequence of CIDR range values into consolidated ranges.

Parameters:

Parameter Type Default Description
ranges Sequence required The CIDR ranges to merge
new Callable Creates range object Create a new range from (CIDR, attr)
cidr Callable lambda x: r[0] Return the CIDR in the range
attr Callable lambda r: r[1] Extract attribute from a range

The cidr callable takes a single argument, the range object being used.

The new callable takes two parameters (cidr, attr).

For details on attr see the merge() section.

Returns: list of IP network. All addresses are converted to either an IPv4Network or an IPv6Network

Exceptions

  • ImproperRangeEndBeforeStart: Raised when a range has an end value that comes before its start value (using the default or custom cmp callable)

License

BSD-2-Clause

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

range_merge-0.1.0.tar.gz (8.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

range_merge-0.1.0-py3-none-any.whl (10.8 kB view details)

Uploaded Python 3

File details

Details for the file range_merge-0.1.0.tar.gz.

File metadata

  • Download URL: range_merge-0.1.0.tar.gz
  • Upload date:
  • Size: 8.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.9.15 {"installer":{"name":"uv","version":"0.9.15","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for range_merge-0.1.0.tar.gz
Algorithm Hash digest
SHA256 ffffde7f88db00b4f062b82c3124a5be09cb409237162d8d76e8a23835a5bf5f
MD5 6603151ec70d725b3cc87090d70b98ec
BLAKE2b-256 106c9e94cd5e2ca879d50fe05ea5a19b0d7b674751e4fad4064abcc4895a894a

See more details on using hashes here.

File details

Details for the file range_merge-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: range_merge-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 10.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.9.15 {"installer":{"name":"uv","version":"0.9.15","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for range_merge-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 05bceae23ffe7fed6726cc0feb75d001c1a411584f77a944fbb833e4f0c97508
MD5 6994e8b94e229d8526b6079bdab46750
BLAKE2b-256 6cc456022691c3838e38b65175b7fe2a36fe1251e6e142d8b9f4483be0f0ab87

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page