Skip to content
Merged

Dev #30

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
## Changelog

### [v5.19.1](https://github.com/panates/valgen/compare/v5.19.0...v5.19.1) -
### [v5.19.2](https://github.com/panates/valgen/compare/v5.19.1...v5.19.2) -

#### 🛠 Refactoring and Updates

- refactor: Refactored isDateString options @Eray Hanoğlu

### [v5.19.1](https://github.com/panates/valgen/compare/v5.19.0...v5.19.1) - 19 January 2026

#### 🚀 New Features

Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "valgen",
"description": "Fast runtime type validator, converter and io (encoding/decoding) library",
"version": "5.19.1",
"version": "5.19.2",
"author": "Panates",
"license": "MIT",
"private": true,
Expand Down
3 changes: 2 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import * as vg from './rules/index.js';

export * from './constants.js';
export * from './core/index.js';
export type { DatePrecision } from './rules/type-rules/is-date.js';
export type { IsObject } from './rules/type-rules/is-object.js';

const isAlpha = vg.isAlpha();
Expand Down Expand Up @@ -71,7 +72,7 @@ const toDateString = (
const trim = options?.trim ?? 'seconds';
let validator = toDateStringValidators.get(trim);
if (!validator) {
validator = vg.isDateString({ coerce: true, trim });
validator = vg.isDateString({ coerce: true, precisionMax: trim });
toDateStringValidators.set(trim, validator);
}
return validator(input, options, ctx);
Expand Down
58 changes: 34 additions & 24 deletions src/rules/type-rules/is-date.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export function isDate(options?: IsDateOptions) {
export interface IsDateStringOptions extends ValidationOptions {
precisionMin?: DatePrecision;
precisionMax?: DatePrecision;
trim?: DatePrecision;
trim?: boolean;
timeZone?: boolean | number;
}

Expand All @@ -74,38 +74,46 @@ export interface IsDateStringOptions extends ValidationOptions {
* @validator isDateString
*/
export function isDateString(options?: IsDateStringOptions) {
const trimIdx = PRECISION_INDEX[options?.trim || 'tz'] || 9;
const precisionMin = options?.precisionMin || options?.trim || 'minutes';
const trim = options?.trim;
const precisionMax = options?.precisionMax || 'tz';
const precisionMaxIdx = PRECISION_INDEX[precisionMax] || 9;
const precisionMaxIdx = PRECISION_INDEX[precisionMax] || 8;
let precisionMin = options?.precisionMin || 'minutes';
const precisionMinIdx = Math.min(
trimIdx,
precisionMaxIdx,
PRECISION_INDEX[precisionMin] || 6,
);
precisionMin = (PRECISION_INDEX_KEYS[
PRECISION_INDEX_VALUES.indexOf(precisionMinIdx)
] || precisionMin) as any;
return validator<string, Date | number | string>(
'isDateString',
(input: any, context: Context, _this): Nullish<string> => {
const coerce = options?.coerce ?? context.coerce;
const parsed = coerceDateString(input, options?.trim);
const parsed = coerceDateString(input, trim ? precisionMax : undefined);
if (parsed) {
if (
parsed.precision >= precisionMinIdx &&
parsed.precision <= precisionMaxIdx
) {
return coerce ? parsed.value : input;
if (parsed.precision < precisionMinIdx) {
context.fail(
_this,
`Minimum date precision should be ${precisionMin}`,
input,
options,
);
}
if (parsed.precision > precisionMaxIdx) {
context.fail(
_this,
`Maximum date precision should be ${precisionMax}`,
input,
options,
);
}
return coerce ? parsed.value : input;
}
context.fail(
_this,
`Value is not valid date string` +
(options?.precisionMin || options?.precisionMax
? ` with required precision`
: ''),
`Value "${input}" is not a valid date string`,
input,
{
...options,
},
options,
);
},
options,
Expand All @@ -120,12 +128,13 @@ const DATE_PATTERN2 =

function coerceDateString(
input: any,
precision?: DatePrecision,
trimPrecision?: DatePrecision,
): Nullish<{
value: string;
precision: number;
}> {
const precisionIndex = (precision ? PRECISION_INDEX[precision] : 9) || 9;
const precisionIndex =
(trimPrecision ? PRECISION_INDEX[trimPrecision] : 9) || 9;
let dateParts: (string | undefined)[] | undefined;
let detectedPrecision = 0;
if (input instanceof Date || typeof input === 'number') {
Expand Down Expand Up @@ -174,9 +183,8 @@ function coerceDateString(
}
}
if (!dateParts) return;
detectedPrecision = dateParts.findIndex(v => !v);
if (detectedPrecision === -1)
detectedPrecision = Math.min(dateParts.length, 8);
detectedPrecision = dateParts[7] ? 8 : dateParts.findIndex(v => !v);
if (detectedPrecision < 0) detectedPrecision = Math.min(dateParts.length, 8);
let value = dateParts[0] || '0000';
if (precisionIndex > 1) value += '-' + (dateParts[1] || '01');
if (precisionIndex > 2) value += '-' + (dateParts[2] || '01');
Expand All @@ -187,7 +195,7 @@ function coerceDateString(
if (precisionIndex > 7) value += dateParts[7] || '';
return {
value,
precision: detectedPrecision || 8,
precision: Math.min(precisionIndex, detectedPrecision),
};
}

Expand All @@ -208,6 +216,8 @@ const PRECISION_INDEX: Record<DatePrecision, number> = {
ms: 7,
tz: 8,
};
const PRECISION_INDEX_KEYS = Object.keys(PRECISION_INDEX);
const PRECISION_INDEX_VALUES = Object.values(PRECISION_INDEX);

function setPrecision(d: Date, precision?: string) {
switch (precision) {
Expand Down
124 changes: 45 additions & 79 deletions test/type-rules/is-date-string.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,37 +24,23 @@ describe('isDateString', () => {
expect(isDateString('2020-01-10T08:30')).toEqual('2020-01-10T08:30');
expect(isDateString('2020-01-10 08:30')).toEqual('2020-01-10 08:30');

expect(() => isDateString('2020-01-10T08')).toThrow(
'Value is not valid date string',
);
expect(() => isDateString('2020-01-10')).toThrow(
'Value is not valid date string',
);
expect(() => isDateString(undefined as any)).toThrow(
'Value is not valid date string',
);
expect(() => isDateString(null as any)).toThrow(
'Value is not valid date string',
);
expect(() => isDateString('invalid')).toThrow(
'Value is not valid date string',
);
expect(() => isDateString('2020-01-10T08')).toThrow();
expect(() => isDateString('2020-01-10')).toThrow();
expect(() => isDateString(undefined as any)).toThrow();
expect(() => isDateString(null as any)).toThrow();
expect(() => isDateString('invalid')).toThrow();
});

it('should validate date string with precisionMin', () => {
expect(vg.isDateString({ precisionMin: 'day' })('2020-11-01')).toEqual(
'2020-11-01',
);
expect(() => vg.isDateString({ precisionMin: 'day' })('2020')).toThrow(
'Value is not valid date string with required precision',
);
expect(() => vg.isDateString({ precisionMin: 'day' })('2020')).toThrow();

expect(vg.isDateString({ precisionMin: 'month' })('2020-11')).toEqual(
'2020-11',
);
expect(() => vg.isDateString({ precisionMin: 'month' })('2020')).toThrow(
'Value is not valid date string with required precision',
);
expect(() => vg.isDateString({ precisionMin: 'month' })('2020')).toThrow();
});

it('should validate date string with precisionMax', () => {
Expand All @@ -63,21 +49,23 @@ describe('isDateString', () => {
);
expect(() =>
vg.isDateString({ precisionMax: 'day' })('2020-01-01T13:30'),
).toThrow('Value is not valid date string with required precision');
).toThrow();
});

it('should coerce to date with given precision', () => {
expect(
vg.isDateString({
coerce: true,
trim: 'min',
trim: true,
precisionMax: 'min',
precisionMin: 'yr',
})('2020-11-01'),
).toEqual('2020-11-01T00:00');
expect(
vg.isDateString({
coerce: true,
trim: 'ms',
trim: true,
precisionMax: 'ms',
precisionMin: 'yr',
})(new Date('2020-11-01T00:00:00.1')),
).toEqual('2020-11-01T00:00:00.100');
Expand All @@ -86,100 +74,78 @@ describe('isDateString', () => {
).toEqual('2020-11-01T00:00:00');
});

it('should trim to given precision', () => {
it('should trim date string to given precision', () => {
expect(
vg.isDateString({ coerce: true, trim: 'yr' })(
vg.isDateString({ coerce: true, precisionMax: 'yr', trim: true })(
'2020-11-01T00:00:00+03:00',
),
).toEqual('2020');
expect(
vg.isDateString({ coerce: true, trim: 'month' })(
vg.isDateString({ coerce: true, precisionMax: 'month', trim: true })(
'2020-11-01T00:00:00+03:00',
),
).toEqual('2020-11');
expect(
vg.isDateString({ coerce: true, trim: 'day' })('2020-11-01'),
vg.isDateString({ coerce: true, precisionMax: 'day', trim: true })(
'2020-11-01',
),
).toEqual('2020-11-01');
expect(
vg.isDateString({ coerce: true, trim: 'hours' })(
vg.isDateString({ coerce: true, precisionMax: 'hours', trim: true })(
'2020-11-01T10:23:45+03:00',
),
).toEqual('2020-11-01T10');
expect(
vg.isDateString({ coerce: true, trim: 'minutes' })(
vg.isDateString({ coerce: true, precisionMax: 'minutes', trim: true })(
'2020-11-01T10:23:45+03:00',
),
).toEqual('2020-11-01T10:23');
expect(
vg.isDateString({ coerce: true, trim: 'seconds' })(
vg.isDateString({ coerce: true, precisionMax: 'seconds', trim: true })(
'2020-11-01T10:23:45+03:00',
),
).toEqual('2020-11-01T10:23:45');
expect(
vg.isDateString({ coerce: true, trim: 'ms' })(
vg.isDateString({ coerce: true, precisionMax: 'ms', trim: true })(
'2020-11-01T10:23:45.123+03:00',
),
).toEqual('2020-11-01T10:23:45.123');
expect(
vg.isDateString({ coerce: true, trim: 'tz' })(
vg.isDateString({ coerce: true, precisionMax: 'tz', trim: true })(
'2020-11-01T10:23:45.123+03:00',
),
).toEqual('2020-11-01T10:23:45.123' + tz);
});

it('should coerce Date to date string', () => {
it('should trim Date to given precision', () => {
const d = new Date('2020-11-01T10:23:45.123');
expect(vg.isDateString({ coerce: true, trim: 'yr' })(d)).toEqual('2020');
expect(vg.isDateString({ coerce: true, trim: 'month' })(d)).toEqual(
'2020-11',
);
expect(vg.isDateString({ coerce: true, trim: 'day' })(d)).toEqual(
'2020-11-01',
);
expect(vg.isDateString({ coerce: true, trim: 'hours' })(d)).toEqual(
'2020-11-01T10',
);
expect(vg.isDateString({ coerce: true, trim: 'minutes' })(d)).toEqual(
'2020-11-01T10:23',
);
expect(vg.isDateString({ coerce: true, trim: 'seconds' })(d)).toEqual(
'2020-11-01T10:23:45',
);
expect(vg.isDateString({ coerce: true, trim: 'milliseconds' })(d)).toEqual(
'2020-11-01T10:23:45.123',
);
expect(vg.isDateString({ coerce: true, trim: 'tz' })(d)).toEqual(
'2020-11-01T10:23:45.123+03:00',
);
});

it('should coerce non signed numbers to date string', () => {
expect(
vg.isDateString({
coerce: true,
precisionMin: 'yr',
})('20201101'),
).toEqual('2020-11-01T00:00:00');
vg.isDateString({ coerce: true, precisionMax: 'yr', trim: true })(d),
).toEqual('2020');
expect(
vg.isDateString({
coerce: true,
precisionMin: 'yr',
})('20201101153223.123'),
).toEqual('2020-11-01T15:32:23.123');
vg.isDateString({ coerce: true, precisionMax: 'month', trim: true })(d),
).toEqual('2020-11');
expect(
vg.isDateString({ coerce: true })('20250823000600.0000+0300'),
).toEqual('2025-08-23T00:06:00.0000+03:00');
vg.isDateString({ coerce: true, precisionMax: 'day', trim: true })(d),
).toEqual('2020-11-01');
expect(
vg.isDateString({
coerce: true,
precisionMin: 'yr',
})('20201101153223.123+03'),
).toEqual('2020-11-01T15:32:23.123+03');
vg.isDateString({ coerce: true, precisionMax: 'hours', trim: true })(d),
).toEqual('2020-11-01T10');
expect(
vg.isDateString({ coerce: true, precisionMax: 'minutes', trim: true })(d),
).toEqual('2020-11-01T10:23');
expect(
vg.isDateString({ coerce: true, precisionMax: 'seconds', trim: true })(d),
).toEqual('2020-11-01T10:23:45');
expect(
vg.isDateString({
coerce: true,
precisionMin: 'yr',
})('20201101153223.123+0330'),
).toEqual('2020-11-01T15:32:23.123+03:30');
precisionMax: 'milliseconds',
trim: true,
})(d),
).toEqual('2020-11-01T10:23:45.123');
expect(
vg.isDateString({ coerce: true, precisionMax: 'tz', trim: true })(d),
).toEqual('2020-11-01T10:23:45.123+03:00');
});
});