effect 3.21.4 → 3.21.5

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/src/Cron.ts CHANGED
@@ -44,6 +44,8 @@ export interface Cron extends Pipeable, Equal.Equal, Inspectable {
44
44
  readonly months: ReadonlySet<number>
45
45
  readonly weekdays: ReadonlySet<number>
46
46
  /** @internal */
47
+ readonly and: boolean
48
+ /** @internal */
47
49
  readonly first: {
48
50
  readonly second: number
49
51
  readonly minute: number
@@ -89,6 +91,7 @@ const CronProto = {
89
91
  [Hash.symbol](this: Cron): number {
90
92
  return pipe(
91
93
  Hash.hash(this.tz),
94
+ Hash.combine(Hash.hash(this.and)),
92
95
  Hash.combine(Hash.array(Arr.fromIterable(this.seconds))),
93
96
  Hash.combine(Hash.array(Arr.fromIterable(this.minutes))),
94
97
  Hash.combine(Hash.array(Arr.fromIterable(this.hours))),
@@ -142,6 +145,7 @@ export const make = (values: {
142
145
  readonly days: Iterable<number>
143
146
  readonly months: Iterable<number>
144
147
  readonly weekdays: Iterable<number>
148
+ readonly and?: boolean | undefined
145
149
  readonly tz?: DateTime.TimeZone | undefined
146
150
  }): Cron => {
147
151
  const o: Mutable<Cron> = Object.create(CronProto)
@@ -151,6 +155,7 @@ export const make = (values: {
151
155
  o.days = new Set(Arr.sort(values.days, N.Order))
152
156
  o.months = new Set(Arr.sort(values.months, N.Order))
153
157
  o.weekdays = new Set(Arr.sort(values.weekdays, N.Order))
158
+ o.and = values.and === true
154
159
  o.tz = Option.fromNullable(values.tz)
155
160
 
156
161
  const seconds = Array.from(o.seconds)
@@ -291,7 +296,7 @@ export const isParseError = (u: unknown): u is ParseError => hasProperty(u, Pars
291
296
  * @category constructors
292
297
  */
293
298
  export const parse = (cron: string, tz?: DateTime.TimeZone | string): Either.Either<Cron, ParseError> => {
294
- const segments = cron.split(" ").filter(String.isNonEmpty)
299
+ const segments = cron.trim().split(/\s+/).filter(String.isNonEmpty)
295
300
  if (segments.length !== 5 && segments.length !== 6) {
296
301
  return Either.left(
297
302
  new ParseError({
@@ -322,7 +327,18 @@ export const parse = (cron: string, tz?: DateTime.TimeZone | string): Either.Eit
322
327
  days: parseSegment(days, dayOptions),
323
328
  months: parseSegment(months, monthOptions),
324
329
  weekdays: parseSegment(weekdays, weekdayOptions)
325
- }).pipe(Either.map(make))
330
+ }).pipe(Either.map(({ days, hours, minutes, months, seconds, tz, weekdays }) =>
331
+ make({
332
+ tz,
333
+ seconds: seconds.values,
334
+ minutes: minutes.values,
335
+ hours: hours.values,
336
+ days: days.values,
337
+ months: months.values,
338
+ weekdays: weekdays.values,
339
+ and: (days.wildcard || weekdays.wildcard) && days.values.size !== 0 && weekdays.values.size !== 0
340
+ })
341
+ ))
326
342
  }
327
343
 
328
344
  /**
@@ -410,6 +426,11 @@ export const match = (cron: Cron, date: DateTime.DateTime.Input): boolean => {
410
426
  return true
411
427
  }
412
428
 
429
+ if (cron.and) {
430
+ return (cron.days.size === 0 || cron.days.has(parts.day)) &&
431
+ (cron.weekdays.size === 0 || cron.weekdays.has(parts.weekDay))
432
+ }
433
+
413
434
  if (cron.weekdays.size === 0) {
414
435
  return cron.days.has(parts.day)
415
436
  }
@@ -553,48 +574,60 @@ const stepCron = (cron: Cron, startFrom: DateTime.DateTime.Input | undefined, di
553
574
  }
554
575
 
555
576
  if (cron.weekdays.size !== 0 || cron.days.size !== 0) {
556
- let a: number = prev ? -Infinity : Infinity
557
- let b: number = prev ? -Infinity : Infinity
558
-
559
- if (cron.weekdays.size !== 0) {
560
- const currentWeekday = current.getUTCDay()
561
- const nextWeekday = table.weekday[currentWeekday]
562
- if (nextWeekday === undefined) {
563
- a = prev
564
- ? currentWeekday - 7 + boundary.weekday
565
- : 7 - currentWeekday + boundary.weekday
566
- } else {
567
- a = nextWeekday - currentWeekday
577
+ if (cron.and) {
578
+ const matchesDay = cron.days.size === 0 || cron.days.has(current.getUTCDate())
579
+ const matchesWeekday = cron.weekdays.size === 0 || cron.weekdays.has(current.getUTCDay())
580
+ if (!matchesDay || !matchesWeekday) {
581
+ current.setUTCDate(current.getUTCDate() + tick)
582
+ current.setUTCHours(boundary.hour, boundary.minute, boundary.second)
583
+ adjustDst(current)
584
+ continue
568
585
  }
569
- }
570
-
571
- // Only check day-of-month if weekday constraint not already satisfied (they're OR'd)
572
- if (cron.days.size !== 0 && a !== 0) {
573
- const currentDay = current.getUTCDate()
574
- const nextDay = table.day[currentDay]
575
- if (nextDay === undefined) {
576
- if (prev) {
577
- // When wrapping to previous month, calculate days back:
578
- // Current day offset + gap from end of prev month to target day
579
- // Example: June 3 May 20 with boundary.day=20: -(3 + (31 - 20)) = -14
580
- const prevMonthDays = daysInMonth(
581
- new Date(Date.UTC(current.getUTCFullYear(), current.getUTCMonth(), 0))
582
- )
583
- b = -(currentDay + (prevMonthDays - boundary.day))
586
+ } else {
587
+ let a: number = prev ? -Infinity : Infinity
588
+ let b: number = prev ? -Infinity : Infinity
589
+
590
+ if (cron.weekdays.size !== 0) {
591
+ const currentWeekday = current.getUTCDay()
592
+ const nextWeekday = table.weekday[currentWeekday]
593
+ if (nextWeekday === undefined) {
594
+ a = prev
595
+ ? currentWeekday - 7 + boundary.weekday
596
+ : 7 - currentWeekday + boundary.weekday
584
597
  } else {
598
+ a = nextWeekday - currentWeekday
599
+ }
600
+ }
601
+
602
+ // Only check day-of-month if weekday constraint not already satisfied (they're OR'd)
603
+ if (cron.days.size !== 0 && a !== 0) {
604
+ const currentDay = current.getUTCDate()
605
+ const nextDay = table.day[currentDay]
606
+ if (nextDay === undefined) {
607
+ if (prev) {
608
+ const prevMonthDays = daysInMonth(
609
+ new Date(Date.UTC(current.getUTCFullYear(), current.getUTCMonth(), 0))
610
+ )
611
+ b = -(currentDay + (prevMonthDays - boundary.day))
612
+ } else {
613
+ b = daysInMonth(current) - currentDay + boundary.day
614
+ }
615
+ } else if (!prev && nextDay > daysInMonth(current)) {
616
+ // The next matching day does not exist in the current month. Setting it
617
+ // directly would overflow and skip earlier matching days next month.
585
618
  b = daysInMonth(current) - currentDay + boundary.day
619
+ } else {
620
+ b = nextDay - currentDay
586
621
  }
587
- } else {
588
- b = nextDay - currentDay
589
622
  }
590
- }
591
623
 
592
- const addDays = prev ? Math.max(a, b) : Math.min(a, b)
593
- if (addDays !== 0) {
594
- current.setUTCDate(current.getUTCDate() + addDays)
595
- current.setUTCHours(boundary.hour, boundary.minute, boundary.second)
596
- adjustDst(current)
597
- continue
624
+ const addDays = prev ? Math.max(a, b) : Math.min(a, b)
625
+ if (addDays !== 0) {
626
+ current.setUTCDate(current.getUTCDate() + addDays)
627
+ current.setUTCHours(boundary.hour, boundary.minute, boundary.second)
628
+ adjustDst(current)
629
+ continue
630
+ }
598
631
  }
599
632
  }
600
633
 
@@ -602,11 +635,11 @@ const stepCron = (cron: Cron, startFrom: DateTime.DateTime.Input | undefined, di
602
635
  const currentMonth = current.getUTCMonth() + 1
603
636
  const nextMonth = table.month[currentMonth]
604
637
  const clampBoundaryDay = (targetMonthIndex: number): number => {
605
- if (cron.days.size !== 0) {
638
+ if (cron.days.size !== 0 && cron.weekdays.size === 0) {
606
639
  return boundary.day
607
640
  }
608
641
  const maxDayInMonth = daysInMonth(new Date(Date.UTC(current.getUTCFullYear(), targetMonthIndex, 1)))
609
- return Math.min(boundary.day, maxDayInMonth)
642
+ return prev ? maxDayInMonth : 1
610
643
  }
611
644
  if (nextMonth === undefined) {
612
645
  current.setUTCFullYear(current.getUTCFullYear() + tick)
@@ -661,6 +694,7 @@ export const sequenceReverse = function*(cron: Cron, startFrom?: DateTime.DateTi
661
694
  * @since 2.0.0
662
695
  */
663
696
  export const Equivalence: equivalence.Equivalence<Cron> = equivalence.make((self, that) =>
697
+ self.and === that.and &&
664
698
  restrictionsEquals(self.seconds, that.seconds) &&
665
699
  restrictionsEquals(self.minutes, that.minutes) &&
666
700
  restrictionsEquals(self.hours, that.hours) &&
@@ -700,6 +734,12 @@ interface SegmentOptions {
700
734
  min: number
701
735
  max: number
702
736
  aliases?: Record<string, number> | undefined
737
+ normalize?: ((value: number) => number) | undefined
738
+ }
739
+
740
+ interface ParsedSegment {
741
+ readonly values: Set<number>
742
+ readonly wildcard: boolean
703
743
  }
704
744
 
705
745
  const secondOptions: SegmentOptions = {
@@ -743,7 +783,8 @@ const monthOptions: SegmentOptions = {
743
783
 
744
784
  const weekdayOptions: SegmentOptions = {
745
785
  min: 0,
746
- max: 6,
786
+ max: 7,
787
+ normalize: (value) => value === 7 ? 0 : value,
747
788
  aliases: {
748
789
  sun: 0,
749
790
  mon: 1,
@@ -758,17 +799,21 @@ const weekdayOptions: SegmentOptions = {
758
799
  const parseSegment = (
759
800
  input: string,
760
801
  options: SegmentOptions
761
- ): Either.Either<ReadonlySet<number>, ParseError> => {
762
- const capacity = options.max - options.min + 1
802
+ ): Either.Either<ParsedSegment, ParseError> => {
763
803
  const values = new Set<number>()
764
804
  const fields = input.split(",")
765
-
766
- for (const field of fields) {
767
- const [raw, step] = splitStep(field)
768
- if (raw === "*" && step === undefined) {
769
- return Either.right(new Set())
805
+ const first = splitStep(fields[0]!)
806
+ const wildcard = first[0] === "*"
807
+ const normalize = options.normalize ?? ((value: number) => value)
808
+ const add = wildcard && (first[1] === undefined || first[1] === 1)
809
+ ? constVoid
810
+ : (value: number) => {
811
+ values.add(normalize(value))
770
812
  }
771
813
 
814
+ for (let index = 0; index < fields.length; index++) {
815
+ const field = fields[index]!
816
+ const [raw, step] = index === 0 ? first : splitStep(field)
772
817
  if (step !== undefined) {
773
818
  if (!Number.isInteger(step)) {
774
819
  return Either.left(new ParseError({ message: `Expected step value to be a positive integer`, input }))
@@ -782,8 +827,11 @@ const parseSegment = (
782
827
  }
783
828
 
784
829
  if (raw === "*") {
830
+ if (index === 0 && (step === undefined || step === 1)) {
831
+ continue
832
+ }
785
833
  for (let i = options.min; i <= options.max; i += step ?? 1) {
786
- values.add(i)
834
+ add(i)
787
835
  }
788
836
  } else {
789
837
  const [left, right] = splitRange(raw, options.aliases)
@@ -797,7 +845,9 @@ const parseSegment = (
797
845
  }
798
846
 
799
847
  if (right === undefined) {
800
- values.add(left)
848
+ for (let i = left; i <= (step === undefined ? left : options.max); i += step ?? 1) {
849
+ add(i)
850
+ }
801
851
  } else {
802
852
  if (!Number.isInteger(right)) {
803
853
  return Either.left(new ParseError({ message: `Expected a positive integer`, input }))
@@ -812,23 +862,20 @@ const parseSegment = (
812
862
  }
813
863
 
814
864
  for (let i = left; i <= right; i += step ?? 1) {
815
- values.add(i)
865
+ add(i)
816
866
  }
817
867
  }
818
868
  }
819
-
820
- if (values.size >= capacity) {
821
- return Either.right(new Set())
822
- }
823
869
  }
824
870
 
825
- return Either.right(values)
871
+ return Either.right({ values, wildcard })
826
872
  }
827
873
 
828
874
  const splitStep = (input: string): [string, number | undefined] => {
829
875
  const seperator = input.indexOf("/")
830
876
  if (seperator !== -1) {
831
- return [input.slice(0, seperator), Number(input.slice(seperator + 1))]
877
+ const step = input.slice(seperator + 1)
878
+ return [input.slice(0, seperator), decimalRegex.test(step) ? Number(step) : NaN]
832
879
  }
833
880
 
834
881
  return [input, undefined]
@@ -844,5 +891,7 @@ const splitRange = (input: string, aliases?: Record<string, number>): [number, n
844
891
  }
845
892
 
846
893
  function aliasOrValue(field: string, aliases?: Record<string, number>): number {
847
- return aliases?.[field.toLocaleLowerCase()] ?? Number(field)
894
+ return aliases?.[field.toLocaleLowerCase()] ?? (decimalRegex.test(field) ? Number(field) : NaN)
848
895
  }
896
+
897
+ const decimalRegex = /^\d+$/
@@ -482,6 +482,14 @@ export const cron: {
482
482
  return makeWithState<[boolean, [number, number, number]], unknown, [number, number]>(
483
483
  [true, [Number.MIN_SAFE_INTEGER, 0, 0]],
484
484
  (now, _, [initial, previous]) => {
485
+ if (now === Number.POSITIVE_INFINITY) {
486
+ return core.succeed([
487
+ [false, previous],
488
+ [previous[1], previous[2]],
489
+ ScheduleDecision.done
490
+ ])
491
+ }
492
+
485
493
  if (now < previous[0]) {
486
494
  return core.succeed([
487
495
  [false, previous],
@@ -1,4 +1,4 @@
1
- let moduleVersion = "3.21.4"
1
+ let moduleVersion = "3.21.5"
2
2
 
3
3
  export const getCurrentVersion = () => moduleVersion
4
4