RRStack produces plain‑language descriptions of rules by compiling a normalized descriptor (AST) and rendering it through a translator. The built‑in translator, “strict‑en,” generates clear English phrases for common RRULE patterns and continuous spans.
Highlights
By default, boolean options are opt‑in: includeTimeZone is false, includeBounds is false.
Instance method
const text = stack.describeRule(0);
// e.g., "Active for 1 hour every day at 5:00"
const withZone = stack.describeRule(0, { includeTimeZone: true });
// e.g., "Active for 1 hour every day at 5:00 (timezone UTC)"
Pure helper (compile on the fly)
import { describeRule, RRStack } from '@karmaniverous/rrstack';
const text = describeRule(ruleJson, RRStack.asTimeZoneId('UTC'), 'ms');
// default: timezone label omitted
export interface DescribeOptions {
includeTimeZone?: boolean; // "(timezone <tz>)"
includeBounds?: boolean; // append "[from …; until …]" when clamps exist
formatTimeZone?: (tzId: string) => string; // customize tz label
boundsFormat?: string; // Luxon format for bound timestamps (when includeBounds)
translator?: 'strict-en' | DescribeTranslator;
translatorOptions?: TranslatorOptions;
}
Translator options (strict-en):
export interface TranslatorOptions {
frequency?: Partial<FrequencyLexicon>; // labels for "every day/week/..."
timeFormat?: 'hm' | 'hms' | 'auto'; // 24h or 12h handled via hourCycle
hourCycle?: 'h23' | 'h12';
ordinals?: 'long' | 'short'; // "third" vs "3rd"
locale?: string; // Luxon setLocale
lowercase?: boolean; // default true
}
When includeBounds is true, descriptions append brackets:
[from <formatted-from>; until <formatted-until>]
boundsFormat customizes the timestamp rendering (Luxon toFormat) in the rule’s timezone.[starts, ends).Examples:
stack.describeRule(0, {
includeBounds: true,
boundsFormat: 'yyyy-LL-dd HH:mm',
});
// "... [from 2025-04-01 00:00; until 2025-04-02 00:00]"
Monthly — third Tuesday
import { RRule } from 'rrule';
const text = describeRule(
{
effect: 'active',
duration: { hours: 1 },
options: {
freq: 'monthly',
bysetpos: 3,
byweekday: [RRule.TU],
byhour: [5],
byminute: [0],
bysecond: [0],
},
},
RRStack.asTimeZoneId('UTC'),
'ms',
);
// "Active for 1 hour every month on the third tuesday at 5:00"
Monthly — last Tuesday
import { RRule } from 'rrule';
const text = describeRule(
{
effect: 'active',
duration: { hours: 1 },
options: {
freq: 'monthly',
byweekday: [RRule.TU.nth(-1)],
byhour: [5],
byminute: [0],
bysecond: [0],
},
},
RRStack.asTimeZoneId('UTC'),
'ms',
);
// "Active for 1 hour every month on the last tuesday at 5:00"
Monthly — by‑month‑day
const text = describeRule(
{
effect: 'active',
duration: { hours: 1 },
options: {
freq: 'monthly',
bymonthday: [15],
byhour: [5],
byminute: [0],
bysecond: [0],
},
},
RRStack.asTimeZoneId('UTC'),
'ms',
);
// "Active for 1 hour every month on the 15th at 5:00"
Yearly — multiple months
const text = describeRule(
{
effect: 'active',
duration: { hours: 1 },
options: {
freq: 'yearly',
bymonth: [1, 3, 7],
bymonthday: [5],
byhour: [5],
byminute: [0],
bysecond: [0],
},
},
RRStack.asTimeZoneId('UTC'),
'ms',
);
// "Active for 1 hour every year in january, march and july at 5:00"
export type FrequencyAdjectiveLabels = Record<FrequencyStr, string>;
export type FrequencyNounLabels = Record<FrequencyStr, string>;
export interface FrequencyLexicon {
adjective: FrequencyAdjectiveLabels;
noun: FrequencyNounLabels;
pluralize?: (noun: string, n: number) => string;
}
Exports:
FREQUENCY_ADJECTIVE_EN, FREQUENCY_NOUN_EN, FREQUENCY_LEXICON_ENtoFrequencyOptions(labels?) → ordered options for pickersSee type definitions in Core API and Types.