From c848a4797c38ce1a664938c56bd5ae4ec8ef2a8a Mon Sep 17 00:00:00 2001 From: xeruf <27jf@pm.me> Date: Tue, 30 Jul 2024 17:11:43 +0300 Subject: [PATCH] feat: implement recursive task progress and subtask completion count --- README.md | 12 +++++++----- src/task.rs | 7 +++++++ src/tasks.rs | 39 ++++++++++++++++++++++++++++++++++++++- 3 files changed, 52 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index d1f6a08..6f056aa 100644 --- a/README.md +++ b/README.md @@ -69,14 +69,16 @@ An active tag or state filter will also set that attribute for newly created tas - `description` - accumulated notes on the task - `path` - name including parent tasks - `rpath` - name including parent tasks up to active task -- `time` - time tracked -- `rtime` - time tracked including subtasks -- TBI: `progress` - how many subtasks are complete -- TBI: `progressp` - subtask completion in percent +- `time` - time tracked on this task +- `rtime` - time tracked on this tasks and all recursive subtasks +- `progress` - recursive subtask completion in percent +- `subtasks` - how many direct subtasks are complete For debugging: `props`, `alltags`, `descriptions` -TODO: Combined formatting and recursion specifiers +TBI: Combined formatting and recursion specifiers - +for example progress count/percentage and recursive or not. +Subtask progress immediate/all/leafs. ## Plans diff --git a/src/task.rs b/src/task.rs index f59a3d8..3942608 100644 --- a/src/task.rs +++ b/src/task.rs @@ -224,6 +224,13 @@ impl TryFrom for State { } } impl State { + pub(crate) fn is_open(&self) -> bool { + match self { + State::Open | State::Active => true, + _ => false, + } + } + pub(crate) fn kind(&self) -> Kind { match self { State::Open => Kind::from(1630), diff --git a/src/tasks.rs b/src/tasks.rs index dd43f47..b32b75c 100644 --- a/src/tasks.rs +++ b/src/tasks.rs @@ -1,6 +1,8 @@ use std::collections::{BTreeSet, HashMap}; +use std::fmt::{Display, Formatter, write}; use std::io::{Error, stdout, Write}; -use std::iter::once; +use std::iter::{once, Sum}; +use std::ops::Add; use chrono::{Local, TimeZone}; use chrono::LocalResult::Single; @@ -43,6 +45,7 @@ impl Tasks { tasks: Default::default(), properties: vec![ "state".into(), + "progress".into(), "rtime".into(), "hashtags".into(), "rpath".into(), @@ -80,6 +83,22 @@ impl Tasks { }) } + fn total_progress(&self, id: &EventId) -> Option { + self.tasks.get(id).and_then(|t| match t.pure_state() { + State::Closed => None, + State::Done => Some(1.0), + _ => { + let count = t.children.len() as f32; + Some( + t.children + .iter() + .filter_map(|e| self.total_progress(e).map(|p| p / count)) + .sum(), + ) + } + }) + } + // Parents pub(crate) fn get_parent(&self, id: Option) -> Option { @@ -236,6 +255,24 @@ impl Tasks { self.properties .iter() .map(|p| match p.as_str() { + "subtasks" => { + let mut total = 0; + let mut done = 0; + for subtask in task.children.iter().filter_map(|id| self.get_by_id(id)) + { + let state = subtask.pure_state(); + total += &(state != State::Closed).into(); + done += &(state == State::Done).into(); + } + if total > 0 { + format!("{done}/{total}") + } else { + "".to_string() + } + } + "progress" => self + .total_progress(&task.event.id) + .map_or(String::new(), |p| format!("{:2}%", p * 100.0)), "path" => self.get_task_path(Some(task.event.id)), "rpath" => self.relative_path(task.event.id), "rtime" => {