MediaWiki  master
DateFormatter.php
Go to the documentation of this file.
1 <?php
25 
39  private $regexes;
40 
47  private const RULES = [
48  self::ALL => [
49  self::MD => self::MD,
50  self::DM => self::DM,
51  ],
52  self::NONE => [
53  self::ISO => self::ISO,
54  ],
55  self::MDY => [
56  self::DM => self::MD,
57  ],
58  self::DMY => [
59  self::MD => self::DM,
60  ],
61  ];
62 
66  private $xMonths = [];
67 
71  private $monthNames = [];
72 
76  private const PREFERENCE_IDS = [
77  'default' => self::NONE,
78  'dmy' => self::DMY,
79  'mdy' => self::MDY,
80  'ymd' => self::YMD,
81  'ISO 8601' => self::ISO,
82  ];
83 
85  private const TARGET_FORMATS = [
86  self::DMY => 'j F Y',
87  self::YDM => 'Y, j F',
88  self::MDY => 'F j, Y',
89  self::YMD => 'Y F j',
90  self::DM => 'j F',
91  self::MD => 'F j',
92  self::ISO => 'y-m-d',
93  ];
94 
96  private const ALL = -1;
97 
99  private const NONE = 0;
100 
102  private const MDY = 1;
103 
105  private const DMY = 2;
106 
108  private const YMD = 3;
109 
111  private const ISO = 4;
112 
114  private const YDM = 5;
115 
117  private const DM = 6;
118 
120  private const MD = 7;
121 
123  private const LAST = 7;
124 
128  public function __construct( Language $lang ) {
129  $monthRegexParts = [];
130  for ( $i = 1; $i <= 12; $i++ ) {
131  $monthName = $lang->getMonthName( $i );
132  $monthAbbrev = $lang->getMonthAbbreviation( $i );
133  $this->monthNames[$i] = $monthName;
134  $monthRegexParts[] = preg_quote( $monthName, '/' );
135  $monthRegexParts[] = preg_quote( $monthAbbrev, '/' );
136  $this->xMonths[mb_strtolower( $monthName )] = $i;
137  $this->xMonths[mb_strtolower( $monthAbbrev )] = $i;
138  }
139 
140  // Partial regular expressions
141  $monthNames = implode( '|', $monthRegexParts );
142  $dm = "(?<day>\d{1,2})[ _](?<monthName>{$monthNames})";
143  $md = "(?<monthName>{$monthNames})[ _](?<day>\d{1,2})";
144  $y = '(?<year>\d{1,4}([ _]BC|))';
145  $iso = '(?<isoYear>-?\d{4})-(?<isoMonth>\d{2})-(?<isoDay>\d{2})';
146 
147  $this->regexes = [
148  self::DMY => "/^{$dm}(?: *, *| +){$y}$/iu",
149  self::YDM => "/^{$y}(?: *, *| +){$dm}$/iu",
150  self::MDY => "/^{$md}(?: *, *| +){$y}$/iu",
151  self::YMD => "/^{$y}(?: *, *| +){$md}$/iu",
152  self::DM => "/^{$dm}$/iu",
153  self::MD => "/^{$md}$/iu",
154  self::ISO => "/^{$iso}$/iu",
155  ];
156  }
157 
167  public static function getInstance( Language $lang = null ) {
168  $lang ??= MediaWikiServices::getInstance()->getContentLanguage();
169  return MediaWikiServices::getInstance()->getDateFormatterFactory()->get( $lang );
170  }
171 
181  public function reformat( $preference, $text, $options = [] ) {
182  if ( isset( self::PREFERENCE_IDS[$preference] ) ) {
183  $userFormatId = self::PREFERENCE_IDS[$preference];
184  } else {
185  $userFormatId = self::NONE;
186  }
187  for ( $source = 1; $source <= self::LAST; $source++ ) {
188  if ( isset( self::RULES[$userFormatId][$source] ) ) {
189  # Specific rules
190  // @phan-suppress-next-line PhanTypeInvalidDimOffset
191  $target = self::RULES[$userFormatId][$source];
192  } elseif ( isset( self::RULES[self::ALL][$source] ) ) {
193  # General rules
194  // @phan-suppress-next-line PhanTypeInvalidDimOffset
195  $target = self::RULES[self::ALL][$source];
196  } elseif ( $userFormatId ) {
197  # User preference
198  $target = $userFormatId;
199  } else {
200  # Default
201  $target = $source;
202  }
203  // @phan-suppress-next-line PhanTypeMismatchDimFetchNullable
204  $format = self::TARGET_FORMATS[$target];
205  $regex = $this->regexes[$source];
206 
207  $text = preg_replace_callback( $regex,
208  function ( $match ) use ( $format ) {
209  $text = '';
210 
211  // Pre-generate y/Y stuff because we need the year for the <span> title.
212  if ( !isset( $match['isoYear'] ) && isset( $match['year'] ) ) {
213  $match['isoYear'] = $this->makeIsoYear( $match['year'] );
214  }
215  if ( !isset( $match['year'] ) && isset( $match['isoYear'] ) ) {
216  $match['year'] = $this->makeNormalYear( $match['isoYear'] );
217  }
218 
219  if ( !isset( $match['isoMonth'] ) ) {
220  $m = $this->makeIsoMonth( $match['monthName'] );
221  if ( $m === false ) {
222  // Fail
223  return $match[0];
224  } else {
225  $match['isoMonth'] = $m;
226  }
227  }
228 
229  if ( !isset( $match['isoDay'] ) ) {
230  $match['isoDay'] = sprintf( '%02d', $match['day'] );
231  }
232 
233  $formatLength = strlen( $format );
234  for ( $p = 0; $p < $formatLength; $p++ ) {
235  $char = $format[$p];
236  switch ( $char ) {
237  case 'd': // ISO day of month
238  $text .= $match['isoDay'];
239  break;
240  case 'm': // ISO month
241  $text .= $match['isoMonth'];
242  break;
243  case 'y': // ISO year
244  // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset False positive
245  $text .= $match['isoYear'];
246  break;
247  case 'j': // ordinary day of month
248  if ( !isset( $match['day'] ) ) {
249  $text .= intval( $match['isoDay'] );
250  } else {
251  $text .= $match['day'];
252  }
253  break;
254  case 'F': // long month
255  $m = intval( $match['isoMonth'] );
256  if ( $m > 12 || $m < 1 ) {
257  // Fail
258  return $match[0];
259  } else {
260  $text .= $this->monthNames[$m];
261  }
262  break;
263  case 'Y': // ordinary (optional BC) year
264  // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset False positive
265  $text .= $match['year'];
266  break;
267  default:
268  $text .= $char;
269  }
270  }
271 
272  $isoBits = [];
273  if ( isset( $match['isoYear'] ) ) {
274  $isoBits[] = $match['isoYear'];
275  }
276  $isoBits[] = $match['isoMonth'];
277  $isoBits[] = $match['isoDay'];
278  $isoDate = implode( '-', $isoBits );
279 
280  // Output is not strictly HTML (it's wikitext), but <span> is allowed.
281  return Html::rawElement( 'span',
282  [ 'class' => 'mw-formatted-date', 'title' => $isoDate ], $text );
283  }, $text
284  );
285  }
286  return $text;
287  }
288 
294  private function makeIsoMonth( $monthName ) {
295  $isoMonth = $this->xMonths[mb_strtolower( $monthName )] ?? false;
296  if ( $isoMonth === false ) {
297  return false;
298  }
299  return sprintf( '%02d', $isoMonth );
300  }
301 
307  private function makeIsoYear( $year ) {
308  // Assumes the year is in a nice format, as enforced by the regex
309  if ( substr( $year, -2 ) == 'BC' ) {
310  $num = intval( substr( $year, 0, -3 ) ) - 1;
311  // PHP bug note: sprintf( "%04d", -1 ) fails poorly
312  $text = sprintf( '-%04d', $num );
313  } else {
314  $text = sprintf( '%04d', $year );
315  }
316  return $text;
317  }
318 
325  private function makeNormalYear( $iso ) {
326  if ( $iso[0] == '-' ) {
327  $text = ( intval( substr( $iso, 1 ) ) + 1 ) . ' BC';
328  } else {
329  $text = intval( $iso );
330  }
331  return $text;
332  }
333 }
Date formatter.
__construct(Language $lang)
static getInstance(Language $lang=null)
Get a DateFormatter object.
reformat( $preference, $text, $options=[])
static rawElement( $element, $attribs=[], $contents='')
Returns an HTML element in a string.
Definition: Html.php:214
Base class for language-specific code.
Definition: Language.php:54
Service locator for MediaWiki core services.
$source
if(!isset( $args[0])) $lang