MDL-57791 analytics: Always absolute full class names
[moodle.git] / analytics / classes / calculable.php
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/>.
17 /**
18  * Calculable dataset items abstract class.
19  *
20  * @package   core_analytics
21  * @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
22  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23  */
25 namespace core_analytics;
27 defined('MOODLE_INTERNAL') || die();
29 /**
30  * Calculable dataset items abstract class.
31  *
32  * @package   core_analytics
33  * @copyright 2016 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 calculable {
38     /**
39      * Neutral calculation outcome.
40      */
41     const OUTCOME_NEUTRAL = 0;
43     /**
44      * Very positive calculation outcome.
45      */
46     const OUTCOME_VERY_POSITIVE = 1;
48     /**
49      * Positive calculation outcome.
50      */
51     const OUTCOME_OK = 2;
53     /**
54      * Negative calculation outcome.
55      */
56     const OUTCOME_NEGATIVE = 3;
58     /**
59      * Very negative calculation outcome.
60      */
61     const OUTCOME_VERY_NEGATIVE = 4;
63     /**
64      * @var array[]
65      */
66     protected $sampledata = array();
68     /**
69      * Returns a visible name for the indicator.
70      *
71      * Used as column identificator.
72      *
73      * Defaults to the indicator class name.
74      *
75      * @return string
76      */
77     public static function get_name() {
78         return '\\' . get_called_class();
79     }
81     /**
82      * The class id is the calculable class full qualified class name.
83      *
84      * @return string
85      */
86     public function get_id() {
87         return '\\' . get_class($this);
88     }
90     /**
91      * add_sample_data
92      *
93      * @param array $data
94      * @return void
95      */
96     public function add_sample_data($data) {
97         $this->sampledata = $this->array_merge_recursive_keep_keys($this->sampledata, $data);
98     }
100     /**
101      * clear_sample_data
102      *
103      * @return void
104      */
105     public function clear_sample_data() {
106         $this->sampledata = array();
107     }
109     /**
110      * Returns the visible value of the calculated value.
111      *
112      * @param float $value
113      * @param string|false $subtype
114      * @return string
115      */
116     public function get_display_value($value, $subtype = false) {
117         return $value;
118     }
120     /**
121      * Returns how good the calculated value is.
122      *
123      * Use one of \core_analytics\calculable::OUTCOME_* values.
124      *
125      * @param float $value
126      * @param string|false $subtype
127      * @return int
128      */
129     public function get_calculation_outcome($value, $subtype = false) {
130         throw new \coding_exception('Please overwrite get_calculation_outcome method');
131     }
133     /**
134      * Retrieve the specified element associated to $sampleid.
135      *
136      * @param string $elementname
137      * @param int $sampleid
138      * @return \stdClass|false An \stdClass object or false if it can not be found.
139      */
140     protected function retrieve($elementname, $sampleid) {
141         if (empty($this->sampledata[$sampleid]) || empty($this->sampledata[$sampleid][$elementname])) {
142             // We don't throw an exception because indicators should be able to
143             // try multiple tables until they find something they can use.
144             return false;
145         }
146         return $this->sampledata[$sampleid][$elementname];
147     }
149     /**
150      * Returns the number of weeks a time range contains.
151      *
152      * Useful for calculations that depend on the time range duration. Note that it returns
153      * a float, rounding the float may lead to inaccurate results.
154      *
155      * @param int $starttime
156      * @param int $endtime
157      * @return float
158      */
159     protected function get_time_range_weeks_number($starttime, $endtime) {
160         if ($endtime <= $starttime) {
161             throw new \coding_exception('End time timestamp should be greater than start time.');
162         }
164         $starttimedt = new \DateTime();
165         $starttimedt->setTimestamp($starttime);
166         $starttimedt->setTimezone(\DateTimeZone::UTC);
167         $endtimedt = new \DateTime();
168         $endtimedt->setTimestamp($endtime);
169         $endtimedt->setTimezone(\DateTimeZone::UTC);
171         $diff = $endtimedt->getTimestamp() - $starttimedt->getTimestamp();
172         return $diff / WEEKSECS;
173     }
175     /**
176      * Limits the calculated value to the minimum and maximum values.
177      *
178      * @param float $calculatedvalue
179      * @return float|null
180      */
181     protected function limit_value($calculatedvalue) {
182         return max(min($calculatedvalue, static::get_max_value()), static::get_min_value());
183     }
185     /**
186      * Classifies the provided value into the provided range according to the ranges predicates.
187      *
188      * Use:
189      * - eq as 'equal'
190      * - ne as 'not equal'
191      * - lt as 'lower than'
192      * - le as 'lower or equal than'
193      * - gt as 'greater than'
194      * - ge as 'greater or equal than'
195      *
196      * @throws \coding_exception
197      * @param int|float $value
198      * @param array $ranges e.g. [ ['lt', 20], ['ge', 20] ]
199      * @return float
200      */
201     protected function classify_value($value, $ranges) {
203         // To automatically return calculated values from min to max values.
204         $rangeweight = (static::get_max_value() - static::get_min_value()) / (count($ranges) - 1);
206         foreach ($ranges as $key => $range) {
208             $match = false;
210             if (count($range) != 2) {
211                 throw \coding_exception('classify_value() $ranges array param should contain 2 items, the predicate ' .
212                     'e.g. greater (gt), lower or equal (le)... and the value.');
213             }
215             list($predicate, $rangevalue) = $range;
217             switch ($predicate) {
218                 case 'eq':
219                     if ($value == $rangevalue) {
220                         $match = true;
221                     }
222                     break;
223                 case 'ne':
224                     if ($value != $rangevalue) {
225                         $match = true;
226                     }
227                     break;
228                 case 'lt':
229                     if ($value < $rangevalue) {
230                         $match = true;
231                     }
232                     break;
233                 case 'le':
234                     if ($value <= $rangevalue) {
235                         $match = true;
236                     }
237                     break;
238                 case 'gt':
239                     if ($value > $rangevalue) {
240                         $match = true;
241                     }
242                     break;
243                 case 'ge':
244                     if ($value >= $rangevalue) {
245                         $match = true;
246                     }
247                     break;
248                 default:
249                     throw new \coding_exception('Unrecognised predicate ' . $predicate . '. Please use eq, ne, lt, le, ge or gt.');
250             }
252             // Calculate and return a linear calculated value for the provided value.
253             if ($match) {
254                 return round(static::get_min_value() + ($rangeweight * $key), 2);
255             }
256         }
258         throw new \coding_exception('The provided value "' . $value . '" can not be fit into any of the provided ranges, you ' .
259             'should provide ranges for all possible values.');
260     }
262     /**
263      * Merges arrays recursively keeping the same keys the original arrays have.
264      *
265      * @link http://php.net/manual/es/function.array-merge-recursive.php#114818
266      * @return array
267      */
268     private function array_merge_recursive_keep_keys() {
269         $arrays = func_get_args();
270         $base = array_shift($arrays);
272         foreach ($arrays as $array) {
273             reset($base);
274             while (list($key, $value) = each($array)) {
275                 if (is_array($value) && !empty($base[$key]) && is_array($base[$key])) {
276                     $base[$key] = $this->array_merge_recursive_keep_keys($base[$key], $value);
277                 } else {
278                     if (isset($base[$key]) && is_int($key)) {
279                         $key++;
280                     }
281                     $base[$key] = $value;
282                 }
283             }
284         }
286         return $base;
287     }