Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.0.x will end 8 May 2023 (12 months).
  • Bug fixes for security issues in 4.0.x will end 13 November 2023 (18 months).
  • PHP version: minimum PHP 7.3.0 Note: the minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is also supported.
   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  /**
  18   * Time splitting method that generates predictions regularly.
  19   *
  20   * @package   core_analytics
  21   * @copyright 2019 David Monllao {@link http://www.davidmonllao.com}
  22   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23   */
  24  
  25  namespace core_analytics\local\time_splitting;
  26  
  27  defined('MOODLE_INTERNAL') || die();
  28  
  29  /**
  30   * Time splitting method that generates predictions periodically.
  31   *
  32   * @package   core_analytics
  33   * @copyright 2019 David Monllao {@link http://www.davidmonllao.com}
  34   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  35   */
  36  abstract class periodic extends base {
  37  
  38      /**
  39       * The periodicity of the predictions / training data generation.
  40       *
  41       * @return \DateInterval
  42       */
  43      abstract protected function periodicity();
  44  
  45      /**
  46       * Gets the next range with start on the provided time.
  47       *
  48       * @param  \DateTimeImmutable $time
  49       * @return array
  50       */
  51      abstract protected function get_next_range(\DateTimeImmutable $time);
  52  
  53      /**
  54       * Get the start of the first time range.
  55       *
  56       * @return int A timestamp.
  57       */
  58      abstract protected function get_first_start();
  59  
  60      /**
  61       * Returns whether the analysable can be processed by this time splitting method or not.
  62       *
  63       * @param \core_analytics\analysable $analysable
  64       * @return bool
  65       */
  66      public function is_valid_analysable(\core_analytics\analysable $analysable) {
  67          if (!$analysable->get_start()) {
  68              return false;
  69          }
  70          return true;
  71      }
  72  
  73      /**
  74       * define_ranges
  75       *
  76       * @return array
  77       */
  78      protected function define_ranges() {
  79  
  80          $periodicity = $this->periodicity();
  81  
  82          if ($this->analysable->get_end()) {
  83              $end = (new \DateTimeImmutable())->setTimestamp($this->analysable->get_end());
  84          }
  85          $nexttime = (new \DateTimeImmutable())->setTimestamp($this->get_first_start());
  86  
  87          $now = new \DateTimeImmutable('now', \core_date::get_server_timezone_object());
  88  
  89          $range = $this->get_next_range($nexttime);
  90          if (!$range) {
  91              $nexttime = $nexttime->add($periodicity);
  92              $range = $this->get_next_range($nexttime);
  93  
  94              if (!$range) {
  95                  throw new \coding_exception('The get_next_range implementation is broken. The difference between two consecutive
  96                      ranges can not be more than the periodicity.');
  97              }
  98          }
  99  
 100          $ranges = [];
 101          $endreached = false;
 102          while (($this->ready_to_predict($range) || $this->ready_to_train($range)) && !$endreached) {
 103              $ranges[] = $range;
 104              $nexttime = $nexttime->add($periodicity);
 105              $range = $this->get_next_range($nexttime);
 106  
 107              $endreached = (!empty($end) && $nexttime > $end);
 108          }
 109  
 110          if ($ranges && !$endreached) {
 111              // If this analysable is not finished we adjust the start and end of the last element in $ranges
 112              // so that it ends in time().The reason is that the start of these ranges is based on the analysable
 113              // start and the end is calculated based on the start. This is to prevent the same issue we had in MDL-65348.
 114              //
 115              // An example of the situation we want to avoid is:
 116              // A course started on a Monday, in 2015. It has no end date. Now the system is upgraded to Moodle 3.8, which
 117              // includes this code. This happens on Wednesday. Periodic ranges (e.g. weekly) will be calculated from a Monday
 118              // so the data provided by the time-splitting method would be from Monday to Monday, when we really want to
 119              // provide data from Wednesday to the past Wednesday.
 120              $ranges = $this->update_last_range($ranges);
 121          }
 122  
 123          return $ranges;
 124      }
 125  
 126      /**
 127       * Overwritten as all generated rows are comparable.
 128       *
 129       * @return bool
 130       */
 131      public function include_range_info_in_training_data() {
 132          return false;
 133      }
 134  
 135      /**
 136       * Overwritting as the last range may be for prediction.
 137       *
 138       * @return array
 139       */
 140      public function get_training_ranges() {
 141          // Cloning the array.
 142          $trainingranges = $this->ranges;
 143  
 144          foreach ($trainingranges as $rangeindex => $range) {
 145              if (!$this->ready_to_train($range)) {
 146                  unset($trainingranges[$rangeindex]);
 147              }
 148          }
 149  
 150          return $trainingranges;
 151      }
 152  
 153      /**
 154       * Allows child classes to update the last range provided.
 155       *
 156       * @param  array  $ranges
 157       * @return array
 158       */
 159      protected function update_last_range(array $ranges) {
 160          return $ranges;
 161      }
 162  }