1 ;;; org-duration.el --- Library handling durations -*- lexical-binding: t; -*-
3 ;; Copyright (C) 2017-2019 Free Software Foundation, Inc.
5 ;; Author: Nicolas Goaziou <mail@nicolasgoaziou.fr>
6 ;; Keywords: outlines, hypermedia, calendar, wp
8 ;; This file is part of GNU Emacs.
10 ;; GNU Emacs is free software: you can redistribute it and/or modify
11 ;; it under the terms of the GNU General Public License as published by
12 ;; the Free Software Foundation, either version 3 of the License, or
13 ;; (at your option) any later version.
15 ;; GNU Emacs is distributed in the hope that it will be useful,
16 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
17 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 ;; GNU General Public License for more details.
20 ;; You should have received a copy of the GNU General Public License
21 ;; along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>.
25 ;; This library provides tools to manipulate durations. A duration
26 ;; can have multiple formats:
34 ;; More accurately, it consists of numbers and units, as defined in
35 ;; variable `org-duration-units', separated with white spaces, and
36 ;; a "H:MM" or "H:MM:SS" part. White spaces are tolerated between the
37 ;; number and its relative unit. Variable `org-duration-format'
38 ;; controls durations default representation.
40 ;; The library provides functions allowing to convert a duration to,
41 ;; and from, a number of minutes: `org-duration-to-minutes' and
42 ;; `org-duration-from-minutes'. It also provides two lesser tools:
43 ;; `org-duration-p', and `org-duration-h:mm-only-p'.
45 ;; Users can set the number of minutes per unit, or define new units,
46 ;; in `org-duration-units'. The library also supports canonical
47 ;; duration, i.e., a duration that doesn't depend on user's settings,
48 ;; through optional arguments.
58 (defconst org-duration-canonical-units
62 "Canonical time duration units.
63 See `org-duration-units' for details.")
65 (defcustom org-duration-units
71 ("y" .
,(* 60 24 365.25)))
72 "Conversion factor to minutes for a duration.
74 Each entry has the form (UNIT . MODIFIER).
76 In a duration string, a number followed by UNIT is multiplied by
77 the specified number of MODIFIER to obtain a duration in minutes.
79 For example, the following value
85 (\"m\" . ,(* 60 8 5 4))
86 (\"y\" . ,(* 60 8 5 4 10)))
88 is meaningful if you work an average of 8 hours per day, 5 days
89 a week, 4 weeks a month and 10 months a year.
91 When setting this variable outside the Customize interface, make
92 sure to call the following command:
94 \\[org-duration-set-regexps]"
97 :package-version
'(Org .
"9.1")
98 :set
(lambda (var val
) (set-default var val
) (org-duration-set-regexps))
99 :initialize
'custom-initialize-changed
101 (const :tag
"H:MM" h
:mm
)
102 (const :tag
"H:MM:SS" h
:mm
:ss
)
103 (alist :key-type
(string :tag
"Unit")
104 :value-type
(number :tag
"Modifier"))))
106 (defcustom org-duration-format
'(("d" . nil
) (special . h
:mm
))
107 "Format definition for a duration.
109 The value can be set to, respectively, the symbols `h:mm:ss' or
110 `h:mm', which means a duration is expressed as, respectively,
111 a \"H:MM:SS\" or \"H:MM\" string.
113 Alternatively, the value can be a list of entries following the
118 UNIT is a unit string, as defined in `org-duration-units'. The
119 time duration is formatted using only the time components that
122 Units with a zero value are skipped, unless REQUIRED? is non-nil.
123 In that case, the unit is always used.
125 Eventually, the list can contain one of the following special
131 Units shorter than an hour are ignored. The hours and
132 minutes part of the duration is expressed unconditionally
133 with H:MM, or H:MM:SS, pattern.
135 (special . PRECISION)
137 A duration is expressed with a single unit, PRECISION being
138 the number of decimal places to show. The unit chosen is the
139 first one required or with a non-zero integer part. If there
140 is no such unit, the smallest one is used.
144 ((\"d\" . nil) (\"h\" . t) (\"min\" . t))
146 means a duration longer than a day is expressed in days, hours
147 and minutes, whereas a duration shorter than a day is always
148 expressed in hours and minutes, even when shorter than an hour.
150 On the other hand, the value
152 ((\"d\" . nil) (\"min\" . nil))
154 means a duration longer than a day is expressed in days and
155 minutes, whereas a duration shorter than a day is expressed
156 entirely in minutes, even when longer than an hour.
160 ((\"d\" . nil) (special . h:mm))
162 means that any duration longer than a day is expressed with both
163 a \"d\" unit and a \"H:MM\" part, whereas a duration shorter than
164 a day is expressed only as a \"H:MM\" string.
168 ((\"d\" . nil) (\"h\" . nil) (special . 2))
170 expresses a duration longer than a day as a decimal number, with
171 a 2-digits fractional part, of \"d\" unit. A duration shorter
172 than a day uses \"h\" unit instead."
176 :package-version
'(Org .
"9.1")
178 (const :tag
"Use H:MM" h
:mm
)
179 (const :tag
"Use H:MM:SS" h
:mm
:ss
)
180 (repeat :tag
"Use units"
182 (cons :tag
"Use units"
184 (choice (const :tag
"Skip when zero" nil
)
185 (const :tag
"Always used" t
)))
186 (cons :tag
"Use a single decimal unit"
188 (integer :tag
"Number of decimals"))
189 (cons :tag
"Use both units and H:MM"
192 (cons :tag
"Use both units and H:MM:SS"
197 ;;; Internal variables and functions
199 (defconst org-duration--h
:mm-re
200 "\\`[ \t]*[0-9]+\\(?::[0-9]\\{2\\}\\)\\{1,2\\}[ \t]*\\'"
201 "Regexp matching a duration expressed with H:MM or H:MM:SS format.
202 See `org-duration--h:mm:ss-re' to only match the latter. Hours
203 can use any number of digits.")
205 (defconst org-duration--h
:mm
:ss-re
206 "\\`[ \t]*[0-9]+\\(?::[0-9]\\{2\\}\\)\\{2\\}[ \t]*\\'"
207 "Regexp matching a duration expressed H:MM:SS format.
208 See `org-duration--h:mm-re' to also support H:MM format. Hours
209 can use any number of digits.")
211 (defvar org-duration--unit-re nil
212 "Regexp matching a duration with an unit.
213 Allowed units are defined in `org-duration-units'. Match group
214 1 contains the bare number. Match group 2 contains the unit.")
216 (defvar org-duration--full-re nil
217 "Regexp matching a duration expressed with units.
218 Allowed units are defined in `org-duration-units'.")
220 (defvar org-duration--mixed-re nil
221 "Regexp matching a duration expressed with units and H:MM or H:MM:SS format.
222 Allowed units are defined in `org-duration-units'. Match group
223 1 contains units part. Match group 2 contains H:MM or H:MM:SS
226 (defun org-duration--modifier (unit &optional canonical
)
227 "Return modifier associated to string UNIT.
228 When optional argument CANONICAL is non-nil, refer to
229 `org-duration-canonical-units' instead of `org-duration-units'."
230 (or (cdr (assoc unit
(if canonical
231 org-duration-canonical-units
232 org-duration-units
)))
233 (error "Unknown unit: %S" unit
)))
239 (defun org-duration-set-regexps ()
240 "Set duration related regexps."
242 (setq org-duration--unit-re
243 (concat "\\([0-9]+\\(?:\\.[0-9]*\\)?\\)[ \t]*"
244 ;; Since user-defined units in `org-duration-units'
245 ;; can differ from canonical units in
246 ;; `org-duration-canonical-units', include both in
248 (regexp-opt (mapcar #'car
(append org-duration-canonical-units
251 (setq org-duration--full-re
252 (format "\\`[ \t]*%s\\(?:[ \t]+%s\\)*[ \t]*\\'"
253 org-duration--unit-re
254 org-duration--unit-re
))
255 (setq org-duration--mixed-re
256 (format "\\`[ \t]*\\(?1:%s\\(?:[ \t]+%s\\)*\\)[ \t]+\
257 \\(?2:[0-9]+\\(?::[0-9][0-9]\\)\\{1,2\\}\\)[ \t]*\\'"
258 org-duration--unit-re
259 org-duration--unit-re
)))
262 (defun org-duration-p (s)
263 "Non-nil when string S is a time duration."
265 (or (string-match-p org-duration--full-re s
)
266 (string-match-p org-duration--mixed-re s
)
267 (string-match-p org-duration--h
:mm-re s
))))
270 (defun org-duration-to-minutes (duration &optional canonical
)
271 "Return number of minutes of DURATION string.
273 When optional argument CANONICAL is non-nil, ignore
274 `org-duration-units' and use standard time units value.
276 A bare number is translated into minutes. The empty string is
279 Return value as a float. Raise an error if duration format is
282 ((equal duration
"") 0.0)
283 ((numberp duration
) (float duration
))
284 ((string-match-p org-duration--h
:mm-re duration
)
285 (pcase-let ((`(,hours
,minutes
,seconds
)
286 (mapcar #'string-to-number
(split-string duration
":"))))
287 (+ (/ (or seconds
0) 60.0) minutes
(* 60 hours
))))
288 ((string-match-p org-duration--full-re duration
)
291 (while (string-match org-duration--unit-re duration s
)
292 (setq s
(match-end 0))
293 (let ((value (string-to-number (match-string 1 duration
)))
294 (unit (match-string 2 duration
)))
295 (cl-incf minutes
(* value
(org-duration--modifier unit canonical
)))))
297 ((string-match org-duration--mixed-re duration
)
298 (let ((units-part (match-string 1 duration
))
299 (hms-part (match-string 2 duration
)))
300 (+ (org-duration-to-minutes units-part
)
301 (org-duration-to-minutes hms-part
))))
302 ((string-match-p "\\`[0-9]+\\(\\.[0-9]*\\)?\\'" duration
)
303 (float (string-to-number duration
)))
304 (t (error "Invalid duration format: %S" duration
))))
307 (defun org-duration-from-minutes (minutes &optional fmt canonical
)
308 "Return duration string for a given number of MINUTES.
310 Format duration according to `org-duration-format' or FMT, when
313 When optional argument CANONICAL is non-nil, ignore
314 `org-duration-units' and use standard time units value.
316 Raise an error if expected format is unknown."
317 (pcase (or fmt org-duration-format
)
319 (format "%d:%02d" (/ minutes
60) (mod minutes
60)))
321 (let* ((whole-minutes (floor minutes
))
322 (seconds (mod (* 60 minutes
) 60)))
324 (org-duration-from-minutes whole-minutes
'h
:mm
)
326 ((pred atom
) (error "Invalid duration format specification: %S" fmt
))
327 ;; Mixed format. Call recursively the function on both parts.
328 ((and duration-format
329 (let `(special .
,(and mode
(or `h
:mm
:ss
`h
:mm
)))
330 (assq 'special duration-format
)))
331 (let* ((truncated-format
332 ;; Remove "special" mode from duration format in order to
333 ;; recurse properly. Also remove units smaller or equal
334 ;; to an hour since H:MM part takes care of it.
338 (`(,(and unit
(pred stringp
)) .
,_
)
339 (> (org-duration--modifier unit canonical
) 60))
342 (min-modifier ;smallest modifier above hour
343 (and truncated-format
346 (org-duration--modifier (car p
) canonical
))
347 truncated-format
)))))
348 (if (or (null min-modifier
) (< minutes min-modifier
))
349 ;; There is not unit above the hour or the smallest unit
350 ;; above the hour is too large for the number of minutes we
351 ;; need to represent. Use H:MM or H:MM:SS syntax.
352 (org-duration-from-minutes minutes mode canonical
)
353 ;; Represent minutes above hour using provided units and H:MM
355 (let* ((units-part (* min-modifier
(/ (floor minutes
) min-modifier
)))
356 (minutes-part (- minutes units-part
)))
358 (org-duration-from-minutes units-part truncated-format canonical
)
360 (org-duration-from-minutes minutes-part mode
))))))
364 (let ((digits (cdr (assq 'special duration-format
))))
366 (or (wholenump digits
)
367 (error "Unknown formatting directive: %S" digits
))
368 (format "%%.%df" digits
))))
371 ;; Ignore special format cells.
372 (lambda (pair) (pcase pair
(`(special .
,_
) t
) (_ nil
)))
375 (> (org-duration--modifier (car a
) canonical
)
376 (org-duration--modifier (car b
) canonical
))))))
378 ;; Fractional duration: use first unit that is either required
379 ;; or smaller than MINUTES.
387 (<= (org-duration--modifier u canonical
)
390 ;; Fall back to smallest unit.
391 (org-last selected-units
))))
392 (modifier (org-duration--modifier unit canonical
)))
393 (concat (format fractional
(/ (float minutes
) modifier
)) unit
)))
394 ;; Otherwise build duration string according to available
400 (pcase-let* ((`(,unit .
,required?
) units
)
401 (modifier (org-duration--modifier unit canonical
)))
402 (cond ((<= modifier minutes
)
403 (let ((value (floor minutes modifier
)))
404 (cl-decf minutes
(* value modifier
))
405 (format " %d%s" value unit
)))
406 (required?
(concat " 0" unit
))
410 ;; No unit can properly represent MINUTES. Use the smallest
413 (pcase-let ((`((,unit .
,_
)) (last selected-units
)))
414 (concat "0" unit
))))))))
417 (defun org-duration-h:mm-only-p
(times)
418 "Non-nil when every duration in TIMES has \"H:MM\" or \"H:MM:SS\" format.
420 TIMES is a list of duration strings.
422 Return nil if any duration is expressed with units, as defined in
423 `org-duration-units'. Otherwise, if any duration is expressed
424 with \"H:MM:SS\" format, return `h:mm:ss'. Otherwise, return
429 (cond ((string-match-p org-duration--full-re time
)
431 ((string-match-p org-duration--mixed-re time
)
434 ((string-match-p org-duration--h
:mm
:ss-re time
)
435 (setq hms-flag
'h
:mm
:ss
))))
436 (or hms-flag
'h
:mm
))))
441 (org-duration-set-regexps)
443 (provide 'org-duration
)
444 ;;; org-duration.el ends here