Skip to content

Instantly share code, notes, and snippets.

@PseudoSky
Created February 9, 2024 18:05
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save PseudoSky/7e2ec441bc7fbbe6ae41aeedd68c14e2 to your computer and use it in GitHub Desktop.
Save PseudoSky/7e2ec441bc7fbbe6ae41aeedd68c14e2 to your computer and use it in GitHub Desktop.
Utility to create numeric range validators
const RegexRange = {
cache: {},
clearCache: () => (RegexRange.cache = {})
}
interface CacheEntry {
min: number
a: number
max: number
b: number
isPadded?: any
maxLen?: number
negatives?: any
positives?: any
result?: any
string?: any
}
interface Pattern {
pattern: string
digits: number[]
string?: string
}
/**
* Zip strings (`for in` can be used on string characters)
*/
const zip = (a, b) => {
let arr = []
for (let i = 0; i < a.length; i++) {
arr.push([a[i], b[i]])
}
return arr
}
const compare = (a, b) => {
return a > b ? 1 : b > a ? -1 : 0
}
const push = (arr, ele) => {
if (arr.indexOf(ele) === -1) { arr.push(ele); }
return arr
}
const contains = (arr, key, val) => {
for (let i = 0; i < arr.length; i++) {
if (arr[i][key] === val) {
return true
}
}
return false
}
const countNines = (min, len) => {
return String(min).slice(0, -len) + '9'.repeat(len)
}
const countZeros = (integer, zeros) => {
return integer - (integer % Math.pow(10, zeros))
}
const toQuantifier = (digits) => {
let start = digits[0]
let stop = digits[1] ? (`,${digits[1]}`) : ''
if (!stop && (!start || start === 1)) {
return ''
}
return `{${start}${stop}}`
}
const toCharacterClass = (a, b) => {
return `[${a + ((b - a === 1) ? '' : '-') + b}]`
}
const padding = (str) => {
return /^-?(0+)[1-9]/.exec(str)
}
const padZeros = (val, token) => {
if (token.isPadded) {
let diff = Math.abs(token.maxLen - String(val).length)
switch (diff) {
case 0:
return ''
case 1:
return '0'
default: {
return `0{${diff}}`
}
}
}
return val
}
const filterPatterns = (arr, comparison, prefix, intersection, options) => {
let res = []
for (let i = 0; i < arr.length; i++) {
let token = arr[i]
let ele = token.string
if (options.relaxZeros !== false) {
if (prefix === '-' && ele.charAt(0) === '0') {
if (ele.charAt(1) === '{') {
ele = `0*${ele.replace(/^0\{\d+\}/, '')}`
} else {
ele = `0*${ele.slice(1)}`
}
}
}
if (!intersection && !contains(comparison, 'string', ele)) {
res.push(prefix + ele)
}
if (intersection && contains(comparison, 'string', ele)) {
res.push(prefix + ele)
}
}
return res
}
const splitToRanges = (min, max) => {
min = Number(min)
max = Number(max)
let nines = 1
let stops = [max]
let stop = +countNines(min, nines)
while (min <= stop && stop <= max) {
stops = push(stops, stop)
nines += 1
stop = +countNines(min, nines)
}
let zeros = 1
stop = countZeros(max + 1, zeros) - 1
while (min < stop && stop <= max) {
stops = push(stops, stop)
zeros += 1
stop = countZeros(max + 1, zeros) - 1
}
stops.sort(compare)
return stops
}
/**
* Convert a range to a regex pattern
* @param {Number} `start`
* @param {Number} `stop`
* @return {String}
*/
const rangeToPattern = (start, stop, options): Pattern => {
if (start === stop) {
return {
pattern: String(start),
digits: [],
}
}
let zipped = zip(String(start), String(stop))
let len = zipped.length, i = -1
let pattern = ''
let digits = 0
while (++i < len) {
let numbers = zipped[i]
let startDigit = numbers[0]
let stopDigit = numbers[1]
if (startDigit === stopDigit) {
pattern += startDigit
} else if (startDigit !== '0' || stopDigit !== '9') {
pattern += toCharacterClass(startDigit, stopDigit)
} else {
digits += 1
}
}
if (digits) {
pattern += options.shorthand ? '\\d' : '[0-9]'
}
return { pattern: pattern, digits: [digits] }
}
const splitToPatterns = (min, max, token, options) => {
let ranges = splitToRanges(min, max)
let len = ranges.length
let idx = -1
let tokens = []
let start = min
let prev
while (++idx < len) {
let range = ranges[idx]
let obj = rangeToPattern(start, range, options)
let zeros = ''
if (!token.isPadded && prev && prev.pattern === obj.pattern) {
if (prev.digits.length > 1) {
prev.digits.pop()
}
prev.digits.push(obj.digits[0])
prev.string = prev.pattern + toQuantifier(prev.digits)
start = range + 1
continue
}
if (token.isPadded) {
zeros = padZeros(range, token)
}
obj.string = zeros + obj.pattern + toQuantifier(obj.digits)
tokens.push(obj)
start = range + 1
prev = obj
}
return tokens
}
const siftPatterns = (neg, pos, options) => {
let onlyNegative = filterPatterns(neg, pos, '-', false, options) || []
let onlyPositive = filterPatterns(pos, neg, '', false, options) || []
let intersected = filterPatterns(neg, pos, '-?', true, options) || []
let subpatterns = onlyNegative.concat(intersected).concat(onlyPositive)
return subpatterns.join('|')
}
const toRegexRange = (boundA, boundB, options) => {
let min = parseInt(boundA, 10)
let max = parseInt(boundB, 10)
// UNBOUNDED STRICTLY NUMBER
if (isNaN(min) && isNaN(max)) {
return '(-?[0-9]+)'
}
if (isNaN(min)) {
return toRegexRange(Number.MIN_SAFE_INTEGER, boundB, options)
}
if (isNaN(max)) {
return toRegexRange(boundA, Number.MAX_SAFE_INTEGER, options)
}
// BOUNDS OUT OF ORDER
if (!(isNaN(min) || isNaN(max)) && max < min) {
return toRegexRange(boundB, boundA, options)
}
if (max === Number.MAX_SAFE_INTEGER && min === Number.MIN_SAFE_INTEGER) {
return '(-?[0-9]+)'
}
// UNBOUNDED MAX
if (max === Number.MAX_SAFE_INTEGER) {
if (min === 0) {
return '([0-9]+)'
} else if (min < 0) {
return `(${toRegexRange(min, 0, options)}|([0-9]+))`
} else {
return `(${toRegexRange(min, min * 10, options)}[0-9]*)`
}
}
// UNBOUNDED MIN
if (min === Number.MIN_SAFE_INTEGER) {
if (min === 0) {
return '(-[0-9]+)'
} else if (max < 0) {
return `(${toRegexRange(max * 10, max, options)}[0-9]*)`
} else {
return `(${toRegexRange(0, max, options)}|(-[0-9]+))`
}
}
options = options || {}
let relax = String(options.relaxZeros)
let shorthand = String(options.shorthand)
let capture = String(options.capture)
let key = `${min}:${max}=${relax}${shorthand}${capture}`
if (RegexRange.cache.hasOwnProperty(key)) {
return RegexRange.cache[key].result
}
let a = Math.min(min, max)
let b = Math.max(min, max)
// DISTANCE OF 1 '(a|b)'
if (Math.abs(a - b) === 1) {
let result = `${min}|${max}`
if (options.capture) {
return `(${result})`
}
return result
}
let isPadded = padding(min) || padding(max)
let positives = []
let negatives = []
let token: CacheEntry = { min, max, a, b }
if (isPadded) {
token.isPadded = isPadded
token.maxLen = String(token.max).length
}
if (a < 0) {
let newMin = b < 0 ? Math.abs(b) : 1
let newMax = Math.abs(a)
negatives = splitToPatterns(newMin, newMax, token, options)
a = token.a = 0
}
if (b >= 0) {
positives = splitToPatterns(a, b, token, options)
}
token.negatives = negatives
token.positives = positives
token.result = siftPatterns(negatives, positives, options)
if (options.capture && (positives.length + negatives.length) > 1) {
token.result = `(${token.result})`
}
RegexRange.cache[key] = token
return token.result
}
/* escapePattern
* A function that replaces regular expression escape/reserved characters
* Used primarily for sanitizing search queries
*
* EXAMPLE:
* searchQuery="(([a-z]+))+(aaaaaaaaaa)+hacks"
* escapePattern(searchQuery)
*
* -> "\(\(\[a\-z\]\+\)\)\+\(aaaaaaaaaa\)\+hacks"
*/
export const escapePattern = (s: string) => {
return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&')
}
const regexAny = (patterns: string[]) => {
return new RegExp(patterns.join('|'))
}
/* mergePatterns
* -------------------
* Description: Takes an array of values and builds a regular expression that matches any of the values
*
* Returns: The return value is the string of the regular expression
*
* Example:
* mergePatterns([
* '.*sky.*',
* 'run away',
* 'and hide',
* ])
* -> "/.*sky.*|run away|and hide/"
*/
export const mergePatterns = (values: (string | number)[]) => {
const joinedPatterns = values.map(v => v.toString()).map(v => {
// extract the pertinent parts of the regex
const start = v.startsWith('/') ? 1 : 0
const end = v.endsWith('/') ? v.length - 1 : v.length
return v.slice(start, end)
}).join('|')
return `/${joinedPatterns}/`
}
/* rangeToRegex
* -------------------
* Description:
* Accepts two numbers or number like strings representing an upper and lower bound of a numeric range. The function returns a regular expression that will match any number within the range for string values.
*
* Returns: A regular expression
*
* Example:
*
* rangeToRegex(1, 90)
* -> /^([1-9]|[1-8][0-9]|90)$/
*
* rangeToRegex(null, 90)
* -> /^(([0-9]|[1-8][0-9]|90)|(-[0-9]+))$/
*
* rangeToRegex(90, null)
* /^((9[0-9]|[1-8][0-9]{2}|900)[0-9]*)$/
*
* rangeToRegex(-90, null)
* /^((-[1-9]|-[1-8][0-9]|-90|0)|([0-9]+))$/
*/
export const rangeToRegex = (n1?: string | number, n2?: string | number) => new RegExp('^' + toRegexRange(n1, n2, {
relaxZeros: false,
capture: true,
}) + '$')
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment