286f544b1bc017c00d612df26a927f893a85338b
[spider.git] / perl / DXUtil.pm
1 #
2 # various utilities which are exported globally
3 #
4 # Copyright (c) 1998 - Dirk Koopman G1TLH
5 #
6 #
7 #
8
9 package DXUtil;
10
11
12 use Date::Parse;
13 use IO::File;
14 use File::Copy;
15 use Data::Dumper;
16 use Time::HiRes qw(gettimeofday tv_interval);
17 use Text::Wrap;
18 use Socket qw(AF_INET6 AF_INET inet_pton);
19
20 use strict;
21
22 use vars qw(@month %patmap $pi $d2r $r2d @ISA @EXPORT);
23
24 require Exporter;
25 @ISA = qw(Exporter);
26 @EXPORT = qw(atime ztime cldate cldatetime slat slong yesno promptf 
27                          parray parraypairs phex phash shellregex readfilestr writefilestr
28                          filecopy ptimelist
29              print_all_fields cltounix unpad is_callsign is_latlong
30                          is_qra is_freq is_digits is_pctext is_pcflag insertitem deleteitem
31                          is_prefix dd is_ipaddr $pi $d2r $r2d localdata localdata_mv
32                          diffms _diffms _diffus difft parraydifft is_ztime basecall
33                          normalise_call is_numeric
34             );
35
36
37 @month = qw(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec);
38 %patmap = (
39                    '*' => '.*',
40                    '?' => '.',
41                    '[' => '[',
42                    ']' => ']'
43 );
44
45 $pi = 3.141592653589;
46 $d2r = ($pi/180);
47 $r2d = (180/$pi);
48
49 our $ptonok;
50
51 BEGIN {
52         $ptonok = !defined inet_pton(AF_INET,  '016.17.184.1')
53                 && !defined inet_pton(AF_INET6, '2067::1:')
54                 # Some old versions of Socket are hopelessly broken
55                 && length(inet_pton(AF_INET, '1.1.1.1')) == 4;
56 }
57
58 # a full time for logging and other purposes
59 sub atime
60 {
61         my $t = shift;
62         my ($sec,$min,$hour,$mday,$mon,$year) = gmtime((defined $t) ? $t : time);
63         $year += 1900;
64         my $buf = sprintf "%02d%s%04d\@%02d:%02d:%02d", $mday, $month[$mon], $year, $hour, $min, $sec;
65         return $buf;
66 }
67
68 # get a zulu time in cluster format (2300Z)
69 sub ztime
70 {
71         my $t = shift;
72         $t = defined $t ? $t : time;
73         my $dst = shift;
74         my ($sec,$min,$hour) = $dst ? localtime($t): gmtime($t);
75         my $buf = sprintf "%02d%02d%s", $hour, $min, ($dst) ? '' : 'Z';
76         return $buf;
77 }
78
79 # get a cluster format date (23-Jun-1998)
80 sub cldate
81 {
82         my $t = shift;
83         $t = defined $t ? $t : time;
84         my $dst = shift;
85         my ($sec,$min,$hour,$mday,$mon,$year) = $dst ? localtime($t) : gmtime($t);
86         $year += 1900;
87         my $buf = sprintf "%2d-%s-%04d", $mday, $month[$mon], $year;
88         return $buf;
89 }
90
91 # return a cluster style date time
92 sub cldatetime
93 {
94         my $t = shift;
95         my $dst = shift;
96         my $date = cldate($t, $dst);
97         my $time = ztime($t, $dst);
98         return "$date $time";
99 }
100
101 # return a unix date from a cluster date and time
102 sub cltounix
103 {
104         my $date = shift;
105         my $time = shift;
106         my ($thisyear) = (gmtime)[5] + 1900;
107
108         return 0 unless $date =~ /^\s*(\d+)-(\w\w\w)-([12][90]\d\d)$/;
109         return 0 if $3 > 2036;
110         return 0 unless abs($thisyear-$3) <= 1;
111         $date = "$1 $2 $3";
112         return 0 unless $time =~ /^([012]\d)([012345]\d)Z$/;
113         $time = "$1:$2 +0000";
114         my $r = str2time("$date $time");
115         return $r unless $r;
116         return $r == -1 ? undef : $r;
117 }
118
119 # turn a latitude in degrees into a string
120 sub slat
121 {
122         my $n = shift;
123         my ($deg, $min, $let);
124         $let = $n >= 0 ? 'N' : 'S';
125         $n = abs $n;
126         $deg = int $n;
127         $min = int ((($n - $deg) * 60) + 0.5);
128         return "$deg $min $let";
129 }
130
131 # turn a longitude in degrees into a string
132 sub slong
133 {
134         my $n = shift;
135         my ($deg, $min, $let);
136         $let = $n >= 0 ? 'E' : 'W';
137         $n = abs $n;
138         $deg = int $n;
139         $min = int ((($n - $deg) * 60) + 0.5);
140         return "$deg $min $let";
141 }
142
143 # turn a true into 'yes' and false into 'no'
144 sub yesno
145 {
146         my $n = shift;
147         return $n ? $main::yes : $main::no;
148 }
149
150 # provide a data dumpered version of the object passed
151 sub dd
152 {
153         my $value = shift;
154         my $dd = new Data::Dumper([$value]);
155         $dd->Indent(0);
156         $dd->Terse(1);
157     $dd->Quotekeys($] < 5.005 ? 1 : 0);
158         $value = $dd->Dumpxs;
159         $value =~ s/([\r\n\t])/sprintf("%%%02X", ord($1))/eg;
160         $value =~ s/^\s*\[//;
161     $value =~ s/\]\s*$//;
162         
163         return $value;
164 }
165
166 # format a prompt with its current value and return it with its privilege
167 sub promptf
168 {
169         my ($line, $value, $promptl) = @_;
170         my ($priv, $prompt, $action) = split ',', $line;
171
172         # if there is an action treat it as a subroutine and replace $value
173         if ($action) {
174                 my $q = qq{\$value = $action(\$value)};
175                 eval $q;
176         } elsif (ref $value) {
177                 $value = dd($value);
178         }
179         $promptl ||= 15;
180         $prompt = sprintf "%${promptl}s: %s", $prompt, $value;
181         return ($priv, $prompt);
182 }
183
184 # turn a hex field into printed hex
185 sub phex
186 {
187         my $val = shift;
188         return sprintf '%X', $val;
189 }
190
191 # take an arg as a hash of call=>time pairs and print it
192 sub ptimelist
193 {
194         my $ref = shift;
195         my $out;
196         for (sort keys %$ref) {
197                 $out .= "$_=" . atime($ref->{$_}) . ", ";
198         }
199         chop $out;
200         chop $out;
201         return $out;    
202 }
203
204 # take an arg as an array list and print it
205 sub parray
206 {
207         my $ref = shift;
208         return ref $ref ? join(', ', sort @{$ref}) : $ref;
209 }
210
211 # take the arg as an array reference and print as a list of pairs
212 sub parraypairs
213 {
214         my $ref = shift;
215         my $i;
216         my $out;
217
218         for ($i = 0; $i < @$ref; $i += 2) {
219                 my $r1 = @$ref[$i];
220                 my $r2 = @$ref[$i+1];
221                 $out .= "$r1-$r2, ";
222         }
223         chop $out;                                      # remove last space
224         chop $out;                                      # remove last comma
225         return $out;
226 }
227
228 # take the arg as a hash reference and print it out as such
229 sub phash
230 {
231         my $ref = shift;
232         my $out;
233
234         foreach my $k (sort keys %$ref) {
235                 $out .= "${k}=>$ref->{$k}, ";
236         }
237         $out =~ s/, $// if $out;
238         return $out;
239 }
240
241 sub _sort_fields
242 {
243         my $ref = shift;
244         my @a = split /,/, $ref->field_prompt(shift); 
245         my @b = split /,/, $ref->field_prompt(shift); 
246         return lc $a[1] cmp lc $b[1];
247 }
248
249 # print all the fields for a record according to privilege
250 #
251 # The prompt record is of the format '<priv>,<prompt>[,<action>'
252 # and is expanded by promptf above
253 #
254 sub print_all_fields
255 {
256         my $self = shift;                       # is a dxchan
257         my $ref = shift;                        # is a thingy with field_prompt and fields methods defined
258         my @out;
259         my @fields = $ref->fields;
260         my $field;
261         my $width = $self->width - 1;
262         my $promptl = 0;
263         $width ||= 80;
264
265         # find the maximum length of the prompt
266         foreach $field (@fields) {
267                 if (defined $ref->{$field}) {
268                         my (undef, $prompt, undef) = split ',', $ref->field_prompt($field);
269                         $promptl = length $prompt if length $prompt > $promptl;
270                 }
271         }
272
273         # now do print
274         foreach $field (sort {_sort_fields($ref, $a, $b)} @fields) {
275                 if (defined $ref->{$field}) {
276                         my ($priv, $ans) = promptf($ref->field_prompt($field), $ref->{$field}, $promptl);
277                         my @tmp;
278                         if (length $ans > $width) {
279                                 $Text::Wrap::columns = $width-2;
280                                 my ($p, $a) = split /: /, $ans, 2;
281                                 @tmp = split/\n/, Text::Wrap::wrap("$p: ", (' ' x $promptl) . ': ', $a);
282                         } else {
283                                 push @tmp, $ans;
284                         }
285                         push @out, @tmp if ($self->priv >= $priv);
286                 }
287         }
288         return @out;
289 }
290
291 # generate a regex from a shell type expression 
292 # see 'perl cookbook' 6.9
293 sub shellregex
294 {
295         my $in = shift;
296         $in =~ s{(.)} { $patmap{$1} || "\Q$1" }ge;
297         $in =~ s|\\/|/|g;
298         return '^' . $in . "\$";
299 }
300
301 # read in a file into a string and return it. 
302 # the filename can be split into a dir and file and the 
303 # file can be in upper or lower case.
304 # there can also be a suffix
305 sub readfilestr
306 {
307         my ($dir, $file, $suffix) = @_;
308         my $fn;
309         my $f;
310         if ($suffix) {
311                 $f = uc $file;
312                 $fn = "$dir/$f.$suffix";
313                 unless (-e $fn) {
314                         $f = lc $file;
315                         $fn = "$dir/$file.$suffix";
316                 }
317         } elsif ($file) {
318                 $f = uc $file;
319                 $fn = "$dir/$file";
320                 unless (-e $fn) {
321                         $f = lc $file;
322                         $fn = "$dir/$file";
323                 }
324         } else {
325                 $fn = $dir;
326         }
327
328         my $fh = new IO::File $fn;
329         my $s = undef;
330         if ($fh) {
331                 local $/ = undef;
332                 $s = <$fh>;
333                 $fh->close;
334         }
335         return $s;
336 }
337
338 # write out a file in the format required for reading
339 # in via readfilestr, it expects the same arguments 
340 # and a reference to an object
341 sub writefilestr
342 {
343         my $dir = shift;
344         my $file = shift;
345         my $suffix = shift;
346         my $obj = shift;
347         my $fn;
348         my $f;
349         
350         confess('no object to write in writefilestr') unless $obj;
351         confess('object not a reference in writefilestr') unless ref $obj;
352         
353         if ($suffix) {
354                 $f = uc $file;
355                 $fn = "$dir/$f.$suffix";
356                 unless (-e $fn) {
357                         $f = lc $file;
358                         $fn = "$dir/$file.$suffix";
359                 }
360         } elsif ($file) {
361                 $f = uc $file;
362                 $fn = "$dir/$file";
363                 unless (-e $fn) {
364                         $f = lc $file;
365                         $fn = "$dir/$file";
366                 }
367         } else {
368                 $fn = $dir;
369         }
370
371         my $fh = new IO::File ">$fn";
372         if ($fh) {
373                 my $dd = new Data::Dumper([ $obj ]);
374                 $dd->Indent(1);
375                 $dd->Terse(1);
376                 $dd->Quotekeys(0);
377                 #       $fh->print(@_) if @_ > 0;     # any header comments, lines etc
378                 $fh->print($dd->Dumpxs);
379                 $fh->close;
380         }
381 }
382
383 sub filecopy
384 {
385         copy(@_) or return $!;
386 }
387
388 # remove leading and trailing spaces from an input string
389 sub unpad
390 {
391         my $s = shift;
392         $s =~ s/^\s*//;
393         $s =~ s/\s*$//;
394         return $s;
395 }
396
397 # check that a field only has callsign characters in it
398 sub is_callsign
399 {
400         return $_[0] =~ m!^
401                                           (?:\d?[A-Z]{1,2}\d{0,2}/)?    # out of area prefix /  
402                                           (?:\d?[A-Z]{1,2}\d{1,5})      # main prefix one (required) - lengthened for special calls 
403                                           [A-Z]{1,8}                # callsign letters (required)
404                                           (?:-(?:\d{1,2}))?         # - nn possibly (eg G8BPQ-8)
405                                           (?:/[0-9A-Z]{1,7})?       # / another prefix, callsign or special label (including /MM, /P as well as /EURO or /LGT) possibly
406                                           (?:/(?:AM?|MM?|P))?       # finally /A /AM /M /MM /P 
407                                           $!xo;
408
409         # longest callign allowed is 1X11/1Y11XXXXX-11/XXXXXXX/MM
410 }
411
412 sub is_prefix
413 {
414         return $_[0] =~ m!^(?:[A-Z]{1,2}\d+ | \d[A-Z]{1,2}}\d+)!x        # basic prefix
415 }
416         
417
418 # check that a PC protocol field is valid text
419 sub is_pctext
420 {
421         return undef unless length $_[0];
422         return undef if $_[0] =~ /[\x00-\x08\x0a-\x1f\x80-\x9f]/;
423         return 1;
424 }
425
426 # check that a PC prot flag is fairly valid (doesn't check the difference between 1/0 and */-)
427 sub is_pcflag
428 {
429         return $_[0] =~ /^[01\*\-]+$/;
430 }
431
432 # check that a thing is a frequency
433 sub is_freq
434 {
435         return $_[0] =~ /^\d+(?:\.\d+)?$/;
436 }
437
438 # check that a thing is just digits
439 sub is_digits
440 {
441         return $_[0] =~ /^[\d]+$/;
442 }
443
444 # does it look like a qra locator?
445 sub is_qra
446 {
447         return unless length $_[0] == 4 || length $_[0] == 6;
448         return $_[0] =~ /^[A-Ra-r][A-Ra-r]\d\d(?:[A-Xa-x][A-Xa-x])?$/;
449 }
450
451 # does it look like a valid lat/long
452 sub is_latlong
453 {
454         return $_[0] =~ /^\s*\d{1,2}\s+\d{1,2}\s*[NnSs]\s+1?\d{1,2}\s+\d{1,2}\s*[EeWw]\s*$/;
455 }
456
457 # is it an ip address?
458 sub is_ipaddr
459 {
460         if ($ptonok) {
461                 if ($_[0] =~ /:/) {
462                         if (inet_pton(AF_INET6, $_[0])) {
463                                 return ($_[0] =~ /([:0-9a-f]+)/);
464                         }
465                 } else {
466                         if (inet_pton(AF_INET, $_[0])) {
467                                 return ($_[0] =~ /([\.\d]+)/);
468                         }
469                 }
470         } else {
471                 if ($_[0] =~ /:/) {
472                         return ($_[0] =~ /^(:?:?(?:[0-9a-f]{1,4}\:)?(?:\:[0-9a-f]{1,4}(?:\:\:)?){1,8})$/i);     
473                 } else {
474                         return ($_[0] =~ /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/);
475                 }
476         }
477         return undef;
478 }
479
480 # is it a zulu time hhmmZ
481 sub is_ztime
482 {
483         return $_[0] =~ /^(?:(?:2[0-3])|(?:[01][0-9]))[0-5][0-9]Z$/;
484 }
485
486 # insert an item into a list if it isn't already there returns 1 if there 0 if not
487 sub insertitem
488 {
489         my $list = shift;
490         my $item = shift;
491         
492         return 1 if grep {$_ eq $item } @$list;
493         push @$list, $item;
494         return 0;
495 }
496
497 # delete an item from a list if it is there returns no deleted 
498 sub deleteitem
499 {
500         my $list = shift;
501         my $item = shift;
502         my $n = @$list;
503         
504         @$list = grep {$_ ne $item } @$list;
505         return $n - @$list;
506 }
507
508 # find the correct local_data directory
509 # basically, if there is a local_data directory with this filename and it is younger than the
510 # equivalent one in the (system) data directory then return that name rather than the system one
511 sub localdata
512 {
513         my $ifn = shift;
514         my $lfn = "$main::local_data/$ifn";
515         my $dfn =  "$main::data/$ifn";
516         
517         if (-e "$main::local_data") {
518                 if ((-e $dfn) && (-e $lfn)) {
519                         $lfn = $dfn if -M $dfn < -M $lfn;
520                 } else {
521                         $lfn = $dfn if -e $dfn;
522                 }
523         } else {
524                 $lfn = $dfn;
525         }
526
527         return $lfn;
528 }
529
530 # move a file or a directory from data -> local_data if isn't there already
531 sub localdata_mv
532 {
533         my $ifn = shift;
534         if (-e "$main::data/$ifn" ) {
535                 unless (-e "$main::local_data/$ifn") {
536                         move("$main::data/$ifn", "$main::local_data/$ifn") or die "localdata_mv: cannot move $ifn from '$main::data' -> '$main::local_data' $!\n";
537                 }
538         }
539 }
540
541 # measure the time taken for something to happen; use Time::HiRes qw(gettimeofday tv_interval);
542 sub _diffms
543 {
544         my $ta = shift;
545         my $tb = shift || [gettimeofday];
546         my $a = int($ta->[0] * 1000) + int($ta->[1] / 1000); 
547         my $b = int($tb->[0] * 1000) + int($tb->[1] / 1000);
548         return $b - $a;
549 }
550
551 # and in microseconds
552 sub _diffus
553 {
554         my $ta = shift;
555         my $tb = shift || [gettimeofday];
556         my $a = int($ta->[0] * 1000000) + int($ta->[1]); 
557         my $b = int($tb->[0] * 1000000) + int($tb->[1]);
558         return $b - $a;
559 }
560
561 sub diffms
562 {
563         my $call = shift;
564         my $line = shift;
565         my $ta = shift;
566         my $no = shift;
567         my $tb = shift;
568         my $msecs = _diffms($ta, $tb);
569
570         $line =~ s|\s+$||;
571         my $s = "subprocess stats cmd: '$line' $call ${msecs}mS";
572         $s .= " $no lines" if $no;
573         DXDebug::dbg($s);
574 }
575
576 # expects either an array reference or two times (in the correct order [start, end])
577 sub difft
578 {
579         my $b = shift;
580         my $adds = shift // 0;
581         
582         my $t;
583         if (ref $b eq 'ARRAY') {
584                 $t = $b->[1] - $b->[0];
585         } else {
586                 if ($adds && $adds =~ /^\d+$/ && $adds >= $b) {
587                         $t = $adds - $b;
588                         $adds = shift;
589                 } else {
590                         $t = $main::systime - $b;
591                 }
592         }
593         return '-(ve)' if $t < 0;
594         my ($y,$d,$h,$m,$s);
595         my $out = '';
596         $y = int $t / (86400*365);
597         $out .= sprintf ("%s${y}y", $adds?' ':'') if $y;
598         $t -= $y * 86400 * 365;
599         $d = int $t / 86400;
600         $out .= sprintf ("%s${d}d", $adds?' ':'') if $d;
601         $t -= $d * 86400;
602         $h = int $t / 3600;
603         $out .= sprintf ("%s${h}h", $adds?' ':'') if $h;
604         $t -= $h * 3600;
605         $m = int $t / 60;
606         $out .= sprintf ("%s${m}m", $adds?' ':'') if $m;
607         if (($d == 0 && $adds) || (int $adds && $adds == 2)) {
608                 $s = int $t % 60;
609                 $out .= sprintf ("%s${s}s", $adds?' ':'') if $s;
610                 $out ||= sprintf ("%s0s", $adds?' ':'');
611         }
612         $out = '0s' unless length $out;
613         return $out;
614 }
615
616 # print an array ref of difft refs
617 sub parraydifft
618 {
619         my $r = shift;
620         my $out = '';
621         for (@$r) {
622                 my $s = $_->[2] ? "($_->[2])" : '';
623                 $out .= sprintf "%s=%s$s, ", atime($_->[0]), difft($_->[0], $_->[1]);
624         }
625         $out =~ s/,\s*$//;
626         return $out;
627 }
628
629 sub basecall
630 {
631         my ($r) = $_[0] =~ m{^((?:[\w\d]+/)?[\w\d]+(?:/[\w\d]+)*)(?:-\d+)?(?:-\#)?$};
632         return $r;
633 }
634
635 sub normalise_call
636 {
637         my ($c, $ssid) = $_[0] =~ m|^((?:[\w\d]+/)?[\d\w]+(?:/[\w\d]+)*)(?:-(\d+))?(?:-\#)?$|;
638         my $ncall = $c;
639         $ssid += 0;
640         $ncall .= "-$ssid" if $ssid;
641         return $ncall;
642 }
643
644 sub is_numeric
645 {
646         return $_[0] =~ /^[\.\d]+$/;
647 }