From c919c523dcef013b8ce0294a25571c1485d03df2 Mon Sep 17 00:00:00 2001 From: James Lu Date: Sat, 9 Jun 2018 17:03:40 -0700 Subject: [PATCH] utils: add remove_range() """ Removes a range string of (one-indexed) items from the list. Range strings are indices or ranges of them joined together with a ",": e.g. "5", "2", "2-10", "1,3,5-8" See test/test_utils.py for more complete examples. """ --- test/test_utils.py | 89 ++++++++++++++++++++++++++++++++++++++++++++++ utils.py | 51 ++++++++++++++++++++++++++ 2 files changed, 140 insertions(+) diff --git a/test/test_utils.py b/test/test_utils.py index 3e8868e..dc5ee8e 100644 --- a/test/test_utils.py +++ b/test/test_utils.py @@ -41,5 +41,94 @@ class UtilsTestCase(unittest.TestCase): "\x0305t\x030,1h\x0307,02e\x0308,06 \x0309,13q\x0303,15u\x0311,14i\x0310,05c\x0312,04k\x0302,07 \x0306,08b\x0313,09r\x0305,10o\x0304,12w\x0307,02n\x0308,06 \x0309,13f\x0303,15o\x0311,14x\x0310,05 \x0312,04j\x0302,07u\x0306,08m\x0313,09p\x0305,10s\x0304,12 \x0307,02o\x0308,06v\x0309,13e\x0303,15r\x0311,14 \x0310,05t\x0312,04h\x0302,07e\x0306,08 \x0313,09l\x0305,10a\x0304,12z\x0307,02y\x0308,06 \x0309,13d\x0303,15o\x0311,14g\x0f"), "the quick brown fox jumps over the lazy dog") + def test_remove_range(self): + self.assertEqual(utils.remove_range( + "1", [1,2,3,4,5,6,7,8,9]), + [2,3,4,5,6,7,8,9]) + + self.assertEqual(utils.remove_range( + "2,4", [1,2,3,4,5,6,7,8,9]), + [1,3,5,6,7,8,9]) + + self.assertEqual(utils.remove_range( + "1-4", [1,2,3,4,5,6,7,8,9]), + [5,6,7,8,9]) + + self.assertEqual(utils.remove_range( + "1-3,7", [1,2,3,4,5,6,7,8,9]), + [4,5,6,8,9]) + + self.assertEqual(utils.remove_range( + "1-3,5-9", [1,2,3,4,5,6,7,8,9]), + [4]) + + self.assertEqual(utils.remove_range( + "1-2,3-5,6-9", [1,2,3,4,5,6,7,8,9]), + []) + + # Anti-patterns, but should be legal + self.assertEqual(utils.remove_range( + "4,2", [1,2,3,4,5,6,7,8,9]), + [1,3,5,6,7,8,9]) + self.assertEqual(utils.remove_range( + "4,4,4", [1,2,3,4,5,6,7,8,9]), + [1,2,3,5,6,7,8,9]) + + # Empty subranges should be filtered away + self.assertEqual(utils.remove_range( + ",2,,4,", [1,2,3,4,5,6,7,8,9]), + [1,3,5,6,7,8,9]) + + # Not enough items + with self.assertRaises(IndexError): + utils.remove_range( + "5", ["abcd", "efgh"]) + with self.assertRaises(IndexError): + utils.remove_range( + "1-5", ["xyz", "cake"]) + + # Ranges going in reverse or invalid + with self.assertRaises(ValueError): + utils.remove_range( + "5-2", [":)", ":D", "^_^"]) + utils.remove_range( + "2-2", [":)", ":D", "^_^"]) + + # 0th element + with self.assertRaises(ValueError): + utils.remove_range( + "5,0", list(range(50))) + + # List can't contain None + with self.assertRaises(ValueError): + utils.remove_range( + "1-2", [None, "", 0, False]) + + # Malformed indices + with self.assertRaises(ValueError): + utils.remove_range( + " ", ["some", "clever", "string"]) + utils.remove_range( + " ,,, ", ["some", "clever", "string"]) + utils.remove_range( + "a,b,c,1,2,3", ["some", "clever", "string"]) + + # Malformed ranges + with self.assertRaises(ValueError): + utils.remove_range( + "1,2-", [":)", ":D", "^_^"]) + utils.remove_range( + "-", [":)", ":D", "^_^"]) + utils.remove_range( + "1-2-3", [":)", ":D", "^_^"]) + utils.remove_range( + "-1-2", [":)", ":D", "^_^"]) + utils.remove_range( + "3--", [":)", ":D", "^_^"]) + utils.remove_range( + "--5", [":)", ":D", "^_^"]) + utils.remove_range( + "-3--5", ["we", "love", "emotes"]) + if __name__ == '__main__': unittest.main() diff --git a/utils.py b/utils.py index d099c4c..47e8734 100644 --- a/utils.py +++ b/utils.py @@ -735,3 +735,54 @@ def strip_irc_formatting(text): for char in _irc_formatting_chars: text = text.replace(char, '') return text + +_subrange_re = re.compile(r'(?P(\d+))-(?P(\d+))') +def remove_range(rangestr, mylist): + """ + Removes a range string of (one-indexed) items from the list. + Range strings are indices or ranges of them joined together with a ",": + e.g. "5", "2", "2-10", "1,3,5-8" + + See test/test_utils.py for more complete examples. + """ + if None in mylist: + raise ValueError("mylist must not contain None!") + + # Split and filter out empty subranges + ranges = filter(None, rangestr.split(',')) + if not ranges: + raise ValueError("Invalid range string %r" % rangestr) + + for subrange in ranges: + match = _subrange_re.match(subrange) + if match: + start = int(match.group('start')) + end = int(match.group('end')) + + if end <= start: + raise ValueError("Range start (%d) is <= end (%d) in range string %r" % + (start, end, rangestr)) + elif 0 in (end, start): + raise ValueError("Got range index 0 in range string %r, this function is one-indexed" % + rangestr) + + # For our purposes, make sure the start and end are within the list + mylist[start-1], mylist[end-1] + + # Replace the entire range with None's + log.debug('utils.remove_range: removing items from %s to %s: %s', start, end, mylist[start-1:end]) + mylist[start-1:end] = [None] * (end-(start-1)) + + elif subrange in string.digits: + index = int(subrange) + if index == 0: + raise ValueError("Got index 0 in range string %r, this function is one-indexed" % + rangestr) + log.debug('utils.remove_range: removing item %s: %s', index, mylist[index-1]) + mylist[index-1] = None + + else: + raise ValueError("Got invalid subrange %r in range string %r" % + (subrange, rangestr)) + + return list(filter(lambda x: x is not None, mylist))