ts.el icon indicating copy to clipboard operation
ts.el copied to clipboard

Emacs timestamp and date-time library

#+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.

  • Contents :PROPERTIES: :TOC: :include siblings :ignore this :END: :CONTENTS:
  • [[#examples][Examples]]
  • [[#usage][Usage]]
    • [[#accessors][Accessors]]
    • [[#adjustors][Adjustors]]
    • [[#comparators][Comparators]]
    • [[#duration][Duration]]
    • [[#formatting][Formatting]]
    • [[#parsing][Parsing]]
    • [[#misc][Misc.]]
  • [[#tips][Tips]]
  • [[#changelog][Changelog]] :END:
  • Examples

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 than concat', 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 the ts-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, so now' 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

  • Usage

** Accessors

  • ~ts-B (STRUCT)~ :: Access slot "month-name" of ~ts~ struct ~STRUCT~.
  • ~ts-H (STRUCT)~ :: Access slot "hour" of ~ts~ struct ~STRUCT~.
  • ~ts-M (STRUCT)~ :: Access slot "minute" of ~ts~ struct ~STRUCT~.
  • ~ts-S (STRUCT)~ :: Access slot "second" of ~ts~ struct ~STRUCT~.
  • ~ts-Y (STRUCT)~ :: Access slot "year" of ~ts~ struct ~STRUCT~.
  • ~ts-b (STRUCT)~ :: Access slot "month-abbr" of ~ts~ struct ~STRUCT~.
  • ~ts-d (STRUCT)~ :: Access slot "day" of ~ts~ struct ~STRUCT~.
  • ~ts-day (STRUCT)~ :: Access slot "day" of ~ts~ struct ~STRUCT~.
  • ~ts-day-abbr (STRUCT)~ :: Access slot "day-abbr" of ~ts~ struct ~STRUCT~.
  • ~ts-day-name (STRUCT)~ :: Access slot "day-name" of ~ts~ struct ~STRUCT~.
  • ~ts-day-of-month-num (STRUCT)~ :: Access slot "day" of ~ts~ struct ~STRUCT~.
  • ~ts-day-of-week-abbr (STRUCT)~ :: Access slot "day-abbr" of ~ts~ struct ~STRUCT~.
  • ~ts-day-of-week-name (STRUCT)~ :: Access slot "day-name" of ~ts~ struct ~STRUCT~.
  • ~ts-day-of-week-num (STRUCT)~ :: Access slot "dow" of ~ts~ struct ~STRUCT~.
  • ~ts-day-of-year (STRUCT)~ :: Access slot "doy" of ~ts~ struct ~STRUCT~.
  • ~ts-dom (STRUCT)~ :: Access slot "day" of ~ts~ struct ~STRUCT~.
  • ~ts-dow (STRUCT)~ :: Access slot "dow" of ~ts~ struct ~STRUCT~.
  • ~ts-doy (STRUCT)~ :: Access slot "doy" of ~ts~ struct ~STRUCT~.
  • ~ts-hour (STRUCT)~ :: Access slot "hour" of ~ts~ struct ~STRUCT~.
  • ~ts-internal (STRUCT)~ :: Access slot "internal" of ~ts~ struct ~STRUCT~. Slot represents an Emacs-internal time value (e.g. as returned by ~current-time~).
  • ~ts-m (STRUCT)~ :: Access slot "month" of ~ts~ struct ~STRUCT~.
  • ~ts-min (STRUCT)~ :: Access slot "minute" of ~ts~ struct ~STRUCT~.
  • ~ts-minute (STRUCT)~ :: Access slot "minute" of ~ts~ struct ~STRUCT~.
  • ~ts-month (STRUCT)~ :: Access slot "month" of ~ts~ struct ~STRUCT~.
  • ~ts-month-abbr (STRUCT)~ :: Access slot "month-abbr" of ~ts~ struct ~STRUCT~.
  • ~ts-month-name (STRUCT)~ :: Access slot "month-name" of ~ts~ struct ~STRUCT~.
  • ~ts-month-num (STRUCT)~ :: Access slot "month" of ~ts~ struct ~STRUCT~.
  • ~ts-moy (STRUCT)~ :: Access slot "month" of ~ts~ struct ~STRUCT~.
  • ~ts-sec (STRUCT)~ :: Access slot "second" of ~ts~ struct ~STRUCT~.
  • ~ts-second (STRUCT)~ :: Access slot "second" of ~ts~ struct ~STRUCT~.
  • ~ts-tz-abbr (STRUCT)~ :: Access slot "tz-abbr" of ~ts~ struct ~STRUCT~.
  • ~ts-tz-offset (STRUCT)~ :: Access slot "tz-offset" of ~ts~ struct ~STRUCT~.
  • ~ts-unix (STRUCT)~ :: Access slot "unix" of ~ts~ struct ~STRUCT~.
  • ~ts-week (STRUCT)~ :: Access slot "woy" of ~ts~ struct ~STRUCT~.
  • ~ts-week-of-year (STRUCT)~ :: Access slot "woy" of ~ts~ struct ~STRUCT~.
  • ~ts-woy (STRUCT)~ :: Access slot "woy" of ~ts~ struct ~STRUCT~.
  • ~ts-year (STRUCT)~ :: Access slot "year" of ~ts~ struct ~STRUCT~.

** Adjustors

  • ~ts-apply (&rest SLOTS TS)~ :: Return new timestamp based on ~TS~ with new slot values. Fill timestamp slots, overwrite given slot values, and return new timestamp with Unix timestamp value derived from new slot values. ~SLOTS~ is a list of alternating key-value pairs like that passed to ~make-ts~.

  • ~ts-adjust (&rest ADJUSTMENTS)~ :: Return new timestamp having applied ~ADJUSTMENTS~ to ~TS~. ~ADJUSTMENTS~ should be a series of alternating ~SLOTS~ and ~VALUES~ by which to adjust them. For example, this form returns a new timestamp that is 47 hours into the future:

    ~(ts-adjust ’hour -1 ’day +2 (ts-now))~

    Since the timestamp argument is last, it’s suitable for use in a threading macro.

  • ~ts-dec (SLOT VALUE TS)~ :: Return a new timestamp based on ~TS~ with its ~SLOT~ decremented by ~VALUE~. ~SLOT~ should be specified as a plain symbol, not a keyword.

  • ~ts-inc (SLOT VALUE TS)~ :: Return a new timestamp based on ~TS~ with its ~SLOT~ incremented by ~VALUE~. ~SLOT~ should be specified as a plain symbol, not a keyword.

  • ~ts-update (TS)~ :: Return timestamp ~TS~ after updating its Unix timestamp from its other slots. Non-destructive. To be used after setting slots with, e.g. ~ts-fill~.

Destructive

  • ~ts-adjustf (TS &rest ADJUSTMENTS)~ :: Return timestamp ~TS~ having applied ~ADJUSTMENTS~. This function is destructive, as it calls ~setf~ on ~TS~.

    ~ADJUSTMENTS~ should be a series of alternating ~SLOTS~ and ~VALUES~ by which to adjust them. For example, this form adjusts a timestamp to 47 hours into the future:

    ~(let ((ts (ts-now))) (ts-adjustf ts ’hour -1 ’day +2))~

  • ~ts-decf (PLACE &optional (VALUE 1))~ :: Decrement timestamp ~PLACE~ by ~VALUE~ (default 1), update its Unix timestamp, and return the new value of ~PLACE~.

  • ~ts-incf (PLACE &optional (VALUE 1))~ :: Increment timestamp ~PLACE~ by ~VALUE~ (default 1), update its Unix timestamp, and return the new value of ~PLACE~.

** Comparators

  • ~ts-in (BEG END TS)~ :: Return non-nil if ~TS~ is within range ~BEG~ to ~END~, inclusive. All arguments should be ~ts~ structs.
  • ~ts< (A B)~ :: Return non-nil if timestamp ~A~ is less than timestamp ~B~.
  • ~ts<= (A B)~ :: Return non-nil if timestamp ~A~ is <= timestamp ~B~.
  • ~ts= (A B)~ :: Return non-nil if timestamp ~A~ is the same as timestamp ~B~. Compares only the timestamps’ ~unix~ slots. Note that a timestamp’s Unix slot is a float and may differ by less than one second, causing them to be unequal even if all of the formatted parts of the timestamp are the same.
  • ~ts> (A B)~ :: Return non-nil if timestamp ~A~ is greater than timestamp ~B~.
  • ~ts>= (A B)~ :: Return non-nil if timestamp ~A~ is >= timestamp ~B~.

** Duration

  • ~ts-human-duration (SECONDS)~ :: Return plist describing duration ~SECONDS~ in years, days, hours, minutes, and seconds. This is a simple calculation that does not account for leap years, leap seconds, etc.
  • ~ts-human-format-duration (SECONDS &optional ABBREVIATE)~ :: Return human-formatted string describing duration ~SECONDS~. If ~SECONDS~ is less than 1, returns ~"0 seconds"~. If ~ABBREVIATE~ is non-nil, return a shorter version, without spaces. This is a simple calculation that does not account for leap years, leap seconds, etc.

** Formatting

  • ~ts-format (&optional TS-OR-FORMAT-STRING TS)~ :: Format timestamp with ~format-time-string~. If ~TS-OR-FORMAT-STRING~ is a timestamp or nil, use the value of ~ts-default-format~. If both ~TS-OR-FORMAT-STRING~ and ~TS~ are nil, use the current time.

** Parsing

  • ~ts-parse (STRING)~ :: Return new ~ts~ struct, parsing ~STRING~ with ~parse-time-string~.
  • ~ts-parse-fill (FILL STRING)~ :: Return new ~ts~ struct, parsing ~STRING~ with ~parse-time-string~. Empty hour/minute/second values are filled according to ~FILL~: if ~begin~, with 0; if ~end~, hour is filled with 23 and minute/second with 59; if nil, an error may be signaled when time values are empty. Note that when ~FILL~ is ~end~, a time value like "12:12" is filled to "12:12:00", not "12:12:59".
  • ~ts-parse-org (ORG-TS-STRING)~ :: Return timestamp object for Org timestamp string ~ORG-TS-STRING~. Note that function ~org-parse-time-string~ is called, which should be loaded before calling this function.
  • ~ts-parse-org-fill (FILL ORG-TS-STRING)~ :: Return timestamp object for Org timestamp string ~ORG-TS-STRING~. Note that function ~org-parse-time-string~ is called, which should be loaded before calling this function. Hour/minute/second values are filled according to ~FILL~: if ~begin~, with 0; if ~end~, hour is filled with 23 and minute/second with 59. Note that ~org-parse-time-string~ does not support timestamps that contain seconds.
  • ~ts-parse-org-element (ELEMENT)~ :: Return timestamp object for Org timestamp element ~ELEMENT~. Element should be like one parsed by ~org-element~, the first element of which is ~timestamp~. Assumes timestamp is not a range.

** Misc.

  • ~copy-ts (TS)~ :: Return copy of timestamp struct ~TS~.

  • ~ts-difference (A B)~ :: Return difference in seconds between timestamps ~A~ and ~B~.

  • ~ts-diff~ :: Alias for ~ts-difference~.

  • ~ts-fill (TS &optional ZONE)~ :: Return ~TS~ having filled all slots from its Unix timestamp. This is non-destructive. ~ZONE~ is passed to ~format-time-string~, which see.

  • ~ts-now~ :: Return ~ts~ struct set to now.

  • ~ts-p (STRUCT)~ ::

  • ~ts-reset (TS)~ :: Return ~TS~ with all slots cleared except ~unix~. Non-destructive. The same as:

    ~(make-ts :unix (ts-unix ts))~

  • ~ts-defstruct (&rest ARGS)~ :: Like ~cl-defstruct~, but with additional slot options.

    Additional slot options and values:

    ~:accessor-init~: a sexp that initializes the slot in the accessor if the slot is nil. The symbol ~struct~ will be bound to the current struct. The accessor is defined after the struct is fully defined, so it may refer to the struct definition (e.g. by using the ~cl-struct~ ~pcase~ macro).

    ~:aliases~: ~A~ list of symbols which will be aliased to the slot accessor, prepended with the struct name (e.g. a struct ~ts~ with slot ~year~ and alias ~y~ would create an alias ~ts-y~).

  • Tips :PROPERTIES: :TOC: :depth 0 :END:

** ~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.)

  • Changelog :PROPERTIES: :TOC: :depth 0 :END:

** 0.3

Added

  • Function ~ts-fill~ accepts a ~ZONE~ argument, like that passed to ~format-time-string~, which see.

** 0.2.1

Fixed

  • ~ts-human-format-duration~ returned an empty string for durations less than 1 second (now returns ~"0 seconds"~).

** 0.2

Added

  • Functions ~ts-parse-fill~ and ~ts-parse-org-fill~.
  • Function ~ts-in~.

Changed

  • Function ~ts-now~ is no longer inlined. This allows it to be changed at runtime with, e.g. ~cl-letf~, which is helpful in testing.

Fixed

  • Save match data in =ts-fill=. (Function =split-string=, which is called in it, modifies the match data.)
  • Save match data in =ts-parse-org=. (Function =org-parse-time-string=, which is called in it, modifies the match data.)

Documentation

  • Improve description and commentary.

** 0.1

First tagged release. Published to MELPA.

  • License :PROPERTIES: :TOC: :ignore this :END:

GPLv3

Local Variables:

eval: (require 'org-make-toc)

before-save-hook: org-make-toc

org-export-with-properties: ()

org-export-with-title: t

End: