bed36e2bc0d912a276c3d937f84e767d8434ec5e
[usenet/newsstats.git] / groupstats.pl
1 #! /usr/bin/perl -W
2 #
3 # groupstats.pl
4 #
5 # This script will get statistical data on newgroup usage
6 # from a database.
7
8 # It is part of the NewsStats package.
9 #
10 # Copyright (c) 2010 Thomas Hochstein <thh@inter.net>
11 #
12 # It can be redistributed and/or modified under the same terms under 
13 # which Perl itself is published.
14
15 BEGIN {
16   our $VERSION = "0.01";
17   use File::Basename;
18   push(@INC, dirname($0));
19 }
20 use strict;
21
22 use NewsStats qw(:DEFAULT :TimePeriods :Output :SQLHelper);
23
24 use DBI;
25
26 ################################# Main program #################################
27
28 ### read commandline options
29 my %Options = &ReadOptions('m:p:an:o:t:l:b:iscqdf:g:');
30
31 ### read configuration
32 my %Conf = %{ReadConfig('newsstats.conf')};
33
34 ### override configuration via commandline options
35 my %ConfOverride;
36 $ConfOverride{'DBTableGrps'}  = $Options{'g'} if $Options{'g'};
37 &OverrideConfig(\%Conf,\%ConfOverride);
38
39 ### check for incompatible command line options
40 # you can't mix '-t', '-b' and '-l'
41 # -b/-l take preference over -t, and -b takes preference over -l
42 # you can't use '-f' with '-b' or '-l'
43 if ($Options{'b'} or $Options{'l'}) {
44   if ($Options{'f'}) {
45     # drop -f
46     warn ("$MySelf: W: You cannot save the report to monthly files when using top lists (-b) or levels (-l). Filename template '-f $Options{'f'}' was ignored.\n");
47     undef($Options{'f'});
48   };
49   if ($Options{'t'}) {
50     # drop -t
51     warn ("$MySelf: W: You cannot combine thresholds (-t) and top lists (-b) or levels (-l). Threshold '-t $Options{'t'}' was ignored.\n");
52     undef($Options{'t'});
53   };
54   if ($Options{'b'} and $Options{'l'}) {
55     # drop -l
56     warn ("$MySelf: W: You cannot combine top lists (-b) and levels (-l). Level '-l $Options{'l'}' was ignored.\n");
57     undef($Options{'l'});
58   };
59   # -q/-d don't work with -b or -l
60   warn ("$MySelf: W: Sorting by number of postings (-q) ignored due to top list mode (-b) / levels (-l).\n") if $Options{'q'};
61   warn ("$MySelf: W: Reverse sorting (-d) ignored due to top list mode (-b) / levels (-l).\n") if $Options{'d'};
62 };
63
64 ### check output type
65 # default output type to 'pretty'
66 $Options{'o'} = 'pretty' if !$Options{'o'};
67 # fail if more than one newsgroup is combined with 'dumpgroup' type
68 die ("$MySelf: E: You cannot combine newsgroup lists (-n) with more than one group with '-o dumpgroup'!\n") if ($Options{'o'} eq 'dumpgroup' and defined($Options{'n'}) and $Options{'n'} =~ /:|\*/);
69 # accept 'dumpgroup' only with -n
70 if ($Options{'o'} eq 'dumpgroup' and !defined($Options{'n'})) {
71   $Options{'o'} = 'dump';
72   warn ("$MySelf: W: You must submit exactly one newsgroup ('-n news.group') for '-o dumpgroup'. Output type was set to 'dump'.\n");
73 };
74 # set output type to 'pretty' for -l
75 if ($Options{'l'} and $Options{'o'} ne 'pretty') {
76   $Options{'o'} = 'pretty';
77   warn ("$MySelf: W: Output type forced to '-o pretty' due to usage of '-l'.\n");
78 };
79 # set output type to 'dump' for -f
80 if ($Options{'f'} and $Options{'o'} ne 'dump') {
81   $Options{'o'} = 'dump';
82   warn ("$MySelf: W: Output type forced to '-o dump' due to usage of '-f'.\n");
83 };
84
85 ### init database
86 my $DBHandle = InitDB(\%Conf,1);
87
88 ### get time period
89 my ($StartMonth,$EndMonth);
90 # if '-a' is set, set start/end month from database
91 # FIXME - it doesn't make that much sense to get first/last month from database to query it
92 #         with a time period that equals no time period ...
93 if ($Options{'a'}) {
94   undef($Options{'m'});
95   undef($Options{'p'});
96   my $DBQuery = $DBHandle->prepare(sprintf("SELECT MIN(month),MAX(month) FROM %s.%s",$Conf{'DBDatabase'},$Conf{'DBTableGrps'}));
97   $DBQuery->execute or die sprintf("$MySelf: E: Can't get MIN/MAX month from %s.%s: %s\n",$Conf{'DBDatabase'},$Conf{'DBTableGrps'},$DBI::errstr);
98   ($StartMonth,$EndMonth) = $DBQuery->fetchrow_array;
99 } else {
100   ($StartMonth,$EndMonth) = &GetTimePeriod($Options{'m'},$Options{'p'});
101 };
102 # if -p or -a are set: drop -m
103 undef $Options{'m'} if ($Options{'p'} or $Options{'a'});
104 # if time period is more than one month: force output type to '-o pretty' or '-o dumpgroup'
105 if ($Options{'o'} eq 'dump' and ($Options{'p'} or $Options{'a'})) {
106   if (defined($Options{'n'}) and $Options{'n'} !~ /:|\*/) {
107     # just one newsgroup is defined
108     warn ("$MySelf: W: You cannot combine time periods (-p) with '-o dump', changing output type to '-o dumpgroup'.\n");
109     $Options{'o'} = 'dumpgroup';
110   } elsif (!defined($Options{'f'})) {
111     # more than one newsgroup - and no file output
112     warn ("$MySelf: W: You cannot combine time periods (-p) with '-o dump', changing output type to '-o pretty'.\n");
113     $Options{'o'} = 'pretty';
114   }
115 };
116
117 ### create report
118 # get list of newsgroups (-n)
119 my ($QueryGroupList,$QueryThreshold,@GroupList,@Params);
120 my $Newsgroups = $Options{'n'};
121 if ($Newsgroups) {
122   # explode list of newsgroups for WHERE clause
123   ($QueryGroupList,@GroupList) = &SQLGroupList($Newsgroups);
124 } else {
125   # set to dummy value (always true)
126   $QueryGroupList = 1;
127 };
128
129 # manage thresholds
130 if (defined($Options{'t'})) {
131   if ($Options{'i'}) {
132     # -i: list groups below threshold
133     $QueryThreshold .= ' postings < ?';
134   } else {
135     # default: list groups above threshold
136     $QueryThreshold .= ' postings > ?';
137   };
138   # push threshold to Params
139   push @Params,$Options{'t'};
140 } else {
141   # set to dummy value (always true)
142   $QueryThreshold = 1;  
143 }
144
145 # construct WHERE clause
146 # $QueryGroupList is "list of newsgroup" (or 1),
147 # $QueryThreshold is threshold definition (or 1),
148 # &SQLHierarchies() takes care of the exclusion of hierarchy levels (.ALL)
149 # according to setting of -s
150 my $WhereClause = sprintf('month BETWEEN ? AND ? AND %s AND %s %s',$QueryGroupList,$QueryThreshold,&SQLHierarchies($Options{'s'}));
151
152 # get length of longest newsgroup delivered by query for formatting purposes
153 # FIXME
154 my $MaxLength = &GetMaxLength($DBHandle,$Conf{'DBTableGrps'},'newsgroup',$WhereClause,$StartMonth,$EndMonth,(@GroupList,@Params));
155
156 my ($OrderClause,$DBQuery);
157 # -b (best of / top list) defined?
158 if (!defined($Options{'b'}) and !defined($Options{'l'})) {
159   # default: neither -b nor -l
160   # set ordering (ORDER BY) to "newsgroups" or "postings", "ASC" or "DESC"
161   # according to -q and -d
162   $OrderClause = 'newsgroup';
163   $OrderClause = 'postings' if $Options{'q'};
164   $OrderClause .= ' DESC' if $Options{'d'};
165   # prepare query: get number of postings per group from groups table for given months and newsgroups
166   $DBQuery = $DBHandle->prepare(sprintf("SELECT month,newsgroup,postings FROM %s.%s WHERE %s ORDER BY month,%s",$Conf{'DBDatabase'},$Conf{'DBTableGrps'},$WhereClause,$OrderClause));
167 } elsif ($Options{'b'}) {
168   # -b is set (then -l can't be!)
169   # set sorting order (-i): top or flop list?
170   if ($Options{'i'}) {
171     $OrderClause = 'postings';
172   } else {
173     $OrderClause = 'postings DESC';
174   };
175   # set -b to 10 if < 1 (Top 10)
176   $Options{'b'} = 10 if $Options{'b'} !~ /^\d*$/ or $Options{'b'} < 1;
177   # push LIMIT to Params
178   push @Params,$Options{'b'};
179   # prepare query: get sum of postings per group from groups table for given months and newsgroups with LIMIT
180   $DBQuery = $DBHandle->prepare(sprintf("SELECT newsgroup,SUM(postings) AS postings FROM %s.%s WHERE %s GROUP BY newsgroup ORDER BY %s,newsgroup LIMIT ?",$Conf{'DBDatabase'},$Conf{'DBTableGrps'},$WhereClause,$OrderClause));
181 } else {
182   # -l must be set now, as all other cases have been taken care of
183   # which kind of level (-i): more than -l x or less than -l x?
184   my ($Level);
185   if ($Options{'i'}) {
186     $Level = '<';
187   } else {
188     $Level = '>';
189   };
190   # prepare and execute query: get list of newsgroups meeting level condition
191   $DBQuery = $DBHandle->prepare(sprintf("SELECT newsgroup FROM %s.%s WHERE %s GROUP BY newsgroup HAVING MAX(postings) %s ?",$Conf{'DBDatabase'},$Conf{'DBTableGrps'},$WhereClause,$Level));
192   $DBQuery->execute($StartMonth,$EndMonth,@GroupList,$Options{'l'})
193     or die sprintf("$MySelf: E: Can't get groups data for %s to %s from %s.%s: %s\n",$StartMonth,$EndMonth,$Conf{'DBDatabase'},$Conf{'DBTableGrps'},$DBI::errstr);
194   # add newsgroups to a comma-seperated list ready for IN(...) query
195   my $GroupList;
196   while (my ($Newsgroup) = $DBQuery->fetchrow_array) {
197     $GroupList .= ',' if (defined($GroupList) and $GroupList ne '');
198     $GroupList .= "'$Newsgroup'";
199   };
200   $DBQuery = $DBHandle->prepare(sprintf("SELECT month,newsgroup,postings FROM %s.%s WHERE newsgroup IN (%s) AND %s ORDER BY newsgroup,month",$Conf{'DBDatabase'},$Conf{'DBTableGrps'},$GroupList,$WhereClause));
201 };
202
203 # execute query
204 $DBQuery->execute($StartMonth,$EndMonth,@GroupList,@Params)
205   or die sprintf("$MySelf: E: Can't get groups data for %s to %s from %s.%s: %s\n",$StartMonth,$EndMonth,$Conf{'DBDatabase'},$Conf{'DBTableGrps'},$DBI::errstr);
206
207 # output results
208 # reset caption (-c) if -f is set
209 undef($Options{'c'}) if $Options{'f'};
210 # print caption (-c) with time period if -m or -p is set
211 if ($Options{'c'}) {
212   if ($Options{'m'}) {
213     printf ("----- Report for %s\n",$StartMonth);
214   } else {
215     printf ("----- Report from %s to %s %s\n",$StartMonth,$EndMonth,$Options{'a'} ? '(all months)' : '');
216   };
217 };
218 # print caption (-c) with newsgroup list if -n is set
219 printf ("----- Newsgroups: %s\n",join(',',split(/:/,$Newsgroups))) if $Options{'c'} and $Options{'n'};
220 # print caption (-c) with threshold if -t is set, taking -i in account
221 printf ("----- Threshold: %s %u\n",$Options{'i'} ? '<' : '>',$Options{'t'}) if $Options{'c'} and $Options{'t'};
222 if (!defined($Options{'b'})  and !defined($Options{'l'})) {
223   # default: neither -b nor -l
224   &OutputData($Options{'o'},$Options{'f'},$DBQuery,$MaxLength);
225 } elsif ($Options{'b'}) {
226   # -b is set (then -l can't be!)
227   # we have to read in the query results ourselves, as they do not have standard layout
228   while (my ($Newsgroup,$Postings) = $DBQuery->fetchrow_array) {
229     # we just assign "top x" or "bottom x" instead of a month for the caption and force an output type of pretty
230     print &FormatOutput('pretty', ($Options{'i'} ? 'Bottom ' : 'Top ').$Options{'b'}, $Newsgroup, $Postings, $MaxLength);
231   };
232 } else {
233   # -l must be set now, as all other cases have been taken care of
234   # print caption (-c) with level, taking -i in account
235   printf ("----- Newsgroups with %s than %u postings over the whole time period\n",$Options{'i'} ? 'less' : 'more',$Options{'l'}) if $Options{'c'};
236   # we have to read in the query results ourselves, as they do not have standard layout
237   while (my ($Month,$Newsgroup,$Postings) = $DBQuery->fetchrow_array) {
238     # we just switch $Newsgroups and $Month for output generation
239     print &FormatOutput($Options{'o'}, $Newsgroup, $Month, $Postings, 7);
240   };
241 };
242
243 ### close handles
244 $DBHandle->disconnect;
245
246 __END__
247
248 ################################ Documentation #################################
249
250 =head1 NAME
251
252 groupstats - create reports on newsgroup usage
253
254 =head1 SYNOPSIS
255
256 B<groupstats> [B<-Vhiscqd>] [B<-m> I<YYYY-MM> | B<-p> I<YYYY-MM:YYYY-MM> | B<-a>] [B<-n> I<newsgroup(s)>] [B<-t> I<threshold>] [B<-l> I<level>] [B<-b> I<number>] [B<-o> I<output type>] [B<-f> I<filename template>] [B<-g> I<database table>]
257
258 =head1 REQUIREMENTS
259
260 See doc/README: Perl 5.8.x itself and the following modules from CPAN:
261
262 =over 2
263
264 =item -
265
266 Config::Auto
267
268 =item -
269
270 DBI
271
272 =back
273
274 =head1 DESCRIPTION
275
276 This script create reports on newsgroup usage (number of postings per
277 group per month) taken from result tables created by
278 F<gatherstats.pl>.
279
280 The time period to act on defaults to last month; you can assign
281 another month via the B<-m> switch or a time period via the B<-p>
282 switch; the latter takes preference.
283
284 B<groupstats> will process all newsgroups by default; you can limit
285 that to only some newsgroups by supplying a list of those groups via
286 B<-n> (see below). You can include hierarchy levels in the output by
287 adding the B<-s> switch (see below).
288
289 Furthermore you can set a threshold via B<-t> so that only newsgroups
290 with more postings per month will be included in the report. You can
291 invert that by the B<-i> switch so only newsgroups with less than
292 I<threshold> postings per month will be included.
293
294 You can sort the output by number of postings per month instead of the
295 default (alphabetical list of newsgroups) by using B<-q>; you can
296 reverse the sorting order (from highest to lowest or in reversed
297 alphabetical order) by using B<-d>.
298
299 Furthermore, you can create a list of newsgroups that had consistently
300 more (or less) than x postings per month during the whole report
301 period by using B<-l> (together with B<i> as needed).
302
303 Last but not least you can create a "best of" list of the top x
304 newsgroups via B<-b> (or a "worst of" list by adding B<i>).
305
306 By default, B<groupstats> will dump an alphabetical list of newsgroups,
307 one per line, followed by the number of postings in that group, for
308 every month. You can change the output format by using B<-o> (see
309 below). Captions can be added by setting the B<-c> switch.
310
311 =head2 Configuration
312
313 B<groupstats> will read its configuration from F<newsstats.conf>
314 which should be present in the same directory via Config::Auto.
315
316 See doc/INSTALL for an overview of possible configuration options.
317
318 You can override configuration options via the B<-g> switch.
319
320 =head1 OPTIONS
321
322 =over 3
323
324 =item B<-V> (version)
325
326 Print out version and copyright information on B<yapfaq> and exit.
327
328 =item B<-h> (help)
329
330 Print this man page and exit.
331
332 =item B<-m> I<YYYY-MM> (month)
333
334 Set processing period to a month in YYYY-MM format. Ignored if B<-p>
335 or B<-a> is set.
336
337 =item B<-p> I<YYYY-MM:YYYY-MM> (period)
338
339 Set processing period to a time period between two month, each in
340 YYYY-MM format, separated by a colon. Overrides B<-m>. Ignored if
341 B<-a> is set.
342
343 =item B<-a> (all)
344
345 Set no processing period (process whole database). Overrides B<-m>
346 and B<-p>.
347
348 =item B<-n> I<newsgroup(s)> (newsgroups)
349
350 Limit processing to a certain set of newsgroups. I<newsgroup(s)> can
351 be a single newsgroup name (de.alt.test), a newsgroup hierarchy
352 (de.alt.*) or a list of either of these, separated by colons, for
353 example
354
355    de.test:de.alt.test:de.newusers.*
356
357 =item B<-t> I<threshold> (threshold)
358
359 Only include newsgroups with more than I<threshold> postings per
360 month. Can be inverted by the B<-i> switch so that only newsgroups
361 with less than I<threshold> postings will be included.
362
363 This setting will be ignored if B<-l> or B<-b> is set.
364
365 =item B<-l> I<level> (level)
366
367 Only include newsgroups with more than I<level> postings per
368 month, every month during the whole reporting period. Can be inverted
369 by the B<-i> switch so that only newsgroups with less than I<level>
370 postings every single month will be included. Output will be ordered
371 by newsgroup name, followed by month.
372
373 This setting will be ignored if B<-b> is set. Overrides B<-t> and
374 can't be used together with B<-q>, B<-d> or B<-f>.
375
376 =item B<-b> I<n> (best of)
377
378 Create a list of the I<n> newsgroups with the most postings over the
379 whole reporting period. Can be inverted by the B<-i> switch so that a
380 list of the I<n> newsgroups with the least postings over the whole
381 period is generated. Output will be ordered by sum of postings.
382
383 Overrides B<-t> and B<-l> and can't be used together with B<-q>, B<-d>
384 or B<-f>. Output format is set to I<pretty> (see below).
385
386 =item B<-i> (invert)
387
388 Used in conjunction with B<-t>, B<-l> or B<-b> to set a lower
389 threshold or level or generate a "bottom list" instead of a top list.
390
391 =item B<-s> (sum per hierarchy level)
392
393 Include "virtual" groups for every hierarchy level in output, for
394 example:
395
396     de.alt.ALL 10
397     de.alt.test 5
398     de.alt.admin 7
399
400 See the B<gatherstats> man page for details.
401
402 =item B<-o> I<output type> (output format)
403
404 Set output format. Default is I<pretty>, which will print a header for
405 each new month, followed by an alphabetical list of newsgroups, each
406 on a new line, followed by the number of postings in that month.
407 B<groupstats> will try to align newsgroup names and posting counts.
408 Usage of B<-b> will force this format; it cannot be used together with
409 B<-f>.
410
411 I<dump> format is used to create an easily parsable output consisting
412 of an alphabetical list of newsgroups, each on a new line, followed by
413 the number of postings in that month, without any alignment. This
414 default format can't be used with time periods of more than one month.
415 Usage of B<-f> will force this format.
416
417 I<list> format is like I<dump>, but will print the month in front of
418 the newsgroup name.
419
420 I<dumpgroup> format can only be use with a group list (see B<-n>) of
421 exactly one newsgroup and is like I<dump>, but will output months,
422 followed by the number of postings.
423
424 =item B<-c> (captions)
425
426 Add captions to output (reporting period, newsgroups list, threshold
427 and so on).
428
429 This setting will be ignored if B<-f> is set.
430
431 =item B<-q> (quantity of postings)
432
433 Sort by number of postings instead of by newsgroup names.
434
435 Cannot be used with B<-l> or B<-b>.
436
437 =item B<-d> (descending)
438
439 Change sort order to descending.
440
441 Cannot be used with B<-l> or B<-b>.
442
443 =item B<-f> I<filename template> (output file)
444
445 Save output to file instead of dumping it to STDOUT. B<groupstats>
446 will create one file for each month, with filenames composed by
447 adding year and month to the I<filename template>, for example
448 with B<-f> I<stats>:
449
450     stats-2010-01
451     stats-2010-02
452     ... and so on
453
454 This setting will be ignored if B<-l> or B<-b> is set. Output format
455 is set to I<dump> (see above).
456
457 =item B<-g> I<table> (postings per group table)
458
459 Override I<DBTableGrps> from F<newsstats.conf>.
460
461 =back
462
463 =head1 INSTALLATION
464
465 See doc/INSTALL.
466
467 =head1 EXAMPLES
468
469 Show number of postings per group for lasth month in I<dump> format:
470
471     groupstats
472
473 Show that report for January of 2010 and de.alt.* plus de.test,
474 including display of hierarchy levels:
475
476     groupstats -m 2010-01 -n de.alt.*:de.test -s
477
478 Show that report for the year of 2010 in I<pretty> format:
479
480     groupstats -p 2010-01:2010-12 -o pretty
481
482 Only show newsgroups with less than 30 postings last month, ordered
483 by number of postings, descending, in I<pretty> format:
484
485     groupstats -iqdt 30 -o pretty
486
487 Show top 10 for the first half-year of of 2010 in I<pretty> format:
488
489     groupstats -p 2010-01:2010-06 -b 10 -o pretty
490
491 Report all groups that had less than 30 postings every singele month
492 in the year of 2010 (I<pretty> format is forced)
493
494     groupstats -p 2010-01:2010-12 -il 30
495
496 =head1 FILES
497
498 =over 4
499
500 =item F<groupstats.pl>
501
502 The script itself.
503
504 =item F<NewsStats.pm>
505
506 Library functions for the NewsStats package.
507
508 =item F<newsstats.conf>
509
510 Runtime configuration file for B<yapfaq>.
511
512 =back
513
514 =head1 BUGS
515
516 Please report any bugs or feature requests to the author or use the
517 bug tracker at L<http://bugs.th-h.de/>!
518
519 =head1 SEE ALSO
520
521 =over 2
522
523 =item -
524
525 doc/README
526
527 =item -
528
529 doc/INSTALL
530
531 =item -
532
533 gatherstats -h
534
535 =back
536
537 This script is part of the B<NewsStats> package.
538
539 =head1 AUTHOR
540
541 Thomas Hochstein <thh@inter.net>
542
543 =head1 COPYRIGHT AND LICENSE
544
545 Copyright (c) 2010 Thomas Hochstein <thh@inter.net>
546
547 This program is free software; you may redistribute it and/or modify it
548 under the same terms as Perl itself.
549
550 =cut
This page took 0.02538 seconds and 3 git commands to generate.