A secure and optimized RRULE (RFC 5545) parser with leap year correction support
npm install @volt-package/rrule
Security-hardened and high-performance RRULE parser compliant with RFC 5545 (iCalendar)
- β
Security First: ReDoS attack prevention, input validation, resource limits
- β‘ High Performance: Lazy evaluation, O(1) memory usage, LRU caching
- π°π· Korean Business Logic: Leap year correction (Feb 29 β Feb 28)
- π¦ Zero Dependencies: No external dependencies
- π§ Full TypeScript Support: Type-safe guaranteed
- π Internationalization: English/Korean natural language conversion
``bash`
npm install @volt-package/rruleor
bun add @volt-package/rrule
`typescript
import { RRule, toText } from '@volt-package/rrule';
// Create from RRULE string
const rrule = new RRule('FREQ=DAILY;COUNT=5');
// Or create from options object
const rrule2 = new RRule({
freq: 'WEEKLY',
dtstart: new Date('2024-01-01'),
count: 10,
byDay: ['MO', 'WE', 'FR']
});
// Get all dates
const dates = rrule.all();
console.log(dates);
// Convert to natural language
console.log(toText(rrule)); // "every day for 5 times"
console.log(toText(rrule, { language: 'ko' })); // "λ§€μΌ 5ν"
`
`typescript
// Import core functionality only (no toText)
// Bundle size: 4.4 KB (gzipped) vs 5.5 KB (full)
import { RRule, RRuleSet } from '@volt-package/rrule/core';
const rrule = new RRule('FREQ=DAILY;COUNT=5');
const dates = rrule.all();
// Import toText separately only if needed
import { toText } from '@volt-package/rrule';
console.log(toText(rrule));
`
`typescript
// Memory-efficient iteration
for (const date of rrule.iterator()) {
console.log(date);
}
// Get first N dates
const firstFive = rrule.take(5);
// Get dates within range
const dates = rrule.between(
new Date('2024-01-01'),
new Date('2024-12-31')
);
`
`typescript
// Daily
new RRule('FREQ=DAILY;COUNT=7');
// Weekly (specific weekdays)
new RRule('FREQ=WEEKLY;BYDAY=MO,WE,FR;COUNT=10');
// Monthly (specific days)
new RRule('FREQ=MONTHLY;BYMONTHDAY=1,15;COUNT=12');
// Yearly
new RRule('FREQ=YEARLY;BYMONTH=1,7;COUNT=5');
`
#### BYSETPOS: Select Specific Positions
`typescript
// Last Friday of each month
new RRule('FREQ=MONTHLY;BYDAY=FR;BYSETPOS=-1;COUNT=12');
// First and third Monday of each month
new RRule('FREQ=MONTHLY;BYDAY=MO;BYSETPOS=1,3;COUNT=24');
`
#### BYYEARDAY: Specific Days of Year
`typescript
// Days 1, 100, 200 of each year
new RRule('FREQ=YEARLY;BYYEARDAY=1,100,200;COUNT=6');
// Last day of each year (using negative)
new RRule('FREQ=YEARLY;BYYEARDAY=-1;COUNT=5');
`
#### BYWEEKNO: ISO Week Numbers
`typescript`
// Mondays of weeks 1 and 20 each year
new RRule('FREQ=YEARLY;BYWEEKNO=1,20;BYDAY=MO;COUNT=4');
`typescript
import { RRuleSet } from '@volt-package/rrule';
const set = new RRuleSet();
// Weekday workdays
set.rrule({
freq: 'DAILY',
dtstart: new Date('2024-01-01'),
until: new Date('2024-12-31'),
byDay: ['MO', 'TU', 'WE', 'TH', 'FR']
});
// Exclude holidays
set.exdate(new Date('2024-01-01')); // New Year
set.exdate(new Date('2024-12-25')); // Christmas
// Add special work days
set.rdate(new Date('2024-12-31')); // Year-end special work
const workDays = set.all();
`
`typescript
import { RRule, toText } from '@volt-package/rrule';
const rrule = new RRule('FREQ=MONTHLY;BYDAY=FR;BYSETPOS=-1;COUNT=12');
// English
console.log(toText(rrule));
// "every month on the last Friday for 12 times"
// Korean
console.log(toText(rrule, { language: 'ko' }));
// "λ§€μ λ§μ§λ§ κΈμμΌμ 12ν"
// Or pass options directly
console.log(toText({ freq: 'DAILY', count: 5 }));
// "every day for 5 times"
`
`typescript
// Yearly recurrence starting from Feb 29
const rrule = new RRule({
freq: 'YEARLY',
dtstart: new Date('2024-02-29'), // Leap year
count: 5
});
const dates = rrule.all();
// 2024-02-29 (leap year)
// 2025-02-28 (non-leap year β auto-corrected)
// 2026-02-28 (non-leap year β auto-corrected)
// 2027-02-28 (non-leap year β auto-corrected)
// 2028-02-29 (leap year)
`
typescript
// Uses scanner-based parser (instead of regex)
// Safe against malicious input
`$3
`typescript
// Input length limit
const rrule = new RRule('FREQ=DAILY...', {
maxInputLength: 2000 // default
});// Allowlist for RFC 5545 standard keywords only
`$3
`typescript
const rrule = new RRule('FREQ=SECONDLY;COUNT=10000', {
maxIterations: 730, // Maximum iteration count
maxDuration: 5, // Maximum duration (years)
timeout: 50 // Operation timeout (ms)
});
`π― API Documentation
$3
#### Constructor
`typescript
new RRule(options: RRuleOptions | string, safetyConfig?: SafetyConfig)
`#### Methods
#####
all(): Date[]
Returns all dates as array (memory warning)#####
iterator(): Generator
Returns lazy evaluation generator (recommended)#####
take(n: number): Date[]
Returns first N dates#####
first(): Date | null
Returns first date#####
between(after: Date, before: Date, includeEnds?: boolean): Date[]
Returns dates within range#####
toString(): string
Converts to RRULE string#####
getOptions(): Readonly
Returns options object$3
#####
toText(rrule: RRule | RRuleOptions, options?: { language?: 'en' | 'ko' }): string
Converts RRULE to natural language text`typescript
import { RRule, toText } from '@volt-package/rrule';const rrule = new RRule('FREQ=DAILY;COUNT=5');
console.log(toText(rrule)); // "every day for 5 times"
console.log(toText(rrule, { language: 'ko' })); // "λ§€μΌ 5ν"
`$3
`typescript
interface RRuleOptions {
freq: 'YEARLY' | 'MONTHLY' | 'WEEKLY' | 'DAILY' | 'HOURLY' | 'MINUTELY' | 'SECONDLY';
dtstart?: Date;
until?: Date;
count?: number;
interval?: number;
byMonth?: number[]; // 1-12
byMonthDay?: number[]; // 1-31, -1 = last day
byDay?: Weekday[]; // 'MO', 'TU', 'WE', 'TH', 'FR', 'SA', 'SU'
byHour?: number[]; // 0-23
byMinute?: number[]; // 0-59
bySecond?: number[]; // 0-59
bySetPos?: number[]; // Position selection (1 = first, -1 = last)
byYearDay?: number[]; // 1-366, -1 = last day
byWeekNo?: number[]; // 1-53, ISO week number
wkst?: Weekday; // Week start day
tzid?: string; // IANA timezone identifier
}
`$3
#### Methods
#####
rrule(rrule: RRule | RRuleOptions | string): RRuleSet
Add inclusion rule#####
rdate(date: Date): RRuleSet
Add inclusion date#####
exrule(rrule: RRule | RRuleOptions | string): RRuleSet
Add exclusion rule#####
exdate(date: Date): RRuleSet
Add exclusion date#####
all(limit?: number): Date[]
Returns all dates#####
between(after: Date, before: Date, includeEnds?: boolean): Date[]
Returns dates within range#####
first(): Date | null
Returns first date#####
iterator(): Generator
Returns lazy evaluation generatorποΈ Architecture
`
Input RRULE String
β
[Tokenizer] β ReDoS prevention (Scanner approach)
β
[Parser] β Allowlist validation
β
[RRuleOptions] β LRU cache
β
[Iterator] β Lazy evaluation (Generator)
β
[SafetyController] β Resource limits
β
Output Date[]
`π Performance
- Bundle Size:
- Full bundle: 5.5 KB (gzipped) / 19 KB (minified)
- Core-only: 4.4 KB (gzipped) / 16 KB (minified) - 20% smaller
- Memory: O(1) - Generator pattern usage
- Parsing: LRU cache minimizes repeated parsing
- Computation: Early exit prevents unnecessary calculations
- Security: Input validation and timeout ensure safety
π§ͺ Testing
`bash
Run all tests
bun testSpecific test file
bun test tests/rrule.test.tsBuild
bun run build
`Test Coverage:
- β
Basic recurrence patterns
- β
Advanced filters (BYSETPOS, BYYEARDAY, BYWEEKNO)
- β
RRuleSet combinations
- β
Leap year logic
- β
Security features
- β
Natural language conversion
π Supported RFC 5545 Features
| Feature | Support | Description |
|---------|---------|-------------|
| FREQ | β
| 7 frequencies (YEARLY ~ SECONDLY) |
| DTSTART | β
| Start date |
| UNTIL | β
| End date |
| COUNT | β
| Repetition count |
| INTERVAL | β
| Interval |
| BYDAY | β
| Day of week filter |
| BYMONTH | β
| Month filter |
| BYMONTHDAY | β
| Day of month filter |
| BYHOUR | β
| Hour filter |
| BYMINUTE | β
| Minute filter |
| BYSECOND | β
| Second filter |
| BYSETPOS | β
| Position selection |
| BYYEARDAY | β
| Day of year |
| BYWEEKNO | β
| ISO week number |
| WKST | β
| Week start day |
| TZID | β οΈ | Basic support (storage/access only) |
| RRULE | β
| Inclusion rule |
| RDATE | β
| Inclusion date |
| EXRULE | β
| Exclusion rule |
| EXDATE | β
| Exclusion date |
π§ Development
`bash
Install dependencies
bun installDevelopment mode
bun run build:watchTest watch mode
bun test --watchType check
bunx tsc --noEmitBuild
bun run build
``MIT License - Feel free to use!
Issues and PRs are welcome!
This project references features from jkbrzt/rrule with enhanced security and performance.