Add tests and documentation to number utils
Some checks are pending
Tests and checks on push / run-checks-and-tests (push) Waiting to run
Updates translation progress / update-translation-progress-issue (push) Waiting to run

Also improved safeNumberParse robustness
This commit is contained in:
Kalle 2025-07-06 22:00:54 +03:00
parent 5ba85f8db2
commit 5a2fcf7a9f
2 changed files with 163 additions and 3 deletions

View File

@ -1,9 +1,30 @@
import * as R from "remeda";
/**
* Rounds a number to a specified number of decimal places.
*
* @example
* ```typescript
* roundToNDecimalPlaces(3.14159); // returns 3.14
* roundToNDecimalPlaces(3.14159, 3); // returns 3.142
* roundToNDecimalPlaces(2.5, 0); // returns 3
* ```
*/
export function roundToNDecimalPlaces(num: number, n = 2) {
return Number((Math.round(num * 10 ** n) / 10 ** n).toFixed(n));
}
/**
* Truncates a number to a specified number of decimal places without rounding.
*
* @example
* ```typescript
* cutToNDecimalPlaces(3.9999, 2); // returns 3.99
* cutToNDecimalPlaces(3.12, 1); // returns 3.1
* cutToNDecimalPlaces(100, 2); // returns 100
* cutToNDecimalPlaces(3.0001, 2); // returns 3
* ```
*/
export function cutToNDecimalPlaces(num: number, n = 2) {
const multiplier = 10 ** n;
const truncatedNum = Math.trunc(num * multiplier) / multiplier;
@ -11,13 +32,47 @@ export function cutToNDecimalPlaces(num: number, n = 2) {
return Number(n > 0 ? result.replace(/\.?0+$/, "") : result);
}
/**
* Calculates the average (arithmetic mean) of an array of numbers.
* Returns 0 if the array is empty.
*
* @example
* ```typescript
* averageArray([2, 4, 6, 8]); // returns 5
* averageArray([-2, -4, -6, -8]); // returns -5
* averageArray([10, -10, 20, -20]); // returns 0
* averageArray([42]); // returns 42
* averageArray([]); // returns 0
* ```
*/
export function averageArray(arr: number[]) {
if (arr.length === 0) return 0;
return R.sum(arr) / arr.length;
}
/**
* Safely parses a string into a number, returning `null` if the input is `null`,
* empty, or not a valid number.
*
* Trims whitespace from the input before parsing. If the trimmed string is empty
* or cannot be converted to a valid number, returns `null`.
*
* @example
* ```typescript
* safeNumberParse("42"); // returns 42
* safeNumberParse(" 3.14 "); // returns 3.14
* safeNumberParse(""); // returns null
* safeNumberParse("abc"); // returns null
* safeNumberParse(null); // returns null
* ```
*/
export function safeNumberParse(value: string | null) {
if (value === null) return null;
const result = Number(value);
const trimmed = value.trim();
if (trimmed === "") return null;
const result = Number(trimmed);
return Number.isNaN(result) ? null : result;
}

View File

@ -1,5 +1,45 @@
import { describe, expect, test } from "vitest";
import { cutToNDecimalPlaces } from "./number";
import { describe, expect, it, test } from "vitest";
import {
averageArray,
cutToNDecimalPlaces,
roundToNDecimalPlaces,
safeNumberParse,
} from "./number";
describe("roundToNDecimalPlaces()", () => {
it("rounds to 2 decimal places by default", () => {
expect(roundToNDecimalPlaces(1.234)).toBe(1.23);
expect(roundToNDecimalPlaces(1.235)).toBe(1.24);
expect(roundToNDecimalPlaces(1.2)).toBe(1.2);
expect(roundToNDecimalPlaces(1)).toBe(1);
});
it("rounds to 0 decimal places", () => {
expect(roundToNDecimalPlaces(1.6, 0)).toBe(2);
expect(roundToNDecimalPlaces(1.4, 0)).toBe(1);
expect(roundToNDecimalPlaces(2.5, 0)).toBe(3);
});
it("rounds to 3 decimal places", () => {
expect(roundToNDecimalPlaces(1.23456, 3)).toBe(1.235);
expect(roundToNDecimalPlaces(1.23444, 3)).toBe(1.234);
});
it("handles negative numbers", () => {
expect(roundToNDecimalPlaces(-1.2345, 2)).toBe(-1.23);
expect(roundToNDecimalPlaces(-1.2355, 2)).toBe(-1.24);
});
it("handles zero", () => {
expect(roundToNDecimalPlaces(0, 2)).toBe(0);
expect(roundToNDecimalPlaces(0, 0)).toBe(0);
});
it("handles large numbers", () => {
expect(roundToNDecimalPlaces(123456.789, 1)).toBe(123456.8);
expect(roundToNDecimalPlaces(123456.789, 0)).toBe(123457);
});
});
describe("cutToNDecimalPlaces()", () => {
test("cutOff truncates decimal places correctly", () => {
@ -22,3 +62,68 @@ describe("cutToNDecimalPlaces()", () => {
expect(result).toBe(3);
});
});
describe("averageArray()", () => {
it("returns the average of positive numbers", () => {
const result = averageArray([2, 4, 6, 8]);
expect(result).toBe(5);
});
it("returns the average of negative numbers", () => {
const result = averageArray([-2, -4, -6, -8]);
expect(result).toBe(-5);
});
it("returns the average of mixed positive and negative numbers", () => {
const result = averageArray([10, -10, 20, -20]);
expect(result).toBe(0);
});
it("returns the value itself for a single-element array", () => {
const result = averageArray([42]);
expect(result).toBe(42);
});
it("returns 0 for an empty array", () => {
const result = averageArray([]);
expect(result).toBe(0);
});
});
describe("safeNumberParse()", () => {
it("returns null for null input", () => {
expect(safeNumberParse(null)).toBeNull();
});
it("parses valid integer string", () => {
expect(safeNumberParse("42")).toBe(42);
});
it("parses valid float string", () => {
expect(safeNumberParse("3.14")).toBe(3.14);
});
it("returns null for non-numeric string", () => {
expect(safeNumberParse("abc")).toBeNull();
});
it("returns null for empty string", () => {
expect(safeNumberParse("")).toBeNull();
});
it("parses string with leading/trailing spaces", () => {
expect(safeNumberParse(" 7 ")).toBe(7);
});
it("parses negative numbers", () => {
expect(safeNumberParse("-123")).toBe(-123);
});
it("parses zero", () => {
expect(safeNumberParse("0")).toBe(0);
});
it("returns null for string with only spaces", () => {
expect(safeNumberParse(" ")).toBeNull();
});
});