Page Menu
Home
WickedGov Phorge
Search
Configure Global Search
Log In
Files
F1429888
DateFormatter.js
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
18 KB
Referenced Files
None
Subscribers
None
DateFormatter.js
View Options
'use strict'
;
const
config
=
require
(
'./config.json'
);
const
instanceCache
=
{
user
:
null
,
utc
:
null
,
site
:
null
};
let
supportsOffsetCache
=
null
;
// Private function collection.
// JSDOC doesn't like top-level function declarations.
// These can probably be private static methods when we have ES2022.
const
priv
=
{
/**
* Determine whether the browser supports offset strings like "+00:00", which
* were introduced in ECMA-402, 11th edition (2024).
*
* @return {boolean}
*/
supportsOffset
()
{
if
(
supportsOffsetCache
===
null
)
{
try
{
// eslint-disable-next-line no-new
new
Intl
.
DateTimeFormat
(
undefined
,
{
timeZone
:
'+01:00'
}
);
supportsOffsetCache
=
true
;
}
catch
(
e
)
{
supportsOffsetCache
=
false
;
}
}
return
supportsOffsetCache
;
},
/**
* Convert an offset in minutes to a string suitable for the timeZone option to
* the Intl.DateTimeFormat constructor. We use the MW sign convention for the
* input, where positive is positive, not the inverted JS convention.
*
* @param {number} offset
* @return {string|undefined}
*/
offsetToZone
(
offset
)
{
const
hour
=
Math
.
floor
(
Math
.
abs
(
offset
)
/
60
);
const
minute
=
Math
.
abs
(
offset
)
%
60
;
if
(
priv
.
supportsOffset
()
)
{
const
pad
=
(
x
)
=>
`
${
x
<
10
?
'0'
:
''
}${
x
}
`
;
return
`
${
offset
<
0
?
'-'
:
'+'
}${
pad
(
hour
)
}
:
${
pad
(
minute
)
}
`
;
}
if
(
minute
!==
0
)
{
// Can't represent this zone -- just use the system time zone
return
undefined
;
}
return
`Etc/GMT
${
offset
<
0
?
'+'
:
'-'
}${
hour
}
`
;
},
/**
* Check if a zone name is usable as an input to the Intl.DateTimeFormat constructor.
* If this returns false, we will use the offset instead.
*
* @param {string} zoneName
* @return {boolean}
*/
isKnownZoneName
(
zoneName
)
{
// Feature test for Safari 11.1
// eslint-disable-next-line compat/compat
return
Intl
.
supportedValuesOf
&&
Intl
.
supportedValuesOf
(
'timeZone'
).
includes
(
zoneName
);
},
/**
* Convert a MW time zone option value to an identifier suitable for
* Intl.DateTimeFormat. UserOptionsModule does some normalisation so this is
* not quite as complex as UserTimeCorrection::parse()
*
* @param {string|null} optionValue The user option value, if any
* @param {string} localZone The configured default zone
* @param {number} localOffset The configured default offset, in minutes
* @return {string|undefined}
*/
normalizeZone
(
optionValue
,
localZone
,
localOffset
)
{
if
(
optionValue
)
{
const
parts
=
optionValue
.
split
(
'|'
,
3
);
if
(
parts
[
0
]
===
'ZoneInfo'
)
{
if
(
priv
.
isKnownZoneName
(
parts
[
2
]
)
)
{
return
parts
[
2
];
}
else
{
return
priv
.
offsetToZone
(
parseInt
(
parts
[
1
],
10
)
);
}
}
else
if
(
parts
[
0
]
===
'Offset'
)
{
return
priv
.
offsetToZone
(
parts
[
1
]
);
}
}
return
priv
.
isKnownZoneName
(
localZone
)
?
localZone
:
priv
.
offsetToZone
(
localOffset
);
},
/**
* Get the normalized user time zone
*
* @return {string|undefined}
*/
getUserZone
()
{
return
priv
.
normalizeZone
(
mw
.
user
.
options
.
get
(
'timecorrection'
),
config
.
localZone
,
config
.
localOffset
);
},
/**
* Get the normalized site default zone
*
* @return {string|undefined}
*/
getSiteZone
()
{
return
priv
.
normalizeZone
(
null
,
config
.
localZone
,
config
.
localOffset
);
},
/**
* Get the date style desired by the user, e.g. "dmy".
*
* @return {string}
*/
getUserDateStyle
()
{
const
def
=
config
.
defaultStyle
;
const
style
=
mw
.
user
.
options
.
get
(
'date'
,
def
);
return
(
!
style
||
style
===
'default'
)
?
def
:
style
;
},
/**
* Convert an array of parts as returned by Intl.DateTimeFormat.formatParts
* to an object.
*
* @param {Array} parts
* @return {Object}
*/
partsToInfo
(
parts
)
{
const
info
=
{};
for
(
const
part
of
parts
)
{
info
[
part
.
type
]
=
part
.
value
;
}
return
info
;
},
/**
* Substitute parameters into the given pattern string
*
* @param {Object} info
* @param {string} pattern
* @return {string}
*/
formatPattern
(
info
,
pattern
)
{
return
pattern
.
replace
(
/\{(\w+)}/g
,
(
_
,
name
)
=>
name
in
info
?
info
[
name
]
:
'?'
);
},
/**
* Convert an English "long offset" part value to a string like "+01:00"
*
* @param {string} offset
* @return {string}
*/
longOffsetToIsoOffset
(
offset
)
{
const
m
=
/(GMT|UTC)?([+-][0-9]{2}:[0-9]{2})/
.
exec
(
offset
);
return
m
?
m
[
2
]
:
'+00:00'
;
}
};
/**
* @description
* Time and date formatter class.
*
* The main aim of this class is to make it easy to produce a date which is
* consistent with the way server-side MediaWiki formats dates, in the current
* user's timezone and preferred format.
*
* The input format is always a native Date object. This isn't a date parser
* class.
*
* The most common operations are available as static methods, which have no
* dependency on "this", so they can be called via destructuring assignment.
*
* The constructor is internal. Instances should be obtained via the static
* factory methods.
*
* @example
* // Static interface
* const { formatTimeAndDate } = require( 'mediawiki.DateFormatter' );
* const now = formatTimeAndDate( new Date() );
*
* // Non-static interface
* const DateFormatter = require( 'mediawiki.DateFormatter' );
* const mwTimestamp = DateFormatter.forUtc().formatMw( new Date() );
*
* @since 1.44
* @exports mediawiki.DateFormatter
*/
class
DateFormatter
{
/**
* Get a DateFormatter instance configured for the current user
*
* @return {module:mediawiki.DateFormatter}
*/
static
forUser
()
{
if
(
!
instanceCache
.
user
)
{
instanceCache
.
user
=
new
DateFormatter
(
config
.
locales
,
config
.
formats
,
priv
.
getUserZone
(),
priv
.
getUserDateStyle
()
);
}
return
instanceCache
.
user
;
}
/**
* Get a DateFormatter instance, configured for the current user except with
* the time zone set to UTC.
*
* @return {module:mediawiki.DateFormatter}
*/
static
forUtc
()
{
if
(
!
instanceCache
.
utc
)
{
instanceCache
.
utc
=
new
DateFormatter
(
config
.
locales
,
config
.
formats
,
'Etc/GMT'
,
priv
.
getUserDateStyle
()
);
}
return
instanceCache
.
utc
;
}
/**
* Get a DateFormatter instance, configured for the current user except with
* the site default time zone.
*
* @return {module:mediawiki.DateFormatter}
*/
static
forSiteZone
()
{
if
(
!
instanceCache
.
site
)
{
instanceCache
.
site
=
new
DateFormatter
(
config
.
locales
,
config
.
formats
,
priv
.
getSiteZone
(),
priv
.
getUserDateStyle
()
);
}
return
instanceCache
.
site
;
}
/**
* Format a Date to a time and date string (static variant)
*
* @param {Date} date
* @return {string}
*/
static
formatTimeAndDate
(
date
)
{
return
DateFormatter
.
forUser
().
formatTimeAndDate
(
date
);
}
/**
* Format a Date to a date string (static variant)
*
* @param {Date} date
* @return {string}
*/
static
formatDate
(
date
)
{
return
DateFormatter
.
forUser
().
formatDate
(
date
);
}
/**
* Format a Date to a time string (static variant)
*
* @param {Date} date
* @return {string}
*/
static
formatTime
(
date
)
{
return
DateFormatter
.
forUser
().
formatTime
(
date
);
}
/**
* Format a Date to a date string without the year (static variant)
*
* @param {Date} date
* @return {string}
*/
static
formatPrettyDate
(
date
)
{
return
DateFormatter
.
forUser
().
formatPrettyDate
(
date
);
}
// No static method for formatMw() -- use forUtc().formatMw()
/**
* Format a Date in ISO 8601 format, including timezone designator.
* (static variant)
*
* @param {Date} date
* @return {string}
*/
static
formatIso
(
date
)
{
return
DateFormatter
.
forUser
().
formatIso
(
date
);
}
/**
* Format a Date in ISO 8601 format, without the timezone designator,
* implying unqualified local time, and without seconds. This is suitable
* for passing to a datetime-local input. (static variant)
*
* @param {Date} date
* @return {string}
*/
static
formatForDateTimeInput
(
date
)
{
return
DateFormatter
.
forUser
().
formatForDateTimeInput
(
date
);
}
/**
* Format a date range, showing time and date. (static variant)
*
* TODO: Have MW provide localisation.
* This function depends on the browser for localisation. It does not
* properly respect the user's date format preferences, and has limited
* language support.
*
* @param {Date} date1
* @param {Date} date2
* @return {string}
*/
static
formatTimeAndDateRange
(
date1
,
date2
)
{
return
DateFormatter
.
forUser
().
formatTimeAndDateRange
(
date1
,
date2
);
}
/**
* Format a date range, showing time only. (static variant)
*
* TODO: Have MW provide localisation.
*
* @param {Date} date1
* @param {Date} date2
* @return {string}
*/
static
formatTimeRange
(
date1
,
date2
)
{
return
DateFormatter
.
forUser
().
formatTimeRange
(
date1
,
date2
);
}
/**
* Format a date range, showing date only. (static variant)
*
* TODO: Have MW provide localisation.
*
* @param {Date} date1
* @param {Date} date2
* @return {string}
*/
static
formatDateRange
(
date1
,
date2
)
{
return
DateFormatter
.
forUser
().
formatDateRange
(
date1
,
date2
);
}
/**
* Get the short name of the time zone, e.g. "PST".
*
* @param {Date} date The reference date
* @return {string}
*/
static
getShortZoneName
(
date
)
{
return
DateFormatter
.
forUser
().
getShortZoneName
(
date
);
}
/**
* Get the time zone offset as a number of minutes, in the sign-preserving
* convention, e.g. "+01:00" becomes 60.
*
* @param {Date} date The reference date for DST
* @return {number}
*/
static
getZoneOffsetMinutes
(
date
)
{
return
DateFormatter
.
forUser
().
getZoneOffsetMinutes
(
date
);
}
/**
* Clear the instance cache. For testing.
*
* @internal
* @ignore
*/
static
clearInstanceCache
()
{
instanceCache
.
user
=
null
;
instanceCache
.
utc
=
null
;
instanceCache
.
site
=
null
;
supportsOffsetCache
=
null
;
}
/**
* @internal
* @hideconstructor
*
* @param {string[]} locales The locale fallback chain
* @param {Object} formats The available date formats, indexed by combined
* style+type key, e.g. "dmy pretty". The property values are objects with
* the following properties:
* - locale: {string} A locale name, overriding the locales parameter
* - pattern: {string} A pattern for formatPattern()
* - options: {object} Options to pass to Intl.DateTimeFormat()
* - error: {string} A parse error relating to the localisation source
* @param {string|undefined} zone The normalized time zone identifier
* @param {string} style The selected date style, one of those available in
* formats, e.g. "dmy". The user's preferred format with fallbacks applied.
*/
constructor
(
locales
,
formats
,
zone
,
style
)
{
this
.
locales
=
locales
;
this
.
formats
=
Object
.
assign
(
{},
formats
);
const
machineTemplate
=
{
locale
:
'en'
,
options
:
{
year
:
'numeric'
,
month
:
'2-digit'
,
day
:
'2-digit'
,
hour
:
'2-digit'
,
minute
:
'2-digit'
,
second
:
'2-digit'
,
hour12
:
false
,
timeZoneName
:
'longOffset'
}
};
this
.
formats
.
datetime
=
Object
.
assign
(
{
pattern
:
'{year}-{month}-{day}T{hour}:{minute}'
},
machineTemplate
);
this
.
formats
.
iso
=
Object
.
assign
(
{
pattern
:
'{year}-{month}-{day}T{hour}:{minute}:{second}{mwZoneOffset}'
},
machineTemplate
);
this
.
formats
.
mw
=
Object
.
assign
(
{
pattern
:
'{year}{month}{day}{hour}{minute}{second}'
},
machineTemplate
);
this
.
formats
.
monthNumber
=
{
locale
:
'en'
,
options
:
{
month
:
'numeric'
}
};
this
.
zone
=
zone
;
this
.
intlFormatters
=
{};
this
.
style
=
style
;
}
/**
* Format a Date to a time and date string (non-static)
*
* @param {Date} date
* @return {string}
*/
formatTimeAndDate
(
date
)
{
return
this
.
formatInternal
(
this
.
style
,
'both'
,
date
);
}
/**
* Format a Date to a date string (non-static)
*
* @param {Date} date
* @return {string}
*/
formatDate
(
date
)
{
return
this
.
formatInternal
(
this
.
style
,
'date'
,
date
);
}
/**
* Format a Date to a time string (non-static)
*
* @param {Date} date
* @return {string}
*/
formatTime
(
date
)
{
return
this
.
formatInternal
(
this
.
style
,
'time'
,
date
);
}
/**
* Format a Date to a date string without the year (non-static)
*
* @param {Date} date
* @return {string}
*/
formatPrettyDate
(
date
)
{
return
this
.
formatInternal
(
this
.
style
,
'pretty'
,
date
);
}
/**
* Format a Date to a MediaWiki 14-character timestamp. Since MediaWiki
* timestamps are conventionally UTC, this should typically be called on an
* instance retrieved with forUtc().
*
* @param {Date} date
* @return {string}
*/
formatMw
(
date
)
{
return
this
.
formatInternal
(
null
,
'mw'
,
date
);
}
/**
* Format a Date in ISO 8601 format, including timezone designator.
* (non-static)
*
* @param {Date} date
* @return {string}
*/
formatIso
(
date
)
{
return
this
.
formatInternal
(
null
,
'iso'
,
date
);
}
/**
* Format a Date in ISO 8601 format, without the timezone designator,
* implying unqualified local time, and without seconds. This is suitable
* for passing to a datetime-local input. (non-static)
*
* @param {Date} date
* @return {string}
*/
formatForDateTimeInput
(
date
)
{
return
this
.
formatInternal
(
null
,
'datetime'
,
date
);
}
/**
* Format a date range, showing time and date. (non-static)
*
* TODO: Have MW provide localisation.
* This function depends on the browser for localisation. It does not
* properly respect the user's date format preferences, and has limited
* language support.
*
* @param {Date} date1
* @param {Date} date2
* @return {string}
*/
formatTimeAndDateRange
(
date1
,
date2
)
{
return
this
.
formatRangeInternal
(
this
.
style
,
'both'
,
date1
,
date2
);
}
/**
* Format a date range, showing time only. (non-static)
*
* TODO: Have MW provide localisation.
*
* @param {Date} date1
* @param {Date} date2
* @return {string}
*/
formatTimeRange
(
date1
,
date2
)
{
return
this
.
formatRangeInternal
(
this
.
style
,
'time'
,
date1
,
date2
);
}
/**
* Format a date range, showing date only. (non-static)
*
* TODO: Have MW provide localisation.
*
* @param {Date} date1
* @param {Date} date2
* @return {string}
*/
formatDateRange
(
date1
,
date2
)
{
return
this
.
formatRangeInternal
(
this
.
style
,
'date'
,
date1
,
date2
);
}
/**
* Get the short name of the time zone, e.g. "PST".
*
* @param {Date} date The reference date
* @return {string}
*/
getShortZoneName
(
date
)
{
return
this
.
getLocalInfoInternal
(
'timeZoneName'
,
'short'
,
date
);
}
/**
* Get the time zone offset as a number of minutes, in the sign-preserving
* convention, e.g. "+01:00" becomes 60.
*
* @param {Date} date The reference date for DST
* @return {number}
*/
getZoneOffsetMinutes
(
date
)
{
const
[
h
,
m
]
=
priv
.
longOffsetToIsoOffset
(
this
.
getLocalInfoInternal
(
'timeZoneName'
,
'longOffset'
,
date
)
).
split
(
':'
);
return
h
*
60
+
Math
.
sign
(
+
h
)
*
m
;
}
/**
* Format a date/time with a specified style.
*
* @internal
* @ignore
*
* @param {string|null} style The style, e.g. "dmy", or null to use an
* internal type identifier for a machine-readable style.
* @param {string} type The type, e.g. "both" for both time and date. If
* style is null then this can be an internal style added by the
* constructor.
* @param {Date} date
* @return {string}
*/
formatInternal
(
style
,
type
,
date
)
{
const
formatName
=
this
.
makeValidFormatName
(
style
,
type
);
const
formatter
=
this
.
getIntlFormatInternal
(
formatName
);
const
pattern
=
this
.
formats
[
formatName
].
pattern
;
if
(
pattern
)
{
const
info
=
priv
.
partsToInfo
(
formatter
.
formatToParts
(
date
)
);
if
(
pattern
.
includes
(
'{mwMonth'
)
)
{
const
i
=
+
this
.
getIntlFormatInternal
(
'monthNumber'
).
format
(
date
);
[
info
.
mwMonth
,
info
.
mwMonthGen
,
info
.
mwMonthAbbrev
]
=
config
.
months
[
i
]
||
[];
}
if
(
pattern
.
includes
(
'{mwZoneOffset}'
)
)
{
info
.
mwZoneOffset
=
priv
.
longOffsetToIsoOffset
(
info
.
timeZoneName
);
}
return
priv
.
formatPattern
(
info
,
pattern
);
}
else
{
return
formatter
.
format
(
date
);
}
}
/**
* Validate a style/type and combine them into a single string, falling
* back to the default style if the user style is not available with the
* specified type.
*
* @internal
* @ignore
*
* @param {string|null} style
* @param {string} type
* @return {string}
*/
makeValidFormatName
(
style
,
type
)
{
if
(
!
style
)
{
return
type
;
}
// Try the specified style, then the localisation's default style
for
(
const
tryStyle
of
[
style
,
config
.
defaultStyle
]
)
{
const
name
=
`
${
tryStyle
}
${
type
}
`
;
if
(
name
in
this
.
formats
)
{
return
name
;
}
}
// Perhaps an invalid type, or bad config?
throw
new
Error
(
`Unable to find a valid date format for "
${
style
}
${
type
}
"`
);
}
/**
* Format a time/date range with a specified style
*
* @internal
* @ignore
*
* @param {string} style
* @param {string} type
* @param {Date} date1
* @param {Date} date2
* @return {string}
*/
formatRangeInternal
(
style
,
type
,
date1
,
date2
)
{
const
formatName
=
this
.
makeValidFormatName
(
style
,
type
);
const
formatter
=
this
.
getIntlFormatInternal
(
formatName
);
const
pattern
=
this
.
formats
[
formatName
].
rangePattern
;
if
(
pattern
)
{
return
priv
.
formatPattern
(
priv
.
partsToInfo
(
formatter
.
formatRangeToParts
(
date1
,
date2
)
),
pattern
);
}
else
{
return
formatter
.
formatRange
(
date1
,
date2
);
}
}
/**
* Get a DateTimeFormat object configured for the specified combined
* type/style.
*
* @internal
* @ignore
*
* @param {string} formatName
* @return {Intl.DateTimeFormat}
*/
getIntlFormatInternal
(
formatName
)
{
if
(
!
(
formatName
in
this
.
intlFormatters
)
)
{
if
(
!
(
formatName
in
this
.
formats
)
)
{
throw
new
Error
(
`Unknown date format "
${
formatName
}
"`
);
}
const
format
=
this
.
formats
[
formatName
];
const
locale
=
format
.
locale
||
this
.
locales
;
const
options
=
Object
.
assign
(
{},
format
.
options
);
options
.
timeZone
=
this
.
zone
;
this
.
intlFormatters
[
formatName
]
=
new
Intl
.
DateTimeFormat
(
locale
,
options
);
}
return
this
.
intlFormatters
[
formatName
];
}
/**
* Extract a part value from a formatted date, with the given configuration
* for that part value.
*
* @internal
* @ignore
*
* @param {string} fieldName
* @param {string} configValue
* @param {Date} date
* @return {string}
*/
getLocalInfoInternal
(
fieldName
,
configValue
,
date
)
{
const
key
=
`info
${
fieldName
}
.
${
configValue
}
`
;
if
(
!
this
.
formats
[
key
]
)
{
this
.
formats
[
key
]
=
{
options
:
{
[
fieldName
]
:
configValue
}
};
}
const
formatter
=
this
.
getIntlFormatInternal
(
key
);
const
info
=
priv
.
partsToInfo
(
formatter
.
formatToParts
(
date
)
);
return
info
[
fieldName
]
||
''
;
}
}
if
(
window
.
QUnit
)
{
DateFormatter
.
priv
=
priv
;
DateFormatter
.
config
=
config
;
}
module
.
exports
=
DateFormatter
;
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Sat, May 16, 18:09 (8 h, 32 m)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
d5/e2/7d7b7e50dbdf300b83079f4badb5
Default Alt Text
DateFormatter.js (18 KB)
Attached To
Mode
rMWPROD MediaWiki Production
Attached
Detach File
Event Timeline
Log In to Comment