Skip to content

Commit

Permalink
add ISO8601 duration formatting
Browse files Browse the repository at this point in the history
  • Loading branch information
fubhy committed Jan 26, 2025
1 parent 502223a commit 88a9ef1
Show file tree
Hide file tree
Showing 2 changed files with 108 additions and 0 deletions.
70 changes: 70 additions & 0 deletions packages/effect/src/Duration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -845,3 +845,73 @@ export const format = (self: DurationInput): string => {

return pieces.join(" ")
}

/**
* Formats a Duration into an ISO8601 duration string.
*
* The ISO8601 duration format is generally specified as P[n]Y[n]M[n]DT[n]H[n]M[n]S. However, since
* the `Duration` type does not support years or months, this function will only output the days, hours,
* minutes and seconds. Thus, the effective format is P[n]DT[n]H[n]M[n]S.
*
* Milliseconds and nanoseconds are expressed as fractional seconds.
*
* @throws `RangeError` If the duration is not finite.
*
* @example
* ```typescript
* formatIso(Duration.days(1)) // => "P1D"
* formatIso(Duration.minutes(90)) // => "PT1H30M"
* formatIso(Duration.millis(1500)) // => "PT1.5S"
* ```
*
* @since 3.13.0
* @category conversions
*/
export const unsafeFormatIso = (self: DurationInput): string => {
const duration = decode(self)
if (!isFinite(duration)) {
throw new RangeError("Cannot format infinite duration")
}

const fragments = []
const {
days,
hours,
millis,
minutes,
nanos,
seconds
} = parts(duration)

if (days >= 7) {
const rest = days % 7
const weeks = (days - rest) / 7
fragments.push(`${weeks}W`)
if (rest !== 0) {
fragments.push(`${rest}D`)
}
} else if (days !== 0) {
fragments.push(`${days}D`)
}

if (hours !== 0 || minutes !== 0 || seconds !== 0 || millis !== 0 || nanos !== 0) {
fragments.push("T")

if (hours !== 0) {
fragments.push(`${hours}H`)
}

if (minutes !== 0) {
fragments.push(`${minutes}M`)
}

if (seconds !== 0 || millis !== 0 || nanos !== 0) {
const total = BigInt(seconds) * bigint1e9 + BigInt(millis) * bigint1e6 + BigInt(nanos)
const str = (Number(total) / 1e9).toFixed(9).replace(/\.?0+$/, "")
fragments.push(`${str}S`)
}
}

return `P${fragments.join("") || "T0S"}`
}

Check failure on line 917 in packages/effect/src/Duration.ts

View workflow job for this annotation

GitHub Actions / Lint

Extra line break(s)
38 changes: 38 additions & 0 deletions packages/effect/test/Duration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -520,4 +520,42 @@ describe("Duration", () => {
expect(Duration.toWeeks("2 weeks")).toBe(2)
expect(Duration.toWeeks("14 days")).toBe(2)
})

it("unsafeFormatIso", () => {
expect(Duration.unsafeFormatIso(Duration.zero)).toBe("PT0S")
expect(Duration.unsafeFormatIso(Duration.seconds(2))).toBe("PT2S")
expect(Duration.unsafeFormatIso(Duration.minutes(5))).toBe("PT5M")
expect(Duration.unsafeFormatIso(Duration.hours(3))).toBe("PT3H")
expect(Duration.unsafeFormatIso(Duration.days(1))).toBe("P1D")

expect(Duration.unsafeFormatIso(Duration.minutes(90))).toBe("PT1H30M")
expect(Duration.unsafeFormatIso(Duration.hours(25))).toBe("P1DT1H")
expect(Duration.unsafeFormatIso(Duration.days(7))).toBe("P1W")
expect(Duration.unsafeFormatIso(Duration.days(10))).toBe("P1W3D")

expect(Duration.unsafeFormatIso(Duration.millis(1500))).toBe("PT1.5S")
expect(Duration.unsafeFormatIso(Duration.micros(1500n))).toBe("PT0.0015S")
expect(Duration.unsafeFormatIso(Duration.nanos(1500n))).toBe("PT0.0000015S")

expect(Duration.unsafeFormatIso(
Duration.days(1).pipe(
Duration.sum(Duration.hours(2)),
Duration.sum(Duration.minutes(30))
)
)).toBe("P1DT2H30M")

expect(Duration.unsafeFormatIso(
Duration.hours(2).pipe(
Duration.sum(Duration.minutes(30)),
Duration.sum(Duration.millis(1500))
)
)).toBe("PT2H30M1.5S")

expect(Duration.unsafeFormatIso("1 day")).toBe("P1D")
expect(Duration.unsafeFormatIso("90 minutes")).toBe("PT1H30M")
expect(Duration.unsafeFormatIso("1.5 seconds")).toBe("PT1.5S")

expect(() => Duration.unsafeFormatIso(Duration.infinity))
.toThrow(new RangeError("Cannot format infinite duration"))
})
})

0 comments on commit 88a9ef1

Please sign in to comment.