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->select( $tables, $fields, $conds, $type, $options, $joins );
93 wfDebug( __METHOD__ . "-queryend\n" );
94
95 // Start processing the data
96 $dateFormat = $so->getDateFormat();
97 $increment = self::getIncrement( $opts->getValue( 'scale' ) );
98
99 $labels = $so->labels();
100 $keys = array_keys( $labels );
101 $values = array_pad( [], count( $labels ), 0 );
102 $defaults = array_combine( $keys, $values );
103
104 $data = [];
105 // Allow 10 seconds in the future for processing time
106 $lastValue = $end ?? $now + 10;
107 while ( $cutoff <= $lastValue ) {
108 $date = $language->sprintfDate( $dateFormat, wfTimestamp( TS_MW, $cutoff ) );
109 $cutoff += $increment;
110 $data[$date] = $defaults;
111 }
112 // Ensure $lastValue is within range, in case the loop above jumped over it
113 $data[$language->sprintfDate( $dateFormat, wfTimestamp( TS_MW, $lastValue ) )] = $defaults;
114
115 // Processing
116 $labelToIndex = array_flip( $labels );
117
118 foreach ( $res as $row ) {
119 $indexLabels = $so->indexOf( $row );
120 if ( $indexLabels === false ) {
121 continue;
122 }
123
124 foreach ( $indexLabels as $i ) {
125 if ( !isset( $labelToIndex[$i] ) ) {
126 continue;
127 }
128 $date = $language->sprintfDate( $dateFormat, $so->getTimestamp( $row ) );
129 // Ignore values outside range
130 if ( !isset( $data[$date] ) ) {
131 continue;
132 }
133
134 $data[$date][$labelToIndex[$i]]++;
135 }
136 }
137
138 // Don't display dummy label
139 if ( count( $labels ) === 1 && $labels[0] === 'all' ) {
140 $labels = [];
141 }
142
143 foreach ( $labels as &$label ) {
144 if ( !str_contains( $label, '@' ) ) {
145 continue;
146 }
147 [ $groupId, $code ] = explode( '@', $label, 2 );
148 if ( $code && $groupId ) {
149 $code = Utilities::getLanguageName( $code, $language->getCode() ) . " ($code)";
150 $group = MessageGroups::getGroup( $groupId );
151 $group = $group ? $group->getLabel() : $groupId;
152 $label = "$group @ $code";
153 } elseif ( $code ) {
154 $label = Utilities::getLanguageName( $code, $language->getCode() ) . " ($code)";
155 } elseif ( $groupId ) {
156 $group = MessageGroups::getGroup( $groupId );
157 $label = $group ? $group->getLabel() : $groupId;
158 }
159 }
160
161 // Indicator that the last value is not full
162 if ( $end === null ) {
163 // Warning: do not user array_splice, which does not preserve numerical keys
164 $last = end( $data );
165 $key = key( $data );
166 unset( $data[$key] );
167 $data[ "$key*" ] = $last;
168 }
169
170 return [ $labels, $data ];
171 }
172
174 private function getStatsProvider( string $type, TranslationStatsGraphOptions $opts ): TranslationStatsInterface {
175 $specs = $this->getGraphSpecifications();
176 return $this->objectFactory->createObject(
177 $specs[$type],
178 [
179 'allowClassName' => true,
180 'extraArgs' => [ $opts ],
181 ]
182 );
183 }
184
189 private static function roundTimestampToCutoff(
190 string $scale, int $cutoff, string $direction = 'earlier'
191 ): int {
192 $dir = $direction === 'earlier' ? -1 : 1;
193
194 /* Ensure that the first item in the graph has full data even
195 * if it doesn't align with the given 'days' boundary */
196 if ( $scale === 'hours' ) {
197 $cutoff += self::roundingAddition( $cutoff, 3600, $dir );
198 } elseif ( $scale === 'days' ) {
199 $cutoff += self::roundingAddition( $cutoff, 86400, $dir );
200 } elseif ( $scale === 'weeks' ) {
201 /* Here we assume that week starts on monday, which does not
202 * always hold true. Go Xwards day by day until we are on monday */
203 while ( date( 'D', $cutoff ) !== 'Mon' ) {
204 $cutoff += $dir * 86400;
205 }
206 // Round to nearest day
207 $cutoff -= ( $cutoff % 86400 );
208 } elseif ( $scale === 'months' ) {
209 // Go Xwards/ day by day until we are on the first day of the month
210 while ( date( 'j', $cutoff ) !== '1' ) {
211 $cutoff += $dir * 86400;
212 }
213 // Round to nearest day
214 $cutoff -= ( $cutoff % 86400 );
215 } elseif ( $scale === 'years' ) {
216 // Go Xwards/ day by day until we are on the first day of the year
217 while ( date( 'z', $cutoff ) !== '0' ) {
218 $cutoff += $dir * 86400;
219 }
220 // Round to nearest day
221 $cutoff -= ( $cutoff % 86400 );
222 }
223
224 return $cutoff;
225 }
226
227 private static function roundingAddition( int $ts, int $amount, int $dir ): int {
228 if ( $dir === -1 ) {
229 return -1 * ( $ts % $amount );
230 } else {
231 return $amount - ( $ts % $amount );
232 }
233 }
234
241 private static function getIncrement( string $scale ): int {
242 $increment = 3600 * 24;
243 if ( $scale === 'years' ) {
244 $increment = 3600 * 24 * 350;
245 } elseif ( $scale === 'months' ) {
246 /* We use increment to fill up the values. Use number small enough
247 * to ensure we hit each month */
248 $increment = 3600 * 24 * 15;
249 } elseif ( $scale === 'weeks' ) {
250 $increment = 3600 * 24 * 7;
251 } elseif ( $scale === 'hours' ) {
252 $increment = 3600;
253 }
254
255 return $increment;
256 }
257}
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