(require 'cl-lib)
(require 'widget)
(set-time-zone-rule "UTC")
(defgroup epoch nil
"Epoch Time Conversion."
:prefix "epoch-"
:group 'external
:group 'applications)
(defun truncate (arg &optional divisor)
"Truncate a floating point number to an int.
Rounds ARG toward zero.
With optional DIVISOR, truncate ARG/DIVISOR."
(when divisor
(setq arg (/ arg divisor)))
(string-to-number (replace-regexp-in-string "\\..*$" "" (number-to-string arg))))
(defun float-to-string (num)
"Return string version of float NUM.
If NUM ends with '.0', that part is removed."
(replace-regexp-in-string "\\.0$" "" (number-to-string num)))
(defun time-to-epoch (&optional time)
"Return epoch (seconds since 1970-01-01) from TIME (or current time if nil)."
(interactive)
(unless time
(setq time (format-time-string "%Y-%m-%d %H:%M:%S" nil t)))
(when (< (length time) 11)
(setq time (concat time " 00:00:00")))
(truncate (float-time (date-to-time time))))
(defun epoch-to-time (&optional epoch)
"Return time from EPOCH (or current time if nil)."
(interactive)
(unless epoch
(setq epoch (truncate (float-time (current-time)))))
(when (stringp epoch)
(setq epoch (string-to-number epoch)))
(format-time-string "%Y-%m-%d %H:%M:%S" (seconds-to-time epoch) t))
(defun epoch-parse-time (time &optional current-time)
"Return epoch time from parsing TIME.
Supported types:
Internal: (HIGH LOW . IGNORED)
ISO: YYYY-MM-DD HH:MM:SS.SSS
Decoded: (SECONDS MINUTES HOUR DAY MONTH YEAR DOW DST ZONE)
Epoch: SECONDS
Historic: -SECONDS
For decoded type any values may be nil and will be replaced with
those from the current time.
If CURRENT-TIME is non-nil, it is used in place of
`current-time'. This allows for making multiple calls using a
consistent time."
(if current-time
(setq current-time (seconds-to-time (epoch-parse-time current-time)))
(setq current-time (current-time)))
(cond
((listp time)
(cond
((<= (length time) 4)
(truncate (float-time time)))
((= (length time) 9)
(let ((time
(cl-do ((time time (cdr time))
(current-time (decode-time current-time) (cdr current-time))
result)
((not time) (nreverse result))
(push (or (car time) (car current-time)) result))))
(truncate (float-time (apply #'encode-time time)))))
(t nil)))
((numberp time)
(if (> time 0)
time
(+ (truncate (float-time current-time)) time)))
((stringp time)
(truncate (float-time (date-to-time time))))
(t nil)))
(defun epoch-parse-times (times)
"Return a list of Emacs times from parsing TIMES.
Supported types are listed in the `epoch-parse-time' function
definition."
(remove nil (mapcar 'epoch-parse-time times)))
(defcustom epoch-initial-time-examples
`((0 (nil nil nil nil nil nil nil nil nil))
(,(* -1 60 60) (nil nil nil nil nil nil nil nil nil))
(0 (0 0 9 nil nil nil nil nil nil))
(,(* -1 60 60 24) (nil nil nil nil nil nil nil nil nil))
(,(* -1 60 60 24 7) (0 0 9 nil nil nil nil nil nil))
(,(* -1 60 60 24 14) (0 0 9 nil nil nil nil nil nil))
(,(* -1 60 60 24 28) (0 0 9 nil nil nil nil nil nil)))
"Initial list of times to query.
List of tuples that are used for a two phase parse. The first
element of the tuple is used to call `epoch-parse-time' using a
fixed value for CURRENT-TIME based on `current-time'. The result
of that call is then passed in as CURRENT-TIME for a second call
to `epoch-parse-time', using the second value in the tuple. This
allows for time addition as well as time filtering.
Supported types:
Internal: (HIGH LOW . IGNORED)
ISO: YYYY-MM-DD HH:MM:SS.SSS
Decoded: (SECONDS MINUTES HOUR DAY MONTH YEAR DOW DST ZONE)
Epoch: SECONDS
Historic: -SECONDS
For decoded type any values may be nil and will be replaced with
those from the current time."
:type 'list
:group 'epoch)
(defvar epoch-time-examples
nil
"Examples of `time-to-epoch' and `epoch-to-time' times queried.
Initialized with `epoch-time-examples-reset' function.")
(defun epoch-time-examples-reset ()
"Reset `epoch-time-examples' to initial values."
(let ((time (current-time))
examples)
(dolist (entry epoch-initial-time-examples)
(push
(epoch-parse-time (cadr entry)
(epoch-parse-time (car entry) time))
examples))
(setq epoch-time-examples (nreverse examples))))
(epoch-time-examples-reset)
(defvar epoch-buffer-name
"*Epoch Time Conversion*"
"Buffer name to use for epoch interface.")
(defvar widget-time-epoch)
(defvar widget-time-string)
(defun epoch (&optional times epoch)
"Interactive epoch-to-time and time-to-epoch converter interface.
If TIMES is non-nil, it adds them to the examples list."
(interactive)
(when (and times (not (listp times)))
(setq times (list times)))
(let (buffer time-examples all-times last-time) (when (get-buffer epoch-buffer-name)
(kill-buffer epoch-buffer-name))
(setq buffer (get-buffer-create epoch-buffer-name))
(set-buffer buffer)
(kill-all-local-variables)
(make-local-variable 'widget-time-epoch)
(make-local-variable 'widget-time-string)
(when times
(dolist (time (epoch-parse-times times))
(cl-pushnew time time-examples :test '=)))
(dolist (times epoch-time-examples)
(cl-pushnew times time-examples :test '=))
(setq time-examples (nreverse time-examples))
(setq all-times (sort all-times '<))
(setq last-time (or (car times) (car epoch-time-examples) (truncate (float-time (current-time)))))
(widget-insert (concat (propertize "Epoch Time Conversion" 'face 'font-lock-keyword-face) "\n\n"))
(widget-insert (propertize "Epoch: " 'face 'font-lock-keyword-face))
(setq widget-time-epoch
(widget-create
'editable-field
:value (float-to-string last-time)
:size 10
:notify (lambda (widget &rest ignore)
(let* ((value (widget-value widget))
(time-string (ignore-errors (epoch-to-time (string-to-number value)))))
(when time-string
(save-excursion
(widget-value-set widget-time-string time-string)))))))
(widget-insert " ")
(widget-create 'push-button
:value "+"
:notify `(lambda (&rest ignore)
(let ((time-epoch (string-to-number (widget-value widget-time-epoch))))
(delete time-epoch epoch-time-examples)
(push time-epoch epoch-time-examples)
(kill-buffer nil)
(epoch ,times))))
(widget-insert "\n\n")
(widget-insert (propertize "Time: " 'face 'font-lock-keyword-face))
(setq widget-time-string
(widget-create
'editable-field
:value (epoch-to-time last-time)
:size 19
:notify (lambda (widget &rest ignore)
(let* ((value (widget-value widget))
(time-epoch (ignore-errors (float-to-string (time-to-epoch value)))))
(when time-epoch
(save-excursion
(widget-value-set widget-time-epoch time-epoch)))))))
(widget-insert " ")
(widget-create 'push-button
:value "+"
:notify `(lambda (&rest ignore)
(let ((time-epoch (string-to-number (widget-value widget-time-epoch))))
(delete time-epoch epoch-time-examples)
(push time-epoch epoch-time-examples)
(kill-buffer nil)
(epoch ,times))))
(widget-insert "\n\n")
(widget-insert (concat (propertize "Times:" 'face 'font-lock-keyword-face) "\n\n"))
(cl-do ((time-examples time-examples (cdr time-examples))
(cnt 1 (1+ cnt)))
((not time-examples))
(let ((time-epoch (car time-examples))
(time-string (epoch-to-time (car time-examples))))
(widget-insert " ")
(widget-insert (float-to-string time-epoch))
(widget-insert " ")
(widget-insert time-string)
(when (member time-epoch epoch-time-examples)
(widget-insert " ")
(widget-create 'push-button
:value "-"
:notify `(lambda (&rest ignore)
(setq epoch-time-examples (remove ,time-epoch epoch-time-examples))
(kill-buffer nil)
(epoch ,times)))))
(widget-insert "\n"))
(widget-insert "\n")
(widget-create 'push-button
:value "Configure"
:notify (lambda (&rest ignore)
(kill-buffer nil)
(customize-group "epoch")))
(widget-insert " ")
(widget-create 'push-button
:value "Reset Times"
:notify `(lambda (&rest ignore)
(kill-buffer nil)
(epoch-time-examples-reset)
(epoch ,times)))
(widget-insert "\n")
(use-local-map widget-keymap)
(widget-setup)
(switch-to-buffer buffer)
(goto-char (point-min))
(widget-forward 1)))
(provide 'epoch)