alle Funktionen implementiert
This commit is contained in:
@@ -15,3 +15,4 @@ rusqlite = { version = "0.35.0", features = ["chrono", "bundled"] }
|
||||
langtime = "0.2.1"
|
||||
serde = { version = "1.0.217", features = ["derive"] }
|
||||
tabled = "0.19.0"
|
||||
serde_json = "1.0.140"
|
||||
205
src/commands/display.rs
Normal file
205
src/commands/display.rs
Normal file
@@ -0,0 +1,205 @@
|
||||
use anyhow::Result;
|
||||
use chrono::{DateTime, Duration, Local};
|
||||
use serde_json::to_string_pretty;
|
||||
use tabled::builder::Builder;
|
||||
use tabled::settings::object::Rows;
|
||||
use tabled::settings::themes::Colorization;
|
||||
use tabled::settings::{Border, Color, Padding, Style};
|
||||
|
||||
|
||||
use crate::database::get_sheet_entries;
|
||||
use crate::style::{style_string, Styles};
|
||||
use crate::State;
|
||||
use crate::Entry;
|
||||
use crate::utils::{day_begin, day_end, format_duration, is_same_day};
|
||||
|
||||
|
||||
pub struct ReadableOptions {
|
||||
pub show_ids: bool,
|
||||
pub show_timesheet: bool,
|
||||
pub show_partial_sum: bool,
|
||||
pub show_total: bool,
|
||||
pub show_headings: bool,
|
||||
//pub padding: usize,
|
||||
}
|
||||
|
||||
impl ReadableOptions {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
show_ids: false,
|
||||
show_timesheet: false,
|
||||
show_partial_sum : false,
|
||||
show_total : false,
|
||||
show_headings : false,
|
||||
// padding: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn complete() -> Self {
|
||||
Self {
|
||||
show_ids: true,
|
||||
show_timesheet: true,
|
||||
show_partial_sum: true,
|
||||
show_total: true,
|
||||
show_headings: true,
|
||||
// padding: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn display_tasks(
|
||||
print_json: &bool,
|
||||
sheet: Option<&String>,
|
||||
start: Option<DateTime<Local>>,
|
||||
end: Option<DateTime<Local>>,
|
||||
filter_by_date: &bool,
|
||||
ids: &bool,
|
||||
state: &State,
|
||||
) -> Result<()> {
|
||||
// Getting the data
|
||||
let sheet = sheet.unwrap_or(&state.current_sheet);
|
||||
let mut entries = get_sheet_entries(sheet, &state.database)?;
|
||||
|
||||
if entries.is_empty() {
|
||||
println!(
|
||||
"{} {}",
|
||||
style_string("No sheet found with name:", Styles::Message),
|
||||
sheet
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Sorting
|
||||
entries.sort_by(|a, b| a.start.cmp(&b.start));
|
||||
|
||||
// Filtering
|
||||
let mut start = start;
|
||||
let mut end = end;
|
||||
|
||||
if *filter_by_date {
|
||||
start = start.map(day_begin);
|
||||
end = end.map(day_end);
|
||||
}
|
||||
|
||||
entries.retain(|e| {
|
||||
if start.is_some() && e.start < start.unwrap() {
|
||||
return false;
|
||||
}
|
||||
|
||||
if end.is_some() && e.start > end.unwrap() {
|
||||
return false;
|
||||
}
|
||||
|
||||
true
|
||||
});
|
||||
|
||||
// Displaying
|
||||
match print_json {
|
||||
true => print_all_tasks_json(&entries)?,
|
||||
false => {
|
||||
let mut options = ReadableOptions::complete();
|
||||
options.show_ids = *ids;
|
||||
|
||||
print_all_tasks_readable(sheet, &entries, &options);
|
||||
}
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn print_all_tasks_readable(sheet: &str, entries: &Vec<Entry>, options: &ReadableOptions) {
|
||||
if options.show_timesheet {
|
||||
println!("{} {}", style_string("Timesheet:", Styles::Title), sheet);
|
||||
}
|
||||
|
||||
let mut builder = Builder::new();
|
||||
|
||||
if options.show_headings {
|
||||
let h_id = match options.show_ids {
|
||||
true => "ID",
|
||||
false => "",
|
||||
};
|
||||
|
||||
let headings = vec![h_id, "Date", "Start", "End", "Duration", "Task"];
|
||||
builder.push_record(headings);
|
||||
}
|
||||
|
||||
let mut prev_date = None;
|
||||
let mut day_sum = Duration::zero();
|
||||
for entry in entries {
|
||||
let mut print_date = true;
|
||||
let mut print_partial = false;
|
||||
let is_same = prev_date.is_some() && is_same_day(prev_date.unwrap(), &entry.start);
|
||||
|
||||
if is_same {
|
||||
print_date = false;
|
||||
} else if prev_date.is_some() {
|
||||
print_partial = true;
|
||||
}
|
||||
|
||||
prev_date = Some(&entry.start);
|
||||
|
||||
if print_partial && options.show_partial_sum {
|
||||
builder.push_record(vec!["", "", "", "", &format_duration(&day_sum)]);
|
||||
day_sum = Duration::zero();
|
||||
}
|
||||
|
||||
day_sum = day_sum + entry.get_duration();
|
||||
|
||||
let id = match options.show_ids {
|
||||
true => entry.id.unwrap().to_string(),
|
||||
false => "".to_string(),
|
||||
};
|
||||
|
||||
let date = match print_date {
|
||||
true => entry.start.format("%a %b %d, %Y").to_string(),
|
||||
false => "".to_string(),
|
||||
};
|
||||
|
||||
let start = entry.start.format("%H:%M:%S").to_string();
|
||||
|
||||
let end = match entry.end {
|
||||
Some(d) => d.format("%H:%M:%S").to_string(),
|
||||
None => "".to_string(),
|
||||
};
|
||||
|
||||
builder.push_record(vec![
|
||||
&id,
|
||||
&date,
|
||||
&start,
|
||||
&end,
|
||||
&format_duration(&entry.get_duration()),
|
||||
&entry.name,
|
||||
]);
|
||||
}
|
||||
|
||||
if options.show_partial_sum {
|
||||
builder.push_record(vec!["", "", "", "", &format_duration(&day_sum)]);
|
||||
}
|
||||
|
||||
let total = entries.iter().map(|e| e.get_duration()).sum();
|
||||
if options.show_total {
|
||||
builder.push_record(vec!["Total", "", "", "", &format_duration(&total)]);
|
||||
}
|
||||
|
||||
let mut table = builder.build();
|
||||
table.with(Style::empty());
|
||||
table.with(Padding::new(2, 2, 0, 0));
|
||||
|
||||
if options.show_headings {
|
||||
table.with(Colorization::exact([Color::BOLD], Rows::first()));
|
||||
}
|
||||
|
||||
if options.show_total {
|
||||
table.with(Colorization::exact([Color::BOLD], Rows::last()));
|
||||
table.modify(Rows::last(), Border::new().top('-'));
|
||||
}
|
||||
|
||||
println!("{}", table);
|
||||
}
|
||||
|
||||
pub fn print_all_tasks_json(entries: &Vec<Entry>) -> Result<()> {
|
||||
println!("{}", to_string_pretty(entries)?);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,7 +1,11 @@
|
||||
use anyhow::Result;
|
||||
use crate::database::{get_entry_by_id, running_entry};
|
||||
use crate::{Entry, State};
|
||||
|
||||
use langtime::parse;
|
||||
|
||||
use crate::commands::display::{print_all_tasks_readable, ReadableOptions};
|
||||
use crate::database::{get_entry_by_id, running_entry, update_entry};
|
||||
use crate::style::{style_string, Styles};
|
||||
use crate::State;
|
||||
|
||||
pub fn edit_task(
|
||||
id: &Option<usize>,
|
||||
@@ -29,7 +33,32 @@ pub fn edit_task(
|
||||
|
||||
let mut entry = entry.unwrap_or_else(|| running_entry.unwrap());
|
||||
|
||||
if let Some(start) = start {
|
||||
entry.start = parse(start)?;
|
||||
}
|
||||
|
||||
if let Some(end) = end {
|
||||
entry.end = Some(parse(end)?);
|
||||
}
|
||||
|
||||
if let Some(move_to) = move_to {
|
||||
entry.sheet = move_to.to_string();
|
||||
}
|
||||
|
||||
if let Some(notes) = notes {
|
||||
entry.name = notes.to_string();
|
||||
}
|
||||
|
||||
update_entry(&entry, &state.database)?;
|
||||
|
||||
// Display output
|
||||
println!("{}", style_string("Entry updated:", Styles::Message));
|
||||
|
||||
let mut options = ReadableOptions::new();
|
||||
options.show_headings = true;
|
||||
options.show_ids = true;
|
||||
|
||||
print_all_tasks_readable("", &vec![entry], &options);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
99
src/commands/kill.rs
Normal file
99
src/commands/kill.rs
Normal file
@@ -0,0 +1,99 @@
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::database::{
|
||||
get_all_sheets, get_entry_by_id, remove_entries_by_sheet, remove_entry_by_id,
|
||||
};
|
||||
use crate::style::{style_string, Styles};
|
||||
use crate::State;
|
||||
|
||||
|
||||
pub fn kill_task(id: &usize, state: &mut State) -> Result<()> {
|
||||
let entry = get_entry_by_id(id, &state.database)?;
|
||||
|
||||
// Guard for non-existent entries
|
||||
if entry.is_none() {
|
||||
println!(
|
||||
"{} {}",
|
||||
style_string("Entry not found. Id:", Styles::Message),
|
||||
id
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let entry = entry.unwrap();
|
||||
|
||||
if !confirm_action(&format!(
|
||||
"Are you sure you want to remove entry {}?",
|
||||
entry.name
|
||||
)) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
remove_entry_by_id(id, &state.database)?;
|
||||
|
||||
println!("{} {}", style_string("Removed entry:", Styles::Message), id);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn kill_sheet(sheet: &str, state: &mut State) -> Result<()> {
|
||||
let sheets = get_all_sheets(&state.database)?;
|
||||
|
||||
// Guard for non-existent sheets
|
||||
if !sheets.iter().any(|s| s == sheet) {
|
||||
println!(
|
||||
"{} {}",
|
||||
style_string("Sheet not found:", Styles::Message),
|
||||
sheet
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if !confirm_action(&format!("Are you sure you want to remove sheet {}?", sheet)) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
remove_entries_by_sheet(sheet, &state.database)?;
|
||||
|
||||
// Check edge cases for sheets that are in use
|
||||
if state.current_sheet == sheet {
|
||||
move_to_last_sheet(state)?;
|
||||
} else if state.last_sheet == sheet {
|
||||
state.last_sheet = "default".to_string()
|
||||
}
|
||||
|
||||
println!(
|
||||
"{} {}",
|
||||
style_string("Removed sheet:", Styles::Message),
|
||||
sheet
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn move_to_last_sheet(state: &mut State) -> Result<()> {
|
||||
// If there is a last sheet, move to it, otherwise move
|
||||
// to the first sheet available, or "default" if none exist.
|
||||
let sheets = get_all_sheets(&state.database)?;
|
||||
let last_sheet_exists = sheets.iter().any(|s| s == &state.last_sheet);
|
||||
|
||||
match last_sheet_exists {
|
||||
true => state.change_sheet(&state.last_sheet.clone())?,
|
||||
false => {
|
||||
match !sheets.is_empty() {
|
||||
true => state.change_sheet(&sheets[0])?,
|
||||
false => state.change_sheet("default")?,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
fn confirm_action(msg: &str) -> bool {
|
||||
println!("{} ", msg);
|
||||
let mut input = String::new();
|
||||
std::io::stdin().read_line(&mut input).unwrap();
|
||||
input.trim().to_lowercase() == "y"
|
||||
}
|
||||
@@ -3,9 +3,19 @@ mod in_cmd;
|
||||
mod out;
|
||||
mod lists;
|
||||
mod edit;
|
||||
mod display;
|
||||
mod sheet;
|
||||
mod kill;
|
||||
mod month;
|
||||
|
||||
pub use current::current_task;
|
||||
pub use in_cmd::start_task;
|
||||
pub use out::stop_task;
|
||||
pub use lists::list_sheets;
|
||||
pub use edit::edit_task;
|
||||
pub use display::display_tasks;
|
||||
pub use sheet::checkout_sheet;
|
||||
pub use sheet::rename_sheet;
|
||||
pub use kill::kill_sheet;
|
||||
pub use kill::kill_task;
|
||||
pub use month::display_month;
|
||||
20
src/commands/month.rs
Normal file
20
src/commands/month.rs
Normal file
@@ -0,0 +1,20 @@
|
||||
use anyhow::Result;
|
||||
use chrono::Local;
|
||||
|
||||
use crate::commands::display_tasks;
|
||||
use crate::utils::get_month_boundaries;
|
||||
use crate::State;
|
||||
|
||||
pub fn display_month(
|
||||
json: &bool,
|
||||
ids: &bool,
|
||||
month: Option<&String>,
|
||||
sheet: Option<&String>,
|
||||
state: &mut State,
|
||||
) -> Result<()> {
|
||||
let now = Local::now().format("%Y-%m").to_string();
|
||||
let month = month.unwrap_or(&now);
|
||||
let (start, end) = get_month_boundaries(month)?;
|
||||
|
||||
display_tasks(json, sheet, Some(start), Some(end), &true, ids, state)
|
||||
}
|
||||
46
src/commands/sheet.rs
Normal file
46
src/commands/sheet.rs
Normal file
@@ -0,0 +1,46 @@
|
||||
use anyhow::Result;
|
||||
|
||||
use crate:: {
|
||||
database::update_sheet_name,
|
||||
style::{style_string, Styles},
|
||||
State,
|
||||
};
|
||||
|
||||
pub fn checkout_sheet(name: &str, state: &mut State) -> Result<()> {
|
||||
if state.current_sheet == name {
|
||||
println!(
|
||||
"{} {}",
|
||||
style_string("Already on sheet:", Styles::Message),
|
||||
name
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let name = if name == "-" {
|
||||
state.last_sheet.clone()
|
||||
} else {
|
||||
name.to_string()
|
||||
};
|
||||
|
||||
state.change_sheet(&name)?;
|
||||
|
||||
println!(
|
||||
"{} {}",
|
||||
style_string("Switched to sheet:", Styles::Message),
|
||||
name
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn rename_sheet(name: &str, new_name: &str, state: &mut State) -> Result<()> {
|
||||
update_sheet_name(name, new_name, &state.database)?;
|
||||
if state.current_sheet == name {
|
||||
state.update_sheet_name(new_name)?;
|
||||
}
|
||||
|
||||
println!(
|
||||
"{}",
|
||||
style_string("Sheet renamed successfully", Styles::Message)
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
@@ -104,7 +104,7 @@ pub fn write_entry(entry: &Entry, db: &Connection) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn update_entry(entry: &Entry, db: &Connection) -> Result<()> {
|
||||
pub fn update_entry(entry: &Entry, db: &Connection) -> Result<()> {
|
||||
let query = "
|
||||
UPDATE entries SET
|
||||
note = :note,
|
||||
@@ -219,3 +219,32 @@ pub fn get_entry_by_id(id: &usize, db: &Connection) -> Result<Option<Entry>> {
|
||||
let entry = entries.next();
|
||||
entry.transpose().context(format!("Failed to read entry"))
|
||||
}
|
||||
|
||||
pub fn update_sheet_name(old_name: &str, new_name: &str, db: &Connection) -> Result<()> {
|
||||
let query = "UPDATE entries SET sheet = ? WHERE sheet = ?";
|
||||
let mut stmt = db.prepare(query)?;
|
||||
stmt.execute([new_name, &old_name])?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn remove_entry_by_id(id: &usize, db: &Connection) -> Result<()> {
|
||||
let query = "
|
||||
DELETE FROM entries WHERE id = ?;
|
||||
";
|
||||
|
||||
let mut stmt = db.prepare(query)?;
|
||||
stmt.execute([id])?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn remove_entries_by_sheet(sheet: &str, db: &Connection) -> Result<()> {
|
||||
let query = "
|
||||
DELETE FROM entries WHERE sheet = ?;
|
||||
";
|
||||
|
||||
let mut stmt = db.prepare(query)?;
|
||||
stmt.execute([sheet])?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
use chrono::{DateTime, Duration, Local};
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Eq, PartialOrd, PartialEq ,Ord, Clone)]
|
||||
#[derive(Eq, PartialOrd, PartialEq ,Ord, Clone, Serialize)]
|
||||
pub struct Entry {
|
||||
pub id: Option<usize>,
|
||||
pub start: DateTime<Local>,
|
||||
|
||||
64
src/main.rs
64
src/main.rs
@@ -9,11 +9,10 @@ mod utils;
|
||||
use crate::database::create_tables;
|
||||
use crate::style::{style_string, Styles};
|
||||
use anyhow::{Context, Result};
|
||||
use clap::{ArgAction, Args, Parser, Subcommand};
|
||||
use clap::{Args, Parser, Subcommand};
|
||||
use commands::*;
|
||||
use config::Config;
|
||||
use database::{connect_to_db, ensure_db_exists};
|
||||
use directories::ProjectDirs;
|
||||
pub use entry::Entry;
|
||||
use langtime::parse;
|
||||
pub use state::State;
|
||||
@@ -60,6 +59,20 @@ enum SubCommands {
|
||||
/// The timesehet to display, or the current one
|
||||
sheet: Option<String>,
|
||||
},
|
||||
/// Like 'Display' but for a specific month, or the current one
|
||||
Month {
|
||||
/// Show a JSON representation instead of a human-readable one
|
||||
#[arg(long)]
|
||||
json: bool,
|
||||
/// Show the tasks IDs
|
||||
#[arg(short, long)]
|
||||
ids: bool,
|
||||
/// The specific month to show. The format is yyyy-mm (e.g. "2024-03")
|
||||
#[arg(short, long)]
|
||||
month: Option<String>,
|
||||
/// The timesheet to display, or the current one
|
||||
sheet: Option<String>,
|
||||
},
|
||||
/// Change timesheet
|
||||
Sheet {
|
||||
name: String,
|
||||
@@ -87,6 +100,21 @@ enum SubCommands {
|
||||
},
|
||||
/// Shows the active task for the current sheet
|
||||
Current,
|
||||
/// Removes a task or a whole timesheet
|
||||
Kill {
|
||||
#[command(flatten)]
|
||||
kill_args: KillArgs,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Args, Debug)]
|
||||
#[group(required = true, multiple = false)]
|
||||
struct KillArgs {
|
||||
/// The ID of the task to remove
|
||||
#[arg(long)]
|
||||
id: Option<usize>,
|
||||
/// The name of the timesheet to remove
|
||||
sheet: Option<String>,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
@@ -125,8 +153,29 @@ fn cli() -> Result<()> {
|
||||
end,
|
||||
filter_by_date,
|
||||
sheet,
|
||||
} => {}
|
||||
SubCommands::Sheet {name, rename} => {}
|
||||
} => {
|
||||
display_tasks(
|
||||
json,
|
||||
sheet.as_ref(),
|
||||
start.as_ref().map(|s| parse(s)).transpose()?,
|
||||
end.as_ref().map(|e| parse(e)).transpose()?,
|
||||
filter_by_date,
|
||||
ids,
|
||||
&state,
|
||||
).context("Could not display tasks")?;
|
||||
}
|
||||
SubCommands::Month {
|
||||
json,
|
||||
ids,
|
||||
month,
|
||||
sheet,
|
||||
} => {
|
||||
display_month(json,ids, month.as_ref(), sheet.as_ref(), &mut state).context("Could not display month")?;
|
||||
}
|
||||
SubCommands::Sheet {name, rename} => match rename {
|
||||
None => checkout_sheet(name, &mut state).context("Could not checkout sheet")?,
|
||||
Some(new_name) => rename_sheet(name, new_name, &mut state).context("Could not rename sheet")?,
|
||||
}
|
||||
SubCommands::List => {
|
||||
list_sheets(&state).context("Could not list sheets")?;
|
||||
}
|
||||
@@ -142,6 +191,13 @@ fn cli() -> Result<()> {
|
||||
} => {
|
||||
edit_task(id,start,end,move_to,notes, &mut state).context("Could not edit task")?;
|
||||
}
|
||||
SubCommands::Kill {kill_args, } => {
|
||||
if let Some(id) = &kill_args.id {
|
||||
kill_task(id, &mut state).context("Could not kill task")?;
|
||||
} else if let Some(sheet) = &kill_args.sheet {
|
||||
kill_sheet(sheet, &mut state).context("Could not delete the timesheet")?;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
13
src/state.rs
13
src/state.rs
@@ -49,6 +49,19 @@ impl State {
|
||||
Ok(state)
|
||||
}
|
||||
|
||||
pub fn change_sheet(&mut self, sheet: &str) -> Result<()> {
|
||||
self.last_sheet = self.current_sheet.clone();
|
||||
self.current_sheet = sheet.to_string();
|
||||
self.update_file()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn update_sheet_name(&mut self, sheet: &str) -> Result<()> {
|
||||
self.current_sheet = sheet.to_string();
|
||||
self.update_file()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn update_file(&self) -> Result<()> {
|
||||
let proj_dirs = ProjectDirs::from("de", "schacht-analyse", "timetracker")
|
||||
.ok_or(anyhow!("Could not determine project directories"))?;
|
||||
|
||||
57
src/utils.rs
57
src/utils.rs
@@ -1,5 +1,5 @@
|
||||
use anyhow::Result;
|
||||
use chrono::{DateTime, Local,Duration, LocalResult, NaiveDateTime, TimeZone};
|
||||
use chrono::{DateTime, Datelike, Duration, Local,LocalResult, NaiveDateTime, TimeZone};
|
||||
|
||||
pub fn str_to_datetime(s: &str) -> Result<DateTime<Local>> {
|
||||
let no_tz = s.replace("+00:00", "");
|
||||
@@ -21,3 +21,58 @@ pub fn format_duration(d: &Duration) -> String {
|
||||
|
||||
format!("{}:{:0>2}:{:0>2}", hours, minutes, seconds)
|
||||
}
|
||||
|
||||
pub fn day_begin(dt: DateTime<Local>) -> DateTime<Local> {
|
||||
Local
|
||||
.with_ymd_and_hms(dt.year(), dt.month(), dt.day(), 0, 0, 0)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub fn day_end(dt: DateTime<Local>) -> DateTime<Local> {
|
||||
Local
|
||||
.with_ymd_and_hms(dt.year(), dt.month(), dt.day(), 23, 59, 59)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub fn is_same_day(dt1: &DateTime<Local>, dt2: &DateTime<Local>) -> bool {
|
||||
dt1.year() == dt2.year() && dt1.month() == dt2.month() && dt1.day() == dt2.day()
|
||||
}
|
||||
|
||||
pub fn get_month_boundaries(month: &str) -> Result<(DateTime<Local>, DateTime<Local>)> {
|
||||
let start = get_month_from_string(month)?;
|
||||
let end = get_last_day_of_month(start)?;
|
||||
|
||||
Ok((start, end))
|
||||
}
|
||||
|
||||
// The month is written as 2024-01
|
||||
pub fn get_month_from_string(month_str: &str) -> Result<DateTime<Local>> {
|
||||
let year = month_str.split('-').next().unwrap().parse::<i32>().unwrap();
|
||||
let month = month_str.split('-').nth(1).unwrap().parse::<u32>().unwrap();
|
||||
|
||||
let res = Local.with_ymd_and_hms(year, month, 1, 0, 0, 0);
|
||||
|
||||
match res {
|
||||
chrono::LocalResult::None => Err(anyhow::anyhow!("Invalid month")),
|
||||
chrono::LocalResult::Single(dt) => Ok(dt),
|
||||
chrono::LocalResult::Ambiguous(_, _) => Err(anyhow::anyhow!("Ambiguous month")),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_last_day_of_month(dt: DateTime<Local>) -> Result<DateTime<Local>> {
|
||||
let mut month = dt.month() + 1;
|
||||
let mut year = dt.year();
|
||||
|
||||
if month > 12 {
|
||||
month = 1;
|
||||
year += 1;
|
||||
}
|
||||
|
||||
let res = Local.with_ymd_and_hms(year, month, 1, 0, 0, 0);
|
||||
|
||||
match res {
|
||||
LocalResult::None => Err(anyhow::anyhow!("Invalid month")),
|
||||
LocalResult::Single(dt) => Ok(dt - Duration::days(1)),
|
||||
LocalResult::Ambiguous(_, _) => Err(anyhow::anyhow!("Ambiguous month")),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user