Translate extension for MediaWiki
 
Loading...
Searching...
No Matches
TranslationStatsDataProvider.php
1<?php
2declare( strict_types = 1 );
3
4namespace MediaWiki\Extension\Translate\Statistics;
5
6use Language;
7use MediaWiki\Config\ServiceOptions;
10use Wikimedia\ObjectFactory\ObjectFactory;
11use const TS_MW;
12
20 public const CONSTRUCTOR_OPTIONS = [
21 'TranslateStatsProviders'
22 ];
23
25 private $objectFactory;
27 private $options;
28
29 public function __construct( ServiceOptions $options, ObjectFactory $objectFactory ) {
30 $this->options = $options;
31 $this->objectFactory = $objectFactory;
32 }
33
34 private function getGraphSpecifications(): array {
35 return array_filter( $this->options->get( 'TranslateStatsProviders' ) );
36 }
37
38 public function getGraphTypes(): array {
39 return array_keys( $this->getGraphSpecifications() );
40 }
41
48 public function getGraphData( TranslationStatsGraphOptions $opts, Language $language ): array {
49 $dbr = wfGetDB( DB_REPLICA );
50
51 $so = $this->getStatsProvider( $opts->getValue( 'count' ), $opts );
52
53 $fixedStart = $opts->getValue( 'start' ) !== '';
54
55 $now = time();
56 $period = 3600 * 24 * $opts->getValue( 'days' );
57
58 if ( $fixedStart ) {
59 $cutoff = (int)wfTimestamp( TS_UNIX, $opts->getValue( 'start' ) );
60 } else {
61 $cutoff = $now - $period;
62 }
63 $cutoff = self::roundTimestampToCutoff( $opts->getValue( 'scale' ), $cutoff, 'earlier' );
64
65 $start = $cutoff;
66
67 if ( $fixedStart ) {
68 $end = self::roundTimestampToCutoff( $opts->getValue( 'scale' ), $start + $period, 'later' ) - 1;
69 } else {
70 $end = null;
71 }
72
73 $tables = [];
74 $fields = [];
75 $conds = [];
76 $type = __METHOD__;
77 $options = [];
78 $joins = [];
79
80 $so->preQuery( $tables, $fields, $conds, $type, $options, $joins, $start, $end );
81 $res = $dbr->select( $tables, $fields, $conds, $type, $options, $joins );
82 wfDebug( __METHOD__ . "-queryend\n" );
83
84 // Start processing the data
85 $dateFormat = $so->getDateFormat();
86 $increment = self::getIncrement( $opts->getValue( 'scale' ) );
87
88 $labels = $so->labels();
89 $keys = array_keys( $labels );
90 $values = array_pad( [], count( $labels ), 0 );
91 $defaults = array_combine( $keys, $values );
92
93 $data = [];
94 // Allow 10 seconds in the future for processing time
95 $lastValue = $end ?? $now + 10;
96 while ( $cutoff <= $lastValue ) {
97 $date = $language->sprintfDate( $dateFormat, wfTimestamp( TS_MW, $cutoff ) );
98 $cutoff += $increment;
99 $data[$date] = $defaults;
100 }
101 // Ensure $lastValue is within range, in case the loop above jumped over it
102 $data[$language->sprintfDate( $dateFormat, wfTimestamp( TS_MW, $lastValue ) )] = $defaults;
103
104 // Processing
105 $labelToIndex = array_flip( $labels );
106
107 foreach ( $res as $row ) {
108 $indexLabels = $so->indexOf( $row );
109 if ( $indexLabels === false ) {
110 continue;
111 }
112
113 foreach ( $indexLabels as $i ) {
114 if ( !isset( $labelToIndex[$i] ) ) {
115 continue;
116 }
117 $date = $language->sprintfDate( $dateFormat, $so->getTimestamp( $row ) );
118 // Ignore values outside range
119 if ( !isset( $data[$date] ) ) {
120 continue;
121 }
122
123 $data[$date][$labelToIndex[$i]]++;
124 }
125 }
126
127 // Don't display dummy label
128 if ( count( $labels ) === 1 && $labels[0] === 'all' ) {
129 $labels = [];
130 }
131
132 foreach ( $labels as &$label ) {
133 if ( strpos( $label, '@' ) === false ) {
134 continue;
135 }
136 [ $groupId, $code ] = explode( '@', $label, 2 );
137 if ( $code && $groupId ) {
138 $code = TranslateUtils::getLanguageName( $code, $language->getCode() ) . " ($code)";
139 $group = MessageGroups::getGroup( $groupId );
140 $group = $group ? $group->getLabel() : $groupId;
141 $label = "$group @ $code";
142 } elseif ( $code ) {
143 $label = TranslateUtils::getLanguageName( $code, $language->getCode() ) . " ($code)";
144 } elseif ( $groupId ) {
145 $group = MessageGroups::getGroup( $groupId );
146 $label = $group ? $group->getLabel() : $groupId;
147 }
148 }
149
150 // Indicator that the last value is not full
151 if ( $end === null ) {
152 // Warning: do not user array_splice, which does not preserve numerical keys
153 $last = end( $data );
154 $key = key( $data );
155 unset( $data[$key] );
156 $data[ "$key*" ] = $last;
157 }
158
159 return [ $labels, $data ];
160 }
161
163 private function getStatsProvider( string $type, TranslationStatsGraphOptions $opts ): TranslationStatsInterface {
164 $specs = $this->getGraphSpecifications();
165 return $this->objectFactory->createObject(
166 $specs[$type],
167 [
168 'allowClassName' => true,
169 'extraArgs' => [ $opts ],
170 ]
171 );
172 }
173
178 private static function roundTimestampToCutoff(
179 string $scale, int $cutoff, string $direction = 'earlier'
180 ): int {
181 $dir = $direction === 'earlier' ? -1 : 1;
182
183 /* Ensure that the first item in the graph has full data even
184 * if it doesn't align with the given 'days' boundary */
185 if ( $scale === 'hours' ) {
186 $cutoff += self::roundingAddition( $cutoff, 3600, $dir );
187 } elseif ( $scale === 'days' ) {
188 $cutoff += self::roundingAddition( $cutoff, 86400, $dir );
189 } elseif ( $scale === 'weeks' ) {
190 /* Here we assume that week starts on monday, which does not
191 * always hold true. Go Xwards day by day until we are on monday */
192 while ( date( 'D', $cutoff ) !== 'Mon' ) {
193 $cutoff += $dir * 86400;
194 }
195 // Round to nearest day
196 $cutoff -= ( $cutoff % 86400 );
197 } elseif ( $scale === 'months' ) {
198 // Go Xwards/ day by day until we are on the first day of the month
199 while ( date( 'j', $cutoff ) !== '1' ) {
200 $cutoff += $dir * 86400;
201 }
202 // Round to nearest day
203 $cutoff -= ( $cutoff % 86400 );
204 } elseif ( $scale === 'years' ) {
205 // Go Xwards/ day by day until we are on the first day of the year
206 while ( date( 'z', $cutoff ) !== '0' ) {
207 $cutoff += $dir * 86400;
208 }
209 // Round to nearest day
210 $cutoff -= ( $cutoff % 86400 );
211 }
212
213 return $cutoff;
214 }
215
216 private static function roundingAddition( int $ts, int $amount, int $dir ): int {
217 if ( $dir === -1 ) {
218 return -1 * ( $ts % $amount );
219 } else {
220 return $amount - ( $ts % $amount );
221 }
222 }
223
230 private static function getIncrement( string $scale ): int {
231 $increment = 3600 * 24;
232 if ( $scale === 'years' ) {
233 $increment = 3600 * 24 * 350;
234 } elseif ( $scale === 'months' ) {
235 /* We use increment to fill up the values. Use number small enough
236 * to ensure we hit each month */
237 $increment = 3600 * 24 * 15;
238 } elseif ( $scale === 'weeks' ) {
239 $increment = 3600 * 24 * 7;
240 } elseif ( $scale === 'hours' ) {
241 $increment = 3600;
242 }
243
244 return $increment;
245 }
246}
getGraphData(TranslationStatsGraphOptions $opts, Language $language)
Fetches and preprocesses graph data that can be fed to graph drawer.
Factory class for accessing message groups individually by id or all of them as an list.
static getGroup( $id)
Fetch a message group by id.
Essentially random collection of helper functions, similar to GlobalFunctions.php.
static getLanguageName( $code, $language='en')
Returns a localised language name.