Katkat

Implement Adjacent-Line Uniq

VantaSoftware Engineer · Onsite

Last 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:

  1. Case-folding option (ignore_case): When True, lines are grouped if their case-folded representations are identical. When False, exact case matching is required.
  2. 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.
  3. Preserving original line: The representative line emitted for each group must be the first original spelling encountered in that consecutive run.
  4. 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 is False.
    • mode: A string among "all", "repeated", or "unique". Default is "all".
  • Output:
    • A list of tuples (line, count) where line is the original string from the first occurrence in the run, and count is the number of consecutive occurrences.

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^5
  • 0 <= len(line) <= 10^4 for each line in lines
  • mode is 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 by mode).
  • 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 'ß')?