@randsum/notation
Version:
Dice notation parser and types for the @randsum ecosystem
78 lines (70 loc) • 2.48 kB
text/typescript
import { coreNotationPattern } from './coreNotationPattern'
import type { DiceNotation } from './types'
import { suggestNotationFix } from './suggestions'
import { buildNotationPattern } from './parse/parseModifiers'
// Cache the complete pattern since schemas never change at runtime
// eslint-disable-next-line no-restricted-syntax
let cachedPattern: RegExp | null = null
/**
* Get the complete notation pattern (core notation + all modifier patterns).
* Caches the RegExp and resets lastIndex before each use.
*/
function getCompleteNotationPattern(): RegExp {
cachedPattern ??= new RegExp(
[coreNotationPattern.source, buildNotationPattern().source].join('|'),
'g'
)
cachedPattern.lastIndex = 0
return cachedPattern
}
/**
* Type guard that checks if a value is valid dice notation.
*
* @param argument - Value to check
* @returns True if argument is valid dice notation, false otherwise
*
* @example
* ```ts
* if (isDiceNotation("4d6L")) {
* // TypeScript knows this is DiceNotation here
* }
* ```
*/
export function isDiceNotation(argument: unknown): argument is DiceNotation {
if (typeof argument !== 'string') return false
const trimmedArg = argument.trim()
const basicTest = coreNotationPattern.test(trimmedArg)
if (!basicTest) return false
const cleanArg = trimmedArg.replace(/\s/g, '')
const remaining = cleanArg.replaceAll(getCompleteNotationPattern(), '')
return remaining.length === 0
}
/**
* Error thrown when a string is not valid dice notation.
*/
export class NotationParseError extends Error {
public readonly code = 'INVALID_NOTATION' as const
public readonly suggestion: string | undefined
constructor(notation: string, reason: string, suggestion?: string) {
const message = suggestion
? `Invalid notation "${notation}": ${reason}. Did you mean "${suggestion}"?`
: `Invalid notation "${notation}": ${reason}`
super(message)
this.name = 'NotationParseError'
this.suggestion = suggestion
}
}
/**
* Validates a string as DiceNotation, throwing if invalid.
*
* @param input - String to validate
* @returns The input narrowed to DiceNotation
* @throws NotationParseError if input is not valid dice notation
*/
export function notation(input: string): DiceNotation {
if (!isDiceNotation(input)) {
const suggestion = suggestNotationFix(input)
throw new NotationParseError(input, 'String does not match dice notation pattern', suggestion)
}
return input
}