Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions extensions/ql-vscode/src/pure/time.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
* Contains an assortment of helper functions for working with time, dates, and durations.
*/


const durationFormatter = new Intl.RelativeTimeFormat('en', {
numeric: 'auto',
});

// Months and years are approximate
const MINUTE_IN_MILLIS = 1000 * 60;
const HOUR_IN_MILLIS = 60 * MINUTE_IN_MILLIS;
const DAY_IN_MILLIS = 24 * HOUR_IN_MILLIS;
const MONTH_IN_MILLIS = 30 * DAY_IN_MILLIS;
const YEAR_IN_MILLIS = 365 * DAY_IN_MILLIS;

/**
* Converts a number of milliseconds into a human-readable string with units, indicating a relative time in the past or future.
*
* @param relativeTimeMillis The duration in milliseconds. A negative number indicates a duration in the past. And a positive number is
* the future.
* @returns A humanized duration. For example, "in 2 minutes", "2 minutes ago", "yesterday", or "tomorrow".
*/
export function humanizeRelativeTime(relativeTimeMillis?: number) {
if (relativeTimeMillis === undefined) {
return '';
}

if (Math.abs(relativeTimeMillis) < HOUR_IN_MILLIS) {
return durationFormatter.format(Math.floor(relativeTimeMillis / MINUTE_IN_MILLIS), 'minute');
} else if (Math.abs(relativeTimeMillis) < DAY_IN_MILLIS) {
return durationFormatter.format(Math.floor(relativeTimeMillis / HOUR_IN_MILLIS), 'hour');
} else if (Math.abs(relativeTimeMillis) < MONTH_IN_MILLIS) {
return durationFormatter.format(Math.floor(relativeTimeMillis / DAY_IN_MILLIS), 'day');
} else if (Math.abs(relativeTimeMillis) < YEAR_IN_MILLIS) {
return durationFormatter.format(Math.floor(relativeTimeMillis / MONTH_IN_MILLIS), 'month');
} else {
return durationFormatter.format(Math.floor(relativeTimeMillis / YEAR_IN_MILLIS), 'year');
}
}

/**
* Converts a number of milliseconds into a human-readable string with units, indicating an amount of time.
* Negative numbers have no meaning and are considered to be "Less than a minute".
*
* @param millis The number of milliseconds to convert.
* @returns A humanized duration. For example, "2 minutes", "2 hours", "2 days", or "2 months".
*/
export function humanizeUnit(millis?: number): string {
// assume a blank or empty string is a zero
// assume anything less than 0 is a zero
if (!millis || millis < MINUTE_IN_MILLIS) {
return 'Less than a minute';
}
let unit: string;
let unitDiff: number;
if (millis < HOUR_IN_MILLIS) {
unit = 'minute';
unitDiff = Math.floor(millis / MINUTE_IN_MILLIS);
} else if (millis < DAY_IN_MILLIS) {
unit = 'hour';
unitDiff = Math.floor(millis / HOUR_IN_MILLIS);
} else if (millis < MONTH_IN_MILLIS) {
unit = 'day';
unitDiff = Math.floor(millis / DAY_IN_MILLIS);
} else if (millis < YEAR_IN_MILLIS) {
unit = 'month';
unitDiff = Math.floor(millis / MONTH_IN_MILLIS);
} else {
unit = 'year';
unitDiff = Math.floor(millis / YEAR_IN_MILLIS);
}

return createFormatter(unit).format(unitDiff);
}

function createFormatter(unit: string) {
return Intl.NumberFormat('en-US', {
style: 'unit',
unit,
unitDisplay: 'long'
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -395,7 +395,8 @@ export async function getRepositoriesMetadata(credentials: Credentials, nwos: st
const owner = node.owner.login;
const name = node.name;
const starCount = node.stargazerCount;
const lastUpdated = Date.now() - new Date(node.updatedAt).getTime();
// lastUpdated is always negative since it happened in the past.
const lastUpdated = new Date(node.updatedAt).getTime() - Date.now();
metadata[`${owner}/${name}`] = {
starCount, lastUpdated
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { URLSearchParams } from 'url';
import { SHOW_QUERY_TEXT_MSG } from '../query-history';
import { AnalysesResultsManager } from './analyses-results-manager';
import { AnalysisResults } from './shared/analysis-result';
import { humanizeUnit } from '../pure/time';

export class RemoteQueriesInterfaceManager {
private panel: WebviewPanel | undefined;
Expand Down Expand Up @@ -249,23 +250,7 @@ export class RemoteQueriesInterfaceManager {

private getDuration(startTime: number, endTime: number): string {
const diffInMs = startTime - endTime;
return this.formatDuration(diffInMs);
}

private formatDuration(ms: number): string {
const seconds = ms / 1000;
const minutes = seconds / 60;
const hours = minutes / 60;
const days = hours / 24;
if (days > 1) {
return `${days.toFixed(2)} days`;
} else if (hours > 1) {
return `${hours.toFixed(2)} hours`;
} else if (minutes > 1) {
return `${minutes.toFixed(2)} minutes`;
} else {
return `${seconds.toFixed(2)} seconds`;
}
return humanizeUnit(diffInMs);
}

private formatDate = (millis: number): string => {
Expand Down
34 changes: 5 additions & 29 deletions extensions/ql-vscode/src/remote-queries/view/LastUpdated.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import * as React from 'react';
import { RepoPushIcon } from '@primer/octicons-react';
import styled from 'styled-components';

import { humanizeRelativeTime } from '../../pure/time';

const IconContainer = styled.span`
flex-grow: 0;
text-align: right;
Expand All @@ -17,13 +19,15 @@ const Duration = styled.span`
type Props = { lastUpdated?: number };

const LastUpdated = ({ lastUpdated }: Props) => (
// lastUpdated will be undefined for older results that were
// created before the lastUpdated field was added.
Number.isFinite(lastUpdated) ? (
<>
<IconContainer>
<RepoPushIcon size={16} />
</IconContainer>
<Duration>
{humanizeDuration(lastUpdated)}
{humanizeRelativeTime(lastUpdated)}
</Duration>
</>
) : (
Expand All @@ -32,31 +36,3 @@ const LastUpdated = ({ lastUpdated }: Props) => (
);

export default LastUpdated;

const formatter = new Intl.RelativeTimeFormat('en', {
numeric: 'auto'
});

// All these are approximate, specifically months and years
const MINUTE_IN_MILLIS = 1000 * 60;
const HOUR_IN_MILLIS = 60 * MINUTE_IN_MILLIS;
const DAY_IN_MILLIS = 24 * HOUR_IN_MILLIS;
const MONTH_IN_MILLIS = 30 * DAY_IN_MILLIS;
const YEAR_IN_MILLIS = 365 * DAY_IN_MILLIS;

function humanizeDuration(diff?: number) {
if (!diff) {
return '';
}
if (diff < HOUR_IN_MILLIS) {
return formatter.format(- Math.floor(diff / MINUTE_IN_MILLIS), 'minute');
} else if (diff < DAY_IN_MILLIS) {
return formatter.format(- Math.floor(diff / HOUR_IN_MILLIS), 'hour');
} else if (diff < MONTH_IN_MILLIS) {
return formatter.format(- Math.floor(diff / DAY_IN_MILLIS), 'day');
} else if (diff < YEAR_IN_MILLIS) {
return formatter.format(- Math.floor(diff / MONTH_IN_MILLIS), 'month');
} else {
return formatter.format(- Math.floor(diff / YEAR_IN_MILLIS), 'year');
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -71,14 +71,18 @@ const openQueryTextVirtualFile = (queryResult: RemoteQueryResult) => {
});
};

function createResultsDescription(queryResult: RemoteQueryResult) {
const reposCount = `${queryResult.totalRepositoryCount} ${queryResult.totalRepositoryCount === 1 ? 'repository' : 'repositories'}`;
return `${queryResult.totalResultCount} results from running against ${reposCount} (${queryResult.executionDuration}), ${queryResult.executionTimestamp}`;
}

const sumAnalysesResults = (analysesResults: AnalysisResults[]) =>
analysesResults.reduce((acc, curr) => acc + getAnalysisResultCount(curr), 0);

const QueryInfo = (queryResult: RemoteQueryResult) => (
<>
<VerticalSpace size={1} />
{queryResult.totalResultCount} results from running against {queryResult.totalRepositoryCount} repositories
({queryResult.executionDuration}), {queryResult.executionTimestamp}
{createResultsDescription(queryResult)}
<VerticalSpace size={1} />
<span>
<a className="vscode-codeql__query-info-link" href="#" onClick={() => openQueryFile(queryResult)}>
Expand Down
73 changes: 73 additions & 0 deletions extensions/ql-vscode/test/pure-tests/time.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { expect } from 'chai';
import 'mocha';

import { humanizeRelativeTime, humanizeUnit } from '../../src/pure/time';

describe('Time', () => {
it('should return a humanized unit', () => {
expect(humanizeUnit(undefined)).to.eq('Less than a minute');
expect(humanizeUnit(0)).to.eq('Less than a minute');
expect(humanizeUnit(-1)).to.eq('Less than a minute');
expect(humanizeUnit(1000 * 60 - 1)).to.eq('Less than a minute');
expect(humanizeUnit(1000 * 60)).to.eq('1 minute');
expect(humanizeUnit(1000 * 60 * 2 - 1)).to.eq('1 minute');
expect(humanizeUnit(1000 * 60 * 2)).to.eq('2 minutes');
expect(humanizeUnit(1000 * 60 * 60)).to.eq('1 hour');
expect(humanizeUnit(1000 * 60 * 60 * 2)).to.eq('2 hours');
expect(humanizeUnit(1000 * 60 * 60 * 24)).to.eq('1 day');
expect(humanizeUnit(1000 * 60 * 60 * 24 * 2)).to.eq('2 days');

// assume every month has 30 days
expect(humanizeUnit(1000 * 60 * 60 * 24 * 30)).to.eq('1 month');
expect(humanizeUnit(1000 * 60 * 60 * 24 * 30 * 2)).to.eq('2 months');
expect(humanizeUnit(1000 * 60 * 60 * 24 * 30 * 12)).to.eq('12 months');

// assume every year has 365 days
expect(humanizeUnit(1000 * 60 * 60 * 24 * 365)).to.eq('1 year');
expect(humanizeUnit(1000 * 60 * 60 * 24 * 365 * 2)).to.eq('2 years');
});

it('should return a humanized duration positive', () => {
expect(humanizeRelativeTime(undefined)).to.eq('');
expect(humanizeRelativeTime(0)).to.eq('this minute');
expect(humanizeRelativeTime(1)).to.eq('this minute');
expect(humanizeRelativeTime(1000 * 60 - 1)).to.eq('this minute');
expect(humanizeRelativeTime(1000 * 60)).to.eq('in 1 minute');
expect(humanizeRelativeTime(1000 * 60 * 2 - 1)).to.eq('in 1 minute');
expect(humanizeRelativeTime(1000 * 60 * 2)).to.eq('in 2 minutes');
expect(humanizeRelativeTime(1000 * 60 * 60)).to.eq('in 1 hour');
expect(humanizeRelativeTime(1000 * 60 * 60 * 2)).to.eq('in 2 hours');
expect(humanizeRelativeTime(1000 * 60 * 60 * 24)).to.eq('tomorrow');
expect(humanizeRelativeTime(1000 * 60 * 60 * 24 * 2)).to.eq('in 2 days');

// assume every month has 30 days
expect(humanizeRelativeTime(1000 * 60 * 60 * 24 * 30)).to.eq('next month');
expect(humanizeRelativeTime(1000 * 60 * 60 * 24 * 30 * 2)).to.eq('in 2 months');
expect(humanizeRelativeTime(1000 * 60 * 60 * 24 * 30 * 12)).to.eq('in 12 months');

// assume every year has 365 days
expect(humanizeRelativeTime(1000 * 60 * 60 * 24 * 365)).to.eq('next year');
expect(humanizeRelativeTime(1000 * 60 * 60 * 24 * 365 * 2)).to.eq('in 2 years');
});

it('should return a humanized duration negative', () => {
expect(humanizeRelativeTime(-1)).to.eq('1 minute ago');
expect(humanizeRelativeTime(-1000 * 60)).to.eq('1 minute ago');
expect(humanizeRelativeTime(-1000 * 60 - 1)).to.eq('2 minutes ago');
expect(humanizeRelativeTime(-1000 * 60 * 2)).to.eq('2 minutes ago');
expect(humanizeRelativeTime(-1000 * 60 * 2 - 1)).to.eq('3 minutes ago');
expect(humanizeRelativeTime(-1000 * 60 * 60)).to.eq('1 hour ago');
expect(humanizeRelativeTime(-1000 * 60 * 60 * 2)).to.eq('2 hours ago');
expect(humanizeRelativeTime(-1000 * 60 * 60 * 24)).to.eq('yesterday');
expect(humanizeRelativeTime(-1000 * 60 * 60 * 24 * 2)).to.eq('2 days ago');

// assume every month has 30 days
expect(humanizeRelativeTime(-1000 * 60 * 60 * 24 * 30)).to.eq('last month');
expect(humanizeRelativeTime(-1000 * 60 * 60 * 24 * 30 * 2)).to.eq('2 months ago');
expect(humanizeRelativeTime(-1000 * 60 * 60 * 24 * 30 * 12)).to.eq('12 months ago');

// assume every year has 365 days
expect(humanizeRelativeTime(-1000 * 60 * 60 * 24 * 365)).to.eq('last year');
expect(humanizeRelativeTime(-1000 * 60 * 60 * 24 * 365 * 2)).to.eq('2 years ago');
});
});