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}