I think the most general [and cryptic] solution could be this
function hms[seconds] {
return [3600, 60]
.reduceRight[
[pipeline, breakpoint] => remainder =>
[Math.floor[remainder / breakpoint]].concat[pipeline[remainder % breakpoint]],
r => [r]
][seconds]
.map[amount => amount.toString[].padStart[2, '0']]
.join['-'];
}
Or to copy & paste the shortest version
function hms[seconds] {
return [3600, 60]
.reduceRight[
[p, b] => r => [Math.floor[r / b]].concat[p[r % b]],
r => [r]
][seconds]
.map[a => a.toString[].padStart[2, '0']]
.join['-'];
}
Some example outputs:
> hms[0]
< "00-00-00"
> hms[5]
< "00-00-05"
> hms[60]
< "00-01-00"
> hms[3785]
< "01-03-05"
> hms[37850]
< "10-30-50"
> hms[378500]
< "105-08-20"
How it works
Algorithm
- To get hours you divide total seconds by 3600 and floor it.
- To get minutes you divide remainder by 60 and floor it.
- To get seconds you just use the remainder.
It would also be nice to keep individual amounts in an array for easier formatting.
For example given the input of 3785s the output should be [1, 3, 5]
, that is 1 hour, 3 minutes and 5 seconds.
Creating pipeline
Naming the 3600 and 60 constants "breakpoints" you can write this algorithm into function as this
function divideAndAppend[remainder, breakpoint, callback] {
return [Math.floor[remainder / breakpoint]].concat[callback[remainder % breakpoint]];
}
It returns an array where first item is the amount for given breakpoint and the rest of the array is given by the callback. Reusing the divideAndAppend
in callback function will give you a pipeline of composed
divideAndAppend
functions. Each one of these computes amount per given breakpoint and append it to the array making your desired output.
Then you also need the "final" callback that ends this pipeline. In another words you used all breakpoints and now you have only the remainder. Since you have already the answer at 3] you should use some sort of identity function, in this case remainder => [remainder]
.
You can now write the pipeline like this
let pipeline = r3 => divideAndAppend[
r3,
3600,
r2 => divideAndAppend[
r2,
60,
r1 => [r1]]];
> pipeline[3785]
< [1, 3, 5]
Cool right?
Generalizing using for-loop
Now you can generalize with a variable amount of breakpoints and create a for-loop that will compose individial divideAndAppend
functions into the pipeline. You start with the identity function r1 => [r1]
, then use the 60
breakpoint and finally use the 3600
breakpoint.
let breakpoints = [60, 3600];
let pipeline = r => [r];
for [const b of breakpoints] {
const previousPipeline = pipeline;
pipeline = r => divideAndAppend[r, b, previousPipeline];
}
> pipeline[3785]
< [1, 3, 5]
Using Array.prototype.reduce[]
Now you can rewrite this for-loop into reducer for shorter and more functional code. In other words rewrite function composition into the reducer.
let pipeline = [60, 3600].reduce[
[ppln, b] => r => divideAndAppend[r, b, ppln],
r => [r]
];
> pipeline[3785]
< [1, 3, 5]
The
accumulator ppln
is the pipeline and you are composing it using the previous version of it. The initial pipeline is r => [r]
.
You can now inline the function divideAndAppend
and use Array.prototype.reduceRight
which is the same as [].reverse[].reduce[...]
to make the breakpoints definitions more natural.
let pipeline = [3600, 60]
.reduceRight[
[ppln, b] => r => [Math.floor[r / b]].concat[ppln[r % b]],
r => [r]
];
Which is the final form. Then you just appy mapping to string with padded 0's on left and join the strings with :
separator;
More generalizations
Wrapping the reducer into function
function decompose[total, breakpoints] {
return breakpoints.reduceRight[
[p, b] => r => [Math.floor[r / b]].concat[p[r % b]],
r => [r]
][total];
}
> decompose[3785, [3600, 60]]
< [1, 3, 5]
you now have very general algorithm you can work with. For example:
Convert easily [the weird] us length standards
Given the standards
1 foot | 12 inches |
1 yard | 3 feet |
1 mile | 1760 yards |
> decompose[123_456, [1760 * 3 * 12, 3 * 12, 12]]
< [1, 1669, 1, 0]
123456 in = 1 mi, 1669 yd, 1 feet and 0 in
Or you can somewhat convert to decimal or binary representations
> decompose[123_456, [100_000, 10_000, 1000, 100, 10]]
< [1, 2, 3, 4, 5, 6]
> decompose[127, [128, 64, 32, 16, 8, 4, 2]]
< [0, 1, 1, 1, 1, 1, 1, 1]
Works also with floating point breakpoints
Since Javascript supports mod
operator with floating point numbers, you can also do
> decompose[26.5, [20, 2.5]]
< [1, 2, 1.5]
The edge case of no breakpoints is also naturally covered
> decompose[123, []]
< [123]