ts.el
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
Examples
Get parts of the current date:
;; 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"
Increment the current date:
;; 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")
What day of the week was 2 days ago?
(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"
Get timestamp for this time last week:
(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")
Some accessors have aliases similar to format-time-string
constructors:
(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
Parse a string into a timestamp object and reformat it:
(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"
Format the difference between two timestamps:
(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"
Parse an Org timestamp element directly from org-element-context
and find the difference between it and now:
(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"
Parse an Org timestamp string (which has a repeater) and format the year and month:
;; 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"
How long ago was this date in 1970?
(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")
How long ago did the epoch begin?
(ts-human-format-duration
(ts-diff (ts-now) (make-ts :unix 0)))
;;=> "49 years, 227 days, 12 hours, 12 minutes, 30 seconds"
In which of the last 100 years was Christmas on a Saturday?
(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)
For a more interesting example, does a timestamp fall within the previous calendar week?
;; 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)
Usage
Accessors
ts-B (STRUCT)
- Access slot “month-name” of
ts
structSTRUCT
. ts-H (STRUCT)
- Access slot “hour” of
ts
structSTRUCT
. ts-M (STRUCT)
- Access slot “minute” of
ts
structSTRUCT
. ts-S (STRUCT)
- Access slot “second” of
ts
structSTRUCT
. ts-Y (STRUCT)
- Access slot “year” of
ts
structSTRUCT
. ts-b (STRUCT)
- Access slot “month-abbr” of
ts
structSTRUCT
. ts-d (STRUCT)
- Access slot “day” of
ts
structSTRUCT
. ts-day (STRUCT)
- Access slot “day” of
ts
structSTRUCT
. ts-day-abbr (STRUCT)
- Access slot “day-abbr” of
ts
structSTRUCT
. ts-day-name (STRUCT)
- Access slot “day-name” of
ts
structSTRUCT
. ts-day-of-month-num (STRUCT)
- Access slot “day” of
ts
structSTRUCT
. ts-day-of-week-abbr (STRUCT)
- Access slot “day-abbr” of
ts
structSTRUCT
. ts-day-of-week-name (STRUCT)
- Access slot “day-name” of
ts
structSTRUCT
. ts-day-of-week-num (STRUCT)
- Access slot “dow” of
ts
structSTRUCT
. ts-day-of-year (STRUCT)
- Access slot “doy” of
ts
structSTRUCT
. ts-dom (STRUCT)
- Access slot “day” of
ts
structSTRUCT
. ts-dow (STRUCT)
- Access slot “dow” of
ts
structSTRUCT
. ts-doy (STRUCT)
- Access slot “doy” of
ts
structSTRUCT
. ts-hour (STRUCT)
- Access slot “hour” of
ts
structSTRUCT
. ts-internal (STRUCT)
- Access slot “internal” of
ts
structSTRUCT
. Slot represents an Emacs-internal time value (e.g. as returned bycurrent-time
). ts-m (STRUCT)
- Access slot “month” of
ts
structSTRUCT
. ts-min (STRUCT)
- Access slot “minute” of
ts
structSTRUCT
. ts-minute (STRUCT)
- Access slot “minute” of
ts
structSTRUCT
. ts-month (STRUCT)
- Access slot “month” of
ts
structSTRUCT
. ts-month-abbr (STRUCT)
- Access slot “month-abbr” of
ts
structSTRUCT
. ts-month-name (STRUCT)
- Access slot “month-name” of
ts
structSTRUCT
. ts-month-num (STRUCT)
- Access slot “month” of
ts
structSTRUCT
. ts-moy (STRUCT)
- Access slot “month” of
ts
structSTRUCT
. ts-sec (STRUCT)
- Access slot “second” of
ts
structSTRUCT
. ts-second (STRUCT)
- Access slot “second” of
ts
structSTRUCT
. ts-tz-abbr (STRUCT)
- Access slot “tz-abbr” of
ts
structSTRUCT
. ts-tz-offset (STRUCT)
- Access slot “tz-offset” of
ts
structSTRUCT
. ts-unix (STRUCT)
- Access slot “unix” of
ts
structSTRUCT
. ts-week (STRUCT)
- Access slot “woy” of
ts
structSTRUCT
. ts-week-of-year (STRUCT)
- Access slot “woy” of
ts
structSTRUCT
. ts-woy (STRUCT)
- Access slot “woy” of
ts
structSTRUCT
. ts-year (STRUCT)
- Access slot “year” of
ts
structSTRUCT
.
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 tomake-ts
. ts-adjust (&rest ADJUSTMENTS)
- Return new timestamp having applied
ADJUSTMENTS
toTS
.ADJUSTMENTS
should be a series of alternatingSLOTS
andVALUES
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 itsSLOT
decremented byVALUE
.SLOT
should be specified as a plain symbol, not a keyword. ts-inc (SLOT VALUE TS)
- Return a new timestamp based on
TS
with itsSLOT
incremented byVALUE
.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 appliedADJUSTMENTS
. This function is destructive, as it callssetf
onTS
.ADJUSTMENTS
should be a series of alternatingSLOTS
andVALUES
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
byVALUE
(default 1), update its Unix timestamp, and return the new value ofPLACE
. ts-incf (PLACE &optional (VALUE 1))
- Increment timestamp
PLACE
byVALUE
(default 1), update its Unix timestamp, and return the new value ofPLACE
.
Comparators
ts-in (BEG END TS)
- Return non-nil if
TS
is within rangeBEG
toEND
, inclusive. All arguments should bets
structs. ts< (A B)
- Return non-nil if timestamp
A
is less than timestampB
. ts<= (A B)
- Return non-nil if timestamp
A
is <= timestampB
. ts= (A B)
- Return non-nil if timestamp
A
is the same as timestampB
. 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 timestampB
. ts>= (A B)
- Return non-nil if timestamp
A
is >= timestampB
.
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
. IfSECONDS
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
. IfTS-OR-FORMAT-STRING
is a timestamp or nil, use the value ofts-default-format
. If bothTS-OR-FORMAT-STRING
andTS
are nil, use the current time.
Parsing
ts-parse (STRING)
- Return new
ts
struct, parsingSTRING
withparse-time-string
. ts-parse-fill (FILL STRING)
- Return new
ts
struct, parsingSTRING
withparse-time-string
. Empty hour/minute/second values are filled according toFILL
: ifbegin
, with 0; ifend
, hour is filled with 23 and minute/second with 59; if nil, an error may be signaled when time values are empty. Note that whenFILL
isend
, 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 functionorg-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 functionorg-parse-time-string
is called, which should be loaded before calling this function. Hour/minute/second values are filled according toFILL
: ifbegin
, with 0; ifend
, hour is filled with 23 and minute/second with 59. Note thatorg-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 byorg-element
, the first element of which istimestamp
. 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
andB
. 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 toformat-time-string
, which see. ts-now
- Return
ts
struct set to now. ts-p (STRUCT)
ts-reset (TS)
- Return
TS
with all slots cleared exceptunix
. 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 symbolstruct
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 thecl-struct
pcase
macro).:aliases
:A
list of symbols which will be aliased to the slot accessor, prepended with the struct name (e.g. a structts
with slotyear
and aliasy
would create an aliasts-y
).
Tips
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:
(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))))
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 Emacs Package Developer’s Handbook for the bench-multi-lexical
macro.)
Changelog
0.3
Added
- Function
ts-fill
accepts aZONE
argument, like that passed toformat-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
andts-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
. (Functionsplit-string
, which is called in it, modifies the match data.) - Save match data in
ts-parse-org
. (Functionorg-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
GPLv3