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