diff -r cca2ed4e8b41 Lib/collections/__init__.py --- a/Lib/collections/__init__.py Thu Dec 17 10:35:05 2015 +0000 +++ b/Lib/collections/__init__.py Sun Dec 20 17:03:49 2015 +0100 @@ -43,6 +43,11 @@ except ImportError: pass +try: + from _collections import subseq_index +except ImportError: + pass + ################################################################################ ### OrderedDict diff -r cca2ed4e8b41 Lib/test/test_collections.py --- a/Lib/test/test_collections.py Thu Dec 17 10:35:05 2015 +0000 +++ b/Lib/test/test_collections.py Sun Dec 20 17:03:49 2015 +0100 @@ -19,6 +19,7 @@ from collections import UserDict, UserString, UserList from collections import ChainMap from collections import deque +from collections import subseq_index from collections.abc import Awaitable, Coroutine, AsyncIterator, AsyncIterable from collections.abc import Hashable, Iterable, Iterator, Generator from collections.abc import Sized, Container, Callable @@ -1622,6 +1623,57 @@ self.assertEqual(dict(c), {'a': 5, 'b': 2, 'c': 1, 'd': 1, 'r':2 }) +class BuggyInt(int): + def __eq__(self, other): + raise ValueError('spam') + + +class TestSubseqIndex(unittest.TestCase): + def test_lists(self): + self.assertEqual(subseq_index([1, 2, 3, 4, 5], [1, 2, 3]), 0) + self.assertEqual(subseq_index([1, 2, 4, 1, 2, 5, 1, 2, 6], [2, 6]), 7) + self.assertEqual(subseq_index([1, 2, 3, 4, 5], [3]), 2) + self.assertEqual(subseq_index([1, 2, 3], (1, 2, 3)), 0) + self.assertIsNone(subseq_index([1, 2, 3, 4, 5], [1, 2, 4])) + self.assertIsNone(subseq_index([1, 2, 3, 4, 5], [])) + self.assertIsNone(subseq_index([], [])) + self.assertIsNone(subseq_index([], [1, 2, 3])) + self.assertIsNone(subseq_index([1, 2, 3], [3, 2, 1])) + self.assertIsNone(subseq_index([1, 2, 3, 4, 5], [1, 2, 3, 4, 5, 6])) + + def test_tuples(self): + self.assertEqual(subseq_index((1, 2, 3), (3,)), 2) + self.assertEqual(subseq_index((1, 2, 3), [3]), 2) + self.assertIsNone(subseq_index((1, 2, 3), (4,))) + self.assertIsNone(subseq_index((1, 2, 3), ())) + self.assertIsNone(subseq_index((1, 2, 3), (2, 3, 4))) + + def test_strings(self): + self.assertEqual(subseq_index('xxyxzyzzzy', 'yz'), 5) + self.assertIsNone(subseq_index('xxyxzyzzzy', 'xzz')) + + def test_inherited(self): + self.assertEqual(subseq_index(UserList([1, 2, 3]), [1]), 0) + self.assertEqual(subseq_index([1, 2, 3], UserList([1])), 0) + self.assertEqual(subseq_index(UserList([1, 2, 3]), UserList([1])), 0) + + def test_exceptions(self): + args_tests = [ + (), + ([1, 2, 3],), + ([1, 2, 3], 3), + ({1, 2, 3}, [3]), + ([1, 2, 3], {3}), + ([1, 2, 3], {3: 'spam'}) + ] + for args in args_tests: + with self.assertRaises(TypeError): + subseq_index(*args) + with self.assertRaises(ValueError): + # Raised by BuggyInt.__eq__() + subseq_index([1, 2, 3], [BuggyInt(3)]) + + ################################################################################ ### Run tests ################################################################################ @@ -1630,7 +1682,7 @@ NamedTupleDocs = doctest.DocTestSuite(module=collections) test_classes = [TestNamedTuple, NamedTupleDocs, TestOneTrickPonyABCs, TestCollectionABCs, TestCounter, TestChainMap, - TestUserObjects, + TestUserObjects, TestSubseqIndex, ] support.run_unittest(*test_classes) support.run_doctest(collections, verbose) diff -r cca2ed4e8b41 Modules/_collectionsmodule.c --- a/Modules/_collectionsmodule.c Thu Dec 17 10:35:05 2015 +0000 +++ b/Modules/_collectionsmodule.c Sun Dec 20 17:03:49 2015 +0100 @@ -2317,6 +2317,71 @@ Py_RETURN_NONE; } + +PyDoc_STRVAR(subseq_index_doc, +"subseq_index(seq, subseq) -> int\n\ +\n\ +Return first index of subseq within seq.\n\ +Return None if subseq is not present."); + +static PyObject * +subseq_index(PyObject *self, PyObject *args) +{ + Py_ssize_t i, j, k, stop, seqlen, sublen; + PyObject *seq, *subseq, *fast_seq, *fast_subseq, *result = Py_None; + PyObject **seq_items, **subseq_items; + int equal; + + if (!PyArg_UnpackTuple(args, "subseq_index", 2, 2, &seq, &subseq)) + return NULL; + + if (!PySequence_Check(seq) || !PySequence_Check(subseq)) { + PyErr_SetString(PyExc_TypeError, "arguments must be sequences"); + return NULL; + } + fast_seq = PySequence_Fast(seq, "seq must be iterable"); + if (fast_seq == NULL) + return NULL; + + fast_subseq = PySequence_Fast(subseq, "subseq must be iterable"); + if (fast_subseq == NULL) { + Py_DECREF(fast_seq); + return NULL; + } + seqlen = PySequence_Fast_GET_SIZE(fast_seq); + sublen = PySequence_Fast_GET_SIZE(fast_subseq); + stop = seqlen - sublen + 1; + + if (sublen == 0 || sublen > seqlen) + goto done; + + seq_items = PySequence_Fast_ITEMS(fast_seq); + subseq_items = PySequence_Fast_ITEMS(fast_subseq); + + for (i = 0; i < stop; i++) { + for (j = 0, k = i; j < sublen; j++, k++) { + equal = PyObject_RichCompareBool(subseq_items[j], + seq_items[k], Py_EQ); + if (!equal) + break; + if (equal < 0) { + result = NULL; + goto done; + } + } + if (j == sublen) { + result = PyLong_FromSsize_t(i); + goto done; + } + } + + done: + Py_DECREF(fast_seq); + Py_DECREF(fast_subseq); + Py_XINCREF(result); + return result; +} + /* module level code ********************************************************/ PyDoc_STRVAR(module_doc, @@ -2327,6 +2392,7 @@ static struct PyMethodDef module_functions[] = { {"_count_elements", _count_elements, METH_VARARGS, _count_elements_doc}, + {"subseq_index", subseq_index, METH_VARARGS, subseq_index_doc}, {NULL, NULL} /* sentinel */ };