2020-03-13 15:44:24 +05:30
import dateformat from 'dateformat' ;
import { pick , omit , isEqual , isEmpty } from 'lodash' ;
2020-06-23 00:09:42 +05:30
import { DATETIME _RANGE _TYPES } from './constants' ;
2020-03-13 15:44:24 +05:30
import { secondsToMilliseconds } from './datetime_utility' ;
const MINIMUM _DATE = new Date ( 0 ) ;
const DEFAULT _DIRECTION = 'before' ;
2021-03-08 18:12:59 +05:30
const durationToMillis = ( duration ) => {
2020-03-13 15:44:24 +05:30
if ( Object . entries ( duration ) . length === 1 && Number . isFinite ( duration . seconds ) ) {
return secondsToMilliseconds ( duration . seconds ) ;
}
2020-04-22 19:07:51 +05:30
// eslint-disable-next-line @gitlab/require-i18n-strings
2020-03-13 15:44:24 +05:30
throw new Error ( 'Invalid duration: only `seconds` is supported' ) ;
} ;
const dateMinusDuration = ( date , duration ) => new Date ( date . getTime ( ) - durationToMillis ( duration ) ) ;
const datePlusDuration = ( date , duration ) => new Date ( date . getTime ( ) + durationToMillis ( duration ) ) ;
2021-03-08 18:12:59 +05:30
const isValidDuration = ( duration ) => Boolean ( duration && Number . isFinite ( duration . seconds ) ) ;
2020-03-13 15:44:24 +05:30
2021-03-08 18:12:59 +05:30
const isValidDateString = ( dateString ) => {
2020-03-13 15:44:24 +05:30
if ( typeof dateString !== 'string' || ! dateString . trim ( ) ) {
return false ;
}
2021-11-18 22:05:49 +05:30
let isoFormatted ;
try {
isoFormatted = dateformat ( dateString , 'isoUtcDateTime' ) ;
} catch ( e ) {
if ( e instanceof TypeError ) {
// not a valid date string
return false ;
}
throw e ;
}
return ! Number . isNaN ( Date . parse ( isoFormatted ) ) ;
2020-03-13 15:44:24 +05:30
} ;
const handleRangeDirection = ( { direction = DEFAULT _DIRECTION , anchorDate , minDate , maxDate } ) => {
let startDate ;
let endDate ;
if ( direction === DEFAULT _DIRECTION ) {
startDate = minDate ;
endDate = anchorDate ;
} else {
startDate = anchorDate ;
endDate = maxDate ;
}
return {
startDate ,
endDate ,
} ;
} ;
/ * *
* Converts a fixed range to a fixed range
* @ param { Object } fixedRange - A range with fixed start and
* end ( e . g . "midnight January 1st 2020 to midday January31st 2020" )
* /
const convertFixedToFixed = ( { start , end } ) => ( {
start ,
end ,
} ) ;
/ * *
* Converts an anchored range to a fixed range
* @ param { Object } anchoredRange - A duration of time
* relative to a fixed point in time ( e . g . , " the 30 minutes
* before midnight January 1 st 2020 ", or " the 2 days
* after midday on the 11 th of May 2019 " )
* /
const convertAnchoredToFixed = ( { anchor , duration , direction } ) => {
const anchorDate = new Date ( anchor ) ;
const { startDate , endDate } = handleRangeDirection ( {
minDate : dateMinusDuration ( anchorDate , duration ) ,
maxDate : datePlusDuration ( anchorDate , duration ) ,
direction ,
anchorDate ,
} ) ;
return {
start : startDate . toISOString ( ) ,
end : endDate . toISOString ( ) ,
} ;
} ;
/ * *
* Converts a rolling change to a fixed range
*
* @ param { Object } rollingRange - A time range relative to
* now ( e . g . , "last 2 minutes" , or "next 2 days" )
* /
const convertRollingToFixed = ( { duration , direction } ) => {
// Use Date.now internally for easier mocking in tests
const now = new Date ( Date . now ( ) ) ;
return convertAnchoredToFixed ( {
duration ,
direction ,
anchor : now . toISOString ( ) ,
} ) ;
} ;
/ * *
* Converts an open range to a fixed range
*
* @ param { Object } openRange - A time range relative
* to an anchor ( e . g . , " before midnight on the 1 st of
* January 2020 ", or " after midday on the 11 th of May 2019 " )
* /
const convertOpenToFixed = ( { anchor , direction } ) => {
// Use Date.now internally for easier mocking in tests
const now = new Date ( Date . now ( ) ) ;
const { startDate , endDate } = handleRangeDirection ( {
minDate : MINIMUM _DATE ,
maxDate : now ,
direction ,
anchorDate : new Date ( anchor ) ,
} ) ;
return {
start : startDate . toISOString ( ) ,
end : endDate . toISOString ( ) ,
} ;
} ;
/ * *
* Handles invalid date ranges
* /
const handleInvalidRange = ( ) => {
2020-04-22 19:07:51 +05:30
// eslint-disable-next-line @gitlab/require-i18n-strings
2020-03-13 15:44:24 +05:30
throw new Error ( 'The input range does not have the right format.' ) ;
} ;
const handlers = {
invalid : handleInvalidRange ,
fixed : convertFixedToFixed ,
anchored : convertAnchoredToFixed ,
rolling : convertRollingToFixed ,
open : convertOpenToFixed ,
} ;
/ * *
* Validates and returns the type of range
*
* @ param { Object } Date time range
* @ returns { String } ` key ` value for one of the handlers
* /
export function getRangeType ( range ) {
const { start , end , anchor , duration } = range ;
if ( ( start || end ) && ! anchor && ! duration ) {
2020-06-23 00:09:42 +05:30
return isValidDateString ( start ) && isValidDateString ( end )
? DATETIME _RANGE _TYPES . fixed
: DATETIME _RANGE _TYPES . invalid ;
2020-03-13 15:44:24 +05:30
}
if ( anchor && duration ) {
2020-06-23 00:09:42 +05:30
return isValidDateString ( anchor ) && isValidDuration ( duration )
? DATETIME _RANGE _TYPES . anchored
: DATETIME _RANGE _TYPES . invalid ;
2020-03-13 15:44:24 +05:30
}
if ( duration && ! anchor ) {
2020-06-23 00:09:42 +05:30
return isValidDuration ( duration ) ? DATETIME _RANGE _TYPES . rolling : DATETIME _RANGE _TYPES . invalid ;
2020-03-13 15:44:24 +05:30
}
if ( anchor && ! duration ) {
2020-06-23 00:09:42 +05:30
return isValidDateString ( anchor ) ? DATETIME _RANGE _TYPES . open : DATETIME _RANGE _TYPES . invalid ;
2020-03-13 15:44:24 +05:30
}
2020-06-23 00:09:42 +05:30
return DATETIME _RANGE _TYPES . invalid ;
2020-03-13 15:44:24 +05:30
}
/ * *
* convertToFixedRange Transforms a ` range of time ` into a ` fixed range of time ` .
*
* The following types of a ` ranges of time ` can be represented :
*
* Fixed Range : A range with fixed start and end ( e . g . "midnight January 1st 2020 to midday January 31st 2020" )
* Anchored Range : A duration of time relative to a fixed point in time ( e . g . , "the 30 minutes before midnight January 1st 2020" , or "the 2 days after midday on the 11th of May 2019" )
* Rolling Range : A time range relative to now ( e . g . , "last 2 minutes" , or "next 2 days" )
* Open Range : A time range relative to an anchor ( e . g . , "before midnight on the 1st of January 2020" , or "after midday on the 11th of May 2019" )
*
* @ 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 relative to an anchor
*
* type FixedRange = {
* start : ISO8601 ;
* end : ISO8601 ;
* label : string ;
* }
*
* type AnchoredRange = {
* anchor : ISO8601 ;
* duration : Duration ;
* direction : Direction ; // defaults to 'before'
* label : string ;
* }
*
* type RollingRange = {
* duration : Duration ;
* direction : Direction ; // defaults to 'before'
* label : string ;
* }
*
* type OpenRange = {
* anchor : ISO8601 ;
* direction : Direction ; // defaults to 'before'
* label : string ;
* }
*
* type DateTimeRange = FixedRange | AnchoredRange | RollingRange | OpenRange ;
*
*
* @ returns { FixedRange } An object with a start and end in ISO8601 format .
* /
2021-03-08 18:12:59 +05:30
export const convertToFixedRange = ( dateTimeRange ) =>
2020-03-13 15:44:24 +05:30
handlers [ getRangeType ( dateTimeRange ) ] ( dateTimeRange ) ;
/ * *
* Returns a copy of the object only with time range
* properties relevant to time range calculation .
*
* Filtered properties are :
* - 'start'
* - 'end'
* - 'anchor'
* - 'duration'
* - 'direction' : if direction is already the default , its removed .
*
* @ param { Object } timeRange - A time range object
* @ returns Copy of time range
* /
2021-03-08 18:12:59 +05:30
const pruneTimeRange = ( timeRange ) => {
2020-03-13 15:44:24 +05:30
const res = pick ( timeRange , [ 'start' , 'end' , 'anchor' , 'duration' , 'direction' ] ) ;
if ( res . direction === DEFAULT _DIRECTION ) {
return omit ( res , 'direction' ) ;
}
return res ;
} ;
/ * *
* Returns true if the time ranges are equal according to
* the time range calculation properties
*
* @ param { Object } timeRange - A time range object
* @ param { Object } other - Time range object to compare with .
* @ returns true if the time ranges are equal , false otherwise
* /
export const isEqualTimeRanges = ( timeRange , other ) => {
const tr1 = pruneTimeRange ( timeRange ) ;
const tr2 = pruneTimeRange ( other ) ;
return isEqual ( tr1 , tr2 ) ;
} ;
/ * *
* Searches for a time range in a array of time ranges using
* only the properies relevant to time ranges calculation .
*
* @ param { Object } timeRange - Time range to search ( needle )
* @ param { Array } timeRanges - Array of time tanges ( haystack )
* /
export const findTimeRange = ( timeRange , timeRanges ) =>
2021-03-08 18:12:59 +05:30
timeRanges . find ( ( element ) => isEqualTimeRanges ( element , timeRange ) ) ;
2020-03-13 15:44:24 +05:30
// Time Ranges as URL Parameters Utils
/ * *
* List of possible time ranges parameters
* /
export const timeRangeParamNames = [ 'start' , 'end' , 'anchor' , 'duration_seconds' , 'direction' ] ;
/ * *
* Converts a valid time range to a flat key - value pairs object .
*
* Duration is flatted to avoid having nested objects .
*
* @ param { Object } A time range
* @ returns key - value pairs object that can be used as parameters in a URL .
* /
2021-03-08 18:12:59 +05:30
export const timeRangeToParams = ( timeRange ) => {
2020-03-13 15:44:24 +05:30
let params = pruneTimeRange ( timeRange ) ;
if ( timeRange . duration ) {
const durationParms = { } ;
2021-03-08 18:12:59 +05:30
Object . keys ( timeRange . duration ) . forEach ( ( key ) => {
2020-03-13 15:44:24 +05:30
durationParms [ ` duration_ ${ key } ` ] = timeRange . duration [ key ] . toString ( ) ;
} ) ;
params = { ... durationParms , ... params } ;
params = omit ( params , 'duration' ) ;
}
return params ;
} ;
/ * *
* Converts a valid set of flat params to a time range object
*
* Parameters that are not part of time range object are ignored .
*
* @ param { params } params - key - value pairs object .
* /
2021-03-08 18:12:59 +05:30
export const timeRangeFromParams = ( params ) => {
2020-03-13 15:44:24 +05:30
const timeRangeParams = pick ( params , timeRangeParamNames ) ;
let range = Object . entries ( timeRangeParams ) . reduce ( ( acc , [ key , val ] ) => {
// unflatten duration
if ( key . startsWith ( 'duration_' ) ) {
acc . duration = acc . duration || { } ;
acc . duration [ key . slice ( 'duration_' . length ) ] = parseInt ( val , 10 ) ;
return acc ;
}
return { [ key ] : val , ... acc } ;
} , { } ) ;
range = pruneTimeRange ( range ) ;
return ! isEmpty ( range ) ? range : null ;
} ;