diff --git a/elisp/chronometrist-events.el b/elisp/chronometrist-events.el index aa0083c..2dd9453 100644 --- a/elisp/chronometrist-events.el +++ b/elisp/chronometrist-events.el @@ -97,23 +97,22 @@ Where START and STOP are ISO-8601 timestamps." (defun chronometrist-events-maybe-split (event) "Split EVENT if it spans midnight. Return a list of two events if EVENT was split, else nil" - (when (plist-get event :stop) - (let ((split-time (chronometrist-midnight-spanning-p (chronometrist-iso-timestamp->ts - (plist-get event :start)) - (chronometrist-iso-timestamp->ts - (plist-get event :stop))))) - (when split-time - (-let ((((start-1 . stop-1) (start-2 . stop-2)) split-time) - ;; plist-put modifies lists in-place. The resulting bugs - ;; left me puzzled for a while. - (event-1 (cl-copy-list event)) - (event-2 (cl-copy-list event))) - (list (-> event-1 - (plist-put :start start-1) - (plist-put :stop stop-1)) - (-> event-2 - (plist-put :start start-2) - (plist-put :stop stop-2)))))))) + (let ((split-time (chronometrist-midnight-spanning-p (chronometrist-iso-timestamp->ts + (plist-get event :start)) + (chronometrist-iso-timestamp->ts + (plist-get event :stop))))) + (when split-time + (-let ((((start-1 . stop-1) (start-2 . stop-2)) split-time) + ;; plist-put modifies lists in-place. The resulting bugs + ;; left me puzzled for a while. + (event-1 (cl-copy-list event)) + (event-2 (cl-copy-list event))) + (list (-> event-1 + (plist-put :start start-1) + (plist-put :stop stop-1)) + (-> event-2 + (plist-put :start start-2) + (plist-put :stop stop-2))))))) ;; TODO - Maybe strip dates from values, since they're part of the key ;; anyway. Consider using a state machine. @@ -143,7 +142,6 @@ were none." ;; to be replaced by plist-query (defun chronometrist-events-subset (start end) "Return a subset of `chronometrist-events'. - The subset will contain values between dates START and END (both inclusive). diff --git a/elisp/chronometrist-queries.el b/elisp/chronometrist-queries.el index c662121..89579b2 100644 --- a/elisp/chronometrist-queries.el +++ b/elisp/chronometrist-queries.el @@ -31,14 +31,23 @@ The data is obtained from `chronometrist-file', via `chronometrist-events'. TS should be a ts struct (see `ts.el'). The return value is seconds, as an integer." - (let* ((events-today (chronometrist-sexp-read ts (ts-adjust 'day 1 ts))) + (let* ((today-start ts) + (today-end (ts-adjust 'day 1 ts)) + (events-today (chronometrist-sexp-read today-start today-end)) (task-events (when events-today (chronometrist-filter-events-task events-today task)))) (if task-events - (->> (chronometrist-events->ts-pairs task-events) - (chronometrist-ts-pairs->durations) - (-reduce #'+) - (truncate)) + (--> (cl-loop with new with list + for event in task-events + do (setq new (chronometrist-events-maybe-split event)) + if new append new into list + else collect event into list + finally return list) + (chronometrist-filter-events-time it today-start today-end) + (chronometrist-events->ts-pairs it) + (chronometrist-ts-pairs->durations it) + (-reduce #'+ it) + (truncate it)) ;; no events for this task on TS, i.e. no time spent 0))) @@ -70,10 +79,18 @@ EVENTS should be a list of property lists in the form (:name \"NAME\" :start START :stop STOP ...), where START and STOP are ISO-8601 time strings." (cl-loop for event in events - when (equal (plist-get event :name) - task) + when (equal task (plist-get event :name)) collect event)) +(defun chronometrist-filter-events-time (events begin end) + "From EVENTS, return the ones between BEGIN and END." + (cl-loop with start with stop + for event in events + do (setq start (chronometrist-iso-timestamp->ts (plist-get event :start)) + stop (chronometrist-iso-timestamp->ts (plist-get event :stop))) + when (or (ts<= begin start) + (ts<= end stop)) + collect event)) (provide 'chronometrist-queries) diff --git a/elisp/chronometrist-sexp.el b/elisp/chronometrist-sexp.el index 10fea83..45054ea 100644 --- a/elisp/chronometrist-sexp.el +++ b/elisp/chronometrist-sexp.el @@ -32,51 +32,47 @@ If not supplied, TS-BEG is the beginning of today and TS-END is the beginning of tomorrow, i.e. the events for today are returned. If TS-BEG or TS-END is between the :start and :stop time of an -event, the event will be split into two, and only the matching -event will be included. Thus, events returned will not span -midnights." - (chronometrist-sexp-in-file chronometrist-file - (goto-char (point-max)) - ;; there is no range - collect everything, maybe split - (if (and (not ts-beg) (not ts-end)) - (cl-loop with expr with split-events with list do - (if (bobp) - (setq expr nil) - (backward-list 1) - (setq expr (read (current-buffer)) - split-events (chronometrist-events-maybe-split expr)) - (backward-list 1)) - while expr - when split-events append split-events into list - unless split-events collect expr into list - finally return list) - ;; there is a range - (cl-loop with expr with start with stop with split-events with list - do (if (bobp) - (setq expr nil) - (backward-list 1) - (setq expr (read (current-buffer))) - (backward-list 1)) - ;; loop till we reach the beginning of the range - while - (and expr - (setq start (chronometrist-iso-timestamp->ts - (plist-get expr :start)) - stop (plist-get expr :stop) - stop (if stop (chronometrist-iso-timestamp->ts stop) (ts-now))) - ;; don't go past TS-BEG - (or (ts> start ts-beg) - (ts> stop ts-beg))) - when (or (ts-in ts-beg ts-end start) - (ts-in ts-beg ts-end stop)) - ;; expr is within range - collect (let ((split-events (chronometrist-events-maybe-split expr))) - (cond ((ts< start ts-beg) - (cl-second split-events)) - ((ts< ts-end stop) - (cl-first split-events)) - (t expr))) into list - finally return list)))) +event, the event will be included. Thus, the first and the last +events may extend outside the given range. + +The events returned may also cross the +`chronometrist-day-start-time', which can affect duration +calculations. See `chronometrist-events-maybe-split'. + +If the latest (first) event does not have a :stop property, it +will be added with the current time as the value." + (let ((no-range-p (and (not ts-beg) (not ts-end)))) + (chronometrist-sexp-in-file chronometrist-file + (goto-char (point-max)) + (cl-loop + with expr with start with stop + do + (if (bobp) + (setq expr nil) + (backward-list 1) + (setq expr (read (current-buffer))) + (backward-list 1)) + ;; loop till we reach the beginning of the range + while + (and expr + (setq start (chronometrist-iso-timestamp->ts + (plist-get expr :start)) + stop (plist-get expr :stop) + stop (if stop + (chronometrist-iso-timestamp->ts stop) + (let ((now (ts-now))) + (plist-put expr :stop (chronometrist-ts->iso now)) + now))) + ;; don't go past TS-BEG + (or no-range-p + (ts> start ts-beg) + (ts> stop ts-beg))) + when + (or no-range-p + (ts-in ts-beg ts-end start) + (ts-in ts-beg ts-end stop)) + ;; expr is within range + collect expr)))) (defun chronometrist-sexp-last () "Return last s-expression from `chronometrist-file'." diff --git a/tests/chronometrist-sexp-tests.el b/tests/chronometrist-sexp-tests.el index 445618b..fe8aff6 100644 --- a/tests/chronometrist-sexp-tests.el +++ b/tests/chronometrist-sexp-tests.el @@ -12,26 +12,26 @@ (before-all (setq chronometrist-file-old chronometrist-file chronometrist-file "tests/test.sexp")) (after-all (setq chronometrist-file chronometrist-file-old)) - (it "returns all events if no arguments are given, splitting ones which span midnights" - (expect (length (chronometrist-sexp-read)) :to-equal 13)) + (it "returns all events if no arguments are given" + (expect (length (chronometrist-sexp-read)) :to-equal 11)) (it "returns events between a certain time" (expect (length (chronometrist-sexp-read (chronometrist-iso-date->ts "2020-05-10") (chronometrist-iso-date->ts "2020-05-11"))) :to-equal 3) (expect (length - (chronometrist-sexp-read (chronometrist-iso-date->ts "2020-05-02") - (chronometrist-iso-date->ts "2020-05-05"))) - :to-equal 4)) - (it "splits events if they cross the given times" + (chronometrist-sexp-read (chronometrist-iso-date->ts "2018-01-02") + (chronometrist-iso-date->ts "2018-01-05"))) + :to-equal 2)) + (it "includes events whose start or end crosses the given ranges" (expect (chronometrist-sexp-read (chronometrist-iso-date->ts "2018-01-03") (chronometrist-iso-date->ts "2018-01-04")) :to-equal '((:name "Cooking" :start "2018-01-03T23:00:00+0530" - :stop "2018-01-04T00:00:00+0530") + :stop "2018-01-04T01:00:00+0530") (:name "Programming" - :start "2018-01-03T00:00:00+0530" + :start "2018-01-02T23:00:00+0530" :stop "2018-01-03T01:00:00+0530"))))) ;; Local Variables: