Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 99 additions & 0 deletions project_enigma/assumptions.md
Original file line number Diff line number Diff line change
@@ -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 | `[]` |
77 changes: 77 additions & 0 deletions project_enigma/clarifications.md
Original file line number Diff line number Diff line change
@@ -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.
41 changes: 38 additions & 3 deletions project_enigma/decoding_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__":
Expand All @@ -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})")