| 117 | * the second occurrence). This matches vixie-cron behavior. |
| 118 | */ |
| 119 | export function computeNextCronRun( |
| 120 | fields: CronFields, |
| 121 | from: Date, |
| 122 | ): Date | null { |
| 123 | const minuteSet = new Set(fields.minute) |
| 124 | const hourSet = new Set(fields.hour) |
| 125 | const domSet = new Set(fields.dayOfMonth) |
| 126 | const monthSet = new Set(fields.month) |
| 127 | const dowSet = new Set(fields.dayOfWeek) |
| 128 | |
| 129 | // Is the field wildcarded (full range)? |
| 130 | const domWild = fields.dayOfMonth.length === 31 |
| 131 | const dowWild = fields.dayOfWeek.length === 7 |
| 132 | |
| 133 | // Round up to the next whole minute (strictly after `from`) |
| 134 | const t = new Date(from.getTime()) |
| 135 | t.setSeconds(0, 0) |
| 136 | t.setMinutes(t.getMinutes() + 1) |
| 137 | |
| 138 | const maxIter = 366 * 24 * 60 |
| 139 | for (let i = 0; i < maxIter; i++) { |
| 140 | const month = t.getMonth() + 1 |
| 141 | if (!monthSet.has(month)) { |
| 142 | // Jump to start of next month |
| 143 | t.setMonth(t.getMonth() + 1, 1) |
| 144 | t.setHours(0, 0, 0, 0) |
| 145 | continue |
| 146 | } |
| 147 | |
| 148 | const dom = t.getDate() |
| 149 | const dow = t.getDay() |
| 150 | // When both dom/dow are constrained, either match is sufficient (OR semantics) |
| 151 | const dayMatches = |
| 152 | domWild && dowWild |
| 153 | ? true |
| 154 | : domWild |
| 155 | ? dowSet.has(dow) |
| 156 | : dowWild |
| 157 | ? domSet.has(dom) |
| 158 | : domSet.has(dom) || dowSet.has(dow) |
| 159 | |
| 160 | if (!dayMatches) { |
| 161 | // Jump to start of next day |
| 162 | t.setDate(t.getDate() + 1) |
| 163 | t.setHours(0, 0, 0, 0) |
| 164 | continue |
| 165 | } |
| 166 | |
| 167 | if (!hourSet.has(t.getHours())) { |
| 168 | t.setHours(t.getHours() + 1, 0, 0, 0) |
| 169 | continue |
| 170 | } |
| 171 | |
| 172 | if (!minuteSet.has(t.getMinutes())) { |
| 173 | t.setMinutes(t.getMinutes() + 1) |
| 174 | continue |
| 175 | } |
| 176 | |