Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
5 changes: 5 additions & 0 deletions .changeset/fix-duration-decimal-precision.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"effect": patch
---

Preserve integral precision when parsing decimal nano and micro duration inputs
18 changes: 16 additions & 2 deletions packages/effect/src/Duration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ const TypeId = "~effect/time/Duration"

const bigint0 = BigInt(0)
const bigint1 = BigInt(1)
const bigint2 = BigInt(2)
const bigint10 = BigInt(10)
const bigint24 = BigInt(24)
const bigint60 = BigInt(60)
const bigint1e3 = BigInt(1_000)
Expand All @@ -38,8 +40,20 @@ const roundTiesAwayFromZero = (input: number): bigint =>

const roundMillisToNanos = (millis: number): bigint => roundTiesAwayFromZero(millis * 1_000_000)

const parseNanos = (input: string, scale: bigint): bigint =>
input.includes(".") ? roundTiesAwayFromZero(Number(input) * Number(scale)) : BigInt(input) * scale
const parseNanos = (input: string, scale: bigint): bigint => {
const decimalIndex = input.indexOf(".")
if (decimalIndex === -1) return BigInt(input) * scale

const isNegative = input[0] === "-"
const fractional = input.slice(decimalIndex + 1)
const fractionalScale = bigint10 ** BigInt(fractional.length)
const scaled = (
BigInt(input.slice(isNegative ? 1 : 0, decimalIndex)) * fractionalScale + BigInt(fractional)
) * scale
const rounded = scaled / fractionalScale +
(scaled % fractionalScale * bigint2 >= fractionalScale ? bigint1 : bigint0)
return isNegative ? -rounded : rounded
}

const nanosToHrTime = (nanos: bigint): [seconds: number, nanos: number] => {
const sign = nanos < bigint0 ? -bigint1 : bigint1
Expand Down
10 changes: 10 additions & 0 deletions packages/effect/test/Duration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,20 @@ describe("Duration", () => {
deepStrictEqual(Duration.fromInputUnsafe("10 nanos"), Duration.nanos(10n))
deepStrictEqual(Duration.fromInputUnsafe("1.5 nanos"), Duration.nanos(2n))
deepStrictEqual(Duration.fromInputUnsafe("-1.5 nanos"), Duration.nanos(-2n))
deepStrictEqual(Duration.fromInputUnsafe("9007199254740993.1 nanos"), Duration.nanos(9_007_199_254_740_993n))
deepStrictEqual(Duration.fromInputUnsafe("-9007199254740993.5 nanos"), Duration.nanos(-9_007_199_254_740_994n))
deepStrictEqual(Duration.fromInputUnsafe("1 micro"), Duration.micros(1n))
deepStrictEqual(Duration.fromInputUnsafe("10 micros"), Duration.micros(10n))
deepStrictEqual(Duration.fromInputUnsafe("1.5 micros"), Duration.nanos(1500n))
deepStrictEqual(Duration.fromInputUnsafe("-1.5 micros"), Duration.nanos(-1500n))
deepStrictEqual(
Duration.fromInputUnsafe("9007199254740993.1 micros"),
Duration.nanos(9_007_199_254_740_993_100n)
)
deepStrictEqual(
Duration.fromInputUnsafe("-9007199254740993.0005 micros"),
Duration.nanos(-9_007_199_254_740_993_001n)
)
deepStrictEqual(Duration.fromInputUnsafe("1 milli"), Duration.millis(1))
deepStrictEqual(Duration.fromInputUnsafe("10 millis"), Duration.millis(10))
deepStrictEqual(Duration.fromInputUnsafe("1 second"), Duration.seconds(1))
Expand Down
Loading