react-native-roster 0.0.0 → 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NOTICE.md +29 -0
- package/README.md +378 -2
- package/dist/src/adapters/rrule/cache.d.ts +15 -0
- package/dist/src/adapters/rrule/cache.d.ts.map +1 -0
- package/dist/src/adapters/rrule/cache.js +53 -0
- package/dist/src/adapters/rrule/cache.js.map +1 -0
- package/dist/src/adapters/rrule/envelope.d.ts +3 -0
- package/dist/src/adapters/rrule/envelope.d.ts.map +1 -0
- package/dist/src/adapters/rrule/envelope.js +9 -0
- package/dist/src/adapters/rrule/envelope.js.map +1 -0
- package/dist/src/adapters/rrule/expand.d.ts +4 -0
- package/dist/src/adapters/rrule/expand.d.ts.map +1 -0
- package/dist/src/adapters/rrule/expand.js +92 -0
- package/dist/src/adapters/rrule/expand.js.map +1 -0
- package/dist/src/adapters/rrule/hash.d.ts +3 -0
- package/dist/src/adapters/rrule/hash.d.ts.map +1 -0
- package/dist/src/adapters/rrule/hash.js +15 -0
- package/dist/src/adapters/rrule/hash.js.map +1 -0
- package/dist/src/adapters/rrule/index.d.ts +5 -0
- package/dist/src/adapters/rrule/index.d.ts.map +1 -0
- package/dist/src/adapters/rrule/index.js +12 -0
- package/dist/src/adapters/rrule/index.js.map +1 -0
- package/dist/src/adapters/rrule/net.d.ts +6 -0
- package/dist/src/adapters/rrule/net.d.ts.map +1 -0
- package/dist/src/adapters/rrule/net.js +31 -0
- package/dist/src/adapters/rrule/net.js.map +1 -0
- package/dist/src/adapters/rrule/occurrences.d.ts +6 -0
- package/dist/src/adapters/rrule/occurrences.d.ts.map +1 -0
- package/dist/src/adapters/rrule/occurrences.js +252 -0
- package/dist/src/adapters/rrule/occurrences.js.map +1 -0
- package/dist/src/adapters/rrule/types.d.ts +62 -0
- package/dist/src/adapters/rrule/types.d.ts.map +1 -0
- package/dist/src/adapters/rrule/types.js +3 -0
- package/dist/src/adapters/rrule/types.js.map +1 -0
- package/dist/src/adapters/rrule/validate.d.ts +6 -0
- package/dist/src/adapters/rrule/validate.d.ts.map +1 -0
- package/dist/src/adapters/rrule/validate.js +64 -0
- package/dist/src/adapters/rrule/validate.js.map +1 -0
- package/dist/src/components/layers/layers.types.d.ts +15 -0
- package/dist/src/components/layers/layers.types.d.ts.map +1 -0
- package/dist/src/components/layers/layers.types.js +3 -0
- package/dist/src/components/layers/layers.types.js.map +1 -0
- package/dist/src/components/layers/parts/gap.d.ts +3 -0
- package/dist/src/components/layers/parts/gap.d.ts.map +1 -0
- package/dist/src/components/layers/parts/gap.js +7 -0
- package/dist/src/components/layers/parts/gap.js.map +1 -0
- package/dist/src/components/layers/parts/interval.d.ts +3 -0
- package/dist/src/components/layers/parts/interval.d.ts.map +1 -0
- package/dist/src/components/layers/parts/interval.js +22 -0
- package/dist/src/components/layers/parts/interval.js.map +1 -0
- package/dist/src/components/layers/utils/styles.d.ts +7 -0
- package/dist/src/components/layers/utils/styles.d.ts.map +1 -0
- package/dist/src/components/layers/utils/styles.js +24 -0
- package/dist/src/components/layers/utils/styles.js.map +1 -0
- package/dist/src/components/primitives/press-point.d.ts +3 -0
- package/dist/src/components/primitives/press-point.d.ts.map +1 -0
- package/dist/src/components/primitives/press-point.js +7 -0
- package/dist/src/components/primitives/press-point.js.map +1 -0
- package/dist/src/components/primitives/press-point.types.d.ts +14 -0
- package/dist/src/components/primitives/press-point.types.d.ts.map +1 -0
- package/dist/src/components/primitives/press-point.types.js +3 -0
- package/dist/src/components/primitives/press-point.types.js.map +1 -0
- package/dist/src/components/primitives/press-point.web.d.ts +3 -0
- package/dist/src/components/primitives/press-point.web.d.ts.map +1 -0
- package/dist/src/components/primitives/press-point.web.js +17 -0
- package/dist/src/components/primitives/press-point.web.js.map +1 -0
- package/dist/src/components/primitives/region-style.d.ts +9 -0
- package/dist/src/components/primitives/region-style.d.ts.map +1 -0
- package/dist/src/components/primitives/region-style.js +20 -0
- package/dist/src/components/primitives/region-style.js.map +1 -0
- package/dist/src/components/roster/index.d.ts +3 -0
- package/dist/src/components/roster/index.d.ts.map +1 -0
- package/dist/src/components/roster/index.js +56 -0
- package/dist/src/components/roster/index.js.map +1 -0
- package/dist/src/components/roster/lanes/interval-hover.d.ts +3 -0
- package/dist/src/components/roster/lanes/interval-hover.d.ts.map +1 -0
- package/dist/src/components/roster/lanes/interval-hover.js +7 -0
- package/dist/src/components/roster/lanes/interval-hover.js.map +1 -0
- package/dist/src/components/roster/lanes/interval-hover.types.d.ts +15 -0
- package/dist/src/components/roster/lanes/interval-hover.types.d.ts.map +1 -0
- package/dist/src/components/roster/lanes/interval-hover.types.js +3 -0
- package/dist/src/components/roster/lanes/interval-hover.types.js.map +1 -0
- package/dist/src/components/roster/lanes/interval-hover.web.d.ts +3 -0
- package/dist/src/components/roster/lanes/interval-hover.web.d.ts.map +1 -0
- package/dist/src/components/roster/lanes/interval-hover.web.js +20 -0
- package/dist/src/components/roster/lanes/interval-hover.web.js.map +1 -0
- package/dist/src/components/roster/lanes/lane.d.ts +10 -0
- package/dist/src/components/roster/lanes/lane.d.ts.map +1 -0
- package/dist/src/components/roster/lanes/lane.js +37 -0
- package/dist/src/components/roster/lanes/lane.js.map +1 -0
- package/dist/src/components/roster/lanes/parts/incomplete.d.ts +8 -0
- package/dist/src/components/roster/lanes/parts/incomplete.d.ts.map +1 -0
- package/dist/src/components/roster/lanes/parts/incomplete.js +30 -0
- package/dist/src/components/roster/lanes/parts/incomplete.js.map +1 -0
- package/dist/src/components/roster/lanes/parts/lane-label.d.ts +3 -0
- package/dist/src/components/roster/lanes/parts/lane-label.d.ts.map +1 -0
- package/dist/src/components/roster/lanes/parts/lane-label.js +18 -0
- package/dist/src/components/roster/lanes/parts/lane-label.js.map +1 -0
- package/dist/src/components/roster/layout.d.ts +17 -0
- package/dist/src/components/roster/layout.d.ts.map +1 -0
- package/dist/src/components/roster/layout.js +24 -0
- package/dist/src/components/roster/layout.js.map +1 -0
- package/dist/src/components/roster/parts/body.d.ts +7 -0
- package/dist/src/components/roster/parts/body.d.ts.map +1 -0
- package/dist/src/components/roster/parts/body.js +29 -0
- package/dist/src/components/roster/parts/body.js.map +1 -0
- package/dist/src/components/roster/parts/corner.d.ts +2 -0
- package/dist/src/components/roster/parts/corner.d.ts.map +1 -0
- package/dist/src/components/roster/parts/corner.js +7 -0
- package/dist/src/components/roster/parts/corner.js.map +1 -0
- package/dist/src/components/roster/parts/empty.d.ts +2 -0
- package/dist/src/components/roster/parts/empty.d.ts.map +1 -0
- package/dist/src/components/roster/parts/empty.js +9 -0
- package/dist/src/components/roster/parts/empty.js.map +1 -0
- package/dist/src/components/roster/parts/grid.d.ts +3 -0
- package/dist/src/components/roster/parts/grid.d.ts.map +1 -0
- package/dist/src/components/roster/parts/grid.js +16 -0
- package/dist/src/components/roster/parts/grid.js.map +1 -0
- package/dist/src/components/roster/parts/header-cell.d.ts +5 -0
- package/dist/src/components/roster/parts/header-cell.d.ts.map +1 -0
- package/dist/src/components/roster/parts/header-cell.js +9 -0
- package/dist/src/components/roster/parts/header-cell.js.map +1 -0
- package/dist/src/components/roster/parts/header.d.ts +3 -0
- package/dist/src/components/roster/parts/header.d.ts.map +1 -0
- package/dist/src/components/roster/parts/header.js +13 -0
- package/dist/src/components/roster/parts/header.js.map +1 -0
- package/dist/src/components/roster/parts/lane-label-column.d.ts +3 -0
- package/dist/src/components/roster/parts/lane-label-column.d.ts.map +1 -0
- package/dist/src/components/roster/parts/lane-label-column.js +18 -0
- package/dist/src/components/roster/parts/lane-label-column.js.map +1 -0
- package/dist/src/components/roster/roster.types.d.ts +146 -0
- package/dist/src/components/roster/roster.types.d.ts.map +1 -0
- package/dist/src/components/roster/roster.types.js +3 -0
- package/dist/src/components/roster/roster.types.js.map +1 -0
- package/dist/src/components/roster/use-roster.d.ts +3 -0
- package/dist/src/components/roster/use-roster.d.ts.map +1 -0
- package/dist/src/components/roster/use-roster.js +106 -0
- package/dist/src/components/roster/use-roster.js.map +1 -0
- package/dist/src/components/roster/utils/body-content-key.d.ts +3 -0
- package/dist/src/components/roster/utils/body-content-key.d.ts.map +1 -0
- package/dist/src/components/roster/utils/body-content-key.js +29 -0
- package/dist/src/components/roster/utils/body-content-key.js.map +1 -0
- package/dist/src/components/roster/utils/ticks.d.ts +4 -0
- package/dist/src/components/roster/utils/ticks.d.ts.map +1 -0
- package/dist/src/components/roster/utils/ticks.js +60 -0
- package/dist/src/components/roster/utils/ticks.js.map +1 -0
- package/dist/src/components/schedule/days/day.d.ts +20 -0
- package/dist/src/components/schedule/days/day.d.ts.map +1 -0
- package/dist/src/components/schedule/days/day.js +26 -0
- package/dist/src/components/schedule/days/day.js.map +1 -0
- package/dist/src/components/schedule/days/header-layout.d.ts +9 -0
- package/dist/src/components/schedule/days/header-layout.d.ts.map +1 -0
- package/dist/src/components/schedule/days/header-layout.js +9 -0
- package/dist/src/components/schedule/days/header-layout.js.map +1 -0
- package/dist/src/components/schedule/days/layout.d.ts +18 -0
- package/dist/src/components/schedule/days/layout.d.ts.map +1 -0
- package/dist/src/components/schedule/days/layout.js +9 -0
- package/dist/src/components/schedule/days/layout.js.map +1 -0
- package/dist/src/components/schedule/days/parts/column.d.ts +3 -0
- package/dist/src/components/schedule/days/parts/column.d.ts.map +1 -0
- package/dist/src/components/schedule/days/parts/column.js +33 -0
- package/dist/src/components/schedule/days/parts/column.js.map +1 -0
- package/dist/src/components/schedule/days/parts/day-header.d.ts +5 -0
- package/dist/src/components/schedule/days/parts/day-header.d.ts.map +1 -0
- package/dist/src/components/schedule/days/parts/day-header.js +10 -0
- package/dist/src/components/schedule/days/parts/day-header.js.map +1 -0
- package/dist/src/components/schedule/days/parts/grid.d.ts +3 -0
- package/dist/src/components/schedule/days/parts/grid.d.ts.map +1 -0
- package/dist/src/components/schedule/days/parts/grid.js +14 -0
- package/dist/src/components/schedule/days/parts/grid.js.map +1 -0
- package/dist/src/components/schedule/days/parts/now-line.d.ts +5 -0
- package/dist/src/components/schedule/days/parts/now-line.d.ts.map +1 -0
- package/dist/src/components/schedule/days/parts/now-line.js +9 -0
- package/dist/src/components/schedule/days/parts/now-line.js.map +1 -0
- package/dist/src/components/schedule/days/parts/skipped-date.d.ts +4 -0
- package/dist/src/components/schedule/days/parts/skipped-date.d.ts.map +1 -0
- package/dist/src/components/schedule/days/parts/skipped-date.js +17 -0
- package/dist/src/components/schedule/days/parts/skipped-date.js.map +1 -0
- package/dist/src/components/schedule/days/parts/transition.d.ts +3 -0
- package/dist/src/components/schedule/days/parts/transition.d.ts.map +1 -0
- package/dist/src/components/schedule/days/parts/transition.js +42 -0
- package/dist/src/components/schedule/days/parts/transition.js.map +1 -0
- package/dist/src/components/schedule/index.d.ts +3 -0
- package/dist/src/components/schedule/index.d.ts.map +1 -0
- package/dist/src/components/schedule/index.js +45 -0
- package/dist/src/components/schedule/index.js.map +1 -0
- package/dist/src/components/schedule/layout.d.ts +15 -0
- package/dist/src/components/schedule/layout.d.ts.map +1 -0
- package/dist/src/components/schedule/layout.js +10 -0
- package/dist/src/components/schedule/layout.js.map +1 -0
- package/dist/src/components/schedule/parts/gutter.d.ts +3 -0
- package/dist/src/components/schedule/parts/gutter.d.ts.map +1 -0
- package/dist/src/components/schedule/parts/gutter.js +9 -0
- package/dist/src/components/schedule/parts/gutter.js.map +1 -0
- package/dist/src/components/schedule/parts/incomplete.d.ts +6 -0
- package/dist/src/components/schedule/parts/incomplete.d.ts.map +1 -0
- package/dist/src/components/schedule/parts/incomplete.js +9 -0
- package/dist/src/components/schedule/parts/incomplete.js.map +1 -0
- package/dist/src/components/schedule/schedule.types.d.ts +109 -0
- package/dist/src/components/schedule/schedule.types.d.ts.map +1 -0
- package/dist/src/components/schedule/schedule.types.js +3 -0
- package/dist/src/components/schedule/schedule.types.js.map +1 -0
- package/dist/src/components/schedule/use-schedule-viewport.d.ts +7 -0
- package/dist/src/components/schedule/use-schedule-viewport.d.ts.map +1 -0
- package/dist/src/components/schedule/use-schedule-viewport.js +15 -0
- package/dist/src/components/schedule/use-schedule-viewport.js.map +1 -0
- package/dist/src/components/schedule/use-schedule.d.ts +3 -0
- package/dist/src/components/schedule/use-schedule.d.ts.map +1 -0
- package/dist/src/components/schedule/use-schedule.js +71 -0
- package/dist/src/components/schedule/use-schedule.js.map +1 -0
- package/dist/src/components/schedule/utils/days.d.ts +17 -0
- package/dist/src/components/schedule/utils/days.d.ts.map +1 -0
- package/dist/src/components/schedule/utils/days.js +49 -0
- package/dist/src/components/schedule/utils/days.js.map +1 -0
- package/dist/src/core/axis.d.ts +18 -0
- package/dist/src/core/axis.d.ts.map +1 -0
- package/dist/src/core/axis.js +58 -0
- package/dist/src/core/axis.js.map +1 -0
- package/dist/src/core/cache.d.ts +29 -0
- package/dist/src/core/cache.d.ts.map +1 -0
- package/dist/src/core/cache.js +33 -0
- package/dist/src/core/cache.js.map +1 -0
- package/dist/src/core/columns.d.ts +3 -0
- package/dist/src/core/columns.d.ts.map +1 -0
- package/dist/src/core/columns.js +45 -0
- package/dist/src/core/columns.js.map +1 -0
- package/dist/src/core/coverage.d.ts +3 -0
- package/dist/src/core/coverage.d.ts.map +1 -0
- package/dist/src/core/coverage.js +60 -0
- package/dist/src/core/coverage.js.map +1 -0
- package/dist/src/core/flag.d.ts +3 -0
- package/dist/src/core/flag.d.ts.map +1 -0
- package/dist/src/core/flag.js +17 -0
- package/dist/src/core/flag.js.map +1 -0
- package/dist/src/core/hash.d.ts +5 -0
- package/dist/src/core/hash.d.ts.map +1 -0
- package/dist/src/core/hash.js +29 -0
- package/dist/src/core/hash.js.map +1 -0
- package/dist/src/core/hit-test.d.ts +6 -0
- package/dist/src/core/hit-test.d.ts.map +1 -0
- package/dist/src/core/hit-test.js +23 -0
- package/dist/src/core/hit-test.js.map +1 -0
- package/dist/src/core/index.d.ts +12 -0
- package/dist/src/core/index.d.ts.map +1 -0
- package/dist/src/core/index.js +30 -0
- package/dist/src/core/index.js.map +1 -0
- package/dist/src/core/layout.d.ts +3 -0
- package/dist/src/core/layout.d.ts.map +1 -0
- package/dist/src/core/layout.js +96 -0
- package/dist/src/core/layout.js.map +1 -0
- package/dist/src/core/order.d.ts +7 -0
- package/dist/src/core/order.d.ts.map +1 -0
- package/dist/src/core/order.js +12 -0
- package/dist/src/core/order.js.map +1 -0
- package/dist/src/core/scale.d.ts +9 -0
- package/dist/src/core/scale.d.ts.map +1 -0
- package/dist/src/core/scale.js +41 -0
- package/dist/src/core/scale.js.map +1 -0
- package/dist/src/core/snap.d.ts +10 -0
- package/dist/src/core/snap.d.ts.map +1 -0
- package/dist/src/core/snap.js +43 -0
- package/dist/src/core/snap.js.map +1 -0
- package/dist/src/core/spans.d.ts +3 -0
- package/dist/src/core/spans.d.ts.map +1 -0
- package/dist/src/core/spans.js +60 -0
- package/dist/src/core/spans.js.map +1 -0
- package/dist/src/core/types.d.ts +104 -0
- package/dist/src/core/types.d.ts.map +1 -0
- package/dist/src/core/types.js +3 -0
- package/dist/src/core/types.js.map +1 -0
- package/dist/src/core/zone.d.ts +10 -0
- package/dist/src/core/zone.d.ts.map +1 -0
- package/dist/src/core/zone.js +103 -0
- package/dist/src/core/zone.js.map +1 -0
- package/dist/src/index.d.ts +27 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +63 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/nativewind/index.d.ts +61 -0
- package/dist/src/nativewind/index.d.ts.map +1 -0
- package/dist/src/nativewind/index.js +24 -0
- package/dist/src/nativewind/index.js.map +1 -0
- package/llms.txt +481 -0
- package/package.json +154 -8
- package/src/adapters/rrule/README.md +41 -0
- package/src/adapters/rrule/cache.ts +50 -0
- package/src/adapters/rrule/envelope.ts +7 -0
- package/src/adapters/rrule/expand.ts +105 -0
- package/src/adapters/rrule/hash.ts +13 -0
- package/src/adapters/rrule/index.ts +11 -0
- package/src/adapters/rrule/net.ts +30 -0
- package/src/adapters/rrule/occurrences.ts +288 -0
- package/src/adapters/rrule/types.ts +52 -0
- package/src/adapters/rrule/validate.ts +78 -0
- package/src/components/layers/README.md +11 -0
- package/src/components/layers/layers.types.ts +6 -0
- package/src/components/layers/parts/gap.tsx +5 -0
- package/src/components/layers/parts/interval.tsx +24 -0
- package/src/components/layers/utils/styles.ts +24 -0
- package/src/components/primitives/README.md +12 -0
- package/src/components/primitives/press-point.tsx +5 -0
- package/src/components/primitives/press-point.types.ts +5 -0
- package/src/components/primitives/press-point.web.tsx +19 -0
- package/src/components/primitives/region-style.ts +21 -0
- package/src/components/roster/README.md +18 -0
- package/src/components/roster/index.tsx +90 -0
- package/src/components/roster/lanes/interval-hover.tsx +5 -0
- package/src/components/roster/lanes/interval-hover.types.ts +11 -0
- package/src/components/roster/lanes/interval-hover.web.tsx +19 -0
- package/src/components/roster/lanes/lane.tsx +88 -0
- package/src/components/roster/lanes/parts/incomplete.tsx +44 -0
- package/src/components/roster/lanes/parts/lane-label.tsx +41 -0
- package/src/components/roster/layout.tsx +68 -0
- package/src/components/roster/parts/body.tsx +91 -0
- package/src/components/roster/parts/corner.tsx +3 -0
- package/src/components/roster/parts/empty.tsx +5 -0
- package/src/components/roster/parts/grid.tsx +26 -0
- package/src/components/roster/parts/header-cell.tsx +10 -0
- package/src/components/roster/parts/header.tsx +18 -0
- package/src/components/roster/parts/lane-label-column.tsx +31 -0
- package/src/components/roster/roster.types.ts +155 -0
- package/src/components/roster/use-roster.ts +133 -0
- package/src/components/roster/utils/body-content-key.ts +46 -0
- package/src/components/roster/utils/ticks.ts +62 -0
- package/src/components/schedule/README.md +18 -0
- package/src/components/schedule/days/day.tsx +76 -0
- package/src/components/schedule/days/header-layout.tsx +12 -0
- package/src/components/schedule/days/layout.tsx +38 -0
- package/src/components/schedule/days/parts/column.tsx +59 -0
- package/src/components/schedule/days/parts/day-header.tsx +18 -0
- package/src/components/schedule/days/parts/grid.tsx +21 -0
- package/src/components/schedule/days/parts/now-line.tsx +11 -0
- package/src/components/schedule/days/parts/skipped-date.tsx +22 -0
- package/src/components/schedule/days/parts/transition.tsx +70 -0
- package/src/components/schedule/index.tsx +109 -0
- package/src/components/schedule/layout.tsx +42 -0
- package/src/components/schedule/parts/gutter.tsx +16 -0
- package/src/components/schedule/parts/incomplete.tsx +13 -0
- package/src/components/schedule/schedule.types.ts +103 -0
- package/src/components/schedule/use-schedule-viewport.ts +13 -0
- package/src/components/schedule/use-schedule.ts +75 -0
- package/src/components/schedule/utils/days.ts +54 -0
- package/src/core/README.md +25 -0
- package/src/core/axis.ts +58 -0
- package/src/core/cache.ts +38 -0
- package/src/core/columns.ts +49 -0
- package/src/core/coverage.ts +56 -0
- package/src/core/flag.ts +14 -0
- package/src/core/hash.ts +28 -0
- package/src/core/hit-test.ts +27 -0
- package/src/core/index.ts +34 -0
- package/src/core/layout.ts +101 -0
- package/src/core/order.ts +18 -0
- package/src/core/scale.ts +45 -0
- package/src/core/snap.ts +52 -0
- package/src/core/spans.ts +58 -0
- package/src/core/types.ts +92 -0
- package/src/core/zone.ts +100 -0
- package/src/index.ts +49 -0
- package/src/nativewind/README.md +14 -0
- package/src/nativewind/index.ts +30 -0
- package/index.js +0 -4
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# rrule adapter
|
|
2
|
+
|
|
3
|
+
The shipped adapter. It converts a `RuleSet` of RFC 5545 rules and dated
|
|
4
|
+
overrides into absolute intervals and gaps for one window, with exact
|
|
5
|
+
provenance per span.
|
|
6
|
+
|
|
7
|
+
- Public subpath: `react-native-roster/rrule`
|
|
8
|
+
- Exports: `expandRuleSet`, `envelopeFor`, `clearExpandCache`, `expandStats`,
|
|
9
|
+
`resetExpandStats`, and the `RosterRule`, `RosterDate`, `RuleSet`,
|
|
10
|
+
`ExpandOptions`, `ExpandResult`, and `ExpandStats` types
|
|
11
|
+
- Imports: `../../core` only; never components or another adapter
|
|
12
|
+
- Owns: `rrule-temporal` and the Temporal polyfill; root and core never load them
|
|
13
|
+
|
|
14
|
+
## Supported input
|
|
15
|
+
|
|
16
|
+
`RosterRule` covers DAILY, WEEKLY, and MONTHLY with `interval`, `count`,
|
|
17
|
+
`until`, `wkst`, `byweekday`, `bymonth`, `bymonthday`, and `bysetpos`, plus
|
|
18
|
+
`hourstart`, `hourend`, and an IANA `timezone`. `RosterDate` is a single local
|
|
19
|
+
date, whole day or with hours. Both are `include` or `exclude`. YEARLY and
|
|
20
|
+
unbounded strings are rejected by validation.
|
|
21
|
+
|
|
22
|
+
## File map
|
|
23
|
+
|
|
24
|
+
| File | Role |
|
|
25
|
+
| --- | --- |
|
|
26
|
+
| `index.ts` | the entry point; the only barrel |
|
|
27
|
+
| `types.ts` | input and result types |
|
|
28
|
+
| `validate.ts` | input rejection before any engine work |
|
|
29
|
+
| `envelope.ts` | widens a window to whole local days per rule zone |
|
|
30
|
+
| `occurrences.ts` | drives the engine and applies every calendar correction |
|
|
31
|
+
| `net.ts` | nets includes against excludes into intervals and gaps |
|
|
32
|
+
| `cache.ts`, `hash.ts` | retained occurrences and envelopes keyed by content |
|
|
33
|
+
| `expand.ts` | the orchestrator: validate, envelope, expand, cap, net, attach |
|
|
34
|
+
|
|
35
|
+
## Reference
|
|
36
|
+
|
|
37
|
+
- [Recurrence semantics](../../../docs/recurrence.md): the rules `occurrences.ts` follows
|
|
38
|
+
- [Caches](../../../docs/caches.md): retention, keys, and counters
|
|
39
|
+
- [Adapters guide](../../../docs/adapters.md): the consumer view for any upstream shape
|
|
40
|
+
- [Adding an adapter](../../../docs/adding-an-adapter.md): the contributor recipe
|
|
41
|
+
- Tests: `test/adapters/rrule`
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { Window } from '../../core';
|
|
2
|
+
import { envelopeFor } from './envelope';
|
|
3
|
+
import type { ExpandStats } from './types';
|
|
4
|
+
|
|
5
|
+
export type Occurrences = { spans: Window[]; capped: boolean };
|
|
6
|
+
export const occurrences = new Map<string, Occurrences>();
|
|
7
|
+
const envelopes = new Map<string, Window>();
|
|
8
|
+
const counts: ExpandStats = { rules: 0, dates: 0, expanded: 0, cacheHits: 0, cacheMisses: 0 };
|
|
9
|
+
|
|
10
|
+
// Map insertion order is the LRU: reads move entries to the newest end.
|
|
11
|
+
// Envelope retention selects bounds only; no netted result is retained.
|
|
12
|
+
export function touch<T>(cache: Map<string, T>, key: string, value: T): T {
|
|
13
|
+
cache.delete(key);
|
|
14
|
+
cache.set(key, value);
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function trim<T>(cache: Map<string, T>, limit: number): void {
|
|
19
|
+
while (cache.size > limit) cache.delete(cache.keys().next().value as string);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function retainedEnvelope(window: Window, limit: number): Window {
|
|
23
|
+
trim(envelopes, limit);
|
|
24
|
+
for (const [id, envelope] of [...envelopes].reverse()) {
|
|
25
|
+
if (envelope.start <= window.start && envelope.end >= window.end) {
|
|
26
|
+
return touch(envelopes, id, envelope);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const envelope = envelopeFor(window);
|
|
30
|
+
touch(envelopes, JSON.stringify(envelope), envelope);
|
|
31
|
+
trim(envelopes, limit);
|
|
32
|
+
return envelope;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function recordStats(stats: ExpandStats): void {
|
|
36
|
+
for (const key of Object.keys(counts) as Array<keyof ExpandStats>) counts[key] += stats[key];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function expandStats(): ExpandStats {
|
|
40
|
+
return { ...counts };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function resetExpandStats(): void {
|
|
44
|
+
for (const key of Object.keys(counts) as Array<keyof ExpandStats>) counts[key] = 0;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function clearExpandCache(): void {
|
|
48
|
+
occurrences.clear();
|
|
49
|
+
envelopes.clear();
|
|
50
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Window } from '../../core';
|
|
2
|
+
|
|
3
|
+
// Absolute 48-hour padding exceeds the 26-hour difference between view zones.
|
|
4
|
+
// Includes, excludes, and dates share these bounds before subtraction.
|
|
5
|
+
export function envelopeFor(window: Window): Window {
|
|
6
|
+
return { start: window.start - 48 * 3_600_000, end: window.end + 48 * 3_600_000 };
|
|
7
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { Interval, Source, Window } from '../../core';
|
|
2
|
+
import { occurrences, recordStats, retainedEnvelope, touch, trim } from './cache';
|
|
3
|
+
import { envelopeFor } from './envelope';
|
|
4
|
+
import { bodyKey } from './hash';
|
|
5
|
+
import { net } from './net';
|
|
6
|
+
import { enumerate, validateDates } from './occurrences';
|
|
7
|
+
import type {
|
|
8
|
+
ExpandOptions,
|
|
9
|
+
ExpandResult,
|
|
10
|
+
ExpandStats,
|
|
11
|
+
RosterDate,
|
|
12
|
+
RosterRule,
|
|
13
|
+
RuleSet,
|
|
14
|
+
} from './types';
|
|
15
|
+
import { nonnegativeInteger, validateInput, validateWindow } from './validate';
|
|
16
|
+
|
|
17
|
+
export function expandRuleSet(
|
|
18
|
+
set: RuleSet,
|
|
19
|
+
window: Window,
|
|
20
|
+
options: ExpandOptions = {},
|
|
21
|
+
): ExpandResult {
|
|
22
|
+
validateWindow(window);
|
|
23
|
+
const maxEntries = nonnegativeInteger(options.cache?.maxEntries ?? 2000, 'cache.maxEntries');
|
|
24
|
+
const maxEnvelopes = nonnegativeInteger(options.cache?.maxEnvelopes ?? 4, 'cache.maxEnvelopes');
|
|
25
|
+
const cap = nonnegativeInteger(
|
|
26
|
+
options.caps?.perRuleOccurrences ?? 400,
|
|
27
|
+
'caps.perRuleOccurrences',
|
|
28
|
+
);
|
|
29
|
+
let remaining = nonnegativeInteger(
|
|
30
|
+
options.caps?.totalOccurrences ?? 10000,
|
|
31
|
+
'caps.totalOccurrences',
|
|
32
|
+
);
|
|
33
|
+
const rules = [...set.rules].sort(byId);
|
|
34
|
+
const dates = [...set.dates].sort(byId);
|
|
35
|
+
const ordered = [...rules, ...dates];
|
|
36
|
+
for (const input of ordered) {
|
|
37
|
+
validateInput(input);
|
|
38
|
+
validateDates(input);
|
|
39
|
+
}
|
|
40
|
+
const stats: ExpandStats = {
|
|
41
|
+
rules: set.rules.length,
|
|
42
|
+
dates: set.dates.length,
|
|
43
|
+
expanded: 0,
|
|
44
|
+
cacheHits: 0,
|
|
45
|
+
cacheMisses: 0,
|
|
46
|
+
};
|
|
47
|
+
if (window.start === window.end) {
|
|
48
|
+
recordStats(stats);
|
|
49
|
+
return {
|
|
50
|
+
intervals: [],
|
|
51
|
+
gaps: [],
|
|
52
|
+
envelope: envelopeFor(window),
|
|
53
|
+
complete: true,
|
|
54
|
+
truncated: [],
|
|
55
|
+
stats,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
const envelope = retainedEnvelope(window, maxEnvelopes);
|
|
59
|
+
trim(occurrences, maxEntries);
|
|
60
|
+
const includes: Interval[] = [];
|
|
61
|
+
const excludes: Interval[] = [];
|
|
62
|
+
const truncated: ExpandResult['truncated'] = [];
|
|
63
|
+
let dropped = false;
|
|
64
|
+
for (const input of ordered) {
|
|
65
|
+
const key = JSON.stringify([bodyKey(input), envelope.start, envelope.end, cap]);
|
|
66
|
+
let list = occurrences.get(key);
|
|
67
|
+
if (list) {
|
|
68
|
+
touch(occurrences, key, list);
|
|
69
|
+
stats.cacheHits++;
|
|
70
|
+
} else {
|
|
71
|
+
list = enumerate(input, envelope, cap);
|
|
72
|
+
touch(occurrences, key, list);
|
|
73
|
+
trim(occurrences, maxEntries);
|
|
74
|
+
stats.expanded++;
|
|
75
|
+
stats.cacheMisses++;
|
|
76
|
+
}
|
|
77
|
+
// No partial admission: the first list that cannot fit ends the admitted
|
|
78
|
+
// prefix. Fetch all later lists anyway to report their exact retained counts.
|
|
79
|
+
dropped ||= list.spans.length > remaining;
|
|
80
|
+
if (dropped || list.capped) {
|
|
81
|
+
truncated.push({
|
|
82
|
+
id: input.id,
|
|
83
|
+
droppedAtLeast: list.capped ? 1 : list.spans.length,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
if (dropped) continue;
|
|
87
|
+
remaining -= list.spans.length;
|
|
88
|
+
const source: Source = { kind: 'date' in input ? 'date' : 'rule', id: input.id };
|
|
89
|
+
if ('date' in input && input.note !== undefined) source.label = input.note;
|
|
90
|
+
const target = input.kind === 'include' ? includes : excludes;
|
|
91
|
+
for (const span of list.spans) target.push({ ...span, sources: [source] });
|
|
92
|
+
}
|
|
93
|
+
recordStats(stats);
|
|
94
|
+
return {
|
|
95
|
+
...net(includes, excludes, window),
|
|
96
|
+
envelope: { ...envelope },
|
|
97
|
+
complete: truncated.length === 0,
|
|
98
|
+
truncated,
|
|
99
|
+
stats,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function byId(a: RosterRule | RosterDate, b: RosterRule | RosterDate): number {
|
|
104
|
+
return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
|
|
105
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { structuralKey } from '../../core/hash';
|
|
2
|
+
import type { RosterDate, RosterRule } from './types';
|
|
3
|
+
|
|
4
|
+
// Use the core's collision-free canonical encoding as the content hash.
|
|
5
|
+
// Identity and display notes are attached fresh, never cached in occurrence lists.
|
|
6
|
+
export function bodyKey(input: RosterRule | RosterDate): string {
|
|
7
|
+
const { id: _id, ...body } = input;
|
|
8
|
+
if ('date' in body) {
|
|
9
|
+
const { note: _note, ...date } = body;
|
|
10
|
+
return structuralKey(date);
|
|
11
|
+
}
|
|
12
|
+
return structuralKey(body);
|
|
13
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { clearExpandCache, expandStats, resetExpandStats } from './cache';
|
|
2
|
+
export { envelopeFor } from './envelope';
|
|
3
|
+
export { expandRuleSet } from './expand';
|
|
4
|
+
export type {
|
|
5
|
+
ExpandOptions,
|
|
6
|
+
ExpandResult,
|
|
7
|
+
ExpandStats,
|
|
8
|
+
RosterDate,
|
|
9
|
+
RosterRule,
|
|
10
|
+
RuleSet,
|
|
11
|
+
} from './types';
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Gap, Interval, Window } from '../../core';
|
|
2
|
+
import { sourceSpans } from '../../core/spans';
|
|
3
|
+
|
|
4
|
+
export function net(
|
|
5
|
+
includes: Interval[],
|
|
6
|
+
excludes: Interval[],
|
|
7
|
+
window: Window,
|
|
8
|
+
): { intervals: Interval[]; gaps: Gap[] } {
|
|
9
|
+
const covered = sourceSpans(includes, window);
|
|
10
|
+
const removed = sourceSpans(excludes, window);
|
|
11
|
+
const intervals: Interval[] = [];
|
|
12
|
+
const gaps: Gap[] = [];
|
|
13
|
+
let first = 0;
|
|
14
|
+
for (const include of covered) {
|
|
15
|
+
let start = include.start;
|
|
16
|
+
while (first < removed.length && (removed[first] as Gap).end <= start) first++;
|
|
17
|
+
for (let i = first; i < removed.length; i++) {
|
|
18
|
+
const exclude = removed[i] as Gap;
|
|
19
|
+
if (exclude.start >= include.end) break;
|
|
20
|
+
if (start < exclude.start)
|
|
21
|
+
intervals.push({ start, end: exclude.start, sources: include.sources });
|
|
22
|
+
const end = Math.min(include.end, exclude.end);
|
|
23
|
+
gaps.push({ start: Math.max(start, exclude.start), end, sources: exclude.sources });
|
|
24
|
+
start = end;
|
|
25
|
+
}
|
|
26
|
+
if (start < include.end) intervals.push({ start, end: include.end, sources: include.sources });
|
|
27
|
+
}
|
|
28
|
+
// Include source changes cannot split an otherwise identical removed span.
|
|
29
|
+
return { intervals, gaps: sourceSpans(gaps, window) };
|
|
30
|
+
}
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import type * as RRuleModule from 'rrule-temporal' with { 'resolution-mode': 'import' };
|
|
2
|
+
import type * as Spec from 'temporal-spec' with { 'resolution-mode': 'import' };
|
|
3
|
+
import type { Weekday, Window } from '../../core';
|
|
4
|
+
import type { Occurrences } from './cache';
|
|
5
|
+
import type { RosterDate, RosterRule } from './types';
|
|
6
|
+
|
|
7
|
+
// 1.6.0 is an ES module package with a CommonJS build under its require condition
|
|
8
|
+
// and declarations resolved under import. Keep its actual types while selecting
|
|
9
|
+
// the require export for our CJS build and Metro source entry.
|
|
10
|
+
const { allowedWeekdays, RRuleTemporal } = require('rrule-temporal') as typeof RRuleModule;
|
|
11
|
+
// Both engines must use the same polyfill export; mixed import/require copies
|
|
12
|
+
// fail the recurrence engine's ZonedDateTime instanceof check.
|
|
13
|
+
const { Temporal } = require('@js-temporal/polyfill') as { Temporal: typeof Spec.Temporal };
|
|
14
|
+
|
|
15
|
+
type RuleDateTime = Spec.Temporal.PlainDateTime | Spec.Temporal.ZonedDateTime;
|
|
16
|
+
|
|
17
|
+
export function enumerate(
|
|
18
|
+
input: RosterRule | RosterDate,
|
|
19
|
+
envelope: Window,
|
|
20
|
+
cap: number,
|
|
21
|
+
// Internal reference path for testing against original-anchor enumeration.
|
|
22
|
+
advanceAnchor = true,
|
|
23
|
+
): Occurrences {
|
|
24
|
+
const result: Occurrences = { spans: [], capped: false };
|
|
25
|
+
const admit = (date: Spec.Temporal.PlainDate): boolean => {
|
|
26
|
+
const span = hoursFor(date, input);
|
|
27
|
+
if (span.start >= envelope.end || span.end <= envelope.start || span.start >= span.end)
|
|
28
|
+
return true;
|
|
29
|
+
if (result.spans.length === cap) {
|
|
30
|
+
result.capped = true;
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
result.spans.push(span);
|
|
34
|
+
return true;
|
|
35
|
+
};
|
|
36
|
+
if ('date' in input) {
|
|
37
|
+
admit(Temporal.PlainDate.from(input.date));
|
|
38
|
+
return result;
|
|
39
|
+
}
|
|
40
|
+
const original = dateTime(input.dtstart, input.timezone);
|
|
41
|
+
const originalWall = plain(original);
|
|
42
|
+
// A cross-date rollback can leave the end on the preceding wall date.
|
|
43
|
+
// Include the following date completely; envelope clipping discards extra candidates.
|
|
44
|
+
const upper = Temporal.Instant.fromEpochMilliseconds(envelope.end)
|
|
45
|
+
.toZonedDateTimeISO(input.timezone)
|
|
46
|
+
.add({ days: 2 })
|
|
47
|
+
.startOfDay();
|
|
48
|
+
const until =
|
|
49
|
+
input.until === undefined
|
|
50
|
+
? upper.toPlainDateTime()
|
|
51
|
+
: dateTime(input.until, input.timezone, true);
|
|
52
|
+
const untilDate =
|
|
53
|
+
input.until !== undefined && /^\d{4}-\d{2}-\d{2}$/.test(input.until)
|
|
54
|
+
? Temporal.PlainDate.from(input.until)
|
|
55
|
+
: undefined;
|
|
56
|
+
if (
|
|
57
|
+
!untilDate &&
|
|
58
|
+
(until instanceof Temporal.ZonedDateTime
|
|
59
|
+
? Temporal.ZonedDateTime.compare(
|
|
60
|
+
until,
|
|
61
|
+
anchorFor(original.toPlainDate(), original, input.timezone),
|
|
62
|
+
) < 0
|
|
63
|
+
: Temporal.PlainDateTime.compare(until, originalWall) < 0)
|
|
64
|
+
)
|
|
65
|
+
return result;
|
|
66
|
+
const anchorWallTime = original.toPlainTime();
|
|
67
|
+
// Iterate wall dates in UTC so zone skips cannot normalize a candidate date.
|
|
68
|
+
// The callback applies the original date cutoff or the exact datetime UNTIL.
|
|
69
|
+
const untilDateEnd = (untilDate ?? until.toPlainDate())
|
|
70
|
+
// An instant after a midnight rollback can have the preceding local date.
|
|
71
|
+
// Keep the later wall date available for the callback's exact-instant filter.
|
|
72
|
+
.add({ days: until instanceof Temporal.ZonedDateTime ? 1 : 0 })
|
|
73
|
+
.toPlainDateTime('23:59:59.999')
|
|
74
|
+
.toZonedDateTime('UTC');
|
|
75
|
+
const upperDate = upper.toPlainDateTime().toZonedDateTime('UTC');
|
|
76
|
+
// Only midnight anchors with interval 1 and no COUNT may skip periods.
|
|
77
|
+
const anchor =
|
|
78
|
+
advanceAnchor &&
|
|
79
|
+
(input.interval === undefined || input.interval === 1) &&
|
|
80
|
+
input.count === undefined &&
|
|
81
|
+
anchorWallTime.equals('00:00')
|
|
82
|
+
? advance(originalWall, envelope, input)
|
|
83
|
+
: originalWall;
|
|
84
|
+
const positional = !!input.bysetpos?.length;
|
|
85
|
+
const unit = periodUnit(input);
|
|
86
|
+
const firstPeriod = periodStart(anchor.toPlainDate(), input);
|
|
87
|
+
const bound =
|
|
88
|
+
Temporal.ZonedDateTime.compare(untilDateEnd, upperDate) < 0 ? untilDateEnd : upperDate;
|
|
89
|
+
const lastPeriod = periodStart(bound.toPlainDate(), input);
|
|
90
|
+
if (Temporal.PlainDate.compare(firstPeriod, lastPeriod) > 0) return result;
|
|
91
|
+
// 1.5.2 tests UNTIL only while visiting candidates in its monthly loop.
|
|
92
|
+
// Every supported loop advances at least one authored period per iteration.
|
|
93
|
+
// Exhaustion after the final query period is complete, even with no candidates.
|
|
94
|
+
const maxIterations =
|
|
95
|
+
Math.floor(firstPeriod.until(lastPeriod, { largestUnit: unit })[unit] / (input.interval ?? 1)) +
|
|
96
|
+
1;
|
|
97
|
+
const engine = new RRuleTemporal({
|
|
98
|
+
freq: input.frequency,
|
|
99
|
+
dtstart: firstPeriod.toPlainDateTime(anchorWallTime).toZonedDateTime('UTC'),
|
|
100
|
+
// Positions must see the entire final period before UNTIL and envelope admission.
|
|
101
|
+
until: positional
|
|
102
|
+
? lastPeriod
|
|
103
|
+
.add({ [unit]: 1 })
|
|
104
|
+
.toZonedDateTime('UTC')
|
|
105
|
+
.subtract({ milliseconds: 1 })
|
|
106
|
+
: bound,
|
|
107
|
+
interval: input.interval,
|
|
108
|
+
wkst: allowedWeekdays[input.wkst ?? 0],
|
|
109
|
+
byDay:
|
|
110
|
+
input.frequency !== 'DAILY' && input.byweekday?.length
|
|
111
|
+
? input.byweekday.map((day) => allowedWeekdays[day])
|
|
112
|
+
: input.frequency === 'WEEKLY' && !input.bymonthday?.length
|
|
113
|
+
? [allowedWeekdays[(original.dayOfWeek - 1) as Weekday]]
|
|
114
|
+
: undefined,
|
|
115
|
+
byMonth: input.bymonth,
|
|
116
|
+
// Explicitly preserve the implicit monthly day; 1.5.2's fallback otherwise
|
|
117
|
+
// constrains a 31st through February and drifts subsequent months.
|
|
118
|
+
byMonthDay: input.bymonthday?.length
|
|
119
|
+
? input.bymonthday
|
|
120
|
+
: input.frequency === 'MONTHLY' && !input.byweekday?.length
|
|
121
|
+
? [original.day]
|
|
122
|
+
: undefined,
|
|
123
|
+
tzid: 'UTC',
|
|
124
|
+
includeDtstart: false,
|
|
125
|
+
maxIterations,
|
|
126
|
+
});
|
|
127
|
+
// Version 1.5.2 can replay the iterator when applying its final count filter.
|
|
128
|
+
// Admit each local date once so replay cannot consume COUNT or the cap twice.
|
|
129
|
+
const visited = new Set<string>();
|
|
130
|
+
let admitted = 0;
|
|
131
|
+
const visit = (date: Spec.Temporal.PlainDate): boolean => {
|
|
132
|
+
if (result.capped || admitted === input.count) return false;
|
|
133
|
+
if (Temporal.PlainDate.compare(date, original.toPlainDate()) < 0) return true;
|
|
134
|
+
const key = date.toString();
|
|
135
|
+
if (visited.has(key)) return true;
|
|
136
|
+
visited.add(key);
|
|
137
|
+
if (untilDate) {
|
|
138
|
+
if (Temporal.PlainDate.compare(date, untilDate) > 0) return true;
|
|
139
|
+
} else if (
|
|
140
|
+
until instanceof Temporal.ZonedDateTime
|
|
141
|
+
? Temporal.ZonedDateTime.compare(anchorFor(date, original, input.timezone), until) > 0
|
|
142
|
+
: Temporal.PlainDateTime.compare(date.toPlainDateTime(anchorWallTime), until) > 0
|
|
143
|
+
)
|
|
144
|
+
return true;
|
|
145
|
+
if (!date.toZonedDateTime(input.timezone).toPlainDate().equals(date)) return true;
|
|
146
|
+
// Lifetime COUNT includes existing dates before the envelope, but never skipped dates.
|
|
147
|
+
admitted++;
|
|
148
|
+
return admit(date);
|
|
149
|
+
};
|
|
150
|
+
let candidateCount = 0;
|
|
151
|
+
let period = '';
|
|
152
|
+
let candidates: Spec.Temporal.PlainDate[] = [];
|
|
153
|
+
const flush = (): boolean => {
|
|
154
|
+
const selected = candidates.filter((_, index) =>
|
|
155
|
+
input.bysetpos?.some((position) =>
|
|
156
|
+
position > 0 ? index === position - 1 : index === candidates.length + position,
|
|
157
|
+
),
|
|
158
|
+
);
|
|
159
|
+
candidates = [];
|
|
160
|
+
return selected.every(visit);
|
|
161
|
+
};
|
|
162
|
+
try {
|
|
163
|
+
engine.all((occurrence, index) => {
|
|
164
|
+
const date = occurrence.toPlainDate();
|
|
165
|
+
// Replayed callback indices restart at zero. They must not enlarge a
|
|
166
|
+
// buffered period, especially when the whole query has only one candidate.
|
|
167
|
+
if (positional && index < candidateCount) return false;
|
|
168
|
+
candidateCount++;
|
|
169
|
+
// DAILY BYDAY re-anchors the engine; filter the authored sequence here.
|
|
170
|
+
if (
|
|
171
|
+
input.frequency === 'DAILY' &&
|
|
172
|
+
input.byweekday?.length &&
|
|
173
|
+
!input.byweekday.includes((date.dayOfWeek - 1) as Weekday)
|
|
174
|
+
)
|
|
175
|
+
return true;
|
|
176
|
+
if (!positional) return visit(date);
|
|
177
|
+
const key = periodStart(date, input).toString();
|
|
178
|
+
if (key !== period) {
|
|
179
|
+
if (!flush()) return false;
|
|
180
|
+
period = key;
|
|
181
|
+
}
|
|
182
|
+
candidates.push(date);
|
|
183
|
+
return true;
|
|
184
|
+
});
|
|
185
|
+
} catch (error) {
|
|
186
|
+
if (
|
|
187
|
+
!(error instanceof Error) ||
|
|
188
|
+
error.message !== `Maximum iterations (${maxIterations}) exceeded in all()`
|
|
189
|
+
)
|
|
190
|
+
throw error;
|
|
191
|
+
}
|
|
192
|
+
flush();
|
|
193
|
+
return result;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Validate temporal fields even when the display window has no elapsed time.
|
|
197
|
+
export function validateDates(input: RosterRule | RosterDate): void {
|
|
198
|
+
if ('date' in input) {
|
|
199
|
+
Temporal.PlainDate.from(input.date).toZonedDateTime(input.timezone);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
plain(dateTime(input.dtstart, input.timezone)).toZonedDateTime(input.timezone);
|
|
203
|
+
if (input.until !== undefined)
|
|
204
|
+
plain(dateTime(input.until, input.timezone, true)).toZonedDateTime(input.timezone);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function hoursFor(date: Spec.Temporal.PlainDate, input: RosterRule | RosterDate): Window {
|
|
208
|
+
const midnight = date.toPlainDateTime();
|
|
209
|
+
const dayStart = date.toZonedDateTime(input.timezone);
|
|
210
|
+
if (!dayStart.toPlainDate().equals(date)) {
|
|
211
|
+
return { start: dayStart.epochMilliseconds, end: dayStart.epochMilliseconds };
|
|
212
|
+
}
|
|
213
|
+
const wholeDay = input.hourstart === undefined && input.hourend === undefined;
|
|
214
|
+
// Whole-day bounds resolve PlainDates to first instants. Explicit hours use
|
|
215
|
+
// compatible wall time, choosing the earlier repeat and advancing skips.
|
|
216
|
+
const at = (hour: number) => {
|
|
217
|
+
const wall = midnight.add({ milliseconds: Math.round(hour * 3_600_000) });
|
|
218
|
+
return (wholeDay ? wall.toPlainDate() : wall).toZonedDateTime(input.timezone).epochMilliseconds;
|
|
219
|
+
};
|
|
220
|
+
return { start: at(input.hourstart ?? 0), end: at(input.hourend ?? 24) };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function dateTime(value: string, timezone: string, endOfDate = false): RuleDateTime {
|
|
224
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(value)) {
|
|
225
|
+
const date = Temporal.PlainDate.from(value);
|
|
226
|
+
return date.toPlainDateTime(endOfDate ? '23:59:59.999' : '00:00');
|
|
227
|
+
}
|
|
228
|
+
if (/(?:Z|[+-]\d{2}:?\d{2})(?:\[[^\]]*\])?$/i.test(value)) {
|
|
229
|
+
return Temporal.Instant.from(value).toZonedDateTimeISO(timezone);
|
|
230
|
+
}
|
|
231
|
+
return Temporal.PlainDateTime.from(value);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function plain(value: RuleDateTime): Spec.Temporal.PlainDateTime {
|
|
235
|
+
return value instanceof Temporal.ZonedDateTime ? value.toPlainDateTime() : value;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Keep the original wall time and back up one period so WKST/BYDAY cannot lose
|
|
239
|
+
// an earlier candidate. A monthly jump must never constrain the 31st to the 28th.
|
|
240
|
+
function advance(
|
|
241
|
+
original: Spec.Temporal.PlainDateTime,
|
|
242
|
+
envelope: Window,
|
|
243
|
+
input: RosterRule,
|
|
244
|
+
): Spec.Temporal.PlainDateTime {
|
|
245
|
+
// Include the local date crossing the left edge, regardless of anchor wall time.
|
|
246
|
+
const lower = Temporal.Instant.fromEpochMilliseconds(envelope.start)
|
|
247
|
+
.toZonedDateTimeISO(input.timezone)
|
|
248
|
+
.startOfDay();
|
|
249
|
+
const unit =
|
|
250
|
+
input.frequency === 'MONTHLY' ? 'months' : input.frequency === 'WEEKLY' ? 'weeks' : 'days';
|
|
251
|
+
const originalDate = original.toPlainDate();
|
|
252
|
+
const interval = input.interval ?? 1;
|
|
253
|
+
const distance = originalDate.until(lower.toPlainDate(), { largestUnit: unit })[unit];
|
|
254
|
+
let steps = Math.max(0, Math.floor(distance / interval) - 1);
|
|
255
|
+
while (steps > 0) {
|
|
256
|
+
const candidate = originalDate.add({ [unit]: steps * interval });
|
|
257
|
+
if (
|
|
258
|
+
(unit !== 'weeks' || candidate.dayOfWeek === originalDate.dayOfWeek) &&
|
|
259
|
+
(unit !== 'months' || candidate.day === originalDate.day) &&
|
|
260
|
+
candidate.toZonedDateTime(input.timezone).toPlainDate().equals(candidate)
|
|
261
|
+
)
|
|
262
|
+
return candidate.toPlainDateTime(original.toPlainTime());
|
|
263
|
+
steps--;
|
|
264
|
+
}
|
|
265
|
+
return original;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Prefer the original offset in repeated time; otherwise resolve skips compatibly.
|
|
269
|
+
function anchorFor(
|
|
270
|
+
date: Spec.Temporal.PlainDate,
|
|
271
|
+
original: RuleDateTime,
|
|
272
|
+
timezone: string,
|
|
273
|
+
): Spec.Temporal.ZonedDateTime {
|
|
274
|
+
return original instanceof Temporal.ZonedDateTime
|
|
275
|
+
? original.with({ year: date.year, month: date.month, day: date.day }, { offset: 'prefer' })
|
|
276
|
+
: date.toPlainDateTime(original.toPlainTime()).toZonedDateTime(timezone);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function periodUnit(input: RosterRule): 'days' | 'weeks' | 'months' {
|
|
280
|
+
return input.frequency === 'MONTHLY' ? 'months' : input.frequency === 'WEEKLY' ? 'weeks' : 'days';
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function periodStart(date: Spec.Temporal.PlainDate, input: RosterRule): Spec.Temporal.PlainDate {
|
|
284
|
+
if (input.frequency === 'MONTHLY') return date.with({ day: 1 });
|
|
285
|
+
if (input.frequency === 'WEEKLY')
|
|
286
|
+
return date.subtract({ days: (date.dayOfWeek - 1 - (input.wkst ?? 0) + 7) % 7 });
|
|
287
|
+
return date;
|
|
288
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { Gap, Interval, Weekday, Window } from '../../core';
|
|
2
|
+
|
|
3
|
+
export type RosterRule = {
|
|
4
|
+
id: string;
|
|
5
|
+
kind: 'include' | 'exclude';
|
|
6
|
+
frequency: 'DAILY' | 'WEEKLY' | 'MONTHLY';
|
|
7
|
+
dtstart: string;
|
|
8
|
+
until?: string;
|
|
9
|
+
count?: number;
|
|
10
|
+
interval?: number;
|
|
11
|
+
wkst?: Weekday;
|
|
12
|
+
byweekday?: Weekday[];
|
|
13
|
+
bymonth?: number[];
|
|
14
|
+
bymonthday?: number[];
|
|
15
|
+
bysetpos?: number[];
|
|
16
|
+
/** Local hours in timezone, end-exclusive; 24 means next local midnight. */
|
|
17
|
+
hourstart: number;
|
|
18
|
+
hourend: number;
|
|
19
|
+
timezone: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type RosterDate = {
|
|
23
|
+
id: string;
|
|
24
|
+
kind: 'include' | 'exclude';
|
|
25
|
+
date: string;
|
|
26
|
+
timezone: string;
|
|
27
|
+
note?: string;
|
|
28
|
+
/** Both hours present for a partial day, or both absent for the whole day. */
|
|
29
|
+
hourstart?: number;
|
|
30
|
+
hourend?: number;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export type RuleSet = { rules: RosterRule[]; dates: RosterDate[] };
|
|
34
|
+
export type ExpandOptions = {
|
|
35
|
+
cache?: { maxEntries?: number; maxEnvelopes?: number };
|
|
36
|
+
caps?: { perRuleOccurrences?: number; totalOccurrences?: number };
|
|
37
|
+
};
|
|
38
|
+
export type ExpandStats = {
|
|
39
|
+
rules: number;
|
|
40
|
+
dates: number;
|
|
41
|
+
expanded: number;
|
|
42
|
+
cacheHits: number;
|
|
43
|
+
cacheMisses: number;
|
|
44
|
+
};
|
|
45
|
+
export type ExpandResult = {
|
|
46
|
+
intervals: Interval[];
|
|
47
|
+
gaps: Gap[];
|
|
48
|
+
envelope: Window;
|
|
49
|
+
complete: boolean;
|
|
50
|
+
truncated: Array<{ id: string; droppedAtLeast: number }>;
|
|
51
|
+
stats: ExpandStats;
|
|
52
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { Window } from '../../core';
|
|
2
|
+
import type { RosterDate, RosterRule } from './types';
|
|
3
|
+
|
|
4
|
+
export function validateWindow(window: Window): void {
|
|
5
|
+
if (
|
|
6
|
+
!Number.isSafeInteger(window.start) ||
|
|
7
|
+
!Number.isSafeInteger(window.end) ||
|
|
8
|
+
window.start > window.end
|
|
9
|
+
) {
|
|
10
|
+
throw new RangeError('window must have integer epoch milliseconds with start <= end');
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function nonnegativeInteger(value: number, name: string): number {
|
|
15
|
+
if (!Number.isSafeInteger(value) || value < 0)
|
|
16
|
+
throw new RangeError(`${name} must be a nonnegative integer`);
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function validateInput(input: RosterRule | RosterDate): void {
|
|
21
|
+
if (!input.id || !['include', 'exclude'].includes(input.kind)) {
|
|
22
|
+
throw new RangeError('Each rule or date needs an id and an include or exclude kind');
|
|
23
|
+
}
|
|
24
|
+
// Bound date strings before any regex sees them; a full offset-and-zone
|
|
25
|
+
// form is under 50 characters, and unbounded input would scan quadratically.
|
|
26
|
+
for (const name of ['dtstart', 'until', 'date'] as const) {
|
|
27
|
+
const value =
|
|
28
|
+
name in input ? (input as Partial<Record<typeof name, unknown>>)[name] : undefined;
|
|
29
|
+
if (value !== undefined && (typeof value !== 'string' || value.length > 64))
|
|
30
|
+
throw new RangeError(`${input.id}: ${name} must be a string of at most 64 characters`);
|
|
31
|
+
}
|
|
32
|
+
const { hourstart, hourend } = input;
|
|
33
|
+
if ((hourstart === undefined) !== (hourend === undefined)) {
|
|
34
|
+
throw new RangeError(`${input.id}: hourstart and hourend must both be present or both absent`);
|
|
35
|
+
}
|
|
36
|
+
if ('frequency' in input) {
|
|
37
|
+
if (!['DAILY', 'WEEKLY', 'MONTHLY'].includes(input.frequency))
|
|
38
|
+
throw new RangeError('Unsupported frequency');
|
|
39
|
+
if (hourstart === undefined) throw new RangeError('Rules require hourstart and hourend');
|
|
40
|
+
for (const name of ['count', 'interval'] as const) {
|
|
41
|
+
const value = input[name];
|
|
42
|
+
if (value !== undefined && (!Number.isSafeInteger(value) || value <= 0))
|
|
43
|
+
throw new RangeError(`${name} must be a positive integer`);
|
|
44
|
+
}
|
|
45
|
+
validateNumbers(input.wkst === undefined ? undefined : [input.wkst], 'wkst', 0, 6);
|
|
46
|
+
validateNumbers(input.byweekday, 'byweekday', 0, 6);
|
|
47
|
+
validateNumbers(input.bymonth, 'bymonth', 1, 12);
|
|
48
|
+
validateNumbers(input.bymonthday, 'bymonthday', -31, 31, true);
|
|
49
|
+
validateNumbers(input.bysetpos, 'bysetpos', -366, 366, true);
|
|
50
|
+
}
|
|
51
|
+
if (
|
|
52
|
+
hourstart !== undefined &&
|
|
53
|
+
hourend !== undefined &&
|
|
54
|
+
(!Number.isFinite(hourstart) ||
|
|
55
|
+
!Number.isFinite(hourend) ||
|
|
56
|
+
hourstart < 0 ||
|
|
57
|
+
hourstart >= hourend ||
|
|
58
|
+
hourend > 24)
|
|
59
|
+
) {
|
|
60
|
+
throw new RangeError(`${input.id}: hours must satisfy 0 <= hourstart < hourend <= 24`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function validateNumbers(
|
|
65
|
+
values: number[] | undefined,
|
|
66
|
+
name: string,
|
|
67
|
+
min: number,
|
|
68
|
+
max: number,
|
|
69
|
+
nonzero = false,
|
|
70
|
+
): void {
|
|
71
|
+
if (
|
|
72
|
+
values?.some(
|
|
73
|
+
(value) => !Number.isInteger(value) || value < min || value > max || (nonzero && value === 0),
|
|
74
|
+
)
|
|
75
|
+
) {
|
|
76
|
+
throw new RangeError(`Invalid ${name}`);
|
|
77
|
+
}
|
|
78
|
+
}
|