indicium/simple/
tests.rs

1#![allow(clippy::cognitive_complexity)]
2#![allow(clippy::too_many_lines)]
3
4#[test]
5fn simple() {
6    use crate::simple::internal::string_keywords::SplitContext;
7    use crate::simple::{AutocompleteType, Indexable, SearchIndex, SearchType};
8    use kstring::KString;
9    use pretty_assertions::assert_eq;
10
11    #[derive(Clone, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
12    struct MyStruct {
13        title: String,
14        year: u16,
15        body: String,
16    }
17
18    impl Indexable for MyStruct {
19        fn strings(&self) -> Vec<String> {
20            vec![self.title.clone(), self.year.to_string(), self.body.clone()]
21        }
22    }
23
24    let my_vec = [
25        MyStruct {
26            title: "Harold Godwinson".to_string(),
27            year: 1066,
28            body: "Last crowned Anglo-Saxon king of England.".to_string(),
29        },
30        MyStruct {
31            title: "Edgar Ætheling".to_string(),
32            year: 1066,
33            body: "Last male member of the royal house of Cerdic of Wessex.".to_string(),
34        },
35        MyStruct {
36            title: "William the Conqueror".to_string(),
37            year: 1066,
38            body: "First Norman monarch of England.".to_string(),
39        },
40        MyStruct {
41            title: "William Rufus".to_string(),
42            year: 1087,
43            body: "Third son of William the Conqueror.".to_string(),
44        },
45        MyStruct {
46            title: "Henry Beauclerc".to_string(),
47            year: 1100,
48            body: "Fourth son of William the Conqueror.".to_string(),
49        }
50    ];
51
52    let mut search_index: SearchIndex<usize> = SearchIndex::default();
53
54    // Ensure that the default index splitting behaves as expected:
55
56    let string_keywords: Vec<KString> = search_index.string_keywords(
57        "All is not lost, the unconquerable will, and study of revenge, \
58        immortal hate, and the courage never to submit or yield.",
59        &SplitContext::Indexing,
60    );
61
62    assert_eq!(
63        string_keywords,
64        [
65            "all",
66            "is",
67            "not",
68            "lost",
69            "unconquerable",
70            "will",
71            "study",
72            "revenge",
73            "immortal",
74            "hate",
75            "courage",
76            "never",
77            "submit",
78            "yield"
79        ]
80    );
81
82    // Ensure that the default search splitting behaves as expected:
83
84    let string_keywords: Vec<KString> = search_index.string_keywords(
85        "He prayeth best, who loveth best All things both great and small; For \
86        the dear God who loveth us, He made and loveth all.",
87        &SplitContext::Searching,
88    );
89
90    assert_eq!(
91        string_keywords,
92        [
93            "he", "prayeth", "best", "who", "loveth", "best", "all", "things", "both", "great",
94            "small", "dear", "god", "who", "loveth", "us", "he", "made", "loveth", "all"
95        ]
96    );
97
98    // Ensure that the default index splitting removes very short and very long
99    // words:
100
101    let string_keywords: Vec<KString> = search_index.string_keywords(
102        "Digby was a floccinaucinihilipilificator at heart—which is an \
103        eight-dollar word meaning a joker who does not believe in anything he \
104        can't bite.",
105        &SplitContext::Indexing,
106    );
107
108    assert_eq!(
109        string_keywords,
110        [
111            "digby", "was", "heart", "which", "is", "eight", "dollar", "word", "meaning", "joker",
112            "who", "does", "not", "believe", "anything", "he", "can't", "bite"
113        ]
114    );
115
116    my_vec
117        .iter()
118        .enumerate()
119        .for_each(|(index, element)| search_index.insert(&index, element));
120
121    // Continue search tests:
122    let search_results = search_index.search("third william");
123    assert_eq!(search_results, vec![&3]);
124
125    let search_results = search_index.search_type(&SearchType::Keyword, "Wessex");
126    assert_eq!(search_results, vec![&1]);
127
128    // Search for `last` or `wessex`. `Edgar Ætheling` contains both keywords,
129    // so he should be returned first. `Harold Godwinson` only contains `last`
130    // so he should be returned last:
131    let search_results = search_index.search_type(&SearchType::Or, "last Wessex");
132    assert_eq!(search_results, vec![&1, &0]);
133
134    let search_results = search_index.search_type(&SearchType::Or, "last England");
135    assert_eq!(search_results, vec![&0, &1, &2]);
136
137    let search_results = search_index.search_type(&SearchType::And, "Conqueror third");
138    assert_eq!(search_results, vec![&3]);
139
140    let search_results = search_index.search_type(&SearchType::Live, "Last m");
141    assert_eq!(search_results, vec![&1]);
142
143    // Ensure that fuzzy matching is working with live searches:
144    #[cfg(any(feature = "eddie", feature = "rapidfuzz", feature = "strsim"))]
145    let search_results = search_index.search_type(&SearchType::Live, "1066 Harry");
146    #[cfg(any(feature = "eddie", feature = "rapidfuzz", feature = "strsim"))]
147    assert_eq!(search_results, vec![&0]);
148
149    let autocomplete_options = search_index.autocomplete_type(&AutocompleteType::Keyword, "E");
150    assert_eq!(
151        autocomplete_options,
152        vec![
153            "edgar".to_string(),
154            "edgar ætheling".to_string(),
155            "england".to_string()
156        ]
157    );
158
159    let autocomplete_options = search_index.autocomplete_type(&AutocompleteType::Global, "1100 e");
160    assert_eq!(
161        autocomplete_options,
162        vec![
163            "1100 edgar".to_string(),
164            "1100 edgar ætheling".to_string(),
165            "1100 england".to_string()
166        ]
167    );
168
169    // Test fuzzy-matching for global autocompletion:
170    #[cfg(any(feature = "eddie", feature = "rapidfuzz", feature = "strsim"))]
171    let autocomplete_options =
172        search_index.autocomplete_type(&AutocompleteType::Global, "1100 Englelund");
173    #[cfg(any(feature = "eddie", feature = "rapidfuzz", feature = "strsim"))]
174    assert!(autocomplete_options.contains(&"1100 england".to_string()));
175
176    // The only `w` keywords that `1087` should contain are `William` and
177    // `William Rufus`. `Wessex` exists in the index but it is not related to
178    // `1087`:
179    let autocomplete_options = search_index.autocomplete_type(&AutocompleteType::Context, "1087 W");
180    assert_eq!(
181        autocomplete_options,
182        vec!["1087 william".to_string(), "1087 william rufus".to_string()]
183    );
184
185    // Test fuzzy-matching for context autocompletion:
186    #[cfg(any(feature = "eddie", feature = "rapidfuzz", feature = "strsim"))]
187    let autocomplete_options =
188        search_index.autocomplete_type(&AutocompleteType::Context, "1087 Willy");
189    #[cfg(any(feature = "eddie", feature = "rapidfuzz", feature = "strsim"))]
190    assert_eq!(
191        autocomplete_options,
192        vec!["1087 william".to_string(), "1087 william rufus".to_string()]
193    );
194
195    // Ensure that `Context` autocomplete works with an empty search string /
196    // single keyword. Context autocomplete works in two parts - an `And` search
197    // for the preceding keywords, and an autocomplete for the last keyword:
198    let autocomplete_options = search_index.autocomplete_type(&AutocompleteType::Context, "108");
199    assert_eq!(autocomplete_options, vec!["1087".to_string()]);
200
201    // Test internal global fuzzy keyword search interface:
202    /* #[cfg(feature = "eddie")]
203    let similar_keyword = search_index.eddie_substitute(&"Willy".to_lowercase());
204    #[cfg(feature = "rapidfuzz")]
205    let similar_keyword = search_index.rapidfuzz_substitute(&"Willy".to_lowercase());
206    #[cfg(feature = "strsim")]
207    let similar_keyword = search_index.strsim_substitute(&"Willy".to_lowercase());
208    #[cfg(any(feature = "eddie", feature = "rapidfuzz", feature = "strsim"))]
209    assert_eq!(similar_keyword, Some("william")); */
210
211    // Test internal global fuzzy autocompletion interface:
212    #[cfg(feature = "eddie")]
213    let similar_autocompletions = search_index.eddie_global(&[], &"Normy".to_lowercase());
214    #[cfg(feature = "rapidfuzz")]
215    let similar_autocompletions = search_index.rapidfuzz_global(&[], &"Normy".to_lowercase());
216    #[cfg(feature = "strsim")]
217    let similar_autocompletions = search_index.strsim_global(&[], &"Normy".to_lowercase());
218    #[cfg(any(feature = "eddie", feature = "rapidfuzz", feature = "strsim"))]
219    let similar_autocompletions_vec: Vec<&KString> = similar_autocompletions
220        .into_iter()
221        .map(|(keyword, _keys)| keyword)
222        .collect();
223    #[cfg(any(feature = "eddie", feature = "rapidfuzz", feature = "strsim"))]
224    assert_eq!(similar_autocompletions_vec, vec![&"norman".to_string()]);
225
226    // Test `Indexable` trait implementation for `ToString` generics:
227
228    let my_vec: Vec<&str> = vec![
229        "Vopnafjarðarhreppur",                   // 0
230        "Weapon Fjord Municipality",             // 1
231        "Annerveenschekanaal",                   // 2
232        "Channel through the peat of Annen",     // 3
233        "Cadibarrawirracanna",                   // 4
234        "The stars were dancing",                // 5
235        "Newtownmountkennedy",                   // 6
236        "A new town near Mt. Kennedy",           // 7
237        "Cottonshopeburnfoot",                   // 8
238        "The end of the Cottonshope Burn",       // 9
239        "Nyugotszenterzsébet",                   // 10
240        "Western St. Elizabeth",                 // 11
241        "Balatonszentgyörgy",                    // 12
242        "St. George by Balaton",                 // 13
243        "Kirkjubæjarklaustur",                   // 14
244        "Church farm monastery",                 // 15
245        "Jászalsószentgyörgy",                   // 16
246        "Lower St. George in Jászság",           // 17
247        "Krammerjachtensluis",                   // 18
248        "Lock on the river Krammer of the hunt", // 19
249    ]; // vec!
250
251    let mut search_index: SearchIndex<usize> = SearchIndex::default();
252
253    my_vec
254        .iter()
255        .enumerate()
256        .for_each(|(index, element)| search_index.insert(&index, element));
257
258    // Keyword search:
259    let search_results = search_index.search_type(&SearchType::Keyword, "Cottonshope");
260    assert_eq!(search_results, vec![&9]);
261
262    // Or search:
263    let search_results = search_index.search_type(&SearchType::Or, "George Elizabeth");
264    assert_eq!(search_results, vec![&11, &13, &17]);
265
266    // And search:
267    let search_results = search_index.search_type(&SearchType::And, "George Jászság");
268    assert_eq!(search_results, vec![&17]);
269
270    // Live search:
271    let search_results = search_index.search_type(&SearchType::Live, "Geo");
272    assert_eq!(search_results, vec![&13, &17]);
273
274    // Fuzzy matching:
275    #[cfg(any(feature = "eddie", feature = "rapidfuzz", feature = "strsim"))]
276    let search_results = search_index.search_type(&SearchType::Live, "rivers");
277    #[cfg(any(feature = "eddie", feature = "rapidfuzz", feature = "strsim"))]
278    assert_eq!(search_results, vec![&19]);
279
280    // Fuzzy matching:
281    #[cfg(any(feature = "eddie", feature = "rapidfuzz", feature = "strsim"))]
282    let search_results = search_index.search_type(&SearchType::Live, "peat of Annan");
283    #[cfg(any(feature = "eddie", feature = "rapidfuzz", feature = "strsim"))]
284    assert_eq!(search_results, vec![&3]);
285
286    // Keyword autocomplete:
287    let autocomplete_options = search_index.autocomplete_type(&AutocompleteType::Keyword, "Chan");
288    assert_eq!(autocomplete_options, vec!["channel".to_string()]);
289
290    // Global autocomplete:
291    let autocomplete_options = search_index.autocomplete_type(&AutocompleteType::Global, "Lo");
292    assert_eq!(
293        autocomplete_options,
294        vec!["lock".to_string(), "lower".to_string()]
295    );
296
297    // Context autocomplete:
298    let autocomplete_options =
299        search_index.autocomplete_type(&AutocompleteType::Context, "Krammer Lo");
300    assert_eq!(autocomplete_options, vec!["krammer lock".to_string()]);
301
302    // Fuzzy matching context autocomplete:
303    #[cfg(any(feature = "eddie", feature = "rapidfuzz", feature = "strsim"))]
304    let autocomplete_options =
305        search_index.autocomplete_type(&AutocompleteType::Context, "stars are dancers");
306    #[cfg(any(feature = "eddie", feature = "rapidfuzz", feature = "strsim"))]
307    assert_eq!(autocomplete_options, vec!["stars are dancing".to_string()]);
308
309    // Test UTF-8:
310    let mut search_index = crate::simple::SearchIndex::<usize>::default();
311    search_index.insert(&0, &"лол"); // lol in Cyrillic
312    search_index.insert(&1, &"lol");
313    search_index.insert(&2, &"lol лол");
314    search_index.insert(&3, &"лол lol");
315    let search_results = search_index.search("лол");
316    assert_eq!(search_results, vec![&0, &2, &3]);
317} // fn
318
319#[test]
320fn simple_all() {
321    use crate::simple::SearchIndex;
322    #[cfg(any(feature = "strsim", feature = "eddie", feature = "rapidfuzz"))]
323    use crate::simple::SearchType;
324    #[cfg(any(feature = "strsim", feature = "eddie", feature = "rapidfuzz"))]
325    use pretty_assertions::assert_eq;
326
327    // The "all" test. Looks at 277 words that start with the prefix "all":
328
329    let all_vec: Vec<&str> = vec![
330        "allanite",         // 0
331        "allanites",        // 1
332        "allantoic",        // 2
333        "allantoid",        // 3
334        "allantoides",      // 4
335        "allantoids",       // 5
336        "allantoin",        // 6
337        "allantoins",       // 7
338        "allantois",        // 8
339        "allargando",       // 9
340        "allay",            // 10
341        "allayed",          // 11
342        "allayer",          // 12
343        "allayers",         // 13
344        "allaying",         // 14
345        "allays",           // 15
346        "allee",            // 16
347        "allees",           // 17
348        "allegation",       // 18
349        "allegations",      // 19
350        "allege",           // 20
351        "alleged",          // 21
352        "allegedly",        // 22
353        "alleger",          // 23
354        "allegers",         // 24
355        "alleges",          // 25
356        "allegiance",       // 26
357        "allegiances",      // 27
358        "allegiant",        // 28
359        "allegiants",       // 29
360        "alleging",         // 30
361        "allegoric",        // 31
362        "allegorical",      // 32
363        "allegorically",    // 33
364        "allegoricalness",  // 34
365        "allegories",       // 35
366        "allegorise",       // 36
367        "allegorised",      // 37
368        "allegorises",      // 38
369        "allegorising",     // 39
370        "allegorist",       // 40
371        "allegorists",      // 41
372        "allegorization",   // 42
373        "allegorizations",  // 43
374        "allegorize",       // 44
375        "allegorized",      // 45
376        "allegorizer",      // 46
377        "allegorizers",     // 47
378        "allegorizes",      // 48
379        "allegorizing",     // 49
380        "allegory",         // 50
381        "allegretto",       // 51
382        "allegrettos",      // 52
383        "allegro",          // 53
384        "allegros",         // 54
385        "allele",           // 55
386        "alleles",          // 56
387        "allelic",          // 57
388        "allelism",         // 58
389        "allelisms",        // 59
390        "allelomorph",      // 60
391        "allelomorphic",    // 61
392        "allelomorphism",   // 62
393        "allelomorphisms",  // 63
394        "allelomorphs",     // 64
395        "allelopathic",     // 65
396        "allelopathies",    // 66
397        "allelopathy",      // 67
398        "alleluia",         // 68
399        "alleluias",        // 69
400        "allemande",        // 70
401        "allemandes",       // 71
402        "allergen",         // 72
403        "allergenic",       // 73
404        "allergenicities",  // 74
405        "allergenicity",    // 75
406        "allergens",        // 76
407        "allergic",         // 77
408        "allergies",        // 78
409        "allergin",         // 79
410        "allergins",        // 80
411        "allergist",        // 81
412        "allergists",       // 82
413        "allergy",          // 83
414        "allethrin",        // 84
415        "allethrins",       // 85
416        "alleviant",        // 86
417        "alleviants",       // 87
418        "alleviate",        // 88
419        "alleviated",       // 89
420        "alleviates",       // 90
421        "alleviating",      // 91
422        "alleviation",      // 92
423        "alleviations",     // 93
424        "alleviator",       // 94
425        "alleviators",      // 95
426        "alley",            // 96
427        "alleys",           // 97
428        "alleyway",         // 98
429        "alleyways",        // 99
430        "allheal",          // 100
431        "allheals",         // 101
432        "alliable",         // 102
433        "alliaceous",       // 103
434        "alliance",         // 104
435        "alliances",        // 105
436        "allicin",          // 106
437        "allicins",         // 107
438        "allied",           // 108
439        "allies",           // 109
440        "alligator",        // 110
441        "alligators",       // 111
442        "alliterate",       // 112
443        "alliterated",      // 113
444        "alliterates",      // 114
445        "alliterating",     // 115
446        "alliteration",     // 116
447        "alliterations",    // 117
448        "alliterative",     // 118
449        "alliteratively",   // 119
450        "allium",           // 120
451        "alliums",          // 121
452        "alloantibodies",   // 122
453        "alloantibody",     // 123
454        "alloantigen",      // 124
455        "alloantigens",     // 125
456        "allobar",          // 126
457        "allobars",         // 127
458        "allocable",        // 128
459        "allocatable",      // 129
460        "allocate",         // 130
461        "allocated",        // 131
462        "allocates",        // 132
463        "allocating",       // 133
464        "allocation",       // 134
465        "allocations",      // 135
466        "allocator",        // 136
467        "allocators",       // 137
468        "allocution",       // 138
469        "allocutions",      // 139
470        "allod",            // 140
471        "allodia",          // 141
472        "allodial",         // 142
473        "allodium",         // 143
474        "allods",           // 144
475        "allogamies",       // 145
476        "allogamous",       // 146
477        "allogamy",         // 147
478        "allogeneic",       // 148
479        "allogenic",        // 149
480        "allograft",        // 150
481        "allografted",      // 151
482        "allografting",     // 152
483        "allografts",       // 153
484        "allograph",        // 154
485        "allographic",      // 155
486        "allographs",       // 156
487        "allometric",       // 157
488        "allometries",      // 158
489        "allometry",        // 159
490        "allomorph",        // 160
491        "allomorphic",      // 161
492        "allomorphism",     // 162
493        "allomorphisms",    // 163
494        "allomorphs",       // 164
495        "allonge",          // 165
496        "allonges",         // 166
497        "allonym",          // 167
498        "allonyms",         // 168
499        "allopath",         // 169
500        "allopathies",      // 170
501        "allopaths",        // 171
502        "allopathy",        // 172
503        "allopatric",       // 173
504        "allopatrically",   // 174
505        "allopatries",      // 175
506        "allopatry",        // 176
507        "allophane",        // 177
508        "allophanes",       // 178
509        "allophone",        // 179
510        "allophones",       // 180
511        "allophonic",       // 181
512        "alloplasm",        // 182
513        "alloplasms",       // 183
514        "allopolyploid",    // 184
515        "allopolyploids",   // 185
516        "allopolyploidy",   // 186
517        "allopurinol",      // 187
518        "allopurinols",     // 188
519        "allosaur",         // 189
520        "allosaurs",        // 190
521        "allosaurus",       // 191
522        "allosauruses",     // 192
523        "allosteric",       // 193
524        "allosterically",   // 194
525        "allosteries",      // 195
526        "allostery",        // 196
527        "allot",            // 197
528        "allotetraploid",   // 198
529        "allotetraploids",  // 199
530        "allotetraploidy",  // 200
531        "allotment",        // 201
532        "allotments",       // 202
533        "allotrope",        // 203
534        "allotropes",       // 204
535        "allotropic",       // 205
536        "allotropies",      // 206
537        "allotropy",        // 207
538        "allots",           // 208
539        "allotted",         // 209
540        "allottee",         // 210
541        "allottees",        // 211
542        "allotter",         // 212
543        "allotters",        // 213
544        "allotting",        // 214
545        "allotype",         // 215
546        "allotypes",        // 216
547        "allotypic",        // 217
548        "allotypically",    // 218
549        "allotypies",       // 219
550        "allotypy",         // 220
551        "allover",          // 221
552        "allovers",         // 222
553        "allow",            // 223
554        "allowable",        // 224
555        "allowables",       // 225
556        "allowably",        // 226
557        "allowance",        // 227
558        "allowanced",       // 228
559        "allowances",       // 229
560        "allowancing",      // 230
561        "allowed",          // 231
562        "allowedly",        // 232
563        "allowing",         // 233
564        "allows",           // 234
565        "alloxan",          // 235
566        "alloxans",         // 236
567        "alloy",            // 237
568        "alloyed",          // 238
569        "alloying",         // 239
570        "alloys",           // 240
571        "alls",             // 241
572        "allseed",          // 242
573        "allseeds",         // 243
574        "allspice",         // 244
575        "allspices",        // 245
576        "allude",           // 246
577        "alluded",          // 247
578        "alludes",          // 248
579        "alluding",         // 249
580        "allure",           // 250
581        "allured",          // 251
582        "allurement",       // 252
583        "allurements",      // 253
584        "allurer",          // 254
585        "allurers",         // 255
586        "allures",          // 256
587        "alluring",         // 257
588        "alluringly",       // 258
589        "allusion",         // 259
590        "allusions",        // 260
591        "allusive",         // 261
592        "allusively",       // 262
593        "allusiveness",     // 263
594        "allusivenesses",   // 264
595        "alluvia",          // 265
596        "alluvial",         // 266
597        "alluvials",        // 267
598        "alluvion",         // 268
599        "alluvions",        // 269
600        "alluvium",         // 270
601        "alluviums",        // 271
602        "ally",             // 272
603        "allying",          // 273
604        "allyl",            // 274
605        "allylic",          // 275
606        "allyls",           // 276
607    ]; // vec!
608
609    let mut search_index: SearchIndex<usize> = SearchIndex::default();
610
611    all_vec
612        .iter()
613        .enumerate()
614        .for_each(|(index, element)| search_index.insert(&index, element));
615
616    // Fuzzy matching:
617    #[cfg(any(feature = "eddie", feature = "rapidfuzz", feature = "strsim"))]
618    let search_results = search_index.search_type(&SearchType::Live, "ally");
619    #[cfg(any(feature = "eddie", feature = "rapidfuzz", feature = "strsim"))]
620    let search_results: Vec<&str> = search_results.into_iter().map(|key| all_vec[*key]).collect();
621    #[cfg(any(feature = "eddie", feature = "rapidfuzz", feature = "strsim"))]
622    assert_eq!(search_results, vec!["ally", "allying", "allyl", "allylic", "allyls"]);
623} // fn
624
625#[test]
626#[cfg(feature = "unicode-normalization")]
627fn unicode_normalization() {
628    use crate::simple::{SearchIndex, SearchType};
629    use pretty_assertions::assert_eq;
630
631    let mut search_index: SearchIndex<usize> = SearchIndex::default();
632
633    // Compatibility equivalents that NFKC should normalize:
634    search_index.insert(&0, &"file");           // ASCII "fi"
635    search_index.insert(&1, &"file");            // U+FB01 Latin small ligature fi
636    search_index.insert(&2, &"flood");           // U+FB02 Latin small ligature fl
637    search_index.insert(&3, &"flood");          // ASCII "fl"
638    search_index.insert(&4, &"①②③");           // Circled digits
639    search_index.insert(&5, &"123");            // ASCII digits
640    search_index.insert(&6, &"Ω resistor");     // U+2126 Ohm sign
641    search_index.insert(&7, &"Ω resistor");     // U+03A9 Greek capital omega
642    search_index.insert(&8, &"fullwidth");  // Fullwidth ASCII
643    search_index.insert(&9, &"fullwidth");      // ASCII
644
645    // Ligature searches should find both forms:
646    let results = search_index.search_type(&SearchType::Keyword, "file");
647    assert_eq!(results, vec![&0, &1]);
648
649    let results = search_index.search_type(&SearchType::Keyword, "file");
650    assert_eq!(results, vec![&0, &1]);
651
652    let results = search_index.search_type(&SearchType::Keyword, "flood");
653    assert_eq!(results, vec![&2, &3]);
654
655    // Ohm sign and Greek omega should match:
656    let results = search_index.search_type(&SearchType::Keyword, "resistor");
657    assert_eq!(results, vec![&6, &7]);
658
659    // Fullwidth and ASCII should match:
660    let results = search_index.search_type(&SearchType::Keyword, "fullwidth");
661    assert_eq!(results, vec![&8, &9]);
662
663    let results = search_index.search_type(&SearchType::Keyword, "fullwidth");
664    assert_eq!(results, vec![&8, &9]);
665}
666
667#[test]
668#[cfg(feature = "unicode-normalization")]
669fn unicode_normalization_case_insensitive() {
670    use crate::simple::{SearchIndex, SearchType};
671    use pretty_assertions::assert_eq;
672
673    let mut search_index: SearchIndex<usize> = SearchIndex::default();
674
675    // Case folding combined with normalization:
676    search_index.insert(&0, &"FILE");  // Fullwidth uppercase
677    search_index.insert(&1, &"file");       // Ligature lowercase
678    search_index.insert(&2, &"FILE");      // ASCII uppercase
679    search_index.insert(&3, &"file");      // ASCII lowercase
680
681    // All four should match regardless of search case:
682    let results = search_index.search_type(&SearchType::Keyword, "file");
683    assert_eq!(results, vec![&0, &1, &2, &3]);
684
685    let results = search_index.search_type(&SearchType::Keyword, "FILE");
686    assert_eq!(results, vec![&0, &1, &2, &3]);
687}
688
689#[test]
690#[cfg(feature = "unicode-normalization")]
691fn unicode_normalization_case_sensitive() {
692    use crate::simple::{SearchIndex, SearchIndexBuilder, SearchType};
693    use pretty_assertions::assert_eq;
694
695    let mut search_index: SearchIndex<usize> = SearchIndexBuilder::default()
696        .case_sensitive(true)
697        .build();
698
699    // Case folding combined with normalization:
700    search_index.insert(&0, &"FILE");  // Fullwidth uppercase
701    search_index.insert(&1, &"file");       // Ligature lowercase
702    search_index.insert(&2, &"FILE");      // ASCII uppercase
703    search_index.insert(&3, &"file");      // ASCII lowercase
704
705    // All four should match regardless of search case:
706    let results = search_index.search_type(&SearchType::Keyword, "file");
707    assert_eq!(results, vec![&1, &3]);
708
709    let results = search_index.search_type(&SearchType::Keyword, "FILE");
710    assert_eq!(results, vec![&0, &2]);
711}