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