Compare commits
13 Commits
dev
...
date-prope
Author | SHA1 | Date |
---|---|---|
contrapunctus | c6ed69d0f6 | |
contrapunctus | a911dbf16e | |
contrapunctus | 73477552cc | |
contrapunctus | 4c3f220f43 | |
contrapunctus | b43dea5bc5 | |
contrapunctus | 993281ed6d | |
contrapunctus | d1107a50e1 | |
contrapunctus | bf40c72250 | |
contrapunctus | 2aef569656 | |
contrapunctus | 92564720e6 | |
contrapunctus | b0b73bd1c9 | |
contrapunctus | 0eea7f99d5 | |
contrapunctus | 6dc89776b7 |
|
@ -217,10 +217,10 @@ Return new position of point."
|
||||||
;; [[file:chronometrist.org::*current-task][current-task:1]]
|
;; [[file:chronometrist.org::*current-task][current-task:1]]
|
||||||
(cl-defun chronometrist-current-task (&optional (backend (chronometrist-active-backend)))
|
(cl-defun chronometrist-current-task (&optional (backend (chronometrist-active-backend)))
|
||||||
"Return the name of the active task as a string, or nil if not clocked in."
|
"Return the name of the active task as a string, or nil if not clocked in."
|
||||||
(let ((last-event (chronometrist-latest-record backend)))
|
(let ((last-interval (chronometrist-latest-record backend)))
|
||||||
(if (plist-member last-event :stop)
|
(if (plist-member last-interval :stop)
|
||||||
nil
|
nil
|
||||||
(plist-get last-event :name))))
|
(plist-get last-interval :name))))
|
||||||
;; current-task:1 ends here
|
;; current-task:1 ends here
|
||||||
|
|
||||||
;; [[file:chronometrist.org::*install-directory][install-directory:1]]
|
;; [[file:chronometrist.org::*install-directory][install-directory:1]]
|
||||||
|
@ -305,50 +305,49 @@ Return value is a ts struct (see `ts.el')."
|
||||||
((&plist :start start-2 :stop stop-2) (cl-second split-time))
|
((&plist :start start-2 :stop stop-2) (cl-second split-time))
|
||||||
;; `plist-put' modifies lists in-place. The resulting bugs
|
;; `plist-put' modifies lists in-place. The resulting bugs
|
||||||
;; left me puzzled for a while.
|
;; left me puzzled for a while.
|
||||||
(event-1 (cl-copy-list plist))
|
(interval-1 (cl-copy-list plist))
|
||||||
(event-2 (cl-copy-list plist)))
|
(interval-2 (cl-copy-list plist)))
|
||||||
(list (-> event-1
|
(list (-> interval-1
|
||||||
(plist-put :start start-1)
|
(plist-put :start start-1)
|
||||||
(plist-put :stop stop-1))
|
(plist-put :stop stop-1))
|
||||||
(-> event-2
|
(-> interval-2
|
||||||
(plist-put :start start-2)
|
(plist-put :start start-2)
|
||||||
(plist-put :stop stop-2))))))))
|
(plist-put :stop stop-2))))))))
|
||||||
;; split-plist:1 ends here
|
;; split-plist:1 ends here
|
||||||
|
|
||||||
;; [[file:chronometrist.org::*events-update][events-update:1]]
|
;; [[file:chronometrist.org::*ht-update][ht-update:1]]
|
||||||
(defun chronometrist-events-update (plist hash-table &optional replace)
|
(defun chronometrist-ht-update (plist hash-table &optional replace)
|
||||||
"Return HASH-TABLE with PLIST added as the latest interval.
|
"Return HASH-TABLE with PLIST added as the latest interval.
|
||||||
If REPLACE is non-nil, replace the last interval with PLIST."
|
If REPLACE is non-nil, replace the last interval with PLIST."
|
||||||
(let* ((date (->> (plist-get plist :start)
|
(let* ((date (->> (plist-get plist :start)
|
||||||
(chronometrist-iso-to-ts )
|
(chronometrist-iso-to-ts )
|
||||||
(ts-format "%F" )))
|
(ts-format "%F" )))
|
||||||
(events-today (gethash date hash-table)))
|
(intervals-today (gethash date hash-table)))
|
||||||
(--> (if replace (-drop-last 1 events-today) events-today)
|
(--> (if replace (-drop-last 1 intervals-today) intervals-today)
|
||||||
(append it (list plist))
|
(append it (list plist))
|
||||||
(puthash date it hash-table))
|
(puthash date it hash-table))
|
||||||
hash-table))
|
hash-table))
|
||||||
;; events-update:1 ends here
|
;; ht-update:1 ends here
|
||||||
|
|
||||||
;; [[file:chronometrist.org::*last-date][last-date:1]]
|
;; [[file:chronometrist.org::*ht-last-date][ht-last-date:1]]
|
||||||
(defun chronometrist-events-last-date (hash-table)
|
(defun chronometrist-ht-last-date (hash-table)
|
||||||
"Return an ISO-8601 date string for the latest date present in `chronometrist-events'."
|
"Return an ISO-8601 date string for the latest date present in HASH-TABLE."
|
||||||
(--> (hash-table-keys hash-table)
|
(--> (hash-table-keys hash-table)
|
||||||
(last it)
|
(last it)
|
||||||
(car it)))
|
(cl-first it)))
|
||||||
;; last-date:1 ends here
|
;; ht-last-date:1 ends here
|
||||||
|
|
||||||
;; [[file:chronometrist.org::*events-last][events-last:1]]
|
;; [[file:chronometrist.org::*ht-last][ht-last:1]]
|
||||||
(cl-defun chronometrist-events-last (&optional (backend (chronometrist-active-backend)))
|
(cl-defun chronometrist-ht-last (&optional hash-table)
|
||||||
"Return the last plist from `chronometrist-events'."
|
"Return the last plist from HASH-TABLE."
|
||||||
(let* ((hash-table (chronometrist-backend-hash-table backend))
|
(--> (chronometrist-ht-last-date hash-table)
|
||||||
(last-date (chronometrist-events-last-date hash-table)))
|
(gethash it hash-table)
|
||||||
(--> (gethash last-date hash-table)
|
(last it)
|
||||||
(last it)
|
(cl-first it)))
|
||||||
(car it))))
|
;; ht-last:1 ends here
|
||||||
;; events-last:1 ends here
|
|
||||||
|
|
||||||
;; [[file:chronometrist.org::#program-data-structures-events-subset][events-subset:1]]
|
;; [[file:chronometrist.org::#program-data-structures-ht-subset][ht-subset:1]]
|
||||||
(defun chronometrist-events-subset (start end hash-table)
|
(defun chronometrist-ht-subset (start end hash-table)
|
||||||
"Return a subset of HASH-TABLE.
|
"Return a subset of HASH-TABLE.
|
||||||
The subset will contain values between dates START and END (both
|
The subset will contain values between dates START and END (both
|
||||||
inclusive).
|
inclusive).
|
||||||
|
@ -363,18 +362,18 @@ treated as though their time is 00:00:00."
|
||||||
(puthash key value subset)))
|
(puthash key value subset)))
|
||||||
hash-table)
|
hash-table)
|
||||||
subset))
|
subset))
|
||||||
;; events-subset:1 ends here
|
;; ht-subset:1 ends here
|
||||||
|
|
||||||
;; [[file:chronometrist.org::*task-time-one-day][task-time-one-day:1]]
|
;; [[file:chronometrist.org::*task-time-one-day][task-time-one-day:1]]
|
||||||
(cl-defun chronometrist-task-time-one-day (task &optional (date (chronometrist-date-ts)) (backend (chronometrist-active-backend)))
|
(cl-defun chronometrist-task-time-one-day (task &optional (date (chronometrist-date-ts)) (backend (chronometrist-active-backend)))
|
||||||
"Return total time spent on TASK today or on DATE, an ISO-8601 date.
|
"Return total time spent on TASK today or on DATE, an ISO-8601 date.
|
||||||
The return value is seconds, as an integer."
|
The return value is seconds, as an integer."
|
||||||
(let ((task-events (chronometrist-task-records-for-date backend task date)))
|
(let ((task-intervals (chronometrist-task-records-for-date backend task date)))
|
||||||
(if task-events
|
(if task-intervals
|
||||||
(->> (chronometrist-plists-to-durations task-events)
|
(->> (chronometrist-plists-to-durations task-intervals)
|
||||||
(-reduce #'+)
|
(-reduce #'+)
|
||||||
(truncate))
|
(truncate))
|
||||||
;; no events for this task on DATE, i.e. no time spent
|
;; no intervals for this task on DATE, i.e. no time spent
|
||||||
0)))
|
0)))
|
||||||
;; task-time-one-day:1 ends here
|
;; task-time-one-day:1 ends here
|
||||||
|
|
||||||
|
@ -389,16 +388,16 @@ Return value is seconds as an integer."
|
||||||
;; active-time-on:1 ends here
|
;; active-time-on:1 ends here
|
||||||
|
|
||||||
;; [[file:chronometrist.org::*count-active-days][count-active-days:1]]
|
;; [[file:chronometrist.org::*count-active-days][count-active-days:1]]
|
||||||
(cl-defun chronometrist-statistics-count-active-days (task table)
|
(cl-defun chronometrist-count-active-days (task hash-table)
|
||||||
"Return the number of days the user spent any time on TASK.
|
"Return the number of days the user spent any time on TASK.
|
||||||
TABLE must be a hash table - if not supplied, `chronometrist-events' is used.
|
HASH-TABLE must be a hash table as returned by `chronometrist-to-hash-table'.
|
||||||
|
|
||||||
This will not return correct results if TABLE contains records
|
This will not return correct results if HASH-TABLE contains
|
||||||
which span midnights."
|
records which span midnights."
|
||||||
(cl-loop for events being the hash-values of table
|
(cl-loop for intervals being the hash-values of hash-table
|
||||||
count (seq-find (lambda (event)
|
count (seq-find (lambda (event)
|
||||||
(equal task (plist-get event :name)))
|
(equal task (plist-get event :name)))
|
||||||
events)))
|
intervals)))
|
||||||
;; count-active-days:1 ends here
|
;; count-active-days:1 ends here
|
||||||
|
|
||||||
;; [[file:chronometrist.org::*task-list][task-list:1]]
|
;; [[file:chronometrist.org::*task-list][task-list:1]]
|
||||||
|
@ -413,6 +412,32 @@ active backend."
|
||||||
:group 'chronometrist)
|
:group 'chronometrist)
|
||||||
;; task-list:1 ends here
|
;; task-list:1 ends here
|
||||||
|
|
||||||
|
;; [[file:chronometrist.org::*record-properties][record-properties:1]]
|
||||||
|
(defun chronometrist-record-properties (plist-group)
|
||||||
|
"Return properties for DATE-RECORDS, if any.
|
||||||
|
PLIST-GROUP must be a tagged list as returned by
|
||||||
|
`chronometrist-latest-record'."
|
||||||
|
(cl-loop with valuep
|
||||||
|
for elt in plist-group
|
||||||
|
when (keywordp elt)
|
||||||
|
collect (progn (setq valuep t) elt) into plist
|
||||||
|
else when valuep
|
||||||
|
collect (progn (setq valuep nil) elt) into plist
|
||||||
|
else when (and (not (keywordp elt)) (not valuep))
|
||||||
|
do (cl-return plist)))
|
||||||
|
;; record-properties:1 ends here
|
||||||
|
|
||||||
|
;; [[file:chronometrist.org::*record-intervals][record-intervals:1]]
|
||||||
|
(defun chronometrist-record-intervals (record)
|
||||||
|
"Return intervals (as a list of plists) from RECORD.
|
||||||
|
RECORD must be a tagged plist as returned by
|
||||||
|
`chronometrist-latest-record'."
|
||||||
|
(cl-loop for elt being the elements of record
|
||||||
|
using (index i)
|
||||||
|
when (chronometrist-plist-p elt)
|
||||||
|
return (seq-drop record i)))
|
||||||
|
;; record-intervals:1 ends here
|
||||||
|
|
||||||
;; [[file:chronometrist.org::*iso-to-ts][iso-to-ts:1]]
|
;; [[file:chronometrist.org::*iso-to-ts][iso-to-ts:1]]
|
||||||
(defun chronometrist-iso-to-ts (timestamp)
|
(defun chronometrist-iso-to-ts (timestamp)
|
||||||
"Convert TIMESTAMP to a TS struct. (see `ts.el')
|
"Convert TIMESTAMP to a TS struct. (see `ts.el')
|
||||||
|
@ -454,9 +479,6 @@ TS should be a ts struct (see `ts.el')."
|
||||||
Optional argument UNIX-TIME should be a time value (see
|
Optional argument UNIX-TIME should be a time value (see
|
||||||
`current-time') accepted by `format-time-string'."
|
`current-time') accepted by `format-time-string'."
|
||||||
(format-time-string "%FT%T%z" unix-time))
|
(format-time-string "%FT%T%z" unix-time))
|
||||||
|
|
||||||
;; Note - this assumes that an event never crosses >1 day. This seems
|
|
||||||
;; sufficient for all conceivable cases.
|
|
||||||
;; format-time-iso8601:1 ends here
|
;; format-time-iso8601:1 ends here
|
||||||
|
|
||||||
;; [[file:chronometrist.org::*split-time][split-time:1]]
|
;; [[file:chronometrist.org::*split-time][split-time:1]]
|
||||||
|
@ -473,13 +495,13 @@ Return a list in the form
|
||||||
(:start <day start time on second day>
|
(:start <day start time on second day>
|
||||||
:stop STOP-TIME))"
|
:stop STOP-TIME))"
|
||||||
;; FIXME - time zones are ignored; may cause issues with
|
;; FIXME - time zones are ignored; may cause issues with
|
||||||
;; time-zone-spanning events
|
;; time-zone-spanning intervals
|
||||||
|
|
||||||
;; The time on which the first provided day starts (according to `chronometrist-day-start-time')
|
;; The time on which the first provided day starts (according to `chronometrist-day-start-time')
|
||||||
(let* ((stop-ts (chronometrist-iso-to-ts stop-time))
|
(let* ((stop-ts (chronometrist-iso-to-ts stop-time))
|
||||||
(first-day-start (chronometrist-apply-time day-start-time start-time))
|
(first-day-start (chronometrist-apply-time day-start-time start-time))
|
||||||
(next-day-start (ts-adjust 'hour 24 first-day-start)))
|
(next-day-start (ts-adjust 'hour 24 first-day-start)))
|
||||||
;; Does the event stop time exceed the next day start time?
|
;; Does the interval stop time exceed the next day start time?
|
||||||
(when (ts< next-day-start stop-ts)
|
(when (ts< next-day-start stop-ts)
|
||||||
(let ((split-time (ts-format "%FT%T%z" next-day-start)))
|
(let ((split-time (ts-format "%FT%T%z" next-day-start)))
|
||||||
(list `(:start ,start-time :stop ,split-time)
|
(list `(:start ,start-time :stop ,split-time)
|
||||||
|
@ -499,8 +521,8 @@ SECONDS must be a positive integer."
|
||||||
|
|
||||||
;; [[file:chronometrist.org::*interval][interval:1]]
|
;; [[file:chronometrist.org::*interval][interval:1]]
|
||||||
(defun chronometrist-interval (plist)
|
(defun chronometrist-interval (plist)
|
||||||
"Return the period of time covered by EVENT as a time value.
|
"Return the period of time covered by PLIST as a time value.
|
||||||
EVENT should be a plist (see `chronometrist-file')."
|
PLIST should be a plist (see `chronometrist-file')."
|
||||||
(let* ((start-ts (chronometrist-iso-to-ts (plist-get plist :start)))
|
(let* ((start-ts (chronometrist-iso-to-ts (plist-get plist :start)))
|
||||||
(stop-iso (plist-get plist :stop))
|
(stop-iso (plist-get plist :stop))
|
||||||
;; Add a stop time if it does not exist.
|
;; Add a stop time if it does not exist.
|
||||||
|
@ -863,7 +885,7 @@ Return path of new file if successfully created, and nil if it already exists.")
|
||||||
|
|
||||||
;; [[file:chronometrist.org::*latest-date-records][latest-date-records:1]]
|
;; [[file:chronometrist.org::*latest-date-records][latest-date-records:1]]
|
||||||
(cl-defgeneric chronometrist-latest-date-records (backend)
|
(cl-defgeneric chronometrist-latest-date-records (backend)
|
||||||
"Return intervals of latest day in BACKEND as a tagged list (\"DATE\" PLIST*).
|
"Return intervals of latest day in BACKEND as a tagged list (\"DATE\" [<KEYWORD> <VALUE>]* PLIST+).
|
||||||
Return nil if BACKEND contains no records.")
|
Return nil if BACKEND contains no records.")
|
||||||
;; latest-date-records:1 ends here
|
;; latest-date-records:1 ends here
|
||||||
|
|
||||||
|
@ -987,13 +1009,16 @@ OLD-PATH and NEW-PATH are the old and new values of
|
||||||
(cl-defgeneric chronometrist-to-hash-table (backend)
|
(cl-defgeneric chronometrist-to-hash-table (backend)
|
||||||
"Return data in BACKEND as a hash table in chronological order.
|
"Return data in BACKEND as a hash table in chronological order.
|
||||||
Hash table keys are ISO-8601 date strings. Hash table values are
|
Hash table keys are ISO-8601 date strings. Hash table values are
|
||||||
lists of records, represented by plists. Both hash table keys and
|
lists of intervals (plists), optionally preceded by keyword-value
|
||||||
hash table values must be in chronological order.")
|
pairs. Both hash table keys and hash table values must be in
|
||||||
|
chronological order. Intervals in hash table values must not span
|
||||||
|
across days.")
|
||||||
;; to-hash-table:1 ends here
|
;; to-hash-table:1 ends here
|
||||||
|
|
||||||
;; [[file:chronometrist.org::*to-list][to-list:1]]
|
;; [[file:chronometrist.org::*to-list][to-list:1]]
|
||||||
(cl-defgeneric chronometrist-to-list (backend)
|
(cl-defgeneric chronometrist-to-list (backend)
|
||||||
"Return all records in BACKEND as a list of plists.")
|
"Return all intervals in BACKEND as a list of plists.
|
||||||
|
The resulting list does not contain record key-values.")
|
||||||
;; to-list:1 ends here
|
;; to-list:1 ends here
|
||||||
|
|
||||||
;; [[file:chronometrist.org::*memory-layer-empty-p][memory-layer-empty-p:1]]
|
;; [[file:chronometrist.org::*memory-layer-empty-p][memory-layer-empty-p:1]]
|
||||||
|
@ -1032,7 +1057,8 @@ hash table values must be in chronological order.")
|
||||||
:documentation "Full path to backend file, with extension.")
|
:documentation "Full path to backend file, with extension.")
|
||||||
(hash-table :initform (chronometrist-make-hash-table)
|
(hash-table :initform (chronometrist-make-hash-table)
|
||||||
:initarg :hash-table
|
:initarg :hash-table
|
||||||
:accessor chronometrist-backend-hash-table)
|
:accessor chronometrist-backend-hash-table
|
||||||
|
:documentation "Hash table as returned by `chronometrist-to-hash-table'.")
|
||||||
(file-watch :initform nil
|
(file-watch :initform nil
|
||||||
:initarg :file-watch
|
:initarg :file-watch
|
||||||
:accessor chronometrist-backend-file-watch
|
:accessor chronometrist-backend-file-watch
|
||||||
|
@ -1387,7 +1413,7 @@ STREAM (which is the value of `current-buffer')."
|
||||||
(chronometrist-backend-run-assertions backend)
|
(chronometrist-backend-run-assertions backend)
|
||||||
(with-slots (hash-table) backend
|
(with-slots (hash-table) backend
|
||||||
(when-let*
|
(when-let*
|
||||||
((latest-date (chronometrist-events-last-date hash-table))
|
((latest-date (chronometrist-ht-last-date hash-table))
|
||||||
(records (gethash latest-date hash-table)))
|
(records (gethash latest-date hash-table)))
|
||||||
(cons latest-date records))))
|
(cons latest-date records))))
|
||||||
;; latest-date-records:1 ends here
|
;; latest-date-records:1 ends here
|
||||||
|
@ -1496,7 +1522,7 @@ This is meant to be run in `chronometrist-file' when using an s-expression backe
|
||||||
`chronometrist-plist-backend' file."
|
`chronometrist-plist-backend' file."
|
||||||
(with-slots (hash-table) backend
|
(with-slots (hash-table) backend
|
||||||
(-let [(new-plist &as &plist :name new-task) (chronometrist-latest-record backend)]
|
(-let [(new-plist &as &plist :name new-task) (chronometrist-latest-record backend)]
|
||||||
(setf hash-table (chronometrist-events-update new-plist hash-table))
|
(setf hash-table (chronometrist-ht-update new-plist hash-table))
|
||||||
(chronometrist-add-to-task-list new-task backend))))
|
(chronometrist-add-to-task-list new-task backend))))
|
||||||
;; on-add:1 ends here
|
;; on-add:1 ends here
|
||||||
|
|
||||||
|
@ -1506,8 +1532,8 @@ This is meant to be run in `chronometrist-file' when using an s-expression backe
|
||||||
`chronometrist-plist-backend' file is modified."
|
`chronometrist-plist-backend' file is modified."
|
||||||
(with-slots (hash-table) backend
|
(with-slots (hash-table) backend
|
||||||
(-let (((new-plist &as &plist :name new-task) (chronometrist-latest-record backend))
|
(-let (((new-plist &as &plist :name new-task) (chronometrist-latest-record backend))
|
||||||
((&plist :name old-task) (chronometrist-events-last backend)))
|
((&plist :name old-task) (chronometrist-ht-last hash-table)))
|
||||||
(setf hash-table (chronometrist-events-update new-plist hash-table t))
|
(setf hash-table (chronometrist-ht-update new-plist hash-table t))
|
||||||
(chronometrist-remove-from-task-list old-task backend)
|
(chronometrist-remove-from-task-list old-task backend)
|
||||||
(chronometrist-add-to-task-list new-task backend))))
|
(chronometrist-add-to-task-list new-task backend))))
|
||||||
;; on-modify:1 ends here
|
;; on-modify:1 ends here
|
||||||
|
@ -1517,8 +1543,8 @@ This is meant to be run in `chronometrist-file' when using an s-expression backe
|
||||||
"Function run when the newest plist in a
|
"Function run when the newest plist in a
|
||||||
`chronometrist-plist-backend' file is deleted."
|
`chronometrist-plist-backend' file is deleted."
|
||||||
(with-slots (hash-table) backend
|
(with-slots (hash-table) backend
|
||||||
(-let (((&plist :name old-task) (chronometrist-events-last))
|
(-let (((&plist :name old-task) (chronometrist-ht-last hash-table))
|
||||||
(date (chronometrist-events-last-date hash-table)))
|
(date (chronometrist-ht-last-date hash-table)))
|
||||||
;; `chronometrist-remove-from-task-list' checks the hash table to determine
|
;; `chronometrist-remove-from-task-list' checks the hash table to determine
|
||||||
;; if `chronometrist-task-list' is to be updated. Thus, the hash table must
|
;; if `chronometrist-task-list' is to be updated. Thus, the hash table must
|
||||||
;; not be updated until the task list is.
|
;; not be updated until the task list is.
|
||||||
|
@ -1742,8 +1768,8 @@ Return value is either a list in the form
|
||||||
;; [[file:chronometrist.org::*to-list][to-list:1]]
|
;; [[file:chronometrist.org::*to-list][to-list:1]]
|
||||||
(cl-defmethod chronometrist-to-list ((backend chronometrist-plist-group-backend))
|
(cl-defmethod chronometrist-to-list ((backend chronometrist-plist-group-backend))
|
||||||
(chronometrist-backend-run-assertions backend)
|
(chronometrist-backend-run-assertions backend)
|
||||||
(chronometrist-loop-sexp-file for expr in (chronometrist-backend-file backend)
|
(chronometrist-loop-sexp-file for record in (chronometrist-backend-file backend)
|
||||||
append (reverse (cl-rest expr))))
|
append (chronometrist-record-intervals record)))
|
||||||
;; to-list:1 ends here
|
;; to-list:1 ends here
|
||||||
|
|
||||||
;; [[file:chronometrist.org::*to-hash-table][to-hash-table:1]]
|
;; [[file:chronometrist.org::*to-hash-table][to-hash-table:1]]
|
||||||
|
@ -1776,9 +1802,10 @@ Return value is either a list in the form
|
||||||
"Function run when a new plist-group is added at the end of a
|
"Function run when a new plist-group is added at the end of a
|
||||||
`chronometrist-plist-group-backend' file."
|
`chronometrist-plist-group-backend' file."
|
||||||
(with-slots (hash-table) backend
|
(with-slots (hash-table) backend
|
||||||
(-let [(date plist) (chronometrist-latest-date-records backend)]
|
(-let* (((record &as date . rest) (chronometrist-latest-date-records backend))
|
||||||
(puthash date plist hash-table)
|
((interval) (chronometrist-record-intervals record)))
|
||||||
(chronometrist-add-to-task-list (plist-get plist :name) backend))))
|
(puthash date rest hash-table)
|
||||||
|
(chronometrist-add-to-task-list (plist-get interval :name) backend))))
|
||||||
;; on-add:1 ends here
|
;; on-add:1 ends here
|
||||||
|
|
||||||
;; [[file:chronometrist.org::*on-modify][on-modify:1]]
|
;; [[file:chronometrist.org::*on-modify][on-modify:1]]
|
||||||
|
@ -1787,7 +1814,7 @@ Return value is either a list in the form
|
||||||
`chronometrist-plist-group-backend' file is modified."
|
`chronometrist-plist-group-backend' file is modified."
|
||||||
(with-slots (hash-table) backend
|
(with-slots (hash-table) backend
|
||||||
(-let* (((date . plists) (chronometrist-latest-date-records backend))
|
(-let* (((date . plists) (chronometrist-latest-date-records backend))
|
||||||
(old-date (chronometrist-events-last-date hash-table))
|
(old-date (chronometrist-ht-last-date hash-table))
|
||||||
(old-plists (gethash old-date hash-table)))
|
(old-plists (gethash old-date hash-table)))
|
||||||
(puthash date plists hash-table)
|
(puthash date plists hash-table)
|
||||||
(cl-loop for plist in old-plists
|
(cl-loop for plist in old-plists
|
||||||
|
@ -1801,7 +1828,7 @@ Return value is either a list in the form
|
||||||
"Function run when the newest plist-group in a
|
"Function run when the newest plist-group in a
|
||||||
`chronometrist-plist-group-backend' file is deleted."
|
`chronometrist-plist-group-backend' file is deleted."
|
||||||
(with-slots (hash-table) backend
|
(with-slots (hash-table) backend
|
||||||
(-let* ((old-date (chronometrist-events-last-date hash-table))
|
(-let* ((old-date (chronometrist-ht-last-date hash-table))
|
||||||
(old-plists (gethash old-date hash-table)))
|
(old-plists (gethash old-date hash-table)))
|
||||||
(cl-loop for plist in old-plists
|
(cl-loop for plist in old-plists
|
||||||
do (chronometrist-remove-from-task-list (plist-get plist :name) backend))
|
do (chronometrist-remove-from-task-list (plist-get plist :name) backend))
|
||||||
|
@ -2945,9 +2972,9 @@ displayed. They must be ts structs (see `ts.el').")
|
||||||
It simply operates on the entire hash table TABLE (see
|
It simply operates on the entire hash table TABLE (see
|
||||||
`chronometrist-to-hash-table' for table format), so ensure that TABLE is
|
`chronometrist-to-hash-table' for table format), so ensure that TABLE is
|
||||||
reduced to the desired range using
|
reduced to the desired range using
|
||||||
`chronometrist-events-subset'."
|
`chronometrist-ht-subset'."
|
||||||
(cl-loop for task in (chronometrist-task-list) collect
|
(cl-loop for task in (chronometrist-task-list) collect
|
||||||
(let* ((active-days (chronometrist-statistics-count-active-days task table))
|
(let* ((active-days (chronometrist-count-active-days task table))
|
||||||
(active-percent (cl-case (plist-get chronometrist-statistics--ui-state :mode)
|
(active-percent (cl-case (plist-get chronometrist-statistics--ui-state :mode)
|
||||||
('week (* 100 (/ active-days 7.0)))))
|
('week (* 100 (/ active-days 7.0)))))
|
||||||
(active-percent (if (zerop active-days)
|
(active-percent (if (zerop active-days)
|
||||||
|
@ -2974,12 +3001,12 @@ reduced to the desired range using
|
||||||
('week
|
('week
|
||||||
(let* ((start (plist-get chronometrist-statistics--ui-state :start))
|
(let* ((start (plist-get chronometrist-statistics--ui-state :start))
|
||||||
(end (plist-get chronometrist-statistics--ui-state :end))
|
(end (plist-get chronometrist-statistics--ui-state :end))
|
||||||
(ht (chronometrist-events-subset start end hash-table)))
|
(ht (chronometrist-ht-subset start end hash-table)))
|
||||||
(chronometrist-statistics-rows-internal ht)))
|
(chronometrist-statistics-rows-internal ht)))
|
||||||
(t ;; `chronometrist-statistics--ui-state' is nil, show current week's data
|
(t ;; `chronometrist-statistics--ui-state' is nil, show current week's data
|
||||||
(let* ((start (chronometrist-previous-week-start (chronometrist-date-ts)))
|
(let* ((start (chronometrist-previous-week-start (chronometrist-date-ts)))
|
||||||
(end (ts-adjust 'day 7 start))
|
(end (ts-adjust 'day 7 start))
|
||||||
(ht (chronometrist-events-subset start end hash-table)))
|
(ht (chronometrist-ht-subset start end hash-table)))
|
||||||
(setq chronometrist-statistics--ui-state `(:mode week :start ,start :end ,end))
|
(setq chronometrist-statistics--ui-state `(:mode week :start ,start :end ,end))
|
||||||
(chronometrist-statistics-rows-internal ht))))))
|
(chronometrist-statistics-rows-internal ht))))))
|
||||||
;; rows:1 ends here
|
;; rows:1 ends here
|
||||||
|
|
|
@ -48,8 +48,11 @@ Quite recently, after around two years of Chronometrist developement and use, I
|
||||||
:PROPERTIES:
|
:PROPERTIES:
|
||||||
:DESCRIPTION: Explanation of some terms used later
|
:DESCRIPTION: Explanation of some terms used later
|
||||||
:END:
|
:END:
|
||||||
Chronometrist records /time intervals/ (earlier called "events") as plists. Each plist contains at least a =:name "<name>"=, a =:start "<iso-timestamp>"=, and (except in case of an ongoing task) a =:stop "<iso-timestamp>"=.
|
Chronometrist deals with two entities -
|
||||||
|
+ intervals (earlier called "events") :: a continuous range of time spent on a task. These are currently represented as plists. Each plist contains at least a =:name "<name>"=, a =:start "<iso-timestamp>"=, and (except in case of an ongoing task) a =:stop "<iso-timestamp>"=. Optionally, it may contain other keys.
|
||||||
|
+ records :: these associate a date with time intervals (and possibly key-values). These are currently represented as lists in the form =("<ISO-8601 date>" [<keyword> <value>]* <interval>+)=.
|
||||||
|
|
||||||
|
Some other terms
|
||||||
+ row :: a row of a table in a =tabulated-list-mode= buffer; an element of =tabulated-list-entries=.
|
+ row :: a row of a table in a =tabulated-list-mode= buffer; an element of =tabulated-list-entries=.
|
||||||
+ schema :: the column descriptor of a table in a =tabulated-list-mode= buffer; the value of =tabulated-list-format=.
|
+ schema :: the column descriptor of a table in a =tabulated-list-mode= buffer; the value of =tabulated-list-format=.
|
||||||
|
|
||||||
|
@ -78,7 +81,7 @@ One of the earliest 'optimizations' of great importance turned out to simply be
|
||||||
*** Preserve hash table state for some commands
|
*** Preserve hash table state for some commands
|
||||||
NOTE - this has been replaced with a more general optimization - see next section.
|
NOTE - this has been replaced with a more general optimization - see next section.
|
||||||
|
|
||||||
The next one was released in v0.5. Till then, any time the [[* chronometrist-file][=chronometrist-file=]] was modified, we'd clear the =chronometrist-events= hash table and read data into it again. The reading itself is nearly-instant, even with ~2 years' worth of data [fn:2] (it uses Emacs' [[elisp:(describe-function 'read)][=read=]], after all), but the splitting of [[#explanation-midnight-spanning-intervals][midnight-spanning events]] is the real performance killer.
|
The next one was released in v0.5. Till then, any time the [[* chronometrist-file][=chronometrist-file=]] was modified, we'd clear the hash table and read data into it again. The reading itself is nearly-instant, even with ~2 years' worth of data [fn:2] (it uses Emacs' [[elisp:(describe-function 'read)][=read=]], after all), but the splitting of [[#explanation-midnight-spanning-intervals][midnight-spanning intervals]] is the real performance killer.
|
||||||
|
|
||||||
After the optimization...
|
After the optimization...
|
||||||
1. Two backend functions (=chronometrist-sexp-new= and =chronometrist-sexp-replace-last=) were modified to set a flag (=chronometrist--inhibit-read-p=) before saving the file.
|
1. Two backend functions (=chronometrist-sexp-new= and =chronometrist-sexp-replace-last=) were modified to set a flag (=chronometrist--inhibit-read-p=) before saving the file.
|
||||||
|
@ -90,14 +93,14 @@ After the optimization...
|
||||||
|
|
||||||
There are still some operations which [[* refresh-file][=chronometrist-refresh-file=]] runs unconditionally - which is to say there is scope for further optimization, if or when required.
|
There are still some operations which [[* refresh-file][=chronometrist-refresh-file=]] runs unconditionally - which is to say there is scope for further optimization, if or when required.
|
||||||
|
|
||||||
[fn:2] As indicated by exploratory work in the =parsimonious-reading= branch, where I made a loop to only =read= and collect s-expressions from the file. It was near-instant...until I added event splitting to it.
|
[fn:2] As indicated by exploratory work in the =parsimonious-reading= branch, where I made a loop to only =read= and collect s-expressions from the file. It was near-instant...until I added interval splitting to it.
|
||||||
|
|
||||||
*** Determine type of change made to file
|
*** Determine type of change made to file
|
||||||
Most changes, whether made through user-editing or by Chronometrist commands, happen at the end of the file. We try to detect the kind of change made - whether the last expression was modified, removed, or whether a new expression was added to the end - and make the corresponding change to =chronometrist-events=, instead of doing a full parse again (=chronometrist-events-populate=). The increase in responsiveness has been significant.
|
Most changes, whether made through user-editing or by Chronometrist commands, happen at the end of the file. We try to detect the kind of change made - whether the last expression was modified, removed, or whether a new expression was added to the end - and make the corresponding change to the hash table, instead of doing a full parse again (=chronometrist-to-hash-table=). The increase in responsiveness has been significant.
|
||||||
|
|
||||||
When =chronometrist-refresh-file= is run by the file system watcher, it uses =chronometrist-file-hash= to assign indices and a hash to =chronometrist--file-state=. The next time the file changes, =chronometrist-file-change-type= compares this state to the current state of the file to determine the type of change made.
|
When =chronometrist-refresh-file= is run by the file system watcher, it uses =chronometrist-file-hash= to assign indices and a hash to =chronometrist--file-state=. The next time the file changes, =chronometrist-file-change-type= compares this state to the current state of the file to determine the type of change made.
|
||||||
|
|
||||||
Challenges -
|
**** Challenges
|
||||||
1. Correctly detecting the type of change
|
1. Correctly detecting the type of change
|
||||||
2. Updating =chronometrist-task-list= and the Chronometrist buffer, when a new task is added or the last interval for a task is removed (v0.6.4)
|
2. Updating =chronometrist-task-list= and the Chronometrist buffer, when a new task is added or the last interval for a task is removed (v0.6.4)
|
||||||
3. Handling changes made to an active interval after midnight
|
3. Handling changes made to an active interval after midnight
|
||||||
|
@ -106,7 +109,7 @@ Challenges -
|
||||||
* =:modify= - normally, replace in table; for spanning intervals, split and replace
|
* =:modify= - normally, replace in table; for spanning intervals, split and replace
|
||||||
* =:remove= - normally, remove from table; for spanning intervals, split and remove
|
* =:remove= - normally, remove from table; for spanning intervals, split and remove
|
||||||
|
|
||||||
Effects on the task list
|
**** Effects on the task list
|
||||||
1. When a plist is added, the =:name= might be new, in which case we need to add it to the task list.
|
1. When a plist is added, the =:name= might be new, in which case we need to add it to the task list.
|
||||||
2. When the last plist is modified, the =:name= may have changed -
|
2. When the last plist is modified, the =:name= may have changed -
|
||||||
1. the =:name= might be new and require addition to the task list.
|
1. the =:name= might be new and require addition to the task list.
|
||||||
|
@ -115,10 +118,10 @@ Effects on the task list
|
||||||
|
|
||||||
** Midnight-spanning intervals
|
** Midnight-spanning intervals
|
||||||
:PROPERTIES:
|
:PROPERTIES:
|
||||||
:DESCRIPTION: Events starting on one day and ending on another
|
:DESCRIPTION: Intervals starting on one day and ending on another
|
||||||
:CUSTOM_ID: explanation-midnight-spanning-intervals
|
:CUSTOM_ID: explanation-midnight-spanning-intervals
|
||||||
:END:
|
:END:
|
||||||
A unique problem in working with Chronometrist, one I had never foreseen, was tasks which start on one day and end on another. For instance, you start working on something at =2021-01-01 23:00= hours and stop on =2021-01-02 01:00=.
|
A unique problem in working with Chronometrist, one I had never foreseen, was intervals which start on one day and end on another. For instance, you start working on something at =2021-01-01 23:00= hours and stop on =2021-01-02 01:00=.
|
||||||
|
|
||||||
These mess up data consumption in all sorts of unforeseen ways, especially interval calculations and acquiring intervals for a specific date. In case of two of the most common operations throughout the program -
|
These mess up data consumption in all sorts of unforeseen ways, especially interval calculations and acquiring intervals for a specific date. In case of two of the most common operations throughout the program -
|
||||||
1. finding the intervals recorded on a given date
|
1. finding the intervals recorded on a given date
|
||||||
|
@ -126,21 +129,21 @@ These mess up data consumption in all sorts of unforeseen ways, especially inter
|
||||||
|
|
||||||
There are a few different approaches of dealing with them. (Currently, Chronometrist uses #3.)
|
There are a few different approaches of dealing with them. (Currently, Chronometrist uses #3.)
|
||||||
|
|
||||||
*** Check the code of the first event of the day (timeclock format)
|
*** Check the code of the first interval of the day (timeclock format)
|
||||||
:PROPERTIES:
|
:PROPERTIES:
|
||||||
:DESCRIPTION: When the code of the first event in the day is "o", it's a midnight-spanning event.
|
:DESCRIPTION: When the code of the first interval in the day is "o", it's a midnight-spanning interval.
|
||||||
:END:
|
:END:
|
||||||
+ Advantage - very simple to detect
|
+ Advantage - very simple to detect
|
||||||
+ Disadvantage - "in" and "out" events must be represented separately
|
+ Disadvantage - "in" and "out" intervals must be represented separately
|
||||||
|
|
||||||
*** Split them at the file level
|
*** Split them at the file level
|
||||||
+ Advantage - operation is performed only once for each such event + simpler data-consuming code + reduced post-parsing load.
|
+ Advantage - operation is performed only once for each such interval + simpler data-consuming code + reduced post-parsing load.
|
||||||
+ What happens when the user changes their day-start-time? The split-up events are now split wrongly, and the second event may get split /again./
|
+ What happens when the user changes their day-start-time? The split-up intervals are now split wrongly, and the second interval may get split /again./
|
||||||
|
|
||||||
Possible solutions -
|
Possible solutions -
|
||||||
1. Add function to check if, for two events A and B, the =:stop= of A is the same as the =:start= of B, and that all their other tags are identical. Then we can re-split them according to the new day-start-time.
|
1. Add function to check if, for two intervals A and B, the =:stop= of A is the same as the =:start= of B, and that all their other tags are identical. Then we can re-split them according to the new day-start-time.
|
||||||
+ Implemented as [[#program-data-structures-plists-split-p][plist-split-p]]
|
+ Implemented as [[#program-data-structures-plists-split-p][plist-split-p]]
|
||||||
2. Add a =:split= tag to split events. It can denote that the next event was originally a part of this one.
|
2. Add a =:split= tag to split intervals. It can denote that the next interval was originally a part of this one.
|
||||||
3. Re-check and update the file when the day-start-time changes.
|
3. Re-check and update the file when the day-start-time changes.
|
||||||
- Possible with ~add-variable-watcher~ or ~:custom-set~ in Customize (thanks bpalmer)
|
- Possible with ~add-variable-watcher~ or ~:custom-set~ in Customize (thanks bpalmer)
|
||||||
|
|
||||||
|
@ -150,7 +153,7 @@ This strategy is implemented in the [[#program-backend-plist-group][plist-group]
|
||||||
Handled by ~chronometrist-sexp-events-populate~
|
Handled by ~chronometrist-sexp-events-populate~
|
||||||
+ Advantage - simpler data-consuming code.
|
+ Advantage - simpler data-consuming code.
|
||||||
|
|
||||||
*** Split them at the data-consumer level (e.g. when calculating time for one day/getting events for one day)
|
*** Split them at the data-consumer level (e.g. when calculating time for one day/getting intervals for one day)
|
||||||
+ Advantage - reduced repetitive post-parsing load.
|
+ Advantage - reduced repetitive post-parsing load.
|
||||||
|
|
||||||
** Point restore behaviour
|
** Point restore behaviour
|
||||||
|
@ -251,18 +254,16 @@ Further details are stored in properties -
|
||||||
:END:
|
:END:
|
||||||
|
|
||||||
*** ts
|
*** ts
|
||||||
ts.el struct
|
=ts.el= struct
|
||||||
* Used by nearly all internal functions
|
* Used by nearly all internal functions
|
||||||
|
|
||||||
*** iso-timestamp
|
*** iso-timestamp
|
||||||
="YYYY-MM-DDTHH:MM:SSZ"=
|
="YYYY-MM-DDTHH:MM:SSZ"=
|
||||||
* Used in the s-expression file format
|
* Used for intervals
|
||||||
* Read by chronometrist-sexp-events-populate
|
|
||||||
* Used in the plists in the chronometrist-events hash table values
|
|
||||||
|
|
||||||
*** iso-date
|
*** iso-date
|
||||||
="YYYY-MM-DD"=
|
="YYYY-MM-DD"=
|
||||||
* Used as hash table keys in chronometrist-events - can't use ts structs for keys, you'd have to make a hash table predicate which uses ts=
|
* Used as hash table keys - can't use =ts= structs for keys, you'd have to make a hash table predicate which uses =ts==
|
||||||
|
|
||||||
*** seconds
|
*** seconds
|
||||||
integer seconds as duration
|
integer seconds as duration
|
||||||
|
@ -535,7 +536,6 @@ If FIRSTONLY is non-nil, return only the first keybinding found."
|
||||||
#+END_SRC
|
#+END_SRC
|
||||||
|
|
||||||
*** day-start-time :custom:variable:
|
*** day-start-time :custom:variable:
|
||||||
=chronometrist-events-maybe-split= refers to this, but I'm not sure this has the desired effect at the moment—haven't even tried using it.
|
|
||||||
#+BEGIN_SRC emacs-lisp
|
#+BEGIN_SRC emacs-lisp
|
||||||
(defcustom chronometrist-day-start-time "00:00:00"
|
(defcustom chronometrist-day-start-time "00:00:00"
|
||||||
"The time at which a day is considered to start, in \"HH:MM:SS\".
|
"The time at which a day is considered to start, in \"HH:MM:SS\".
|
||||||
|
@ -686,10 +686,10 @@ Return new position of point."
|
||||||
#+BEGIN_SRC emacs-lisp
|
#+BEGIN_SRC emacs-lisp
|
||||||
(cl-defun chronometrist-current-task (&optional (backend (chronometrist-active-backend)))
|
(cl-defun chronometrist-current-task (&optional (backend (chronometrist-active-backend)))
|
||||||
"Return the name of the active task as a string, or nil if not clocked in."
|
"Return the name of the active task as a string, or nil if not clocked in."
|
||||||
(let ((last-event (chronometrist-latest-record backend)))
|
(let ((last-interval (chronometrist-latest-record backend)))
|
||||||
(if (plist-member last-event :stop)
|
(if (plist-member last-interval :stop)
|
||||||
nil
|
nil
|
||||||
(plist-get last-event :name))))
|
(plist-get last-interval :name))))
|
||||||
#+END_SRC
|
#+END_SRC
|
||||||
|
|
||||||
*** install-directory :variable:
|
*** install-directory :variable:
|
||||||
|
@ -748,12 +748,6 @@ FORMAT-STRING and ARGS are passed to `format'."
|
||||||
:PROPERTIES:
|
:PROPERTIES:
|
||||||
:CUSTOM_ID: program-data-structures
|
:CUSTOM_ID: program-data-structures
|
||||||
:END:
|
:END:
|
||||||
Reading directly from the file could be difficult, especially when your most common query is "get all intervals recorded on <date>" [fn:4] - and so, we maintain the hash table =chronometrist-events=, where each key is a date in the ISO-8601 format. The plists in this hash table are free of [[#explanation-midnight-spanning-intervals][midnight-spanning intervals]], making code which consumes it easier to write.
|
|
||||||
|
|
||||||
The data from =chronometrist-events= is used by most (all?) interval-consuming functions, but is never written to the user's file itself.
|
|
||||||
|
|
||||||
[fn:4] it might be the case that the [[#program-backend][file format]] is not suited to our most frequent operation...
|
|
||||||
|
|
||||||
*** reset :command:
|
*** reset :command:
|
||||||
#+BEGIN_SRC emacs-lisp
|
#+BEGIN_SRC emacs-lisp
|
||||||
(defun chronometrist-reset ()
|
(defun chronometrist-reset ()
|
||||||
|
@ -799,12 +793,12 @@ Return value is a ts struct (see `ts.el')."
|
||||||
((&plist :start start-2 :stop stop-2) (cl-second split-time))
|
((&plist :start start-2 :stop stop-2) (cl-second split-time))
|
||||||
;; `plist-put' modifies lists in-place. The resulting bugs
|
;; `plist-put' modifies lists in-place. The resulting bugs
|
||||||
;; left me puzzled for a while.
|
;; left me puzzled for a while.
|
||||||
(event-1 (cl-copy-list plist))
|
(interval-1 (cl-copy-list plist))
|
||||||
(event-2 (cl-copy-list plist)))
|
(interval-2 (cl-copy-list plist)))
|
||||||
(list (-> event-1
|
(list (-> interval-1
|
||||||
(plist-put :start start-1)
|
(plist-put :start start-1)
|
||||||
(plist-put :stop stop-1))
|
(plist-put :stop stop-1))
|
||||||
(-> event-2
|
(-> interval-2
|
||||||
(plist-put :start start-2)
|
(plist-put :start start-2)
|
||||||
(plist-put :stop stop-2))))))))
|
(plist-put :stop stop-2))))))))
|
||||||
#+END_SRC
|
#+END_SRC
|
||||||
|
@ -833,48 +827,47 @@ Return value is a ts struct (see `ts.el')."
|
||||||
:stop "2021-02-13T00:03:46+0530")))))
|
:stop "2021-02-13T00:03:46+0530")))))
|
||||||
#+END_SRC
|
#+END_SRC
|
||||||
|
|
||||||
*** events-update :writer:
|
*** ht-update :writer:
|
||||||
#+BEGIN_SRC emacs-lisp
|
#+BEGIN_SRC emacs-lisp
|
||||||
(defun chronometrist-events-update (plist hash-table &optional replace)
|
(defun chronometrist-ht-update (plist hash-table &optional replace)
|
||||||
"Return HASH-TABLE with PLIST added as the latest interval.
|
"Return HASH-TABLE with PLIST added as the latest interval.
|
||||||
If REPLACE is non-nil, replace the last interval with PLIST."
|
If REPLACE is non-nil, replace the last interval with PLIST."
|
||||||
(let* ((date (->> (plist-get plist :start)
|
(let* ((date (->> (plist-get plist :start)
|
||||||
(chronometrist-iso-to-ts )
|
(chronometrist-iso-to-ts )
|
||||||
(ts-format "%F" )))
|
(ts-format "%F" )))
|
||||||
(events-today (gethash date hash-table)))
|
(intervals-today (gethash date hash-table)))
|
||||||
(--> (if replace (-drop-last 1 events-today) events-today)
|
(--> (if replace (-drop-last 1 intervals-today) intervals-today)
|
||||||
(append it (list plist))
|
(append it (list plist))
|
||||||
(puthash date it hash-table))
|
(puthash date it hash-table))
|
||||||
hash-table))
|
hash-table))
|
||||||
#+END_SRC
|
#+END_SRC
|
||||||
|
|
||||||
*** last-date :reader:
|
*** ht-last-date :reader:
|
||||||
#+BEGIN_SRC emacs-lisp
|
#+BEGIN_SRC emacs-lisp
|
||||||
(defun chronometrist-events-last-date (hash-table)
|
(defun chronometrist-ht-last-date (hash-table)
|
||||||
"Return an ISO-8601 date string for the latest date present in `chronometrist-events'."
|
"Return an ISO-8601 date string for the latest date present in HASH-TABLE."
|
||||||
(--> (hash-table-keys hash-table)
|
(--> (hash-table-keys hash-table)
|
||||||
(last it)
|
(last it)
|
||||||
(car it)))
|
(cl-first it)))
|
||||||
#+END_SRC
|
#+END_SRC
|
||||||
|
|
||||||
*** events-last :reader:
|
*** ht-last :reader:
|
||||||
#+BEGIN_SRC emacs-lisp
|
#+BEGIN_SRC emacs-lisp
|
||||||
(cl-defun chronometrist-events-last (&optional (backend (chronometrist-active-backend)))
|
(cl-defun chronometrist-ht-last (&optional hash-table)
|
||||||
"Return the last plist from `chronometrist-events'."
|
"Return the last plist from HASH-TABLE."
|
||||||
(let* ((hash-table (chronometrist-backend-hash-table backend))
|
(--> (chronometrist-ht-last-date hash-table)
|
||||||
(last-date (chronometrist-events-last-date hash-table)))
|
(gethash it hash-table)
|
||||||
(--> (gethash last-date hash-table)
|
(last it)
|
||||||
(last it)
|
(cl-first it)))
|
||||||
(car it))))
|
|
||||||
#+END_SRC
|
#+END_SRC
|
||||||
|
|
||||||
*** events-subset :reader:
|
*** ht-subset :reader:
|
||||||
:PROPERTIES:
|
:PROPERTIES:
|
||||||
:VALUE: hash table
|
:VALUE: hash table
|
||||||
:CUSTOM_ID: program-data-structures-events-subset
|
:CUSTOM_ID: program-data-structures-ht-subset
|
||||||
:END:
|
:END:
|
||||||
#+BEGIN_SRC emacs-lisp
|
#+BEGIN_SRC emacs-lisp
|
||||||
(defun chronometrist-events-subset (start end hash-table)
|
(defun chronometrist-ht-subset (start end hash-table)
|
||||||
"Return a subset of HASH-TABLE.
|
"Return a subset of HASH-TABLE.
|
||||||
The subset will contain values between dates START and END (both
|
The subset will contain values between dates START and END (both
|
||||||
inclusive).
|
inclusive).
|
||||||
|
@ -896,12 +889,12 @@ treated as though their time is 00:00:00."
|
||||||
(cl-defun chronometrist-task-time-one-day (task &optional (date (chronometrist-date-ts)) (backend (chronometrist-active-backend)))
|
(cl-defun chronometrist-task-time-one-day (task &optional (date (chronometrist-date-ts)) (backend (chronometrist-active-backend)))
|
||||||
"Return total time spent on TASK today or on DATE, an ISO-8601 date.
|
"Return total time spent on TASK today or on DATE, an ISO-8601 date.
|
||||||
The return value is seconds, as an integer."
|
The return value is seconds, as an integer."
|
||||||
(let ((task-events (chronometrist-task-records-for-date backend task date)))
|
(let ((task-intervals (chronometrist-task-records-for-date backend task date)))
|
||||||
(if task-events
|
(if task-intervals
|
||||||
(->> (chronometrist-plists-to-durations task-events)
|
(->> (chronometrist-plists-to-durations task-intervals)
|
||||||
(-reduce #'+)
|
(-reduce #'+)
|
||||||
(truncate))
|
(truncate))
|
||||||
;; no events for this task on DATE, i.e. no time spent
|
;; no intervals for this task on DATE, i.e. no time spent
|
||||||
0)))
|
0)))
|
||||||
#+END_SRC
|
#+END_SRC
|
||||||
|
|
||||||
|
@ -918,16 +911,16 @@ Return value is seconds as an integer."
|
||||||
|
|
||||||
*** count-active-days :function:
|
*** count-active-days :function:
|
||||||
#+BEGIN_SRC emacs-lisp
|
#+BEGIN_SRC emacs-lisp
|
||||||
(cl-defun chronometrist-statistics-count-active-days (task table)
|
(cl-defun chronometrist-count-active-days (task hash-table)
|
||||||
"Return the number of days the user spent any time on TASK.
|
"Return the number of days the user spent any time on TASK.
|
||||||
TABLE must be a hash table - if not supplied, `chronometrist-events' is used.
|
HASH-TABLE must be a hash table as returned by `chronometrist-to-hash-table'.
|
||||||
|
|
||||||
This will not return correct results if TABLE contains records
|
This will not return correct results if HASH-TABLE contains
|
||||||
which span midnights."
|
records which span midnights."
|
||||||
(cl-loop for events being the hash-values of table
|
(cl-loop for intervals being the hash-values of hash-table
|
||||||
count (seq-find (lambda (event)
|
count (seq-find (lambda (event)
|
||||||
(equal task (plist-get event :name)))
|
(equal task (plist-get event :name)))
|
||||||
events)))
|
intervals)))
|
||||||
#+END_SRC
|
#+END_SRC
|
||||||
|
|
||||||
*** task-list :variable:
|
*** task-list :variable:
|
||||||
|
@ -946,6 +939,36 @@ active backend."
|
||||||
:group 'chronometrist)
|
:group 'chronometrist)
|
||||||
#+END_SRC
|
#+END_SRC
|
||||||
|
|
||||||
|
*** record-properties :function:
|
||||||
|
[[file:tests/chronometrist-tests.org::#date-properties][tests]]
|
||||||
|
|
||||||
|
#+BEGIN_SRC elisp
|
||||||
|
(defun chronometrist-record-properties (plist-group)
|
||||||
|
"Return properties for DATE-RECORDS, if any.
|
||||||
|
PLIST-GROUP must be a tagged list as returned by
|
||||||
|
`chronometrist-latest-record'."
|
||||||
|
(cl-loop with valuep
|
||||||
|
for elt in plist-group
|
||||||
|
when (keywordp elt)
|
||||||
|
collect (progn (setq valuep t) elt) into plist
|
||||||
|
else when valuep
|
||||||
|
collect (progn (setq valuep nil) elt) into plist
|
||||||
|
else when (and (not (keywordp elt)) (not valuep))
|
||||||
|
do (cl-return plist)))
|
||||||
|
#+END_SRC
|
||||||
|
|
||||||
|
*** record-intervals :function:
|
||||||
|
#+BEGIN_SRC elisp
|
||||||
|
(defun chronometrist-record-intervals (record)
|
||||||
|
"Return intervals (as a list of plists) from RECORD.
|
||||||
|
RECORD must be a tagged plist as returned by
|
||||||
|
`chronometrist-latest-record'."
|
||||||
|
(cl-loop for elt being the elements of record
|
||||||
|
using (index i)
|
||||||
|
when (chronometrist-plist-p elt)
|
||||||
|
return (seq-drop record i)))
|
||||||
|
#+END_SRC
|
||||||
|
|
||||||
** Time functions
|
** Time functions
|
||||||
*** iso-to-ts :function:
|
*** iso-to-ts :function:
|
||||||
#+BEGIN_SRC emacs-lisp
|
#+BEGIN_SRC emacs-lisp
|
||||||
|
@ -1006,12 +1029,11 @@ TS should be a ts struct (see `ts.el')."
|
||||||
Optional argument UNIX-TIME should be a time value (see
|
Optional argument UNIX-TIME should be a time value (see
|
||||||
`current-time') accepted by `format-time-string'."
|
`current-time') accepted by `format-time-string'."
|
||||||
(format-time-string "%FT%T%z" unix-time))
|
(format-time-string "%FT%T%z" unix-time))
|
||||||
|
|
||||||
;; Note - this assumes that an event never crosses >1 day. This seems
|
|
||||||
;; sufficient for all conceivable cases.
|
|
||||||
#+END_SRC
|
#+END_SRC
|
||||||
|
|
||||||
*** FIXME split-time :reader:
|
*** FIXME split-time :reader:
|
||||||
|
Note - this assumes that an event never crosses >1 day. This seems sufficient for all conceivable cases.
|
||||||
|
|
||||||
It does not matter here that the =:stop= dates in the returned plists are different from the =:start=, because =chronometrist-events-populate= uses only the date segment of the =:start= values as hash table keys. (The hash table keys form the rest of the program's notion of "days", and that of which plists belong to which day.)
|
It does not matter here that the =:stop= dates in the returned plists are different from the =:start=, because =chronometrist-events-populate= uses only the date segment of the =:start= values as hash table keys. (The hash table keys form the rest of the program's notion of "days", and that of which plists belong to which day.)
|
||||||
|
|
||||||
#+BEGIN_SRC emacs-lisp
|
#+BEGIN_SRC emacs-lisp
|
||||||
|
@ -1028,13 +1050,13 @@ Return a list in the form
|
||||||
(:start <day start time on second day>
|
(:start <day start time on second day>
|
||||||
:stop STOP-TIME))"
|
:stop STOP-TIME))"
|
||||||
;; FIXME - time zones are ignored; may cause issues with
|
;; FIXME - time zones are ignored; may cause issues with
|
||||||
;; time-zone-spanning events
|
;; time-zone-spanning intervals
|
||||||
|
|
||||||
;; The time on which the first provided day starts (according to `chronometrist-day-start-time')
|
;; The time on which the first provided day starts (according to `chronometrist-day-start-time')
|
||||||
(let* ((stop-ts (chronometrist-iso-to-ts stop-time))
|
(let* ((stop-ts (chronometrist-iso-to-ts stop-time))
|
||||||
(first-day-start (chronometrist-apply-time day-start-time start-time))
|
(first-day-start (chronometrist-apply-time day-start-time start-time))
|
||||||
(next-day-start (ts-adjust 'hour 24 first-day-start)))
|
(next-day-start (ts-adjust 'hour 24 first-day-start)))
|
||||||
;; Does the event stop time exceed the next day start time?
|
;; Does the interval stop time exceed the next day start time?
|
||||||
(when (ts< next-day-start stop-ts)
|
(when (ts< next-day-start stop-ts)
|
||||||
(let ((split-time (ts-format "%FT%T%z" next-day-start)))
|
(let ((split-time (ts-format "%FT%T%z" next-day-start)))
|
||||||
(list `(:start ,start-time :stop ,split-time)
|
(list `(:start ,start-time :stop ,split-time)
|
||||||
|
@ -1084,8 +1106,8 @@ SECONDS must be a positive integer."
|
||||||
*** interval :function:
|
*** interval :function:
|
||||||
#+BEGIN_SRC emacs-lisp
|
#+BEGIN_SRC emacs-lisp
|
||||||
(defun chronometrist-interval (plist)
|
(defun chronometrist-interval (plist)
|
||||||
"Return the period of time covered by EVENT as a time value.
|
"Return the period of time covered by PLIST as a time value.
|
||||||
EVENT should be a plist (see `chronometrist-file')."
|
PLIST should be a plist (see `chronometrist-file')."
|
||||||
(let* ((start-ts (chronometrist-iso-to-ts (plist-get plist :start)))
|
(let* ((start-ts (chronometrist-iso-to-ts (plist-get plist :start)))
|
||||||
(stop-iso (plist-get plist :stop))
|
(stop-iso (plist-get plist :stop))
|
||||||
;; Add a stop time if it does not exist.
|
;; Add a stop time if it does not exist.
|
||||||
|
@ -1349,7 +1371,6 @@ IN-SUBLIST, if non-nil, means point is inside an inner list."
|
||||||
:PROPERTIES:
|
:PROPERTIES:
|
||||||
:CUSTOM_ID: program-backend
|
:CUSTOM_ID: program-backend
|
||||||
:END:
|
:END:
|
||||||
|
|
||||||
*** chronometrist-file :custom:variable:
|
*** chronometrist-file :custom:variable:
|
||||||
#+BEGIN_SRC emacs-lisp
|
#+BEGIN_SRC emacs-lisp
|
||||||
(defcustom chronometrist-file
|
(defcustom chronometrist-file
|
||||||
|
@ -1532,7 +1553,7 @@ Return path of new file if successfully created, and nil if it already exists.")
|
||||||
***** latest-date-records :generic:function:
|
***** latest-date-records :generic:function:
|
||||||
#+BEGIN_SRC emacs-lisp
|
#+BEGIN_SRC emacs-lisp
|
||||||
(cl-defgeneric chronometrist-latest-date-records (backend)
|
(cl-defgeneric chronometrist-latest-date-records (backend)
|
||||||
"Return intervals of latest day in BACKEND as a tagged list (\"DATE\" PLIST*).
|
"Return intervals of latest day in BACKEND as a tagged list (\"DATE\" [<KEYWORD> <VALUE>]* PLIST+).
|
||||||
Return nil if BACKEND contains no records.")
|
Return nil if BACKEND contains no records.")
|
||||||
#+END_SRC
|
#+END_SRC
|
||||||
|
|
||||||
|
@ -1671,14 +1692,17 @@ OLD-PATH and NEW-PATH are the old and new values of
|
||||||
(cl-defgeneric chronometrist-to-hash-table (backend)
|
(cl-defgeneric chronometrist-to-hash-table (backend)
|
||||||
"Return data in BACKEND as a hash table in chronological order.
|
"Return data in BACKEND as a hash table in chronological order.
|
||||||
Hash table keys are ISO-8601 date strings. Hash table values are
|
Hash table keys are ISO-8601 date strings. Hash table values are
|
||||||
lists of records, represented by plists. Both hash table keys and
|
lists of intervals (plists), optionally preceded by keyword-value
|
||||||
hash table values must be in chronological order.")
|
pairs. Both hash table keys and hash table values must be in
|
||||||
|
chronological order. Intervals in hash table values must not span
|
||||||
|
across days.")
|
||||||
#+END_SRC
|
#+END_SRC
|
||||||
|
|
||||||
***** to-list :generic:function:
|
***** to-list :generic:function:
|
||||||
#+BEGIN_SRC emacs-lisp
|
#+BEGIN_SRC emacs-lisp
|
||||||
(cl-defgeneric chronometrist-to-list (backend)
|
(cl-defgeneric chronometrist-to-list (backend)
|
||||||
"Return all records in BACKEND as a list of plists.")
|
"Return all intervals in BACKEND as a list of plists.
|
||||||
|
The resulting list does not contain record key-values.")
|
||||||
#+END_SRC
|
#+END_SRC
|
||||||
|
|
||||||
***** memory-layer-empty-p :generic:function:
|
***** memory-layer-empty-p :generic:function:
|
||||||
|
@ -1710,6 +1734,9 @@ These can be implemented in terms of the minimal protocol above.
|
||||||
:PROPERTIES:
|
:PROPERTIES:
|
||||||
:CUSTOM_ID: file-backend-mixin
|
:CUSTOM_ID: file-backend-mixin
|
||||||
:END:
|
:END:
|
||||||
|
Reading directly from the file could be difficult, especially when your most common query is "get all intervals recorded on <date>" - and so, each backend maintains a hash table, where each key is a date in the ISO-8601 format. The plists in this hash table are free of [[#explanation-midnight-spanning-intervals][midnight-spanning intervals]], making code which consumes it easier to write.
|
||||||
|
|
||||||
|
The data from the backend hash table is used by most (all?) interval-consuming functions, but is never written to the user's file itself.
|
||||||
|
|
||||||
#+BEGIN_SRC emacs-lisp
|
#+BEGIN_SRC emacs-lisp
|
||||||
(defclass chronometrist-file-backend-mixin ()
|
(defclass chronometrist-file-backend-mixin ()
|
||||||
|
@ -1732,7 +1759,8 @@ These can be implemented in terms of the minimal protocol above.
|
||||||
:documentation "Full path to backend file, with extension.")
|
:documentation "Full path to backend file, with extension.")
|
||||||
(hash-table :initform (chronometrist-make-hash-table)
|
(hash-table :initform (chronometrist-make-hash-table)
|
||||||
:initarg :hash-table
|
:initarg :hash-table
|
||||||
:accessor chronometrist-backend-hash-table)
|
:accessor chronometrist-backend-hash-table
|
||||||
|
:documentation "Hash table as returned by `chronometrist-to-hash-table'.")
|
||||||
(file-watch :initform nil
|
(file-watch :initform nil
|
||||||
:initarg :file-watch
|
:initarg :file-watch
|
||||||
:accessor chronometrist-backend-file-watch
|
:accessor chronometrist-backend-file-watch
|
||||||
|
@ -2310,7 +2338,7 @@ In this backend, it's easier to implement this in terms of [[#program-backend-pl
|
||||||
(chronometrist-backend-run-assertions backend)
|
(chronometrist-backend-run-assertions backend)
|
||||||
(with-slots (hash-table) backend
|
(with-slots (hash-table) backend
|
||||||
(when-let*
|
(when-let*
|
||||||
((latest-date (chronometrist-events-last-date hash-table))
|
((latest-date (chronometrist-ht-last-date hash-table))
|
||||||
(records (gethash latest-date hash-table)))
|
(records (gethash latest-date hash-table)))
|
||||||
(cons latest-date records))))
|
(cons latest-date records))))
|
||||||
#+END_SRC
|
#+END_SRC
|
||||||
|
@ -2426,7 +2454,7 @@ This is meant to be run in `chronometrist-file' when using an s-expression backe
|
||||||
`chronometrist-plist-backend' file."
|
`chronometrist-plist-backend' file."
|
||||||
(with-slots (hash-table) backend
|
(with-slots (hash-table) backend
|
||||||
(-let [(new-plist &as &plist :name new-task) (chronometrist-latest-record backend)]
|
(-let [(new-plist &as &plist :name new-task) (chronometrist-latest-record backend)]
|
||||||
(setf hash-table (chronometrist-events-update new-plist hash-table))
|
(setf hash-table (chronometrist-ht-update new-plist hash-table))
|
||||||
(chronometrist-add-to-task-list new-task backend))))
|
(chronometrist-add-to-task-list new-task backend))))
|
||||||
#+END_SRC
|
#+END_SRC
|
||||||
|
|
||||||
|
@ -2437,8 +2465,8 @@ This is meant to be run in `chronometrist-file' when using an s-expression backe
|
||||||
`chronometrist-plist-backend' file is modified."
|
`chronometrist-plist-backend' file is modified."
|
||||||
(with-slots (hash-table) backend
|
(with-slots (hash-table) backend
|
||||||
(-let (((new-plist &as &plist :name new-task) (chronometrist-latest-record backend))
|
(-let (((new-plist &as &plist :name new-task) (chronometrist-latest-record backend))
|
||||||
((&plist :name old-task) (chronometrist-events-last backend)))
|
((&plist :name old-task) (chronometrist-ht-last hash-table)))
|
||||||
(setf hash-table (chronometrist-events-update new-plist hash-table t))
|
(setf hash-table (chronometrist-ht-update new-plist hash-table t))
|
||||||
(chronometrist-remove-from-task-list old-task backend)
|
(chronometrist-remove-from-task-list old-task backend)
|
||||||
(chronometrist-add-to-task-list new-task backend))))
|
(chronometrist-add-to-task-list new-task backend))))
|
||||||
#+END_SRC
|
#+END_SRC
|
||||||
|
@ -2449,8 +2477,8 @@ This is meant to be run in `chronometrist-file' when using an s-expression backe
|
||||||
"Function run when the newest plist in a
|
"Function run when the newest plist in a
|
||||||
`chronometrist-plist-backend' file is deleted."
|
`chronometrist-plist-backend' file is deleted."
|
||||||
(with-slots (hash-table) backend
|
(with-slots (hash-table) backend
|
||||||
(-let (((&plist :name old-task) (chronometrist-events-last))
|
(-let (((&plist :name old-task) (chronometrist-ht-last hash-table))
|
||||||
(date (chronometrist-events-last-date hash-table)))
|
(date (chronometrist-ht-last-date hash-table)))
|
||||||
;; `chronometrist-remove-from-task-list' checks the hash table to determine
|
;; `chronometrist-remove-from-task-list' checks the hash table to determine
|
||||||
;; if `chronometrist-task-list' is to be updated. Thus, the hash table must
|
;; if `chronometrist-task-list' is to be updated. Thus, the hash table must
|
||||||
;; not be updated until the task list is.
|
;; not be updated until the task list is.
|
||||||
|
@ -2515,11 +2543,11 @@ This is largely like the plist backend, but plists are grouped by date by wrappi
|
||||||
|
|
||||||
#+BEGIN_SRC emacs-lisp :tangle no :load no
|
#+BEGIN_SRC emacs-lisp :tangle no :load no
|
||||||
("<ISO-8601 date>"
|
("<ISO-8601 date>"
|
||||||
|
[:keyword <value>]* ;; key-values for the date itself
|
||||||
(:name "Task Name"
|
(:name "Task Name"
|
||||||
[:keyword <value>]*
|
[:keyword <value>]* ;; key-values for a plist
|
||||||
:start "<ISO-8601 time>"
|
:start "<ISO-8601 time>"
|
||||||
:stop "<ISO-8601 time>")
|
:stop "<ISO-8601 time>")+)
|
||||||
...)
|
|
||||||
#+END_SRC
|
#+END_SRC
|
||||||
|
|
||||||
This makes it easy and computationally cheap to perform our most common query - getting the plists on a given day. [[#explanation-midnight-spanning-intervals][Midnight-spanning intervals]] are split in the file itself. The downside is that the user, if editing it by hand, must take care to split the intervals.
|
This makes it easy and computationally cheap to perform our most common query - getting the plists on a given day. [[#explanation-midnight-spanning-intervals][Midnight-spanning intervals]] are split in the file itself. The downside is that the user, if editing it by hand, must take care to split the intervals.
|
||||||
|
@ -2736,8 +2764,8 @@ Return value is either a list in the form
|
||||||
#+BEGIN_SRC emacs-lisp
|
#+BEGIN_SRC emacs-lisp
|
||||||
(cl-defmethod chronometrist-to-list ((backend chronometrist-plist-group-backend))
|
(cl-defmethod chronometrist-to-list ((backend chronometrist-plist-group-backend))
|
||||||
(chronometrist-backend-run-assertions backend)
|
(chronometrist-backend-run-assertions backend)
|
||||||
(chronometrist-loop-sexp-file for expr in (chronometrist-backend-file backend)
|
(chronometrist-loop-sexp-file for record in (chronometrist-backend-file backend)
|
||||||
append (reverse (cl-rest expr))))
|
append (chronometrist-record-intervals record)))
|
||||||
#+END_SRC
|
#+END_SRC
|
||||||
|
|
||||||
**** to-hash-table :reader:method:
|
**** to-hash-table :reader:method:
|
||||||
|
@ -2773,9 +2801,10 @@ Return value is either a list in the form
|
||||||
"Function run when a new plist-group is added at the end of a
|
"Function run when a new plist-group is added at the end of a
|
||||||
`chronometrist-plist-group-backend' file."
|
`chronometrist-plist-group-backend' file."
|
||||||
(with-slots (hash-table) backend
|
(with-slots (hash-table) backend
|
||||||
(-let [(date plist) (chronometrist-latest-date-records backend)]
|
(-let* (((record &as date . rest) (chronometrist-latest-date-records backend))
|
||||||
(puthash date plist hash-table)
|
((interval) (chronometrist-record-intervals record)))
|
||||||
(chronometrist-add-to-task-list (plist-get plist :name) backend))))
|
(puthash date rest hash-table)
|
||||||
|
(chronometrist-add-to-task-list (plist-get interval :name) backend))))
|
||||||
#+END_SRC
|
#+END_SRC
|
||||||
|
|
||||||
**** on-modify :writer:method:
|
**** on-modify :writer:method:
|
||||||
|
@ -2785,7 +2814,7 @@ Return value is either a list in the form
|
||||||
`chronometrist-plist-group-backend' file is modified."
|
`chronometrist-plist-group-backend' file is modified."
|
||||||
(with-slots (hash-table) backend
|
(with-slots (hash-table) backend
|
||||||
(-let* (((date . plists) (chronometrist-latest-date-records backend))
|
(-let* (((date . plists) (chronometrist-latest-date-records backend))
|
||||||
(old-date (chronometrist-events-last-date hash-table))
|
(old-date (chronometrist-ht-last-date hash-table))
|
||||||
(old-plists (gethash old-date hash-table)))
|
(old-plists (gethash old-date hash-table)))
|
||||||
(puthash date plists hash-table)
|
(puthash date plists hash-table)
|
||||||
(cl-loop for plist in old-plists
|
(cl-loop for plist in old-plists
|
||||||
|
@ -2800,7 +2829,7 @@ Return value is either a list in the form
|
||||||
"Function run when the newest plist-group in a
|
"Function run when the newest plist-group in a
|
||||||
`chronometrist-plist-group-backend' file is deleted."
|
`chronometrist-plist-group-backend' file is deleted."
|
||||||
(with-slots (hash-table) backend
|
(with-slots (hash-table) backend
|
||||||
(-let* ((old-date (chronometrist-events-last-date hash-table))
|
(-let* ((old-date (chronometrist-ht-last-date hash-table))
|
||||||
(old-plists (gethash old-date hash-table)))
|
(old-plists (gethash old-date hash-table)))
|
||||||
(cl-loop for plist in old-plists
|
(cl-loop for plist in old-plists
|
||||||
do (chronometrist-remove-from-task-list (plist-get plist :name) backend))
|
do (chronometrist-remove-from-task-list (plist-get plist :name) backend))
|
||||||
|
@ -4101,9 +4130,9 @@ displayed. They must be ts structs (see `ts.el').")
|
||||||
It simply operates on the entire hash table TABLE (see
|
It simply operates on the entire hash table TABLE (see
|
||||||
`chronometrist-to-hash-table' for table format), so ensure that TABLE is
|
`chronometrist-to-hash-table' for table format), so ensure that TABLE is
|
||||||
reduced to the desired range using
|
reduced to the desired range using
|
||||||
`chronometrist-events-subset'."
|
`chronometrist-ht-subset'."
|
||||||
(cl-loop for task in (chronometrist-task-list) collect
|
(cl-loop for task in (chronometrist-task-list) collect
|
||||||
(let* ((active-days (chronometrist-statistics-count-active-days task table))
|
(let* ((active-days (chronometrist-count-active-days task table))
|
||||||
(active-percent (cl-case (plist-get chronometrist-statistics--ui-state :mode)
|
(active-percent (cl-case (plist-get chronometrist-statistics--ui-state :mode)
|
||||||
('week (* 100 (/ active-days 7.0)))))
|
('week (* 100 (/ active-days 7.0)))))
|
||||||
(active-percent (if (zerop active-days)
|
(active-percent (if (zerop active-days)
|
||||||
|
@ -4131,12 +4160,12 @@ reduced to the desired range using
|
||||||
('week
|
('week
|
||||||
(let* ((start (plist-get chronometrist-statistics--ui-state :start))
|
(let* ((start (plist-get chronometrist-statistics--ui-state :start))
|
||||||
(end (plist-get chronometrist-statistics--ui-state :end))
|
(end (plist-get chronometrist-statistics--ui-state :end))
|
||||||
(ht (chronometrist-events-subset start end hash-table)))
|
(ht (chronometrist-ht-subset start end hash-table)))
|
||||||
(chronometrist-statistics-rows-internal ht)))
|
(chronometrist-statistics-rows-internal ht)))
|
||||||
(t ;; `chronometrist-statistics--ui-state' is nil, show current week's data
|
(t ;; `chronometrist-statistics--ui-state' is nil, show current week's data
|
||||||
(let* ((start (chronometrist-previous-week-start (chronometrist-date-ts)))
|
(let* ((start (chronometrist-previous-week-start (chronometrist-date-ts)))
|
||||||
(end (ts-adjust 'day 7 start))
|
(end (ts-adjust 'day 7 start))
|
||||||
(ht (chronometrist-events-subset start end hash-table)))
|
(ht (chronometrist-ht-subset start end hash-table)))
|
||||||
(setq chronometrist-statistics--ui-state `(:mode week :start ,start :end ,end))
|
(setq chronometrist-statistics--ui-state `(:mode week :start ,start :end ,end))
|
||||||
(chronometrist-statistics-rows-internal ht))))))
|
(chronometrist-statistics-rows-internal ht))))))
|
||||||
#+END_SRC
|
#+END_SRC
|
||||||
|
@ -4560,7 +4589,7 @@ range.")
|
||||||
#+END_SRC
|
#+END_SRC
|
||||||
|
|
||||||
**** intervals-for-range :reader:
|
**** intervals-for-range :reader:
|
||||||
This is basically like [[#program-data-structures-events-subset][chronometrist-events-subset]], but returns a list instead of a hash table. Might replace one with the other in the future.
|
This is basically like [[#program-data-structures-ht-subset][chronometrist-ht-subset]], but returns a list instead of a hash table. Might replace one with the other in the future.
|
||||||
|
|
||||||
#+BEGIN_SRC emacs-lisp
|
#+BEGIN_SRC emacs-lisp
|
||||||
(defun chronometrist-details-intervals-for-range (range table)
|
(defun chronometrist-details-intervals-for-range (range table)
|
||||||
|
|
|
@ -233,6 +233,27 @@ BACKEND-VAR is bound to each backend in
|
||||||
(should (seq-every-p #'stringp task-list))))
|
(should (seq-every-p #'stringp task-list))))
|
||||||
#+END_SRC
|
#+END_SRC
|
||||||
|
|
||||||
|
*** date-properties
|
||||||
|
:PROPERTIES:
|
||||||
|
:CUSTOM_ID: date-properties
|
||||||
|
:END:
|
||||||
|
#+BEGIN_SRC emacs-lisp
|
||||||
|
(ert-deftest chronometrist-date-properties ()
|
||||||
|
(should
|
||||||
|
(not (chronometrist-date-properties
|
||||||
|
'((:name "Programming" :start "2022-02-12T06:54:01+0530" :stop "2022-02-12T07:41:33+0530")
|
||||||
|
(:name "OSM" :start "2022-02-12T21:43:50+0530" :stop "2022-02-12T22:14:38+0530")
|
||||||
|
(:name "Programming" :start "2022-02-12T23:44:37+0530" :stop "2022-02-13T00:00:00+0530")))))
|
||||||
|
(should
|
||||||
|
(equal
|
||||||
|
(chronometrist-date-properties
|
||||||
|
'(:foo 1 :bar "2" :baz (frob)
|
||||||
|
(:name "Programming" :start "2022-02-12T06:54:01+0530" :stop "2022-02-12T07:41:33+0530")
|
||||||
|
(:name "OSM" :start "2022-02-12T21:43:50+0530" :stop "2022-02-12T22:14:38+0530")
|
||||||
|
(:name "Programming" :start "2022-02-12T23:44:37+0530" :stop "2022-02-13T00:00:00+0530")))
|
||||||
|
'(:foo 1 :bar "2" :baz (frob)))))
|
||||||
|
#+END_SRC
|
||||||
|
|
||||||
** time functions
|
** time functions
|
||||||
*** format-duration-long :pure:
|
*** format-duration-long :pure:
|
||||||
#+BEGIN_SRC emacs-lisp
|
#+BEGIN_SRC emacs-lisp
|
||||||
|
|
Reference in New Issue