# command to display time (duration) in human readable form

I spent 3 hours trying to write a command to format a time (duration) expressed as a floating number (in seconds) in a nice form as follows:

if the time is < 60 write as a float with 3 decimals (i.e. milliseconds)
else let h,m,s be the hours, minutes and seconds corresponding to the time
if h=0 then write as m:ss (ss always with 2 digits, m can have only 1)
else write h:mm:ss (mm and ss with 2 digits, h can have only 1)

So 1.25678 gives 1.256, 125.35 gives 2:05, 18249 (which equals to 5*3600+4*60+9) gives 5:04:09.

I tried several packages for the comparison/formatting. No success. Can someone help me ? Thanks !

• You wrote, inter alia, that "1.25678 gives 1.256". This looks like truncation (to the nearest smaller millisecond) is required. Is this interpretation correct, or would you prefer rounding? Please advise. – Mico Feb 5 '16 at 16:41
• I wrote 1.256 simply truncating but rounding to the nearest is better. – didou Feb 6 '16 at 0:51

You can do it with expl3 and its fp module.

\documentclass{article}
\usepackage{xparse}

\ExplSyntaxOn
\NewDocumentCommand{\duration}{m}
{
\didou_duration:n { #1 }
}

\fp_new:N \l_didou_duration_hrs_fp
\fp_new:N \l_didou_duration_min_fp
\fp_new:N \l_didou_duration_sec_fp

\cs_new_protected:Nn \didou_duration:n
{
\fp_compare:nTF { #1 < 60 }
{% easy case
\fp_eval:n { round(#1,3) }
}
{
\fp_compare:nTF { #1 < 3600 }
{% only minutes
\didou_duration_minutes:n { #1 }
}
{% hours
\didou_duration_hours:n { #1 }
}
}
}

\cs_new_protected:Nn \didou_duration_minutes:n
{
\fp_set:Nn \l_didou_duration_min_fp { trunc( #1/60,0 ) }
\fp_set:Nn \l_didou_duration_sec_fp
{
round( #1-\l_didou_duration_min_fp * 60,0 )
}
% now print
\fp_eval:n { \l_didou_duration_min_fp }
:
\fp_compare:nT { \l_didou_duration_sec_fp < 10 } { 0 } % two digits
\fp_eval:n { \l_didou_duration_sec_fp }
}
\cs_generate_variant:Nn \didou_duration_minutes:n { V }

\cs_new_protected:Nn \didou_duration_hours:n
{
\fp_set:Nn \l_didou_duration_hrs_fp { trunc( #1/3600,0 ) }
\fp_set:Nn \l_didou_duration_min_fp { #1 - \l_didou_duration_hrs_fp * 3600 }
% print the hours
\fp_eval:n { \l_didou_duration_hrs_fp } :
% go to minutes
\fp_compare:nT { \l_didou_duration_min_fp < 600 } { 0 }
\didou_duration_minutes:V \l_didou_duration_min_fp
}

\ExplSyntaxOff

\begin{document}

\duration{1.25678}

\duration{125.35}

\duration{249}

\duration{18249}

\duration{2318249}

\end{document}

If needed, here's a fully expandable version:

\documentclass{article}
\usepackage{xparse}

\ExplSyntaxOn
\DeclareExpandableDocumentCommand{\duration}{m}
{
\didou_duration:n { #1 }
}

\cs_new:Nn \didou_duration:n
{
\fp_compare:nTF { #1 < 60 }
{% easy case
\fp_eval:n { round(#1,3) }
}
{
\fp_compare:nTF { #1 < 3600 }
{% only minutes
\didou_duration_minutes:n { #1 }
}
{% hours
\didou_duration_hours:n { #1 }
}
}
}

\cs_new:Nn \didou_duration_minutes:n
{
\fp_eval:n { trunc( #1/60,0 ) }
:
\fp_compare:nT { round( #1 - trunc( #1/60,0 ) * 60,0 ) < 10 } { 0 } % two digits
\fp_eval:n { round( #1- trunc( #1/60,0 ) * 60,0 ) }
}
\cs_generate_variant:Nn \didou_duration_minutes:n { f }

\cs_new:Nn \didou_duration_hours:n
{
% print the hours
\fp_eval:n { trunc( #1/3600,0 ) } :
% go to minutes
\fp_compare:nT { trunc( #1/3600,0 ) < 600 } { 0 }
\didou_duration_minutes:f { \fp_eval:n { #1 - trunc( #1/3600,0 ) * 3600} }
}

\ExplSyntaxOff

\begin{document}

\duration{1.25678}

\duration{125.35}

\duration{249}

\duration{18249}

\duration{2318249}

\edef\test{\duration{2318249}}\texttt{\meaning\test}

\end{document}

• It would seem that the OP wants truncation to the nearest millisecond, i.e., \duration{1.25678} should evaluate to "1.256", not "1.257". – Mico Feb 5 '16 at 16:11
• @Mico In that case, use trunc instead of round in the “easy case”. But 1.257 is nearer to 1.25678 than 1.256 – egreg Feb 5 '16 at 16:12
• Let's see if the OP weighs in and expresses a preference for truncation or rounding of 1.25678. – Mico Feb 5 '16 at 16:19
• Another issue: \duration{1} gives 1, whereas the OP's specifications suggest the "correct" answer should be 1.000. – Mico Feb 5 '16 at 20:47
• @Mico That's easy to add, if needed. – egreg Feb 5 '16 at 20:54

Using \pgfmathparse is problematic for this question because integer division fails with a "Dimension too large" error around 16500. In particular, without some additional care, any solution using \pgfmathparse will not be able to cope with the OP's example of 18249 because this number is too large.

For large (integer) calculations the xint family of packages are hard to beat (unfortunately, I find its documentation difficult to follow because few examples are given). Using xinttools and xintfrac, together with siunitx we can answer this question and, in particular, cope with the "large" example of 18249.

Code

\documentclass{article}
\usepackage{siunitx,xinttools,xintfrac}

\newcommand\Duration[1]{%
\xintifLt{#1}{60}{% less than 60 => print as seconds, with milliseconds
\num[round-mode=places,round-precision=3]{#1}%
}{% have minutes seconds and possibly hours
\xintAssign\xintiiDivision{\xintNum{#1}}{3600}\to\hours\minutes%
\xintAssign\xintiiDivision{\minutes}{60}\to\minutes\seconds%
\xintiiifGt{\hours}{0}{% hours>0%
\hours:\num[minimum-integer-digits=2]{\minutes}:\num[minimum-integer-digits=2]{\seconds}%
}{% only minutes and seconds
\minutes:\num[minimum-integer-digits=2]{\seconds}%
}%
}%
}

\begin{document}

1.25678: \Duration{1.25678}

125.35: \Duration{125.35}

18249: \Duration{18249}

\end{document}

Output

This produces the desired output:

Some explanation

Given that I struggled a little to work out how to use the xint commands a few words of explanation are in order.

1. The macro \xintiiDivision returns the quotient and the remainder and then \xintAssign is used to assign (these two outputs) to macros.

2. The comparison operator \xintifLt accepts decimal numbers whereas \xintiiifGt is faster and expects an integer. Thanks to @jfbu for explaining this in the comments.

3. The \num command from siunitx is used to format the output and, in particular, pad with leading zeros via minimum-integer-digits=2.

Quite possibly the xint commands can be used more efficiently!

• Brillant ! Thank you. Now I try to understand... – didou Feb 5 '16 at 7:16
• I added an % in the then-part for safety (e.g. inclusion in tabular environment) – didou Feb 5 '16 at 8:07
• FYI, This requires a recent version of xinit packages v1.2e 2015/11/22 (the one distributed with my ubuntu 14.04 does not work, i.e. version 1.09kb) – didou Feb 5 '16 at 11:06
• I am using xintcore 2014/11/07 v1.1a, xint 2014/11/07 v1.1a and xintfrac 2014/11/07 v1.1a which ship with TeXLive 2015. It works with these. – user30471 Feb 5 '16 at 11:42
• @didou version 1.09kb dates back to 2014/02/13. Release 1.1 of 2014/10/28 added quite a few things, then 1.2 (2015/10/10) boosted the speed of the core arithmetic routines. Some bugs introduced in this 1.2 were fixed during the fall of 2015. The package is yet to tackle arbitrary precision floating point math functions. – user4686 Feb 5 '16 at 23:39

Here is a solution using apnum package:

\input apnum

\def\duration#1{{\apFRAC=0         % "/" means integer division
\def\zero{0}%
\evaldef\S{#1/1}%               \S: number of seconds (without fraction part)
\evaldef\H{\S/3600}%            \H: number of hours
\evaldef\Hrest{\S - 3600*\H}%   \Hrest: number of seconds without hours
\evaldef\M{\Hrest/60}%          \M: minutes in \Hrest
\evaldef\Mrest{\Hrest - 60*\M}% \Mrest: number of seconds without minutes
\ifx\H\zero
\ifx\M\zero \miliseconds#1...\end         % print: seconds.miliseconds
\else \M:\twodigits\Mrest \fi             % print: minutes:seconds
\else \H:\twodigits\M:\twodigits\Mrest \fi  % print: hours:minutes:seconds
}}
\def\twodigits#1{\ifnum#1<10 0\fi#1}
\def\miliseconds #1.#2#3#4\end{#1\ifx.#2\else.#2\ifx.#3\else#3\fi\fi}

1.25678: \duration{1.25678}  % prints: 1.25

125.35:  \duration{125.35}   % prints: 2:05

18249:   \duration{18249}    % prints: 5:04:09

\bye

Edit And second solution. It uses only TeX primitives and \newcount macro. And the idea is the same as above. The time is limited to 2^31 seconds now, it means 68 years.

\newcount\S \newcount\H  \newcount\Hrest  \newcount\M  \newcount\Mrest

\def\duration#1{%
\setS#1.\end
\H=\S \divide\H by3600
\Hrest=\H \multiply\Hrest by-3600 \advance\Hrest by\S
\M=\Hrest \divide\M by60
\Mrest=\M \multiply\Mrest by-60 \advance\Mrest by\Hrest
\ifnum \H=0
\ifnum\M=0 \miliseconds#1...\end             % print: seconds.miliseconds
\else \the\M:\twodigits\Mrest \fi            % print: minutes:seconds
\else \the\H:\twodigits\M:\twodigits\Mrest \fi % print: hours:minutes:seconds
}
\def\setS#1.#2\end{\S=#1\relax}
\def\twodigits#1{\ifnum#1<10 0\fi\the#1}
\def\miliseconds #1.#2#3#4\end{#1\ifx.#2\else.#2\ifx.#3\else#3\fi\fi}

1.25678: \duration{1.25678}

125.35:  \duration{125.35}

18249:   \duration{18249}

\bye

As pointed out by Andrew, pgfmath cannot by default cannot handle numbers larger than 16383.99999; however, the pgfmath library can handle floating point numbers thereby allowing (nearly) arbitrarily large numbers to be handled. The limitation then becomes the precision, and ultimately certain digits are truncated.

# Floating Point Version

This function will handle (nearly) arbitrarily large numbers; however, it trades off accuracy. This only becomes significant if you have handling about 8-9 digits (including the decimals).

\usepackage{pgf}
\usepackage{pgfmath}
\usepgflibrary{fpu}

\def\formattime#1{
\edef\seconds{#1}
\pgfkeys{/pgf/fpu=true}
% Calculate hours and adjust seconds
\pgfmathsetmacro\hours{int(\seconds / 3600)}
\pgfmathsetmacro\seconds{\seconds - 3600 * \hours}
% Calculate minutes
\pgfmathsetmacro\minutes{floor(\seconds / 60)}
% Adjust minutes in case we get 60 minutes
\pgfmathsetmacro\hours{\minutes >= 60 ? \hours + 1 : \hours}
\pgfmathsetmacro\minutes{\minutes >= 60 ? \minutes - 60 : \minutes}
% Adjust the seconds to account for the minutes
\pgfmathsetmacro\seconds{\seconds - 60 * \minutes}
% Adjust the seconds in case we get 60 seconds
\pgfmathsetmacro\minutes{\seconds >= 60 ? \minutes + 1 : \minutes}
\pgfmathsetmacro\seconds{\seconds >= 60 ? \seconds - 60 : \seconds}
% We now have all the right values; we just need to display them properly.
% For the minutes, divide by 10 and print without decimal separator so we get
\pgfmathsetmacro\minutes{\minutes/10}
% For the seconds, we need to separate the integral from the decimal part.
\pgfmathparse{floor(\seconds)/10}
% The floor function may push '0' to '-1'
\pgfmathparse{\pgfmathresult < 0 ? 0 : \pgfmathresult}
\pgfmathprintnumberto[fixed, zerofill, precision=1, dec sep={}]{\pgfmathresult}{\secondsINT}
\pgfmathparse{round(1000 * (\seconds - floor(\seconds))}
\pgfmathparse{\pgfmathresult > 999 ? \pgfmathresult - 1000 : \pgfmathresult}
\pgfmathprintnumberto[int detect]{\pgfmathresult}{\secondsDEC}
% Display the results
\pgfmathprintnumber[int detect]{\hours}:%
\pgfmathprintnumber[fixed, zerofill, precision=1, dec sep={}]{\minutes}:%
\secondsINT.\secondsDEC
}

# Standard Version

This function performs all the arithmetic internally by handling numbers as dimensions and as a consequence, there is a hard cutoff at 16383.99999. (The units is technically pt I believe, which corresponds to a little under 1.4 metres at 300dpi)

Note that the fpu library is still loaded in this case for the \pgfmathprintnumber functions.

\usepackage{pgf}
\usepackage{pgfmath}
\usepgflibrary{fpu}

\def\formattime#1{
\edef\seconds{#1}
% Calculate hours and adjust seconds
\pgfmathsetmacro\hours{int(\seconds / 3600)}
\pgfmathsetmacro\seconds{\seconds - 3600 * \hours}
% Calculate minutes
\pgfmathsetmacro\minutes{floor(\seconds / 60)}
% Adjust the seconds to account for the minutes
\pgfmathsetmacro\seconds{\seconds - 60 * \minutes}
% We now have all the right values; we just need to display them properly.
% For the minutes, divide by 10 and print without decimal separator so we get
\pgfmathsetmacro\minutes{\minutes/10}
% For the seconds, we need to separate the integral from the decimal part.
\pgfmathparse{floor(\seconds)/10}
% The floor function may push '0' to '-1'
\pgfmathparse{\pgfmathresult < 0 ? 0 : \pgfmathresult}
\pgfmathprintnumberto[fixed, zerofill, precision=1, dec sep={}]{\pgfmathresult}{\secondsINT}
\pgfmathparse{round(1000 * (\seconds - floor(\seconds))}
\pgfmathparse{\pgfmathresult > 999 ? \pgfmathresult - 1000 : \pgfmathresult}
\pgfmathprintnumberto[int detect]{\pgfmathresult}{\secondsDEC}
% Display the results
\pgfmathprintnumber[int detect]{\hours}:%
\pgfmathprintnumber[fixed, zerofill, precision=1, dec sep={}]{\minutes}:%
\secondsINT.\secondsDEC
}

# Results

Testing both functions above with the following inputs

\formattime{1.3579}
\formattime{60}
\formattime{136.14}
\formattime{3600}
\formattime{86400}
\formattime{276872}
\formattime{123456789}

Gives you

Floating Point | Standard    |      Expected
--------------------|-------------|-----------------
0:00:01.358    | 0:00:01.358 |      0:00:01.458
0:01:00.0      | 0:01:00.0   |      0:01:00.0
0:02:16.140    | 0:02:16.140 |      0:02:16.140
1:00:00.0      | 1:00:00.0   |      1:00:00.0
24:00:00.0      | 4:33:03.0   |     24:00:00.0
76:54:31.990    | 4:33:03.0   |     76:54:32.0
34,293:33:00.0      | 4:33:03.0   | 34,293:33:09.0

As you can see, the last two in the floating point function suffer due to precision, and the last three reach the ceiling in the standard function.

Lastly, it shouldn't be too difficult to extend this so that empty hour and minute fields are not displayed, but I hope that's a good start.

• There's a \pgfmathsetmacro command that you use instead of \pgfmathparse to store the result of the calculation in a macro. The main issue here, as I said in my answer, is that \formmattime{1829} blows up. – user30471 Feb 5 '16 at 11:59
• I've fixed it so that large number don't cause it to blow up, though very large numbers will result in a loss of precision. – JP-Ellis Feb 5 '16 at 13:24
• If I understood the OP's requirements correctly, he/she wants truncation to the nearest millisecond. Hence, \formattime{1.3579} should return "1.357" rather than "1.358". – Mico Feb 5 '16 at 16:14
• The OP only says that "if the time is < 60 write as a float with 3 decimals (i.e. milliseconds)". To me, this means it isn't truncation but actually should do the appropriate rounding. Admittedly, they do give one example which indicates truncation. – JP-Ellis Feb 5 '16 at 16:26
• Let's see if the OP weighs in and clarifies whether rounding or truncation is required. – Mico Feb 5 '16 at 16:39

(Updated the Lua code after the OP clarified that rounding rather than truncation should be applied to the seconds in the duration calculations.)

Here's a LuaLaTeX-based solution. The preamble sets up both a Lua function named formatted_duration and a TeX macro named \fdur. The TeX macro takes one argument (the duration in seconds and fractions of a second), which it passes to the Lua function to perform the hour-minute-second calculations and the required reformatting.

\documentclass{article}

%% Lua-side code
\usepackage{luacode}
\begin{luacode}
function round(num, idp)
local mult = 10^(idp or 0)
return math.floor(num * mult + 0.5) / mult
end

function formatted_duration ( n )
local h=0; local m=0; local s
n = tonumber ( n )
-- First, calculate hours, minutes, and seconds
if n >= 3600 then
h = math.floor (n / 3600)
n = n - h*3600
end
if n >= 60 then
m = math.floor ( n / 60 )
n = n - m*60
end
s = round ( n , 3 )

-- Next, format h, m and s as needed, then return the formatted string
if h>0 then
return tex.sprint ( h .. ":" .. string.format("%02d", m ) ..
":" .. string.format("%02d", round (s,0) ) )
elseif m>0 then
return tex.sprint ( m .. ":" .. string.format("%02d", round (s,0) ) )
else
return tex.sprint ( string.format ( "%.3f", s ) )
end
end
\end{luacode}

%% TeX-side code
\newcommand\fdur[1]{\directlua{formatted_duration(\luastring{#1})}}

\begin{document}
\begin{tabular}{ll}
Input & Output of \texttt{\string\fdur} \\[1ex]
1 & \fdur{1}             \\ % -> "1.000"
1.25678 & \fdur{1.25678} \\ % -> "1.257"
125.35  & \fdur{125.35}  \\ % -> "2:05"
18249   & \fdur{18249}   \\ % -> "5:04:09"
\end{tabular}
\end{document}

Already many good and diverse answers. I am adding one for another completely expandable macro. Complete expandability's constraint saves us from having to cook up names for temporary macros.

In the code below, one could make \Duration@i recursive and obtain in this way an expandable macro for conversion to sexagesimal notation, with arbitrary big integers on input. But after hours one is more tempted into using "days" and "years", also if one year counts as 365.25 days, then things are not pure-integer any more.

The update adds a bit simpler code at bottom of answer. (I was a bit rusty using xint package.)

\documentclass[border=10pt]{standalone}

\usepackage{xintfrac}% for expandably computing with decimals and fractions
\usepackage{xinttools}% programming utilities

\makeatletter
\newcommand*\Duration [1]{%
% this macro is expandable, which means it completely expands inside
% an \edef for example. But this does not matter much in real use case,
% except perhaps to use it directly in a \write to write to an external
% file. But no TeX primitive requiring expandability exists which is
% designed to handle the h:m:s or m:s output format.
%
% However to achieve expandability one is not allowed to define
% intermediate macros with \newcommand, or \pgfmathparse, etc... (or
% \xintAssign). As we are not allowed to define on the fly macros, we
% don't have to worry about choosing names. In the end, this makes for
% cleaner looking code. With lots (well, here only 2 or 3) tiny
% pre-prepared helper macros.
%
% Nota Bene: a TeX macro with "undelimited" parameters, like a LaTeX2e
% "command", will gobble spaces between its arguments. This explains
% why there aren't many %'s to avoid end of line spaces in the code below.
%
\xintifLt{#1}{60}
{% #1 < 60, #1 may be a decimal or even a fraction like 355/113
\xintRound {3}{#1}% rounds to three decimal places
}
{% #1 >= 60, drop the fractional part and proceeds with h:m:s
% truncation to an integer is done with \xintNum{#1} which is the
% same as \xintiTrunc {0}{#1}. To round, rather than truncate
% there is \xintiRound {0}{#1}.
\xintListWithSep
{:}
{\expandafter\Duration@i
\romannumeral0\xintiidivision{\xintNum{#1}}{60}}%
}%
}%

\newcommand*\Duration@i [1]{% #1 = 60h+m, seconds already split off
% indeed seconds are now in a virtual #2 argument which we don't use
% here.
\expandafter\Duration@ii\romannumeral0\xintiidivision{#1}{60}%
}%

\newcommand*\Duration@ii [3]{% #1=h, #2=m, #3=s
% Drop the h part if it is zero, pass to a formatting macro the
% seconds and possibly the minutes.
%
% We use \xintiiifGt because #1 may be >= 2^31. Else etoolbox's
% conditionals can be used (I think with name \ifnumgreater or like
% that). \xintiiifGt was in xint of TL2015; in TL2014 there is only
% \xintifGt which is more powerful as it handles also decimals and
% fractions, naturally it is a bit less efficient.
%
% Expansion of \Duration@format _is not done by_ \xintListWithSep.
% It will happen only later, if \Duration is expanded in an \edef
% for example.
\xintiiifGt {#1}{0}
{{#1}{\Duration@format {#2}}{\Duration@format{#3}}}
{{#2}{\Duration@format {#3}}}%
}%

% utility macro to insert a leading zero if needed
% #1 = integer from 0 to 99
\newcommand*\Duration@format [1]{\expandafter\@gobble\the\numexpr 100+#1\relax}

\makeatother

\newcommand*\TEST [1]{#1&$\to$&\Duration{#1}\\}
\begin{document}

\begin{tabular}{lcl}
\TEST {1.25678}
\TEST {125.35}
\TEST {18249}
\TEST {123456789123456789.123}
\end{tabular}

\end{document}

A variant not using package xinttools. This variant rounds, rather than truncates, the decimal input #1, if #1 is at least 60. More tests.

\documentclass[border=10pt]{standalone}

\usepackage{xintfrac}% for expandably computing with decimals and fractions

\makeatletter
\newcommand*\Duration [1]{% #1 is assumed non-negative.
\xintifLt{#1}{60}
{% #1 < 60, #1 may be a decimal or even a fraction like 355/113
\xintRound {3}{#1}% this rounds to three decimal places
}
{% #1 >= 60,
% we round the #1 to an integer (rounding away from zero)
% and proceed to h:m:s output
\expandafter\Duration@a
\romannumeral0\xintiidivision{\xintiRound{0}{#1}}{60}%
}%
}%

\newcommand*\Duration@a [2]{% #1 = 60h+m, #2=seconds
\expandafter\Duration@b
\romannumeral0\xintiidivision{#1}{60}:\Duration@format{#2}%
}%

\newcommand*\Duration@b [2]{% #1=h, #2=m
% the #1 here may be arbitrarily large integer
% macro \xintiiifZero needs xint 1.1 or later, else use \xintifZero
%
\xintiiifZero {#1}
{#2}% integer #1 is zero
{#1:\Duration@format {#2}}% integer #1 is positive
}%

% utility macro to insert a leading zero if needed
% #1 = integer from 0 to 99
% (there are many ways to do that)
\newcommand*\Duration@format [1]{\expandafter\@gobble\the\numexpr 100+#1\relax}

\makeatother

\newcommand*\TEST [1]{#1&$\to$&\Duration{#1}\\}
\begin{document}

\begin{tabular}{lcl}
\TEST {0.0001}
\TEST {0.001}
\TEST {1}
\TEST {1.25678}
\TEST {1.3579}
\TEST {60}
\TEST {125.35}
\TEST {136.14}
\TEST {249}
\TEST {3600}
\TEST {18249}
\TEST {86400}
\TEST {276872}
\TEST {2318249}
\TEST {123456789}
\TEST {999999999999}
\TEST {999999999999999}
\TEST {123456789123456789.123}
\TEST {123456789123456789.5}
\end{tabular}
\end{document}