@@ -2,6 +2,7 @@ import 'package:fuzzy/fuzzy.dart';
2
2
import 'package:test/test.dart' ;
3
3
4
4
import 'fixtures/books.dart' ;
5
+ import 'fixtures/games.dart' ;
5
6
6
7
final defaultList = ['Apple' , 'Orange' , 'Banana' ];
7
8
final defaultOptions = FuzzyOptions (
@@ -11,6 +12,7 @@ final defaultOptions = FuzzyOptions(
11
12
maxPatternLength: 32 ,
12
13
isCaseSensitive: false ,
13
14
tokenSeparator: RegExp (r' +' ),
15
+ minTokenCharLength: 1 ,
14
16
findAllMatches: false ,
15
17
minMatchCharLength: 1 ,
16
18
shouldSort: true ,
@@ -30,6 +32,16 @@ Fuzzy setup({
30
32
);
31
33
}
32
34
35
+ Fuzzy <T > setupGeneric <T >({
36
+ List <T > itemList,
37
+ FuzzyOptions <T > options,
38
+ }) {
39
+ return Fuzzy <T >(
40
+ itemList,
41
+ options: options,
42
+ );
43
+ }
44
+
33
45
void main () {
34
46
group ('Empty list of strings' , () {
35
47
Fuzzy fuse;
@@ -50,7 +62,6 @@ void main() {
50
62
});
51
63
test ('empty result is returned' , () {
52
64
final result = fuse.search ('Bla' );
53
- print (result);
54
65
expect (result.isEmpty, true );
55
66
});
56
67
});
@@ -128,6 +139,25 @@ void main() {
128
139
});
129
140
});
130
141
142
+ group ('Include arrayIndex in result list' , () {
143
+ Fuzzy fuse;
144
+ setUp (() {
145
+ fuse = setup ();
146
+ });
147
+
148
+ test ('When performing a fuzzy search for the term "ran"' , () {
149
+ final result = fuse.search ('ran' );
150
+
151
+ expect (result.length, 2 , reason: 'we get a list of containing 2 items' );
152
+
153
+ expect (result[0 ].item, equals ('Orange' ));
154
+ expect (result[0 ].matches.single.arrayIndex, 1 );
155
+
156
+ expect (result[1 ].item, equals ('Banana' ));
157
+ expect (result[1 ].matches.single.arrayIndex, 2 );
158
+ });
159
+ });
160
+
131
161
group ('Weighted search on typed list' , () {
132
162
test ('When searching for the term "John Smith" with author weighted higher' ,
133
163
() {
@@ -210,6 +240,141 @@ void main() {
210
240
});
211
241
});
212
242
243
+ group ('Weighted search considers all keys in score' , () {
244
+ Fuzzy <Game > getFuzzy ({double tournamentWeight, double stageWeight}) {
245
+ return Fuzzy <Game >(
246
+ customGameList,
247
+ options: FuzzyOptions (
248
+ keys: [
249
+ WeightedKey (
250
+ getter: (i) => i.tournament,
251
+ weight: tournamentWeight,
252
+ name: 'tournament' ),
253
+ WeightedKey (
254
+ getter: (i) => i.stage, weight: stageWeight, name: 'stage' ),
255
+ ],
256
+ tokenize: true ,
257
+ ),
258
+ );
259
+ }
260
+
261
+ test ('When searching for "WorldCup Final", where weights are equal' , () {
262
+ final fuse = getFuzzy (
263
+ tournamentWeight: 0.5 ,
264
+ stageWeight: 0.5 ,
265
+ );
266
+ final result = fuse.search ('WorldCup Final' );
267
+
268
+ void expectLess (String a, String b) {
269
+ double scoreOf (String s) =>
270
+ result.singleWhere ((e) => e.item.toString () == s).score;
271
+ expect (scoreOf (a), lessThan (scoreOf (b)));
272
+ }
273
+
274
+ expectLess ('WorldCup Final' , 'WorldCup Semi-finals' );
275
+ expectLess ('WorldCup Semi-finals' , 'WorldCup Groups' );
276
+ expectLess ('WorldCup Groups' , 'ChampionsLeague Final' );
277
+ expectLess ('ChampionsLeague Final' , 'ChampionsLeague Semi-finals' );
278
+ });
279
+
280
+ test (
281
+ 'When searching for "WorldCup Final", where the tournament is weighted higher' ,
282
+ () {
283
+ final fuse = getFuzzy (
284
+ tournamentWeight: 0.8 ,
285
+ stageWeight: 0.2 ,
286
+ );
287
+ final result = fuse.search ('WorldCup Final' );
288
+
289
+ void expectLess (String a, String b) {
290
+ double scoreOf (String s) =>
291
+ result.singleWhere ((e) => e.item.toString () == s).score;
292
+ expect (scoreOf (a), lessThan (scoreOf (b)));
293
+ }
294
+
295
+ expectLess ('WorldCup Final' , 'WorldCup Semi-finals' );
296
+ expectLess ('WorldCup Semi-finals' , 'WorldCup Groups' );
297
+ expectLess ('WorldCup Groups' , 'ChampionsLeague Final' );
298
+ expectLess ('ChampionsLeague Final' , 'ChampionsLeague Semi-finals' );
299
+ });
300
+
301
+ test (
302
+ 'When searching for "WorldCup Final", where the stage is weighted higher' ,
303
+ () {
304
+ final fuse = getFuzzy (
305
+ tournamentWeight: 0.2 ,
306
+ stageWeight: 0.8 ,
307
+ );
308
+ final result = fuse.search ('WorldCup Final' );
309
+
310
+ void expectLess (String a, String b) {
311
+ double scoreOf (String s) =>
312
+ result.singleWhere ((e) => e.item.toString () == s).score;
313
+ expect (scoreOf (a), lessThan (scoreOf (b)));
314
+ }
315
+
316
+ expectLess ('WorldCup Final' , 'WorldCup Semi-finals' );
317
+ expectLess ('WorldCup Semi-finals' , 'WorldCup Groups' );
318
+ expectLess ('ChampionsLeague Final' , 'WorldCup Groups' );
319
+ expectLess ('ChampionsLeague Final' , 'ChampionsLeague Semi-finals' );
320
+ });
321
+ });
322
+
323
+ group ('Weighted search with a single key equals non-weighted search' , () {
324
+ String gameDescription (Game g) => '${g .tournament } ${g .stage }' ;
325
+
326
+ test ('When searching for "WorldCup semi-final"' , () {
327
+ final fuseNoKeys = Fuzzy (
328
+ customGameList.map ((g) => gameDescription (g)).toList (),
329
+ options: FuzzyOptions (),
330
+ );
331
+ Fuzzy fuseSingleKey = Fuzzy <Game >(
332
+ customGameList,
333
+ options: FuzzyOptions (
334
+ keys: [
335
+ WeightedKey (
336
+ name: 'desc' , getter: (g) => gameDescription (g), weight: 1 ),
337
+ ],
338
+ ),
339
+ );
340
+ final resultNoKeys = fuseNoKeys.search ('WorldCup semi-final' );
341
+ final resultSingleKey = fuseSingleKey.search ('WorldCup semi-final' );
342
+
343
+ // Check for equality using 'toString()', otherwise it checks for
344
+ // identity equality (i.e. same objects instead of same contents)
345
+ expect (resultNoKeys.toString (), equals (resultSingleKey.toString ()));
346
+
347
+ expect (resultNoKeys[0 ].item, 'WorldCup Semi-finals' );
348
+ expect (resultNoKeys[0 ].score, lessThan (resultNoKeys[1 ].score));
349
+ });
350
+ });
351
+
352
+ group ('FuzzyOptions normalizes the keys weights' , () {
353
+ test ("WeightedKey doesn't allow creating a non-positive weight" , () {
354
+ expect (
355
+ () => WeightedKey <String >(name: 'name' , getter: (i) => i, weight: - 1 ),
356
+ throwsA (isA <AssertionError >()));
357
+ expect (
358
+ () => WeightedKey <String >(name: 'name' , getter: (i) => i, weight: 0 ),
359
+ throwsA (isA <AssertionError >()));
360
+ expect (
361
+ () => WeightedKey <String >(name: 'name' , getter: (i) => i, weight: 1 ),
362
+ returnsNormally);
363
+ });
364
+
365
+ test ('Normalizes weights' , () {
366
+ var options = FuzzyOptions (keys: [
367
+ WeightedKey <String >(name: 'name1' , getter: (i) => i, weight: 0.5 ),
368
+ WeightedKey <String >(name: 'name2' , getter: (i) => i, weight: 0.5 ),
369
+ WeightedKey <String >(name: 'name3' , getter: (i) => i, weight: 3 ),
370
+ ]);
371
+
372
+ expect (options.keys[0 ].weight, 0.125 );
373
+ expect (options.keys[1 ].weight, 0.125 );
374
+ expect (options.keys[2 ].weight, 0.75 );
375
+ });
376
+ });
377
+
213
378
group (
214
379
'Search with match all tokens in a list of strings with leading and trailing whitespace' ,
215
380
() {
@@ -232,6 +397,56 @@ void main() {
232
397
});
233
398
});
234
399
400
+ group (
401
+ 'Search with tokenize where the search pattern starts or ends with the tokenSeparator' ,
402
+ () {
403
+ group ('With the default tokenSeparator, which is white space' , () {
404
+ Fuzzy fuse;
405
+ setUp (() {
406
+ fuse = setup (overwriteOptions: FuzzyOptions (tokenize: true ));
407
+ });
408
+
409
+ test ('When the search pattern starts with white space' , () {
410
+ final result = fuse.search (' Apple' );
411
+
412
+ expect (result.length, 1 , reason: 'we get a list of exactly 1 item' );
413
+ expect (result[0 ].item, equals ('Apple' ));
414
+ });
415
+
416
+ test ('When the search pattern ends with white space' , () {
417
+ final result = fuse.search ('Apple ' );
418
+
419
+ expect (result.length, 1 , reason: 'we get a list of exactly 1 item' );
420
+ expect (result[0 ].item, equals ('Apple' ));
421
+ });
422
+
423
+ test ('When the search pattern contains white space in the middle' , () {
424
+ final result = fuse.search ('Apple Orange' );
425
+
426
+ expect (result.length, 2 , reason: 'we get a list of exactly 2 itens' );
427
+ expect (result[0 ].item, equals ('Orange' ));
428
+ expect (result[1 ].item, equals ('Apple' ));
429
+ });
430
+ });
431
+
432
+ group ('With a custom tokenSeparator' , () {
433
+ Fuzzy fuse;
434
+ setUp (() {
435
+ fuse = setup (
436
+ overwriteOptions:
437
+ FuzzyOptions (tokenize: true , tokenSeparator: RegExp (';' )));
438
+ });
439
+
440
+ test ('When the search pattern ends with a tokenSeparator match' , () {
441
+ final result = fuse.search ('Apple;Orange;' );
442
+
443
+ expect (result.length, 2 , reason: 'we get a list of exactly 2 itens' );
444
+ expect (result[0 ].item, equals ('Orange' ));
445
+ expect (result[1 ].item, equals ('Apple' ));
446
+ });
447
+ });
448
+ });
449
+
235
450
group ('Search with match all tokens' , () {
236
451
Fuzzy fuse;
237
452
setUp (() {
@@ -271,6 +486,29 @@ void main() {
271
486
});
272
487
});
273
488
489
+ group ('Search with tokenize includes token average on result score' , () {
490
+ Fuzzy fuse;
491
+ setUp (() {
492
+ final customList = ['Apple and Orange Juice' ];
493
+ fuse = setup (
494
+ itemList: customList,
495
+ overwriteOptions: FuzzyOptions (threshold: 0.1 , tokenize: true ),
496
+ );
497
+ });
498
+
499
+ test ('When searching for the term "Apple Juice"' , () {
500
+ final result = fuse.search ('Apple Juice' );
501
+
502
+ // By using a lower threshold, we guarantee that the full text score
503
+ // ("apple juice" on "Apple and Orange Juice") returns a score of 1.0,
504
+ // while the token searches return 0.0 (perfect matches) for "Apple" and
505
+ // "Juice". Thus, the token score average is 0.0, and the result score
506
+ // should be (1.0 + 0.0) / 2 = 0.5
507
+ expect (result.length, 1 );
508
+ expect (result[0 ].score, 0.5 );
509
+ });
510
+ });
511
+
274
512
group ('Searching with default options' , () {
275
513
Fuzzy fuse;
276
514
setUp (() {
@@ -316,6 +554,45 @@ void main() {
316
554
});
317
555
});
318
556
557
+ group ('Searching with minTokenCharLength' , () {
558
+ Fuzzy <Book > setUp ({int minTokenCharLength}) => setupGeneric <Book >(
559
+ itemList: customBookList,
560
+ options: FuzzyOptions (
561
+ threshold: 0.3 ,
562
+ tokenize: true ,
563
+ minTokenCharLength: minTokenCharLength,
564
+ keys: [
565
+ WeightedKey (getter: (i) => i.title, weight: 0.5 , name: 'title' ),
566
+ WeightedKey (getter: (i) => i.author, weight: 0.5 , name: 'author' ),
567
+ ],
568
+ ),
569
+ );
570
+
571
+ test ('When searching for "Plants x Zombies" with min = 1' , () {
572
+ final fuse = setUp (minTokenCharLength: 1 );
573
+ final result = fuse.search ('Plants x Zombies' );
574
+
575
+ expect (result.length, 1 , reason: 'We get a match with 1 item' );
576
+ expect (result.single.item.author, 'John X' ,
577
+ reason: 'Due to the X on John X' );
578
+ });
579
+
580
+ test ('When searching for "Plants x Zombies" with min = 2' , () {
581
+ final fuse = setUp (minTokenCharLength: 2 );
582
+ final result = fuse.search ('Plants x Zombies' );
583
+
584
+ expect (result.length, 0 , reason: 'We get no matches' );
585
+ });
586
+
587
+ test ('When searching for a pattern smaller than the length' , () {
588
+ final fuse = setUp (minTokenCharLength: 100 );
589
+ final result = fuse.search ('John' );
590
+
591
+ expect (result.length, 3 ,
592
+ reason: 'We still get matches because of full text search' );
593
+ });
594
+ });
595
+
319
596
group ('Searching with minCharLength' , () {
320
597
Fuzzy fuse;
321
598
setUp (() {
0 commit comments