Skip to content

Commit 939b0e4

Browse files
authored
Render negative times (#148)
This is particularly important for timers like Google Home that don't stay while ringing until explicitly dismissed; this lets me see how long the timer was ringing for.
1 parent 96c4b4e commit 939b0e4

File tree

2 files changed

+62
-24
lines changed

2 files changed

+62
-24
lines changed

src/format-time.ts

Lines changed: 43 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,22 @@
88
EDIT: This has been turned into a general-purpose time formatting library.
99
*/
1010

11-
type Resolution = "seconds"|"minutes"|"automatic";
11+
type Resolution = "seconds" | "minutes" | "automatic";
1212

1313

14-
const leftPad = (num: number) => (num < 10 ? `0${num}` : num);
14+
const leftPad = (num: number) => {
15+
num = Math.abs(num);
16+
return (num < 10 ? `0${num}` : num);
17+
};
18+
19+
20+
/** Rounds away from zero. */
21+
const roundUp = (num: number) => num > 0 ? Math.ceil(num) : Math.floor(num);
1522

1623
/** Returns "minutes" if seconds>=1hr, otherwise minutes. */
1724
function automaticRes(seconds: number) {
18-
if (seconds >= 3600) return "minutes"
19-
return "seconds"
25+
if (seconds >= 3600) return "minutes"
26+
return "seconds"
2027
}
2128

2229

@@ -28,48 +35,60 @@ export function formatFromResolution(seconds: number, resolution: Resolution): s
2835
}
2936

3037
function joinWithColons(h: number, m: number, s: number): string {
31-
if (h > 0) return `${h}:${leftPad(m)}:${leftPad(s)}`;
32-
if (m > 0) return `${m}:${leftPad(s)}`;
38+
if (h) return `${h}:${leftPad(m)}:${leftPad(s)}`;
39+
if (m) return `${m}:${leftPad(s)}`;
3340
return "" + s;
3441
}
3542
const hmsTime = (d: number) => {
36-
const h = Math.floor(d / 3600);
37-
const m = Math.floor((d % 3600) / 60);
38-
const s = Math.floor((d % 3600) % 60);
43+
const h = Math.trunc(d / 3600);
44+
const m = Math.trunc((d % 3600) / 60);
45+
const s = Math.trunc((d % 3600) % 60);
3946
return joinWithColons(h, m, s);
4047
}
4148
const hmTime = (d: number) => {
42-
const h = Math.floor(d / 3600);
49+
const h = Math.trunc(d / 3600);
4350
const m = Math.ceil((d % 3600) / 60); // Round up the minutes (#86)
4451
return joinWithColons(0, h, m);
4552
}
4653

54+
/** Like Math.truncate(), but truncates `-.5` to `'-0'` */
55+
function truncateWithSign(d: number): string {
56+
const result = Math.trunc(d);
57+
if (d < 0 && result === 0) return `-${-result}`;
58+
return result.toString();
59+
}
60+
4761
export default function formatTime(d: number, format: string) {
4862
if (format == 'hms') return hmsTime(d)
4963
if (format == 'hm') return hmTime(d)
50-
if (format == 'd') return ''+Math.ceil(d / 24 / 3600)
51-
if (format == 'h') return ''+Math.ceil(d / 3600)
52-
if (format == 'm') return ''+Math.ceil(d / 60)
53-
if (format == 's') return ''+Math.ceil(d)
5464

65+
// When rendering a single component, always round up to count consistent whole units.
66+
if (format == 'd') return '' + Math.ceil(d / 24 / 3600)
67+
if (format == 'h') return '' + Math.ceil(d / 3600)
68+
if (format == 'm') return '' + Math.ceil(d / 60)
69+
if (format == 's') return '' + Math.ceil(d)
70+
71+
72+
// When rendering multiple components, round towards zero because the fraction should
73+
// be represented by the next unit.
5574
return format.replace(/%(\w+)/g, (match, S) => {
5675
const sl = S.toLowerCase()
5776
if (sl.startsWith('hms')) return hmsTime(d) + S.substring(3)
5877
if (sl.startsWith('hm')) return hmTime(d) + S.substring(2)
59-
// 1 lowercase letter: ceil
78+
// 1 lowercase letter: round up
6079
if (S.startsWith('d')) return Math.ceil(d / 24 / 3600) + S.substring(1)
6180
if (S.startsWith('h')) return Math.ceil(d / 3600) + S.substring(1)
6281
if (S.startsWith('m')) return Math.ceil(d / 60) + S.substring(1)
6382
if (S.startsWith('s')) return Math.ceil(d) + S.substring(1)
64-
// 2 capital letter: pad + floor
65-
if (S.startsWith('HH')) return leftPad(Math.floor((d % (3600 * 24)) / 3600)) + S.substring(2)
66-
if (S.startsWith('MM')) return leftPad(Math.floor((d % 3600) / 60)) + S.substring(2)
67-
if (S.startsWith('SS')) return leftPad(Math.floor(d % 60)) + S.substring(2)
68-
// 1 capital letter: floor
69-
if (S.startsWith('D')) return Math.floor(d / 24 / 3600) + S.substring(1)
70-
if (S.startsWith('H')) return Math.floor((d % (3600 * 24)) / 3600) + S.substring(1)
71-
if (S.startsWith('M')) return Math.floor((d % 3600) / 60) + S.substring(1)
72-
if (S.startsWith('S')) return Math.floor(d % 60) + S.substring(1)
83+
// 2 capital letter: pad + round down
84+
if (S.startsWith('HH')) return leftPad(Math.trunc((d % (3600 * 24)) / 3600)) + S.substring(2)
85+
if (S.startsWith('MM')) return leftPad(Math.trunc((d % 3600) / 60)) + S.substring(2)
86+
if (S.startsWith('SS')) return leftPad(Math.trunc(d % 60)) + S.substring(2)
87+
// 1 capital letter: round down, always include sign (for next component)
88+
if (S.startsWith('D')) return truncateWithSign(d / 24 / 3600) + S.substring(1)
89+
if (S.startsWith('H')) return truncateWithSign((d % (3600 * 24)) / 3600) + S.substring(1)
90+
if (S.startsWith('M')) return truncateWithSign((d % 3600) / 60) + S.substring(1)
91+
if (S.startsWith('S')) return truncateWithSign(d % 60) + S.substring(1)
7392
return match
7493
})
7594
}

test/format-time.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ it("Can render longer times", () => {
2828
expect(formatTime(d, 's')).toBe('100')
2929
expect(formatTime(d, 'the time is %hms!')).toBe('the time is 1:40!')
3030
expect(formatTime(d, 'the time is %hm!')).toBe('the time is 2!')
31+
32+
expect(formatTime(-d, 'hms')).toBe('-1:40')
33+
expect(formatTime(-d, 'hm')).toBe('-1')
34+
expect(formatTime(-d, 'd')).toBe('0')
3135
});
3236

3337
it("Can render long times", () => {
@@ -43,4 +47,19 @@ it("Can render long times", () => {
4347
expect(formatTime(d, 'the time is %s seconds')).toBe('the time is 10000 seconds')
4448
expect(formatTime(d, 'the time is %D:%H:%M:%S')).toBe('the time is 0:2:46:40')
4549
expect(formatTime(d, 'the time is %D:%HH:%MM:%SS')).toBe('the time is 0:02:46:40')
50+
51+
expect(formatTime(-d, 'hms')).toBe('-2:46:40')
52+
expect(formatTime(-d, 'hm')).toBe('-2:46')
53+
expect(formatTime(-d, 'h')).toBe('-2')
54+
expect(formatTime(-d, 'm')).toBe('-166')
55+
expect(formatTime(-d, 'the time is %d days')).toBe('the time is 0 days')
56+
expect(formatTime(-d, 'the time is %m minutes')).toBe('the time is -166 minutes')
57+
expect(formatTime(-d, 'the time is %D:%HH:%MM:%SS')).toBe('the time is -0:02:46:40')
58+
expect(formatTime(-d, 'the time is %H:%MM:%SS')).toBe('the time is -2:46:40')
4659
});
60+
61+
it('can render negative times', () => {
62+
expect(formatTime(-30, 'hms')).toBe('-30')
63+
expect(formatTime(-60, 'hms')).toBe('-1:00')
64+
expect(formatTime(-90, 'hms')).toBe('-1:30')
65+
});

0 commit comments

Comments
 (0)