skip to content

How do you mask the leading digits of a fixed-length record when strings are immutable?

level: middleimportance: nice to knowfreq 34%

answer

  1. the length never changes here
  2. immutable means whole-value replacement only
  3. what does one slice-and-concatenate copy?
  4. copy out once, edit, copy back once
  5. cost independent of how many positions change

basics

~20 s

Copy the record once into a mutable character array, overwrite the digits in place at O(1) each, then convert back once. That is two copies per record instead of one full copy per masked character.

solid answer

~40 s

Immutable strings offer no way to overwrite a position, so the tempting approach — rebuilding the record with a slice-and-concatenate per digit — allocates and copies the entire record once per masked character, giving O(k * d) work for a record of length k with d masked digits. Instead, materialise the record once into a mutable character array, write the mask character directly at each index (constant time per write, no allocation), and convert the array back to a string once at the end. That is exactly two linear copies per record regardless of how many positions you change. Note this is not a builder problem: the record's length never changes, so growable capacity buys nothing. A fixed-size mutable array is the structure that matches the operation — random-position writes on a known length.

code

pseudocode · 6 lines
pseudocode
chars = to_mutable_array(record)          // one copy, O(k)
visible = 4                               // keep the last 4 characters
for i in 0..length(chars) - visible - 1
    chars[i] = '*'                        // in place, O(1), no allocation
...
return from_array(chars)                  // one copy, O(k)

go deeper

for a junior

Know that immutable strings cannot be edited at a position, and that the standard workaround is to copy the characters into a mutable array, change them there, and build a new string once at the end.

for a middle

Explain the cost difference: one slice-and-concatenate per masked position copies the whole record each time, while the array round trip costs two linear copies no matter how many positions change. Say why growable capacity is irrelevant when the length is fixed.

for a senior

Demonstrate the judgment that the structure should match the operation — appending an unknown amount versus writing at known positions of a known length — and raise the handling of the extra mutable copy of sensitive data as part of the design.

for a principal

Own the guidance your team follows: when a bulk transformation should materialise into mutable form once versus stream through, and how redaction is treated as a data-handling control with lifetime rules rather than as output formatting.

## The task A fixed-length record — say a 16-character account number inside an export file — must be redacted before it leaves the system: every character except the last four becomes a mask character. Ten million records go through this path. ## The wrong shape, and why it is quadratic-ish With immutable strings the naive approach is to rebuild the record for each position: ``` for each masked index i: record = prefix_of(record, i) + "*" + suffix_from(record, i+1) ``` Every line of that loop allocates a new record of length k and copies all k characters. Masking d positions therefore costs Θ(k * d) time and produces d throwaway records. For a 16-character number with 12 masked positions that is 192 character copies plus a dozen allocations to change 12 characters. Over ten million records, the allocation churn alone is the story. This is the same disease as building a document with repeated concatenation, transplanted: **the operation you want is a single-character write, but the data type only offers whole-value replacement.** ## The right shape Copy out once, edit in place, copy back once: 1. Materialise the record's characters into a mutable array — one linear copy. 2. Write the mask character directly at each index — constant time each, no allocation. 3. Build the result string from the array — one linear copy. Total: 2k character copies and 2 allocations per record, independent of how many positions you edit. Masking one character or fifteen costs the same. That is the whole trick, and it generalises to any bulk in-place transformation of a string: normalising case over a scanned span, swapping a delimiter, zero-filling a field, reversing a segment. ## Why not a growable buffer? Because redaction does not change the length. A growable buffer exists to solve one problem — you do not know the final size, so it manages capacity and reallocation for you. Here the final size is exactly the input size, and every write lands at a position you already know. Its growth machinery would sit unused, and (depending on the design) it may not even expose the random-position writes you actually need. Reaching for a builder here is a symptom of pattern-matching on "strings are immutable, so use a builder" rather than on the operation being performed. The general rule worth taking away: **builders are for appending an unknown amount; a fixed mutable array is for editing a known amount.** They are not competitors. ## Cost in context For a ten-million-record file, the copy-out/copy-back pair costs 2k character copies per record — the same order as the cost of *reading* the record from input in the first place. The redaction is therefore essentially free relative to the I/O, whereas the slice-and-concatenate version multiplies the per-record work by the number of masked positions and swamps it. When an interviewer asks you to justify the rewrite, this is the argument: not "the array is faster", but "the array makes the per-record cost independent of the number of edits". ## Two cautions **Index arithmetic assumes fixed-width units.** Writing at index i is only meaningful if index i identifies the character you think it does; with variable-width text encodings, positions in the underlying units and positions in characters diverge. For a digit-only fixed-format record that is not an issue, but it is why bulk in-place edits are safe on structured fixed-width fields and risky on free text. **The mutable array is a real copy of sensitive data.** Redaction implies the original was sensitive; you now hold a second mutable copy of it. Overwriting the array's contents when you are done — rather than merely dropping the reference — is the difference between redaction as an output formatting step and redaction as a data-handling control. ## Ecosystem note Whether you need this workaround at all depends on the string type you hold. C++ and Rust give you a mutable, in-place-writable string buffer, so the copy-out step is unnecessary; Java, Python and JavaScript make strings immutable, so the round trip through a mutable array is the standard shape. The algorithmic point — one copy out, k constant-time writes, one copy back — is identical in every case.

  • Why not use a growable character buffer for the redaction instead?
    Because nothing grows. A growable buffer earns its keep when the final length is unknown and it must manage capacity and reallocation; here the output length equals the input length and every write lands at a known index. Its growth machinery would go unused, and the operation you actually need is random-position writing. A fixed mutable array of the record's length expresses that precisely.
  • How do you justify the rewrite to a reviewer on a ten-million-record file?
    The array version costs two linear copies per record, regardless of how many positions are masked — the same order as reading the record from input, so it is effectively free. The slice-and-concatenate version multiplies per-record work by the number of masked positions and allocates a throwaway record for each. The argument is that cost stops scaling with the number of edits, not that the array is inherently faster.

Correcting a page with a pen versus retyping the page for every correction. Both end up correct; only one of them scales with the number of corrections.

saying these in an interview costs you the question

  • Thinks you can assign to a character position of an immutable string
  • Rebuilds the record once per masked character
  • Reaches for a growable buffer when the length never changes
  • Assumes the copy out and back costs as much as it saves
  • Ignores that the mutable array is a second copy of sensitive data

context