Implement in-file fixing of midnight-spanning events

This commit is contained in:
contrapunctus 2019-09-07 01:06:33 +05:30
parent e6f745039b
commit 05af9d05ff
4 changed files with 100 additions and 30 deletions

View File

@ -14,6 +14,11 @@
This is not guaranteed to be accurate - see (info \"(elisp)Timers\").")
(defcustom chronometrist-day-start-time "00:00:00"
"The time at which a day is considered to start, in \"HH:MM:SS\".
The default is midnight, i.e. \"00:00:00\".")
(provide 'chronometrist-custom)
;; Local Variables:

View File

@ -1,14 +1,17 @@
;;; chronometrist-events.el --- Event management and querying code for Chronometrist -*- lexical-binding: t; -*-
(require 'plist-pp)
(require 'subr-x)
;;; Commentary:
;;
(require 'subr-x)
;;; Code:
(defvar chronometrist-events (make-hash-table :test #'equal))
;; As we're no longer dealing with vectors, these are deprecated, and
;; will be removed once the rest of the codebase is migrated.
(defun chronometrist-vfirst (vector)
"Return the first element of VECTOR."
(elt vector 0))
@ -26,38 +29,59 @@
chronometrist-events)
dates))
(defun chronometrist-events-clean ()
"Clean `chronometrist-events' so that events can be processed accurately.
(defun chronometrist-day-start (timestamp)
"Get start of day (according to `chronometrist-day-start-time') for TIMESTAMP.
TIMESTAMP must be a time string in the ISO-8601 format.
Return value is a time value (see `current-time')."
(let* ((timestamp-date (->> timestamp
(parse-iso8601-time-string)
(decode-time)
(-drop 3)
(-take 3))))
(--> chronometrist-day-start-time
(split-string it ":")
(mapcar #'string-to-number it)
(reverse it)
(append it timestamp-date)
(apply #'encode-time it))))
(defun chronometrist-file-clean ()
"Clean `chronometrist-file' so that events can be processed accurately.
This function splits midnight-spanning intervals into two. It
must be called after `chronometrist-populate'.
must be called before running `chronometrist-populate'.
It returns t if the table was modified, else nil."
;; for each key-value, see if the first event has an "o" code
(let (prev-date modified)
(maphash (lambda (key value)
(when (-> value (chronometrist-vfirst) (chronometrist-vfirst) (equal "o"))
;; Add new "o" event on previous date with 24:00:00
;; as end time, reusing the ending reason.
;; Add new "i" event on current date with 00:00:00
;; as start time, with the same project.
(let* ((reason (->> value (chronometrist-vfirst) (chronometrist-vlast)))
(prev-events (gethash prev-date chronometrist-events))
(prev-event (chronometrist-vlast prev-events))
(o-event (vconcat ["o"] prev-date `[24 0 0 ,reason]))
(current-event (chronometrist-vfirst value))
(project (chronometrist-vlast prev-event))
(i-event (vconcat ["i"] key `[0 0 0 ,project])))
(--> prev-events
(vconcat it (vector o-event))
(puthash prev-date it chronometrist-events))
(--> (vconcat (vector i-event) value)
(puthash key it chronometrist-events))
(setq modified t)))
(setq prev-date key)) ; this assumes that the first event of the first date doesn't
; have an "o" code (which a correct file shouldn't)
chronometrist-events)
(let ((buffer (find-file-noselect chronometrist-file))
modified
expr)
(with-current-buffer (find-file-noselect chronometrist-file)
(save-excursion
(goto-char (point-min))
(while (setq expr (ignore-errors (read (current-buffer))))
(let ((split-time (chronometrist-events-midnight-spanning-p (plist-get expr :start)
(plist-get expr :stop))))
(when split-time
(let ((first-start (plist-get (first split-time) :start))
(first-stop (plist-get (first split-time) :stop))
(second-start (plist-get (second split-time) :start))
(second-stop (plist-get (second split-time) :stop)))
(backward-list 1)
(chronometrist-delete-list)
(-> expr
(plist-put :start first-start)
(plist-put :stop first-stop)
(plist-pp buffer))
(when (looking-at-p "\n\n")
(delete-char 2))
(-> expr
(plist-put :start second-start)
(plist-put :stop second-stop)
(plist-pp buffer))
(setq modified t))))))
(save-buffer))
modified))
;; TODO - Maybe strip dates from values, since they're part of the key
@ -88,6 +112,7 @@ This function always returns nil."
(puthash index expression chronometrist-events)))
nil)))
;; to be replaced by plist-query
(defun chronometrist-events-subset (start-date end-date)
"Return a subset of `chronometrist-events'.

View File

@ -38,6 +38,35 @@ NUMBER should be an integer (0-6) - see
(car
(rassoc number chronometrist-report-weekday-number-alist)))
(defun chronometrist-unix-time->iso8601 (unix-time)
"Return UNIX-TIME as an ISO-8601 format time string.
UNIX-TIME must be a time value (see `current-time') accepted by
`format-time-string'."
(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.
(defun chronometrist-events-midnight-spanning-p (start-time stop-time)
"Return non-nil if START-TIME and STOP-TIME cross a midnight.
Return value is a list in the form
\((:start START-TIME
:stop <day-start time on initial day>)
(:start <day start time on second day>
:stop STOP-TIME))"
;; FIXME - time zones are ignored; may cause issues with
;; time-zone-spanning events
(let* ((first-day-start (chronometrist-day-start start-time))
(next-day-start (time-add first-day-start
'(0 . 86400)))
(stop-time-unix (parse-iso8601-time-string stop-time)))
(when (time-less-p next-day-start stop-time-unix)
(list `(:start ,start-time
:stop ,(chronometrist-unix-time->iso8601 first-day-start))
`(:start ,(chronometrist-unix-time->iso8601 next-day-start)
:stop ,stop-time)))))
;; Local Variables:
;; nameless-current-name: "chronometrist"
;; End:

View File

@ -48,3 +48,14 @@ Implementation of the new format has begun.
Of course, :production and :individual may not necessarily make sense outside the context of these particular tags. Therefore, this will require supporting arbitary plist keys.
* Which, in turn, will complicate the UI.
* Also, presented to the user, the keys will become "fields"...which won't be able to have spaces in them, unless we want to have keywords with escaped spaces :\
## Midnight-spanning events
Not sure how to deal with these. Previously, we checked if the first event for a day had an "o" code. Some possibilities -
1. Split them at the file level
* Advantage - operation is performed only once for each such event (no repeated work) + benefits of both simplified data-consuming code and reduced post-parsing load.
2. Split them at the hash-table-level (i.e. rewrite chronometrist-events-clean)
* Advantage - simplifies data-consuming code.
3. Split them at the data-consumer level (e.g. before calculating time for one day or getting events for one day)
* Advantage - should reduce repetitive post-parsing load.
They are an issue not only in calculating time spent in $TIME_RANGE, but also acquiring the events for a day.