Skip to content

Commit

Permalink
[FEATURE] Array picking and sampling (#8)
Browse files Browse the repository at this point in the history
  • Loading branch information
vojtechpavlu authored Mar 17, 2024
1 parent 82f8829 commit db00881
Show file tree
Hide file tree
Showing 9 changed files with 330 additions and 4 deletions.
38 changes: 38 additions & 0 deletions src/baseGenerators/array/ArrayPicker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { randomItemFromArray } from '../../utils';
import {
GeneratedValue,
ValueGenerator,
ValueGeneratorConfig,
} from '../ValueGenerator';

/**
* Configuration for the array picker defining the array
*/
export type ArrayPickerConfig = {
array: any[];
} & ValueGeneratorConfig;

/**
* Randomly picks a single element from the given array and returns it as a value.
*/
export class ArrayPicker extends ValueGenerator<
GeneratedValue,
ArrayPickerConfig
> {
constructor(config: ArrayPickerConfig) {
if (config.array === null || config.array === undefined) {
throw new Error(`Property 'array' is required`);
} else if (!Array.isArray(config.array)) {
throw new Error(`Property 'array' must be of type 'array'`);
} else if (config.array.length < 1) {
throw new Error(`Property 'array' must have at least one item`);
}

super(config);
}

get = (): GeneratedValue => {
let value = randomItemFromArray(this.config.array);
return this.pipe(value);
};
}
51 changes: 51 additions & 0 deletions src/baseGenerators/array/ArraySampleGenerator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import {
GeneratedValue,
ValueGenerator,
ValueGeneratorConfig,
} from '../ValueGenerator';
import { randomSampleFromArray } from '../../utils';

/**
* Configuration for taking a sample from the given array.
*/
export type ArraySampleConfig = {
array: any[];
sampleSize: number;
} & ValueGeneratorConfig;

/**
* This constant generator simply returns the number specified
* in the configuration.
*/
export class ArraySampleGenerator extends ValueGenerator<
GeneratedValue,
ArraySampleConfig
> {
constructor(config: ArraySampleConfig) {
if (config.array === null || config.array === undefined) {
throw new Error(`Property 'array' is required`);
} else if (!Array.isArray(config.array)) {
throw new Error(`Property 'array' must be of type 'array'`);
} else if (config.array.length < 1) {
throw new Error(`Property 'array' must have at least one item`);
} else if (config.sampleSize === undefined || config.sampleSize === null) {
throw new Error(`Property 'sampleSize' is required`);
} else if (config.sampleSize < 1) {
throw new Error(
`Property 'sampleSize' has to be at least 1 (got: ${config.sampleSize})`,
);
} else if (config.sampleSize > config.array.length) {
throw new Error(
`Property 'sampleSize' has to be less or equal to size of the given array`,
);
}

super(config);
}

get = (): GeneratedValue => {
return this.pipe(
randomSampleFromArray(this.config.array, this.config.sampleSize),
);
};
}
42 changes: 42 additions & 0 deletions src/baseGenerators/array/ConstantArrayGenerator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import {
GeneratedValue,
ValueGenerator,
ValueGeneratorConfig,
} from '../ValueGenerator';

/**
* Configuration specifying which array of given items should the
* constant generator produce.
*/
export type ConstantArrayConfig = {
array: any[];
} & ValueGeneratorConfig;

/**
* This constant generator simply returns the whole array specified
* in the configuration.
*/
export class ConstantArrayGenerator extends ValueGenerator<
GeneratedValue,
ConstantArrayConfig
> {
constructor(config: ConstantArrayConfig) {
if (config.array === null || config.array === undefined) {
throw new Error(`Property 'array' is required`);
} else if (!Array.isArray(config.array)) {
throw new Error(`Property 'array' must be of type 'array'`);
} else if (config.array.length < 1) {
throw new Error(`Property 'array' must have at least one item`);
}

super(config);
}

get = (): GeneratedValue => {
return this.pipe(this.arrayDeepCopy(this.config.array));
};

private arrayDeepCopy = (arr: any[]) => {
return JSON.parse(JSON.stringify(arr));
};
}
3 changes: 3 additions & 0 deletions src/baseGenerators/array/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './ArrayPicker';
export * from './ConstantArrayGenerator';
export * from './ArraySampleGenerator';
45 changes: 42 additions & 3 deletions src/baseGenerators/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,27 @@ import {
ValueGeneratorConfig,
} from './ValueGenerator';
import {
ConstantNumberConfig,
ConstantNumberGenerator,
FloatGenerator,
FloatGeneratorConfig,
IntegerGenerator,
IntegerGeneratorConfig,
} from './numeric';
import { StringGeneratorConfig, StringOfLengthGenerator } from './string';
import {
ConstantStringConfig,
ConstantStringGenerator,
StringGeneratorConfig,
StringOfLengthGenerator,
} from './string';
import {
ArrayPicker,
ArrayPickerConfig,
ArraySampleConfig,
ArraySampleGenerator,
ConstantArrayConfig,
ConstantArrayGenerator,
} from './array';

/**
* Type definition for builders of ValueGenerators
Expand Down Expand Up @@ -104,21 +119,45 @@ export const registerValueGenerator = <
/** Registry for Value generator builders */
const VALUE_GENERATOR_REGISTRY: ValueGeneratorRegistry<any, any> = {};

// Register numerics
registerValueGenerator(
'range-integer',
(config: IntegerGeneratorConfig) => new IntegerGenerator(config),
);

registerValueGenerator(
'range-float',
(config: FloatGeneratorConfig) => new FloatGenerator(config),
);
registerValueGenerator(
'constant-number',
(config: ConstantNumberConfig) => new ConstantNumberGenerator(config),
);

// Register strings
registerValueGenerator(
'string-of-length',
(config: StringGeneratorConfig) => new StringOfLengthGenerator(config),
);
registerValueGenerator(
'constant-string',
(config: ConstantStringConfig) => new ConstantStringGenerator(config),
);

// Register arrays
registerValueGenerator(
'array-picker',
(config: ArrayPickerConfig) => new ArrayPicker(config),
);
registerValueGenerator(
'constant-array',
(config: ConstantArrayConfig) => new ConstantArrayGenerator(config),
);
registerValueGenerator(
'array-sample',
(config: ArraySampleConfig) => new ArraySampleGenerator(config),
);

export * from './numeric';
export * from './ValueGenerator';
export * from './numeric';
export * from './string';
export * from './array';
3 changes: 2 additions & 1 deletion src/utils/random/arrays.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ export const shuffleArray = <T>(array: T[]): T[] => {
throw new Error('Given argument must be an array with at least one item');
}

const copy: T[] = array.slice();
// Make a deep copy
const copy: T[] = JSON.parse(JSON.stringify(array));

for (let current = array.length - 1; current > 0; current--) {
const switchIndex = randomInteger(0, current);
Expand Down
44 changes: 44 additions & 0 deletions test/baseGenerators/array/ArrayPicker.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { ArrayPicker } from '../../../src';

describe('ArrayPicker generator class', () => {
it('should return an item from the given array', () => {
const array = ['a', 'b', 'c'];
const generator = new ArrayPicker({ array });
expect(array.includes(generator.get())).toBe(true);
});

it('should fail on empty array', () => {
const array: any[] = [];
expect(() => new ArrayPicker({ array })).toThrow();
});

it('should fail on non-array', () => {
const array: string = '';
// @ts-ignore
expect(() => new ArrayPicker({ array })).toThrow();
});

it('should fail on null', () => {
// @ts-ignore
const array: any[] = null;
// @ts-ignore
expect(() => new ArrayPicker({ array })).toThrow();
});

it('should fail on undefined', () => {
// @ts-ignore
const array: any[] = undefined;
// @ts-ignore
expect(() => new ArrayPicker({ array })).toThrow();
});

it('should pipe the value', () => {
const array: any[] = ['a'];
const generator = new ArrayPicker({
array,
pipes: [(value: string) => value.toUpperCase()],
});

expect(generator.get()).toBe('A');
});
});
63 changes: 63 additions & 0 deletions test/baseGenerators/array/ArraySampleGenerator.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { ArraySampleGenerator } from '../../../src';

describe('ArraySampleGenerator class', () => {
it('should return a specified number of items', () => {
const array = ['a', 'b', 'c', 'd'];
const generator = new ArraySampleGenerator({
array,
sampleSize: 2,
});

expect(generator.get().length).toBe(2);
});

it('should return a subset of the configured array', () => {
const array = ['a', 'b', 'c', 'd'];
const generator = new ArraySampleGenerator({
array,
sampleSize: 2,
});

const generatedItems = generator.get();

generatedItems.forEach((item: string) => {
expect(array.includes(item)).toBe(true);
});
});

it('should throw on zero sample size', () => {
const config = {
array: ['a', 'b', 'c', 'd'],
sampleSize: 0,
};

expect(() => new ArraySampleGenerator(config)).toThrow();
});

it('should throw on negative sample size', () => {
const config = {
array: ['a', 'b', 'c', 'd'],
sampleSize: -1,
};

expect(() => new ArraySampleGenerator(config)).toThrow();
});

it('should throw on empty array', () => {
const config = {
array: [],
sampleSize: 1,
};

expect(() => new ArraySampleGenerator(config)).toThrow();
});

it('should throw on greater sample size than array length', () => {
const config = {
array: ['a', 'b', 'c', 'd'],
sampleSize: 15,
};

expect(() => new ArraySampleGenerator(config)).toThrow();
});
});
45 changes: 45 additions & 0 deletions test/baseGenerators/array/ConstantArrayGenerator.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { ConstantArrayGenerator } from '../../../src';

describe('ConstantArrayGenerator class', () => {
it('should return array of the same size', () => {
const array = ['a', 'b', 'c'];
const generator = new ConstantArrayGenerator({ array });

expect(generator.get().length).toBe(array.length);
});

it('should return exactly same array as given', () => {
const array = ['a', 'b', 'c'];
const generator = new ConstantArrayGenerator({ array });

const generated = generator.get();

generated.forEach((item: any, idx: number) => {
expect(item).toBe(array[idx]);
});
});

it('should throw on empty array', () => {
const array: any[] = [];

expect(() => new ConstantArrayGenerator({ array })).toThrow();
});

it('should throw on null', () => {
const array = null;
// @ts-ignore
expect(() => new ConstantArrayGenerator({ array })).toThrow();
});

it('should throw on undefined', () => {
const array = undefined;
// @ts-ignore
expect(() => new ConstantArrayGenerator({ array })).toThrow();
});

it('should throw on non-array argument', () => {
const array = 'not array';
// @ts-ignore
expect(() => new ConstantArrayGenerator({ array })).toThrow();
});
});

0 comments on commit db00881

Please sign in to comment.