1<?php 2/** 3 * Matomo - free/libre analytics platform 4 * 5 * @link https://matomo.org 6 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later 7 * 8 */ 9namespace Piwik; 10 11use Piwik\Container\StaticContainer; 12use Piwik\Period\Factory; 13use Piwik\Period\Range; 14use Piwik\Translation\Translator; 15 16/** 17 * Date range representation. 18 * 19 * Piwik allows users to view aggregated statistics for single days and for date 20 * ranges consisting of several days. When requesting data, a **date** string and 21 * a **period** string must be used to specify the date range that the data regards. 22 * This is the class Piwik uses to represent and manipulate those date ranges. 23 * 24 * There are five types of periods in Piwik: day, week, month, year and range, 25 * where **range** is any date range. The reason the other periods exist instead 26 * of just **range** is that Piwik will pre-archive reports for days, weeks, months 27 * and years, while every custom date range is archived on-demand. 28 * 29 * @api 30 */ 31abstract class Period 32{ 33 /** 34 * Array of subperiods 35 * @var Period[] 36 */ 37 protected $subperiods = array(); 38 protected $subperiodsProcessed = false; 39 40 /** 41 * @var string 42 */ 43 protected $label = null; 44 45 /** 46 * @var Date 47 */ 48 protected $date = null; 49 50 /** 51 * @var Translator 52 */ 53 protected $translator; 54 55 /** 56 * Constructor. 57 * 58 * @param Date $date 59 * @ignore 60 */ 61 public function __construct(Date $date) 62 { 63 $this->date = clone $date; 64 65 $this->translator = StaticContainer::get('Piwik\Translation\Translator'); 66 } 67 68 public function __sleep() 69 { 70 return [ 71 'date', 72 ]; 73 } 74 75 public function __wakeup() 76 { 77 $this->translator = StaticContainer::get('Piwik\Translation\Translator'); 78 } 79 80 /** 81 * Returns true if `$dateString` and `$period` represent multiple periods. 82 * 83 * Will return true for date/period combinations where date references multiple 84 * dates and period is not `'range'`. For example, will return true for: 85 * 86 * - **date** = `2012-01-01,2012-02-01` and **period** = `'day'` 87 * - **date** = `2012-01-01,2012-02-01` and **period** = `'week'` 88 * - **date** = `last7` and **period** = `'month'` 89 * 90 * etc. 91 * 92 * @static 93 * @param $dateString string The **date** query parameter value. 94 * @param $period string The **period** query parameter value. 95 * @return boolean 96 */ 97 public static function isMultiplePeriod($dateString, $period) 98 { 99 return is_string($dateString) 100 && (preg_match('/^(last|previous){1}([0-9]*)$/D', $dateString, $regs) 101 || Range::parseDateRange($dateString)) 102 && $period != 'range'; 103 } 104 105 /** 106 * Checks the given date format whether it is a correct date format and if not, throw an exception. 107 * 108 * For valid date formats have a look at the {@link \Piwik\Date::factory()} method and 109 * {@link isMultiplePeriod()} method. 110 * 111 * @param string $dateString 112 * @throws \Exception If `$dateString` is in an invalid format or if the time is before 113 * Tue, 06 Aug 1991. 114 */ 115 public static function checkDateFormat($dateString) 116 { 117 if (self::isMultiplePeriod($dateString, 'day')) { 118 return; 119 } 120 121 Date::factory($dateString); 122 } 123 124 /** 125 * Returns the first day of the period. 126 * 127 * @return Date 128 */ 129 public function getDateStart() 130 { 131 $this->generate(); 132 133 if (count($this->subperiods) == 0) { 134 return $this->getDate(); 135 } 136 137 $periods = $this->getSubperiods(); 138 139 /** @var $currentPeriod Period */ 140 $currentPeriod = $periods[0]; 141 while ($currentPeriod->getNumberOfSubperiods() > 0) { 142 $periods = $currentPeriod->getSubperiods(); 143 $currentPeriod = $periods[0]; 144 } 145 146 return $currentPeriod->getDate(); 147 } 148 149 /** 150 * Returns the start date & time of this period. 151 * 152 * @return Date 153 */ 154 public function getDateTimeStart() 155 { 156 return $this->getDateStart()->getStartOfDay(); 157 } 158 159 /** 160 * Returns the end date & time of this period. 161 * 162 * @return Date 163 */ 164 public function getDateTimeEnd() 165 { 166 return $this->getDateEnd()->getEndOfDay(); 167 } 168 169 /** 170 * Returns the last day of the period. 171 * 172 * @return Date 173 */ 174 public function getDateEnd() 175 { 176 $this->generate(); 177 178 if (count($this->subperiods) == 0) { 179 return $this->getDate(); 180 } 181 182 $periods = $this->getSubperiods(); 183 184 /** @var $currentPeriod Period */ 185 $currentPeriod = $periods[count($periods) - 1]; 186 while ($currentPeriod->getNumberOfSubperiods() > 0) { 187 $periods = $currentPeriod->getSubperiods(); 188 $currentPeriod = $periods[count($periods) - 1]; 189 } 190 191 return $currentPeriod->getDate(); 192 } 193 194 /** 195 * Returns the period ID. 196 * 197 * @return int A unique integer for this type of period. 198 */ 199 public function getId() 200 { 201 return Piwik::$idPeriods[$this->getLabel()]; 202 } 203 204 /** 205 * Returns the label for the current period. 206 * 207 * @return string `"day"`, `"week"`, `"month"`, `"year"`, `"range"` 208 */ 209 public function getLabel() 210 { 211 return $this->label; 212 } 213 214 /** 215 * @return Date 216 */ 217 protected function getDate() 218 { 219 return $this->date; 220 } 221 222 protected function generate() 223 { 224 $this->subperiodsProcessed = true; 225 } 226 227 /** 228 * Returns the number of available subperiods. 229 * 230 * @return int 231 */ 232 public function getNumberOfSubperiods() 233 { 234 $this->generate(); 235 return count($this->subperiods); 236 } 237 238 /** 239 * Returns the set of Period instances that together make up this period. For a year, 240 * this would be 12 months. For a month this would be 28-31 days. Etc. 241 * 242 * @return Period[] 243 */ 244 public function getSubperiods() 245 { 246 $this->generate(); 247 return $this->subperiods; 248 } 249 250 /** 251 * Returns whether the date `$date` is within the current period or not. 252 * 253 * Note: the time component of the period's dates and `$date` is ignored. 254 * 255 * @param Date $today 256 * @return bool 257 */ 258 public function isDateInPeriod(Date $date) 259 { 260 $ts = $date->getStartOfDay()->getTimestamp(); 261 return $ts >= $this->getDateStart()->getStartOfDay()->getTimestamp() 262 && $ts < $this->getDateEnd()->addDay(1)->getStartOfDay()->getTimestamp(); 263 } 264 265 /** 266 * Returns whether the given period date range intersects with this one. 267 * 268 * @param Period $other 269 * @return bool 270 */ 271 public function isPeriodIntersectingWith(Period $other) 272 { 273 return !($this->getDateEnd()->getTimestamp() < $other->getDateStart()->getTimestamp() 274 || $this->getDateStart()->getTimestamp() > $other->getDateEnd()->getTimestamp()); 275 } 276 277 /** 278 * Returns the start day and day after the end day for this period in the given timezone. 279 * 280 * @param Date[] $timezone 281 */ 282 public function getBoundsInTimezone(string $timezone) 283 { 284 $date1 = $this->getDateTimeStart()->setTimezone($timezone); 285 $date2 = $this->getDateTimeEnd()->setTimezone($timezone); 286 287 return [$date1, $date2]; 288 } 289 290 /** 291 * Add a date to the period. 292 * 293 * Protected because adding periods after initialization is not supported. 294 * 295 * @param \Piwik\Period $period Valid Period object 296 * @ignore 297 */ 298 protected function addSubperiod($period) 299 { 300 $this->subperiods[] = $period; 301 } 302 303 /** 304 * Returns a list of strings representing the current period. 305 * 306 * @param string $format The format of each individual day. 307 * @return array|string An array of string dates that this period consists of. 308 */ 309 public function toString($format = "Y-m-d") 310 { 311 $this->generate(); 312 313 $dateString = array(); 314 foreach ($this->subperiods as $period) { 315 $childPeriodStr = $period->toString($format); 316 if (is_array($childPeriodStr)) { 317 $childPeriodStr = implode(",", $childPeriodStr); 318 } 319 320 $dateString[] = $childPeriodStr; 321 } 322 323 return $dateString; 324 } 325 326 /** 327 * See {@link toString()}. 328 * 329 * @return string 330 */ 331 public function __toString() 332 { 333 return implode(",", $this->toString()); 334 } 335 336 /** 337 * Returns a pretty string describing this period. 338 * 339 * @return string 340 */ 341 abstract public function getPrettyString(); 342 343 /** 344 * Returns a short string description of this period that is localized with the currently used 345 * language. 346 * 347 * @return string 348 */ 349 abstract public function getLocalizedShortString(); 350 351 /** 352 * Returns a long string description of this period that is localized with the currently used 353 * language. 354 * 355 * @return string 356 */ 357 abstract public function getLocalizedLongString(); 358 359 /** 360 * Returns the label of the period type that is one size smaller than this one, or null if 361 * it's the smallest. 362 * 363 * Range periods and other such 'period collections' are not considered as separate from 364 * the value type of the collection. So a range period will return the result of the 365 * subperiod's `getImmediateChildPeriodLabel()` method. 366 * 367 * @ignore 368 * @return string|null 369 */ 370 abstract public function getImmediateChildPeriodLabel(); 371 372 /** 373 * Returns the label of the period type that is one size bigger than this one, or null 374 * if it's the biggest. 375 * 376 * Range periods and other such 'period collections' are not considered as separate from 377 * the value type of the collection. So a range period will return the result of the 378 * subperiod's `getParentPeriodLabel()` method. 379 * 380 * @ignore 381 */ 382 abstract public function getParentPeriodLabel(); 383 384 /** 385 * Returns the date range string comprising two dates 386 * 387 * @return string eg, `'2012-01-01,2012-01-31'`. 388 */ 389 public function getRangeString() 390 { 391 $dateStart = $this->getDateStart(); 392 $dateEnd = $this->getDateEnd(); 393 394 return $dateStart->toString("Y-m-d") . "," . $dateEnd->toString("Y-m-d"); 395 } 396 397 /** 398 * @param string $format 399 * 400 * @return mixed 401 */ 402 protected function getTranslatedRange($format) 403 { 404 $dateStart = $this->getDateStart(); 405 $dateEnd = $this->getDateEnd(); 406 list($formatStart, $formatEnd) = $this->explodeFormat($format); 407 408 $string = $dateStart->getLocalized($formatStart); 409 $string .= $dateEnd->getLocalized($formatEnd, false); 410 411 return $string; 412 } 413 414 /** 415 * Explodes the given format into two pieces. One that can be user for start date and the other for end date 416 * 417 * @param $format 418 * @return array 419 */ 420 protected function explodeFormat($format) 421 { 422 $intervalTokens = array( 423 array('d', 'E', 'C'), 424 array('M', 'L'), 425 array('y') 426 ); 427 428 $offset = strlen($format); 429 // replace string literals encapsulated by ' with same country of * 430 $cleanedFormat = preg_replace_callback('/(\'[^\']+\')/', array($this, 'replaceWithStars'), $format); 431 432 // search for first duplicate date field 433 foreach ($intervalTokens AS $tokens) { 434 if (preg_match_all('/[' . implode('|', $tokens) . ']+/', $cleanedFormat, $matches, PREG_OFFSET_CAPTURE) && 435 count($matches[0]) > 1 && $offset > $matches[0][1][1] 436 ) { 437 $offset = $matches[0][1][1]; 438 } 439 } 440 441 return array(substr($format, 0, $offset), substr($format, $offset)); 442 } 443 444 private function replaceWithStars($matches) 445 { 446 return str_repeat("*", strlen($matches[0])); 447 } 448 449 protected function getRangeFormat($short = false) 450 { 451 $maxDifference = 'D'; 452 if ($this->getDateStart()->toString('y') != $this->getDateEnd()->toString('y')) { 453 $maxDifference = 'Y'; 454 } elseif ($this->getDateStart()->toString('m') != $this->getDateEnd()->toString('m')) { 455 $maxDifference = 'M'; 456 } 457 458 $dateTimeFormatProvider = StaticContainer::get('Piwik\Intl\Data\Provider\DateTimeFormatProvider'); 459 460 return $dateTimeFormatProvider->getRangeFormatPattern($short, $maxDifference); 461 } 462 463 /** 464 * Returns all child periods that exist within this periods entire date range. Cascades 465 * downwards over all period types that are smaller than this one. For example, month periods 466 * will cascade to week and day periods and year periods will cascade to month, week and day 467 * periods. 468 * 469 * The method will not return periods that are outside the range of this period. 470 * 471 * @return Period[] 472 * @ignore 473 */ 474 public function getAllOverlappingChildPeriods() 475 { 476 return $this->getAllOverlappingChildPeriodsInRange($this->getDateStart(), $this->getDateEnd()); 477 } 478 479 private function getAllOverlappingChildPeriodsInRange(Date $dateStart, Date $dateEnd) 480 { 481 $result = array(); 482 483 $childPeriodType = $this->getImmediateChildPeriodLabel(); 484 if (empty($childPeriodType)) { 485 return $result; 486 } 487 488 $childPeriods = Factory::build($childPeriodType, $dateStart->toString() . ',' . $dateEnd->toString()); 489 return array_merge($childPeriods->getSubperiods(), $childPeriods->getAllOverlappingChildPeriodsInRange($dateStart, $dateEnd)); 490 } 491} 492