diff --git a/project_enigma/assumptions.md b/project_enigma/assumptions.md new file mode 100644 index 0000000..ce9a353 --- /dev/null +++ b/project_enigma/assumptions.md @@ -0,0 +1,99 @@ +# The Detective's Log — `assumptions.md` + +The client's note described the format only loosely. Below are the "hidden +rules" I reverse-engineered from the test cases, and the reasoning behind each. + +## The core structure (the breakthrough) + +The note says: *"a number indicating the count of values measured in each +measurement cycle, followed by the measured values."* + +The decisive clue was `("dz_a_aazzaaa", [28, 53, 1])`. No simple prefix of that +string sums to 28, but it falls out cleanly if you read the stream as repeating +**cycles**: + +``` +[count C][number_1][number_2] ... [number_C] -> sum of those C numbers +``` + +So each cycle reads one **count**, then sums the next **C numbers** to produce +one output value, and repeats until the stream is consumed. + +Walking through `dz_a_aazzaaa`: + +| Step | Count | Numbers read | Sum | +|-------|----------|------------------------------|--------| +| 1 | `d` = 4 | `z_`=26, `a`=1, `_`=0, `a`=1 | **28** | +| 2 | `a` = 1 | `zza` = 26+26+1 | **53** | +| 3 | `a` = 1 | `a` = 1 | **1** | + +This same model reproduces `("abbcc", [2, 6])` (count 1 → `b`; count 2 → `cc`) +and `("abcdabcdab", [2, 7, 7])` (count 1 → `b`; count 3 → `dab`; count 3 → `dab`). + +## Rule 1 — letter values + +`a`=1, `b`=2, … `z`=26, exactly as stated. + +## Rule 2 — `z` is an accumulator ("a number > 26") + +The note: *"Numbers higher than 26 are encoded with multiple characters that are +added together [and] terminated with the first non-'z' character."* + +I read this as: a single **number** is a run of zero-or-more `z`s (each worth +26) followed by **one** terminating non-`z` character that adds its own value +and ends the number. + +- `a` → 1 +- `zz` + `a` → 26 + 26 + 1 = 53 (confirmed by case 3) +- `z` + `d` → 26 + 4 = 30 (the count in case 7 → reads 30 numbers + that sum to 34) + +## Rule 3 — the count is itself a number + +The count is read with the *same* z-accumulation logic, which is why +`zdaaaa...` (case 7) yields a single value: `z`+`d` = a count of **30**, then +the next 30 letters (26 `a`s + 4 `b`s) are summed → **34**. + +## Rule 4 — undefined characters (`_`, spaces, anything non a–z) = 0 + +Every test case treats `_` as contributing nothing. Examples: +`("a_", [0])` → count `a`=1, next number `_`=0 → value 0. In value position an +underscore is simply a zero (`("aab___", [1, 0, 0])` → count 2 reads `__` = 0). + +## Rule 5 — a count of 0 terminates the package (emitting one `0`) + +This was the subtlest rule, forced by three cases that would otherwise produce +extra output: + +- `("_ad", [0])` — if `_`=0 in the count position only paused, `ad` would + decode to `4` and we'd get `[0, 4]`. It doesn't, so the package **stops**. +- `("__", [0])` and `("_zzzb", [0])` — same: the leading `_` (count 0) emits a + single `0` and ends decoding; the rest of the string is ignored. + +So: when the count evaluates to 0, append one `0` and terminate the current +package. (Counts coming from letters are always ≥ 1, so this never fires +mid-stream for "real" data.) + +## Rule 6 — space separates independent packages + +`("_ _", [0, 0])` is the only case with a space, and it cannot come from a +single package (the leading `_` would stop after the first `0`). It only works +if the space splits the input into two packages `"_"` and `"_"`, each decoding +to `[0]`, concatenated. So a space is a hard package boundary. + +## Rule 7 — empty / exhausted input + +- `("", [])` — empty input yields an empty list. +- If a cycle's count asks for more numbers than remain, we sum what's left and + stop (the loop ends naturally when the stream is exhausted). + +## Summary of the decision table + +| Situation | Behaviour | +|--------------------------------------------|------------------------------------| +| `a`–`y` in number position | adds 1–25, ends the number | +| `z` in number position | adds 26, continues the run | +| `_` / space / undefined in number position | adds 0, ends the number | +| count evaluates to `0` | emit one `0`, terminate package | +| space between packages | start a fresh package, concatenate | +| empty string | `[]` | diff --git a/project_enigma/clarifications.md b/project_enigma/clarifications.md new file mode 100644 index 0000000..fd55537 --- /dev/null +++ b/project_enigma/clarifications.md @@ -0,0 +1,77 @@ +# The Interrogation — `clarifications.md` + +The current implementation passes all provided test cases, but several rules +were inferred from a single example each, and a few cases look contradictory. +Below are the questions I'd put to the original engineer, grouped by risk. + +## A. Contradictions in the existing test cases + +1. **Does a `0` count terminate the whole package, or just emit a zero and + continue?** + - `("_ad", [0])` and `("__", [0])` imply that a `0` count **stops** decoding + entirely — the trailing `ad` / `_` are discarded. + - But `("aab___", [1, 0, 0])` shows decoding *continuing* through underscores + when they sit in a **value** position (the `b`=2 count consumes `__`), + before a final `0` count closes it out. + - I resolved this as "**`0` in the count position terminates**; `0` in a value + position is just a zero." Is that the intended distinction, or should a + malformed/zero count instead be skipped, or raise an error? + +2. **Is a space a package separator, or a value?** + - `("_ _", [0, 0])` only works if space splits the stream into two packages. + Yet the note never mentions spaces or multiple packages in one string. + - Confirm: can a single input legitimately contain **multiple packages** + separated by spaces? Are other separators (newline, comma) also valid? + Should leading/trailing/double spaces produce empty packages? + +3. **What is the count actually counting — characters or numbers?** + - With single-letter values these are indistinguishable, but case 3 proves it + counts **numbers** (so `zza` counts as *one* item worth 53, not three). + - Please confirm the count is "number of measured *values* in the cycle," + each value being a full z-accumulated number. + +## B. Underspecified behaviour (inferred from one example) + +4. **Undefined characters.** I map `_`, space, and anything outside `a–z` to + `0`. Is `0` the correct sentinel, or should unknown bytes be rejected as + corrupt data? Are there other meaningful symbols (digits, uppercase) the + real feed can contain? + +5. **A `z`-run with no terminator** (e.g. a value ending in `...zz`). The note + says a number ends at "the first non-`z` character," but gives no rule when + the stream ends first. I currently take the accumulated `26 × n`. Should a + dangling `z`-run instead be an error, or be ignored? + +6. **Count larger than the remaining numbers.** If a cycle declares a count of + 5 but only 2 numbers remain, I sum the 2 available and stop. Should this + instead be flagged as a truncated/invalid package? + +7. **Is there an upper bound on a value or a count?** z-accumulation makes both + unbounded (`zzzz...` keeps adding 26). Real measurement hardware presumably + has a max range — should we validate against it? + +## C. Production-hardening questions + +8. **Error strategy.** For a production decoder, do you want lenient behaviour + (best-effort decode, as now) or strict validation that raises on anything + that doesn't match the grammar? Lenient decoding can silently mask corrupt + telemetry. + +9. **Empty vs. zero.** `("", [])` returns an empty list while `("_", [0])` + returns `[0]`. Is "no measurements" (`[]`) semantically different from "one + measurement of zero" (`[0]`) downstream? This matters for averaging/billing. + +10. **Round-trip / encoder.** Is there a matching *encoder*, and must + `decode(encode(x)) == x`? If so, the ambiguous cases above (zero counts, + spaces) need a single canonical encoding so the round-trip is stable. + +11. **Character set & encoding.** Is the input guaranteed ASCII lowercase, or + could it arrive as Unicode / different case / a byte stream? Confirm so we + can validate input at the boundary. + +## Recommended production defaults (pending answers) + +Until the above are confirmed, I'd propose treating a malformed package +(unterminated `z`-run, count exceeding available numbers, unexpected symbol) as +a **validation error** surfaced to the caller rather than silently coerced — so +corrupt telemetry is caught, not averaged into the metrics. diff --git a/project_enigma/decoding_utils.py b/project_enigma/decoding_utils.py index 775b218..14cdaff 100644 --- a/project_enigma/decoding_utils.py +++ b/project_enigma/decoding_utils.py @@ -9,8 +9,43 @@ def decode_measurements(encoded_string: str) -> list[int]: Returns: list[int]: The list of decoded integers. """ - pass # Remove this pass and place your logic here to decode the string into a list of integers based on the specified encoding rules. - return [] # Placeholder return statement; replace with actual decoding logic. + # A "number" is z-accumulated: each 'z' adds 26 and the run continues; the + # first non-'z' char adds its value (a..z -> 1..26, anything else -> 0) and + # ends the number. Returns (value, next_position). + def read_number(s, pos): + total = 0 + while pos < len(s) and s[pos] == "z": + total += 26 + pos += 1 + if pos < len(s): + ch = s[pos] + total += ord(ch) - ord("a") + 1 if "a" <= ch <= "z" else 0 + pos += 1 + return total, pos + + # Decode one space-free package: read a count, then sum that many numbers. + def decode_package(s): + values = [] + pos = 0 + while pos < len(s): + count, pos = read_number(s, pos) + if count == 0: # undefined char in count position -> emit 0, stop + values.append(0) + break + value = 0 + for _ in range(count): + if pos >= len(s): + break + num, pos = read_number(s, pos) + value += num + values.append(value) + return values + + # A space separates independent packages; concatenate their results. + result = [] + for package in encoded_string.split(" "): + result.extend(decode_package(package)) + return result if __name__ == "__main__": @@ -37,4 +72,4 @@ def decode_measurements(encoded_string: str) -> list[int]: for encoded, expected in test_cases: result = decode_measurements(encoded) status = "PASS" if result == expected else "FAIL" - print(f"{status}: decode_measurements({encoded!r}) = {result} (expected {expected})") + print(f"{status}: decode_measurements({encoded!r}) = {result} (expected {expected})") \ No newline at end of file