5 # This script will get statistical data on newgroup usage
8 # It is part of the NewsStats package.
10 # Copyright (c) 2010-2012 Thomas Hochstein <thh@inter.net>
12 # It can be redistributed and/or modified under the same terms under
13 # which Perl itself is published.
16 our $VERSION = "0.01";
18 push(@INC, dirname($0));
22 use NewsStats qw(:DEFAULT :TimePeriods :Output :SQLHelper ReadGroupList);
25 use Getopt::Long qw(GetOptions);
26 Getopt::Long::config ('bundling');
28 ################################# Main program #################################
30 ### read commandline options
31 my ($OptBoundType,$OptCaptions,$OptCheckgroupsFile,$OptComments,
32 $OptFileTemplate,$OptFormat,$OptGroupBy,$OptGroupsDB,$LowBound,$OptMonth,
33 $OptNewsgroups,$OptOrderBy,$OptReportType,$OptSums,$UppBound);
34 GetOptions ('b|boundary=s' => \$OptBoundType,
35 'c|captions!' => \$OptCaptions,
36 'checkgroups=s' => \$OptCheckgroupsFile,
37 'comments!' => \$OptComments,
38 'filetemplate=s' => \$OptFileTemplate,
39 'f|format=s' => \$OptFormat,
40 'g|group-by=s' => \$OptGroupBy,
41 'groupsdb=s' => \$OptGroupsDB,
42 'l|lower=i' => \$LowBound,
43 'm|month=s' => \$OptMonth,
44 'n|newsgroups=s' => \$OptNewsgroups,
45 'o|order-by=s' => \$OptOrderBy,
46 'r|report=s' => \$OptReportType,
47 's|sums!' => \$OptSums,
48 'u|upper=i' => \$UppBound,
49 'h|help' => \&ShowPOD,
50 'V|version' => \&ShowVersion) or exit 1;
52 # $OptComments defaults to TRUE
53 $OptComments = 1 if (!defined($OptComments));
54 # force --nocomments when --filetemplate is used
55 $OptComments = 0 if ($OptFileTemplate);
58 if ($OptBoundType =~ /level/i) {
59 $OptBoundType = 'level';
60 } elsif ($OptBoundType =~ /av(era)?ge?/i) {
61 $OptBoundType = 'average';
62 } elsif ($OptBoundType =~ /sums?/i) {
63 $OptBoundType = 'sum';
65 $OptBoundType = 'default';
68 # parse $OptReportType
70 if ($OptReportType =~ /av(era)?ge?/i) {
71 $OptReportType = 'average';
72 } elsif ($OptReportType =~ /sums?/i) {
73 $OptReportType = 'sum';
75 $OptReportType = 'default';
78 # read list of newsgroups from --checkgroups
79 # into a hash reference
80 my $ValidGroups = &ReadGroupList($OptCheckgroupsFile) if $OptCheckgroupsFile;
82 ### read configuration
83 my %Conf = %{ReadConfig($HomePath.'/newsstats.conf')};
85 ### override configuration via commandline options
87 $ConfOverride{'DBTableGrps'} = $OptGroupsDB if $OptGroupsDB;
88 &OverrideConfig(\%Conf,\%ConfOverride);
91 my $DBHandle = InitDB(\%Conf,1);
93 ### get time period and newsgroups, prepare SQL 'WHERE' clause
95 # and set caption for output and expression for SQL 'WHERE' clause
96 my ($CaptionPeriod,$SQLWherePeriod) = &GetTimePeriod($OptMonth);
97 # bail out if --month is invalid
98 &Bleat(2,"--month option has an invalid format - ".
99 "please use 'YYYY-MM', 'YYYY-MM:YYYY-MM' or 'ALL'!") if !$CaptionPeriod;
100 # get list of newsgroups and set expression for SQL 'WHERE' clause
101 # with placeholders as well as a list of newsgroup to bind to them
102 my ($SQLWhereNewsgroups,@SQLBindNewsgroups) = &SQLGroupList($OptNewsgroups)
105 ### build SQL WHERE clause (and HAVING clause, if needed)
106 my ($SQLWhereClause,$SQLHavingClause);
107 # $OptBoundType 'level'
108 if ($OptBoundType and $OptBoundType ne 'default') {
109 $SQLWhereClause = SQLBuildClause('where',$SQLWherePeriod,
110 $SQLWhereNewsgroups,&SQLHierarchies($OptSums));
111 $SQLHavingClause = SQLBuildClause('having',&SQLSetBounds($OptBoundType,
112 $LowBound,$UppBound));
113 # $OptBoundType 'threshold' / 'default' or none
115 $SQLWhereClause = SQLBuildClause('where',$SQLWherePeriod,
116 $SQLWhereNewsgroups,&SQLHierarchies($OptSums),
117 &SQLSetBounds('default',$LowBound,$UppBound));
120 ### get sort order and build SQL 'ORDER BY' clause
121 # default to 'newsgroup' for $OptBoundType 'level' or 'average'
122 $OptGroupBy = 'newsgroup' if (!$OptGroupBy and
123 $OptBoundType and $OptBoundType ne 'default');
124 # force to 'month' for $OptReportType 'average' or 'sum'
125 $OptGroupBy = 'month' if ($OptReportType and $OptReportType ne 'default');
126 # parse $OptGroupBy to $GroupBy, create ORDER BY clause $SQLOrderClause
127 my ($GroupBy,$SQLOrderClause) = SQLSortOrder($OptGroupBy, $OptOrderBy);
128 # $GroupBy will contain 'month' or 'newsgroup' (parsed result of $OptGroupBy)
129 # set it to 'month' or 'key' for OutputData()
130 $GroupBy = ($GroupBy eq 'month') ? 'month' : 'key';
132 ### get report type and build SQL 'SELECT' query
134 my $SQLGroupClause = '';
135 my $Precision = 0; # number of digits right of decimal point for output
136 if ($OptReportType and $OptReportType ne 'default') {
137 $SQLGroupClause = 'GROUP BY newsgroup';
138 # change $SQLOrderClause: replace everything before 'postings'
139 $SQLOrderClause =~ s/BY.+postings/BY postings/;
140 if ($OptReportType eq 'average') {
141 $SQLSelect = "'All months',newsgroup,AVG(postings)";
143 # change $SQLOrderClause: replace 'postings' with 'AVG(postings)'
144 $SQLOrderClause =~ s/postings/AVG(postings)/;
145 } elsif ($OptReportType eq 'sum') {
146 $SQLSelect = "'All months',newsgroup,SUM(postings)";
147 # change $SQLOrderClause: replace 'postings' with 'SUM(postings)'
148 $SQLOrderClause =~ s/postings/SUM(postings)/;
151 $SQLSelect = 'month,newsgroup,postings';
154 ### get length of longest newsgroup name delivered by query
155 ### for formatting purposes
156 my $Field = ($GroupBy eq 'month') ? 'newsgroup' : 'month';
157 my $MaxLength = &GetMaxLength($DBHandle,$Conf{'DBTableGrps'},
158 $Field,$SQLWhereClause,$SQLHavingClause,
161 ### build and execute SQL query
163 # special query preparation for $OptBoundType 'level', 'average' or 'sums'
164 if ($OptBoundType and $OptBoundType ne 'default') {
165 # prepare and execute first query:
166 # get list of newsgroups meeting level conditions
167 $DBQuery = $DBHandle->prepare(sprintf('SELECT newsgroup FROM %s.%s %s '.
168 'GROUP BY newsgroup %s',
169 $Conf{'DBDatabase'},$Conf{'DBTableGrps'},
170 $SQLWhereClause,$SQLHavingClause));
171 $DBQuery->execute(@SQLBindNewsgroups)
172 or &Bleat(2,sprintf("Can't get groups data for %s from %s.%s: %s\n",
173 $CaptionPeriod,$Conf{'DBDatabase'},$Conf{'DBTableGrps'},
175 # add newsgroups to a comma-seperated list ready for IN(...) query
177 while (my ($Newsgroup) = $DBQuery->fetchrow_array) {
178 $GroupList .= ',' if $GroupList;
179 $GroupList .= "'$Newsgroup'";
181 # enhance $WhereClause
183 $SQLWhereClause = SQLBuildClause('where',$SQLWhereClause,
184 sprintf('newsgroup IN (%s)',$GroupList));
186 # condition cannot be satisfied;
187 # force query to fail by adding '0=1'
188 $SQLWhereClause = SQLBuildClause('where',$SQLWhereClause,'0=1');
193 $DBQuery = $DBHandle->prepare(sprintf('SELECT %s FROM %s.%s %s %s %s',
195 $Conf{'DBDatabase'},$Conf{'DBTableGrps'},
196 $SQLWhereClause,$SQLGroupClause,$
200 $DBQuery->execute(@SQLBindNewsgroups)
201 or &Bleat(2,sprintf("Can't get groups data for %s from %s.%s: %s\n",
202 $CaptionPeriod,$Conf{'DBDatabase'},$Conf{'DBTableGrps'},
206 # set default to 'pretty'
207 $OptFormat = 'pretty' if !$OptFormat;
208 # print captions if --caption is set
209 if ($OptCaptions && $OptComments) {
210 # print time period with report type
211 my $CaptionReportType= '(number of postings for each month)';
212 if ($OptReportType and $OptReportType ne 'default') {
213 $CaptionReportType= '(average number of postings for each month)'
214 if $OptReportType eq 'average';
215 $CaptionReportType= '(number of all postings for that time period)'
216 if $OptReportType eq 'sum';
218 printf("# ----- Report for %s %s\n",$CaptionPeriod,$CaptionReportType);
219 # print newsgroup list if --newsgroups is set
220 printf("# ----- Newsgroups: %s\n",join(',',split(/:/,$OptNewsgroups)))
222 # print boundaries, if set
223 my $CaptionBoundary= '(counting only month fulfilling this condition)';
224 if ($OptBoundType and $OptBoundType ne 'default') {
225 $CaptionBoundary= '(every single month)' if $OptBoundType eq 'level';
226 $CaptionBoundary= '(on average)' if $OptBoundType eq 'average';
227 $CaptionBoundary= '(all month summed up)' if $OptBoundType eq 'sum';
229 printf("# ----- Threshold: %s %s x %s %s %s\n",
230 $LowBound ? $LowBound : '',$LowBound ? '=>' : '',
231 $UppBound ? '<=' : '',$UppBound ? $UppBound : '',$CaptionBoundary)
232 if ($LowBound or $UppBound);
233 # print primary and secondary sort order
234 printf("# ----- Grouped by %s (%s), sorted %s%s\n",
235 ($GroupBy eq 'month') ? 'Months' : 'Newsgroups',
236 ($OptGroupBy and $OptGroupBy =~ /-?desc$/i) ? 'descending' : 'ascending',
237 ($OptOrderBy and $OptOrderBy =~ /posting/i) ? 'by number of postings ' : '',
238 ($OptOrderBy and $OptOrderBy =~ /-?desc$/i) ? 'descending' : 'ascending');
242 &OutputData($OptFormat,$OptComments,$GroupBy,$Precision,
243 $OptCheckgroupsFile ? $ValidGroups : '',
244 $OptFileTemplate,$DBQuery,$MaxLength);
247 $DBHandle->disconnect;
251 ################################ Documentation #################################
255 groupstats - create reports on newsgroup usage
259 B<groupstats> [B<-Vhcs> B<--comments>] [B<-m> I<YYYY-MM>[:I<YYYY-MM>] | I<all>] [B<-n> I<newsgroup(s)>] [B<--checkgroups> I<checkgroups file>] [B<-r> I<report type>] [B<-l> I<lower boundary>] [B<-u> I<upper boundary>] [B<-b> I<boundary type>] [B<-g> I<group by>] [B<-o> I<order by>] [B<-f> I<output format>] [B<--filetemplate> I<filename template>] [B<--groupsdb> I<database table>]
267 This script create reports on newsgroup usage (number of postings per
268 group per month) taken from result tables created by
271 =head2 Features and options
273 =head3 Time period and newsgroups
275 The time period to act on defaults to last month; you can assign another
276 time period or a single month (or drop all time constraints) via the
277 B<--month> option (see below).
279 B<groupstats> will process all newsgroups by default; you can limit
280 processing to only some newsgroups by supplying a list of those groups via
281 B<--newsgroups> option (see below). You can include hierarchy levels in
282 the output by adding the B<--sums> switch (see below). Optionally
283 newsgroups not present in a checkgroups file can be excluded from output,
284 sse B<--checkgroups> below.
288 You can choose between different B<--report> types: postings per month,
289 average postings per month or all postings summed up; for details, see
292 =head3 Upper and lower boundaries
294 Furthermore you can set an upper and/or lower boundary to exclude some
295 results from output via the B<--lower> and B<--upper> options,
296 respectively. By default, all newsgroups with more and/or less postings
297 per month will be excluded from the result set (i.e. not shown and not
298 considered for average and sum reports). You can change the meaning of
299 those boundaries with the B<--boundary> option. For details, please see
302 =head3 Sorting and formatting the output
304 By default, all results are grouped by month; you can group results by
305 newsgroup instead via the B<--groupy-by> option. Within those groups, the
306 list of newsgroups (or months) is sorted alphabetically (or
307 chronologically, respectively) ascending. You can change that order (and
308 sort by number of postings) with the B<--order-by> option. For details and
309 exceptions, please see below.
311 The results will be formatted as a kind of table; you can change the
312 output format to a simple list or just a list of newsgroups and number of
313 postings with the B<--format> option. Captions will be added by means of
314 the B<--caption> option; all comments (and captions) can be supressed by
315 using B<--nocomments>.
317 Last but not least you can redirect all output to a number of files, e.g.
318 one for each month, by submitting the B<--filetemplate> option, see below.
319 Captions and comments are automatically disabled in this case.
323 B<groupstats> will read its configuration from F<newsstats.conf>
324 which should be present in the same directory via Config::Auto.
326 See doc/INSTALL for an overview of possible configuration options.
328 You can override some configuration options via the B<--groupsdb> option.
334 =item B<-V>, B<--version>
336 Print out version and copyright information and exit.
338 =item B<-h>, B<--help>
340 Print this man page and exit.
342 =item B<-m>, B<--month> I<YYYY-MM[:YYYY-MM]|all>
344 Set processing period to a single month in YYYY-MM format or to a time
345 period between two month in YYYY-MM:YYYY-MM format (two month, separated
346 by a colon). By using the keyword I<all> instead, you can set no
347 processing period to process the whole database.
349 =item B<-n>, B<--newsgroups> I<newsgroup(s)>
351 Limit processing to a certain set of newsgroups. I<newsgroup(s)> can
352 be a single newsgroup name (de.alt.test), a newsgroup hierarchy
353 (de.alt.*) or a list of either of these, separated by colons, for
356 de.test:de.alt.test:de.newusers.*
358 =item B<-s>, B<--sums|--nosums> (sum per hierarchy level)
360 Include "virtual" groups for every hierarchy level in output, for
367 See the B<gatherstats> man page for details.
369 =item B<--checkgroups> I<filename>
371 Restrict output to those newgroups present in a file in checkgroups format
372 (one newgroup name per line; everything after the first whitespace on each
373 line is ignored). All other newsgroups will be removed from output.
375 =item B<-r>, B<--report> I<default|average|sums>
377 Choose the report type: I<default>, I<average> or I<sums>
379 By default, B<groupstats> will report the number of postings for each
380 newsgroup in each month. But it can also report the average number of
381 postings per group for all months or the total sum of postings per group
384 For report types I<average> and I<sums>, the B<group-by> option has no
385 meaning and will be silently ignored (see below).
387 =item B<-l>, B<--lower> I<lower boundary>
389 Set the lower boundary. See B<--boundary> below.
391 =item B<-l>, B<--upper> I<upper boundary>
393 Set the upper boundary. See B<--boundary> below.
395 =item B<-b>, B<--boundary> I<boundary type>
397 Set the boundary type to one of I<default>, I<level>, I<average> or
400 By default, all newsgroups with more postings per month than the upper
401 boundary and/or less postings per month than the lower boundary will be
402 excluded from further processing. For the default report that means each
403 month only newsgroups with a number of postings between the boundaries
404 will be displayed. For the other report types, newsgroups with a number of
405 postings exceeding the boundaries in all (!) months will not be
408 For example, lets take a list of newsgroups like this:
411 de.comp.datenbanken.misc 6
412 de.comp.datenbanken.ms-access 84
413 de.comp.datenbanken.mysql 88
415 de.comp.datenbanken.misc 8
416 de.comp.datenbanken.ms-access 126
417 de.comp.datenbanken.mysql 21
419 de.comp.datenbanken.misc 24
420 de.comp.datenbanken.ms-access 83
421 de.comp.datenbanken.mysql 36
423 With C<groupstats --month 2012-01:2012-03 --lower 25 --report sums>,
424 you'll get the following result:
427 de.comp.datenbanken.ms-access 293
428 de.comp.datenbanken.mysql 124
430 de.comp.datenbanken.misc has not been considered even though it has 38
431 postings in total, because it has less than 25 postings in every single
432 month. If you want to list all newsgroups with more than 25 postings U<in
433 total>, you'll have to set the boundary type to I<sum>, see below.
435 A boundary type of I<level> will show only those newsgroups - at all -
436 that satisfy the boundaries in each and every single month. With the above
437 list of newsgroups and
438 C<groupstats --month 2012-01:2012-03 --lower 25 --boundary level --report sums>,
439 you'll get this result:
442 de.comp.datenbanken.ms-access 293
444 de.comp.datenbanken.mysql has not been considered because it had less than
445 25 postings in 2012-02.
447 You can use that to get a list of newsgroups that have more (or less) then
448 x postings during the whole reporting period.
450 A boundary type of I<average> will show only those newsgroups - at all -that
451 satisfy the boundaries on average. With the above list of newsgroups and
452 C<groupstats --month 2012-01:2012-03 --lower 25 --boundary avg --report sums>,
453 you'll get this result:
456 de.comp.datenbanken.ms-access 293
457 de.comp.datenbanken.mysql 145
459 The average number of postings in the three groups is:
461 de.comp.datenbanken.misc 12.67
462 de.comp.datenbanken.ms-access 97.67
463 de.comp.datenbanken.mysql 48.33
465 Last but not least, a boundary type of I<sums> will show only those
466 newsgroups - at all - that satisfy the boundaries with the total sum of
467 all postings during the reporting period. With the above list of
469 C<groupstats --month 2012-01:2012-03 --lower 25 --boundary sum --report sums>,
470 you'll finally get this result:
473 de.comp.datenbanken.misc 38
474 de.comp.datenbanken.ms-access 293
475 de.comp.datenbanken.mysql 145
478 =item B<-g>, B<--group-by> I<month[-desc]|newsgroups[-desc]>
480 By default, all results are grouped by month, sorted chronologically in
481 ascending order, like this:
484 de.comp.datenbanken.ms-access 84
485 de.comp.datenbanken.mysql 88
487 de.comp.datenbanken.ms-access 126
488 de.comp.datenbanken.mysql 21
490 The results can be grouped by newsgroups instead via
491 B<--group-by> I<newsgroup>:
493 ----- de.comp.datenbanken.ms-access:
496 ----- de.comp.datenbanken.mysql:
500 By appending I<-desc> to the group-by option parameter, you can reverse
501 the sort order - e.g. B<--group-by> I<month-desc> will give:
504 de.comp.datenbanken.ms-access 126
505 de.comp.datenbanken.mysql 21
507 de.comp.datenbanken.ms-access 84
508 de.comp.datenbanken.mysql 88
510 Average and sums reports (see above) will always be grouped by months;
511 this option will therefore be ignored.
513 =item B<-o>, B<--order-by> I<default[-desc]|postings[-desc]>
515 Within each group (a single month or single newsgroup, see above), the
516 report will be sorted by newsgroup names in ascending alphabetical order
517 by default. You can change the sort order to descending or sort by number
520 =item B<-f>, B<--format> I<pretty|list|dump>
522 Select the output format, I<pretty> being the default:
525 de.comp.datenbanken.ms-access 84
526 de.comp.datenbanken.mysql 88
528 de.comp.datenbanken.ms-access 126
529 de.comp.datenbanken.mysql 21
531 I<list> format looks like this:
533 2012-01 de.comp.datenbanken.ms-access 84
534 2012-01 de.comp.datenbanken.mysql 88
535 2012-02 de.comp.datenbanken.ms-access 126
536 2012-02 de.comp.datenbanken.mysql 21
538 And I<dump> format looks like this:
541 de.comp.datenbanken.ms-access 84
542 de.comp.datenbanken.mysql 88
544 de.comp.datenbanken.ms-access 126
545 de.comp.datenbanken.mysql 21
547 You can remove the comments by using B<--nocomments>, see below.
549 =item B<-c>, B<--captions|--nocaptions>
551 Add captions to output, like this:
553 ----- Report for 2012-01 to 2012-02 (number of postings for each month)
554 ----- Newsgroups: de.comp.datenbanken.*
555 ----- Threshold: 10 => x <= 20 (on average)
556 ----- Grouped by Newsgroups (ascending), sorted by number of postings descending
560 =item B<--comments|--nocomments>
562 Add comments (group headers) to I<dump> and I<pretty> output. True by default.
564 Use I<--nocomments> to suppress anything except newsgroup names/months and
565 numbers of postings. This is enforced when using B<--filetemplate>, see below.
567 =item B<--filetemplate> I<filename template>
569 Save output to file(s) instead of dumping it to STDOUT. B<groupstats> will
570 create one file for each month (or each newsgroup, accordant to the
571 setting of B<--group-by>, see above), with filenames composed by adding
572 year and month (or newsgroup names) to the I<filename template>, for
573 example with B<--filetemplate> I<stats>:
579 B<--nocomments> is enforced, see above.
581 =item B<--groupsdb> I<database table>
583 Override I<DBTableGrps> from F<newsstats.conf>.
593 Show number of postings per group for lasth month in I<pretty> format:
597 Show that report for January of 2010 and de.alt.* plus de.test,
598 including display of hierarchy levels:
600 groupstats --month 2010-01 --newsgroups de.alt.*:de.test --sums
602 Only show newsgroups with 30 postings or less last month, ordered
603 by number of postings, descending, in I<pretty> format:
605 groupstats --upper 30 --order-by postings-desc
607 Show the total of all postings for the year of 2010 for all groups that
608 had 30 postings or less in every single month in that year, ordered by
609 number of postings in descending order:
611 groupstats -m 2010-01:2010-12 -u 30 -b level -r sums -o postings-desc
613 The same for the average number of postings in the year of 2010:
615 groupstats -m 2010-01:2010-12 -u 30 -b level -r avg -o postings-desc
617 List number of postings per group for eacht month of 2010 and redirect
618 output to one file for each month, namend stats-2010-01 and so on, in
619 machine-readable form (without formatting):
621 groupstats -m 2010-01:2010-12 -f dump --filetemplate stats
628 =item F<groupstats.pl>
632 =item F<NewsStats.pm>
634 Library functions for the NewsStats package.
636 =item F<newsstats.conf>
638 Runtime configuration file.
644 Please report any bugs or feature requests to the author or use the
645 bug tracker at L<http://bugs.th-h.de/>!
665 This script is part of the B<NewsStats> package.
669 Thomas Hochstein <thh@inter.net>
671 =head1 COPYRIGHT AND LICENSE
673 Copyright (c) 2010-2012 Thomas Hochstein <thh@inter.net>
675 This program is free software; you may redistribute it and/or modify it
676 under the same terms as Perl itself.