Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ensure Redactable works with Console.log #4153

Draft
wants to merge 15 commits into
base: next-minor
Choose a base branch
from
Draft
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
added Cron.unsafeParse and allow passing the tz parameter as `str…
…ing` (#4106)
fubhy authored and effect-bot committed Dec 19, 2024
commit 4cb4c925fe653e420d1280e99239246559bf515e
5 changes: 5 additions & 0 deletions .changeset/polite-trainers-give.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"effect": minor
---

Added `Cron.unsafeParse` and allow passing the `Cron.parse` time zone parameter as `string`.
35 changes: 33 additions & 2 deletions packages/effect/src/Cron.ts
Original file line number Diff line number Diff line change
@@ -256,7 +256,7 @@ export const isParseError = (u: unknown): u is ParseError => hasProperty(u, Pars
* @since 2.0.0
* @category constructors
*/
export const parse = (cron: string, tz?: DateTime.TimeZone): Either.Either<Cron, ParseError> => {
export const parse = (cron: string, tz?: DateTime.TimeZone | string): Either.Either<Cron, ParseError> => {
const segments = cron.split(" ").filter(String.isNonEmpty)
if (segments.length !== 5 && segments.length !== 6) {
return Either.left(ParseError(`Invalid number of segments in cron expression`, cron))
@@ -267,16 +267,47 @@ export const parse = (cron: string, tz?: DateTime.TimeZone): Either.Either<Cron,
}

const [seconds, minutes, hours, days, months, weekdays] = segments
const zone = tz === undefined || dateTime.isTimeZone(tz) ?
Either.right(tz) :
Either.fromOption(dateTime.zoneFromString(tz), () => ParseError(`Invalid time zone in cron expression`, tz))

return Either.all({
tz: zone,
seconds: parseSegment(seconds, secondOptions),
minutes: parseSegment(minutes, minuteOptions),
hours: parseSegment(hours, hourOptions),
days: parseSegment(days, dayOptions),
months: parseSegment(months, monthOptions),
weekdays: parseSegment(weekdays, weekdayOptions)
}).pipe(Either.map((segments) => make({ ...segments, tz })))
}).pipe(Either.map(make))
}

/**
* Parses a cron expression into a `Cron` instance.
*
* Throws on failure.
*
* @param cron - The cron expression to parse.
*
* @example
* ```ts
* import { Cron } from "effect"
*
* // At 04:00 on every day-of-month from 8 through 14.
* assert.deepStrictEqual(Cron.unsafeParse("0 4 8-14 * *"), Cron.make({
* minutes: [0],
* hours: [4],
* days: [8, 9, 10, 11, 12, 13, 14],
* months: [],
* weekdays: []
* }))
* ```
*
* @since 2.0.0
* @category constructors
*/
export const unsafeParse = (cron: string, tz?: DateTime.TimeZone | string): Cron => Either.getOrThrow(parse(cron, tz))

/**
* Checks if a given `Date` falls within an active `Cron` time window.
*
6 changes: 5 additions & 1 deletion packages/effect/src/Schedule.ts
Original file line number Diff line number Diff line change
@@ -5,6 +5,7 @@ import type * as Cause from "./Cause.js"
import type * as Chunk from "./Chunk.js"
import type * as Context from "./Context.js"
import type * as Cron from "./Cron.js"
import type * as DateTime from "./DateTime.js"
import type * as Duration from "./Duration.js"
import type * as Effect from "./Effect.js"
import type * as Either from "./Either.js"
@@ -403,7 +404,10 @@ export const count: Schedule<number> = internal.count
* @since 2.0.0
* @category constructors
*/
export const cron: (expression: string | Cron.Cron) => Schedule<[number, number]> = internal.cron
export const cron: {
(cron: Cron.Cron): Schedule<[number, number]>
(expression: string, tz?: DateTime.TimeZone | string): Schedule<[number, number]>
} = internal.cron

/**
* Cron-like schedule that recurs every specified `day` of month. Won't recur
8 changes: 6 additions & 2 deletions packages/effect/src/internal/schedule.ts
Original file line number Diff line number Diff line change
@@ -3,6 +3,7 @@ import * as Chunk from "../Chunk.js"
import * as Clock from "../Clock.js"
import * as Context from "../Context.js"
import * as Cron from "../Cron.js"
import type * as DateTime from "../DateTime.js"
import * as Duration from "../Duration.js"
import type * as Effect from "../Effect.js"
import * as Either from "../Either.js"
@@ -413,8 +414,11 @@ export const mapInputEffect = dual<
)))

/** @internal */
export const cron = (expression: string | Cron.Cron): Schedule.Schedule<[number, number]> => {
const parsed = Cron.isCron(expression) ? Either.right(expression) : Cron.parse(expression)
export const cron: {
(expression: Cron.Cron): Schedule.Schedule<[number, number]>
(expression: string, tz?: DateTime.TimeZone | string): Schedule.Schedule<[number, number]>
} = (expression: string | Cron.Cron, tz?: DateTime.TimeZone | string): Schedule.Schedule<[number, number]> => {
const parsed = Cron.isCron(expression) ? Either.right(expression) : Cron.parse(expression, tz)
return makeWithState<[boolean, [number, number, number]], unknown, [number, number]>(
[true, [Number.MIN_SAFE_INTEGER, 0, 0]],
(now, _, [initial, previous]) => {
18 changes: 11 additions & 7 deletions packages/effect/test/Cron.test.ts
Original file line number Diff line number Diff line change
@@ -7,7 +7,7 @@ import * as Option from "effect/Option"
import { assertFalse, assertTrue, deepStrictEqual } from "effect/test/util"
import { describe, it } from "vitest"

const parse = (input: string, tz?: DateTime.TimeZone) => Either.getOrThrowWith(Cron.parse(input, tz), identity)
const parse = (input: string, tz?: DateTime.TimeZone | string) => Either.getOrThrowWith(Cron.parse(input, tz), identity)
const match = (input: Cron.Cron | string, date: DateTime.DateTime.Input) =>
Cron.match(Cron.isCron(input) ? input : parse(input), date)
const next = (input: Cron.Cron | string, after?: DateTime.DateTime.Input) =>
@@ -151,10 +151,12 @@ describe("Cron", () => {
})

it("handles transition into daylight savings time", () => {
const berlin = DateTime.zoneUnsafeMakeNamed("Europe/Berlin")
const make = (date: string) => DateTime.makeZonedFromString(date).pipe(Option.getOrThrow)
const sequence = Cron.sequence(parse("30 * * * *", berlin), make("2024-03-31T00:00:00.000+01:00[Europe/Berlin]"))
const next = (): DateTime.Zoned => DateTime.unsafeMakeZoned(sequence.next().value, { timeZone: berlin })
const sequence = Cron.sequence(
parse("30 * * * *", "Europe/Berlin"),
make("2024-03-31T00:00:00.000+01:00[Europe/Berlin]")
)
const next = (): DateTime.Zoned => DateTime.unsafeMakeZoned(sequence.next().value, { timeZone: "Europe/Berlin" })

const a = make("2024-03-31T00:30:00.000+01:00[Europe/Berlin]")
const b = make("2024-03-31T01:30:00.000+01:00[Europe/Berlin]")
@@ -168,10 +170,12 @@ describe("Cron", () => {
})

it("handles transition out of daylight savings time", () => {
const berlin = DateTime.zoneUnsafeMakeNamed("Europe/Berlin")
const make = (date: string) => DateTime.makeZonedFromString(date).pipe(Option.getOrThrow)
const sequence = Cron.sequence(parse("30 * * * *", berlin), make("2024-10-27T00:00:00.000+02:00[Europe/Berlin]"))
const next = (): DateTime.Zoned => DateTime.unsafeMakeZoned(sequence.next().value, { timeZone: berlin })
const sequence = Cron.sequence(
parse("30 * * * *", "Europe/Berlin"),
make("2024-10-27T00:00:00.000+02:00[Europe/Berlin]")
)
const next = (): DateTime.Zoned => DateTime.unsafeMakeZoned(sequence.next().value, { timeZone: "Europe/Berlin" })

const a = make("2024-10-27T00:30:00.000+02:00[Europe/Berlin]")
// const x = make("2024-10-27T01:30:00.000+02:00[Europe/Berlin]") // TODO: Our implementation skips this.