Coding Interview Workshop · Lesson 1

Clarify the contract before aggregating

Aggregate quantities by an exact ordinal key.

Clarify the observable contract

Aggregate quantities by an exact ordinal key. Empty keys and negative quantities are invalid; zero is permitted. Repeated keys add into a checked long total. The result does not promise dictionary enumeration order, so the display explicitly sorts keys. This contract distinguishes Tea from tea rather than silently applying a culture or a case-folding rule.

The useful first questions are whether case distinguishes products, whether negative rows represent corrections, and whether the result order matters. Record those answers before choosing a data structure. Here a dictionary represents membership and totals, while the final ordering is a presentation step with its own cost and comparer.

Walk through the verified program

The first tea contributes two, Tea creates another bucket with one, and the second tea adds three to the original bucket. Sorting ordinal keys displays Tea before tea. A separate negative row throws the documented argument exception, which the harness turns into negative rejected:true. The input row array is never modified.

A loop invariant makes the reasoning concise: after each accepted row, every stored total equals the sum of quantities for that exact key in the processed prefix. TryGetValue supplies zero only when no entry exists. Publishing the dictionary after the loop means a rejected row cannot expose a half-finished return value.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;

var rows = new[] { new Row("tea", 2), new Row("Tea", 1), new Row("tea", 3) };
var totals = Aggregate(rows);
foreach (var row in totals.OrderBy(x => x.Key, StringComparer.Ordinal)) Console.WriteLine(row.Key + ":" + row.Value);
bool invalid = false; try { Aggregate(new[] { new Row("tea", -1) }); } catch (ArgumentOutOfRangeException) { invalid = true; }
Console.WriteLine("negative rejected:" + invalid.ToString().ToLowerInvariant());
static Dictionary<string,long> Aggregate(IEnumerable<Row> rows) {
    var totals = new Dictionary<string,long>(StringComparer.Ordinal);
    foreach (var row in rows) {
        if (string.IsNullOrEmpty(row.Key) || row.Quantity < 0) throw new ArgumentOutOfRangeException(nameof(rows));
        totals.TryGetValue(row.Key, out long previous); totals[row.Key] = checked(previous + row.Quantity);
    }
    return totals;
}
sealed record Row(string Key, int Quantity);

Account for time, storage and allocations

For n rows and u distinct keys, aggregation uses expected O(n) dictionary operations and O(u) stored totals; string hashing also examines key text. Display sorting costs O(u log u) comparisons and additional sorting storage. Checked long arithmetic rejects overflow rather than silently changing an otherwise valid total.

Count key storage and the final ordered enumeration separately from the row array supplied by the caller. The algorithm does not clone each key string, but retaining dictionary entries retains references to those strings. If key lengths vary widely, describing work only in row count hides significant hashing and comparison work.

Review assumptions and likely defects

Case normalization, negative adjustments and blank-key handling are business decisions, not harmless implementation details. Returning a partially filled dictionary after an exception would create a different contract. Local construction makes failure discard the unpublished result; it does not provide a database transaction or a thread-safe aggregation service.

A review should also ask whether the caller can supply an infinite sequence or mutate row sources during enumeration. The sample assumes finite, stable input and is deliberately synchronous. These are reasonable interview preconditions when stated; they become bugs only when the implementation is advertised with stronger guarantees than it provides.

Practice one: discriminate edge cases

Add zero-quantity rows and an empty key as separate cases. Predict that zero preserves the total while an empty key fails. Hint: validity and contribution are different questions, so do not use quantity greater than zero as the only admission rule.

Keep an independent expected-result table rather than recomputing expectations through the same aggregation code. For example, a zero row for Tea must still leave Tea separate from tea. An invalid row should be tested in a separate invocation so a failure does not prevent the successful ordering fixture from being checked.

Practice two: defend an extension

Extend the contract to accept signed stock adjustments without losing a nonnegative final-stock invariant. Decide whether validity applies after every row or only after all rows, and provide an example where those policies disagree. Preserve an explicit key comparer and checked arithmetic.

A defensible extension first chooses prefix-valid stock or final-valid stock. With initial stock zero, adjustments minus one then plus one are invalid under the first policy and valid under the second. Store the proposed result locally until its chosen validation point, and reject arithmetic overflow regardless of which business policy is selected.

Evaluate correctness and interview reasoning

A strong interview explanation starts with the exact-key and nonnegative-input contract, states the processed-prefix invariant, and then separates expected aggregation cost from sorted-display cost. It supports correctness with a duplicate-key example, a mixed-case example and an invalid row. A senior extension identifies the publication boundary and the finite-input assumption before discussing concurrency or persistence. No part of this console fixture reserves inventory, coordinates replicas or promises dictionary iteration order. The useful deliverable is a small deterministic solution whose limits are as easy to inspect as its successful output.

A working example

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;

var rows = new[] { new Row("tea", 2), new Row("Tea", 1), new Row("tea", 3) };
var totals = Aggregate(rows);
foreach (var row in totals.OrderBy(x => x.Key, StringComparer.Ordinal)) Console.WriteLine(row.Key + ":" + row.Value);
bool invalid = false; try { Aggregate(new[] { new Row("tea", -1) }); } catch (ArgumentOutOfRangeException) { invalid = true; }
Console.WriteLine("negative rejected:" + invalid.ToString().ToLowerInvariant());
static Dictionary<string,long> Aggregate(IEnumerable<Row> rows) {
    var totals = new Dictionary<string,long>(StringComparer.Ordinal);
    foreach (var row in rows) {
        if (string.IsNullOrEmpty(row.Key) || row.Quantity < 0) throw new ArgumentOutOfRangeException(nameof(rows));
        totals.TryGetValue(row.Key, out long previous); totals[row.Key] = checked(previous + row.Quantity);
    }
    return totals;
}
sealed record Row(string Key, int Quantity);

Expected output

Tea:1
tea:5
negative rejected:true
Try this
Add zero-quantity rows and an empty key as separate cases. Predict that zero preserves the total while an empty key fails. Hint: validity and contribution are different questions, so do not use quantity greater than zero as the only admission rule.

Further reading

Interview questions for this lesson