UNPKG

@bluelovers/fill-range

Version:

Fill in a range of numbers or letters, optionally passing an increment or `step` to use, or create a regex-compatible range with `options.toRegex`

445 lines (374 loc) 11.7 kB
import { inspect } from 'util'; import { toRegexRange, IOptions as IOptionsToRegexRange } from '@bluelovers/to-regex-range2'; export interface IOptions<V = string | number> extends IOptionsToRegexRange { /** * The increment to use for the range. Can be used with letters or numbers. * @example * // numbers * console.log(fill('1', '10', 2)); //=> [ '1', '3', '5', '7', '9' ] * console.log(fill('1', '10', 3)); //=> [ '1', '4', '7', '10' ] * console.log(fill('1', '10', 4)); //=> [ '1', '5', '9' ] * * // letters * console.log(fill('a', 'z', 5)); //=> [ 'a', 'f', 'k', 'p', 'u', 'z' ] * console.log(fill('a', 'z', 7)); //=> [ 'a', 'h', 'o', 'v' ] * console.log(fill('a', 'z', 9)); //=> [ 'a', 'j', 's' ] */ step?: number, /** * By default, null is returned when an invalid range is passed. Enable this option to throw a RangeError on invalid ranges. */ strictRanges?: boolean, /** * Cast all returned values to strings. By default, integers are returned as numbers. * @example * console.log(fill(1, 5)); //=> [ 1, 2, 3, 4, 5 ] * console.log(fill(1, 5, { stringify: true })); //=> [ '1', '2', '3', '4', '5' ] * */ stringify?: boolean, /** * Create a regex-compatible source string, instead of expanding values to an array. * @example * // alphabetical range * console.log(fill('a', 'e', { toRegex: true })); //=> '[a-e]' * // alphabetical with step * console.log(fill('a', 'z', 3, { toRegex: true })); //=> 'a|d|g|j|m|p|s|v|y' * // numerical range * console.log(fill('1', '100', { toRegex: true })); //=> '[1-9]|[1-9][0-9]|100' * // numerical range with zero padding * console.log(fill('000001', '100000', { toRegex: true })); * //=> '0{5}[1-9]|0{4}[1-9][0-9]|0{3}[1-9][0-9]{2}|0{2}[1-9][0-9]{3}|0[1-9][0-9]{4}|100000' */ toRegex?: boolean, /** * Customize each value in the returned array (or string). (you can also pass this function as the last argument to fill()). * @example * // add zero padding * console.log(fill(1, 5, value => String(value).padStart(4, '0'))); * //=> ['0001', '0002', '0003', '0004', '0005'] */ transform?(val: number, index?: number): V, /** * set limit size */ limit?: number, /** * only allow start < stop */ strictOrder?: boolean, } interface IParts { negatives: number[], positives: number[], } const enum EnumNegative { negative = '-', none = '', } function isObject(val: unknown): val is IOptions { return val !== null && typeof val === 'object' && !Array.isArray(val); } const transform = (toNumber: boolean) => { if (toNumber === true) return value => Number(value); return value => String(value); }; const isValidValue = (value): value is number | string => { return typeof value === 'number' || (typeof value === 'string' && value !== ''); }; const isNumber = (num: unknown): num is number => Number.isInteger(+num); const zeros = input => { let value = `${input}`; let index = -1; if (value[0] === '-') value = value.slice(1); if (value === '0') return false; while (value[++index] === '0'); return index > 0; }; const stringify = (start, end, options: IOptions) => { if (typeof start === 'string' || typeof end === 'string') { return true; } return options.stringify === true; }; const pad = (input: any, maxLength: number, toNumber: boolean) => { if (maxLength > 0) { input = toMaxLen(input, maxLength); } if (toNumber === false) { return String(input); } return input; }; const toMaxLen = (input: string, _maxLength: number) => { let { result, negative, maxLength } = _prefixNegative(input, _maxLength); return negative + result.padStart(maxLength, '0') }; function _partsSort(part: number[]) { part.sort((a, b) => a < b ? -1 : a > b ? 1 : 0); } function _partsCapturePrefix(options: IOptions) { return options.capture ? '' as const : '?:' as const; } function _prefixNegative(input: string, maxLength: number) { const negative = input[0] === EnumNegative.negative ? EnumNegative.negative : EnumNegative.none; if (negative === EnumNegative.negative) { input = input.slice(1); maxLength--; } return { result: input, negative, maxLength, } } function _join(part: (string|number)[]) { return part.join('|') } const toSequence = (parts: IParts, options: IOptions) => { _partsSort(parts.negatives); _partsSort(parts.positives); let prefix = _partsCapturePrefix(options); let positives = ''; let negatives = ''; let result: string; if (parts.positives.length) { positives = _join(parts.positives); } if (parts.negatives.length) { negatives = `-(${prefix}${_join(parts.negatives)})`; } if (positives && negatives) { result = `${positives}|${negatives}`; } else { result = positives || negatives; } if (options.wrap) { return `(${prefix}${result})`; } return result; }; const toRange = (a, b, isNumbers, options: IOptions) => { if (isNumbers) { return toRegexRange(a, b, { wrap: false, ...options }); } const start = String.fromCharCode(a); if (a === b) return start; const stop = String.fromCharCode(b); return `[${start}-${stop}]`; }; const toRegex = (start, end, options: IOptions): string => { if (Array.isArray(start)) { const wrap = options.wrap === true; const prefix = _partsCapturePrefix(options); start = _join(start); return wrap ? `(${prefix}${start})` : start; } return toRegexRange(start, end, options); }; const rangeError = (...args) => { // @ts-ignore return new RangeError('Invalid range arguments: ' + inspect(...args)); }; const invalidRange = (start, end, options: IOptions): string[] => { if (options.strictRanges === true) throw rangeError([start, end], options); return []; }; const invalidStep = (step, options: IOptions): string[] => { if (options.strictRanges === true) { throw new TypeError(`Expected step "${step}" to be a number`); } return []; }; function _handleLimit(options: IOptions) { return options.limit > 0 ? options.limit! : Infinity; } function _handleStep(step: number) { return Math.max(Math.abs(step), 1) } function _handleOptions(opts: IOptions, clone?: boolean) { if (clone === true) { opts = { ...opts }; } if (opts.capture === true) opts.wrap = true; return opts; } function _handleDescending(start: number, end: number, options: IOptions) { const descending = start > end; if (descending === true && options.strictOrder) { throw rangeError([start, end], options); } return descending } const fillNumbers = (start, end, step = 1, options: IOptions = {}): string[] | string => { let a = Number(start); let b = Number(end); if (!Number.isInteger(a) || !Number.isInteger(b)) { if (options.strictRanges === true) throw rangeError([start, end], options); return []; } // fix negative zero if (a === 0) a = 0; if (b === 0) b = 0; const descending = _handleDescending(a, b, options); const startString = String(start); const endString = String(end); const stepString = String(step); step = _handleStep(step); const padded = zeros(startString) || zeros(endString) || zeros(stepString); const maxLen = padded ? Math.max(startString.length, endString.length, stepString.length) : 0; const toNumber = padded === false && stringify(start, end, options) === false; const format = options.transform || transform(toNumber); if (options.toRegex && step === 1) { return toRange(toMaxLen(String(start), maxLen), toMaxLen(String(end), maxLen), true, options); } const parts: IParts = { negatives: [], positives: [] }; const push = num => parts[num < 0 ? 'negatives' : 'positives'].push(Math.abs(num)); const range: any[] = []; let index = 0; const limit = _handleLimit(options); while (descending ? a >= b : a <= b) { if (options.toRegex === true && step > 1) { push(a); } else { range.push(pad(format(a, index), maxLen, toNumber)); } a = descending ? a - step : a + step; index++; if (index >= limit) break; } if (options.toRegex === true) { return step > 1 ? toSequence(parts, options) : toRegex(range, null, { wrap: false, ...options }); } return range; }; function fillLetters(start, end, step: number, options: IOptions & { toRegex: true, }): string function fillLetters<V>(start, end, step?: number, options?: IOptions<V>): V[] | string function fillLetters(start, end, step = 1, options: IOptions = {}): any[] | string { if ((!isNumber(start) && start.length > 1) || (!isNumber(end) && end.length > 1)) { return invalidRange(start, end, options) as any; } const format = options.transform || (val => String.fromCharCode(val)); let a = `${start}`.charCodeAt(0); let b = `${end}`.charCodeAt(0); const descending = _handleDescending(a, b, options); const min = Math.min(a, b); const max = Math.max(a, b); if (options.toRegex === true && step === 1) { return toRange(min, max, false, options); } const range: any[] = []; let index = 0; const limit = _handleLimit(options); while (descending ? a >= b : a <= b) { range.push(format(a, index)); a = descending ? a - step : a + step; index++; if (index >= limit) break; } if (options.toRegex === true) { return toRegex(range, null, { wrap: false, ...options }); } return range; } export function fill<V = number | string>(start: number | string, end: number | string, step: IOptions<V> & { toRegex?: false, }, options?: never ): V[] export function fill<V = number | string>(start: number | string, end: number | string, step: number, options?: IOptions<V> & { toRegex?: false, } ): V[] export function fill<V = number | string>(start: number | string, end: number | string, step: IOptions<V>["transform"], options?: never ): V[] export function fill(start: number | string, end: number | string, step: IOptions & { toRegex: true, }, options?: IOptions ): string export function fill(start: number | string, end: number | string, step: number | IOptions["transform"], options: IOptions & { toRegex: true, } ): string export function fill<R extends any[] | string = string[] | string>(start: number | string, end?: number | string, step?: number | IOptions["transform"] | IOptions, options?: IOptions ): R export function fill(start: number | string, end?: number | string, step?: number | IOptions["transform"] | IOptions, options: IOptions = {}): any[] | string { const _s = isValidValue(start); if ((typeof end === 'undefined' || end === null) && _s) { return [start] as any; } if (!_s || !isValidValue(end)) { return invalidRange(start, end, options); } if (typeof step === 'function') { //return fill(start, end, 1, { transform: step }); [step, options] = [1, { transform: step }]; } if (isObject(step)) { //return fill(start, end, 0, step); [step, options] = [0, step]; } let opts: IOptions = options; step = step || opts.step || 1; if (!isNumber(step)) { if (step != null && !isObject(step)) return invalidStep(step, opts); //return fill(start, end, 1, step as IOptions); [step, opts] = [1, opts]; } opts = _handleOptions(opts, true); if (isNumber(start) && isNumber(end)) { return fillNumbers(start, end, step, opts); } return fillLetters(start, end, _handleStep(step), opts); } Object.defineProperty(fill, '__esModule', { value: true }); Object.defineProperty(fill, 'fill', { value: fill }); Object.defineProperty(fill, 'default', { value: fill }); export default fill;