afsort/
lib.rs

1/*!
2
3The afsort crate implements a sorting algorithm based on
4[American Flag sort](https://en.wikipedia.org/wiki/American_flag_sort). The implementation is
5currently limited to sort byte slices, e.g. Strings. The main motivation is to sort strings of
6text, so most of the benchmarks are based on English text strings. When sorting English words,
7this implementation seems to be about 40% faster than `sort_unstable` from the Rust standard
8library.
9
10For small input, this method falls back to the standard library.
11
12# Installation
13
14Add the dependency to your `Cargo.toml`:
15
16```ignore
17[dependencies]
18afsort = "0.1"
19```
20In your crate root:
21```ignore
22extern crate afsort;
23```
24
25**Note on upgrading 0.1.x -> 0.2.x**: The method `afsort::sort_unstable(&mut [AsRef<u8>])` has
26been removed. Use the af_sort_unstable from the `AFSortable` trait instead.
27
28# Usage
29
30You can now afsort to e.g. sort arrays of strings or string slices.
31
32```rust
33use afsort::AFSortable;
34let mut strings = vec!("red", "green", "blue");
35strings.af_sort_unstable();
36assert_eq!(strings, vec!["blue", "green", "red"]);
37```
38
39It also works on u8, u16, u32 and u64:
40
41```rust
42use afsort::AFSortable;
43let mut strings = vec!(1u32, 2u32, 7u32);
44strings.af_sort_unstable();
45assert_eq!(strings, vec![1u32, 2u32, 7u32]);
46```
47
48You can also sort by an extractor function, e.g.:
49
50```rust
51use afsort;
52let mut tuples = vec![("b", 2), ("a", 1)];
53afsort::sort_unstable_by(&mut tuples, |t| &t.0);
54assert_eq!(tuples, vec![("a", 1), ("b", 2)]);
55```
56
57The `af_sort_unstable()` method is implemented for all slices of values that implement the
58`afsort::DigitAt` and the `Ord` traits. The `DigitAt` trait is implemented for `&str`
59, `String`, `[u8]`, `u8`, `u16`, `u32` and `u64`. All of these also implement Ord. You can also
60implement this trait for any other type.
61
62# Motivation
63
64Essentially, I noticed that sorting of strings took a long time when using the
65[fst](https://github.com/BurntSushi/fst) crate, since it requires the input to be ordered.
66Since sorting strings is a general problem, this is now a crate.
67
68# Performance
69
70As mentioned, this implementation seems to be about 40% faster than the sort in the standard
71library, when sorting strings of random English words.  It is slower for strings that are
72already sorted. The implementation is fairly naive, so I would not be surprised if it could
73be improved further.
74
75For numbers, it currently seems to be slower than the standard library. I suspect this is due
76to more swaps happening in afsort than in the standard library. I want to fix this.
77
78This will be heavily affected by the distribution of values in the input though. As always with
79performance: _your milage may vary_. Profile your usage.
80
81You can run the benchmark tests using `cargo bench` (currently requires nightly rust), like this:
82
83```ignore
84% cargo bench
85    Finished release [optimized] target(s) in 0.0 secs
86     Running target/release/deps/afsort-2f0d4e495216be99
87running 10 tests
88test tests::correct_radix_for_u16 ... ignored
89test tests::correct_radix_for_u32 ... ignored
90test tests::correct_radix_for_u64 ... ignored
91test tests::correct_radix_for_u8 ... ignored
92test tests::sorts_strings_same_as_unstable ... ignored
93test tests::sorts_tuples_same_as_unstable ... ignored
94test tests::sorts_u16_same_as_unstable ... ignored
95test tests::sorts_u32_same_as_unstable ... ignored
96test tests::sorts_u64_same_as_unstable ... ignored
97test tests::sorts_u8_same_as_unstable ... ignored
98test result: ok. 0 passed; 0 failed; 10 ignored; 0 measured; 0 filtered out
99     Running target/release/deps/bench-42a0c77149fb906a
100running 16 tests
101test sort_en_strings_lower_10_000_af   ... bench:   1,881,300 ns/iter (+/- 618,858)
102test sort_en_strings_lower_10_000_std  ... bench:   2,594,388 ns/iter (+/- 767,774)
103test sort_en_strings_rand_100_000_af   ... bench:  23,101,465 ns/iter (+/- 12,052,025)
104test sort_en_strings_rand_100_000_std  ... bench:  31,536,516 ns/iter (+/- 12,910,887)
105test sort_en_strings_rand_10_000_af    ... bench:   1,588,372 ns/iter (+/- 568,509)
106test sort_en_strings_rand_10_000_std   ... bench:   2,193,132 ns/iter (+/- 648,297)
107test sort_en_strings_sorted_10_000_af  ... bench:     806,419 ns/iter (+/- 128,186)
108test sort_en_strings_sorted_10_000_std ... bench:     589,161 ns/iter (+/- 340,707)
109test sort_u16_1_000_000_af             ... bench:  19,442,855 ns/iter (+/- 1,642,992)
110test sort_u16_1_000_000_std            ... bench:  21,401,736 ns/iter (+/- 3,607,120)
111test sort_u32_1_000_000_af             ... bench:  31,682,863 ns/iter (+/- 5,254,810)
112test sort_u32_1_000_000_std            ... bench:  30,809,651 ns/iter (+/- 1,623,271)
113test sort_u64_1_000_000_af             ... bench:  39,730,940 ns/iter (+/- 6,139,556)
114test sort_u64_1_000_000_std            ... bench:  32,477,660 ns/iter (+/- 1,733,969)
115test sort_u8_1_000_af                  ... bench:      11,330 ns/iter (+/- 702)
116test sort_u8_1_000_std                 ... bench:       8,764 ns/iter (+/- 163)
117test result: ok. 0 passed; 0 failed; 0 ignored; 16 measured; 0 filtered out
118```
119# Limitations
120
121The American Flag algorithm is unstable, in the same way that sort_unstable in the standard
122library. That is, equal elements might be re-ordered.
123
124This crate can _only_ sort strings based on their `utf-8` byte values. For many problems, this
125is fine. However, if you want to sort strings for display to a user, Locale might matter. This
126crate does not try to address this issue.
127
128# Testing
129
130Testing is done using the [quickcheck](https://github.com/BurntSushi/quickcheck) crate. We run
131about 50k different variations of input strings & numbers. We treat the standard library's
132sort_unstable as the gold standard. This means that it is very likely that this library is as
133bug-free (at least in a functional sense) as the standard library.
134
135*/
136
137#[cfg(test)]
138extern crate quickcheck;
139
140use std::borrow::Cow;
141
142/// Specifies that a type can deliver a radix at a certain digit/depth.
143pub trait DigitAt {
144    /// Extracts a radix value at a certain digit for a type. Should return None if no value exists
145    /// at the digit.
146    ///
147    /// #Example
148    ///
149    /// ```rust
150    /// use afsort::DigitAt;
151    ///
152    /// let num = 0x0502u16;
153    /// assert_eq!(Some(5), num.get_digit_at(0));
154    /// assert_eq!(Some(2), num.get_digit_at(1));
155    /// assert_eq!(None, num.get_digit_at(2));
156    /// ```
157    #[inline]
158    fn get_digit_at(&self, digit: usize) -> Option<u8>;
159}
160
161impl DigitAt for u8 {
162    #[inline]
163    fn get_digit_at(&self, digit: usize) -> Option<u8> {
164        if digit == 0 {
165            Some(*self)
166        } else {
167            None
168        }
169    }
170}
171
172impl DigitAt for u16 {
173    #[inline]
174    fn get_digit_at(&self, digit: usize) -> Option<u8> {
175        match digit {
176            0 => Some(((self & 0xFF00) >> 8) as u8),
177            1 => Some((self & 0xFF) as u8),
178            _ => None,
179        }
180    }
181}
182
183impl DigitAt for u32 {
184    #[inline]
185    fn get_digit_at(&self, digit: usize) -> Option<u8> {
186        match digit {
187            0 => Some(((self & 0xFF000000) >> 24) as u8),
188            1 => Some(((self & 0xFF0000) >> 16) as u8),
189            2 => Some(((self & 0xFF00) >> 8) as u8),
190            3 => Some((*self & 0xFF) as u8),
191            _ => None,
192        }
193    }
194}
195
196impl DigitAt for u64 {
197    #[inline]
198    fn get_digit_at(&self, digit: usize) -> Option<u8> {
199        match digit {
200            0 => Some(((self & 0xFF00000000000000) >> 56) as u8),
201            1 => Some(((self & 0xFF000000000000) >> 48) as u8),
202            2 => Some(((self & 0xFF0000000000) >> 40) as u8),
203            3 => Some(((self & 0xFF00000000) >> 32) as u8),
204            4 => Some(((self & 0xFF000000) >> 24) as u8),
205            5 => Some(((self & 0xFF0000) >> 16) as u8),
206            6 => Some(((self & 0xFF00) >> 8) as u8),
207            7 => Some((self & 0xFF) as u8),
208            _ => None,
209        }
210    }
211}
212
213impl<'a> DigitAt for &'a str {
214    #[inline]
215    fn get_digit_at(&self, digit: usize) -> Option<u8> {
216        if self.len() > digit {
217            Some(self.as_bytes()[digit])
218        } else {
219            None
220        }
221    }
222}
223
224impl<'a> DigitAt for String {
225    #[inline]
226    fn get_digit_at(&self, digit: usize) -> Option<u8> {
227        if self.len() > digit {
228            Some(self.as_bytes()[digit])
229        } else {
230            None
231        }
232    }
233}
234
235impl<'a> DigitAt for [u8] {
236    #[inline]
237    fn get_digit_at(&self, digit: usize) -> Option<u8> {
238        if self.len() > digit {
239            Some(self[digit])
240        } else {
241            None
242        }
243    }
244}
245
246impl<'a> DigitAt for &'a [u8] {
247    #[inline]
248    fn get_digit_at(&self, digit: usize) -> Option<u8> {
249        if self.len() > digit {
250            Some(self[digit])
251        } else {
252            None
253        }
254    }
255}
256
257impl<'a> DigitAt for Cow<'a, str> {
258    #[inline]
259    fn get_digit_at(&self, digit: usize) -> Option<u8> {
260        if self.len() > digit {
261            Some(self.as_bytes()[digit])
262        } else {
263            None
264        }
265    }
266}
267
268impl<'a> DigitAt for AsRef<DigitAt> {
269    #[inline]
270    fn get_digit_at(&self, digit: usize) -> Option<u8> {
271        self.as_ref().get_digit_at(digit)
272    }
273}
274
275/// Enhances slices of `DigitAt` implementors to have a `af_sort_unstable` method.
276///
277/// #Example
278///
279/// ```rust
280/// use afsort::AFSortable;
281///
282/// let mut strings = vec!["c", "a", "b"];
283/// strings.af_sort_unstable();
284/// assert_eq!(strings, vec!["a", "b", "c"]);
285/// ```
286
287pub trait AFSortable {
288    #[inline]
289    fn af_sort_unstable(&mut self);
290}
291
292impl<T> AFSortable for [T]
293where
294    T: DigitAt + Ord,
295{
296    #[inline]
297    fn af_sort_unstable(&mut self) {
298        sort_unstable_by(self, ident);
299    }
300}
301
302#[inline]
303fn ident<T>(t: &T) -> &T {
304    &t
305}
306
307/// Sort method which accepts function to convert elements to &[u8].
308///
309/// #Example
310///
311/// ```rust
312/// let mut tuples = vec![("b", 2), ("a", 1)];
313///afsort::sort_unstable_by(&mut tuples, |t| &t.0);
314///assert_eq!(tuples, vec![("a", 1), ("b", 2)]);
315/// ```
316///
317/// Footnote: The explicit type annotacion in the closure seems to be needed (even though it should
318/// not). See
319/// [this discussion](https://users.rust-lang.org/t/lifetime-issue-with-str-in-closure/13137).
320#[inline]
321pub fn sort_unstable_by<T, O, S>(vec: &mut [T], sort_by: S)
322where
323    O: Ord + DigitAt + ?Sized,
324    S: Fn(&T) -> &O,
325{
326    sort_req(vec, &sort_by, 0);
327}
328
329fn sort_req<T, O, S>(vec: &mut [T], sort_by: &S, depth: usize)
330where
331    O: Ord + DigitAt + ?Sized,
332    S: Fn(&T) -> &O,
333{
334    if vec.len() <= 32 {
335        vec.sort_unstable_by(|e1, e2| sort_by(e1).cmp(sort_by(e2)));
336        return;
337    }
338    let mut min = u16::max_value();
339    let mut max = 0u16;
340    {
341        //Find min/max to be able to allocate less memory
342        for elem in vec.iter() {
343            match sort_by(elem).get_digit_at(depth) {
344                Some(v) => {
345                    let radix_val = v as u16;
346                    if radix_val < min {
347                        min = radix_val;
348                    }
349                    if radix_val > max {
350                        max = radix_val;
351                    }
352                }
353                None => (),
354            }
355        }
356    }
357    //No item had a value for this depth
358    if min == u16::max_value() {
359        return;
360    }
361
362    // +2 instead of +1 for special 0 bucket
363    let num_items = (max - min + 2) as usize;
364    let mut counts: Vec<usize> = vec![0usize; num_items as usize];
365    {
366        //Count occurences per value. Elements without a value gets
367        //the special value 0, while others get the u8 value +1.
368        for elem in vec.iter() {
369            let radix_val = match sort_by(elem).get_digit_at(depth) {
370                Some(r) => r as u16 + 1 - min,
371                None => 0,
372            };
373            counts[radix_val as usize] += 1;
374        }
375    }
376
377    let mut offsets: Vec<usize> = vec![0usize; num_items as usize];
378    {
379        //Sets the offsets for each count
380        let mut sum = 0usize;
381        for i in 0..counts.len() {
382            offsets[i as usize] = sum;
383            sum += counts[i as usize];
384        }
385    }
386    {
387        //Swap objects into the correct bucket, based on the offsets
388        let mut next_free = offsets.clone();
389        let mut block = 0usize;
390        let mut i = 0usize;
391        while block < counts.len() - 1 {
392            if i >= offsets[block as usize + 1] as usize {
393                block += 1;
394            } else {
395                let radix_val = match sort_by(&vec[i]).get_digit_at(depth) {
396                    Some(r) => r as u16 + 1 - min,
397                    None => 0,
398                };
399                if radix_val == block as u16 {
400                    i += 1;
401                } else {
402                    vec.swap(i as usize, next_free[radix_val as usize] as usize);
403                    next_free[radix_val as usize] += 1;
404                }
405            }
406        }
407    }
408    {
409        //Within each bucket, sort recursively. We can skip the first, since all elements
410        //in it have no radix at this depth, and thus are equal.
411        for i in 1..offsets.len() - 1 {
412            sort_req(
413                &mut vec[offsets[i as usize] as usize..offsets[i as usize + 1] as usize],
414                sort_by,
415                depth + 1,
416            );
417        }
418        sort_req(&mut vec[offsets[offsets.len() - 1]..], sort_by, depth + 1);
419    }
420}
421
422#[cfg(test)]
423mod tests {
424    use super::AFSortable;
425    use super::DigitAt;
426    use quickcheck::QuickCheck;
427    use std::borrow::Cow;
428
429    #[test]
430    fn sorts_strings_same_as_unstable() {
431        fn compare_sort(mut strings: Vec<String>) -> bool {
432            let mut copy = strings.clone();
433            copy.sort_unstable();
434            strings.af_sort_unstable();
435            strings == copy
436        }
437        QuickCheck::new()
438            .tests(50000)
439            .quickcheck(compare_sort as fn(Vec<String>) -> bool);
440    }
441
442    #[test]
443    fn sorts_cow_str_same_as_unstable() {
444        fn compare_sort(strings: Vec<String>) -> bool {
445            let mut cows: Vec<Cow<str>> = strings.into_iter().map(|s| Cow::Owned(s)).collect();
446            let mut copy = cows.clone();
447            copy.sort_unstable();
448            cows.af_sort_unstable();
449            cows == copy
450        }
451        QuickCheck::new()
452            .tests(50000)
453            .quickcheck(compare_sort as fn(Vec<String>) -> bool);
454    }
455
456    #[test]
457    fn sorts_u8_ref_same_as_unstable() {
458        fn compare_sort(nums: Vec<Vec<u8>>) -> bool {
459            let mut refs: Vec<&[u8]> = nums.iter().map(|i| i.as_slice()).collect();
460            let mut copy = refs.clone();
461            copy.sort_unstable();
462            refs.af_sort_unstable();
463            refs == copy
464        }
465        QuickCheck::new()
466            .tests(50000)
467            .quickcheck(compare_sort as fn(Vec<Vec<u8>>) -> bool);
468    }
469
470    #[test]
471    fn sorts_u8_same_as_unstable() {
472        fn compare_sort(mut nums: Vec<u8>) -> bool {
473            let mut copy = nums.clone();
474            copy.sort_unstable();
475            nums.af_sort_unstable();
476            nums == copy
477        }
478        QuickCheck::new()
479            .tests(50000)
480            .quickcheck(compare_sort as fn(Vec<u8>) -> bool);
481    }
482
483    #[test]
484    fn sorts_u16_same_as_unstable() {
485        fn compare_sort(mut nums: Vec<u16>) -> bool {
486            let mut copy = nums.clone();
487            copy.sort_unstable();
488            nums.af_sort_unstable();
489            nums == copy
490        }
491        QuickCheck::new()
492            .tests(50000)
493            .quickcheck(compare_sort as fn(Vec<u16>) -> bool);
494    }
495
496    #[test]
497    fn sorts_u32_same_as_unstable() {
498        fn compare_sort(mut nums: Vec<u32>) -> bool {
499            let mut copy = nums.clone();
500            copy.sort_unstable();
501            nums.af_sort_unstable();
502            nums == copy
503        }
504        QuickCheck::new()
505            .tests(50000)
506            .quickcheck(compare_sort as fn(Vec<u32>) -> bool);
507    }
508
509    #[test]
510    fn sorts_u64_same_as_unstable() {
511        fn compare_sort(mut nums: Vec<u64>) -> bool {
512            let mut copy = nums.clone();
513            copy.sort_unstable();
514            nums.af_sort_unstable();
515            nums == copy
516        }
517        QuickCheck::new()
518            .tests(50000)
519            .quickcheck(compare_sort as fn(Vec<u64>) -> bool);
520    }
521
522    #[test]
523    fn sorts_tuples_same_as_unstable() {
524        fn compare_sort(mut tuples: Vec<(String, u8)>) -> bool {
525            let mut copy = tuples.clone();
526            copy.sort_unstable_by(|t1, t2| t1.0.cmp(&t2.0));
527            super::sort_unstable_by(&mut tuples, |t| &t.0);
528            tuples.into_iter().map(|t| t.0).collect::<Vec<String>>()
529                == copy.into_iter().map(|t| t.0).collect::<Vec<String>>()
530        }
531        QuickCheck::new()
532            .tests(50000)
533            .quickcheck(compare_sort as fn(Vec<(String, u8)>) -> bool);
534    }
535
536    #[test]
537    fn correct_radix_for_u8() {
538        let num = 0x50u8;
539        assert_eq!(Some(num), num.get_digit_at(0));
540        assert_eq!(None, num.get_digit_at(1));
541        assert_eq!(None, num.get_digit_at(5));
542    }
543
544    #[test]
545    fn correct_radix_for_u16() {
546        let num = 0x3050u16;
547        assert_eq!(Some(0x30), num.get_digit_at(0));
548        assert_eq!(Some(0x50), num.get_digit_at(1));
549        assert_eq!(None, num.get_digit_at(2));
550        assert_eq!(None, num.get_digit_at(5));
551    }
552
553    #[test]
554    fn correct_radix_for_u32() {
555        let num = 0x70103050u32;
556        assert_eq!(Some(0x70), num.get_digit_at(0));
557        assert_eq!(Some(0x10), num.get_digit_at(1));
558        assert_eq!(Some(0x30), num.get_digit_at(2));
559        assert_eq!(Some(0x50), num.get_digit_at(3));
560        assert_eq!(None, num.get_digit_at(4));
561        assert_eq!(None, num.get_digit_at(7));
562    }
563
564    #[test]
565    fn correct_radix_for_u64() {
566        let num = 0x2040608070103050u64;
567        assert_eq!(Some(0x20), num.get_digit_at(0));
568        assert_eq!(Some(0x40), num.get_digit_at(1));
569        assert_eq!(Some(0x60), num.get_digit_at(2));
570        assert_eq!(Some(0x80), num.get_digit_at(3));
571        assert_eq!(Some(0x70), num.get_digit_at(4));
572        assert_eq!(Some(0x10), num.get_digit_at(5));
573        assert_eq!(Some(0x30), num.get_digit_at(6));
574        assert_eq!(Some(0x50), num.get_digit_at(7));
575        assert_eq!(None, num.get_digit_at(8));
576        assert_eq!(None, num.get_digit_at(13));
577    }
578}