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/forty-friends-hammer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"effect": minor
---

Add round and sumAll to BigDecimal
221 changes: 221 additions & 0 deletions packages/effect/src/BigDecimal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1126,3 +1126,224 @@ export const isNegative = (n: BigDecimal): boolean => n.value < bigint0
* @category predicates
*/
export const isPositive = (n: BigDecimal): boolean => n.value > bigint0

const isBigDecimalArgs = (args: IArguments) => isBigDecimal(args[0])

/**
* Calculate the ceiling of a `BigDecimal` at the given scale.
*
* @example
* ```ts
* import * as assert from "node:assert"
* import { ceil, unsafeFromString } from "effect/BigDecimal"
*
* assert.deepStrictEqual(ceil(unsafeFromString("145"), -1), unsafeFromString("150"))
* assert.deepStrictEqual(ceil(unsafeFromString("-14.5")), unsafeFromString("-14"))
* ```
*
* @since 3.16.0
* @category math
*/
export const ceil: {
(scale: number): (self: BigDecimal) => BigDecimal
(self: BigDecimal, scale?: number): BigDecimal
} = dual(isBigDecimalArgs, (self: BigDecimal, scale: number = 0): BigDecimal => {
const truncated = truncate(self, scale)

if (isPositive(self) && lessThan(truncated, self)) {
return sum(truncated, make(1n, scale))
}

return truncated
})

/**
* Calculate the floor of a `BigDecimal` at the given scale.
*
* @example
* ```ts
* import * as assert from "node:assert"
* import { floor, unsafeFromString } from "effect/BigDecimal"
*
* assert.deepStrictEqual(floor(unsafeFromString("145"), -1), unsafeFromString("140"))
* assert.deepStrictEqual(floor(unsafeFromString("-14.5")), unsafeFromString("-15"))
* ```
*
* @since 3.16.0
* @category math
*/
export const floor: {
(scale: number): (self: BigDecimal) => BigDecimal
(self: BigDecimal, scale?: number): BigDecimal
} = dual(isBigDecimalArgs, (self: BigDecimal, scale: number = 0): BigDecimal => {
const truncated = truncate(self, scale)

if (isNegative(self) && greaterThan(truncated, self)) {
return sum(truncated, make(-1n, scale))
}

return truncated
})

/**
* Truncate a `BigDecimal` at the given scale. This is the same operation as rounding away from zero.
*
* @example
* ```ts
* import * as assert from "node:assert"
* import { truncate, unsafeFromString } from "effect/BigDecimal"
*
* assert.deepStrictEqual(truncate(unsafeFromString("145"), -1), unsafeFromString("140"))
* assert.deepStrictEqual(truncate(unsafeFromString("-14.5")), unsafeFromString("-14"))
* ```
*
* @since 3.16.0
* @category math
*/
export const truncate: {
(scale: number): (self: BigDecimal) => BigDecimal
(self: BigDecimal, scale?: number): BigDecimal
} = dual(isBigDecimalArgs, (self: BigDecimal, scale: number = 0): BigDecimal => {
if (self.scale <= scale) {
return self
}

// BigInt division truncates towards zero
return make(self.value / (10n ** BigInt(self.scale - scale)), scale)
})

/**
* Internal function used by `round` for `half-even` and `half-odd` rounding modes.
*
* Returns the digit at the position of the given `scale` within the `BigDecimal`.
*
* @internal
*/
export const digitAt: {
(scale: number): (self: BigDecimal) => bigint
(self: BigDecimal, scale: number): bigint
} = dual(2, (self: BigDecimal, scale: number): bigint => {
if (self.scale < scale) {
return 0n
}

const scaled = self.value / (10n ** BigInt(self.scale - scale))
return scaled % 10n
})

/**
* Rounding modes for `BigDecimal`.
*
* `ceil`: round towards positive infinity
* `floor`: round towards negative infinity
* `to-zero`: round towards zero
* `from-zero`: round away from zero
* `half-ceil`: round to the nearest neighbor; if equidistant round towards positive infinity
* `half-floor`: round to the nearest neighbor; if equidistant round towards negative infinity
* `half-to-zero`: round to the nearest neighbor; if equidistant round towards zero
* `half-from-zero`: round to the nearest neighbor; if equidistant round away from zero
* `half-even`: round to the nearest neighbor; if equidistant round to the neighbor with an even digit
* `half-odd`: round to the nearest neighbor; if equidistant round to the neighbor with an odd digit
*
* @since 3.16.0
* @category math
*/
export type RoundingMode =
| "ceil"
| "floor"
| "to-zero"
| "from-zero"
| "half-ceil"
| "half-floor"
| "half-to-zero"
| "half-from-zero"
| "half-even"
| "half-odd"

/**
* Rounds a `BigDecimal` at the given scale with the specified rounding mode.
*
* @example
* ```ts
* import * as assert from "node:assert"
* import { round, unsafeFromString } from "effect/BigDecimal"
*
* assert.deepStrictEqual(round(unsafeFromString("145"), { mode: "from-zero", scale: -1 }), unsafeFromString("150"))
* assert.deepStrictEqual(round(unsafeFromString("-14.5")), unsafeFromString("-15"))
* ```
*
* @since 3.16.0
* @category math
*/
export const round: {
(options: { scale?: number; mode?: RoundingMode }): (self: BigDecimal) => BigDecimal
(n: BigDecimal, options?: { scale?: number; mode?: RoundingMode }): BigDecimal
} = dual(isBigDecimalArgs, (self: BigDecimal, options?: { scale?: number; mode?: RoundingMode }): BigDecimal => {
const mode = options?.mode ?? "half-from-zero"
const scale = options?.scale ?? 0

switch (mode) {
case "ceil":
return ceil(self, scale)

case "floor":
return floor(self, scale)

case "to-zero":
return truncate(self, scale)

case "from-zero":
return (isPositive(self) ? ceil(self, scale) : floor(self, scale))

case "half-ceil":
return floor(sum(self, make(5n, scale + 1)), scale)

case "half-floor":
return ceil(sum(self, make(-5n, scale + 1)), scale)

case "half-to-zero":
return isNegative(self)
? floor(sum(self, make(5n, scale + 1)), scale)
: ceil(sum(self, make(-5n, scale + 1)), scale)

case "half-from-zero":
return isNegative(self)
? ceil(sum(self, make(-5n, scale + 1)), scale)
: floor(sum(self, make(5n, scale + 1)), scale)
}

const halfCeil = floor(sum(self, make(5n, scale + 1)), scale)
const halfFloor = ceil(sum(self, make(-5n, scale + 1)), scale)
const digit = digitAt(halfCeil, scale)

switch (mode) {
case "half-even":
return equals(halfCeil, halfFloor) ? halfCeil : (digit % 2n === 0n) ? halfCeil : halfFloor

case "half-odd":
return equals(halfCeil, halfFloor) ? halfCeil : (digit % 2n === 0n) ? halfFloor : halfCeil
}
})

/**
* Takes an `Iterable` of `BigDecimal`s and returns their sum as a single `BigDecimal`
*
* @example
* ```ts
* import * as assert from "node:assert"
* import { unsafeFromString, sumAll } from "effect/BigDecimal"
*
* assert.deepStrictEqual(sumAll([unsafeFromString("2"), unsafeFromString("3"), unsafeFromString("4")]), unsafeFromString("9"))
* ```
*
* @category math
* @since 3.16.0
*/
export const sumAll = (collection: Iterable<BigDecimal>): BigDecimal => {
let out = zero
for (const n of collection) {
out = sum(out, n)
}

return out
}
Loading