Skip to content

Forecast: records across columns

Each quarter is a column: the fields run down the rows. Typing a period in the empty column after the last quarter adds one.

forecast.xlsxLoads as you scroll to it

Try: add 2027-Q3 after the last quarter; skip a quarter; type Q1 2027 as a period; overwrite a margin formula.

forecast.ts
import { defineSchema } from '@ruled/sheet';
const QUARTER = /^(\d{4})-Q([1-4])$/;
export const forecast = defineSchema({
id: 'docs.forecast',
name: 'Quarterly forecast',
sheets: [
{
name: 'Forecast',
tables: [
{
key: 'quarters',
at: 'A1',
// One record per column: each quarter runs down its own column.
orientation: 'columns',
records: { min: 1, max: 12 },
fields: [
{ key: 'period', label: 'Period', type: 'text', required: true, unique: true, pattern: QUARTER.source, patternHint: 'a quarter like 2027-Q1' },
{ key: 'revenue', label: 'Revenue', type: 'number', required: true, min: 0, decimals: 2 },
{ key: 'cost', label: 'Cost', type: 'number', required: true, min: 0, decimals: 2 },
{ key: 'margin', label: 'Margin', type: 'number', formula: 'require' },
{ key: 'marginPct', label: 'Margin %', type: 'percent', formula: 'require', min: 0 },
],
rules: [
// Quarters must follow on from each other.
(table) => {
const issues = [];
let previous: number | undefined;
for (const r of table.records) {
const m = QUARTER.exec(String(r.values.period ?? ''));
if (!m) continue;
const q = Number(m[1]) * 4 + Number(m[2]);
if (previous !== undefined && q !== previous + 1) {
issues.push({ record: r.index, field: 'period', message: 'A quarter is missing before this one.' });
}
previous = q;
}
return issues;
},
],
},
],
},
],
});
  • orientation: 'columns' turns the table on its side: records across, fields down.
  • The quarters-follow-on rule is a table rule in code, returning an issue for each period out of sequence.