Browse Source

Add tags matching to clock tables

Adam Elliott writes:

> I have attached a git patch against master that implements a new
> parameter to clock tables, "tags".  This parameter is a tags-query as a
> string and is used to filter the headlines which are consulted when
> building the clock table.
>
> In my search of the archives to see if this feature already existed, I
> found a reference here:
>  http://article.gmane.org/gmane.emacs.orgmode/17304
> suggesting it was difficult.  The patch is not so large, though, so
> perhaps I am missing something.
>
> My rationale in implementing this feature was to keep track of the
> occasional task item that is not billable, yet still makes sense to
> include in the overall project structure.  Of course I could just avoid
> clocking the task item, or manually delete clock lines before generating
> a report, but this feature reduces the chance for error; no doubt there
> are other workflows enabled with this feature as well.  I don't make
> significant use of tags myself, but I know many do.
>
> In order to maintain a sensible report, headlines that don't match the
> tag filter may be included if they have descendants that do.  Any time
> clocked directly on non-matching headlines, however, is excluded.
>
> Specifying even a simple filter noticeably slows down clock table
> generation for non-toy reports, particularly for clock table reports
> with :step.  If there is no filter, though, there is no degradation in
> performance.
>
> Tag filter syntax is the standard one, as described at:
>  http://orgmode.org/manual/Matching-tags-and-properties.html
> Only tags are considered at the moment, although I suspect querying
> against all properties would be possible (if even slower).
>
> Examples:
>
> * development
>  CLOCK: => 1:00
> *** task 1
>    CLOCK: => 1:00
> *** task 2                                              :must:
> ***** task 2a
>      CLOCK: => 1:00
> ***** task 2b                                           :mustnot:
>      CLOCK: => 1:00
>
> Note I am using an unconventional but legal(ish) clock format for
> brevity.  Clock tables are also pruned to only relevant lines.
>
> [1] #+BEGIN: clocktable
> |   | *Total time* | *4:00* |      |      |
> |---+--------------+--------+------+------|
> | 1 | development  | 4:00   |      |      |
> | 2 | task 1       |        | 1:00 |      |
> | 2 | task 2       |        | 2:00 |      |
> | 3 | task 2a      |        |      | 1:00 |
> | 3 | task 2b      |        |      | 1:00 |
>
> [2] #+BEGIN: clocktable :tags "must"
> |   | *Total time* | *2:00* |      |      |
> |---+--------------+--------+------+------|
> | 1 | development  | 2:00   |      |      |
> | 2 | task 2       |        | 2:00 |      |
> | 3 | task 2a      |        |      | 1:00 |
> | 3 | task 2b      |        |      | 1:00 |
>
> [3] #+BEGIN: clocktable :tags "-mustnot"
> |   | *Total time* | *3:00* |      |      |
> |---+--------------+--------+------+------|
> | 1 | development  | 3:00   |      |      |
> | 2 | task 1       |        | 1:00 |      |
> | 2 | task 2       |        | 1:00 |      |
> | 3 | task 2a      |        |      | 1:00 |
>
> [4] #+BEGIN: clocktable :tags "must-mustnot"
> |   | *Total time* | *1:00* |      |      |
> |---+--------------+--------+------+------|
> | 1 | development  | 1:00   |      |      |
> | 2 | task 2       |        | 1:00 |      |
> | 3 | task 2a      |        |      | 1:00 |
>
> [5] #+BEGIN: clocktable :tags "must+mustnot"
> |   | *Total time* | *1:00* |      |      |
> |---+--------------+--------+------+------|
> | 1 | development  | 1:00   |      |      |
> | 2 | task 2       |        | 1:00 |      |
> | 3 | task 2b      |        |      | 1:00 |
>
> As you can see, in examples 2, 4, and 5, the time clocked on
> "development" itself is being removed.  Example 2 illustrates the effect
> of tag inheritance.
>
> Adam
Carsten Dominik 10 years ago
parent
commit
c59da3a3dd
3 changed files with 51 additions and 13 deletions
  1. 1 0
      doc/org.texi
  2. 4 0
      lisp/ChangeLog
  3. 46 13
      lisp/org-clock.el

+ 1 - 0
doc/org.texi

@@ -5582,6 +5582,7 @@ new table.  The @samp{BEGIN} line can specify options:
 :tend        @r{A time string specifying when to stop considering times.}
 :step        @r{@code{week} or @code{day}, to split the table into chunks.}
              @r{To use this, @code{:block} or @code{:tstart}, @code{:tend} are needed.}
+:tags        @r{A tags match to select entries that should contribute}
 :link        @r{Link the item headlines in the table to their origins.}
 :formula     @r{Content of a @code{#+TBLFM} line to be added and evaluated.}
              @r{As a special case, @samp{:formula %} adds a column with % time.}

+ 4 - 0
lisp/ChangeLog

@@ -1,5 +1,9 @@
 2010-04-23  Carsten Dominik  <carsten.dominik@gmail.com>
 
+	* org-clock.el (org-clock-sum): New argument HEADLINE-FILTER.
+	(org-clock-sum): Add property to selected headlines.
+	(org-dblock-write:clocktable): Make tags matcher.
+
 	* org.el (org-set-autofill-regexps): XEmacs compatibility.
 
 	* org-latex.el (org-export-latex-set-initial-vars): Allow "-"

+ 46 - 13
lisp/org-clock.el

@@ -1311,10 +1311,13 @@ With prefix arg SELECT, offer recently clocked tasks for selection."
   "Holds the file total time in minutes, after a call to `org-clock-sum'.")
 (make-variable-buffer-local 'org-clock-file-total-minutes)
 
-(defun org-clock-sum (&optional tstart tend)
+(defun org-clock-sum (&optional tstart tend headline-filter)
   "Sum the times for each subtree.
 Puts the resulting times in minutes as a text property on each headline.
-TSTART and TEND can mark a time range to be considered."
+TSTART and TEND can mark a time range to be considered.  HEADLINE-FILTER is a
+zero-arg function that, if specified, is called for each headline in the time 
+range with point at the headline.  Headlines for which HEADLINE-FILTER returns
+nil are excluded from the clock summation."
   (interactive)
   (let* ((bmp (buffer-modified-p))
 	 (re (concat "^\\(\\*+\\)[ \t]\\|^[ \t]*"
@@ -1330,7 +1333,9 @@ TSTART and TEND can mark a time range to be considered."
     (if (stringp tend) (setq tend (org-time-string-to-seconds tend)))
     (if (consp tstart) (setq tstart (org-float-time tstart)))
     (if (consp tend) (setq tend (org-float-time tend)))
-    (remove-text-properties (point-min) (point-max) '(:org-clock-minutes t))
+    (remove-text-properties (point-min) (point-max)
+                            '(:org-clock-minutes t
+                              :org-clock-force-headline-inclusion t))
     (save-excursion
       (goto-char (point-max))
       (while (re-search-backward re nil t)
@@ -1359,15 +1364,34 @@ TSTART and TEND can mark a time range to be considered."
 	      (let ((time (floor (- (org-float-time)
 				    (org-float-time org-clock-start-time)) 60)))
 		(setq t1 (+ t1 time))))
-	  (setq level (- (match-end 1) (match-beginning 1)))
-	  (when (or (> t1 0) (> (aref ltimes level) 0))
-	    (loop for l from 0 to level do
-		  (aset ltimes l (+ (aref ltimes l) t1)))
-	    (setq t1 0 time (aref ltimes level))
-	    (loop for l from level to (1- lmax) do
-		  (aset ltimes l 0))
-	    (goto-char (match-beginning 0))
-	    (put-text-property (point) (point-at-eol) :org-clock-minutes time)))))
+	  (let* ((headline-forced
+                  (get-text-property (point)
+                                     :org-clock-force-headline-inclusion))
+                 (headline-included
+                  (or (null headline-filter)
+                      (save-excursion
+                        (save-match-data (funcall headline-filter))))))
+	    (setq level (- (match-end 1) (match-beginning 1)))
+	    (when (or (> t1 0) (> (aref ltimes level) 0))
+	      (when (or headline-included headline-forced)
+                (if headline-included
+                    (loop for l from 0 to level do
+                          (aset ltimes l (+ (aref ltimes l) t1))))
+		(setq time (aref ltimes level))
+		(goto-char (match-beginning 0))
+		(put-text-property (point) (point-at-eol) :org-clock-minutes time)
+                (if headline-filter
+                    (save-excursion
+                      (save-match-data
+                        (while
+                            (> (funcall outline-level) 1)
+                          (outline-up-heading 1 t)
+                          (put-text-property
+                           (point) (point-at-eol)
+                           :org-clock-force-headline-inclusion t))))))
+	      (setq t1 0)
+	      (loop for l from level to (1- lmax) do
+		    (aset ltimes l 0)))))))
       (setq org-clock-file-total-minutes (aref ltimes 0)))
     (set-buffer-modified-p bmp)))
 
@@ -1687,6 +1711,8 @@ the currently selected interval size."
 	   (te (plist-get params :tend))
 	   (block (plist-get params :block))
 	   (link (plist-get params :link))
+	   (tags (plist-get params :tags))
+	   (matcher (if tags (cdr (org-make-tags-matcher tags))))
 	   ipos time p level hlc hdl tsp props content recalc formula pcol
 	   cc beg end pos tbl tbl1 range-text rm-file-column scope-is-list st)
       (setq org-clock-file-total-minutes nil)
@@ -1768,7 +1794,14 @@ the currently selected interval size."
 	(goto-char pos)
 
 	(unless scope-is-list
-	  (org-clock-sum ts te)
+	  (org-clock-sum ts te
+			 (unless (null matcher)
+			   (lambda ()
+			     (let ((tags-list
+				    (org-split-string
+				     (or (org-entry-get (point) "ALLTAGS") "")
+				     ":")))
+			       (eval matcher)))))
 	  (goto-char (point-min))
 	  (setq st t)
 	  (while (or (and (bobp) (prog1 st (setq st nil))