koala73/worldmonitor · error
Delivery dates must be inside the selected horizon.
Error message
Delivery dates must be inside the selected horizon.
What it means
Each delivery row's date, parsed via date(), must fall within the planning window: on or after startDate and no later than dateAt(startDate, horizonDays - 1). Deliveries outside the horizon cannot be attributed to any day in the computed balance, so the parser rejects them explicitly instead of dropping them silently.
Solutions
- Clamp or filter delivery dates to [startDate, dateAt(startDate, horizonDays - 1)] before calling the parser.
- Recompute the horizon to cover all existing deliveries instead of shrinking it after data entry.
- Use plain YYYY-MM-DD date strings consistently to avoid timezone drift at day boundaries.
- In the import path, validate each row.date against the horizon and report the offending index to the user.
Example fix
// before
calculateOperationalBalance({ ...input, horizonDays: 10, deliveries: [{ date: '2026-10-01', quantity: 5, unit: 'units', costUsd: null }] });
// after
const inHorizon = input.deliveries.filter(d => d.date >= input.startDate && d.date <= '2026-09-19');
calculateOperationalBalance({ ...input, horizonDays: 10, deliveries: inHorizon }); Defensive patterns
Strategy: validation
Validate before calling
const start = input.startDate; // 'YYYY-MM-DD'
const end = new Date(start); end.setUTCDate(end.getUTCDate() + input.horizonDays - 1);
const endStr = end.toISOString().slice(0, 10);
const bad = input.deliveries.filter(d => d.date < start || d.date > endStr);
if (bad.length) throw new Error(`Delivery dates outside ${start}..${endStr}: ${bad.map(d => d.date).join(', ')}`); Try / catch
try {
const snapshot = calculateOperationalBalance(input);
} catch (err) {
if (err instanceof Error && err.message === 'Delivery dates must be inside the selected horizon.') {
highlightOffendingDatesInUi(input);
} else throw err;
} Prevention
- Constrain the delivery date picker's min/max attributes to the horizon whenever the horizon changes.
- Use plain YYYY-MM-DD strings everywhere to avoid timezone day-shifts.
- Re-sort and re-validate all delivery rows after any change to startDate or horizonDays.
When it happens
Trigger: Calling parseOperationalInput/calculateOperationalBalance where any entry of deliveries or alternativeDeliveries has a date before the input's startDate, after startDate + horizonDays - 1 days, an invalid date string that parses to an out-of-range value, or a timezone-shifted ISO date that rolls to the next/previous day.
Common situations: The user changed the horizon after entering deliveries; a copy-pasted date one year off; UTC vs local timezone shifting '2026-09-10T00:00Z' past a boundary; month/day swapped in a hand-edited JSON export.
Understand the failure class
Background: "value must be between 0 and 1" / "out of range" / "must not be negative" errors: fixing range-validation failures across open-source libraries — this error's family across 42 libraries.
Related errors
- start_date must not be after end_date
- Invalid scorecard bloc selection.
- countryCode must be a 2-letter ISO 3166-1 alpha-2 code
- chokepointId must be a canonical chokepoint id
- iso2 must be a 2-letter uppercase ISO country code
AI-assisted analysis of koala73/worldmonitor@7d06c8633d (2026-09-15).
Data as JSON: /api/errors/4147d452b3ff8fec.
Report an issue: GitHub.
Appendix: source
Thrown at src/utils/operational-balance.ts:42
}
return value;
}
export function parseOperationalInput(value: unknown): OperationalInput {
const input = record(value);
if (typeof input.operation !== 'string' || !input.operation.trim() || input.operation.length > 100) throw new Error('Enter an operation name of 1-100 characters.');
if (typeof input.unit !== 'string' || !/^[\p{L}\p{N} %./-]{1,24}$/u.test(input.unit) || !input.unit.trim()) throw new Error('Enter one quantity unit of 1-24 characters.');
const unit = input.unit.trim();
const startDate = date(input.startDate);
if (!Number.isInteger(input.horizonDays) || (input.horizonDays as number) < 1 || (input.horizonDays as number) > MAX_OPERATIONAL_DAYS) throw new Error('Horizon must be 1-90 whole days.');
const horizonDays = input.horizonDays as number;
if (input.basis !== 'example' && input.basis !== 'user') throw new Error('Input basis must be example or user.');
const deliveries = (value: unknown): OperationalDelivery[] => {
if (!Array.isArray(value) || value.length > MAX_OPERATIONAL_DELIVERIES) throw new Error('Use at most 30 deliveries per list.');
return value.map(value => {
const row = record(value);
const deliveryDate = date(row.date);
if (deliveryDate < startDate || deliveryDate > dateAt(startDate, horizonDays - 1)) throw new Error('Delivery dates must be inside the selected horizon.');
if (row.unit !== unit) throw new Error(`Every delivery must use ${unit}; mixed units are not converted.`);
return { date: deliveryDate, unit, quantity: quantity(row.quantity, 'Delivery quantity'), costUsd: row.costUsd === null || row.costUsd === undefined ? null : quantity(row.costUsd, 'Delivery cost') };
});
};
return { operation: input.operation.trim(), basis: input.basis, unit, startDate, horizonDays,
startingStock: quantity(input.startingStock, 'Starting stock'), dailyDemand: quantity(input.dailyDemand, 'Daily demand'),
deliveries: deliveries(input.deliveries), alternativeDeliveries: deliveries(input.alternativeDeliveries),
alternativeDailyDemand: input.alternativeDailyDemand === null ? null : quantity(input.alternativeDailyDemand, 'Alternative daily demand') };
}
export function calculateOperationalBalance(value: unknown): OperationalSnapshot {
const input = parseOperationalInput(value);
const balance = (deliveries: OperationalDelivery[], demand: number): OperationalBalance => {
let stock = input.startingStock;
const days = Array.from({ length: input.horizonDays }, (_, index) => {
const date = dateAt(input.startDate, index);
const arrivals = round(deliveries.filter(row => row.date === date).reduce((sum, row) => sum + row.quantity, 0));
const available = round(stock + arrivals);View on GitHub (pinned to 7d06c8633d)