@@ -6,6 +6,11 @@ const Moment = require('moment-timezone');
66const moment = extendMoment ( Moment ) ;
77
88type QueryDateRange = [ string , string ] ;
9+ type SqlInterval = string ;
10+ type TimeSeriesOptions = {
11+ timestampPrecision : number
12+ } ;
13+ type ParsedInterval = Partial < Record < unitOfTime . DurationConstructor , number > > ;
914
1015export const TIME_SERIES : Record < string , ( range : DateRange , timestampPrecision : number ) => QueryDateRange [ ] > = {
1116 day : ( range : DateRange , digits ) => Array . from ( range . snapTo ( 'day' ) . by ( 'day' ) )
@@ -26,8 +31,103 @@ export const TIME_SERIES: Record<string, (range: DateRange, timestampPrecision:
2631 . map ( d => [ d . format ( `YYYY-MM-DDT00:00:00.${ '0' . repeat ( digits ) } ` ) , d . endOf ( 'quarter' ) . format ( `YYYY-MM-DDT23:59:59.${ '9' . repeat ( digits ) } ` ) ] ) ,
2732} ;
2833
29- type TimeSeriesOptions = {
30- timestampPrecision : number
34+ /**
35+ * Parse PostgreSQL-like interval string into object
36+ * E.g. '2 years 15 months 100 weeks 99 hours 15 seconds'
37+ * Negative units are also supported
38+ * E.g. '-2 months 5 days -10 hours'
39+ */
40+ function parseSqlInterval ( intervalStr : SqlInterval ) : ParsedInterval {
41+ const regex = / ( - ? \d + ) \s * ( y e a r | q u a r t e r | m o n t h | w e e k | d a y | h o u r | m i n u t e | s e c o n d ) s ? / g;
42+ const interval : ParsedInterval = { } ;
43+
44+ for ( const match of intervalStr . matchAll ( regex ) ) {
45+ const value = parseInt ( match [ 1 ] , 10 ) ;
46+ const unit = match [ 2 ] as unitOfTime . DurationConstructor ;
47+ interval [ unit ] = value ;
48+ }
49+
50+ return interval ;
51+ }
52+
53+ function addInterval ( date : moment . Moment , interval : ParsedInterval ) : moment . Moment {
54+ const res = date . clone ( ) ;
55+
56+ Object . entries ( interval ) . forEach ( ( [ key , value ] ) => {
57+ res . add ( value , key as unitOfTime . DurationConstructor ) ;
58+ } ) ;
59+
60+ return res ;
61+ }
62+
63+ function subtractInterval ( date : moment . Moment , interval : ParsedInterval ) : moment . Moment {
64+ const res = date . clone ( ) ;
65+
66+ Object . entries ( interval ) . forEach ( ( [ key , value ] ) => {
67+ res . subtract ( value , key as unitOfTime . DurationConstructor ) ;
68+ } ) ;
69+
70+ return res ;
71+ }
72+
73+ /**
74+ * Returns the closest date prior to date parameter aligned with the offset and interval
75+ * If no offset provided, the beginning of the year will be taken as pivot point
76+ */
77+ function alignToOffset ( date : moment . Moment , interval : ParsedInterval , offset ?: ParsedInterval ) : moment . Moment {
78+ let alignedDate = date . clone ( ) ;
79+ let intervalOp ;
80+
81+ const startOfYear = moment ( ) . year ( date . year ( ) ) . startOf ( 'year' ) ;
82+ let offsetDate = offset ? addInterval ( startOfYear , offset ) : startOfYear ;
83+
84+ if ( date . isBefore ( offsetDate ) ) {
85+ intervalOp = offsetDate . isBefore ( startOfYear ) ? addInterval : subtractInterval ;
86+
87+ while ( date . isBefore ( offsetDate ) ) {
88+ offsetDate = intervalOp ( offsetDate , interval ) ;
89+ }
90+ alignedDate = offsetDate ;
91+ } else if ( offsetDate . isBefore ( startOfYear ) ) {
92+ intervalOp = offsetDate . isBefore ( startOfYear ) ? addInterval : subtractInterval ;
93+
94+ while ( date . isAfter ( offsetDate ) ) {
95+ alignedDate = offsetDate . clone ( ) ;
96+ offsetDate = intervalOp ( offsetDate , interval ) ;
97+ }
98+ } else {
99+ intervalOp = offsetDate . isBefore ( startOfYear ) ? subtractInterval : addInterval ;
100+
101+ while ( date . isAfter ( offsetDate ) ) {
102+ alignedDate = offsetDate . clone ( ) ;
103+ offsetDate = intervalOp ( offsetDate , interval ) ;
104+ }
105+ }
106+
107+ return alignedDate ;
108+ }
109+
110+ export const timeSeriesFromCustomInterval = ( intervalStr : string , [ startStr , endStr ] : QueryDateRange , offsetStr : string | undefined , options : TimeSeriesOptions = { timestampPrecision : 3 } ) : QueryDateRange [ ] => {
111+ const intervalParsed = parseSqlInterval ( intervalStr ) ;
112+ const offsetParsed = offsetStr ? parseSqlInterval ( offsetStr ) : undefined ;
113+ const start = moment ( startStr ) ;
114+ const end = moment ( endStr ) ;
115+ let alignedStart = alignToOffset ( start , intervalParsed , offsetParsed ) ;
116+
117+ const dates : QueryDateRange [ ] = [ ] ;
118+
119+ while ( alignedStart . isBefore ( end ) ) {
120+ const s = alignedStart . clone ( ) ;
121+ alignedStart = addInterval ( alignedStart , intervalParsed ) ;
122+ dates . push ( [
123+ s . format ( `YYYY-MM-DDTHH:mm:ss.${ '0' . repeat ( options . timestampPrecision ) } ` ) ,
124+ alignedStart . clone ( )
125+ . subtract ( 1 , 'second' )
126+ . format ( `YYYY-MM-DDTHH:mm:ss.${ '9' . repeat ( options . timestampPrecision ) } ` )
127+ ] ) ;
128+ }
129+
130+ return dates ;
31131} ;
32132
33133export const timeSeries = ( granularity : string , dateRange : QueryDateRange , options : TimeSeriesOptions = { timestampPrecision : 3 } ) : QueryDateRange [ ] => {
0 commit comments