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