1
1
import { isIE } from '../core/utils' ;
2
+ import { DatePart , DatePartInfo } from '../directives/date-time-editor/date-time-editor.common' ;
2
3
3
4
/**
4
5
* This enum is used to keep the date validation result.
@@ -27,6 +28,9 @@ const enum DateChars {
27
28
DayChar = 'd'
28
29
}
29
30
31
+ const DATE_CHARS = [ 'h' , 'H' , 'm' , 's' , 'S' , 't' , 'T' ] ;
32
+ const TIME_CHARS = [ 'd' , 'D' , 'M' , 'y' , 'Y' ] ;
33
+
30
34
/**
31
35
* @hidden
32
36
*/
@@ -46,6 +50,242 @@ export abstract class DatePickerUtil {
46
50
private static readonly PROMPT_CHAR = '_' ;
47
51
private static readonly DEFAULT_LOCALE = 'en' ;
48
52
53
+ /**
54
+ * Parse a Date value from masked string input based on determined date parts
55
+ * @param inputData masked value to parse
56
+ * @param dateTimeParts Date parts array for the mask
57
+ */
58
+ public static parseValueFromMask ( inputData : string , dateTimeParts : DatePartInfo [ ] , promptChar ?: string ) : Date | null {
59
+ const parts : { [ key in DatePart ] : number } = { } as any ;
60
+ dateTimeParts . forEach ( dp => {
61
+ let value = parseInt ( this . getCleanVal ( inputData , dp , promptChar ) , 10 ) ;
62
+ if ( ! value ) {
63
+ value = dp . type === DatePart . Date || dp . type === DatePart . Month ? 1 : 0 ;
64
+ }
65
+ parts [ dp . type ] = value ;
66
+ } ) ;
67
+
68
+ if ( parts [ DatePart . Month ] < 1 || 12 < parts [ DatePart . Month ] ) {
69
+ return null ;
70
+ }
71
+
72
+ // TODO: Century threshold
73
+ if ( parts [ DatePart . Year ] < 50 ) {
74
+ parts [ DatePart . Year ] += 2000 ;
75
+ }
76
+
77
+ if ( parts [ DatePart . Date ] > DatePickerUtil . daysInMonth ( parts [ DatePart . Year ] , parts [ DatePart . Month ] ) ) {
78
+ return null ;
79
+ }
80
+
81
+ if ( parts [ DatePart . Hours ] > 23 || parts [ DatePart . Minutes ] > 59 || parts [ DatePart . Seconds ] > 59 ) {
82
+ return null ;
83
+ }
84
+
85
+ return new Date (
86
+ parts [ DatePart . Year ] || 2000 ,
87
+ parts [ DatePart . Month ] - 1 || 0 ,
88
+ parts [ DatePart . Date ] || 1 ,
89
+ parts [ DatePart . Hours ] || 0 ,
90
+ parts [ DatePart . Minutes ] || 0 ,
91
+ parts [ DatePart . Seconds ] || 0
92
+ ) ;
93
+ }
94
+
95
+ private static ensureLeadingZero ( part : DatePartInfo ) {
96
+ switch ( part . type ) {
97
+ case DatePart . Date :
98
+ case DatePart . Month :
99
+ case DatePart . Hours :
100
+ case DatePart . Minutes :
101
+ case DatePart . Seconds :
102
+ if ( part . format . length === 1 ) {
103
+ part . format = part . format . repeat ( 2 ) ;
104
+ }
105
+ break ;
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Parse the mask into date/time and literal parts
111
+ */
112
+ public static parseDateTimeFormat ( mask : string , locale : string = DatePickerUtil . DEFAULT_LOCALE ) : DatePartInfo [ ] {
113
+ const format = mask || DatePickerUtil . getDefaultInputFormat ( locale ) ;
114
+ const dateTimeParts : DatePartInfo [ ] = [ ] ;
115
+ const formatArray = Array . from ( format ) ;
116
+ let currentPart : DatePartInfo = null ;
117
+ let position = 0 ;
118
+
119
+ for ( let i = 0 ; i < formatArray . length ; i ++ , position ++ ) {
120
+ const type = DatePickerUtil . determineDatePart ( formatArray [ i ] ) ;
121
+ if ( currentPart ) {
122
+ if ( currentPart . type === type ) {
123
+ currentPart . format += formatArray [ i ] ;
124
+ if ( i < formatArray . length - 1 ) {
125
+ continue ;
126
+ }
127
+ }
128
+
129
+ DatePickerUtil . ensureLeadingZero ( currentPart ) ;
130
+ currentPart . end = currentPart . start + currentPart . format . length ;
131
+ position = currentPart . end ;
132
+ dateTimeParts . push ( currentPart ) ;
133
+ }
134
+
135
+ currentPart = {
136
+ start : position ,
137
+ end : position + formatArray [ i ] . length ,
138
+ type : type ,
139
+ format : formatArray [ i ]
140
+ } ;
141
+ }
142
+
143
+ return dateTimeParts ;
144
+ }
145
+
146
+ public static getDefaultInputFormat ( locale : string ) : string {
147
+ if ( ! Intl || ! Intl . DateTimeFormat || ! Intl . DateTimeFormat . prototype . formatToParts ) {
148
+ // TODO: fallback with Intl.format for IE?
149
+ return DatePickerUtil . SHORT_DATE_MASK ;
150
+ }
151
+ const parts = DatePickerUtil . getDefaultLocaleMask ( locale ) ;
152
+ parts . forEach ( p => {
153
+ if ( p . type !== DatePart . Year && p . type !== DatePickerUtil . SEPARATOR ) {
154
+ p . formatType = FormatDesc . TwoDigits ;
155
+ }
156
+ } ) ;
157
+
158
+ return DatePickerUtil . getMask ( parts ) ;
159
+ }
160
+
161
+ public static isDateOrTimeChar ( char : string ) : boolean {
162
+ return DATE_CHARS . indexOf ( char ) !== - 1 || TIME_CHARS . indexOf ( char ) !== - 1 ;
163
+ }
164
+
165
+ public static spinDate ( delta : number , newDate : Date , isSpinLoop : boolean ) : void {
166
+ const maxDate = DatePickerUtil . daysInMonth ( newDate . getFullYear ( ) , newDate . getMonth ( ) ) ;
167
+ let date = newDate . getDate ( ) + delta ;
168
+ if ( date > maxDate ) {
169
+ date = isSpinLoop ? date % maxDate : maxDate ;
170
+ } else if ( date < 1 ) {
171
+ date = isSpinLoop ? maxDate + ( date % maxDate ) : 1 ;
172
+ }
173
+
174
+ newDate . setDate ( date ) ;
175
+ }
176
+
177
+ public static spinMonth ( delta : number , newDate : Date , isSpinLoop : boolean ) : void {
178
+ const maxDate = DatePickerUtil . daysInMonth ( newDate . getFullYear ( ) , newDate . getMonth ( ) + delta ) ;
179
+ if ( newDate . getDate ( ) > maxDate ) {
180
+ newDate . setDate ( maxDate ) ;
181
+ }
182
+
183
+ const maxMonth = 11 ;
184
+ const minMonth = 0 ;
185
+ let month = newDate . getMonth ( ) + delta ;
186
+ if ( month > maxMonth ) {
187
+ month = isSpinLoop ? ( month % maxMonth ) - 1 : maxMonth ;
188
+ } else if ( month < minMonth ) {
189
+ month = isSpinLoop ? maxMonth + ( month % maxMonth ) + 1 : minMonth ;
190
+ }
191
+
192
+ newDate . setMonth ( month ) ;
193
+ }
194
+
195
+ public static spinYear ( delta : number , newDate : Date ) : void {
196
+ const maxDate = DatePickerUtil . daysInMonth ( newDate . getFullYear ( ) + delta , newDate . getMonth ( ) ) ;
197
+ if ( newDate . getDate ( ) > maxDate ) {
198
+ // clip to max to avoid leap year change shifting the entire value
199
+ newDate . setDate ( maxDate ) ;
200
+ }
201
+ newDate . setFullYear ( newDate . getFullYear ( ) + delta ) ;
202
+ }
203
+
204
+ public static spinHours ( delta : number , newDate : Date , isSpinLoop : boolean ) : void {
205
+ const maxHour = 23 ;
206
+ const minHour = 0 ;
207
+ let hours = newDate . getHours ( ) + delta ;
208
+ if ( hours > maxHour ) {
209
+ hours = isSpinLoop ? hours % maxHour - 1 : maxHour ;
210
+ } else if ( hours < minHour ) {
211
+ hours = isSpinLoop ? maxHour + ( hours % maxHour ) + 1 : minHour ;
212
+ }
213
+
214
+ newDate . setHours ( hours ) ;
215
+ }
216
+
217
+ public static spinMinutes ( delta : number , newDate : Date , isSpinLoop : boolean ) : void {
218
+ const maxMinutes = 59 ;
219
+ const minMinutes = 0 ;
220
+ let minutes = newDate . getMinutes ( ) + delta ;
221
+ if ( minutes > maxMinutes ) {
222
+ minutes = isSpinLoop ? minutes % maxMinutes - 1 : maxMinutes ;
223
+ } else if ( minutes < minMinutes ) {
224
+ minutes = isSpinLoop ? maxMinutes + ( minutes % maxMinutes ) + 1 : minMinutes ;
225
+ }
226
+
227
+ newDate . setMinutes ( minutes ) ;
228
+ }
229
+
230
+ public static spinSeconds ( delta : number , newDate : Date , isSpinLoop : boolean ) : void {
231
+ const maxSeconds = 59 ;
232
+ const minSeconds = 0 ;
233
+ let seconds = newDate . getSeconds ( ) + delta ;
234
+ if ( seconds > maxSeconds ) {
235
+ seconds = isSpinLoop ? seconds % maxSeconds - 1 : maxSeconds ;
236
+ } else if ( seconds < minSeconds ) {
237
+ seconds = isSpinLoop ? maxSeconds + ( seconds % maxSeconds ) + 1 : minSeconds ;
238
+ }
239
+
240
+ newDate . setSeconds ( seconds ) ;
241
+ }
242
+
243
+ public static spinAmPm ( newDate : Date , currentDate : Date , amPmFromMask : string ) : Date {
244
+ switch ( amPmFromMask ) {
245
+ case 'AM' :
246
+ newDate = new Date ( newDate . setHours ( newDate . getHours ( ) + 12 ) ) ;
247
+ break ;
248
+ case 'PM' :
249
+ newDate = new Date ( newDate . setHours ( newDate . getHours ( ) - 12 ) ) ;
250
+ break ;
251
+ }
252
+ if ( newDate . getDate ( ) !== currentDate . getDate ( ) ) {
253
+ return currentDate ;
254
+ }
255
+
256
+ return newDate ;
257
+ }
258
+
259
+ private static getCleanVal ( inputData : string , datePart : DatePartInfo , promptChar ?: string ) : string {
260
+ return DatePickerUtil . trimEmptyPlaceholders ( inputData . substring ( datePart . start , datePart . end ) , promptChar ) ;
261
+ }
262
+
263
+ private static determineDatePart ( char : string ) : DatePart {
264
+ switch ( char ) {
265
+ case 'd' :
266
+ case 'D' :
267
+ return DatePart . Date ;
268
+ case 'M' :
269
+ return DatePart . Month ;
270
+ case 'y' :
271
+ case 'Y' :
272
+ return DatePart . Year ;
273
+ case 'h' :
274
+ case 'H' :
275
+ return DatePart . Hours ;
276
+ case 'm' :
277
+ return DatePart . Minutes ;
278
+ case 's' :
279
+ case 'S' :
280
+ return DatePart . Seconds ;
281
+ case 't' :
282
+ case 'T' :
283
+ return DatePart . AmPm ;
284
+ default :
285
+ return DatePart . Literal ;
286
+ }
287
+ }
288
+
49
289
/**
50
290
* This method generates date parts structure based on editor mask and locale.
51
291
* @param maskValue: string
@@ -204,7 +444,7 @@ export abstract class DatePickerUtil {
204
444
return { state : DateState . Invalid , value : inputValue } ;
205
445
}
206
446
207
- if ( ( day < 1 ) || ( day > DatePickerUtil . daysInMonth ( fullYear , month + 1 ) ) || ( day === NaN ) ) {
447
+ if ( ( day < 1 ) || ( day > DatePickerUtil . daysInMonth ( fullYear , month ) ) || ( day === NaN ) ) {
208
448
return { state : DateState . Invalid , value : inputValue } ;
209
449
}
210
450
@@ -220,8 +460,8 @@ export abstract class DatePickerUtil {
220
460
* This method replaces prompt chars with empty string.
221
461
* @param value
222
462
*/
223
- public static trimUnderlines ( value : string ) : string {
224
- const result = value . replace ( / _ / g , '' ) ;
463
+ public static trimEmptyPlaceholders ( value : string , promptChar ? : string ) : string {
464
+ const result = value . replace ( new RegExp ( promptChar || '_' , 'g' ) , '' ) ;
225
465
return result ;
226
466
}
227
467
@@ -339,6 +579,10 @@ export abstract class DatePickerUtil {
339
579
return '' ;
340
580
}
341
581
582
+ public static daysInMonth ( fullYear : number , month : number ) : number {
583
+ return new Date ( fullYear , month + 1 , 0 ) . getDate ( ) ;
584
+ }
585
+
342
586
private static getYearFormatType ( format : string ) : string {
343
587
switch ( format . match ( new RegExp ( DateChars . YearChar , 'g' ) ) . length ) {
344
588
case 1 : {
@@ -394,7 +638,7 @@ export abstract class DatePickerUtil {
394
638
} ) ;
395
639
} else {
396
640
dateStruct . push ( {
397
- type : formatToParts [ i ] . type ,
641
+ type : formatToParts [ i ] . type
398
642
} ) ;
399
643
}
400
644
}
@@ -410,7 +654,7 @@ export abstract class DatePickerUtil {
410
654
break ;
411
655
}
412
656
case DateParts . Year : {
413
- dateStruct [ i ] . formatType = formatterOptions . month ;
657
+ dateStruct [ i ] . formatType = formatterOptions . year ;
414
658
break ;
415
659
}
416
660
}
@@ -464,14 +708,10 @@ export abstract class DatePickerUtil {
464
708
return { min : minValue , max : maxValue } ;
465
709
}
466
710
467
- private static daysInMonth ( fullYear : number , month : number ) : number {
468
- return new Date ( fullYear , month , 0 ) . getDate ( ) ;
469
- }
470
-
471
711
private static getDateValueFromInput ( dateFormatParts : any [ ] , type : DateParts , inputValue : string , trim : boolean = true ) : string {
472
712
const partPosition = DatePickerUtil . getDateFormatPart ( dateFormatParts , type ) . position ;
473
713
const result = inputValue . substring ( partPosition [ 0 ] , partPosition [ 1 ] ) ;
474
- return ( trim ) ? DatePickerUtil . trimUnderlines ( result ) : result ;
714
+ return ( trim ) ? DatePickerUtil . trimEmptyPlaceholders ( result ) : result ;
475
715
}
476
716
477
717
private static getDayValueFromInput ( dateFormatParts : any [ ] , inputValue : string , trim : boolean = true ) : string {
0 commit comments