Commit 31e56f5b authored by Miguel Rincon's avatar Miguel Rincon

Adds a format for date ranges

The date ranges can be used to represent periods of time
relative to arbitrary points in time, such as a fixed date or the
current moment.
parent 658809ca
import { secondsToMilliseconds } from './datetime_utility';
const MINIMUM_DATE = new Date(0);
const durationToMillis = duration => {
if (Object.entries(duration).length === 1 && Number.isFinite(duration.seconds)) {
return secondsToMilliseconds(duration.seconds);
}
// eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings
throw new Error('The only duration allowed is Number of `seconds`.');
};
const dateMinusDuration = (date, duration) => new Date(date.getTime() - durationToMillis(duration));
const datePlusDuration = (date, duration) => new Date(date.getTime() + durationToMillis(duration));
/**
* convertToFixedRange Transforms a `range of time` into a `fixed range of time`.
*
* A a range of time can be understood as an arbitrary period
* of time that can represent points in time relative to the
* present moment. Some examples can be:
*
* -
* - From January 1st onwards
* -
* - Last 2 days
* - The next 2 days
* - Today so far
*
* The range of time can take different shapes according to
* the point of time and type of time range it represents.
*
* The following types of ranges can be represented:
*
* - Fixed Range: fixed start and ends (e.g. From January 1st 2020 to January 31st 2020)
* - Anchored Range: a fixed points in time (2 minutes before January 1st, 1 day after )
* - Rolling Range: a time range relative to now (Last 2 minutes, Next 2 days)
* - Open Range: a time range relative to now (Before 1st of January, After 1st of January)
*
* @param {Object} dateTimeRange - A Time Range representation
* It contains the data needed to create a fixed time range plus
* a label (recommended) to indicate the range that is covered.
*
* A definition via a TypeScript notation is presented below:
*
*
* type Duration = { // A duration of time, always in seconds
* seconds: number;
* }
*
* type Direction = 'before' | 'after'; // Direction of time
*
* type FixedRange = {
* start: ISO8601;
* end: ISO8601;
* label: string;
* }
*
* type AnchoredRange = {
* anchor: ISO8601;
* duration: Duration;
* direction: Direction;
* label: string;
* }
*
* type RollingRange = {
* duration: Duration;
* direction: Direction;
* label: string;
* }
*
* type OpenRange = {
* anchor: ISO8601;
* direction: Direction;
* label: string;
* }
*
* type DateTimeRange = FixedRange | AnchoredRange | RollingRange | OpenRange;
*
*
* @returns An object with a fixed startTime and endTime that
* corresponds to the input time.
*/
export const convertToFixedRange = dateTimeRange => {
if (dateTimeRange.startTime && !dateTimeRange.endTime) {
// eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings
throw new Error('The input fixed range does not have an end time.');
} else if (!dateTimeRange.startTime && dateTimeRange.endTime) {
// eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings
throw new Error('The input fixed range does not have an end time.');
} else if (dateTimeRange.startTime && dateTimeRange.endTime) {
return {
startTime: new Date(dateTimeRange.startTime).toISOString(),
endTime: new Date(dateTimeRange.endTime).toISOString(),
};
} else if (dateTimeRange.anchor || dateTimeRange.duration) {
const now = new Date(Date.now());
const { direction = 'before', duration } = dateTimeRange;
const anchorDate = dateTimeRange.anchor ? new Date(dateTimeRange.anchor) : now;
let startDate;
let endDate;
if (direction === 'before') {
startDate = duration ? dateMinusDuration(anchorDate, duration) : MINIMUM_DATE;
endDate = anchorDate;
} else {
startDate = anchorDate;
endDate = duration ? datePlusDuration(anchorDate, duration) : now;
}
return {
startTime: startDate.toISOString(),
endTime: endDate.toISOString(),
};
}
// eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings
throw new Error('The input range does not have the right format.');
};
export default { convertToFixedRange };
import _ from 'lodash';
import * as datetimeRange from '~/lib/utils/datetime_range';
const MOCK_NOW = Date.UTC(2020, 0, 23, 20); // 2020-01-23T20:00:00.000Z
describe('Date time range utils', () => {
describe('convertToFixedRange', () => {
const { convertToFixedRange } = datetimeRange;
beforeEach(() => {
Date.now = jest.spyOn(Date, 'now').mockImplementation(() => MOCK_NOW);
});
afterEach(() => {
Date.now.mockRestore();
});
describe('When a fixed range is input', () => {
const defaultFixedRange = {
startTime: '2020-01-01T00:00:00.000Z',
endTime: '2020-01-31T23:59:00.000Z',
label: 'January 2020',
};
const mockFixedRange = params => ({ ...defaultFixedRange, ...params });
it('it converts a fixed range to an equal fixed range', () => {
const aFixedRange = mockFixedRange();
expect(convertToFixedRange(aFixedRange)).toEqual({
startTime: defaultFixedRange.startTime,
endTime: defaultFixedRange.endTime,
});
});
it('it throws an error when fixed range does not contain an end time', () => {
const aFixedRangeMissingEnd = _.omit(mockFixedRange(), 'endTime');
expect(() => convertToFixedRange(aFixedRangeMissingEnd)).toThrow();
});
it('it throws an error when fixed range does not contain a start time', () => {
const aFixedRangeMissingStart = _.omit(mockFixedRange(), 'startTime');
expect(() => convertToFixedRange(aFixedRangeMissingStart)).toThrow();
});
it('it throws an error when the dates cannot be parsed', () => {
const wrongStart = mockFixedRange({ startTime: 'I_CANNOT_BE_PARSED' });
const wrongEnd = mockFixedRange({ endTime: 'I_CANNOT_BE_PARSED' });
expect(() => convertToFixedRange(wrongStart)).toThrow();
expect(() => convertToFixedRange(wrongEnd)).toThrow();
});
});
describe('When an anchored range is input', () => {
const defaultAnchoredRange = {
anchor: '2020-01-01T00:00:00.000Z',
direction: 'after',
duration: {
seconds: 60 * 2,
},
label: 'First two minutes of 2020',
};
const mockAnchoredRange = params => ({ ...defaultAnchoredRange, ...params });
it('it converts to a fixed range', () => {
const anAnchoredRange = mockAnchoredRange();
expect(convertToFixedRange(anAnchoredRange)).toEqual({
startTime: '2020-01-01T00:00:00.000Z',
endTime: '2020-01-01T00:02:00.000Z',
});
});
it('it converts to a fixed range with a `before` direction', () => {
const anAnchoredRange = mockAnchoredRange({ direction: 'before' });
expect(convertToFixedRange(anAnchoredRange)).toEqual({
startTime: '2019-12-31T23:58:00.000Z',
endTime: '2020-01-01T00:00:00.000Z',
});
});
it('it converts to a fixed range without an explicit direction, defaulting to `before`', () => {
const anAnchoredRange = _.omit(mockAnchoredRange(), 'direction');
expect(convertToFixedRange(anAnchoredRange)).toEqual({
startTime: '2019-12-31T23:58:00.000Z',
endTime: '2020-01-01T00:00:00.000Z',
});
});
it('it throws an error when the anchor cannot be parsed', () => {
const wrongAnchor = mockAnchoredRange({ anchor: 'I_CANNOT_BE_PARSED' });
expect(() => convertToFixedRange(wrongAnchor)).toThrow();
});
});
describe('when a rolling range is input', () => {
it('it converts to a fixed range', () => {
const aRollingRange = {
direction: 'after',
duration: {
seconds: 60 * 2,
},
label: 'Next 2 minutes',
};
expect(convertToFixedRange(aRollingRange)).toEqual({
startTime: '2020-01-23T20:00:00.000Z',
endTime: '2020-01-23T20:02:00.000Z',
});
});
it('it converts to a fixed range with an implicit `before` direction', () => {
const aRollingRangeWithNoDirection = {
duration: {
seconds: 60 * 2,
},
label: 'Last 2 minutes',
};
expect(convertToFixedRange(aRollingRangeWithNoDirection)).toEqual({
startTime: '2020-01-23T19:58:00.000Z',
endTime: '2020-01-23T20:00:00.000Z',
});
});
it('it throws an error when the duration is not the right format', () => {
const wrongDuration = {
direction: 'before',
duration: {
minutes: 2,
},
label: 'Last 2 minutes',
};
expect(() => convertToFixedRange(wrongDuration)).toThrow();
});
it('it throws an error when the duration is not ', () => {
const wrongAnchor = {
anchor: 'CAN_T_PARSE_THIS',
direction: 'after',
label: '2020 so far',
};
expect(() => convertToFixedRange(wrongAnchor)).toThrow();
});
});
describe('when an open range is input', () => {
it('it converts to a fixed range with an `after` direction', () => {
const soFar2020 = {
anchor: '2020-01-01T00:00:00.000Z',
direction: 'after',
label: '2020 so far',
};
expect(convertToFixedRange(soFar2020)).toEqual({
startTime: '2020-01-01T00:00:00.000Z',
endTime: '2020-01-23T20:00:00.000Z',
});
});
it('it converts to a fixed range with the explicit `before` direction', () => {
const before2020 = {
anchor: '2020-01-01T00:00:00.000Z',
direction: 'before',
label: 'Before 2020',
};
expect(convertToFixedRange(before2020)).toEqual({
startTime: '1970-01-01T00:00:00.000Z',
endTime: '2020-01-01T00:00:00.000Z',
});
});
it('it converts to a fixed range with the implicit `before` direction', () => {
const alsoBefore2020 = {
anchor: '2020-01-01T00:00:00.000Z',
label: 'Before 2020',
};
expect(convertToFixedRange(alsoBefore2020)).toEqual({
startTime: '1970-01-01T00:00:00.000Z',
endTime: '2020-01-01T00:00:00.000Z',
});
});
it('it throws an error when the anchor cannot be parsed', () => {
const wrongAnchor = {
anchor: 'CAN_T_PARSE_THIS',
direction: 'after',
label: '2020 so far',
};
expect(() => convertToFixedRange(wrongAnchor)).toThrow();
});
});
});
});
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment