Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(string): adds support for generating ULID #2524

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
4bb549a
feat(string): adds support for generating ULID
brunocleite Nov 3, 2023
493c3e0
Merge branch 'next' into feat/adds-support-for-ulid-generation
brunocleite Nov 7, 2023
b6dc97b
Merge branch 'next' into feat/adds-support-for-ulid-generation
brunocleite Mar 14, 2024
b63d07b
Merge branch 'next' into feat/adds-support-for-ulid-generation
ST-DDT Mar 15, 2024
9ba1bbe
fix(string): update ulid values (#2648)
brunocleite Mar 18, 2024
12fed6e
Merge remote-tracking branch 'origin/feat/adds-support-for-ulid-gener…
brunocleite Mar 18, 2024
f41fd27
Merge branch 'next' into feat/adds-support-for-ulid-generation
ST-DDT Mar 19, 2024
6e7a967
feat(string): updates ulid implementation using refDate
brunocleite Mar 19, 2024
70a4bf4
Merge remote-tracking branch 'origin/feat/adds-support-for-ulid-gener…
brunocleite Mar 19, 2024
702061d
feat(string): updates ulid refDate documentation
brunocleite Mar 19, 2024
6fcba2c
fix(string): updates ulid regex to match all possible ULID values
brunocleite Mar 20, 2024
805cfbd
Merge branch 'next' into feat/adds-support-for-ulid-generation
ST-DDT Jul 4, 2024
23bbf60
fix(string): uses up-to-date standard on handling date errors and upd…
brunocleite Jul 4, 2024
1ca84cc
Update src/modules/string/index.ts
brunocleite Jul 5, 2024
e762041
feat(internal): move 'toDate' function into an internal helper and up…
brunocleite Jul 5, 2024
10794e1
Merge remote-tracking branch 'origin/feat/adds-support-for-ulid-gener…
brunocleite Jul 5, 2024
66158c3
feat(string): adds ulid example with refDate
brunocleite Jul 5, 2024
8d16f2b
feat(string): extract base32 methods into its own file
brunocleite Jul 5, 2024
9c53fb5
feat(internal): adds tests for toDate
brunocleite Jul 5, 2024
ccadadf
feat(internal): adds tests for base32
brunocleite Jul 5, 2024
43141fb
feat(string): fix typo on ulid jsdoc
brunocleite Jul 5, 2024
56c57ea
feat(string): fix linting errors
brunocleite Jul 5, 2024
54096d7
Update src/internal/base32.ts
brunocleite Jul 5, 2024
1c4d39d
fix(string): fix ulid comment
brunocleite Jul 5, 2024
0a824fa
Update src/modules/string/index.ts
brunocleite Jul 5, 2024
49654c2
Merge remote-tracking branch 'origin/feat/adds-support-for-ulid-gener…
brunocleite Jul 5, 2024
2f5aa42
fix(string): adds test to match base32 characters and toMatchSnapshot()
brunocleite Jul 5, 2024
34316a6
Update src/internal/base32.ts
brunocleite Jul 8, 2024
c3b8697
Update src/internal/base32.ts
brunocleite Jul 8, 2024
ec1fd5b
Update test/internal/base32.spec.ts
brunocleite Jul 8, 2024
7e6ba0a
fix(string): refactoring and lint fixes
brunocleite Jul 8, 2024
09b6523
Merge remote-tracking branch 'origin/feat/adds-support-for-ulid-gener…
brunocleite Jul 8, 2024
eb25568
Merge branch 'next' into feat/adds-support-for-ulid-generation
brunocleite Jul 8, 2024
d1d3d23
Update src/modules/string/index.ts
brunocleite Jul 8, 2024
1d318b4
Update src/modules/string/index.ts
brunocleite Jul 8, 2024
9941efa
Merge branch 'next' into feat/adds-support-for-ulid-generation
ST-DDT Jul 8, 2024
c7080e1
Update test/internal/base32.spec.ts
brunocleite Jul 8, 2024
52c2872
Update src/modules/string/index.ts
brunocleite Jul 8, 2024
af7376d
Update test/internal/date.spec.ts
brunocleite Jul 8, 2024
1d63041
Update test/modules/string.spec.ts
brunocleite Jul 8, 2024
b02d751
fix(string): import fix and test snapshot
brunocleite Jul 8, 2024
d7ee011
Merge branch 'next' into feat/adds-support-for-ulid-generation
brunocleite Jul 8, 2024
8f9aecc
Merge branch 'next' into feat/adds-support-for-ulid-generation
ST-DDT Sep 14, 2024
8acad7a
Merge branch 'next' into feat/adds-support-for-ulid-generation
ST-DDT Sep 16, 2024
ddd7e59
Merge branch 'next' into feat/adds-support-for-ulid-generation
ST-DDT Sep 23, 2024
9646279
Merge remote-tracking branch 'faker/next' into feat/adds-support-for-…
brunocleite Sep 27, 2024
f809eef
🎉 feat(snapshot): add snapshot for dateToBase32 encoding test
brunocleite Oct 1, 2024
16517df
Merge branch 'next' into feat/adds-support-for-ulid-generation
xDivisionByZerox Oct 10, 2024
c8347cd
Merge branch 'next' into feat/adds-support-for-ulid-generation
Shinigami92 Oct 10, 2024
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
21 changes: 21 additions & 0 deletions src/internal/base32.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/**
* Crockford's Base32 - Excludes I, L, O, and U which may be confused with numbers
*/
export const CROCKFORDS_BASE32 = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';

/**
* Encodes a Date into 10 characters base32 string.
*
* @param date The Date to encode.
*/
export function dateToBase32(date: Date): string {
let value = date.valueOf();
let result = '';
for (let len = 10; len > 0; len--) {
const mod = value % 32;
result = CROCKFORDS_BASE32[mod] + result;
value = (value - mod) / 32;
}

return result;
}
22 changes: 22 additions & 0 deletions src/internal/date.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { FakerError } from '../errors/faker-error';

/**
* Converts a date passed as a `string`, `number` or `Date` to a valid `Date` object.
*
* @param date The date to convert.
* @param name The reference name used for error messages. Defaults to `'refDate'`.
*
* @throws If the given date is invalid.
*/
export function toDate(
date: string | Date | number,
name: string = 'refDate'
): Date {
const converted = new Date(date);

if (Number.isNaN(converted.valueOf())) {
throw new FakerError(`Invalid ${name} date: ${date.toString()}`);
}

return converted;
}
19 changes: 1 addition & 18 deletions src/modules/date/index.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,10 @@
import type { Faker } from '../..';
import type { DateEntryDefinition } from '../../definitions';
import { FakerError } from '../../errors/faker-error';
import { toDate } from '../../internal/date';
import { assertLocaleData } from '../../internal/locale-proxy';
import { SimpleModuleBase } from '../../internal/module-base';

/**
* Converts a date passed as a `string`, `number` or `Date` to a valid `Date` object.
*
* @param date The date to convert.
* @param name The reference name used for error messages. Defaults to `'refDate'`.
*
* @throws If the given date is invalid.
*/
function toDate(date: string | Date | number, name: string = 'refDate'): Date {
const converted = new Date(date);

if (Number.isNaN(converted.valueOf())) {
throw new FakerError(`Invalid ${name} date: ${date.toString()}`);
}

return converted;
}

/**
* Module to generate dates (without methods requiring localized data).
*/
Expand Down
33 changes: 33 additions & 0 deletions src/modules/string/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { FakerError } from '../../errors/faker-error';
import { CROCKFORDS_BASE32, dateToBase32 } from '../../internal/base32';
import { toDate } from '../../internal/date';
import { SimpleModuleBase } from '../../internal/module-base';
import type { LiteralUnion } from '../../internal/types';

Expand Down Expand Up @@ -704,6 +706,37 @@ export class StringModule extends SimpleModuleBase {
.replaceAll('y', () => this.faker.number.hex({ min: 0x8, max: 0xb }));
}

/**
* Returns a ULID ([Universally Unique Lexicographically Sortable Identifier](https://github.com/ulid/spec)).
*
* @param options The optional options object.
* @param options.refDate The timestamp to encode into the ULID.
* The encoded timestamp is represented by the first 10 characters of the result.
* Defaults to `faker.defaultRefDate()`.
*
* @example
* faker.string.ulid() // '01ARZ3NDEKTSV4RRFFQ69G5FAV'
ST-DDT marked this conversation as resolved.
Show resolved Hide resolved
* faker.string.ulid({ refDate: '2020-01-01T00:00:00.000Z' }) // '01DXF6DT00CX9QNNW7PNXQ3YR8'
*
* @since 9.1.0
*/
ulid(
options: {
/**
* The date to use as reference point for the newly generated ULID encoded timestamp.
brunocleite marked this conversation as resolved.
Show resolved Hide resolved
* The encoded timestamp is represented by the first 10 characters of the result.
*
* @default faker.defaultRefDate()
*/
refDate?: string | Date | number;
} = {}
): string {
const { refDate = this.faker.defaultRefDate() } = options;
const date = toDate(refDate);

return dateToBase32(date) + this.fromCharacters(CROCKFORDS_BASE32, 16);
}

/**
* Generates a [Nano ID](https://github.com/ai/nanoid).
*
Expand Down
3 changes: 3 additions & 0 deletions test/internal/__snapshots__/base32.spec.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`dateToBase32() > encodes current date correctly 1`] = `"01GWX1T800"`;
35 changes: 35 additions & 0 deletions test/internal/base32.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { describe, expect, it } from 'vitest';
import { CROCKFORDS_BASE32, dateToBase32 } from '../../src/internal/base32';

describe('dateToBase32()', () => {
it('encodes current date correctly', () => {
const date = new Date('2023-04-01T00:00:00Z');
const encoded = dateToBase32(date);
expect(encoded).toHaveLength(10);
brunocleite marked this conversation as resolved.
Show resolved Hide resolved
expect(encoded).toMatchSnapshot();
for (const char of encoded) {
expect(CROCKFORDS_BASE32).toContain(char);
}
});

it('encodes epoch start date correctly', () => {
const date = new Date('1970-01-01T00:00:00Z');
const encoded = dateToBase32(date);
expect(encoded).toBe('0000000000');
});

it('returns different encodings for dates one millisecond apart', () => {
const date1 = new Date('2023-04-01T00:00:00.000Z');
const date2 = new Date('2023-04-01T00:00:00.001Z');
const encoded1 = dateToBase32(date1);
const encoded2 = dateToBase32(date2);
expect(encoded1).not.toBe(encoded2);
});

it('encodes same date consistently', () => {
const date = new Date('2023-04-01T00:00:00Z');
const encoded1 = dateToBase32(date);
const encoded2 = dateToBase32(date);
expect(encoded1).toBe(encoded2);
});
});
22 changes: 22 additions & 0 deletions test/internal/date.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { describe, expect, it } from 'vitest';
import { FakerError } from '../../src';
import { toDate } from '../../src/internal/date';

describe('toDate()', () => {
it('should convert a string date to a valid Date object', () => {
const dateString = '2024-07-05';
expect(toDate(dateString)).toEqual(new Date(dateString));
});

it('should convert a string datetime to a valid Date object', () => {
const timestamp = '2024-07-05T15:49:19+0000';
expect(toDate(timestamp)).toEqual(new Date(timestamp));
});

it('should throw a FakerError for an invalid date string', () => {
const timestamp = 'aaaa-07-05T15:49:19+0000';
expect(() => toDate(timestamp)).toThrow(
new FakerError(`Invalid refDate date: ${timestamp}`)
);
});
});
18 changes: 18 additions & 0 deletions test/modules/__snapshots__/string.spec.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,12 @@ exports[`string > 42 > symbol > with length parameter 5`] = `">%*,/"`;

exports[`string > 42 > symbol > with length range 1`] = `"}\\>%%"\`>[!~_'&"`;

exports[`string > 42 > ulid > with Date refDate 1`] = `"01EZ2S259ZBYQK441VKP0ZT655"`;

exports[`string > 42 > ulid > with number refDate 1`] = `"01EZ2S259ZBYQK441VKP0ZT655"`;

exports[`string > 42 > ulid > with string refDate 1`] = `"01EZ2S259ZBYQK441VKP0ZT655"`;

exports[`string > 42 > uuid 1`] = `"5fb9220d-9b0f-4d32-a248-6492457c3890"`;

exports[`string > 42 > uuid 2`] = `"21ffc41a-7170-4e4a-9488-2fcfe9e13056"`;
Expand Down Expand Up @@ -338,6 +344,12 @@ exports[`string > 1211 > symbol > with length parameter 5`] = `"~]-|<"`;

exports[`string > 1211 > symbol > with length range 1`] = `"{(~@@],[_]?_.\`\`'=',~"`;

exports[`string > 1211 > ulid > with Date refDate 1`] = `"01EZ2S259ZXW7ZNNRBPTRMTDVV"`;

exports[`string > 1211 > ulid > with number refDate 1`] = `"01EZ2S259ZXW7ZNNRBPTRMTDVV"`;

exports[`string > 1211 > ulid > with string refDate 1`] = `"01EZ2S259ZXW7ZNNRBPTRMTDVV"`;

exports[`string > 1211 > uuid 1`] = `"ee3faac5-bdca-4d6d-9d39-35fc6e8f34b8"`;

exports[`string > 1211 > uuid 2`] = `"d64428b2-b736-43d9-970b-2b4c8739d1d7"`;
Expand Down Expand Up @@ -512,6 +524,12 @@ exports[`string > 1337 > symbol > with length parameter 5`] = `"]'*@:"`;

exports[`string > 1337 > symbol > with length range 1`] = `"&)/+;)~\\$-?%"`;

exports[`string > 1337 > ulid > with Date refDate 1`] = `"01EZ2S259Z858EAG8ZQ3CM4ZES"`;

exports[`string > 1337 > ulid > with number refDate 1`] = `"01EZ2S259Z858EAG8ZQ3CM4ZES"`;

exports[`string > 1337 > ulid > with string refDate 1`] = `"01EZ2S259Z858EAG8ZQ3CM4ZES"`;

exports[`string > 1337 > uuid 1`] = `"4247584f-b16a-42f7-8cc5-69c34a72638d"`;

exports[`string > 1337 > uuid 2`] = `"f6880bf2-25b0-450c-a5b7-fd99f401ff75"`;
Expand Down
27 changes: 27 additions & 0 deletions test/modules/string.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,16 @@ describe('string', () => {

t.itRepeated('uuid', 5);

t.describe('ulid', (t) => {
const ulidRefDate = '2021-02-21T17:09:15.711Z';

t.it('with string refDate', { refDate: ulidRefDate })
.it('with Date refDate', { refDate: new Date(ulidRefDate) })
.it('with number refDate', {
refDate: new Date(ulidRefDate).getTime(),
});
});

t.describe('nanoid', (t) => {
t.itRepeated('noArgs', 5)
.it('with length parameter', 30)
Expand Down Expand Up @@ -750,6 +760,23 @@ describe('string', () => {
});
});

describe(`ulid`, () => {
it.each(['invalid', Number.NaN, new Date(Number.NaN)] as const)(
'should reject invalid refDates %s',
(refDate) => {
expect(() => faker.string.ulid({ refDate })).toThrow(
new FakerError(`Invalid refDate date: ${refDate.toString()}`)
);
}
);

it('generates a valid ULID', () => {
const ulid = faker.string.ulid();
const regex = /^[0-7][0-9A-HJKMNP-TV-Z]{25}$/;
expect(ulid).toMatch(regex);
});
});

describe(`nanoid`, () => {
it('generates a valid Nano ID', () => {
const id = faker.string.nanoid();
Expand Down