@@ -284,3 +284,219 @@ describe('Order', () => {
284284 }
285285 } ) ;
286286} ) ;
287+
288+ class StringPair {
289+ constructor (
290+ readonly s1 : string ,
291+ readonly s2 : string
292+ ) { }
293+ }
294+
295+ class StringPairGenerator {
296+ constructor ( private stringGenerator : StringGenerator ) { }
297+
298+ next ( ) : StringPair {
299+ const prefix = this . stringGenerator . next ( ) ;
300+ const s1 = prefix + this . stringGenerator . next ( ) ;
301+ const s2 = prefix + this . stringGenerator . next ( ) ;
302+ return new StringPair ( s1 , s2 ) ;
303+ }
304+ }
305+
306+ class StringGenerator {
307+ private static readonly DEFAULT_SURROGATE_PAIR_PROBABILITY = 0.33 ;
308+ private static readonly DEFAULT_MAX_LENGTH = 20 ;
309+
310+ private readonly rnd : Random ;
311+ private readonly surrogatePairProbability : number ;
312+ private readonly maxLength : number ;
313+
314+ constructor ( seed : number ) ;
315+ constructor ( rnd : Random , surrogatePairProbability : number , maxLength : number ) ;
316+ constructor (
317+ seedOrRnd : number | Random ,
318+ surrogatePairProbability ?: number ,
319+ maxLength ?: number
320+ ) {
321+ if ( typeof seedOrRnd === 'number' ) {
322+ this . rnd = new Random ( seedOrRnd ) ;
323+ this . surrogatePairProbability =
324+ StringGenerator . DEFAULT_SURROGATE_PAIR_PROBABILITY ;
325+ this . maxLength = StringGenerator . DEFAULT_MAX_LENGTH ;
326+ } else {
327+ this . rnd = seedOrRnd ;
328+ this . surrogatePairProbability = StringGenerator . validateProbability (
329+ surrogatePairProbability !
330+ ) ;
331+ this . maxLength = StringGenerator . validateLength ( maxLength ! ) ;
332+ }
333+ }
334+
335+ private static validateProbability ( probability : number ) : number {
336+ if ( ! Number . isFinite ( probability ) ) {
337+ throw new Error (
338+ `invalid surrogate pair probability: ${ probability } (must be between 0.0 and 1.0, inclusive)`
339+ ) ;
340+ } else if ( probability < 0.0 ) {
341+ throw new Error (
342+ `invalid surrogate pair probability: ${ probability } (must be greater than or equal to zero)`
343+ ) ;
344+ } else if ( probability > 1.0 ) {
345+ throw new Error (
346+ `invalid surrogate pair probability: ${ probability } (must be less than or equal to 1)`
347+ ) ;
348+ }
349+ return probability ;
350+ }
351+
352+ private static validateLength ( length : number ) : number {
353+ if ( length < 0 ) {
354+ throw new Error (
355+ `invalid maximum string length: ${ length } (must be greater than or equal to zero)`
356+ ) ;
357+ }
358+ return length ;
359+ }
360+
361+ next ( ) : string {
362+ const length = this . rnd . nextInt ( this . maxLength + 1 ) ;
363+ const sb = new StringBuilder ( ) ;
364+ while ( sb . length ( ) < length ) {
365+ const codePoint = this . nextCodePoint ( ) ;
366+ sb . appendCodePoint ( codePoint ) ;
367+ }
368+ return sb . toString ( ) ;
369+ }
370+
371+ private isNextSurrogatePair ( ) : boolean {
372+ return StringGenerator . nextBoolean ( this . rnd , this . surrogatePairProbability ) ;
373+ }
374+
375+ private static nextBoolean ( rnd : Random , probability : number ) : boolean {
376+ if ( probability === 0.0 ) {
377+ return false ;
378+ } else if ( probability === 1.0 ) {
379+ return true ;
380+ } else {
381+ return rnd . nextFloat ( ) < probability ;
382+ }
383+ }
384+
385+ private nextCodePoint ( ) : number {
386+ if ( this . isNextSurrogatePair ( ) ) {
387+ return this . nextSurrogateCodePoint ( ) ;
388+ } else {
389+ return this . nextNonSurrogateCodePoint ( ) ;
390+ }
391+ }
392+
393+ private nextSurrogateCodePoint ( ) : number {
394+ const highSurrogateMin = 0xd800 ;
395+ const highSurrogateMax = 0xdbff ;
396+ const lowSurrogateMin = 0xdc00 ;
397+ const lowSurrogateMax = 0xdfff ;
398+
399+ const highSurrogate = this . nextCodePointRange (
400+ highSurrogateMin ,
401+ highSurrogateMax
402+ ) ;
403+ const lowSurrogate = this . nextCodePointRange (
404+ lowSurrogateMin ,
405+ lowSurrogateMax
406+ ) ;
407+
408+ return ( highSurrogate - 0xd800 ) * 0x400 + ( lowSurrogate - 0xdc00 ) + 0x10000 ;
409+ }
410+
411+ private nextNonSurrogateCodePoint ( ) : number {
412+ let codePoint ;
413+ do {
414+ codePoint = this . nextCodePointRange ( 0 , 0xffff ) ; // BMP range
415+ } while ( codePoint >= 0xd800 && codePoint <= 0xdfff ) ; // Exclude surrogate range
416+
417+ return codePoint ;
418+ }
419+
420+ private nextCodePointRange ( min : number , max : number ) : number {
421+ const rangeSize = max - min + 1 ;
422+ const offset = this . rnd . nextInt ( rangeSize ) ;
423+ return min + offset ;
424+ }
425+ }
426+
427+ class Random {
428+ private seed : number ;
429+
430+ constructor ( seed : number ) {
431+ this . seed = seed ;
432+ }
433+
434+ nextInt ( max : number ) : number {
435+ this . seed = ( this . seed * 9301 + 49297 ) % 233280 ;
436+ const rnd = this . seed / 233280 ;
437+ return Math . floor ( rnd * max ) ;
438+ }
439+
440+ nextFloat ( ) : number {
441+ this . seed = ( this . seed * 9301 + 49297 ) % 233280 ;
442+ return this . seed / 233280 ;
443+ }
444+ }
445+
446+ class StringBuilder {
447+ private buffer : string [ ] = [ ] ;
448+
449+ append ( str : string ) : StringBuilder {
450+ this . buffer . push ( str ) ;
451+ return this ;
452+ }
453+
454+ appendCodePoint ( codePoint : number ) : StringBuilder {
455+ this . buffer . push ( String . fromCodePoint ( codePoint ) ) ;
456+ return this ;
457+ }
458+
459+ toString ( ) : string {
460+ return this . buffer . join ( '' ) ;
461+ }
462+
463+ length ( ) : number {
464+ return this . buffer . join ( '' ) . length ;
465+ }
466+ }
467+
468+ describe ( 'CompareUtf8Strings' , ( ) => {
469+ it ( 'compareUtf8Strings should return correct results' , ( ) => {
470+ const errors = [ ] ;
471+ const seed = Math . floor ( Math . random ( ) * Number . MAX_SAFE_INTEGER ) ;
472+ let passCount = 0 ;
473+ const stringGenerator = new StringGenerator ( new Random ( seed ) , 0.33 , 20 ) ;
474+ const stringPairGenerator = new StringPairGenerator ( stringGenerator ) ;
475+
476+ for ( let i = 0 ; i < 1000000 && errors . length < 10 ; i ++ ) {
477+ const { s1, s2} = stringPairGenerator . next ( ) ;
478+
479+ const actual = order . compareUtf8Strings ( s1 , s2 ) ;
480+ const expected = Buffer . from ( s1 , 'utf8' ) . compare ( Buffer . from ( s2 , 'utf8' ) ) ;
481+
482+ if ( actual === expected ) {
483+ passCount ++ ;
484+ } else {
485+ errors . push (
486+ `compareUtf8Strings(s1="${ s1 } ", s2="${ s2 } ") returned ${ actual } , ` +
487+ `but expected ${ expected } (i=${ i } , s1.length=${ s1 . length } , s2.length=${ s2 . length } )`
488+ ) ;
489+ }
490+ }
491+
492+ if ( errors . length > 0 ) {
493+ console . error (
494+ `${ errors . length } test cases failed, ${ passCount } test cases passed, seed=${ seed } ;`
495+ ) ;
496+ errors . forEach ( ( error , index ) =>
497+ console . error ( `errors[${ index } ]: ${ error } ` )
498+ ) ;
499+ throw new Error ( 'Test failed' ) ;
500+ }
501+ } ) . timeout ( 20000 ) ;
502+ } ) ;
0 commit comments