Skip to content

Commit 8ff21d6

Browse files
authored
Merge pull request #1365 from github/aeisenberg/time
Extract time functions
2 parents 0d9f4e8 + 615cf86 commit 8ff21d6

File tree

6 files changed

+171
-49
lines changed

6 files changed

+171
-49
lines changed
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/*
2+
* Contains an assortment of helper functions for working with time, dates, and durations.
3+
*/
4+
5+
6+
const durationFormatter = new Intl.RelativeTimeFormat('en', {
7+
numeric: 'auto',
8+
});
9+
10+
// Months and years are approximate
11+
const MINUTE_IN_MILLIS = 1000 * 60;
12+
const HOUR_IN_MILLIS = 60 * MINUTE_IN_MILLIS;
13+
const DAY_IN_MILLIS = 24 * HOUR_IN_MILLIS;
14+
const MONTH_IN_MILLIS = 30 * DAY_IN_MILLIS;
15+
const YEAR_IN_MILLIS = 365 * DAY_IN_MILLIS;
16+
17+
/**
18+
* Converts a number of milliseconds into a human-readable string with units, indicating a relative time in the past or future.
19+
*
20+
* @param relativeTimeMillis The duration in milliseconds. A negative number indicates a duration in the past. And a positive number is
21+
* the future.
22+
* @returns A humanized duration. For example, "in 2 minutes", "2 minutes ago", "yesterday", or "tomorrow".
23+
*/
24+
export function humanizeRelativeTime(relativeTimeMillis?: number) {
25+
if (relativeTimeMillis === undefined) {
26+
return '';
27+
}
28+
29+
if (Math.abs(relativeTimeMillis) < HOUR_IN_MILLIS) {
30+
return durationFormatter.format(Math.floor(relativeTimeMillis / MINUTE_IN_MILLIS), 'minute');
31+
} else if (Math.abs(relativeTimeMillis) < DAY_IN_MILLIS) {
32+
return durationFormatter.format(Math.floor(relativeTimeMillis / HOUR_IN_MILLIS), 'hour');
33+
} else if (Math.abs(relativeTimeMillis) < MONTH_IN_MILLIS) {
34+
return durationFormatter.format(Math.floor(relativeTimeMillis / DAY_IN_MILLIS), 'day');
35+
} else if (Math.abs(relativeTimeMillis) < YEAR_IN_MILLIS) {
36+
return durationFormatter.format(Math.floor(relativeTimeMillis / MONTH_IN_MILLIS), 'month');
37+
} else {
38+
return durationFormatter.format(Math.floor(relativeTimeMillis / YEAR_IN_MILLIS), 'year');
39+
}
40+
}
41+
42+
/**
43+
* Converts a number of milliseconds into a human-readable string with units, indicating an amount of time.
44+
* Negative numbers have no meaning and are considered to be "Less than a minute".
45+
*
46+
* @param millis The number of milliseconds to convert.
47+
* @returns A humanized duration. For example, "2 minutes", "2 hours", "2 days", or "2 months".
48+
*/
49+
export function humanizeUnit(millis?: number): string {
50+
// assume a blank or empty string is a zero
51+
// assume anything less than 0 is a zero
52+
if (!millis || millis < MINUTE_IN_MILLIS) {
53+
return 'Less than a minute';
54+
}
55+
let unit: string;
56+
let unitDiff: number;
57+
if (millis < HOUR_IN_MILLIS) {
58+
unit = 'minute';
59+
unitDiff = Math.floor(millis / MINUTE_IN_MILLIS);
60+
} else if (millis < DAY_IN_MILLIS) {
61+
unit = 'hour';
62+
unitDiff = Math.floor(millis / HOUR_IN_MILLIS);
63+
} else if (millis < MONTH_IN_MILLIS) {
64+
unit = 'day';
65+
unitDiff = Math.floor(millis / DAY_IN_MILLIS);
66+
} else if (millis < YEAR_IN_MILLIS) {
67+
unit = 'month';
68+
unitDiff = Math.floor(millis / MONTH_IN_MILLIS);
69+
} else {
70+
unit = 'year';
71+
unitDiff = Math.floor(millis / YEAR_IN_MILLIS);
72+
}
73+
74+
return createFormatter(unit).format(unitDiff);
75+
}
76+
77+
function createFormatter(unit: string) {
78+
return Intl.NumberFormat('en-US', {
79+
style: 'unit',
80+
unit,
81+
unitDisplay: 'long'
82+
});
83+
}

extensions/ql-vscode/src/remote-queries/gh-actions-api-client.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -395,7 +395,8 @@ export async function getRepositoriesMetadata(credentials: Credentials, nwos: st
395395
const owner = node.owner.login;
396396
const name = node.name;
397397
const starCount = node.stargazerCount;
398-
const lastUpdated = Date.now() - new Date(node.updatedAt).getTime();
398+
// lastUpdated is always negative since it happened in the past.
399+
const lastUpdated = new Date(node.updatedAt).getTime() - Date.now();
399400
metadata[`${owner}/${name}`] = {
400401
starCount, lastUpdated
401402
};

extensions/ql-vscode/src/remote-queries/remote-queries-interface.ts

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import { URLSearchParams } from 'url';
2727
import { SHOW_QUERY_TEXT_MSG } from '../query-history';
2828
import { AnalysesResultsManager } from './analyses-results-manager';
2929
import { AnalysisResults } from './shared/analysis-result';
30+
import { humanizeUnit } from '../pure/time';
3031

3132
export class RemoteQueriesInterfaceManager {
3233
private panel: WebviewPanel | undefined;
@@ -249,23 +250,7 @@ export class RemoteQueriesInterfaceManager {
249250

250251
private getDuration(startTime: number, endTime: number): string {
251252
const diffInMs = startTime - endTime;
252-
return this.formatDuration(diffInMs);
253-
}
254-
255-
private formatDuration(ms: number): string {
256-
const seconds = ms / 1000;
257-
const minutes = seconds / 60;
258-
const hours = minutes / 60;
259-
const days = hours / 24;
260-
if (days > 1) {
261-
return `${days.toFixed(2)} days`;
262-
} else if (hours > 1) {
263-
return `${hours.toFixed(2)} hours`;
264-
} else if (minutes > 1) {
265-
return `${minutes.toFixed(2)} minutes`;
266-
} else {
267-
return `${seconds.toFixed(2)} seconds`;
268-
}
253+
return humanizeUnit(diffInMs);
269254
}
270255

271256
private formatDate = (millis: number): string => {

extensions/ql-vscode/src/remote-queries/view/LastUpdated.tsx

Lines changed: 5 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import * as React from 'react';
22
import { RepoPushIcon } from '@primer/octicons-react';
33
import styled from 'styled-components';
44

5+
import { humanizeRelativeTime } from '../../pure/time';
6+
57
const IconContainer = styled.span`
68
flex-grow: 0;
79
text-align: right;
@@ -17,13 +19,15 @@ const Duration = styled.span`
1719
type Props = { lastUpdated?: number };
1820

1921
const LastUpdated = ({ lastUpdated }: Props) => (
22+
// lastUpdated will be undefined for older results that were
23+
// created before the lastUpdated field was added.
2024
Number.isFinite(lastUpdated) ? (
2125
<>
2226
<IconContainer>
2327
<RepoPushIcon size={16} />
2428
</IconContainer>
2529
<Duration>
26-
{humanizeDuration(lastUpdated)}
30+
{humanizeRelativeTime(lastUpdated)}
2731
</Duration>
2832
</>
2933
) : (
@@ -32,31 +36,3 @@ const LastUpdated = ({ lastUpdated }: Props) => (
3236
);
3337

3438
export default LastUpdated;
35-
36-
const formatter = new Intl.RelativeTimeFormat('en', {
37-
numeric: 'auto'
38-
});
39-
40-
// All these are approximate, specifically months and years
41-
const MINUTE_IN_MILLIS = 1000 * 60;
42-
const HOUR_IN_MILLIS = 60 * MINUTE_IN_MILLIS;
43-
const DAY_IN_MILLIS = 24 * HOUR_IN_MILLIS;
44-
const MONTH_IN_MILLIS = 30 * DAY_IN_MILLIS;
45-
const YEAR_IN_MILLIS = 365 * DAY_IN_MILLIS;
46-
47-
function humanizeDuration(diff?: number) {
48-
if (!diff) {
49-
return '';
50-
}
51-
if (diff < HOUR_IN_MILLIS) {
52-
return formatter.format(- Math.floor(diff / MINUTE_IN_MILLIS), 'minute');
53-
} else if (diff < DAY_IN_MILLIS) {
54-
return formatter.format(- Math.floor(diff / HOUR_IN_MILLIS), 'hour');
55-
} else if (diff < MONTH_IN_MILLIS) {
56-
return formatter.format(- Math.floor(diff / DAY_IN_MILLIS), 'day');
57-
} else if (diff < YEAR_IN_MILLIS) {
58-
return formatter.format(- Math.floor(diff / MONTH_IN_MILLIS), 'month');
59-
} else {
60-
return formatter.format(- Math.floor(diff / YEAR_IN_MILLIS), 'year');
61-
}
62-
}

extensions/ql-vscode/src/remote-queries/view/RemoteQueries.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,14 +71,18 @@ const openQueryTextVirtualFile = (queryResult: RemoteQueryResult) => {
7171
});
7272
};
7373

74+
function createResultsDescription(queryResult: RemoteQueryResult) {
75+
const reposCount = `${queryResult.totalRepositoryCount} ${queryResult.totalRepositoryCount === 1 ? 'repository' : 'repositories'}`;
76+
return `${queryResult.totalResultCount} results from running against ${reposCount} (${queryResult.executionDuration}), ${queryResult.executionTimestamp}`;
77+
}
78+
7479
const sumAnalysesResults = (analysesResults: AnalysisResults[]) =>
7580
analysesResults.reduce((acc, curr) => acc + getAnalysisResultCount(curr), 0);
7681

7782
const QueryInfo = (queryResult: RemoteQueryResult) => (
7883
<>
7984
<VerticalSpace size={1} />
80-
{queryResult.totalResultCount} results from running against {queryResult.totalRepositoryCount} repositories
81-
({queryResult.executionDuration}), {queryResult.executionTimestamp}
85+
{createResultsDescription(queryResult)}
8286
<VerticalSpace size={1} />
8387
<span>
8488
<a className="vscode-codeql__query-info-link" href="#" onClick={() => openQueryFile(queryResult)}>
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { expect } from 'chai';
2+
import 'mocha';
3+
4+
import { humanizeRelativeTime, humanizeUnit } from '../../src/pure/time';
5+
6+
describe('Time', () => {
7+
it('should return a humanized unit', () => {
8+
expect(humanizeUnit(undefined)).to.eq('Less than a minute');
9+
expect(humanizeUnit(0)).to.eq('Less than a minute');
10+
expect(humanizeUnit(-1)).to.eq('Less than a minute');
11+
expect(humanizeUnit(1000 * 60 - 1)).to.eq('Less than a minute');
12+
expect(humanizeUnit(1000 * 60)).to.eq('1 minute');
13+
expect(humanizeUnit(1000 * 60 * 2 - 1)).to.eq('1 minute');
14+
expect(humanizeUnit(1000 * 60 * 2)).to.eq('2 minutes');
15+
expect(humanizeUnit(1000 * 60 * 60)).to.eq('1 hour');
16+
expect(humanizeUnit(1000 * 60 * 60 * 2)).to.eq('2 hours');
17+
expect(humanizeUnit(1000 * 60 * 60 * 24)).to.eq('1 day');
18+
expect(humanizeUnit(1000 * 60 * 60 * 24 * 2)).to.eq('2 days');
19+
20+
// assume every month has 30 days
21+
expect(humanizeUnit(1000 * 60 * 60 * 24 * 30)).to.eq('1 month');
22+
expect(humanizeUnit(1000 * 60 * 60 * 24 * 30 * 2)).to.eq('2 months');
23+
expect(humanizeUnit(1000 * 60 * 60 * 24 * 30 * 12)).to.eq('12 months');
24+
25+
// assume every year has 365 days
26+
expect(humanizeUnit(1000 * 60 * 60 * 24 * 365)).to.eq('1 year');
27+
expect(humanizeUnit(1000 * 60 * 60 * 24 * 365 * 2)).to.eq('2 years');
28+
});
29+
30+
it('should return a humanized duration positive', () => {
31+
expect(humanizeRelativeTime(undefined)).to.eq('');
32+
expect(humanizeRelativeTime(0)).to.eq('this minute');
33+
expect(humanizeRelativeTime(1)).to.eq('this minute');
34+
expect(humanizeRelativeTime(1000 * 60 - 1)).to.eq('this minute');
35+
expect(humanizeRelativeTime(1000 * 60)).to.eq('in 1 minute');
36+
expect(humanizeRelativeTime(1000 * 60 * 2 - 1)).to.eq('in 1 minute');
37+
expect(humanizeRelativeTime(1000 * 60 * 2)).to.eq('in 2 minutes');
38+
expect(humanizeRelativeTime(1000 * 60 * 60)).to.eq('in 1 hour');
39+
expect(humanizeRelativeTime(1000 * 60 * 60 * 2)).to.eq('in 2 hours');
40+
expect(humanizeRelativeTime(1000 * 60 * 60 * 24)).to.eq('tomorrow');
41+
expect(humanizeRelativeTime(1000 * 60 * 60 * 24 * 2)).to.eq('in 2 days');
42+
43+
// assume every month has 30 days
44+
expect(humanizeRelativeTime(1000 * 60 * 60 * 24 * 30)).to.eq('next month');
45+
expect(humanizeRelativeTime(1000 * 60 * 60 * 24 * 30 * 2)).to.eq('in 2 months');
46+
expect(humanizeRelativeTime(1000 * 60 * 60 * 24 * 30 * 12)).to.eq('in 12 months');
47+
48+
// assume every year has 365 days
49+
expect(humanizeRelativeTime(1000 * 60 * 60 * 24 * 365)).to.eq('next year');
50+
expect(humanizeRelativeTime(1000 * 60 * 60 * 24 * 365 * 2)).to.eq('in 2 years');
51+
});
52+
53+
it('should return a humanized duration negative', () => {
54+
expect(humanizeRelativeTime(-1)).to.eq('1 minute ago');
55+
expect(humanizeRelativeTime(-1000 * 60)).to.eq('1 minute ago');
56+
expect(humanizeRelativeTime(-1000 * 60 - 1)).to.eq('2 minutes ago');
57+
expect(humanizeRelativeTime(-1000 * 60 * 2)).to.eq('2 minutes ago');
58+
expect(humanizeRelativeTime(-1000 * 60 * 2 - 1)).to.eq('3 minutes ago');
59+
expect(humanizeRelativeTime(-1000 * 60 * 60)).to.eq('1 hour ago');
60+
expect(humanizeRelativeTime(-1000 * 60 * 60 * 2)).to.eq('2 hours ago');
61+
expect(humanizeRelativeTime(-1000 * 60 * 60 * 24)).to.eq('yesterday');
62+
expect(humanizeRelativeTime(-1000 * 60 * 60 * 24 * 2)).to.eq('2 days ago');
63+
64+
// assume every month has 30 days
65+
expect(humanizeRelativeTime(-1000 * 60 * 60 * 24 * 30)).to.eq('last month');
66+
expect(humanizeRelativeTime(-1000 * 60 * 60 * 24 * 30 * 2)).to.eq('2 months ago');
67+
expect(humanizeRelativeTime(-1000 * 60 * 60 * 24 * 30 * 12)).to.eq('12 months ago');
68+
69+
// assume every year has 365 days
70+
expect(humanizeRelativeTime(-1000 * 60 * 60 * 24 * 365)).to.eq('last year');
71+
expect(humanizeRelativeTime(-1000 * 60 * 60 * 24 * 365 * 2)).to.eq('2 years ago');
72+
});
73+
});

0 commit comments

Comments
 (0)