2

Here my org table:

|   | Start date             | End date               | State | Duration |
|---+------------------------+------------------------+-------+----------|
| # | <2019-01-02 Wed 10:30> | <2019-01-02 Wed 12:35> | Taken |          |
| # | <2019-01-02 Thu 13:00> | <2019-01-02 Thu 18:00> | Taken |          |
|---+------------------------+------------------------+-------+----------|
|   |                        |                        |       |          |

I need in column Duration to get the next result:

|   | Start date             | End date               | State | Duration |
|---+------------------------+------------------------+-------+----------|
| # | <2019-01-02 Wed 10:30> | <2019-01-02 Wed 12:35> | Taken | 01:35    |
| # | <2019-01-02 Thu 13:00> | <2019-01-02 Thu 18:00> | Taken | 05:00    |
|---+------------------------+------------------------+-------+----------|
|   |                        |                        |       | 06.35   |

What is table formula for this?

2 Answers 2

5

Solution with calc hms duration strings

If you are a bit more flexible with the duration format you can use the hms form of calc. The advantage is that you can further process these values with calc formulas. For an example you can do the total sum of the durations.

The following lisp function avoids truncation errors in the time calculations. You can copy it to your init file if you like. "Start date" and "End date" can also differ in day. Full days are counted with worktime-per-day hours. Only business days are counted.

(defun calcFunc-dateDiffToHMS (date1 date2 worktime-per-day)
  "Calculate the difference of DATE1 and DATE2 in HMS form.
Each day counts with WORKTIME-PER-DAY hours."
  (cl-labels ((dateTrunc (date)
             (calcFunc-date (calcFunc-year date)
                    (calcFunc-month date)
                    (calcFunc-day date)))
          (datep (date)
             (and (listp date)
              (eq (car date) 'date))))
    (if (and (datep date1)
         (datep date2))
    (let* ((business-days (calcFunc-bsub
                   (dateTrunc date1)
                   (dateTrunc date2))))
      (calcFunc-add
       (calcFunc-hms (calcFunc-mul business-days worktime-per-day) 0 0)
       (calcFunc-sub (calcFunc-time date1) (calcFunc-time date2)))
      )
      0)))

Example for the usage:

|   | Start date             | End date               | State | Duration   |
|---+------------------------+------------------------+-------+------------|
| # | <2019-01-02 Wed 10:30> | <2019-01-02 Wed 12:35> | Taken | 2@ 5' 0"   |
| # | <2019-01-02 Thu 13:00> | <2019-01-31 Thu 18:00> | Taken | 173@ 0' 0" |
|---+------------------------+------------------------+-------+------------|
|   |                        |                        |       | 175@ 5' 0" |
#+TBLFM: $5=dateDiffToHMS($3,$2,8)::@>$>=vsum(@I$>..@II$>)

Simple method to get colon-separated duration strings

A simple but hacky way to get a format that is close to your desired format would be to set:

(setq org-calc-default-modes (plist-put org-calc-default-modes 'calc-hms-format "%s:%s:%s"))

Example for the usage:

|   | Start date             | End date               | State | Duration |
|---+------------------------+------------------------+-------+----------|
| # | <2019-01-02 Wed 10:30> | <2019-01-02 Wed 12:35> | Taken |    2:5:0 |
| # | <2019-01-02 Thu 13:00> | <2019-01-31 Thu 18:00> | Taken |  173:0:0 |
|---+------------------------+------------------------+-------+----------|
|   |                        |                        |       |   #ERROR |
#+TBLFM: $5=dateDiffToHMS($3,$2,8)::@>$>=vsum(@I$>..@II$>)

The example already shows the benefits and the problems. The benefit is that you get the colon-separated form of the duration string. The problems are that

  1. one-digit numbers are not padded with zeros and
  2. one cannot use the colon-spearated duration strings for further calculations such as the total sum in the last cell.

More complicated but very general solution

The following lisp code defines filters for org table formulas in the customizable variable org-table-filters. These filters can be used for filtering the input values of table formulas after reading them off the table and for filtering the output values of those formulas before writing them into the table.

Two filters are currently pre-defined:

  • org-to-hms for transforming org duration strings like 2:05:00 into calc hms strings like 2@ 5' 0"
  • hms-to-org for the inverse operation of org-to-hms

Thereby org-to-hms and hms-to-org are actually the filter-ids (short FID). A FID is a shortcut for the actual function defined in org-table-filters.

You can use <(FID) and >(FID) as equation modes to apply the filter FID as input and output filter, respectively.

There follows the required Elisp code and afterwards an application example. If you like the code you can copy and paste it into your init file.

(defcustom org-table-filters '(("org-to-hms" . org-time-string-to-calc-hms)
                   ("hms-to-org" . org-calc-hms-to-org-time-string))
  "Alist of filters for org table formulas.
They can be applied for reading the arguments and writing the results.
The `car' of each member is the identifier of the filter
the `cdr' is the function to be called."
  :group 'org-table
  :type '(repeat (cons (string :tag "Identifier of the filter") (symbol "Filter function"))))

(defun org-calc-hms-to-org-time-string (str)
  "Transform calc hms duration to org time string in STR and visa versa."
  (if (string-match "-?\\(?:\\([0-9]+\\)@\\)? *\\([0-9]+\\)' *\\([0-9.eE+-]+\\)\"" str)
      (let ((hour (string-to-number (or (match-string 1 str) "0")))
        (min (string-to-number (match-string 2 str)))
        (sec (string-to-number (match-string 3 str))))
    (format "%d:%02d:%02d" hour min sec))
    str))

(defun org-time-string-to-calc-hms (str)
  "Transform org time string STR into calc hms format."
  (if (string-match "\\(-?\\)\\([0-9]+\\):\\([0-9]+\\):\\([0-9]+\\)" str)
      (let ((minus (match-string 1 str))
        (hour (string-to-number (or (match-string 2 str) "0")))
        (min (string-to-number (match-string 3 str)))
        (sec (string-to-number (match-string 4 str))))
    (if minus
        (format "-(%d@ %d' %d\")" hour min sec)
      (format "%d@ %d' %d\"" hour min sec)))
    str))

(defmacro org-table-filter (flags kind)
  "Return filter in FLAGS.
KIND may be \"<\" for input filter and \">\" for output filter.
If there is no filter of the requested kind in FLAGS return `identity'."
  `(if (string-match ,(concat kind "(\\([^)]+\\))") ,flags)
       (prog1
       (or (cdr (assoc-string (match-string 1 ,flags) org-table-filters)) #'identity)
     (setq ,flags (replace-match "" nil nil ,flags)))
     #'identity))

(defun org-table-input-filter (str filter)
  "Apply FILTER to string STR.
STR can also be a list.
In that case apply \\fn to each element of that list."
  (cond
   ((listp str)
    (mapcar (lambda (el)
          (org-table-input-filter el filter))
        str))
   ((stringp str)
    (funcall filter str))
   (t str)))

(defun org-table-eval-formula-filters (oldfun _arg equation &rest _args)
  "Apply filters to the arguments and the result of a table EQUATION.
This is an :override advice for OLDFUN `org-table-eval-formula'."
  (cl-multiple-value-bind
      (eq flags) (split-string equation ";")
    (if (stringp flags)
      (let ((input-filter (org-table-filter flags "<"))
        (output-filter (org-table-filter flags ">")))
    (cl-letf* ((old-justify-field-maybe (symbol-function 'org-table-justify-field-maybe))
           (old-table-make-reference (symbol-function 'org-table-make-reference))
           ((symbol-function 'org-table-make-reference)
            (lambda (elements &rest __args)
              (apply old-table-make-reference
                 (org-table-input-filter elements input-filter)
                 __args)))
           ((symbol-function 'org-table-justify-field-maybe)
            (lambda (&optional new)
              (funcall old-justify-field-maybe (and (stringp new)
                                (funcall output-filter new))))))
      (apply oldfun _arg (concat eq ";" flags) _args)))
      (apply oldfun _arg equation _args))))

(advice-add 'org-table-eval-formula :around #'org-table-eval-formula-filters)

Usage example:

|   | Start date             | End date               | State | Duration |
|---+------------------------+------------------------+-------+----------|
| # | <2019-01-02 Wed 10:30> | <2019-01-02 Wed 12:35> | Taken |  2:05:00 |
| # | <2019-01-02 Thu 13:00> | <2019-01-03 Thu 18:00> | Taken | 13:00:00 |
| # | <2019-01-03 Thu 13:00> | <2019-01-03 Thu 13:30> | Taken |  0:30:00 |
|---+------------------------+------------------------+-------+----------|
|   |                        |                        |       | 15:35:00 |
#+TBLFM: $5=dateDiffToHMS($3,$2,8);>(hms-to-org)::@>$>=vsum(@I$>..@II$>);>(hms-to-org)<(org-to-hms)

The results have exactly the required format and calculating the total sum of org durations is possible.

Tested with...

  • emacs-version: GNU Emacs 26.1 (build 1, x86_64-unknown-cygwin, GTK+ Version 3.22.28) of 2018-05-28
  • org-version: 9.1.14
1
  • Hi Tobias, how can I write dateDiffToHMS with just two parameters, when there is no worktime per day limit? Could you elaborate on what the function does in specific lines? Commented Jul 2, 2021 at 9:44
4

Borrowing the answer from SE -> https://stackoverflow.com/a/17681657/8544157

And adapting @juanleon 's answer to your structure, (you will need to add column 6):

#+TBLFM: $5=date(<$3>)-date(<$2>)::$6=60*60*24*$5;T

First step substracting the starting date from the end date.

Second step since the result is in days, simply multiply (* <your duration> 60 24 24)

The ;T, by the way, outputs the desired format.

|   | Start date             | End date               | State | Dur/days | formated |                                                                   
|---+------------------------+------------------------+-------+----------+----------|                                                         
| # | <2019-01-02 Wed 10:30> | <2019-01-02 Wed 12:35> | Taken | 0.086806 | 02:05:00 |                                                         
| # | <2019-01-02 Thu 13:00> | <2019-01-02 Thu 18:00> | Taken | 0.208333 | 04:59:59 |                                                         
|---+------------------------+------------------------+-------+----------+----------|                                                         
|   |                        |                        |       |          |          |                                                         
#+TBLFM: $5=date(<$3>)-date(<$2>)::$6=60*60*24*$5;T                                   
7
  • 2
    I'm not need column Dur/days. Is it possible without this column? Commented Jan 2, 2019 at 14:42
  • Why " 04:59:59" . Must be 05:00:00 Commented Jan 2, 2019 at 15:50
  • unfortunately you lose a second in the conversion to decimal that calc does.. Need to find a way to rectify. Commented Jan 2, 2019 at 15:56
  • This is a hack (+ 1 second). It's not good but work #+TBLFM: $6=date(<$3>)-date(<$2>)::$7=60*60*24*$6+1;T Commented Jan 2, 2019 at 15:58
  • I would suggest adding less than 1 second as the problem is due to rounding errors; any value <1 should do so just add 0.5, say.
    – éric
    Commented Jan 4, 2019 at 10:37

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.