265 lines
11 KiB
EmacsLisp
265 lines
11 KiB
EmacsLisp
(require 'timeclock)
|
|
(require 'dash)
|
|
(require 'cl-lib)
|
|
|
|
;; ## VARIABLES ##
|
|
(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-time-re-file "[0-9]\\{2\\}:[0-9]\\{2\\}:[0-9]\\{2\\}"
|
|
"Regular expression to represent a timestamp in the file
|
|
`timeclock-file'. This is distinct from
|
|
`chronometrist-time-re-ui' (which see).")
|
|
|
|
(defun chronometrist-buffer-exists? (buffer-name)
|
|
(--> (buffer-list)
|
|
(mapcar #'buffer-name it)
|
|
(member buffer-name it)))
|
|
|
|
(defun chronometrist-buffer-visible? (buffer-or-buffer-name)
|
|
"Returns 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-timestamp->list (date-time-string)
|
|
"Convert a string timestamp to a list of integers."
|
|
(--> date-time-string
|
|
(split-string it "[-/ :]")
|
|
(mapcar #'string-to-number it)))
|
|
|
|
(defun chronometrist-timestamp-list->seconds (date-time-list)
|
|
"Convert DATE-TIME (which must be a list in the form (YEAR
|
|
MONTH DAY HOURS MINUTES SECONDS), as returned by
|
|
`timestamp->list') to seconds since the UNIX epoch
|
|
(see (info \"(elisp)Time of Day\"))."
|
|
(->> date-time-list
|
|
(reverse)
|
|
(apply #'encode-time)))
|
|
|
|
(defun chronometrist-timestamp->seconds (date-time-string)
|
|
"Convert a string timestamp in the form \"HH:MM:SS\" to seconds
|
|
since the UNIX epoch (see (info \"(elisp)Time of Day\"))."
|
|
(chronometrist-timestamp-list->seconds
|
|
(chronometrist-timestamp->list date-time-string)))
|
|
|
|
(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)))))
|
|
|
|
(defun chronometrist-first-event-spans-midnight? (target-date project)
|
|
"Return t if the first event of PROJECT on TARGET-DATE in
|
|
`timeclock-file' spans midnight (i.e. its clock-in time was
|
|
before midnight), else nil.
|
|
|
|
TARGET-DATE must be a date in the form \"YYYY/MM/DD\", and
|
|
PROJECT a string."
|
|
;; Go to the first event for TARGET-DATE, ignoring the project
|
|
(goto-char (point-min))
|
|
(re-search-forward target-date nil t)
|
|
(beginning-of-line)
|
|
;; Is it a clock-out event?
|
|
(and (looking-at-p "^o ")
|
|
;; We have a midnight-spanning range. Is it a range for PROJECT?
|
|
(progn
|
|
(forward-line -1)
|
|
(beginning-of-line)
|
|
(looking-at-p (concat "i " chronometrist-date-re " " chronometrist-time-re-file " " project)))))
|
|
|
|
(defun chronometrist-get-end-time (target-date)
|
|
"Return the date and time of the next clock-out event after
|
|
point in the file `timeclock-file'.
|
|
|
|
If there is no clock-out event after point, return the current
|
|
date and time.
|
|
|
|
If the clock-out time is past midnight, return the date in
|
|
TARGET-DATE with the time at midnight (\"24:00:00\").
|
|
|
|
Return value is a string in the form \"YYYY/MM/DD HH:MM:SS\"
|
|
|
|
TARGET-DATE must be a date in the form \"YYYY/MM/DD\"
|
|
|
|
Point must be on a clock-in event having the same date as
|
|
TARGET-DATE."
|
|
(let* ((date-time (if (progn
|
|
(forward-line)
|
|
(beginning-of-line)
|
|
(looking-at-p "^o "))
|
|
(progn
|
|
(re-search-forward "o ")
|
|
(buffer-substring-no-properties (point)
|
|
(+ 10 1 8 (point))))
|
|
(format-time-string "%Y/%m/%d %T")))
|
|
(date-time-list (chronometrist-timestamp->list date-time))
|
|
(target-date-list (chronometrist-timestamp->list target-date)))
|
|
(if (chronometrist-time-interval-span-midnight? target-date-list date-time-list)
|
|
(concat target-date " 24:00:00")
|
|
date-time)))
|
|
|
|
;; The multiple calls to re-search-forward/backward to get point at
|
|
;; the right spot are just ugly :\ (See if you can use match data
|
|
;; instead)
|
|
;;
|
|
;; Could be refactored - one function to get ranges for an activity,
|
|
;; one to convert them to seconds, one to subtract them (get an
|
|
;; interval from two timestamps), and one to output the time vector
|
|
;; from chronometrist-seconds-to-hms in the desired format
|
|
;;
|
|
;; Better idea - get all events for target-date as a list of strings,
|
|
;; one per line. Operate on that. Will probably make for much nicer
|
|
;; code.
|
|
|
|
(defun chronometrist-project-time-one-day (project &optional date)
|
|
"Read `timeclock-file' and return total time spent on PROJECT
|
|
today or on DATE.
|
|
|
|
DATE must be a list containing calendrical information (see (info
|
|
\"(elisp)Time Conversion\")), the time for that date is shown,
|
|
otherwise calculate time for today.
|
|
|
|
The return value is a vector in the form [HOURS MINUTES SECONDS]"
|
|
(if (not (member project timeclock-project-list))
|
|
(error (concat "Unknown project: " project))
|
|
(with-current-buffer (find-file-noselect timeclock-file)
|
|
(save-excursion
|
|
(let* ((target-date (if date
|
|
(format "%04d/%02d/%02d"
|
|
(elt date 5)
|
|
(elt date 4)
|
|
(elt date 3))
|
|
(format-time-string "%Y/%m/%d")))
|
|
(search-re (concat target-date " " chronometrist-time-re-file " " project))
|
|
(interval-list nil)
|
|
(first-event-midnight (chronometrist-first-event-spans-midnight? target-date
|
|
project)))
|
|
;; (goto-char (point-min))
|
|
(while (if first-event-midnight
|
|
(re-search-forward chronometrist-time-re-file nil t 2)
|
|
(re-search-forward (concat "i " search-re) nil t))
|
|
(re-search-backward target-date nil t)
|
|
(let* ((start-time (if first-event-midnight
|
|
(concat target-date " 00:00:00")
|
|
(buffer-substring-no-properties
|
|
(point)
|
|
(+ 10 1 8 (point)))))
|
|
(end-time (if first-event-midnight
|
|
(buffer-substring-no-properties
|
|
(point)
|
|
(+ 10 1 8 (point)))
|
|
(chronometrist-get-end-time target-date)))
|
|
(start-time-s (chronometrist-timestamp->seconds start-time))
|
|
(end-time-s (chronometrist-timestamp->seconds end-time))
|
|
(interval (elt (time-subtract end-time-s
|
|
start-time-s)
|
|
1)))
|
|
(setq interval-list
|
|
(append interval-list (list interval))
|
|
;; quick hack
|
|
first-event-midnight nil)))
|
|
(->>
|
|
interval-list
|
|
(-reduce #'+)
|
|
(chronometrist-seconds-to-hms)))))))
|
|
|
|
;; tests -
|
|
;; (mapcar #'chronometrist-format-time
|
|
;; '((0 0 0) (0 0 1) (0 0 10) (0 1 10) (0 10 10) (1 10 10) (10 10 10)))
|
|
;; => ("" "00:01" "00:10" "01:10" "10:10" "01:10:10" "10:10:10")
|
|
;; (mapcar #'chronometrist-format-time
|
|
;; '([0 0 0] [0 0 1] [0 0 10] [0 1 10] [0 10 10] [1 10 10] [10 10 10]))
|
|
;; => ("" "00:01" "00:10" "01:10" "10:10" "01:10:10" "10:10:10")
|
|
(defun chronometrist-format-time (time)
|
|
"Format and display TIME as a string, where TIME is a vector or
|
|
a list of the form [HOURS MINUTES SECONDS] or (HOURS MINUTES
|
|
SECONDS)."
|
|
(let ((h (elt time 0))
|
|
(m (elt time 1))
|
|
(s (elt time 2))
|
|
(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-timeclock-file (&optional button)
|
|
(interactive)
|
|
(find-file-other-window timeclock-file)
|
|
(goto-char (point-max)))
|
|
|
|
(defun chronometrist-common-create-timeclock-file ()
|
|
"Create `timeclock-file' if it doesn't already exist."
|
|
(unless (file-exists-p timeclock-file)
|
|
(with-current-buffer (find-file-noselect timeclock-file)
|
|
(write-file timeclock-file))))
|
|
|
|
(defun chronometrist-common-file-empty-p (file)
|
|
(let ((size (elt (file-attributes file) 7)))
|
|
(if (zerop size) t nil)))
|
|
|
|
(defun chronometrist-common-clear-buffer (buffer)
|
|
(with-current-buffer buffer
|
|
(goto-char (point-min))
|
|
(delete-region (point-min) (point-max))))
|
|
|
|
(provide 'chronometrist-lib)
|
|
|
|
;; Local Variables:
|
|
;; nameless-current-name: "chronometrist"
|
|
;; End:
|