Commit 12c055a2 authored by Thomas Randolph's avatar Thomas Randolph Committed by Miguel Rincon

Separate each type of range into a separate handler function

There are basically 2 big updates in this commit:

1) Split up each range type handler into a separate function
    (plus a few helper functions).
2) Export a type infer-er

The type infer-er is useful internally, but also helps me
feel more confident about code changes. In theory no 
type should ever collide with another type, so this 
function is kind of like the "canary in the coal mine" 
in that we should always be able to test a 1:1 range 
type from this function. If we can test the types 
thoroughly, we can be more confident each respective 
handler is working on the right set of data.

On that note, splitting up the logic for each range type 
makes me feel much more confident that each one is 
handled in a way that doesn't conflict with any other. 
More importantly, any potential changes to an existing 
range type or additions of new range types will: 
A) Not cause any changes to the existing ones and
B) Scale gracefully

On that final note of graceful scaling, I find the (very
common) method of basically extending a long 
if/else if/else block to be frightening from the 
perspective of ever needing to add things. I've found 
the method I use here (a dictionary lookup, in a bunch 
of cases) to be much easier to reason about: every 
addition adds a single dictionary entry, and the JS 
selects it (O(1)) when it's needed.
parent 31e56f5b
...@@ -14,6 +14,113 @@ const dateMinusDuration = (date, duration) => new Date(date.getTime() - duration ...@@ -14,6 +14,113 @@ const dateMinusDuration = (date, duration) => new Date(date.getTime() - duration
const datePlusDuration = (date, duration) => new Date(date.getTime() + durationToMillis(duration)); const datePlusDuration = (date, duration) => new Date(date.getTime() + durationToMillis(duration));
function handleRangeDirection({ direction = 'before', anchor, before, after }) {
let startDate;
let endDate;
if (direction === 'before') {
startDate = before;
endDate = anchor;
} else {
startDate = anchor;
endDate = after;
}
return {
startDate,
endDate,
};
}
function convertFixedToFixed(range) {
return {
startTime: new Date(range.startTime).toISOString(),
endTime: new Date(range.endTime).toISOString(),
};
}
function convertAnchoredToFixed(range) {
const anchor = new Date(range.anchor);
const { startDate, endDate } = handleRangeDirection({
before: dateMinusDuration(anchor, range.duration),
after: datePlusDuration(anchor, range.duration),
direction: range.direction,
anchor,
});
return {
startTime: startDate.toISOString(),
endTime: endDate.toISOString(),
};
}
function convertRollingToFixed(range) {
const now = new Date(Date.now());
return convertAnchoredToFixed({
duration: range.duration,
direction: range.direction,
anchor: now.toISOString(),
});
}
function convertOpenToFixed(range) {
const now = new Date(Date.now());
const anchor = new Date(range.anchor);
const { startDate, endDate } = handleRangeDirection({
before: MINIMUM_DATE,
after: now,
direction: range.direction,
anchor,
});
return {
startTime: startDate.toISOString(),
endTime: endDate.toISOString(),
};
}
function handleInvalidRange(range) {
const hasStart = range.startTime;
const hasEnd = range.endTime;
/* eslint-disable @gitlab/i18n/no-non-i18n-strings */
const messages = {
[true]: 'The input range does not have the right format.',
[!hasStart && hasEnd]: 'The input fixed range does not have a start time.',
[hasStart && !hasEnd]: 'The input fixed range does not have an end time.',
};
/* eslint-enable @gitlab/i18n/no-non-i18n-strings */
throw new Error(messages.true);
}
const handlers = {
invalid: handleInvalidRange,
fixed: convertFixedToFixed,
anchored: convertAnchoredToFixed,
rolling: convertRollingToFixed,
open: convertOpenToFixed,
};
export function getRangeType(range) {
const hasStart = range.startTime;
const hasEnd = range.endTime;
const hasAnchor = range.anchor;
const hasDuration = range.duration;
const types = {
fixed: hasStart && hasEnd,
anchored: hasAnchor && hasDuration,
rolling: hasDuration && !hasAnchor,
open: hasAnchor && !hasDuration,
};
return (Object.entries(types).find(([, truthy]) => truthy) || ['invalid'])[0];
}
/** /**
* convertToFixedRange Transforms a `range of time` into a `fixed range of time`. * convertToFixedRange Transforms a `range of time` into a `fixed range of time`.
* *
...@@ -82,41 +189,5 @@ const datePlusDuration = (date, duration) => new Date(date.getTime() + durationT ...@@ -82,41 +189,5 @@ const datePlusDuration = (date, duration) => new Date(date.getTime() + durationT
* @returns An object with a fixed startTime and endTime that * @returns An object with a fixed startTime and endTime that
* corresponds to the input time. * corresponds to the input time.
*/ */
export const convertToFixedRange = dateTimeRange => { export const convertToFixedRange = dateTimeRange =>
if (dateTimeRange.startTime && !dateTimeRange.endTime) { handlers[getRangeType(dateTimeRange)](dateTimeRange);
// 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 };
...@@ -2,8 +2,25 @@ import _ from 'lodash'; ...@@ -2,8 +2,25 @@ import _ from 'lodash';
import * as datetimeRange from '~/lib/utils/datetime_range'; import * as datetimeRange from '~/lib/utils/datetime_range';
const MOCK_NOW = Date.UTC(2020, 0, 23, 20); // 2020-01-23T20:00:00.000Z const MOCK_NOW = Date.UTC(2020, 0, 23, 20); // 2020-01-23T20:00:00.000Z
const rangeTypes = {
fixed: [{ startTime: 'exists', endTime: 'exists' }],
anchored: [{ anchor: 'exists', duration: 'exists' }],
rolling: [{ duration: 'exists' }],
open: [{ anchor: 'exists' }],
invalid: [{ startTime: 'exists' }, { endTime: 'exists' }, {}, { junk: 'exists' }],
};
describe('Date time range utils', () => { describe('Date time range utils', () => {
describe('getRangeType', () => {
const { getRangeType } = datetimeRange;
it('it correctly infers the range type from the input object', () => {
Object.entries(rangeTypes).forEach(([type, examples]) => {
examples.forEach(example => expect(getRangeType(example)).toEqual(type));
});
});
});
describe('convertToFixedRange', () => { describe('convertToFixedRange', () => {
const { convertToFixedRange } = datetimeRange; const { convertToFixedRange } = datetimeRange;
......
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