~/problems / Arrays & hashing / Hash maps and counting

Turn a tick stream into table rows

medium ~25 min Jane Street

A pricing desk receives quotes one at a time as (timestamp, code, value) records, where code names an instrument. A downstream model wants a table instead: one row per timestamp, with one column per instrument it cares about, columns sorted alphabetically by code.

Implement class RowTransformer:

  • RowTransformer(codes: list[str]): the registered codes (duplicates in the list count once). Records for any other code are ignored completely.
  • columns() -> list[str]: the registered codes in sorted order.
  • process(timestamp: int, code: str, value: float) -> list | None: feed one record. If it completes a row, return that row; otherwise return None.
  • flush() -> list | None: the stream is pausing; return the row that is still being built (or None if there isn't one).

A row is [timestamp, v_1, v_2, ..., v_C], with the values in columns() order. (In C++ and Java a row is a list of nullable doubles, so the timestamp comes back as a double in slot 0. Timestamps fit in 32 bits, so this is exact.)

Rules:

  • Records mostly arrive in non-decreasing timestamp order, grouped by timestamp; the exception is late records (below). The row for timestamp t is open from its first record until a record with a larger timestamp arrives (that call returns the finished row for t and opens the next one) or until flush() returns it.
  • A code with several records at the same timestamp takes the last value.
  • A code with no record at timestamp t carries forward its most recent value from an earlier timestamp, or None if it has never had one.
  • A late record, whose timestamp is below the open row's timestamp or, when no row is open, not above the last returned row's timestamp, is ignored and process returns None.
  • After flush(), the next record with a larger timestamp opens a new row as usual (and returns None, since nothing was open).
rt = RowTransformer(["MSFT", "AAPL", "IBM"])
rt.columns()                    # ["AAPL", "IBM", "MSFT"]
rt.process(1, "MSFT", 410.0)    # None
rt.process(1, "AAPL", 190.5)    # None
rt.process(1, "TSLA", 250.0)    # None  (not registered)
rt.process(2, "AAPL", 191.0)    # [1, 190.5, None, 410.0]
rt.process(2, "AAPL", 191.25)   # None  (replaces 191.0)
rt.process(1, "IBM", 170.0)     # None  (late, ignored)
rt.process(4, "IBM", 171.0)     # [2, 191.25, None, 410.0]
rt.flush()                      # [4, 191.25, 171.0, 410.0]
rt.flush()                      # None

Constraints: up to 10,000 codes and 200,000 records. Each process call must be O(1) on average except for building a finished row, which is O(C). Don't search or sort the code list per record.

Show hint

Map each registered code to its column index once, in the constructor. Keep one list of "latest value per column"; a finished row is just the timestamp plus a copy of that list.

Topic: Hash maps and counting. Counter/defaultdict, complement lookups, prefix-sum + hash map.

Read the visual guide
0:00
Ctrl ' run · Ctrl ↵ submit
esc