alphapapa / ts.el

Emacs timestamp and date-time library
GNU General Public License v3.0
177 stars 15 forks source link
date-time emacs library timestamp

+TITLE: ts.el

+PROPERTY: LOGGING nil

[[https://melpa.org/#/ts][file:https://melpa.org/packages/ts-badge.svg]] [[https://stable.melpa.org/#/ts][file:https://stable.melpa.org/packages/ts-badge.svg]]

~ts~ is a date and time library for Emacs. It aims to be more convenient than patterns like ~(string-to-number (format-time-string "%Y"))~ by providing easy accessors, like ~(ts-year (ts-now))~.

To improve performance (significantly), formatted date parts are computed lazily rather than when a timestamp object is instantiated, and the computed parts are then cached for later access without recomputing. Behind the scenes, this avoids unnecessary ~(string-to-number (format-time-string...~ calls, which are surprisingly expensive.

Get parts of the current date:

+BEGIN_SRC elisp

;; When the current date is 2018-12-08 23:09:14 -0600: (ts-year (ts-now)) ;=> 2018 (ts-month (ts-now)) ;=> 12 (ts-day (ts-now)) ;=> 8 (ts-hour (ts-now)) ;=> 23 (ts-minute (ts-now)) ;=> 9 (ts-second (ts-now)) ;=> 14 (ts-tz-offset (ts-now)) ;=> "-0600"

(ts-dow (ts-now)) ;=> 6 (ts-day-abbr (ts-now)) ;=> "Sat" (ts-day-name (ts-now)) ;=> "Saturday"

(ts-month-abbr (ts-now)) ;=> "Dec" (ts-month-name (ts-now)) ;=> "December"

(ts-tz-abbr (ts-now)) ;=> "CST"

+END_SRC

Increment the current date:

+BEGIN_SRC elisp

;; By 10 years: (list :now (ts-format) :future (ts-format (ts-adjust 'year 10 (ts-now)))) ;;=> ( :now "2018-12-15 22:00:34 -0600" ;; :future "2028-12-15 22:00:34 -0600")

;; By 10 years, 2 months, 3 days, 5 hours, and 4 seconds: (list :now (ts-format) :future (ts-format (ts-adjust 'year 10 'month 2 'day 3 'hour 5 'second 4 (ts-now)))) ;;=> ( :now "2018-12-15 22:02:31 -0600" ;; :future "2029-02-19 03:02:35 -0600")

+END_SRC

What day of the week was 2 days ago?

+BEGIN_SRC elisp

(ts-day-name (ts-dec 'day 2 (ts-now))) ;=> "Thursday"

;; Or, with threading macros: (thread-last (ts-now) (ts-dec 'day 2) ts-day-name) ;=> "Thursday" (->> (ts-now) (ts-dec 'day 2) ts-day-name) ;=> "Thursday"

+END_SRC

Get timestamp for this time last week:

+BEGIN_SRC elisp

(ts-unix (ts-adjust 'day -7 (ts-now))) ;;=> 1543728398.0

;; To confirm that the difference really is 7 days: (/ (- (ts-unix (ts-now)) (ts-unix (ts-adjust 'day -7 (ts-now)))) 86400) ;;=> 7.000000567521762

;; Or human-friendly as a list: (ts-human-duration (ts-difference (ts-now) (ts-dec 'day 7 (ts-now)))) ;;=> (:years 0 :days 7 :hours 0 :minutes 0 :seconds 0)

;; Or as a string: (ts-human-format-duration (ts-difference (ts-now) (ts-dec 'day 7 (ts-now)))) ;;=> "7 days"

;; Or confirm by formatting: (list :now (ts-format) :last-week (ts-format (ts-dec 'day 7 (ts-now)))) ;;=> ( :now "2018-12-08 23:31:37 -0600" ;; :last-week "2018-12-01 23:31:37 -0600")

+END_SRC

Some accessors have aliases similar to ~format-time-string~ constructors:

+BEGIN_SRC elisp

(ts-hour (ts-now)) ;=> 0 (ts-H (ts-now)) ;=> 0

(ts-minute (ts-now)) ;=> 56 (ts-min (ts-now)) ;=> 56 (ts-M (ts-now)) ;=> 56

(ts-second (ts-now)) ;=> 38 (ts-sec (ts-now)) ;=> 38 (ts-S (ts-now)) ;=> 38

(ts-year (ts-now)) ;=> 2018 (ts-Y (ts-now)) ;=> 2018

(ts-month (ts-now)) ;=> 12 (ts-m (ts-now)) ;=> 12

(ts-day (ts-now)) ;=> 9 (ts-d (ts-now)) ;=> 9

+END_SRC

Parse a string into a timestamp object and reformat it:

+BEGIN_SRC elisp

(ts-format (ts-parse "sat dec 8 2018 12:12:12")) ;=> "2018-12-08 12:12:12 -0600"

;; With a threading macro: (->> "sat dec 8 2018 12:12:12" ts-parse ts-format) ;;=> "2018-12-08 12:12:12 -0600"

+END_SRC

Format the difference between two timestamps:

+BEGIN_SRC elisp

(ts-human-format-duration (ts-difference (ts-now) (ts-adjust 'day -400 'hour -2 'minute -1 'second -5 (ts-now)))) ;; => "1 years, 35 days, 2 hours, 1 minutes, 5 seconds"

;; Abbreviated: (ts-human-format-duration (ts-difference (ts-now) (ts-adjust 'day -400 'hour -2 'minute -1 'second -5 (ts-now))) 'abbr) ;; => "1y35d2h1m5s"

+END_SRC

Parse an Org timestamp element directly from ~org-element-context~ and find the difference between it and now:

+BEGIN_SRC elisp

(with-temp-buffer (org-mode) (save-excursion (insert "<2015-09-24 Thu .+1d>")) (ts-human-format-duration (ts-difference (ts-now) (ts-parse-org-element (org-element-context))))) ;;=> "3 years, 308 days, 2 hours, 24 minutes, 21 seconds"

+END_SRC

Parse an Org timestamp string (which has a repeater) and format the year and month:

+BEGIN_SRC elisp

;; Note the use of format' rather thanconcat', because `ts-year' ;; returns the year as a number rather than a string.

(let* ((ts (ts-parse-org "<2015-09-24 Thu .+1d>"))) (format "%s, %s" (ts-month-name ts) (ts-year ts))) ;;=> "September, 2015"

;; Or, using dash.el:

(--> (ts-parse-org "<2015-09-24 Thu .+1d>") (format "%s, %s" (ts-month-name it) (ts-year it))) ;;=> "September, 2015"

;; Or, if you remember the format specifiers:

(ts-format "%B, %Y" (ts-parse-org "<2015-09-24 Thu .+1d>")) ;;=> "September, 2015"

+END_SRC

How long ago was this date in 1970?

+BEGIN_SRC elisp

(let* ((now (ts-now)) (then (ts-apply :year 1970 now))) (list (ts-format then) (ts-human-format-duration (ts-difference now then)))) ;;=> ("1970-08-04 07:07:10 -0500" ;; "49 years, 12 days")

+END_SRC

How long ago did the epoch begin?

+BEGIN_SRC elisp

(ts-human-format-duration (ts-diff (ts-now) (make-ts :unix 0))) ;;=> "49 years, 227 days, 12 hours, 12 minutes, 30 seconds"

+END_SRC

In which of the last 100 years was Christmas on a Saturday?

+BEGIN_SRC elisp

(let ((ts (ts-parse "2019-12-25")) (limit (- (ts-year (ts-now)) 100))) (cl-loop while (>= (ts-year ts) limit) when (string= "Saturday" (ts-day-name ts)) collect (ts-year ts) do (ts-decf (ts-year ts)))) ;;=> (2010 2004 1999 1993 1982 1976 1971 1965 1954 1948 1943 1937 1926 1920)

+END_SRC

For a more interesting example, does a timestamp fall within the previous calendar week?

+BEGIN_SRC elisp

;; First, define a function to return the range of the previous calendar week.

(defun last-week-range () "Return timestamps (BEG . END) spanning the previous calendar week." (let* (;; Bind now' to the current timestamp to ensure all calculations ;; begin from the same timestamp. (In the unlikely event that ;; the execution of this code spanned from one day into the next, ;; that would cause a wrong result.) (now (ts-now)) ;; We start by calculating the offsets for the beginning and ;; ending timestamps using the current day of the week. Note ;; that thets-dow' slot uses the "%w" format specifier, which ;; counts from Sunday to Saturday as a number from 0 to 6. (adjust-beg-day (- (+ 7 (ts-dow now)))) (adjust-end-day (- (- 7 (- 6 (ts-dow now))))) ;; Make beginning/end timestamps based on now', with adjusted ;; day and hour/minute/second values. These functions return ;; new timestamps, sonow' is unchanged. (beg (thread-last now ;; ts-adjust' makes relative adjustments to timestamps. (ts-adjust 'day adjust-beg-day) ;;ts-apply' applies absolute values to timestamps. (ts-apply :hour 0 :minute 0 :second 0))) (end (thread-last now (ts-adjust 'day adjust-end-day) (ts-apply :hour 23 :minute 59 :second 59)))) (cons beg end)))

(-let* (;; Bind the default format string for `ts-format', so the ;; results are easy to understand. (ts-default-format "%a, %Y-%m-%d %H:%M:%S %z") ;; Get the timestamp for 3 days before now. (check-ts (ts-adjust 'day -3 (ts-now))) ;; Get the range for the previous week from the function we defined. ((beg . end) (last-week-range))) (list :last-week-beg (ts-format beg) :check-ts (ts-format check-ts) :last-week-end (ts-format end) :in-range-p (ts-in beg end check-ts))) ;;=> (:last-week-beg "Sun, 2019-08-04 00:00:00 -0500" ;; :check-ts "Fri, 2019-08-09 10:00:34 -0500" ;; :last-week-end "Sat, 2019-08-10 23:59:59 -0500" ;; :in-range-p t)

+END_SRC

** Accessors

** Adjustors

Destructive

** Comparators

** Duration

** Formatting

** Parsing

** Misc.

** ~ts-human-format-duration~ vs. ~format-seconds~

Emacs has a built-in function, ~format-seconds~, that produces output similar to that of ~ts-human-format-duration~. Its output is also controllable with a format string. However, if the output of ~ts-human-format-duration~ is sufficient, it performs much better than ~format-seconds~. This simple benchmark, run 100,000 times, shows that it runs much faster and generates less garbage:

+BEGIN_SRC elisp

(bench-multi-lexical :times 100000 :forms (("ts-human-format-duration" (ts-human-format-duration 15780.910933971405 t)) ("format-seconds" (format-seconds "%yy%dd%hh%mm%ss%z" 15780.910933971405))))

+END_SRC

+RESULTS:

| Form | x faster than next | Total runtime | # of GCs | Total GC runtime | |--------------------------+--------------------+---------------+----------+------------------| | ts-human-format-duration | 5.82 | 0.832945 | 3 | 0.574929 | | format-seconds | slowest | 4.848253 | 17 | 3.288799 |

(See the [[https://github.com/alphapapa/emacs-package-dev-handbook][Emacs Package Developer's Handbook]] for the ~bench-multi-lexical~ macro.)

** 0.3

Added

** 0.2.1

Fixed

** 0.2

Added

Changed

Fixed

Documentation

** 0.1

First tagged release. Published to MELPA.

GPLv3

Local Variables:

eval: (require 'org-make-toc)

before-save-hook: org-make-toc

org-export-with-properties: ()

org-export-with-title: t

End: