A rich-text editor keeps a single style track over its text: a list of ranges like "characters 0 to 10 are a heading," "characters 12 to 20 are a quote." Each character carries at most one style. When the user selects a span and clicks a style button, that span is overwritten — whatever style used to be there is replaced, and the surrounding styles have to give way cleanly. Your job is to apply one such overwrite and return a tidy, normalized range list.
Styles are stored as half-open intervals [start, end) — start is included, end is excluded, so [0, 5) covers characters at indices 0, 1, 2, 3, 4. A range's style is a single string label. You lay a new style over the window [start, end), replacing every style underneath it, and then normalize the result: sorted by start, never overlapping, adjacent ranges that share an identical style merged into one, and zero-length ranges dropped.
type Range = { start: number; end: number; style: string }; // half-open [start, end)
// Overwrite the style on the window [start, end), then return a NEW,
// NORMALIZED range list:
// - ranges fully inside [start, end) are replaced (erased)
// - a range partially overlapping the window is TRIMMED at the boundary
// - a range straddling the whole window is SPLIT: left remainder + the new
// range + right remainder
// - then NORMALIZE: sort by start, drop zero-length ranges, and merge any
// two ADJACENT ranges that share the same style into one
// - a zero-length application (start === end) lays down nothing; it only
// normalizes the input
// The input array and its range objects are never mutated.
function applyStyle(ranges: Range[], start: number, end: number, style: string): Range[];
// Splitting one range in the middle. 'a' covers [0, 10); dropping 'b' over
// [3, 6) overwrites the middle and leaves the two ends of 'a' behind.
applyStyle([{ start: 0, end: 10, style: 'a' }], 3, 6, 'b');
// → [
// { start: 0, end: 3, style: 'a' }, // left remainder
// { start: 3, end: 6, style: 'b' }, // the new range
// { start: 6, end: 10, style: 'a' }, // right remainder
// ]
// Applying a style equal to an adjacent range merges them. 'a' covers [0, 4);
// laying 'a' over the touching window [4, 7) yields one continuous range.
applyStyle([{ start: 0, end: 4, style: 'a' }], 4, 7, 'a');
// → [{ start: 0, end: 7, style: 'a' }]
[start, end) the new style wins outright — there is no merging of two styles on the same character (that is a different problem). A range that the window fully covers is erased; a range the window only clips is trimmed back to the part outside the window.[start, end) includes start, excludes end, exactly like String.prototype.slice. A range whose end equals the window's start shares no character with it, so it is left alone — it just sits adjacent.start, free of overlaps, and free of zero-length ranges. Two ranges that end up touching (prev.end === next.start) and carry the same style become one; different styles never merge. The input may arrive unsorted or with overlaps — your result must still come out clean.start === end you add no new range; you still return the normalized form of the input.0 ≤ start ≤ end), the underlying text string, or styles that are objects rather than strings — those are out of scope (see the solution's Going further).