File Coverage

blib/lib/DiaColloDB/Utils.pm
Criterion Covered Total %
statement 79 553 14.2
branch 0 438 0.0
condition 0 164 0.0
subroutine 31 115 26.9
pod 64 76 84.2
total 174 1346 12.9


line stmt bran cond sub pod time code
1             ## -*- Mode: CPerl -*-
2             ##
3             ## File: DiaColloDB::Utils.pm
4             ## Author: Bryan Jurish <moocow@cpan.org>
5             ## Description: generic DiaColloDB utilities
6              
7             package DiaColloDB::Utils;
8 2     2   14 use DiaColloDB::Logger;
  2         4  
  2         60  
9 2     2   10 use Exporter;
  2         4  
  2         104  
10 2     2   3204 use JSON;
  2         27474  
  2         14  
11 2     2   1382 use IO::Handle;
  2         12550  
  2         118  
12 2     2   1218 use IO::File;
  2         4124  
  2         232  
13 2     2   3110 use IPC::Run;
  2         65940  
  2         112  
14 2     2   20 use File::Basename qw(basename dirname);
  2         6  
  2         176  
15 2     2   18 use File::Path qw(make_path);
  2         6  
  2         142  
16 2     2   1234 use File::Copy qw();
  2         5096  
  2         68  
17 2     2   20 use File::Spec qw(); ##-- for tmpdir()
  2         6  
  2         36  
18 2     2   1734 use File::Temp qw(); ##-- for tempdir(), tempfile()
  2         20982  
  2         78  
19 2     2   20 use Fcntl qw(:DEFAULT SEEK_SET SEEK_CUR SEEK_END);
  2         4  
  2         952  
20 2     2   16 use Time::HiRes qw(gettimeofday tv_interval);
  2         4  
  2         18  
21 2     2   388 use POSIX qw(strftime);
  2         6  
  2         24  
22 2     2   3668 use Carp;
  2         4  
  2         136  
23 2     2   16 use strict;
  2         4  
  2         2978  
24              
25             ##==============================================================================
26             ## Globals
27              
28             our @ISA = qw(Exporter DiaColloDB::Logger);
29             our @EXPORT = qw();
30             our %EXPORT_TAGS =
31             (
32             fcntl => [qw(fcflags fcread fcwrite fctrunc fccreat fcperl fcopen fcgetfl)],
33             json => [qw(jsonxs loadJsonString loadJsonFile saveJsonString saveJsonFile)],
34             sort => [qw(sortCmd csort_to csortuc_to)],
35             run => [qw(crun opencmd runcmd)],
36             env => [qw(env_set env_push env_pop)],
37             pack => [qw(packsize packsingle packeq packFilterFetch packFilterStore)],
38             math => [qw(isNan isInf isFinite $LOG2 log2 min2 max2 lmax lmin lsum)],
39             list => [qw(luniq sluniq xluniq lcounts)],
40             regex => [qw(regex)],
41             html => [qw(htmlesc)],
42             ddc => [qw(ddc_escape)],
43             time => [qw(s2hms s2timestr timestamp)],
44             file => [qw(file_mtime file_timestamp du_file du_glob copyto moveto copyto_a cp_a fh_flush fh_reopen)],
45             si => [qw(si_str)],
46             pdl => [qw(_intersect_p _union_p _complement_p _setdiff_p),
47             qw(readPdlFile writePdlFile writePdlHeader writeCcsHeader mmzeroes mmtemp),
48             qw(maxval mintype),
49             ],
50             temp => [qw($TMPDIR tmpdir tmpfh tmpfile tmparray tmparrayp tmphash)],
51             jobs => [qw(nCores nJobs sortJobs)],
52             );
53             our @EXPORT_OK = map {@$_} values(%EXPORT_TAGS);
54             $EXPORT_TAGS{all} = [@EXPORT_OK];
55              
56             ##==============================================================================
57             ## Functions: Fcntl
58              
59             ## $flags = PACKAGE::fcflags($flags)
60             ## + returns Fcntl flags for symbolic string $flags
61             sub fcflags {
62 0     0 1   my $flags = shift;
63 0   0       $flags //= 'r';
64 0 0         return $flags if ($flags =~ /^[0-9]+$/); ##-- numeric flags are interpreted as Fcntl bitmask
65 0           my $fread = $flags =~ /[r<]/;
66 0           my $fwrite = $flags =~ /[wa>\+]/;
67 0   0       my $fappend = ($flags =~ /[a]/ || $flags =~ />>/);
68 0 0         my $iflags = ($fread
    0          
    0          
69             ? ($fwrite ? (O_RDWR|O_CREAT) : O_RDONLY)
70             : ($fwrite ? (O_WRONLY|O_CREAT) : 0)
71             );
72 0 0 0       $iflags |= O_TRUNC if ($fwrite && !$fappend);
73 0           return $iflags;
74             }
75              
76             ## $fcflags = fcgetfl($fh)
77             ## + returns Fcntl flags for filehandle $fh
78             sub fcgetfl {
79 0 0   0 1   shift if (UNIVERSAL::isa($_[0],__PACKAGE__));
80 0           my $fh = shift;
81 0           return CORE::fcntl($fh,F_GETFL,0);
82             }
83              
84             ## $bool = fcread($flags)
85             ## + returns true if any read-bits are set for $flags
86             sub fcread {
87 0     0 1   my $flags = fcflags(shift);
88 0   0       return ($flags&O_RDONLY)==O_RDONLY || ($flags&O_RDWR)==O_RDWR;
89             }
90              
91             ## $bool = fcwrite($flags)
92             ## + returns true if any write-bits are set for $flags
93             sub fcwrite {
94 0     0 1   my $flags = fcflags(shift);
95 0   0       return ($flags&O_WRONLY)==O_WRONLY || ($flags&O_RDWR)==O_RDWR;
96             }
97              
98             ## $bool = fctrunc($flags)
99             ## + returns true if truncate-bits are set for $flags
100             sub fctrunc {
101 0     0 1   my $flags = fcflags(shift);
102 0           return ($flags&O_TRUNC)==O_TRUNC;
103             }
104              
105             ## $bool = fccreat($flags)
106             sub fccreat {
107 0     0 1   my $flags = fcflags(shift);
108 0           return ($flags&O_CREAT)==O_CREAT;
109             }
110              
111             ## $str = fcperl($flags)
112             ## + return perl mode-string for $flags
113             sub fcperl {
114 0     0 1   my $flags = fcflags(shift);
115 0 0         return (fcread($flags)
    0          
    0          
    0          
    0          
116             ? (fcwrite($flags) ##-- +read
117             ? (fctrunc($flags) ##-- +read,+write
118             ? '+>' : '+<') ##-- +read,+write,+/-trunc
119             : '<')
120             : (fcwrite($flags) ##-- -read
121             ? (fctrunc($flags) ##-- -read,+write
122             ? '>' : '>>') ##-- -read,+write,+/-trunc
123             : '<') ##-- -read,-write : default
124             );
125             }
126              
127             ## $fh_or_undef = fcopen($file,$flags)
128             ## $fh_or_undef = fcopen($file,$flags,$mode,$perms)
129             ## + opens $file with fcntl-style flags $flags
130             sub fcopen {
131 0     0 1   my ($file,$flags,$perms) = @_;
132 0           $flags = fcflags($flags);
133 0   0       $perms //= (0666 & ~umask);
134 0           my $mode = fcperl($flags);
135              
136 0           my ($sysfh);
137 0 0         if (ref($file)) {
138             ##-- dup an existing filehandle
139 0           $sysfh = $file;
140             }
141             else {
142             ##-- use sysopen() to honor O_CREAT and O_TRUNC
143 0 0         sysopen($sysfh, $file, $flags, $perms) or return undef;
144             }
145              
146             ##-- now open perl-fh from system fh
147 0 0         open(my $fh, "${mode}&=", fileno($sysfh)) or return undef;
148 0 0 0       if (fcwrite($flags) && !fctrunc($flags)) {
149             ##-- append mode: seek to end of file
150 0 0         seek($fh, 0, SEEK_END) or return undef;
151             }
152 0           return $fh;
153             }
154              
155             ##==============================================================================
156             ## Functions: JSON
157              
158             ##--------------------------------------------------------------
159             ## JSON: load
160              
161             ## $data = PACKAGE::loadJsonString( $string,%opts)
162             ## $data = PACKAGE::loadJsonString(\$string,%opts)
163             ## + %opts passed to JSON::from_json(), e.g. (relaxed=>0)
164             ## + supports $opts{json} = $json_obj
165             sub loadJsonString {
166 0 0   0 1   my $that = UNIVERSAL::isa($_[0],__PACKAGE__) ? shift : __PACKAGE__;
167 0 0         my $bufr = ref($_[0]) ? $_[0] : \$_[0];
168 0           my %opts = @_[1..$#_];
169 0 0         return $opts{json}->decode($$bufr) if ($opts{json});
170 0           return from_json($$bufr, {utf8=>!utf8::is_utf8($$bufr), relaxed=>1, allow_nonref=>1, %opts});
171             }
172              
173             ## $data = PACKAGE::loadJsonFile($filename_or_handle,%opts)
174             sub loadJsonFile {
175 0 0   0 1   my $that = UNIVERSAL::isa($_[0],__PACKAGE__) ? shift : __PACKAGE__;
176 0           my $file = shift;
177 0 0         my $fh = ref($file) ? $file : IO::File->new("<$file");
178 0 0         return undef if (!$fh);
179 0           binmode($fh,':raw');
180 0           local $/=undef;
181 0           my $buf = <$fh>;
182 0 0         close($fh) if (!ref($file));
183 0           return $that->loadJsonString(\$buf,@_);
184             }
185              
186             ##--------------------------------------------------------------
187             ## JSON: save
188              
189             ## $str = PACKAGE::saveJsonString($data)
190             ## $str = PACKAGE::saveJsonString($data,%opts)
191             ## + %opts passed to JSON::to_json(), e.g. (pretty=>0, canonical=>0)'
192             ## + supports $opts{json} = $json_obj
193             sub saveJsonString {
194 0 0   0 1   my $that = UNIVERSAL::isa($_[0],__PACKAGE__) ? shift : __PACKAGE__;
195 0           my $data = shift;
196 0           my %opts = @_;
197 0 0         return $opts{json}->encode($data) if ($opts{json});
198 0           return to_json($data, {utf8=>1, allow_nonref=>1, allow_unknown=>1, allow_blessed=>1, convert_blessed=>1, pretty=>1, canonical=>1, %opts});
199             }
200              
201             ## $bool = PACKAGE::saveJsonFile($data,$filename_or_handle,%opts)
202             sub saveJsonFile {
203 0 0   0 1   my $that = UNIVERSAL::isa($_[0],__PACKAGE__) ? shift : __PACKAGE__;
204 0           my $data = shift;
205 0           my $file = shift;
206 0 0         my $fh = ref($file) ? $file : IO::File->new(">$file");
207 0 0         $that->logconfess("saveJsonFile() failed to open file '$file': $!") if (!$fh);
208 0           binmode($fh,':raw');
209 0 0         $fh->print($that->saveJsonString($data,@_)) or return undef;
210 0 0         if (!ref($file)) { close($fh) || return undef; }
  0 0          
211 0           return 1;
212             }
213              
214             ##--------------------------------------------------------------
215             ## JSON: object
216              
217             ## $json = jsonxs()
218             ## $json = jsonxs(%opts)
219             ## $json = jsonxs(\%opts)
220             sub jsonxs {
221 0 0   0 0   my $that = UNIVERSAL::isa($_[0],__PACKAGE__) ? shift : __PACKAGE__;
222             my %opts = (
223             utf8=>1, relaxed=>1, allow_nonref=>1, allow_unknown=>1, allow_blessed=>1, convert_blessed=>1, pretty=>1, canonical=>1,
224 0 0         (@_==1 ? %{$_[0]} : @_),
  0            
225             );
226 0           my $jxs = JSON->new;
227 0           foreach (grep {$jxs->can($_)} keys %opts) {
  0            
228 0           $jxs->can($_)->($jxs,$opts{$_});
229             }
230 0           return $jxs;
231             }
232              
233 2     2   1818 BEGIN { *json = \&jsonxs; }
234              
235             ##==============================================================================
236             ## Functions: env
237              
238              
239             ## \%setenv = PACKAGE::env_set(%setenv)
240             sub env_set {
241 0 0   0 1   my $that = UNIVERSAL::isa($_[0],__PACKAGE__) ? shift : __PACKAGE__;
242 0           my %setenv = @_;
243 0           my ($key,$val);
244 0           while (($key,$val)=each(%setenv)) {
245 0 0         if (!defined($val)) {
246             #$that->trace("ENV_UNSET $key");
247 0           delete($ENV{$key});
248             } else {
249             #$that->trace("ENV_SET $key=$val");
250 0           $ENV{$key} = $val;
251             }
252             }
253 0           return \%setenv;
254             }
255              
256             ## \%oldvals = PACKAGE::env_push(%setenv)
257             our @env_stack = qw();
258             sub env_push {
259 0 0   0 1   my $that = UNIVERSAL::isa($_[0],__PACKAGE__) ? shift : __PACKAGE__;
260 0           my %setenv = @_;
261 0           my %oldenv = map {($_=>$ENV{$_})} keys %setenv;
  0            
262 0           push(@env_stack, \%oldenv);
263 0           $that->env_set(%setenv);
264 0           return \%oldenv;
265             }
266              
267             ## \%restored = PACKAGE::env_pop(%setenv)
268             sub env_pop {
269 0 0   0 1   my $that = UNIVERSAL::isa($_[0],__PACKAGE__) ? shift : __PACKAGE__;
270 0           my $oldvals = pop(@env_stack);
271 0 0         $that->env_set(%$oldvals) if ($oldvals);
272 0           return $oldvals;
273             }
274              
275              
276             ##==============================================================================
277             ## Functions: run
278              
279             ## $rc = PACKAGE::runcmd($cmd)
280             ## $rc = PACKAGE::runcmd(@cmd)
281             ## + does log trace at level $TRACE_RUNCMD
282             sub runcmd {
283 0 0   0 0   my $that = UNIVERSAL::isa($_[0],__PACKAGE__) ? shift : __PACKAGE__;
284 0           $that->trace("CMD ", join(' ',@_));
285 0           return system(@_);
286             }
287              
288             ## $fh_or_undef = PACKAGE::opencmd($cmd)
289             ## $fh_or_undef = PACKAGE::opencmd($mode,@argv)
290             ## + does log trace at level $TRACE_RUNCMD
291             sub opencmd {
292 0 0   0 1   my $that = UNIVERSAL::isa($_[0],__PACKAGE__) ? shift : __PACKAGE__;
293 0           $that->trace("CMD ", join(' ',@_));
294 0           my $fh = IO::Handle->new();
295 0 0         if ($#_ > 0) {
296 0           open($fh,$_[0],$_[1],@_[2..$#_])
297             } else {
298 0           open($fh,$_[0]);
299             }
300 0 0         $that->logconfess("opencmd() failed for \`", join(' ',@_), "': $!") if (!$fh);
301 0           return $fh;
302             }
303              
304             ## $bool = crun(@IPC_Run_args)
305             ## + wrapper for IPC::Run::run(@IPC_Run_args) with $ENV{LC_ALL}='C'
306             sub crun {
307 0 0   0 1   my $that = UNIVERSAL::isa($_[0],__PACKAGE__) ? shift : __PACKAGE__;
308             $that->trace("RUN ", join(' ',
309             map {
310 0 0         (ref($_)
  0 0          
311             ? (ref($_) eq 'ARRAY'
312             ? join(' ', @$_)
313             : ref($_))
314             : $_)
315             } @_));
316 0           $that->env_push(LC_ALL=>'C');
317 0           my $rc = IPC::Run::run(@_);
318 0           $that->env_pop();
319 0           return $rc;
320             }
321              
322             ## $cmd_prefix = sortCmd($njobs=$DiaColloDB::NJOBS)
323             ## + returns command-prefix for UNIX sort
324             ## + uses environment variable $DIACOLLO_SORT if present, otherwise $SORT if set
325             ## + defualts to system 'sort' command from PATH with appended sortJobs() options
326             sub sortCmd {
327 0 0   0 1   my $that = UNIVERSAL::isa($_[0],__PACKAGE__) ? shift : __PACKAGE__;
328 0   0       return $ENV{DIACOLLO_SORT} || $ENV{SORT} || ('sort '.$that->sortJobs(@_));
329             }
330              
331             ## $bool = csort_to(\@sortargs, \&catcher)
332             ## + runs system sort and feeds resulting lines to \&catcher
333             sub csort_to {
334 0 0   0 1   my $that = UNIVERSAL::isa($_[0],__PACKAGE__) ? shift : __PACKAGE__;
335 0           my ($sortargs,$catcher) = @_;
336 0           return crun([$that->sortCmd(),@$sortargs], '>', IPC::Run::new_chunker("\n"), $catcher);
337             }
338              
339             ## $bool = csortuc_to(\@sortargs, \&catcher)
340             ## + runs system sort | uniq -c and feeds resulting lines to \&catcher
341             sub csortuc_to {
342 0 0   0 1   my $that = UNIVERSAL::isa($_[0],__PACKAGE__) ? shift : __PACKAGE__;
343 0           my ($sortargs,$catcher) = @_;
344 0           return crun([$that->sortCmd(),@$sortargs], '|', [qw(uniq -c)], '>', IPC::Run::new_chunker("\n"), $catcher);
345             }
346              
347              
348             ##==============================================================================
349             ## Functions: pack filters
350              
351             ## $len = PACKAGE::packsize($packfmt)
352             ## $len = PACKAGE::packsize($packfmt,@args)
353             ## + get pack-size for $packfmt with args @args
354             sub packsize {
355 2     2   18 use bytes; #use bytes; ##-- deprecated in perl v5.18.2
  2         6  
  2         20  
356 2     2   112 no warnings;
  2         4  
  2         1276  
357 0 0   0 1   shift if (UNIVERSAL::isa($_[0],__PACKAGE__));
358 0           return bytes::length(pack($_[0],@_[1..$#_]));
359             }
360              
361             ## $bool = PACKAGE::packsingle($packfmt)
362             ## $bool = PACKAGE::packsingle($packfmt,@args)
363             ## + guess whether $packfmt is a single-element (scalar) format
364             sub packsingle {
365 0 0   0 0   shift if (UNIVERSAL::isa($_[0],__PACKAGE__));
366 0   0       return (packsize($_[0],0)==packsize($_[0],0,0)
367             && $_[0] !~ m{\*|(?:\[(?:[2-9]|[0-9]{2,})\])|(?:[[:alpha:]].*[[:alpha:]])});
368             }
369              
370             ## $bool = PACKAGE::packeq($packfmt1,$packfmt2,$val=0x123456789abcdef)
371             ## + returns true iff $packfmt1 and $packfmt2 are equivalent for $val
372             sub packeq {
373 0 0   0 0   shift if (UNIVERSAL::isa($_[0],__PACKAGE__));
374 0           my ($fmt1,$fmt2,$val) = @_;
375 0   0       $val //= 0x12345678;
376 0           return pack($fmt1,$val) eq pack($fmt2,$val);
377             }
378              
379             ## \&filter_sub = PACKAGE::packFilterStore($pack_template)
380             ## \&filter_sub = PACKAGE::packFilterStore([$pack_template_store, $pack_template_fetch])
381             ## \&filter_sub = PACKAGE::packFilterStore([\&pack_code_store, \&pack_code_fetch])
382             ## + returns a DB_File-style STORE-filter sub for transparent packing of data to $pack_template
383             sub packFilterStore {
384 0 0   0 1   my $that = UNIVERSAL::isa($_[0],__PACKAGE__) ? shift : __PACKAGE__;
385 0           my $packas = shift;
386 0 0         $packas = $packas->[0] if (UNIVERSAL::isa($packas,'ARRAY'));
387 0 0         return $packas if (UNIVERSAL::isa($packas,'CODE'));
388 0 0 0       return undef if (!$packas || $packas eq 'raw');
389 0 0         if ($that->packsingle($packas)) {
390             return sub {
391 0 0   0     $_ = pack($packas,$_) if (defined($_));
392 0           };
393             } else {
394             return sub {
395 0 0   0     $_ = pack($packas, ref($_) ? @$_ : split(/\t/,$_)) if (defined($_));
    0          
396 0           };
397             }
398             }
399              
400             ## \&filter_sub = PACKAGE::packFilterFetch($pack_template)
401             ## \&filter_sub = PACKAGE::packFilterFetch([$pack_template_store, $pack_template_fetch])
402             ## \&filter_sub = PACKAGE::packFilterFetch([\&pack_code_store, \&pack_code_fetch])
403             ## + returns a DB_File-style FETCH-filter sub for transparent unpacking of data from $pack_template
404             sub packFilterFetch {
405 0 0   0 1   my $that = UNIVERSAL::isa($_[0],__PACKAGE__) ? shift : __PACKAGE__;
406 0           my $packas = shift;
407 0 0         $packas = $packas->[1] if (UNIVERSAL::isa($packas,'ARRAY'));
408 0 0         return $packas if (UNIVERSAL::isa($packas,'CODE'));
409 0 0 0       return undef if (!$packas || $packas eq 'raw');
410 0 0         if ($that->packsingle($packas)) {
411             return sub {
412 0     0     $_ = unpack($packas,$_);
413 0           };
414             } else {
415             return sub {
416 0     0     $_ = [unpack($packas,$_)];
417             }
418 0           }
419             }
420              
421             ##==============================================================================
422             ## Math stuff
423              
424             sub isNan {
425 2     2   18 no warnings qw(uninitialized numeric);
  2         4  
  2         150  
426 0   0 0 0   return !($_[0]<=0||$_[0]>=0);
427             }
428             sub isInf {
429 2     2   16 no warnings qw(uninitialized numeric);
  2         2  
  2         280  
430 0   0 0 0   return !($_[0]<=0||$_[0]>=0) || ($_[0]==+"INF") || ($_[0]==-"INF");
431             }
432             sub isFinite {
433 2     2   18 no warnings qw(uninitialized numeric);
  2         4  
  2         218  
434 0   0 0 0   return ($_[0]<=0||$_[0]>=0) && ($_[0]!=+"INF") && ($_[0]!=-"INF");
435             }
436              
437             our ($LOG2);
438             BEGIN {
439 2     2   5952 $LOG2 = log(2.0);
440             }
441              
442             ## $log2 = log2($x)
443             sub log2 {
444 0 0   0 1   return $_[0]==0 ? -inf : log($_[0])/$LOG2;
445             }
446              
447             ## $max2 = max2($x,$y)
448             sub max2 {
449 0 0   0 1   return $_[0] > $_[1] ? $_[0] : $_[1];
450             }
451              
452             ## $min2 = min2($x,$y)
453             sub min2 {
454 0 0   0 1   return $_[0] < $_[1] ? $_[0] : $_[1];
455             }
456              
457             ## $max = lmax(@vals)
458             sub lmax {
459 0     0 0   my $max = undef;
460 0           foreach (@_) {
461 0 0 0       $max = $_ if (!defined($max) || (defined($_) && $_ > $max));
      0        
462             }
463 0           return $max;
464             }
465              
466             ## $min = lmin(@vals)
467             sub lmin {
468 0     0 0   my $min = undef;
469 0           foreach (@_) {
470 0 0 0       $min = $_ if (!defined($min) || (defined($_) && $_ < $min));
      0        
471             }
472 0           return $min;
473             }
474              
475             ## $sum = lsum(@vals)
476             sub lsum {
477 0     0 0   my $sum = 0;
478 0           $sum += $_ foreach (grep {defined($_)} @_);
  0            
479 0           return $sum;
480             }
481              
482             ##==============================================================================
483             ## Functions: lists
484              
485             ## \@l_uniq = luniq(\@l)
486             ## + returns unique defined elements of @l; @l need not be sorted
487             sub luniq {
488 0     0 1   my ($tmp);
489 0 0 0       return [map {defined($tmp) && $tmp eq $_ ? qw() : ($tmp=$_)} sort grep {defined($_)} @{$_[0]//[]}];
  0   0        
  0            
  0            
490             }
491              
492             ## \@l_sorted_uniq = sluniq(\@l_sorted)
493             ## + returns unique defined elements of pre-sorted @l
494             sub sluniq {
495 0     0 1   my ($tmp);
496 0 0 0       return [map {defined($tmp) && $tmp eq $_ ? qw() : ($tmp=$_)} grep {defined($_)} @{$_[0]//[]}];
  0   0        
  0            
  0            
497             }
498              
499             ## \@l_uniq = xluniq(\@l,\&keyfunc)
500             ## + returns elements of @l with unique defined keys according to \&keyfunc (default=\&overload::StrVal)
501             sub xluniq {
502 0     0 1   my ($l,$keyfunc) = @_;
503 0   0       $keyfunc //= \&overload::StrVal;
504 0           my $tmp;
505             return [
506 0           map {$_->[1]}
507 0 0 0       map {defined($tmp) && $tmp->[0] eq $_->[0] ? qw() : ($tmp=$_)}
508 0           sort {$a->[0] cmp $b->[0]}
509 0           grep {defined($_->[0])}
510 0           map {[$keyfunc->($_),$_]}
511 0   0       @{$l//[]}
  0            
512             ];
513             }
514              
515             ## \%l_counts = lcounts(\@l)
516             ## + return occurrence counts for elements of @l
517             sub lcounts {
518 0     0 0   my %counts = qw();
519 0   0       ++$counts{$_} foreach (grep {defined($_)} @{$_[0]//[]});
  0            
  0            
520 0           return \%counts;
521             }
522              
523             ##==============================================================================
524             ## Functions: regexes
525              
526             ## $re = regex($re_str)
527             ## + parses "/"-quoted regex $re_str
528             ## + parses modifiers /[gimsadlu] a la ddc
529             sub regex {
530 0 0   0 1   my $that = UNIVERSAL::isa($_[0],__PACKAGE__) ? shift : __PACKAGE__;
531 0           my $re = shift;
532 0 0         return $re if (ref($re));
533 0           $re =~ s/^\s*\///;
534              
535 0 0         my $mods = ($re =~ s/\/([gimsadlux]*)\s*$// ? $1 : '');
536 0 0         if ($mods =~ s/g//g) {
    0          
537 0           $re = "^(?${mods}:${re})\$"; ##-- parse /g modifier a la ddc
538             } elsif ($mods) {
539 0           $re = "(?${mods}:$re)";
540             }
541              
542 0           return qr{$re};
543             }
544              
545             ##==============================================================================
546             ## Functions: html
547              
548             ## $escaped = htmlesc($str)
549             sub htmlesc {
550             ##-- html escapes
551 0     0 1   my $str = shift;
552 0           $str =~ s/\&/\&amp;/sg;
553 0           $str =~ s/\'/\&#39;/sg;
554 0           $str =~ s/\"/\&quot;/sg;
555 0           $str =~ s/\</\&lt;/sg;
556 0           $str =~ s/\>/\&gt;/sg;
557 0           return $str;
558             }
559              
560             ##==============================================================================
561             ## Functions: ddc
562              
563             ## $escaped_str = ddc_escape($str)
564             ## $escaped_str = ddc_escape($str, $addQuotes=1)
565             sub ddc_escape {
566 0 0   0 0   shift(@_) if (UNIVERSAL::isa($_[0],__PACKAGE__));
567 0 0         return $_[0] if ($_[0] =~ /^[a-zA-Z][a-zA-Z0-9]*$/s); ##-- bareword ok
568 0           my $s = shift;
569 0           $s =~ s/\\/\\\\/g;
570 0           $s =~ s/\'/\\'/g;
571 0 0 0       return !exists($_[1]) || $_[1] ? "'$s'" : $s;
572             }
573              
574             ##==============================================================================
575             ## Functions: time
576              
577             ## $hms = PACKAGE::s2hms($seconds,$sfmt="%06.3f")
578             ## ($h,$m,$s) = PACKAGE::s2hms($seconds,$sfmt="%06.3f")
579             sub s2hms {
580 0 0   0 1   shift(@_) if (UNIVERSAL::isa($_[0],__PACKAGE__));
581 0           my ($secs,$sfmt) = @_;
582 0   0       $sfmt ||= '%06.3f';
583 0           my $h = int($secs/(60*60));
584 0           $secs -= $h*60*60;
585 0           my $m = int($secs/60);
586 0           $secs -= $m*60;
587 0           my $s = sprintf($sfmt, $secs);
588 0 0         return wantarray ? ($h,$m,$s) : sprintf("%02d:%02d:%s", $h,$m,$s);
589             }
590              
591             ## $timestr = PACKAGE::s2timestr($seconds,$sfmt="%f")
592             sub s2timestr {
593 0 0   0 1   shift(@_) if (UNIVERSAL::isa($_[0],__PACKAGE__));
594 0           my ($h,$m,$s) = s2hms(@_);
595 0 0 0       if ($h==0 && $m==0) {
    0          
596 0           $s =~ s/^0+(?!\.)//;
597 0           return "${s}s";
598             }
599             elsif ($h==0) {
600 0           return sprintf("%2dm%ss",$m,$s)
601             }
602 0           return sprintf("%dh%02dm%ss",$h,$m,$s);
603             }
604              
605             ## $rfc_timestamp = PACAKGE->timestamp()
606             ## $rfc_timestamp = PACAKGE->timestamp($time)
607             sub timestamp {
608 0 0   0 1   shift if (UNIVERSAL::isa($_[0],__PACKAGE__));
609 0 0         return POSIX::strftime("%Y-%m-%dT%H:%M:%SZ", (@_ ? gmtime($_[0]) : gmtime()));
610             }
611              
612             ##==============================================================================
613             ## Functions: file
614              
615             ## $mtime = PACKAGE->file_mtime($file_or_fh)
616             sub file_mtime {
617 0 0   0 1   shift if (UNIVERSAL::isa($_[0],__PACKAGE__));
618 0   0       return (stat($_[0]))[9] // 0;
619             }
620              
621             ## $timestamp = PACKAGE->file_timestamp($file_or_fh)
622             sub file_timestamp {
623 0 0   0 1   shift if (UNIVERSAL::isa($_[0],__PACKAGE__));
624 0           return timestamp(file_mtime(@_));
625             }
626              
627             ## $nbytes = du_file(@filenames_or_dirnames_or_fhs)
628             sub du_file {
629 0 0   0 1   shift if (UNIVERSAL::isa($_[0],__PACKAGE__));
630 0           my $du = 0;
631 0           foreach (@_) {
632 0 0 0       $du += (!ref($_) && -d $_ ? du_glob("$_/*") : (-s $_))//0;
      0        
633             }
634 0           return $du;
635             }
636              
637             ## $nbytes = du_glob(@globs)
638             sub du_glob {
639 0 0   0 1   shift if (UNIVERSAL::isa($_[0],__PACKAGE__));
640 0           return du_file(map {glob($_)} @_);
  0            
641             }
642              
643             ## $bool = PACKAGE->copyto($srcfile, $dstdir, %opts)
644             ## $bool = PACKAGE->copyto(\@srcfiles, $dstdir, %opts)
645             ## + copies file(s) $srcfile (first form) or @srcfiles (second form) to $dstdir, creating $dstdir if it doesn't already exist;
646             ## options %opts:
647             ## (
648             ## from => $from, ##-- replace prefix $from in file(s) with $todir; default=undef: flat copy to $todir
649             ## method => \&method, ##-- use CODE-ref \&method as underlying copy routing; default=\&File::Copy::copy
650             ## label => $label, ##-- report errors as '$label'; (default='copyto()')
651             ## )
652             sub copyto {
653 0 0   0 1   my $that = UNIVERSAL::isa($_[0],__PACKAGE__) ? shift : __PACKAGE__;
654 0           my ($srcfiles,$todir,%opts) = @_;
655 0   0       my $method = $opts{method} || \&File::Copy::copy;
656 0   0       my $label = $opts{label} || 'copyto()';
657 0           my $from = $opts{from};
658 0           my ($src,$dst,$dstdir);
659 0 0         foreach $src (UNIVERSAL::isa($srcfiles,'ARRAY') ? @$srcfiles : $srcfiles) {
660 0 0         if (defined($from)) {
661 0           ($dst = $src) =~ s{^\Q$from\E}{$todir};
662             } else {
663 0           $dst = "$todir/".basename($src);
664             }
665 0           $dstdir = dirname($dst);
666 0 0 0       -d $dstdir
667             or make_path($dstdir)
668             or $that->logconfess("$label: failed to create target directory '$dstdir': $!");
669 0 0         $method->($src,$dst)
670             or $that->logconfess("$label: failed to transfer file '$src' to to '$dst': $!");
671             }
672 0           return 1;
673             }
674              
675             ## $bool = PACKAGE->copyto_a($src,$dstdir,%opts)
676             ## + wrapper for PACKAGE->copyto($src,$dstdir, %opts,method=>PACKAGE->can('cp_a'),label=>'copyto_a()')
677             sub copyto_a {
678 0 0   0 1   my $that = UNIVERSAL::isa($_[0],__PACKAGE__) ? shift : __PACKAGE__;
679 0           return $that->copyto(@_, method=>\&cp_a, label=>'copyto_a()');
680             }
681              
682             ## $bool = PACKAGE->moveto($src,$dstdir, %opts)
683             ## + wrapper for PACKAGE->copyto($src,$dstdir, %opts,method=>\&File::Copy::move,label=>'moveto()')
684             sub moveto {
685 0 0   0 1   my $that = UNIVERSAL::isa($_[0],__PACKAGE__) ? shift : __PACKAGE__;
686 0           return $that->copyto(@_, method=>\&File::Copy::move, label=>'moveto()');
687             }
688              
689             ## $bool = PACKAGE->cp_a($src,$dst)
690             ## $bool = PACKAGE->cp_a($src,$dstdir)
691             ## + copies file $src to $dst, propagating ownership, permissions, and timestamps
692             sub cp_a {
693 0 0   0 1   my $that = UNIVERSAL::isa($_[0],__PACKAGE__) ? shift : __PACKAGE__;
694 0           my ($src,$dst) = @_;
695 0 0 0       if (File::Copy->can('syscopy') && File::Copy->can('syscopy') ne File::Copy->can('copy')) {
696             ##-- use File::copy::syscopy() if available
697 0           return File::Copy::syscopy($src,$dst,3);
698             }
699             ##-- copy and then manually propagate file attributes
700 0 0         my $rc = File::Copy::copy($src,$dst) or return undef;
701 0 0         $dst = "$dst/".basename($src) if (-d $dst);
702 0           my @stat = stat($src);
703 0           my ($perm,$gid,$atime,$mtime) = @stat[2,5,8,9];
704 0 0         my $uid = $>==0 ? $stat[4] : $>; ##-- don't try to set uid unless we're running as root
705 0 0 0       $rc &&= CORE::chown($uid,$gid,$dst)
706             or $that->warn("cp_a(): failed to propagate ownership from '$src' to '$dst': $!");
707 0 0 0       $rc &&= CORE::chmod(($perm & 07777), $dst)
708             or $that->warn("cp_a(): failed to propagate persmissions from '$src' to '$dst': $!");
709 0 0 0       $rc &&= CORE::utime($atime,$mtime,$dst)
710             or $that->warn("cp_a(): failed to propagate timestamps from '$src' to '$dst': $!");
711 0           return $rc;
712             }
713              
714             ## $fh_or_undef = PACKAGE->fh_flush($fh)
715             ## + flushes filehandle $fh using its flush() method if available
716             sub fh_flush {
717 0 0   0 1   shift if (UNIVERSAL::isa($_[0],__PACKAGE__));
718 0           my $fh = shift;
719 0 0         return UNIVERSAL::can($fh,'flush') ? $fh->flush() : $fh;
720             }
721              
722             ## $fh_or_undef = PACKAGE->fh_reopen($fh,$file)
723             ## + closes and re-opens filehandle $fh
724             sub fh_reopen {
725 0 0   0 1   shift if (UNIVERSAL::isa($_[0],__PACKAGE__));
726 0           my ($fh,$file) = @_;
727 0           my $flags = fcgetfl($fh) & (~O_TRUNC);
728 0           my @layers0 = PerlIO::get_layers($fh);
729 0 0         CORE::close($fh) || return undef;
730 0 0         CORE::open($fh, fcperl($flags), $file) or return undef;
731 0           my @layers1 = PerlIO::get_layers($fh);
732 0   0       while (@layers0 && @layers1 && $layers0[0] eq $layers1[0]) {
      0        
733 0           shift(@layers0);
734 0           shift(@layers1);
735             }
736 0           binmode($fh,":$_") foreach (@layers1);
737 0           return $fh;
738             }
739              
740              
741              
742             ##==============================================================================
743             ## Utils: SI
744              
745             ## $str = si_str($float)
746             sub si_str {
747 0 0   0 1   shift if (UNIVERSAL::isa($_[0],__PACKAGE__));
748 0           my $x = shift;
749 0 0         return sprintf("%.2fY", $x/10**24) if ($x >= 10**24); ##-- yotta
750 0 0         return sprintf("%.2fZ", $x/10**21) if ($x >= 10**21); ##-- zetta
751 0 0         return sprintf("%.2fE", $x/10**18) if ($x >= 10**18); ##-- exa
752 0 0         return sprintf("%.2fP", $x/10**15) if ($x >= 10**15); ##-- peta
753 0 0         return sprintf("%.2fT", $x/10**12) if ($x >= 10**12); ##-- tera
754 0 0         return sprintf("%.2fG", $x/10**9) if ($x >= 10**9); ##-- giga
755 0 0         return sprintf("%.2fM", $x/10**6) if ($x >= 10**6); ##-- mega
756 0 0         return sprintf("%.2fk", $x/10**3) if ($x >= 10**3); ##-- kilo
757 0 0         return sprintf("%.2f", $x) if ($x >= 1); ##-- (natural units)
758 0 0         return sprintf("%.2fm", $x*10**3) if ($x >= 10**-3); ##-- milli
759 0 0         return sprintf("%.2fu", $x*10**6) if ($x >= 10**-6); ##-- micro
760 0 0         return sprintf("%.2fn", $x*10**9) if ($x >= 10**-9); ##-- nano
761 0 0         return sprintf("%.2fp", $x*10**12) if ($x >= 10**-12); ##-- pico
762 0 0         return sprintf("%.2ff", $x*10**15) if ($x >= 10**-15); ##-- femto
763 0 0         return sprintf("%.2fa", $x*10**18) if ($x >= 10**-18); ##-- atto
764 0 0         return sprintf("%.2fz", $x*10**21) if ($x >= 10**-21); ##-- zepto
765 0 0         return sprintf("%.2fy", $x*10**24) if ($x >= 10**-24); ##-- yocto
766 0           return sprintf("%.2g", $x); ##-- default
767             }
768              
769             ##==============================================================================
770             ## Functions: pdl: setops
771              
772             ## $pi = CLASS::_intersect_p($p1,$p2)
773             ## $pi = CLASS->_intersect_p($p1,$p2)
774             ## + intersection of 2 piddles; undef is treated as the universal set
775             ## + argument piddles MUST be sorted in ascending order
776             sub _intersect_p {
777 0 0   0     shift if (UNIVERSAL::isa($_[0],__PACKAGE__));
778 0 0         return (defined($_[0])
    0          
779             ? (defined($_[1])
780             ? $_[0]->v_intersect($_[1]) ##-- v_intersect is 1.5-3x faster than PDL::Primitive::intersect()
781             : $_[0])
782             : $_[1]);
783             }
784             ## $pu = CLASS::_union_p($p1,$p2)
785             ## $pi = CLASS->_intersect_p($p1,$p2)
786             ## + union of 2 piddles; undef is treated as the universal set
787             ## + argument piddles MUST be sorted in ascending order
788             sub _union_p {
789 0 0   0     shift if (UNIVERSAL::isa($_[0],__PACKAGE__));
790 0 0         return (defined($_[0])
    0          
791             ? (defined($_[1])
792             ? $_[0]->v_union($_[1]) ##-- v_union is 1.5-3x faster than PDL::Primitive::setops($a,'OR',$b)
793             : $_[0])
794             : $_[1]);
795             }
796              
797             ## $pneg = CLASS::_complement_p($p,$N)
798             ## $pneg = CLASS->_complement_p($p,$N)
799             ## + index-piddle negation; undef is treated as the universal set
800             ## + $N is the total number of elements in the index-universe
801 2     2   222 BEGIN { *_not_p = *_negate_p = \&_complement_p; }
802             sub _complement_p {
803 0 0   0     shift if (UNIVERSAL::isa($_[0],__PACKAGE__));
804 0           my ($p,$N) = @_;
805 0 0         if (!defined($p)) {
    0          
806             ##-- neg(\universe) = \emptyset
807 0           return PDL->null->long;
808             }
809             elsif ($p->nelem==0) {
810             ##-- neg(\emptyset) = \universe
811 0           return undef;
812             }
813             else {
814             ##-- non-trivial negation
815             ##
816             ##-- mask: ca. 2.2x faster than v_setdiff
817 2     2   16 no strict 'subs';
  2         6  
  2         2894  
818 0           my $mask = PDL->ones(PDL::byte(),$N);
819 0           (my $tmp=$mask->index($p)) .= 0;
820 0           return $mask->which;
821             ##
822             ##-- v_setdiff: ca. 68% slower than mask
823             #my $U = sequence($p->type, $N);
824             #return scalar($U->v_setdiff($p));
825             }
826             }
827              
828              
829             ## $pdiff = CLASS::_setdiff_p($a,$b,$N)
830             ## $pdiff = CLASS->_setdiff_p($a,$b,$N)
831             ## + index-piddle difference; undef is treated as the universal set
832             ## + $N is the total number of elements in the index-universe
833             sub _setdiff_p {
834 0 0   0     shift if (UNIVERSAL::isa($_[0],__PACKAGE__));
835 0           my ($a,$b,$N) = @_;
836 0 0         if (!defined($a)) {
    0          
    0          
    0          
837             ##-- \universe - b = \neg(b)
838 0           return _complement_p($b,$N);
839             }
840             elsif (!defined($b)) {
841             ##-- a - \universe = \emptyset
842 0           return PDL->null->long;
843             }
844             elsif ($a->nelem==0) {
845             ##-- \empyset - b = \emptyset
846 0           return $a;
847             }
848             elsif ($b->nelem==0) {
849             ##-- a - \emptyset = a
850 0           return $a;
851             }
852             else {
853             ##-- non-trivial setdiff
854 0           return scalar($a->v_setdiff($b));
855             }
856             }
857              
858             ##==============================================================================
859             ## Functions: pdl: I/O
860              
861             ## $pdl_or_undef = CLASS->readPdlFile($basename, %opts)
862             ## + %opts:
863             ## class=>$class, # one of qw(PDL PDL::CCS::Nd)
864             ## mmap =>$bool, # use mapfraw() (default=1)
865             ## log=>$level, # log-level (default=undef: off)
866             ## ... # other keys passed to CLASS->mapfraw() rsp. CLASS->readfraw()
867             sub readPdlFile {
868             #require PDL::IO::FastRaw;
869 0 0   0 1   my $that = UNIVERSAL::isa($_[0],__PACKAGE__) ? shift : __PACKAGE__;
870 0           my ($file,%opts) = @_;
871 0   0       my $class = $opts{class} // 'PDL';
872 0   0       my $mmap = $opts{mmap} // 1;
873 0   0       my $ro = (!$mmap || (exists($opts{ReadOnly}) ? $opts{ReadOnly} : (!-w "$file.hdr"))) || 0;
874 0           $that->vlog($opts{log}, "readPdlFile($file) [class=$class,mmap=$mmap,ReadOnly=$ro]");
875 0           delete @opts{qw(class mmap ReadOnly verboseIO)};
876 0 0         return undef if (!-e "$file.hdr");
877 0 0         return $mmap ? $class->mapfraw($file,{%opts,ReadOnly=>$ro}) : $class->readfraw($file,\%opts);
878             }
879              
880             ## $bool = CLASS->writePdlFile($pdl_or_undef, $basename, %opts)
881             ## + unlinks target file(s) if $pdl is not defined
882             ## + %opts:
883             ## log => $bool, # log-level (default=undef: off)
884             ## ... # other keys passed to $pdl->writefraw()
885             sub writePdlFile {
886             #require PDL::IO::FastRaw;
887 0 0   0 1   my $that = UNIVERSAL::isa($_[0],__PACKAGE__) ? shift : __PACKAGE__;
888 0           my ($pdl,$file,%opts) = @_;
889 0 0         if (defined($pdl)) {
890             ##-- write: raw
891 0           $that->vlog($opts{log}, "writePdlFile($file)");
892 0           delete($opts{verboseIO});
893 0           return $pdl->writefraw($file,\%opts);
894             }
895             else {
896             ##-- write: undef: unlink
897 0           $that->vlog($opts{log}, "writePdlFile($file): unlink");
898 0           foreach (grep {-e "file$_"} ('','.hdr','.ix','.ix.hdr','.nz','.nz.hdr','.fits')) {
  0            
899 0 0         unlink("file$_") or $that->logconfess(__PACKAGE__, "::writePdlFile(): failed to unlink '$file$_': $!");
900             }
901             }
902 0           return 1;
903             }
904              
905             ## $bool = CLASS->writePdlHeader($filename, $type, $ndims, @dims)
906             ## + writes a PDL::IO::FastRaw-style header $filename (e.g. "pdl.hdr")
907             ## + adapted from PDL::IO::FastRaw::_writefrawhdr()
908             ## + arguments
909             ## $type ##-- PDL::Type or integer
910             ## $ndims ##-- number of piddle dimensions
911             ## @dims ##-- dimension size list, piddle, or ARRAY-ref
912             sub writePdlHeader {
913 0 0   0 1   my $that = UNIVERSAL::isa($_[0],__PACKAGE__) ? shift : __PACKAGE__;
914 0           my ($file,$type,$ndims,@dims) = @_;
915 0 0         $that->logconfess("writePdlHeader(): missing required parameter (FILE,TYPE,NDIMS,DIMS...)") if (@_ < 3);
916 0 0         $type = $type->enum if (UNIVERSAL::isa($type,'PDL::Type'));
917 0 0         @dims = map {UNIVERSAL::isa($_,'PDL') ? $_->list : (UNIVERSAL::isa($_,'ARRAY') ? @$_ : $_)} @dims;
  0 0          
918 0 0         open(my $fh, ">$file")
919             or return undef;
920             #$that->logconfess("writePdlHeader(): open failed for '$file': $!");
921 0           print $fh join("\n", $type, $ndims, join(' ', @dims), '');
922 0           close($fh);
923             }
924              
925             ## $bool = CLASS->writeCcsHeader($filename, $itype, $vtype, $pdims, %opts)
926             ## + writes a PDL::CCS::IO::FastRaw-style header $filename (e.g. "pdl.hdr")
927             ## + arguments:
928             ## $itype, ##-- PDL::Type for index (default: PDL::CCS::Utils::ccs_indx())
929             ## $vtype, ##-- PDL::Type for values (default: $PDL::IO::Misc::deftype)
930             ## $pdims, ##-- dimension piddle or ARRAY-ref
931             ## + %opts: ##-- passed to PDL::CCS::Nd->newFromWich
932             sub writeCcsHeader {
933 0 0   0 1   my $that = UNIVERSAL::isa($_[0],__PACKAGE__) ? shift : __PACKAGE__;
934 0 0         $that->logconfess("writeCcsFile(): missing required parameter (FILE,ITYPE,VTYPE,DIMS...)") if (@_ < 3);
935 0           my ($file,$itype,$vtype,$pdims,%opts) = @_;
936 0 0         $itype = PDL::CCS::Utils::ccs_indx() if (!defined($itype));
937 0 0         $vtype = $PDL::IO::Misc::deftype if (!defined($vtype));
938 0 0         $pdims = PDL->pdl($itype, $pdims) if (!UNIVERSAL::isa($pdims,'PDL'));
939 0           my $ccs = PDL::CCS::Nd->newFromWhich(PDL->zeroes($itype,$pdims->nelem,1),
940             PDL->zeroes($vtype,2),
941             pdims=>$pdims, sorted=>1, steal=>1, %opts);
942 0           return PDL::CCS::IO::Common::_ccsio_write_header($ccs, $file);
943             }
944              
945             ##==============================================================================
946             ## Functions: pdl: mmap temporaries
947              
948             ## $pdl = mmzeroes($file?, $type?, @dims, \%opts?)
949             ## $pdl = $pdl->mmzeroes($file?, $type?, \%opts?)
950             ## + create a temporary mmap()ed pdl using DiaColloDB::PDL::MM; %opts:
951             ## (
952             ## file => $template, ##-- file basename or File::Temp template; default='pdlXXXX'
953             ## suffix => $suffix, ##-- File::Temp::tempfile() suffix (default='.pdl')
954             ## log => $level, ##-- logging verbosity (default=undef: off)
955             ## temp => $bool, ##-- delete on END (default: $file =~ /X{4}/)
956             ## )
957             sub mmzeroes {
958 0     0 1   require DiaColloDB::PDL::MM;
959 0 0         shift if (UNIVERSAL::isa($_[0],__PACKAGE__));
960 0           return DiaColloDB::PDL::MM::new(@_);
961             }
962             sub mmtemp {
963 0     0 1   require DiaColloDB::PDL::MM;
964 0 0         shift if (UNIVERSAL::isa($_[0],__PACKAGE__));
965 0           return DiaColloDB::PDL::MM::mmtemp(@_);
966             }
967              
968             ## $bool = mmunlink(@mmfiles)
969             ## $bool = mmunlink($mmpdl,@mmfiles)
970             ## + unlinkes file(s) generated by mmzeroes($basename)
971             sub mmunlink {
972 0     0 1   require DiaColloDB::PDL::MM;
973 0 0         shift if (UNIVERSAL::isa($_[0],__PACKAGE__));
974 0           return DiaColloDB::PDL::MM::unlink(@_);
975             }
976              
977             ##==============================================================================
978             ## Functions: pdl: misc
979              
980             ## $type = CLASS->mintype($pdl, @types)
981             ## $type = CLASS->mintype($maxval, @types)
982             ## + returns minimum PDL::Types type from @types required for representing $maxval ($pdl->max if passed as a PDL)
983             ## + @types defaults to all known PDL types
984             sub mintype {
985 0 0   0 1   my $that = UNIVERSAL::isa($_[0],__PACKAGE__) ? shift : __PACKAGE__;
986 0           my ($arg,@types) = @_;
987 0 0         $arg = $arg->max if (UNIVERSAL::isa($arg,'PDL'));
988 0 0         @types = map {$_->{ioname}} values(%PDL::Types::typehash) if (!@types);
  0            
989 0 0         @types = sort {$a->enum <=> $b->enum} map {ref($_) ? $_ : (PDL->can($_) ? PDL->can($_)->() : qw())} @types;
  0 0          
  0            
990 0           foreach my $type (@types) {
991 0 0         return $type if (maxval($type) >= $arg);
992             }
993 0           return PDL::float(); ##-- float is enough to represent anything, in principle
994             }
995             BEGIN {
996 2     2   60 *PDL::mintype = \&mintype;
997             }
998              
999             ## $maxval = $type->maxval()
1000             ## $maxval = CLASS::maxval($type_or_name)
1001             sub maxval {
1002 2     2   16 no warnings 'pack';
  2         4  
  2         352  
1003 0     0 1   my $type = shift;
1004 0 0 0       $type = PDL->can($type)->() if (!ref($type) && PDL->can($type));
1005 0 0         return 'inf' if ($type >= PDL::float());
1006 0           my $nbits = 8*length(pack($PDL::Types::pack[$type->enum],0));
1007 0           return (PDL->pdl($type,2)->pow(PDL->sequence($type,$nbits+1))-1)->double->max;
1008             }
1009             BEGIN {
1010 2     2   186 *PDL::Type::maxval = \&maxval;
1011             }
1012              
1013              
1014             ## ($vals,$counts) = $pdl->valcounts()
1015             ## + wrapper for $pdl->flat->qsort->rle() with masking lifted from MUDL::PDL::Smooth
1016             sub valcounts {
1017 0     0 1   my $pdl = shift;
1018 0           my ($counts,$vals) = $pdl->flat->qsort->rle;
1019 0           my $mask = ($counts > 0);
1020 0           return ($vals->where($mask), $counts->where($mask));
1021             }
1022             BEGIN {
1023 2     2   16 no warnings 'redefine'; ##-- avoid irritating "PDL::valcounts redefined" messages when running together with (legacy) MUDL & DocClassify code
  2         4  
  2         74  
1024 2     2   3922 *PDL::valcounts = \&valcounts;
1025             }
1026              
1027             ##==============================================================================
1028             ## Functions: temporaries
1029              
1030             ## $TMPDIR : global temp directory to use
1031             our $TMPDIR = undef;
1032              
1033             ## TMPFILES : temporary files to be unlinked on END
1034             our @TMPFILES = qw();
1035             END {
1036 2     2   1249932 foreach (@TMPFILES) {
1037 0 0 0       !-e $_
1038             or CORE::unlink($_)
1039             or __PACKAGE__->logwarn("failed to unlink temporary file $_ in final cleanup");
1040             }
1041             }
1042              
1043             ## $tmpdir = CLASS->tmpdir()
1044             ## $tmpdir = CLASS_>tmpdir($template, %opts)
1045             ## + in first form, get name of global tempdir ($TMPDIR || File::Spec::tmpdir())
1046             ## + in second form, create and return a new temporary directory via File::Temp::tempdir()
1047             sub tmpdir {
1048 0 0   0 1   my $that = UNIVERSAL::isa($_[0],__PACKAGE__) ? shift : __PACKAGE__;
1049 0   0       my $tmpdir = $TMPDIR || File::Spec->tmpdir();
1050 0 0         return @_ ? File::Temp::tempdir($_[0], DIR=>$tmpdir, @_[1..$#_]) : $tmpdir;
1051             }
1052              
1053             ## $fh = CLASS->tmpfh()
1054             ## $fh = CLASS->tmpfh($template_or_filename, %opts)
1055             ## + get a new temporary filehandle or undef on error
1056             ## + in list context, returns ($fh,$filename) or empty list on error
1057             sub tmpfh {
1058 0 0   0 1   my $that = UNIVERSAL::isa($_[0],__PACKAGE__) ? shift : __PACKAGE__;
1059 0   0       my $template = shift // 'tmpXXXXX';
1060 0           my ($fh,$filename);
1061 0 0         if ($template =~ /X{4}/) {
1062             ##-- use File::Temp::tempfile()
1063 0 0         ($fh,$filename) = File::Temp::tempfile($template, DIR=>$that->tmpdir(), @_) or return qw();
1064             } else {
1065             ##-- use literal filename, honoring DIR, TMPDIR, and SUFFIX options
1066 0           my %opts = @_;
1067 0           $filename = $template;
1068 0 0 0       do { $opts{DIR} =~ s{/$}{}; $filename = "$opts{DIR}/$filename"; } if ($filename !~ m{^/} && defined($opts{DIR}));
  0            
  0            
1069 0 0 0       $filename = $that->tmpdir."/".$filename if ($filename !~ m{^/} && $opts{TMPDIR});
1070 0 0         $filename .= $opts{SUFFIX} if (defined($opts{SUFFIX}));
1071 0 0         CORE::open($fh, ($opts{APPEND} ? '+<' : '+>'), $filename)
    0          
1072             or $that->logconfess("tmpfh(): open failed for file '$filename': $!");
1073 0 0         push(@TMPFILES, $filename) if ($opts{UNLINK});
1074             }
1075 0 0         return wantarray ? ($fh,$filename) : $fh;
1076             }
1077              
1078             ## $filename = CLASS->tmpfile()
1079             ## $filename = CLASS->tmpfile($template, %opts)
1080             sub tmpfile {
1081 0 0   0 1   my $that = UNIVERSAL::isa($_[0],__PACKAGE__) ? shift : __PACKAGE__;
1082 0 0         my ($fh,$filename) = $that->tmpfh(@_) or return undef;
1083 0           $fh->close();
1084 0           return $filename;
1085             }
1086              
1087             ## \@tmparray = CLASS->tmparray($template, %opts)
1088             ## + ties a new temporary array via $class (default='Tie::File::Indexed::JSON')
1089             ## + calls tie(my @tmparray, 'DiaColloDB::Temp::Array', $tmpfilename, %opts)
1090             sub tmparray {
1091 0 0   0 1   my $that = UNIVERSAL::isa($_[0],__PACKAGE__) ? shift : __PACKAGE__;
1092 0           my ($template,%opts) = @_;
1093              
1094             ##-- load target module
1095 0 0         eval { require "DiaColloDB/Temp/Array.pm" }
  0            
1096             or $that->logconfess("tmparray(): failed to load class DiaColloDB::Temp::Array: $@");
1097              
1098             ##-- default options
1099 0   0       $template //= 'dcdbXXXXXX';
1100 0   0       $opts{SUFFIX} //= '.tmpa';
1101 0 0         $opts{UNLINK} = 1 if (!exists($opts{UNLINK}));
1102 0 0         $opts{APPEND} = 0 if (!exists($opts{APPEND}));
1103              
1104             ##-- tie it up
1105 0           my $tmpfile = $that->tmpfile($template, %opts);
1106 0 0         tie(my @tmparray, 'DiaColloDB::Temp::Array', $tmpfile, %opts)
1107             or $that->logconfess("tmparray(): failed to tie file '$tmpfile' via DiaColloDB::Temp::Array: $@");
1108 0           return \@tmparray;
1109             }
1110              
1111             ## \@tmparrayp = CLASS->tmparrayp($template, $packas, %opts)
1112             ## + ties a new temporary integer-array via DiaColloDB::PackedFile)
1113             ## + calls tie(my @tmparray, 'DiaColloDB::PackedFile', $tmpfilename, %opts)
1114             sub tmparrayp {
1115 0 0   0 1   my $that = UNIVERSAL::isa($_[0],__PACKAGE__) ? shift : __PACKAGE__;
1116 0           my ($template,$packas,%opts) = @_;
1117              
1118             ##-- load target module
1119 0 0         eval { require "DiaColloDB/PackedFile.pm" }
  0            
1120             or $that->logconfess("tmparrayp(): failed to load class DiaColloDB::PackedFile: $@");
1121              
1122             ##-- default options
1123 0   0       $template //= 'dcdbXXXXXX';
1124 0   0       $opts{SUFFIX} //= '.pf';
1125 0 0         $opts{UNLINK} = 1 if (!exists($opts{UNLINK}));
1126 0 0         $opts{APPEND} = 0 if (!exists($opts{APPEND}));
1127              
1128             ##-- tie it up
1129 0           my $tmpfile = $that->tmpfile($template, %opts);
1130 0 0         my $mode = 'rw'.($opts{APPEND} ? 'a' : '');
1131 0 0         tie(my @tmparray, 'DiaColloDB::PackedFile', $tmpfile, $mode, packas=>$packas, temp=>$opts{UNLINK}, %opts)
1132             or $that->logconfess("tmparrayp(): failed to tie file '$tmpfile' via DiaColloDB::PackedFile: $@");
1133 0           return \@tmparray;
1134             }
1135              
1136             ## \%tmphash = CLASS->tmphash($template, %opts)
1137             ## + ties a new temporary hash via $class (default='DB_File')
1138             ## + calls tie(my @tmparray, $class, $tmpfilename, temp=>1, %opts)
1139             sub tmphash {
1140 0 0   0 1   my $that = UNIVERSAL::isa($_[0],__PACKAGE__) ? shift : __PACKAGE__;
1141 0           my ($template,%opts) = @_;
1142              
1143             ##-- load target module
1144 0 0         eval { require "DiaColloDB/Temp/Hash.pm" }
  0            
1145             or $that->logconfess("tmparray(): failed to load class DiaColloDB::Temp::Hash: $@");
1146              
1147             ##-- default options
1148 0   0       $template //= 'dcdbXXXXXX';
1149 0   0       $opts{SUFFIX} //= '.tmph';
1150 0 0         $opts{UNLINK} = 1 if (!exists($opts{UNLINK}));
1151 0 0         $opts{APPEND} = 0 if (!exists($opts{APPEND}));
1152 0 0 0       $opts{flags} //= fcflags('rw'.($opts{APPEND} ? 'a' : ''));
1153              
1154             ##-- tie it up
1155 0           my $tmpfile = $that->tmpfile($template, %opts);
1156 0 0         tie(my %tmphash, 'DiaColloDB::Temp::Hash', $tmpfile, %opts)
1157             or $that->logconfess("tmphash(): failed to tie file '$tmpfile' via DiaColloDB::Temp::Hash: $@");
1158 0           return \%tmphash;
1159             }
1160              
1161             ##==============================================================================
1162             ## Functions: jobs: parallelization
1163              
1164             ## %NCORES : cache for nCores() utility ($cpuinfo_file=>$n, ...)
1165             our %NCORES = qw();
1166              
1167             ## $ncores = PACKAGE::nCores()
1168             ## $ncores = PACKAGE::nCores($proc_cpuinfo_filename)
1169             ## + returns the number of CPU cores on the system according to /proc/cpuinfo, or zero if unavailable
1170             ## + caches result in %NCORES
1171             sub nCores {
1172 0 0   0 1   my $that = UNIVERSAL::isa($_[0],__PACKAGE__) ? shift : __PACKAGE__;
1173 0   0       my $filename = shift || '/proc/cpuinfo';
1174 0 0         return $NCORES{$filename} if (exists($NCORES{$filename}));
1175              
1176 0 0         if (CORE::open(my $fh, "<$filename")) {
    0          
1177 0           my $ncores = 0;
1178 0           while (defined($_=<$fh>)) {
1179 0 0         ++$ncores if (/^processor\s*:/);
1180             }
1181 0           close($fh);
1182 0           $NCORES{$filename} = $ncores;
1183             }
1184             elsif (CORE::open(my $pipefh, "nproc|")) {
1185 0           my $ncores = <$pipefh>;
1186 0           chomp $ncores;
1187 0           close($pipefh);
1188 0 0         $NCORES{$filename} = $NCORES{'nproc|'} = $ncores if ($ncores);
1189             }
1190 0   0       return ($NCORES{$filename} //= 0);
1191             }
1192              
1193             ## $njobs = PACKAGE::nJobs($njobs=$DiaColloDB::NJOBS)
1194             ## + gets non-negative number of jobs for user request $njobs (default=-1)
1195             ## + returns nCores() if $njobs is negative
1196             ## + returns int($njobs*nCores()) if (0 < $njobs < 1)
1197             ## + otherwise returns $njobs+0
1198             sub nJobs {
1199 0 0   0 1   my $that = UNIVERSAL::isa($_[0],__PACKAGE__) ? shift : __PACKAGE__;
1200 0 0         my $njobs = @_ ? shift : $DiaColloDB::NJOBS;
1201 0   0       $njobs //= -1;
1202 0 0         return $that->nCores() if ($njobs < 0);
1203 0 0 0       return $njobs*$that->nCores if (0 < $njobs && $njobs < 1);
1204 0           return int($njobs+0);
1205             }
1206              
1207             ## $sort_parallel_option = sortJobs($njobs=$DiaColloDB::NJOBS)
1208             ## + returns --parallel option for 'sort' calls to use $njobs jobs
1209             sub sortJobs {
1210 0 0   0 1   my $that = UNIVERSAL::isa($_[0],__PACKAGE__) ? shift : __PACKAGE__;
1211 0 0         my $njobs = @_ ? shift : $DiaColloDB::NJOBS;
1212 0 0 0       my $args = (!$njobs || $njobs < 1 ? '' : ("--parallel=".nJobs($njobs)));
1213 0 0         return $args ? ($args) : qw() if (wantarray);
    0          
1214 0           return $args;
1215             }
1216              
1217             ##==============================================================================
1218             ## Footer
1219             1; ##-- be happy