ts-data-forge 6.11.0 → 6.13.0

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.
@@ -1,4 +1,5 @@
1
- import { type Decrement, type Increment, type Index, type Int, type Min, type NaNType, type NegativeIndex, type NonNegativeNumber, type NonZeroNumber, type PositiveNumber, type PositiveSafeIntWithSmallInt, type RelaxedExclude, type SmallInt, type SmallUint } from 'ts-type-forge';
1
+ import { type Decrement, type FiniteNumber, type Increment, type Index, type Int, type Min, type NaNType, type NegativeIndex, type NonNegativeNumber, type NonZeroNumber, type PositiveNumber, type PositiveSafeIntWithSmallInt, type RelaxedExclude, type SmallInt, type SmallUint } from 'ts-type-forge';
2
+ import { Result } from '../functional/index.mjs';
2
3
  import { type SmallPositiveInt } from '../types.mjs';
3
4
  /**
4
5
  * Namespace providing utility functions for number manipulation and validation.
@@ -33,6 +34,134 @@ export declare namespace Num {
33
34
  * @returns The numeric representation of `n`.
34
35
  */
35
36
  export const from: (n: unknown) => number;
37
+ /**
38
+ * Safely parses a base-10 integer from a string, returning a {@link Result}
39
+ * that is `Ok<Int>` for valid input and `Err<Error>` otherwise.
40
+ *
41
+ * This is a stricter alternative to both `parseInt` and `Number`:
42
+ *
43
+ * - Unlike `parseInt('12abc', 10)` (which returns `12`), trailing
44
+ * non-numeric characters make the whole input invalid and yield `Err`.
45
+ * - Unlike `Number('')` / `Number(' ')` (which return `0`), empty or
46
+ * whitespace-only input yields `Err`.
47
+ *
48
+ * The empty-string case is rejected by delegating to `parseInt` (which
49
+ * returns `NaN` there) rather than hard-coding a check, while the trailing-
50
+ * garbage case is rejected via `Number`. Valid input is truncated toward
51
+ * zero, so `'12.9'` becomes `12` and `'-3.5'` becomes `-3`.
52
+ *
53
+ * Only base 10 is supported. Use `Result.unwrapOk` (optionally with a
54
+ * `?? Number.NaN` fallback) or `Result.unwrapOkOr` to get a plain number
55
+ * back.
56
+ *
57
+ * @example
58
+ *
59
+ * ```ts
60
+ * assert.strictEqual(
61
+ * Result.unwrapOkOr(Num.safeParseInt('123'), Number.NaN),
62
+ * 123,
63
+ * );
64
+ *
65
+ * assert.strictEqual(
66
+ * Result.unwrapOkOr(Num.safeParseInt('12.9'), Number.NaN),
67
+ * 12,
68
+ * );
69
+ *
70
+ * assert.strictEqual(
71
+ * Result.unwrapOkOr(Num.safeParseInt('-12.9'), Number.NaN),
72
+ * -12,
73
+ * );
74
+ *
75
+ * assert.strictEqual(Number.parseInt('-12.9', 10), -12);
76
+ *
77
+ * // Native `parseInt` ignores trailing non-numeric characters
78
+ *
79
+ * assert.strictEqual(Number.parseInt('123abc', 10), 123);
80
+ *
81
+ * assert.isTrue(Number.isNaN(Number('123abc')));
82
+ *
83
+ * assert.isTrue(Result.isErr(Num.safeParseInt('123abc')));
84
+ *
85
+ * // Whitespace is not a valid integer, so we return an error instead of coercing to 0.
86
+ *
87
+ * assert.isTrue(Number.isNaN(Number.parseInt(' ', 10)));
88
+ *
89
+ * assert.strictEqual(Number(' '), 0); // Native `Number` coerces whitespace to 0
90
+ *
91
+ * assert.isTrue(Result.isErr(Num.safeParseInt('')));
92
+ *
93
+ * assert.strictEqual(Result.unwrapOk(Num.safeParseInt(' ')), undefined);
94
+ * ```
95
+ *
96
+ * @param s The string to parse.
97
+ * @returns `Result.ok(parsedInt)` for valid input, otherwise `Result.err`
98
+ * wrapping an `Error` describing the invalid input.
99
+ */
100
+ export const safeParseInt: (s: string) => Result<Int, Error>;
101
+ /**
102
+ * Safely parses a finite floating-point number from a string, returning a
103
+ * {@link Result} that is `Ok<FiniteNumber>` for valid input and `Err<Error>`
104
+ * otherwise.
105
+ *
106
+ * This is a stricter alternative to both `parseFloat` and `Number`:
107
+ *
108
+ * - Unlike `parseFloat('12abc')` (which returns `12`), trailing non-numeric
109
+ * characters make the whole input invalid and yield `Err`.
110
+ * - Unlike `Number('')` / `Number(' ')` (which return `0`), empty or
111
+ * whitespace-only input yields `Err`.
112
+ * - Unlike `Number('Infinity')` (which returns `Infinity`), non-finite values
113
+ * yield `Err`.
114
+ *
115
+ * The empty-string case is rejected by delegating to `parseFloat` (which
116
+ * returns `NaN` there) rather than hard-coding a check, while the trailing-
117
+ * garbage case is rejected via `Number`. Decimal values are preserved as-is,
118
+ * so `'12.9'` stays `12.9`.
119
+ *
120
+ * Use `Result.unwrapOk` (optionally with a `?? Number.NaN` fallback) or
121
+ * `Result.unwrapOkOr` to get a plain number back.
122
+ *
123
+ * @example
124
+ *
125
+ * ```ts
126
+ * assert.strictEqual(
127
+ * Result.unwrapOkOr(Num.safeParseFloat('12.9'), Number.NaN),
128
+ * 12.9,
129
+ * );
130
+ *
131
+ * assert.strictEqual(
132
+ * Result.unwrapOkOr(Num.safeParseFloat('-3.5'), Number.NaN),
133
+ * -3.5,
134
+ * );
135
+ *
136
+ * assert.strictEqual(
137
+ * Result.unwrapOkOr(Num.safeParseFloat('1e3'), Number.NaN),
138
+ * 1000,
139
+ * );
140
+ *
141
+ * // Native `parseFloat` ignores trailing non-numeric characters
142
+ *
143
+ * assert.strictEqual(Number.parseFloat('12px'), 12);
144
+ *
145
+ * assert.isTrue(Result.isErr(Num.safeParseFloat('12px')));
146
+ *
147
+ * // Whitespace is not a valid number, so we return an error instead of coercing to 0.
148
+ *
149
+ * assert.isTrue(Result.isErr(Num.safeParseFloat('')));
150
+ *
151
+ * assert.isTrue(Result.isErr(Num.safeParseFloat(' ')));
152
+ *
153
+ * // Infinity and NaN are not finite, so they are rejected.
154
+ *
155
+ * assert.isTrue(Result.isErr(Num.safeParseFloat('Infinity')));
156
+ *
157
+ * assert.isTrue(Result.isErr(Num.safeParseFloat('NaN')));
158
+ * ```
159
+ *
160
+ * @param s The string to parse.
161
+ * @returns `Result.ok(parsedFloat)` for valid finite input, otherwise
162
+ * `Result.err` wrapping an `Error` describing the invalid input.
163
+ */
164
+ export const safeParseFloat: (s: string) => Result<FiniteNumber, Error>;
36
165
  /**
37
166
  * Type guard that checks if a number is non-zero.
38
167
  *
@@ -1 +1 @@
1
- {"version":3,"file":"num.d.mts","sourceRoot":"","sources":["../../src/number/num.mts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,SAAS,EACd,KAAK,SAAS,EACd,KAAK,KAAK,EACV,KAAK,GAAG,EACR,KAAK,GAAG,EACR,KAAK,OAAO,EACZ,KAAK,aAAa,EAClB,KAAK,iBAAiB,EACtB,KAAK,aAAa,EAClB,KAAK,cAAc,EACnB,KAAK,2BAA2B,EAChC,KAAK,cAAc,EACnB,KAAK,QAAQ,EACb,KAAK,SAAS,EAEf,MAAM,eAAe,CAAC;AAEvB,OAAO,EAAE,KAAK,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAErD;;;;;;;;;;;;;;GAcG;AACH,yBAAiB,GAAG,CAAC;IACnB;;;;;;;;;;;;;;;OAeG;IACH,MAAM,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,EAAE,OAAO,KAAK,MAAe,CAAC;IAEnD;;;;;;;;;;;;;;;;;;;;;;;;;;OA0BG;IACH,MAAM,CAAC,MAAM,SAAS,GAAI,CAAC,SAAS,MAAM,EACxC,KAAK,CAAC,KACL,GAAG,IAAI,aAAa,GAAG,cAAc,CAAC,CAAC,EAAE,CAAC,CAAc,CAAC;IAI5D;;;;;;;;;;;;;;;;;;;;;;;;;OAyBG;IACH,MAAM,CAAC,MAAM,aAAa,GAAI,CAAC,SAAS,MAAM,EAC5C,KAAK,CAAC,KACL,GAAG,IAAI,iBAAiB,GAAG,cAAc,CAAC,CAAC,EAAE,aAAa,CAAC,IAAI,CAAC,CACzD,CAAC;IAEX;;;;;;;;;;;;;;;;;;;;;;OAsBG;IACH,MAAM,CAAC,MAAM,UAAU,GAAI,CAAC,SAAS,MAAM,EACzC,KAAK,CAAC,KACL,GAAG,IAAI,cAAc,GAAG,cAAc,CAAC,CAAC,EAAE,aAAa,CAAC,IAAI,CAAC,GAAG,CAAC,CAC3D,CAAC;IAEV;;;;;;;;;;;;;;;;;;OAkBG;IACH,MAAM,CAAC,MAAM,SAAS,GACnB,YAAY,MAAM,EAAE,YAAY,MAAM,MACtC,GAAG,MAAM,KAAG,OACsB,CAAC;IAEtC;;;;;;;;;;;;;;;;;;OAkBG;IACH,MAAM,CAAC,MAAM,kBAAkB,GAC5B,YAAY,MAAM,EAAE,YAAY,MAAM,MACtC,GAAG,MAAM,KAAG,OACuB,CAAC;IAEvC;;;;;;;;;OASG;IACH,KAAK,EAAE,GAAG,QAAQ,CAAC;SAAG,CAAC,IAAI,SAAS,GAAG,KAAK,CAAC,CAAC,CAAC;KAAE,CAAC,CAAC;IAEnD;;;;;;;;;OASG;IACH,KAAK,GAAG,GAAG,QAAQ,CAAC;SAAG,CAAC,IAAI,SAAS,GAAG,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC;KAAE,CAAC,CAAC;IAExD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA6BG;IACH,MAAM,CAAC,MAAM,aAAa,GACvB,CAAC,SAAS,SAAS,EAAE,CAAC,SAAS,SAAS,EAAE,YAAY,CAAC,EAAE,YAAY,CAAC,MACtE,GAAG,MAAM,KAAG,CAAC,IAAI,cAAc,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CACY,CAAC;IAEjE;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA4BG;IACH,MAAM,CAAC,MAAM,sBAAsB,GAChC,CAAC,SAAS,SAAS,EAAE,CAAC,SAAS,SAAS,EAAE,YAAY,CAAC,EAAE,YAAY,CAAC,MACtE,GAAG,MAAM,KAAG,CAAC,IAAI,cAAc,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CACY,CAAC;IAElE;;;;;;;;;;;;;;;;;;;;;;;OAuBG;IACH,MAAM,UAAU,KAAK,CACnB,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,GACjB,MAAM,CAAC;IAGV,MAAM,UAAU,KAAK,CACnB,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,GACjB,CAAC,MAAM,EAAE,MAAM,KAAK,MAAM,CAAC;IAyB9B;;;;;;;;;;OAUG;IACH,MAAM,CAAC,MAAM,GAAG,GAAI,GAAG,MAAM,EAAE,GAAG,aAAa,GAAG,QAAQ,CAAC,KAAK,CAAC,KAAG,MAE7D,CAAC;IAER;;;;;;;;;;;;OAYG;IACH,MAAM,CAAC,MAAM,MAAM,GACjB,GAAG,MAAM,EACT,GAAG,aAAa,GAAG,QAAQ,CAAC,KAAK,CAAC,KACjC,MAEwC,CAAC;IAE5C;;;;;;;;;OASG;IACH,MAAM,CAAC,MAAM,OAAO,GAClB,KAAK,MAAM,EACX,WAAW,2BAA2B,KACrC,MAKF,CAAC;IAEF;;;;;;;;;OASG;IAEH,MAAM,CAAC,MAAM,UAAU,GAAI,KAAK,MAAM,KAAG,GAAmC,CAAC;IAE7E;;;;;;;;;OASG;IACH,MAAM,CAAC,MAAM,KAAK,GAChB,OAAO,2BAA2B,KACjC,CAAC,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,CAM1B,CAAC;IAEF;;;;;;;;;;OAUG;IACH,MAAM,CAAC,MAAM,gBAAgB,GAAI,CAAC,SAAS,MAAM,EAC/C,KAAK,CAAC,KACL,cAAc,CAAC,CAAC,EAAE,OAAO,CAAC,GAAG,SAIS,CAAC;IAE1C;;;;;;;;;;OAUG;IACH,MAAM,CAAC,MAAM,SAAS,GAAI,CAAC,SAAS,SAAS,EAAE,GAAG,CAAC,KAAG,SAAS,CAAC,CAAC,CAExC,CAAC;IAE1B;;;;;;;;;;OAUG;IACH,MAAM,CAAC,MAAM,SAAS,GAAI,CAAC,SAAS,gBAAgB,EAAE,GAAG,CAAC,KAAG,SAAS,CAAC,CAAC,CAE/C,CAAC;;CAC3B"}
1
+ {"version":3,"file":"num.d.mts","sourceRoot":"","sources":["../../src/number/num.mts"],"names":[],"mappings":"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"}
@@ -1,3 +1,7 @@
1
+ import { err } from '../functional/result/impl/result-err.mjs';
2
+ import { ok } from '../functional/result/impl/result-ok.mjs';
3
+ import '@sindresorhus/is';
4
+
1
5
  /**
2
6
  * Namespace providing utility functions for number manipulation and validation.
3
7
  *
@@ -32,6 +36,155 @@ var Num;
32
36
  * @returns The numeric representation of `n`.
33
37
  */
34
38
  Num.from = Number;
39
+ /**
40
+ * Safely parses a base-10 integer from a string, returning a {@link Result}
41
+ * that is `Ok<Int>` for valid input and `Err<Error>` otherwise.
42
+ *
43
+ * This is a stricter alternative to both `parseInt` and `Number`:
44
+ *
45
+ * - Unlike `parseInt('12abc', 10)` (which returns `12`), trailing
46
+ * non-numeric characters make the whole input invalid and yield `Err`.
47
+ * - Unlike `Number('')` / `Number(' ')` (which return `0`), empty or
48
+ * whitespace-only input yields `Err`.
49
+ *
50
+ * The empty-string case is rejected by delegating to `parseInt` (which
51
+ * returns `NaN` there) rather than hard-coding a check, while the trailing-
52
+ * garbage case is rejected via `Number`. Valid input is truncated toward
53
+ * zero, so `'12.9'` becomes `12` and `'-3.5'` becomes `-3`.
54
+ *
55
+ * Only base 10 is supported. Use `Result.unwrapOk` (optionally with a
56
+ * `?? Number.NaN` fallback) or `Result.unwrapOkOr` to get a plain number
57
+ * back.
58
+ *
59
+ * @example
60
+ *
61
+ * ```ts
62
+ * assert.strictEqual(
63
+ * Result.unwrapOkOr(Num.safeParseInt('123'), Number.NaN),
64
+ * 123,
65
+ * );
66
+ *
67
+ * assert.strictEqual(
68
+ * Result.unwrapOkOr(Num.safeParseInt('12.9'), Number.NaN),
69
+ * 12,
70
+ * );
71
+ *
72
+ * assert.strictEqual(
73
+ * Result.unwrapOkOr(Num.safeParseInt('-12.9'), Number.NaN),
74
+ * -12,
75
+ * );
76
+ *
77
+ * assert.strictEqual(Number.parseInt('-12.9', 10), -12);
78
+ *
79
+ * // Native `parseInt` ignores trailing non-numeric characters
80
+ *
81
+ * assert.strictEqual(Number.parseInt('123abc', 10), 123);
82
+ *
83
+ * assert.isTrue(Number.isNaN(Number('123abc')));
84
+ *
85
+ * assert.isTrue(Result.isErr(Num.safeParseInt('123abc')));
86
+ *
87
+ * // Whitespace is not a valid integer, so we return an error instead of coercing to 0.
88
+ *
89
+ * assert.isTrue(Number.isNaN(Number.parseInt(' ', 10)));
90
+ *
91
+ * assert.strictEqual(Number(' '), 0); // Native `Number` coerces whitespace to 0
92
+ *
93
+ * assert.isTrue(Result.isErr(Num.safeParseInt('')));
94
+ *
95
+ * assert.strictEqual(Result.unwrapOk(Num.safeParseInt(' ')), undefined);
96
+ * ```
97
+ *
98
+ * @param s The string to parse.
99
+ * @returns `Result.ok(parsedInt)` for valid input, otherwise `Result.err`
100
+ * wrapping an `Error` describing the invalid input.
101
+ */
102
+ Num.safeParseInt = (s) => {
103
+ const viaNumber = Number(s);
104
+ // `Number('')` / `Number(' ')` は 0 を返すが、`parseInt` は NaN を返す。
105
+ // 末尾不正文字 ('12abc' 等) は `Number` 側が NaN にするので、両者が共に
106
+ // 有効な場合のみ採用することで空文字・空白のみ・末尾不正をまとめて弾く。
107
+ return Number.isNaN(viaNumber) || Number.isNaN(Number.parseInt(s, 10))
108
+ ? err(new Error(`safeParseInt: "${s}" is not a valid base-10 integer`))
109
+ : // eslint-disable-next-line total-functions/no-unsafe-type-assertion, ts-data-forge/prefer-as-int
110
+ ok(Math.trunc(viaNumber));
111
+ };
112
+ /**
113
+ * Safely parses a finite floating-point number from a string, returning a
114
+ * {@link Result} that is `Ok<FiniteNumber>` for valid input and `Err<Error>`
115
+ * otherwise.
116
+ *
117
+ * This is a stricter alternative to both `parseFloat` and `Number`:
118
+ *
119
+ * - Unlike `parseFloat('12abc')` (which returns `12`), trailing non-numeric
120
+ * characters make the whole input invalid and yield `Err`.
121
+ * - Unlike `Number('')` / `Number(' ')` (which return `0`), empty or
122
+ * whitespace-only input yields `Err`.
123
+ * - Unlike `Number('Infinity')` (which returns `Infinity`), non-finite values
124
+ * yield `Err`.
125
+ *
126
+ * The empty-string case is rejected by delegating to `parseFloat` (which
127
+ * returns `NaN` there) rather than hard-coding a check, while the trailing-
128
+ * garbage case is rejected via `Number`. Decimal values are preserved as-is,
129
+ * so `'12.9'` stays `12.9`.
130
+ *
131
+ * Use `Result.unwrapOk` (optionally with a `?? Number.NaN` fallback) or
132
+ * `Result.unwrapOkOr` to get a plain number back.
133
+ *
134
+ * @example
135
+ *
136
+ * ```ts
137
+ * assert.strictEqual(
138
+ * Result.unwrapOkOr(Num.safeParseFloat('12.9'), Number.NaN),
139
+ * 12.9,
140
+ * );
141
+ *
142
+ * assert.strictEqual(
143
+ * Result.unwrapOkOr(Num.safeParseFloat('-3.5'), Number.NaN),
144
+ * -3.5,
145
+ * );
146
+ *
147
+ * assert.strictEqual(
148
+ * Result.unwrapOkOr(Num.safeParseFloat('1e3'), Number.NaN),
149
+ * 1000,
150
+ * );
151
+ *
152
+ * // Native `parseFloat` ignores trailing non-numeric characters
153
+ *
154
+ * assert.strictEqual(Number.parseFloat('12px'), 12);
155
+ *
156
+ * assert.isTrue(Result.isErr(Num.safeParseFloat('12px')));
157
+ *
158
+ * // Whitespace is not a valid number, so we return an error instead of coercing to 0.
159
+ *
160
+ * assert.isTrue(Result.isErr(Num.safeParseFloat('')));
161
+ *
162
+ * assert.isTrue(Result.isErr(Num.safeParseFloat(' ')));
163
+ *
164
+ * // Infinity and NaN are not finite, so they are rejected.
165
+ *
166
+ * assert.isTrue(Result.isErr(Num.safeParseFloat('Infinity')));
167
+ *
168
+ * assert.isTrue(Result.isErr(Num.safeParseFloat('NaN')));
169
+ * ```
170
+ *
171
+ * @param s The string to parse.
172
+ * @returns `Result.ok(parsedFloat)` for valid finite input, otherwise
173
+ * `Result.err` wrapping an `Error` describing the invalid input.
174
+ */
175
+ Num.safeParseFloat = (s) => {
176
+ const viaNumber = Number(s);
177
+ // `Number('')` / `Number(' ')` は 0 を返すが、`parseFloat` は NaN を返す。
178
+ // 末尾不正文字 ('12abc' 等) は `Number` 側が NaN にするので、両者が共に
179
+ // 非 NaN かつ有限の場合のみ採用することで空文字・空白のみ・末尾不正・
180
+ // Infinity をまとめて弾く。
181
+ return Number.isNaN(viaNumber) ||
182
+ !Number.isFinite(viaNumber) ||
183
+ Number.isNaN(Number.parseFloat(s))
184
+ ? err(new Error(`safeParseFloat: "${s}" is not a valid finite number`))
185
+ : // eslint-disable-next-line total-functions/no-unsafe-type-assertion, ts-data-forge/prefer-as-int
186
+ ok(viaNumber);
187
+ };
35
188
  /**
36
189
  * Type guard that checks if a number is non-zero.
37
190
  *
@@ -1 +1 @@
1
- {"version":3,"file":"num.mjs","sources":["../../src/number/num.mts"],"sourcesContent":[null],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"num.mjs","sources":["../../src/number/num.mts"],"sourcesContent":[null],"names":["Result.err","Result.ok"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ts-data-forge",
3
- "version": "6.11.0",
3
+ "version": "6.13.0",
4
4
  "private": false,
5
5
  "keywords": [
6
6
  "typescript",
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  type Decrement,
3
+ type FiniteNumber,
3
4
  type Increment,
4
5
  type Index,
5
6
  type Int,
@@ -16,6 +17,7 @@ import {
16
17
  type UnknownBrand,
17
18
  } from 'ts-type-forge';
18
19
  import { expectType } from '../expect-type.mjs';
20
+ import { Result } from '../functional/index.mjs';
19
21
  import { type SmallPositiveInt } from '../types.mjs';
20
22
 
21
23
  /**
@@ -52,6 +54,163 @@ export namespace Num {
52
54
  */
53
55
  export const from: (n: unknown) => number = Number;
54
56
 
57
+ /**
58
+ * Safely parses a base-10 integer from a string, returning a {@link Result}
59
+ * that is `Ok<Int>` for valid input and `Err<Error>` otherwise.
60
+ *
61
+ * This is a stricter alternative to both `parseInt` and `Number`:
62
+ *
63
+ * - Unlike `parseInt('12abc', 10)` (which returns `12`), trailing
64
+ * non-numeric characters make the whole input invalid and yield `Err`.
65
+ * - Unlike `Number('')` / `Number(' ')` (which return `0`), empty or
66
+ * whitespace-only input yields `Err`.
67
+ *
68
+ * The empty-string case is rejected by delegating to `parseInt` (which
69
+ * returns `NaN` there) rather than hard-coding a check, while the trailing-
70
+ * garbage case is rejected via `Number`. Valid input is truncated toward
71
+ * zero, so `'12.9'` becomes `12` and `'-3.5'` becomes `-3`.
72
+ *
73
+ * Only base 10 is supported. Use `Result.unwrapOk` (optionally with a
74
+ * `?? Number.NaN` fallback) or `Result.unwrapOkOr` to get a plain number
75
+ * back.
76
+ *
77
+ * @example
78
+ *
79
+ * ```ts
80
+ * assert.strictEqual(
81
+ * Result.unwrapOkOr(Num.safeParseInt('123'), Number.NaN),
82
+ * 123,
83
+ * );
84
+ *
85
+ * assert.strictEqual(
86
+ * Result.unwrapOkOr(Num.safeParseInt('12.9'), Number.NaN),
87
+ * 12,
88
+ * );
89
+ *
90
+ * assert.strictEqual(
91
+ * Result.unwrapOkOr(Num.safeParseInt('-12.9'), Number.NaN),
92
+ * -12,
93
+ * );
94
+ *
95
+ * assert.strictEqual(Number.parseInt('-12.9', 10), -12);
96
+ *
97
+ * // Native `parseInt` ignores trailing non-numeric characters
98
+ *
99
+ * assert.strictEqual(Number.parseInt('123abc', 10), 123);
100
+ *
101
+ * assert.isTrue(Number.isNaN(Number('123abc')));
102
+ *
103
+ * assert.isTrue(Result.isErr(Num.safeParseInt('123abc')));
104
+ *
105
+ * // Whitespace is not a valid integer, so we return an error instead of coercing to 0.
106
+ *
107
+ * assert.isTrue(Number.isNaN(Number.parseInt(' ', 10)));
108
+ *
109
+ * assert.strictEqual(Number(' '), 0); // Native `Number` coerces whitespace to 0
110
+ *
111
+ * assert.isTrue(Result.isErr(Num.safeParseInt('')));
112
+ *
113
+ * assert.strictEqual(Result.unwrapOk(Num.safeParseInt(' ')), undefined);
114
+ * ```
115
+ *
116
+ * @param s The string to parse.
117
+ * @returns `Result.ok(parsedInt)` for valid input, otherwise `Result.err`
118
+ * wrapping an `Error` describing the invalid input.
119
+ */
120
+ export const safeParseInt = (s: string): Result<Int, Error> => {
121
+ const viaNumber = Number(s);
122
+
123
+ // `Number('')` / `Number(' ')` は 0 を返すが、`parseInt` は NaN を返す。
124
+ // 末尾不正文字 ('12abc' 等) は `Number` 側が NaN にするので、両者が共に
125
+ // 有効な場合のみ採用することで空文字・空白のみ・末尾不正をまとめて弾く。
126
+ return Number.isNaN(viaNumber) || Number.isNaN(Number.parseInt(s, 10))
127
+ ? Result.err(
128
+ new Error(`safeParseInt: "${s}" is not a valid base-10 integer`),
129
+ )
130
+ : // eslint-disable-next-line total-functions/no-unsafe-type-assertion, ts-data-forge/prefer-as-int
131
+ Result.ok(Math.trunc(viaNumber) as Int);
132
+ };
133
+
134
+ /**
135
+ * Safely parses a finite floating-point number from a string, returning a
136
+ * {@link Result} that is `Ok<FiniteNumber>` for valid input and `Err<Error>`
137
+ * otherwise.
138
+ *
139
+ * This is a stricter alternative to both `parseFloat` and `Number`:
140
+ *
141
+ * - Unlike `parseFloat('12abc')` (which returns `12`), trailing non-numeric
142
+ * characters make the whole input invalid and yield `Err`.
143
+ * - Unlike `Number('')` / `Number(' ')` (which return `0`), empty or
144
+ * whitespace-only input yields `Err`.
145
+ * - Unlike `Number('Infinity')` (which returns `Infinity`), non-finite values
146
+ * yield `Err`.
147
+ *
148
+ * The empty-string case is rejected by delegating to `parseFloat` (which
149
+ * returns `NaN` there) rather than hard-coding a check, while the trailing-
150
+ * garbage case is rejected via `Number`. Decimal values are preserved as-is,
151
+ * so `'12.9'` stays `12.9`.
152
+ *
153
+ * Use `Result.unwrapOk` (optionally with a `?? Number.NaN` fallback) or
154
+ * `Result.unwrapOkOr` to get a plain number back.
155
+ *
156
+ * @example
157
+ *
158
+ * ```ts
159
+ * assert.strictEqual(
160
+ * Result.unwrapOkOr(Num.safeParseFloat('12.9'), Number.NaN),
161
+ * 12.9,
162
+ * );
163
+ *
164
+ * assert.strictEqual(
165
+ * Result.unwrapOkOr(Num.safeParseFloat('-3.5'), Number.NaN),
166
+ * -3.5,
167
+ * );
168
+ *
169
+ * assert.strictEqual(
170
+ * Result.unwrapOkOr(Num.safeParseFloat('1e3'), Number.NaN),
171
+ * 1000,
172
+ * );
173
+ *
174
+ * // Native `parseFloat` ignores trailing non-numeric characters
175
+ *
176
+ * assert.strictEqual(Number.parseFloat('12px'), 12);
177
+ *
178
+ * assert.isTrue(Result.isErr(Num.safeParseFloat('12px')));
179
+ *
180
+ * // Whitespace is not a valid number, so we return an error instead of coercing to 0.
181
+ *
182
+ * assert.isTrue(Result.isErr(Num.safeParseFloat('')));
183
+ *
184
+ * assert.isTrue(Result.isErr(Num.safeParseFloat(' ')));
185
+ *
186
+ * // Infinity and NaN are not finite, so they are rejected.
187
+ *
188
+ * assert.isTrue(Result.isErr(Num.safeParseFloat('Infinity')));
189
+ *
190
+ * assert.isTrue(Result.isErr(Num.safeParseFloat('NaN')));
191
+ * ```
192
+ *
193
+ * @param s The string to parse.
194
+ * @returns `Result.ok(parsedFloat)` for valid finite input, otherwise
195
+ * `Result.err` wrapping an `Error` describing the invalid input.
196
+ */
197
+ export const safeParseFloat = (s: string): Result<FiniteNumber, Error> => {
198
+ const viaNumber = Number(s);
199
+
200
+ // `Number('')` / `Number(' ')` は 0 を返すが、`parseFloat` は NaN を返す。
201
+ // 末尾不正文字 ('12abc' 等) は `Number` 側が NaN にするので、両者が共に
202
+ // 非 NaN かつ有限の場合のみ採用することで空文字・空白のみ・末尾不正・
203
+ // Infinity をまとめて弾く。
204
+ return Number.isNaN(viaNumber) ||
205
+ !Number.isFinite(viaNumber) ||
206
+ Number.isNaN(Number.parseFloat(s))
207
+ ? Result.err(
208
+ new Error(`safeParseFloat: "${s}" is not a valid finite number`),
209
+ )
210
+ : // eslint-disable-next-line total-functions/no-unsafe-type-assertion, ts-data-forge/prefer-as-int
211
+ Result.ok(viaNumber as FiniteNumber);
212
+ };
213
+
55
214
  /**
56
215
  * Type guard that checks if a number is non-zero.
57
216
  *
@@ -1,6 +1,6 @@
1
1
  import { type NonZeroNumber } from 'ts-type-forge';
2
2
  import { expectType } from '../expect-type.mjs';
3
- import { pipe } from '../functional/index.mjs';
3
+ import { pipe, Result } from '../functional/index.mjs';
4
4
  import { asNonZeroFiniteNumber } from './branded-types/index.mjs';
5
5
  import { Num } from './num.mjs';
6
6
 
@@ -132,6 +132,126 @@ describe('Num test', () => {
132
132
  });
133
133
  });
134
134
 
135
+ describe('safeParseInt', () => {
136
+ test('parses valid integer strings into Ok', () => {
137
+ expect(Result.unwrapOk(Num.safeParseInt('123'))).toBe(123);
138
+
139
+ expect(Result.unwrapOk(Num.safeParseInt('-42'))).toBe(-42);
140
+
141
+ expect(Result.unwrapOk(Num.safeParseInt('+7'))).toBe(7);
142
+
143
+ expect(Result.unwrapOk(Num.safeParseInt('0'))).toBe(0);
144
+
145
+ expect(Result.unwrapOk(Num.safeParseInt(' 12 '))).toBe(12);
146
+ });
147
+
148
+ test('truncates non-integer numeric strings toward zero', () => {
149
+ expect(Result.unwrapOk(Num.safeParseInt('12.9'))).toBe(12);
150
+
151
+ expect(Result.unwrapOk(Num.safeParseInt('-3.5'))).toBe(-3);
152
+
153
+ expect(Result.unwrapOk(Num.safeParseInt('1e3'))).toBe(1000);
154
+ });
155
+
156
+ test('rejects trailing non-numeric characters (unlike parseInt)', () => {
157
+ assert.isTrue(Result.isErr(Num.safeParseInt('123abc')));
158
+
159
+ assert.isTrue(Result.isErr(Num.safeParseInt('12px')));
160
+
161
+ assert.isTrue(Result.isErr(Num.safeParseInt('abc')));
162
+ });
163
+
164
+ test('rejects empty / whitespace-only input (unlike Number)', () => {
165
+ assert.isTrue(Result.isErr(Num.safeParseInt('')));
166
+
167
+ assert.isTrue(Result.isErr(Num.safeParseInt(' ')));
168
+ });
169
+
170
+ test('rejects non-finite words', () => {
171
+ assert.isTrue(Result.isErr(Num.safeParseInt('Infinity')));
172
+
173
+ assert.isTrue(Result.isErr(Num.safeParseInt('NaN')));
174
+ });
175
+
176
+ test('the Err carries a descriptive Error', () => {
177
+ const result = Num.safeParseInt('nope');
178
+
179
+ assert.isTrue(Result.isErr(result));
180
+
181
+ if (Result.isErr(result)) {
182
+ expect(Result.unwrapErr(result)).toBeInstanceOf(Error);
183
+ }
184
+ });
185
+
186
+ test('composes with Result.unwrapOk + nullish fallback', () => {
187
+ expect(Result.unwrapOk(Num.safeParseInt('42')) ?? Number.NaN).toBe(42);
188
+
189
+ expect(Result.unwrapOk(Num.safeParseInt('')) ?? Number.NaN).toBeNaN();
190
+ });
191
+ });
192
+
193
+ describe('safeParseFloat', () => {
194
+ test('parses valid numeric strings into Ok', () => {
195
+ expect(Result.unwrapOk(Num.safeParseFloat('123'))).toBe(123);
196
+
197
+ expect(Result.unwrapOk(Num.safeParseFloat('-42'))).toBe(-42);
198
+
199
+ expect(Result.unwrapOk(Num.safeParseFloat('+7'))).toBe(7);
200
+
201
+ expect(Result.unwrapOk(Num.safeParseFloat('0'))).toBe(0);
202
+
203
+ expect(Result.unwrapOk(Num.safeParseFloat(' 12 '))).toBe(12);
204
+ });
205
+
206
+ test('preserves decimal values', () => {
207
+ expect(Result.unwrapOk(Num.safeParseFloat('12.9'))).toBe(12.9);
208
+
209
+ expect(Result.unwrapOk(Num.safeParseFloat('-3.5'))).toBe(-3.5);
210
+
211
+ expect(Result.unwrapOk(Num.safeParseFloat('1e3'))).toBe(1000);
212
+ });
213
+
214
+ test('rejects trailing non-numeric characters (unlike parseFloat)', () => {
215
+ assert.isTrue(Result.isErr(Num.safeParseFloat('123abc')));
216
+
217
+ assert.isTrue(Result.isErr(Num.safeParseFloat('12px')));
218
+
219
+ assert.isTrue(Result.isErr(Num.safeParseFloat('abc')));
220
+ });
221
+
222
+ test('rejects empty / whitespace-only input (unlike Number)', () => {
223
+ assert.isTrue(Result.isErr(Num.safeParseFloat('')));
224
+
225
+ assert.isTrue(Result.isErr(Num.safeParseFloat(' ')));
226
+ });
227
+
228
+ test('rejects non-finite values', () => {
229
+ assert.isTrue(Result.isErr(Num.safeParseFloat('Infinity')));
230
+
231
+ assert.isTrue(Result.isErr(Num.safeParseFloat('-Infinity')));
232
+
233
+ assert.isTrue(Result.isErr(Num.safeParseFloat('NaN')));
234
+ });
235
+
236
+ test('the Err carries a descriptive Error', () => {
237
+ const result = Num.safeParseFloat('nope');
238
+
239
+ assert.isTrue(Result.isErr(result));
240
+
241
+ if (Result.isErr(result)) {
242
+ expect(Result.unwrapErr(result)).toBeInstanceOf(Error);
243
+ }
244
+ });
245
+
246
+ test('composes with Result.unwrapOk + nullish fallback', () => {
247
+ expect(Result.unwrapOk(Num.safeParseFloat('3.14')) ?? Number.NaN).toBe(
248
+ 3.14,
249
+ );
250
+
251
+ expect(Result.unwrapOk(Num.safeParseFloat('')) ?? Number.NaN).toBeNaN();
252
+ });
253
+ });
254
+
135
255
  describe('isInRange', () => {
136
256
  test('checks range (lower inclusive, upper exclusive)', () => {
137
257
  const inRange = Num.isInRange(0, 10);