From 4722c8991b090836eba7f7faabc9630b63e5a6ce Mon Sep 17 00:00:00 2001 From: Ben S Date: Sun, 1 Jun 2014 11:54:31 +0100 Subject: [PATCH] Implement natural sorting on filenames --- exa.rs | 1 + file.rs | 13 ++++++++----- options.rs | 2 +- sort.rs | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 59 insertions(+), 6 deletions(-) create mode 100644 sort.rs diff --git a/exa.rs b/exa.rs index 78f2d4e..559f684 100644 --- a/exa.rs +++ b/exa.rs @@ -14,6 +14,7 @@ pub mod format; pub mod file; pub mod unix; pub mod options; +pub mod sort; fn main() { let args = os::args(); diff --git a/file.rs b/file.rs index eabc203..c857ca6 100644 --- a/file.rs +++ b/file.rs @@ -5,6 +5,7 @@ use colours::{Plain, Style, Black, Red, Green, Yellow, Blue, Purple, Cyan}; use column::{Column, Permissions, FileName, FileSize, User, Group}; use format::{format_metric_bytes, format_IEC_bytes}; use unix::{get_user_name, get_group_name}; +use sort::SortPart; static MEDIA_TYPES: &'static [&'static str] = &[ "png", "jpeg", "jpg", "gif", "bmp", "tiff", "tif", @@ -27,6 +28,7 @@ pub struct File<'a> { pub ext: Option<&'a str>, pub path: &'a Path, pub stat: io::FileStat, + pub parts: Vec>, } impl<'a> File<'a> { @@ -44,10 +46,11 @@ impl<'a> File<'a> { }; return File { - path: path, - stat: stat, - name: filename, - ext: File::ext(filename), + path: path, + stat: stat, + name: filename, + ext: File::ext(filename), + parts: SortPart::split_into_parts(filename), }; } @@ -66,7 +69,7 @@ impl<'a> File<'a> { pub fn display(&self, column: &Column) -> String { match *column { Permissions => self.permissions_string(), - FileName => self.file_colour().paint(self.name.as_slice()), + FileName => self.file_colour().paint(self.name), FileSize(use_iec) => self.file_size(use_iec), // Display the ID if the user/group doesn't exist, which diff --git a/options.rs b/options.rs index 9533abe..22636ba 100644 --- a/options.rs +++ b/options.rs @@ -80,7 +80,7 @@ impl Options { .collect(); match self.sortField { - Name => files.sort_by(|a, b| a.name.cmp(&b.name)), + Name => files.sort_by(|a, b| a.parts.cmp(&b.parts)), Size => files.sort_by(|a, b| a.stat.size.cmp(&b.stat.size)), Extension => files.sort_by(|a, b| { let exts = a.ext.cmp(&b.ext); diff --git a/sort.rs b/sort.rs new file mode 100644 index 0000000..22c269e --- /dev/null +++ b/sort.rs @@ -0,0 +1,49 @@ +// This is an implementation of "natural sort order". See +// http://blog.codinghorror.com/sorting-for-humans-natural-sort-order/ +// for more information and examples. It tries to sort "9" before +// "10", which makes sense to those regular human types. + +// It works by splitting an input string into several parts, and then +// comparing based on those parts. A SortPart derives TotalOrd, so a +// Vec will automatically have natural sorting. + +#[deriving(Eq, Ord, TotalEq, TotalOrd)] +pub enum SortPart<'a> { + Stringular(&'a str), + Numeric(u32), +} + +impl<'a> SortPart<'a> { + pub fn from_string(is_digit: bool, slice: &'a str) -> SortPart<'a> { + if is_digit { + Numeric(from_str::(slice).unwrap()) + } else { + Stringular(slice) + } + } + + // The logic here is taken from my question at + // http://stackoverflow.com/q/23969191/3484614 + + pub fn split_into_parts<'a>(input: &'a str) -> Vec> { + let mut parts = vec![]; + + if input.is_empty() { + return parts + } + + let mut is_digit = input.char_at(0).is_digit(); + let mut start = 0; + + for (i, c) in input.char_indices() { + if is_digit != c.is_digit() { + parts.push(SortPart::from_string(is_digit, input.slice(start, i))); + is_digit = !is_digit; + start = i; + } + } + + parts.push(SortPart::from_string(is_digit, input.slice_from(start))); + parts + } +}