Skip to content

Commit fe59901

Browse files
jrudderfubhy
authored andcommitted
Add round and sumAll functions to BigDecimal (#4920)
Co-authored-by: Sebastian Lorenz <fubhy@fubhy.com>
1 parent 271493b commit fe59901

File tree

3 files changed

+400
-48
lines changed

3 files changed

+400
-48
lines changed

.changeset/forty-friends-hammer.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"effect": minor
3+
---
4+
5+
Add round and sumAll to BigDecimal

packages/effect/src/BigDecimal.ts

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1126,3 +1126,224 @@ export const isNegative = (n: BigDecimal): boolean => n.value < bigint0
11261126
* @category predicates
11271127
*/
11281128
export const isPositive = (n: BigDecimal): boolean => n.value > bigint0
1129+
1130+
const isBigDecimalArgs = (args: IArguments) => isBigDecimal(args[0])
1131+
1132+
/**
1133+
* Calculate the ceiling of a `BigDecimal` at the given scale.
1134+
*
1135+
* @example
1136+
* ```ts
1137+
* import * as assert from "node:assert"
1138+
* import { ceil, unsafeFromString } from "effect/BigDecimal"
1139+
*
1140+
* assert.deepStrictEqual(ceil(unsafeFromString("145"), -1), unsafeFromString("150"))
1141+
* assert.deepStrictEqual(ceil(unsafeFromString("-14.5")), unsafeFromString("-14"))
1142+
* ```
1143+
*
1144+
* @since 3.16.0
1145+
* @category math
1146+
*/
1147+
export const ceil: {
1148+
(scale: number): (self: BigDecimal) => BigDecimal
1149+
(self: BigDecimal, scale?: number): BigDecimal
1150+
} = dual(isBigDecimalArgs, (self: BigDecimal, scale: number = 0): BigDecimal => {
1151+
const truncated = truncate(self, scale)
1152+
1153+
if (isPositive(self) && lessThan(truncated, self)) {
1154+
return sum(truncated, make(1n, scale))
1155+
}
1156+
1157+
return truncated
1158+
})
1159+
1160+
/**
1161+
* Calculate the floor of a `BigDecimal` at the given scale.
1162+
*
1163+
* @example
1164+
* ```ts
1165+
* import * as assert from "node:assert"
1166+
* import { floor, unsafeFromString } from "effect/BigDecimal"
1167+
*
1168+
* assert.deepStrictEqual(floor(unsafeFromString("145"), -1), unsafeFromString("140"))
1169+
* assert.deepStrictEqual(floor(unsafeFromString("-14.5")), unsafeFromString("-15"))
1170+
* ```
1171+
*
1172+
* @since 3.16.0
1173+
* @category math
1174+
*/
1175+
export const floor: {
1176+
(scale: number): (self: BigDecimal) => BigDecimal
1177+
(self: BigDecimal, scale?: number): BigDecimal
1178+
} = dual(isBigDecimalArgs, (self: BigDecimal, scale: number = 0): BigDecimal => {
1179+
const truncated = truncate(self, scale)
1180+
1181+
if (isNegative(self) && greaterThan(truncated, self)) {
1182+
return sum(truncated, make(-1n, scale))
1183+
}
1184+
1185+
return truncated
1186+
})
1187+
1188+
/**
1189+
* Truncate a `BigDecimal` at the given scale. This is the same operation as rounding away from zero.
1190+
*
1191+
* @example
1192+
* ```ts
1193+
* import * as assert from "node:assert"
1194+
* import { truncate, unsafeFromString } from "effect/BigDecimal"
1195+
*
1196+
* assert.deepStrictEqual(truncate(unsafeFromString("145"), -1), unsafeFromString("140"))
1197+
* assert.deepStrictEqual(truncate(unsafeFromString("-14.5")), unsafeFromString("-14"))
1198+
* ```
1199+
*
1200+
* @since 3.16.0
1201+
* @category math
1202+
*/
1203+
export const truncate: {
1204+
(scale: number): (self: BigDecimal) => BigDecimal
1205+
(self: BigDecimal, scale?: number): BigDecimal
1206+
} = dual(isBigDecimalArgs, (self: BigDecimal, scale: number = 0): BigDecimal => {
1207+
if (self.scale <= scale) {
1208+
return self
1209+
}
1210+
1211+
// BigInt division truncates towards zero
1212+
return make(self.value / (10n ** BigInt(self.scale - scale)), scale)
1213+
})
1214+
1215+
/**
1216+
* Internal function used by `round` for `half-even` and `half-odd` rounding modes.
1217+
*
1218+
* Returns the digit at the position of the given `scale` within the `BigDecimal`.
1219+
*
1220+
* @internal
1221+
*/
1222+
export const digitAt: {
1223+
(scale: number): (self: BigDecimal) => bigint
1224+
(self: BigDecimal, scale: number): bigint
1225+
} = dual(2, (self: BigDecimal, scale: number): bigint => {
1226+
if (self.scale < scale) {
1227+
return 0n
1228+
}
1229+
1230+
const scaled = self.value / (10n ** BigInt(self.scale - scale))
1231+
return scaled % 10n
1232+
})
1233+
1234+
/**
1235+
* Rounding modes for `BigDecimal`.
1236+
*
1237+
* `ceil`: round towards positive infinity
1238+
* `floor`: round towards negative infinity
1239+
* `to-zero`: round towards zero
1240+
* `from-zero`: round away from zero
1241+
* `half-ceil`: round to the nearest neighbor; if equidistant round towards positive infinity
1242+
* `half-floor`: round to the nearest neighbor; if equidistant round towards negative infinity
1243+
* `half-to-zero`: round to the nearest neighbor; if equidistant round towards zero
1244+
* `half-from-zero`: round to the nearest neighbor; if equidistant round away from zero
1245+
* `half-even`: round to the nearest neighbor; if equidistant round to the neighbor with an even digit
1246+
* `half-odd`: round to the nearest neighbor; if equidistant round to the neighbor with an odd digit
1247+
*
1248+
* @since 3.16.0
1249+
* @category math
1250+
*/
1251+
export type RoundingMode =
1252+
| "ceil"
1253+
| "floor"
1254+
| "to-zero"
1255+
| "from-zero"
1256+
| "half-ceil"
1257+
| "half-floor"
1258+
| "half-to-zero"
1259+
| "half-from-zero"
1260+
| "half-even"
1261+
| "half-odd"
1262+
1263+
/**
1264+
* Rounds a `BigDecimal` at the given scale with the specified rounding mode.
1265+
*
1266+
* @example
1267+
* ```ts
1268+
* import * as assert from "node:assert"
1269+
* import { round, unsafeFromString } from "effect/BigDecimal"
1270+
*
1271+
* assert.deepStrictEqual(round(unsafeFromString("145"), { mode: "from-zero", scale: -1 }), unsafeFromString("150"))
1272+
* assert.deepStrictEqual(round(unsafeFromString("-14.5")), unsafeFromString("-15"))
1273+
* ```
1274+
*
1275+
* @since 3.16.0
1276+
* @category math
1277+
*/
1278+
export const round: {
1279+
(options: { scale?: number; mode?: RoundingMode }): (self: BigDecimal) => BigDecimal
1280+
(n: BigDecimal, options?: { scale?: number; mode?: RoundingMode }): BigDecimal
1281+
} = dual(isBigDecimalArgs, (self: BigDecimal, options?: { scale?: number; mode?: RoundingMode }): BigDecimal => {
1282+
const mode = options?.mode ?? "half-from-zero"
1283+
const scale = options?.scale ?? 0
1284+
1285+
switch (mode) {
1286+
case "ceil":
1287+
return ceil(self, scale)
1288+
1289+
case "floor":
1290+
return floor(self, scale)
1291+
1292+
case "to-zero":
1293+
return truncate(self, scale)
1294+
1295+
case "from-zero":
1296+
return (isPositive(self) ? ceil(self, scale) : floor(self, scale))
1297+
1298+
case "half-ceil":
1299+
return floor(sum(self, make(5n, scale + 1)), scale)
1300+
1301+
case "half-floor":
1302+
return ceil(sum(self, make(-5n, scale + 1)), scale)
1303+
1304+
case "half-to-zero":
1305+
return isNegative(self)
1306+
? floor(sum(self, make(5n, scale + 1)), scale)
1307+
: ceil(sum(self, make(-5n, scale + 1)), scale)
1308+
1309+
case "half-from-zero":
1310+
return isNegative(self)
1311+
? ceil(sum(self, make(-5n, scale + 1)), scale)
1312+
: floor(sum(self, make(5n, scale + 1)), scale)
1313+
}
1314+
1315+
const halfCeil = floor(sum(self, make(5n, scale + 1)), scale)
1316+
const halfFloor = ceil(sum(self, make(-5n, scale + 1)), scale)
1317+
const digit = digitAt(halfCeil, scale)
1318+
1319+
switch (mode) {
1320+
case "half-even":
1321+
return equals(halfCeil, halfFloor) ? halfCeil : (digit % 2n === 0n) ? halfCeil : halfFloor
1322+
1323+
case "half-odd":
1324+
return equals(halfCeil, halfFloor) ? halfCeil : (digit % 2n === 0n) ? halfFloor : halfCeil
1325+
}
1326+
})
1327+
1328+
/**
1329+
* Takes an `Iterable` of `BigDecimal`s and returns their sum as a single `BigDecimal`
1330+
*
1331+
* @example
1332+
* ```ts
1333+
* import * as assert from "node:assert"
1334+
* import { unsafeFromString, sumAll } from "effect/BigDecimal"
1335+
*
1336+
* assert.deepStrictEqual(sumAll([unsafeFromString("2"), unsafeFromString("3"), unsafeFromString("4")]), unsafeFromString("9"))
1337+
* ```
1338+
*
1339+
* @category math
1340+
* @since 3.16.0
1341+
*/
1342+
export const sumAll = (collection: Iterable<BigDecimal>): BigDecimal => {
1343+
let out = zero
1344+
for (const n of collection) {
1345+
out = sum(out, n)
1346+
}
1347+
1348+
return out
1349+
}

0 commit comments

Comments
 (0)