/** * Seed planning arithmetic (CONCEPT.md D38), independent of the database. */ /** The parts of `from`..`to` (inclusive) that no range in `covered` (inclusive pairs) overlaps, oldest first. */ export function uncovered(from: number, to: number, covered: [number, number][]): [number, number][] { const gaps: [number, number][] = [] let next = from for (const [start, end] of [...covered].sort((a, b) => a[0] - b[0])) { if (end < next) continue if (start > to) break if (start > next) gaps.push([next, Math.min(start - 1, to)]) next = Math.max(next, end + 1) if (next > to) return gaps } if (next <= to) gaps.push([next, to]) return gaps } /** `gaps` cut into ranges of at most `size` articles, newest first (from the top of the newest gap down). */ export function cut(gaps: [number, number][], size: number): [number, number][] { const ranges: [number, number][] = [] for (const [from, to] of [...gaps].sort((a, b) => b[1] - a[1])) { for (let end = to; end >= from; end -= size) ranges.push([Math.max(from, end - size + 1), end]) } return ranges }