@ -1,3 +1,5 @@
import { Logger } from './logger' ;
// NOTE@eamodio If this changes we need to update the replacement function too (since its parameter number/order relies on the matching)
const customDateTimeFormatParserRegex =
/(?<literal>\[.*?\])|(?<year>YYYY|YY)|(?<month>M{1,4})|(?<day>Do|DD?)|(?<weekday>d{2,4})|(?<hour>HH?|hh?)|(?<minute>mm?)|(?<second>ss?)|(?<fractionalSecond>SSS)|(?<dayPeriod>A|a)|(?<timeZoneName>ZZ?)/g ;
@ -81,50 +83,68 @@ export function createFromDateDelta(
export function fromNow ( date : Date | number , short? : boolean ) : string {
const elapsed = ( typeof date === 'number' ? date : date.getTime ( ) ) - new Date ( ) . getTime ( ) ;
for ( const [ unit , threshold , divisor , shortUnit ] of relativeUnitThresholds ) {
const elapsedABS = Math . abs ( elapsed ) ;
if ( elapsedABS >= threshold || threshold === 1000 /* second */ ) {
if ( short ) {
if ( locale == null ) {
if ( defaultShortRelativeTimeFormat != null ) {
locale = defaultShortRelativeTimeFormat . resolvedOptions ( ) . locale ;
} else if ( defaultRelativeTimeFormat != null ) {
locale = defaultRelativeTimeFormat . resolvedOptions ( ) . locale ;
} else {
let marker = 0 ;
try {
for ( const [ unit , threshold , divisor , shortUnit ] of relativeUnitThresholds ) {
const elapsedABS = Math . abs ( elapsed ) ;
if ( elapsedABS >= threshold || threshold === 1000 /* second */ ) {
if ( short ) {
if ( locale == null ) {
if ( defaultShortRelativeTimeFormat != null ) {
marker = 1 ;
locale = defaultShortRelativeTimeFormat . resolvedOptions ( ) . locale ;
} else if ( defaultRelativeTimeFormat != null ) {
marker = 2 ;
locale = defaultRelativeTimeFormat . resolvedOptions ( ) . locale ;
} else {
marker = 3 ;
defaultShortRelativeTimeFormat = new Intl . RelativeTimeFormat ( defaultLocales , {
localeMatcher : 'best fit' ,
numeric : 'always' ,
style : 'narrow' ,
} ) ;
marker = 4 ;
locale = defaultShortRelativeTimeFormat . resolvedOptions ( ) . locale ;
}
}
if ( locale === 'en' || locale ? . startsWith ( 'en-' ) ) {
const value = Math . round ( elapsedABS / divisor ) ;
return ` ${ value } ${ shortUnit } ` ;
}
if ( defaultShortRelativeTimeFormat == null ) {
marker = 5 ;
defaultShortRelativeTimeFormat = new Intl . RelativeTimeFormat ( defaultLocales , {
localeMatcher : 'best fit' ,
numeric : 'always' ,
style : 'narrow' ,
} ) ;
locale = defaultShortRelativeTimeFormat . resolvedOptions ( ) . locale ;
}
}
if ( locale === 'en' || locale ? . startsWith ( 'en-' ) ) {
const value = Math . round ( elapsedABS / divisor ) ;
return ` ${ value } ${ shortUnit } ` ;
marker = 6 ;
return defaultShortRelativeTimeFormat . format ( Math . round ( elapsed / divisor ) , unit ) ;
}
if ( defaultShortRelativeTimeFormat == null ) {
defaultShortRelativeTimeFormat = new Intl . RelativeTimeFormat ( defaultLocales , {
if ( defaultRelativeTimeFormat == null ) {
marker = 7 ;
defaultRelativeTimeFormat = new Intl . RelativeTimeFormat ( defaultLocales , {
localeMatcher : 'best fit' ,
numeric : 'always' ,
style : 'narrow' ,
numeric : 'auto ' ,
style : 'long ' ,
} ) ;
}
return defaultShort RelativeTimeFormat . format ( Math . round ( elapsed / divisor ) , unit ) ;
marker = 8 ;
return defaultRelativeTimeFormat . format ( Math . round ( elapsed / divisor ) , unit ) ;
}
if ( defaultRelativeTimeFormat == null ) {
defaultRelativeTimeFormat = new Intl . RelativeTimeFormat ( defaultLocales , {
localeMatcher : 'best fit' ,
numeric : 'auto' ,
style : 'long' ,
} ) ;
}
return defaultRelativeTimeFormat . format ( Math . round ( elapsed / divisor ) , unit ) ;
}
} catch ( ex ) {
Logger . error (
ex ,
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
` fromNow( ${ date } , ${ short } ) marker= ${ marker } defaultLocales= ${ defaultLocales ? . join ( ',' ) } locale= ${ locale } ` ,
) ;
throw ex ;
}
return '' ;
@ -140,35 +160,12 @@ export function formatDate(
const key = ` ${ locale ? ? '' } : ${ format } ` ;
let formatter = dateTimeFormatCache . get ( key ) ;
if ( formatter == null ) {
const options = getDateTimeFormatOptionsFromFormatString ( format ) ;
let locales ;
if ( locale == null ) {
locales = defaultLocales ;
} else if ( locale === 'system' ) {
locales = undefined ;
} else {
locales = [ locale ] ;
}
formatter = new Intl . DateTimeFormat ( locales , options ) ;
if ( cache ) {
dateTimeFormatCache . set ( key , formatter ) ;
}
}
if ( format == null || dateTimeFormatRegex . test ( format ) ) {
return formatter . format ( date ) ;
}
function getTimeFormatter ( format : TimeStyle ) {
const key = ` ${ locale ? ? '' } :time: ${ format } ` ;
let marker = 0 ;
try {
let formatter = dateTimeFormatCache . get ( key ) ;
if ( formatter == null ) {
const options : Intl.DateTimeFormatOptions = { localeMatcher : 'best fit' , timeStyle : format } ;
marker = 1 ;
const options = getDateTimeFormatOptionsFromFormatString ( format ) ;
let locales ;
if ( locale == null ) {
@ -179,58 +176,99 @@ export function formatDate(
locales = [ locale ] ;
}
marker = 2 ;
formatter = new Intl . DateTimeFormat ( locales , options ) ;
if ( cache ) {
dateTimeFormatCache . set ( key , formatter ) ;
}
}
return formatter ;
}
if ( format == null || dateTimeFormatRegex . test ( format ) ) {
marker = 3 ;
return formatter . format ( date ) ;
}
const parts = formatter . formatToParts ( date ) ;
return format . replace (
customDateTimeFormatParserRegex ,
(
_match ,
literal ,
_year ,
_month ,
_day ,
_weekday ,
_hour ,
_minute ,
_second ,
_fractionalSecond ,
_dayPeriod ,
_timeZoneName ,
_offset ,
_s ,
groups ,
) = > {
if ( literal != null ) return ( literal as string ) . substring ( 1 , literal . length - 1 ) ;
for ( const key in groups ) {
const value = groups [ key ] ;
if ( value == null ) continue ;
const part = parts . find ( p = > p . type === key ) ;
if ( value === 'Do' && part ? . type === 'day' ) {
return formatWithOrdinal ( Number ( part . value ) ) ;
} else if ( value === 'a' && part ? . type === 'dayPeriod' ) {
// For some reason the Intl.DateTimeFormat doesn't honor the `dayPeriod` value and always returns the long version, so use the "short" timeStyle instead
const dayPeriod = getTimeFormatter ( 'short' )
. formatToParts ( date )
. find ( p = > p . type === 'dayPeriod' ) ;
return ` ${ ( dayPeriod ? ? part ) ? . value ? ? '' } ` ;
function getTimeFormatter ( format : TimeStyle ) {
const key = ` ${ locale ? ? '' } :time: ${ format } ` ;
let formatter = dateTimeFormatCache . get ( key ) ;
if ( formatter == null ) {
const options : Intl.DateTimeFormatOptions = { localeMatcher : 'best fit' , timeStyle : format } ;
let locales ;
if ( locale == null ) {
locales = defaultLocales ;
} else if ( locale === 'system' ) {
locales = undefined ;
} else {
locales = [ locale ] ;
}
marker = 6 ;
formatter = new Intl . DateTimeFormat ( locales , options ) ;
if ( cache ) {
dateTimeFormatCache . set ( key , formatter ) ;
}
return part ? . value ? ? '' ;
}
return '' ;
} ,
) ;
return formatter ;
}
marker = 4 ;
const parts = formatter . formatToParts ( date ) ;
marker = 5 ;
return format . replace (
customDateTimeFormatParserRegex ,
(
_match ,
literal ,
_year ,
_month ,
_day ,
_weekday ,
_hour ,
_minute ,
_second ,
_fractionalSecond ,
_dayPeriod ,
_timeZoneName ,
_offset ,
_s ,
groups ,
) = > {
if ( literal != null ) return ( literal as string ) . substring ( 1 , literal . length - 1 ) ;
for ( const key in groups ) {
const value = groups [ key ] ;
if ( value == null ) continue ;
const part = parts . find ( p = > p . type === key ) ;
if ( value === 'Do' && part ? . type === 'day' ) {
return formatWithOrdinal ( Number ( part . value ) ) ;
} else if ( value === 'a' && part ? . type === 'dayPeriod' ) {
// For some reason the Intl.DateTimeFormat doesn't honor the `dayPeriod` value and always returns the long version, so use the "short" timeStyle instead
const dayPeriod = getTimeFormatter ( 'short' )
. formatToParts ( date )
. find ( p = > p . type === 'dayPeriod' ) ;
return ` ${ ( dayPeriod ? ? part ) ? . value ? ? '' } ` ;
}
return part ? . value ? ? '' ;
}
return '' ;
} ,
) ;
} catch ( ex ) {
Logger . error (
ex ,
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
` formatDate( ${ date } , ${ format } , ${ locale } , ${ cache } ) marker= ${ marker } defaultLocales= ${ defaultLocales ? . join (
',' ,
) } locale = $ { locale } ` ,
) ;
throw ex ;
}
}
export function getDateDifference (
@ -364,22 +402,33 @@ export function formatNumeric(
const key = ` ${ locale ? ? '' } : ${ style } ` ;
let formatter = numberFormatCache . get ( key ) ;
if ( formatter == null ) {
const options : Intl.NumberFormatOptions = { localeMatcher : 'best fit' , style : style } ;
let marker = 0 ;
try {
let formatter = numberFormatCache . get ( key ) ;
if ( formatter == null ) {
const options : Intl.NumberFormatOptions = { localeMatcher : 'best fit' , style : style } ;
let locales ;
if ( locale == null ) {
locales = defaultLocales ;
} else if ( locale === 'system' ) {
locales = undefined ;
} else {
locales = [ locale ] ;
let locales ;
if ( locale == null ) {
locales = defaultLocales ;
} else if ( locale === 'system' ) {
locales = undefined ;
} else {
locales = [ locale ] ;
}
marker = 1 ;
formatter = new Intl . NumberFormat ( locales , options ) ;
numberFormatCache . set ( key , formatter ) ;
}
formatter = new Intl . NumberFormat ( locales , options ) ;
numberFormatCache . set ( key , formatter ) ;
marker = 2 ;
return formatter . format ( value ) ;
} catch ( ex ) {
Logger . error (
ex ,
` formatNumeric( ${ value } , ${ style } , ${ locale } ) marker= ${ marker } defaultLocales= ${ defaultLocales ? . join ( ',' ) } ` ,
) ;
throw ex ;
}
return formatter . format ( value ) ;
}