ex-cli 1.22.0

Command line tool to find, filter, sort and list files.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
use crate::cli::order::{DirKind, OrderKind};
use crate::config::Config;
use crate::fs::file::File;
use itertools::{EitherOrBoth, Itertools, ZipLongest};
use std::cmp::Ordering;
use std::path::Components;

pub struct Sorter<'a> {
    config: &'a Config,
}

impl<'a> Sorter<'a> {
    pub fn new(config: &'a Config) -> Self {
        Self { config }
    }

    pub fn sort_files(&self, files: &mut Vec<File>) {
        files.sort_unstable_by(|x, y| self.cmp_files(x, y));
    }

    fn cmp_files(&self, left: &File, right: &File) -> Ordering {
        for order in self.config.sort_order() {
            let result = match order {
                OrderKind::Dir => Self::cmp_dirs(left, right),
                OrderKind::Group => Self::cmp_groups(left, right),
                OrderKind::Name => Self::cmp_names(left, right),
                OrderKind::Ext => Self::cmp_exts(left, right),
                OrderKind::Size(dir) => Self::cmp_sizes(left, right, dir),
                OrderKind::Time(dir) => Self::cmp_times(left, right, dir),
            };
            if result != Ordering::Equal {
                return result;
            }
        }
        Ordering::Equal
    }

    fn cmp_dirs(left: &File, right: &File) -> Ordering {
        for pair in Self::zip_dirs(left, right) {
            let result = match pair {
                EitherOrBoth::Both(left, right) => {
                    let left = left.as_os_str().to_str().unwrap_or_default();
                    let right = right.as_os_str().to_str().unwrap_or_default();
                    Self::cmp_strings(left, right)
                }
                EitherOrBoth::Left(_) => Ordering::Greater,
                EitherOrBoth::Right(_) => Ordering::Less,
            };
            if result != Ordering::Equal {
                return result;
            }
        }
        Self::cmp_groups(left, right)
    }

    fn zip_dirs<'b>(left: &'b File, right: &'b File) -> ZipLongest<Components<'b>, Components<'b>> {
        let left = left.abs_dir.components();
        let right = right.abs_dir.components();
        left.zip_longest(right)
    }

    fn cmp_groups(left: &File, right: &File) -> Ordering {
        let left = left.group_dir_before_file();
        let right = right.group_dir_before_file();
        left.cmp(&right)
    }

    fn cmp_names(left: &File, right: &File) -> Ordering {
        Self::cmp_strings(&left.file_name, &right.file_name)
    }

    fn cmp_exts(left: &File, right: &File) -> Ordering {
        Self::cmp_strings(&left.file_ext, &right.file_ext)
    }

    fn cmp_strings(left: &str, right: &str) -> Ordering {
        let result = natord::compare_ignore_case(left, right);
        if result != Ordering::Equal {
            return result;
        }
        let result = natord::compare(left, right);
        if result != Ordering::Equal {
            return result;
        }
        Ordering::Equal
    }

    fn cmp_sizes(left: &File, right: &File, dir: &DirKind) -> Ordering {
        let result = left.file_size.cmp(&right.file_size);
        match dir {
            DirKind::Asc => result,
            DirKind::Desc => result.reverse(),
        }
    }

    fn cmp_times(left: &File, right: &File, dir: &DirKind) -> Ordering {
        let result = left.file_time.cmp(&right.file_time);
        match dir {
            DirKind::Asc => result,
            DirKind::Desc => result.reverse(),
        }
    }
}

// noinspection RsLift
#[cfg(test)]
mod tests {
    use crate::cli::file::FileKind;
    use crate::cli::order::{DirKind, OrderKind};
    use crate::config::Config;
    use crate::fs::file::File;
    use crate::sorter::Sorter;
    use chrono::{DateTime, Utc};
    use googletest::prelude::*;
    use std::ffi::OsStr;
    use std::path::{PathBuf, MAIN_SEPARATOR_STR};

    #[gtest]
    fn test_files_are_sorted_by_path() {
        let files = create_files();
        let paths = sort_files(files, vec![OrderKind::Dir, OrderKind::Name]);
        expect_that!(paths, elements_are![
            "index",
            "cheese/",
            "cheese/index",
            "cheese/english/",
            "cheese/english/cheddar.xy",
            "cheese/english/stilton.ij",
            "cheese/french/",
            "cheese/french/bleu.xy",
            "cheese/french/brie.ij",
            "fruit/",
            "fruit/citrus/",
            "fruit/citrus/lemon.xy",
            "fruit/citrus/orange.ij",
            "fruit/other/",
            "fruit/other/apple.xy",
            "fruit/other/banana.ij",
            "fruit/other/cherry.xy",
            "fruit/other/date.ij",
            "fruit.zip/",
            "fruit.zip/citrus/",
            "fruit.zip/citrus/lemon.xy",
            "fruit.zip/citrus/orange.ij",
            "fruit.zip/other/",
            "fruit.zip/other/apple.xy",
            "fruit.zip/other/banana.ij",
            "fruit.zip/other/cherry.xy",
            "fruit.zip/other/date.ij",
        ]);
    }

    #[gtest]
    fn test_files_are_sorted_by_dir_and_size() {
        let files = create_files();
        let paths = sort_files(files, vec![OrderKind::Dir, OrderKind::Size(DirKind::Asc), OrderKind::Name]);
        expect_that!(paths, elements_are![
            "index",                      //    42
            "cheese/",                    //     0
            "cheese/index",               //    99
            "cheese/english/",            //     0
            "cheese/english/stilton.ij",  //    91
            "cheese/english/cheddar.xy",  //  6363
            "cheese/french/",             //     0
            "cheese/french/brie.ij",      //  8681
            "cheese/french/bleu.xy",      // 20122
            "fruit/",                     //     0
            "fruit/citrus/",              //     0
            "fruit/citrus/lemon.xy",      //   108
            "fruit/citrus/orange.ij",     //   173
            "fruit/other/",               //     0
            "fruit/other/date.ij",        //     6
            "fruit/other/banana.ij",      //    73
            "fruit/other/apple.xy",       //   467
            "fruit/other/cherry.xy",      //   795
            "fruit.zip/",                 //     0
            "fruit.zip/citrus/",          //     0
            "fruit.zip/citrus/lemon.xy",  //   108
            "fruit.zip/citrus/orange.ij", //   173
            "fruit.zip/other/",           //     0
            "fruit.zip/other/date.ij",    //     6
            "fruit.zip/other/banana.ij",  //    73
            "fruit.zip/other/apple.xy",   //   467
            "fruit.zip/other/cherry.xy",  //   795
        ]);
    }

    #[gtest]
    fn test_files_are_sorted_by_dir_and_time() {
        let files = create_files();
        let paths = sort_files(files, vec![OrderKind::Dir, OrderKind::Time(DirKind::Asc), OrderKind::Name]);
        expect_that!(paths, elements_are![
            "index",                      // 2023-07-05 13:05:42
            "cheese/",                    // 2023-01-16 11:25:51
            "cheese/index",               // 2022-12-23 21:43:18
            "cheese/english/",            // 2023-08-04 10:57:26
            "cheese/english/cheddar.xy",  // 2022-09-10 04:03:38
            "cheese/english/stilton.ij",  // 2023-04-07 03:08:29
            "cheese/french/",             // 2022-10-06 23:36:27
            "cheese/french/bleu.xy",      // 2022-10-28 05:49:07
            "cheese/french/brie.ij",      // 2023-02-28 11:01:59
            "fruit/",                     // 2023-04-18 17:09:31
            "fruit/citrus/",              // 2023-07-13 08:58:00
            "fruit/citrus/orange.ij",     // 2023-03-27 05:45:33
            "fruit/citrus/lemon.xy",      // 2023-06-29 21:16:08
            "fruit/other/",               // 2023-08-14 01:53:00
            "fruit/other/date.ij",        // 2022-11-25 14:42:53
            "fruit/other/cherry.xy",      // 2022-12-19 02:12:38
            "fruit/other/apple.xy",       // 2023-05-10 11:24:22
            "fruit/other/banana.ij",      // 2023-06-10 02:06:38
            "fruit.zip/",                 // 2023-04-18 17:09:31
            "fruit.zip/citrus/",          // 2023-07-13 08:58:00
            "fruit.zip/citrus/orange.ij", // 2023-03-27 05:45:33
            "fruit.zip/citrus/lemon.xy",  // 2023-06-29 21:16:08
            "fruit.zip/other/",           // 2023-08-14 01:53:00
            "fruit.zip/other/date.ij",    // 2022-11-25 14:42:53
            "fruit.zip/other/cherry.xy",  // 2022-12-19 02:12:38
            "fruit.zip/other/apple.xy",   // 2023-05-10 11:24:22
            "fruit.zip/other/banana.ij",  // 2023-06-10 02:06:38
        ]);
    }

    #[gtest]
    fn test_files_are_sorted_by_name() {
        let files = create_files();
        let paths = sort_files(files, vec![OrderKind::Name, OrderKind::Dir]);
        expect_that!(paths, elements_are![
            "cheese/",                    // -
            "cheese/english/",            // -
            "cheese/french/",             // -
            "fruit/",                     // -
            "fruit/citrus/",              // -
            "fruit/other/",               // -
            "fruit.zip/",                 // -
            "fruit.zip/citrus/",          // -
            "fruit.zip/other/",           // -
            "fruit/other/apple.xy",       // apple.xy
            "fruit.zip/other/apple.xy",   // apple.xy
            "fruit/other/banana.ij",      // banana.ij
            "fruit.zip/other/banana.ij",  // banana.ij
            "cheese/french/bleu.xy",      // bleu.xy
            "cheese/french/brie.ij",      // brie.ij
            "cheese/english/cheddar.xy",  // cheddar.xy
            "fruit/other/cherry.xy",      // cherry.xy
            "fruit.zip/other/cherry.xy",  // cherry.xy
            "fruit/other/date.ij",        // date.ij
            "fruit.zip/other/date.ij",    // date.ij
            "index",                      // index
            "cheese/index",               // index
            "fruit/citrus/lemon.xy",      // lemon.xy
            "fruit.zip/citrus/lemon.xy",  // lemon.xy
            "fruit/citrus/orange.ij",     // orange.ij
            "fruit.zip/citrus/orange.ij", // orange.ij
            "cheese/english/stilton.ij",  // stilton.ij
        ]);
    }

    #[gtest]
    fn test_files_are_sorted_by_ext() {
        let files = create_files();
        let paths = sort_files(files, vec![OrderKind::Ext, OrderKind::Dir, OrderKind::Name]);
        expect_that!(paths, elements_are![
            "index",                      // -
            "cheese/",                    // -
            "cheese/index",               // -
            "cheese/english/",            // -
            "cheese/french/",             // -
            "fruit/",                     // -
            "fruit/citrus/",              // -
            "fruit/other/",               // -
            "fruit.zip/",                 // -
            "fruit.zip/citrus/",          // -
            "fruit.zip/other/",           // -
            "cheese/english/stilton.ij",  // .ij
            "cheese/french/brie.ij",      // .ij
            "fruit/citrus/orange.ij",     // .ij
            "fruit/other/banana.ij",      // .ij
            "fruit/other/date.ij",        // .ij
            "fruit.zip/citrus/orange.ij", // .ij
            "fruit.zip/other/banana.ij",  // .ij
            "fruit.zip/other/date.ij",    // .ij
            "cheese/english/cheddar.xy",  // .xy
            "cheese/french/bleu.xy",      // .xy
            "fruit/citrus/lemon.xy",      // .xy
            "fruit/other/apple.xy",       // .xy
            "fruit/other/cherry.xy",      // .xy
            "fruit.zip/citrus/lemon.xy",  // .xy
            "fruit.zip/other/apple.xy",   // .xy
            "fruit.zip/other/cherry.xy",  // .xy
        ]);
    }

    #[gtest]
    fn test_files_are_sorted_by_ext_and_size() {
        let files = create_files();
        let paths = sort_files(files, vec![OrderKind::Ext, OrderKind::Size(DirKind::Asc), OrderKind::Dir, OrderKind::Name]);
        expect_that!(paths, elements_are![
            "cheese/",                    // -       0
            "cheese/english/",            // -       0
            "cheese/french/",             // -       0
            "fruit/",                     // -       0
            "fruit/citrus/",              // -       0
            "fruit/other/",               // -       0
            "fruit.zip/",                 // -       0
            "fruit.zip/citrus/",          // -       0
            "fruit.zip/other/",           // -       0
            "index",                      // -      42
            "cheese/index",               // -      99
            "fruit/other/date.ij",        // .ij     6
            "fruit.zip/other/date.ij",    // .ij     6
            "fruit/other/banana.ij",      // .ij    73
            "fruit.zip/other/banana.ij",  // .ij    73
            "cheese/english/stilton.ij",  // .ij    91
            "fruit/citrus/orange.ij",     // .ij   173
            "fruit.zip/citrus/orange.ij", // .ij   173
            "cheese/french/brie.ij",      // .ij  8681
            "fruit/citrus/lemon.xy",      // .xy   108
            "fruit.zip/citrus/lemon.xy",  // .xy   108
            "fruit/other/apple.xy",       // .xy   467
            "fruit.zip/other/apple.xy",   // .xy   467
            "fruit/other/cherry.xy",      // .xy   795
            "fruit.zip/other/cherry.xy",  // .xy   795
            "cheese/english/cheddar.xy",  // .xy  6363
            "cheese/french/bleu.xy",      // .xy 20122
        ]);
    }

    #[gtest]
    fn test_files_are_sorted_by_size_asc() {
        let files = create_files();
        let paths = sort_files(files, vec![OrderKind::Size(DirKind::Asc), OrderKind::Dir, OrderKind::Name]);
        expect_that!(paths, elements_are![
            "cheese/",                    //     0
            "cheese/english/",            //     0
            "cheese/french/",             //     0
            "fruit/",                     //     0
            "fruit/citrus/",              //     0
            "fruit/other/",               //     0
            "fruit.zip/",                 //     0
            "fruit.zip/citrus/",          //     0
            "fruit.zip/other/",           //     0
            "fruit/other/date.ij",        //     6
            "fruit.zip/other/date.ij",    //     6
            "index",                      //    42
            "fruit/other/banana.ij",      //    73
            "fruit.zip/other/banana.ij",  //    73
            "cheese/english/stilton.ij",  //    91
            "cheese/index",               //    99
            "fruit/citrus/lemon.xy",      //   108
            "fruit.zip/citrus/lemon.xy",  //   108
            "fruit/citrus/orange.ij",     //   173
            "fruit.zip/citrus/orange.ij", //   173
            "fruit/other/apple.xy",       //   467
            "fruit.zip/other/apple.xy",   //   467
            "fruit/other/cherry.xy",      //   795
            "fruit.zip/other/cherry.xy",  //   795
            "cheese/english/cheddar.xy",  //  6363
            "cheese/french/brie.ij",      //  8681
            "cheese/french/bleu.xy",      // 20122
        ]);
    }

    #[gtest]
    fn test_files_are_sorted_by_size_desc() {
        let files = create_files();
        let paths = sort_files(files, vec![OrderKind::Size(DirKind::Desc), OrderKind::Dir, OrderKind::Name]);
        expect_that!(paths, elements_are![
            "cheese/french/bleu.xy",      // 20122
            "cheese/french/brie.ij",      //  8681
            "cheese/english/cheddar.xy",  //  6363
            "fruit/other/cherry.xy",      //   795
            "fruit.zip/other/cherry.xy",  //   795
            "fruit/other/apple.xy",       //   467
            "fruit.zip/other/apple.xy",   //   467
            "fruit/citrus/orange.ij",     //   173
            "fruit.zip/citrus/orange.ij", //   173
            "fruit/citrus/lemon.xy",      //   108
            "fruit.zip/citrus/lemon.xy",  //   108
            "cheese/index",               //    99
            "cheese/english/stilton.ij",  //    91
            "fruit/other/banana.ij",      //    73
            "fruit.zip/other/banana.ij",  //    73
            "index",                      //    42
            "fruit/other/date.ij",        //     6
            "fruit.zip/other/date.ij",    //     6
            "cheese/",                    //     0
            "cheese/english/",            //     0
            "cheese/french/",             //     0
            "fruit/",                     //     0
            "fruit/citrus/",              //     0
            "fruit/other/",               //     0
            "fruit.zip/",                 //     0
            "fruit.zip/citrus/",          //     0
            "fruit.zip/other/",           //     0
        ]);
    }

    #[gtest]
    fn test_files_are_sorted_by_time_asc() {
        let files = create_files();
        let paths = sort_files(files, vec![OrderKind::Time(DirKind::Asc), OrderKind::Dir, OrderKind::Name]);
        expect_that!(paths, elements_are![
            "cheese/english/cheddar.xy",  // 2022-09-10 04:03:38
            "cheese/french/",             // 2022-10-06 23:36:27
            "cheese/french/bleu.xy",      // 2022-10-28 05:49:07
            "fruit/other/date.ij",        // 2022-11-25 14:42:53
            "fruit.zip/other/date.ij",    // 2022-11-25 14:42:53
            "fruit/other/cherry.xy",      // 2022-12-19 02:12:38
            "fruit.zip/other/cherry.xy",  // 2022-12-19 02:12:38
            "cheese/index",               // 2022-12-23 21:43:18
            "cheese/",                    // 2023-01-16 11:25:51
            "cheese/french/brie.ij",      // 2023-02-28 11:01:59
            "fruit/citrus/orange.ij",     // 2023-03-27 05:45:33
            "fruit.zip/citrus/orange.ij", // 2023-03-27 05:45:33
            "cheese/english/stilton.ij",  // 2023-04-07 03:08:29
            "fruit/",                     // 2023-04-18 17:09:31
            "fruit.zip/",                 // 2023-04-18 17:09:31
            "fruit/other/apple.xy",       // 2023-05-10 11:24:22
            "fruit.zip/other/apple.xy",   // 2023-05-10 11:24:22
            "fruit/other/banana.ij",      // 2023-06-10 02:06:38
            "fruit.zip/other/banana.ij",  // 2023-06-10 02:06:38
            "fruit/citrus/lemon.xy",      // 2023-06-29 21:16:08
            "fruit.zip/citrus/lemon.xy",  // 2023-06-29 21:16:08
            "index",                      // 2023-07-05 13:05:42
            "fruit/citrus/",              // 2023-07-13 08:58:00
            "fruit.zip/citrus/",          // 2023-07-13 08:58:00
            "cheese/english/",            // 2023-08-04 10:57:26
            "fruit/other/",               // 2023-08-14 01:53:00
            "fruit.zip/other/",           // 2023-08-14 01:53:00
        ]);
    }

    #[gtest]
    fn test_files_are_sorted_by_time_desc() {
        let files = create_files();
        let paths = sort_files(files, vec![OrderKind::Time(DirKind::Desc), OrderKind::Dir, OrderKind::Name]);
        expect_that!(paths, elements_are![
            "fruit/other/",               // 2023-08-14 01:53:00
            "fruit.zip/other/",           // 2023-08-14 01:53:00
            "cheese/english/",            // 2023-08-04 10:57:26
            "fruit/citrus/",              // 2023-07-13 08:58:00
            "fruit.zip/citrus/",          // 2023-07-13 08:58:00
            "index",                      // 2023-07-05 13:05:42
            "fruit/citrus/lemon.xy",      // 2023-06-29 21:16:08
            "fruit.zip/citrus/lemon.xy",  // 2023-06-29 21:16:08
            "fruit/other/banana.ij",      // 2023-06-10 02:06:38
            "fruit.zip/other/banana.ij",  // 2023-06-10 02:06:38
            "fruit/other/apple.xy",       // 2023-05-10 11:24:22
            "fruit.zip/other/apple.xy",   // 2023-05-10 11:24:22
            "fruit/",                     // 2023-04-18 17:09:31
            "fruit.zip/",                 // 2023-04-18 17:09:31
            "cheese/english/stilton.ij",  // 2023-04-07 03:08:29
            "fruit/citrus/orange.ij",     // 2023-03-27 05:45:33
            "fruit.zip/citrus/orange.ij", // 2023-03-27 05:45:33
            "cheese/french/brie.ij",      // 2023-02-28 11:01:59
            "cheese/",                    // 2023-01-16 11:25:51
            "cheese/index",               // 2022-12-23 21:43:18
            "fruit/other/cherry.xy",      // 2022-12-19 02:12:38
            "fruit.zip/other/cherry.xy",  // 2022-12-19 02:12:38
            "fruit/other/date.ij",        // 2022-11-25 14:42:53
            "fruit.zip/other/date.ij",    // 2022-11-25 14:42:53
            "cheese/french/bleu.xy",      // 2022-10-28 05:49:07
            "cheese/french/",             // 2022-10-06 23:36:27
            "cheese/english/cheddar.xy",  // 2022-09-10 04:03:38
        ]);
    }

    #[gtest]
    fn test_files_are_sorted_by_group() {
        let files = create_files();
        let paths = sort_files(files, vec![OrderKind::Group, OrderKind::Dir, OrderKind::Name]);
        expect_that!(paths, elements_are![
            "cheese/",                    // dir
            "cheese/english/",            // dir
            "cheese/french/",             // dir
            "fruit/",                     // dir
            "fruit/citrus/",              // dir
            "fruit/other/",               // dir
            "fruit.zip/",                 // dir
            "fruit.zip/citrus/",          // dir
            "fruit.zip/other/",           // dir
            "index",                      // file
            "cheese/index",               // file
            "cheese/english/cheddar.xy",  // file
            "cheese/english/stilton.ij",  // file
            "cheese/french/bleu.xy",      // file
            "cheese/french/brie.ij",      // file
            "fruit/citrus/lemon.xy",      // file
            "fruit/citrus/orange.ij",     // file
            "fruit/other/apple.xy",       // file
            "fruit/other/banana.ij",      // file
            "fruit/other/cherry.xy",      // file
            "fruit/other/date.ij",        // file
            "fruit.zip/citrus/lemon.xy",  // file
            "fruit.zip/citrus/orange.ij", // file
            "fruit.zip/other/apple.xy",   // file
            "fruit.zip/other/banana.ij",  // file
            "fruit.zip/other/cherry.xy",  // file
            "fruit.zip/other/date.ij",    // file
        ]);
    }

    #[gtest]
    fn test_files_are_sorted_by_case_insensitive_path() {
        let files = create_case();
        let paths = sort_files(files, vec![OrderKind::Dir, OrderKind::Name]);
        expect_that!(paths, elements_are![
            "AAA.DD",
            "AAA.dd",
            "aaa.DD",
            "aaa.dd",
            "AAA.EE",
            "AAA.ee",
            "aaa.EE",
            "aaa.ee",
            "AAA.FF",
            "AAA.ff",
            "aaa.FF",
            "aaa.ff",
            "BBB.DD",
            "BBB.dd",
            "bbb.DD",
            "bbb.dd",
            "BBB.EE",
            "BBB.ee",
            "bbb.EE",
            "bbb.ee",
            "BBB.FF",
            "BBB.ff",
            "bbb.FF",
            "bbb.ff",
            "CCC.DD",
            "CCC.dd",
            "ccc.DD",
            "ccc.dd",
            "CCC.EE",
            "CCC.ee",
            "ccc.EE",
            "ccc.ee",
            "CCC.FF",
            "CCC.ff",
            "ccc.FF",
            "ccc.ff",
        ]);
    }

    #[gtest]
    fn test_files_are_sorted_by_case_insensitive_ext() {
        let files = create_case();
        let paths = sort_files(files, vec![OrderKind::Ext, OrderKind::Dir, OrderKind::Name]);
        expect_that!(paths, elements_are![
            "AAA.DD",
            "aaa.DD",
            "BBB.DD",
            "bbb.DD",
            "CCC.DD",
            "ccc.DD",
            "AAA.dd",
            "aaa.dd",
            "BBB.dd",
            "bbb.dd",
            "CCC.dd",
            "ccc.dd",
            "AAA.EE",
            "aaa.EE",
            "BBB.EE",
            "bbb.EE",
            "CCC.EE",
            "ccc.EE",
            "AAA.ee",
            "aaa.ee",
            "BBB.ee",
            "bbb.ee",
            "CCC.ee",
            "ccc.ee",
            "AAA.FF",
            "aaa.FF",
            "BBB.FF",
            "bbb.FF",
            "CCC.FF",
            "ccc.FF",
            "AAA.ff",
            "aaa.ff",
            "BBB.ff",
            "bbb.ff",
            "CCC.ff",
            "ccc.ff",
        ]);
    }

    #[gtest]
    fn test_files_are_sorted_before_dirs() {
        let files = create_folder();
        let paths = sort_files(files, vec![OrderKind::Dir, OrderKind::Name]);
        expect_that!(paths, elements_are![
            "xyz",
            "abc/",
            "abc/xyz",
            "abc/def/",
            "abc/def/xyz",
        ]);
    }

    #[gtest]
    fn test_numeric_files_are_sorted_by_dir() {
        let files = create_numeric1();
        let paths = sort_files(files, vec![OrderKind::Dir, OrderKind::Name]);
        expect_that!(paths, elements_are![
            "9/",
            "9/9/",
            "9/9/9/",
            "9/9/10/",
            "9/10/",
            "9/10/9/",
            "9/10/10/",
            "10/",
            "10/9/",
            "10/9/9/",
            "10/9/10/",
            "10/10/",
            "10/10/9/",
            "10/10/10/",
        ]);
    }

    #[gtest]
    fn test_numeric_files_are_sorted_by_name() {
        let files = create_numeric2();
        let paths = sort_files(files, vec![OrderKind::Dir, OrderKind::Name]);
        expect_that!(paths, elements_are![
            "9",
            "9_9",
            "9_9_9",
            "9_9_10",
            "9_10",
            "9_10_9",
            "9_10_10",
            "10",
            "10_9",
            "10_9_9",
            "10_9_10",
            "10_10",
            "10_10_9",
            "10_10_10",
        ]);
    }

    #[gtest]
    fn test_numeric_files_are_sorted_by_ext() {
        let files = create_numeric3();
        let paths = sort_files(files, vec![OrderKind::Ext, OrderKind::Dir, OrderKind::Name]);
        expect_that!(paths, elements_are![
            "file.9",
            "file.9_9",
            "file.9_9_9",
            "file.9_9_10",
            "file.9_10",
            "file.9_10_9",
            "file.9_10_10",
            "file.10",
            "file.10_9",
            "file.10_9_9",
            "file.10_9_10",
            "file.10_10",
            "file.10_10_9",
            "file.10_10_10",
        ]);
    }

    fn create_files() -> Vec<File> {
        vec![
            create_file(FileKind::Other, "2023-07-05T13:05:42Z", 42, "index"),
            create_file(FileKind::Dir, "2023-01-16T11:25:51Z", 0, "cheese"),
            create_file(FileKind::Other, "2022-12-23T21:43:18Z", 99, "cheese/index"),
            create_file(FileKind::Dir, "2023-08-04T10:57:26Z", 0, "cheese/english"),
            create_file(FileKind::Other, "2022-09-10T04:03:38Z", 6363, "cheese/english/cheddar.xy"),
            create_file(FileKind::Other, "2023-04-07T03:08:29Z", 91, "cheese/english/stilton.ij"),
            create_file(FileKind::Dir, "2022-10-06T23:36:27Z", 0, "cheese/french"),
            create_file(FileKind::Other, "2022-10-28T05:49:07Z", 20122, "cheese/french/bleu.xy"),
            create_file(FileKind::Other, "2023-02-28T11:01:59Z", 8681, "cheese/french/brie.ij"),
            create_file(FileKind::Dir, "2023-04-18T17:09:31Z", 0, "fruit"),
            create_file(FileKind::Dir, "2023-07-13T08:58:00Z", 0, "fruit/citrus"),
            create_file(FileKind::Other, "2023-06-29T21:16:08Z", 108, "fruit/citrus/lemon.xy"),
            create_file(FileKind::Other, "2023-03-27T05:45:33Z", 173, "fruit/citrus/orange.ij"),
            create_file(FileKind::Dir, "2023-08-14T01:53:00Z", 0, "fruit/other"),
            create_file(FileKind::Other, "2023-05-10T11:24:22Z", 467, "fruit/other/apple.xy"),
            create_file(FileKind::Other, "2023-06-10T02:06:38Z", 73, "fruit/other/banana.ij"),
            create_file(FileKind::Other, "2022-12-19T02:12:38Z", 795, "fruit/other/cherry.xy"),
            create_file(FileKind::Other, "2022-11-25T14:42:53Z", 6, "fruit/other/date.ij"),
            create_file(FileKind::Dir, "2023-04-18T17:09:31Z", 0, "fruit.zip"),
            create_file(FileKind::Dir, "2023-07-13T08:58:00Z", 0, "fruit.zip/citrus"),
            create_file(FileKind::Other, "2023-06-29T21:16:08Z", 108, "fruit.zip/citrus/lemon.xy"),
            create_file(FileKind::Other, "2023-03-27T05:45:33Z", 173, "fruit.zip/citrus/orange.ij"),
            create_file(FileKind::Dir, "2023-08-14T01:53:00Z", 0, "fruit.zip/other"),
            create_file(FileKind::Other, "2023-05-10T11:24:22Z", 467, "fruit.zip/other/apple.xy"),
            create_file(FileKind::Other, "2023-06-10T02:06:38Z", 73, "fruit.zip/other/banana.ij"),
            create_file(FileKind::Other, "2022-12-19T02:12:38Z", 795, "fruit.zip/other/cherry.xy"),
            create_file(FileKind::Other, "2022-11-25T14:42:53Z", 6, "fruit.zip/other/date.ij"),
        ]
    }

    fn create_case() -> Vec<File> {
        vec![
            create_file(FileKind::Other, "1970-01-01T00:00:00Z", 0, "aaa.dd"),
            create_file(FileKind::Other, "1970-01-01T00:00:00Z", 0, "aaa.DD"),
            create_file(FileKind::Other, "1970-01-01T00:00:00Z", 0, "aaa.ee"),
            create_file(FileKind::Other, "1970-01-01T00:00:00Z", 0, "aaa.EE"),
            create_file(FileKind::Other, "1970-01-01T00:00:00Z", 0, "aaa.ff"),
            create_file(FileKind::Other, "1970-01-01T00:00:00Z", 0, "aaa.FF"),
            create_file(FileKind::Other, "1970-01-01T00:00:00Z", 0, "AAA.dd"),
            create_file(FileKind::Other, "1970-01-01T00:00:00Z", 0, "AAA.DD"),
            create_file(FileKind::Other, "1970-01-01T00:00:00Z", 0, "AAA.ee"),
            create_file(FileKind::Other, "1970-01-01T00:00:00Z", 0, "AAA.EE"),
            create_file(FileKind::Other, "1970-01-01T00:00:00Z", 0, "AAA.ff"),
            create_file(FileKind::Other, "1970-01-01T00:00:00Z", 0, "AAA.FF"),
            create_file(FileKind::Other, "1970-01-01T00:00:00Z", 0, "bbb.dd"),
            create_file(FileKind::Other, "1970-01-01T00:00:00Z", 0, "bbb.DD"),
            create_file(FileKind::Other, "1970-01-01T00:00:00Z", 0, "bbb.ee"),
            create_file(FileKind::Other, "1970-01-01T00:00:00Z", 0, "bbb.EE"),
            create_file(FileKind::Other, "1970-01-01T00:00:00Z", 0, "bbb.ff"),
            create_file(FileKind::Other, "1970-01-01T00:00:00Z", 0, "bbb.FF"),
            create_file(FileKind::Other, "1970-01-01T00:00:00Z", 0, "BBB.dd"),
            create_file(FileKind::Other, "1970-01-01T00:00:00Z", 0, "BBB.DD"),
            create_file(FileKind::Other, "1970-01-01T00:00:00Z", 0, "BBB.ee"),
            create_file(FileKind::Other, "1970-01-01T00:00:00Z", 0, "BBB.EE"),
            create_file(FileKind::Other, "1970-01-01T00:00:00Z", 0, "BBB.ff"),
            create_file(FileKind::Other, "1970-01-01T00:00:00Z", 0, "BBB.FF"),
            create_file(FileKind::Other, "1970-01-01T00:00:00Z", 0, "ccc.dd"),
            create_file(FileKind::Other, "1970-01-01T00:00:00Z", 0, "ccc.DD"),
            create_file(FileKind::Other, "1970-01-01T00:00:00Z", 0, "ccc.ee"),
            create_file(FileKind::Other, "1970-01-01T00:00:00Z", 0, "ccc.EE"),
            create_file(FileKind::Other, "1970-01-01T00:00:00Z", 0, "ccc.ff"),
            create_file(FileKind::Other, "1970-01-01T00:00:00Z", 0, "ccc.FF"),
            create_file(FileKind::Other, "1970-01-01T00:00:00Z", 0, "CCC.dd"),
            create_file(FileKind::Other, "1970-01-01T00:00:00Z", 0, "CCC.DD"),
            create_file(FileKind::Other, "1970-01-01T00:00:00Z", 0, "CCC.ee"),
            create_file(FileKind::Other, "1970-01-01T00:00:00Z", 0, "CCC.EE"),
            create_file(FileKind::Other, "1970-01-01T00:00:00Z", 0, "CCC.ff"),
            create_file(FileKind::Other, "1970-01-01T00:00:00Z", 0, "CCC.FF"),
        ]
    }

    fn create_folder() -> Vec<File> {
        vec![
            create_file(FileKind::Dir, "1970-01-01T00:00:00Z", 0, "abc"),
            create_file(FileKind::Dir, "1970-01-01T00:00:00Z", 0, "abc/def"),
            create_file(FileKind::Other, "1970-01-01T00:00:00Z", 0, "abc/def/xyz"),
            create_file(FileKind::Other, "1970-01-01T00:00:00Z", 0, "abc/xyz"),
            create_file(FileKind::Other, "1970-01-01T00:00:00Z", 0, "xyz"),
        ]
    }

    fn create_numeric1() -> Vec<File> {
        vec![
            create_file(FileKind::Dir, "1970-01-01T00:00:00Z", 0, "10"),
            create_file(FileKind::Dir, "1970-01-01T00:00:00Z", 0, "10/10"),
            create_file(FileKind::Dir, "1970-01-01T00:00:00Z", 0, "10/10/10"),
            create_file(FileKind::Dir, "1970-01-01T00:00:00Z", 0, "10/10/9"),
            create_file(FileKind::Dir, "1970-01-01T00:00:00Z", 0, "10/9"),
            create_file(FileKind::Dir, "1970-01-01T00:00:00Z", 0, "10/9/10"),
            create_file(FileKind::Dir, "1970-01-01T00:00:00Z", 0, "10/9/9"),
            create_file(FileKind::Dir, "1970-01-01T00:00:00Z", 0, "9"),
            create_file(FileKind::Dir, "1970-01-01T00:00:00Z", 0, "9/10"),
            create_file(FileKind::Dir, "1970-01-01T00:00:00Z", 0, "9/10/10"),
            create_file(FileKind::Dir, "1970-01-01T00:00:00Z", 0, "9/10/9"),
            create_file(FileKind::Dir, "1970-01-01T00:00:00Z", 0, "9/9"),
            create_file(FileKind::Dir, "1970-01-01T00:00:00Z", 0, "9/9/10"),
            create_file(FileKind::Dir, "1970-01-01T00:00:00Z", 0, "9/9/9"),
        ]
    }

    fn create_numeric2() -> Vec<File> {
        vec![
            create_file(FileKind::Other, "1970-01-01T00:00:00Z", 0, "10"),
            create_file(FileKind::Other, "1970-01-01T00:00:00Z", 0, "10_10"),
            create_file(FileKind::Other, "1970-01-01T00:00:00Z", 0, "10_10_10"),
            create_file(FileKind::Other, "1970-01-01T00:00:00Z", 0, "10_10_9"),
            create_file(FileKind::Other, "1970-01-01T00:00:00Z", 0, "10_9"),
            create_file(FileKind::Other, "1970-01-01T00:00:00Z", 0, "10_9_10"),
            create_file(FileKind::Other, "1970-01-01T00:00:00Z", 0, "10_9_9"),
            create_file(FileKind::Other, "1970-01-01T00:00:00Z", 0, "9"),
            create_file(FileKind::Other, "1970-01-01T00:00:00Z", 0, "9_10"),
            create_file(FileKind::Other, "1970-01-01T00:00:00Z", 0, "9_10_10"),
            create_file(FileKind::Other, "1970-01-01T00:00:00Z", 0, "9_10_9"),
            create_file(FileKind::Other, "1970-01-01T00:00:00Z", 0, "9_9"),
            create_file(FileKind::Other, "1970-01-01T00:00:00Z", 0, "9_9_10"),
            create_file(FileKind::Other, "1970-01-01T00:00:00Z", 0, "9_9_9"),
        ]
    }

    fn create_numeric3() -> Vec<File> {
        vec![
            create_file(FileKind::Other, "1970-01-01T00:00:00Z", 0, "file.10"),
            create_file(FileKind::Other, "1970-01-01T00:00:00Z", 0, "file.10_10"),
            create_file(FileKind::Other, "1970-01-01T00:00:00Z", 0, "file.10_10_10"),
            create_file(FileKind::Other, "1970-01-01T00:00:00Z", 0, "file.10_10_9"),
            create_file(FileKind::Other, "1970-01-01T00:00:00Z", 0, "file.10_9"),
            create_file(FileKind::Other, "1970-01-01T00:00:00Z", 0, "file.10_9_10"),
            create_file(FileKind::Other, "1970-01-01T00:00:00Z", 0, "file.10_9_9"),
            create_file(FileKind::Other, "1970-01-01T00:00:00Z", 0, "file.9"),
            create_file(FileKind::Other, "1970-01-01T00:00:00Z", 0, "file.9_10"),
            create_file(FileKind::Other, "1970-01-01T00:00:00Z", 0, "file.9_10_10"),
            create_file(FileKind::Other, "1970-01-01T00:00:00Z", 0, "file.9_10_9"),
            create_file(FileKind::Other, "1970-01-01T00:00:00Z", 0, "file.9_9"),
            create_file(FileKind::Other, "1970-01-01T00:00:00Z", 0, "file.9_9_10"),
            create_file(FileKind::Other, "1970-01-01T00:00:00Z", 0, "file.9_9_9"),
        ]
    }

    fn create_file(
        file_type: FileKind,
        file_time: &str,
        file_size: u64,
        rel_path: &str,
    ) -> File {
        let abs_path = PathBuf::from("/root/").join(rel_path);
        let rel_path = PathBuf::from(rel_path);
        let file_depth = rel_path.components().count();
        let file_time = DateTime::parse_from_rfc3339(file_time).unwrap().with_timezone(&Utc);
        if file_type == FileKind::Dir {
            let file_name = String::from("");
            let file_ext = String::from("");
            File::new(abs_path, rel_path, file_depth, None, file_name, file_ext, file_type)
                .with_size(file_size)
                .with_time(file_time)
        } else {
            let abs_dir = PathBuf::from(abs_path.parent().unwrap());
            let rel_dir = PathBuf::from(rel_path.parent().unwrap());
            let file_name = String::from(abs_path.file_name().and_then(OsStr::to_str).unwrap());
            let file_ext = String::from(abs_path.extension().and_then(OsStr::to_str).unwrap_or_default());
            File::new(abs_dir, rel_dir, file_depth, None, file_name, file_ext, file_type)
                .with_size(file_size)
                .with_time(file_time)
        }
    }

    fn sort_files(mut files: Vec<File>, order: Vec<OrderKind>) -> Vec<String> {
        let config = create_config(order);
        let sorter = Sorter::new(&config);
        sorter.sort_files(&mut files);
        files.into_iter().map(format_file).collect()
    }

    fn format_file(file: File) -> String {
        file.rel_dir
            .join(file.file_name)
            .to_str()
            .map(|x| String::from(x))
            .map(|x| x.replace(MAIN_SEPARATOR_STR, "/"))
            .unwrap()
    }

    fn create_config(order: Vec<OrderKind>) -> Config {
        Config::default().with_sort_order(order)
    }
}