From f2fd0b9923c38842490f47f0d699599ec41d380a Mon Sep 17 00:00:00 2001 From: Alexander Kireev Date: Thu, 11 Jun 2026 04:44:43 +0700 Subject: [PATCH] fix: Duration.decode and Duration.times throwing on fractional values --- .changeset/duration-fractional-nanos-micros.md | 9 +++++++++ packages/effect/src/Duration.ts | 10 +++++++--- packages/effect/test/Duration.test.ts | 7 +++++++ 3 files changed, 23 insertions(+), 3 deletions(-) create mode 100644 .changeset/duration-fractional-nanos-micros.md diff --git a/.changeset/duration-fractional-nanos-micros.md b/.changeset/duration-fractional-nanos-micros.md new file mode 100644 index 00000000000..15be7a7fb2f --- /dev/null +++ b/.changeset/duration-fractional-nanos-micros.md @@ -0,0 +1,9 @@ +--- +"effect": patch +--- + +Fix `Duration.decode` and `Duration.times` throwing on fractional values. + +`Duration.decode` accepts a decimal mantissa (e.g. `"1.5 micros"`), but the `nanos`/`micros` units passed the raw string to `BigInt`, which throws on non-integers. Fractional `nanos`/`micros` are now scaled to whole nanoseconds (rounded), e.g. `Duration.decode("1.5 micros")` is `1500` nanos and `Duration.decode("1.5 nanos")` is `2` nanos. + +`Duration.times` accepts any `number` multiplier, but multiplying a nanosecond-backed `Duration` by a non-integer threw because `BigInt` cannot convert a float. Non-integer multipliers are now supported, e.g. `Duration.times(Duration.nanos(2n), 2.5)` is `5` nanos. diff --git a/packages/effect/src/Duration.ts b/packages/effect/src/Duration.ts index 677138be127..9554b409f5a 100644 --- a/packages/effect/src/Duration.ts +++ b/packages/effect/src/Duration.ts @@ -113,13 +113,14 @@ export const decode = (input: DurationInput): Duration => { if (match) { const [_, valueStr, unit] = match const value = Number(valueStr) + const isFractional = valueStr.includes(".") switch (unit) { case "nano": case "nanos": - return nanos(BigInt(valueStr)) + return isFractional ? make(BigInt(Math.round(value))) : nanos(BigInt(valueStr)) case "micro": case "micros": - return micros(BigInt(valueStr)) + return isFractional ? make(BigInt(Math.round(value * 1_000))) : micros(BigInt(valueStr)) case "milli": case "millis": return millis(value) @@ -651,7 +652,10 @@ export const times: { (self: DurationInput, times: number): Duration => match(self, { onMillis: (millis) => make(millis * times), - onNanos: (nanos) => make(nanos * BigInt(times)) + onNanos: (nanos) => + Number.isInteger(times) + ? make(nanos * BigInt(times)) + : make(BigInt(Math.round(Number(nanos) * times))) }) ) diff --git a/packages/effect/test/Duration.test.ts b/packages/effect/test/Duration.test.ts index 5250e6bd0e7..455dad9a926 100644 --- a/packages/effect/test/Duration.test.ts +++ b/packages/effect/test/Duration.test.ts @@ -39,6 +39,10 @@ describe("Duration", () => { deepStrictEqual(Duration.decode("1.5 seconds"), Duration.seconds(1.5)) deepStrictEqual(Duration.decode("-1.5 seconds"), Duration.zero) + deepStrictEqual(Duration.decode("1.5 micros"), Duration.nanos(1500n)) + deepStrictEqual(Duration.decode("1.5 nanos"), Duration.nanos(2n)) + deepStrictEqual(Duration.decode("-1.5 nanos"), Duration.zero) + deepStrictEqual(Duration.decode([500, 123456789]), Duration.nanos(500123456789n)) deepStrictEqual(Duration.decode([-500, 123456789]), Duration.zero) deepStrictEqual(Duration.decode([Infinity, 0]), Duration.infinity) @@ -224,6 +228,9 @@ describe("Duration", () => { deepStrictEqual(Duration.times(Duration.seconds(Infinity), 60), Duration.seconds(Infinity)) deepStrictEqual(Duration.times("1 seconds", 60), Duration.minutes(1)) + + deepStrictEqual(Duration.times(Duration.nanos(2n), 2.5), Duration.nanos(5n)) + deepStrictEqual(Duration.times(Duration.nanos(3n), 1.5), Duration.nanos(5n)) }) it("sum", () => {