diff --git a/Cargo.toml b/Cargo.toml index 8415f474..a0e9d03f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,6 +35,7 @@ members = [ "pwd", "rm", "rmdir", + "seq", "sleep", "touch", "true", diff --git a/seq/Cargo.toml b/seq/Cargo.toml new file mode 100644 index 00000000..6b346d91 --- /dev/null +++ b/seq/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "seq" +version = "0.1.0" +authors = ["John Pradeep Vincent "] +license = "MPL-2.0-no-copyleft-exception" +build = "build.rs" +edition = "2018" +description = """ +Print numbers from FIRST to LAST, in steps of INCREMENT. +""" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[dependencies] +clap = { version = "^2.33.0", features = ["wrap_help"] } +sugars = "^1.0.0" + +[build-dependencies] +clap = "^2.33.0" diff --git a/seq/build.rs b/seq/build.rs new file mode 100644 index 00000000..9985c3eb --- /dev/null +++ b/seq/build.rs @@ -0,0 +1,24 @@ +use std::env; + +use clap::Shell; + +#[path = "src/cli.rs"] +mod cli; + +fn main() { + let mut app = cli::create_app(); + + let out_dir = match env::var("OUT_DIR") { + Ok(dir) => dir, + Err(err) => { + eprintln!("No OUT_DIR: {}", err); + return; + }, + }; + + app.gen_completions("seq", Shell::Zsh, out_dir.clone()); + app.gen_completions("seq", Shell::Fish, out_dir.clone()); + app.gen_completions("seq", Shell::Bash, out_dir.clone()); + app.gen_completions("seq", Shell::PowerShell, out_dir.clone()); + app.gen_completions("seq", Shell::Elvish, out_dir); +} diff --git a/seq/src/cli.rs b/seq/src/cli.rs new file mode 100644 index 00000000..8476888d --- /dev/null +++ b/seq/src/cli.rs @@ -0,0 +1,30 @@ +use clap::{ + crate_authors, crate_description, crate_name, crate_version, App, AppSettings::ColoredHelp, Arg, +}; + +pub(crate) fn create_app<'a, 'b>() -> App<'a, 'b> { + App::new(crate_name!()) + .version(crate_version!()) + .author(crate_authors!()) + .about(crate_description!()) + .help_message("Display help information.") + .version_message("Display version information.") + .help_short("?") + .settings(&[ColoredHelp]) + .arg(Arg::with_name("FIRST INCREMENT LAST").multiple(true)) + .arg( + Arg::with_name("SEPERATOR") + .short("s") + .long("separator") + .help("use STRING to separate numbers (default: \\n)") + .hide_default_value(true) + .default_value("\n"), + ) + .arg( + Arg::with_name("WIDTH") + .short("w") + .long("equal-width") + .help("equalize width by padding with leading zeroes") + .takes_value(false), + ) +} diff --git a/seq/src/main.rs b/seq/src/main.rs new file mode 100644 index 00000000..1d01f76b --- /dev/null +++ b/seq/src/main.rs @@ -0,0 +1,179 @@ +use std::process; + +mod cli; + +fn main() { + let matches = cli::create_app().get_matches(); + + if let Some(values) = matches.values_of("FIRST INCREMENT LAST") { + let args: Vec<&str> = values.collect(); + if args.len() > 3 { + eprintln!("seq: extra operand '{}'\n Try 'seq --help' for more information.", args[3]); + process::exit(1); + } + let seperator = matches.value_of("SEPERATOR").map(String::from).unwrap(); + let decimals = max_decimal_digits(&args); + let padding = if matches.is_present("WIDTH") { Some(max_digits(&args)) } else { None }; + let (first, inc, last) = find_operands(&args); + let seq = Seq::new(first, inc, last, decimals, seperator, padding); + + for val in seq.into_iter() { + print!("{}", val); + } + println!(""); + } else { + eprintln!("seq: missing operand\n Try 'seq --help' for more information."); + process::exit(1); + } +} + +fn find_operands(args: &[&str]) -> (f64, f64, f64) { + match args.len() { + 1 => (1.0, 1.0, args[0].parse::().unwrap()), + 2 => (args[0].parse().unwrap(), 1.0, args[1].parse().unwrap()), + _ => (args[0].parse().unwrap(), args[1].parse().unwrap(), args[2].parse().unwrap()), + } +} + +struct Seq { + first: f64, + inc: f64, + last: f64, + decimals: usize, + seperator: String, + padding: Option, +} + +impl Seq { + fn new( + first: f64, inc: f64, last: f64, decimals: usize, seperator: String, padding: Option, + ) -> Seq { + Seq { first, inc, last, decimals, seperator, padding } + } +} + +fn max_decimal_digits(args: &[&str]) -> usize { + args.iter().map(|v| v.len() - v.find(".").map(|pos| pos + 1).unwrap_or(v.len())).max().unwrap() +} + +fn max_digits(args: &[&str]) -> usize { + args.iter().map(|v| v.find(".").unwrap_or(v.len())).max().unwrap() +} + +impl IntoIterator for Seq { + type IntoIter = SeqIterator; + type Item = String; + + fn into_iter(self) -> Self::IntoIter { + let current = self.first; + SeqIterator { seq: self, current } + } +} + +struct SeqIterator { + seq: Seq, + current: f64, +} + +impl Iterator for SeqIterator { + type Item = String; + + fn next(&mut self) -> Option { + if self.current > self.seq.last { + None + } else { + let value = format!("{:.*}", self.seq.decimals, self.current); + let digits = value.find(".").unwrap_or(value.len()); + + let mut value = match self.seq.padding { + Some(width) if width > digits => { + let mut padded = "0".repeat(width - digits); + padded.push_str(&value); + padded + }, + _ => value, + }; + + if self.current + self.seq.inc <= self.seq.last { + value.push_str(&self.seq.seperator); + } + self.current += self.seq.inc; + Some(value) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn should_find_max_decimal_digits() { + assert_eq!(max_decimal_digits(&["1.22", "1.3", "1"]), 2); + assert_eq!(max_decimal_digits(&["1", "1.3", "1.1"]), 1); + assert_eq!(max_decimal_digits(&["1"]), 0); + } + + #[test] + fn should_find_max_digits() { + assert_eq!(max_digits(&["1.22", "12.3", "1"]), 2); + assert_eq!(max_digits(&["1.22", "12.3", "123"]), 3); + assert_eq!(max_digits(&["1.22", "-152.3", "123"]), 4); + } + + #[test] + fn should_find_operands() { + assert_eq!(find_operands(&["2", "3", "10"]), (2.0, 3.0, 10.0)); + assert_eq!(find_operands(&["2", "10"]), (2.0, 1.0, 10.0)); + assert_eq!(find_operands(&["3"]), (1.0, 1.0, 3.0)); + } + + fn to_string(xs: Vec<&str>) -> Vec { + xs.into_iter().map(|v: &str| v.to_owned()).collect::>() + } + + #[test] + fn should_generate_sequence() { + assert_eq!( + Seq::new(1.0, 1.0, 3.0, 0, "".to_owned(), None).into_iter().collect::>(), + to_string(vec!["1", "2", "3"]) + ); + + assert_eq!( + Seq::new(1.0, 1.0, 3.0, 0, ",".to_owned(), None).into_iter().collect::>(), + to_string(vec!["1,", "2,", "3"]) + ); + + assert_eq!( + Seq::new(1.0, 1.0, 3.0, 1, "".to_owned(), None).into_iter().collect::>(), + to_string(vec!["1.0", "2.0", "3.0"]) + ); + + assert_eq!( + Seq::new(1.0, 0.2, 2.0, 1, "".to_owned(), None).into_iter().collect::>(), + to_string(vec!["1.0", "1.2", "1.4", "1.6", "1.8", "2.0"]) + ); + + assert_eq!( + Seq::new(1.0, 0.2, 2.0, 3, "".to_owned(), None).into_iter().collect::>(), + to_string(vec!["1.000", "1.200", "1.400", "1.600", "1.800", "2.000"]) + ); + + assert_eq!( + Seq::new(-2.0, 1.0, 2.0, 0, "".to_owned(), None).into_iter().collect::>(), + to_string(vec!["-2", "-1", "0", "1", "2"]) + ); + + assert_eq!( + Seq::new(1.0, 1.0, 3.0, 0, "".to_owned(), None).into_iter().collect::>(), + to_string(vec!["1", "2", "3"]) + ); + + assert_eq!( + Seq::new(1.0, 5.0, 16.0, 0, "".to_owned(), Some(2)) + .into_iter() + .collect::>(), + to_string(vec!["01", "06", "11", "16"]) + ); + } +}