FastPrepStateful Log Transform and Search

Stateful Log Transform and Search

Rippling logoRippling● MediumFULLTIMEPHONE SCREEN

Problem statement

Process an ordered operation sequence for a logger that stores transformed logs in insertion order.

  • ["REMOVE", log, target]: delete every non-overlapping literal occurrence of the non-empty target from left to right, store the result, and output its zero-based insertion index.
  • ["TRUNCATE", log, n]: keep the first n characters, store the result, and output its zero-based insertion index.
  • ["CAPITALIZE", log]: convert every ASCII lowercase letter to uppercase, leave all other characters unchanged, store the result, and output its zero-based insertion index.
  • ["SEARCH", deduplicate, keyword1, ...]: use case-sensitive literal-substring OR matching over stored logs. Visit logs in insertion order. If deduplicate is true, output each matching log once. If it is false, output the log once for each distinct input keyword it contains, in keyword-input order. Repeated keyword arguments count once.

The method returns one row per operation. A write row contains its insertion index as one decimal string. A search row contains its ordered matching logs.

Independent handlers

Also support configurable handlers. A log call sends the same original message to each currently registered handler in registration order; handlers are independent, not a transformation pipeline. This finite operation adapter captures printed output and private array storage:

  • ["ADD_HANDLER", "FILTER", target]: register a printing handler that removes non-overlapping literal occurrences of the non-empty target, scanning the original message left to right. Do not repeatedly remove newly formed matches.
  • ["ADD_HANDLER", "TRUNCATE", n]: register a printing handler that keeps the first min(n, message.length) characters.
  • ["ADD_HANDLER", "UPPERCASE"]: register a printing handler that converts ASCII lowercase letters to uppercase.
  • ["ADD_HANDLER", "ARRAY"]: register a handler that appends the original message to its own private array and prints nothing.
  • Each ADD_HANDLER outputs a one-string row with the zero-based handler ID, counting all handler kinds.
  • ["LOG", message]: invoke every currently registered handler and output a row containing only this call's printed strings in handler order. Empty printed strings are entries; an array-only call or a call with no handlers returns an empty row.
  • ["READ_ARRAY", handlerId]: return a snapshot of that ARRAY handler's messages in arrival order. The ID always names an existing ARRAY handler. Later log calls must not change an earlier output row.
  • ["SEARCH_ARRAY", handlerId, deduplicate, keyword1, ...]: search the messages currently held by that ARRAY handler, in arrival order. Use the same case-sensitive literal-substring OR matching and the same deduplicate output rules as SEARCH. The ID always names an existing ARRAY handler, and the row is a snapshot.

Multiple handlers of any kind are allowed. Newly registered handlers receive only future log calls. Each ARRAY handler has separate storage. The legacy transformed-log store used by REMOVE, TRUNCATE, CAPITALIZE and SEARCH is separate from these private arrays: legacy writes do not invoke handlers, LOG does not write to that store, SEARCH reads only that store, and READ_ARRAY and SEARCH_ARRAY read only the named handler's array. This separation preserves the original operations while exposing the independent-handler task.

Keep handler creation separate from the core logger dispatcher. A handler should be injectable through one common callable or interface so that adding a new handler does not change the LOG loop. Discuss extending it with a database handler that opens a new connection for each log call and a remote-API handler, manual dependency injection, and configuration or repeated-work optimizations. Also discuss how search should treat punctuation if keywords were matched as whole words instead of substrings, for example whether retry should match retry.. These are design follow-ups; the judged method performs no external I/O.

Function

processLogs(operations: String[][]) → String[][]

Examples

Example 1

operations = [["REMOVE","banana bandana","ana"],["CAPITALIZE","Mix 42"],["TRUNCATE","abcdef","3"],["SEARCH","true","A","band"],["SEARCH","false","a","b"]]return = [["0"],["1"],["2"],["bna band"],["bna band","bna band","abc","abc"]]

Removing non-overlapping ana occurrences stores bna band. The deduplicated search returns it once. The non-deduplicated search emits it for both a and b, then emits abc for those two keywords.

Example 2

operations = [["CAPITALIZE","alpha"],["TRUNCATE","alphabet","5"],["REMOVE","aaaa","aa"],["SEARCH","true","alpha","ALP"],["SEARCH","false","a","aa"]]return = [["0"],["1"],["2"],["ALPHA","alpha"],["alpha"]]

Search is case-sensitive. The empty stored log matches neither keyword. With deduplication disabled, lowercase alpha contains a but not aa.

Example 3

operations = [["TRUNCATE","hello","0"],["SEARCH","true","x"],["REMOVE","xxxyxx","xx"],["SEARCH","false","x","x","y"]]return = [["0"],[],["1"],["xy","xy"]]

Truncating to zero stores an empty log. Removing xx left to right from xxxyxx stores xy. Repeated keyword x counts once, so xy is emitted for distinct keywords x and y.

Example 4

operations = [["ADD_HANDLER","FILTER","world"],["ADD_HANDLER","TRUNCATE","5"],["ADD_HANDLER","UPPERCASE"],["ADD_HANDLER","ARRAY"],["LOG","hello world"],["READ_ARRAY","3"]]return = [["0"],["1"],["2"],["3"],["hello ","hello","HELLO WORLD"],["hello world"]]

All three printing handlers receive hello world independently, and ARRAY retains the unchanged original. Its handler ID is 3 because IDs count every registered handler.

Example 5

operations = [["ADD_HANDLER","ARRAY"],["ADD_HANDLER","UPPERCASE"],["LOG","disk full, retry"],["LOG","retry ok"],["ADD_HANDLER","ARRAY"],["LOG","Disk ok"],["SEARCH_ARRAY","0","true","retry","disk"],["SEARCH_ARRAY","0","false","retry","disk"],["SEARCH_ARRAY","2","true","ok"],["SEARCH","true","ok"]]return = [["0"],["1"],["DISK FULL, RETRY"],["RETRY OK"],["2"],["DISK OK"],["disk full, retry","retry ok"],["disk full, retry","disk full, retry","retry ok"],["Disk ok"],[]]

Handler 0 stores both early messages unchanged, while the UPPERCASE handler prints them. Handler 2 is registered later, so it stores only Disk ok. Search is case-sensitive, so disk does not match Disk ok. With deduplication off, disk full, retry appears once for each of its two keywords. SEARCH reads only the legacy store, which is still empty.

Constraints

  • 1 <= operations.length <= 10^4.
  • Logs, non-empty removal targets, and keywords contain at most 200 printable ASCII characters.
  • Each truncation length is between 0 and the corresponding input-log length.
  • Each SEARCH or SEARCH_ARRAY supplies at least one keyword. Its deduplicate value is true or false.
  • The sum of stored log count * distinct keyword count across all SEARCH operations, plus searched array size * distinct keyword count across all SEARCH_ARRAY operations, is at most 2 * 10^5.
  • At most 20 handlers are registered. Handler filter targets contain 1 through 200 printable ASCII characters. Handler truncation limits are integers from 0 through 200; the earlier bound relative to the input-log length applies to legacy TRUNCATE writes.
  • LOG messages contain 0 through 200 printable ASCII characters. Each operation has the described fields and valid handler references. The returned output contains at most 2 * 10^5 string entries; this extra bound applies to inputs containing the added handler operations.

More Rippling problems

See Rippling hiring insights
Saved
public String[][] processLogs(String[][] operations) {
  // write your code here
}
operations[["REMOVE","banana bandana","ana"],["CAPITALIZE","Mix 42"],["TRUNCATE","abcdef","3"],["SEARCH","true","A","band"],["SEARCH","false","a","b"]]
expected[["0"], ["1"], ["2"], ["bna band"], ["bna band", "bna band", "abc", "abc"]]
Checking account…