Katkat

Restaurant Listing CSV Linter: Required, Length, Banned Terms, Overlap

StripeSoftware Engineer · Technical Screen

Last updated

Write a configurable linter for restaurant-listing CSV rows that reports missing fields, length violations, whole-word banned terms, and too-low or too-high word overlap between fields after normalization. Tests robust CSV parsing, text normalization, rule-driven design and edge-case thinking.

A food-delivery marketplace onboards restaurants from partner CSV exports. Before a batch is imported, a linter checks every row and reports which rules it breaks. The linter only reports problems. It never fixes, drops, or reorders rows.

def lint(csv_text: str, cfg: LintConfig) -> list[tuple[int, list[str]]]:
    """One entry per data row: (1-based data row number, failure codes in rule order)."""

All the rule parameters live in a config object, and nothing is hard-coded:

@dataclass
class LintConfig:
    required: list[str]                          # fields that must be non-blank
    lengths: dict[str, tuple[int, int]]          # field -> (min, max) after trimming
    banned: list[str]                            # words or phrases, matched as whole words
    ignore_words: set[str]                       # dropped before similarity comparison
    similarity: list[tuple[str, str, float | None, float | None]]  # (field_a, field_b, min, max)

Part 1: field checks

For every row, in this order:

  1. MISSING:<field> for each required field that is absent or only whitespace.
  2. LENGTH:<field> for each field in lengths that is present but whose trimmed length is outside [min, max].
  3. BANNED:<field>:<term> for each non-blank field and each banned term found in it. Matching is case-insensitive and whole-word, so "best" matches "Best tools" but not "bestseller". A phrase like "money back" matches across any spacing or punctuation.

The CSV has a header row. Quoted values can contain commas, so use a real CSV parser. A row with the wrong number of columns reports only MALFORMED.

Part 2: similarity between fields

Normalize text by Unicode NFKC, casefold, replace every non-alphanumeric character with a space, split, and remove ignore_words. Compare the two word sets with Jaccard similarity (|A∩B| / |A∪B|). For each (a, b, lo, hi) rule where both fields are non-blank and both word sets are non-empty: report LOW_OVERLAP:a~b if the similarity is below lo, and HIGH_OVERLAP:a~b if it is above hi.

Example

id,display_name,legal_name,tagline
101,Blue Fern Bakery,Blue Fern Bakery LLC,"Sourdough, pastries, and coffee"
102,  ,Harbor Tools Inc.,Best tools
103,QuickFix,Northside Appliance Repair Co,Guaranteed results or your money back
104,Moss & Stone,"moss and stone, ltd",Moss & Stone
105,Lone Row
cfg = LintConfig(
    required=["id", "display_name", "legal_name"],
    lengths={"display_name": (2, 30), "tagline": (5, 40)},
    banned=["guaranteed", "money back", "best"],
    ignore_words={"llc", "inc", "co", "ltd", "and", "the"},
    similarity=[("display_name", "legal_name", 0.5, None),
                ("tagline", "display_name", None, 0.8)],
)
lint(text, cfg) == [
    (1, []),
    (2, ["MISSING:display_name", "BANNED:tagline:best"]),
    (3, ["BANNED:tagline:guaranteed", "BANNED:tagline:money back",
         "LOW_OVERLAP:display_name~legal_name"]),
    (4, ["HIGH_OVERLAP:tagline~display_name"]),
    (5, ["MALFORMED"]),
]

Hints

Hint 1

Use the csv module. A quoted value with commas in it will break a hand-written split.

Hint 2

Write one normalization function (NFKC, casefold, punctuation to spaces, split) and use it for both banned terms and overlap.

Hint 3

To match a phrase as whole words, compare word tuples against contiguous slices of the field's word list.

Hint 4

Decide what happens when a word set is empty after removing ignored words, before you divide by zero.

Follow-up questions

  • The file is 20 GB. How do you stream it and still report results in order?
  • Product wants fuzzy banned-term matching (e.g. 'guarant33d'). How would you approach it without too many false positives?
  • How would you let non-engineers add new rule types through config alone?
  • How would you report column positions or suggested fixes alongside each failure code?