1 ;;; org-duration.el --- Library handling durations -*- lexical-binding: t; -*-
3 ;; Copyright (C) 2017 Nicolas Goaziou
5 ;; Author: Nicolas Goaziou <mail@nicolasgoaziou.fr>
6 ;; Keywords: outlines, hypermedia, calendar, wp
8 ;; This program is free software; you can redistribute it and/or modify
9 ;; it under the terms of the GNU General Public License as published by
10 ;; the Free Software Foundation, either version 3 of the License, or
11 ;; (at your option) any later version.
13 ;; This program is distributed in the hope that it will be useful,
14 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
15 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 ;; GNU General Public License for more details.
18 ;; You should have received a copy of the GNU General Public License
19 ;; along with this program. If not, see <http://www.gnu.org/licenses/>.
23 ;; This library provides tools to manipulate durations. A duration
24 ;; can have multiple formats:
32 ;; More accurately, it consists of numbers and units, as defined in
33 ;; variable `org-duration-units', separated with white spaces, and
34 ;; a "H:MM" or "H:MM:SS" part. White spaces are tolerated between the
35 ;; number and its relative unit. Variable `org-duration-format'
36 ;; controls durations default representation.
38 ;; The library provides functions allowing to convert a duration to,
39 ;; and from, a number of minutes: `org-duration-to-minutes' and
40 ;; `org-duration-from-minutes'. It also provides two lesser tools:
41 ;; `org-duration-p', and `org-duration-h:mm-only-p'.
43 ;; Users can set the number of minutes per unit, or define new units,
44 ;; in `org-duration-units'. The library also supports canonical
45 ;; duration, i.e., a duration that doesn't depend on user's settings,
46 ;; through optional arguments.
52 (declare-function org-trim
"org-trim" (s &optional keep-lead
))
57 (defconst org-duration-canonical-units
61 "Canonical time duration units.
62 See `org-duration-units' for details.")
64 (defcustom org-duration-units
70 ("y" .
,(* 60 24 365.25)))
71 "Conversion factor to minutes for a duration.
73 Each entry has the form (UNIT . MODIFIER).
75 In a duration string, a number followed by UNIT is multiplied by
76 the specified number of MODIFIER to obtain a duration in minutes.
78 For example, the following value
84 (\"m\" . ,(* 60 8 5 4))
85 (\"y\" . ,(* 60 8 5 4 10)))
87 is meaningful if you work an average of 8 hours per day, 5 days
88 a week, 4 weeks a month and 10 months a year.
90 When setting this variable outside the Customize interface, make
91 sure to call the following command:
93 \\[org-duration-set-regexps]"
96 :package-version
'(Org .
"9.1")
97 :set
(lambda (var val
) (set-default var val
) (org-duration-set-regexps))
98 :initialize
'custom-initialize-changed
100 (const :tag
"H:MM" 'h
:mm
)
101 (const :tag
"H:MM:SS" 'h
:mm
:ss
)
102 (alist :key-type
(string :tag
"Unit")
103 :value-type
(number :tag
"Modifier"))))
105 (defcustom org-duration-format
'(("d" . nil
) (special . h
:mm
))
106 "Format definition for a duration.
108 The value can be set to, respectively, `h:mm:ss' or `h:mm', which
109 means a duration is expressed as, respectively, a \"H:MM:SS\" or
112 Alternatively, the value can be a list of entries following the
117 UNIT is a unit string, as defined in `org-duration-units'. The
118 time duration is formatted using only the time components that
119 are specified here. If a time unit in missing, it falls back to
120 the next smallest unit.
122 A non-nil REQUIRED? value for these keys indicates that the
123 corresponding time component should always be included, even if
126 Eventually, the list can contain one of the following special
132 Units shorter than an hour are ignored. The hours and
133 minutes part of the duration is expressed unconditionally
134 with H:MM, or H:MM:SS, pattern.
136 (special . PRECISION)
138 A duration is expressed with a single unit, PRECISION being
139 the number of decimal places to show. The unit chosen is the
140 first one required or with a non-zero integer part. If there
141 is no such unit, the smallest one is used.
145 ((\"d\" . nil) (\"h\" . t) (\"min\" . t))
147 means a duration longer than a day is expressed in days, hours
148 and minutes, whereas a duration shorter than a day is always
149 expressed in hours and minutes, even when shorter than an hour.
151 On the other hand, the value
153 ((\"d\" . nil) (\"min\" . nil))
155 means a duration longer than a day is expressed in days and
156 minutes, whereas a duration shorter than a day is expressed
157 entirely in minutes, even when longer than an hour.
161 ((\"d\" . nil) (special . h:mm))
163 means that any duration longer than a day is expressed with both
164 a \"d\" unit and a \"H:MM\" part, whereas a duration shorter than
165 a day is expressed only as a \"H:MM\" string.
169 ((\"d\" . nil) (\"h\" . nil) (special . 2))
171 expresses a duration longer than a day as a decimal number, with
172 a 2-digits fractional part, of \"d\" unit. A duration shorter
173 than a day uses \"h\" unit instead."
177 :package-version
'(Org .
"9.1")
179 (const :tag
"Use H:MM" h
:mm
)
180 (const :tag
"Use H:MM:SS" h
:mm
:ss
)
181 (repeat :tag
"Use units"
183 (cons :tag
"Use units"
185 (choice (const :tag
"Skip when zero" nil
)
186 (const :tag
"Always used" t
)))
187 (cons :tag
"Use a single decimal unit"
189 (integer :tag
"Number of decimals"))
190 (cons :tag
"Use both units and H:MM"
193 (cons :tag
"Use both units and H:MM:SS"
198 ;;; Internal variables and functions
200 (defconst org-duration--h
:mm-re
201 "\\`[ \t]*[0-9]+\\(?::[0-9]\\{2\\}\\)\\{1,2\\}[ \t]*\\'"
202 "Regexp matching a duration expressed with H:MM or H:MM:SS format.
203 See `org-duration--h:mm:ss-re' to only match the latter. Hours
204 can use any number of digits.")
206 (defconst org-duration--h
:mm
:ss-re
207 "\\`[ \t]*[0-9]+\\(?::[0-9]\\{2\\}\\)\\{2\\}[ \t]*\\'"
208 "Regexp matching a duration expressed H:MM:SS format.
209 See `org-duration--h:mm-re' to also support H:MM format. Hours
210 can use any number of digits.")
212 (defvar org-duration--unit-re nil
213 "Regexp matching a duration with an unit.
214 Allowed units are defined in `org-duration-units'. Match group
215 1 contains the bare number. Match group 2 contains the unit.")
217 (defvar org-duration--full-re nil
218 "Regexp matching a duration expressed with units.
219 Allowed units are defined in `org-duration-units'.")
221 (defvar org-duration--mixed-re nil
222 "Regexp matching a duration expressed with units and H:MM or H:MM:SS format.
223 Allowed units are defined in `org-duration-units'. Match group
224 1 contains units part. Match group 2 contains H:MM or H:MM:SS
227 (defun org-duration--modifier (unit &optional canonical
)
228 "Return modifier associated to string UNIT.
229 When optional argument CANONICAL is non-nil, refer to
230 `org-duration-canonical-units' instead of `org-duration-units'."
231 (or (cdr (assoc unit
(if canonical
232 org-duration-canonical-units
233 org-duration-units
)))
234 (error "Unknown unit: %S" unit
)))
240 (defun org-duration-set-regexps ()
241 "Set duration related regexps."
243 (setq org-duration--unit-re
244 (concat "\\([0-9]+\\(?:\\.[0-9]*\\)?\\)[ \t]*"
245 ;; Since user-defined units in `org-duration-units'
246 ;; can differ from canonical units in
247 ;; `org-duration-canonical-units', include both in
249 (regexp-opt (mapcar #'car
(append org-duration-canonical-units
252 (setq org-duration--full-re
253 (format "\\`[ \t]*%s\\(?:[ \t]+%s\\)*[ \t]*\\'"
254 org-duration--unit-re
255 org-duration--unit-re
))
256 (setq org-duration--mixed-re
257 (format "\\`[ \t]*\\(?1:%s\\(?:[ \t]+%s\\)*\\)[ \t]+\
258 \\(?2:[0-9]+\\(?::[0-9][0-9]\\)\\{1,2\\}\\)[ \t]*\\'"
259 org-duration--unit-re
260 org-duration--unit-re
)))
263 (defun org-duration-p (s)
264 "Non-nil when string S is a time duration."
266 (or (string-match-p org-duration--full-re s
)
267 (string-match-p org-duration--mixed-re s
)
268 (string-match-p org-duration--h
:mm-re s
))))
271 (defun org-duration-to-minutes (duration &optional canonical
)
272 "Return number of minutes of DURATION string.
274 When optional argument CANONICAL is non-nil, ignore
275 `org-duration-units' and use standard time units value.
277 As a special case, a bare number represents minutes.
279 Return value as a float. Raise an error if duration format is
282 ((string-match-p org-duration--h
:mm-re duration
)
283 (pcase-let ((`(,hours
,minutes
,seconds
)
284 (mapcar #'string-to-number
(split-string duration
":"))))
285 (+ (/ (or seconds
0) 60.0) minutes
(* 60 hours
))))
286 ((string-match-p org-duration--full-re duration
)
289 (while (setq s
(string-match org-duration--unit-re duration
(1+ s
)))
290 (let ((value (string-to-number (match-string 1 duration
)))
291 (unit (match-string 2 duration
)))
292 (cl-incf minutes
(* value
(org-duration--modifier unit canonical
)))))
294 ((string-match org-duration--mixed-re duration
)
295 (let ((units-part (match-string 1 duration
))
296 (hms-part (match-string 2 duration
)))
297 (+ (org-duration-to-minutes units-part
)
298 (org-duration-to-minutes hms-part
))))
299 ((string-match-p "\\`[0-9]+\\(\\.[0-9]*\\)?\\'" duration
)
300 (float (string-to-number duration
)))
301 (t (error "Invalid duration format: %S" duration
))))
304 (defun org-duration-from-minutes (minutes &optional fmt canonical
)
305 "Return duration string for a given number of MINUTES.
307 Format duration according to `org-duration-format' or FMT, when
310 When optional argument CANONICAL is non-nil, ignore
311 `org-duration-units' and use standard time units value.
313 Raise an error if expected format is unknown."
314 (pcase (or fmt org-duration-format
)
316 (let ((minutes (floor minutes
)))
317 (format "%d:%02d" (/ minutes
60) (mod minutes
60))))
319 (let ((seconds (floor (* 60 minutes
)) ))
321 (org-duration-from-minutes (/ seconds
60) 'h
:mm
)
323 ((pred atom
) (error "Invalid duration format specification: %S" fmt
))
324 ;; Mixed format. Call recursively the function on both parts.
325 ((and duration-format
326 (let `(special .
,(and mode
(or `h
:mm
:ss
`h
:mm
)))
327 (assq 'special duration-format
)))
328 (let* ((truncated-format
329 ;; Remove "special" mode from duration format in order to
330 ;; recurse properly. Also remove units smaller or equal
331 ;; to an hour since H:MM part takes care of it.
335 (`(,(and unit
(pred stringp
)) .
,_
)
336 (> (org-duration--modifier unit canonical
) 60))
339 (min-modifier ;smallest modifier above hour
340 (and truncated-format
343 (org-duration--modifier (car p
) canonical
))
344 truncated-format
)))))
345 (if (or (null min-modifier
) (< minutes min-modifier
))
346 ;; There is not unit above the hour or the smallest unit
347 ;; above the hour is too large for the number of minutes we
348 ;; need to represent. Use H:MM or H:MM:SS syntax.
349 (org-duration-from-minutes minutes mode canonical
)
350 ;; Represent minutes above hour using provided units and H:MM
352 (let* ((units-part (* min-modifier
(floor (/ minutes min-modifier
))))
353 (minutes-part (- minutes units-part
)))
355 (org-duration-from-minutes units-part truncated-format canonical
)
357 (org-duration-from-minutes minutes-part mode
))))))
361 (let ((digits (cdr (assq 'special duration-format
))))
363 (or (wholenump digits
)
364 (error "Unknown formatting directive: %S" digits
))
365 (format "%%.%df" digits
))))
368 ;; Ignore special format cells.
369 (lambda (pair) (pcase pair
(`(special .
,_
) t
) (_ nil
)))
372 (> (org-duration--modifier (car a
) canonical
)
373 (org-duration--modifier (car b
) canonical
))))))
375 ;; Fractional duration: use first unit that is either required
376 ;; or smaller than MINUTES.
384 (<= (org-duration--modifier u canonical
)
387 ;; Fall back to smallest unit.
388 (org-last selected-units
))))
389 (modifier (org-duration--modifier unit canonical
)))
390 (concat (format fractional
(/ (float minutes
) modifier
)) unit
)))
391 ;; Otherwise build duration string according to available
397 (pcase-let* ((`(,unit .
,required?
) units
)
398 (modifier (org-duration--modifier unit canonical
)))
399 (cond ((<= modifier minutes
)
400 (let ((value (floor (/ minutes modifier
))))
401 (cl-decf minutes
(* value modifier
))
402 (format " %d%s" value unit
)))
403 (required?
(concat " 0" unit
))
407 ;; No unit can properly represent MINUTES. Use the smallest
410 (pcase-let ((`((,unit .
,_
)) (last selected-units
)))
412 (if (not fractional
) "0"
413 (let ((modifier (org-duration--modifier unit canonical
)))
414 (format fractional
(/ (float minutes
) modifier
))))
418 (defun org-duration-h:mm-only-p
(times)
419 "Non-nil when every duration in TIMES has \"H:MM\" or \"H:MM:SS\" format.
421 TIMES is a list of duration strings.
423 Return nil if any duration is expressed with units, as defined in
424 `org-duration-units'. Otherwise, if any duration is expressed
425 with \"H:MM:SS\" format, return `h:mm:ss'. Otherwise, return
430 (cond ((string-match-p org-duration--full-re time
)
432 ((string-match-p org-duration--mixed-re time
)
435 ((string-match-p org-duration--h
:mm
:ss-re time
)
436 (setq hms-flag
'h
:mm
:ss
))))
437 (or hms-flag
'h
:mm
))))
442 (org-duration-set-regexps)
444 (provide 'org-duration
)
445 ;;; org-duration.el ends here