@@ -284,3 +284,219 @@ describe('Order', () => {
284
284
}
285
285
} ) ;
286
286
} ) ;
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