koala73/worldmonitor · error
Horizon must be 1-90 whole days.
Error message
Horizon must be 1-90 whole days.
What it means
parseOperationalInput validates the operational-balance worksheet input before any calculation. The horizon (planning window length) must be an integer number of whole days between 1 and MAX_OPERATIONAL_DAYS (90). This throw fires when horizonDays is missing, non-numeric, fractional, or outside that range, because a broken horizon would make every downstream date computation meaningless.
Solutions
- Ensure input.horizonDays is a JS number coerced with Number() or parseInt(value, 10) before calling parseOperationalInput.
- Clamp or re-prompt the user for a horizon in the 1-90 range at the form/UI layer before submission.
- If the value comes from imported worksheet JSON, validate/normalize it against the operationalExample() shape first.
- Confirm the field is actually named horizonDays on the input object (not horizon or days).
Example fix
// before
calculateOperationalBalance({ operation: 'Ops', basis: 'user', unit: 'units', startDate: '2026-09-10', horizonDays: form.horizon, startingStock: 100, dailyDemand: 20, deliveries: [], alternativeDeliveries: [], alternativeDailyDemand: null });
// after
const horizonDays = Number.parseInt(form.horizon, 10);
if (!Number.isInteger(horizonDays) || horizonDays < 1 || horizonDays > 90) throw new Error('Pick a horizon of 1-90 whole days.');
calculateOperationalBalance({ operation: 'Ops', basis: 'user', unit: 'units', startDate: '2026-09-10', horizonDays, startingStock: 100, dailyDemand: 20, deliveries: [], alternativeDeliveries: [], alternativeDailyDemand: null }); Defensive patterns
Strategy: validation
Validate before calling
function isValidHorizon(v: unknown): v is number {
return Number.isInteger(v) && (v as number) >= 1 && (v as number) <= 90;
}
if (!isValidHorizon(input.horizonDays)) throw new Error('Pick a horizon of 1-90 whole days.'); Type guard
const isWholeDays1to90 = (v: unknown): v is number => typeof v === 'number' && Number.isInteger(v) && v >= 1 && v <= 90;
Try / catch
try {
const snapshot = calculateOperationalBalance(rawInput);
} catch (err) {
if (err instanceof Error && err.message === 'Horizon must be 1-90 whole days.') {
resetHorizonFieldToDefault();
} else throw err;
} Prevention
- Coerce form strings with Number.parseInt(value, 10) before assigning horizonDays.
- Use a bounded integer slider/stepper (min 1, max 90, step 1) in the UI.
- Validate the whole input object against operationalExample()'s shape before every parse call.
When it happens
Trigger: Calling parseOperationalInput (directly or via calculateOperationalBalance/importOperationalWorksheet) with input.horizonDays set to a non-integer (e.g. 10.5), a number < 1 (0, -5), a number > 90 (365), a numeric string like '30' that was never converted, or omitted entirely (undefined).
Common situations: A UI form submits the raw input string instead of parseInt() of it; a persisted worksheet JSON was hand-edited to a yearly horizon; a defaults object forgets horizonDays; a slider component emits fractional steps.
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
- 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
- alertThreshold must be between 0 and 100
AI-assisted analysis of koala73/worldmonitor@7d06c8633d (2026-09-15).
Data as JSON: /api/errors/d69d94d376052d0a.
Report an issue: GitHub.
Appendix: source
Thrown at src/utils/operational-balance.ts:34
if (typeof value !== 'number' || !Number.isFinite(value) || value < 0 || value > 1e6 || round(value) !== value) {
throw new Error(`${label} must be between 0 and 1 million with at most 6 decimal places.`);
}
return value;
}
function date(value: unknown): string {
if (typeof value !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(value) || !Number.isFinite(Date.parse(value)) || new Date(value).toISOString().slice(0, 10) !== value || value < '1900-01-01' || value > '9998-12-31') {
throw new Error('Use a valid date from 1900 through 9998.');
}
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') };
}
View on GitHub (pinned to 7d06c8633d)