diff --git a/.changeset/fix-array-non-finite-indexes.md b/.changeset/fix-array-non-finite-indexes.md
new file mode 100644
index 00000000000..dcfd9db0fd6
--- /dev/null
+++ b/.changeset/fix-array-non-finite-indexes.md
@@ -0,0 +1,5 @@
+---
+"effect": patch
+---
+
+Fix Array index operations handling `NaN` and fractional indexes.
diff --git a/packages/effect/src/Array.ts b/packages/effect/src/Array.ts
index 35eb7002231..c35cf612a90 100644
--- a/packages/effect/src/Array.ts
+++ b/packages/effect/src/Array.ts
@@ -909,7 +909,7 @@ export const length = (self: ReadonlyArray): number => self.length
/** @internal */
export function isOutOfBounds(i: number, as: ReadonlyArray): boolean {
- return i < 0 || i >= as.length
+ return !Number.isFinite(i) || i < 0 || i >= as.length
}
const clamp = (i: number, as: ReadonlyArray): number => Math.floor(Math.min(Math.max(0, i), as.length))
@@ -1875,10 +1875,11 @@ export const insertAt: {
(self: Iterable, i: number, b: B): Option.Option>
} = dual(3, (self: Iterable, i: number, b: B): Option.Option> => {
const out: Array = Array.from(self) // copy because `splice` mutates the array
- if (i < 0 || i > out.length) {
+ const index = Math.floor(i)
+ if (index !== out.length && isOutOfBounds(index, out)) {
return Option.none()
}
- out.splice(i, 0, b)
+ out.splice(index, 0, b)
return Option.some(out as any)
})
@@ -1966,12 +1967,13 @@ export const modify: {
): Option.Option | B>>
} = dual(3, (self: Iterable, i: number, f: (a: A) => B): Option.Option> => {
const arr = Array.from(self)
- if (isOutOfBounds(i, arr)) {
+ const index = Math.floor(i)
+ if (isOutOfBounds(index, arr)) {
return Option.none()
}
const out: Array = arr
- const b = f(arr[i])
- out[i] = b
+ const b = f(arr[index])
+ out[index] = b
return Option.some(out)
})
@@ -2004,10 +2006,11 @@ export const remove: {
(self: Iterable, i: number): Array
} = dual(2, (self: Iterable, i: number): Array => {
const out = Array.from(self)
- if (isOutOfBounds(i, out)) {
+ const index = Math.floor(i)
+ if (isOutOfBounds(index, out)) {
return out
}
- out.splice(i, 1)
+ out.splice(index, 1)
return out
})
diff --git a/packages/effect/test/ArrayNaNIndex.test.ts b/packages/effect/test/ArrayNaNIndex.test.ts
new file mode 100644
index 00000000000..9c5f258c338
--- /dev/null
+++ b/packages/effect/test/ArrayNaNIndex.test.ts
@@ -0,0 +1,40 @@
+import { assert, describe, it } from "@effect/vitest"
+import { Array as Arr, Option } from "effect"
+
+const input = [1, 2, 3]
+
+describe("Array NaN indexes", () => {
+ it("rejects NaN in get", () => {
+ assert.deepStrictEqual(Arr.get(input, Number.NaN), Option.none())
+ })
+
+ it("rejects NaN in getUnsafe", () => {
+ assert.throws(() => Arr.getUnsafe(input, Number.NaN), /Index out of bounds/)
+ })
+
+ it("rejects NaN in insertAt", () => {
+ assert.deepStrictEqual(Arr.insertAt(input, Number.NaN, 4), Option.none())
+ })
+
+ it("rejects NaN in replace", () => {
+ assert.deepStrictEqual(Arr.replace(input, Number.NaN, 4), Option.none())
+ })
+
+ it("rejects NaN in modify", () => {
+ assert.deepStrictEqual(Arr.modify(input, Number.NaN, (n) => n * 2), Option.none())
+ })
+
+ it("treats removing NaN as an out-of-bounds no-op", () => {
+ assert.deepStrictEqual(Arr.remove(input, Number.NaN), input)
+ })
+})
+
+describe("Array fractional indexes", () => {
+ it("floors the index in replace", () => {
+ assert.deepStrictEqual(Arr.replace(input, 1.5, 4), Option.some([1, 4, 3]))
+ })
+
+ it("floors the index in modify", () => {
+ assert.deepStrictEqual(Arr.modify(input, 1.5, (n) => n * 2), Option.some([1, 4, 3]))
+ })
+})