Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/alphapapa/ts.el

Emacs timestamp and date-time library
https://github.com/alphapapa/ts.el

date-time emacs library timestamp

Last synced: 3 months ago
JSON representation

Emacs timestamp and date-time library

Awesome Lists containing this project

README

        

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