Add upgrade path.
[usenet/newsstats.git] / NewsStats.pm
1 # NewsStats.pm
2 #
3 # Library functions for the NewsStats package.
4 #
5 # Copyright (c) 2010 Thomas Hochstein <thh@inter.net>
6 #
7 # This module can be redistributed and/or modified under the same terms under 
8 # which Perl itself is published.
9
10 package NewsStats;
11
12 use strict;
13 use warnings;
14 our ($VERSION, @ISA, @EXPORT, @EXPORT_OK, %EXPORT_TAGS);
15
16 require Exporter;
17 @ISA = qw(Exporter);
18 @EXPORT = qw(
19   $MySelf
20   $MyVersion
21   $PackageVersion
22   ReadOptions
23   ReadConfig
24   OverrideConfig
25   InitDB
26 );
27 @EXPORT_OK = qw(
28   GetTimePeriod
29   LastMonth
30   CheckMonth
31   SplitPeriod
32   ListMonth
33   ListNewsgroups
34   OutputData
35   FormatOutput
36   SQLHierarchies
37   SQLGroupList
38   GetMaxLenght
39 );
40 %EXPORT_TAGS = ( TimePeriods => [qw(GetTimePeriod LastMonth CheckMonth SplitPeriod ListMonth)],
41                  Output      => [qw(OutputData FormatOutput)],
42                  SQLHelper   => [qw(SQLHierarchies SQLGroupList GetMaxLenght)]);
43 $VERSION = '0.1';
44 our $PackageVersion = '0.1';
45
46 use Data::Dumper;
47 use File::Basename;
48 use Getopt::Std;
49
50 use Config::Auto;
51 use DBI;
52
53 #####-------------------------------- Vars --------------------------------#####
54
55 our $MySelf = fileparse($0, '.pl');
56 our $MyVersion = "$MySelf $::VERSION (NewsStats.pm $VERSION)";
57
58 #####------------------------------- Basics -------------------------------#####
59
60 ################################################################################
61 sub ReadOptions {
62 ################################################################################
63 ### read commandline options and act on standard options -h and -V
64 ### IN : $Params: list of legal commandline paramaters (without -h and -V)
65 ### OUT: a hash containing the commandline options
66   $Getopt::Std::STANDARD_HELP_VERSION = 1;
67
68   my ($Params) = @_;
69   my %Options;
70   
71   getopts('Vh'.$Params, \%Options);
72
73   # -V: display version
74   &ShowVersion if ($Options{'V'});
75
76   # -h: feed myself to perldoc
77   &ShowPOD if ($Options{'h'});
78
79   return %Options;
80 };
81 ################################################################################
82
83 ################################################################################
84 sub ShowVersion {
85 ################################################################################
86 ### display version and exit
87   print "NewsStats v$PackageVersion\n$MyVersion\nCopyright (c) 2010 Thomas Hochstein <thh\@inter.net>\n";
88   print "This program is free software; you may redistribute it and/or modify it under the same terms as Perl itself.\n";
89   exit(100);
90 };
91 ################################################################################
92
93 ################################################################################
94 sub ShowPOD {
95 ################################################################################
96 ### feed myself to perldoc and exit
97   exec('perldoc', $0);
98   exit(100);
99 };
100 ################################################################################
101
102 ################################################################################
103 sub ReadConfig {
104 ################################################################################
105 ### read config via Config::Auto
106 ### IN : $ConfFile: config filename
107 ### OUT: reference to a hash containing the configuration
108   my ($ConfFile) = @_;
109   return Config::Auto::parse($ConfFile, format => 'equal');
110 };
111 ################################################################################
112
113 ################################################################################
114 sub OverrideConfig  {
115 ################################################################################
116 ### override configuration values
117 ### IN : $ConfigR  : reference to configuration hash
118 ###      $OverrideR: reference to a hash containing overrides
119   my ($ConfigR,$OverrideR) = @_;
120   my %Override = %$OverrideR;
121   # Config hash empty?
122   warn "$MySelf W: Empty configuration hash passed to OverrideConfig().\n" if ( keys %$ConfigR < 1);
123   # return if no overrides
124   return if (keys %Override < 1 or keys %$ConfigR < 1);
125   foreach my $Key (keys %Override) {
126     $$ConfigR{$Key} = $Override{$Key};
127   };
128 };
129 ################################################################################
130
131 ################################################################################
132 sub InitDB {
133 ################################################################################
134 ### initialise database connection
135 ### IN : $ConfigR: reference to configuration hash
136 ###      $Die    : if TRUE, die if connection fails
137 ### OUT: DBHandle
138   my ($ConfigR,$Die) = @_;
139   my %Conf = %$ConfigR;
140   my $DBHandle = DBI->connect(sprintf('DBI:%s:database=%s;host=%s',$Conf{'DBDriver'},$Conf{'DBDatabase'},$Conf{'DBHost'}), $Conf{'DBUser'}, $Conf{'DBPw'}, { PrintError => 0 });
141   if (!$DBHandle) {
142     die ("$MySelf: E: $DBI::errstr\n") if (defined($Die) and $Die);
143     warn("$MySelf: W: $DBI::errstr\n");
144   };
145   return $DBHandle;
146 };
147 ################################################################################
148
149 #####------------------------------ GetStats ------------------------------#####
150
151 ################################################################################
152 sub ListNewsgroups {
153 ################################################################################
154 ### explode a (scalar) list of newsgroup names to a list of newsgroup and
155 ### hierarchy names where every newsgroup and hierarchy appears only once:
156 ### de.alt.test,de.alt.admin -> de.ALL, de.alt.ALL, de.alt.test, de.alt.admin
157 ### IN : $Newsgroups: a list of newsgroups (content of Newsgroups: header)
158 ### OUT: %Newsgroups: hash containing all newsgroup and hierarchy names as keys
159   my ($Newsgroups) = @_;
160   my %Newsgroups;
161   chomp($Newsgroups);
162   # remove whitespace from contents of Newsgroups:
163   $Newsgroups =~ s/\s//;
164   # call &HierarchyCount for each newsgroup in $Newsgroups:
165   for (split /,/, $Newsgroups) {
166     # add original newsgroup to %Newsgroups
167     $Newsgroups{$_} = 1;
168     # add all hierarchy elements to %Newsgroups, amended by '.ALL',
169     # i.e. de.alt.ALL and de.ALL
170     foreach (ParseHierarchies($_)) {
171       $Newsgroups{$_.'.ALL'} = 1;
172     }
173   };
174   return %Newsgroups;
175 };
176
177 ################################################################################
178 sub ParseHierarchies {
179 ################################################################################
180 ### return a list of all hierarchy levels a newsgroup belongs to
181 ### (for de.alt.test.moderated that would be de/de.alt/de.alt.test)
182 ### IN : $Newsgroup  : a newsgroup name
183 ### OUT: @Hierarchies: array containing all hierarchies the newsgroup belongs to
184   my ($Newsgroup) = @_;
185   my @Hierarchies;
186   # strip trailing dots
187   $Newsgroup =~ s/(.+)\.+$/$1/;
188   # butcher newsgroup name by "." and add each hierarchy to @Hierarchies
189   # i.e. de.alt.test: "de.alt" and "de"
190   while ($Newsgroup =~ /\./) {
191     $Newsgroup =~ s/^((?:\.?[^.]+)*)\.[^.]+$/$1/;
192     push @Hierarchies, $Newsgroup;
193   };
194   return @Hierarchies;
195 };
196
197 ################################################################################
198
199 #####----------------------------- TimePeriods ----------------------------#####
200
201 ################################################################################
202 sub GetTimePeriod {
203 ################################################################################
204 ### get a time period to act on, in order of preference: by default the
205 ### last month; or a month submitted by -m YYYY-MM; or a time period submitted
206 ### by -p YYYY-MM:YYYY-MM
207 ### IN : $Month,$Period: contents of -m and -p
208 ### OUT: $StartMonth, $EndMonth (identical if period is just one month)
209   my ($Month,$Period) = @_;
210   # exit if -m is set and not like YYYY-MM
211   die "$MySelf: E: Wrong date format - use '$MySelf -m YYYY-MM'!\n" if not &CheckMonth($Month);
212   # warn if -m and -p is set
213   warn "$MySelf: W: Time period assigned by '-p' takes precendece over month assigned by '-m'.\n" if ($Month && $Period);
214   # default: set -m to last month
215   $Month = &LastMonth if (!defined($Month) and !defined($Period));
216   # set $StartMonth, $EndMonth
217   my ($StartMonth, $EndMonth);
218   if ($Period) {
219     # -p: get date range
220     ($StartMonth, $EndMonth) = &SplitPeriod($Period);
221     die "$MySelf: E: Wrong format for time period - use '$MySelf -p YYYY-MM:YYYY-MM'!\n" if !defined($StartMonth);
222   } else {
223     # set $StartMonth = $EndMonth = $Month if -p is not set
224     $StartMonth = $EndMonth = $Month;
225   };
226   return ($StartMonth, $EndMonth);
227 };
228
229 ################################################################################
230 sub LastMonth {
231 ################################################################################
232 ### get last month from todays date in YYYY-MM format
233 ### OUT: last month as YYYY-MM
234   # get today's date
235   my (undef,undef,undef,undef,$Month,$Year,undef,undef,undef) = localtime(time);
236   # $Month is already defined from 0 to 11, so no need to decrease it by 1
237   $Year += 1900;
238   if ($Month < 1) {
239     $Month = 12;
240     $Year--;
241   };
242   # return last month
243   return sprintf('%4d-%02d',$Year,$Month);
244 };
245
246 ################################################################################
247 sub CheckMonth {
248 ################################################################################
249 ### check if input is a valid month in YYYY-MM form
250 ### IN : $Month: month
251 ### OUT: TRUE / FALSE
252   my ($Month) = @_;
253   return 0 if (defined($Month) and $Month !~ /^\d{4}-\d{2}$/);
254   return 1;
255 };
256
257 ################################################################################
258 sub SplitPeriod {
259 ################################################################################
260 ### split a time period denoted by YYYY-MM:YYYY-MM into start and end month
261 ### IN : $Period: time period
262 ### OUT: $StartMonth, Â$EndMonth
263   my ($Period) = @_;
264   return (undef,undef) if $Period !~ /^\d{4}-\d{2}:\d{4}-\d{2}$/;
265   my ($StartMonth, $EndMonth) = split /:/, $Period;
266   # switch parameters as necessary
267   if ($EndMonth gt $StartMonth) {
268     return ($StartMonth, $EndMonth);
269   } else {
270     return ($EndMonth, $StartMonth);
271   };
272 };
273
274 ################################################################################
275 sub ListMonth {
276 ################################################################################
277 ### return a list of months (YYYY-MM) between start and end month
278 ### IN : $StartMonth, $EndMonth
279 ### OUT: @Months: array containing all months from $StartMonth to $EndMonth
280   my ($StartMonth, $EndMonth) = @_;
281   return (undef,undef) if ($StartMonth !~ /^\d{4}-\d{2}$/ or $EndMonth !~ /^\d{4}-\d{2}$/);
282   # return if $StartMonth = $EndMonth
283   return ($StartMonth) if ($StartMonth eq $EndMonth);
284   # set $Year, $Month from $StartMonth
285   my ($Year, $Month) = split /-/, $StartMonth;
286   # define @Months
287   my (@Months);
288   until ("$Year-$Month" gt $EndMonth) {
289     push @Months, "$Year-$Month";
290     $Month = "$Month"; # force string context
291     $Month++;
292     if ($Month > 12) {
293       $Month = '01';
294       $Year++;
295     };
296   };
297   return @Months;
298 };
299
300 #####---------------------------- OutputFormats ---------------------------#####
301
302 ################################################################################
303 sub OutputData {
304 ################################################################################
305 ### read database query results from DBHandle and print results with formatting
306 ### IN : $Format : format specifier
307 ###      $DBQuery: database query handle with executed query,
308 ###                containing $Month, $Key, $Value
309 ###      $PadGroup: padding length for newsgroups field (optional) for 'pretty'
310   my ($Format, $DBQuery,$PadGroup) = @_;
311   while (my ($Month, $Key, $Value) = $DBQuery->fetchrow_array) {
312     print &FormatOutput($Format, $Month, $Key, $Value, $PadGroup);
313   };
314 };
315
316 ################################################################################
317 sub FormatOutput {
318 ################################################################################
319 ### format information for output according to format specifier
320 ### IN : $Format  : format specifier
321 ###      $Month   : month (as YYYY-MM)
322 ###      $Key     : newsgroup, client, ...
323 ###      $Value   : number of postings with that attribute
324 ###      $PadGroup: padding length for key field (optional) for 'pretty'
325 ### OUT: $Output: formatted output
326   my ($Format, $Month, $Key, $Value, $PadGroup) = @_;
327
328   # define output types
329   my %LegalOutput;
330   @LegalOutput{('dump','dumpgroup','list','pretty')} = ();
331   # bail out if format is unknown
332   die "$MySelf: E: Unknown output type '$Format'!\n" if !exists($LegalOutput{$Format});
333
334   my ($Output);
335   # keep last month in mind
336   our ($LastIteration);
337   if ($Format eq 'dump') {
338     # output as dump (ng nnnnn)
339     $Output = sprintf ("%s %u\n",$Key,$Value);
340   } elsif ($Format eq 'dumpgroup') {
341     # output as dump (YYYY-NN: nnnnn)
342     $Output = sprintf ("%s: %5u\n",$Month,$Value);
343   } elsif ($Format eq 'list') {
344     # output as list (YYYY-NN: ng nnnnn)
345     $Output = sprintf ("%s: %s %u\n",$Month,$Key,$Value);
346   } elsif ($Format eq 'pretty') {
347     # output as table
348     $Output = sprintf ("----- %s:\n",$Month) if (!defined($LastIteration) or $Month ne $LastIteration);
349     $LastIteration = $Month;
350     $Output .= sprintf ($PadGroup ? sprintf("%%-%us %%5u\n",$PadGroup) : "%s %u\n",$Key,$Value);
351   };
352   return $Output;
353 };
354
355 #####------------------------- QueryModifications -------------------------#####
356
357 ################################################################################
358 sub SQLHierarchies {
359 ################################################################################
360 ### add exclusion of hierarchy levels (de.alt.ALL) from SQL query by
361 ### amending the WHERE clause if $ShowHierarchies is false (or don't, if it is
362 ### true, accordingly)
363 ### IN : $ShowHierarchies: boolean value
364 ### OUT: SQL code
365   my ($ShowHierarchies) = @_;
366   return $ShowHierarchies ? '' : "AND newsgroup NOT LIKE '%.ALL'";
367 };
368
369 ################################################################################
370 sub GetMaxLenght {
371 ################################################################################
372 ### get length of longest field in future query result
373 ### IN : $DBHandle   : database handel
374 ###      $Table      : table to query
375 ###      $Field      : field to check
376 ###      $WhereClause: WHERE clause
377 ###      @BindVars   : bind variables for WHERE clause
378 ### OUT: $Length: length of longest instnace of $Field
379   my ($DBHandle,$Table,$Field,$WhereClause,@BindVars) = @_;
380   my $DBQuery = $DBHandle->prepare(sprintf("SELECT MAX(LENGTH(%s)) FROM %s WHERE %s",$Field,$Table,$WhereClause));
381   $DBQuery->execute(@BindVars) or warn sprintf("$MySelf: W: Can't get field length for %s from table %s: $DBI::errstr\n",$Field,$Table);
382   my ($Length) = $DBQuery->fetchrow_array;
383   return $Length;
384 };
385
386 ################################################################################
387 sub SQLGroupList {
388 ################################################################################
389 ### explode list of newsgroups separated by : (with wildcards) to a SQL WHERE
390 ### clause
391 ### IN : $Newsgroups: list of newsgroups (group.one.*:group.two:group.three.*)
392 ### OUT: SQL code, list of newsgroups
393   my ($Newsgroups) = @_;
394   $Newsgroups =~ s/\*/%/g;
395   return ('newsgroup LIKE ?', $Newsgroups) if $Newsgroups !~ /:/;
396   my $SQL = '(';
397   my @GroupList = split /:/, $Newsgroups;
398   foreach (@GroupList) {
399      $SQL .= ' OR ' if $SQL gt '(';
400      $SQL .= 'newsgroup LIKE ?';
401   };
402   $SQL .= ')';
403   return ($SQL,@GroupList);
404 };
405
406 #####------------------------------- done ---------------------------------#####
407 1;
408
409
This page took 0.020838 seconds and 3 git commands to generate.