empty program arguments do not need to be parsed
[barcat.git] / barcat
1 #!/usr/bin/perl -CA
2 use 5.014;
3 use warnings;
4 use utf8;
5 use List::Util qw( min max sum );
6 use open qw( :std :utf8 );
7 use re '/msx';
8
9 our $VERSION = '1.07';
10
11 my %opt;
12 if (@ARGV) {
13 require Getopt::Long;
14 Getopt::Long->import('2.33', qw( :config gnu_getopt ));
15 GetOptions(\%opt,
16         'ascii|a!',
17         'color|c!',
18         'C' => sub { $opt{color} = 0 },
19         'field|f=s' => sub {
20                 eval {
21                         local $_ = $_[1];
22                         $opt{anchor} = /\A[0-9]+\z/ ? qr/(?:\S*\h+){$_}\K/ : qr/$_/;
23                 } or die $@ =~ s/(?:\ at\ \N+)?\Z/ for option $_[0]/r;
24         },
25         'human-readable|H!',
26         'interval|t:i',
27         'trim|length|l=s' => sub {
28                 my ($optname, $optval) = @_;
29                 $optval =~ s/%$// and $opt{trimpct}++;
30                 $optval =~ m/\A-?[0-9]+\z/ or die(
31                         "Value \"$optval\" invalid for option $optname",
32                         " (number or percentage expected)\n"
33                 );
34                 $opt{trim} = $optval;
35         },
36         'value-length=i',
37         'hidemin=i',
38         'hidemax=i',
39         'minval=f',
40         'maxval=f',
41         'limit|L:s' => sub {
42                 my ($optname, $optval) = @_;
43                 $optval ||= 0;
44                 $optval =~ /\A-[0-9]+\z/ and $optval .= '-';  # tail shorthand
45                 ($opt{hidemin}, $opt{hidemax}) =
46                 $optval =~ m/\A (?: (-? [0-9]+)? - )? ([0-9]+)? \z/ or die(
47                         "Value \"$optval\" invalid for option limit",
48                         " (range expected)\n"
49                 );
50         },
51         'header!',
52         'markers|m=s',
53         'graph-format=s' => sub {
54                 $opt{'graph-format'} = substr $_[1], 0, 1;
55         },
56         'spark:s' => sub {
57                 $opt{spark} = [split //,
58                         $_[1] || ($opt{ascii} ? ' ..oOO' : ' ▁▂▃▄▅▆▇█')
59                 ];
60         },
61         'palette=s' => sub {
62                 $opt{palette} = {
63                         fire   => [qw( 90 31 91 33 93 97 96 )],
64                         fire88 => [map {"38;5;$_"} qw(
65                                 80  32 48 64  68 72 76  77 78 79  47
66                         )],
67                         fire256=> [map {"38;5;$_"} qw(
68                                 235  52 88 124 160 196
69                                 202 208 214 220 226  227 228 229 230 231  159
70                         )],
71                         ramp88 => [map {"38;5;$_"} qw(
72                                 64 65 66 67 51 35 39 23 22 26 25 28
73                         )],
74                         whites => [qw( 1;30 0;37 1;37 )],
75                         greys  => [map {"38;5;$_"} 52, 235..255, 47],
76                 }->{$_[1]} // [ split /[^0-9;]/, $_[1] ];
77         },
78         'stat|s!',
79         'signal-stat=s',
80         'unmodified|u!',
81         'width|w=i',
82         'version' => sub {
83                 say "barcat version $VERSION";
84                 exit;
85         },
86         'usage|h' => sub {
87                 /^=/ ? last : print for readline *DATA;  # text between __END__ and pod
88                 exit;
89         },
90         'help|?'  => sub {
91                 require Pod::Usage;
92                 Pod::Usage::pod2usage(
93                         -exitval => 0, -perldocopt => '-oman', -verbose => 2,
94                 );
95         },
96 ) or exit 64;  # EX_USAGE
97 }
98
99 $opt{width} ||= $ENV{COLUMNS} || qx(tput cols) || 80 unless $opt{spark};
100 $opt{color} //= -t *STDOUT;  # enable on tty
101 $opt{'graph-format'} //= '-';
102 $opt{trim}   *= $opt{width} / 100 if $opt{trimpct};
103 $opt{units}   = [split //, ' kMGTPEZYyzafpn'.($opt{ascii} ? 'u' : 'μ').'m']
104         if $opt{'human-readable'};
105 $opt{anchor} //= qr/\A/;
106 $opt{'value-length'} = 6 if $opt{units};
107 $opt{'value-length'} = 1 if $opt{unmodified};
108 $opt{'signal-stat'} //= exists $SIG{INFO} ? 'INFO' : 'QUIT';
109 $opt{markers} //= '=avg >31.73v <68.27v +50v |0';
110 $opt{palette} //= $opt{color} && [31, 90, 32];
111 $opt{hidemin} = ($opt{hidemin} || 1) - 1;
112 $opt{input} = (@ARGV && $ARGV[0] =~ m/\A[-0-9]/) ? \@ARGV : undef
113         and undef $opt{interval};
114
115 $opt{'sum-format'} = sub { sprintf '%.8g', $_[0] };
116 $opt{'calc-format'} = sub { sprintf '%*.*f', 0, 2, $_[0] };
117 $opt{'value-format'} = $opt{units} && sub {
118         my $unit = (
119                 log(abs $_[0] || 1) / log(10)
120                 - 3 * (abs($_[0]) < .9995)   # shift to smaller unit if below 1
121                 + 1e-15  # float imprecision
122         );
123         my $decimal = ($unit % 3) == ($unit < 0);
124         $unit -= log($decimal ? .995 : .9995) / log(10);  # rounded
125         $decimal = ($unit % 3) == ($unit < 0);
126         $decimal &&= $_[0] !~ /^-?0*[0-9]{1,3}$/;  # integer 0..999
127         sprintf('%*.*f%1s',
128                 3 + ($_[0] < 0), # digits plus optional negative sign
129                 $decimal,  # tenths
130                 $_[0] / 1000 ** int($unit/3),  # number
131                 $#{$opt{units}} * 1.5 < abs $unit ? sprintf('e%d', $unit) :
132                         $opt{units}->[$unit/3]  # suffix
133         );
134 };
135
136
137 my (@lines, @values, @order);
138
139 $SIG{$_} = \&show_stat for $opt{'signal-stat'} || ();
140 $SIG{ALRM} = sub {
141         show_lines();
142         alarm $opt{interval} if defined $opt{interval} and $opt{interval} > 0;
143 };
144 $SIG{INT} = \&show_exit;
145
146 if (defined $opt{interval}) {
147         $opt{interval} ||= 1;
148         alarm $opt{interval} if $opt{interval} > 0;
149
150         eval {
151                 require Tie::Array::Sorted;
152                 tie @order, 'Tie::Array::Sorted', sub { $_[1] <=> $_[0] };
153         } or warn $@, "Expect slowdown with large datasets!\n";
154 }
155
156 my $valmatch = qr<
157         $opt{anchor} ( \h* -? [0-9]* [.]? [0-9]+ (?: e[+-]?[0-9]+ )? |)
158 >x;
159 while (defined ($_ = $opt{input} ? shift @{ $opt{input} } : readline)) {
160         s/\r?\n\z//;
161         s/\A\h*// unless $opt{unmodified};
162         my $valnum = s/$valmatch/\n/ && $1;
163         push @values, $valnum;
164         push @order, $valnum if length $valnum;
165         if (defined $opt{trim} and defined $valnum) {
166                 my $trimpos = abs $opt{trim};
167                 $trimpos -= length $valnum if $opt{unmodified};
168                 if ($trimpos <= 1) {
169                         $_ = substr $_, 0, 2;
170                 }
171                 elsif (length > $trimpos) {
172                         # cut and replace (intentional lvalue for speed, contrary to PBP)
173                         substr($_, $trimpos - 1) = $opt{ascii} ? '>' : '…';
174                 }
175         }
176         push @lines, $_;
177         show_lines() if defined $opt{interval} and $opt{interval} < 0
178                 and $. % $opt{interval} == 0;
179 }
180
181 if ($opt{'zero-missing'}) {
182         push @values, (0) x 10;
183 }
184
185 $SIG{INT} = 'DEFAULT';
186
187 sub color {
188         $opt{color} and defined $_[0] or return '';
189         return "\e[$_[0]m" if defined wantarray;
190         $_ = color(@_) . $_ . color(0) if defined;
191 }
192
193 sub show_lines {
194
195 state $nr =
196         $opt{hidemin} < 0 ? @lines + $opt{hidemin} + 1 :
197         $opt{hidemin};
198 @lines > $nr or return;
199
200 my $limit = $#lines;
201 if (defined $opt{hidemax}) {
202         if ($opt{hidemin} and $opt{hidemin} < 0) {
203                 $limit -= $opt{hidemax} - 1;
204         }
205         else {
206                 $limit = $opt{hidemax} - 1;
207         }
208 }
209
210 @order = sort { $b <=> $a } @order unless tied @order;
211 my $maxval = $opt{maxval} // (
212         $opt{hidemax} ? max grep { length } @values[$nr .. $limit] :
213         $order[0]
214 ) // 0;
215 my $minval = $opt{minval} // min $order[-1] // (), 0;
216 my $range = $maxval - $minval;
217 my $lenval = $opt{'value-length'} // max map { length } @order;
218 my $len    = defined $opt{trim} && $opt{trim} <= 0 ? -$opt{trim} + 1 :
219         max map { length $values[$_] && length $lines[$_] }
220                 0 .. min $#lines, $opt{hidemax} || ();  # left padding
221 my $size   = defined $opt{width} && $range &&
222         ($opt{width} - $lenval - $len) / $range;  # bar multiplication
223
224 my @barmark;
225 if ($opt{markers} and $size > 0) {
226         for my $markspec (split /\h/, $opt{markers}) {
227                 my ($char, $func) = split //, $markspec, 2;
228                 my $pos = eval {
229                         if ($func eq 'avg') {
230                                 return sum(@order) / @order;
231                         }
232                         elsif ($func =~ /\A([0-9.]+)v\z/) {
233                                 die "Invalid marker $char: percentile $1 out of bounds\n" if $1 > 100;
234                                 my $index = $#order * $1 / 100;
235                                 return ($order[$index] + $order[$index + .5]) / 2;
236                         }
237                         elsif ($func =~ /\A-?[0-9.]+\z/) {
238                                 return $func;
239                         }
240                         else {
241                                 die "Unknown marker $char: $func\n";
242                         }
243                 };
244                 defined $pos or do {
245                         warn $@ if $@;
246                         next;
247                 };
248                 $pos -= $minval;
249                 $pos >= 0 or next;
250                 color(36) for $barmark[$pos * $size] = $char;
251         }
252
253         state $lastmax = $maxval;
254         if ($maxval > $lastmax) {
255                 print ' ' x ($lenval + $len);
256                 printf color(90);
257                 printf '%-*s',
258                         ($lastmax - $minval) * $size + .5,
259                         '-' x (($values[$nr - 1] - $minval) * $size);
260                 print color(92);
261                 say '+' x (($range - $lastmax) * $size + .5);
262                 print color(0);
263                 $lastmax = $maxval;
264         }
265 }
266
267 say(
268         color(31), sprintf('%*s', $lenval, $minval),
269         color(90), '-', color(36), '+',
270         color(32), sprintf('%*s', $size * $range - 3, $maxval),
271         color(90), '-', color(36), '+',
272         color(0),
273 ) if $opt{header};
274
275 while ($nr <= $limit) {
276         my $val = $values[$nr];
277         my $rel = length $val && $range && ($val - $minval) / $range;
278         my $color = !length $val || !$opt{palette} ? undef :
279                 $val == $order[0] ? $opt{palette}->[-1] : # max
280                 $val == $order[-1] ? $opt{palette}->[0] : # min
281                 $opt{palette}->[ $rel * ($#{$opt{palette}} - 1) + 1 ];
282
283         if ($opt{spark}) {
284                 say '' if $opt{width} and $nr and $nr % $opt{width} == 0;
285                 print color($color), $opt{spark}->[
286                         !$val || !$#{$opt{spark}} ? 0 : # blank
287                         $val == $order[0] ? -1 : # max
288                         $val == $order[-1] ? 1 : # min
289                         $#{$opt{spark}} < 3 ? 1 :
290                         $rel * ($#{$opt{spark}} - 3) + 2.5
291                 ];
292                 next;
293         }
294
295         if (length $val) {
296                 $val = $opt{'value-format'} ? $opt{'value-format'}->($val) :
297                         sprintf "%*s", $lenval, $val;
298                 color($color) for $val;
299         }
300         my $line = $lines[$nr] =~ s/\n/$val/r;
301         if (not length $val) {
302                 say $line;
303                 next;
304         }
305         printf '%-*s', $len + length($val), $line;
306         print $barmark[$_] // $opt{'graph-format'}
307                 for 1 .. $size && (($values[$nr] || 0) - $minval) * $size + .5;
308         say '';
309 }
310 continue {
311         $nr++;
312 }
313 say $opt{palette} ? color(0) : '' if $opt{spark};
314
315         return $nr;
316 }
317
318 sub show_stat {
319         if ($opt{hidemin} or $opt{hidemax}) {
320                 my $linemin = $opt{hidemin};
321                 my $linemax = ($opt{hidemax} || @lines) - 1;
322                 if ($linemin < 0) {
323                         $linemin += @lines;
324                         $linemax = @lines - $linemax;
325                 }
326                 printf '%.8g of ', $opt{'sum-format'}->(
327                         sum(grep {length} @values[$linemin .. $linemax]) // 0
328                 );
329         }
330         if (@order) {
331                 my $total = sum @order;
332                 printf '%s total', color(1) . $opt{'sum-format'}->($total) . color(0);
333                 printf ' in %d values', scalar @order;
334                 printf ' over %d lines', scalar @lines if @order != @lines;
335                 printf(' (%s min, %s avg, %s max)',
336                         color(31) . ($opt{'value-format'} || sub {$_[0]})->($order[-1]) . color(0),
337                         color(36) . ($opt{'value-format'} || $opt{'calc-format'})->($total / @order) . color(0),
338                         color(32) . ($opt{'value-format'} || sub {$_[0]})->($order[0]) . color(0),
339                 );
340         }
341         say '';
342         return 1;
343 }
344
345 sub show_exit {
346         show_lines();
347         show_stat() if $opt{stat};
348         exit 130 if @_;  # 0x80+signo
349         exit;
350 }
351
352 show_exit();
353
354 __END__
355 Usage:
356   barcat [OPTIONS] [FILES|NUMBERS]
357
358 Options:
359   -a, --[no-]ascii         Restrict user interface to ASCII characters
360   -c, --[no-]color         Force colored output of values and bar markers
361   -f, --field=(N|REGEXP)   Compare values after a given number of whitespace
362                            separators
363       --header             Prepend a chart axis with minimum and maximum
364                            values labeled
365   -H, --human-readable     Format values using SI unit prefixes
366   -t, --interval[=(N|-LINES)]
367                            Output partial progress every given number of
368                            seconds or input lines
369   -l, --length=[-]SIZE[%]  Trim line contents (between number and bars)
370   -L, --limit[=(N|-LAST|START-[END])]
371                            Stop output after a number of lines
372       --graph-format=CHAR  Glyph to repeat for the graph line
373   -m, --markers=FORMAT     Statistical positions to indicate on bars
374       --min=N, --max=N     Bars extend from 0 or the minimum value if lower
375       --palette=(PRESET|COLORS)
376                            Override colors of parsed numbers
377       --spark[=CHARS]      Replace lines by sparklines
378   -s, --stat               Total statistics after all data
379   -u, --unmodified         Do not reformat values, keeping leading whitespace
380       --value-length=SIZE  Reserved space for numbers
381   -w, --width=COLUMNS      Override the maximum number of columns to use
382   -h, --usage              Overview of available options
383       --help               Full documentation
384       --version            Version information
385
386 =encoding utf8
387
388 =head1 NAME
389
390 barcat - graph to visualize input values
391
392 =head1 SYNOPSIS
393
394 B<barcat> [<options>] [<file>... | <numbers>]
395
396 =head1 DESCRIPTION
397
398 Visualizes relative sizes of values read from input
399 (parameters, file(s) or STDIN).
400 Contents are concatenated similar to I<cat>,
401 but numbers are reformatted and a bar graph is appended to each line.
402
403 Don't worry, barcat does not drink and divide.
404 It can has various options for input and output (re)formatting,
405 but remains limited to one-dimensional charts.
406 For more complex graphing needs
407 you'll need a larger animal like I<gnuplot>.
408
409 =head1 OPTIONS
410
411 =over
412
413 =item -a, --[no-]ascii
414
415 Restrict user interface to ASCII characters,
416 replacing default UTF-8 by their closest approximation.
417 Input is always interpreted as UTF-8 and shown as is.
418
419 =item -c, --[no-]color
420
421 Force colored output of values and bar markers.
422 Defaults on if output is a tty,
423 disabled otherwise such as when piped or redirected.
424
425 =item -f, --field=(<number> | <regexp>)
426
427 Compare values after a given number of whitespace separators,
428 or matching a regular expression.
429
430 Unspecified or I<-f0> means values are at the start of each line.
431 With I<-f1> the second word is taken instead.
432 A string can indicate the starting position of a value
433 (such as I<-f:> if preceded by colons),
434 or capture the numbers itself,
435 for example I<-f'(\d+)'> for the first digits anywhere.
436
437 =item --header
438
439 Prepend a chart axis with minimum and maximum values labeled.
440
441 =item -H, --human-readable
442
443 Format values using SI unit prefixes,
444 turning long numbers like I<12356789> into I<12.4M>.
445 Also changes an exponent I<1.602176634e-19> to I<160.2z>.
446 Short integers are aligned but kept without decimal point.
447
448 =item -t, --interval[=(<seconds> | -<lines>)]
449
450 Output partial progress every given number of seconds or input lines.
451 An update can also be forced by sending a I<SIGALRM> alarm signal.
452
453 =item -l, --length=[-]<size>[%]
454
455 Trim line contents (between number and bars)
456 to a maximum number of characters.
457 The exceeding part is replaced by an abbreviation sign,
458 unless C<--length=0>.
459
460 Prepend a dash (i.e. make negative) to enforce padding
461 regardless of encountered contents.
462
463 =item -L, --limit[=(<count> | -<last> | <start>-[<end>])]
464
465 Stop output after a number of lines.
466 A single value indicates the last line number (like C<head>),
467 or first line counting from the bottom if negative (like C<tail>).
468 A specific range can be given by two values.
469
470 All input is still counted and analyzed for statistics,
471 but disregarded for padding and bar size.
472
473 =item --graph-format=<character>
474
475 Glyph to repeat for the graph line.
476 Defaults to a dash C<->.
477
478 =item -m, --markers=<format>
479
480 Statistical positions to indicate on bars.
481 A single indicator glyph precedes each position:
482
483 =over 2
484
485 =item <number>
486
487 Exact value to match on the axis.
488 A vertical bar at the zero crossing is displayed by I<|0>
489 for negative values.
490 For example I<:3.14> would show a colon at pi.
491
492 =item <percentage>I<v>
493
494 Ranked value at the given percentile.
495 The default shows I<+> at I<50v> for the mean or median;
496 the middle value or average between middle values.
497 One standard deviation right of the mean is at about I<68.3v>.
498 The default includes I<< >31.73v <68.27v >>
499 to encompass all I<normal> results, or 68% of all entries, by B<< <--> >>.
500
501 =item I<avg>
502
503 Matches the average;
504 the sum of all values divided by the number of counted lines.
505 Indicated by default as I<=>.
506
507 =back
508
509 =item --min=<number>, --max=<number>
510
511 Bars extend from 0 or the minimum value if lower,
512 to the largest value encountered.
513 These options can be set to customize this range.
514
515 =item --palette=(<preset> | <color>...)
516
517 Override colors of parsed numbers.
518 Can be any CSI escape, such as I<90> for default dark grey,
519 or alternatively I<1;30> for bright black.
520
521 In case of additional colors,
522 the last is used for values equal to the maximum, the first for minima.
523 If unspecified, these are green and red respectively (I<31 90 32>).
524 Multiple intermediate colors will be distributed
525 relative to the size of values.
526
527 Predefined color schemes are named I<whites> and I<fire>,
528 or I<greys> and I<fire256> for 256-color variants.
529
530 =item --spark[=<characters>]
531
532 Replace lines by I<sparklines>,
533 single characters corresponding to input values.
534 A specified sequence of unicode characters will be used for
535 Of a specified sequence of unicode characters,
536 the first one will be used for non-values,
537 the last one for the maximum,
538 the second (if any) for the minimum,
539 and any remaining will be distributed over the range of values.
540 Unspecified, block fill glyphs U+2581-2588 will be used.
541
542 =item -s, --stat
543
544 Total statistics after all data.
545
546 =item -u, --unmodified
547
548 Do not reformat values, keeping leading whitespace.
549 Keep original value alignment, which may be significant in some programs.
550
551 =item --value-length=<size>
552
553 Reserved space for numbers.
554
555 =item -w, --width=<columns>
556
557 Override the maximum number of columns to use.
558 Appended graphics will extend to fill up the entire screen.
559
560 =item -h, --usage
561
562 Overview of available options.
563
564 =item --help
565
566 Full documentation
567 rendered by perldoc.
568
569 =item --version
570
571 Version information.
572
573 =back
574
575 =head1 EXAMPLES
576
577 Draw a sine wave:
578
579     seq 30 | awk '{print sin($1/10)}' | barcat
580
581 Compare file sizes (with human-readable numbers):
582
583     du -d0 -b * | barcat -H
584
585 Memory usage of user processes with long names truncated:
586
587     ps xo %mem,pid,cmd | barcat -l40
588
589 Monitor network latency from prefixed results:
590
591     ping google.com | barcat -f'time=\K' -t
592
593 Commonly used after counting, for example users on the current server:
594
595     users | tr ' ' '\n' | sort | uniq -c | barcat
596
597 Letter frequencies in text files:
598
599     cat /usr/share/games/fortunes/*.u8 |
600     perl -CS -nE 'say for grep length, split /\PL*/, uc' |
601     sort | uniq -c | barcat
602
603 Number of HTTP requests per day:
604
605     cat log/access.log | cut -d\  -f4 | cut -d: -f1 | uniq -c | barcat
606
607 Any kind of database query with counts, preserving returned alignment:
608
609     echo 'SELECT count(*),schemaname FROM pg_tables GROUP BY 2' |
610     psql -t | barcat -u
611
612 In PostgreSQL from within the client:
613
614         postgres=> SELECT sin(generate_series(0, 3, .1)) \g |barcat
615
616 Earthquakes worldwide magnitude 1+ in the last 24 hours:
617
618     curl https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/1.0_day.csv |
619     column -tns, | barcat -f4 -u -l80%
620
621 External datasets, like movies per year:
622
623     curl https://github.com/prust/wikipedia-movie-data/raw/master/movies.json -L |
624     perl -054 -nlE 'say if s/^"year"://' | uniq -c | barcat
625
626 But please get I<jq> to process JSON
627 and replace the manual selection by C<< jq '.[].year' >>.
628
629 Pokémon height comparison:
630
631     curl https://github.com/Biuni/PokemonGO-Pokedex/raw/master/pokedex.json -L |
632     jq -r '.pokemon[] | [.height,.num,.name] | join(" ")' | barcat
633
634 USD/EUR exchange rate from CSV provided by the ECB:
635
636     curl https://sdw.ecb.europa.eu/export.do \
637          -Gd 'node=SEARCHRESULTS&q=EXR.D.USD.EUR.SP00.A&exportType=csv' |
638     grep '^[12]' | barcat -f',\K' --value-length=7
639
640 Total population history in XML from the World Bank:
641
642     curl http://api.worldbank.org/v2/country/1W/indicator/SP.POP.TOTL -L |
643     xmllint --xpath '//*[local-name()="date" or local-name()="value"]' - |
644     sed -r 's,</wb:value>,\n,g; s,(<[^>]+>)+, ,g' | barcat -f1 -H
645
646 And of course various Git statistics, such commit count by year:
647
648     git log --pretty=%ci | cut -b-4 | uniq -c | barcat
649
650 Or the top 3 most frequent authors with statistics over all:
651
652     git shortlog -sn | barcat -L3 -s
653
654 Sparkline graphics of simple input given as inline parameters:
655
656         barcat --spark= 3 1 4 1 5 0 9 2 4
657
658 Activity graph of the last days (substitute date C<-v-{}d> on BSD):
659
660     ( git log --pretty=%ci --since=30day | cut -b-10
661       seq 0 30 | xargs -i date +%F -d-{}day ) |
662     sort | uniq -c | awk '$1--' | barcat --spark
663
664 =head1 AUTHOR
665
666 Mischa POSLAWSKY <perl@shiar.org>
667
668 =head1 LICENSE
669
670 GPL3+.