From c669103ccbaf76982c8042be2f5a013a92afe64e Mon Sep 17 00:00:00 2001 From: Adam Gibson Date: Mon, 23 May 2016 13:39:22 +0800 Subject: [PATCH 1/3] Added range2list() and list2range() functions to strutils.py - added applicable tests to test_strutils.py --- boltons/strutils.py | 115 ++++++++++++++++++++++++++++++++++++++++- tests/test_strutils.py | 19 +++++++ 2 files changed, 133 insertions(+), 1 deletion(-) diff --git a/boltons/strutils.py b/boltons/strutils.py index e56ad7d..41f2fa9 100644 --- a/boltons/strutils.py +++ b/boltons/strutils.py @@ -31,7 +31,7 @@ __all__ = ['camel2under', 'under2camel', 'slugify', 'split_punct_ws', 'asciify', 'is_ascii', 'is_uuid', 'html2text', 'strip_ansi', 'bytes2human', 'find_hashtags', 'a10n', 'gunzip_bytes', 'iter_splitlines', 'indent', 'escape_shell_args', - 'args2cmd', 'args2sh'] + 'args2cmd', 'args2sh', 'range2list', 'list2range'] _punct_ws_str = string.punctuation + string.whitespace @@ -845,3 +845,116 @@ def args2cmd(args, sep=' '): result.append('"') return ''.join(result) + + +def range2list(range_string, delim=',', range_delim='-'): + """Returns a sorted list of integers based on *range_string*. Reverse of :func:`list2range`. + + Args: + range_string (str): String of comma separated integers or ranges (e.g. '1,2,4-6,8'). Typical of a custom page + range string used in printer dialogs. + delim (char): Defaults to ','. Separates integers and contiguous ranges of integers. + range_delim (char): Defaults to '-'. Indicates a contiguous range of integers. + + >>> range2list('1,3,5-8,10-11,15') + [1, 3, 5, 6, 7, 8, 10, 11, 15] + """ + output = [] + + for x in range_string.strip().split(sep=delim): + + # Range + if range_delim in x: + range_limits = list(map(int, x.split(sep=range_delim))) + output += list(range(min(range_limits), max(range_limits)+1)) + + # Empty String + elif not x: + continue + + # Integer + else: + output.append(int(x)) + + return sorted(output) + + +def list2range(int_list, delim=',', range_delim='-', delim_space=False): + """Returns a sorted range string from a list of integers (*int_list*). Contiguous ranges of integers are + collapsed to min and max values. Reverse of :func:`range2list`. + + Args: + int_list (list): List of integers to be converted into a range string (e.g. [1,2,4,5,6,8]). + delim (char): Defaults to ','. Separates integers and contiguous ranges of integers. + range_delim (char): Defaults to '-'. Indicates a contiguous range of integers. + delim_space (bool): Defaults to ``False``. If ``True``, adds a space after all *delim* characters. + + + >>> list2range([1,3,5,6,7,8,10,11,15]) + '1,3,5-8,10-11,15' + """ + output = [] + contig_range = collections.deque() + + for x in sorted(int_list): + + # Handle current (and first) value. + if len(contig_range) < 1: + contig_range.append(x) + + # Handle current value, given multiple previous values are contiguous. + elif len(contig_range) > 1: + delta = x - contig_range[-1] + + # Current value is contiguous. + if delta == 1: + contig_range.append(x) + + # Current value is non-contiguous. + elif delta > 1: + range_substr = '{0:d}{1}{2:d}'.format(min(contig_range), range_delim, max(contig_range)) + output.append(range_substr) + contig_range.clear() + contig_range.append(x) + + # Current value repeated. + else: + continue + + # Handle current value, given that contiguous integers haven't been previously detected. + else: + delta = x - contig_range[0] + + # Current value is contiguous. + if delta == 1: + contig_range.append(x) + + # Current value is non-contiguous. + elif delta > 1: + output.append('{:d}'.format(contig_range.popleft())) + contig_range.append(x) + + # Current value repeated. + else: + continue + + # Handle the last value. + else: + + # Last value is non-contiguous. + if len(contig_range) == 1: + output.append('{:d}'.format(contig_range.popleft())) + contig_range.clear() + + # Last value is part of contiguous range. + elif len(contig_range) > 1: + range_substr = '{0:d}{1}{2:d}'.format(min(contig_range), range_delim, max(contig_range)) + output.append(range_substr) + contig_range.clear() + + if delim_space: + output_str = (delim+' ').join(output) + else: + output_str = delim.join(output) + + return output_str diff --git a/tests/test_strutils.py b/tests/test_strutils.py index dd4981f..46e2967 100644 --- a/tests/test_strutils.py +++ b/tests/test_strutils.py @@ -24,3 +24,22 @@ def test_is_uuid(): assert strutils.is_uuid(str(uuid.uuid4())) == True assert strutils.is_uuid(str(uuid.uuid4()), version=1) == False assert strutils.is_uuid(set('garbage')) == False + + +def test_range2list(): + assert strutils.range2list("1,3,5-8,10-11,15") == [1, 3, 5, 6, 7, 8, 10, 11, 15] + + assert strutils.range2list("1,3,5-8,10-11,15,") == [1, 3, 5, 6, 7, 8, 10, 11, 15] + assert strutils.range2list(",1,3,5-8,10-11,15") == [1, 3, 5, 6, 7, 8, 10, 11, 15] + assert strutils.range2list(" 1, 3 ,5-8,10-11,15 ") == [1, 3, 5, 6, 7, 8, 10, 11, 15] + assert strutils.range2list("3,1,5-8,10-11,15") == [1, 3, 5, 6, 7, 8, 10, 11, 15] + + assert strutils.range2list("5-8") == [5, 6, 7, 8] + assert strutils.range2list("8-5") == [5, 6, 7, 8] + +def test_list2range(): + assert strutils.list2range([1, 3, 5, 6, 7, 8, 10, 11, 15]) == '1,3,5-8,10-11,15' + assert strutils.list2range([5, 6, 7, 8]) == '5-8' + + assert strutils.list2range([1, 3, 5, 6, 7, 8, 10, 11, 15], delim_space=True) == '1, 3, 5-8, 10-11, 15' + assert strutils.list2range([5, 6, 7, 8], delim_space=True) == '5-8' From c079000e3e2509fefa2a1ee636b82d2194b0b027 Mon Sep 17 00:00:00 2001 From: Adam Gibson Date: Mon, 23 May 2016 14:16:55 +0800 Subject: [PATCH 2/3] Python 2 compatibility fixes for range2list() and list2range() functions in strutils.py --- boltons/strutils.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/boltons/strutils.py b/boltons/strutils.py index 41f2fa9..52dc331 100644 --- a/boltons/strutils.py +++ b/boltons/strutils.py @@ -861,11 +861,11 @@ def range2list(range_string, delim=',', range_delim='-'): """ output = [] - for x in range_string.strip().split(sep=delim): + for x in range_string.strip().split(delim): # Range if range_delim in x: - range_limits = list(map(int, x.split(sep=range_delim))) + range_limits = list(map(int, x.split(range_delim))) output += list(range(min(range_limits), max(range_limits)+1)) # Empty String @@ -931,7 +931,7 @@ def list2range(int_list, delim=',', range_delim='-', delim_space=False): # Current value is non-contiguous. elif delta > 1: - output.append('{:d}'.format(contig_range.popleft())) + output.append('{0:d}'.format(contig_range.popleft())) contig_range.append(x) # Current value repeated. @@ -943,7 +943,7 @@ def list2range(int_list, delim=',', range_delim='-', delim_space=False): # Last value is non-contiguous. if len(contig_range) == 1: - output.append('{:d}'.format(contig_range.popleft())) + output.append('{0:d}'.format(contig_range.popleft())) contig_range.clear() # Last value is part of contiguous range. From f79f0288c89a419dc5b3bf414e9e66e6e23d969c Mon Sep 17 00:00:00 2001 From: Adam Gibson Date: Mon, 23 May 2016 16:11:50 +0800 Subject: [PATCH 3/3] Rename range2list() & list2range() functions to parse_int_list() & format_int_list(). --- boltons/strutils.py | 14 +++++++------- tests/test_strutils.py | 26 +++++++++++++------------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/boltons/strutils.py b/boltons/strutils.py index 52dc331..faa43c9 100644 --- a/boltons/strutils.py +++ b/boltons/strutils.py @@ -31,7 +31,7 @@ __all__ = ['camel2under', 'under2camel', 'slugify', 'split_punct_ws', 'asciify', 'is_ascii', 'is_uuid', 'html2text', 'strip_ansi', 'bytes2human', 'find_hashtags', 'a10n', 'gunzip_bytes', 'iter_splitlines', 'indent', 'escape_shell_args', - 'args2cmd', 'args2sh', 'range2list', 'list2range'] + 'args2cmd', 'args2sh', 'parse_int_list', 'format_int_list'] _punct_ws_str = string.punctuation + string.whitespace @@ -847,8 +847,8 @@ def args2cmd(args, sep=' '): return ''.join(result) -def range2list(range_string, delim=',', range_delim='-'): - """Returns a sorted list of integers based on *range_string*. Reverse of :func:`list2range`. +def parse_int_list(range_string, delim=',', range_delim='-'): + """Returns a sorted list of integers based on *range_string*. Reverse of :func:`format_int_list`. Args: range_string (str): String of comma separated integers or ranges (e.g. '1,2,4-6,8'). Typical of a custom page @@ -856,7 +856,7 @@ def range2list(range_string, delim=',', range_delim='-'): delim (char): Defaults to ','. Separates integers and contiguous ranges of integers. range_delim (char): Defaults to '-'. Indicates a contiguous range of integers. - >>> range2list('1,3,5-8,10-11,15') + >>> parse_int_list('1,3,5-8,10-11,15') [1, 3, 5, 6, 7, 8, 10, 11, 15] """ output = [] @@ -879,9 +879,9 @@ def range2list(range_string, delim=',', range_delim='-'): return sorted(output) -def list2range(int_list, delim=',', range_delim='-', delim_space=False): +def format_int_list(int_list, delim=',', range_delim='-', delim_space=False): """Returns a sorted range string from a list of integers (*int_list*). Contiguous ranges of integers are - collapsed to min and max values. Reverse of :func:`range2list`. + collapsed to min and max values. Reverse of :func:`parse_int_list`. Args: int_list (list): List of integers to be converted into a range string (e.g. [1,2,4,5,6,8]). @@ -890,7 +890,7 @@ def list2range(int_list, delim=',', range_delim='-', delim_space=False): delim_space (bool): Defaults to ``False``. If ``True``, adds a space after all *delim* characters. - >>> list2range([1,3,5,6,7,8,10,11,15]) + >>> format_int_list([1,3,5,6,7,8,10,11,15]) '1,3,5-8,10-11,15' """ output = [] diff --git a/tests/test_strutils.py b/tests/test_strutils.py index 46e2967..a4e8cc8 100644 --- a/tests/test_strutils.py +++ b/tests/test_strutils.py @@ -26,20 +26,20 @@ def test_is_uuid(): assert strutils.is_uuid(set('garbage')) == False -def test_range2list(): - assert strutils.range2list("1,3,5-8,10-11,15") == [1, 3, 5, 6, 7, 8, 10, 11, 15] +def test_parse_int_list(): + assert strutils.parse_int_list("1,3,5-8,10-11,15") == [1, 3, 5, 6, 7, 8, 10, 11, 15] - assert strutils.range2list("1,3,5-8,10-11,15,") == [1, 3, 5, 6, 7, 8, 10, 11, 15] - assert strutils.range2list(",1,3,5-8,10-11,15") == [1, 3, 5, 6, 7, 8, 10, 11, 15] - assert strutils.range2list(" 1, 3 ,5-8,10-11,15 ") == [1, 3, 5, 6, 7, 8, 10, 11, 15] - assert strutils.range2list("3,1,5-8,10-11,15") == [1, 3, 5, 6, 7, 8, 10, 11, 15] + assert strutils.parse_int_list("1,3,5-8,10-11,15,") == [1, 3, 5, 6, 7, 8, 10, 11, 15] + assert strutils.parse_int_list(",1,3,5-8,10-11,15") == [1, 3, 5, 6, 7, 8, 10, 11, 15] + assert strutils.parse_int_list(" 1, 3 ,5-8,10-11,15 ") == [1, 3, 5, 6, 7, 8, 10, 11, 15] + assert strutils.parse_int_list("3,1,5-8,10-11,15") == [1, 3, 5, 6, 7, 8, 10, 11, 15] - assert strutils.range2list("5-8") == [5, 6, 7, 8] - assert strutils.range2list("8-5") == [5, 6, 7, 8] + assert strutils.parse_int_list("5-8") == [5, 6, 7, 8] + assert strutils.parse_int_list("8-5") == [5, 6, 7, 8] -def test_list2range(): - assert strutils.list2range([1, 3, 5, 6, 7, 8, 10, 11, 15]) == '1,3,5-8,10-11,15' - assert strutils.list2range([5, 6, 7, 8]) == '5-8' +def test_format_int_list(): + assert strutils.format_int_list([1, 3, 5, 6, 7, 8, 10, 11, 15]) == '1,3,5-8,10-11,15' + assert strutils.format_int_list([5, 6, 7, 8]) == '5-8' - assert strutils.list2range([1, 3, 5, 6, 7, 8, 10, 11, 15], delim_space=True) == '1, 3, 5-8, 10-11, 15' - assert strutils.list2range([5, 6, 7, 8], delim_space=True) == '5-8' + assert strutils.format_int_list([1, 3, 5, 6, 7, 8, 10, 11, 15], delim_space=True) == '1, 3, 5-8, 10-11, 15' + assert strutils.format_int_list([5, 6, 7, 8], delim_space=True) == '5-8'