Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
4 changed files
with
266 additions
and
0 deletions.
There are no files selected for viewing
125 changes: 125 additions & 0 deletions
125
packages/fast-check/src/arbitrary/_internals/mappers/UintToBase32String.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
/** @internal */ | ||
const encodeSymbolLookupTable: Record<number, string> = { | ||
10: 'A', | ||
11: 'B', | ||
12: 'C', | ||
13: 'D', | ||
14: 'E', | ||
15: 'F', | ||
16: 'G', | ||
17: 'H', | ||
18: 'J', | ||
19: 'K', | ||
20: 'M', | ||
21: 'N', | ||
22: 'P', | ||
23: 'Q', | ||
24: 'R', | ||
25: 'S', | ||
26: 'T', | ||
27: 'V', | ||
28: 'W', | ||
29: 'X', | ||
30: 'Y', | ||
31: 'Z', | ||
}; | ||
|
||
/** @internal */ | ||
const decodeSymbolLookupTable: Record<string, number> = { | ||
'0': 0, | ||
O: 0, | ||
'1': 1, | ||
'2': 2, | ||
'3': 3, | ||
'4': 4, | ||
'5': 5, | ||
'6': 6, | ||
'7': 7, | ||
'8': 8, | ||
'9': 9, | ||
A: 10, | ||
B: 11, | ||
C: 12, | ||
D: 13, | ||
E: 14, | ||
F: 15, | ||
G: 16, | ||
H: 17, | ||
J: 18, | ||
K: 19, | ||
M: 20, | ||
N: 21, | ||
P: 22, | ||
Q: 23, | ||
R: 24, | ||
S: 25, | ||
T: 26, | ||
V: 27, | ||
W: 28, | ||
X: 29, | ||
Y: 30, | ||
Z: 31, | ||
}; | ||
|
||
/** @internal */ | ||
function getBaseLog(x: number, y: number) { | ||
return Math.log(y) / Math.log(x); | ||
} | ||
|
||
/** @internal */ | ||
function encodeSymbol(symbol: number) { | ||
return symbol < 10 ? String(symbol) : encodeSymbolLookupTable[symbol]; | ||
} | ||
|
||
/** @internal */ | ||
function pad(value: string, constLength: number) { | ||
return ( | ||
Array(constLength - value.length) | ||
.fill('0') | ||
.join('') + value | ||
); | ||
} | ||
|
||
/** @internal */ | ||
export function uintToBase32StringMapper(num: number, constLength: number | undefined = undefined) { | ||
if (num === 0) return pad('0', constLength ?? 1); | ||
|
||
let base32Str = '', | ||
remaining = num, | ||
symbolsLeft = Math.floor(getBaseLog(32, num)) + 1; | ||
while (symbolsLeft > 0) { | ||
const val = Math.pow(32, symbolsLeft - 1); | ||
const symbol = Math.floor(remaining / val); | ||
|
||
base32Str += encodeSymbol(symbol); | ||
|
||
remaining -= symbol * val; | ||
symbolsLeft--; | ||
} | ||
|
||
return pad(base32Str, constLength ?? base32Str.length); | ||
} | ||
|
||
/** @internal */ | ||
export function paddedUintToBase32StringMapper(constLength: number) { | ||
return (num: number) => uintToBase32StringMapper(num, constLength); | ||
} | ||
|
||
/** @internal */ | ||
const Base32Regex = /^[0-9A-HJKMNP-TV-Z]+$/; | ||
|
||
/** @internal */ | ||
export function uintToBase32StringUnmapper(value: unknown) { | ||
if (typeof value !== 'string') { | ||
throw new Error('Unsupported type'); | ||
} | ||
|
||
const normalizedBase32str = value.toUpperCase(); | ||
if (!Base32Regex.test(normalizedBase32str)) { | ||
throw new Error('Unsupported type'); | ||
} | ||
|
||
const symbols = normalizedBase32str.split('').map((char) => decodeSymbolLookupTable[char]); | ||
|
||
return symbols.reduce((prev, curr, i) => prev + curr * Math.pow(32, symbols.length - 1 - i), 0); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
import { Arbitrary } from '../check/arbitrary/definition/Arbitrary'; | ||
import { tuple } from './tuple'; | ||
import { integer } from './integer'; | ||
import { paddedUintToBase32StringMapper, uintToBase32StringUnmapper } from './_internals/mappers/UintToBase32String'; | ||
|
||
/** | ||
* For ulid | ||
* | ||
* According to {@link https://github.com/ulid/spec | ulid spec} | ||
* | ||
* No mixed case, only upper case digits (0-9A-Z except for: I,L,O,U) | ||
* | ||
* @remarks Since 3.11.0 | ||
* @public | ||
*/ | ||
export function ulid(): Arbitrary<string> { | ||
const timestampPartArbitrary = integer({ min: 0, max: 0xffffffffffff }); // 48 bits | ||
// Numeric literals with absolute values equal to 2^53 or greater are too large to be represented accurately as integers. | ||
// Therefore we split the 80 bit randomness part into two integers of 40 bits length each. | ||
const randomnessPartOneArbitrary = integer({ min: 0, max: 0xffffffffff }); // 40 bits | ||
const randomnessPartTwoArbitrary = integer({ min: 0, max: 0xffffffffff }); // 40 bits | ||
|
||
return tuple(timestampPartArbitrary, randomnessPartOneArbitrary, randomnessPartTwoArbitrary).map( | ||
([date, random1, random2]) => { | ||
return [ | ||
paddedUintToBase32StringMapper(10)(date), // 10 chars of base32 -> 48 bits | ||
paddedUintToBase32StringMapper(8)(random1), // 8 chars of base32 -> 40 bits | ||
paddedUintToBase32StringMapper(8)(random2), | ||
].join(''); | ||
}, | ||
(value) => { | ||
if (typeof value !== 'string' || value.length !== 26) { | ||
throw new Error('Unsupported type'); | ||
} | ||
|
||
return [value.slice(0, 10), value.slice(10, 18), value.slice(18)].map(uintToBase32StringUnmapper) as [ | ||
number, | ||
number, | ||
number | ||
]; | ||
} | ||
); | ||
} |
19 changes: 19 additions & 0 deletions
19
packages/fast-check/test/unit/arbitrary/_internals/mappers/UintToBase32String.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
import fc from 'fast-check'; | ||
import { | ||
paddedUintToBase32StringMapper, | ||
uintToBase32StringUnmapper, | ||
} from '../../../../../src/arbitrary/_internals/mappers/UintToBase32String'; | ||
|
||
describe('uintToBase32StringUnmapper', () => { | ||
it('is able to unmap any mapped value', () => | ||
fc.assert( | ||
fc.property(fc.integer({ min: 0 }), (input) => { | ||
// Arrange | ||
const mapped = paddedUintToBase32StringMapper(10)(input); | ||
// Act | ||
const out = uintToBase32StringUnmapper(mapped); | ||
// Assert | ||
expect(out).toEqual(input); | ||
}) | ||
)); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
import fc from 'fast-check'; | ||
import { ulid } from '../../../src/arbitrary/ulid'; | ||
import { fakeArbitraryStaticValue } from './__test-helpers__/ArbitraryHelpers'; | ||
|
||
import * as _IntegerMock from '../../../src/arbitrary/integer'; | ||
import { Arbitrary } from '../../../src/check/arbitrary/definition/Arbitrary'; | ||
import { fakeRandom } from './__test-helpers__/RandomHelpers'; | ||
import { | ||
assertProduceSameValueGivenSameSeed, | ||
assertProduceCorrectValues, | ||
assertProduceValuesShrinkableWithoutContext, | ||
assertShrinkProducesSameValueWithoutInitialContext, | ||
} from './__test-helpers__/ArbitraryAssertions'; | ||
const IntegerMock: { integer: (ct: { min: number; max: number }) => Arbitrary<number> } = _IntegerMock; | ||
|
||
function beforeEachHook() { | ||
jest.resetModules(); | ||
jest.restoreAllMocks(); | ||
fc.configureGlobal({ beforeEach: beforeEachHook }); | ||
} | ||
beforeEach(beforeEachHook); | ||
|
||
describe('ulid', () => { | ||
it('should produce the minimal ulid given all minimal generated values', () => { | ||
// Arrange | ||
const { instance: mrng } = fakeRandom(); | ||
const integer = jest.spyOn(IntegerMock, 'integer'); | ||
integer.mockImplementation(({ min }) => { | ||
const { instance } = fakeArbitraryStaticValue(() => min); | ||
return instance; | ||
}); | ||
|
||
// Act | ||
const arb = ulid(); | ||
const out = arb.generate(mrng, undefined); | ||
|
||
// Assert | ||
expect(out.value).toBe('0'.repeat(26)); | ||
}); | ||
|
||
it('should produce the maximal ulid given all maximal generated values', () => { | ||
// Arrange | ||
const { instance: mrng } = fakeRandom(); | ||
const integer = jest.spyOn(IntegerMock, 'integer'); | ||
integer.mockImplementation(({ max }) => { | ||
const { instance } = fakeArbitraryStaticValue(() => max); | ||
return instance; | ||
}); | ||
|
||
// Act | ||
const arb = ulid(); | ||
const out = arb.generate(mrng, undefined); | ||
|
||
// Assert | ||
expect(out.value).toBe('7ZZZZZZZZZZZZZZZZZZZZZZZZZ'); | ||
}); | ||
}); | ||
|
||
describe('uuid (integration)', () => { | ||
const isCorrect = (u: string) => { | ||
expect(u).toMatch(/^[0-7][0-9A-HJKMNP-TV-Z]{25}$/); | ||
}; | ||
|
||
it('should produce the same values given the same seed', () => { | ||
assertProduceSameValueGivenSameSeed(ulid); | ||
}); | ||
|
||
it('should only produce correct values', () => { | ||
assertProduceCorrectValues(ulid, isCorrect); | ||
}); | ||
|
||
it('should produce values seen as shrinkable without any context', () => { | ||
assertProduceValuesShrinkableWithoutContext(ulid); | ||
}); | ||
|
||
it('should be able to shrink to the same values without initial context', () => { | ||
assertShrinkProducesSameValueWithoutInitialContext(ulid); | ||
}); | ||
}); |