MediaWiki  master
DateFormatter.php
Go to the documentation of this file.
1 <?php
25 
39  private $regexes;
40 
47  private $rules = [];
48 
52  private $xMonths = [];
53 
57  private $monthNames = [];
58 
62  private $preferenceIDs;
63 
65  private $targetFormats;
66 
68  const ALL = -1;
69 
71  const NONE = 0;
72 
74  const MDY = 1;
75 
77  const DMY = 2;
78 
80  const YMD = 3;
81 
83  const ISO = 4;
84 
86  const LASTPREF = 4;
87 
89  const YDM = 5;
90 
92  const DM = 6;
93 
95  const MD = 7;
96 
98  const LAST = 7;
99 
103  public function __construct( Language $lang ) {
104  $monthRegexParts = [];
105  for ( $i = 1; $i <= 12; $i++ ) {
106  $monthName = $lang->getMonthName( $i );
107  $monthAbbrev = $lang->getMonthAbbreviation( $i );
108  $this->monthNames[$i] = $monthName;
109  $monthRegexParts[] = preg_quote( $monthName, '/' );
110  $monthRegexParts[] = preg_quote( $monthAbbrev, '/' );
111  $this->xMonths[mb_strtolower( $monthName )] = $i;
112  $this->xMonths[mb_strtolower( $monthAbbrev )] = $i;
113  }
114 
115  // Partial regular expressions
116  $monthNames = implode( '|', $monthRegexParts );
117  $dm = "(?<day>\d{1,2})[ _](?<monthName>{$monthNames})";
118  $md = "(?<monthName>{$monthNames})[ _](?<day>\d{1,2})";
119  $y = '(?<year>\d{1,4}([ _]BC|))';
120  $iso = '(?<isoYear>-?\d{4})-(?<isoMonth>\d{2})-(?<isoDay>\d{2})';
121 
122  $this->regexes = [
123  self::DMY => "/^{$dm}(?: *, *| +){$y}$/iu",
124  self::YDM => "/^{$y}(?: *, *| +){$dm}$/iu",
125  self::MDY => "/^{$md}(?: *, *| +){$y}$/iu",
126  self::YMD => "/^{$y}(?: *, *| +){$md}$/iu",
127  self::DM => "/^{$dm}$/iu",
128  self::MD => "/^{$md}$/iu",
129  self::ISO => "/^{$iso}$/iu",
130  ];
131 
132  // Target date formats
133  $this->targetFormats = [
134  self::DMY => 'j F Y',
135  self::YDM => 'Y, j F',
136  self::MDY => 'F j, Y',
137  self::YMD => 'Y F j',
138  self::DM => 'j F',
139  self::MD => 'F j',
140  self::ISO => 'y-m-d',
141  ];
142 
143  // Rules
144  // pref source target
145  $this->rules[self::DMY][self::MD] = self::DM;
146  $this->rules[self::ALL][self::MD] = self::MD;
147  $this->rules[self::MDY][self::DM] = self::MD;
148  $this->rules[self::ALL][self::DM] = self::DM;
149  $this->rules[self::NONE][self::ISO] = self::ISO;
150 
151  $this->preferenceIDs = [
152  'default' => self::NONE,
153  'dmy' => self::DMY,
154  'mdy' => self::MDY,
155  'ymd' => self::YMD,
156  'ISO 8601' => self::ISO,
157  ];
158  }
159 
169  public static function getInstance( Language $lang = null ) {
170  $lang = $lang ?? MediaWikiServices::getInstance()->getContentLanguage();
171  return MediaWikiServices::getInstance()->getDateFormatterFactory()->get( $lang );
172  }
173 
183  public function reformat( $preference, $text, $options = [] ) {
184  if ( isset( $this->preferenceIDs[$preference] ) ) {
185  $preference = $this->preferenceIDs[$preference];
186  } else {
187  $preference = self::NONE;
188  }
189  for ( $source = 1; $source <= self::LAST; $source++ ) {
190  if ( isset( $this->rules[$preference][$source] ) ) {
191  # Specific rules
192  $target = $this->rules[$preference][$source];
193  } elseif ( isset( $this->rules[self::ALL][$source] ) ) {
194  # General rules
195  $target = $this->rules[self::ALL][$source];
196  } elseif ( $preference ) {
197  # User preference
198  $target = $preference;
199  } else {
200  # Default
201  $target = $source;
202  }
203  $regex = $this->regexes[$source];
204 
205  $text = preg_replace_callback( $regex,
206  function ( $match ) use ( $target ) {
207  $format = $this->targetFormats[$target];
208 
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  $text .= $match['isoYear'];
245  break;
246  case 'j': // ordinary day of month
247  if ( !isset( $match['day'] ) ) {
248  $text .= intval( $match['isoDay'] );
249  } else {
250  $text .= $match['day'];
251  }
252  break;
253  case 'F': // long month
254  $m = intval( $match['isoMonth'] );
255  if ( $m > 12 || $m < 1 ) {
256  // Fail
257  return $match[0];
258  } else {
259  $text .= $this->monthNames[$m];
260  }
261  break;
262  case 'Y': // ordinary (optional BC) year
263  $text .= $match['year'];
264  break;
265  default:
266  $text .= $char;
267  }
268  }
269 
270  $isoBits = [];
271  if ( isset( $match['isoYear'] ) ) {
272  $isoBits[] = $match['isoYear'];
273  }
274  $isoBits[] = $match['isoMonth'];
275  $isoBits[] = $match['isoDay'];
276  $isoDate = implode( '-', $isoBits );
277 
278  // Output is not strictly HTML (it's wikitext), but <span> is whitelisted.
279  $text = Html::rawElement( 'span',
280  [ 'class' => 'mw-formatted-date', 'title' => $isoDate ], $text );
281 
282  return $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 }
int [] $xMonths
Month numbers by lowercase name.
reformat( $preference, $text, $options=[])
const YMD
e.g.
if(!isset( $args[0])) $lang
static rawElement( $element, $attribs=[], $contents='')
Returns an HTML element in a string.
Definition: Html.php:209
$source
makeNormalYear( $iso)
Make a year from an ISO year, for instance: &#39;400 BC&#39; from &#39;-0399&#39;.
const YDM
e.g.
static getInstance(Language $lang=null)
Get a DateFormatter object.
const ALL
Used as a preference ID for rules that apply regardless of preference.
getMonthAbbreviation( $key)
Definition: Language.php:864
__construct(Language $lang)
int [] $preferenceIDs
A map of descriptive preference text to internal format ID.
const LAST
The highest ID that is a valid target format.
const MDY
e.g.
string [] $monthNames
Month names by number.
makeIsoYear( $year)
Make an ISO year from a year name, for instance: &#39;-1199&#39; from &#39;1200 BC&#39;.
string [] $regexes
Date format regexes indexed the class constants.
makeIsoMonth( $monthName)
Makes an ISO month, e.g.
const LASTPREF
The highest ID that is a valid user preference.
const DM
e.g.
const MD
e.g.
int [][] $rules
Array of special rules.
Date formatter.
const ISO
e.g.
string [] $targetFormats
Format strings similar to those used by date(), indexed by ID.
const DMY
e.g.
const NONE
No preference: the date may be left in the same format as the input.
getMonthName( $key)
Definition: Language.php:837