Skip to content

Commit

Permalink
feat(shuffle): add string, array, and object shuffle capabaility
Browse files Browse the repository at this point in the history
Also refactor and reorganize each capability and combine them also into
a unified method.
  • Loading branch information
richrdkng committed Sep 6, 2023
1 parent 0c742f0 commit 3260059
Show file tree
Hide file tree
Showing 24 changed files with 1,167 additions and 393 deletions.
16 changes: 16 additions & 0 deletions packages/individual/shuffle/package-lock.json

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

1 change: 1 addition & 0 deletions packages/individual/shuffle/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
"@semantic-release/changelog": "^6.0.3",
"@semantic-release/exec": "^6.0.3",
"@semantic-release/git": "^10.0.1",
"@testyard/stats": "^1.4.1",
"@tsconfig/node16": "^16.1.1",
"@types/jest": "^29.5.4",
"@types/semantic-release": "^20.0.1",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
// TODO: handle typed arrays

import { RandomGenerator, RandomEngine } from '@grandom/core'

import { getRest } from '../utils'
// import ExcludeFilter from './ExcludeFilter'

export interface ShuffleArrayOptions<T = any> {
/**
* Filters the input array by element.
*
* @param element The specific element to filter from the input array.
* @returns Whether to keep the specific element in the shuffle pool.
*/
filter?: (element: T) => boolean

/**
* Exclude one or multiple elements from the input array.
*/
exclude?: T | T[]
}

export default class RandomArrayShuffle extends RandomGenerator {
constructor (engine: RandomEngine) {
super(engine)

this.shuffle = this.shuffle.bind(this)
}

// -----------------------------------------------------------------------------------------------

canBeParsed (arg1: any): boolean {
return Array.isArray(arg1)
}

parse (arg1: any, arg2: any, arg3: any): any {
if (arg1.length < 1) {
return []
}

const { count, options } = getRest(arg1, arg2, arg3)

const filter = typeof options.filter === 'function'
? options.filter as ShuffleArrayOptions['filter']
: undefined

const length = count === -1
? arg1.length
: count

const pool: any[] = []

for (let i = 0; i < length; i++) {
const element = arg1[i]

if (filter?.(element) === false) {
continue
}

pool.push(element)
}

this._engine.shuffleArray(pool)

return pool
}

// -----------------------------------------------------------------------------------------------

/**
* Shuffles (mixes) the input array elements randomly and returns them
* as a new array (the input array is not modified).
*
* @param array The array to use to shuffle.
*/
shuffle <T> (array: ArrayLike<T>): T

/**
* Shuffles (mixes) the input array elements randomly and returns them
* as a new array (the input array is not modified).
*
* @param array The array to use to shuffle.
* @param count The count (length) of the returned array.
*/
shuffle <T> (
array: ArrayLike<T>,
count: number
): T

/**
* Shuffles (mixes) the input array elements randomly and returns them
* as a new array (the input array is not modified).
*
* @param array The array to use to shuffle.
* @param options Shuffle options (filtering and fallback).
*/
shuffle <T> (
array: ArrayLike<T>,
options: ShuffleArrayOptions<T>
): T

/**
* Shuffles (mixes) the input array elements randomly and returns them
* as a new array (the input array is not modified).
*
* @param array The array to use to shuffle.
* @param count The count (length) of the returned array.
* @param options Shuffle options (filtering and fallback).
*/
shuffle <T> (
array: ArrayLike<T>,
count: number,
options: ShuffleArrayOptions<T>
): T

// -----------------------------------------------------------------------------------------------

shuffle (arg1: any, arg2?: any, arg3?: any): any {
if (this.canBeParsed(arg1)) {
return this.parse(arg1, arg2, arg3)
}

throw new TypeError(
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
`Must be called with an array, got: ${arg1} (typeof === '${typeof arg1}').`
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { RandomGenerator, RandomEngine } from '@grandom/core'

import { getRest } from '../utils'
// import ExcludeFilter from './ExcludeFilter'

export interface ShuffleObjectOptions<T = any> {
/**
* Filters the input object by entry.
*
* @param entry The specific entry to filter from the input object.
* @returns Whether to keep the specific entry in the shuffle pool.
*/
filter?: (key: keyof T, value: T[keyof T]) => boolean

/**
* Exclude one or multiple elements from the input array.
*/
exclude?: T
}

export default class RandomObjectShuffle extends RandomGenerator {
constructor (engine: RandomEngine) {
super(engine)

this.shuffle = this.shuffle.bind(this)
}

// -----------------------------------------------------------------------------------------------

canBeParsed (arg1: any): boolean {
return arg1 !== null && typeof arg1 === 'object'
}

parse (arg1: any, arg2: any, arg3: any): any {
const keys = Object.keys(arg1)

if (keys.length < 1) {
return {}
}

const { count, options } = getRest(arg1, arg2, arg3)

const filter = typeof options.filter === 'function'
? options.filter as ShuffleObjectOptions['filter']
: undefined

const length = count === -1
? keys.length
: count

const pool: Record<string, any> = {}

this._engine.shuffleArray(keys)

for (let i = 0; i < length; i++) {
const key = keys[i]
const value = arg1[key]

if (filter?.(key, value) === false) {
continue
}

pool[key] = value
}

return pool
}

// -----------------------------------------------------------------------------------------------

/**
* Shuffles (mixes) the input object entries randomly and returns them
* as a new object (the input object is not modified).
*
* @param object The object to use to shuffle.
*/
shuffle <T extends Record<string, any>> (object: T): T

/**
* Shuffles (mixes) the input object entries randomly and returns them
* as a new object (the input object is not modified).
*
* @param object The object to use to shuffle.
* @param count The count (length) of the returned object.
*/
shuffle <T extends Record<string, any>> (
object: T,
count: number
): T

/**
* Shuffles (mixes) the input object entries randomly and returns them
* as a new object (the input object is not modified).
*
* @param object The object to use to shuffle.
* @param options Shuffle options (filtering and fallback).
*/
shuffle <T extends Record<string, any>> (
object: T,
options: ShuffleObjectOptions<T>
): T

/**
* Shuffles (mixes) the input object entries randomly and returns them
* as a new object (the input object is not modified).
*
* @param object The object to use to shuffle.
* @param count The count (length) of the returned object.
* @param options Shuffle options (filtering and fallback).
*/
shuffle <T extends Record<string, any>> (
object: T,
count: number,
options: ShuffleObjectOptions<T>
): T

// -----------------------------------------------------------------------------------------------

shuffle (arg1: any, arg2?: any, arg3?: any): any {
if (this.canBeParsed(arg1)) {
return this.parse(arg1, arg2, arg3)
}

throw new TypeError(
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
`Must be called with an object, got: ${arg1} (typeof === '${typeof arg1}').`
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
interface FilterEntry {
string?: string
regexp?: RegExp
}

export default class ExcludeFilter {
private readonly _filterEntries: FilterEntry[] = []

constructor (exclude: string | RegExp | Array<string | RegExp>) {
if ((typeof exclude !== 'string' && typeof exclude !== 'object') || exclude === null) {
throw new TypeError(
'Filter must be a string, RegExp, or Array<string | RegExp>, got: ' +
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
`${exclude} (typeof === '${typeof exclude}').`
)
}

const filters = Array.isArray(exclude)
? exclude
: [exclude]

let i = 0

for (const filter of filters) {
if (typeof filter === 'string') {
this._filterEntries.push({ string: filter })
} else if (filter instanceof RegExp) {
this._filterEntries.push({ regexp: filter })
} else {
throw new TypeError(
`Filter[${i}] must be a string, or a RegExp, got: ` +
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
`${filter} (typeof === '${typeof filter}').`
)
}

i++
}
}

get numFilters (): number {
return this._filterEntries.length
}

matches (value: string): boolean {
for (const filterEntry of this._filterEntries) {
if (value === filterEntry.string) {
return true
}

if (filterEntry.regexp?.test(value) === true) {
return true
}
}

return false
}
}
Loading

0 comments on commit 3260059

Please sign in to comment.