effect 3.21.3 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "effect",
3
- "version": "3.21.3",
3
+ "version": "3.21.5",
4
4
  "description": "The missing standard library for TypeScript, for writing production-grade software.",
5
5
  "license": "MIT",
6
6
  "repository": {
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+$/
package/src/Graph.ts CHANGED
@@ -1923,6 +1923,37 @@ export const isAcyclic = <N, E, T extends Kind = "directed">(
1923
1923
  return graph.isAcyclic.value
1924
1924
  }
1925
1925
 
1926
+ if (graph.type === "undirected") {
1927
+ const visited = new Set<NodeIndex>()
1928
+
1929
+ for (const startNode of graph.nodes.keys()) {
1930
+ if (visited.has(startNode)) {
1931
+ continue
1932
+ }
1933
+
1934
+ visited.add(startNode)
1935
+ const stack: Array<{ node: NodeIndex; parent: NodeIndex | null }> = [{ node: startNode, parent: null }]
1936
+
1937
+ while (stack.length > 0) {
1938
+ const { node, parent } = stack.pop()!
1939
+ const nodeNeighbors = getUndirectedNeighbors(graph as any, node)
1940
+
1941
+ for (const neighbor of nodeNeighbors) {
1942
+ if (!visited.has(neighbor)) {
1943
+ visited.add(neighbor)
1944
+ stack.push({ node: neighbor, parent: node })
1945
+ } else if (neighbor !== parent) {
1946
+ graph.isAcyclic = Option.some(false)
1947
+ return false
1948
+ }
1949
+ }
1950
+ }
1951
+ }
1952
+
1953
+ graph.isAcyclic = Option.some(true)
1954
+ return true
1955
+ }
1956
+
1926
1957
  // Stack-safe DFS cycle detection using iterative approach
1927
1958
  const visited = new Set<NodeIndex>()
1928
1959
  const recursionStack = new Set<NodeIndex>()
@@ -2103,6 +2134,21 @@ const getUndirectedNeighbors = <N, E>(
2103
2134
  return Array.from(neighbors)
2104
2135
  }
2105
2136
 
2137
+ const getTraversalNeighbors = <N, E, T extends Kind>(
2138
+ graph: Graph<N, E, T> | MutableGraph<N, E, T>,
2139
+ nodeIndex: NodeIndex,
2140
+ direction: Direction
2141
+ ): Array<NodeIndex> =>
2142
+ graph.type === "undirected"
2143
+ ? getUndirectedNeighbors(graph as any, nodeIndex)
2144
+ : neighborsDirected(graph, nodeIndex, direction)
2145
+
2146
+ const getTraversableNeighbor = <N, E, T extends Kind>(
2147
+ graph: Graph<N, E, T> | MutableGraph<N, E, T>,
2148
+ current: NodeIndex,
2149
+ edge: Edge<E>
2150
+ ): NodeIndex => graph.type === "undirected" && edge.target === current ? edge.source : edge.target
2151
+
2106
2152
  /**
2107
2153
  * Find connected components in an undirected graph.
2108
2154
  * Each component is represented as an array of node indices.
@@ -2432,7 +2478,7 @@ export const dijkstra = <N, E, T extends Kind = "directed">(
2432
2478
  for (const edgeIndex of adjacencyList) {
2433
2479
  const edge = graph.edges.get(edgeIndex)
2434
2480
  if (edge !== undefined) {
2435
- const neighbor = edge.target
2481
+ const neighbor = getTraversableNeighbor(graph, currentNode, edge)
2436
2482
  const weight = cost(edge.data)
2437
2483
 
2438
2484
  // Validate non-negative weights
@@ -2564,6 +2610,15 @@ export const floydWarshall = <N, E, T extends Kind = "directed">(
2564
2610
  next.get(i)!.set(j, j)
2565
2611
  edgeMatrix.get(i)!.set(j, edgeData.data)
2566
2612
  }
2613
+
2614
+ if (graph.type === "undirected") {
2615
+ const reverseWeight = dist.get(j)!.get(i)!
2616
+ if (weight < reverseWeight) {
2617
+ dist.get(j)!.set(i, weight)
2618
+ next.get(j)!.set(i, i)
2619
+ edgeMatrix.get(j)!.set(i, edgeData.data)
2620
+ }
2621
+ }
2567
2622
  }
2568
2623
 
2569
2624
  // Floyd-Warshall main loop
@@ -2757,7 +2812,7 @@ export const astar = <N, E, T extends Kind = "directed">(
2757
2812
  for (const edgeIndex of adjacencyList) {
2758
2813
  const edge = graph.edges.get(edgeIndex)
2759
2814
  if (edge !== undefined) {
2760
- const neighbor = edge.target
2815
+ const neighbor = getTraversableNeighbor(graph, currentNode, edge)
2761
2816
  const weight = cost(edge.data)
2762
2817
 
2763
2818
  // Validate non-negative weights
@@ -2893,6 +2948,14 @@ export const bellmanFord = <N, E, T extends Kind = "directed">(
2893
2948
  weight,
2894
2949
  edgeData: edgeData.data
2895
2950
  })
2951
+ if (graph.type === "undirected" && edgeData.source !== edgeData.target) {
2952
+ edges.push({
2953
+ source: edgeData.target,
2954
+ target: edgeData.source,
2955
+ weight,
2956
+ edgeData: edgeData.data
2957
+ })
2958
+ }
2896
2959
  }
2897
2960
 
2898
2961
  // Relax edges up to V-1 times
@@ -2934,14 +2997,8 @@ export const bellmanFord = <N, E, T extends Kind = "directed">(
2934
2997
  affectedNodes.add(node)
2935
2998
 
2936
2999
  // Add all nodes reachable from this node
2937
- const adjacencyList = graph.adjacency.get(node)
2938
- if (adjacencyList !== undefined) {
2939
- for (const edgeIndex of adjacencyList) {
2940
- const edge = graph.edges.get(edgeIndex)
2941
- if (edge !== undefined) {
2942
- queue.push(edge.target)
2943
- }
2944
- }
3000
+ for (const neighbor of getTraversalNeighbors(graph, node, "outgoing")) {
3001
+ queue.push(neighbor)
2945
3002
  }
2946
3003
  }
2947
3004
 
@@ -3259,7 +3316,7 @@ export const dfs = <N, E, T extends Kind = "directed">(
3259
3316
  continue
3260
3317
  }
3261
3318
 
3262
- const neighbors = neighborsDirected(graph, current, direction)
3319
+ const neighbors = getTraversalNeighbors(graph, current, direction)
3263
3320
  for (let i = neighbors.length - 1; i >= 0; i--) {
3264
3321
  const neighbor = neighbors[i]
3265
3322
  if (!discovered.has(neighbor)) {
@@ -3336,7 +3393,7 @@ export const bfs = <N, E, T extends Kind = "directed">(
3336
3393
  if (!discovered.has(current)) {
3337
3394
  discovered.add(current)
3338
3395
 
3339
- const neighbors = neighborsDirected(graph, current, direction)
3396
+ const neighbors = getTraversalNeighbors(graph, current, direction)
3340
3397
  for (const neighbor of neighbors) {
3341
3398
  if (!discovered.has(neighbor)) {
3342
3399
  queue.push(neighbor)
@@ -3418,6 +3475,10 @@ export const topo = <N, E, T extends Kind = "directed">(
3418
3475
  graph: Graph<N, E, T> | MutableGraph<N, E, T>,
3419
3476
  config: TopoConfig = {}
3420
3477
  ): NodeWalker<N> => {
3478
+ if (graph.type === "undirected") {
3479
+ throw new Error("Cannot perform topological sort on undirected graph")
3480
+ }
3481
+
3421
3482
  // Check if graph is acyclic first
3422
3483
  if (!isAcyclic(graph)) {
3423
3484
  throw new Error("Cannot perform topological sort on cyclic graph")
@@ -3562,7 +3623,7 @@ export const dfsPostOrder = <N, E, T extends Kind = "directed">(
3562
3623
 
3563
3624
  if (!current.visitedChildren) {
3564
3625
  current.visitedChildren = true
3565
- const neighbors = neighborsDirected(graph, current.node, direction)
3626
+ const neighbors = getTraversalNeighbors(graph, current.node, direction)
3566
3627
 
3567
3628
  for (let i = neighbors.length - 1; i >= 0; i--) {
3568
3629
  const neighbor = neighbors[i]
@@ -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.3"
1
+ let moduleVersion = "3.21.5"
2
2
 
3
3
  export const getCurrentVersion = () => moduleVersion
4
4