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-array-non-finite-indexes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"effect": patch
---

Fix Array index operations handling `NaN` and fractional indexes.
19 changes: 11 additions & 8 deletions packages/effect/src/Array.ts
Original file line number Diff line number Diff line change
Expand Up @@ -909,7 +909,7 @@ export const length = <A>(self: ReadonlyArray<A>): number => self.length

/** @internal */
export function isOutOfBounds<A>(i: number, as: ReadonlyArray<A>): boolean {
return i < 0 || i >= as.length
return !Number.isFinite(i) || i < 0 || i >= as.length
}

const clamp = <A>(i: number, as: ReadonlyArray<A>): number => Math.floor(Math.min(Math.max(0, i), as.length))
Expand Down Expand Up @@ -1875,10 +1875,11 @@ export const insertAt: {
<A, B>(self: Iterable<A>, i: number, b: B): Option.Option<NonEmptyArray<A | B>>
} = dual(3, <A, B>(self: Iterable<A>, i: number, b: B): Option.Option<NonEmptyArray<A | B>> => {
const out: Array<A | B> = 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)
})

Expand Down Expand Up @@ -1966,12 +1967,13 @@ export const modify: {
): Option.Option<ReadonlyArray.With<S, ReadonlyArray.Infer<S> | B>>
} = dual(3, <A, B>(self: Iterable<A>, i: number, f: (a: A) => B): Option.Option<Array<A | B>> => {
const arr = Array.from(self)
if (isOutOfBounds(i, arr)) {
const index = Math.floor(i)
if (isOutOfBounds(index, arr)) {
return Option.none()
}
const out: Array<A | B> = arr
const b = f(arr[i])
out[i] = b
const b = f(arr[index])
out[index] = b
return Option.some(out)
})

Expand Down Expand Up @@ -2004,10 +2006,11 @@ export const remove: {
<A>(self: Iterable<A>, i: number): Array<A>
} = dual(2, <A>(self: Iterable<A>, i: number): Array<A> => {
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
})

Expand Down
40 changes: 40 additions & 0 deletions packages/effect/test/ArrayNaNIndex.test.ts
Original file line number Diff line number Diff line change
@@ -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]))
})
})
Loading