@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
text/typescript
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;