Skip to content

Commit 89108b1

Browse files
committed
Add minDigits
1 parent b005d70 commit 89108b1

File tree

33 files changed

+718
-0
lines changed

33 files changed

+718
-0
lines changed

library/src/actions/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ export * from './metadata/index.ts';
5555
export * from './mimeType/index.ts';
5656
export * from './minBytes/index.ts';
5757
export * from './minEntries/index.ts';
58+
export * from './minDigitChars/index.ts';
5859
export * from './minGraphemes/index.ts';
5960
export * from './minLength/index.ts';
6061
export * from './minSize/index.ts';
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './minDigitChars.ts';
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { describe, expectTypeOf, test } from 'vitest';
2+
import type { InferInput, InferIssue, InferOutput } from '../../types/index.ts';
3+
import {
4+
minDigitChars,
5+
type MinDigitCharsAction,
6+
type MinDigitCharsIssue,
7+
} from './minDigitChars.ts';
8+
9+
describe('minDigitChars', () => {
10+
describe('should return action object', () => {
11+
test('with undefined message', () => {
12+
type Action = MinDigitCharsAction<string, 10, undefined>;
13+
expectTypeOf(minDigitChars<string, 10>(10)).toEqualTypeOf<Action>();
14+
expectTypeOf(
15+
minDigitChars<string, 10, undefined>(10, undefined)
16+
).toEqualTypeOf<Action>();
17+
});
18+
19+
test('with string message', () => {
20+
expectTypeOf(
21+
minDigitChars<string, 10, 'message'>(10, 'message')
22+
).toEqualTypeOf<MinDigitCharsAction<string, 10, 'message'>>();
23+
});
24+
25+
test('with function message', () => {
26+
expectTypeOf(
27+
minDigitChars<string, 10, () => string>(10, () => 'message')
28+
).toEqualTypeOf<MinDigitCharsAction<string, 10, () => string>>();
29+
});
30+
});
31+
32+
describe('should infer correct types', () => {
33+
type Input = 'example string';
34+
type Action = MinDigitCharsAction<Input, 10, undefined>;
35+
36+
test('of input', () => {
37+
expectTypeOf<InferInput<Action>>().toEqualTypeOf<Input>();
38+
});
39+
40+
test('of output', () => {
41+
expectTypeOf<InferOutput<Action>>().toEqualTypeOf<Input>();
42+
});
43+
44+
test('of issue', () => {
45+
expectTypeOf<InferIssue<Action>>().toEqualTypeOf<
46+
MinDigitCharsIssue<Input, 10>
47+
>();
48+
});
49+
});
50+
});
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { describe, expect, test } from 'vitest';
2+
import type { StringIssue } from '../../schemas/index.ts';
3+
import { _getDigitCount } from '../../utils/index.ts';
4+
import { expectActionIssue, expectNoActionIssue } from '../../vitest/index.ts';
5+
import {
6+
minDigitChars,
7+
type MinDigitCharsAction,
8+
type MinDigitCharsIssue,
9+
} from './minDigitChars.ts';
10+
11+
describe('minDigitChars', () => {
12+
describe('should return action object', () => {
13+
const baseAction: Omit<MinDigitCharsAction<string, 5, never>, 'message'> = {
14+
kind: 'validation',
15+
type: 'min_digits',
16+
reference: minDigitChars,
17+
expects: '>=5',
18+
requirement: 5,
19+
async: false,
20+
'~run': expect.any(Function),
21+
};
22+
23+
test('with undefined message', () => {
24+
const action: MinDigitCharsAction<string, 5, undefined> = {
25+
...baseAction,
26+
message: undefined,
27+
};
28+
expect(minDigitChars(5)).toStrictEqual(action);
29+
expect(minDigitChars(5, undefined)).toStrictEqual(action);
30+
});
31+
32+
test('with string message', () => {
33+
expect(minDigitChars(5, 'message')).toStrictEqual({
34+
...baseAction,
35+
message: 'message',
36+
} satisfies MinDigitCharsAction<string, 5, string>);
37+
});
38+
39+
test('with function message', () => {
40+
const message = () => 'message';
41+
expect(minDigitChars(5, message)).toStrictEqual({
42+
...baseAction,
43+
message,
44+
} satisfies MinDigitCharsAction<string, 5, typeof message>);
45+
});
46+
});
47+
48+
describe('should return dataset without issues', () => {
49+
const action = minDigitChars(5);
50+
51+
test('for untyped inputs', () => {
52+
const issues: [StringIssue] = [
53+
{
54+
kind: 'schema',
55+
type: 'string',
56+
input: null,
57+
expected: 'string',
58+
received: 'null',
59+
message: 'message',
60+
},
61+
];
62+
expect(
63+
action['~run']({ typed: false, value: null, issues }, {})
64+
).toStrictEqual({
65+
typed: false,
66+
value: null,
67+
issues,
68+
});
69+
});
70+
71+
test('for valid strings', () => {
72+
expectNoActionIssue(action, ['12345', '123456', '45foobarbaz123']);
73+
});
74+
});
75+
76+
describe('should return dataset with issues', () => {
77+
const action = minDigitChars(5, 'message');
78+
const baseIssue: Omit<
79+
MinDigitCharsIssue<string, 5>,
80+
'input' | 'received'
81+
> = {
82+
kind: 'validation',
83+
type: 'min_digits',
84+
expected: '>=5',
85+
message: 'message',
86+
requirement: 5,
87+
};
88+
89+
test('for invalid strings', () => {
90+
expectActionIssue(
91+
action,
92+
baseIssue,
93+
['', 'foo', 'abc1234'],
94+
(value) => `${_getDigitCount(value)}`
95+
);
96+
});
97+
});
98+
});
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import type {
2+
BaseIssue,
3+
BaseValidation,
4+
ErrorMessage,
5+
} from '../../types/index.ts';
6+
import { _addIssue, _getDigitCount } from '../../utils/index.ts';
7+
8+
/**
9+
* Min digits issue interface.
10+
*/
11+
export interface MinDigitCharsIssue<
12+
TInput extends string,
13+
TRequirement extends number,
14+
> extends BaseIssue<TInput> {
15+
/**
16+
* The issue kind.
17+
*/
18+
readonly kind: 'validation';
19+
/**
20+
* The issue type.
21+
*/
22+
readonly type: 'min_digits';
23+
/**
24+
* The expected property.
25+
*/
26+
readonly expected: `>=${TRequirement}`;
27+
/**
28+
* The received property.
29+
*/
30+
readonly received: `${number}`;
31+
/**
32+
* The minimum digits.
33+
*/
34+
readonly requirement: TRequirement;
35+
}
36+
37+
/**
38+
* Min digits action interface.
39+
*/
40+
export interface MinDigitCharsAction<
41+
TInput extends string,
42+
TRequirement extends number,
43+
TMessage extends
44+
| ErrorMessage<MinDigitCharsIssue<TInput, TRequirement>>
45+
| undefined,
46+
> extends BaseValidation<
47+
TInput,
48+
TInput,
49+
MinDigitCharsIssue<TInput, TRequirement>
50+
> {
51+
/**
52+
* The action type.
53+
*/
54+
readonly type: 'min_digits';
55+
/**
56+
* The action reference.
57+
*/
58+
readonly reference: typeof minDigitChars;
59+
/**
60+
* The expected property.
61+
*/
62+
readonly expects: `>=${TRequirement}`;
63+
/**
64+
* The minimum digits.
65+
*/
66+
readonly requirement: TRequirement;
67+
/**
68+
* The error message.
69+
*/
70+
readonly message: TMessage;
71+
}
72+
73+
/**
74+
* Creates a min digits validation action.
75+
*
76+
* @param requirement The minimum digits.
77+
*
78+
* @returns A min digits action.
79+
*/
80+
export function minDigitChars<
81+
TInput extends string,
82+
const TRequirement extends number,
83+
>(
84+
requirement: TRequirement
85+
): MinDigitCharsAction<TInput, TRequirement, undefined>;
86+
87+
/**
88+
* Creates a min digits validation action.
89+
*
90+
* @param requirement The minimum digits.
91+
* @param message The error message.
92+
*
93+
* @returns A min digits action.
94+
*/
95+
export function minDigitChars<
96+
TInput extends string,
97+
const TRequirement extends number,
98+
const TMessage extends
99+
| ErrorMessage<MinDigitCharsIssue<TInput, TRequirement>>
100+
| undefined,
101+
>(
102+
requirement: TRequirement,
103+
message: TMessage
104+
): MinDigitCharsAction<TInput, TRequirement, TMessage>;
105+
106+
// @__NO_SIDE_EFFECTS__
107+
export function minDigitChars(
108+
requirement: number,
109+
message?: ErrorMessage<MinDigitCharsIssue<string, number>>
110+
): MinDigitCharsAction<
111+
string,
112+
number,
113+
ErrorMessage<MinDigitCharsIssue<string, number>> | undefined
114+
> {
115+
return {
116+
kind: 'validation',
117+
type: 'min_digits',
118+
reference: minDigitChars,
119+
async: false,
120+
expects: `>=${requirement}`,
121+
requirement,
122+
message,
123+
'~run'(dataset, config) {
124+
if (dataset.typed) {
125+
const count = _getDigitCount(dataset.value);
126+
if (count < this.requirement) {
127+
_addIssue(this, 'digits', dataset, config, {
128+
received: `${count}`,
129+
});
130+
}
131+
}
132+
return dataset;
133+
},
134+
};
135+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { describe, expect, test } from 'vitest';
2+
import { _getDigitCount } from './_getDigitCount.ts';
3+
4+
describe('_getDigitCount', () => {
5+
test('should return digit count', () => {
6+
expect(_getDigitCount('')).toBe(0);
7+
expect(_getDigitCount('hello world')).toBe(0);
8+
expect(_getDigitCount('he22o world')).toBe(2);
9+
expect(_getDigitCount('hello world111')).toBe(3);
10+
expect(_getDigitCount('111hello world111')).toBe(6);
11+
expect(_getDigitCount('123')).toBe(3);
12+
expect(_getDigitCount('😀1')).toBe(1);
13+
});
14+
});
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/**
2+
* Returns the digit count of the input.
3+
*
4+
* @param input The input to be measured.
5+
*
6+
* @returns The digit count.
7+
*
8+
* @internal
9+
*/
10+
// @__NO_SIDE_EFFECTS__
11+
export function _getDigitCount(input: string): number {
12+
return (input.match(/\d/gu) || []).length;
13+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './_getDigitCount.ts';

library/src/utils/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export * from './_addIssue/index.ts';
22
export * from './_getByteCount/index.ts';
3+
export * from './_getDigitCount/index.ts';
34
export * from './_getGraphemeCount/index.ts';
45
export * from './_getStandardProps/index.ts';
56
export * from './_getWordCount/index.ts';

0 commit comments

Comments
 (0)