Compare commits

...
This repository has been archived on 2022-05-13. You can view files and clone it, but cannot push or open issues or pull requests.

14 Commits

Author SHA1 Message Date
contrapunctus 6fb599104b Redesign API; revert sexp-read to earlier behaviour
sexp-read goes back to not splitting events, not even when they cross
the provided timestamps. Splitting events is left to data consuming
functions via events-maybe-split, since not all of them need it. (e.g.
history generation for tags/keys/values)

Created
filter-events-time
2020-05-31 03:23:54 +05:30
contrapunctus e9a9209067 Modify chronometrist-sexp-read to split day-crossing events 2020-05-31 00:09:43 +05:30
contrapunctus 39e2a04a25 Change key-values to use chronometrist-sexp-read
A lot of now-unnecessary reversing was removed.
2020-05-29 16:06:04 +05:30
contrapunctus cec9475a57 Change sexp-read to split events exceeding the given range
Create
ts->iso
filter-task-events

Update
events-maybe-split to use ts.el
active-time-one-day to use ts.el
day-start to use ts.el
midnight-spanning-p to use ts.el
statistics-count-average-time-spent to use filter-events-task

Move
day-start from events.el to time.el
midnight-spanning-p from time.el to events.el

Remove
task-events-in-day (repalced by filter-events-task)
2020-05-29 10:54:46 +05:30
contrapunctus 2bfa99d0d8 Rename sexp-between -> sexp-read, return all events by default 2020-05-28 08:47:17 +05:30
contrapunctus 812b664074 Reindent ecukes feature definition 2020-05-28 01:46:02 +05:30
contrapunctus 5c0add84db Merge branch 'dev' into parsimonious-reading 2020-05-25 03:06:16 +05:30
contrapunctus fc050dea71 Merge branch 'dev' into parsimonious-reading 2020-05-24 02:13:19 +05:30
contrapunctus 56c3c2b325 Merge branch 'dev' into parsimonious-reading 2020-05-24 00:01:39 +05:30
contrapunctus 98107b1e36 Remove reference to removed local variable
This was causing chronometrist-sexp-last to return nil, and the
ignore-errors was, well, ignoring-errors, resulting in all sorts of
mysterious bugs.

Added a test to prevent it from happening again.
2020-05-23 20:30:53 +05:30
contrapunctus 7fac851f37 Merge branch 'dev' into parsimonious-reading 2020-05-22 01:16:27 +05:30
contrapunctus 4edb213326 chronometrist-sexp-query-till -> chronometrist-sexp-between
Implement correct behaviour (removing
chronometrist-common-plist-date-match-p), add tests, add to reference.
2020-05-21 13:37:57 +05:30
contrapunctus 422e60c621 Merge branch 'dev' into parsimonious-reading 2020-05-21 09:20:08 +05:30
contrapunctus 321b8e07e4 Create function sexp-query-till
And consequently -
common-plist-date-match-p
timestamp-less-p
timestamp->iso-date.
2020-05-16 12:13:59 +05:30
13 changed files with 336 additions and 223 deletions

View File

@ -6,6 +6,6 @@
("cs" . "chronometrist-statistics")
("cd" . "chronometrist-diary")
("cx" . "chronometrist-sexp")))
(outline-regexp . ";;;+ ")))
(outline-regexp . ";;;; #+ ")))
(dired-mode . ((dired-omit-mode . t)
(dired-omit-extensions . (".html" ".texi")))))

View File

@ -138,10 +138,10 @@
*** chronometrist-events.el
1. Variable - chronometrist-events
* keys - iso-date
2. Function - chronometrist-day-start (timestamp)
* iso-timestamp -> encode-time
3. Function - chronometrist-file-clean ()
2. Function - chronometrist-file-clean ()
* commented out, unused
3. Function - chronometrist-midnight-spanning-p (start-ts stop-ts)
* ts ts -> ((iso . iso) (iso . iso))
4. Function - chronometrist-events-maybe-split (event)
5. Function - chronometrist-events-populate ()
6. Function - chronometrist-tasks-from-table ()
@ -163,14 +163,16 @@
6. Function - chronometrist-plist-pp-to-string (object)
7. Function - chronometrist-plist-pp (object &optional stream)
*** chronometrist-queries.el
1. Function - chronometrist-last ()
1. Function - chronometrist-sexp-query-till (&optional (ts (chronometrist-date)))
* &optional ts -> events
2. Function - chronometrist-last ()
* -> plist
2. Function - chronometrist-task-time-one-day (task &optional (ts (ts-now)))
3. Function - chronometrist-task-time-one-day (task &optional (ts (ts-now)))
* String &optional ts -> seconds
3. Function - chronometrist-active-time-one-day (&optional ts)
4. Function - chronometrist-active-time-one-day (&optional (ts (chronometrist-date)))
* &optional ts -> seconds
4. Function - chronometrist-statistics-count-active-days (task &optional (table chronometrist-events))
5. Function - chronometrist-task-events-in-day (task ts)
5. Function - chronometrist-statistics-count-active-days (task &optional (table chronometrist-events))
6. Function - chronometrist-filter-events-task (events task)
*** chronometrist-report-custom.el
1. Custom variable - chronometrist-report-buffer-name
2. Custom variable - chronometrist-report-week-start-day
@ -240,17 +242,20 @@
12. Command - chronometrist-statistics-previous-range (arg)
13. Command - chronometrist-statistics-next-range (arg)
*** chronometrist-time.el
1. Function - chronometrist-iso-timestamp->ts (timestamp)
1. Function - chronometrist-ts->iso (ts)
* ts -> iso-timestamp
2. Function - chronometrist-iso-timestamp->ts (timestamp)
* iso-timestamp -> ts
2. Function - chronometrist-iso-date->ts (date)
3. Function - chronometrist-iso-date->ts (date)
* iso-date -> ts
3. Function - chronometrist-date (&optional (ts (ts-now)))
4. Function - chronometrist-date (&optional (ts (ts-now)))
* &optional ts -> ts (with time 00:00:00)
4. Function - chronometrist-format-time-iso8601 (&optional unix-time)
5. Function - chronometrist-midnight-spanning-p (start-time stop-time)
6. Function - chronometrist-seconds-to-hms (seconds)
5. Function - chronometrist-format-time-iso8601 (&optional unix-time)
6. Function - chronometrist-day-start (ts)
* ts -> ts
7. Function - chronometrist-seconds-to-hms (seconds)
* seconds -> list-duration
7. Function - chronometrist-interval (event)
8. Function - chronometrist-interval (event)
* event -> duration
*** chronometrist-timer.el
1. Internal Variable - chronometrist--timer-object
@ -283,7 +288,7 @@
*** chronometrist-sexp
1. Macro - chronometrist-sexp-in-file (file &rest body)
2. Function - chronometrist-sexp-open-log ()
3. Function - chronometrist-sexp-between (&optional (ts-beg (chronometrist-date)) (ts-end (ts-adjust 'day +1 (chronometrist-date))))
3. Function - chronometrist-sexp-read (&optional ts-beg ts-end)
4. Function - chronometrist-sexp-query-till (&optional (date (chronometrist-date)))
5. Function - chronometrist-sexp-last ()
* -> plist

View File

@ -23,9 +23,9 @@
(require 'chronometrist-time)
(require 'chronometrist-sexp)
;; ## VARIABLES ##
;;; Code:
;;;; Variables
(defvar chronometrist-empty-time-string "-")
(defvar chronometrist-date-re "[0-9]\\{4\\}/[0-9]\\{2\\}/[0-9]\\{2\\}")
@ -51,6 +51,7 @@ must correspond to the output from `chronometrist-format-time'.")
Used to prevent more than one watch being added for the same
file.")
;;;; Functions
(defun chronometrist-current-task ()
"Return the name of the currently clocked-in task, or nil if not clocked in."
(chronometrist-sexp-current-task))

View File

@ -1,4 +1,4 @@
;;; chronometrist-events.el --- Event management and querying code for Chronometrist -*- lexical-binding: t; -*-
;;; chronometrist-events.el --- event plist functions for Chronometrist -*- lexical-binding: t; -*-
;; Author: contrapunctus <xmpp:contrapunctus@jabber.fr>
@ -33,24 +33,6 @@ Values are lists containing events, where each event is a list in
the form (:name \"NAME\" :tags (TAGS) <key value pairs> ...
:start TIME :stop TIME).")
(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-list (->> 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-list)
(apply #'encode-time it))))
;; (defun chronometrist-file-clean ()
;; "Clean `chronometrist-file' so that events can be processed accurately.
;; NOTE - currently unused.
@ -90,28 +72,47 @@ Return value is a time value (see `current-time')."
;; (save-buffer)))
;; modified))
;; Note - this assumes that an event never crosses >1 day. This seems
;; sufficient for all conceivable cases.
(defun chronometrist-midnight-spanning-p (start-ts stop-ts)
"Return non-nil if START-TS and STOP-TS cross a midnight.
START-TS and STOP-TS should be ts structs (see `ts.el')
Return two events in the form
\((:start START
:stop <`chronometrist-day-start' on second day>)
(:start <`chronometrist-day-start' on second day>
:stop STOP))
Where START and STOP are ISO-8601 timestamps."
;; The time on which the first provided day starts (according to `chronometrist-day-start-time')
(let* ((first-day-start (chronometrist-day-start start-ts))
(next-day-start (ts-adjust 'day 1 first-day-start)))
;; Does the event stop time exceed the next day start time?
(when (ts< next-day-start stop-ts)
(list (cons (chronometrist-ts->iso start-ts)
(chronometrist-ts->iso next-day-start))
(cons (chronometrist-ts->iso next-day-start)
(chronometrist-ts->iso stop-ts))))))
(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 (plist-get event :start)
(plist-get event :stop))))
(when split-time
(let ((first-start (plist-get (cl-first split-time) :start))
(first-stop (plist-get (cl-first split-time) :stop))
(second-start (plist-get (cl-second split-time) :start))
(second-stop (plist-get (cl-second split-time) :stop))
;; 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 first-start)
(plist-put :stop first-stop))
(-> event-2
(plist-put :start second-start)
(plist-put :stop second-stop))))))))
Return a list of two events if EVENT was split, else nil"
(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.
@ -141,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).

View File

@ -22,12 +22,18 @@
;; For more information, please refer to <https://unlicense.org>
;;; Commentary:
;;
;; TODO - the pattern of setting up `chronometrist-history-suggestion-limit' is
;; repeated thrice in the `*-populate' functions. Make a
;; `chronometrist-with-partial-history' macro for it.
;;; Code:
(require 'chronometrist-sexp)
(defcustom chronometrist-history-suggestion-limit 60
"Maximum number of days (from today) to use for tag/key/value history."
:group 'chronometrist-key-values)
(defvar chronometrist--tag-suggestions nil
"Suggestions for tags.
Used as history by `chronometrist-tags-prompt'.")
@ -106,30 +112,35 @@ as symbol and/or strings.")
(defun chronometrist-tags-history-populate ()
"Add keys and values to `chronometrist-tags-history' by querying `chronometrist-events'."
(let ((table chronometrist-tags-history))
(let ((table chronometrist-tags-history)
(events (if (integerp chronometrist-history-suggestion-limit)
(--> (abs chronometrist-history-suggestion-limit)
(- it)
(ts-adjust 'day it (ts-now))
(chronometrist-sexp-read it (ts-now)))
(chronometrist-sexp-read))))
(clrhash table)
(cl-loop for plist in (chronometrist-events-query chronometrist-events :get '(:name :tags))
do (let* ((name (plist-get plist :name))
(tags (plist-get plist :tags))
(existing-tags (gethash name table)))
(when tags
(puthash name
(if existing-tags
(append existing-tags `(,tags))
`(,tags))
table))))
(cl-loop
for event in events do
(let* ((name (plist-get event :name))
(tags (plist-get event :tags))
(existing-tags (gethash name table)))
(when tags
(puthash name
(if existing-tags
(append existing-tags `(,tags))
`(,tags))
table))))
;; (message "%s" table)
;; We can't use `chronometrist-ht-history-prep' to do this, because it uses
;; `-flatten'; the values of `chronometrist-tags-history' hold tag combinations
;; (as lists), not individual tags.
(cl-loop for task being the hash-keys of table
using (hash-values tag-lists)
do (puthash task
;; Because remove-duplicates keeps the _last_
;; occurrence, trying to avoid this `reverse' by
;; switching the args in the call to `append'
;; above will not get you the correct behavior!
(-> (cl-remove-duplicates tag-lists :test #'equal)
(reverse))
(cl-remove-duplicates tag-lists
:test #'equal
:from-end t)
table))))
(defun chronometrist-tags-history-combination-strings (task)
@ -154,7 +165,9 @@ This is used to provide completion for individual tags, in
`completing-read-multiple' in `chronometrist-tags-prompt'."
(--> (gethash task chronometrist-tags-history)
(-flatten it)
(cl-remove-duplicates it :test #'equal)
(cl-remove-duplicates it
:test #'equal
:from-end t)
(cl-loop for elt in it
collect (if (stringp elt)
elt
@ -187,9 +200,9 @@ used in `chronometrist-before-out-functions'."
(chronometrist-maybe-string-to-symbol))))
(when input
(--> (append last-tags input)
(reverse it)
(cl-remove-duplicates it :test #'equal)
(reverse it)
(cl-remove-duplicates it
:test #'equal
:from-end t)
(chronometrist-append-to-last it nil)))
t))
@ -219,15 +232,12 @@ values are lists containing values (as strings).")
"Prepare history hash tables for use in prompts.
Each value in hash table TABLE must be a list. Each value will be
reversed and will have duplicate elements removed."
(maphash (lambda (key value)
(puthash key
;; placing `reverse' after `remove-duplicates'
;; to get a list in reverse chronological order
(-> (-flatten value)
(cl-remove-duplicates :test #'equal)
(reverse))
table))
table))
(cl-loop for key being the hash-keys of table
using (hash-values value) do
(puthash key
(-> (-flatten value)
(cl-remove-duplicates :test #'equal :from-end t))
table)))
(defun chronometrist-key-history-populate ()
"Populate `chronometrist-key-history' from `chronometrist-file'.
@ -239,30 +249,32 @@ leading \":\" is removed."
;; add each task as a key
(mapc (lambda (task)
(puthash task nil chronometrist-key-history))
;; ;; Not necessary, if the only place this is called is `chronometrist-refresh-file'
;; (setq chronometrist--task-list (chronometrist-tasks-from-table))
chronometrist-task-list)
(cl-loop
for hv being the hash-values of chronometrist-events do
(cl-loop
for plist in hv do
(let* ((name (plist-get plist :name))
(old-hv (gethash name chronometrist-events))
(keys (->> (chronometrist-plist-remove plist
:name :start
:stop :tags)
(seq-filter #'keywordp))))
(cl-loop
for key in keys do
(when key
(let ((key-string (->> (symbol-name key)
(s-chop-prefix ":")
(list))))
(puthash name
(if old-hv
(append old-hv key-string)
key-string)
chronometrist-key-history)))))))
(let ((events (if (integerp chronometrist-history-suggestion-limit)
(--> (abs chronometrist-history-suggestion-limit)
(- it)
(ts-adjust 'day it (ts-now))
(chronometrist-sexp-read it (ts-now)))
(chronometrist-sexp-read))))
(cl-loop
for event in events do
(let* ((name (plist-get event :name))
(old-hv (gethash name chronometrist-key-history))
(keys (->> (chronometrist-plist-remove event
:name :start
:stop :tags)
(seq-filter #'keywordp))))
(cl-loop
for key in keys
when key do
(let ((key-string (->> (symbol-name key)
(s-chop-prefix ":")
(list))))
(puthash name
(if old-hv
(append old-hv key-string)
key-string)
chronometrist-key-history))))))
(chronometrist-ht-history-prep chronometrist-key-history))
(defun chronometrist-value-history-populate ()
@ -270,32 +282,35 @@ leading \":\" is removed."
The values are stored in `chronometrist-value-history'."
;; Note - while keys are Lisp keywords, values may be any Lisp
;; object, including lists
(let ((table chronometrist-value-history)
user-kvs)
(let ((table chronometrist-value-history)
(events (if (integerp chronometrist-history-suggestion-limit)
(chronometrist-sexp-read (ts-adjust 'day (- (abs chronometrist-history-suggestion-limit))
(ts-now))
(ts-now))
(chronometrist-sexp-read))))
(clrhash table)
(cl-loop
for plist-list being the hash-values of chronometrist-events do
with user-kvs
for event in events do
;; We call them user-kvs because we filter out Chronometrist's
;; reserved key-values
(setq user-kvs (chronometrist-plist-remove event
:name :tags
:start :stop))
(cl-loop
for plist in plist-list do
;; We call them user-kvs because we filter out Chronometrist's
;; reserved key-values
(setq user-kvs (chronometrist-plist-remove plist
:name :tags
:start :stop))
(cl-loop
for (key1 val1) on user-kvs by #'cddr do
(let* ((key1-string (->> (symbol-name key1)
(s-chop-prefix ":")))
(key1-ht (gethash key1-string table))
(val1 (if (not (stringp val1))
(list
(format "%s" val1))
(list val1))))
(puthash key1-string
(if key1-ht
(append key1-ht val1)
val1)
table)))))
for (key1 val1) on user-kvs by #'cddr do
(let* ((key1-string (->> (symbol-name key1)
(s-chop-prefix ":")))
(key1-ht (gethash key1-string table))
(val1 (if (not (stringp val1))
(list
(format "%s" val1))
(list val1))))
(puthash key1-string
(if key1-ht
(append key1-ht val1)
val1)
table))))
(chronometrist-ht-history-prep table)))
(defvar chronometrist-kv-read-mode-map

View File

@ -24,29 +24,37 @@
"Return the last entry from `chronometrist-file' as a plist."
(chronometrist-sexp-last))
(cl-defun chronometrist-task-time-one-day (task &optional (ts (ts-now)))
(cl-defun chronometrist-task-time-one-day (task &optional (ts (chronometrist-date)))
"Return total time spent on TASK today or (if supplied) on timestamp TS.
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 ((task-events (chronometrist-task-events-in-day task 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)))
(defun chronometrist-active-time-one-day (&optional ts)
(cl-defun chronometrist-active-time-one-day (&optional (ts (chronometrist-date)))
"Return the total active time on TS (if non-nil) or today.
TS must be a ts struct (see `ts.el')
Return value is seconds as an integer."
(->> chronometrist-task-list
(--map (chronometrist-task-time-one-day it ts))
TS must be a ts struct (see `ts.el')."
(->> (--map (chronometrist-task-time-one-day it ts) chronometrist-task-list)
(-reduce #'+)
(truncate)))
@ -65,22 +73,24 @@ which span midnights. (see `chronometrist-events-clean')"
table)
count))
(defun chronometrist-task-events-in-day (task ts)
"Get events for TASK on TS.
TS should be a ts struct (see `ts.el').
Returns a list of events, where each event is a property list in
the form (:name \"NAME\" :start START :stop STOP ...), where
START and STOP are ISO-8601 time strings.
This will not return correct results if TABLE contains records
which span midnights. (see `chronometrist-events-clean')"
(->> (gethash (ts-format "%F" ts) chronometrist-events)
(mapcar (lambda (event)
(when (equal task (plist-get event :name))
event)))
(seq-filter #'identity)))
(defun chronometrist-filter-events-task (events task)
"Return events for TASK from EVENTS.
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 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)

View File

@ -1,10 +1,10 @@
;;; chronometrist-sexp.el --- s-expression backend for Chronometrist -*- lexical-binding: t; -*-
;;; Commentary:
;;
(require 'chronometrist-custom)
(require 'chronometrist-plist-pp)
(require 'ts)
;;; Code:
@ -13,7 +13,8 @@
;; chronometrist-plist-pp (-plist-pp)
(defmacro chronometrist-sexp-in-file (file &rest body)
(declare (indent defun))
"Run BODY in a buffer visiting FILE, restoring point afterwards."
(declare (indent defun) (debug t))
`(with-current-buffer (find-file-noselect ,file)
(save-excursion ,@body)))
@ -23,13 +24,62 @@
(find-file-other-window chronometrist-file)
(goto-char (point-max)))
(cl-defun chronometrist-sexp-read (&optional ts-beg ts-end)
"Return events between TS-BEG and TS-END.
Events are a list of plists, in reverse chronological order.
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 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'."
(chronometrist-sexp-in-file chronometrist-file
(goto-char (point-max))
(backward-list)
(ignore-errors
(read buffer))))
(ignore-errors (read (current-buffer)))))
(defun chronometrist-sexp-current-task ()
"Return the name of the currently clocked-in task, or nil if not clocked in."
@ -88,7 +138,7 @@ BUFFER is the buffer to operate in - default is one accessing `chronometrist-fil
;; newline before it
(unless (bobp) (insert "\n"))
(unless (bolp) (insert "\n"))
(chronometrist-plist-pp plist buffer)
(chronometrist-plist-pp plist (current-buffer))
(save-buffer)))
(defun chronometrist-sexp-delete-list (&optional arg)
@ -105,7 +155,7 @@ BUFFER is the buffer to operate in - default is one accessing `chronometrist-fil
(insert "\n"))
(backward-list 1)
(chronometrist-sexp-delete-list)
(chronometrist-plist-pp plist buffer)
(chronometrist-plist-pp plist (current-buffer))
(save-buffer)))
(defun chronometrist-sexp-reindent-buffer ()

View File

@ -95,10 +95,13 @@ TABLE should be a hash table - if not supplied,
(let ((days 0)
(per-day-time-list))
(maphash (lambda (key _value)
(let ((events-in-day (chronometrist-task-events-in-day task (chronometrist-iso-date->ts key))))
(when events-in-day
(let* ((ts (chronometrist-iso-date->ts key))
(events-in-day (chronometrist-sexp-read ts (ts-adjust 'day 1 ts)))
(task-events (when events-in-day
(chronometrist-filter-events-task events-in-day task))))
(when task-events
(setq days (1+ days))
(->> (chronometrist-events->ts-pairs events-in-day)
(->> (chronometrist-events->ts-pairs task-events)
(chronometrist-ts-pairs->durations)
(-reduce #'+)
(list)

View File

@ -25,6 +25,9 @@
;;; Code:
(defun chronometrist-ts->iso (ts)
(ts-format "%FT%T%z" ts))
(defun chronometrist-iso-timestamp->ts (timestamp)
"Return new ts struct, parsing TIMESTAMP with `parse-iso8601-time-string'."
(-let [(second minute hour day month year dow _dst utcoff)
@ -60,34 +63,13 @@ Optional argument UNIX-TIME should 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-midnight-spanning-p (start-time stop-time)
"Return non-nil if START-TIME and STOP-TIME cross a midnight.
(defun chronometrist-day-start (ts)
"Return the timestamp for `chronometrist-day-start-time' on the date represented by TS.
TS must be a ts struct (see `ts.el').
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
;; The time on which the first provided day starts (according to `chronometrist-day-start-time')
(let* ((first-day-start (chronometrist-day-start start-time))
;; HACK - won't work with custom day-start time
;; (first-day-end (parse-iso8601-time-string
;; (concat (chronometrist-date (parse-iso8601-time-string start-time))
;; "24:00:00")))
(next-day-start (time-add first-day-start
'(0 . 86400)))
(stop-time-unix (parse-iso8601-time-string stop-time)))
;; Does the event stop time exceed the next day start time?
(when (time-less-p next-day-start stop-time-unix)
(list `(:start ,start-time
:stop ,(chronometrist-format-time-iso8601 next-day-start))
`(:start ,(chronometrist-format-time-iso8601 next-day-start)
:stop ,stop-time)))))
Return value is a TS struct."
(-let [(h m s) (mapcar #'string-to-number (split-string chronometrist-day-start-time ":"))]
(ts-apply :hour h :minute m :second s ts)))
(defun chronometrist-seconds-to-hms (seconds)
"Convert SECONDS to a vector in the form [HOURS MINUTES SECONDS].

View File

@ -174,11 +174,10 @@ If FIRSTONLY is non-nil, return only the first keybinding found."
;; chronometrist-mode-map))
(keybind-toggle (chronometrist-format-keybinds 'chronometrist-toggle-task chronometrist-mode-map t)))
(goto-char (point-max))
(-->
(chronometrist-active-time-one-day)
(chronometrist-format-time it)
(format "%s%- 26s%s" w "Total" it)
(insert it))
(--> (chronometrist-active-time-one-day)
(chronometrist-format-time it)
(format "%s%- 26s%s" w "Total" it)
(insert it))
(insert "\n")
(insert w (format "% 17s" "Keys") w (format "% 17s" "----"))
(chronometrist-print-keybind 'chronometrist-add-new-task)
@ -222,8 +221,13 @@ Argument _FS-EVENT is ignored."
;; (chronometrist-file-clean)
(run-hooks 'chronometrist-file-change-hook)
;; REVIEW - can we move most/all of this to the `chronometrist-file-change-hook'?
(chronometrist-events-populate)
(setq chronometrist-task-list (chronometrist-tasks-from-table))
;; (chronometrist-events-populate)
(setq chronometrist-task-list
(cl-loop for event in (chronometrist-sexp-read)
collect (plist-get event :name) into names
finally return
(cl-remove-duplicates (sort names #'string-lessp)
:test #'equal)))
(chronometrist-tags-history-populate)
(chronometrist-key-history-populate)
(chronometrist-value-history-populate)

View File

@ -3,29 +3,29 @@ Feature: Point restore in Chronometrist
# What about when we want point to be on the last project instead? In which situations should that happen?
Background:
Given I open Chronometrist
And I go to a random point in the buffer
Given I open Chronometrist
And I go to a random point in the buffer
Scenario: Simple re-open 1
When I kill its buffer
And I open it again
Then the position of point should be preserved
Scenario: Simple re-open 2
When I toggle its buffer
And I open it again
Then the position of point should be preserved
Scenario: Simple re-open 2
When I toggle its buffer
And I open it again
Then the position of point should be preserved
Scenario: Timer with buffer current
When buffer is current
Then the timer should preserve the position of point
Scenario: Timer with buffer current
When buffer is current
Then the timer should preserve the position of point
Scenario: Timer with buffer not current, but visible
When buffer is not current
But it is visible in another window
Then the timer should preserve the position of point
Scenario: Timer with buffer not current, but visible
When buffer is not current
But it is visible in another window
Then the timer should preserve the position of point
Scenario: Previous/next week keys
When I open chronometrist-report
And I view the previous/next week
Then the position of point should be preserved
Scenario: Previous/next week keys
When I open chronometrist-report
And I view the previous/next week
Then the position of point should be preserved

View File

@ -0,0 +1,39 @@
;; -*- lexical-binding: t; -*-
(require 'buttercup)
(require 'chronometrist-sexp)
(describe "chronometrist-sexp-last"
:var ((chronometrist-file "tests/test.sexp"))
(it "should return a plist"
(expect (consp (chronometrist-sexp-last))
:to-be t)))
(describe "chronometrist-sexp-read"
(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"
(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 "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-04T01:00:00+0530")
(:name "Programming"
:start "2018-01-02T23:00:00+0530"
:stop "2018-01-03T01:00:00+0530")))))
;; Local Variables:
;; nameless-current-name: "chronometrist"
;; End:

View File

@ -22,6 +22,10 @@
:start "2018-01-02T23:00:00+0530"
:stop "2018-01-03T01:00:00+0530")
(:name "Cooking"
:start "2018-01-03T23:00:00+0530"
:stop "2018-01-04T01:00:00+0530")
(:name "Programming"
:tags (bug-hunting)
:project "Chronometrist"
@ -45,4 +49,4 @@
:tags (reading)
:book "Smalltalk-80: The Language and Its Implementation"
:start "2020-05-10T16:33:17+0530"
:stop "2020-05-10T17:10:48+0530")
:stop "2020-05-10T17:10:48+0530")