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

  1. Clamp or filter delivery dates to [startDate, dateAt(startDate, horizonDays - 1)] before calling the parser.
  2. Recompute the horizon to cover all existing deliveries instead of shrinking it after data entry.
  3. Use plain YYYY-MM-DD date strings consistently to avoid timezone drift at day boundaries.
  4. 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

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


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)