org-duration.el 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446
  1. ;;; org-duration.el --- Library handling durations -*- lexical-binding: t; -*-
  2. ;; Copyright (C) 2017 Nicolas Goaziou
  3. ;; Author: Nicolas Goaziou <mail@nicolasgoaziou.fr>
  4. ;; Keywords: outlines, hypermedia, calendar, wp
  5. ;; This program is free software; you can redistribute it and/or modify
  6. ;; it under the terms of the GNU General Public License as published by
  7. ;; the Free Software Foundation, either version 3 of the License, or
  8. ;; (at your option) any later version.
  9. ;; This program is distributed in the hope that it will be useful,
  10. ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. ;; GNU General Public License for more details.
  13. ;; You should have received a copy of the GNU General Public License
  14. ;; along with this program. If not, see <http://www.gnu.org/licenses/>.
  15. ;;; Commentary:
  16. ;; This library provides tools to manipulate durations. A duration
  17. ;; can have multiple formats:
  18. ;;
  19. ;; - 3:12
  20. ;; - 1:23:45
  21. ;; - 1y 3d 3h 4min
  22. ;; - 3d 13:35
  23. ;; - 2.35h
  24. ;;
  25. ;; More accurately, it consists of numbers and units, as defined in
  26. ;; variable `org-duration-units', separated with white spaces, and
  27. ;; a "H:MM" or "H:MM:SS" part. White spaces are tolerated between the
  28. ;; number and its relative unit. Variable `org-duration-format'
  29. ;; controls durations default representation.
  30. ;;
  31. ;; The library provides functions allowing to convert a duration to,
  32. ;; and from, a number of minutes: `org-duration-to-minutes' and
  33. ;; `org-duration-from-minutes'. It also provides two lesser tools:
  34. ;; `org-duration-p', and `org-duration-h:mm-only-p'.
  35. ;;
  36. ;; Users can set the number of minutes per unit, or define new units,
  37. ;; in `org-duration-units'. The library also supports canonical
  38. ;; duration, i.e., a duration that doesn't depend on user's settings,
  39. ;; through optional arguments.
  40. ;;; Code:
  41. (require 'cl-lib)
  42. (require 'org-macs)
  43. (declare-function org-trim "org-trim" (s &optional keep-lead))
  44. ;;; Public variables
  45. (defconst org-duration-canonical-units
  46. `(("min" . 1)
  47. ("h" . 60)
  48. ("d" . ,(* 60 24)))
  49. "Canonical time duration units.
  50. See `org-duration-units' for details.")
  51. (defcustom org-duration-units
  52. `(("min" . 1)
  53. ("h" . 60)
  54. ("d" . ,(* 60 24))
  55. ("w" . ,(* 60 24 7))
  56. ("m" . ,(* 60 24 30))
  57. ("y" . ,(* 60 24 365.25)))
  58. "Conversion factor to minutes for a duration.
  59. Each entry has the form (UNIT . MODIFIER).
  60. In a duration string, a number followed by UNIT is multiplied by
  61. the specified number of MODIFIER to obtain a duration in minutes.
  62. For example, the following value
  63. \\=`((\"min\" . 1)
  64. (\"h\" . 60)
  65. (\"d\" . ,(* 60 8))
  66. (\"w\" . ,(* 60 8 5))
  67. (\"m\" . ,(* 60 8 5 4))
  68. (\"y\" . ,(* 60 8 5 4 10)))
  69. is meaningful if you work an average of 8 hours per day, 5 days
  70. a week, 4 weeks a month and 10 months a year.
  71. When setting this variable outside the Customize interface, make
  72. sure to call the following command:
  73. \\[org-duration-set-regexps]"
  74. :group 'org-agenda
  75. :version "26.1"
  76. :package-version '(Org . "9.1")
  77. :set (lambda (var val) (set-default var val) (org-duration-set-regexps))
  78. :initialize 'custom-initialize-changed
  79. :type '(choice
  80. (const :tag "H:MM" 'h:mm)
  81. (const :tag "H:MM:SS" 'h:mm:ss)
  82. (alist :key-type (string :tag "Unit")
  83. :value-type (number :tag "Modifier"))))
  84. (defcustom org-duration-format '(("d" . nil) (special . h:mm))
  85. "Format definition for a duration.
  86. The value can be set to, respectively, `h:mm:ss' or `h:mm', which
  87. means a duration is expressed as, respectively, a \"H:MM:SS\" or
  88. \"H:MM\" string.
  89. Alternatively, the value can be a list of entries following the
  90. pattern:
  91. (UNIT . REQUIRED?)
  92. UNIT is a unit string, as defined in `org-duration-units'. The
  93. time duration is formatted using only the time components that
  94. are specified here. If a time unit in missing, it falls back to
  95. the next smallest unit.
  96. A non-nil REQUIRED? value for these keys indicates that the
  97. corresponding time component should always be included, even if
  98. its value is 0.
  99. Eventually, the list can contain an entry indicating special
  100. formatting needs. It can follow one of the three following
  101. patterns:
  102. (special . h:mm)
  103. (special . h:mm:ss)
  104. (special . PRECISION)
  105. When any of the first two is present, a duration is expressed in
  106. mixed mode, where the hours and minutes of the duration are
  107. expressed as a \"H:MM:SS\" or \"H:MM\" string while still using
  108. other units defined.
  109. With the last pattern, a duration is expressed with a single
  110. unit, PRECISION being the number of decimal places to show. The
  111. unit chosen is the first one required or with a non-zero integer
  112. part. If there is no such unit, the smallest one is used.
  113. For example,
  114. ((\"d\" . nil) (\"h\" . t) (\"min\" . t))
  115. means a duration longer than a day is expressed in days, hours
  116. and minutes, whereas a duration shorter than a day is always
  117. expressed in hours and minutes, even when shorter than an hour.
  118. On the other hand, the value
  119. ((\"d\" . nil) (\"min\" . nil))
  120. means a duration longer than a day is expressed in days and
  121. minutes, whereas a duration shorter than a day is expressed
  122. entirely in minutes, even when longer than an hour.
  123. The following format
  124. ((\"d\" . nil) (special . h:mm))
  125. means that any duration longer than a day is expressed with both
  126. a \"d\" unit and a \"H:MM\" part, whereas a duration shorter than
  127. a day is expressed only as a \"H:MM\" string.
  128. Eventually,
  129. ((\"d\" . nil) (\"h\" . nil) (special . 2))
  130. expresses a duration longer than a day as a decimal number, with
  131. a 2-digits fractional part, of \"d\" unit. A duration shorter
  132. than a day uses \"h\" unit instead."
  133. :group 'org-time
  134. :group 'org-clock
  135. :version "26.1"
  136. :package-version '(Org . "9.1")
  137. :type '(choice
  138. (const :tag "Use H:MM" h:mm)
  139. (const :tag "Use H:MM:SS" h:mm:ss)
  140. (repeat :tag "Use units"
  141. (choice
  142. (cons :tag "Use units"
  143. (string :tag "Unit")
  144. (choice (const :tag "Skip when zero" nil)
  145. (const :tag "Always used" t)))
  146. (cons :tag "Use a single decimal unit"
  147. (const special)
  148. (integer :tag "Number of decimals"))
  149. (cons :tag "Use both units and H:MM"
  150. (const special)
  151. (const h:mm))
  152. (cons :tag "Use both units and H:MM:SS"
  153. (const special)
  154. (const h:mm:ss))))))
  155. ;;; Internal variables and functions
  156. (defconst org-duration--h:mm-re
  157. "\\`[ \t]*[0-9]+\\(?::[0-9]\\{2\\}\\)\\{1,2\\}[ \t]*\\'"
  158. "Regexp matching a duration expressed with H:MM or H:MM:SS format.
  159. See `org-duration--h:mm:ss-re' to only match the latter. Hours
  160. can use any number of digits.")
  161. (defconst org-duration--h:mm:ss-re
  162. "\\`[ \t]*[0-9]+\\(?::[0-9]\\{2\\}\\)\\{2\\}[ \t]*\\'"
  163. "Regexp matching a duration expressed H:MM:SS format.
  164. See `org-duration--h:mm-re' to also support H:MM format. Hours
  165. can use any number of digits.")
  166. (defvar org-duration--unit-re nil
  167. "Regexp matching a duration with an unit.
  168. Allowed units are defined in `org-duration-units'. Match group
  169. 1 contains the bare number. Match group 2 contains the unit.")
  170. (defvar org-duration--full-re nil
  171. "Regexp matching a duration expressed with units.
  172. Allowed units are defined in `org-duration-units'.")
  173. (defvar org-duration--mixed-re nil
  174. "Regexp matching a duration expressed with units and H:MM or H:MM:SS format.
  175. Allowed units are defined in `org-duration-units'. Match group
  176. 1 contains units part. Match group 2 contains H:MM or H:MM:SS
  177. part.")
  178. (defun org-duration--modifier (unit &optional canonical)
  179. "Return modifier associated to string UNIT.
  180. When optional argument CANONICAL is non-nil, refer to
  181. `org-duration-canonical-units' instead of `org-duration-units'."
  182. (or (cdr (assoc unit (if canonical
  183. org-duration-canonical-units
  184. org-duration-units)))
  185. (error "Unknown unit: %S" unit)))
  186. ;;; Public functions
  187. ;;;###autoload
  188. (defun org-duration-set-regexps ()
  189. "Set duration related regexps."
  190. (interactive)
  191. (setq org-duration--unit-re
  192. (concat "\\([0-9]+\\(?:\\.[0-9]*\\)?\\)[ \t]*"
  193. ;; Since user-defined units in `org-duration-units'
  194. ;; can differ from canonical units in
  195. ;; `org-duration-canonical-units', include both in
  196. ;; regexp.
  197. (regexp-opt (mapcar #'car (append org-duration-canonical-units
  198. org-duration-units))
  199. t)))
  200. (setq org-duration--full-re
  201. (format "\\`[ \t]*%s\\(?:[ \t]+%s\\)*[ \t]*\\'"
  202. org-duration--unit-re
  203. org-duration--unit-re))
  204. (setq org-duration--mixed-re
  205. (format "\\`[ \t]*\\(?1:%s\\(?:[ \t]+%s\\)*\\)[ \t]+\
  206. \\(?2:[0-9]+\\(?::[0-9][0-9]\\)\\{1,2\\}\\)[ \t]*\\'"
  207. org-duration--unit-re
  208. org-duration--unit-re)))
  209. ;;;###autoload
  210. (defun org-duration-p (s)
  211. "Non-nil when string S is a time duration."
  212. (and (stringp s)
  213. (or (string-match-p org-duration--full-re s)
  214. (string-match-p org-duration--mixed-re s)
  215. (string-match-p org-duration--h:mm-re s))))
  216. ;;;###autoload
  217. (defun org-duration-to-minutes (duration &optional canonical)
  218. "Return number of minutes of DURATION string.
  219. When optional argument CANONICAL is non-nil, ignore
  220. `org-duration-units' and use standard time units value.
  221. As a special case, a bare number represents minutes.
  222. Return value as a float. Raise an error if duration format is
  223. not recognized."
  224. (cond
  225. ((string-match-p org-duration--h:mm-re duration)
  226. (pcase-let ((`(,hours ,minutes ,seconds)
  227. (mapcar #'string-to-number (split-string duration ":"))))
  228. (+ (/ (or seconds 0) 60.0) minutes (* 60 hours))))
  229. ((string-match-p org-duration--full-re duration)
  230. (let ((minutes 0)
  231. (s -1))
  232. (while (setq s (string-match org-duration--unit-re duration (1+ s)))
  233. (let ((value (string-to-number (match-string 1 duration)))
  234. (unit (match-string 2 duration)))
  235. (cl-incf minutes (* value (org-duration--modifier unit canonical)))))
  236. (float minutes)))
  237. ((string-match org-duration--mixed-re duration)
  238. (let ((units-part (match-string 1 duration))
  239. (hms-part (match-string 2 duration)))
  240. (+ (org-duration-to-minutes units-part)
  241. (org-duration-to-minutes hms-part))))
  242. ((string-match-p "\\`[0-9]+\\(\\.[0-9]*\\)?\\'" duration)
  243. (float (string-to-number duration)))
  244. (t (error "Invalid duration format: %S" duration))))
  245. ;;;###autoload
  246. (defun org-duration-from-minutes (minutes &optional fmt canonical)
  247. "Return duration string for a given number of MINUTES.
  248. Format duration according to `org-duration-format' or FMT, when
  249. non-nil.
  250. When optional argument CANONICAL is non-nil, ignore
  251. `org-duration-units' and use standard time units value.
  252. Raise an error if expected format is unknown."
  253. (pcase (or fmt org-duration-format)
  254. (`h:mm
  255. (let ((minutes (floor minutes)))
  256. (format "%d:%02d" (/ minutes 60) (mod minutes 60))))
  257. (`h:mm:ss
  258. (let ((seconds (floor (* 60 minutes)) ))
  259. (format "%s:%02d"
  260. (org-duration-from-minutes (/ seconds 60) 'h:mm)
  261. (mod seconds 60))))
  262. ((pred atom) (error "Invalid duration format specification: %S" fmt))
  263. ;; Mixed format. Call recursively the function on both parts.
  264. ((and duration-format
  265. (let `(special . ,(and mode (or `h:mm:ss `h:mm)))
  266. (assq 'special duration-format)))
  267. (let* ((truncated-format
  268. ;; Remove "special" mode from duration format in order to
  269. ;; recurse properly. Also remove units smaller or equal
  270. ;; to an hour since H:MM part takes care of it.
  271. (cl-remove-if-not
  272. (lambda (pair)
  273. (pcase pair
  274. (`(,(and unit (pred stringp)) . ,_)
  275. (> (org-duration--modifier unit canonical) 60))
  276. (_ nil)))
  277. duration-format))
  278. (min-modifier ;smallest modifier above hour
  279. (and truncated-format
  280. (apply #'min
  281. (mapcar (lambda (p)
  282. (org-duration--modifier (car p) canonical))
  283. truncated-format)))))
  284. (if (or (null min-modifier) (< minutes min-modifier))
  285. ;; There is not unit above the hour or the smallest unit
  286. ;; above the hour is too large for the number of minutes we
  287. ;; need to represent. Use H:MM or H:MM:SS syntax.
  288. (org-duration-from-minutes minutes mode canonical)
  289. ;; Represent minutes above hour using provided units and H:MM
  290. ;; or H:MM:SS below.
  291. (let* ((units-part (* min-modifier (floor (/ minutes min-modifier))))
  292. (minutes-part (- minutes units-part)))
  293. (concat
  294. (org-duration-from-minutes units-part truncated-format canonical)
  295. " "
  296. (org-duration-from-minutes minutes-part mode))))))
  297. ;; Units format.
  298. (duration-format
  299. (let* ((fractional
  300. (let ((digits (cdr (assq 'special duration-format))))
  301. (and digits
  302. (or (wholenump digits)
  303. (error "Unknown formatting directive: %S" digits))
  304. (format "%%.%df" digits))))
  305. (selected-units
  306. (sort (cl-remove-if
  307. ;; Ignore special format cells.
  308. (lambda (pair) (pcase pair (`(special . ,_) t) (_ nil)))
  309. duration-format)
  310. (lambda (a b)
  311. (> (org-duration--modifier (car a) canonical)
  312. (org-duration--modifier (car b) canonical))))))
  313. (cond
  314. ;; Fractional duration: use first unit that is either required
  315. ;; or smaller than MINUTES.
  316. (fractional
  317. (let* ((unit (car
  318. (or (cl-find-if
  319. (lambda (pair)
  320. (pcase pair
  321. (`(,u . ,req?)
  322. (or req?
  323. (<= (org-duration--modifier u canonical)
  324. minutes)))))
  325. selected-units)
  326. ;; Fall back to smallest unit.
  327. (org-last selected-units))))
  328. (modifier (org-duration--modifier unit canonical)))
  329. (concat (format fractional (/ (float minutes) modifier)) unit)))
  330. ;; Otherwise build duration string according to available
  331. ;; units.
  332. ((org-string-nw-p
  333. (org-trim
  334. (mapconcat
  335. (lambda (units)
  336. (pcase-let* ((`(,unit . ,required?) units)
  337. (modifier (org-duration--modifier unit canonical)))
  338. (cond ((<= modifier minutes)
  339. (let ((value (floor (/ minutes modifier))))
  340. (cl-decf minutes (* value modifier))
  341. (format " %d%s" value unit)))
  342. (required? (concat " 0" unit))
  343. (t ""))))
  344. selected-units
  345. ""))))
  346. ;; No unit can properly represent MINUTES. Use the smallest
  347. ;; one anyway.
  348. (t
  349. (pcase-let ((`((,unit . ,_)) (last selected-units)))
  350. (concat
  351. (if (not fractional) "0"
  352. (let ((modifier (org-duration--modifier unit canonical)))
  353. (format fractional (/ (float minutes) modifier))))
  354. unit))))))))
  355. ;;;###autoload
  356. (defun org-duration-h:mm-only-p (times)
  357. "Non-nil when every duration in TIMES has \"H:MM\" or \"H:MM:SS\" format.
  358. TIMES is a list of duration strings.
  359. Return nil if any duration is expressed with units, as defined in
  360. `org-duration-units'. Otherwise, if any duration is expressed
  361. with \"H:MM:SS\" format, return `h:mm:ss'. Otherwise, return
  362. `h:mm'."
  363. (let (hms-flag)
  364. (catch :exit
  365. (dolist (time times)
  366. (cond ((string-match-p org-duration--full-re time)
  367. (throw :exit nil))
  368. ((string-match-p org-duration--mixed-re time)
  369. (throw :exit nil))
  370. (hms-flag nil)
  371. ((string-match-p org-duration--h:mm:ss-re time)
  372. (setq hms-flag 'h:mm:ss))))
  373. (or hms-flag 'h:mm))))
  374. ;;; Initialization
  375. (org-duration-set-regexps)
  376. (provide 'org-duration)
  377. ;;; org-duration.el ends here