Implement Adjacent-Line Uniq
VantaSoftware Engineer · OnsiteLast updated
Implement a Unix-style adjacent-line deduplication function that groups consecutive identical lines, supports case-insensitive matching, and filters output based on run counts in linear time.
Problem Statement
Implement an adjacent-line deduplication utility similar to the Unix uniq command. Unlike standard set-based deduplication, uniq operates strictly on consecutive adjacent lines without prior sorting.
Write a function adjacent_uniq that processes an iterable of strings and groups consecutive lines sharing the same key. The function should support:
- Case-folding option (
ignore_case): WhenTrue, lines are grouped if their case-folded representations are identical. WhenFalse, exact case matching is required. - Run modes (
mode):"all": Output all consecutive groups."repeated": Output only groups that appear two or more times consecutively."unique": Output only groups that appear exactly once.
- Preserving original line: The representative line emitted for each group must be the first original spelling encountered in that consecutive run.
- Run count: Each emitted entry should include both the representative string and its consecutive run count:
(line, count).
Your solution must run in O(N) time and operate cleanly over the input.
Function Signature
def adjacent_uniq(
lines: list[str],
ignore_case: bool = False,
mode: str = "all"
) -> list[tuple[str, int]]:
pass
Input & Output Format
- Input:
lines: A list of strings.ignore_case: A boolean indicating whether to compare lines case-insensitively (True) or strictly (False). Default isFalse.mode: A string among"all","repeated", or"unique". Default is"all".
- Output:
- A list of tuples
(line, count)wherelineis the original string from the first occurrence in the run, andcountis the number of consecutive occurrences.
- A list of tuples
Examples
Example 1
lines = ["apple", "Apple", "apple", "banana", "banana", "cherry"]
# Exact matching, all runs
adjacent_uniq(lines, ignore_case=False, mode="all")
# Output: [("apple", 1), ("Apple", 1), ("apple", 1), ("banana", 2), ("cherry", 1)]
# Case-insensitive, all runs
adjacent_uniq(lines, ignore_case=True, mode="all")
# Output: [("apple", 3), ("banana", 2), ("cherry", 1)]
Explanation: When ignore_case=True, the first three lines match case-insensitively and form a run of 3 starting with "apple".
Example 2
lines = ["a", "A", "b", "c", "c", "d"]
# Case-insensitive, only repeated
adjacent_uniq(lines, ignore_case=True, mode="repeated")
# Output: [("a", 2), ("c", 2)]
# Case-insensitive, only unique
adjacent_uniq(lines, ignore_case=True, mode="unique")
# Output: [("b", 1), ("d", 1)]
Constraints
0 <= len(lines) <= 10^50 <= len(line) <= 10^4for eachlineinlinesmodeis guaranteed to be one of"all","repeated", or"unique".
Edge Cases
- Empty input list returns an empty list
[]. - All lines identical should yield a single run of length
N(filtered appropriately bymode). - Alternating patterns (e.g.,
["a", "b", "a"]) do not merge across non-adjacent occurrences.
Hints
Hint 1
Maintain the current group's normalized comparison key, its original representative string, and an integer count as you iterate through the list.
Hint 2
Derive the comparison key separately from the representative string so that you preserve the first occurrence's exact casing while comparing normalized values.
Hint 3
When a key change occurs or when reaching the end of the input, check if the accumulated count satisfies the requested mode before adding to the result.
Follow-up questions
- How would you refactor this to support streaming large inputs (e.g., a generator yielding lines) with O(1) auxiliary memory?
- How would you support Unix uniq's field-skipping (`-f`) or character-skipping (`-s`) options?
- How does Python's `str.lower()` differ from `str.casefold()` when dealing with multi-lingual Unicode input (e.g., German 'ß')?
Solution walkthrough is for members
Get the full walkthrough with reference code and complexity analysis, plus Kitty Coach at 300 messages a day. Hints, the code runner and community answers stay free.