import { DateMarker , timeAsMs } from './marker' ; import { CalendarSystem } from './calendar-system' ; import { Locale } from './locale' ; import { DateFormatter , DateFormattingContext } from './DateFormatter' ; import { ZonedMarker } from './zoned-marker' ; import { formatTimeZoneOffset } from './formatting-utils' ; import { memoize } from '../util/memoize' ; const EXTENDED_SETTINGS_AND_SEVERITIES = { week : 3 , separator : 0 , omitZeroMinute : 0 , meridiem : 0 , omitCommas : 0 , } ; const STANDARD_DATE_PROP_SEVERITIES = { timeZoneName : 7 , era : 6 , year : 5 , month : 4 , day : 2 , weekday : 2 , hour : 1 , minute : 1 , second : 1 , } ; const MERIDIEM_RE = /\s*([ap])\.?m\.?/i ; const COMMA_RE = /,/g ; const MULTI_SPACE_RE = /\s+/g ; const LTR_RE = /\u200e/g ; const UTC_RE = /UTC|GMT/ ; export interface NativeFormatterOptions extends Intl . DateTimeFormatOptions { week ? : 'long' | 'short' | 'narrow' | 'numeric' ; meridiem ? : 'lowercase' | 'short' | 'narrow' | boolean ; omitZeroMinute ? : boolean ; omitCommas ? : boolean ; separator ? : string ; } export class NativeFormatter implements DateFormatter { standardDateProps : any ; extendedSettings : any ; severity : number ; private buildFormattingFunc : typeof buildFormattingFunc ; constructor ( formatSettings ) { let standardDateProps = { } ; let extendedSettings = { } ; let severity = 0 ; for ( let name in formatSettings ) { if ( name in EXTENDED_SETTINGS_AND_SEVERITIES ) { extendedSettings [ name ] = formatSettings [ name ] ; severity = Math . max ( EXTENDED_SETTINGS_AND_SEVERITIES [ name ] , severity ) ; } else { standardDateProps [ name ] = formatSettings [ name ] ; if ( name in STANDARD_DATE_PROP_SEVERITIES ) { severity = Math . max ( STANDARD_DATE_PROP_SEVERITIES [ name ] , severity ) ; } } } this . standardDateProps = standardDateProps ; this . extendedSettings = extendedSettings ; this . severity = severity ; this . buildFormattingFunc = memoize ( buildFormattingFunc ) ; } format ( date , context ) { return this . buildFormattingFunc ( this . standardDateProps , this . extendedSettings , context ) ( date ) ; } formatRange ( start , end , context , betterDefaultSeparator ? ) { let { standardDateProps , extendedSettings } = this ; let diffSeverity = computeMarkerDiffSeverity ( start . marker , end . marker , context . calendarSystem ) ; if ( ! diffSeverity ) { return this . format ( start , context ) ; } let biggestUnitForPartial = diffSeverity ; if ( biggestUnitForPartial > 1 && ( standardDateProps . year === 'numeric' || standardDateProps . year === '2-digit' ) && ( standardDateProps . month === 'numeric' || standardDateProps . month === '2-digit' ) && ( standardDateProps . day === 'numeric' || standardDateProps . day === '2-digit' ) ) { biggestUnitForPartial = 1 ; } let full0 = this . format ( start , context ) ; let full1 = this . format ( end , context ) ; if ( full0 === full1 ) { return full0 ; } let partialDateProps = computePartialFormattingOptions ( standardDateProps , biggestUnitForPartial ) ; let partialFormattingFunc = buildFormattingFunc ( partialDateProps , extendedSettings , context ) ; let partial0 = partialFormattingFunc ( start ) ; let partial1 = partialFormattingFunc ( end ) ; let insertion = findCommonInsertion ( full0 , partial0 , full1 , partial1 ) ; let separator = extendedSettings . separator || betterDefaultSeparator || context . defaultSeparator || '' ; if ( insertion ) { return insertion . before + partial0 + separator + partial1 + insertion . after ; } return full0 + separator + full1 ; } getLargestUnit ( ) { switch ( this . severity ) { case 7 : case 6 : case 5 : return 'year' ; case 4 : return 'month' ; case 3 : return 'week' ; case 2 : return 'day' ; default : return 'time' ; } } } function buildFormattingFunc ( standardDateProps , extendedSettings , context ) { let standardDatePropCnt = Object . keys ( standardDateProps ) . length ; if ( standardDatePropCnt === 1 && standardDateProps . timeZoneName === 'short' ) { return ( date ) => ( formatTimeZoneOffset ( date . timeZoneOffset ) ) ; } if ( standardDatePropCnt === 0 && extendedSettings . week ) { return ( date ) => ( formatWeekNumber ( context . computeWeekNumber ( date . marker ) , context . weekText , context . weekTextLong , context . locale , extendedSettings . week ) ) ; } return buildNativeFormattingFunc ( standardDateProps , extendedSettings , context ) ; } function buildNativeFormattingFunc ( standardDateProps , extendedSettings , context ) { standardDateProps = { ... standardDateProps } ; extendedSettings = { ... extendedSettings } ; sanitizeSettings ( standardDateProps , extendedSettings ) ; standardDateProps . timeZone = 'UTC' ; let normalFormat = new Intl . DateTimeFormat ( context . locale . codes , standardDateProps ) ; let zeroFormat ; if ( extendedSettings . omitZeroMinute ) { let zeroProps = { ... standardDateProps } ; delete zeroProps . minute ; zeroFormat = new Intl . DateTimeFormat ( context . locale . codes , zeroProps ) ; } return ( date ) => { let { marker } = date ; let format ; if ( zeroFormat && ! marker . getUTCMinutes ( ) ) { format = zeroFormat ; } else { format = normalFormat ; } let s = format . format ( marker ) ; return postProcess ( s , date , standardDateProps , extendedSettings , context ) ; } ; } function sanitizeSettings ( standardDateProps , extendedSettings ) { if ( standardDateProps . timeZoneName ) { if ( ! standardDateProps . hour ) { standardDateProps . hour = '2-digit' ; } if ( ! standardDateProps . minute ) { standardDateProps . minute = '2-digit' ; } } if ( standardDateProps . timeZoneName === 'long' ) { standardDateProps . timeZoneName = 'short' ; } if ( extendedSettings . omitZeroMinute && ( standardDateProps . second || standardDateProps . millisecond ) ) { delete extendedSettings . omitZeroMinute ; } } function postProcess ( s , date , standardDateProps , extendedSettings , context ) { s = s . replace ( LTR_RE , '' ) ; if ( standardDateProps . timeZoneName === 'short' ) { s = injectTzoStr ( s , ( context . timeZone === 'UTC' || date . timeZoneOffset == null ) ? 'UTC' : formatTimeZoneOffset ( date . timeZoneOffset ) ) ; } if ( extendedSettings . omitCommas ) { s = s . replace ( COMMA_RE , '' ) . trim ( ) ; } if ( extendedSettings . omitZeroMinute ) { s = s . replace ( ':00' , '' ) ; } if ( extendedSettings . meridiem === false ) { s = s . replace ( MERIDIEM_RE , '' ) . trim ( ) ; } else if ( extendedSettings . meridiem === 'narrow' ) { s = s . replace ( MERIDIEM_RE , ( m0 , m1 ) => m1 . toLocaleLowerCase ( ) ) ; } else if ( extendedSettings . meridiem === 'short' ) { s = s . replace ( MERIDIEM_RE , ( m0 , m1 ) => `${ m1 . toLocaleLowerCase ( ) }m` ) ; } else if ( extendedSettings . meridiem === 'lowercase' ) { s = s . replace ( MERIDIEM_RE , ( m0 ) => m0 . toLocaleLowerCase ( ) ) ; } s = s . replace ( MULTI_SPACE_RE , ' ' ) ; s = s . trim ( ) ; return s ; } function injectTzoStr ( s , tzoStr ) { let replaced = false ; s = s . replace ( UTC_RE , ( ) => { replaced = true ; return tzoStr ; } ) ; if ( ! replaced ) { s += ` ${ tzoStr }` ; } return s ; } function formatWeekNumber ( num , weekText , weekTextLong , locale , display ? ) { let parts = [ ] ; if ( display === 'long' ) { parts . push ( weekTextLong ) ; } else if ( display === 'short' || display === 'narrow' ) { parts . push ( weekText ) ; } if ( display === 'long' || display === 'short' ) { parts . push ( ' ' ) ; } parts . push ( locale . simpleNumberFormat . format ( num ) ) ; if ( locale . options . direction === 'rtl' ) { parts . reverse ( ) ; } return parts . join ( '' ) ; } function computeMarkerDiffSeverity ( d0 , d1 , ca ) { if ( ca . getMarkerYear ( d0 ) !== ca . getMarkerYear ( d1 ) ) { return 5 ; } if ( ca . getMarkerMonth ( d0 ) !== ca . getMarkerMonth ( d1 ) ) { return 4 ; } if ( ca . getMarkerDay ( d0 ) !== ca . getMarkerDay ( d1 ) ) { return 2 ; } if ( timeAsMs ( d0 ) !== timeAsMs ( d1 ) ) { return 1 ; } return 0 ; } function computePartialFormattingOptions ( options , biggestUnit ) { let partialOptions = { } ; for ( let name in options ) { if ( ! ( name in STANDARD_DATE_PROP_SEVERITIES ) || STANDARD_DATE_PROP_SEVERITIES [ name ] <= biggestUnit ) { partialOptions [ name ] = options [ name ] ; } } return partialOptions ; } function findCommonInsertion ( full0 , partial0 , full1 , partial1 ) { let i0 = 0 ; while ( i0 < full0 . length ) { let found0 = full0 . indexOf ( partial0 , i0 ) ; if ( found0 === - 1 ) { break ; } let before0 = full0 . substr ( 0 , found0 ) ; i0 = found0 + partial0 . length ; let after0 = full0 . substr ( i0 ) ; let i1 = 0 ; while ( i1 < full1 . length ) { let found1 = full1 . indexOf ( partial1 , i1 ) ; if ( found1 === - 1 ) { break ; } let before1 = full1 . substr ( 0 , found1 ) ; i1 = found1 + partial1 . length ; let after1 = full1 . substr ( i1 ) ; if ( before0 === before1 && after0 === after1 ) { return { before : before0 , after : after0 , } ; } } } return null ; }