From 6d85bf11d8bb4bc5c41643aba43ae74f6616d181 Mon Sep 17 00:00:00 2001 From: Billy Tobon Date: Sat, 14 Jun 2025 17:21:58 +0200 Subject: [PATCH 1/7] Improved testing and error management --- .gitignore | 3 + week/Cargo.toml | 2 + week/src/lib.rs | 210 +++++++++++++++++++++++++++++----------- week/src/main.rs | 30 +++--- week/tests/cli.rs | 43 -------- week/tests/cli_tests.rs | 82 ++++++++++++++++ 6 files changed, 255 insertions(+), 115 deletions(-) delete mode 100644 week/tests/cli.rs create mode 100644 week/tests/cli_tests.rs diff --git a/.gitignore b/.gitignore index 6985cf1..07e5213 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,6 @@ Cargo.lock # MSVC Windows builds of rustc generate these, which store debugging information *.pdb + +# IDE files +.idea/ diff --git a/week/Cargo.toml b/week/Cargo.toml index dc561d6..2d54e5f 100644 --- a/week/Cargo.toml +++ b/week/Cargo.toml @@ -5,6 +5,8 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +#TODO: update dependencies? + [dependencies] chrono = "0.4.34" clap = { version = "=4.4.18", features = ["derive"] } diff --git a/week/src/lib.rs b/week/src/lib.rs index 526dda4..e9c6e98 100644 --- a/week/src/lib.rs +++ b/week/src/lib.rs @@ -1,94 +1,194 @@ - -use chrono::{Datelike, NaiveDate, ParseError, Local}; +use chrono::{Datelike, Local, NaiveDate}; use regex::Regex; +use anyhow::{Context, Result}; - -pub fn year_week(date: Option) -> Result { - +pub fn year_week(date: Option) -> Result { let this_week = Local::now().date_naive().iso_week().week(); date.map_or(Ok(this_week), |day| week(&day)) - } -#[allow(dead_code)] -fn old_week(str_date: &str) -> Result { //TODO: will remove - - let iso_week = NaiveDate::parse_from_str(str_date, "%Y-%m-%d"). - or(NaiveDate::parse_from_str(str_date, "%Y/%m/%d"))?. //support date w/o year (for current) - iso_week(); - - Ok(iso_week.week()) - - } - - -fn yearless_week(str_date: &str) -> Result { +fn yearless_week(str_date: &str) -> Result { //TODO: add in docs that 01-01 is ok + // Check for day/month, day.month, or day-month + let re = Regex::new(r"^(?\d{1,2})[-/.](?\d{1,2})$") + .context("Failed to compile regex")?; - //check for day/month or day.month - let re = Regex::new(r"(?[0-9]{1,2})[/|.](?[0-9]{1,2})").unwrap(); + let caps = re.captures(str_date) + .context("Date format not recognized")?; - let caps = re.captures(str_date).unwrap(); - let day = caps.name("day").unwrap().as_str().parse::().unwrap(); - let month = caps.name("month").unwrap().as_str().parse::().unwrap(); + let day = caps["day"].parse::() + .context("Invalid day")?; + let month = caps["month"].parse::() + .context("Invalid month")?; - let current_date = chrono::Utc::now(); - let year = current_date.year(); + let year = chrono::Utc::now().year(); - Ok(NaiveDate::from_ymd_opt(year, month, day).unwrap()) + NaiveDate::from_ymd_opt(year, month, day) + .context("Invalid date") } - fn week(str_date: &str) -> Result { +fn week(str_date: &str) -> Result { + let iso_week = NaiveDate::parse_from_str(str_date, "%d-%m-%Y") + .or(NaiveDate::parse_from_str(str_date, "%d/%m/%Y")) + .or_else(|_| yearless_week(str_date)) + .context("Failed to parse date")? + .iso_week(); - let iso_week = NaiveDate::parse_from_str(str_date, "%d-%m-%Y"). - or(NaiveDate::parse_from_str(str_date, "%d/%m/%Y")). - or_else(|_| yearless_week(str_date) )?. - iso_week(); - Ok(iso_week.week()) - - } - - +} #[cfg(test)] mod tests { + use super::*; + use chrono::{Local, Datelike}; + #[test] + fn test_week_with_dd_mm_yyyy_format() { + // Test valid dates in dd-mm-yyyy format + assert_eq!(week("01-01-2024").unwrap(), 1); + assert_eq!(week("15-06-2024").unwrap(), 24); + assert_eq!(week("31-12-2024").unwrap(), 1); // Week 1 of 2025 + assert_eq!(week("29-02-2024").unwrap(), 9); // Leap year + } - use crate::{year_week, week}; + #[test] + fn test_week_with_dd_slash_mm_yyyy_format() { + // Test valid dates in dd/mm/yyyy format + assert_eq!(week("01/01/2024").unwrap(), 1); + assert_eq!(week("15/06/2024").unwrap(), 24); + assert_eq!(week("31/12/2024").unwrap(), 1); + assert_eq!(week("29/02/2024").unwrap(), 9); // Leap year + } #[test] - fn test_week_slashes(){ + fn test_week_with_yearless_dates() { + // Assuming yearless_week function handles formats like "01-01" or "15/06" + // You'll need to adjust these based on your yearless_week implementation + let result = week("01-01"); + assert!(result.is_ok(), "Should handle yearless format"); + + let result = week("15/06"); + assert!(result.is_ok(), "Should handle yearless format with slash"); + } - let date_slash = Some("19/02/2023".to_string()); - assert_eq!(year_week(date_slash),Ok(7)); + #[test] + fn test_week_with_invalid_dates() { + // Test various invalid date formats + assert!(week("invalid").is_err()); + assert!(week("32-01-2024").is_err()); // Invalid day + assert!(week("01-13-2024").is_err()); // Invalid month + assert!(week("29-02-2023").is_err()); // Invalid leap year date + assert!(week("2024-01-01").is_err()); // Wrong format order + assert!(week("").is_err()); // Empty string + // assert!(week("01-01-99").is_err()); // Two-digit year // TODO: Fix this test + } + #[test] + fn test_week_error_messages() { + // Test that error messages contain expected text + match week("invalid") { + Err(error) => { + let error_msg = format!("{}", error); + assert!(error_msg.contains("Failed to parse date")); + } + Ok(_) => panic!("Expected an error, but got Ok"), + } + + match week("32-01-2024") { + Err(error) => { + let error_msg = format!("{}", error); + assert!(error_msg.contains("Failed to parse date")); + } + Ok(_) => panic!("Expected an error, but got Ok"), + } + } + #[test] + fn test_week_boundary_cases() { + // Test week boundaries and edge cases + assert_eq!(week("04-01-2021").unwrap(), 1); // First Monday of 2021 + assert_eq!(week("03-01-2021").unwrap(), 53); // Sunday before (week 53 of 2020) + + // Test last week of year vs first week of next year + assert_eq!(week("28-12-2020").unwrap(), 53); + assert_eq!(week("04-01-2021").unwrap(), 1); } #[test] - fn test_week_dashes(){ - let date_dash = Some("19-02-2023".to_string()); - assert_eq!(year_week(date_dash),Ok(7)); + fn test_year_week_with_none_returns_current_week() { + // Test that None returns the current week + let result = year_week(None).unwrap(); + let expected_week = Local::now().date_naive().iso_week().week(); + assert_eq!(result, expected_week); + } + #[test] + fn test_year_week_with_valid_date() { + // Test with valid date strings + let result = year_week(Some("01-01-2024".to_string())); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), 1); + + let result = year_week(Some("15/06/2024".to_string())); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), 24); } #[test] - fn test_this_week(){ - - let date_dash = None; - let weeks = 1..52; //I don't know when is the reader running this test ยฏ\_(ใƒ„)_/ยฏ - let this_week = year_week(date_dash).unwrap(); - assert!(weeks.contains(&this_week)); + fn test_year_week_with_invalid_date() { + // Test with invalid date strings + let result = year_week(Some("invalid".to_string())); + assert!(result.is_err()); + + let error_msg = format!("{}", result.unwrap_err()); + assert!(error_msg.contains("Failed to parse date")); + let result = year_week(Some("32-01-2024".to_string())); + assert!(result.is_err()); + + let error_msg = format!("{}", result.unwrap_err()); + assert!(error_msg.contains("Failed to parse date")); } #[test] - fn test_yearless_date(){ + fn test_year_week_with_empty_string() { + let result = year_week(Some("".to_string())); + assert!(result.is_err()); - let yearless_date = "02.01"; // This date should always land on week 1 - let week = week(yearless_date).ok().unwrap(); + let error_msg = format!("{}", result.unwrap_err()); + assert!(error_msg.contains("Failed to parse date")); + } - assert_eq!(1,week) + #[test] + fn test_year_week_multiple_formats() { + // Test that both supported formats work through year_week + let dash_result = year_week(Some("15-06-2024".to_string())); + let slash_result = year_week(Some("15/06/2024".to_string())); + + assert!(dash_result.is_ok()); + assert!(slash_result.is_ok()); + assert_eq!(dash_result.unwrap(), slash_result.unwrap()); + } + #[test] + fn test_current_week_consistency() { + // Test that multiple calls to year_week(None) return the same result + // (assuming they run in quick succession) + let week1 = year_week(None).unwrap(); + let week2 = year_week(None).unwrap(); + assert_eq!(week1, week2); } -} \ No newline at end of file + #[test] + fn test_week_number_ranges() { + // Test that week numbers are in valid range (1-53) + let test_dates = vec![ + "01-01-2024", "15-03-2024", "30-06-2024", + "15-09-2024", "31-12-2024" + ]; + + for date in test_dates { + let week_num = week(date).unwrap(); + assert!(week_num >= 1 && week_num <= 53, + "Week number {} for date {} is out of range", week_num, date); + } + } +} diff --git a/week/src/main.rs b/week/src/main.rs index efad84e..54628cb 100644 --- a/week/src/main.rs +++ b/week/src/main.rs @@ -1,28 +1,24 @@ -use clap::Parser; use anyhow::{Context, Ok, Result}; +use clap::Parser; use week::year_week; - /// Simple utility to get week number -#[derive(Parser,Debug)] +#[derive(Parser, Debug)] #[command(version, about, long_about = None)] -struct Args{ - // Optional date on "%d-%m-%Y", "%d/%m/%Y" , "%d/%m" or "%d.%m" format, if no date pased, takes the current date. - #[arg(short, long)] - date: Option - +struct Args { + // Optional date on "%d-%m-%Y", "%d/%m/%Y" , "%d/%m" or "%d.%m" format, if no date passed, takes the current date. + #[arg(short, long)] //TODO: support %d.%m.%Y and %d-%m + date: Option, } -fn main() -> Result<()>{ - +fn main() -> Result<()> { let args = Args::parse(); - let date = args.date; + let date = args.date; - let week_of_year = year_week(date).with_context(|| format!("Unable to parse date"))?; + let week_of_year = year_week(date).with_context(|| "Unable to parse date".to_string())?; println!("Is weeek {}", week_of_year); Ok(()) - + + + + } - - - - diff --git a/week/tests/cli.rs b/week/tests/cli.rs deleted file mode 100644 index 63f74f3..0000000 --- a/week/tests/cli.rs +++ /dev/null @@ -1,43 +0,0 @@ -use assert_cmd::prelude::*; // Add methods on commands -use predicates::prelude::*; // Used for writing assertions -use std::process::Command; // Run programs - - -#[test] -fn todays_week() -> Result<(), Box> { - - - let mut cmd = Command::cargo_bin("week")?; - cmd.assert().success().stdout(predicate::str::is_match("Is weeek \\d{1,2}\\n").unwrap()); //.stdout(predicate::str::is_match("[0-9][0-9]")); - Ok(()) -} - - -#[test] -fn date_week() -> Result<(), Box> { - - let mut cmd = Command::cargo_bin("week")?; - cmd.arg("--date").arg("19-02-2023"); - cmd.assert().success().stdout(predicate::str::contains("Is weeek 7")); - Ok(()) -} - - -#[test] -fn yearless_week() -> Result<(), Box> { - - - let mut cmd = Command::cargo_bin("week")?; - cmd.arg("--date").arg("01/01"); - cmd.assert().success().stdout(predicate::str::is_match("Is weeek \\d{1,2}\\n").unwrap()); //.stdout(predicate::str::is_match("[0-9][0-9]")); - Ok(()) -} - -#[test] -fn invalid_date() -> Result<(), Box>{ - let mut cmd = Command::cargo_bin("week")?; - cmd.arg("--date").arg("202302/19"); - cmd.assert().failure(); - Ok(()) - -} \ No newline at end of file diff --git a/week/tests/cli_tests.rs b/week/tests/cli_tests.rs new file mode 100644 index 0000000..577758d --- /dev/null +++ b/week/tests/cli_tests.rs @@ -0,0 +1,82 @@ +use assert_cmd::prelude::*; // Add methods on commands +use predicates::prelude::*; // Used for writing assertions +use std::process::Command; // Run programs +use anyhow::Result; // Friendlier error management + + + +#[test] +fn test_current_week_output() -> Result<()> { + let mut cmd = Command::cargo_bin("week")?; + cmd.assert() + .success() + .stdout(predicate::str::is_match(r"^Is weeek \d{1,2}\n$")?); + Ok(()) +} + +#[test] +fn date_week() -> Result<(), Box> { + let mut cmd = Command::cargo_bin("week")?; + cmd.arg("--date").arg("19-02-2023"); + cmd.assert() + .success() + .stdout(predicate::str::contains("Is weeek 7")); + Ok(()) +} + +#[test] +fn test_specific_date_formats() -> Result<()> { + let test_cases = vec![ + ("01-01-2024", "1"), // First week of 2024 + ("01/01/2024", "1"), // Same date, different format + ("15/03/2024", "11"), // Mid-March 2024 + //("31.12.2023", "52"), // Last week of 2023 // TODO: Fix this tests, depends on the calendar + ]; + + for (input_date, expected_week) in test_cases { + let mut cmd = Command::cargo_bin("week")?; + cmd.arg("--date").arg(input_date) + .assert() + .success() + .stdout(format!("Is weeek {}\n", expected_week)); + } + Ok(()) +} + +#[test] +fn test_invalid_date_formats() -> Result<()> { + let invalid_dates = vec![ + "invalid", + "32-01-2024", // Invalid day + "01-13-2024", // Invalid month + "2024-01-01", // Wrong format order + "", // Empty string + ]; + + for invalid_date in invalid_dates { + let mut cmd = Command::cargo_bin("week")?; + cmd.arg("--date").arg(invalid_date) + .assert() + .failure() + .stderr(predicate::str::contains("Failed to parse date")); + } + Ok(()) +} + +#[test] +fn yearless_week() -> Result<(), Box> { + let mut cmd = Command::cargo_bin("week")?; + cmd.arg("--date").arg("01/01"); + cmd.assert() + .success() + .stdout(predicate::str::is_match("Is weeek \\d{1,2}\\n").unwrap()); //.stdout(predicate::str::is_match("[0-9][0-9]")); + Ok(()) +} + +#[test] +fn invalid_date() -> Result<(), Box> { + let mut cmd = Command::cargo_bin("week")?; + cmd.arg("--date").arg("202302/19"); + cmd.assert().failure(); + Ok(()) +} From 715aa3f0929cc3210a0b72c49a77223aaa939f01 Mon Sep 17 00:00:00 2001 From: Billy Tobon Date: Sat, 14 Jun 2025 17:42:02 +0200 Subject: [PATCH 2/7] More explicit Cargo file --- week/Cargo.toml | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/week/Cargo.toml b/week/Cargo.toml index 2d54e5f..23d1464 100644 --- a/week/Cargo.toml +++ b/week/Cargo.toml @@ -1,8 +1,15 @@ [package] name = "week" -version = "0.1.0" +version = "0.1.1" edition = "2021" - +rust-version = "1.70" +authors = ["Billy Tobon "] +description = "Simple CLI to get the ISO week number for a given date" +repository = "https://github.com/billyto/week" +license = "MIT" +readme = "README.md" +keywords = ["cli", "date", "week", "calendar", "iso"] +categories = ["command-line-utilities", "date-and-time"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html #TODO: update dependencies? From 1d774bb8b666df6b3b970be0528eeb79a084c559 Mon Sep 17 00:00:00 2001 From: Billy Tobon Date: Sat, 14 Jun 2025 21:26:48 +0200 Subject: [PATCH 3/7] Adds support for %d.%m%.%Y and %d-%m --- README.md | 16 ++++++--------- week/src/lib.rs | 53 ++++++++++++++++++++---------------------------- week/src/main.rs | 4 ++-- 3 files changed, 30 insertions(+), 43 deletions(-) diff --git a/README.md b/README.md index d585d82..d636e5f 100644 --- a/README.md +++ b/README.md @@ -9,16 +9,15 @@ Simple CLI utility to return the ISO week number starting from 1. ``` week //The week of the year for today -week [--date date] //Gives the week for 'date' +week [--date date] //Week for a given 'date' ``` ## Suported Date formats * %d-%m-%Y - * %d/%m/%Y - +* %d.%m%.%Y +* %d-%m * %d/%m - * %d.%m @@ -26,10 +25,7 @@ week [--date date] //Gives the week for 'date' ### TODOs: -[x] fix valid formats, no yeat first and right %xx descriptors - -[x] CICD for release version - +[x] ~~fix valid formats, no yeat first and right %xx descriptors~~ +[x] ~~CICD for release version~~ [] Better cli args descriptions - -[] Tests on ErrorParse +[x] ~~Tests on ErrorParse~~ diff --git a/week/src/lib.rs b/week/src/lib.rs index e9c6e98..58cd2fe 100644 --- a/week/src/lib.rs +++ b/week/src/lib.rs @@ -7,7 +7,7 @@ pub fn year_week(date: Option) -> Result { date.map_or(Ok(this_week), |day| week(&day)) } -fn yearless_week(str_date: &str) -> Result { //TODO: add in docs that 01-01 is ok +fn yearless_week(str_date: &str) -> Result { // Check for day/month, day.month, or day-month let re = Regex::new(r"^(?\d{1,2})[-/.](?\d{1,2})$") .context("Failed to compile regex")?; @@ -29,6 +29,7 @@ fn yearless_week(str_date: &str) -> Result { //TODO: add in docs tha fn week(str_date: &str) -> Result { let iso_week = NaiveDate::parse_from_str(str_date, "%d-%m-%Y") .or(NaiveDate::parse_from_str(str_date, "%d/%m/%Y")) + .or(NaiveDate::parse_from_str(str_date, "%d.%m.%Y")) .or_else(|_| yearless_week(str_date)) .context("Failed to parse date")? .iso_week(); @@ -42,7 +43,7 @@ mod tests { use chrono::{Local, Datelike}; #[test] - fn test_week_with_dd_mm_yyyy_format() { + fn test_week_with_dash_dd_mm_yyyy_format() { // Test valid dates in dd-mm-yyyy format assert_eq!(week("01-01-2024").unwrap(), 1); assert_eq!(week("15-06-2024").unwrap(), 24); @@ -51,7 +52,7 @@ mod tests { } #[test] - fn test_week_with_dd_slash_mm_yyyy_format() { + fn test_week_with_slash_dd_mm_yyyy_format() { // Test valid dates in dd/mm/yyyy format assert_eq!(week("01/01/2024").unwrap(), 1); assert_eq!(week("15/06/2024").unwrap(), 24); @@ -59,15 +60,27 @@ mod tests { assert_eq!(week("29/02/2024").unwrap(), 9); // Leap year } + #[test] + fn test_week_with_dot_dd_mm_yyyy_format() { + // Test valid dates in dd.mm.yyyy format + assert_eq!(week("01.01.2024").unwrap(), 1); + assert_eq!(week("15.06.2024").unwrap(), 24); + assert_eq!(week("31.12.2024").unwrap(), 1); + assert_eq!(week("29.02.2024").unwrap(), 9); // Leap year + } + #[test] fn test_week_with_yearless_dates() { - // Assuming yearless_week function handles formats like "01-01" or "15/06" + // Assuming yearless_week function handles formats like "01-01", "16.07" or "15/06" // You'll need to adjust these based on your yearless_week implementation let result = week("01-01"); assert!(result.is_ok(), "Should handle yearless format"); let result = week("15/06"); assert!(result.is_ok(), "Should handle yearless format with slash"); + + let result = week("16.07"); + assert!(result.is_ok(), "Should handle yearless format with slash"); } #[test] @@ -120,18 +133,7 @@ mod tests { assert_eq!(result, expected_week); } - #[test] - fn test_year_week_with_valid_date() { - // Test with valid date strings - let result = year_week(Some("01-01-2024".to_string())); - assert!(result.is_ok()); - assert_eq!(result.unwrap(), 1); - - let result = year_week(Some("15/06/2024".to_string())); - assert!(result.is_ok()); - assert_eq!(result.unwrap(), 24); - } - + #[test] fn test_year_week_with_invalid_date() { // Test with invalid date strings @@ -162,10 +164,13 @@ mod tests { // Test that both supported formats work through year_week let dash_result = year_week(Some("15-06-2024".to_string())); let slash_result = year_week(Some("15/06/2024".to_string())); + let dot_result = year_week(Some("15.06.2024".to_string())); assert!(dash_result.is_ok()); assert!(slash_result.is_ok()); + assert!(dot_result.is_ok()); assert_eq!(dash_result.unwrap(), slash_result.unwrap()); + } #[test] @@ -176,19 +181,5 @@ mod tests { let week2 = year_week(None).unwrap(); assert_eq!(week1, week2); } - - #[test] - fn test_week_number_ranges() { - // Test that week numbers are in valid range (1-53) - let test_dates = vec![ - "01-01-2024", "15-03-2024", "30-06-2024", - "15-09-2024", "31-12-2024" - ]; - - for date in test_dates { - let week_num = week(date).unwrap(); - assert!(week_num >= 1 && week_num <= 53, - "Week number {} for date {} is out of range", week_num, date); - } - } + } diff --git a/week/src/main.rs b/week/src/main.rs index 54628cb..c81c019 100644 --- a/week/src/main.rs +++ b/week/src/main.rs @@ -6,8 +6,8 @@ use week::year_week; #[derive(Parser, Debug)] #[command(version, about, long_about = None)] struct Args { - // Optional date on "%d-%m-%Y", "%d/%m/%Y" , "%d/%m" or "%d.%m" format, if no date passed, takes the current date. - #[arg(short, long)] //TODO: support %d.%m.%Y and %d-%m + // Optional date on "%d-%m-%Y", "%d/%m/%Y" , %d.%m.%Y, %d-%m, "%d/%m" or "%d.%m" format, if no date passed, takes the current date. + #[arg(short, long)] date: Option, } fn main() -> Result<()> { From 942ab7a396a506f83c62e7a0be2c0cda8fc4fe42 Mon Sep 17 00:00:00 2001 From: Billy Tobon Date: Sat, 14 Jun 2025 21:42:57 +0200 Subject: [PATCH 4/7] Improves CLI attributes descriptions and help --- week/src/main.rs | 43 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 37 insertions(+), 6 deletions(-) diff --git a/week/src/main.rs b/week/src/main.rs index c81c019..879cce5 100644 --- a/week/src/main.rs +++ b/week/src/main.rs @@ -1,20 +1,51 @@ -use anyhow::{Context, Ok, Result}; use clap::Parser; +use anyhow::{Context, Ok, Result}; use week::year_week; -/// Simple utility to get week number +/// Simple utility to get the ISO week number (1-53) for any date +/// +/// Returns the current week number if no date is provided. +/// Supports multiple date formats for convenience. #[derive(Parser, Debug)] -#[command(version, about, long_about = None)] +#[command( + name = "week", + version, + about = "Get ISO week number for any date", + long_about = "A simple CLI utility that returns the ISO week number (1-53) for a given date.\n\ + If no date is provided, returns the current week number.\n\ + \n\ + Examples:\n \ + week # Current week\n \ + week -d 19-02-2023 # Week for specific date\n \ + week --date 15/03 # Week for date in current year" +)] struct Args { - // Optional date on "%d-%m-%Y", "%d/%m/%Y" , %d.%m.%Y, %d-%m, "%d/%m" or "%d.%m" format, if no date passed, takes the current date. - #[arg(short, long)] + /// Date to get week number for + /// + /// Supported formats: + /// - DD-MM-YYYY (e.g., 19-02-2023) + /// - DD/MM/YYYY (e.g., 19/02/2023) + /// - DD.MM.YYYY (e.g., 19.02.2023) + /// - DD/MM (e.g., 19.02) - uses current year + /// - DD/MM (e.g., 19/02) - uses current year + /// - DD.MM (e.g., 19.02) - uses current year + /// + /// If not provided, returns the current week number + #[arg( + short = 'd', + long = "date", + value_name = "DATE", + help = "Date to get week number for (various formats supported)" + )] date: Option, } fn main() -> Result<()> { let args = Args::parse(); let date = args.date; - let week_of_year = year_week(date).with_context(|| "Unable to parse date".to_string())?; + + let week_of_year = year_week(date) + .with_context(|| "Unable to parse date. Please use format: DD.MM.YYYY, DD-MM-YYYY, DD/MM/YYYY, DD-MM, DD/MM, or DD.MM")?; println!("Is weeek {}", week_of_year); Ok(()) From cd3c0b2775338ef713d4b6f47e741ba2d728b773 Mon Sep 17 00:00:00 2001 From: Billy Tobon Date: Mon, 16 Jun 2025 21:03:31 +0200 Subject: [PATCH 5/7] Better readme file --- README.md | 157 +++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 139 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index d636e5f..78a8f31 100644 --- a/README.md +++ b/README.md @@ -1,31 +1,152 @@ +# week + [![Build & Test](https://github.com/billyto/week/actions/workflows/build.yml/badge.svg)](https://github.com/billyto/week/actions/workflows/build.yml) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) +A simple and fast CLI utility to get ISO week numbers (1-53) for any date. -# week -Simple CLI utility to return the ISO week number starting from 1. +## ๐Ÿš€ Quick Start + +```bash +# Get current week number +week + +# Get week number for a specific date +week --date 19-02-2023 +week -d 15/03/2024 +week -d 01.01 +``` + +## ๐Ÿ“– Usage + +``` +week [OPTIONS] + +OPTIONS: + -d, --date Date to get week number for (various formats supported) + -h, --help Print help information + -V, --version Print version information +``` + +### Examples + +```bash +# Current week +$ week +Week 24 + +# Specific date with year +$ week --date 19-02-2023 +Week 7 + +$ week -d 25/12/2023 +Week 52 + +# Date without year (uses current year) +$ week -d 15/03 +Week 11 + +$ week -d 01.01 +Week 1 +``` + +## ๐Ÿ“… Supported Date Formats + +The tool accepts dates in multiple convenient formats: + +| Format | Example | Description | +|--------------|--------------|-------------------------------| +| `DD-MM-YYYY` | `19-02-2023` | Day-Month-Year with dashes | +| `DD/MM/YYYY` | `19/02/2023` | Day-Month-Year with slashes | +| `DD.MM.YYYY` | `19.02.2023` | Day-Month-Year with periods | +| `DD-MM` | `19-02` | Day-Month (uses current year) | +| `DD/MM` | `19/02` | Day-Month (uses current year) | +| `DD.MM` | `19.02` | Day-Month (uses current year) | + +> **Note**: All formats use DD-MM ordering (day first, then month) -## Use cases +## ๐Ÿ› ๏ธ Installation +### From Source + +```bash +# Clone the repository +git clone https://github.com/billyto/week.git +cd week + +# Build and install +cargo install --path week +``` + +### From Releases + +Download pre-built binaries from the [releases page](https://github.com/billyto/week/releases) for: +- Windows (x86_64) +- Linux (x86_64) +- macOS (x86_64) + +## ๐Ÿงช Development + +### Prerequisites + +- Rust 1.70.0 or later +- Cargo + +### Building + +```bash +cd week +cargo build --release +``` + +### Running Tests + +```bash +# Unit tests +cargo test + +# Integration tests +cargo test --test cli ``` -week //The week of the year for today -week [--date date] //Week for a given 'date' +### Project Structure + +``` +week/ +โ”œโ”€โ”€ src/ +โ”‚ โ”œโ”€โ”€ lib.rs # Core date parsing logic +โ”‚ โ””โ”€โ”€ main.rs # CLI interface +โ”œโ”€โ”€ tests/ +โ”‚ โ””โ”€โ”€ cli_tests.rs # Integration tests +โ””โ”€โ”€ Cargo.toml # Dependencies and metadata ``` -## Suported Date formats -* %d-%m-%Y -* %d/%m/%Y -* %d.%m%.%Y -* %d-%m -* %d/%m -* %d.%m +## ๐Ÿ“‹ About ISO Week Numbers + +This tool returns ISO week numbers according to [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601): + +- Week numbers range from 1 to 53 +- Week 1 is the first week with at least 4 days in the new year +- Weeks start on Monday +- Some years have 53 weeks + + +## ๐Ÿ“ License + +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. + +## ๐Ÿ—บ๏ธ Roadmap +- [x] Support multiple date formats +- [x] CI/CD pipeline for releases +- [x] Comprehensive CLI help +- [x] Integration tests +- [ ] Add more date format support (ISO 8601, US format) +- [ ] Verbose and Quiet flags +- [ ] Cowsay option ๐Ÿฎ ---- +## ๐Ÿ› Known Issues -### TODOs: +- Error handling for invalid dates could be more descriptive +- Limited to Gregorian calendar only -[x] ~~fix valid formats, no yeat first and right %xx descriptors~~ -[x] ~~CICD for release version~~ -[] Better cli args descriptions -[x] ~~Tests on ErrorParse~~ From dd9c13ac12b96175210f8e63592095e0e8a9a107 Mon Sep 17 00:00:00 2001 From: Billy Tobon Date: Mon, 16 Jun 2025 21:59:51 +0200 Subject: [PATCH 6/7] Updates libs and check some tests --- README.md | 1 + week/Cargo.toml | 12 ++++++------ week/src/lib.rs | 3 ++- week/tests/cli_tests.rs | 2 +- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 78a8f31..4c2083f 100644 --- a/README.md +++ b/README.md @@ -149,4 +149,5 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file - Error handling for invalid dates could be more descriptive - Limited to Gregorian calendar only +- It will parse two-digit years as 00YY, isntead of output an error message ([chrono library issue](https://github.com/chronotope/chrono/issues/332)) diff --git a/week/Cargo.toml b/week/Cargo.toml index 23d1464..9c93c88 100644 --- a/week/Cargo.toml +++ b/week/Cargo.toml @@ -15,13 +15,13 @@ categories = ["command-line-utilities", "date-and-time"] #TODO: update dependencies? [dependencies] -chrono = "0.4.34" -clap = { version = "=4.4.18", features = ["derive"] } -anyhow = "1.0.80" -regex = "1.10.3" +chrono = "0.4.41" +clap = { version = "=4.5.40", features = ["derive"] } +anyhow = "1.0.98" +regex = "1.11.1" [dev-dependencies] -assert_cmd = "2.0.14" -predicates = "3.1.0" +assert_cmd = "2.0.17" +predicates = "3.1.3" diff --git a/week/src/lib.rs b/week/src/lib.rs index 58cd2fe..342d985 100644 --- a/week/src/lib.rs +++ b/week/src/lib.rs @@ -92,7 +92,8 @@ mod tests { assert!(week("29-02-2023").is_err()); // Invalid leap year date assert!(week("2024-01-01").is_err()); // Wrong format order assert!(week("").is_err()); // Empty string - // assert!(week("01-01-99").is_err()); // Two-digit year // TODO: Fix this test + // TODO: Fix this test w chrono 0.5 + //assert!(week("01-01-99").is_err()); // Two-digit year } #[test] diff --git a/week/tests/cli_tests.rs b/week/tests/cli_tests.rs index 577758d..c9cc43d 100644 --- a/week/tests/cli_tests.rs +++ b/week/tests/cli_tests.rs @@ -30,7 +30,7 @@ fn test_specific_date_formats() -> Result<()> { ("01-01-2024", "1"), // First week of 2024 ("01/01/2024", "1"), // Same date, different format ("15/03/2024", "11"), // Mid-March 2024 - //("31.12.2023", "52"), // Last week of 2023 // TODO: Fix this tests, depends on the calendar + ("31.12.2023", "52"), // Last week of 2023 ]; for (input_date, expected_week) in test_cases { From 815d8ed4c69c7e71736e97af4db1474bdc3ad832 Mon Sep 17 00:00:00 2001 From: Billy Tobon <137653+billyto@users.noreply.github.com> Date: Mon, 16 Jun 2025 22:14:38 +0200 Subject: [PATCH 7/7] Update week/Cargo.toml Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- week/Cargo.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/week/Cargo.toml b/week/Cargo.toml index 9c93c88..88eef20 100644 --- a/week/Cargo.toml +++ b/week/Cargo.toml @@ -12,7 +12,6 @@ keywords = ["cli", "date", "week", "calendar", "iso"] categories = ["command-line-utilities", "date-and-time"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html -#TODO: update dependencies? [dependencies] chrono = "0.4.41"