1373 lines
41 KiB
Rust
1373 lines
41 KiB
Rust
use serde_yaml;
|
|
use serde::ser::{Serialize};
|
|
use serde;
|
|
use chrono::{DateTime, Local};
|
|
use chrono::naive::NaiveDateTime;
|
|
use regex::Regex;
|
|
use croner;
|
|
use clap::Parser;
|
|
use std::io::Write;
|
|
use tempdir::TempDir;
|
|
|
|
fn main() {
|
|
let flags = Flags::new();
|
|
|
|
let mut db = DB::new(flags.path).unwrap();
|
|
|
|
match flags.add.clone() {
|
|
Some(s) => {
|
|
let t = match flags.add_schedule {
|
|
Some(sch) => Task::from_string_schedule(s, sch),
|
|
None => Task::from_string(s),
|
|
};
|
|
db.tasks_and_metadatas[0].tasks.0.push(t);
|
|
},
|
|
_ => {},
|
|
};
|
|
|
|
if flags.edit {
|
|
db = db.edit().expect("failed to edit")
|
|
}
|
|
|
|
if flags.edit || flags.add.is_some() {
|
|
db.save(flags.dry_run).expect("failed to save");
|
|
}
|
|
|
|
if flags.list {
|
|
println!("{}", db.due().to_string());
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Parser)]
|
|
struct Flags {
|
|
#[arg(short = 'f', long = "path", default_value="$PTTODO_FILE")]
|
|
path: String,
|
|
|
|
#[arg(short = 'a', long = "add")]
|
|
add: Option<String>,
|
|
|
|
#[arg(short = 's', long = "add-schedule")]
|
|
add_schedule: Option<String>,
|
|
|
|
#[arg(short = 'e', long = "edit", default_value="false")]
|
|
edit: bool,
|
|
|
|
#[arg(short = 'd', long = "dry-run", default_value="false")]
|
|
dry_run: bool,
|
|
|
|
#[arg(short = 'l', long = "list", default_value="true")]
|
|
list: bool,
|
|
}
|
|
|
|
impl Flags {
|
|
fn new() -> Flags {
|
|
let mut result = Flags::parse();
|
|
|
|
if result.path.get(..1) == Some("$") {
|
|
result.path = std::env::var(
|
|
result.path.get(1..).unwrap()
|
|
).expect(format!("'{}' unset", result.path).as_str());
|
|
}
|
|
|
|
result
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, PartialEq, Clone)]
|
|
pub struct DB {
|
|
tasks_and_metadatas: Vec<TasksAndMetadata>,
|
|
cwd: String,
|
|
}
|
|
|
|
impl DB {
|
|
pub fn new(path: String) -> Result<DB, String> {
|
|
let metadata = match std::fs::metadata(path.clone()) {
|
|
Ok(v) => Ok(v),
|
|
Err(msg) => Err(format!("failed to load {}: {}", path, msg)),
|
|
}?;
|
|
let mut files = vec![];
|
|
if metadata.is_file() {
|
|
files.push(path.clone());
|
|
} else if metadata.is_dir() {
|
|
match std::fs::read_dir(path.clone()) {
|
|
Ok(paths) => {
|
|
files.extend(paths
|
|
.filter(|x| x.is_ok())
|
|
.map(|x| x.unwrap())
|
|
.filter(|x| x.metadata().unwrap().is_file())
|
|
.map(|x| x.path().display().to_string())
|
|
);
|
|
Ok(())
|
|
},
|
|
Err(msg) => Err(format!("failed to read {}: {}", path.clone(), msg)),
|
|
}?;
|
|
}
|
|
|
|
let mut result = vec![];
|
|
for file in files {
|
|
let item = TasksAndMetadata::new(file)?;
|
|
result.push(item);
|
|
}
|
|
Ok(DB{
|
|
tasks_and_metadatas: result,
|
|
cwd: match metadata.is_file() {
|
|
true => std::path::Path::new(&path).parent().expect("root?").display().to_string(),
|
|
_ => path.clone(),
|
|
},
|
|
})
|
|
}
|
|
|
|
fn new_empty(cwd: String) -> DB {
|
|
DB{tasks_and_metadatas: vec![], cwd: cwd.clone()}
|
|
}
|
|
|
|
pub fn incomplete(&self) -> Tasks {
|
|
let mut result = Tasks::new();
|
|
for set in &self.tasks_and_metadatas {
|
|
result.0.extend(set.tasks.incomplete().0);
|
|
}
|
|
result
|
|
}
|
|
|
|
pub fn due(&self) -> Tasks {
|
|
let mut result = Tasks::new();
|
|
for set in &self.tasks_and_metadatas {
|
|
result.0.extend(set.tasks.due().0);
|
|
}
|
|
result
|
|
}
|
|
|
|
pub fn edit(&self) -> Result<DB, String> {
|
|
self._edit(|cwd: &String| {
|
|
std::process::Command::new("/bin/sh")
|
|
.current_dir(cwd)
|
|
.arg("-c")
|
|
.arg("vim -p ./*")
|
|
.spawn()
|
|
.expect("failed to start vim")
|
|
.wait()
|
|
.expect("failed to vim");
|
|
})
|
|
}
|
|
|
|
fn _edit(&self, edit: impl Fn(&String)) -> Result<DB, String> {
|
|
let d = TempDir::new(&TS::now().to_string()).expect("failed to create a temp dir");
|
|
for set in &self.tasks_and_metadatas {
|
|
let base = set.file.split("/").last().unwrap().to_string();
|
|
let f = d.path().join(base);
|
|
set.save_due_as(f.display().to_string())?;
|
|
}
|
|
|
|
loop {
|
|
let cwd = d.path().display().to_string();
|
|
edit(&cwd);
|
|
let mut ok = true;
|
|
for f in std::fs::read_dir(&cwd).expect("failed to list edited files") {
|
|
ok = ok && TasksAndMetadata::new(f.expect("failed to list edited file").path().display().to_string()).is_ok();
|
|
}
|
|
if ok {
|
|
break;
|
|
}
|
|
}
|
|
|
|
let mut result = vec![];
|
|
for f in std::fs::read_dir(d.path()).expect("failed to list edited files") {
|
|
let f = f.expect("failed to list edited file").path();
|
|
let base = f.display().to_string().split("/").last().unwrap().to_string();
|
|
let set = match self.tasks_and_metadatas
|
|
.iter()
|
|
.filter(|tasks_and_metadata| tasks_and_metadata.file.ends_with(format!("/{}", base).as_str()))
|
|
.nth(0) {
|
|
Some(set) => set.clone(),
|
|
None => TasksAndMetadata::new_with(
|
|
format!("{}/{}",
|
|
self.cwd,
|
|
base,
|
|
),
|
|
TS::now(),
|
|
Tasks::new(),
|
|
),
|
|
};
|
|
|
|
let edited = TasksAndMetadata::new(f.display().to_string()).expect("failed to read edited tasks");
|
|
|
|
let was_due = set.due();
|
|
let mut now_due = vec![];
|
|
let mut not_due = set.not_due().tasks.0;
|
|
|
|
for task in &edited.tasks.0 {
|
|
if task.is_due() {
|
|
eprintln!("now_due.push({:?})", task.clone());
|
|
now_due.push(task.clone());
|
|
} else if !not_due.contains(task) {
|
|
eprintln!("not_due.push({:?})", task.clone());
|
|
not_due.push(task.clone());
|
|
}
|
|
}
|
|
|
|
let mut already_due = vec![];
|
|
let mut already_not_due = vec![];
|
|
for task in &was_due.tasks.0 {
|
|
let mut without_ts = task.clone();
|
|
without_ts.unset("ts".to_string());
|
|
|
|
let mut with_ts = task.clone();
|
|
with_ts.set("ts".to_string(), TS::now().to_string());
|
|
|
|
if now_due.contains(&without_ts) {
|
|
} else if not_due.contains(task) {
|
|
} else if task.is_due() {
|
|
eprintln!("already_due.push({:?})", with_ts.clone());
|
|
already_due.push(with_ts.clone());
|
|
} else {
|
|
eprintln!("already_not_due.push({:?})", task.clone());
|
|
already_not_due.push(task.clone());
|
|
}
|
|
}
|
|
|
|
let mut new_tasks = Tasks::new();
|
|
new_tasks.0.extend(now_due);
|
|
new_tasks.0.extend(already_due);
|
|
new_tasks.0.extend(not_due);
|
|
new_tasks.0.extend(already_not_due);
|
|
|
|
result.push(TasksAndMetadata::new_with(
|
|
set.file.clone(),
|
|
set.version.clone(),
|
|
new_tasks,
|
|
));
|
|
}
|
|
|
|
Ok(DB{tasks_and_metadatas: result, cwd: self.cwd.clone()})
|
|
}
|
|
|
|
pub fn save(&self, dry_run: bool) -> Result<(), String> {
|
|
for i in &self.tasks_and_metadatas {
|
|
i.save(dry_run)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test_db {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn resolved_scheduled_recurring() {
|
|
assert!(false, "not impl")
|
|
}
|
|
|
|
#[test]
|
|
fn resolved_scheduled_non_recurring() {
|
|
assert!(false, "not impl")
|
|
}
|
|
|
|
#[test]
|
|
fn conflicting_save_handled() {
|
|
use std::ops::Add;
|
|
|
|
let d = TempDir::new(&TS::now().to_string()).expect("failed to create a temp dir");
|
|
let d = d.path().display().to_string();
|
|
let p = format!("{}/f", &d);
|
|
{
|
|
let mut f = std::fs::File::create(p.clone()).expect("failed to create a file in cwd");
|
|
f.write_all(b"- x").expect("failed to create a file");
|
|
}
|
|
|
|
let mut db = DB::new(d.clone()).expect("failed to open tempd");
|
|
assert_eq!(1, db.tasks_and_metadatas[0].tasks.len());
|
|
|
|
std::fs::File::open(p)
|
|
.expect("failed to open file to change modtime")
|
|
.set_modified(std::time::SystemTime::now().add(std::time::Duration::new(10, 0)))
|
|
.expect("failed to change modtime");
|
|
|
|
db.tasks_and_metadatas[0].tasks.0.push(Task::new());
|
|
db.save(false).expect("save as conflicting update shouldve saved as a different file");
|
|
|
|
let db = DB::new(d.clone()).expect("failed to open tempd again");
|
|
assert_eq!(2, db.tasks_and_metadatas.len());
|
|
assert_eq!(1, db.tasks_and_metadatas[0].tasks.len());
|
|
assert_eq!(2, db.tasks_and_metadatas[1].tasks.len());
|
|
}
|
|
|
|
|
|
#[test]
|
|
fn edit_append_duplicate_keeps_both() {
|
|
use std::io::Seek;
|
|
|
|
let db = new_test_db(1, 1);
|
|
let db_after = db._edit(|cwd| {
|
|
let files_in_dir = std::fs::read_dir(cwd).expect("failed to ls cwd").collect::<Vec<_>>();
|
|
assert_eq!(1, files_in_dir.len());
|
|
let mut f = std::fs::OpenOptions::new()
|
|
.write(true)
|
|
.append(true)
|
|
.open(files_in_dir[0].as_ref().expect("failed to get file in dir").path())
|
|
.expect("failed to open file for append");
|
|
f.seek(std::io::SeekFrom::End(0)).expect("failed to seek to end");
|
|
f.write_all(b"\n- f").expect("failed to append the file in cwd");
|
|
f.write_all(b"\n- f").expect("failed to append the file in cwd");
|
|
}).expect("failed to not edit");
|
|
assert_ne!(db, db_after);
|
|
|
|
assert_eq!(1, db_after.tasks_and_metadatas.len());
|
|
assert_eq!(3, db_after.tasks_and_metadatas[0].tasks.len());
|
|
assert_eq!(db.tasks_and_metadatas[0].tasks.0[0], db_after.tasks_and_metadatas[0].tasks.0[0]);
|
|
assert_eq!(Some("f".to_string()), db_after.tasks_and_metadatas[0].tasks.0[1].get("is".to_string()));
|
|
assert_eq!(Some("f".to_string()), db_after.tasks_and_metadatas[0].tasks.0[2].get("is".to_string()));
|
|
}
|
|
|
|
#[test]
|
|
fn edit_schedule_future() {
|
|
let db = new_test_db(0, 0);
|
|
let db_after = db._edit(|cwd| {
|
|
let mut task = Task::new();
|
|
task.set("k".to_string(), "v".to_string());
|
|
task.set("schedule".to_string(), "2101-02-03".to_string());
|
|
|
|
let mut tasks = Tasks::new();
|
|
tasks.0.push(task);
|
|
|
|
let temp = TasksAndMetadata::new_with(
|
|
format!("{}/only", cwd),
|
|
TS::now(),
|
|
tasks,
|
|
);
|
|
temp.save(false).expect("failed to save");
|
|
}).expect("failed to not edit");
|
|
assert_ne!(db, db_after);
|
|
|
|
assert_eq!(1, db_after.tasks_and_metadatas.len());
|
|
assert_eq!(1, db_after.tasks_and_metadatas[0].tasks.len());
|
|
assert_eq!(0, db_after.tasks_and_metadatas[0].due().tasks.len());
|
|
assert_eq!(1, db_after.tasks_and_metadatas[0].not_due().tasks.len());
|
|
}
|
|
|
|
#[test]
|
|
fn edit_schedule_past() {
|
|
let db = new_test_db(0, 0);
|
|
let db_after = db._edit(|cwd| {
|
|
let mut task = Task::new();
|
|
task.set("k".to_string(), "v".to_string());
|
|
task.set("schedule".to_string(), "2001-02-03".to_string());
|
|
|
|
let mut tasks = Tasks::new();
|
|
tasks.0.push(task);
|
|
|
|
let temp = TasksAndMetadata::new_with(
|
|
format!("{}/only", cwd),
|
|
TS::now(),
|
|
tasks,
|
|
);
|
|
temp.save(false).expect("failed to save");
|
|
}).expect("failed to not edit");
|
|
assert_ne!(db, db_after);
|
|
|
|
assert_eq!(1, db_after.tasks_and_metadatas.len());
|
|
assert_eq!(1, db_after.tasks_and_metadatas[0].tasks.len());
|
|
assert_eq!(1, db_after.tasks_and_metadatas[0].due().tasks.len());
|
|
assert_eq!(0, db_after.tasks_and_metadatas[0].not_due().tasks.len());
|
|
}
|
|
|
|
|
|
#[test]
|
|
fn edit_append_second_file() {
|
|
use std::io::Seek;
|
|
|
|
let db = new_test_db(2, 1);
|
|
let db_after = db._edit(|cwd| {
|
|
let files_in_dir = std::fs::read_dir(cwd).expect("failed to ls cwd").collect::<Vec<_>>();
|
|
assert_eq!(2, files_in_dir.len());
|
|
let mut f = std::fs::OpenOptions::new()
|
|
.write(true)
|
|
.append(true)
|
|
.open(files_in_dir[1].as_ref().expect("failed to get file in dir").path())
|
|
.expect("failed to open file for append");
|
|
f.seek(std::io::SeekFrom::End(0)).expect("failed to seek to end");
|
|
f.write_all(b"\n- f").expect("failed to append the file in cwd");
|
|
}).expect("failed to not edit");
|
|
assert_ne!(db, db_after);
|
|
|
|
assert_eq!(2, db_after.tasks_and_metadatas.len());
|
|
assert_eq!(1, db_after.tasks_and_metadatas[0].tasks.len());
|
|
assert_eq!(2, db_after.tasks_and_metadatas[1].tasks.len());
|
|
assert_eq!(db.tasks_and_metadatas[1].tasks.0[0], db_after.tasks_and_metadatas[1].tasks.0[0]);
|
|
assert_eq!(Some("f".to_string()), db_after.tasks_and_metadatas[1].tasks.0[1].get("is".to_string()));
|
|
}
|
|
|
|
#[test]
|
|
fn edit_append_current_file() {
|
|
use std::io::Seek;
|
|
|
|
let db = new_test_db(1, 1);
|
|
let db_after = db._edit(|cwd| {
|
|
let files_in_dir = std::fs::read_dir(cwd).expect("failed to ls cwd").collect::<Vec<_>>();
|
|
assert_eq!(1, files_in_dir.len());
|
|
let mut f = std::fs::OpenOptions::new()
|
|
.write(true)
|
|
.append(true)
|
|
.open(files_in_dir[0].as_ref().expect("failed to get file in dir").path())
|
|
.expect("failed to open file for append");
|
|
f.seek(std::io::SeekFrom::End(0)).expect("failed to seek to end");
|
|
f.write_all(b"\n- f").expect("failed to append the file in cwd");
|
|
}).expect("failed to not edit");
|
|
assert_ne!(db, db_after);
|
|
|
|
assert_eq!(1, db_after.tasks_and_metadatas.len());
|
|
assert_eq!(2, db_after.tasks_and_metadatas[0].tasks.len());
|
|
assert_eq!(db.tasks_and_metadatas[0].tasks.0[0], db_after.tasks_and_metadatas[0].tasks.0[0]);
|
|
assert_eq!(Some("f".to_string()), db_after.tasks_and_metadatas[0].tasks.0[1].get("is".to_string()));
|
|
}
|
|
|
|
#[test]
|
|
fn edit_insert_new_second_file() {
|
|
let db = new_test_db(1, 1);
|
|
let db_after = db._edit(|cwd| {
|
|
let mut f = std::fs::File::create(format!("{}/new_second_file", cwd)).expect("failed to open a file in cwd");
|
|
f.write_all(b"f").expect("failed to write a file in cwd");
|
|
}).expect("failed to not edit");
|
|
assert_ne!(db, db_after);
|
|
|
|
assert_eq!(2, db_after.tasks_and_metadatas.len());
|
|
assert_eq!(1, db_after.tasks_and_metadatas[1].tasks.len());
|
|
assert_eq!(1, db_after.tasks_and_metadatas[0].tasks.len());
|
|
}
|
|
|
|
#[test]
|
|
fn edit_insert_empty() {
|
|
let db = new_test_db(0, 0);
|
|
let db_after = db._edit(|cwd| {
|
|
assert_eq!(0, std::fs::read_dir(cwd).expect("failed to ls cwd").collect::<Vec<_>>().len());
|
|
assert_ne!("", cwd);
|
|
|
|
let mut f = std::fs::File::create(format!("{}/f", cwd)).expect("failed to open a file in cwd");
|
|
f.write_all(b"f").expect("failed to write a file in cwd");
|
|
|
|
let mut g = std::fs::File::create(format!("{}/g", cwd)).expect("failed to open a file in cwd");
|
|
g.write_all(b"- g: h\n- j: k").expect("failed to write a file in cwd");
|
|
}).expect("failed to not edit");
|
|
assert_ne!(db, db_after);
|
|
|
|
assert_eq!(2, db_after.tasks_and_metadatas.len());
|
|
assert_eq!(1, db_after.tasks_and_metadatas[1].tasks.len());
|
|
assert_eq!(2, db_after.tasks_and_metadatas[0].tasks.len());
|
|
}
|
|
|
|
#[test]
|
|
fn edit_noop_empty() {
|
|
let db = new_test_db(0, 0);
|
|
let db_after = db._edit(|_| {}).expect("failed to not edit");
|
|
assert_eq!(0, db.tasks_and_metadatas.len());
|
|
assert_eq!(db, db_after);
|
|
}
|
|
|
|
fn new_test_db(files: i32, tasks_per_file: i32) -> DB {
|
|
let mut db = DB::new_empty("".to_string());
|
|
|
|
for i in 0..files {
|
|
let mut tasks = Tasks::new();
|
|
for j in 0..tasks_per_file {
|
|
tasks.0.push(Task::from_string(format!("{{\"hello\": \"world[{}]\"}}", j)));
|
|
}
|
|
db.tasks_and_metadatas.push(TasksAndMetadata::new_with(
|
|
format!("{}.yaml", i),
|
|
TS::now(),
|
|
tasks,
|
|
));
|
|
}
|
|
db
|
|
}
|
|
|
|
#[test]
|
|
fn read_dir_files() {
|
|
let db = DB::new("./src/testdata/taskss.d/files.d".to_string()).expect("failed to construct from dir of files");
|
|
assert_eq!(2, db.tasks_and_metadatas.len());
|
|
assert_eq!(2, db.due().len());
|
|
assert_eq!(2, db.incomplete().len());
|
|
}
|
|
|
|
#[test]
|
|
fn read_dir_file() {
|
|
let db = DB::new("./src/testdata/taskss.d/file.d".to_string()).expect("failed to construct from dir of a single file");
|
|
assert_eq!(1, db.tasks_and_metadatas.len());
|
|
assert_eq!(1, db.due().len());
|
|
assert_eq!(1, db.incomplete().len());
|
|
}
|
|
|
|
#[test]
|
|
fn read_single_file() {
|
|
let db = DB::new("./src/testdata/taskss.d/single_file.yaml".to_string()).expect("failed to construct from single file");
|
|
assert_eq!(1, db.tasks_and_metadatas.len());
|
|
assert_eq!(1, db.due().len());
|
|
assert_eq!(2, db.incomplete().len());
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq)]
|
|
pub struct TasksAndMetadata {
|
|
tasks: Tasks,
|
|
file: String,
|
|
version: TS,
|
|
}
|
|
|
|
impl TasksAndMetadata {
|
|
pub fn new_with(file: String, version: TS, tasks: Tasks) -> TasksAndMetadata {
|
|
TasksAndMetadata{
|
|
file: file,
|
|
version: version,
|
|
tasks: tasks,
|
|
}
|
|
}
|
|
|
|
pub fn new(file: String) -> Result<TasksAndMetadata, String> {
|
|
let version = file_version(file.clone())?;
|
|
match Tasks::from_file(file.clone()) {
|
|
Ok(tasks) => Ok(TasksAndMetadata{
|
|
tasks: tasks,
|
|
file: file,
|
|
version: version,
|
|
}),
|
|
Err(msg) => Err(msg),
|
|
}
|
|
}
|
|
|
|
pub fn due(&self) -> TasksAndMetadata {
|
|
let mut cloned = self.clone();
|
|
cloned.tasks = cloned.tasks.due();
|
|
cloned
|
|
}
|
|
|
|
pub fn not_due(&self) -> TasksAndMetadata {
|
|
let mut cloned = self.clone();
|
|
cloned.tasks = cloned.tasks.not_due();
|
|
cloned
|
|
}
|
|
|
|
pub fn save(&self, dry_run: bool) -> Result<(), String> {
|
|
let version = match file_version(self.file.clone()){
|
|
Ok(v) => v,
|
|
Err(_) => self.version.clone(),
|
|
};
|
|
let mut file = self.file.clone();
|
|
if version != self.version {
|
|
file = format!("{}.{}", &self.file, TS::now().to_string());
|
|
eprintln!("saving conflicting {} as {}", &self.file, &file);
|
|
}
|
|
|
|
match dry_run {
|
|
true => {
|
|
match &file == &self.file {
|
|
true => eprintln!("# {}", &file),
|
|
false => eprintln!("# {} <- {}", &file, &self.file),
|
|
};
|
|
eprintln!("{}", self.tasks.to_string());
|
|
Ok(())
|
|
},
|
|
false => self.save_as(file),
|
|
}
|
|
}
|
|
|
|
pub fn save_due_as(&self, file: String) -> Result<(), String> {
|
|
self.due().save_as(file)
|
|
}
|
|
|
|
pub fn save_as(&self, file: String) -> Result<(), String> {
|
|
match std::fs::File::create(&file) {
|
|
Ok(mut f) => {
|
|
match f.write_all(self.tasks.to_string().as_bytes()) {
|
|
Ok(_) => Ok(()),
|
|
Err(msg) => Err(format!("failed to write {}: {}", file, msg)),
|
|
}
|
|
},
|
|
Err(msg) => Err(format!("failed to create {}: {}", file, msg)),
|
|
}
|
|
}
|
|
|
|
//fn debug(&self) -> String {
|
|
// format!(
|
|
// "# {}\n{}",
|
|
// self.file,
|
|
// self.tasks.to_string(),
|
|
// )
|
|
//}
|
|
}
|
|
|
|
fn file_version(file: String) -> Result<TS, String> {
|
|
match std::fs::metadata(file.clone()) {
|
|
Ok(m) => Ok(TS::from_system_time(m.modified().unwrap())),
|
|
Err(msg) => Err(format!("couldnt get version from {}: {}", file, msg)),
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq)]
|
|
pub struct Tasks(Vec<Task>);
|
|
|
|
impl Tasks {
|
|
pub fn new() -> Tasks {
|
|
Tasks(vec![])
|
|
}
|
|
|
|
pub fn from_file(path: String) -> Result<Tasks, String> {
|
|
let r = match std::fs::File::open(path.clone()) {
|
|
Ok(f) => Ok(f),
|
|
Err(msg) => Err(format!("could not open {}: {}", path, msg)),
|
|
}?;
|
|
match Tasks::from_reader(r) {
|
|
Ok(tasks) => Ok(tasks),
|
|
Err(msg) => Err(format!("failed to load from {}: {}", path, msg)),
|
|
}
|
|
}
|
|
|
|
pub fn from_reader(r: impl std::io::Read) -> Result<Tasks, String> {
|
|
let result = Tasks::_from_reader(r)?;
|
|
if !result.is_legacy() {
|
|
return Ok(result);
|
|
}
|
|
let mut v2 = Tasks::new();
|
|
for k in vec!["todo", "scheduled", "done"] {
|
|
v2.0.extend(
|
|
result.0[0]
|
|
.get_value(k.to_string())
|
|
.or(Some(serde_yaml::Value::Null))
|
|
.unwrap()
|
|
.as_sequence()
|
|
.or(Some(&vec![]))
|
|
.unwrap()
|
|
.iter()
|
|
.map(|v| {
|
|
Task::from_value(match k {
|
|
"done" => {
|
|
let mut t = Task::new();
|
|
t.set_value(
|
|
"_done".to_string(),
|
|
v.clone(),
|
|
);
|
|
serde_yaml::Value::from(t.0)
|
|
},
|
|
_ => v.clone(),
|
|
})
|
|
})
|
|
);
|
|
}
|
|
Ok(v2)
|
|
}
|
|
|
|
fn _from_reader(mut r: impl std::io::Read) -> Result<Tasks, String> {
|
|
let mut buff = String::new();
|
|
match r.read_to_string(&mut buff) {
|
|
Err(msg) => {
|
|
return Err(format!("failed to read body: {}", msg));
|
|
},
|
|
_ => {}
|
|
};
|
|
|
|
let mut result = Tasks::new();
|
|
match serde_yaml::from_str::<Vec<serde_yaml::Value>>(&buff) {
|
|
Ok(v) => {
|
|
result.0.extend(v.iter().map(|x| {
|
|
Task::from_value(x.clone())
|
|
}));
|
|
Ok(result)
|
|
},
|
|
Err(msg) => match Task::from_str(buff) {
|
|
Ok(t) => {
|
|
result.0.push(t);
|
|
Ok(result)
|
|
},
|
|
Err(_) => Err(format!("failed to parse yaml: {}", msg)),
|
|
},
|
|
}
|
|
}
|
|
|
|
pub fn to_string(&self) -> String {
|
|
let mut buffer = Vec::new();
|
|
let mut serializer = serde_yaml::Serializer::new(&mut buffer);
|
|
_ = serde_yaml::Value::Sequence(
|
|
self.0.iter()
|
|
.map(|x| {
|
|
let mut x = x.clone();
|
|
if x.is_due() {
|
|
x.unset("ts".to_string());
|
|
}
|
|
x
|
|
})
|
|
.map(|x| {
|
|
let is = x.get_value("is".to_string());
|
|
if x.0.len() == 1 && is.is_some() {
|
|
return is.unwrap();
|
|
}
|
|
serde_yaml::Value::from(x.0.clone())
|
|
})
|
|
.map(|x| serde_yaml::Value::from(x.clone()))
|
|
.collect()
|
|
).serialize(&mut serializer)
|
|
.expect("failed to serialize");
|
|
String::from_utf8(buffer).expect("illegal utf8 characters found")
|
|
}
|
|
|
|
pub fn len(&self) -> usize {
|
|
self.0.len()
|
|
}
|
|
|
|
pub fn incomplete(&self) -> Tasks {
|
|
Tasks(self.0.iter()
|
|
.filter(|x| !x.is_done())
|
|
.map(|x| x.clone())
|
|
.collect()
|
|
)
|
|
}
|
|
|
|
pub fn not_due(&self) -> Tasks {
|
|
Tasks(self.0.iter()
|
|
.filter(|x| !x.is_due())
|
|
.map(|x| x.clone())
|
|
.collect()
|
|
)
|
|
}
|
|
|
|
pub fn due(&self) -> Tasks {
|
|
Tasks(self.0.iter()
|
|
.filter(|x| x.is_due())
|
|
.map(|x| x.clone())
|
|
.collect()
|
|
)
|
|
}
|
|
|
|
fn is_legacy(&self) -> bool {
|
|
self.len() == 1
|
|
&& self.0[0].get_value("done".to_string()).is_some()
|
|
&& self.0[0].get_value("scheduled".to_string()).is_some()
|
|
&& self.0[0].get_value("todo".to_string()).is_some()
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test_tasks {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn due() {
|
|
let tasks = Tasks::from_file("./src/testdata/tasks_due_scheduled_done.yaml".to_string()).expect("failed to open file");
|
|
eprintln!("{:?}", tasks);
|
|
assert_eq!(2, tasks.due().len());
|
|
}
|
|
|
|
#[test]
|
|
fn from_reader_legacy() {
|
|
let tasks = Tasks::from_reader(
|
|
std::fs::File::open("./src/testdata/legacy.yaml").expect("failed to open file")
|
|
).expect("failed to read file");
|
|
assert_eq!(8, tasks.len());
|
|
assert_eq!(5, tasks.due().len());
|
|
assert_eq!("a".to_string(), tasks.due().0[0].get("is".to_string()).expect("missing 0th is"));
|
|
assert_eq!("b".to_string(), tasks.due().0[1].get("todo".to_string()).expect("missing 1st todo"));
|
|
assert_eq!("c".to_string(), tasks.due().0[2].get("is".to_string()).expect("missing 2nd is"));
|
|
assert_eq!("d".to_string(), tasks.due().0[3].get("todo".to_string()).expect("missing 3rd todo"));
|
|
assert_eq!("e".to_string(), tasks.due().0[4].get("todo".to_string()).expect("missing 4th todo"));
|
|
}
|
|
|
|
#[test]
|
|
fn from_reader() {
|
|
let tasks = Tasks::from_reader(
|
|
std::fs::File::open("./src/testdata/mvp.yaml").expect("failed to open file")
|
|
).expect("failed to read file");
|
|
assert_eq!(2, tasks.0.len());
|
|
assert_eq!(1, tasks.0[0].0.len());
|
|
assert!(tasks.0[0].get("is".to_string()).is_some());
|
|
assert_eq!("x".to_string(), tasks.0[0].get("is".to_string()).unwrap());
|
|
assert_eq!(1, tasks.0[1].0.len());
|
|
assert_eq!("y and z".to_string(), tasks.0[1].get("is".to_string()).unwrap());
|
|
}
|
|
}
|
|
|
|
|
|
#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq)]
|
|
pub struct Task(serde_yaml::Mapping);
|
|
|
|
impl Task {
|
|
pub fn new() -> Task {
|
|
Task(serde_yaml::Mapping::new())
|
|
}
|
|
|
|
pub fn from_string(s: String) -> Task {
|
|
let mut t = Task::new();
|
|
t.set("is".to_string(), s);
|
|
t
|
|
}
|
|
|
|
pub fn from_string_schedule(s: String, schedule: String) -> Task {
|
|
let mut t = Task::new();
|
|
t.set("is".to_string(), s);
|
|
t.set("schedule".to_string(), schedule);
|
|
t.set("ts".to_string(), TS::now().to_string());
|
|
t
|
|
}
|
|
|
|
pub fn from_reader(mut r: impl std::io::Read) -> Result<Task, String> {
|
|
let mut buff = String::new();
|
|
match r.read_to_string(&mut buff) {
|
|
Err(msg) => Err(format!("failed to read body: {}", msg)),
|
|
_ => Task::from_str(buff),
|
|
}
|
|
}
|
|
|
|
pub fn from_str(s: String) -> Result<Task, String> {
|
|
match serde_yaml::from_str::<serde_yaml::Value>(&s) {
|
|
Ok(v) => Ok(Task::from_value(v)),
|
|
Err(msg) => Err(format!("failed to read value: {}", msg)),
|
|
}
|
|
}
|
|
|
|
pub fn from_value(v: serde_yaml::Value) -> Task {
|
|
let mut result = Task::new();
|
|
match v.as_mapping() {
|
|
Some(m) => { result.0 = m.clone(); },
|
|
None => { result.set_value("is".to_string(), v); },
|
|
};
|
|
result
|
|
}
|
|
|
|
pub fn is_done(&self) -> bool {
|
|
self.get_value("_done".to_string()).is_some()
|
|
}
|
|
|
|
pub fn is_due(&self) -> bool {
|
|
self.is_due_at(TS::now()) && !self.is_done()
|
|
}
|
|
|
|
fn is_due_at(&self, now: TS) -> bool {
|
|
match self.when() {
|
|
Some(when) => {
|
|
now.unix() >= when.next(self.ts()).unix()
|
|
},
|
|
None => true,
|
|
}
|
|
}
|
|
|
|
fn when(&self) -> Option<When> {
|
|
match self.get("schedule".to_string()) {
|
|
Some(v) => match When::new(v) {
|
|
Ok(when) => Some(when),
|
|
Err(msg) => {
|
|
eprintln!("Task.when(): {}", msg);
|
|
return None;
|
|
},
|
|
},
|
|
None => None,
|
|
}
|
|
}
|
|
|
|
fn ts(&self) -> TS {
|
|
match self.get("ts".to_string()) {
|
|
Some(v) => match TS::new(v) {
|
|
Ok(ts) => ts,
|
|
Err(_) => TS::from_unix(0),
|
|
},
|
|
None => TS::from_unix(0),
|
|
}
|
|
}
|
|
|
|
fn get(&self, k: String) -> Option<String> {
|
|
match self.get_value(k) {
|
|
None => None,
|
|
Some(v) => match v.as_str() {
|
|
Some(s) => Some(s.to_string()),
|
|
None => None,
|
|
},
|
|
}
|
|
}
|
|
|
|
fn get_value(&self, k: String) -> Option<serde_yaml::Value> {
|
|
match self.0.get(k) {
|
|
Some(v) => Some(v.clone()),
|
|
None => None,
|
|
}
|
|
}
|
|
|
|
fn unset(&mut self, k: String) {
|
|
self.0.remove(serde_yaml::Value::String(k));
|
|
}
|
|
|
|
#[allow(dead_code)] // used in test
|
|
fn set(&mut self, k: String, v: String) {
|
|
self.0.insert(
|
|
serde_yaml::Value::String(k),
|
|
serde_yaml::Value::String(v),
|
|
);
|
|
}
|
|
|
|
fn set_value(&mut self, k: String, v: serde_yaml::Value) {
|
|
self.0.insert(
|
|
serde_yaml::Value::String(k),
|
|
v
|
|
);
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test_task {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn from_str() {
|
|
assert!(Task::from_str("{ invalid".to_string()).is_err());
|
|
assert!(Task::from_str("1".to_string()).is_ok());
|
|
assert!(Task::from_str("'1'".to_string()).is_ok());
|
|
assert!(Task::from_str("null".to_string()).is_ok());
|
|
assert!(Task::from_str("true".to_string()).is_ok());
|
|
assert!(Task::from_str("[]".to_string()).is_ok());
|
|
assert!(Task::from_str("{}".to_string()).is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn from_reader() {
|
|
let task = Task::from_reader(
|
|
std::fs::File::open("./src/testdata/mvp.uuid-123-456-xyz.yaml").expect("failed to open file")
|
|
).expect("failed to read 123...");
|
|
assert_eq!(1, task.0.len());
|
|
assert!(task.get("is".to_string()).is_some());
|
|
assert_eq!("plaintext".to_string(), task.get("is".to_string()).unwrap());
|
|
|
|
let task = Task::from_reader(
|
|
std::fs::File::open("./src/testdata/mvp.uuid-789-012-abc.yaml").expect("failed to open file")
|
|
).expect("failed to read 789...");
|
|
assert_eq!(3, task.0.len());
|
|
assert!(task.get("is".to_string()).is_none());
|
|
assert_eq!("todo here".to_string(), task.get("todo".to_string()).unwrap());
|
|
assert_eq!("* * * * *".to_string(), task.get("schedule".to_string()).unwrap());
|
|
assert_eq!("hello world\n".to_string(), task.get("details".to_string()).unwrap());
|
|
assert!(task.is_due());
|
|
}
|
|
|
|
#[test]
|
|
fn is_done() {
|
|
let mut t = Task::new();
|
|
t.set("_done".to_string(), "anything".to_string());
|
|
eprintln!("{:?}", t.0);
|
|
assert!(t.is_done());
|
|
|
|
t.set_value("_done".to_string(), serde_yaml::Value::Null);
|
|
eprintln!("{:?}", t.0);
|
|
assert!(t.is_done());
|
|
}
|
|
|
|
#[test]
|
|
fn crud() {
|
|
let mut t = Task::new();
|
|
t.set("k".to_string(), "v".to_string());
|
|
assert_eq!(
|
|
t.get("k".to_string()),
|
|
Some("v".to_string()),
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn is_due_duration() {
|
|
let mut t = Task::new();
|
|
assert!(t.is_due());
|
|
let then = TS::new("2000-01-02T15:00Z".to_string()).unwrap();
|
|
let now = TS::new("2000-01-02T16:00Z".to_string()).unwrap();
|
|
t.set("ts".to_string(), then.to_string());
|
|
t.set("schedule".to_string(), "1h".to_string());
|
|
assert!(!t.is_due_at(TS::from_unix(now.unix()-1)));
|
|
assert!(t.is_due_at(TS::from_unix(now.unix())));
|
|
assert!(t.is_due_at(TS::from_unix(now.unix()+1)));
|
|
}
|
|
|
|
#[test]
|
|
fn is_due_schedule() {
|
|
let mut t = Task::new();
|
|
assert!(t.is_due());
|
|
let then = TS::new("2000-01-02T15:00Z".to_string()).unwrap();
|
|
let now = TS::new("2000-01-02T16:00Z".to_string()).unwrap();
|
|
t.set("ts".to_string(), then.to_string());
|
|
t.set("schedule".to_string(), "2000-01-02T16:00Z".to_string());
|
|
assert!(!t.is_due_at(TS::from_unix(now.unix()-1)));
|
|
assert!(t.is_due_at(TS::from_unix(now.unix())));
|
|
assert!(t.is_due_at(TS::from_unix(now.unix()+1)));
|
|
}
|
|
|
|
#[test]
|
|
fn is_due_cron() {
|
|
let mut t = Task::new();
|
|
assert!(t.is_due());
|
|
let then = TS::new("2000-01-02T15:00Z".to_string()).unwrap();
|
|
let now = TS::new("2000-01-02T16:00Z".to_string()).unwrap();
|
|
t.set("ts".to_string(), then.to_string());
|
|
t.set("schedule".to_string(), "0 16 * * *".to_string());
|
|
assert!(!t.is_due_at(TS::from_unix(now.unix()-1)));
|
|
assert!(t.is_due_at(TS::from_unix(now.unix())));
|
|
assert!(t.is_due_at(TS::from_unix(now.unix()+1)));
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct When(String);
|
|
|
|
impl When {
|
|
fn new(src: String) -> Result<When, String> {
|
|
match Self::new_duration(src.clone()) {
|
|
Some(x) => { return Ok(x); },
|
|
None => {},
|
|
};
|
|
match Self::new_ts(src.clone()) {
|
|
Some(x) => { return Ok(x); },
|
|
None => {},
|
|
};
|
|
match Self::new_cron(src.clone()) {
|
|
Some(x) => { return Ok(x); },
|
|
None => {},
|
|
};
|
|
Err(format!("cannot parse when: {}", src))
|
|
}
|
|
|
|
fn new_duration(src: String) -> Option<When> {
|
|
match Duration::new(src.clone()) {
|
|
Ok(_) => Some(When{0: src.clone()}),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn new_ts(src: String) -> Option<When> {
|
|
match TS::new(src.clone()) {
|
|
Ok(_) => Some(When{0: src.clone()}),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn new_cron(src: String) -> Option<When> {
|
|
match Cron::new(src.clone()) {
|
|
Ok(_) => Some(When{0: src.clone()}),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn next(&self, now: TS) -> TS {
|
|
match Duration::new(self.0.clone()) {
|
|
Ok(duration) => {
|
|
return TS::from_unix(
|
|
now.unix() + duration.0
|
|
);
|
|
},
|
|
_ => {},
|
|
};
|
|
match TS::new(self.0.clone()) {
|
|
Ok(ts) => {
|
|
return ts;
|
|
},
|
|
_ => {},
|
|
};
|
|
match Cron::new(self.0.clone()) {
|
|
Ok(x) => {
|
|
return x.next(now);
|
|
},
|
|
_ => {},
|
|
};
|
|
assert!(false, "invalid when cooked");
|
|
now
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test_when {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn parse() {
|
|
match When::new("1d2h3m".to_string()) {
|
|
Ok(when) => {
|
|
assert_eq!(
|
|
1714521600 + 60*3 + 60*60*2 + 60*60*24*1,
|
|
when.next(
|
|
TS::new("2024-05-01T00".to_string())
|
|
.unwrap()
|
|
).unix()
|
|
);
|
|
},
|
|
Err(err) => assert!(false, "failed to parse when: {}", err),
|
|
};
|
|
match When::new("2024-05-01T00:00Z".to_string()) {
|
|
Ok(when) => {
|
|
assert_eq!(
|
|
1714521600 ,
|
|
when.next(
|
|
TS::new("2024-05-01T00".to_string())
|
|
.unwrap()
|
|
).unix()
|
|
);
|
|
},
|
|
Err(err) => assert!(false, "failed to parse when: {}", err),
|
|
};
|
|
match When::new("0 1 * * *".to_string()) {
|
|
Ok(when) => {
|
|
assert_eq!(
|
|
1714521600 + 60*60,
|
|
when.next(TS::from_unix(1714521600)).unix()
|
|
);
|
|
},
|
|
Err(err) => assert!(false, "failed to parse when: {}", err),
|
|
};
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct Cron(String);
|
|
|
|
impl Cron {
|
|
fn new(src: String) -> Result<Cron, String> {
|
|
match croner::Cron::new(src.as_str()).parse() {
|
|
Ok(_) => Ok(Cron{0: src}),
|
|
Err(msg) => Err(format!("bad cron: {}", msg)),
|
|
}
|
|
}
|
|
|
|
fn next(&self, now: TS) -> TS {
|
|
match croner::Cron::new(self.0.as_str()).parse() {
|
|
Ok(c) => match c.find_next_occurrence(
|
|
&DateTime::from_timestamp(now.unix() as i64, 0).unwrap(),
|
|
true,
|
|
) {
|
|
Ok(dt) => {
|
|
return TS::from_unix(dt.timestamp() as u64);
|
|
},
|
|
Err(_) => TS::from_unix(0),
|
|
},
|
|
_ => TS::from_unix(0),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test_cron {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn parse() {
|
|
match Cron::new("* * * * *".to_string()) {
|
|
Ok(_) => {}
|
|
Err(err) => assert!(false, "failed to parse cron: {}", err),
|
|
};
|
|
match Cron::new("1 * * * *".to_string()) {
|
|
Ok(c) => assert_eq!(1714525200+60, c.next(TS::from_unix(1714525200)).unix()),
|
|
Err(err) => assert!(false, "failed to parse cron: {}", err),
|
|
};
|
|
}
|
|
}
|
|
|
|
|
|
#[derive(Debug)]
|
|
struct Duration(u64);
|
|
|
|
impl Duration {
|
|
fn new(src: String) -> Result<Duration, String> {
|
|
if src.len() == 0 {
|
|
return Err("no empty duration".to_string());
|
|
}
|
|
let duration = Regex::new(r"^([0-9]+d)?([0-9]+h)?([0-9]+m)?$").unwrap();
|
|
match duration.is_match(&src) {
|
|
false => { return Err("ill formatted duration".to_string()); },
|
|
_ => {},
|
|
};
|
|
let caps = duration.captures(&src).unwrap();
|
|
let mut sum: u64 = 0;
|
|
match caps.get(1) {
|
|
Some(d) => { sum += 60 * 60 * 24 * Self::to_n(d.as_str()); },
|
|
_ => {},
|
|
};
|
|
match caps.get(2) {
|
|
Some(h) => { sum += 60 * 60 * Self::to_n(h.as_str()); },
|
|
_ => {},
|
|
};
|
|
match caps.get(3) {
|
|
Some(m) => { sum += 60 * Self::to_n(m.as_str()); },
|
|
_ => {},
|
|
};
|
|
Ok(Duration{0: sum})
|
|
}
|
|
|
|
fn to_n(s: &str) -> u64 {
|
|
let s = s.to_string();
|
|
let (s, _) = s.split_at(s.len()-1);
|
|
match s.parse::<u64>() {
|
|
Ok(n) => n,
|
|
_ => 0,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test_duration {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn parse() {
|
|
match Duration::new("1d2h3m".to_string()) {
|
|
Ok(d) => assert_eq!(60*60*24 + 60*60*2 + 60*3, d.0),
|
|
Err(err) => assert!(false, "failed to parse duration: {}", err),
|
|
};
|
|
match Duration::new("1d".to_string()) {
|
|
Ok(d) => assert_eq!(60*60*24, d.0),
|
|
Err(err) => assert!(false, "failed to parse duration: {}", err),
|
|
};
|
|
match Duration::new("2h".to_string()) {
|
|
Ok(d) => assert_eq!(60*60*2, d.0),
|
|
Err(err) => assert!(false, "failed to parse duration: {}", err),
|
|
};
|
|
match Duration::new("3m".to_string()) {
|
|
Ok(d) => assert_eq!(60*3, d.0),
|
|
Err(err) => assert!(false, "failed to parse duration: {}", err),
|
|
};
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, PartialEq, Clone)]
|
|
pub struct TS(u64);
|
|
|
|
impl TS {
|
|
fn now() -> TS {
|
|
Self::from_unix(Local::now().timestamp() as u64)
|
|
}
|
|
|
|
fn from_system_time(st: std::time::SystemTime) -> TS {
|
|
TS::from_unix(st.duration_since(std::time::SystemTime::UNIX_EPOCH).unwrap().as_secs())
|
|
}
|
|
|
|
fn from_unix(src: u64) -> TS {
|
|
TS{0: src}
|
|
}
|
|
|
|
fn new(src: String) -> Result<TS, String> {
|
|
// %Y-%m-%dT%H:%MZ
|
|
match DateTime::parse_from_str(
|
|
&format!("{} +0000", src),
|
|
"%Y-%m-%dT%H:%MZ %z",
|
|
) {
|
|
Ok(v) => { return Ok(TS(v.timestamp() as u64)) },
|
|
_ => {},
|
|
};
|
|
|
|
// %Y-%m-%dT%H
|
|
match NaiveDateTime::parse_from_str(
|
|
&format!("{}:00", src),
|
|
"%Y-%m-%dT%H:%M",
|
|
) {
|
|
Ok(v) => { return Ok(TS(v.and_utc().timestamp() as u64)) },
|
|
_ => {},
|
|
};
|
|
|
|
// %Y-%m-%d
|
|
match NaiveDateTime::parse_from_str(
|
|
&format!("{}T00:00", src),
|
|
"%Y-%m-%dT%H:%M",
|
|
) {
|
|
Ok(v) => { return Ok(TS(v.and_utc().timestamp() as u64)) },
|
|
_ => {},
|
|
};
|
|
|
|
// Sun Dec 3 23:29:27 EST 2023
|
|
match DateTime::parse_from_str(
|
|
&format!("{}", src)
|
|
.replace("PDT", "-0800")
|
|
.replace("MDT", "-0700")
|
|
.replace("EDT", "-0400")
|
|
.replace("PST", "-0700")
|
|
.replace("MST", "-0600")
|
|
.replace("EST", "-0500")
|
|
.replace(" 1 ", " 01 ")
|
|
.replace(" 2 ", " 02 ")
|
|
.replace(" 3 ", " 03 ")
|
|
.replace(" 4 ", " 04 ")
|
|
.replace(" 5 ", " 05 ")
|
|
.replace(" 6 ", " 06 ")
|
|
.replace(" 7 ", " 07 ")
|
|
.replace(" 8 ", " 08 ")
|
|
.replace(" 9 ", " 09 ")
|
|
.replace(" ", " "),
|
|
"%a %b %d %H:%M:%S %z %Y",
|
|
) {
|
|
Ok(v) => Ok(TS(v.timestamp() as u64)),
|
|
Err(msg) => Err(format!("failed to parse legacy golang time: {}", msg)),
|
|
}
|
|
}
|
|
|
|
fn unix(&self) -> u64 {
|
|
self.0
|
|
}
|
|
|
|
#[allow(dead_code)] // used in test
|
|
fn to_string(&self) -> String {
|
|
DateTime::from_timestamp(self.0 as i64, 0)
|
|
.unwrap()
|
|
.format("%Y-%m-%dT%H:%MZ")
|
|
.to_string()
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test_ts {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn parse() {
|
|
match TS::new("Tue Nov 7 07:33:11 PST 2023".to_string()) {
|
|
Ok(ts) => {
|
|
assert_eq!("2023-11-07T14:33Z", ts.to_string());
|
|
},
|
|
Err(err) => assert!(false, "failed to parse ts: {}", err),
|
|
};
|
|
match TS::new("Sun Jun 4 05:24:32 PDT 2023".to_string()) {
|
|
Ok(ts) => {
|
|
assert_eq!("2023-06-04T13:24Z", ts.to_string());
|
|
},
|
|
Err(err) => assert!(false, "failed to parse ts: {}", err),
|
|
};
|
|
match TS::new("Sat Nov 4 08:36:01 MDT 2023".to_string()) {
|
|
Ok(ts) => {
|
|
assert_eq!("2023-11-04T15:36Z", ts.to_string());
|
|
},
|
|
Err(err) => assert!(false, "failed to parse ts: {}", err),
|
|
};
|
|
match TS::new("Sun Nov 5 22:27:17 MST 2023".to_string()) {
|
|
Ok(ts) => {
|
|
assert_eq!("2023-11-06T04:27Z", ts.to_string());
|
|
},
|
|
Err(err) => assert!(false, "failed to parse ts: {}", err),
|
|
};
|
|
match TS::new("Thu Apr 18 16:17:41 EDT 2024".to_string()) {
|
|
Ok(ts) => {
|
|
assert_eq!("2024-04-18T20:17Z", ts.to_string());
|
|
},
|
|
Err(err) => assert!(false, "failed to parse ts: {}", err),
|
|
};
|
|
match TS::new("Sun Dec 3 23:29:27 EST 2023".to_string()) {
|
|
Ok(ts) => {
|
|
assert_eq!("2023-12-04T04:29Z", ts.to_string());
|
|
},
|
|
Err(err) => assert!(false, "failed to parse ts: {}", err),
|
|
};
|
|
match TS::new("2024-05-01T00:00Z".to_string()) {
|
|
Ok(ts) => {
|
|
assert_eq!(1714521600, ts.unix());
|
|
assert_eq!("2024-05-01T00:00Z", ts.to_string());
|
|
},
|
|
Err(err) => assert!(false, "failed to parse ts: {}", err),
|
|
};
|
|
match TS::new("2024-05-01T00".to_string()) {
|
|
Ok(ts) => {
|
|
assert_eq!(1714521600, ts.unix());
|
|
assert_eq!("2024-05-01T00:00Z", ts.to_string());
|
|
},
|
|
Err(err) => assert!(false, "failed to parse ts: {}", err),
|
|
};
|
|
match TS::new("2024-05-01".to_string()) {
|
|
Ok(ts) => {
|
|
assert_eq!(1714521600, ts.unix());
|
|
assert_eq!("2024-05-01T00:00Z", ts.to_string());
|
|
},
|
|
Err(err) => assert!(false, "failed to parse ts: {}", err),
|
|
};
|
|
}
|
|
}
|