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