Merge branch 'dev'

This commit is contained in:
contrapunctus 2020-06-30 01:16:01 +05:30
commit faeb0a2040
31 changed files with 1357 additions and 1202 deletions

View File

@ -1,9 +1,11 @@
;;; Directory Local Variables
;;; For more information see (info "(emacs) Directory Variables")
((emacs-lisp-mode
(nameless-aliases
("c" . "chronometrist")
("cr" . "chronometrist-report")
("cs" . "chronometrist-statistics")
("cd" . "chronometrist-diary"))))
((emacs-lisp-mode . ((nameless-aliases . (("c" . "chronometrist")
("cr" . "chronometrist-report")
("cs" . "chronometrist-statistics")
("cd" . "chronometrist-diary")
("cx" . "chronometrist-sexp")))
(outline-regexp . ";;;+ ")))
(dired-mode . ((dired-omit-mode . t)
(dired-omit-extensions . (".html" ".texi")))))

View File

@ -4,6 +4,17 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.5.0] 2020-06-30
### Added
* Support for time goals via optional package `chronometrist-targets`.
* New hook - `chronometrist-file-change-hook`
### Changed
* Use [ts.el](https://github.com/alphapapa/ts.el) structs to represent date-time, wherever possible. (`chronometrist-events` and `chronometrist-file` being notable exceptions)
### Fixed
* Prefix arguments now work with the point on a button, too.
* Bug with missing entries in `chronometrist-key-history`
* Operations for adding a new s-expression and replacing the last s-expression have been optimized. Notably, commands for clocking in/out are now significantly faster.
## [0.4.4]
### Fixed
* Error when adding a task for the first time.

4
Cask
View File

@ -8,6 +8,10 @@
(depends-on "dash" "2.16.0")
(depends-on "cl")
(depends-on "s" "1.12.0")
(depends-on "ts" "0.2")
(files "elisp/*.el")
(development
(depends-on "f")

View File

@ -181,7 +181,7 @@ Chronometrist is released under your choice of [Unlicense](https://unlicense.org
(See files [LICENSE](LICENSE) and [LICENSE.1](LICENSE.1)).
## Thanks
wasamasa, bpalmer and #emacs for all their help and support
wasamasa, bpalmer, aidalgol, and the rest of #emacs for their tireless help and support
jwiegley for timeclock.el, which we used as a backend in earlier versions

View File

@ -37,10 +37,6 @@
5. [ ] Expandable items - show intervals for task today
* [ ] Switch between intervals and tag-combination breakdown
6. Revisit 'no reason' commands - maybe we should ask for tags and key-values with the regular commands, and skip them with the 'no reason' variants?
7. Time targets/goals
* User defines targets for tasks
* Chronometrist displays targets column
* Remind user when about to approach target (e.g. -5 minutes)
*** Key-values [66%]
1. [X] bug - value-history appears in chronological rather than reverse chronological order
2. [X] generate history hash table from chronometrist-file.
@ -81,12 +77,14 @@
Observations
* This means you can't enter symbols via prompt. Can be added if the demand is there...
* This also means you can't have multiple atom values for a keyword...but that's irrelevant, because plists can't have multiple values anyway. :)
*** Tags [100%]
1. [X] generate history from chronometrist-file
* [X] narrow it down to the :name
2. [X] write tags to last expression
3. [X] show task name in prompt
4. [X] bug - tags being added twice
** UX
1. don't suggest nil when asking for first project on first run
2. when starting a project with time of "-" (i.e. not worked on today until now), immediately set time to 0 instead of waiting for the first timer refresh
@ -95,34 +93,51 @@
5. mouse-3 should clock-out without asking for reason.
6. Some way to ask for the reason just before starting a project. Even when clocking out, the reason is asked /before/ clocking out, which adds time to the project.
7. Allow calling chronometrist-in/out from anywhere-within-Emacs (a la timeclock) as well as from the chronometrist buffer.
*** Optimization
**** Ideas to make -refresh-file faster
1. Support multiple files, so we read and process lesser data when one of them changes.
2. Make file writing async
3. Don't refresh from file when clocking in.
4. Only write to the file when Emacs is idle or being killed, and store data in memory (in the events hash table) in the meantime
5. What if commands both write to the file /and/ add to the hash table, so we don't have to re-read the file and re-populate the table for commands? The expensive reading+parsing could be avoided for commands, and only take place for the user changing the file.
* jonasw - store length and hash of previous file, see if the new file has the same hash until old-length bytes.
* Rather than storing and hashing the full length, we could do it until (before) the last s-expression (or last N s-expressions?). That way, we know if the last expression (or last N expressions) have changed.
* Or even the first expression of the current date. That way, we just re-read the events for today. Because chronometrist-events uses dates as keys, it's easy to work on the basis of dates.
** Code [25%]
1. refactor repetitive calls to (format "%04d-%02d-%02d" (elt seq a) (elt seq b) (elt seq c))
2. [X] Use buttercup instead of ert
3. See if it is possible to store buttons in a variable, so *-print-non-tabular functions can be made shorter and less imperative. (see ~make-text-button~)
4. Merge all event-querying functions so that they always operate on an entire hash table (so no 'day' variants),
5. [ ] Use ~substitute-command-keys~ instead of ~chronometrist-format-keybinds~
6. [ ] See if using iteration constructs (especially ~loop~) can lead to nicer code than nested maps
** Code [28%]
1. [ ] Write tests (in buttercup)
2. [ ] Rewrite using cl-loop
1. [ ] chronometrist-statistics-count-active-days
2. [ ] chronometrist-statistics-count-average-time-spent
3. refactor repetitive calls to (format "%04d-%02d-%02d" (elt seq a) (elt seq b) (elt seq c))
4. See if it is possible to store buttons in a variable, so *-print-non-tabular functions can be made shorter and less imperative. (see ~make-text-button~)
5. Merge all event-querying functions so that they always operate on an entire hash table (so no 'day' variants),
6. [ ] Use ~substitute-command-keys~ instead of ~chronometrist-format-keybinds~
7. [ ] recreate -events-clean, remove splitting code from -events-populate
* How should we deal with the active event?
* Earlier, we would add a closing entry and update that on a timer.
8. [ ] Make docstrings consistent - describe inputs and then the return value, in that order.
9. [ ] ~chronometrist-seconds->alert-string~ can probably be replaced by ~org-duration-from-minutes~ - read the format for FMT
10. [X] Decouple storage-related code from rest of the program.
11. [X] See if using iteration constructs (especially ~loop~) can lead to nicer code than nested maps
+1. use variables instead of hardcoded numbers to determine spacing+
* Don't see the benefit
+6. Timeclock already _has_ hooks! :| Why do we re-implement them?+
- I don't know of a way to know the project being clocked into using timeclock hooks.
- With v0.2.0 Chronometrist also has a before-project-stop-functions, which runs before the project is stopped, and can control whether the project actually is stopped.
* Optimization
** Cache
+ Lessons from the parsimonious-reading branch - iterating =read= over the whole file is fast; splitting the events is not.
+ Things we need to read the whole file for - task list, tag/key/value history.
+ Fill =chronometrist-events= only as much as the buffer needing split events requires. e.g. for =chronometrist=, just a day; for =chronometrist-report=, a week; etc.
+ Anything requiring split events will first look in =chronometrist-events=, and if not found, will read from the file and update =chronometrist-events=.
+ When the file changes, use the file byte length and hash strategy described below to know whether to keep the cache.
+ Save cache to a file, so that event splitting is avoided by reading from that.
*** Thoughts
+ =chronometrist-key-value-cache= would basically be the entire file, if =chronometrist-history-suggestion-limit= is nil.
+ history generation for tags/keys/values - which involve the most parsing - doesn't actually need the events to be split at midnights. Why not make that a keyword argument to =chronometrist-sexp-read=, so it's faster for that?
** Ideas to make -refresh-file faster
1. Support multiple files, so we read and process lesser data when one of them changes.
2. Make file writing async
3. Don't refresh from file when clocking in.
4. Only write to the file when Emacs is idle or being killed, and store data in memory (in the events hash table) in the meantime
5. What if commands both write to the file /and/ add to the hash table, so we don't have to re-read the file and re-populate the table for commands? The expensive reading+parsing could be avoided for commands, and only take place for the user changing the file.
* [ ] jonasw - store length and hash of previous file, see if the new file has the same hash until old-length bytes.
* Rather than storing and hashing the full length, we could do it until (before) the last s-expression (or last N s-expressions?). That way, we know if the last expression (or last N expressions) have changed.
* Or even the first expression of the current date. That way, we just re-read the events for today. Because chronometrist-events uses dates as keys, it's easy to work on the basis of dates.
6. [ ] Don't generate tag/keyword/value history from the entire log, just from the last N days (where N is user-customizable).
7. [ ] Just why are we reading the whole file? ~chronometrist~ should not read more than a day; ~chronometrist-report~ should not read more than a week at a time, and so on. Make a branch which works on this logic, see if it is faster.
* chronometrist-report
** Features
1. [ ] Expandable items - show tag-combination-based breakdown
@ -136,3 +151,23 @@
* Documentation [0%]
1. [ ] Make Texinfo documentation
* Time targets/goals [50%]
1. [X] User defines targets for tasks
2. [X] Chronometrist displays targets column
3. [-] Notify user when
+ approaching target (e.g. -5 minutes)
+ target completed
+ exceeding target (target+5)
+ [X] More flexible way to define alerts.
+ [ ] If time goals are defined and there is a task without a goal, just remind the user every 15 minutes of the time they've spent on it
- [ ] Needs an even more flexible way to define alerts...
+ [-] Handle manual file changes; on file change -
- [ ] clearing existing notifications
- [X] if last expression has a :stop value, stop alert timers
+ [ ] Also take time spent so far into account (e.g. don't start approach or complete alerts again if time has been exceeded and we're starting the task again despite that)
4. [ ] Colorize times in Chronometrist buffer
- untouched project with target defined - red
- target ±5 minutes - green
- target*2 and above - red
* Backend abstraction [0%]
1. [ ] Add checks to ~chronometrist-sexp-reindent-buffer~ to ensure it only runs on an s-expression buffer

View File

@ -1,232 +0,0 @@
;;; chronometrist-common.el --- Common definitions for Chronometrist -*- lexical-binding: t; -*-
;; Author: contrapunctus <xmpp:contrapunctus@jabber.fr>
;; This is free and unencumbered software released into the public domain.
;;
;; Anyone is free to copy, modify, publish, use, compile, sell, or
;; distribute this software, either in source code form or as a compiled
;; binary, for any purpose, commercial or non-commercial, and by any
;; means.
;;
;; For more information, please refer to <https://unlicense.org>
;;; Commentary:
;;
(require 'dash)
(require 'cl-lib)
(require 'chronometrist-custom)
(require 'chronometrist-report-custom)
(require 'chronometrist-time)
(declare-function chronometrist-last-expr "chronometrist-sexp.el")
;; ## VARIABLES ##
;;; Code:
(defvar chronometrist-empty-time-string "-")
(defvar chronometrist-date-re "[0-9]\\{4\\}/[0-9]\\{2\\}/[0-9]\\{2\\}")
(defvar chronometrist-time-re-ui
(rx-to-string
`(or
(and (repeat 0 2
(optional (repeat 1 2 digit) ":"))
(repeat 1 2 digit))
,chronometrist-empty-time-string))
"Regular expression to represent a timestamp in `chronometrist'.
This is distinct from `chronometrist-time-re-file' (which see) -
`chronometrist-time-re-ui' is meant for the user interface, and
must correspond to the output from `chronometrist-format-time'.")
(defvar chronometrist-task-list nil
"List of tasks in `chronometrist-file', as returned by `chronometrist-tasks-from-table'.")
(defvar chronometrist--fs-watch nil
"Filesystem watch object.
Used to prevent more than one watch being added for the same
file.")
(defun chronometrist-buffer-exists? (buffer-name)
"Return non-nil if BUFFER-NAME exists."
(--> (buffer-list)
(mapcar #'buffer-name it)
(member buffer-name it)))
(defun chronometrist-buffer-visible? (buffer-or-buffer-name)
"Return t if BUFFER-OR-BUFFER-NAME is visible to user."
;; It'd be simpler to use only the windows of the current frame (-->
;; (selected-frame) (window-list it) ...) - but it wouldn't be
;; robust, because it is possible that a frame partially covers
;; another and the buffer is visible to the user from the latter.
(let ((result (-->
(visible-frame-list)
(mapcar #'window-list it)
(mapcar (lambda (list)
(mapcar #'window-buffer list))
it)
(mapcar (lambda (list)
(mapcar (lambda (buffer)
(if (bufferp buffer-or-buffer-name)
(equal buffer-or-buffer-name buffer)
(equal (buffer-name buffer)
buffer-or-buffer-name)))
list))
it)
(mapcar (lambda (list)
(-filter #'identity list))
it)
(mapcar #'car it)
(car it))))
(if result t nil)))
(defun chronometrist-format-time (duration &optional blank)
"Format DURATION as a string suitable for display in Chronometrist buffers.
DURATION must be a vector or a list of the form [HOURS MINUTES
SECONDS] or (HOURS MINUTES SECONDS).
BLANK is a string to display in place of blank values. If not
supplied, 3 spaces are used."
(let ((h (elt duration 0))
(m (elt duration 1))
(s (elt duration 2))
(blank (if blank blank " ")))
(if (and (zerop h) (zerop m) (zerop s))
" -"
(let ((h (if (zerop h)
blank
(format "%2d:" h)))
(m (cond ((and (zerop h)
(zerop m))
blank)
((zerop h)
(format "%2d:" m))
(t
(format "%02d:" m))))
(s (if (and (zerop h)
(zerop m))
(format "%2d" s)
(format "%02d" s))))
(concat h m s)))))
(defun chronometrist-open-file (&optional _button)
"Open `chronometrist-file' in another window.
Argument _BUTTON is for the purpose of using this command as a
button action."
(interactive)
(find-file-other-window chronometrist-file)
(goto-char (point-max)))
(defun chronometrist-common-create-chronometrist-file ()
"Create `chronometrist-file' if it doesn't already exist."
(unless (file-exists-p chronometrist-file)
(with-current-buffer (find-file-noselect chronometrist-file)
(write-file chronometrist-file))))
(defun chronometrist-common-file-empty-p (file)
"Return t if FILE is empty."
(let ((size (elt (file-attributes file) 7)))
(if (zerop size) t nil)))
(defun chronometrist-common-clear-buffer (buffer)
"Clear the contents of BUFFER."
(with-current-buffer buffer
(goto-char (point-min))
(delete-region (point-min) (point-max))))
(defun chronometrist-format-keybinds (command map &optional firstonly)
"Return the keybindings for COMMAND in MAP as a string.
If FIRSTONLY is non-nil, return only the first keybinding found."
(if firstonly
(key-description
(where-is-internal command map firstonly))
(->> (where-is-internal command map)
(mapcar #'key-description)
(-take 2)
(-interpose ", ")
(apply #'concat))))
(defun chronometrist-events->time-list (events)
"Convert EVENTS to a list of time values.
EVENTS must be a list of valid Chronometrist property lists (see
`chronometrist-file').
For each event, a list of two time values is returned.
For time value format, see (info \"(elisp)Time of Day\")."
(let ((index 0)
(length (length events))
result)
(while (not (= index length))
(let* ((elt (elt events index))
(start-iso (parse-iso8601-time-string (plist-get elt :start)))
(stop (plist-get elt :stop))
(stop-iso (if stop
(parse-iso8601-time-string stop)
(current-time))))
(cl-incf index)
(setq result (append result `((,start-iso ,stop-iso))))))
result))
(defun chronometrist-time-list->sum-of-intervals (time-value-lists)
"From a list of start/end timestamps TIME-VALUES, get the total time interval.
TIME-VALUE-LISTS is a list in the form
\((START STOP) ...)
where START and STOP are time values (see (info \"(elisp)Time of Day\")).
This function obtains the intervals between them, and adds the
intervals to return a single time value.
If TIME-VALUES is nil, return '(0 0)."
(if time-value-lists
(->> time-value-lists
(--map (time-subtract (cadr it) (car it)))
(-reduce #'time-add))
'(0 0)))
(defun chronometrist-delete-list (&optional arg)
"Delete ARG lists after point."
(let ((point-1 (point)))
(forward-sexp (or arg 1))
(delete-region point-1 (point))))
(defun chronometrist-previous-week-start (date-string)
"Find the previous `chronometrist-report-week-start-day' from DATE-STRING.
Return the time value of said day's beginning.
If the day of DATE is the same as the
`chronometrist-report-week-start-day', return DATE.
DATE-STRING must be in the form \"YYYY-MM-DD\"."
(let* ((date-time (chronometrist-iso-date->timestamp date-string))
(date-unix (parse-iso8601-time-string date-time))
(date-list (decode-time date-unix))
(day (elt date-list 6)) ;; 0-6, where 0 = Sunday
(week-start (chronometrist-day-of-week->number chronometrist-report-week-start-day))
(gap (cond ((> day week-start) (- day week-start))
((< day week-start) (+ day (- 7 week-start))))))
(if gap
(time-subtract date-unix `(0 ,(* gap 86400)))
date-unix)))
(defun chronometrist-current-task ()
"Return the name of the currently clocked-in task, or nil if not clocked in."
(let ((last-event (chronometrist-last-expr)))
(if (plist-member last-event :stop)
nil
(plist-get last-event :name))))
;; Local Variables:
;; nameless-current-name: "chronometrist-common"
;; End:
(provide 'chronometrist-common)
;;; chronometrist-common.el ends here

View File

@ -1,95 +0,0 @@
;;; chronometrist-queries.el --- Functions which query Chronometrist data -*- lexical-binding: t; -*-
;; Author: contrapunctus <xmpp:contrapunctus@jabber.fr>
;; This is free and unencumbered software released into the public domain.
;;
;; Anyone is free to copy, modify, publish, use, compile, sell, or
;; distribute this software, either in source code form or as a compiled
;; binary, for any purpose, commercial or non-commercial, and by any
;; means.
;;
;; For more information, please refer to <https://unlicense.org>
;;; Commentary:
;;
;;; Code:
(require 'dash)
(require 'chronometrist-common)
(require 'chronometrist-events)
(defun chronometrist-task-time-one-day (task &optional date-string)
"Return total time spent on TASK today or (if supplied) on DATE-STRING.
The data is obtained from `chronometrist-file', via `chronometrist-events'.
DATE-STRING must be in the form \"YYYY-MM-DD\".
The return value is a vector in the form [HOURS MINUTES SECONDS]"
(let* ((date-string (if date-string date-string (chronometrist-date)))
(task-events (chronometrist-task-events-in-day task date-string))
(last-event (cl-copy-list (car (last task-events))))
(reversed-events-tail (-> task-events
(reverse)
(cdr))))
(if task-events
(->> (if (plist-member last-event :stop)
task-events
;; last-event is a currently ongoing task
(-> (plist-put last-event :stop (chronometrist-format-time-iso8601))
(list)
(append reversed-events-tail)
(reverse)))
(chronometrist-events->time-list)
(chronometrist-time-list->sum-of-intervals)
(cadr)
(chronometrist-seconds-to-hms))
;; no events for this task on DATE-STRING i.e. no time spent
[0 0 0])))
(defun chronometrist-active-time-one-day (&optional date-string)
"Return the total active time on DATE (if non-nil) or today.
DATE-STRING must be in the form \"YYYY-MM-DD\".
Return value is a vector in the form [HOURS MINUTES SECONDS]."
(->> chronometrist-task-list
(--map (chronometrist-task-time-one-day it date-string))
(-reduce #'chronometrist-time-add)))
(defun chronometrist-statistics-count-active-days (task &optional table)
"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.
This will not return correct results if TABLE contains records
which span midnights. (see `chronometrist-events-clean')"
(let ((count 0)
(table (if table table chronometrist-events)))
(maphash (lambda (_date events)
(when (seq-find (lambda (event)
(equal (plist-get event :name) task))
events)
(cl-incf count)))
table)
count))
(defun chronometrist-task-events-in-day (task date-string)
"Get events for TASK on DATE-STRING.
DATE-STRING must be in the form \"YYYY-MM-DD\".
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 date-string chronometrist-events)
(mapcar (lambda (event)
(when (equal task (plist-get event :name))
event)))
(seq-filter #'identity)))
(provide 'chronometrist-queries)
;;; chronometrist-queries.el ends here

View File

@ -1,180 +0,0 @@
;;; chronometrist-time.el --- Time and date functions for Chronometrist -*- lexical-binding: t; -*-
;; Author: contrapunctus <xmpp:contrapunctus@jabber.fr>
(require 'parse-time)
(require 'dash)
(require 's)
(require 'chronometrist-report-custom)
(declare-function chronometrist-day-start "chronometrist-events.el")
;; This is free and unencumbered software released into the public domain.
;;
;; Anyone is free to copy, modify, publish, use, compile, sell, or
;; distribute this software, either in source code form or as a compiled
;; binary, for any purpose, commercial or non-commercial, and by any
;; means.
;;
;; For more information, please refer to <https://unlicense.org>
;;; Commentary:
;; Pretty sure quite a few of these are redundant. Hopefully putting
;; them together in the same file will make it easier to figure out
;; which ones those are.
;;; Code:
(defconst chronometrist-seconds-in-day (* 60 60 24)
"Number of seconds in a day.")
(defun chronometrist-date (&optional time)
"Return date for TIME or today, in the form \"YYYY-MM-DD\"."
(s-left 10 (chronometrist-format-time-iso8601 time)))
;; (defun chronometrist-time (&optional time))
(defun chronometrist-day-of-week->number (day-of-week)
"Return an integer (0-6) representing DAY-OF-WEEK.
DAY-OF-WEEK should be a string, e.g. \"Sunday\" - see
`chronometrist-report-weekday-number-alist'."
(cdr
(assoc-string day-of-week chronometrist-report-weekday-number-alist)))
(defun chronometrist-number->day-of-week (number)
"Return the day of the week (as a string), corresponding to NUMBER.
NUMBER should be an integer (0-6) - see
`chronometrist-report-weekday-number-alist'."
(car
(rassoc number chronometrist-report-weekday-number-alist)))
(defun chronometrist-format-time-iso8601 (&optional unix-time)
"Return current moment as an ISO-8601 format time string.
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))
(defun chronometrist-time-interval-span-midnight? (t1 t2)
"Return t if time range T1 to T2 extends across midnight.
T1 and T2 must be lists in the form (YEAR MONTH DAY HOURS MINUTES
SECONDS), as returned by `timestamp->list'. T2 must be
chronologically more recent than T1."
(let* ((day-1 (elt t1 2))
(day-2 (elt t2 2))
(month-1 (elt t1 1))
(month-2 (elt t2 1)))
;; not Absolutely Perfect™, but should do for most situations
(or (= day-2 (1+ day-1))
(= month-2 (1+ month-1)))))
;; 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.
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 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)))))
(defun chronometrist-time->seconds (duration)
"Convert DURATION to seconds.
DURATION must be a vector in the form [HOURS MINUTES SECONDS]."
(-let [[h m s] duration]
(+ (* h 60 60)
(* m 60)
s)))
(defun chronometrist-seconds-to-hms (seconds)
"Convert SECONDS to a vector in the form [HOURS MINUTES SECONDS].
SECONDS must be a positive integer."
(let* ((seconds (truncate seconds))
(s (% seconds 60))
(m (% (/ seconds 60) 60))
(h (/ seconds 3600)))
(vector h m s)))
(defun chronometrist-time-add (a b)
"Add durations A and B and return a vector in the same form.
A and B should be vectors in the form [HOURS MINUTES SECONDS]."
(let ((h1 (elt a 0))
(m1 (elt a 1))
(s1 (elt a 2))
(h2 (elt b 0))
(m2 (elt b 1))
(s2 (elt b 2)))
(chronometrist-seconds-to-hms (+ (* h1 3600) (* h2 3600)
(* m1 60) (* m2 60)
s1 s2))))
(defun chronometrist-iso-date->timestamp (date)
"Convert DATE to a complete timestamp by adding a time part (T00:00:00)."
;; potential problem - time zones are ignored
(concat date "T00:00:00"))
(defun chronometrist-date->time (date)
"Convert DATE to a time value (see (info \"(elisp)Time of Day\")).
DATE must be a list in the form (YEAR MONTH DAY)."
(->> date (reverse) (apply #'encode-time 0 0 0)))
(defun chronometrist-date-less-p (date1 date2)
"Like `time-less-p' but for dates. Return t if DATE1 is less than DATE2.
Both must be dates in the ISO-8601 format."
(time-less-p (-> date1
(chronometrist-iso-date->timestamp)
(parse-iso8601-time-string))
(-> date2
(chronometrist-iso-date->timestamp)
(parse-iso8601-time-string))))
(defun chronometrist-time-less-or-equal-p (t1 t2)
"Return t if T1 is less than or equal to T2.
T1 and T2 should be time values (see `current-time')."
(or (equal t1 t2)
(time-less-p t1 t2)))
(defun chronometrist-calendrical->date (date)
"Convert calendrical information DATE to a date in the form (YEAR MONTH DAY).
For input format, see (info \"(elisp)Time of Day\")."
(-> date (-slice 3 6) (reverse)))
(defun chronometrist-interval (event)
"Return the period of time covered by EVENT as a time value.
EVENT should be a plist (see `chronometrist-file')."
(let ((start (plist-get event :start))
(stop (plist-get event :stop)))
(time-subtract (parse-iso8601-time-string stop)
(parse-iso8601-time-string start))))
(provide 'chronometrist-time)
;; Local Variables:
;; nameless-current-name: "chronometrist"
;; End:
;;; chronometrist-time.el ends here

View File

@ -14,7 +14,7 @@ Each of them has a corresponding `-custom` file, which contain the Customize gro
[chronometrist-common.el](chronometrist-common.el) contains definitions common to all components.
All three components use timers to keep their buffers updated. [chronometrist-timer.el](chronometrist-timer.el) contains all timer-related code. Note - sometimes, when hacking, timers may cause subtle bugs which are very hard to debug. Restarting Emacs can fix them, so try that as a first sanity check.
All three components use timers to keep their buffers updated. [chronometrist-timer.el](chronometrist-timer.el) contains all timer-related code. Note - sometimes, when hacking or dealing with errors, timers may result in subtle bugs which are very hard to debug. Using `chronometrist-force-restart-timer` or restarting Emacs can fix them, so try that as a first sanity check.
## Browsing the code
I recommend using

View File

@ -37,53 +37,55 @@
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)
**** 3. Split them at the hash-table-level
Handled by ~chronometrist-events-clean~
Handled by ~chronometrist-sexp-events-populate~
* Advantage - simpler data-consuming code.
**** 4. Split them at the data-consumer level (e.g. when calculating time for one day/getting events for one day)
* Advantage - reduced repetitive post-parsing load.
** Reference
* (?) - of dubious utility, a candidate for deprecation
* DEPRECATED - deprecated, slated to be removed in the future
*** Legend of currently-used time formats
| 1. | decode-time | (seconds minutes hours day month year dow dst utcoff) |
| 2. | list-timestamp | (year month day hours minutes seconds) |
| 3. | list-time/list-duration | (hours minutes seconds) |
| 4. | list-date | (year month day) |
| 5. | vector-date | [year month day] |
| 6. | vector-time/vector-duration | [hours minutes seconds] |
| 7. | encode-time (UNIX epoch) | (sec-high sec-low microsec picosec) |
| 8. | seconds | seconds as an integer |
| 9. | iso-timestamp | "YYYY-MM-DDTHH:MM:SSZ" |
| 10. | iso-date | "YYYY-MM-DD" |
| 11. | (DEPRECATED) timeclock-timestamp | "year/month/day hours:minutes:seconds" |
| 12. | (DEPRECATED) timeclock-date | "year/month/day" |
**** 1. ts
ts.el struct
* Used by nearly all internal functions
**** 2. iso-timestamp
"YYYY-MM-DDTHH:MM:SSZ"
* Used in the s-expression file format
* Read by chronometrist-sexp-events-populate
* Used in the plists in the chronometrist-events hash table values
**** 3. iso-date
"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=
**** 4. seconds
integer seconds as duration
* Used for most durations
* May be changed to floating point to allow larger durations. The minimum range of `most-positive-fixnum` is 536870911, which seems to be enough to represent durations of 17 years.
* Used for update intervals (chronometrist-update-interval, chronometrist-change-update-interval)
**** 5. minutes
integer minutes as duration
* Used for goals (chronometrist-goals-list, chronometrist-get-goal) - minutes seems like the ideal unit for users to enter
**** 6. list-duration
(hours minute seconds)
* Only returned by chronometrist-seconds-to-hms, called by chronometrist-format-time
*** chronometrist-common.el
1. Variable - chronometrist-empty-time-string
2. Variable - chronometrist-date-re
3. Variable - chronometrist-time-re-ui
4. Variable - chronometrist-task-list
5. Variable - chronometrist--fs-watch
6. Function - chronometrist-buffer-exists? (buffer-name)
* String -> List?
7. Function - chronometrist-buffer-visible? (buffer-or-buffer-name)
* Buffer | String -> Boolean
8. Function - chronometrist-format-time (duration &optional blank)
* vector-duration | list-duration -> "h:m:s"
9. Command - chronometrist-open-file (&optional button)
10. Function - chronometrist-common-create-chronometrist-file ()
11. Function - chronometrist-common-file-empty-p (file)
12. Function - chronometrist-common-clear-buffer (buffer)
13. Function - chronometrist-format-keybinds (command map &optional firstonly)
14. Function - chronometrist-events->time-list (events)
* (event ...) -> ((encode-time encode-time) ...)
15. Function - chronometrist-time-list->sum-of-intervals (time-value-lists)
* ((encode-time encode-time) ...) -> encode-time
16. Function - chronometrist-delete-list (&optional arg)
17. Function - chronometrist-previous-week-start (date-string)
18. Function - chronometrist-current-task ()
5. Function - chronometrist-task-list-add (task)
6. Internal Variable - chronometrist--fs-watch
7. Function - chronometrist-current-task ()
8. Function - chronometrist-format-time (seconds &optional (blank " "))
* seconds -> "h:m:s"
9. Function - chronometrist-common-file-empty-p (file)
10. Function - chronometrist-common-clear-buffer (buffer)
11. Function - chronometrist-format-keybinds (command map &optional firstonly)
12. Function - chronometrist-events->ts-pairs (events)
* (plist ...) -> ((ts . ts) ...)
13. Function - chronometrist-ts-pairs->durations (ts-pairs)
* ((ts . ts) ...) -> seconds
14. Function - chronometrist-previous-week-start (ts)
* ts -> ts
*** chronometrist-custom.el
1. Custom variable - chronometrist-file
2. Custom variable - chronometrist-buffer-name
@ -93,52 +95,64 @@
6. Custom variable - chronometrist-day-start-time
*** chronometrist-diary-view.el
1. Variable - chronometrist-diary-buffer-name
2. Variable - chronometrist-diary--current-date
2. Internal Variable - chronometrist-diary--current-date
3. Function - chronometrist-intervals-on (date)
4. Function - chronometrist-diary-tasks-reasons-on (date)
5. Function - chronometrist-diary-refresh (&optional ignore-auto noconfirm date)
6. Major Mode - chronometrist-diary-view-mode
7. Command - chronometrist-diary-view (&optional date)
*** chronometrist.el
1. Variable - chronometrist--task-history
2. Variable - chronometrist--point
3. Variable - chronometrist-mode-map
4. Function - chronometrist-task-active? (task)
1. Internal Variable - chronometrist--task-history
2. Internal Variable - chronometrist--point
3. Internal Variable - chronometrist--inhibit-read-p
4. Keymap - chronometrist-mode-map
5. Command - chronometrist-open-log (&optional button)
6. Function - chronometrist-common-create-file ()
7. Function - chronometrist-task-active? (task)
* String -> Boolean
5. Function - chronometrist-activity-indicator ()
6. Function - chronometrist-entries ()
7. Function - chronometrist-task-at-point ()
8. Function - chronometrist-goto-last-task ()
9. Function - chronometrist-print-keybind (command &optional description firstonly)
10. Function - chronometrist-print-non-tabular ()
11. Function - chronometrist-goto-nth-task (n)
12. Function - chronometrist-refresh (&optional ignore-auto noconfirm)
13. Function - chronometrist-refresh-file (fs-event)
14. Command - chronometrist-query-stop ()
15. Variable - chronometrist-before-in-functions
16. Variable - chronometrist-after-in-functions
17. Variable - chronometrist-before-out-functions
18. Variable - chronometrist-after-out-functions
19. Function - chronometrist-run-functions-and-clock-in (task)
20. Function - chronometrist-run-functions-and-clock-out (task)
21. Variable - chronometrist-mode-map
22. Major Mode - chronometrist-mode
23. Function - chronometrist-toggle-task-button (button)
24. Function - chronometrist-add-new-task-button (button)
25. Command - chronometrist-toggle-task (&optional prefix inhibit-hooks)
26. Command - chronometrist-toggle-task-no-hooks (&optional prefix)
27. Command - chronometrist-add-new-task ()
28. Command - chronometrist (&optional arg)
8. Function - chronometrist-use-goals? ()
9. Function - chronometrist-activity-indicator ()
10. Function - chronometrist-entries ()
11. Function - chronometrist-task-at-point ()
12. Function - chronometrist-goto-last-task ()
13. Function - chronometrist-print-keybind (command &optional description firstonly)
14. Function - chronometrist-print-non-tabular ()
15. Function - chronometrist-goto-nth-task (n)
16. Function - chronometrist-refresh (&optional ignore-auto noconfirm)
17. Function - chronometrist-refresh-file (fs-event)
18. Command - chronometrist-query-stop ()
19. Command - chronometrist-in (task &optional _prefix)
20. Command - chronometrist-out (&optional _prefix)
21. Variable - chronometrist-before-in-functions
22. Variable - chronometrist-after-in-functions
23. Variable - chronometrist-before-out-functions
24. Variable - chronometrist-after-out-functions
25. Function - chronometrist-run-functions-and-clock-in (task)
26. Function - chronometrist-run-functions-and-clock-out (task)
27. Keymap - chronometrist-mode-map
28. Major Mode - chronometrist-mode
29. Function - chronometrist-toggle-task-button (button)
30. Function - chronometrist-add-new-task-button (button)
31. Command - chronometrist-toggle-task (&optional prefix inhibit-hooks)
32. Command - chronometrist-toggle-task-no-hooks (&optional prefix)
33. Command - chronometrist-add-new-task ()
34. Command - chronometrist (&optional arg)
*** chronometrist-events.el
1. Variable - chronometrist-events
2. Function - chronometrist-list-midnight-spanning-events ()
3. Function - chronometrist-day-start (timestamp)
4. Function - chronometrist-file-clean ()
5. Function - chronometrist-events-maybe-split (event)
6. Function - chronometrist-events-populate ()
7. Function - chronometrist-tasks-from-table ()
8. Function - chronometrist-events-subset (start-date end-date)
9. Function - chronometrist-events-query-spec-match-p (plist specifiers)
* keys - iso-date
2. Function - chronometrist-day-start (timestamp)
* iso-timestamp -> encode-time
3. Function - chronometrist-file-clean ()
* commented out, unused
4. Function - chronometrist-events-maybe-split (event)
5. Function - chronometrist-events-populate ()
6. Function - chronometrist-tasks-from-table ()
7. Function - chronometrist-events-add (plist)
8. Function - chronometrist-events-replace-last (plist)
9. Function - chronometrist-events-subset (start end)
* ts ts -> hash-table
10. Function - chronometrist-events-query-spec-match-p (plist specifiers)
11. Function - chronometrist-events-query (table &key get specifiers except)
*** chronometrist-migrate.el
1. Variable - chronometrist-migrate-table
2. Function - chronometrist-migrate-populate (in-file)
@ -153,43 +167,47 @@
6. Function - chronometrist-plist-pp-to-string (object)
7. Function - chronometrist-plist-pp (object &optional stream)
*** chronometrist-queries.el
1. Function - chronometrist-task-time-one-day (task &optional date-string)
2. Function - chronometrist-active-time-one-day (&optional date-string)
3. Function - chronometrist-statistics-count-active-days (task &optional table)
4. Function - chronometrist-task-events-in-day (task date-string)
1. Function - chronometrist-last ()
* -> plist
2. Function - chronometrist-task-time-one-day (task &optional (ts (ts-now)))
* String &optional ts -> seconds
3. Function - chronometrist-active-time-one-day (&optional ts)
* &optional ts -> seconds
4. Function - chronometrist-statistics-count-active-days (task &optional (table chronometrist-events))
5. Function - chronometrist-task-events-in-day (task ts)
*** chronometrist-report-custom.el
1. Custom variable - chronometrist-report-buffer-name
2. Custom variable - chronometrist-report-week-start-day
3. Custom variable - chronometrist-report-weekday-number-alist
*** chronometrist-report.el
1. Variable - chronometrist-report--ui-date
2. Variable - chronometrist-report--ui-week-dates
3. Variable - chronometrist-report--point
1. Internal Variable - chronometrist-report--ui-date
2. Internal Variable - chronometrist-report--ui-week-dates
3. Internal Variable - chronometrist-report--point
4. Function - chronometrist-report-date ()
5. Function - chronometrist-report-date->dates-in-week (first-date-in-week)
* ts-1 -> (ts-1 ... ts-7)
6. Function - chronometrist-report-date->week-dates ()
7. Function - chronometrist-report-entries ()
8. Function - chronometrist-report-format-date (format-string time-date)
9. Function - chronometrist-report-print-keybind (command &optional description firstonly)
10. Function - chronometrist-report-print-non-tabular ()
11. Function - chronometrist-report-refresh (&optional ignore-auto noconfirm)
12. Function - chronometrist-report-refresh-file (fs-event)
13. Variable - chronometrist-report-mode-map
14. Major Mode - chronometrist-report-mode
15. Function - chronometrist-report (&optional keep-date)
16. Function - chronometrist-report-previous-week (arg)
17. Function - chronometrist-report-next-week (arg)
*** chronometrist-sexp.el
1. Variable - chronometrist--tag-suggestions
2. Variable - chronometrist--value-suggestions
8. Function - chronometrist-report-print-keybind (command &optional description firstonly)
9. Function - chronometrist-report-print-non-tabular ()
10. Function - chronometrist-report-refresh (&optional _ignore-auto _noconfirm)
11. Function - chronometrist-report-refresh-file (_fs-event)
12. Keymap - chronometrist-report-mode-map
13. Major Mode - chronometrist-report-mode
14. Function - chronometrist-report (&optional keep-date)
15. Function - chronometrist-report-previous-week (arg)
16. Function - chronometrist-report-next-week (arg)
*** chronometrist-key-values.el
1. Internal Variable - chronometrist--tag-suggestions
2. Internal Variable - chronometrist--value-suggestions
3. Function - chronometrist-plist-remove (plist &rest keys)
4. Function - chronometrist-maybe-string-to-symbol (list)
5. Function - chronometrist-maybe-symbol-to-string (list)
6. Command - chronometrist-reindent-buffer ()
7. Function - chronometrist-last-expr ()
8. Function - chronometrist-append-to-last-expr (tags plist)
9. Variable - chronometrist-tags-history
10. Function - chronometrist-tags-history-populate ()
6. Function - chronometrist-append-to-last (tags plist)
7. Variable - chronometrist-tags-history
8. Function - chronometrist-tags-history-populate ()
9. Function - chronometrist-tags-history-add (plist)
10. Function - chronometrist-tags-history-replace-last (plist)
11. Function - chronometrist-tags-history-combination-strings (task)
12. Function - chronometrist-tags-history-individual-strings (task)
13. Function - chronometrist-tags-prompt (task &optional initial-input)
@ -200,62 +218,85 @@
18. Function - chronometrist-ht-history-prep (table)
19. Function - chronometrist-key-history-populate ()
20. Function - chronometrist-value-history-populate ()
21. Command - chronometrist-kv-accept ()
22. Command - chronometrist-kv-reject ()
23. Variable - chronometrist-kv-read-mode-map
24. Major Mode - chronometrist-kv-read-mode
25. Function - chronometrist-kv-completion-quit-key ()
26. Function - chronometrist-string-has-whitespace-p (string)
27. Function - chronometrist-key-prompt (used-keys)
28. Function - chronometrist-value-prompt (key)
29. Function - chronometrist-value-insert (value)
30. Function - chronometrist-kv-add (&rest args)
31. Command - chronometrist-in (task &optional prefix)
32. Command - chronometrist-out (&optional prefix)
21. Keymap - chronometrist-kv-read-mode-map
22. Major Mode - chronometrist-kv-read-mode
23. Function - chronometrist-kv-completion-quit-key ()
24. Function - chronometrist-string-has-whitespace-p (string)
25. Function - chronometrist-key-prompt (used-keys)
26. Function - chronometrist-value-prompt (key)
27. Function - chronometrist-value-insert (value)
28. Function - chronometrist-kv-add (&rest args)
29. Command - chronometrist-kv-accept ()
30. Command - chronometrist-kv-reject ()
*** chronometrist-statistics-custom.el
1. Custom variable - chronometrist-statistics-buffer-name
*** chronometrist-statistics.el
1. Variable - chronometrist-statistics--ui-state
2. Variable - chronometrist-statistics--point
3. Variable - chronometrist-statistics-mode-map
4. Function - chronometrist-statistics-count-average-time-spent (task &optional table)
1. Internal Variable - chronometrist-statistics--ui-state
2. Internal Variable - chronometrist-statistics--point
3. Function - chronometrist-statistics-count-average-time-spent (task &optional (table chronometrist-events))
* string &optional hash-table -> seconds
5. Function - chronometrist-statistics-entries-internal (table)
6. Function - chronometrist-statistics-entries ()
7. Function - chronometrist-statistics-print-keybind (command &optional description firstonly)
8. Function - chronometrist-statistics-format-date (date)
9. Function - chronometrist-statistics-print-non-tabular ()
10. Function - chronometrist-statistics-refresh (&optional ignore-auto noconfirm)
11. Major Mode - chronometrist-statistics-mode
12. Command - chronometrist-statistics (&optional preserve-state)
13. Command - chronometrist-statistics-previous-range (arg)
14. Command - chronometrist-statistics-next-range (arg)
4. Function - chronometrist-statistics-entries-internal (table)
5. Function - chronometrist-statistics-entries ()
6. Function - chronometrist-statistics-print-keybind (command &optional description firstonly)
7. Function - chronometrist-statistics-print-non-tabular ()
8. Function - chronometrist-statistics-refresh (&optional ignore-auto noconfirm)
9. Keymap - chronometrist-statistics-mode-map
10. Major Mode - chronometrist-statistics-mode
11. Command - chronometrist-statistics (&optional preserve-state)
12. Command - chronometrist-statistics-previous-range (arg)
13. Command - chronometrist-statistics-next-range (arg)
*** chronometrist-time.el
1. Constant - chronometrist-seconds-in-day
2. Function - chronometrist-date (&optional time)
3. Function - chronometrist-day-of-week->number (day-of-week)
4. Function - chronometrist-number->day-of-week (number)
5. Function - chronometrist-format-time-iso8601 (&optional unix-time)
6. Function - chronometrist-time-interval-span-midnight? (t1 t2)
* list-timestamp list-timestamp -> Boolean
7. Function - chronometrist-midnight-spanning-p (start-time stop-time)
8. Function - chronometrist-time->seconds (time)
* vector-duration -> seconds
9. Function - chronometrist-seconds-to-hms (seconds)
* seconds -> vector-duration
10. Function - chronometrist-time-add (a b)
* time-vector time-vector -> time-vector
11. Function - chronometrist-iso-date->timestamp (date)
12. Function - chronometrist-date->time (date)
13. Function - chronometrist-date-less-p (date1 date2)
14. Function - chronometrist-time-less-or-equal-p (t1 t2)
15. Function - chronometrist-calendrical->date (date)
16. Function - chronometrist-interval (event)
* event -> encode-time
1. Function - chronometrist-iso-timestamp->ts (timestamp)
* iso-timestamp -> ts
2. Function - chronometrist-iso-date->ts (date)
* iso-date -> ts
3. 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)
* seconds -> list-duration
7. Function - chronometrist-interval (event)
* event -> duration
*** chronometrist-timer.el
1. Variable - chronometrist--timer-object
1. Internal Variable - chronometrist--timer-object
2. Function - chronometrist-timer ()
3. Command - chronometrist-stop-timer ()
4. Command - chronometrist-maybe-start-timer (&optional interactive-test)
5. Command - chronometrist-force-restart-timer ()
6. Command - chronometrist-change-update-interval (arg)
*** chronometrist-goals
1. Internal Variable - chronometrist--timers-list
2. Custom Variable - chronometrist-goals-list nil
3. Function - chronometrist-run-at-time (time repeat function &rest args)
4. Function - chronometrist-seconds->alert-string (seconds)
* seconds -> string
5. Function - chronometrist-approach-alert (task goal spent)
* string minutes minutes
6. Function - chronometrist-complete-alert (task goal spent)
* string minutes minutes
7. Function - chronometrist-exceed-alert (task goal spent)
* string minutes minutes
8. Function - chronometrist-no-goal-alert (task goal spent)
* string minutes minutes
9. Custom Variable - chronometrist-goals-alert-functions
* each function is passed - string minutes minutes
10. Function - chronometrist-get-goal (task &optional (goals-list chronometrist-goals-list))
* String &optional List -> minutes
11. Function - chronometrist-goals-run-alert-timers (task)
12. Function - chronometrist-goals-stop-alert-timers (&optional _task)
13. Function - chronometrist-goals-on-file-change ()
*** 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))))
4. Function - chronometrist-sexp-query-till (&optional (date (chronometrist-date)))
5. Function - chronometrist-sexp-last ()
* -> plist
6. Function - chronometrist-sexp-current-task ()
7. Function - chronometrist-sexp-events-populate ()
8. Function - chronometrist-sexp-create-file ()
9. Function - chronometrist-sexp-new (plist &optional (buffer (find-file-noselect chronometrist-file)))
10. Function - chronometrist-sexp-delete-list (&optional arg)
11. Function - chronometrist-sexp-replace-last (plist)
12. Command - chronometrist-sexp-reindent-buffer ()

View File

@ -0,0 +1,157 @@
;;; chronometrist-common.el --- Common definitions for Chronometrist -*- lexical-binding: t; -*-
;; Author: contrapunctus <xmpp:contrapunctus@jabber.fr>
;; This is free and unencumbered software released into the public domain.
;;
;; Anyone is free to copy, modify, publish, use, compile, sell, or
;; distribute this software, either in source code form or as a compiled
;; binary, for any purpose, commercial or non-commercial, and by any
;; means.
;;
;; For more information, please refer to <https://unlicense.org>
;;; Commentary:
;;
(require 'dash)
(require 'cl-lib)
(require 'ts)
(require 'chronometrist-custom)
(require 'chronometrist-report-custom)
(require 'chronometrist-time)
(require 'chronometrist-sexp)
;; ## VARIABLES ##
;;; Code:
(defvar chronometrist-empty-time-string "-")
(defvar chronometrist-date-re "[0-9]\\{4\\}/[0-9]\\{2\\}/[0-9]\\{2\\}")
(defvar chronometrist-time-re-ui
(rx-to-string
`(or
(and (repeat 0 2
(optional (repeat 1 2 digit) ":"))
(repeat 1 2 digit))
,chronometrist-empty-time-string))
"Regular expression to represent a timestamp in `chronometrist'.
This is distinct from `chronometrist-time-re-file' (which see) -
`chronometrist-time-re-ui' is meant for the user interface, and
must correspond to the output from `chronometrist-format-time'.")
(defvar chronometrist-task-list nil
"List of tasks in `chronometrist-file', as returned by `chronometrist-tasks-from-table'.")
(defun chronometrist-task-list-add (task)
"Add TASK to `chronometrist-task-list', if it is not already present."
(unless (member task chronometrist-task-list)
(--> (append chronometrist-task-list task)
(sort it #'string-lessp)
(setq chronometrist-task-list it))))
(defvar chronometrist--fs-watch nil
"Filesystem watch object.
Used to prevent more than one watch being added for the same
file.")
(defun chronometrist-current-task ()
"Return the name of the currently clocked-in task, or nil if not clocked in."
(chronometrist-sexp-current-task))
(cl-defun chronometrist-format-time (seconds &optional (blank " "))
"Format SECONDS as a string suitable for display in Chronometrist buffers.
SECONDS must be a positive integer.
BLANK is a string to display in place of blank values. If not
supplied, 3 spaces are used."
(-let [(h m s) (chronometrist-seconds-to-hms seconds)]
(if (and (zerop h) (zerop m) (zerop s))
" -"
(let ((h (if (zerop h)
blank
(format "%2d:" h)))
(m (cond ((and (zerop h)
(zerop m))
blank)
((zerop h)
(format "%2d:" m))
(t
(format "%02d:" m))))
(s (if (and (zerop h)
(zerop m))
(format "%2d" s)
(format "%02d" s))))
(concat h m s)))))
(defun chronometrist-common-file-empty-p (file)
"Return t if FILE is empty."
(let ((size (elt (file-attributes file) 7)))
(if (zerop size) t nil)))
(defun chronometrist-common-clear-buffer (buffer)
"Clear the contents of BUFFER."
(with-current-buffer buffer
(goto-char (point-min))
(delete-region (point-min) (point-max))))
(defun chronometrist-format-keybinds (command map &optional firstonly)
"Return the keybindings for COMMAND in MAP as a string.
If FIRSTONLY is non-nil, return only the first keybinding found."
(if firstonly
(key-description
(where-is-internal command map firstonly))
(->> (where-is-internal command map)
(mapcar #'key-description)
(-take 2)
(-interpose ", ")
(apply #'concat))))
(defun chronometrist-events->ts-pairs (events)
"Convert EVENTS to a list of ts struct pairs (see `ts.el').
EVENTS must be a list of valid Chronometrist property lists (see
`chronometrist-file')."
(cl-loop for plist in events collect
(let* ((start (chronometrist-iso-timestamp->ts
(plist-get plist :start)))
(stop (plist-get plist :stop))
(stop (if stop
(chronometrist-iso-timestamp->ts stop)
(ts-now))))
(cons start stop))))
(defun chronometrist-ts-pairs->durations (ts-pairs)
"Return the durations represented by TS-PAIRS.
TS-PAIRS is a list of pairs, where each element is a ts struct (see `ts.el').
Return seconds as an integer, or 0 if TS-PAIRS is nil."
(if ts-pairs
(cl-loop for pair in ts-pairs collect
(ts-diff (cdr pair) (car pair)))
0))
(defun chronometrist-previous-week-start (ts)
"Find the previous `chronometrist-report-week-start-day' from TS.
Return a ts struct for said day's beginning.
If the day of TS is the same as the
`chronometrist-report-week-start-day', return TS.
TS must be a ts struct (see `ts.el')."
(cl-loop until (equal chronometrist-report-week-start-day
(ts-day-name ts))
do (ts-decf (ts-day ts))
finally return ts))
;; Local Variables:
;; nameless-current-name: "chronometrist-common"
;; End:
(provide 'chronometrist-common)
;;; chronometrist-common.el ends here

View File

@ -59,8 +59,6 @@ vectors.\)"
This is not guaranteed to be accurate - see (info \"(elisp)Timers\")."
:type 'integer)
(declare-function chronometrist-format-time "chronometrist-common")
(declare-function chronometrist-last-expr "chronometrist-sexp")
(eval-when-compile (require 'subr-x))
(defcustom chronometrist-activity-indicator "*"

View File

@ -43,7 +43,7 @@ DATE should be a list in the form \"YYYY-MM-DD\"
Each time interval is a string as returned by `chronometrist-seconds-to-hms'."
(->> (gethash date chronometrist-events)
(chronometrist-events->time-list)
(chronometrist-events->ts-pairs)
;; Why were we calling `-partition' here?
;; (-partition 2)
(--map (time-subtract (cadr it) (car it)))

View File

@ -11,15 +11,27 @@
;;
;; For more information, please refer to <https://unlicense.org>
(require 'chronometrist-plist-pp)
;; (require 'chronometrist-plist-pp)
(require 'chronometrist-common)
(require 'chronometrist-sexp)
(require 'ts)
;; external -
;; chronometrist-day-start-time (-custom)
;; chronometrist-midnight-spanning-p (-time)
;; chronometrist-date-less-p (-time)
;;; Commentary:
;;
;;; Code:
(defvar chronometrist-events (make-hash-table :test #'equal))
(defvar chronometrist-events (make-hash-table :test #'equal)
"Each key is a date in the form (YEAR MONTH DAY).
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.
@ -39,43 +51,44 @@ Return value is a time value (see `current-time')."
(append it timestamp-date-list)
(apply #'encode-time it))))
(defun chronometrist-file-clean ()
"Clean `chronometrist-file' so that events can be processed accurately.
;; (defun chronometrist-file-clean ()
;; "Clean `chronometrist-file' so that events can be processed accurately.
;; NOTE - currently unused.
This function splits midnight-spanning intervals into two. It
must be called before running `chronometrist-populate'.
;; This function splits midnight-spanning intervals into two. It
;; must be called before running `chronometrist-populate'.
It returns t if the table was modified, else nil."
(let ((buffer (find-file-noselect chronometrist-file))
modified
expr)
(with-current-buffer buffer
(save-excursion
(goto-char (point-min))
(while (setq expr (ignore-errors (read (current-buffer))))
(when (plist-get expr :stop)
(let ((split-time (chronometrist-midnight-spanning-p (plist-get expr :start)
(plist-get expr :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)))
(backward-list 1)
(chronometrist-delete-list)
(-> expr
(plist-put :start first-start)
(plist-put :stop first-stop)
(chronometrist-plist-pp buffer))
(when (looking-at-p "\n\n")
(delete-char 2))
(-> expr
(plist-put :start second-start)
(plist-put :stop second-stop)
(chronometrist-plist-pp buffer))
(setq modified t))))))
(save-buffer)))
modified))
;; It returns t if the table was modified, else nil."
;; (let ((buffer (find-file-noselect chronometrist-file))
;; modified
;; expr)
;; (with-current-buffer buffer
;; (save-excursion
;; (goto-char (point-min))
;; (while (setq expr (ignore-errors (read (current-buffer))))
;; (when (plist-get expr :stop)
;; (let ((split-time (chronometrist-midnight-spanning-p (plist-get expr :start)
;; (plist-get expr :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)))
;; (backward-list 1)
;; (chronometrist-sexp-delete-list)
;; (-> expr
;; (plist-put :start first-start)
;; (plist-put :stop first-stop)
;; (chronometrist-plist-pp buffer))
;; (when (looking-at-p "\n\n")
;; (delete-char 2))
;; (-> expr
;; (plist-put :start second-start)
;; (plist-put :stop second-stop)
;; (chronometrist-plist-pp buffer))
;; (setq modified t))))))
;; (save-buffer)))
;; modified))
(defun chronometrist-events-maybe-split (event)
"Split EVENT if it spans midnight.
@ -106,47 +119,13 @@ Return a list of two events if EVENT was split, else nil."
;; OPTIMIZE - It should not be necessary to call this unless the file
;; has changed. Any other refresh situations should not require this.
(defun chronometrist-events-populate ()
"Clear hash table `chronometrist-events' and populate it.
"Clear hash table `chronometrist-events' (which see) and populate it.
The data is acquired from `chronometrist-file'.
Each key is a date in the form (YEAR MONTH DAY).
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).
Return final number of events read from file, or nil if there
were none."
(clrhash chronometrist-events)
(with-current-buffer (find-file-noselect chronometrist-file)
(save-excursion
(goto-char (point-min))
(let ((index 0)
expr
pending-expr)
(while (or pending-expr
(setq expr (ignore-errors (read (current-buffer)))))
;; find and split midnight-spanning events during deserialization itself
(let* ((split-expr (chronometrist-events-maybe-split expr))
(new-value (cond (pending-expr
(prog1 pending-expr
(setq pending-expr nil)))
(split-expr
(setq pending-expr (cl-second split-expr))
(cl-first split-expr))
(t expr)))
(new-value-date (->> (plist-get new-value :start)
(s-left 10)))
(existing-value (gethash new-value-date chronometrist-events)))
(unless pending-expr (cl-incf index))
(puthash new-value-date
(if existing-value
(append existing-value
(list new-value))
(list new-value))
chronometrist-events)))
(unless (zerop index) index)))))
(chronometrist-sexp-events-populate))
(defun chronometrist-tasks-from-table ()
"Return a list of task names from `chronometrist-events'."
@ -159,18 +138,38 @@ were none."
(cl-remove-duplicates (sort acc #'string-lessp)
:test #'equal)))
(defun chronometrist-events-add (plist)
"Add new PLIST at the end of `chronometrist-events'."
(let* ((date-today (format-time-string "%Y-%m-%d"))
(events-today (gethash date-today chronometrist-events)))
(--> (list plist)
(append events-today it)
(puthash date-today it chronometrist-events))))
(defun chronometrist-events-replace-last (plist)
"Replace the last plist in `chronometrist-events' with PLIST."
(let* ((date-today (format-time-string "%Y-%m-%d"))
(events-today (gethash date-today chronometrist-events)))
(--> (reverse events-today)
(cdr it)
(append (list plist) it)
(reverse it)
(puthash date-today it chronometrist-events))))
;; to be replaced by plist-query
(defun chronometrist-events-subset (start-date end-date)
(defun chronometrist-events-subset (start end)
"Return a subset of `chronometrist-events'.
The subset will contain values between START-DATE and
END-DATE (both inclusive).
The subset will contain values between dates START and END (both
inclusive).
START-DATE and END-DATE must be dates in the form '(YEAR MONTH DAY)."
(let ((subset (make-hash-table :test #'equal)))
START and END must be ts structs (see `ts.el'). They will be
treated as though their time is 00:00:00."
(let ((subset (make-hash-table :test #'equal))
(start (chronometrist-date start))
(end (chronometrist-date end)))
(maphash (lambda (key value)
(when (and (not (chronometrist-date-less-p key start-date))
(not (chronometrist-date-less-p end-date key)))
(when (ts-in start end (chronometrist-iso-date->ts key))
(puthash key value subset)))
chronometrist-events)
subset))

View File

@ -1,4 +1,4 @@
;;; chronometrist-sexp.el --- S-expression backend for Chronometrist -*- lexical-binding: t; -*-
;;; chronometrist-key-values.el --- add key-values to Chronometrist data -*- lexical-binding: t; -*-
(require 'cl-lib)
(require 'subr-x)
@ -26,6 +26,8 @@
;;; Code:
(require 'chronometrist-sexp)
(defvar chronometrist--tag-suggestions nil
"Suggestions for tags.
Used as history by `chronometrist-tags-prompt'.")
@ -58,71 +60,42 @@ Used as history by `chronometrist--value-suggestions'.")
it)
list))
(defun chronometrist-reindent-buffer ()
"Reindent the current buffer.
This is meant to be run in `chronometrist-file'."
(interactive)
(let (expr)
(goto-char (point-min))
(while (setq expr (ignore-errors (read (current-buffer))))
(backward-list)
(chronometrist-delete-list)
(when (looking-at "\n*")
(delete-region (match-beginning 0)
(match-end 0)))
(chronometrist-plist-pp expr (current-buffer))
(insert "\n")
(unless (eobp)
(insert "\n")))))
(defun chronometrist-last-expr ()
"Return last s-expression from `chronometrist-file'."
(let ((buffer (find-file-noselect chronometrist-file)))
(with-current-buffer buffer
(save-excursion
(goto-char (point-max))
(backward-list)
(ignore-errors
(read buffer))))))
(defun chronometrist-append-to-last-expr (tags plist)
"Add TAGS and PLIST to last s-expression in `chronometrist-file'.
(defun chronometrist-append-to-last (tags plist)
"Add TAGS and PLIST to the last entry in `chronometrist-file'.
TAGS should be a list of symbols and/or strings.
PLIST should be a property list. Properties reserved by
Chronometrist - currently :name, :tags, :start, and :stop - will
be removed."
(let* ((old-expr (chronometrist-last-expr))
(old-name (plist-get old-expr :name))
(old-start (plist-get old-expr :start))
(old-stop (plist-get old-expr :stop))
(old-tags (plist-get old-expr :tags))
(old-kvs (chronometrist-plist-remove old-expr :name :tags :start :stop))
(plist (chronometrist-plist-remove plist :name :tags :start :stop))
(new-tags (if old-tags
(-> (append old-tags tags)
(cl-remove-duplicates :test #'equal))
tags))
(new-kvs (cl-copy-list old-expr))
(new-kvs (if plist
(-> (cl-loop for (key val) on plist by #'cddr
do (plist-put new-kvs key val)
finally return new-kvs)
(chronometrist-plist-remove :name :tags :start :stop))
old-kvs))
(buffer (find-file-noselect chronometrist-file)))
(with-current-buffer buffer
(goto-char (point-max))
(backward-list)
(chronometrist-delete-list)
(-> (append `(:name ,old-name)
(when new-tags `(:tags ,new-tags))
new-kvs
`(:start ,old-start)
(when old-stop `(:stop ,old-stop)))
(chronometrist-plist-pp buffer))
(save-buffer))))
(let* ((old-expr (chronometrist-last))
(old-name (plist-get old-expr :name))
(old-start (plist-get old-expr :start))
(old-stop (plist-get old-expr :stop))
(old-tags (plist-get old-expr :tags))
;; Anything that's left will be the user's key-values.
(old-kvs (chronometrist-plist-remove old-expr :name :tags :start :stop))
;; Prevent the user from adding reserved key-values.
(plist (chronometrist-plist-remove plist :name :tags :start :stop))
(new-tags (if old-tags
(-> (append old-tags tags)
(cl-remove-duplicates :test #'equal))
tags))
;; In case there is an overlap in key-values, we use
;; plist-put to replace old ones with new ones.
(new-kvs (cl-copy-list old-expr))
(new-kvs (if plist
(-> (cl-loop for (key val) on plist by #'cddr
do (plist-put new-kvs key val)
finally return new-kvs)
(chronometrist-plist-remove :name :tags :start :stop))
old-kvs))
(plist (append `(:name ,old-name)
(when new-tags `(:tags ,new-tags))
new-kvs
`(:start ,old-start)
(when old-stop `(:stop ,old-stop)))))
(chronometrist-sexp-replace-last plist)))
;;;; TAGS ;;;;
(defvar chronometrist-tags-history (make-hash-table :test #'equal)
@ -145,20 +118,41 @@ as symbol and/or strings.")
(append existing-tags `(,tags))
`(,tags))
table))))
;; We can't use `chronometrist-ht-history-prep' here, because it uses
;; `-flatten'; `chronometrist-tags-history' holds tag combinations (as lists),
;; not individual tags.
;; 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 list)
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 list :test #'equal)
(-> (cl-remove-duplicates tag-lists :test #'equal)
(reverse))
table))))
(defun chronometrist-tags-history-add (plist)
"Add tags from PLIST to `chronometrist-tags-history'."
(let* ((table chronometrist-tags-history)
(name (plist-get plist :name))
(tags (plist-get plist :tags))
(old-tags (gethash name table)))
(when tags
(puthash name (append tags old-tags) table))))
(defun chronometrist-tags-history-replace-last (plist)
"Replace the latest tag combination for PLIST's task with tags from PLIST."
(let* ((table chronometrist-tags-history)
(name (plist-get plist :name))
(tags (plist-get plist :tags))
(old-tags (gethash name table)))
(if old-tags
(--> (cdr old-tags)
(append tags it)
(puthash name it table))
(puthash name tags table))))
(defun chronometrist-tags-history-combination-strings (task)
"Return list of past tag combinations for TASK.
Each combination is a string, with tags separated by commas.
@ -200,9 +194,10 @@ INITIAL-INPUT is as used in `completing-read'."
'chronometrist--tag-suggestions))
(defun chronometrist-tags-add (&rest _args)
"Read tags from the user, add them to the last s-expr in `chronometrist-file'.
_ARGS are ignored. This function always returns t."
(let* ((last-expr (chronometrist-last-expr))
"Read tags from the user and add them to the last entry in `chronometrist-file'.
_ARGS are ignored. This function always returns t, so it can be
used in `chronometrist-before-out-functions'."
(let* ((last-expr (chronometrist-last))
(last-name (plist-get last-expr :name))
(last-tags (plist-get last-expr :tags))
(input (->> last-tags
@ -212,11 +207,11 @@ _ARGS are ignored. This function always returns t."
(chronometrist-tags-prompt last-name)
(chronometrist-maybe-string-to-symbol))))
(when input
(-> (append last-tags input)
(reverse)
(cl-remove-duplicates :test #'equal)
(reverse)
(chronometrist-append-to-last-expr nil)))
(--> (append last-tags input)
(reverse it)
(cl-remove-duplicates it :test #'equal)
(reverse it)
(chronometrist-append-to-last it nil)))
t))
;;;; KEY-VALUES ;;;;
@ -255,44 +250,36 @@ reversed and will have duplicate elements removed."
table))
table))
;; Since we have discarded sorting-by-frequency, we can now consider
;; implementing this by querying `chronometrist-events' instead of reading the file
(defun chronometrist-key-history-populate ()
"Populate `chronometrist-key-history' from from `chronometrist-file'.
"Populate `chronometrist-key-history' from `chronometrist-file'.
Each hash table key is the name of a task. Each hash table value
is a list containing keywords used with that task, in reverse
chronological order. The keywords are stored as strings and their
leading \":\" is removed."
(clrhash chronometrist-key-history)
;; add each task as a key
(mapc (lambda (task)
(puthash task nil chronometrist-key-history))
;; ;; Not necessary, if the only placed this is called is `chronometrist-refresh-file'
;; (setq chronometrist--task-list (chronometrist-tasks-from-table))
chronometrist-task-list)
(with-current-buffer (find-file-noselect chronometrist-file)
(save-excursion
(goto-char (point-min))
(let (expr)
(while (setq expr (ignore-errors (read (current-buffer))))
(let* ((name (plist-get expr :name))
(name-ht-value (gethash name chronometrist-key-history))
(keys (->> (chronometrist-plist-remove expr :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 name-ht-value
(append name-ht-value key-string)
key-string)
chronometrist-key-history))))))
(chronometrist-ht-history-prep 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 events being the hash-values of chronometrist-events do
(cl-loop for plist in events do
(let ((name (plist-get plist :name))
(keys (->> (chronometrist-plist-remove plist :name :start :stop :tags)
(seq-filter #'keywordp))))
(cl-loop for key in keys do
(when key
(let ((old-keys (gethash name chronometrist-key-history))
(new-key (->> (symbol-name key)
(s-chop-prefix ":")
(list))))
(--> (if old-keys
(append old-keys new-key)
new-key)
(puthash name it chronometrist-key-history))))))))
(chronometrist-ht-history-prep chronometrist-key-history))
;; FIXME - seems to be a little buggy. The latest value for e.g. :song
;; is different from the one that ends up as the last in
;; `chronometrist-value-history' (before being reversed by `chronometrist-ht-history-prep')
(defun chronometrist-value-history-populate ()
"Read values for user-keys from `chronometrist-events'.
The values are stored in `chronometrist-value-history'."
@ -301,73 +288,37 @@ The values are stored in `chronometrist-value-history'."
(let ((table chronometrist-value-history)
user-kvs)
(clrhash table)
(maphash (lambda (_date plist-list)
(cl-loop for plist in plist-list
do (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)))))
chronometrist-events)
(cl-loop
for plist-list being the hash-values of chronometrist-events do
(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)))))
(chronometrist-ht-history-prep table)))
;; TODO - refactor this to use `chronometrist-append-to-last-expr'
(defun chronometrist-kv-accept ()
"Accept the plist in `chronometrist-kv-buffer-name' and add it to `chronometrist-file'."
(interactive)
(let ((backend-buffer (find-file-noselect chronometrist-file))
user-kv-expr
last-expr)
(with-current-buffer (get-buffer chronometrist-kv-buffer-name)
(goto-char (point-min))
(setq user-kv-expr (ignore-errors (read (current-buffer))))
(kill-buffer chronometrist-kv-buffer-name))
(if user-kv-expr
(with-current-buffer backend-buffer
(goto-char (point-max))
(backward-list)
(setq last-expr (ignore-errors (read backend-buffer)))
(backward-list)
(chronometrist-delete-list)
(let ((name (plist-get last-expr :name))
(tags (plist-get last-expr :tags))
(start (plist-get last-expr :start))
(stop (plist-get last-expr :stop))
(old-kvs (chronometrist-plist-remove last-expr :name :tags :start :stop)))
(chronometrist-plist-pp (append (when name `(:name ,name))
(when tags `(:tags ,tags))
old-kvs
user-kv-expr
(when start `(:start ,start))
(when stop `(:stop ,stop)))
backend-buffer))
(save-buffer))
(switch-to-buffer chronometrist-buffer-name)
(chronometrist-refresh))))
(defun chronometrist-kv-reject ()
"Reject the property list in `chronometrist-kv-buffer-name'."
(interactive)
(kill-buffer chronometrist-kv-buffer-name)
(chronometrist-refresh))
(defvar chronometrist-kv-read-mode-map
(let ((map (make-sparse-keymap)))
(define-key map (kbd "C-c C-c") #'chronometrist-kv-accept)
(define-key map (kbd "C-c C-k") #'chronometrist-kv-reject)
map)
"Keymap used by `chronometrist-mode'.")
"Keymap used by `chronometrist-kv-read-mode'.")
(define-derived-mode chronometrist-kv-read-mode emacs-lisp-mode "Key-Values"
"Mode used by `chronometrist' to read key values from the user."
@ -395,8 +346,9 @@ It currently supports ido, ido-ubiquitous, ivy, and helm."
(defun chronometrist-key-prompt (used-keys)
"Prompt the user to enter keys.
USED-KEYS are keys they have already used in this session."
(let ((key-suggestions (--> (chronometrist-last-expr)
USED-KEYS are keys they have already added since the invocation
of `chronometrist-kv-add'."
(let ((key-suggestions (--> (chronometrist-last)
(plist-get it :name)
(gethash it chronometrist-key-history))))
(completing-read (concat "Key ("
@ -443,10 +395,11 @@ In the resulting buffer, users can run `chronometrist-kv-accept'
to add them to the last s-expression in `chronometrist-file', or
`chronometrist-kv-reject' to cancel.
_ARGS are ignored. This function always returns t."
_ARGS are ignored. This function always returns t, so it can be
used in `chronometrist-before-out-functions'."
(let* ((buffer (get-buffer-create chronometrist-kv-buffer-name))
(first-key-p t)
(last-kvs (chronometrist-plist-remove (chronometrist-last-expr)
(last-kvs (chronometrist-plist-remove (chronometrist-last)
:name :tags :start :stop))
(used-keys (->> (seq-filter #'keywordp last-kvs)
(mapcar #'symbol-name)
@ -480,51 +433,32 @@ _ARGS are ignored. This function always returns t."
(if (string-empty-p input)
(throw 'empty-input nil)
(chronometrist-value-insert value)))))
(chronometrist-reindent-buffer)))
(chronometrist-sexp-reindent-buffer)))
t)
;;;; COMMANDS ;;;;
(defun chronometrist-in (task &optional _prefix)
"Clock in to TASK; record current time in `chronometrist-file'.
TASK is the name of the task, a string.
(defun chronometrist-kv-accept ()
"Accept the plist in `chronometrist-kv-buffer-name' and add it to `chronometrist-file'."
(interactive)
(let (user-kv-expr)
(with-current-buffer (get-buffer chronometrist-kv-buffer-name)
(goto-char (point-min))
(setq user-kv-expr (ignore-errors (read (current-buffer))))
(kill-buffer chronometrist-kv-buffer-name))
(if user-kv-expr
(chronometrist-append-to-last nil user-kv-expr)
(chronometrist-refresh))))
PREFIX is ignored."
(interactive "P")
(let ((buffer (find-file-noselect chronometrist-file)))
(with-current-buffer buffer
(goto-char (point-max))
(unless (bobp) (insert "\n"))
(unless (bolp) (insert "\n"))
(chronometrist-plist-pp `(:name ,task
:start ,(format-time-string "%FT%T%z"))
buffer)
(save-buffer))))
(defun chronometrist-kv-reject ()
"Reject the property list in `chronometrist-kv-buffer-name'."
(interactive)
(kill-buffer chronometrist-kv-buffer-name)
(chronometrist-refresh))
(defun chronometrist-out (&optional _prefix)
"Record current moment as stop time to last s-exp in `chronometrist-file'.
PLIST is a property list containing any other information about
this time interval that should be recorded.
PREFIX is ignored."
(interactive "P")
(let ((buffer (find-file-noselect chronometrist-file)))
(with-current-buffer buffer
(goto-char (point-max))
(unless (bobp) (insert "\n"))
(backward-list 1)
(--> (read buffer)
(plist-put it :stop (chronometrist-format-time-iso8601))
(progn
(backward-list 1)
(chronometrist-delete-list)
(chronometrist-plist-pp it buffer)))
(save-buffer))))
(provide 'chronometrist-sexp)
(provide 'chronometrist-key-values)
;; Local Variables:
;; nameless-current-name: "chronometrist"
;; End:
;;; chronometrist-sexp.el ends here
;;; chronometrist-key-values.el ends here

View File

@ -0,0 +1,87 @@
;;; chronometrist-queries.el --- Functions which query Chronometrist data -*- lexical-binding: t; -*-
;; Author: contrapunctus <xmpp:contrapunctus@jabber.fr>
;; This is free and unencumbered software released into the public domain.
;;
;; Anyone is free to copy, modify, publish, use, compile, sell, or
;; distribute this software, either in source code form or as a compiled
;; binary, for any purpose, commercial or non-commercial, and by any
;; means.
;;
;; For more information, please refer to <https://unlicense.org>
;;; Commentary:
;;
;;; Code:
(require 'dash)
(require 'chronometrist-common)
(require 'chronometrist-events)
(defun chronometrist-last ()
"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)))
"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)))
(if task-events
(->> (chronometrist-events->ts-pairs task-events)
(chronometrist-ts-pairs->durations)
(-reduce #'+)
(truncate))
;; no events for this task on TS, i.e. no time spent
0)))
(defun chronometrist-active-time-one-day (&optional ts)
"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))
(-reduce #'+)
(truncate)))
(cl-defun chronometrist-statistics-count-active-days (task &optional (table chronometrist-events))
"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.
This will not return correct results if TABLE contains records
which span midnights. (see `chronometrist-events-clean')"
(let ((count 0))
(maphash (lambda (_date events)
(when (seq-find (lambda (event)
(equal (plist-get event :name) task))
events)
(cl-incf count)))
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)))
(provide 'chronometrist-queries)
;;; chronometrist-queries.el ends here

View File

@ -57,20 +57,15 @@ information (see (info \"(elisp)Time Conversion\"))."
(defun chronometrist-report-date->dates-in-week (first-date-in-week)
"Return a list of dates in a week, starting from FIRST-DATE-IN-WEEK.
Each day is a time value (see (info \"(elisp)Time of Day\")).
Each date is a ts struct (see `ts.el').
FIRST-DATE-IN-WEEK must be a time value representing the first date."
(--> '(0 1 2 3 4 5 6)
;; 1 day = 86400 seconds
(--map (* 86400 it) it)
(--map (list
(car first-date-in-week)
(+ (cadr first-date-in-week) it))
it)))
FIRST-DATE-IN-WEEK must be a ts struct representing the first date."
(cl-loop for i from 0 to 6 collect
(ts-adjust 'day i first-date-in-week)))
(defun chronometrist-report-date->week-dates ()
"Return dates in week as a list.
Each element is calendrical information (see (info \"(elisp)Time Conversion\")).
Each element is a ts struct (see `ts.el').
The first date is the first occurrence of
`chronometrist-report-week-start-day' before the date specified in
@ -81,32 +76,21 @@ The first date is the first occurrence of
(defun chronometrist-report-entries ()
"Create entries to be displayed in the `chronometrist-report' buffer."
(let* ((week-dates (chronometrist-report-date->week-dates))) ;; uses today if chronometrist-report--ui-date is nil
(let* ((week-dates (chronometrist-report-date->week-dates))) ;; uses today if chronometrist-report--ui-date is nil
(setq chronometrist-report--ui-week-dates week-dates)
(mapcar (lambda (task)
(let ((task-daily-time-list
(--map (chronometrist-task-time-one-day task
(chronometrist-date it))
week-dates)))
(list task
(vconcat
(vector task)
(->> task-daily-time-list
(mapcar #'chronometrist-format-time)
(apply #'vector))
(->> task-daily-time-list
(-reduce #'chronometrist-time-add)
(chronometrist-format-time)
(vector))))))
chronometrist-task-list)))
(defun chronometrist-report-format-date (format-string time-date)
"Extract date from TIME-DATE and format it according to FORMAT-STRING."
(->> time-date
(-take 6)
(-drop 3)
(reverse)
(apply #'format format-string)))
(cl-loop for task in chronometrist-task-list collect
(let* ((durations (--map (chronometrist-task-time-one-day task (chronometrist-date it))
week-dates))
(duration-strings (mapcar #'chronometrist-format-time
durations))
(total-duration (->> (-reduce #'+ durations)
(chronometrist-format-time)
(vector))))
(list task
(vconcat
(vector task)
duration-strings ;; vconcat converts lists to vectors
total-duration))))))
(defun chronometrist-report-print-keybind (command &optional description firstonly)
"Insert one or more keybindings for COMMAND into the current buffer.
@ -122,50 +106,39 @@ If FIRSTONLY is non-nil, insert only the first keybinding found."
(defun chronometrist-report-print-non-tabular ()
"Print the non-tabular part of the buffer in `chronometrist-report'."
(let ((inhibit-read-only t)
(w "\n "))
(w "\n ")
(total-time-daily (->> chronometrist-report--ui-week-dates
(mapcar #'chronometrist-date)
(mapcar #'chronometrist-active-time-one-day))))
(goto-char (point-min))
(insert " ")
(insert (mapconcat #'chronometrist-date
(insert (mapconcat (lambda (ts)
(ts-format "%F" ts))
(chronometrist-report-date->week-dates)
" "))
(insert "\n")
(goto-char (point-max))
(insert w (format "%- 21s" "Total"))
(let ((total-time-daily (->> chronometrist-report--ui-week-dates
(mapcar #'chronometrist-date)
(mapcar #'chronometrist-active-time-one-day))))
(->> total-time-daily
(mapcar #'chronometrist-format-time)
(--map (format "% 9s " it))
(apply #'insert))
(->> total-time-daily
(-reduce #'chronometrist-time-add)
(chronometrist-format-time)
(format "% 13s")
(insert)))
(->> total-time-daily
(mapcar #'chronometrist-format-time)
(--map (format "% 9s " it))
(apply #'insert))
(->> total-time-daily
(-reduce #'+)
(chronometrist-format-time)
(format "% 13s")
(insert))
(insert "\n" w)
(insert-text-button "<<"
'action #'chronometrist-report-previous-week
'follow-link t)
(insert-text-button "<<" 'action #'chronometrist-report-previous-week 'follow-link t)
(insert (format "% 4s" " "))
(insert-text-button ">>"
'action #'chronometrist-report-next-week
'follow-link t)
(insert-text-button ">>" 'action #'chronometrist-report-next-week 'follow-link t)
(insert "\n")
(chronometrist-report-print-keybind 'chronometrist-report-previous-week)
(insert-text-button "previous week"
'action #'chronometrist-report-previous-week
'follow-link t)
(insert-text-button "previous week" 'action #'chronometrist-report-previous-week 'follow-link t)
(chronometrist-report-print-keybind 'chronometrist-report-next-week)
(insert-text-button "next week"
'action #'chronometrist-report-next-week
'follow-link t)
(chronometrist-report-print-keybind 'chronometrist-open-file)
(insert-text-button "open log file"
'action #'chronometrist-open-file
'follow-link t)))
(insert-text-button "next week" 'action #'chronometrist-report-next-week 'follow-link t)
(chronometrist-report-print-keybind 'chronometrist-open-log)
(insert-text-button "open log file" 'action #'chronometrist-open-log 'follow-link t)))
(defun chronometrist-report-refresh (&optional _ignore-auto _noconfirm)
"Refresh the `chronometrist-report' buffer, without re-reading `chronometrist-file'."
@ -189,7 +162,7 @@ Argument _FS-EVENT is ignored."
(defvar chronometrist-report-mode-map
(let ((map (make-sparse-keymap)))
(define-key map (kbd "l") #'chronometrist-open-file)
(define-key map (kbd "l") #'chronometrist-open-log)
(define-key map (kbd "b") #'chronometrist-report-previous-week)
(define-key map (kbd "f") #'chronometrist-report-next-week)
;; Works when number of tasks < screen length; after that, you
@ -255,7 +228,7 @@ current week. Otherwise, display data from the week specified by
(t (delete-other-windows)
(unless keep-date
(setq chronometrist-report--ui-date nil))
(chronometrist-common-create-chronometrist-file)
(chronometrist-common-create-file)
(chronometrist-report-mode)
(switch-to-buffer buffer)
(chronometrist-report-refresh-file nil)
@ -269,12 +242,10 @@ With prefix argument ARG, move back ARG weeks."
(abs arg)
1)))
(setq chronometrist-report--ui-date
(thread-first (if chronometrist-report--ui-date
(parse-iso8601-time-string
(chronometrist-iso-date->timestamp chronometrist-report--ui-date))
(current-time))
(time-subtract `(0 ,(* 7 arg 86400)))
(chronometrist-date))))
(ts-adjust 'day (- (* arg 7))
(if chronometrist-report--ui-date
chronometrist-report--ui-date
(ts-now)))))
(setq chronometrist-report--point (point))
(kill-buffer)
(chronometrist-report t))
@ -287,12 +258,10 @@ With prefix argument ARG, move forward ARG weeks."
(abs arg)
1)))
(setq chronometrist-report--ui-date
(thread-first (if chronometrist-report--ui-date
(parse-iso8601-time-string
(chronometrist-iso-date->timestamp chronometrist-report--ui-date))
(current-time))
(time-add `(0 ,(* 7 arg 86400)))
(chronometrist-date)))
(ts-adjust 'day (* arg 7)
(if chronometrist-report--ui-date
chronometrist-report--ui-date
(ts-now))))
(setq chronometrist-report--point (point))
(kill-buffer)
(chronometrist-report t)))

139
elisp/chronometrist-sexp.el Normal file
View File

@ -0,0 +1,139 @@
;;; chronometrist-sexp.el --- s-expression backend for Chronometrist -*- lexical-binding: t; -*-
;;; Commentary:
;;
(require 'chronometrist-custom)
(require 'chronometrist-plist-pp)
;;; Code:
;; chronometrist-file (-custom)
;; chronometrist-events, chronometrist-events-maybe-split (-events)
;; chronometrist-plist-pp (-plist-pp)
(defmacro chronometrist-sexp-in-file (file &rest body)
"Run BODY in a buffer visiting FILE, restoring point afterwards."
(declare (indent defun))
`(with-current-buffer (find-file-noselect ,file)
(save-excursion ,@body)))
;;;; Queries
(defun chronometrist-sexp-open-log ()
"Open `chronometrist-file' in another window."
(find-file-other-window chronometrist-file)
(goto-char (point-max)))
(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 (current-buffer)))))
(defun chronometrist-sexp-current-task ()
"Return the name of the currently clocked-in task, or nil if not clocked in."
(let ((last-event (chronometrist-sexp-last)))
(if (plist-member last-event :stop)
nil
(plist-get last-event :name))))
(defun chronometrist-sexp-events-populate ()
"Populate hash table `chronometrist-events'.
The data is acquired from `chronometrist-file'.
Return final number of events read from file, or nil if there
were none."
(chronometrist-sexp-in-file chronometrist-file
(goto-char (point-min))
(let ((index 0)
expr
pending-expr)
(while (or pending-expr
(setq expr (ignore-errors (read (current-buffer)))))
;; find and split midnight-spanning events during deserialization itself
(let* ((split-expr (chronometrist-events-maybe-split expr))
(new-value (cond (pending-expr
(prog1 pending-expr
(setq pending-expr nil)))
(split-expr
(setq pending-expr (cl-second split-expr))
(cl-first split-expr))
(t expr)))
(new-value-date (->> (plist-get new-value :start)
(s-left 10)))
(existing-value (gethash new-value-date chronometrist-events)))
(unless pending-expr (cl-incf index))
(puthash new-value-date
(if existing-value
(append existing-value
(list new-value))
(list new-value))
chronometrist-events)))
(unless (zerop index) index))))
;;;; Modifications
(defun chronometrist-sexp-create-file ()
"Create `chronometrist-file' if it doesn't already exist."
(unless (file-exists-p chronometrist-file)
(with-current-buffer (find-file-noselect chronometrist-file)
(write-file chronometrist-file))))
(cl-defun chronometrist-sexp-new (plist)
"Add new PLIST at the end of `chronometrist-file'."
(chronometrist-sexp-in-file chronometrist-file
(goto-char (point-max))
;; If we're adding the first s-exp in the file, don't add a
;; newline before it
(unless (bobp) (insert "\n"))
(unless (bolp) (insert "\n"))
(chronometrist-plist-pp plist (current-buffer))
;; Update in-memory (`chronometrist-events', `chronometrist-task-list') too...
(chronometrist-events-add plist)
(chronometrist-task-list-add (plist-get plist :name))
(chronometrist-tags-history-add plist)
;; ...so we can skip some expensive operations.
(setq chronometrist--inhibit-read-p t)
(save-buffer)))
(defun chronometrist-sexp-delete-list (&optional arg)
"Delete ARG lists after point."
(let ((point-1 (point)))
(forward-sexp (or arg 1))
(delete-region point-1 (point))))
(defun chronometrist-sexp-replace-last (plist)
"Replace the last s-expression in `chronometrist-file' with PLIST."
(chronometrist-sexp-in-file chronometrist-file
(goto-char (point-max))
(unless (and (bobp) (bolp))
(insert "\n"))
(backward-list 1)
(chronometrist-sexp-delete-list)
(chronometrist-plist-pp plist (current-buffer))
(chronometrist-events-replace-last plist)
;; We assume here that this function will always be used to replace something with the same :name. At the time of writing, this is indeed the case. The reason for this is that if the replaced plist is the only one in `chronometrist-file' with that :name, the :name should be removed from `chronometrist-task-list', but to ascertain that condition we would have to either read the entire file or map over the hash table, defeating the optimization. Thus, we don't update `chronometrist-task-list' here (unlike `chronometrist-sexp-new')
(chronometrist-tags-history-replace-last plist)
(setq chronometrist--inhibit-read-p t)
(save-buffer)))
(defun chronometrist-sexp-reindent-buffer ()
"Reindent the current buffer.
This is meant to be run in `chronometrist-file' when using the s-expression backend."
(interactive)
(let (expr)
(goto-char (point-min))
(while (setq expr (ignore-errors (read (current-buffer))))
(backward-list)
(chronometrist-sexp-delete-list)
(when (looking-at "\n*")
(delete-region (match-beginning 0)
(match-end 0)))
(chronometrist-plist-pp expr (current-buffer))
(insert "\n")
(unless (eobp)
(insert "\n")))))
(provide 'chronometrist-sexp)
;;; chronometrist-sexp.el ends here

View File

@ -64,9 +64,10 @@
This must be a plist in the form (:MODE :START :END).
:MODE is either 'week, 'month, 'year, 'full, or 'custom. 'week,
'month, and 'year mean display statistics weekly/monthly/yearly
respectively.
:MODE is either 'week, 'month, 'year, 'full, or 'custom.
'week, 'month, and 'year mean display statistics
weekly/monthly/yearly respectively.
'full means display statistics from the beginning to the end of
the `chronometrist-file'.
@ -74,7 +75,7 @@ the `chronometrist-file'.
'custom means display statistics from an arbitrary date range.
:START and :END are the start and end of the date range to be
displayed. They must be dates in the form (YEAR MONTH DAY).")
displayed. They must be ts structs (see `ts.el').")
(defvar chronometrist-statistics--point nil)
@ -82,32 +83,30 @@ displayed. They must be dates in the form (YEAR MONTH DAY).")
;; ## FUNCTIONS ##
(defun chronometrist-statistics-count-average-time-spent (task &optional table)
(cl-defun chronometrist-statistics-count-average-time-spent (task &optional (table chronometrist-events))
"Return the average time the user has spent on TASK from TABLE.
TABLE must be a hash table - if not supplied,
`chronometrist-events' is used.
This will not return correct results if TABLE contains records
which span midnights. (see `chronometrist-events-clean')"
(let ((table (if table table chronometrist-events))
(days 0)
TABLE should be a hash table - if not supplied,
`chronometrist-events' is used."
;; (cl-loop
;; for date being the hash-keys of table
;; (let ((events-in-day (chronometrist-task-events-in-day task (chronometrist-iso-date->ts key))))
;; (when events-in-day)))
(let ((days 0)
(per-day-time-list))
(maphash (lambda (key _value)
(let ((events-in-day (chronometrist-task-events-in-day task key)))
(let ((events-in-day (chronometrist-task-events-in-day task (chronometrist-iso-date->ts key))))
(when events-in-day
(setq days (1+ days))
(->> events-in-day
(chronometrist-events->time-list)
(chronometrist-time-list->sum-of-intervals)
(->> (chronometrist-events->ts-pairs events-in-day)
(chronometrist-ts-pairs->durations)
(-reduce #'+)
(list)
(append per-day-time-list)
(setq per-day-time-list)))))
table)
(if per-day-time-list
(--> per-day-time-list
(-reduce #'time-add it)
(cadr it)
(--> (-reduce #'+ per-day-time-list)
(/ it days))
0)))
@ -130,7 +129,6 @@ reduced to the desired range using
"-"
active-days)))
(average-time (->> (chronometrist-statistics-count-average-time-spent task table)
(chronometrist-seconds-to-hms)
(chronometrist-format-time)
(format "% 5s")))
(content (vector task
@ -148,17 +146,14 @@ reduced to the desired range using
('week
(let* ((start (plist-get chronometrist-statistics--ui-state :start))
(end (plist-get chronometrist-statistics--ui-state :end))
(table (chronometrist-events-subset start end)))
(chronometrist-statistics-entries-internal table)))
(ht (chronometrist-events-subset start end)))
(chronometrist-statistics-entries-internal ht)))
(t ;; `chronometrist-statistics--ui-state' is nil, show current week's data
(let* ((start-long (chronometrist-previous-week-start (chronometrist-date)))
(end-long (time-add start-long
(* chronometrist-seconds-in-day 7)))
(start (chronometrist-calendrical->date start-long))
(end (chronometrist-calendrical->date end-long))
(table (chronometrist-events-subset start end)))
(let* ((start (chronometrist-previous-week-start (chronometrist-date)))
(end (ts-adjust 'day 7 start))
(ht (chronometrist-events-subset start end)))
(setq chronometrist-statistics--ui-state `(:mode week :start ,start :end ,end))
(chronometrist-statistics-entries-internal table)))))
(chronometrist-statistics-entries-internal ht)))))
(defun chronometrist-statistics-print-keybind (command &optional description firstonly)
"Insert the keybindings for COMMAND.
@ -171,11 +166,6 @@ If FIRSTONLY is non-nil, return only the first keybinding found."
" - "
(if description description "")))
(defun chronometrist-statistics-format-date (date)
"Return DATE (YEAR MONTH DAY) as a string in the form \"YYYY-MM-DD\"."
(-let [(year month day) date]
(format "%04d-%02d-%02d" year month day)))
(defun chronometrist-statistics-print-non-tabular ()
"Print the non-tabular part of the buffer in `chronometrist-statistics'."
(let ((w "\n ")
@ -189,8 +179,8 @@ If FIRSTONLY is non-nil, return only the first keybinding found."
(insert ", from")
(insert
(format " %s to %s\n"
(plist-get chronometrist-statistics--ui-state :start)
(plist-get chronometrist-statistics--ui-state :end)))))
(ts-format "%F" (plist-get chronometrist-statistics--ui-state :start))
(ts-format "%F" (plist-get chronometrist-statistics--ui-state :end))))))
(defun chronometrist-statistics-refresh (&optional _ignore-auto _noconfirm)
"Refresh the `chronometrist-statistics' buffer.
@ -211,7 +201,7 @@ value of `revert-buffer-function'."
(defvar chronometrist-statistics-mode-map
(let ((map (make-sparse-keymap)))
(define-key map (kbd "l") #'chronometrist-open-file)
(define-key map (kbd "l") #'chronometrist-open-log)
(define-key map (kbd "b") #'chronometrist-statistics-previous-range)
(define-key map (kbd "f") #'chronometrist-statistics-next-range)
map)
@ -258,22 +248,19 @@ data from the current week. Otherwise, display data from the week
specified by `chronometrist-statistics--ui-state'."
(interactive)
(chronometrist-migrate-check)
(let* ((buffer (get-buffer-create chronometrist-statistics-buffer-name))
(today (chronometrist-date))
(week-start (chronometrist-previous-week-start today))
(week-end (time-add week-start
`(0 ,(* 6 chronometrist-seconds-in-day))))
(week-start-iso (chronometrist-date week-start))
(week-end-iso (chronometrist-date week-end)))
(let* ((buffer (get-buffer-create chronometrist-statistics-buffer-name))
(today (chronometrist-date))
(week-start (chronometrist-previous-week-start today))
(week-end (ts-adjust 'day 6 week-start)))
(with-current-buffer buffer
(cond ((get-buffer-window chronometrist-statistics-buffer-name)
(kill-buffer buffer))
(t ;; (delete-other-windows)
(unless preserve-state
(setq chronometrist-statistics--ui-state `(:mode week
:start ,week-start-iso
:end ,week-end-iso)))
(chronometrist-common-create-chronometrist-file)
(setq chronometrist-statistics--ui-state `(:mode week
:start ,week-start
:end ,week-end)))
(chronometrist-common-create-file)
(chronometrist-statistics-mode)
(switch-to-buffer buffer)
(chronometrist-statistics-refresh))))))
@ -283,24 +270,16 @@ specified by `chronometrist-statistics--ui-state'."
If ARG is a numeric argument, go back that many times."
(interactive "P")
(let* ((arg (if (and arg (numberp arg))
(abs arg)
1))
(start-unix (->> (plist-get chronometrist-statistics--ui-state :start)
(chronometrist-iso-date->timestamp)
(parse-iso8601-time-string))))
(let* ((arg (if (and arg (numberp arg))
(abs arg)
1))
(start (plist-get chronometrist-statistics--ui-state :start)))
(cl-case (plist-get chronometrist-statistics--ui-state :mode)
('week
(let* ((new-start (time-subtract start-unix
(* 7 chronometrist-seconds-in-day arg)))
(new-end (time-add new-start
(* 6 chronometrist-seconds-in-day))))
(plist-put chronometrist-statistics--ui-state
:start
(chronometrist-date new-start))
(plist-put chronometrist-statistics--ui-state
:end
(chronometrist-date new-end)))))
(let* ((new-start (ts-adjust 'day (- (* arg 7)) start))
(new-end (ts-adjust 'day +6 new-start)))
(plist-put chronometrist-statistics--ui-state :start new-start)
(plist-put chronometrist-statistics--ui-state :end new-end))))
(setq chronometrist-statistics--point (point))
(kill-buffer)
(chronometrist-statistics t)))
@ -313,21 +292,13 @@ If ARG is a numeric argument, go forward that many times."
(let* ((arg (if (and arg (numberp arg))
(abs arg)
1))
(start-unix (->> (plist-get chronometrist-statistics--ui-state :start)
(chronometrist-iso-date->timestamp)
(parse-iso8601-time-string))))
(start (plist-get chronometrist-statistics--ui-state :start)))
(cl-case (plist-get chronometrist-statistics--ui-state :mode)
('week
(let* ((new-start (time-add start-unix
(* 7 chronometrist-seconds-in-day arg)))
(new-end (time-add new-start
(* 6 chronometrist-seconds-in-day))))
(plist-put chronometrist-statistics--ui-state
:start
(chronometrist-date new-start))
(plist-put chronometrist-statistics--ui-state
:end
(chronometrist-date new-end)))))
(let* ((new-start (ts-adjust 'day (* arg 7) start))
(new-end (ts-adjust 'day 6 new-start)))
(plist-put chronometrist-statistics--ui-state :start new-start)
(plist-put chronometrist-statistics--ui-state :end new-end))))
(setq chronometrist-statistics--point (point))
(kill-buffer)
(chronometrist-statistics t)))

115
elisp/chronometrist-time.el Normal file
View File

@ -0,0 +1,115 @@
;;; chronometrist-time.el --- Time and date functions for Chronometrist -*- lexical-binding: t; -*-
;; Author: contrapunctus <xmpp:contrapunctus@jabber.fr>
(require 'parse-time)
(require 'dash)
(require 's)
(require 'chronometrist-report-custom)
(declare-function chronometrist-day-start "chronometrist-events.el")
;; This is free and unencumbered software released into the public domain.
;;
;; Anyone is free to copy, modify, publish, use, compile, sell, or
;; distribute this software, either in source code form or as a compiled
;; binary, for any purpose, commercial or non-commercial, and by any
;; means.
;;
;; For more information, please refer to <https://unlicense.org>
;;; Commentary:
;; Pretty sure quite a few of these are redundant. Hopefully putting
;; them together in the same file will make it easier to figure out
;; which ones those are.
;;; Code:
(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)
(decode-time
(parse-iso8601-time-string timestamp))]
(ts-update
(make-ts :hour hour :minute minute :second second
:day day :month month :year year
:dow dow :tz-offset utcoff))))
(defun chronometrist-iso-date->ts (date)
"Return a ts struct (see `ts.el') representing DATE.
DATE should be an ISO-8601 date string (\"YYYY-MM-DD\")."
(let* ((date-list (mapcar #'string-to-number
(split-string date "-")))
(day (caddr date-list))
(month (cadr date-list))
(year (car date-list)))
(ts-update
(make-ts :hour 0 :minute 0 :second 0
:day day :month month :year year))))
(cl-defun chronometrist-date (&optional (ts (ts-now)))
"Return a ts struct representing the time 00:00:00 on today's date.
If TS is supplied, use that date instead of today.
TS should be a ts struct (see `ts.el')."
(ts-apply :hour 0 :minute 0 :second 0 ts))
(defun chronometrist-format-time-iso8601 (&optional unix-time)
"Return current moment as an ISO-8601 format time string.
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.
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)))))
(defun chronometrist-seconds-to-hms (seconds)
"Convert SECONDS to a vector in the form [HOURS MINUTES SECONDS].
SECONDS must be a positive integer."
(let* ((seconds (truncate seconds))
(s (% seconds 60))
(m (% (/ seconds 60) 60))
(h (/ seconds 3600)))
(list h m s)))
(defun chronometrist-interval (event)
"Return the period of time covered by EVENT as a time value.
EVENT should be a plist (see `chronometrist-file')."
(let ((start (plist-get event :start))
(stop (plist-get event :stop)))
(time-subtract (parse-iso8601-time-string stop)
(parse-iso8601-time-string start))))
(provide 'chronometrist-time)
;; Local Variables:
;; nameless-current-name: "chronometrist"
;; End:
;;; chronometrist-time.el ends here

View File

@ -18,7 +18,7 @@
(require 'chronometrist-custom)
(require 'chronometrist-statistics-custom)
(require 'chronometrist-report-custom)
(require 'chronometrist-common)
(require 'chronometrist)
(declare-function chronometrist-refresh "chronometrist.el")
(declare-function chronometrist-report-refresh "chronometrist-report.el")

View File

@ -12,13 +12,11 @@
(require 'subr-x)
(require 'chronometrist-common)
(require 'chronometrist-timer)
(require 'chronometrist-custom)
(require 'chronometrist-report)
(require 'chronometrist-statistics)
(require 'chronometrist-sexp)
(require 'chronometrist-key-values)
(require 'chronometrist-queries)
(require 'chronometrist-migrate)
(require 'chronometrist-sexp)
;; This is free and unencumbered software released into the public domain.
;;
@ -62,15 +60,43 @@
;; ## VARIABLES ##
;;; Code:
;; `chronometrist-goals' is an optional extension. But even these don't make the
;; warnings go away :\
(defvar chronometrist-goals-list)
(declare-function 'chronometrist-get-goal "chronometrist-goals")
(autoload 'chronometrist-maybe-start-timer "chronometrist-timer" nil t)
(autoload 'chronometrist-report "chronometrist-report" nil t)
(autoload 'chronometrist-statistics "chronometrist-statistics" nil t)
(defvar chronometrist--task-history nil)
(defvar chronometrist--point nil)
(defvar chronometrist--inhibit-read-p nil)
(defvar chronometrist-mode-map)
;; ## FUNCTIONS ##
(defun chronometrist-open-log (&optional _button)
"Open `chronometrist-file' in another window.
Argument _BUTTON is for the purpose of using this command as a
button action."
(interactive)
(chronometrist-sexp-open-log))
(defun chronometrist-common-create-file ()
"Create `chronometrist-file' if it doesn't already exist."
(chronometrist-sexp-create-file))
(defun chronometrist-task-active? (task)
"Return t if TASK is currently clocked in, else nil."
(equal (chronometrist-current-task) task))
(defun chronometrist-use-goals? ()
"Return t if `chronometrist-goals' is available and
`chronometrist-goals-list' is bound."
(and (featurep 'chronometrist-goals)
(bound-and-true-p chronometrist-goals-list)))
(defun chronometrist-activity-indicator ()
"Return a string to indicate that a task is active.
See custom variable `chronometrist-activity-indicator'."
@ -88,16 +114,26 @@ See custom variable `chronometrist-activity-indicator'."
(->> chronometrist-task-list
(-sort #'string-lessp)
(--map-indexed
(list it
(vector (number-to-string (1+ it-index))
(list it
'action 'chronometrist-toggle-task-button
'follow-link t)
(-> (chronometrist-task-time-one-day it)
(chronometrist-format-time))
(if (chronometrist-task-active? it)
(chronometrist-activity-indicator)
""))))))
(let* ((task it)
(index (number-to-string (1+ it-index)))
(task-button (list task
'action 'chronometrist-toggle-task-button
'follow-link t))
(task-time (chronometrist-format-time (chronometrist-task-time-one-day task)))
(indicator (if (chronometrist-task-active? task)
(chronometrist-activity-indicator)
""))
(use-goals (chronometrist-use-goals?))
(target (when use-goals
;; this can return nil if there is no goal for a task
(chronometrist-get-goal task)))
(target-str (if target
(format "% 4d" target)
"")))
(list task
(vconcat (vector index task-button task-time indicator)
(when use-goals
(vector target-str))))))))
(defun chronometrist-task-at-point ()
"Return the task at point in the `chronometrist' buffer, or nil if there is no task at point."
@ -115,8 +151,7 @@ See custom variable `chronometrist-activity-indicator'."
(defun chronometrist-goto-last-task ()
"In the `chronometrist' buffer, move point to the line containing the last active task."
(goto-char (point-min))
;; FIXME
;; (re-search-forward timeclock-last-project nil t)
(re-search-forward (plist-get (chronometrist-last) :name) nil t)
(beginning-of-line))
(defun chronometrist-print-keybind (command &optional description firstonly)
@ -138,45 +173,24 @@ If FIRSTONLY is non-nil, return only the first keybinding found."
(w "\n ")
;; (keybind-start-new (chronometrist-format-keybinds 'chronometrist-add-new-task
;; chronometrist-mode-map))
(keybind-toggle (chronometrist-format-keybinds 'chronometrist-toggle-task
chronometrist-mode-map
t)))
(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))
(insert "\n")
(insert w (format "% 17s" "Keys")
w (format "% 17s" "----"))
(insert w (format "% 17s" "Keys") w (format "% 17s" "----"))
(chronometrist-print-keybind 'chronometrist-add-new-task)
(insert-text-button "start a new task"
'action #'chronometrist-add-new-task-button
'follow-link t)
(chronometrist-print-keybind 'chronometrist-toggle-task
"toggle task at point")
(chronometrist-print-keybind 'chronometrist-toggle-task-no-hooks
"toggle without running hooks")
(insert "\n " (format "%s %s - %s"
"<numeric argument N>"
keybind-toggle
"toggle <N>th task"))
(insert-text-button "start a new task" 'action #'chronometrist-add-new-task-button 'follow-link t)
(chronometrist-print-keybind 'chronometrist-toggle-task "toggle task at point")
(chronometrist-print-keybind 'chronometrist-toggle-task-no-hooks "toggle without running hooks")
(insert "\n " (format "%s %s - %s" "<numeric argument N>" keybind-toggle "toggle <N>th task"))
(chronometrist-print-keybind 'chronometrist-report)
(insert-text-button "see weekly report"
'action #'chronometrist-report
'follow-link t)
(chronometrist-print-keybind 'chronometrist-open-file)
(insert-text-button "open log file"
'action #'chronometrist-open-file
'follow-link t)
(insert-text-button "see weekly report" 'action #'chronometrist-report 'follow-link t)
(chronometrist-print-keybind 'chronometrist-open-log)
(insert-text-button "view/edit log file" 'action #'chronometrist-open-log 'follow-link t)
(insert "\n"))))
(defun chronometrist-goto-nth-task (n)
@ -207,22 +221,43 @@ value of `revert-buffer-function'."
"Re-read `chronometrist-file' and refresh the `chronometrist' buffer.
Argument _FS-EVENT is ignored."
;; (chronometrist-file-clean)
(chronometrist-events-populate)
(setq chronometrist-task-list (chronometrist-tasks-from-table))
(chronometrist-tags-history-populate)
(run-hooks 'chronometrist-file-change-hook)
;; REVIEW - can we move most/all of this to the `chronometrist-file-change-hook'?
(if chronometrist--inhibit-read-p
(setq chronometrist--inhibit-read-p nil)
(chronometrist-events-populate)
(setq chronometrist-task-list (chronometrist-tasks-from-table))
(chronometrist-tags-history-populate))
(chronometrist-key-history-populate)
(chronometrist-value-history-populate)
(chronometrist-refresh))
(defun chronometrist-query-stop ()
"Ask the user if they would like to clock out."
(interactive)
(let ((task (chronometrist-current-task)))
(and task
(yes-or-no-p (concat "Stop tracking time for " task "? "))
(chronometrist-out))
t))
(defun chronometrist-in (task &optional _prefix)
"Clock in to TASK; record current time in `chronometrist-file'.
TASK is the name of the task, a string.
PREFIX is ignored."
(interactive "P")
(let ((plist `(:name ,task :start ,(chronometrist-format-time-iso8601))))
(chronometrist-sexp-new plist)
(chronometrist-refresh)))
(defun chronometrist-out (&optional _prefix)
"Record current moment as stop time to last s-exp in `chronometrist-file'.
PREFIX is ignored."
(interactive "P")
(let ((plist (plist-put (chronometrist-last) :stop
(chronometrist-format-time-iso8601))))
(chronometrist-sexp-replace-last plist)))
;; ## HOOKS ##
(defvar chronometrist-before-in-functions nil
@ -256,6 +291,9 @@ return a non-nil value.")
Each function in this hook must accept a single argument, which
is the name of the task to be clocked out of.")
(defvar chronometrist-file-change-hook nil
"Functions to be run after `chronometrist-file' is changed on disk.")
(defun chronometrist-run-functions-and-clock-in (task)
"Run hooks and clock in to TASK."
(run-hook-with-args 'chronometrist-before-in-functions task)
@ -273,7 +311,7 @@ is the name of the task to be clocked out of.")
(let ((map (make-sparse-keymap)))
(define-key map (kbd "RET") #'chronometrist-toggle-task)
(define-key map (kbd "M-RET") #'chronometrist-toggle-task-no-hooks)
(define-key map (kbd "l") #'chronometrist-open-file)
(define-key map (kbd "l") #'chronometrist-open-log)
(define-key map (kbd "r") #'chronometrist-report)
(define-key map [mouse-1] #'chronometrist-toggle-task)
(define-key map [mouse-3] #'chronometrist-toggle-task-no-hooks)
@ -284,10 +322,13 @@ is the name of the task to be clocked out of.")
(define-derived-mode chronometrist-mode tabulated-list-mode "Chronometrist"
"Major mode for `chronometrist'."
(make-local-variable 'tabulated-list-format)
(setq tabulated-list-format [("#" 3 t)
("Task" 25 t)
("Time" 10 t)
("Active" 3 t)])
(setq tabulated-list-format
(vconcat [("#" 3 t)
("Task" 25 t)
("Time" 10 t)
("Active" 10 t)]
(when (chronometrist-use-goals?)
[("Target" 3 t)])))
(make-local-variable 'tabulated-list-entries)
(setq tabulated-list-entries 'chronometrist-entries)
(make-local-variable 'tabulated-list-sort-key)
@ -303,6 +344,8 @@ is the name of the task to be clocked out of.")
Argument _BUTTON is for the purpose of using this as a button
action, and is ignored."
(when current-prefix-arg
(chronometrist-goto-nth-task (prefix-numeric-value current-prefix-arg)))
(let ((current (chronometrist-current-task))
(at-point (chronometrist-task-at-point)))
;; clocked in + point on current = clock out
@ -391,7 +434,8 @@ If numeric argument ARG is 2, run `chronometrist-statistics'."
(interactive "P")
(chronometrist-migrate-check)
(let ((buffer (get-buffer-create chronometrist-buffer-name))
(w (get-buffer-window chronometrist-buffer-name t)))
(w (save-excursion
(get-buffer-window chronometrist-buffer-name t))))
(cond
(arg (cl-case arg
(1 (chronometrist-report))
@ -403,7 +447,7 @@ If numeric argument ARG is 2, run `chronometrist-statistics'."
(cond ((or (not (file-exists-p chronometrist-file))
(chronometrist-common-file-empty-p chronometrist-file))
;; first run
(chronometrist-common-create-chronometrist-file)
(chronometrist-common-create-file)
(let ((inhibit-read-only t))
(chronometrist-common-clear-buffer buffer)
(insert "Welcome to Chronometrist! Hit RET to ")

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,42 @@
;;; -*- lexical-binding: t; -*-
(require 'buttercup)
(require 'chronometrist-common)
(describe
"chronometrist-format-time"
(it "works with lists"
(expect (chronometrist-format-time 0)
:to-equal " -")
(expect (chronometrist-format-time 1)
:to-equal " 1")
(expect (chronometrist-format-time 10)
:to-equal " 10")
(expect (chronometrist-format-time 70)
:to-equal " 1:10")
(expect (chronometrist-format-time (+ (* 10 60) ;; 10 minutes
10)) ;; 10 seconds
:to-equal " 10:10")
(expect (chronometrist-format-time (+ (* 1 60 60) ;; 1 hour
(* 10 60) ;; 10 minutes
10)) ;; 10 seconds
:to-equal " 1:10:10")
(expect (chronometrist-format-time (+ (* 10 60 60) ;; 10 hours
(* 10 60) ;; 10 minutes
10)) ;; 10 seconds
:to-equal "10:10:10")))
(describe
"chronometrist-previous-week-start"
:var ((chronometrist-report-week-start-day "Sunday")
(ts (chronometrist-iso-date->ts "2018-09-02")))
(it "should work with Sundays"
(should (ts= (chronometrist-previous-week-start
(chronometrist-iso-date->ts "2018-09-01"))
(chronometrist-iso-date->ts "2018-08-26")))
(should (ts= ts (chronometrist-previous-week-start (chronometrist-iso-date->ts "2018-09-02"))))
(should (ts= ts (chronometrist-previous-week-start (chronometrist-iso-date->ts "2018-09-03"))))
(should (ts= ts (chronometrist-previous-week-start (chronometrist-iso-date->ts "2018-09-04"))))
(should (ts= ts (chronometrist-previous-week-start (chronometrist-iso-date->ts "2018-09-05"))))
(should (ts= ts (chronometrist-previous-week-start (chronometrist-iso-date->ts "2018-09-06"))))
(should (ts= ts (chronometrist-previous-week-start (chronometrist-iso-date->ts "2018-09-07"))))
(should (ts= ts (chronometrist-previous-week-start (chronometrist-iso-date->ts "2018-09-08"))))))

View File

@ -1,5 +1,5 @@
(require 'buttercup)
(require 'chronometrist-sexp)
(require 'chronometrist-key-values)
(describe
"chronometrist-plist-remove"
@ -34,6 +34,22 @@
:d :a)
:to-equal '(:b 2 :c 3))))
(describe
"chronometrist-key-history"
(before-all
(setq chronometrist-file "tests/test.sexp")
(chronometrist-events-populate)
(setq chronometrist-task-list (chronometrist-tasks-from-table))
(chronometrist-key-history-populate))
(it "should have 6 keys"
(expect (hash-table-count chronometrist-key-history)
:to-be 6))
(it "should store multiple values"
(expect (length (gethash "Programming" chronometrist-key-history))
:to-be 3)
(expect (length (gethash "Arrangement/new edition" chronometrist-key-history))
:to-be 2)))
;; Local Variables:
;; nameless-current-name: "chronometrist"
;; End:

View File

@ -0,0 +1,41 @@
;; -*- lexical-binding: t; -*-
(require 'buttercup)
(require 'ts)
(require 'chronometrist-sexp)
(require 'chronometrist-events)
(require 'chronometrist-queries)
(require 'chronometrist-time)
(describe "chronometrist-task-time-one-day"
:var ((ts-1 (chronometrist-iso-date->ts "2018-01-01"))
(ts-2 (chronometrist-iso-date->ts "2018-01-02"))
(ts-3 (chronometrist-iso-date->ts "2018-01-03"))
(1-hour 3600))
(before-all
(setq chronometrist-file-old chronometrist-file
chronometrist-file "tests/test.sexp")
(chronometrist-events-populate))
(after-all
(setq chronometrist-file chronometrist-file-old))
(it "returns the time spent in one day, in seconds"
(expect (chronometrist-task-time-one-day "Programming" ts-1)
:to-equal 1-hour)
(expect (chronometrist-task-time-one-day "Swimming" ts-1)
:to-equal 1-hour)
(expect (chronometrist-task-time-one-day "Cooking" ts-1)
:to-equal 1-hour)
(expect (chronometrist-task-time-one-day "Guitar" ts-1)
:to-equal 1-hour)
(expect (chronometrist-task-time-one-day "Cycling" ts-1)
:to-equal 1-hour))
(it "works with midnight-crossing events"
(expect (chronometrist-task-time-one-day "Programming" ts-2)
:to-equal 1-hour)
(expect (chronometrist-task-time-one-day "Programming" ts-3)
:to-equal 1-hour)))
;; Local Variables:
;; nameless-current-name: "chronometrist"
;; End:

57
tests/test.sexp Normal file
View File

@ -0,0 +1,57 @@
(:name "Programming"
:start "2018-01-01T00:00:00+0530"
:stop "2018-01-01T01:00:00+0530")
(:name "Swimming"
:start "2018-01-01T02:00:00+0530"
:stop "2018-01-01T03:00:00+0530")
(:name "Cooking"
:start "2018-01-01T04:00:00+0530"
:stop "2018-01-01T05:00:00+0530")
(:name "Guitar"
:start "2018-01-01T06:00:00+0530"
:stop "2018-01-01T07:00:00+0530")
(:name "Cycling"
:start "2018-01-01T08:00:00+0530"
:stop "2018-01-01T09:00:00+0530")
(:name "Programming"
: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"
:component "goals"
:start "2020-05-09T20:03:25+0530"
:stop "2020-05-09T20:05:55+0530")
(:name "Arrangement/new edition"
:tags (new edition)
:song "Songs of Travel"
:composer "Vaughan Williams, Ralph"
:start "2020-05-10T00:04:14+0530"
:stop "2020-05-10T00:25:48+0530")
(:name "Guitar"
:tags (classical warm-up)
:start "2020-05-10T15:41:14+0530"
:stop "2020-05-10T15:55:42+0530")
(:name "Guitar"
:tags (classical solo)
:start "2020-05-10T16:00:00+0530"
:stop "2020-05-10T16:30:00+0530")
(:name "Programming"
:tags (reading)
:book "Smalltalk-80: The Language and Its Implementation"
:start "2020-05-10T16:33:17+0530"
:stop "2020-05-10T17:10:48+0530")