-
-
Notifications
You must be signed in to change notification settings - Fork 186
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Add mixedCase arbitrary Fixes #426 * Extract countToggledBits outside of the class * Remove unused code * Extract computeNextFlags * Draft units * Fix MixedCaseArbitrary tests * Add basic itest for mixedCase * Add more units * Bug for flags=2n, nextSize=1 * Add units related to shrinker * Update snapshots * Better jsdoc * Do not shrink on string if we shrunk on bigint before * Update markdown documentation * Add warning concerning the need for bigint support * Lint code
- Loading branch information
Showing
7 changed files
with
428 additions
and
0 deletions.
There are no files selected for viewing
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
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,123 @@ | ||
import { Random } from '../../random/generator/Random'; | ||
import { Stream } from '../../stream/Stream'; | ||
import { bigUintN } from './BigIntArbitrary'; | ||
import { Arbitrary } from './definition/Arbitrary'; | ||
import { Shrinkable } from './definition/Shrinkable'; | ||
|
||
export interface MixedCaseConstraints { | ||
/** Transform a character to its upper and/or lower case version */ | ||
toggleCase?: (rawChar: string) => string; | ||
} | ||
|
||
/** @hidden */ | ||
export function countToggledBits(n: bigint): number { | ||
let count = 0; | ||
while (n > BigInt(0)) { | ||
if (n & BigInt(1)) ++count; | ||
n >>= BigInt(1); | ||
} | ||
return count; | ||
} | ||
|
||
/** @hidden */ | ||
export function computeNextFlags(flags: bigint, nextSize: number): bigint { | ||
// whenever possible we want to preserve the same number of toggled positions | ||
// whenever possible we want to keep them at the same place | ||
// flags: 1000101 -> 10011 or 11001 (second choice for the moment) | ||
const allowedMask = (BigInt(1) << BigInt(nextSize)) - BigInt(1); | ||
const preservedFlags = flags & allowedMask; | ||
let numMissingFlags = countToggledBits(flags - preservedFlags); | ||
let nFlags = preservedFlags; | ||
for (let mask = BigInt(1); mask <= allowedMask && numMissingFlags !== 0; mask <<= BigInt(1)) { | ||
if (!(nFlags & mask)) { | ||
nFlags |= mask; | ||
--numMissingFlags; | ||
} | ||
} | ||
return nFlags; | ||
} | ||
|
||
/** @hidden */ | ||
class MixedCaseArbitrary extends Arbitrary<string> { | ||
constructor(private readonly stringArb: Arbitrary<string>, private readonly toggleCase: (rawChar: string) => string) { | ||
super(); | ||
} | ||
private computeTogglePositions(chars: string[]): number[] { | ||
const positions: number[] = []; | ||
for (let idx = 0; idx !== chars.length; ++idx) { | ||
if (this.toggleCase(chars[idx]) !== chars[idx]) positions.push(idx); | ||
} | ||
return positions; | ||
} | ||
private wrapper( | ||
rawCase: Shrinkable<string>, | ||
chars: string[], | ||
togglePositions: number[], | ||
flags: bigint | ||
): Shrinkable<string> { | ||
const newChars = chars.slice(); | ||
for (let idx = 0, mask = BigInt(1); idx !== togglePositions.length; ++idx, mask <<= BigInt(1)) { | ||
if (flags & mask) newChars[togglePositions[idx]] = this.toggleCase(newChars[togglePositions[idx]]); | ||
} | ||
return new Shrinkable(newChars.join(''), () => this.shrinkImpl(rawCase, chars, togglePositions, flags)); | ||
} | ||
private shrinkImpl( | ||
rawCase: Shrinkable<string>, | ||
chars: string[], | ||
togglePositions: number[], | ||
flags: bigint | ||
): Stream<Shrinkable<string>> { | ||
return rawCase | ||
.shrink() | ||
.map(s => { | ||
const nChars = [...s.value_]; | ||
const nTogglePositions = this.computeTogglePositions(nChars); | ||
const nFlags = computeNextFlags(flags, nTogglePositions.length); | ||
return this.wrapper(s, nChars, nTogglePositions, nFlags); | ||
}) | ||
.join( | ||
bigUintN(togglePositions.length) | ||
.shrinkableFor(flags) | ||
.shrink() | ||
.map(nFlags => { | ||
return this.wrapper(new Shrinkable(rawCase.value), chars, togglePositions, nFlags.value_); | ||
}) | ||
); | ||
} | ||
generate(mrng: Random): Shrinkable<string> { | ||
const rawCaseShrinkable = this.stringArb.generate(mrng); | ||
|
||
const chars = [...rawCaseShrinkable.value_]; // split into valid unicode (keeps surrogate pairs) | ||
const togglePositions = this.computeTogglePositions(chars); | ||
|
||
const flagsArb = bigUintN(togglePositions.length); | ||
const flags = flagsArb.generate(mrng).value_; // true => toggle the char, false => keep it as-is | ||
|
||
return this.wrapper(rawCaseShrinkable, chars, togglePositions, flags); | ||
} | ||
} | ||
|
||
/** @hidden */ | ||
function defaultToggleCase(rawChar: string) { | ||
const upper = rawChar.toUpperCase(); | ||
if (upper !== rawChar) return upper; | ||
return rawChar.toLowerCase(); | ||
} | ||
|
||
/** | ||
* Randomly switch the case of characters generated by `stringArb` (upper/lower) | ||
* | ||
* WARNING: | ||
* Require bigint support. | ||
* Under-the-hood the arbitrary relies on bigint to compute the flags that should be toggled or not. | ||
* | ||
* @param stringArb Arbitrary able to build string values | ||
* @param constraints Constraints to be applied when computing upper/lower case version | ||
*/ | ||
export function mixedCase(stringArb: Arbitrary<string>, constraints?: MixedCaseConstraints): Arbitrary<string> { | ||
if (typeof BigInt === 'undefined') { | ||
throw new Error(`mixedCase requires BigInt support`); | ||
} | ||
const toggleCase = (constraints && constraints.toggleCase) || defaultToggleCase; | ||
return new MixedCaseArbitrary(stringArb, toggleCase); | ||
} |
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
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
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
25 changes: 25 additions & 0 deletions
25
test/unit/check/arbitrary/MixedCaseArbitrary.itest.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,25 @@ | ||
import { nat } from '../../../../src/check/arbitrary/IntegerArbitrary'; | ||
import { stringOf } from '../../../../src/check/arbitrary/StringArbitrary'; | ||
import { mixedCase } from '../../../../src/check/arbitrary/MixedCaseArbitrary'; | ||
|
||
import * as genericHelper from './generic/GenericArbitraryHelper'; | ||
|
||
declare function BigInt(n: number | bigint | string): bigint; | ||
|
||
describe('MixedCaseArbitrary', () => { | ||
if (typeof BigInt === 'undefined') { | ||
it('no test', () => { | ||
expect(true).toBe(true); | ||
}); | ||
return; | ||
} | ||
describe('mixedCase', () => { | ||
const stringArb = stringOf(nat(3).map(id => ['0', '1', 'A', 'B'][id])); | ||
genericHelper.isValidArbitrary(() => mixedCase(stringArb), { | ||
isStrictlySmallerValue: (v1, v2) => { | ||
return v1.length < v2.length || v1 < v2 /* '0' < 'A' < 'a' */; | ||
}, | ||
isValidValue: (g: string) => typeof g === 'string' && [...g].every(c => '01abAB'.includes(c)) | ||
}); | ||
}); | ||
}); |
Oops, something went wrong.