+ # build up eval to execute
+
+ dbg("Spot::search Spot eval: $expr") if isdbg('searcheval');
+ $expr =~ s/\$r/\$_[0]/g;
+ my $eval = qq{ sub { return $expr; } };
+ dbg("Spot::search Spot eval: $eval") if isdbg('searcheval');
+ my $ecode = eval $eval;
+ return ("Spot search error", $@) if $@;
+
+ my $fh;
+ my $now = $fromdate;
+ my $today = Julian::Day->new($main::systime);
+
+ for ($i = $count = 0; $count < $to && $i < $maxdays; ++$i) { # look thru $maxdays worth of files only
+ last if $now->cmp($todate) <= 0;
+
+
+ my $this = $now->sub($i);
+ my $fn = $fp->fn($this);
+ my $cachekey = _cachek($this);
+ my $rec = 0;
+
+ if ($spotcachedays > 0 && $spotcache{$cachekey}) {
+ foreach my $r (@{$spotcache{$cachekey}}) {
+ ++$rec;
+ if ($dofilter && $dxchan && $dxchan->{spotsfilter}) {
+ my ($gotone, undef) = $dxchan->{spotsfilter}->it(@$r);
+ next unless $gotone;
+ }
+ if (&$ecode($r)) {
+ ++$count;
+ next if $count < $from;
+ push @out, $r;
+ last if $count >= $to;
+ }
+ }
+ dbg("Spot::search cache recs read: $rec") if isdbg('search');
+ } else {
+ if ($readback) {
+ dbg("Spot::search search using tac fn: $fn $i") if isdbg('search');
+ $fh = IO::File->new("$readback $fn |");
+ }
+ else {
+ dbg("Spot::search search fn: $fp->{fn} $i") if isdbg('search');
+ $fh = $fp->open($now->sub($i)); # get the next file
+ }
+ if ($fh) {
+ my $in;
+ while (<$fh>) {
+ chomp;
+ my @r = split /\^/;
+ ++$rec;
+ if ($dofilter && $dxchan && $dxchan->{spotsfilter}) {
+ my ($gotone, undef) = $dxchan->{spotsfilter}->it(@r);
+ next unless $gotone;
+ }
+ if (&$ecode(\@r)) {
+ ++$count;
+ next if $count < $from;
+ if ($readback) {
+ push @out, \@r;
+ last if $count >= $to;
+ } else {
+ push @out, \@r;
+ shift @out if $count >= $to;
+ }
+ }
+ }
+ dbg("Spot::search file recs read: $rec") if isdbg('search');
+ last if $count >= $to; # stop after to
+ }
+ }
+ }
+ return ("Spot search error", $@) if $@;
+
+ @out = sort {$b->[2] <=> $a->[2]} @out if @out;
+ return @out;
+}
+
+# change a freq range->regular expression
+sub ftor
+{
+ my ($a, $b) = @_;
+ return undef unless $a < $b;
+ $b--;
+ my $d = $b - $a;
+ my @a = split //, $a;
+ my @b = split //, $b;
+ my $out;
+ while (@b > @a) {
+ $out .= shift @b;
+ }
+ while (@b) {
+ my $aa = shift @a;
+ my $bb = shift @b;
+ if (@b < (length $d)) {
+ $out .= '\\d';
+ } elsif ($aa eq $bb) {
+ $out .= $aa;
+ } elsif ($aa < $bb) {
+ $out .= "[$aa-$bb]";
+ } else {
+ $out .= "[0-$bb$aa-9]";
+ }
+ }
+ return $out;
+}
+
+# format a spot for user output in list mode
+sub formatl
+{
+ my $t = ztime($_[3]);
+ my $d = cldate($_[3]);
+ my $spotter = "<$_[5]>";
+ my $comment = $_[4] || '';
+ $comment =~ s/\t+/ /g;
+ my $cl = length $comment;
+ my $s = sprintf "%9.1f %-11s %s %s", $_[1], $_[2], $d, $t;
+ my $width = ($_[0] ? $_[0] : 80) - length($spotter) - length($s) - 4;
+
+ $comment = substr $comment, 0, $width if $cl > $width;
+ $comment .= ' ' x ($width-$cl) if $cl < $width;
+
+# return sprintf "%8.1f %-11s %s %s %-28.28s%7s>", $_[0], $_[1], $d, $t, ($_[3]||''), "<$_[4]" ;
+ return "$s $comment$spotter";
+}
+
+# enter the spot for dup checking and return true if it is already a dup
+sub dup
+{
+ my ($freq, $call, $d, $text, $by, $node, $just_find) = @_;
+
+ dbg("Spot::dup: freq=$freq call=$call d=$d text='$text' by=$by node=$node" . ($just_find ? " jf=$just_find" : "")) if isdbg('spotdup');
+
+ # dump if too old
+ return 2 if $d < $main::systime - $dupage;
+
+ # turn the time into minutes (should be already but...)
+ $d = int ($d / 60);
+ $d *= 60;
+
+ my $nd = nearest($timegranularity, $d);
+
+ # remove SSID or area
+ $by =~ s|[-/]\d+$||;
+
+# $freq = sprintf "%.1f", $freq; # normalise frequency
+ $freq = int $freq; # normalise frequency
+
+ my $qrg = nearest($qrggranularity, $freq); # to the nearest however many hz
+
+ $call = substr($call, 0, $maxcalllth) if length $call > $maxcalllth;
+
+ my $dtext ;
+
+ my $l = length $text;
+ $dtext = qq{original:'$text'($l)} if isdbg('spottext');
+
+ chomp $text;
+
+ $text =~ s/\%([0-9A-F][0-9A-F])/chr(hex($1))/eg;
+ $text = uc unpad($text);
+
+ $l = length $text;
+ $dtext .= qq{->afterhex: '$text'($l)} if isdbg('spottext');
+ my @dubious;
+ if (isdbg('spottext')) {
+ (@dubious) = $text =~ /([?\x00-\x08\x0a-\x1F\x7B-\xFF]+)+/;
+ $dtext .= sprintf q{DUBIOUS '%s'}, join '', @dubious if @dubious;
+ }
+
+ my $otext = $text;
+# $text = Encode::encode("iso-8859-1", $text) if $main::can_encode && Encode::is_utf8($text, 1);
+ $text =~ s/^\+\w+\s*//; # remove leading LoTW callsign
+ $text =~ s/\s{2,}[\dA-Z]?[A-Z]\d?$//g if length $text > 24;
+ $text =~ s/\x09+//g;
+ $text =~ s/[\W\x00-\x2F\x7B-\xFF]//g; # tautology, just to make quite sure!
+ $text = substr($text, 0, $duplth) if length $text > $duplth;
+
+ $l = length $text;
+ $dtext .= qq{->final:'$text'($l)} if isdbg('spottext');
+
+ my $t = 0;
+ my $ldupkey;
+
+ # new feature: don't include the origin node in Spot dupes
+ # default = true
+ unless ($no_node_in_dupe) {
+ $ldupkey = $oldstyle ? "X|$call|$by|$node|$freq|$d|$text" : "X|$call|$by|$node|$qrg|$nd|$text";
+
+ $t = DXDupe::find($ldupkey);
+ dbg("Spot::dup ldupkey $ldupkey t '$t'") if isdbg('spotdup');
+ $dtext .= ' DUPE' if $t;
+ dbg("text transforms: $dtext") if length $text && isdbg('spottext');
+ return 1 if $t > 0;
+
+ DXDupe::add($ldupkey, $main::systime+$dupage) unless $just_find;
+ }