DateFormatter.php 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332
  1. <?php
  2. /**
  3. * Date formatter, recognises dates in plain text and formats them accoding to user preferences.
  4. * @todo preferences, OutputPage
  5. * @ingroup Parser
  6. */
  7. class DateFormatter
  8. {
  9. var $mSource, $mTarget;
  10. var $monthNames = '', $rxDM, $rxMD, $rxDMY, $rxYDM, $rxMDY, $rxYMD;
  11. var $regexes, $pDays, $pMonths, $pYears;
  12. var $rules, $xMonths, $preferences;
  13. const ALL = -1;
  14. const NONE = 0;
  15. const MDY = 1;
  16. const DMY = 2;
  17. const YMD = 3;
  18. const ISO1 = 4;
  19. const LASTPREF = 4;
  20. const ISO2 = 5;
  21. const YDM = 6;
  22. const DM = 7;
  23. const MD = 8;
  24. const LAST = 8;
  25. /**
  26. * @todo document
  27. */
  28. function DateFormatter() {
  29. global $wgContLang;
  30. $this->monthNames = $this->getMonthRegex();
  31. for ( $i=1; $i<=12; $i++ ) {
  32. $this->xMonths[$wgContLang->lc( $wgContLang->getMonthName( $i ) )] = $i;
  33. $this->xMonths[$wgContLang->lc( $wgContLang->getMonthAbbreviation( $i ) )] = $i;
  34. }
  35. $this->regexTrail = '(?![a-z])/iu';
  36. # Partial regular expressions
  37. $this->prxDM = '\[\[(\d{1,2})[ _](' . $this->monthNames . ')\]\]';
  38. $this->prxMD = '\[\[(' . $this->monthNames . ')[ _](\d{1,2})\]\]';
  39. $this->prxY = '\[\[(\d{1,4}([ _]BC|))\]\]';
  40. $this->prxISO1 = '\[\[(-?\d{4})]]-\[\[(\d{2})-(\d{2})\]\]';
  41. $this->prxISO2 = '\[\[(-?\d{4})-(\d{2})-(\d{2})\]\]';
  42. # Real regular expressions
  43. $this->regexes[self::DMY] = "/{$this->prxDM} *,? *{$this->prxY}{$this->regexTrail}";
  44. $this->regexes[self::YDM] = "/{$this->prxY} *,? *{$this->prxDM}{$this->regexTrail}";
  45. $this->regexes[self::MDY] = "/{$this->prxMD} *,? *{$this->prxY}{$this->regexTrail}";
  46. $this->regexes[self::YMD] = "/{$this->prxY} *,? *{$this->prxMD}{$this->regexTrail}";
  47. $this->regexes[self::DM] = "/{$this->prxDM}{$this->regexTrail}";
  48. $this->regexes[self::MD] = "/{$this->prxMD}{$this->regexTrail}";
  49. $this->regexes[self::ISO1] = "/{$this->prxISO1}{$this->regexTrail}";
  50. $this->regexes[self::ISO2] = "/{$this->prxISO2}{$this->regexTrail}";
  51. # Extraction keys
  52. # See the comments in replace() for the meaning of the letters
  53. $this->keys[self::DMY] = 'jFY';
  54. $this->keys[self::YDM] = 'Y jF';
  55. $this->keys[self::MDY] = 'FjY';
  56. $this->keys[self::YMD] = 'Y Fj';
  57. $this->keys[self::DM] = 'jF';
  58. $this->keys[self::MD] = 'Fj';
  59. $this->keys[self::ISO1] = 'ymd'; # y means ISO year
  60. $this->keys[self::ISO2] = 'ymd';
  61. # Target date formats
  62. $this->targets[self::DMY] = '[[F j|j F]] [[Y]]';
  63. $this->targets[self::YDM] = '[[Y]], [[F j|j F]]';
  64. $this->targets[self::MDY] = '[[F j]], [[Y]]';
  65. $this->targets[self::YMD] = '[[Y]] [[F j]]';
  66. $this->targets[self::DM] = '[[F j|j F]]';
  67. $this->targets[self::MD] = '[[F j]]';
  68. $this->targets[self::ISO1] = '[[Y|y]]-[[F j|m-d]]';
  69. $this->targets[self::ISO2] = '[[y-m-d]]';
  70. # Rules
  71. # pref source target
  72. $this->rules[self::DMY][self::MD] = self::DM;
  73. $this->rules[self::ALL][self::MD] = self::MD;
  74. $this->rules[self::MDY][self::DM] = self::MD;
  75. $this->rules[self::ALL][self::DM] = self::DM;
  76. $this->rules[self::NONE][self::ISO2] = self::ISO1;
  77. $this->preferences = array(
  78. 'default' => self::NONE,
  79. 'dmy' => self::DMY,
  80. 'mdy' => self::MDY,
  81. 'ymd' => self::YMD,
  82. 'ISO 8601' => self::ISO1,
  83. );
  84. }
  85. /**
  86. * Get a DateFormatter object
  87. *
  88. * @return DateFormatter object
  89. */
  90. public static function &getInstance() {
  91. global $wgMemc;
  92. static $dateFormatter = false;
  93. if ( !$dateFormatter ) {
  94. $dateFormatter = $wgMemc->get( wfMemcKey( 'dateformatter' ) );
  95. if ( !$dateFormatter ) {
  96. $dateFormatter = new DateFormatter;
  97. $wgMemc->set( wfMemcKey( 'dateformatter' ), $dateFormatter, 3600 );
  98. }
  99. }
  100. return $dateFormatter;
  101. }
  102. /**
  103. * @param $preference String: User preference
  104. * @param $text String: Text to reformat
  105. */
  106. function reformat( $preference, $text, $options = array('linked') ) {
  107. $linked = in_array( 'linked', $options );
  108. $match_whole = in_array( 'match-whole', $options );
  109. if ( isset( $this->preferences[$preference] ) ) {
  110. $preference = $this->preferences[$preference];
  111. } else {
  112. $preference = self::NONE;
  113. }
  114. for ( $i=1; $i<=self::LAST; $i++ ) {
  115. $this->mSource = $i;
  116. if ( isset ( $this->rules[$preference][$i] ) ) {
  117. # Specific rules
  118. $this->mTarget = $this->rules[$preference][$i];
  119. } elseif ( isset ( $this->rules[self::ALL][$i] ) ) {
  120. # General rules
  121. $this->mTarget = $this->rules[self::ALL][$i];
  122. } elseif ( $preference ) {
  123. # User preference
  124. $this->mTarget = $preference;
  125. } else {
  126. # Default
  127. $this->mTarget = $i;
  128. }
  129. $regex = $this->regexes[$i];
  130. // Horrible hack
  131. if (!$linked) {
  132. $regex = str_replace( array( '\[\[', '\]\]' ), '', $regex );
  133. }
  134. if ($match_whole) {
  135. // Let's hope this works
  136. $regex = preg_replace( '!^/!', '/^', $regex );
  137. $regex = str_replace( $this->regexTrail,
  138. '$'.$this->regexTrail, $regex );
  139. }
  140. // Another horrible hack
  141. $this->mLinked = $linked;
  142. $text = preg_replace_callback( $regex, array( &$this, 'replace' ), $text );
  143. unset($this->mLinked);
  144. }
  145. return $text;
  146. }
  147. /**
  148. * @param $matches
  149. */
  150. function replace( $matches ) {
  151. # Extract information from $matches
  152. $linked = true;
  153. if ( isset( $this->mLinked ) )
  154. $linked = $this->mLinked;
  155. $bits = array();
  156. $key = $this->keys[$this->mSource];
  157. for ( $p=0; $p < strlen($key); $p++ ) {
  158. if ( $key{$p} != ' ' ) {
  159. $bits[$key{$p}] = $matches[$p+1];
  160. }
  161. }
  162. return $this->formatDate( $bits, $linked );
  163. }
  164. function formatDate( $bits, $link = true ) {
  165. $format = $this->targets[$this->mTarget];
  166. if (!$link) {
  167. // strip piped links
  168. $format = preg_replace( '/\[\[[^|]+\|([^\]]+)\]\]/', '$1', $format );
  169. // strip remaining links
  170. $format = str_replace( array( '[[', ']]' ), '', $format );
  171. }
  172. # Construct new date
  173. $text = '';
  174. $fail = false;
  175. // Pre-generate y/Y stuff because we need the year for the <span> title.
  176. if ( !isset( $bits['y'] ) && isset( $bits['Y'] ) )
  177. $bits['y'] = $this->makeIsoYear( $bits['Y'] );
  178. if ( !isset( $bits['Y'] ) && isset( $bits['y'] ) )
  179. $bits['Y'] = $this->makeNormalYear( $bits['y'] );
  180. if ( !isset( $bits['m'] ) ) {
  181. $m = $this->makeIsoMonth( $bits['F'] );
  182. if ( !$m || $m == '00' ) {
  183. $fail = true;
  184. } else {
  185. $bits['m'] = $m;
  186. }
  187. }
  188. if ( !isset($bits['d']) ) {
  189. $bits['d'] = sprintf( '%02d', $bits['j'] );
  190. }
  191. for ( $p=0; $p < strlen( $format ); $p++ ) {
  192. $char = $format{$p};
  193. switch ( $char ) {
  194. case 'd': # ISO day of month
  195. $text .= $bits['d'];
  196. break;
  197. case 'm': # ISO month
  198. $text .= $bits['m'];
  199. break;
  200. case 'y': # ISO year
  201. $text .= $bits['y'];
  202. break;
  203. case 'j': # ordinary day of month
  204. if ( !isset($bits['j']) ) {
  205. $text .= intval( $bits['d'] );
  206. } else {
  207. $text .= $bits['j'];
  208. }
  209. break;
  210. case 'F': # long month
  211. if ( !isset( $bits['F'] ) ) {
  212. $m = intval($bits['m']);
  213. if ( $m > 12 || $m < 1 ) {
  214. $fail = true;
  215. } else {
  216. global $wgContLang;
  217. $text .= $wgContLang->getMonthName( $m );
  218. }
  219. } else {
  220. $text .= ucfirst( $bits['F'] );
  221. }
  222. break;
  223. case 'Y': # ordinary (optional BC) year
  224. $text .= $bits['Y'];
  225. break;
  226. default:
  227. $text .= $char;
  228. }
  229. }
  230. if ( $fail ) {
  231. $text = $matches[0];
  232. }
  233. $isoBits = array();
  234. if ( isset($bits['y']) )
  235. $isoBits[] = $bits['y'];
  236. $isoBits[] = $bits['m'];
  237. $isoBits[] = $bits['d'];
  238. $isoDate = implode( '-', $isoBits );;
  239. // Output is not strictly HTML (it's wikitext), but <span> is whitelisted.
  240. $text = Xml::tags( 'span',
  241. array( 'class' => 'mw-formatted-date', 'title' => $isoDate ), $text );
  242. return $text;
  243. }
  244. /**
  245. * @todo document
  246. */
  247. function getMonthRegex() {
  248. global $wgContLang;
  249. $names = array();
  250. for( $i = 1; $i <= 12; $i++ ) {
  251. $names[] = $wgContLang->getMonthName( $i );
  252. $names[] = $wgContLang->getMonthAbbreviation( $i );
  253. }
  254. return implode( '|', $names );
  255. }
  256. /**
  257. * Makes an ISO month, e.g. 02, from a month name
  258. * @param $monthName String: month name
  259. * @return string ISO month name
  260. */
  261. function makeIsoMonth( $monthName ) {
  262. global $wgContLang;
  263. $n = $this->xMonths[$wgContLang->lc( $monthName )];
  264. return sprintf( '%02d', $n );
  265. }
  266. /**
  267. * @todo document
  268. * @param $year String: Year name
  269. * @return string ISO year name
  270. */
  271. function makeIsoYear( $year ) {
  272. # Assumes the year is in a nice format, as enforced by the regex
  273. if ( substr( $year, -2 ) == 'BC' ) {
  274. $num = intval(substr( $year, 0, -3 )) - 1;
  275. # PHP bug note: sprintf( "%04d", -1 ) fails poorly
  276. $text = sprintf( '-%04d', $num );
  277. } else {
  278. $text = sprintf( '%04d', $year );
  279. }
  280. return $text;
  281. }
  282. /**
  283. * @todo document
  284. */
  285. function makeNormalYear( $iso ) {
  286. if ( $iso{0} == '-' ) {
  287. $text = (intval( substr( $iso, 1 ) ) + 1) . ' BC';
  288. } else {
  289. $text = intval( $iso );
  290. }
  291. return $text;
  292. }
  293. }