PageRenderTime 44ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 0ms

/core/Period/Range.php

https://github.com/CodeYellowBV/piwik
PHP | 451 lines | 271 code | 45 blank | 135 comment | 74 complexity | cc6a46e759463a39eecaa4c80335e770 MD5 | raw file
Possible License(s): LGPL-3.0, JSON, MIT, GPL-3.0, LGPL-2.1, GPL-2.0, AGPL-1.0, BSD-2-Clause, BSD-3-Clause
  1. <?php
  2. /**
  3. * Piwik - free/libre analytics platform
  4. *
  5. * @link http://piwik.org
  6. * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
  7. *
  8. */
  9. namespace Piwik\Period;
  10. use Exception;
  11. use Piwik\Common;
  12. use Piwik\Date;
  13. use Piwik\Period;
  14. use Piwik\Piwik;
  15. /**
  16. * Arbitrary date range representation.
  17. *
  18. * This class represents a period that contains a list of consecutive days as subperiods
  19. * It is created when the **period** query parameter is set to **range** and is used
  20. * to calculate the subperiods of multiple period requests (eg, when period=day and
  21. * date=2007-07-24,2013-11-15).
  22. *
  23. * The range period differs from other periods mainly in that since it is arbitrary,
  24. * range periods are not pre-archived by the cron core:archive command.
  25. *
  26. * @api
  27. */
  28. class Range extends Period
  29. {
  30. protected $label = 'range';
  31. protected $today;
  32. /**
  33. * Constructor.
  34. *
  35. * @param string $strPeriod The type of period each subperiod is. Either `'day'`, `'week'`,
  36. * `'month'` or `'year'`.
  37. * @param string $strDate The date range, eg, `'2007-07-24,2013-11-15'`.
  38. * @param string $timezone The timezone to use, eg, `'UTC'`.
  39. * @param bool|Date $today The date to use as _today_. Defaults to `Date::factory('today', $timzeone)`.
  40. * @api
  41. */
  42. public function __construct($strPeriod, $strDate, $timezone = 'UTC', $today = false)
  43. {
  44. $this->strPeriod = $strPeriod;
  45. $this->strDate = $strDate;
  46. $this->defaultEndDate = null;
  47. $this->timezone = $timezone;
  48. if ($today === false) {
  49. $today = Date::factory('now', $this->timezone);
  50. }
  51. $this->today = $today;
  52. }
  53. /**
  54. * Returns the current period as a localized short string.
  55. *
  56. * @return string
  57. */
  58. public function getLocalizedShortString()
  59. {
  60. //"30 Dec 08 - 26 Feb 09"
  61. $dateStart = $this->getDateStart();
  62. $dateEnd = $this->getDateEnd();
  63. $template = Piwik::translate('CoreHome_ShortDateFormatWithYear');
  64. $shortDateStart = $dateStart->getLocalized($template);
  65. $shortDateEnd = $dateEnd->getLocalized($template);
  66. $out = "$shortDateStart - $shortDateEnd";
  67. return $out;
  68. }
  69. /**
  70. * Returns the current period as a localized long string.
  71. *
  72. * @return string
  73. */
  74. public function getLocalizedLongString()
  75. {
  76. return $this->getLocalizedShortString();
  77. }
  78. /**
  79. * Returns the start date of the period.
  80. *
  81. * @return Date
  82. * @throws Exception
  83. */
  84. public function getDateStart()
  85. {
  86. $dateStart = parent::getDateStart();
  87. if (empty($dateStart)) {
  88. throw new Exception("Specified date range is invalid.");
  89. }
  90. return $dateStart;
  91. }
  92. /**
  93. * Returns the current period as a string.
  94. *
  95. * @return string
  96. */
  97. public function getPrettyString()
  98. {
  99. $out = Piwik::translate('General_DateRangeFromTo', array($this->getDateStart()->toString(), $this->getDateEnd()->toString()));
  100. return $out;
  101. }
  102. protected function getMaxN($lastN)
  103. {
  104. switch ($this->strPeriod) {
  105. case 'day':
  106. $lastN = min($lastN, 5 * 365);
  107. break;
  108. case 'week':
  109. $lastN = min($lastN, 10 * 52);
  110. break;
  111. case 'month':
  112. $lastN = min($lastN, 10 * 12);
  113. break;
  114. case 'year':
  115. $lastN = min($lastN, 10);
  116. break;
  117. }
  118. return $lastN;
  119. }
  120. /**
  121. * Sets the default end date of the period.
  122. *
  123. * @param Date $oDate
  124. */
  125. public function setDefaultEndDate(Date $oDate)
  126. {
  127. $this->defaultEndDate = $oDate;
  128. }
  129. /**
  130. * Generates the subperiods
  131. *
  132. * @throws Exception
  133. */
  134. protected function generate()
  135. {
  136. if ($this->subperiodsProcessed) {
  137. return;
  138. }
  139. parent::generate();
  140. if (preg_match('/(last|previous)([0-9]*)/', $this->strDate, $regs)) {
  141. $lastN = $regs[2];
  142. $lastOrPrevious = $regs[1];
  143. if (!is_null($this->defaultEndDate)) {
  144. $defaultEndDate = $this->defaultEndDate;
  145. } else {
  146. $defaultEndDate = $this->today;
  147. }
  148. $period = $this->strPeriod;
  149. if ($period == 'range') {
  150. $period = 'day';
  151. }
  152. if ($lastOrPrevious == 'last') {
  153. $endDate = $defaultEndDate;
  154. } elseif ($lastOrPrevious == 'previous') {
  155. if ('month' == $period) {
  156. $endDate = $defaultEndDate->subMonth(1);
  157. } else {
  158. $endDate = $defaultEndDate->subPeriod(1, $period);
  159. }
  160. }
  161. $lastN = $this->getMaxN($lastN);
  162. // last1 means only one result ; last2 means 2 results so we remove only 1 to the days/weeks/etc
  163. $lastN--;
  164. $lastN = abs($lastN);
  165. $startDate = $endDate->addPeriod(-1 * $lastN, $period);
  166. } elseif ($dateRange = Range::parseDateRange($this->strDate)) {
  167. $strDateStart = $dateRange[1];
  168. $strDateEnd = $dateRange[2];
  169. $startDate = Date::factory($strDateStart);
  170. if ($strDateEnd == 'today') {
  171. $strDateEnd = 'now';
  172. } elseif ($strDateEnd == 'yesterday') {
  173. $strDateEnd = 'yesterdaySameTime';
  174. }
  175. // we set the timezone in the Date object only if the date is relative eg. 'today', 'yesterday', 'now'
  176. $timezone = null;
  177. if (strpos($strDateEnd, '-') === false) {
  178. $timezone = $this->timezone;
  179. }
  180. $endDate = Date::factory($strDateEnd, $timezone);
  181. } else {
  182. throw new Exception(Piwik::translate('General_ExceptionInvalidDateRange', array($this->strDate, ' \'lastN\', \'previousN\', \'YYYY-MM-DD,YYYY-MM-DD\'')));
  183. }
  184. if ($this->strPeriod != 'range') {
  185. $this->fillArraySubPeriods($startDate, $endDate, $this->strPeriod);
  186. return;
  187. }
  188. $this->processOptimalSubperiods($startDate, $endDate);
  189. // When period=range, we want End Date to be the actual specified end date,
  190. // rather than the end of the month / week / whatever is used for processing this range
  191. $this->endDate = $endDate;
  192. }
  193. /**
  194. * Given a date string, returns `false` if not a date range,
  195. * or returns the array containing start and end dates.
  196. *
  197. * @param string $dateString
  198. * @return mixed array(1 => dateStartString, 2 => dateEndString) or `false` if the input was not a date range.
  199. */
  200. static public function parseDateRange($dateString)
  201. {
  202. $matched = preg_match('/^([0-9]{4}-[0-9]{1,2}-[0-9]{1,2}),(([0-9]{4}-[0-9]{1,2}-[0-9]{1,2})|today|now|yesterday)$/D', trim($dateString), $regs);
  203. if (empty($matched)) {
  204. return false;
  205. }
  206. return $regs;
  207. }
  208. protected $endDate = null;
  209. /**
  210. * Returns the end date of the period.
  211. *
  212. * @return null|Date
  213. */
  214. public function getDateEnd()
  215. {
  216. if (!is_null($this->endDate)) {
  217. return $this->endDate;
  218. }
  219. return parent::getDateEnd();
  220. }
  221. /**
  222. * Determine which kind of period is best to use.
  223. * See Range.test.php
  224. *
  225. * @param Date $startDate
  226. * @param Date $endDate
  227. */
  228. protected function processOptimalSubperiods($startDate, $endDate)
  229. {
  230. while ($startDate->isEarlier($endDate)
  231. || $startDate == $endDate) {
  232. $endOfPeriod = null;
  233. $month = new Month($startDate);
  234. $endOfMonth = $month->getDateEnd();
  235. $startOfMonth = $month->getDateStart();
  236. $year = new Year($startDate);
  237. $endOfYear = $year->getDateEnd();
  238. $startOfYear = $year->getDateStart();
  239. if ($startDate == $startOfYear
  240. && ($endOfYear->isEarlier($endDate)
  241. || $endOfYear == $endDate
  242. || $endOfYear->isLater($this->today)
  243. )
  244. // We don't use the year if
  245. // the end day is in this year, is before today, and year not finished
  246. && !($endDate->isEarlier($this->today)
  247. && $this->today->toString('Y') == $endOfYear->toString('Y')
  248. && $this->today->compareYear($endOfYear) == 0)
  249. ) {
  250. $this->addSubperiod($year);
  251. $endOfPeriod = $endOfYear;
  252. } else if ($startDate == $startOfMonth
  253. && ($endOfMonth->isEarlier($endDate)
  254. || $endOfMonth == $endDate
  255. || $endOfMonth->isLater($this->today)
  256. )
  257. // We don't use the month if
  258. // the end day is in this month, is before today, and month not finished
  259. && !($endDate->isEarlier($this->today)
  260. && $this->today->toString('Y') == $endOfMonth->toString('Y')
  261. && $this->today->compareMonth($endOfMonth) == 0)
  262. ) {
  263. $this->addSubperiod($month);
  264. $endOfPeriod = $endOfMonth;
  265. } else {
  266. // From start date,
  267. // Process end of week
  268. $week = new Week($startDate);
  269. $startOfWeek = $week->getDateStart();
  270. $endOfWeek = $week->getDateEnd();
  271. $firstDayNextMonth = $startDate->addPeriod(2, 'month')->setDay(1);
  272. $useMonthsNextIteration = $firstDayNextMonth->isEarlier($endDate);
  273. if ($useMonthsNextIteration
  274. && $endOfWeek->isLater($endOfMonth)
  275. ) {
  276. $this->fillArraySubPeriods($startDate, $endOfMonth, 'day');
  277. $endOfPeriod = $endOfMonth;
  278. } // If end of this week is later than end date, we use days
  279. elseif ($this->isEndOfWeekLaterThanEndDate($endDate, $endOfWeek) &&
  280. ($endOfWeek->isEarlier($this->today)
  281. || $startOfWeek->toString() != $startDate->toString()
  282. || $endDate->isEarlier($this->today))
  283. ) {
  284. $this->fillArraySubPeriods($startDate, $endDate, 'day');
  285. break 1;
  286. } elseif ($startOfWeek->isEarlier($startDate)
  287. && $endOfWeek->isEarlier($this->today)
  288. ) {
  289. $this->fillArraySubPeriods($startDate, $endOfWeek, 'day');
  290. $endOfPeriod = $endOfWeek;
  291. } else {
  292. $this->addSubperiod($week);
  293. $endOfPeriod = $endOfWeek;
  294. }
  295. }
  296. $startDate = $endOfPeriod->addDay(1);
  297. }
  298. }
  299. /**
  300. * Adds new subperiods
  301. *
  302. * @param Date $startDate
  303. * @param Date $endDate
  304. * @param string $period
  305. */
  306. protected function fillArraySubPeriods($startDate, $endDate, $period)
  307. {
  308. $arrayPeriods = array();
  309. $endSubperiod = Period\Factory::build($period, $endDate);
  310. $arrayPeriods[] = $endSubperiod;
  311. // set end date to start of end period since we're comparing against start date.
  312. $endDate = $endSubperiod->getDateStart();
  313. while ($endDate->isLater($startDate)) {
  314. $endDate = $endDate->addPeriod(-1, $period);
  315. $subPeriod = Period\Factory::build($period, $endDate);
  316. $arrayPeriods[] = $subPeriod;
  317. }
  318. $arrayPeriods = array_reverse($arrayPeriods);
  319. foreach ($arrayPeriods as $period) {
  320. $this->addSubperiod($period);
  321. }
  322. }
  323. /**
  324. * Returns the date that is one period before the supplied date.
  325. *
  326. * @param bool|string $date The date to get the last date of.
  327. * @param bool|string $period The period to use (either 'day', 'week', 'month', 'year');
  328. *
  329. * @return array An array with two elements, a string for the date before $date and
  330. * a Period instance for the period before $date.
  331. * @api
  332. */
  333. public static function getLastDate($date = false, $period = false)
  334. {
  335. return self::getDateXPeriodsAgo(1, $date, $period);
  336. }
  337. /**
  338. * Returns the date that is X periods before the supplied date.
  339. *
  340. * @param bool|string $date The date to get the last date of.
  341. * @param bool|string $period The period to use (either 'day', 'week', 'month', 'year');
  342. * @param int $subXPeriods How many periods in the past the date should be, for instance 1 or 7.
  343. * If sub period is 365 days and the current year is a leap year we assume you want to get the
  344. * day one year ago and change the value to 366 days therefore.
  345. *
  346. * @return array An array with two elements, a string for the date before $date and
  347. * a Period instance for the period before $date.
  348. * @api
  349. */
  350. public static function getDateXPeriodsAgo($subXPeriods, $date = false, $period = false)
  351. {
  352. if ($date === false) {
  353. $date = Common::getRequestVar('date');
  354. }
  355. if ($period === false) {
  356. $period = Common::getRequestVar('period');
  357. }
  358. if (365 == $subXPeriods && 'day' == $period && Date::today()->isLeapYear()) {
  359. $subXPeriods = 366;
  360. }
  361. // can't get the last date for range periods & dates that use lastN/previousN
  362. $strLastDate = false;
  363. $lastPeriod = false;
  364. if ($period != 'range' && !preg_match('/(last|previous)([0-9]*)/', $date, $regs)) {
  365. if (strpos($date, ',')) // date in the form of 2011-01-01,2011-02-02
  366. {
  367. $rangePeriod = new Range($period, $date);
  368. $lastStartDate = $rangePeriod->getDateStart()->subPeriod($subXPeriods, $period);
  369. $lastEndDate = $rangePeriod->getDateEnd()->subPeriod($subXPeriods, $period);
  370. $strLastDate = "$lastStartDate,$lastEndDate";
  371. } else {
  372. $lastPeriod = Date::factory($date)->subPeriod($subXPeriods, $period);
  373. $strLastDate = $lastPeriod->toString();
  374. }
  375. }
  376. return array($strLastDate, $lastPeriod);
  377. }
  378. /**
  379. * Returns a date range string given a period type, end date and number of periods
  380. * the range spans over.
  381. *
  382. * @param string $period The sub period type, `'day'`, `'week'`, `'month'` and `'year'`.
  383. * @param int $lastN The number of periods of type `$period` that the result range should
  384. * span.
  385. * @param string $endDate The desired end date of the range.
  386. * @param Site $site The site whose timezone should be used.
  387. * @return string The date range string, eg, `'2012-01-02,2013-01-02'`.
  388. * @api
  389. */
  390. public static function getRelativeToEndDate($period, $lastN, $endDate, $site)
  391. {
  392. $last30Relative = new Range($period, $lastN, $site->getTimezone());
  393. $last30Relative->setDefaultEndDate(Date::factory($endDate));
  394. $date = $last30Relative->getDateStart()->toString() . "," . $last30Relative->getDateEnd()->toString();
  395. return $date;
  396. }
  397. private function isEndOfWeekLaterThanEndDate($endDate, $endOfWeek)
  398. {
  399. $isEndOfWeekLaterThanEndDate = $endOfWeek->isLater($endDate);
  400. $isEndDateAlsoEndOfWeek = ($endOfWeek->toString() == $endDate->toString());
  401. $isEndOfWeekLaterThanEndDate = ($isEndOfWeekLaterThanEndDate
  402. || ($isEndDateAlsoEndOfWeek
  403. && $endDate->isLater($this->today)));
  404. return $isEndOfWeekLaterThanEndDate;
  405. }
  406. }