PageRenderTime 28ms CodeModel.GetById 11ms RepoModel.GetById 0ms app.codeStats 0ms

/extensions/WikiCitation/includes/data/WCDate.php

https://github.com/ChuguluGames/mediawiki-svn
PHP | 1119 lines | 862 code | 81 blank | 176 comment | 222 complexity | 51308aaf3c0bf5f26ae3c4015c47c368 MD5 | raw file
  1. <?php
  2. /**
  3. * Part of WikiCitation extension for Mediawiki.
  4. *
  5. * @ingroup WikiCitation
  6. * @file
  7. */
  8. class WCDateNumber {
  9. const unknown = 0;
  10. const day = 1;
  11. const month = 2;
  12. const year = 3;
  13. const notDay = 4;
  14. const notMonth = 5;
  15. const notYear = 6;
  16. public $key, $num;
  17. public $numType;
  18. public function __construct( $key, $num, $type = self::unknown ) {
  19. $this->key = $key;
  20. $this->num = $num;
  21. $this->numType = $type;
  22. }
  23. public function setDay( array &$unknowns, array &$days ) {
  24. switch ( $this->numType ) {
  25. case self::unknown:
  26. case self::notMonth:
  27. case self::notYear:
  28. $this->numType = self::day;
  29. unset( $unknowns[ $this->key ] );
  30. $days[ $this->key ] = $this->num;
  31. return;
  32. default:
  33. return;
  34. }
  35. }
  36. public function setMonth( array &$unknowns, array &$months ) {
  37. switch ( $this->numType ) {
  38. case self::unknown:
  39. case self::notDay:
  40. case self::notYear:
  41. $this->numType = self::month;
  42. unset( $unknowns[ $this->key ] );
  43. $months[ $this->key ] = $this->num;
  44. return;
  45. default:
  46. return;
  47. }
  48. }
  49. public function setYear( array &$unknowns, array &$years ) {
  50. switch ( $this->numType ) {
  51. case self::unknown:
  52. case self::notDay:
  53. case self::notMonth:
  54. $this->numType = self::year;
  55. unset( $unknowns[ $this->key ] );
  56. $years[ $this->key ] = $this->num;
  57. return;
  58. default:
  59. return;
  60. }
  61. }
  62. public function setNotDay( array &$unknowns, array &$months, array &$years ) {
  63. switch ( $this->numType ) {
  64. case self::unknown:
  65. $this->numType = self::notDay;
  66. return;
  67. case self::notMonth:
  68. $this->numType = self::year;
  69. unset( $unknowns[ $this->key ] );
  70. $years[ $this->key ] = $this->num;
  71. return;
  72. case self::notYear:
  73. $this->numType = self::month;
  74. unset( $unknowns[ $this->key ] );
  75. $months[ $this->key ] = $this->num;
  76. return;
  77. default:
  78. return;
  79. }
  80. }
  81. public function setNotMonth( array &$unknowns, array &$days, array &$years ) {
  82. switch ( $this->numType ) {
  83. case self::unknown:
  84. $this->numType = self::notMonth;
  85. return;
  86. case self::notDay:
  87. $this->numType = self::year;
  88. unset( $unknowns[ $this->key ] );
  89. $years[ $this->key ] = $this->num;
  90. return;
  91. case self::notYear:
  92. $this->numType = self::day;
  93. unset( $unknowns[ $this->key ] );
  94. $days[ $this->key ] = $this->num;
  95. return;
  96. default:
  97. return;
  98. }
  99. }
  100. public function setNotYear( array &$unknowns, array &$days, array &$months ) {
  101. switch ( $this->numType ) {
  102. case self::unknown:
  103. $this->numType = self::notYear;
  104. return;
  105. case self::notDay:
  106. $this->numType = self::month;
  107. unset( $unknowns[ $this->key ] );
  108. $months[ $this->key ] = $this->num;
  109. return;
  110. case self::notMonth:
  111. $this->numType = self::day;
  112. unset( $unknowns[ $this->key ] );
  113. $days[ $this->key ] = $this->num;
  114. return;
  115. default:
  116. return;
  117. }
  118. }
  119. public function isNotDay() {
  120. return $this->numType == WCDateNumber::month || $this->numType == WCDateNumber::year || $this->numType == WCDateNumber::notDay;
  121. }
  122. public function isNotMonth() {
  123. return $this->numType == WCDateNumber::day || $this->numType == WCDateNumber::year || $this->numType == WCDateNumber::notMonth;
  124. }
  125. public function isNotYear() {
  126. return $this->numType == WCDateNumber::day || $this->numType == WCDateNumber::month || $this->numType == WCDateNumber::notYear;
  127. }
  128. public function couldBeDay() {
  129. return $this->numType == WCDateNumber::day || $this->numType == WCDateNumber::unknown || $this->numType == WCDateNumber::notMonth || $this->numType == WCDateNumber::notYear;
  130. }
  131. public function couldBeMonth() {
  132. return $this->numType == WCDateNumber::month || $this->numType == WCDateNumber::unknown || $this->numType == WCDateNumber::notDay || $this->numType == WCDateNumber::notYear;
  133. }
  134. public function couldBeYear() {
  135. return $this->numType == WCDateNumber::year || $this->numType == WCDateNumber::unknown || $this->numType == WCDateNumber::notDay || $this->numType == WCDateNumber::notMonth;
  136. }
  137. }
  138. /**
  139. * Data structure WCDate.
  140. * Contains all information needed to be known about a title.
  141. */
  142. class WCDate extends WCData {
  143. # The year, or beginning year of a range.
  144. public $year;
  145. # The end year of a range
  146. public $year2;
  147. # The era (AD, BC, etc.), or beginning era of a range. WCDateTermsEnum::AD, etc.
  148. public $era;
  149. # The end era of a range
  150. public $era2;
  151. # The month, or beginning month of a range.
  152. public $month;
  153. # The end month of a range
  154. public $month2;
  155. # The season, or beginning season of a range. WCDateTermsEnum::spring, etc.
  156. public $season;
  157. # The end season of a range
  158. public $season2;
  159. # The day, or beginning day of a range.
  160. public $day;
  161. # The end day of a range
  162. public $day2;
  163. /**
  164. * Whether the date, or beginning of date range is uncertain
  165. * @var boolean
  166. */
  167. public $isUncertain;
  168. /**
  169. * Whether the second date is uncertain
  170. * @var boolean
  171. */
  172. public $isUncertain2;
  173. /**
  174. * Constructor.
  175. * @param WCCitation $citation = the WCCitation object
  176. * @param WCScopeEnum $scope = the scope (i.e., work, container, series, etc.)
  177. * @param WCParameterEnum $type = the type of property.
  178. * @param string date = the unprocessed date text.
  179. */
  180. public function __construct( $date ) {
  181. # Separate into segments comprising numbers, letters, or special terms.
  182. $adTerms = MagicWord::get( WCDateTermsEnum::$magicWordKeys[ WCDateTermsEnum::AD ] )->getSynonyms();
  183. $bcTerms = MagicWord::get( WCDateTermsEnum::$magicWordKeys[ WCDateTermsEnum::BC ] )->getSynonyms();
  184. $circaTerms = MagicWord::get( WCDateTermsEnum::$magicWordKeys[ WCDateTermsEnum::circa ] )->getSynonyms();
  185. $options = implode( '|', array_merge( $adTerms, $bcTerms, $circaTerms ) );
  186. if ( !preg_match_all( '/'. $options . '|\p{N}+|\p{L}+|./uS', $date, $matches ) ) {
  187. $this->year = 0;
  188. $this->era = WCDateTermsEnum::AD;
  189. $this->month = 0;
  190. $this->day = 0;
  191. $this->isUncertain = True;
  192. return;
  193. }
  194. $chunks = $matches[0];
  195. $numbers = $unknowns = array();
  196. $years = $months = $days = array();
  197. $eras = $seasons = $circas = $yearTerms = $monthTerms = $dayTerms = array();
  198. $counter = 0;
  199. foreach( $chunks as $chunk ) {
  200. # Match month names.
  201. $month = $this->matchMonths( $chunk ); # $month is integer >= 1 or False
  202. if ( $month && count( $months ) < 2 ) {
  203. $months[ $counter ] = $month;
  204. $numbers[ $counter++ ] = new WCDateNumber( $counter, $month, WCDateNumber::month );
  205. continue;
  206. }
  207. # Match date terms.
  208. $dateTermEnum = WCDateTermsEnum::match( $chunk, WCDateTermsEnum::$magicWordArray,
  209. WCDateTermsEnum::$flipMagicWordKeys, 'WCDateTermsEnum' );
  210. if ( $dateTermEnum ) {
  211. switch ( $dateTermEnum->key ) {
  212. case WCDateTermsEnum::AD:
  213. if ( count( $eras ) < 2 ) {
  214. $eras[ $counter++ ] = WCDateTermsEnum::AD;
  215. }
  216. continue 2;
  217. case WCDateTermsEnum::BC:
  218. if ( count( $eras ) < 2 ) {
  219. $eras[ $counter++ ] = WCDateTermsEnum::BC;
  220. }
  221. continue 2;
  222. case WCDateTermsEnum::spring:
  223. if ( count( $seasons ) < 2 ) {
  224. $seasons[ $counter++ ] = WCDateTermsEnum::spring;
  225. }
  226. continue 2;
  227. case WCDateTermsEnum::summer:
  228. if ( count( $seasons ) < 2 ) {
  229. $seasons[ $counter++ ] = WCDateTermsEnum::summer;
  230. }
  231. continue 2;
  232. case WCDateTermsEnum::autumn:
  233. if ( count( $seasons ) < 2 ) {
  234. $seasons[ $counter++ ] = WCDateTermsEnum::autumn;
  235. }
  236. continue 2;
  237. case WCDateTermsEnum::winter:
  238. if ( count( $seasons ) < 2 ) {
  239. $seasons[ $counter++ ] = WCDateTermsEnum::winter;
  240. }
  241. continue 2;
  242. case WCDateTermsEnum::year:
  243. if ( count( $yearTerms ) < 2 ) {
  244. $yearTerms[ $counter++ ] = WCDateTermsEnum::yearTerm;
  245. }
  246. continue 2;
  247. case WCDateTermsEnum::month:
  248. if ( count( $monthTerms ) < 2 ) {
  249. $monthTerms[ $counter++ ] = WCDateTermsEnum::monthTerm;
  250. }
  251. continue 2;
  252. case WCDateTermsEnum::day:
  253. if ( count( $dayTerms ) < 2 ) {
  254. $dayTerms[ $counter++ ] = WCDateTermsEnum::dayTerm;
  255. }
  256. continue 2;
  257. case WCDateTermsEnum::circa:
  258. if ( count( $circas ) < 2 ) {
  259. $circas[ $counter++ ] = WCDateTermsEnum::circa;
  260. }
  261. continue 2;
  262. }
  263. }
  264. # Check for roman numerals (month is often Roman in Hungary, Poland, Romania).
  265. $intTerm = $this->romanToInt( $chunk );
  266. # Convert to integer.
  267. if ( ! $intTerm ) {
  268. $intTerm = (integer) $chunk; # Note, this converts ordinals too.
  269. if ( ! $intTerm ) {
  270. # '00' cannot be month or day, so it must be the two-digit year 2000:
  271. if ( mb_substr( $chunk, 0, 2 ) == '00' ) {
  272. $numbers[ $counter ] = new WCDateNumber( $counter, 2000, WCDateNumber::year );
  273. $years[ $counter ] = 2000;
  274. ++$counter;
  275. continue;
  276. } else {
  277. continue; # not a recognized number
  278. }
  279. }
  280. }
  281. $numbers[ $counter ] = $unknowns[ $counter ] = new WCDateNumber( $counter, $intTerm );
  282. ++$counter;
  283. }
  284. # Look for and handle named Year/Month/Day labels
  285. foreach( $yearTerms as $yearTermKey => $yearTerm ) {
  286. if ( empty( $unknowns ) ) break;
  287. $unknown = $this->searchAdjacentTerm( $unknowns, $yearTermKey, $chunks );
  288. if ( $unknown && count( $years ) < 2 ) {
  289. $years[ $unknown->key ] = $unknown->num;
  290. $unknown->setYear( $unknowns, $years );
  291. }
  292. }
  293. foreach( $monthTerms as $monthTermKey => $monthTerm ) {
  294. if ( empty( $unknowns ) ) break;
  295. $unknown = $this->searchAdjacentTerm( $unknowns, $monthTermKey, $chunks );
  296. if ( $unknown && count( $months ) < 2 ) {
  297. $months[ $unknown->key ] = $unknown->num;
  298. $unknown->setMonth( $unknowns, $months );
  299. }
  300. }
  301. foreach( $dayTerms as $dayTermKey => $dayTerm ) {
  302. if ( empty( $unknowns ) ) break;
  303. $unknown = $this->searchAdjacentTerm( $unknowns, $dayTermKey, $chunks );
  304. if ( $unknown && count( $days ) < 2 ) {
  305. $days[ $unknown->key ] = $unknown->num;
  306. $unknown->setDay( $unknowns, $days );
  307. }
  308. }
  309. # If one or more seasons is specified, treat specially and return.
  310. if ( ! empty( $seasons ) ) {
  311. $this->season = reset( $seasons );
  312. $season2 = next( $seasons );
  313. # Only one season specified
  314. if ( $season2 === False || $season2 == $this->season ) {
  315. $isRange = False;
  316. # Determine year.
  317. if ( count( $years ) >= 1 ) {
  318. $year = reset( $years ); # Use first number as year
  319. $this->assignYearsAndEras( $eras, False, $year->num );
  320. }
  321. elseif ( count( $unknowns ) >= 1 ) {
  322. $year = reset( $unknowns ); # Use first number as year
  323. $this->assignYearsAndEras( $eras, False, $year->num );
  324. }
  325. else {
  326. $curDate = getdate();
  327. $this->year = $curDate['year'];
  328. $this->era = WCDateTermsEnum::AD;
  329. }
  330. }
  331. # Two seasons specified
  332. else {
  333. $isRange = True;
  334. $this->season2 = $season2;
  335. # Determine year
  336. if ( count( $years ) >= 2 ) {
  337. $year = reset( $years )->num;
  338. $year2 = next( $years )->num;
  339. if ( $year2 == $year ) {
  340. $this->assignYearsAndEras( $eras, False, $year );
  341. }
  342. else {
  343. $this->assignYearsAndEras( $eras, True, $year, $year2 );
  344. }
  345. }
  346. elseif ( count( $years ) == 1 ) {
  347. $yearA = reset( $years )->num;
  348. if ( empty( $unknowns ) ) {
  349. $this->assignYearsAndEras( $eras, False, $yearA );
  350. }
  351. else {
  352. $yearB = reset( $unknowns )->num;
  353. if ( $yearA == $yearB ) {
  354. $this->assignYearsAndEras( $eras, False, $yearA );
  355. }
  356. elseif ( $yearA < $yearB ) {
  357. $this->assignYearsAndEras( $eras, True, $yearA, $yearB );
  358. }
  359. else {
  360. $this->assignYearsAndEras( $eras, True, $yearB, $yearA );
  361. }
  362. }
  363. }
  364. elseif ( count( $unknowns ) >= 2 ) {
  365. # Use first two numbers as years.
  366. $year = reset( $unknowns )->num;
  367. $year2 = next( $unknowns )->num;
  368. if ( $year2 == $year ) {
  369. $this->assignYearsAndEras( $eras, False, $year );
  370. }
  371. else {
  372. $this->assignYearsAndEras( $eras, True, $year, $year2 );
  373. }
  374. }
  375. elseif ( count( $unknowns ) == 1 ) {
  376. $year = reset( $unknowns )->num;
  377. $this->assignYearsAndEras( $eras, False, $year );
  378. }
  379. else {
  380. $curDate = getdate();
  381. $this->year = $curDate['year'];
  382. $this->era = WCDateTermsEnum::AD;
  383. }
  384. }
  385. $this->assignUncertainty( $circas, $isRange, $numbers, $chunks );
  386. return;
  387. }
  388. # Handle numbers and/or named months.
  389. switch ( count( $numbers ) ) {
  390. case 0: /****** CASE 0 ******/
  391. $this->finalizeDate( $days, $months, $years, $eras );
  392. $this->assignUncertainty( $circas, False, $numbers, $chunks );
  393. break;
  394. case 1: /****** CASE 1 ******/
  395. /** Must be year
  396. */
  397. if ( ! empty( $unknowns ) ) {
  398. reset( $unknowns ) -> setYear( $unknowns, $years );
  399. }
  400. $this->finalizeDate( $days, $months, $years, $eras );
  401. $this->assignUncertainty( $circas, False, $numbers, $chunks );
  402. break;
  403. case 2: /****** CASE 2 ******/
  404. /** Can be any of the following:
  405. * month-year
  406. * year-year
  407. * year-month
  408. */
  409. $order1 = array( WCDateNumber::month, WCDateNumber::year );
  410. $order2 = array( WCDateNumber::year, WCDateNumber::year );
  411. $order3 = array( WCDateNumber::year, WCDateNumber::month );
  412. if ( ! (
  413. $this->monthUpTo12( $unknowns, $days, $years ) ||
  414. $this->cannotBeDay( $unknowns, $months, $years ) ||
  415. $this->yearAdjacentEra( $unknowns, $years, $eras, $chunks ) ||
  416. $this->yearsInOrder( $unknowns, $days, $months, $years, $eras ) ||
  417. $this->tryOrder( $unknowns, $days, $months, $years, $order1 ) ||
  418. $this->tryOrder( $unknowns, $days, $months, $years, $order2 ) ||
  419. $this->tryOrder( $unknowns, $days, $months, $years, $order3 )
  420. ) ) {
  421. foreach( $unknowns as $unknownKey => $unknown ) {
  422. if ( empty( $months ) ) {
  423. $months[ $unknownKey ] = $unknown->num;
  424. }
  425. elseif ( count( $years ) < 2 ) {
  426. $years[ $unknownKey ] = $unknown->num;
  427. }
  428. }
  429. }
  430. $isRange = $this->finalizeDate( $days, $months, $years, $eras );
  431. $this->assignUncertainty( $circas, $isRange, $numbers, $chunks );
  432. break;
  433. case 3: /****** CASE 3 ******/
  434. /** Can be any of the following:
  435. * day-month-year
  436. * month-day-year
  437. * year-month-day
  438. * month-month-year
  439. * year-month-month
  440. */
  441. $order1 = array( WCDateNumber::day, WCDateNumber::month, WCDateNumber::year );
  442. $order2 = array( WCDateNumber::month, WCDateNumber::day, WCDateNumber::year );
  443. $order3 = array( WCDateNumber::year, WCDateNumber::month, WCDateNumber::day );
  444. $order4 = array( WCDateNumber::month, WCDateNumber::month, WCDateNumber::year );
  445. $order5 = array( WCDateNumber::year, WCDateNumber::month, WCDateNumber::month );
  446. if ( ! (
  447. $this->dayUpTo31( $unknowns, $months, $years ) ||
  448. $this->monthUpTo12( $unknowns, $days, $years ) ||
  449. $this->yearAdjacentEra( $unknowns, $years, $eras, $chunks ) ||
  450. $this->yearsInOrder( $unknowns, $days, $months, $years, $eras ) ||
  451. $this->tryOrder( $unknowns, $days, $months, $years, $order1 ) ||
  452. $this->tryOrder( $unknowns, $days, $months, $years, $order2 ) ||
  453. $this->tryOrder( $unknowns, $days, $months, $years, $order3 ) ||
  454. $this->tryOrder( $unknowns, $days, $months, $years, $order4 ) ||
  455. $this->tryOrder( $unknowns, $days, $months, $years, $order5 )
  456. ) ) {
  457. foreach( $unknowns as $unknownKey => $unknown ) {
  458. if ( empty( $day ) ) {
  459. $days[ $unknownKey ] = $unknown->num;
  460. }
  461. elseif ( empty( $months ) ) {
  462. $months[ $unknownKey ] = $unknown->num;
  463. }
  464. elseif ( empty( $years ) ) {
  465. $years[ $unknownKey ] = $unknown->num;
  466. }
  467. else {
  468. $months[ $unknownKey ] = $unknown->num;
  469. }
  470. }
  471. }
  472. $isRange = $this->finalizeDate( $days, $months, $years, $eras );
  473. $this->assignUncertainty( $circas, $isRange, $numbers, $chunks );
  474. break;
  475. case 4: /****** CASE 4 ******/
  476. /** Can be any of the following:
  477. * day-day-month-year
  478. * month-day-day-year
  479. * year-month-day-day
  480. * month-year-month-year
  481. * year-month-year-month
  482. */
  483. $order1 = array( WCDateNumber::day, WCDateNumber::day, WCDateNumber::month, WCDateNumber::year );
  484. $order2 = array( WCDateNumber::month, WCDateNumber::day, WCDateNumber::day, WCDateNumber::year );
  485. $order3 = array( WCDateNumber::year, WCDateNumber::month, WCDateNumber::day, WCDateNumber::day );
  486. $order4 = array( WCDateNumber::month, WCDateNumber::year, WCDateNumber::month, WCDateNumber::year );
  487. $order5 = array( WCDateNumber::year, WCDateNumber::month, WCDateNumber::year, WCDateNumber::month );
  488. if ( ! (
  489. $this->dayUpTo31( $unknowns, $months, $years ) ||
  490. $this->monthUpTo12( $unknowns, $days, $years ) ||
  491. $this->yearAdjacentEra( $unknowns, $years, $eras, $chunks ) ||
  492. $this->yearsInOrder( $unknowns, $days, $months, $years, $eras ) ||
  493. $this->tryOrder( $unknowns, $days, $months, $years, $order1 ) ||
  494. $this->tryOrder( $unknowns, $days, $months, $years, $order2 ) ||
  495. $this->tryOrder( $unknowns, $days, $months, $years, $order3 ) ||
  496. $this->tryOrder( $unknowns, $days, $months, $years, $order4 ) ||
  497. $this->tryOrder( $unknowns, $days, $months, $years, $order5 )
  498. ) ) {
  499. foreach( $unknowns as $unknownKey => $unknown ) {
  500. if ( count( $day ) < 2 ) {
  501. $days[ $unknownKey ] = $unknown->num;
  502. }
  503. elseif ( empty( $months ) ) {
  504. $months[ $unknownKey ] = $unknown->num;
  505. }
  506. elseif ( empty( $years ) ) {
  507. $years[ $unknownKey ] = $unknown->num;
  508. }
  509. elseif ( count( $months ) < 2 ) {
  510. $months[ $unknownKey ] = $unknown->num;
  511. }
  512. else {
  513. $years[ $unknownKey ] = $unknown->num;
  514. }
  515. }
  516. }
  517. $isRange = $this->finalizeDate( $days, $months, $years, $eras );
  518. $this->assignUncertainty( $circas, $isRange, $numbers, $chunks );
  519. break;
  520. case 5: /****** CASE 5 ******/
  521. /** Can be any of the following:
  522. * day-month-day-month-year
  523. * month-day-month-day-year
  524. * year-month-day-month-day
  525. */
  526. $order1 = array( WCDateNumber::day, WCDateNumber::month, WCDateNumber::day, WCDateNumber::month, WCDateNumber::year );
  527. $order2 = array( WCDateNumber::month, WCDateNumber::day, WCDateNumber::month, WCDateNumber::day, WCDateNumber::year );
  528. $order3 = array( WCDateNumber::year, WCDateNumber::month, WCDateNumber::day, WCDateNumber::month, WCDateNumber::day );
  529. if ( ! (
  530. $this->dayUpTo31( $unknowns, $months, $years ) ||
  531. $this->monthUpTo12( $unknowns, $days, $years ) ||
  532. $this->yearAdjacentEra( $unknowns, $years, $eras, $chunks ) ||
  533. $this->yearsInOrder( $unknowns, $days, $months, $years, $eras ) ||
  534. $this->tryOrder( $unknowns, $days, $months, $years, $order1 ) ||
  535. $this->tryOrder( $unknowns, $days, $months, $years, $order2 ) ||
  536. $this->tryOrder( $unknowns, $days, $months, $years, $order3 )
  537. ) ) {
  538. foreach( $unknowns as $unknownKey => $unknown ) {
  539. if ( count( $day ) < 2 ) {
  540. $days[ $unknownKey ] = $unknown->num;
  541. }
  542. elseif ( count( $months ) < 2 ) {
  543. $months[ $unknownKey ] = $unknown->num;
  544. }
  545. else {
  546. $years[ $unknownKey ] = $unknown->num;
  547. }
  548. }
  549. }
  550. $isRange = $this->finalizeDate( $days, $months, $years, $eras );
  551. $this->assignUncertainty( $circas, $isRange, $numbers, $chunks );
  552. break;
  553. default: /****** CASE 6+ ******/
  554. /** Can be one of the following:
  555. * day-month-year-day-month-year
  556. * month-day-year-month-day-year
  557. * year-month-day-year-month-day
  558. */
  559. $order1 = array( WCDateNumber::day, WCDateNumber::month, WCDateNumber::year, WCDateNumber::day, WCDateNumber::month, WCDateNumber::year );
  560. $order2 = array( WCDateNumber::month, WCDateNumber::day, WCDateNumber::year, WCDateNumber::month, WCDateNumber::day, WCDateNumber::year );
  561. $order3 = array( WCDateNumber::year, WCDateNumber::month, WCDateNumber::day, WCDateNumber::year, WCDateNumber::month, WCDateNumber::day );
  562. if ( ! (
  563. $this->dayUpTo31( $unknowns, $months, $years ) ||
  564. $this->monthUpTo12( $unknowns, $days, $years ) ||
  565. $this->yearAdjacentEra( $unknowns, $years, $eras, $chunks ) ||
  566. $this->yearsInOrder( $unknowns, $days, $months, $years, $eras ) ||
  567. $this->tryOrder( $unknowns, $days, $months, $years, $order1 ) ||
  568. $this->tryOrder( $unknowns, $days, $months, $years, $order2 ) ||
  569. $this->tryOrder( $unknowns, $days, $months, $years, $order3 )
  570. ) ) {
  571. foreach( $unknowns as $unknownKey => $unknown ) {
  572. if ( count( $day ) < 2 ) {
  573. $days[ $unknownKey ] = $unknown->num;
  574. }
  575. elseif ( count( $months ) < 2 ) {
  576. $months[ $unknownKey ] = $unknown->num;
  577. }
  578. else {
  579. $years[ $unknownKey ] = $unknown->num;
  580. }
  581. }
  582. }
  583. $isRange = $this->finalizeDate( $days, $months, $years, $eras );
  584. $this->assignUncertainty( $circas, $isRange, $numbers, $chunks );
  585. }
  586. }
  587. /**
  588. * Determine if $this can be considered a short form of argument $date.
  589. * If so, then determine the number of matches.
  590. *
  591. * @param WCDate $date
  592. * @return integer|boolean
  593. */
  594. public function shortFormMatches( WCData $date ) {
  595. $matches = 0;
  596. if ( isset( $this->year ) ) {
  597. if ( $this->year === $date->year ) ++$matches;
  598. else return False;
  599. }
  600. if ( isset( $this->month ) ) {
  601. if ( $this->month === $date->month ) ++$matches;
  602. else return False;
  603. }
  604. if ( isset( $this->day ) ) {
  605. if ( $this->day === $date->day ) ++$matches;
  606. else return False;
  607. }
  608. if ( isset( $this->season ) ) {
  609. if ( $this->season === $date->season ) ++$matches;
  610. else return False;
  611. }
  612. if ( isset( $this->era ) ) {
  613. if ( $this->era === $date->era ) ++$matches;
  614. else return False;
  615. }
  616. return $matches;
  617. }
  618. public function __toString() {
  619. $text = $this->year;
  620. if ( $this->year2 ) $text .= '–' . $year2;
  621. if ( $this->season ) $text .= ' ' . $season;
  622. if ( $this->season2 ) $text .= '–' . $season2;
  623. if ( $this->month ) $text .= ' ' . $month;
  624. if ( $this->month2 ) $text .= '–' . $month2;
  625. if ( $this->day ) $text .= ' ' . $day;
  626. if ( $this->day2 ) $text .= '–' . $day2;
  627. return $text;
  628. }
  629. /**
  630. * Match localized month names and abbreviations.
  631. *
  632. * @global Language $wgContLang the content Language object
  633. * @param string $chunk the term being tested
  634. * @return int the numerical month ( or 0 if no match)
  635. */
  636. protected function matchMonths( $date ) {
  637. global $wgContLang;
  638. for ( $i=1; $i < 13; $i++ ) {
  639. if ( $date == $wgContLang->getMonthName( $i )
  640. || $date == $wgContLang->getMonthAbbreviation( $i )
  641. || $date == $wgContLang->getMonthNameGen( $i ) ) {
  642. return $i;
  643. }
  644. }
  645. return 0;
  646. }
  647. /**
  648. * Convert a string Roman number to integer.
  649. * If the string does not represent a Roman number, this function returns 0.
  650. * @staticvar array $letters
  651. * @param type $romanNumber = string containing the (possibly) Roman number
  652. * @return int = the integer corresponding to the Roman number (or zero)
  653. */
  654. protected function romanToInt( $romanNumber ) {
  655. static $letters = array(
  656. 'M' => 1000, 'CM' => 900, 'D' => 500, 'CD' => 400, 'C' => 100,
  657. 'XC' => 90, 'L' => 50, 'XL' => 40, 'X' => 10, 'IX' => 9,
  658. 'V' => 5, 'IV' => 4, 'I' => 1,
  659. );
  660. $number = 0;
  661. foreach ( $letters as $key => $value ) {
  662. while ( mb_strpos( $romanNumber, $key ) === 0) {
  663. $result += $value;
  664. $romanNumber = mb_substr( $romanNumber, mb_strlen( $key ) );
  665. }
  666. }
  667. return $number;
  668. }
  669. /**
  670. * If it is a two-digit year, convert to a four-digit year.
  671. * Cutoff between this and last century is 5 years in the future,
  672. * to allow for anticipatory citations of to-be-published material.
  673. * Thus, if in the year 2011 you cited a "01/01/16" publication, the year
  674. * would be interpreted as 2016, but "01/01/17" would be interpreted as
  675. * 1917.
  676. * @param int $year = a two-digit year (this is validated by the function)
  677. * @return int = the corresponding four-digit year
  678. */
  679. protected function adjust2DigitYear( $year ) {
  680. if ( $year >= 100 ) return $year;
  681. $curDate = getdate();
  682. $curYear = $curDate['year'];
  683. # Two digit year plus the current century:
  684. $year = $curYear - $curYear % 100 + $year;
  685. $cutoffYear = $curYear + 5;
  686. if ( $year > $curYear + 10 ) {
  687. return $year - 100;
  688. }
  689. else {
  690. return $year;
  691. }
  692. }
  693. /**
  694. * Assign up to two years and two eras (AD or BC, etc.)
  695. * If the era(s) are not defined, this function assumes AD and converts
  696. * any two-digit years to full years in the current century. Thus, years
  697. * prior to 100 AD require an explicit era designation.
  698. * @param array $eras = array of values comprising either 1=AD or -1=BC
  699. * @param int $isRange = whether or not the date is a range
  700. * @param int $year = the year, or first year
  701. * @param int $year2 = the second year
  702. */
  703. protected function assignYearsAndEras( array $eras, $isRange, $year, $year2 = Null ) {
  704. if ( $isRange ) {
  705. if ( count( $eras ) >= 2 ) {
  706. $this->year = $year;
  707. $this->year2 = $year2;
  708. $this->era = reset( $eras ); # Use first two eras.
  709. $this->era2 = next( $eras );
  710. } elseif ( count ( $eras ) == 1 ) {
  711. $this->year = $year;
  712. $this->year2 = $year2;
  713. $this->era = $this->era2 = reset( $eras );
  714. } else {
  715. $this->year = $this->adjust2DigitYear( $year ); # Assume small years are 2-digit years.
  716. $this->year2 = $this->adjust2DigitYear( $year2 );
  717. $this->era = $this->era2 = WCDateTermsEnum::AD; # Assume AD.
  718. }
  719. } else {
  720. if ( count( $eras ) >= 1 ) {
  721. $this->year = $year;
  722. $this->era = reset( $eras ); # Use first era
  723. } else {
  724. $this->year = $this->adjust2DigitYear( $year ); # Assume small years are 2-digit years.
  725. $this->era = WCDateTermsEnum::AD; # Assume AD.
  726. }
  727. }
  728. }
  729. /**
  730. * Assign uncertainty to one or both of the dates.
  731. * Uncertainty is indicated by "circa" or "c." or equivalent localized
  732. * terms. If there is only one indication of uncertainty, this function
  733. * determines whether the "circa" etc. is closer to $num1 or $num2.
  734. * @param array $circas= a list of "circa" indicators
  735. * @param type $isRange
  736. * @param array $numbers = the list of all numbers that might be uncertain
  737. * @param array $chunks = a list of date terms.
  738. */
  739. protected function assignUncertainty( array $circas, $isRange, array $numbers, array $chunks ) {
  740. if ( $isRange ) {
  741. if ( empty( $circas ) ) {
  742. $this->isUncertain = $this->isUncertain2 = False;
  743. }
  744. # If there are two indications of uncertainty, both dates are uncertain.
  745. elseif ( count( $circas ) >= 2 ) {
  746. $this->isUncertain = $this->isUncertain2 = True;
  747. }
  748. # For just one indication of uncertainty in a range, the uncertain date is the one
  749. # where the number ($num1 or $num2) is most adjacent to the indicator
  750. # of uncertainty.
  751. else {
  752. $circaKey = key( reset( $circas ) );
  753. $uncertainNumber = $this->searchAdjacentTerm( $numbers, $circaKey, $chunks );
  754. if ( $uncertainNumber ) {
  755. $uncertainKey = $uncertainNumber->key;
  756. $uncertainType = $uncertainNumber->numType;
  757. foreach( $numbers as $number ) {
  758. if ( $number->numType == $uncertainType ) {
  759. if ( $number->key == $uncertainKey ) {
  760. # Key matches the first number of that type
  761. $this->isUncertain = True;
  762. $this->isUncertain2 = False;
  763. }
  764. else {
  765. # Key does not match the first number, so probably matches the second
  766. $this->isUncertain = False;
  767. $this->isUncertain2 = True;
  768. }
  769. return;
  770. }
  771. }
  772. }
  773. else {
  774. # None of the terms is adjacent.
  775. $this->isUncertain = False;
  776. return;
  777. }
  778. }
  779. }
  780. else {
  781. $this->isUncertain = ! empty( $circas );
  782. }
  783. }
  784. public function finalizeDate( array $days, array $months, array $years, array $eras ) {
  785. $isRange = False;
  786. $year1 = reset( $years );
  787. if ( $year1 === False ) {
  788. # No year specified
  789. $curDate = getdate();
  790. $this->year = $curDate['year'];
  791. $this->era = WCDateTermsEnum::AD;
  792. }
  793. else {
  794. # At least one year specified
  795. $yearKey1 = key( $years );
  796. $year2 = next( $years );
  797. if ( $year2 === False || $year2 == $year1 ) {
  798. # Only one year specified
  799. $this->assignYearsAndEras( $eras, False, $year1 );
  800. }
  801. else {
  802. # Two years specified
  803. $yearKey2 = key( $years );
  804. $isRange = True;
  805. $this->assignYearsAndEras( $eras, True, $year1, $year2 );
  806. }
  807. }
  808. $month1 = reset( $months );
  809. if ( $month1 === False ) {
  810. # No month specified
  811. return $isRange;
  812. }
  813. else {
  814. # Month is specified
  815. $this->month = $month1;
  816. $month2 = next( $months );
  817. if ( ! ( $month2 === False ) && $month1 != $month2 ) {
  818. # Two months specified
  819. $this->month2 = $month2;
  820. $isRange = True;
  821. }
  822. }
  823. $day1 = reset( $days );
  824. if ( $day1 === False ) {
  825. # No day specified
  826. return $isRange;
  827. }
  828. else {
  829. # Day is specified
  830. $this->day = $day1;
  831. $day2 = next( $days );
  832. if ( $day2 === False || $day2 == $day1 ) {
  833. # Only one day specified
  834. return $isRange;
  835. }
  836. else {
  837. # Two days specified
  838. $this->day2 = $day2;
  839. return True;
  840. }
  841. }
  842. }
  843. /**
  844. * Determine whether the term at key $termKey is adjacent to a number.
  845. * @param array $numbers = an array of WCDateTerm objects
  846. * @param int $termKey = the key of the term in question
  847. * @param array $chunks = chunks of user input
  848. * @return WCDateTerm = WCDateTerm if adjacent, Null if not
  849. */
  850. public function searchAdjacentTerm( array $numbers, $termKey, array $chunks ) {
  851. $s1 = is_set( $numbers[ $termKey - 1 ] );
  852. $s2 = is_set( $numbers[ $termKey + 1 ] );
  853. if ( !$s1 && !$s2 ) {
  854. return Null;
  855. }
  856. elseif ( !$s1 && $s2 ) {
  857. $numberKey = $termKey + 1;
  858. $number = $numbers[ $numberKey ];
  859. $years[ $numberKey ] = $number->num;
  860. return $number;
  861. }
  862. elseif ( $s1 && !$s2 ) {
  863. $numberKey = $termKey - 1;
  864. $number = $numbers[ $numberKey ];
  865. $years[ $numberKey ] = $number->num;
  866. return $number;
  867. }
  868. else {
  869. $number1 = $numbers[ $termKey - 1 ];
  870. $number2 = $numbers[ $termKey + 1 ];
  871. $closestNum = $this->closestNumberToTerm( $termKey, $number1, $number2, $chunks );
  872. if ( $closestNum->key < $termKey ) {
  873. return $number1;
  874. }
  875. else {
  876. return $number2;
  877. }
  878. }
  879. }
  880. public function closestNumberToTerm( $key, WCDateNumber $num1, WCDateNumber $num2, array $chunks ) {
  881. $key1 = $num1->key;
  882. $key2 = $num2->key;
  883. $dist1 = $key - $key1;
  884. $dist2 = $key2 - $key;
  885. $mid1 = implode( '', array_slice( $chunks, $key1 + 1, $dist1 - 1 ) );
  886. $mid2 = implode( '', array_slice( $chunks, $key + 1, $dist2 - 1 ) );
  887. $sp1 = preg_match('/^\p{Zs}*$/uS', $mid1 );
  888. $sp2 = preg_match('/^\p{Zs}*$/uS', $mid2 );
  889. # Is one number (and not the other) separated from the term by merely space?
  890. if ( !$sp1 && $sp2 ) {
  891. return $num2;
  892. }
  893. elseif ( $sp1 && !$sp2 ) {
  894. return $num1;
  895. }
  896. # Pick the number, if any, that has fewer intermediate terms.
  897. elseif ( abs( $dist1 ) < abs( $dist2 ) ) {
  898. return $num1;
  899. }
  900. elseif ( abs( $dist2 ) < abs( $dist1 ) ) {
  901. return $num2;
  902. }
  903. # Pick the number, if any, that has the shortest string between itself and the term:
  904. elseif ( mb_strlen( $mid1 ) < mb_strlen( $mid2 ) ) {
  905. return $num1;
  906. }
  907. elseif ( mb_strlen( $mid2 ) < mb_strlen( $mid1 ) ) {
  908. return $num2;
  909. }
  910. # Anything that makes it this far would look something like "…AAA 12 BBB…" or "…AAA-12-BBB…".
  911. return Null;
  912. }
  913. public function dayUpTo31( array &$unknowns, array &$months, array &$years ) {
  914. foreach( $unknowns as $unknown ) {
  915. if ( $unknown->num > 31 ) {
  916. $unknown->setNotDay( $unknowns, $months, $years );
  917. }
  918. }
  919. return empty( $unknowns );
  920. }
  921. public function monthUpTo12( array &$unknowns, array &$days, array &$years ) {
  922. foreach( $unknowns as $unknown ) {
  923. if ( $unknown->num > 12 ) {
  924. $unknown->setNotMonth( $unknowns, $days, $years );
  925. }
  926. }
  927. return empty( $unknowns );
  928. }
  929. public function yearAdjacentEra( array &$unknowns, array &$years, array $eras, array $chunks ) {
  930. if ( empty( $eras ) || empty( $unknowns ) ) {
  931. return False;
  932. }
  933. foreach ( $eras as $eraKey => $era ) {
  934. $unknown = $this->searchAdjacentTerm( $unknowns, $eraKey, $chunks );
  935. if ( $unknown ) {
  936. $unknown->setYear( $unknowns, $years );
  937. }
  938. }
  939. return empty( $unknowns );
  940. }
  941. public function cannotBeDay( array &$unknowns, &$months, &$years ) {
  942. foreach( $unknowns as $unknown ) {
  943. $unknown->setNotDay( $unknowns, $months, $years );
  944. }
  945. return empty( $unknowns );
  946. }
  947. /**
  948. * Determines whether number can be a year, based on relative year order.
  949. * This is only useful when one year has been defined, but not the other
  950. * year. It assumes that the user will enter a year range in the proper
  951. * numerical order. For example, "09-10" and "7 BC - 4 BC" are proper
  952. * ranges, but if the year 7 BC has already been defined, the next year
  953. * in the range cannot be 10 BC.
  954. * @param array $unknowns
  955. * @param array $days
  956. * @param array $months
  957. * @param array $years
  958. * @param array $eras
  959. * @return boolean = True if all the $unknowns have been exhausted
  960. */
  961. public function yearsInOrder( array &$unknowns, array &$days, array &$months, array &$years, array $eras ) {
  962. if ( count( $years ) == 1 ) {
  963. $year = reset( $years );
  964. $yearKey = key( $years );
  965. # Date range spans the BC-AD era boundary:
  966. if ( empty( $eras ) ) {
  967. foreach( $unknowns as $unknownKey => $unknown ) {
  968. if ( $unknownKey < $yearKey && $unknown->num > $year
  969. || $yearKey < $unknownKey && $year > $unknown->num ) {
  970. $unknown->setNotYear( $unknowns, $days, $months );
  971. }
  972. }
  973. }
  974. else {
  975. $era1 = reset( $eras );
  976. $era2 = next( $eras );
  977. if ( $era2 === False || $era1 === $era2 ) {
  978. if ( $era1 === WCDateTermsEnum::AD ) {
  979. foreach( $unknowns as $unknownKey => $unknown ) {
  980. if ( $unknownKey < $yearKey && $unknown->num > $year
  981. || $yearKey < $unknownKey && $year > $unknown->num ) {
  982. $unknown->setNotYear( $unknowns, $days, $months );
  983. }
  984. }
  985. }
  986. else {
  987. foreach( $unknowns as $unknownKey => $unknown ) {
  988. if ( $unknownKey < $yearKey && $unknown->num < $year
  989. || $yearKey < $unknownKey && $year < $unknown->num ) {
  990. $unknown->setNotYear( $unknowns, $days, $months );
  991. }
  992. }
  993. }
  994. }
  995. else {
  996. return False;
  997. }
  998. }
  999. return empty( $unknowns );
  1000. }
  1001. else {
  1002. return False;
  1003. }
  1004. }
  1005. public function tryOrder( array &$unknowns, array &$days, array &$months, array &$years, array $order ) {
  1006. reset( $order );
  1007. foreach( $unknowns as $unknown ) {
  1008. switch ( current( $order ) ) {
  1009. case WCDateNumber::year:
  1010. if ( $unknown->couldBeYear() ) break;
  1011. else return False;
  1012. case WCDateNumber::month:
  1013. if ( $unknown->couldBeMonth() ) break;
  1014. else return False;
  1015. case WCDateNumber::day:
  1016. if ( $unknown->couldBeDay() ) break;
  1017. else return False;
  1018. }
  1019. next( $order );
  1020. }
  1021. reset( $order );
  1022. foreach( $unknowns as $unknown ) {
  1023. switch ( current( $order ) ) {
  1024. case WCDateNumber::year:
  1025. $unknown->setYear( $unknowns, $years );
  1026. break;
  1027. case WCDateNumber::month:
  1028. $unknown->setMonth( $unknowns, $months );
  1029. break;
  1030. case WCDateNumber::day:
  1031. $unknown->setDay( $unknowns, $days );
  1032. break;
  1033. }
  1034. next( $order );
  1035. }
  1036. return empty( $unknowns );
  1037. }
  1038. }