defaultPredictionPredictorCalculateAverages()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
cc 1
eloc 5
c 3
b 0
f 0
nc 1
nop 1
dl 0
loc 9
rs 10
1
<?php
2
3
namespace Sunnysideup\DefaultPredict\Extension;
4
5
use SilverStripe\Core\Config\Config;
6
use SilverStripe\ORM\DataExtension;
7
use SilverStripe\ORM\DataObject;
8
9
/**
10
 * adds meta tag functionality to the Page_Controller or another DataObject.
11
 *
12
 * @property Site|DefaultPredictExtension $owner
13
 */
14
class DefaultPredictExtension extends DataExtension
15
{
16
    private static $base_default_predictor_limit = 5;
17
18
    private static $base_default_predictor_threshold = 0.5;
19
20
    /**
21
     * the greater the number, the less recency matters.
22
     * with a value of one, recency increases by 100% every step closer to the last record.
23
     * i.e the last record has five times more chance to the influencer
24
     * than the record that was created five steps ago.
25
     */
26
    private static $base_default_predictor_recency_factor = 0.5;
27
28
    private static $base_default_predict_exclude = [
29
        'ID',
30
        'Created',
31
        'LastEdited',
32
        'Version',
33
    ];
34
35
    private static $fields_that_must_be_equal = [
36
        'ClassName',
37
    ];
38
39
    public function populateDefaults()
40
    {
41
        $owner = $this->getOwner();
42
        $className = $owner->ClassName;
43
44
        // work out limit and treshold
45
        $limit = Config::inst()->get($className, 'default_predictor_limit') ?:
46
            Config::inst()->get(DefaultPredictExtension::class, 'base_default_predictor_limit');
47
48
        $threshold = Config::inst()->get($className, 'default_predictor_threshold') ?:
49
            Config::inst()->get(DefaultPredictExtension::class, 'base_default_predictor_threshold');
50
51
        $recencyFactor = Config::inst()->get($className, 'default_predictor_recency_factor') ?:
52
            Config::inst()->get(DefaultPredictExtension::class, 'base_default_predictor_recency_factor');
53
54
        $fieldsThatMustBeEqual = Config::inst()->get($className, 'fields_that_must_be_equal') ?:
55
            Config::inst()->get(DefaultPredictExtension::class, 'fields_that_must_be_equal');
56
57
        $predicts = $this->getDefaultPredictionPredictor($limit, $threshold, $recencyFactor, $fieldsThatMustBeEqual);
58
59
        // get class specific predictions
60
        if ($owner->hasMethod('getSpecificDefaultPredictions')) {
61
            $predicts = $predicts + $owner->getSpecificDefaultPredictions();
62
        }
63
64
        foreach ($predicts as $fieldName => $value) {
65
            $owner->{$fieldName} = $value;
66
        }
67
    }
68
69
    protected function getDefaultPredictionPredictor(int $limit, float $threshold, float $recencyFactor, array $fieldsThatMustBeEqual): array
70
    {
71
        $owner = $this->getOwner();
72
        $className = $owner->ClassName;
73
74
        // set return variable
75
        $predicts = [];
76
77
        // get field names
78
        $fieldNames = $this->getDefaultPredictionFieldNames($className);
79
80
        // must be equal
81
        $mustBeEqual = [];
82
        foreach($fieldsThatMustBeEqual as $fieldThatMustBeEqual) {
83
            if(isset($fieldNames[$fieldThatMustBeEqual])) {
84
                $mustBeEqual[$fieldThatMustBeEqual] = $owner->{$fieldThatMustBeEqual};
85
            }
86
        }
87
        // get last objects, based on limit;
88
        $objects = $className::get()
89
            ->sort(['ID' => 'DESC'])
90
            ->exclude(['ID' => $owner->ID])
91
            ->limit($limit)
92
        ;
93
        if(count($mustBeEqual)) {
94
            $objects = $objects->filter($mustBeEqual);
95
        }
96
        // print_r($objects->column('Purpose'));
97
        // print_r($objects->column('Title'));
98
        //store objects in memory
99
        $objectArray = [];
100
        foreach ($objects as $object) {
101
            $objectArray[] = $object;
102
        }
103
        // put the latest one last so that we can give it more weight.
104
        $objectArray = array_reverse($objectArray);
105
106
        // get the field names
107
        // print_r($fieldNames);
108
        // loop through fields
109
        foreach ($fieldNames as $fieldName) {
110
            $valueArray = [];
111
            // loop through objects
112
            $max = 0;
113
            foreach ($objectArray as $object) {
114
                $value = $object->{$fieldName};
115
                if (! $value) {
116
                    $value = '';
117
                }
118
                // give more weight to the last one used.
119
                for ($y = 0; $y <= $max; ++$y) {
120
                    $valueArray[] = $value;
121
                }
122
                $max += $recencyFactor;
123
            }
124
            // print_r($fieldName);
125
            // print_r($valueArray);
126
            if (count($valueArray)) {
127
                // work out if there is a value that comes back a lot, and, if so, add it to predicts
128
                $possibleValue = $this->defaultPredictionPredictorBestContender($valueArray, $threshold);
129
                if ($possibleValue) {
130
                    $predicts[$fieldName] = $possibleValue;
131
                }
132
            }
133
        }
134
        // print_r($predicts);
135
        return $predicts;
136
    }
137
138
    protected function getDefaultPredictionFieldNames(string $className): array
139
    {
140
        $excludeBase = (array) Config::inst()->get(DefaultPredictExtension::class, 'base_default_predict_exclude');
141
        $excludeMore = (array) Config::inst()->get($className, 'default_predict_exclude');
142
        $exclude = array_merge($excludeBase, $excludeMore);
143
144
        // get db and has_one fields
145
        $fieldsDb = array_keys(Config::inst()->get($className, 'db'));
146
        $fieldsHasOne = array_keys(Config::inst()->get($className, 'has_one'));
147
148
        // add ID part to has_one fields
149
        array_walk($fieldsHasOne, function (&$value, $key) {
0 ignored issues
show
Unused Code introduced by
The parameter $key is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

149
        array_walk($fieldsHasOne, function (&$value, /** @scrutinizer ignore-unused */ $key) {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
150
            $value .= 'ID';
151
        });
152
153
        // return merge of arrays minus the exclude ones.
154
        return array_diff(
155
            array_merge($fieldsDb, $fieldsHasOne),
156
            $exclude
157
        );
158
    }
159
160
    protected function defaultPredictionPredictorBestContender(array $valueArray, float $threshold)
161
    {
162
        $averages = $this->defaultPredictionPredictorCalculateAverages($valueArray);
163
        // sort by the most common one
164
        arsort($averages);
165
        foreach ($averages as $value => $percentage) {
166
            if ($percentage > $threshold) {
167
                return $value;
168
            }
169
170
            return null;
171
        }
172
173
        return null;
174
    }
175
176
    protected function defaultPredictionPredictorCalculateAverages(array $array)
177
    {
178
        $num = count($array); // provides the value for num
179
180
        return array_map(
181
            function ($val) use ($num) {
182
                return $val / $num;
183
            },
184
            array_count_values($array) // provides the value for $val
185
        );
186
    }
187
}
188