mielk-fn 1.0.26 → 1.0.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/.github/workflows/release.yaml +39 -0
  2. package/CHANGELOG.md +7 -0
  3. package/jest.config.js +27 -0
  4. package/package.json +83 -40
  5. package/src/arrays/arrays.spec.ts +113 -0
  6. package/src/arrays/arrays.ts +30 -0
  7. package/src/arrays/index.ts +1 -0
  8. package/src/dates/dates.spec.ts +68 -0
  9. package/src/dates/dates.ts +34 -0
  10. package/src/dates/index.ts +1 -0
  11. package/src/index.ts +5 -0
  12. package/src/internal/helpers.ts +0 -0
  13. package/src/io/index.ts +1 -0
  14. package/src/io/io.spec.ts +81 -0
  15. package/src/io/io.ts +9 -0
  16. package/src/math/index.ts +1 -0
  17. package/src/math/math.spec.ts +94 -0
  18. package/src/math/math.ts +12 -0
  19. package/src/objects/index.ts +1 -0
  20. package/src/objects/objects.spec.ts +257 -0
  21. package/src/objects/objects.ts +58 -0
  22. package/src/regex/index.ts +1 -0
  23. package/src/regex/regex.spec.ts +17 -0
  24. package/src/regex/regex.ts +10 -0
  25. package/src/strings/index.ts +1 -0
  26. package/src/strings/strings.spec.ts +79 -0
  27. package/src/strings/strings.ts +20 -0
  28. package/src/types/index.ts +4 -0
  29. package/src/types/math.ts +4 -0
  30. package/src/types/objects.ts +1 -0
  31. package/src/variables/index.ts +1 -0
  32. package/src/variables/variables.spec.ts +310 -0
  33. package/src/variables/variables.ts +28 -0
  34. package/tsconfig.json +22 -0
  35. package/lib/index.d.ts +0 -40
  36. package/lib/index.js +0 -7
  37. package/lib/methods/arrays.d.ts +0 -11
  38. package/lib/methods/arrays.js +0 -29
  39. package/lib/methods/objects.d.ts +0 -13
  40. package/lib/methods/objects.js +0 -48
  41. package/lib/methods/regex.d.ts +0 -4
  42. package/lib/methods/regex.js +0 -9
  43. package/lib/methods/strings.d.ts +0 -4
  44. package/lib/methods/strings.js +0 -5
  45. package/lib/methods/variables.d.ts +0 -14
  46. package/lib/methods/variables.js +0 -18
  47. package/lib/models/common.d.ts +0 -6
  48. package/lib/models/common.js +0 -1
@@ -0,0 +1,39 @@
1
+ name: release.yaml
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - master
7
+ workflow_dispatch:
8
+
9
+ permissions:
10
+ contents: read
11
+ id-token: write
12
+
13
+ jobs:
14
+ publish:
15
+ runs-on: ubuntu-latest
16
+
17
+ steps:
18
+ - name: Checkout repository
19
+ uses: actions/checkout@v4
20
+
21
+ - name: Setup Node.js
22
+ uses: actions/setup-node@v4
23
+ with:
24
+ node-version: 20
25
+ registry-url: https://registry.npmjs.org
26
+
27
+ - name: Install dependencies
28
+ run: npm install
29
+
30
+ - name: Run tests
31
+ run: npm test
32
+
33
+ - name: Build package
34
+ run: npm run build
35
+
36
+ - name: Publish to npm
37
+ run: npm publish
38
+ env:
39
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
package/CHANGELOG.md ADDED
@@ -0,0 +1,7 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4
+
5
+ ### [1.0.28](https://github.com/mielk/mielk-fn/compare/v1.0.27...v1.0.28) (2026-03-25)
6
+
7
+ ### 1.0.27 (2026-03-25)
package/jest.config.js ADDED
@@ -0,0 +1,27 @@
1
+ export default {
2
+ roots: ['<rootDir>/src'], // Specify the root directory for Jest to look for tests
3
+ transform: {
4
+ '^.+\\.tsx?$': 'ts-jest', // Transform TypeScript files using ts-jest
5
+ },
6
+ testMatch: [
7
+ '**/?(*.)+(spec|test).ts?(x)', // Match test files with .spec.ts or .test.ts extensions
8
+ ],
9
+ moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
10
+ moduleNameMapper: {
11
+ '^(\\.{1,2}/.*)\\.js$': '$1',
12
+ },
13
+ reporters: [
14
+ 'default',
15
+ [
16
+ 'jest-html-reporters',
17
+ {
18
+ publicPath: './reports/html-report',
19
+ filename: 'report.html',
20
+ pageTitle: 'mielk-fn - Test Report',
21
+ expand: true,
22
+ openReport: true,
23
+ groupBy: ['describe'],
24
+ },
25
+ ],
26
+ ],
27
+ };
package/package.json CHANGED
@@ -1,40 +1,83 @@
1
- {
2
- "name": "mielk-fn",
3
- "version": "1.0.26",
4
- "description": "Set of helpful functions",
5
- "main": "lib/index.js",
6
- "type": "module",
7
- "scripts": {
8
- "prepublishOnly": "npm test",
9
- "test": "jest --detectOpenHandles",
10
- "build": "tsc"
11
- },
12
- "repository": {
13
- "type": "git",
14
- "url": "git+https://github.com/mielk/mielk-fn.git"
15
- },
16
- "author": "Tomasz Mielniczek",
17
- "license": "ISC",
18
- "bugs": {
19
- "url": "https://github.com/mielk/mielk-fn/issues"
20
- },
21
- "homepage": "https://github.com/mielk/mielk-fn#readme",
22
- "devDependencies": {
23
- "@babel/cli": "^7.25.7",
24
- "@babel/core": "^7.25.7",
25
- "@babel/node": "^7.25.7",
26
- "@babel/plugin-transform-modules-commonjs": "^7.25.7",
27
- "@babel/preset-env": "^7.25.7",
28
- "@babel/preset-typescript": "^7.25.7",
29
- "@types/jest": "^29.5.13",
30
- "@types/node": "^22.7.4",
31
- "babel-jest": "^29.7.0",
32
- "jest-html-reporters": "^3.1.7",
33
- "ts-jest": "^29.2.5",
34
- "tsc-alias": "^1.8.10",
35
- "typescript": "^5.6.2"
36
- },
37
- "files": [
38
- "lib"
39
- ]
40
- }
1
+ {
2
+ "name": "mielk-fn",
3
+ "version": "1.0.28",
4
+ "keywords": [],
5
+ "author": "mielk",
6
+ "description": "Generic functions used in various projects",
7
+ "type": "module",
8
+ "main": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "import": "./dist/index.js",
14
+ "default": "./dist/index.js"
15
+ },
16
+ "./arrays": {
17
+ "types": "./dist/arrays/index.d.ts",
18
+ "import": "./dist/arrays/index.js",
19
+ "default": "./dist/arrays/index.js"
20
+ },
21
+ "./dates": {
22
+ "types": "./dist/dates/index.d.ts",
23
+ "import": "./dist/dates/index.js",
24
+ "default": "./dist/dates/index.js"
25
+ },
26
+ "./io": {
27
+ "types": "./dist/io/index.d.ts",
28
+ "import": "./dist/io/index.js",
29
+ "default": "./dist/io/index.js"
30
+ },
31
+ "./math": {
32
+ "types": "./dist/math/index.d.ts",
33
+ "import": "./dist/math/index.js",
34
+ "default": "./dist/math/index.js"
35
+ },
36
+ "./objects": {
37
+ "types": "./dist/objects/index.d.ts",
38
+ "import": "./dist/objects/index.js",
39
+ "default": "./dist/objects/index.js"
40
+ },
41
+ "./regex": {
42
+ "types": "./dist/regex/index.d.ts",
43
+ "import": "./dist/regex/index.js",
44
+ "default": "./dist/regex/index.js"
45
+ },
46
+ "./strings": {
47
+ "types": "./dist/strings/index.d.ts",
48
+ "import": "./dist/strings/index.js",
49
+ "default": "./dist/strings/index.js"
50
+ },
51
+ "./types": {
52
+ "types": "./dist/types/index.d.ts",
53
+ "import": "./dist/types/index.js",
54
+ "default": "./dist/types/index.js"
55
+ },
56
+ "./variables": {
57
+ "types": "./dist/variables/index.d.ts",
58
+ "import": "./dist/variables/index.js",
59
+ "default": "./dist/variables/index.js"
60
+ }
61
+ },
62
+ "scripts": {
63
+ "test": "jest --detectOpenHandles",
64
+ "build": "tsc",
65
+ "prepublishOnly": "npm test && npm run build",
66
+ "dev": "tsc -w",
67
+ "release": "standard-version"
68
+ },
69
+ "license": "ISC",
70
+ "repository": {
71
+ "type": "git",
72
+ "url": "git+https://github.com/mielk/mielk-fn.git"
73
+ },
74
+ "homepage": "https://mielk.github.io/mielk-fn/",
75
+ "devDependencies": {
76
+ "@types/jest": "^30.0.0",
77
+ "jest": "^30.3.0",
78
+ "jest-html-reporters": "^3.1.7",
79
+ "standard-version": "^9.5.0",
80
+ "ts-jest": "^29.4.6",
81
+ "typescript": "^5.9.2"
82
+ }
83
+ }
@@ -0,0 +1,113 @@
1
+ import { toIndexedArray, toMap } from "./arrays.js";
2
+
3
+
4
+
5
+ describe('toMap', () => {
6
+ const objects = [
7
+ { name: 'Adam', id: 1 },
8
+ { name: 'Bartek', id: 4 },
9
+ { name: 'Czesiek', id: 5 },
10
+ ];
11
+ const primitives = ['Adam', 'Bartek', 'Czesiek'];
12
+
13
+ it('should create a map from an array of objects', () => {
14
+ const map = toMap(objects, (obj) => obj.id);
15
+ expect(map.size).toBe(3);
16
+ expect(map.get(1)).toEqual(objects[0]);
17
+ expect(map.get(4)).toEqual(objects[1]);
18
+ expect(map.get(5)).toEqual(objects[2]);
19
+ });
20
+
21
+ it('should create a map from an array of primitives', () => {
22
+ const map = toMap(primitives, (str) => str.charAt(0));
23
+ expect(map.size).toBe(3);
24
+ expect(map.get('A')).toEqual(primitives[0]);
25
+ expect(map.get('B')).toEqual(primitives[1]);
26
+ expect(map.get('C')).toEqual(primitives[2]);
27
+ });
28
+
29
+ it('should allow custom value callback', () => {
30
+ const map = toMap(
31
+ objects,
32
+ (obj) => obj.id,
33
+ (obj) => obj.name
34
+ );
35
+ expect(map.size).toBe(3);
36
+ expect(map.get(1)).toBe('Adam');
37
+ expect(map.get(4)).toBe('Bartek');
38
+ expect(map.get(5)).toBe('Czesiek');
39
+ });
40
+
41
+ it('should ignore duplicate keys by default', () => {
42
+ const objectsWithDuplicate = [
43
+ { name: 'Adam', id: 1 },
44
+ { name: 'Bartek', id: 4 },
45
+ { name: 'Czesiek', id: 4 },
46
+ ];
47
+ const map = toMap(objectsWithDuplicate, (obj) => obj.id);
48
+ expect(map.size).toBe(2);
49
+ expect(map.get(1)).toEqual(objectsWithDuplicate[0]);
50
+ expect(map.get(4)).toEqual(objectsWithDuplicate[1]);
51
+ });
52
+ });
53
+
54
+ describe('toIndexedArray', () => {
55
+ it('should return an indexed array with basic objects', () => {
56
+ const items = [
57
+ { name: 'Adam', id: 1 },
58
+ { name: 'Bartek', id: 4 },
59
+ { name: 'Czesiek', id: 5 },
60
+ ];
61
+
62
+ const callback = (item: any) => item.id;
63
+ const result = toIndexedArray(items, callback);
64
+
65
+ expect(result[1]).toEqual({ name: 'Adam', id: 1 });
66
+ expect(result[4]).toEqual({ name: 'Bartek', id: 4 });
67
+ expect(result[5]).toEqual({ name: 'Czesiek', id: 5 });
68
+ });
69
+
70
+ it('should handle custom class instances', () => {
71
+ class Person {
72
+ fullName: string;
73
+ id: number;
74
+
75
+ constructor(fullName: string, id: number) {
76
+ this.fullName = fullName;
77
+ this.id = id;
78
+ }
79
+ }
80
+
81
+ const items = [new Person('Adam', 1), new Person('Bartek', 4), new Person('Czesiek', 5)];
82
+
83
+ const callback = (item: unknown) => {
84
+ if (typeof item === 'object' && item !== null && 'id' in item) {
85
+ return (item as Person).id;
86
+ }
87
+ return -1;
88
+ };
89
+ const result = toIndexedArray(items, callback);
90
+
91
+ expect(result[1]).toEqual(new Person('Adam', 1));
92
+ expect(result[4]).toEqual(new Person('Bartek', 4));
93
+ expect(result[5]).toEqual(new Person('Czesiek', 5));
94
+ });
95
+
96
+ it('should throw an error when the callback returns a non-number value', () => {
97
+ const items = [
98
+ { name: 'Adam', id: '1' },
99
+ { name: 'Bartek', id: '4' },
100
+ { name: 'Czesiek', id: '5' },
101
+ ];
102
+
103
+ const callback = (item: any) => item.id;
104
+ expect(() => toIndexedArray(items, callback)).toThrow(new TypeError('Callback should return a number.'));
105
+ });
106
+
107
+ it('should handle an empty array', () => {
108
+ const items: any[] = [];
109
+ const callback = (item: any) => item.id;
110
+ const result = toIndexedArray(items, callback);
111
+ expect(result).toEqual([]);
112
+ });
113
+ });
@@ -0,0 +1,30 @@
1
+ export const toMap = (
2
+ items: any[],
3
+ keyCallback: (item: any) => string | number,
4
+ valueCallback: ((item: any) => any) = (item) => item,
5
+ ignoreDuplicates = true
6
+ ) => {
7
+ const map = new Map();
8
+ for (const item of items) {
9
+ const key = keyCallback(item);
10
+ const value = valueCallback ? valueCallback(item) : item;
11
+ if (!map.has(key) || !ignoreDuplicates) {
12
+ map.set(key, value);
13
+ }
14
+ }
15
+ return map;
16
+ };
17
+
18
+ export const toIndexedArray = <T>(items: T[], callback: (item: T) => number): T[] => {
19
+ const arr: any[] = [];
20
+ for (const item of items) {
21
+ const index = callback(item);
22
+ if (typeof index !== 'number') {
23
+ throw new TypeError('Callback should return a number.');
24
+ }
25
+ arr[index] = item;
26
+ }
27
+ return arr;
28
+ };
29
+
30
+ export default { toMap, toIndexedArray };
@@ -0,0 +1 @@
1
+ export * from './arrays.js';
@@ -0,0 +1,68 @@
1
+ import { formatDateDefault } from "./dates";
2
+
3
+ describe('formatDateDefault', () => {
4
+ it('should return empty string for undefined', () => {
5
+ expect(formatDateDefault(undefined)).toBe('');
6
+ });
7
+
8
+ it('should return string in format YYYY-MM-DD HH:mm:ss', () => {
9
+ const date = new Date('2024-01-02T03:04:05Z');
10
+
11
+ const result = formatDateDefault(date);
12
+
13
+ expect(result).toMatch(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/);
14
+ });
15
+
16
+ it('should always have 2-digit month, day, hour, minute, second', () => {
17
+ const date = new Date('2024-04-05T06:07:08Z');
18
+
19
+ const result = formatDateDefault(date);
20
+
21
+ const [datePart, timePart] = result.split(' ');
22
+
23
+ const [year, month, day] = datePart.split('-');
24
+ const [hour, minute, second] = timePart.split(':');
25
+
26
+ expect(year.length).toBe(4);
27
+ expect(month.length).toBe(2);
28
+ expect(day.length).toBe(2);
29
+ expect(hour.length).toBe(2);
30
+ expect(minute.length).toBe(2);
31
+ expect(second.length).toBe(2);
32
+ });
33
+
34
+ it('should accept custom timeZone parameter', () => {
35
+ const date = new Date('2024-01-02T03:04:05Z');
36
+
37
+ const result = formatDateDefault(date, 'UTC');
38
+
39
+ expect(result).toMatch(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/);
40
+ });
41
+
42
+ it('should still return valid structure for different time zones', () => {
43
+ const date = new Date('2024-06-15T10:20:30Z');
44
+
45
+ const resultWarsaw = formatDateDefault(date, 'Europe/Warsaw');
46
+ const resultUTC = formatDateDefault(date, 'UTC');
47
+
48
+ expect(resultWarsaw).toMatch(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/);
49
+ expect(resultUTC).toMatch(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/);
50
+
51
+ expect(resultWarsaw).not.toBe(resultUTC);
52
+ });
53
+
54
+ it('should be deterministic', () => {
55
+ const date = new Date('2024-06-15T10:20:30Z');
56
+
57
+ expect(formatDateDefault(date)).toBe(formatDateDefault(date));
58
+ });
59
+
60
+ it('should not mutate input date', () => {
61
+ const date = new Date('2024-01-01T00:00:00Z');
62
+ const time = date.getTime();
63
+
64
+ formatDateDefault(date);
65
+
66
+ expect(date.getTime()).toBe(time);
67
+ });
68
+ });
@@ -0,0 +1,34 @@
1
+ const defaultTimeZone = 'Europe/Warsaw';
2
+ const formatterCache = new Map<string, Intl.DateTimeFormat>();
3
+
4
+ function getFormatter(timeZone: string) {
5
+ if (formatterCache.has(timeZone)) {
6
+ return formatterCache.get(timeZone)!;
7
+ }
8
+
9
+ const formatter = new Intl.DateTimeFormat('en-US', {
10
+ year: 'numeric',
11
+ month: '2-digit',
12
+ day: '2-digit',
13
+ hour: '2-digit',
14
+ minute: '2-digit',
15
+ second: '2-digit',
16
+ hour12: false,
17
+ timeZone,
18
+ });
19
+
20
+ formatterCache.set(timeZone, formatter);
21
+ return formatter;
22
+ }
23
+
24
+
25
+ export function formatDateDefault(date: Date | undefined, timeZone = defaultTimeZone): string {
26
+ if (!date) return '';
27
+
28
+ const formatter = getFormatter(timeZone);
29
+ const parts = formatter.formatToParts(date);
30
+ const map = Object.fromEntries(parts.map((p) => [p.type, p.value])) as Record<Intl.DateTimeFormatPartTypes, string>;
31
+ return `${map.year}-${map.month}-${map.day} ${map.hour}:${map.minute}:${map.second}`;
32
+ }
33
+
34
+ export default { formatDateToDefault: formatDateDefault }
@@ -0,0 +1 @@
1
+ export * from './dates.js';
package/src/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ export * as dates from './dates/index.js';
2
+ export * as numbers from './math/index.js';
3
+ export * as objects from './variables/index.js';
4
+ export * as strings from './strings/index.js';
5
+ export * as types from './types/index.js';
File without changes
@@ -0,0 +1 @@
1
+ export * from './io.js';
@@ -0,0 +1,81 @@
1
+ import { parseJsonFile } from './io.js';
2
+
3
+ describe('parseJson', () => {
4
+ it('should return default object when valid', () => {
5
+ const input = {
6
+ a: 12,
7
+ default: { x: 1, y: 2 },
8
+ };
9
+ expect(parseJsonFile(input)).toEqual({ x: 1, y: 2 });
10
+ });
11
+
12
+ it('should return empty object when default is missing', () => {
13
+ const input = { a: 12, b: 23 };
14
+ expect(parseJsonFile(input)).toEqual({});
15
+ });
16
+
17
+ it('should return empty object when default is null', () => {
18
+ const input = { default: null };
19
+ expect(parseJsonFile(input)).toEqual({});
20
+ });
21
+
22
+ it('should return empty object when default is not an object', () => {
23
+ expect(parseJsonFile({ default: 123 })).toEqual({});
24
+ expect(parseJsonFile({ default: 'text' })).toEqual({});
25
+ expect(parseJsonFile({ default: true })).toEqual({});
26
+ });
27
+
28
+ it('should return empty object when default is an array', () => {
29
+ expect(parseJsonFile({ default: [] })).toEqual({});
30
+ });
31
+
32
+ it('should return empty object when input is null', () => {
33
+ expect(parseJsonFile(null)).toEqual({});
34
+ });
35
+
36
+ it('should return empty object when input is undefined', () => {
37
+ expect(parseJsonFile(undefined)).toEqual({});
38
+ });
39
+
40
+ it('should return empty object when input is primitive', () => {
41
+ expect(parseJsonFile(123)).toEqual({});
42
+ expect(parseJsonFile('text')).toEqual({});
43
+ expect(parseJsonFile(true)).toEqual({});
44
+ });
45
+
46
+ it('should return empty object when input is array', () => {
47
+ expect(parseJsonFile([])).toEqual({});
48
+ });
49
+
50
+ it('should return empty object when default is a special object (Date, Map, etc.)', () => {
51
+ expect(parseJsonFile({ default: new Date() })).toEqual({});
52
+ expect(parseJsonFile({ default: new Map() })).toEqual({});
53
+ });
54
+
55
+ it('should handle nested default object', () => {
56
+ const input = {
57
+ default: {
58
+ nested: {
59
+ value: 42,
60
+ },
61
+ },
62
+ };
63
+
64
+ expect(parseJsonFile(input)).toEqual({
65
+ nested: {
66
+ value: 42,
67
+ },
68
+ });
69
+ });
70
+
71
+ it('should not mutate input object', () => {
72
+ const input = {
73
+ default: { a: 1 },
74
+ };
75
+
76
+ const result = parseJsonFile(input);
77
+
78
+ expect(result).toEqual({ a: 1 });
79
+ expect(input.default).toEqual({ a: 1 });
80
+ });
81
+ });
package/src/io/io.ts ADDED
@@ -0,0 +1,9 @@
1
+ import { isPlainObject } from "../variables";
2
+
3
+ export const parseJsonFile = (file: unknown): Record<string, unknown> => {
4
+ if (file && typeof file === 'object' && 'default' in file) {
5
+ const value = (file as Record<string, unknown>).default;
6
+ return isPlainObject(value) ? value : {};
7
+ }
8
+ return {};
9
+ }
@@ -0,0 +1 @@
1
+ export * from './math.js';
@@ -0,0 +1,94 @@
1
+ import { formatPercent, formatWithSign } from "./math";
2
+
3
+ describe('formatWithSign', () => {
4
+ it('formats positive numbers with + sign', () => {
5
+ expect(formatWithSign(1.234)).toBe('+1');
6
+ });
7
+
8
+ it('formats negative numbers with unicode minus', () => {
9
+ expect(formatWithSign(-1.234)).toBe('−1');
10
+ });
11
+
12
+ it('returns plain zero without sign', () => {
13
+ expect(formatWithSign(0)).toBe('0');
14
+ });
15
+
16
+ it('respects decimals', () => {
17
+ expect(formatWithSign(1.234, 2)).toBe('+1.23');
18
+ expect(formatWithSign(-1.234, 2)).toBe('−1.23');
19
+ });
20
+
21
+ it('rounds values correctly', () => {
22
+ expect(formatWithSign(1.235, 2)).toBe('+1.24');
23
+ expect(formatWithSign(-1.235, 2)).toBe('−1.24');
24
+ });
25
+
26
+ it('handles NaN', () => {
27
+ expect(formatWithSign(NaN)).toBe('NaN');
28
+ });
29
+
30
+ it('handles Infinity', () => {
31
+ expect(formatWithSign(Infinity)).toBe('+Infinity');
32
+ expect(formatWithSign(-Infinity)).toBe('−Infinity');
33
+ });
34
+
35
+ it('handles very large numbers', () => {
36
+ expect(formatWithSign(1e20)).toBe('+100000000000000000000');
37
+ });
38
+
39
+ it('handles very small numbers', () => {
40
+ expect(formatWithSign(1e-10, 0)).toBe('+0');
41
+ });
42
+ });
43
+
44
+ describe('formatPercent', () => {
45
+ it('formats positive percentages without plus by default', () => {
46
+ expect(formatPercent(0.1234)).toBe('12%');
47
+ });
48
+
49
+ it('formats positive percentages with plus when flag is true', () => {
50
+ expect(formatPercent(0.1234, 2, true)).toBe('+12.34%');
51
+ });
52
+
53
+ it('formats negative percentages', () => {
54
+ expect(formatPercent(-0.1234)).toBe('−12%');
55
+ });
56
+
57
+ it('formats negative percentages with decimals', () => {
58
+ expect(formatPercent(-0.1234, 2)).toBe('−12.34%');
59
+ });
60
+
61
+ it('handles zero', () => {
62
+ expect(formatPercent(0)).toBe('0%');
63
+ });
64
+
65
+ it('handles zero with plus flag (still no sign)', () => {
66
+ expect(formatPercent(0, 0, true)).toBe('0%');
67
+ });
68
+
69
+ it('handles zero with fraction and plus flag (still no sign)', () => {
70
+ expect(formatPercent(0, 2, true)).toBe('0.00%');
71
+ });
72
+
73
+ it('respects decimals', () => {
74
+ expect(formatPercent(0.1234, 2)).toBe('12.34%');
75
+ });
76
+
77
+ it('multiplies by 100 correctly', () => {
78
+ expect(formatPercent(1)).toBe('100%');
79
+ expect(formatPercent(0.01)).toBe('1%');
80
+ });
81
+
82
+ it('rounds correctly', () => {
83
+ expect(formatPercent(0.01479, 2)).toBe('1.48%');
84
+ });
85
+
86
+ it('handles NaN', () => {
87
+ expect(formatPercent(NaN)).toBe('NaN%');
88
+ });
89
+
90
+ it('handles Infinity', () => {
91
+ expect(formatPercent(Infinity)).toBe('Infinity%');
92
+ expect(formatPercent(-Infinity)).toBe('−Infinity%');
93
+ });
94
+ });
@@ -0,0 +1,12 @@
1
+ export function formatWithSign(value: number, decimals = 0): string {
2
+ const rounded = value.toFixed(decimals);
3
+ if (isNaN(value)) return 'NaN';
4
+ if (value === 0) return rounded;
5
+ if (value > 0) return '+' + rounded;
6
+ return '−' + rounded.substring(1);
7
+ }
8
+
9
+ export function formatPercent(value: number, decimals = 0, plusPrefixForPositive = false): string {
10
+ if (!plusPrefixForPositive && value > 0) return (value * 100).toFixed(decimals) + '%';
11
+ return formatWithSign(value * 100, decimals) + '%';
12
+ }
@@ -0,0 +1 @@
1
+ export * from './objects.js';