const FIRST_CHAR = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" ; const OTHER_CHAR = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_" ; const FIRST_CHAR_INSENSITIVE = "abcdefghijklmnopqrstuvwxyz" ; const OTHER_CHAR_INSENSITIVE = "0123456789abcdefghijklmnopqrstuvwxyz-_" ; function identChar ( insensitive , c , i ) { return i === 0 ? ( insensitive ? FIRST_CHAR_INSENSITIVE : FIRST_CHAR ) [ c ] : ( insensitive ? OTHER_CHAR_INSENSITIVE : OTHER_CHAR ) [ c ] ; } function increment ( insensitive , counters , i ) { let c = counters [ i ] + 1 ; let carry = false ; if ( i === 0 && c === ( insensitive ? 26 : 52 ) ) { c = 0 ; carry = true ; } else if ( i > 0 && c === ( insensitive ? 38 : 64 ) ) { c = 0 ; carry = true ; } counters [ i ] = c ; return carry ; } function countersForInteger ( caseInsensitive , integer ) { integer = Math . round ( integer ) ; let firstDigitBase = caseInsensitive ? 26 : 52 ; let otherDigitBase = caseInsensitive ? 38 : 64 ; let numDigits = 0 ; let maxNumber = 0 ; let penultimateMax = 0 ; while ( integer > maxNumber ) { penultimateMax = maxNumber ; numDigits ++ ; maxNumber += firstDigitBase * Math . pow ( otherDigitBase , numDigits - 1 ) ; } let counters = new Array ( numDigits ) ; integer -= penultimateMax + 1 ; for ( let c = 0 ; c < numDigits ; c ++ ) { counters [ numDigits - c - 1 ] = ( integer % otherDigitBase ) ; integer = Math . floor ( integer / otherDigitBase ) ; } return counters ; } function integerForCounters ( counters , caseInsensitive ) { let firstDigitBase = caseInsensitive ? 26 : 52 ; let otherDigitBase = caseInsensitive ? 38 : 64 ; let total = 0 ; for ( let i = 0 ; i < counters . length ; i ++ ) { total += ( counters [ i ] + ( i === 0 ? 1 : 0 ) ) * Math . pow ( otherDigitBase , counters . length - i - 1 ) ; } for ( let i = 0 ; i < counters . length - 1 ; i ++ ) { total -= ( otherDigitBase - firstDigitBase - 1 ) * Math . pow ( otherDigitBase , i ) ; } return total ; } export class IdentGenerator { private caseInsensitive : boolean ; lastIdent : string ; private maxIdentCount : number ; private startValue : number ; returnedIdents : Array < string > ; reservedIdents : Set < string > ; private counters : Array < number > ; private identChar : ( c : number , i : number ) => string ; private increment : ( counters : Array < number > , i : number ) => boolean ; constructor ( caseInsensitive = false , startValue = 1 , maxIdentCount = Infinity ) { if ( startValue < 1 ) { throw new RangeError ( "startValue must be at least 1" ) ; } this . caseInsensitive = caseInsensitive ; this . startValue = startValue ; this . maxIdentCount = maxIdentCount ; this . counters = countersForInteger ( caseInsensitive , startValue ) ; this . returnedIdents = [ ] ; this . reservedIdents = new Set ( ) ; this . identChar = identChar . bind ( null , caseInsensitive ) ; this . increment = increment . bind ( null , caseInsensitive ) ; } get currentValue ( ) : number { return integerForCounters ( this . counters , this . caseInsensitive ) ; } nextIdent ( ) { if ( this . returnedIdents . length > 0 ) { return this . returnedIdents . pop ( ) ! ; } let ident ; while ( this . isReserved ( ident = this . generateNextIdent ( ) ) ) { } if ( this . maxIdentCount !== Infinity ) { let identCount = this . currentValue - this . startValue ; if ( identCount > this . maxIdentCount ) { throw new Error ( `Too many identifiers were generated (Max: ${ this . maxIdentCount }).` ) ; } } return this . lastIdent = ident ; } private generateNextIdent ( ) { let nextIdent = this . counters . map ( this . identChar ) . join ( "" ) ; let carry = false ; for ( let i = this . counters . length - 1 ; i >= 0 ; i -- ) { carry = this . increment ( this . counters , i ) ; if ( ! carry ) { break ; } } if ( carry ) this . counters . push ( 0 ) ; return nextIdent ; } /** An iterator that produces an infinite sequence of identifiers. */ * idents ( ) { yield this . nextIdent ( ) ; } /**
     * When a generated ident is no longer in use, it should be returned
     * so it can be re-used.
     */ returnIdent ( ident ) { this . returnedIdents . push ( ident ) ; } reserve ( ... idents ) { for ( let ident of idents ) { this . reservedIdents . add ( ident ) ; } } isReserved ( ident ) { return this . reservedIdents . has ( ident ) ; } } interface IdentGeneratorOptions < Namespace extends string = string > { namespaces : Array < Namespace > ; /**
     * Whether to use case-insensitive identifiers.
     */ caseInsensitive ? : boolean ; /**
     * Sets the starting value. This is a standard base-10 number that is
     * converted to a corresponding identifier.
     *
     * An integer greater than or equal to 1.
     * Defaults to 1.
     */ startValue ? : number ; /**
     * How many identifiers each ident generator should be allowed to produce.
     * Note that if any of the produced identifiers are reserved, the actual
     * number of identifiers returned will be less than the max.
     * Defaults to Infinity.
     */ maxIdentCount ? : number ; } export class IdentGenerators < Namespace extends string = string > { namespaces : { [ name ] : IdentGenerator ; } ; constructor ( options ) { this . namespaces = { } ; options . namespaces . forEach ( ns => { this . namespaces [ ns ] = new IdentGenerator ( ! ! options . caseInsensitive , options . startValue || 1 , options . maxIdentCount || Infinity ) ; } ) ; } /**
     * Look up the ident generator for the given namespace. Raise an error if no
     * no such namespace exists.
     */ get ( namespace ) { let generator = this . namespaces [ namespace ] ; if ( generator ) { return generator ; } else { throw new Error ( "unknown ident namespace: " + namespace ) ; } } /**
     * Generate an ident from the given namespace.
     */ nextIdent ( namespace ) { return this . get ( namespace ) . nextIdent ( ) ; } /**
     * Return an ident to be generated for the next requested ident.
     */ returnIdent ( namespace , ident ) { this . get ( namespace ) . returnIdent ( ident ) ; } param namespace param idents reserve ( namespace , ... idents ) { this . get ( namespace ) . reserve ( ... idents ) ; } }