11import { isIE } from '../core/utils' ;
2+ import { DatePart , DatePartInfo } from '../directives/date-time-editor/date-time-editor.common' ;
23
34/**
45 * This enum is used to keep the date validation result.
@@ -27,6 +28,9 @@ const enum DateChars {
2728 DayChar = 'd'
2829}
2930
31+ const DATE_CHARS = [ 'h' , 'H' , 'm' , 's' , 'S' , 't' , 'T' ] ;
32+ const TIME_CHARS = [ 'd' , 'D' , 'M' , 'y' , 'Y' ] ;
33+
3034/**
3135 * @hidden
3236 */
@@ -46,6 +50,242 @@ export abstract class DatePickerUtil {
4650 private static readonly PROMPT_CHAR = '_' ;
4751 private static readonly DEFAULT_LOCALE = 'en' ;
4852
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+
49289 /**
50290 * This method generates date parts structure based on editor mask and locale.
51291 * @param maskValue: string
@@ -204,7 +444,7 @@ export abstract class DatePickerUtil {
204444 return { state : DateState . Invalid , value : inputValue } ;
205445 }
206446
207- if ( ( day < 1 ) || ( day > DatePickerUtil . daysInMonth ( fullYear , month + 1 ) ) || ( day === NaN ) ) {
447+ if ( ( day < 1 ) || ( day > DatePickerUtil . daysInMonth ( fullYear , month ) ) || ( day === NaN ) ) {
208448 return { state : DateState . Invalid , value : inputValue } ;
209449 }
210450
@@ -220,8 +460,8 @@ export abstract class DatePickerUtil {
220460 * This method replaces prompt chars with empty string.
221461 * @param value
222462 */
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' ) , '' ) ;
225465 return result ;
226466 }
227467
@@ -339,6 +579,10 @@ export abstract class DatePickerUtil {
339579 return '' ;
340580 }
341581
582+ public static daysInMonth ( fullYear : number , month : number ) : number {
583+ return new Date ( fullYear , month + 1 , 0 ) . getDate ( ) ;
584+ }
585+
342586 private static getYearFormatType ( format : string ) : string {
343587 switch ( format . match ( new RegExp ( DateChars . YearChar , 'g' ) ) . length ) {
344588 case 1 : {
@@ -394,7 +638,7 @@ export abstract class DatePickerUtil {
394638 } ) ;
395639 } else {
396640 dateStruct . push ( {
397- type : formatToParts [ i ] . type ,
641+ type : formatToParts [ i ] . type
398642 } ) ;
399643 }
400644 }
@@ -410,7 +654,7 @@ export abstract class DatePickerUtil {
410654 break ;
411655 }
412656 case DateParts . Year : {
413- dateStruct [ i ] . formatType = formatterOptions . month ;
657+ dateStruct [ i ] . formatType = formatterOptions . year ;
414658 break ;
415659 }
416660 }
@@ -464,14 +708,10 @@ export abstract class DatePickerUtil {
464708 return { min : minValue , max : maxValue } ;
465709 }
466710
467- private static daysInMonth ( fullYear : number , month : number ) : number {
468- return new Date ( fullYear , month , 0 ) . getDate ( ) ;
469- }
470-
471711 private static getDateValueFromInput ( dateFormatParts : any [ ] , type : DateParts , inputValue : string , trim : boolean = true ) : string {
472712 const partPosition = DatePickerUtil . getDateFormatPart ( dateFormatParts , type ) . position ;
473713 const result = inputValue . substring ( partPosition [ 0 ] , partPosition [ 1 ] ) ;
474- return ( trim ) ? DatePickerUtil . trimUnderlines ( result ) : result ;
714+ return ( trim ) ? DatePickerUtil . trimEmptyPlaceholders ( result ) : result ;
475715 }
476716
477717 private static getDayValueFromInput ( dateFormatParts : any [ ] , inputValue : string , trim : boolean = true ) : string {
0 commit comments