use crate::error::{SortError, SortResult};
use std::str::FromStr;
#[derive(Debug, Clone)]
pub struct SortKey {
pub start_field: usize,
pub start_char: Option<usize>,
pub end_field: Option<usize>,
pub end_char: Option<usize>,
pub options: SortKeyOptions,
}
#[derive(Debug, Clone, Default)]
pub struct SortKeyOptions {
pub numeric: bool,
pub general_numeric: bool,
pub month: bool,
pub reverse: bool,
pub ignore_case: bool,
pub dictionary_order: bool,
pub ignore_leading_blanks: bool,
pub human_numeric: bool,
pub version: bool,
pub random: bool,
}
impl SortKey {
pub fn parse(keydef: &str) -> SortResult<Self> {
let parts: Vec<&str> = keydef.split(',').collect();
if parts.is_empty() || parts.len() > 2 {
return Err(SortError::parse_error(&format!(
"invalid key specification: {keydef}"
)));
}
let (start_field, start_char, start_opts) = Self::parse_field_spec(parts[0])?;
let (end_field, end_char, end_opts) = if parts.len() == 2 {
let (field, char_pos, opts) = Self::parse_field_spec(parts[1])?;
(Some(field), char_pos, opts)
} else {
(None, None, SortKeyOptions::default())
};
let mut options = start_opts;
if !options.numeric {
options.numeric = end_opts.numeric;
}
if !options.general_numeric {
options.general_numeric = end_opts.general_numeric;
}
if !options.month {
options.month = end_opts.month;
}
if !options.reverse {
options.reverse = end_opts.reverse;
}
if !options.ignore_case {
options.ignore_case = end_opts.ignore_case;
}
if !options.dictionary_order {
options.dictionary_order = end_opts.dictionary_order;
}
if !options.ignore_leading_blanks {
options.ignore_leading_blanks = end_opts.ignore_leading_blanks;
}
if !options.human_numeric {
options.human_numeric = end_opts.human_numeric;
}
if !options.version {
options.version = end_opts.version;
}
if !options.random {
options.random = end_opts.random;
}
Ok(Self {
start_field,
start_char,
end_field,
end_char,
options,
})
}
fn parse_field_spec(spec: &str) -> SortResult<(usize, Option<usize>, SortKeyOptions)> {
if spec.is_empty() {
return Err(SortError::parse_error("empty field specification"));
}
let mut chars = spec.chars().peekable();
let mut field_str = String::new();
let mut char_str = String::new();
let mut options = SortKeyOptions::default();
while let Some(&ch) = chars.peek() {
if ch.is_ascii_digit() {
field_str.push(ch);
chars.next();
} else {
break;
}
}
if field_str.is_empty() {
return Err(SortError::parse_error(&format!(
"invalid field specification: {spec}"
)));
}
let field = field_str
.parse::<usize>()
.map_err(|_| SortError::parse_error(&format!("invalid field number: {field_str}")))?;
if field == 0 {
return Err(SortError::parse_error("field numbers start at 1"));
}
let char_pos = if chars.peek() == Some(&'.') {
chars.next(); while let Some(&ch) = chars.peek() {
if ch.is_ascii_digit() {
char_str.push(ch);
chars.next();
} else {
break;
}
}
if char_str.is_empty() {
None
} else {
let pos = char_str.parse::<usize>().map_err(|_| {
SortError::parse_error(&format!("invalid character position: {char_str}"))
})?;
if pos == 0 {
return Err(SortError::parse_error("character positions start at 1"));
}
Some(pos)
}
} else {
None
};
for ch in chars {
match ch {
'n' => options.numeric = true,
'g' => options.general_numeric = true,
'M' => options.month = true,
'r' => options.reverse = true,
'f' => options.ignore_case = true,
'd' => options.dictionary_order = true,
'b' => options.ignore_leading_blanks = true,
'h' => options.human_numeric = true,
'V' => options.version = true,
'R' => options.random = true,
'i' => {} 'z' => {} _ => {
return Err(SortError::parse_error(&format!("invalid key option: {ch}")));
}
}
}
Ok((field, char_pos, options))
}
}
#[derive(Debug, Clone)]
pub struct SortConfig {
pub mode: SortMode,
pub reverse: bool,
pub unique: bool,
pub stable: bool,
pub check: bool,
pub merge: bool,
pub zero_terminated: bool,
pub ignore_case: bool,
pub dictionary_order: bool,
pub ignore_leading_blanks: bool,
pub ignore_nonprinting: bool,
pub field_separator: Option<char>,
pub keys: Vec<SortKey>,
pub output_file: Option<String>,
pub buffer_size: Option<usize>,
pub parallel_threads: Option<usize>,
pub input_files: Vec<String>,
pub debug: bool,
pub compress_temp: bool,
pub temp_dir: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SortMode {
Lexicographic,
Numeric,
GeneralNumeric,
HumanNumeric,
Month,
Version,
Random,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SortOrder {
Ascending,
Descending,
}
impl Default for SortConfig {
fn default() -> Self {
Self {
mode: SortMode::Lexicographic,
reverse: false,
unique: false,
stable: false,
check: false,
merge: false,
zero_terminated: false,
ignore_case: false,
dictionary_order: false,
ignore_leading_blanks: false,
ignore_nonprinting: false,
field_separator: None,
keys: Vec::new(),
output_file: None,
buffer_size: None,
parallel_threads: None,
input_files: Vec::new(),
debug: false,
compress_temp: false,
temp_dir: None,
}
}
}
impl SortConfig {
pub fn new() -> Self {
Self::default()
}
pub fn with_mode(mut self, mode: SortMode) -> Self {
self.mode = mode;
self
}
pub fn with_reverse(mut self, reverse: bool) -> Self {
self.reverse = reverse;
self
}
pub fn with_unique(mut self, unique: bool) -> Self {
self.unique = unique;
self
}
pub fn with_stable(mut self, stable: bool) -> Self {
self.stable = stable;
self
}
pub fn with_check(mut self, check: bool) -> Self {
self.check = check;
self
}
pub fn with_merge(mut self, merge: bool) -> Self {
self.merge = merge;
self
}
pub fn with_zero_terminated(mut self, zero_terminated: bool) -> Self {
self.zero_terminated = zero_terminated;
self
}
pub fn with_field_separator(mut self, separator: Option<char>) -> Self {
self.field_separator = separator;
self
}
pub fn add_key(mut self, key: SortKey) -> Self {
self.keys.push(key);
self
}
pub fn with_output_file(mut self, output_file: Option<String>) -> Self {
self.output_file = output_file;
self
}
pub fn with_buffer_size(mut self, buffer_size: Option<usize>) -> Self {
self.buffer_size = buffer_size;
self
}
pub fn with_parallel_threads(mut self, threads: Option<usize>) -> Self {
self.parallel_threads = threads;
self
}
pub fn with_input_files(mut self, files: Vec<String>) -> Self {
self.input_files = files;
self
}
pub fn with_debug(mut self, debug: bool) -> Self {
self.debug = debug;
self
}
pub fn set_buffer_size_from_string(&mut self, size_str: &str) -> SortResult<()> {
let size = size_str
.parse::<usize>()
.map_err(|_| SortError::internal("Invalid buffer size"))?;
self.buffer_size = Some(size);
Ok(())
}
pub fn validate(&self) -> SortResult<()> {
if self.check && self.merge {
return Err(SortError::conflicting_options(
"cannot use both --check and --merge",
));
}
if self.check && self.unique {
return Err(SortError::conflicting_options(
"--check is incompatible with --unique",
));
}
if self.merge && self.unique {
}
if let Some(sep) = self.field_separator {
if sep == '\0' && !self.zero_terminated {
return Err(SortError::invalid_field_separator(
"null character separator requires -z option",
));
}
}
if let Some(buffer_size) = self.buffer_size {
if buffer_size < 1024 {
return Err(SortError::invalid_buffer_size(
"buffer size too small (minimum 1KB)",
));
}
const MAX_BUFFER_SIZE: u64 = 8 * 1024 * 1024 * 1024; if buffer_size as u64 > MAX_BUFFER_SIZE {
return Err(SortError::invalid_buffer_size(
"buffer size too large (maximum 8GB)",
));
}
}
if let Some(threads) = self.parallel_threads {
if threads == 0 {
return Err(SortError::thread_pool_error(
"thread count must be positive",
));
}
if threads > 1024 {
return Err(SortError::thread_pool_error(
"too many threads (maximum 1024)",
));
}
}
Ok(())
}
pub fn sort_order(&self) -> SortOrder {
if self.reverse {
SortOrder::Descending
} else {
SortOrder::Ascending
}
}
pub fn random_sort(&self) -> bool {
matches!(self.mode, SortMode::Random)
}
pub fn numeric_sort(&self) -> bool {
matches!(
self.mode,
SortMode::Numeric | SortMode::GeneralNumeric | SortMode::HumanNumeric
)
}
pub fn has_typed_keys(&self) -> bool {
false }
pub fn input_file_count(&self) -> usize {
self.input_files.len()
}
pub fn reading_from_stdin(&self) -> bool {
self.input_files.is_empty() || (self.input_files.len() == 1 && self.input_files[0] == "-")
}
pub fn writing_to_stdout(&self) -> bool {
self.output_file.is_none()
}
pub fn effective_buffer_size(&self) -> usize {
self.buffer_size.unwrap_or(1024 * 1024) }
pub fn effective_thread_count(&self) -> usize {
self.parallel_threads.unwrap_or_else(num_cpus::get)
}
pub fn for_merge(&self) -> Self {
let mut config = self.clone();
config.merge = true;
config.check = false;
config
}
pub fn for_check(&self) -> Self {
let mut config = self.clone();
config.check = true;
config.merge = false;
config.unique = false; config
}
}
impl FromStr for SortMode {
type Err = SortError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"lexicographic" | "text" | "default" => Ok(SortMode::Lexicographic),
"numeric" | "n" => Ok(SortMode::Numeric),
"general-numeric" | "g" => Ok(SortMode::GeneralNumeric),
"human-numeric" | "h" => Ok(SortMode::HumanNumeric),
"month" | "m" => Ok(SortMode::Month),
"version" | "v" => Ok(SortMode::Version),
"random" | "r" => Ok(SortMode::Random),
_ => Err(SortError::parse_error(&format!("unknown sort mode: {s}"))),
}
}
}
impl std::fmt::Display for SortMode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let name = match self {
SortMode::Lexicographic => "lexicographic",
SortMode::Numeric => "numeric",
SortMode::GeneralNumeric => "general-numeric",
SortMode::HumanNumeric => "human-numeric",
SortMode::Month => "month",
SortMode::Version => "version",
SortMode::Random => "random",
};
write!(f, "{name}")
}
}
pub struct SortConfigBuilder {
config: SortConfig,
}
impl SortConfigBuilder {
pub fn new() -> Self {
Self {
config: SortConfig::default(),
}
}
pub fn mode(mut self, mode: SortMode) -> Self {
self.config.mode = mode;
self
}
pub fn reverse(mut self) -> Self {
self.config.reverse = true;
self
}
pub fn unique(mut self) -> Self {
self.config.unique = true;
self
}
pub fn stable(mut self) -> Self {
self.config.stable = true;
self
}
pub fn check(mut self) -> Self {
self.config.check = true;
self
}
pub fn merge(mut self) -> Self {
self.config.merge = true;
self
}
pub fn zero_terminated(mut self) -> Self {
self.config.zero_terminated = true;
self
}
pub fn field_separator(mut self, separator: char) -> Self {
self.config.field_separator = Some(separator);
self
}
pub fn key(mut self, key: SortKey) -> Self {
self.config.keys.push(key);
self
}
pub fn output_file(mut self, file: String) -> Self {
self.config.output_file = Some(file);
self
}
pub fn buffer_size(mut self, size: usize) -> Self {
self.config.buffer_size = Some(size);
self
}
pub fn build(self) -> SortResult<SortConfig> {
self.config.validate()?;
Ok(self.config)
}
}
impl Default for SortConfigBuilder {
fn default() -> Self {
Self::new()
}
}
pub mod presets {
use super::*;
pub fn numeric() -> SortConfig {
SortConfig::new().with_mode(SortMode::Numeric)
}
pub fn version() -> SortConfig {
SortConfig::new().with_mode(SortMode::Version)
}
pub fn human_numeric() -> SortConfig {
SortConfig::new().with_mode(SortMode::HumanNumeric)
}
pub fn case_insensitive() -> SortConfig {
let mut config = SortConfig::new();
config.ignore_case = true;
config
}
pub fn unique() -> SortConfig {
SortConfig::new().with_unique(true)
}
pub fn reverse() -> SortConfig {
SortConfig::new().with_reverse(true)
}
pub fn stable() -> SortConfig {
SortConfig::new().with_stable(true)
}
pub fn merge() -> SortConfig {
SortConfig::new().with_merge(true)
}
pub fn check() -> SortConfig {
SortConfig::new().with_check(true)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config() {
let config = SortConfig::default();
assert_eq!(config.mode, SortMode::Lexicographic);
assert!(!config.reverse);
assert!(!config.unique);
assert!(!config.stable);
}
#[test]
fn test_config_builder() {
let config = SortConfigBuilder::new()
.mode(SortMode::Numeric)
.reverse()
.unique()
.build()
.expect("Failed to build test config");
assert_eq!(config.mode, SortMode::Numeric);
assert!(config.reverse);
assert!(config.unique);
}
#[test]
fn test_sort_mode_from_str() {
assert_eq!(
"numeric"
.parse::<SortMode>()
.expect("Failed to parse numeric mode"),
SortMode::Numeric
);
assert_eq!(
"version"
.parse::<SortMode>()
.expect("Failed to parse version mode"),
SortMode::Version
);
assert!("invalid".parse::<SortMode>().is_err());
}
#[test]
fn test_validate_conflicting_options() {
let config = SortConfig {
check: true,
merge: true,
..Default::default()
};
assert!(config.validate().is_err());
}
#[test]
fn test_effective_buffer_size() {
let config = SortConfig::default();
assert_eq!(config.effective_buffer_size(), 1024 * 1024);
let config = SortConfig::default().with_buffer_size(Some(2048));
assert_eq!(config.effective_buffer_size(), 2048);
}
#[test]
fn test_presets() {
let config = presets::numeric();
assert_eq!(config.mode, SortMode::Numeric);
let config = presets::reverse();
assert!(config.reverse);
let config = presets::unique();
assert!(config.unique);
}
#[test]
fn test_reading_from_stdin() {
let config = SortConfig::default();
assert!(config.reading_from_stdin());
let config = SortConfig::default().with_input_files(vec!["-".to_string()]);
assert!(config.reading_from_stdin());
let config = SortConfig::default().with_input_files(vec!["file.txt".to_string()]);
assert!(!config.reading_from_stdin());
}
}