diff --git a/exercises/practice/sublist/.approaches/config.json b/exercises/practice/sublist/.approaches/config.json index ce54db9c14e..22ae02518b8 100644 --- a/exercises/practice/sublist/.approaches/config.json +++ b/exercises/practice/sublist/.approaches/config.json @@ -1,6 +1,7 @@ { "introduction": { - "authors": ["safwansamsudeen"] + "authors": ["safwansamsudeen"], + "contributors": ["yrahcaz7"] }, "approaches": [ { @@ -8,14 +9,30 @@ "slug": "list-manipulation", "title": "List manipulation", "blurb": "Manipulate and check lists to solve the exercise", - "authors": ["safwansamsudeen"] + "authors": ["safwansamsudeen"], + "contributors": ["yrahcaz7"] }, { "uuid": "61366160-c859-4d16-9085-171428209b8d", "slug": "using-strings", "title": "Using strings", "blurb": "Convert the lists to string and use string manipulation to solve the exercise", - "authors": ["safwansamsudeen"] + "authors": ["safwansamsudeen"], + "contributors": ["yrahcaz7"] + }, + { + "uuid": "b2695c39-c1c7-47f0-bfcd-5e9703674bea", + "slug": "manual-loop", + "title": "Manual looping", + "blurb": "Manually track indexes while looping through the lists to solve the exercise", + "authors": ["yrahcaz7"] + }, + { + "uuid": "a1eeaf9b-a9b3-421e-bfad-44f7e1575450", + "slug": "sort-lists", + "title": "Sorting lists", + "blurb": "Sort the lists to determine the shorter and longer ones to solve the exercise", + "authors": ["yrahcaz7"] } ] } diff --git a/exercises/practice/sublist/.approaches/introduction.md b/exercises/practice/sublist/.approaches/introduction.md index 42f991ef086..13c10b2dba1 100644 --- a/exercises/practice/sublist/.approaches/introduction.md +++ b/exercises/practice/sublist/.approaches/introduction.md @@ -1,24 +1,24 @@ # Introduction -There are two broad ways to solve Sublist. + +There are four broad ways to solve Sublist, though one of them ("using strings") is not recommended. ## General guidance -To write the code, you need to branch out (probably with `if`) into the four different possible conditions, and return the appropriate name of the category. -## Approach: list manipulation +To write the code, you need to branch out (probably with `if`) into the four different possible conditions, and return the appropriate category (`SUBLIST`, `SUPERLIST`, `EQUAL`, or `UNEQUAL`). + +Note that you shouldn't return the category's value directly, as that would introduce [magic values][magic-values] into your code. + +## Approach: List manipulation + The direct approach would be to manipulate and check the given lists to solve this. This solution uses a helper function, which simplifies things, but the approach can be implemented without it. ```python -SUBLIST = 1 -SUPERLIST = 2 -EQUAL = 3 -UNEQUAL = 4 - -def check_sub_sequences(list_one, list_two): - n1 = len(list_one) - n2 = len(list_two) - return any(list_two[i:i+n1] == list_one for i in range(n2 - n1 + 1)) - +def check_sub_sequences(list_a, list_b): + len_a = len(list_a) + len_b = len(list_b) + return any(list_b[i : i + len_a] == list_a for i in range(len_b - len_a + 1)) + def sublist(list_one, list_two): if list_one == list_two: return EQUAL @@ -31,29 +31,89 @@ def sublist(list_one, list_two): Read more on the [detail of this approach][approach-list-manipulation]. -## Approach: using strings -Another seemingly clever approach is to convert the lists to strings and then -use the `in` operator to check for sub-sequences. -**However, this does not work.** +## Approach: Manual looping + +This approach uses a helper function that manually loops through the lists to determine if the first list is a sublist of the second one. +This approach is the longest one by far, though it may be more comprehensible to some. + ```python -SUBLIST = 1 -SUPERLIST = 2 -EQUAL = 3 -UNEQUAL = 4 +def check_sub_sequences(list_a, list_b): + len_a, len_b = len(list_a), len(list_b) + index_a, index_b = 0, 0 + next_index_b = 1 + + while index_a < len_a and index_b < len_b: + if list_a[index_a] == list_b[index_b]: + index_a += 1 + else: + index_a, index_b = 0, next_index_b + next_index_b += 1 + index_b += 1 + + if index_a == len_a: + if len_a == len_b: + return EQUAL + return SUBLIST + return UNEQUAL def sublist(list_one, list_two): - list_one_check = (str(list_one).strip("[]") + ",") - list_two_check = (str(list_two).strip("[]") + ",") + result = check_sub_sequences(list_one, list_two) + + if result == UNEQUAL and check_sub_sequences(list_two, list_one) == SUBLIST: + result = SUPERLIST + return result +``` + +Learn more about the [details of this approach here][approach-manual-loop]. + +## Approach: Sorting lists + +This approach uses the `sorted()` function to determine which list is shorter and which is longer. +Knowing this information, one can implement a simplified version of the list manipulation approach. + +```python +def sublist(list_one, list_two): + if list_one == list_two: + return EQUAL + if not list_one: + return SUBLIST + if not list_two: + return SUPERLIST + + shorter, longer = sorted((list_one, list_two), key=len) + + for index in range(len(longer) - len(shorter) + 1): + if longer[index : index + len(shorter)] == shorter: + return SUPERLIST if longer is list_one else SUBLIST + + return UNEQUAL +``` + +Read more on the [detail of this approach][approach-sort-lists]. + +## Approach: Using strings + +Another seemingly clever approach is to convert the lists to strings and then use the `in` operator to check for sub-sequences. +**However, this does not work.** + +```python +def sublist(list_one, list_two): + list_one_check = str(list_one).strip("[]") + "," + list_two_check = str(list_two).strip("[]") + "," if list_one_check == list_two_check: return EQUAL - elif list_one_check in list_two_check: + if list_one_check in list_two_check: return SUBLIST - elif list_two_check in list_one_check: + if list_two_check in list_one_check: return SUPERLIST return UNEQUAL ``` + To understand more about this approach and **why it fails**, [read here][approach-using-strings]. +[magic-values]: https://stackoverflow.com/questions/47882/what-is-a-magic-number-and-why-is-it-bad [approach-list-manipulation]: https://exercism.org/tracks/python/exercises/sublist/approaches/list-manipulation +[approach-manual-loop]: https://exercism.org/tracks/python/exercises/sublist/approaches/manual-loop +[approach-sort-lists]: https://exercism.org/tracks/python/exercises/sublist/approaches/sort-lists [approach-using-strings]: https://exercism.org/tracks/python/exercises/sublist/approaches/using-strings diff --git a/exercises/practice/sublist/.approaches/list-manipulation/content.md b/exercises/practice/sublist/.approaches/list-manipulation/content.md index ac374b730e7..3e68260292e 100644 --- a/exercises/practice/sublist/.approaches/list-manipulation/content.md +++ b/exercises/practice/sublist/.approaches/list-manipulation/content.md @@ -1,4 +1,5 @@ # List manipulation + The direct approach would be to manipulate and check the given lists to solve this. This solution uses a helper function, which simplifies things, but the approach can be implemented without it. @@ -8,11 +9,11 @@ SUPERLIST = 2 EQUAL = 3 UNEQUAL = 4 -def check_sub_sequences(list_one, list_two): - n1 = len(list_one) - n2 = len(list_two) - return any(list_two[i:i+n1] == list_one for i in range(n2 - n1 + 1)) - +def check_sub_sequences(list_a, list_b): + len_a = len(list_a) + len_b = len(list_b) + return any(list_b[i : i + len_a] == list_a for i in range(len_b - len_a + 1)) + def sublist(list_one, list_two): if list_one == list_two: return EQUAL @@ -23,16 +24,21 @@ def sublist(list_one, list_two): return UNEQUAL ``` -We first check for equality using the `==` operator, if so, then we return `EQUAL`. -A common way to do this differently would be to return `1` directly, but this is better practice as we [remove magic values][magic values]. +~~~~exercism/note +You might wonder why the lists in the helper function are named `list_a` and `list_b` instead of `list_one` and `list_two`. +This is because if the parameters have the same name, Pylint thinks the parameters are being passed in incorrectly when we call `check_sub_sequences(list_two, list_one)`. +(The exact warning generated is [`W1114 arguments-out-of-order`][w1114].) + +[w1114]: https://pylint.readthedocs.io/en/stable/user_guide/messages/warning/arguments-out-of-order.html +~~~~ -After that we call `check_sub_sequences` passing in `list_one` and `list_two`. -In the helper function, we check if `any` of the possible sub-sequences in `list_two` of length `n1` (the length of the first list) are equal to the first list. -If so, then we conclude that `list_one` is a `SUBLIST` of `list_two`. +In this approach, we first check for equality using the `==` operator, and if the lists are equal, then we return `EQUAL`. +After that, we call `check_sub_sequences()`, passing in `list_one` and `list_two` for the parameters `list_a` and `list_b`. -To find whether `list_one` is a `SUPERLIST` of `list_two`, we just reverse this process - pass in the lists in the opposite order. -Thus, we check if `any` of the possible sub-sequences in `list_one` of length `n2` (the length of the second list) are equal to the second list. +In the helper function, we check if `any` of the possible sub-sequences in `list_b` of length `len_a` (the length of `list_a`) are equal to `list_a`. +If so, then we conclude that `list_a` is a `SUBLIST` of `list_b`. -If none of the above conditions are true, we conclude that the two lists are unequal. +To find whether `list_one` is a `SUPERLIST` of `list_two`, we just reverse this process — pass in the lists in the opposite order. +Thus, we check if `any` of the possible sub-sequences in `list_one` of the length of `list_two` are equal to `list_two`. -[magic values]: https://stackoverflow.com/questions/47882/what-is-a-magic-number-and-why-is-it-bad \ No newline at end of file +If none of the above conditions are true, we conclude that the two lists are `UNEQUAL`. diff --git a/exercises/practice/sublist/.approaches/manual-loop/content.md b/exercises/practice/sublist/.approaches/manual-loop/content.md new file mode 100644 index 00000000000..f55e28cc077 --- /dev/null +++ b/exercises/practice/sublist/.approaches/manual-loop/content.md @@ -0,0 +1,56 @@ +# Manual looping + +This approach uses a helper function that manually loops through the lists to determine if the first list is a sublist of the second one. +This approach is the longest one by far, though it may be more comprehensible to some. + +```python +SUBLIST = 1 +SUPERLIST = 2 +EQUAL = 3 +UNEQUAL = 4 + +def check_sub_sequences(list_a, list_b): + len_a, len_b = len(list_a), len(list_b) + index_a, index_b = 0, 0 + next_index_b = 1 + + while index_a < len_a and index_b < len_b: + if list_a[index_a] == list_b[index_b]: + index_a += 1 + else: + index_a, index_b = 0, next_index_b + next_index_b += 1 + index_b += 1 + + if index_a == len_a: + if len_a == len_b: + return EQUAL + return SUBLIST + return UNEQUAL + +def sublist(list_one, list_two): + result = check_sub_sequences(list_one, list_two) + + if result == UNEQUAL and check_sub_sequences(list_two, list_one) == SUBLIST: + result = SUPERLIST + return result +``` + +~~~~exercism/note +You might wonder why the lists in the helper function are named `list_a` and `list_b` instead of `list_one` and `list_two`. +This is because if the parameters have the same name, Pylint thinks the parameters are being passed in incorrectly when we call `check_sub_sequences(list_two, list_one)`. +(The exact warning generated is [`W1114 arguments-out-of-order`][w1114].) + +[w1114]: https://pylint.readthedocs.io/en/stable/user_guide/messages/warning/arguments-out-of-order.html +~~~~ + +In this approach, the first thing `sublist()` does is call the helper function. +That function then loops through the lists, keeping track of an index for both lists so it can test all necessary combinations to determine if `list_one` is a sublist of `list_two`. + +However, the helper function only determines if `list_one` is equal to or a sublist of `list_two`, not if `list_one` is a superlist of `list_two`. +That is why if the helper function returns `UNEQUAL`, `sublist()` needs to make sure that it isn't acutally a superlist. + +`sublist()` does this by calling the helper function with its arguments reversed: `check_sub_sequences(list_two, list_one)`. +If the result is `SUBLIST`, that means `list_two` is a sublist of `list_one`, thus `list_one` must be a superlist of `list_two`. + +Thus all possibilities are covered, and `sublist()` returns the result. diff --git a/exercises/practice/sublist/.approaches/manual-loop/snippet.txt b/exercises/practice/sublist/.approaches/manual-loop/snippet.txt new file mode 100644 index 00000000000..081ed4aae12 --- /dev/null +++ b/exercises/practice/sublist/.approaches/manual-loop/snippet.txt @@ -0,0 +1,8 @@ +while index_one < len(list_one) and index_two < len(list_two): + if list_one[index_one] == list_two[index_two]: + index_one += 1 + else: + index_one = 0 + index_two = next_index_two + next_index_two += 1 + index_two += 1 \ No newline at end of file diff --git a/exercises/practice/sublist/.approaches/sort-lists/content.md b/exercises/practice/sublist/.approaches/sort-lists/content.md new file mode 100644 index 00000000000..ede91c1fac2 --- /dev/null +++ b/exercises/practice/sublist/.approaches/sort-lists/content.md @@ -0,0 +1,44 @@ +# Sorting lists + +This approach uses the `sorted()` function to determine which list is shorter and which is longer. +Knowing this information, one can implement a simplified version of the [list manipulation approach][approach-list-manipulation]. + +```python +SUBLIST = 1 +SUPERLIST = 2 +EQUAL = 3 +UNEQUAL = 4 + +def sublist(list_one, list_two): + if list_one == list_two: + return EQUAL + if not list_one: + return SUBLIST + if not list_two: + return SUPERLIST + + shorter, longer = sorted((list_one, list_two), key=len) + + for index in range(len(longer) - len(shorter) + 1): + if longer[index : index + len(shorter)] == shorter: + return SUPERLIST if longer is list_one else SUBLIST + + return UNEQUAL +``` + +Here, the case of the lists being equal is checked first. +Then the special cases of empty lists are handled, returning `SUBLIST` or `SUPERLIST` as necessary. + +Once those simple cases are out of the way, the `sorted()` function is used with the keyword argument `key` set to the `len()` function. +This makes `sorted()` sort the items according to their length. + +Once `sorted()` does its work, we use multiple assignment to unpack the results into the `shorter` and `longer` variables. +Then, for each slice of length `len(shorter)` in `longer`, we test if that slice is equal to `shorter`. + +If we find such a slice, that means `shorter` is a sublist of `longer`. +Then we use a [conditional expression][conditional-expression] along with the `is` operator to return `SUBLIST` or `SUPERLIST` depending on which of the original lists is `longer`. + +If we do not find such a slice, we can eliminate `SUBLIST` and `SUPERLIST` from the possible categories, thus the two lists must be `UNEQUAL`. + +[approach-list-manipulation]: https://exercism.org/tracks/python/exercises/sublist/approaches/list-manipulation +[conditional-expression]: https://docs.python.org/3/reference/expressions.html#conditional-expressions diff --git a/exercises/practice/sublist/.approaches/sort-lists/snippet.txt b/exercises/practice/sublist/.approaches/sort-lists/snippet.txt new file mode 100644 index 00000000000..f106f76255f --- /dev/null +++ b/exercises/practice/sublist/.approaches/sort-lists/snippet.txt @@ -0,0 +1,8 @@ +def sublist(list_one, list_two): + ... + shorter, longer = sorted((list_one, list_two), key=len) + + for index in range(len(longer) - len(shorter) + 1): + if longer[index : index + len(shorter)] == shorter: + return SUPERLIST if longer is list_one else SUBLIST + return UNEQUAL \ No newline at end of file diff --git a/exercises/practice/sublist/.approaches/using-strings/content.md b/exercises/practice/sublist/.approaches/using-strings/content.md index ff960902dc9..60c49d168b2 100644 --- a/exercises/practice/sublist/.approaches/using-strings/content.md +++ b/exercises/practice/sublist/.approaches/using-strings/content.md @@ -1,13 +1,12 @@ # Using strings + ~~~~exercism/caution -**This approach does not work, and this document exists to explain that.** +**This approach does not work (_it will not generalize to all cases_), and this document exists to explain that.** Please do not use it in your code. ~~~~ -Another seemingly clever solution is to convert the lists to strings and then -use the `in` operator to check for sub-sequences. -Note that this approach, even if it worked, is not as performant as the -previous one. +Another seemingly clever solution is to convert the lists to strings and then use the `in` operator to check for sub-sequences. + ```python SUBLIST = 1 SUPERLIST = 2 @@ -20,28 +19,36 @@ def sublist(list_one, list_two): if list_one_check == list_two_check: return EQUAL - elif list_one_check in list_two_check: + if list_one_check in list_two_check: return SUBLIST - elif list_two_check in list_one_check: + if list_two_check in list_one_check: return SUPERLIST return UNEQUAL ``` + Let's parse the code to see what it does. -In this approach, we convert the lists to strings, so `[1, 2, 3]` becomes `"[1, 2, 3]"`, remove the brackets `"1, 2, 3"`, and add a comma `"1, 2, 3,"`. +In this approach, we convert the lists to strings, so `[1, 2, 3]` becomes `"[1, 2, 3]"`, remove the brackets `"1, 2, 3"`, and add a comma `"1, 2, 3,"`. We check equality and then use the `in` operator to check for `SUBLIST` or `SUPERLIST`, and finally return `UNEQUAL`. -We add a comma because, say, we call `sublist` with `[1, 2]` and `[1, 22]`. `"1, 2" in "1, 22"` evaluates to `True`, so -the **function would wrongly mark it as `SUBLIST`**. +We add a comma because, say, we call `sublist` with `[1, 2]` and `[1, 22]`. `"1, 2" in "1, 22"` evaluates to `True`, so the **function would wrongly mark it as `SUBLIST`**. + +This case can be handled by changing the code like this: + +```python +list_one_check = str(list_one).strip("[]") + "," +list_two_check = str(list_two).strip("[]") + "," +``` + +Yet, even though this code would pass all of the tests in the Exercism test suite, it would still fail in some cases. +For example, if we call `sublist` with `[1, 2]` and `[5, "1, 2,", 7]`, the function would return `SUBLIST` when it should actually return `UNEQUAL`. + +This could be avoided by changing the code to use a separator that isn't the default one: -This test can be overridden by changing the code like this: ```python -list_one_check = str(list_one).strip("[]") + ',' -list_two_check = str(list_two).strip("[]") + ',' +list_one_check = "|".join(str(item) for item in list_one) + "|" +list_two_check = "|".join(str(item) for item in list_two) + "|" ``` -Yet, the test case (which doesn't exist in the Exercism test suite) `["1", "2"]` and `["5", "'1', '2',", "7"]` would -fail. -Students can add any arbitrary string into the representation to try to "defeat" this test - `list_one_check = str -(list_one) + TOKEN`. The test suite currently test `TOKEN = ''`, but not others. +However, this only avoids the (theoretical) test and does not fix the solution. For example, a test with the inputs `[1, 2]` and `[5, "1|2|", 7]` would now fail. -[gen-exp]: https://www.programiz.com/python-programming/generator \ No newline at end of file +No matter what separator is chosen, there will always be at least one input for which the function will return the wrong result. **This is why no approach that converts the lists to strings can ever be correct for all possible inputs.** diff --git a/exercises/practice/sublist/.approaches/using-strings/snippet.txt b/exercises/practice/sublist/.approaches/using-strings/snippet.txt index 26fc3ec0ec7..4d4b6439294 100644 --- a/exercises/practice/sublist/.approaches/using-strings/snippet.txt +++ b/exercises/practice/sublist/.approaches/using-strings/snippet.txt @@ -1,8 +1,8 @@ -# Failing approach +# WARNING: Failing approach def sublist(list_one, list_two): list_one_check = str(list_one).strip("[]") ... - elif list_one_check in list_two_check: + if list_one_check in list_two_check: return SUBLIST ... return UNEQUAL \ No newline at end of file