5 # This script will get statistical data on newgroup usage
8 # It is part of the NewsStats package.
10 # Copyright (c) 2010-2013 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.02";
18 # we're in .../bin, so our module is in ../lib
19 push(@INC, dirname($0).'/../lib');
24 use NewsStats qw(:DEFAULT :TimePeriods :Output :SQLHelper ReadGroupList);
27 use Getopt::Long qw(GetOptions);
28 Getopt::Long::config ('bundling');
30 ################################# Main program #################################
32 ### read commandline options
33 my ($OptBoundType,$OptCaptions,$OptCheckgroupsFile,$OptComments,
34 $OptFileTemplate,$OptFormat,$OptGroupBy,$OptGroupsDB,$LowBound,$OptMonth,
35 $OptNewsgroups,$OptOrderBy,$OptReportType,$OptSums,$UppBound,$OptConfFile);
36 GetOptions ('b|boundary=s' => \$OptBoundType,
37 'c|captions!' => \$OptCaptions,
38 'checkgroups=s' => \$OptCheckgroupsFile,
39 'comments!' => \$OptComments,
40 'filetemplate=s' => \$OptFileTemplate,
41 'f|format=s' => \$OptFormat,
42 'g|group-by=s' => \$OptGroupBy,
43 'groupsdb=s' => \$OptGroupsDB,
44 'l|lower=i' => \$LowBound,
45 'm|month=s' => \$OptMonth,
46 'n|newsgroups=s' => \$OptNewsgroups,
47 'o|order-by=s' => \$OptOrderBy,
48 'r|report=s' => \$OptReportType,
49 's|sums!' => \$OptSums,
50 'u|upper=i' => \$UppBound,
51 'conffile=s' => \$OptConfFile,
52 'h|help' => \&ShowPOD,
53 'V|version' => \&ShowVersion) or exit 1;
55 # $OptComments defaults to TRUE
56 $OptComments = 1 if (!defined($OptComments));
57 # force --nocomments when --filetemplate is used
58 $OptComments = 0 if ($OptFileTemplate);
61 if ($OptBoundType =~ /level/i) {
62 $OptBoundType = 'level';
63 } elsif ($OptBoundType =~ /av(era)?ge?/i) {
64 $OptBoundType = 'average';
65 } elsif ($OptBoundType =~ /sums?/i) {
66 $OptBoundType = 'sum';
68 $OptBoundType = 'default';
71 # parse $OptReportType
73 if ($OptReportType =~ /av(era)?ge?/i) {
74 $OptReportType = 'average';
75 } elsif ($OptReportType =~ /sums?/i) {
76 $OptReportType = 'sum';
78 $OptReportType = 'default';
81 # honor $OptCheckgroupsFile,
82 # warn for $OptSums if set concurrently
84 if ($OptCheckgroupsFile) {
85 # read list of newsgroups from --checkgroups
86 # into a hash reference
87 $ValidGroups = &ReadGroupList($OptCheckgroupsFile);
88 &Bleat(1,"--sums option can't possibly work with --checkgroups option set")
92 ### read configuration
93 my %Conf = %{ReadConfig($OptConfFile)};
95 ### override configuration via commandline options
97 $ConfOverride{'DBTableGrps'} = $OptGroupsDB if $OptGroupsDB;
98 &OverrideConfig(\%Conf,\%ConfOverride);
101 my $DBHandle = InitDB(\%Conf,1);
103 ### get time period and newsgroups, prepare SQL 'WHERE' clause
105 # and set caption for output and expression for SQL 'WHERE' clause
106 my ($CaptionPeriod,$SQLWherePeriod) = &GetTimePeriod($OptMonth);
107 # bail out if --month is invalid
108 &Bleat(2,"--month option has an invalid format - ".
109 "please use 'YYYY-MM', 'YYYY-MM:YYYY-MM' or 'ALL'!") if !$CaptionPeriod;
110 # get list of newsgroups and set expression for SQL 'WHERE' clause
111 # with placeholders as well as a list of newsgroup to bind to them
112 my ($SQLWhereNewsgroups,@SQLBindNewsgroups);
113 if ($OptNewsgroups) {
114 ($SQLWhereNewsgroups,@SQLBindNewsgroups) = &SQLGroupList($OptNewsgroups);
115 # bail out if --newsgroups is invalid
116 &Bleat(2,"--newsgroups option has an invalid format!")
117 if !$SQLWhereNewsgroups;
120 ### build SQL WHERE clause (and HAVING clause, if needed)
121 my ($SQLWhereClause,$SQLHavingClause);
122 # $OptBoundType 'level'
123 if ($OptBoundType and $OptBoundType ne 'default') {
124 $SQLWhereClause = SQLBuildClause('where',$SQLWherePeriod,
125 $SQLWhereNewsgroups,&SQLHierarchies($OptSums));
126 $SQLHavingClause = SQLBuildClause('having',&SQLSetBounds($OptBoundType,
127 $LowBound,$UppBound));
128 # $OptBoundType 'threshold' / 'default' or none
130 $SQLWhereClause = SQLBuildClause('where',$SQLWherePeriod,
131 $SQLWhereNewsgroups,&SQLHierarchies($OptSums),
132 &SQLSetBounds('default',$LowBound,$UppBound));
135 ### get sort order and build SQL 'ORDER BY' clause
136 # force to 'month' for $OptReportType 'average' or 'sum'
137 $OptGroupBy = 'month' if ($OptReportType and $OptReportType ne 'default');
138 # default to 'newsgroup' for $OptBoundType 'level' or 'average'
139 $OptGroupBy = 'newsgroup' if (!$OptGroupBy and
140 $OptBoundType and $OptBoundType ne 'default');
141 # default to 'newsgroup' if $OptGroupBy is not set and
142 # just one newsgroup is requested, but more than one month
143 $OptGroupBy = 'newsgroup' if (!$OptGroupBy and $OptMonth and $OptMonth =~ /:/
144 and $OptNewsgroups and $OptNewsgroups !~ /[:*%]/);
145 # parse $OptGroupBy to $GroupBy, create ORDER BY clause $SQLOrderClause
146 # if $OptGroupBy is still not set, SQLSortOrder() will default to 'month'
147 my ($GroupBy,$SQLOrderClause) = SQLSortOrder($OptGroupBy, $OptOrderBy);
148 # $GroupBy will contain 'month' or 'newsgroup' (parsed result of $OptGroupBy)
149 # set it to 'month' or 'key' for OutputData()
150 $GroupBy = ($GroupBy eq 'month') ? 'month' : 'key';
152 ### get report type and build SQL 'SELECT' query
154 my $SQLGroupClause = '';
155 my $Precision = 0; # number of digits right of decimal point for output
156 if ($OptReportType and $OptReportType ne 'default') {
157 $SQLGroupClause = 'GROUP BY newsgroup';
158 # change $SQLOrderClause: replace everything before 'postings'
159 $SQLOrderClause =~ s/BY.+postings/BY postings/;
160 if ($OptReportType eq 'average') {
161 $SQLSelect = "'All months',newsgroup,AVG(postings)";
163 # change $SQLOrderClause: replace 'postings' with 'AVG(postings)'
164 $SQLOrderClause =~ s/postings/AVG(postings)/;
165 } elsif ($OptReportType eq 'sum') {
166 $SQLSelect = "'All months',newsgroup,SUM(postings)";
167 # change $SQLOrderClause: replace 'postings' with 'SUM(postings)'
168 $SQLOrderClause =~ s/postings/SUM(postings)/;
171 $SQLSelect = 'month,newsgroup,postings';
174 ### get length of longest newsgroup name delivered by query
175 ### for formatting purposes
176 my $Field = ($GroupBy eq 'month') ? 'newsgroup' : 'month';
177 my ($MaxLength,$MaxValLength) = &GetMaxLength($DBHandle,$Conf{'DBTableGrps'},
178 $Field,'postings',$SQLWhereClause,
182 ### build and execute SQL query
184 # special query preparation for $OptBoundType 'level', 'average' or 'sums'
185 if ($OptBoundType and $OptBoundType ne 'default') {
186 # prepare and execute first query:
187 # get list of newsgroups meeting level conditions
188 $DBQuery = $DBHandle->prepare(sprintf('SELECT newsgroup FROM %s.%s %s '.
189 'GROUP BY newsgroup %s',
190 $Conf{'DBDatabase'},$Conf{'DBTableGrps'},
191 $SQLWhereClause,$SQLHavingClause));
192 $DBQuery->execute(@SQLBindNewsgroups)
193 or &Bleat(2,sprintf("Can't get groups data for %s from %s.%s: %s\n",
194 $CaptionPeriod,$Conf{'DBDatabase'},$Conf{'DBTableGrps'},
196 # add newsgroups to a comma-seperated list ready for IN(...) query
198 while (my ($Newsgroup) = $DBQuery->fetchrow_array) {
199 $GroupList .= ',' if $GroupList;
200 $GroupList .= "'$Newsgroup'";
202 # enhance $WhereClause
204 $SQLWhereClause = SQLBuildClause('where',$SQLWhereClause,
205 sprintf('newsgroup IN (%s)',$GroupList));
207 # condition cannot be satisfied;
208 # force query to fail by adding '0=1'
209 $SQLWhereClause = SQLBuildClause('where',$SQLWhereClause,'0=1');
214 $DBQuery = $DBHandle->prepare(sprintf('SELECT %s FROM %s.%s %s %s %s',
216 $Conf{'DBDatabase'},$Conf{'DBTableGrps'},
217 $SQLWhereClause,$SQLGroupClause,
221 $DBQuery->execute(@SQLBindNewsgroups)
222 or &Bleat(2,sprintf("Can't get groups data for %s from %s.%s: %s\n",
223 $CaptionPeriod,$Conf{'DBDatabase'},$Conf{'DBTableGrps'},
227 # set default to 'pretty'
228 $OptFormat = 'pretty' if !$OptFormat;
229 # print captions if --caption is set
230 if ($OptCaptions && $OptComments) {
231 # print time period with report type
232 my $CaptionReportType= '(number of postings for each month)';
233 if ($OptReportType and $OptReportType ne 'default') {
234 $CaptionReportType= '(average number of postings for each month)'
235 if $OptReportType eq 'average';
236 $CaptionReportType= '(number of all postings for that time period)'
237 if $OptReportType eq 'sum';
239 printf("# ----- Report for %s %s\n",$CaptionPeriod,$CaptionReportType);
240 # print newsgroup list if --newsgroups is set
241 printf("# ----- Newsgroups: %s\n",join(',',split(/:/,$OptNewsgroups)))
243 # print boundaries, if set
244 my $CaptionBoundary= '(counting only month fulfilling this condition)';
245 if ($OptBoundType and $OptBoundType ne 'default') {
246 $CaptionBoundary= '(every single month)' if $OptBoundType eq 'level';
247 $CaptionBoundary= '(on average)' if $OptBoundType eq 'average';
248 $CaptionBoundary= '(all month summed up)' if $OptBoundType eq 'sum';
250 printf("# ----- Threshold: %s %s x %s %s %s\n",
251 $LowBound ? $LowBound : '',$LowBound ? '=>' : '',
252 $UppBound ? '<=' : '',$UppBound ? $UppBound : '',$CaptionBoundary)
253 if ($LowBound or $UppBound);
254 # print primary and secondary sort order
255 printf("# ----- Grouped by %s (%s), sorted %s%s\n",
256 ($GroupBy eq 'month') ? 'Months' : 'Newsgroups',
257 ($OptGroupBy and $OptGroupBy =~ /-?desc$/i) ? 'descending' : 'ascending',
258 ($OptOrderBy and $OptOrderBy =~ /posting/i) ? 'by number of postings ' : '',
259 ($OptOrderBy and $OptOrderBy =~ /-?desc$/i) ? 'descending' : 'ascending');
263 &OutputData($OptFormat,$OptComments,$GroupBy,$Precision,
264 $OptCheckgroupsFile ? $ValidGroups : '',
265 $OptFileTemplate,$DBQuery,$MaxLength,$MaxValLength);
268 $DBHandle->disconnect;
272 ################################ Documentation #################################
276 groupstats - create reports on newsgroup usage
280 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>] [B<--conffile> I<filename>]
288 This script create reports on newsgroup usage (number of postings per
289 group per month) taken from result tables created by
292 =head2 Features and options
294 =head3 Time period and newsgroups
296 The time period to act on defaults to last month; you can assign another
297 time period or a single month (or drop all time constraints) via the
298 B<--month> option (see below).
300 B<groupstats> will process all newsgroups by default; you can limit
301 processing to only some newsgroups by supplying a list of those groups via
302 B<--newsgroups> option (see below). You can include hierarchy levels in
303 the output by adding the B<--sums> switch (see below). Optionally
304 newsgroups not present in a checkgroups file can be excluded from output,
305 sse B<--checkgroups> below.
309 You can choose between different B<--report> types: postings per month,
310 average postings per month or all postings summed up; for details, see
313 =head3 Upper and lower boundaries
315 Furthermore you can set an upper and/or lower boundary to exclude some
316 results from output via the B<--lower> and B<--upper> options,
317 respectively. By default, all newsgroups with more and/or less postings
318 per month will be excluded from the result set (i.e. not shown and not
319 considered for average and sum reports). You can change the meaning of
320 those boundaries with the B<--boundary> option. For details, please see
323 =head3 Sorting and formatting the output
325 By default, all results are grouped by month; you can group results by
326 newsgroup instead via the B<--groupy-by> option. Within those groups, the
327 list of newsgroups (or months) is sorted alphabetically (or
328 chronologically, respectively) ascending. You can change that order (and
329 sort by number of postings) with the B<--order-by> option. For details and
330 exceptions, please see below.
332 The results will be formatted as a kind of table; you can change the
333 output format to a simple list or just a list of newsgroups and number of
334 postings with the B<--format> option. Captions will be added by means of
335 the B<--caption> option; all comments (and captions) can be supressed by
336 using B<--nocomments>.
338 Last but not least you can redirect all output to a number of files, e.g.
339 one for each month, by submitting the B<--filetemplate> option, see below.
340 Captions and comments are automatically disabled in this case.
344 B<groupstats> will read its configuration from F<newsstats.conf>
345 which should be present in the same directory via Config::Auto.
347 See doc/INSTALL for an overview of possible configuration options.
349 You can override some configuration options via the B<--groupsdb> option.
355 =item B<-V>, B<--version>
357 Print out version and copyright information and exit.
359 =item B<-h>, B<--help>
361 Print this man page and exit.
363 =item B<-m>, B<--month> I<YYYY-MM[:YYYY-MM]|all>
365 Set processing period to a single month in YYYY-MM format or to a time
366 period between two month in YYYY-MM:YYYY-MM format (two month, separated
367 by a colon). By using the keyword I<all> instead, you can set no
368 processing period to process the whole database.
370 =item B<-n>, B<--newsgroups> I<newsgroup(s)>
372 Limit processing to a certain set of newsgroups. I<newsgroup(s)> can
373 be a single newsgroup name (de.alt.test), a newsgroup hierarchy
374 (de.alt.*) or a list of either of these, separated by colons, for
377 de.test:de.alt.test:de.newusers.*
379 =item B<-s>, B<--sums|--nosums> (sum per hierarchy level)
381 Include "virtual" groups for every hierarchy level in output, for
388 See the B<gatherstats> man page for details.
390 This option does not work together with the B<--checkgroups> option as
391 all "virtual" groups will not be present in the checkgroups file.
393 =item B<--checkgroups> I<filename>
395 Restrict output to those newgroups present in a file in checkgroups format
396 (one newgroup name per line; everything after the first whitespace on each
397 line is ignored). All other newsgroups will be removed from output.
399 Contrary to B<gatherstats>, I<filename> is not a template, but refers to
400 a single file in checkgroups format.
402 The B<--sums> option will not work together with this option as "virtual"
403 groups will not be present in the checkgroups file.
405 =item B<-r>, B<--report> I<default|average|sums>
407 Choose the report type: I<default>, I<average> or I<sums>
409 By default, B<groupstats> will report the number of postings for each
410 newsgroup in each month. But it can also report the average number of
411 postings per group for all months or the total sum of postings per group
414 For report types I<average> and I<sums>, the B<group-by> option has no
415 meaning and will be silently ignored (see below).
417 =item B<-l>, B<--lower> I<lower boundary>
419 Set the lower boundary. See B<--boundary> below.
421 =item B<-l>, B<--upper> I<upper boundary>
423 Set the upper boundary. See B<--boundary> below.
425 =item B<-b>, B<--boundary> I<boundary type>
427 Set the boundary type to one of I<default>, I<level>, I<average> or
430 By default, all newsgroups with more postings per month than the upper
431 boundary and/or less postings per month than the lower boundary will be
432 excluded from further processing. For the default report that means each
433 month only newsgroups with a number of postings between the boundaries
434 will be displayed. For the other report types, newsgroups with a number of
435 postings exceeding the boundaries in all (!) months will not be
438 For example, lets take a list of newsgroups like this:
441 de.comp.datenbanken.misc 6
442 de.comp.datenbanken.ms-access 84
443 de.comp.datenbanken.mysql 88
445 de.comp.datenbanken.misc 8
446 de.comp.datenbanken.ms-access 126
447 de.comp.datenbanken.mysql 21
449 de.comp.datenbanken.misc 24
450 de.comp.datenbanken.ms-access 83
451 de.comp.datenbanken.mysql 36
453 With C<groupstats --month 2012-01:2012-03 --lower 25 --report sums>,
454 you'll get the following result:
457 de.comp.datenbanken.ms-access 293
458 de.comp.datenbanken.mysql 124
460 de.comp.datenbanken.misc has not been considered even though it has 38
461 postings in total, because it has less than 25 postings in every single
462 month. If you want to list all newsgroups with more than 25 postings
463 I<in total>, you'll have to set the boundary type to I<sum>, see below.
465 A boundary type of I<level> will show only those newsgroups - at all -
466 that satisfy the boundaries in each and every single month. With the above
467 list of newsgroups and
468 C<groupstats --month 2012-01:2012-03 --lower 25 --boundary level --report sums>,
469 you'll get this result:
472 de.comp.datenbanken.ms-access 293
474 de.comp.datenbanken.mysql has not been considered because it had less than
475 25 postings in 2012-02 (only).
477 You can use that to get a list of newsgroups that have more (or less) then
478 x postings in every month during the whole reporting period.
480 A boundary type of I<average> will show only those newsgroups - at all -that
481 satisfy the boundaries on average. With the above list of newsgroups and
482 C<groupstats --month 2012-01:2012-03 --lower 25 --boundary avg --report sums>,
483 you'll get this result:
486 de.comp.datenbanken.ms-access 293
487 de.comp.datenbanken.mysql 145
489 The average number of postings in the three groups is:
491 de.comp.datenbanken.misc 12.67
492 de.comp.datenbanken.ms-access 97.67
493 de.comp.datenbanken.mysql 48.33
495 Last but not least, a boundary type of I<sums> will show only those
496 newsgroups - at all - that satisfy the boundaries with the total sum of
497 all postings during the reporting period. With the above list of
499 C<groupstats --month 2012-01:2012-03 --lower 25 --boundary sum --report sums>,
500 you'll finally get this result:
503 de.comp.datenbanken.misc 38
504 de.comp.datenbanken.ms-access 293
505 de.comp.datenbanken.mysql 145
508 =item B<-g>, B<--group-by> I<month[-desc]|newsgroups[-desc]>
510 By default, all results are grouped by month, sorted chronologically in
511 ascending order, like this:
514 de.comp.datenbanken.ms-access 84
515 de.comp.datenbanken.mysql 88
517 de.comp.datenbanken.ms-access 126
518 de.comp.datenbanken.mysql 21
520 The results can be grouped by newsgroups instead via
521 B<--group-by> I<newsgroup>:
523 ----- de.comp.datenbanken.ms-access:
526 ----- de.comp.datenbanken.mysql:
530 By appending I<-desc> to the group-by option parameter, you can reverse
531 the sort order - e.g. B<--group-by> I<month-desc> will give:
534 de.comp.datenbanken.ms-access 126
535 de.comp.datenbanken.mysql 21
537 de.comp.datenbanken.ms-access 84
538 de.comp.datenbanken.mysql 88
540 Average and sums reports (see above) will always be grouped by months;
541 this option will therefore be ignored.
543 =item B<-o>, B<--order-by> I<default[-desc]|postings[-desc]>
545 Within each group (a single month or single newsgroup, see above), the
546 report will be sorted by newsgroup names in ascending alphabetical order
547 by default. You can change the sort order to descending or sort by number
550 =item B<-f>, B<--format> I<pretty|list|dump>
552 Select the output format, I<pretty> being the default:
555 de.comp.datenbanken.ms-access 84
556 de.comp.datenbanken.mysql 88
558 de.comp.datenbanken.ms-access 126
559 de.comp.datenbanken.mysql 21
561 I<list> format looks like this:
563 2012-01 de.comp.datenbanken.ms-access 84
564 2012-01 de.comp.datenbanken.mysql 88
565 2012-02 de.comp.datenbanken.ms-access 126
566 2012-02 de.comp.datenbanken.mysql 21
568 And I<dump> format looks like this:
571 de.comp.datenbanken.ms-access 84
572 de.comp.datenbanken.mysql 88
574 de.comp.datenbanken.ms-access 126
575 de.comp.datenbanken.mysql 21
577 You can remove the comments by using B<--nocomments>, see below.
579 =item B<-c>, B<--captions|--nocaptions>
581 Add captions to output, like this:
583 ----- Report for 2012-01 to 2012-02 (number of postings for each month)
584 ----- Newsgroups: de.comp.datenbanken.*
585 ----- Threshold: 10 => x <= 20 (on average)
586 ----- Grouped by Newsgroups (ascending), sorted by number of postings descending
590 =item B<--comments|--nocomments>
592 Add comments (group headers) to I<dump> and I<pretty> output. True by default.
594 Use I<--nocomments> to suppress anything except newsgroup names/months and
595 numbers of postings. This is enforced when using B<--filetemplate>, see below.
597 =item B<--filetemplate> I<filename template>
599 Save output to file(s) instead of dumping it to STDOUT. B<groupstats> will
600 create one file for each month (or each newsgroup, accordant to the
601 setting of B<--group-by>, see above), with filenames composed by adding
602 year and month (or newsgroup names) to the I<filename template>, for
603 example with B<--filetemplate> I<stats>:
609 B<--nocomments> is enforced, see above.
611 =item B<--groupsdb> I<database table>
613 Override I<DBTableGrps> from F<newsstats.conf>.
615 =item B<--conffile> I<filename>
617 Load configuration from I<filename> instead of F<newsstats.conf>.
627 Show number of postings per group for lasth month in I<pretty> format:
631 Show that report for January of 2010 and de.alt.* plus de.test,
632 including display of hierarchy levels:
634 groupstats --month 2010-01 --newsgroups de.alt.*:de.test --sums
636 Only show newsgroups with 30 postings or less last month, ordered
637 by number of postings, descending, in I<pretty> format:
639 groupstats --upper 30 --order-by postings-desc
641 Show the total of all postings for the year of 2010 for all groups that
642 had 30 postings or less in every single month in that year, ordered by
643 number of postings in descending order:
645 groupstats -m 2010-01:2010-12 -u 30 -b level -r sums -o postings-desc
647 The same for the average number of postings in the year of 2010:
649 groupstats -m 2010-01:2010-12 -u 30 -b level -r avg -o postings-desc
651 List number of postings per group for eacht month of 2010 and redirect
652 output to one file for each month, namend stats-2010-01 and so on, in
653 machine-readable form (without formatting):
655 groupstats -m 2010-01:2010-12 -f dump --filetemplate stats
662 =item F<bin/groupstats.pl>
666 =item F<lib/NewsStats.pm>
668 Library functions for the NewsStats package.
670 =item F<etc/newsstats.conf>
672 Runtime configuration file.
678 Please report any bugs or feature requests to the author or use the
679 bug tracker at L<http://bugs.th-h.de/>!
699 This script is part of the B<NewsStats> package.
703 Thomas Hochstein <thh@inter.net>
705 =head1 COPYRIGHT AND LICENSE
707 Copyright (c) 2010-2013 Thomas Hochstein <thh@inter.net>
709 This program is free software; you may redistribute it and/or modify it
710 under the same terms as Perl itself.