MediaWiki master
DateFormatter.php
Go to the documentation of this file.
1<?php
10namespace MediaWiki\Parser;
11
15
29 private $regexes;
30
37 private const RULES = [
38 self::ALL => [
39 self::MD => self::MD,
40 self::DM => self::DM,
41 ],
42 self::NONE => [
43 self::ISO => self::ISO,
44 ],
45 self::MDY => [
46 self::DM => self::MD,
47 ],
48 self::DMY => [
49 self::MD => self::DM,
50 ],
51 ];
52
56 private $xMonths = [];
57
61 private $monthNames = [];
62
66 private const PREFERENCE_IDS = [
67 'default' => self::NONE,
68 'dmy' => self::DMY,
69 'mdy' => self::MDY,
70 'ymd' => self::YMD,
71 'ISO 8601' => self::ISO,
72 ];
73
75 private const TARGET_FORMATS = [
76 self::MDY => 'F j, Y',
77 self::DMY => 'j F Y',
78 self::YMD => 'Y F j',
79 self::ISO => 'y-m-d',
80 self::YDM => 'Y, j F',
81 self::DM => 'j F',
82 self::MD => 'F j',
83 ];
84
86 private const ALL = -1;
87
89 private const NONE = 0;
90
92 private const MDY = 1;
93
95 private const DMY = 2;
96
98 private const YMD = 3;
99
101 private const ISO = 4;
102
104 private const YDM = 5;
105
107 private const DM = 6;
108
110 private const MD = 7;
111
115 public function __construct( Language $lang ) {
116 $monthRegexParts = [];
117 for ( $i = 1; $i <= 12; $i++ ) {
118 $monthName = $lang->getMonthName( $i );
119 $monthAbbrev = $lang->getMonthAbbreviation( $i );
120 $this->monthNames[$i] = $monthName;
121 $monthRegexParts[] = preg_quote( $monthName, '/' );
122 $monthRegexParts[] = preg_quote( $monthAbbrev, '/' );
123 $this->xMonths[mb_strtolower( $monthName )] = $i;
124 $this->xMonths[mb_strtolower( $monthAbbrev )] = $i;
125 }
126
127 // Partial regular expressions
128 $monthNames = implode( '|', $monthRegexParts );
129 $dm = "(?<day>\d{1,2})[ _](?<monthName>{$monthNames})";
130 $md = "(?<monthName>{$monthNames})[ _](?<day>\d{1,2})";
131 $y = '(?<year>\d{1,4}([ _]BC|))';
132 $iso = '(?<isoYear>-?\d{4})-(?<isoMonth>\d{2})-(?<isoDay>\d{2})';
133
134 $this->regexes = [
135 self::DMY => "/^{$dm}(?: *, *| +){$y}$/iu",
136 self::YDM => "/^{$y}(?: *, *| +){$dm}$/iu",
137 self::MDY => "/^{$md}(?: *, *| +){$y}$/iu",
138 self::YMD => "/^{$y}(?: *, *| +){$md}$/iu",
139 self::DM => "/^{$dm}$/iu",
140 self::MD => "/^{$md}$/iu",
141 self::ISO => "/^{$iso}$/iu",
142 ];
143 }
144
154 public static function getInstance( ?Language $lang = null ) {
155 $lang ??= MediaWikiServices::getInstance()->getContentLanguage();
156 return MediaWikiServices::getInstance()->getDateFormatterFactory()->get( $lang );
157 }
158
168 public function reformat( $preference, $text, $options = [] ) {
169 $userFormatId = self::PREFERENCE_IDS[$preference] ?? self::NONE;
170 foreach ( self::TARGET_FORMATS as $source => $_ ) {
171 if ( isset( self::RULES[$userFormatId][$source] ) ) {
172 # Specific rules
173 $target = self::RULES[$userFormatId][$source];
174 } elseif ( isset( self::RULES[self::ALL][$source] ) ) {
175 # General rules
176 $target = self::RULES[self::ALL][$source];
177 } elseif ( $userFormatId ) {
178 # User preference
179 $target = $userFormatId;
180 } else {
181 # Default
182 $target = $source;
183 }
184 $format = self::TARGET_FORMATS[$target];
185 $regex = $this->regexes[$source];
186
187 $text = preg_replace_callback( $regex,
188 function ( $match ) use ( $format ) {
189 $text = '';
190
191 // Pre-generate y/Y stuff because we need the year for the <span> title.
192 if ( !isset( $match['isoYear'] ) && isset( $match['year'] ) ) {
193 $match['isoYear'] = $this->makeIsoYear( $match['year'] );
194 }
195 if ( !isset( $match['year'] ) && isset( $match['isoYear'] ) ) {
196 $match['year'] = $this->makeNormalYear( $match['isoYear'] );
197 }
198
199 if ( !isset( $match['isoMonth'] ) ) {
200 $m = $this->makeIsoMonth( $match['monthName'] );
201 if ( $m === null ) {
202 // Fail
203 return $match[0];
204 }
205 $match['isoMonth'] = $m;
206 }
207
208 if ( !isset( $match['isoDay'] ) ) {
209 $match['isoDay'] = sprintf( '%02d', $match['day'] );
210 }
211
212 $formatLength = strlen( $format );
213 for ( $p = 0; $p < $formatLength; $p++ ) {
214 $char = $format[$p];
215 switch ( $char ) {
216 case 'd': // ISO day of month
217 $text .= $match['isoDay'];
218 break;
219 case 'm': // ISO month
220 $text .= $match['isoMonth'];
221 break;
222 case 'y': // ISO year
223 $text .= $match['isoYear'];
224 break;
225 case 'j': // ordinary day of month
226 if ( !isset( $match['day'] ) ) {
227 $text .= intval( $match['isoDay'] );
228 } else {
229 $text .= $match['day'];
230 }
231 break;
232 case 'F': // long month
233 $m = intval( $match['isoMonth'] );
234 if ( $m > 12 || $m < 1 ) {
235 // Fail
236 return $match[0];
237 }
238 $text .= $this->monthNames[$m];
239 break;
240 case 'Y': // ordinary (optional BC) year
241 // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset False positive
242 $text .= $match['year'];
243 break;
244 default:
245 $text .= $char;
246 }
247 }
248
249 $isoBits = [];
250 if ( isset( $match['isoYear'] ) ) {
251 $isoBits[] = $match['isoYear'];
252 }
253 $isoBits[] = $match['isoMonth'];
254 $isoBits[] = $match['isoDay'];
255 $isoDate = implode( '-', $isoBits );
256
257 // Output is not strictly HTML (it's wikitext), but <span> is allowed.
258 return Html::rawElement( 'span',
259 [ 'class' => 'mw-formatted-date', 'title' => $isoDate ], $text );
260 }, $text
261 );
262 }
263 return $text;
264 }
265
270 private function makeIsoMonth( $monthName ) {
271 $number = $this->xMonths[mb_strtolower( $monthName )] ?? null;
272 return $number !== null ? sprintf( '%02d', $number ) : null;
273 }
274
280 private function makeIsoYear( $year ) {
281 // Assumes the year is in a nice format, as enforced by the regex
282 if ( substr( $year, -2 ) == 'BC' ) {
283 $num = intval( substr( $year, 0, -3 ) ) - 1;
284 // PHP bug note: sprintf( "%04d", -1 ) fails poorly
285 $text = sprintf( '-%04d', $num );
286 } else {
287 $text = sprintf( '%04d', $year );
288 }
289 return $text;
290 }
291
298 private function makeNormalYear( $iso ) {
299 if ( $iso <= 0 ) {
300 $text = ( intval( substr( $iso, 1 ) ) + 1 ) . ' BC';
301 } else {
302 $text = intval( $iso );
303 }
304 return $text;
305 }
306}
307
309class_alias( DateFormatter::class, 'DateFormatter' );
This class is a collection of static functions that serve two purposes:
Definition Html.php:43
Base class for language-specific code.
Definition Language.php:69
Service locator for MediaWiki core services.
static getInstance()
Returns the global default instance of the top level service locator.
static getInstance(?Language $lang=null)
Get a DateFormatter object.
reformat( $preference, $text, $options=[])
$source