File Coverage

blib/lib/Math/NumSeq/OEIS/File.pm
Criterion Covered Total %
statement 92 388 23.7
branch 21 184 11.4
condition 9 85 10.5
subroutine 21 40 52.5
pod 8 10 80.0
total 151 707 21.3


)? # empty or
line stmt bran cond sub pod time code
1             # Copyright 2011, 2012, 2013, 2014 Kevin Ryde
2              
3             # This file is part of Math-NumSeq.
4             #
5             # Math-NumSeq is free software; you can redistribute it and/or modify
6             # it under the terms of the GNU General Public License as published by the
7             # Free Software Foundation; either version 3, or (at your option) any later
8             # version.
9             #
10             # Math-NumSeq is distributed in the hope that it will be useful, but
11             # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
12             # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
13             # for more details.
14             #
15             # You should have received a copy of the GNU General Public License along
16             # with Math-NumSeq. If not, see .
17              
18              
19             # http://oeis.org/wiki/Clear-cut_examples_of_keywords
20             #
21             # ENHANCE-ME: share most of the a-file/b-file reading with Math::NumSeq::File
22              
23             package Math::NumSeq::OEIS::File;
24 2     2   6247 use 5.004;
  2         4  
25 2     2   6 use strict;
  2         1  
  2         31  
26 2     2   6 use Carp;
  2         2  
  2         84  
27 2     2   786 use POSIX ();
  2         9141  
  2         41  
28 2     2   9 use File::Spec;
  2         3  
  2         31  
29 2     2   767 use Symbol 'gensym';
  2         1290  
  2         99  
30              
31 2     2   9 use vars '$VERSION','@ISA';
  2         2  
  2         89  
32             $VERSION = 72;
33              
34 2     2   349 use Math::NumSeq;
  2         2  
  2         67  
35             @ISA = ('Math::NumSeq');
36             *_to_bigint = \&Math::NumSeq::_to_bigint;
37              
38 2     2   9 use vars '$VERSION';
  2         2  
  2         113  
39             $VERSION = 72;
40              
41 2     2   9 eval q{use Scalar::Util 'weaken'; 1}
  2         2  
  2         99  
42             || eval q{sub weaken { $_[0] = undef }; 1 }
43             || die "Oops, error making a weaken() fallback: $@";
44              
45             # uncomment this to run the ### lines
46             # use Smart::Comments;
47              
48              
49             # use constant name => Math::NumSeq::__('OEIS File');
50 2     2   354 use Math::NumSeq::OEIS;
  2         2  
  2         88  
51             *parameter_info_array = \&Math::NumSeq::OEIS::parameter_info_array;
52              
53             use constant::defer _HAVE_ENCODE => sub {
54 0 0       0 eval { require Encode; 1 } || 0;
  0         0  
  0         0  
55 2     2   7 };
  2         2  
  2         13  
56              
57             sub description {
58 0     0 1 0 my ($class_or_self) = @_;
59 0 0 0     0 if (ref $class_or_self && defined $class_or_self->{'description'}) {
60             # instance
61 0         0 return $class_or_self->{'description'};
62             } else {
63             # class
64 0         0 return Math::NumSeq::__('OEIS sequence from file.');
65             }
66             }
67              
68             sub values_min {
69 0     0 1 0 my ($self) = @_;
70             ### OEIS-File values_min() ...
71 0         0 return _analyze($self)->{'values_min'};
72             }
73             sub values_max {
74 0     0 1 0 my ($self) = @_;
75             ### OEIS-File values_max() ...
76 0         0 return _analyze($self)->{'values_max'};
77             }
78              
79             my %analyze_characteristics = (increasing => 1,
80             increasing_from_i => 1,
81             non_decreasing => 1,
82             non_decreasing_from_i => 1,
83             smaller => 1,
84             );
85             sub characteristic {
86 0     0 1 0 my ($self, $key) = @_;
87 0 0       0 if ($analyze_characteristics{$key}) {
88 0         0 _analyze($self);
89             }
90 0         0 return shift->SUPER::characteristic(@_);
91             }
92              
93             sub oeis_dir {
94 246     246 0 1147 require File::HomeDir;
95 246         4123 return File::Spec->catfile (File::HomeDir->my_home, 'OEIS');
96             }
97             sub anum_to_bfile {
98 0     0 0 0 my ($anum, $prefix) = @_;
99 0   0     0 $prefix ||= 'b';
100 0         0 $anum =~ s/^A/$prefix/;
101 0         0 return "$anum.txt";
102             }
103              
104             #------------------------------------------------------------------------------
105             # Keep track of all instances which exist and on an ithread CLONE re-open
106             # any filehandles in the instances, so they have their own independent file
107             # positions in the new thread.
108              
109             my %instances;
110             sub DESTROY {
111 65     65   74 my ($self) = @_;
112 65         303 delete $instances{$self+0};
113             }
114             sub CLONE {
115 0     0   0 my ($class) = @_;
116 0         0 foreach my $self (values %instances) {
117 0 0       0 next unless $self;
118 0 0       0 next unless $self->{'fh'};
119 0         0 my $pos = _tell($self);
120 0         0 my $fh = gensym;
121 0 0       0 if (open $fh, "< $self->{'filename'}") {
122 0         0 $self->{'fh'} = $fh;
123 0         0 _seek ($self, $pos);
124             } else {
125 0         0 delete $self->{'fh'};
126 0         0 delete $self->{'filename'};
127             }
128             }
129             }
130              
131             #------------------------------------------------------------------------------
132              
133             # The length in decimal digits of the biggest value which fits in a plain
134             # Perl integer. For example on a 32-bit system this is 9 since 9 digit
135             # numbers such as "999_999_999" are the biggest which fit a signed IV
136             # (+2^31).
137             #
138             # The IV size is probed rather than using ~0 since under "perl -Minteger"
139             # have ~0 as -1 rather than the biggest UV ... except "use integer" is not
140             # normally global.
141             #
142             # The NV size is applied to the limit too since not sure should trust values
143             # to stay in IV or UV. This means on a 64-bit integer with 53-bit NV
144             # "double" the limit is 53-bits.
145             #
146 2         4 use constant 1.02 _MAX_DIGIT_LENGTH => do {
147             ### ~0 is: ~0
148              
149 2         13 my $iv = 0;
150 2         7 for (1 .. 256) {
151 130         86 my $new = ($iv << 1) | 1;
152 130 100 66     296 unless ($new > $iv && ($new & 1) == 1) {
153 2         4 last;
154             }
155 128         83 $iv = $new;
156             }
157             ### $iv
158              
159 2         33 require POSIX;
160 2         3 my $nv = POSIX::FLT_RADIX() ** (POSIX::DBL_MANT_DIG()-5);
161             ### $nv
162              
163 2         4 my $iv_len = length($iv) - 1;
164 2         12 my $nv_len = length($nv) - 1;
165 2 50       5424 ($iv_len < $nv_len ? $iv_len : $nv_len) # smaller of the two lengths;
166 2     2   853 };
  2         30  
167             ### _MAX_DIGIT_LENGTH: _MAX_DIGIT_LENGTH()
168              
169              
170             #------------------------------------------------------------------------------
171              
172             # special case a000000.txt files to exclude
173             #
174             my %afile_exclude
175             = (
176             # a003849.txt has replication level words rather than the individual
177             # sequence values.
178             'a003849.txt' => 1,
179              
180             # a027750.txt is unflattened divisors as lists.
181             # Its first line is a correct looking "1 1" so _afile_is_good() doesn't
182             # notice.
183             'a027750.txt' => 1,
184             );
185              
186              
187             # Fields:
188             # fh File handle ref, if reading B-file or A-file
189             #
190             # next_seek File pos to seek $fh for next() to read from.
191             # ith() sets this when it moves the file position.
192             #
193             # array Arrayref of values if using .internal or .html.
194             # array_pos Index 0,1,2,... of next value of $array to return by next().
195             #
196             # i Next $i for next() to return.
197             # When reading a file this is ignored, use the file i instead.
198              
199             sub new {
200             ### OEIS-File new() ...
201 65     65 1 6722 my $self = shift->SUPER::new(@_);
202              
203 65         98 delete $self->{'next_seek'}; # no initial seek
204 65         99 $self->{'characteristic'}->{'integer'} = 1;
205              
206 65         55 my $anum = $self->{'anum'};
207 65         207 (my $num = $anum) =~ s/^A//;
208 65         158 foreach my $basefile ("a$num.txt",
209             "b$num.txt") {
210 130 100       188 next if $afile_exclude{$basefile};
211              
212 120 100 100     395 next if $self->{'_dont_use_afile'} && $basefile =~ /^a/;
213 76 100 66     204 next if $self->{'_dont_use_bfile'} && $basefile =~ /^b/;
214              
215 37         49 my $filename = File::Spec->catfile (oeis_dir(), $basefile);
216             ### $filename
217 37         1133 my $fh = gensym();
218 37 50       669 if (! open $fh, "< $filename") {
219             ### cannot open: $!
220 37         108 next;
221             }
222              
223 0         0 $self->{'filename'} = $filename; # the B-file or A-file name
224 0         0 $self->{'fh'} = $fh;
225 0 0       0 if (! _afile_is_good($self)) {
226             ### this afile not good ...
227 0         0 close delete $self->{'fh'};
228 0         0 delete $self->{'filename'};
229 0         0 next;
230             }
231 0         0 $self->{'fh_i'} = $self->i_start; # at first entry
232              
233             ### opened: $fh
234 0         0 last;
235             }
236              
237 65   33     104 my $have_info = (_read_internal_txt($self, $anum)
238             || _read_internal_html($self, $anum)
239             || _read_html($self, $anum));
240              
241 65 50 33     170 if (! $have_info && ! $self->{'fh'}) {
242 65         7489 croak 'OEIS file(s) not found for A-number "',$anum,'"';
243             }
244              
245 0         0 weaken($instances{$self+0} = $self);
246 0         0 return $self;
247             }
248              
249             sub _analyze {
250 0     0   0 my ($self) = @_;
251              
252 0 0       0 if ($self->{'analyze_done'}) {
253 0         0 return $self;
254             }
255 0         0 $self->{'analyze_done'} = 1;
256              
257             ### _analyze() ...
258              
259 0         0 my $i_start = $self->i_start;
260 0         0 my ($i, $value);
261 0         0 my ($prev_i, $prev_value);
262              
263 0         0 my $values_min;
264 0         0 my $values_max;
265 0         0 my $increasing_from_i = $i_start;
266 0         0 my $non_decreasing_from_i = $i_start;
267 0         0 my $strictly_smaller_count = 0;
268 0         0 my $smaller_count = 0;
269 0         0 my $total_count = 0;
270              
271             my $analyze = sub {
272             ### $prev_value
273             ### $value
274 0 0 0 0   0 if (! defined $values_min || $value < $values_min) {
275 0         0 $values_min = $value;
276             }
277 0 0 0     0 if (! defined $values_max || $value > $values_max) {
278 0         0 $values_max = $value;
279             }
280              
281 0 0       0 if (defined $prev_value) {
282 0         0 my $cmp = ($value <=> $prev_value);
283 0 0       0 if ($cmp < 0) {
284             # value < $prev_value
285 0         0 $increasing_from_i = $i;
286 0         0 $non_decreasing_from_i = $i;
287             }
288 0 0       0 if ($cmp <= 0) {
289             # value <= $prev_value
290 0         0 $increasing_from_i = $i;
291             }
292             }
293              
294 0         0 $total_count++;
295 0         0 $smaller_count += (abs($value) <= $i);
296 0         0 $strictly_smaller_count += ($value < $i);
297              
298 0         0 $prev_i = $value;
299 0         0 $prev_value = $value;
300 0         0 };
301              
302 0 0       0 if (my $fh = $self->{'fh'}) {
303 0         0 my $oldpos = _tell($self);
304 0         0 while (($i, $value) = _readline($self)) {
305 0         0 $analyze->($value);
306 0 0       0 last if $total_count > 200;
307             }
308 0         0 _seek ($self, $oldpos);
309             } else {
310 0         0 $i = $i_start;
311 0         0 foreach (@{$self->{'array'}}) {
  0         0  
312 0         0 $i++;
313 0         0 $value = $_;
314 0         0 $analyze->();
315             }
316             }
317              
318 0   0     0 my $range_is_small = (defined $values_max
319             && $values_max - $values_min <= 16);
320             ### $range_is_small
321              
322             # "full" means whole sequence in sample values
323             # "sign" means negatives in sequence
324 0 0 0     0 if (! defined $self->{'values_min'}
      0        
325             && ($range_is_small
326             || $self->{'characteristic'}->{'OEIS_full'}
327             || ! $self->{'characteristic'}->{'OEIS_sign'})) {
328             ### set values_min: $values_min
329 0         0 $self->{'values_min'} = $values_min;
330             }
331 0 0 0     0 if (! defined $self->{'values_max'}
      0        
332             && ($range_is_small
333             || $self->{'characteristic'}->{'OEIS_full'})) {
334             ### set values_max: $values_max
335 0         0 $self->{'values_max'} = $values_max;
336             }
337              
338 0   0     0 $self->{'characteristic'}->{'smaller'}
339             = ($total_count == 0
340             || ($smaller_count / $total_count >= .9
341             && $strictly_smaller_count > 0));
342             ### decide smaller: $self->{'characteristic'}->{'smaller'}
343              
344             ### $increasing_from_i
345 0 0 0     0 if (defined $prev_i && $increasing_from_i < $prev_i) {
346 0 0       0 if ($increasing_from_i - $i_start < 20) {
347 0         0 $self->{'characteristic'}->{'increasing_from_i'} = $increasing_from_i;
348             }
349 0 0       0 if ($increasing_from_i == $i_start) {
350 0         0 $self->{'characteristic'}->{'increasing'} = 1;
351             }
352             }
353              
354             ### $non_decreasing_from_i
355 0 0 0     0 if (defined $prev_i && $non_decreasing_from_i < $prev_i) {
356 0 0       0 if ($non_decreasing_from_i - $i_start < 20) {
357 0         0 $self->{'characteristic'}->{'non_decreasing_from_i'} = $non_decreasing_from_i;
358             }
359 0 0       0 if ($non_decreasing_from_i == $i_start) {
360 0         0 $self->{'characteristic'}->{'non_decreasing'} = 1;
361             }
362             }
363              
364 0         0 return $self;
365             }
366              
367             # # compare $x <=> $y but in strings in case they're bigger than IV or NV
368             # # my $cmp = _value_cmp ($value, $prev_value);
369             # sub _value_cmp {
370             # my ($x, $y) = @_;
371             # ### _value_cmp(): "$x $y"
372             # ### cmp: $x cmp $y
373             #
374             # my $x_neg = substr($x,0,1) eq '-';
375             # my $y_neg = substr($y,0,1) eq '-';
376             # ### $x_neg
377             # ### $y_neg
378             #
379             # return ($y_neg <=> $x_neg
380             # || ($x_neg ? -1 : 1) * (length($x) <=> length($y)
381             # || $x cmp $y));
382             # }
383              
384             sub _seek {
385 0     0   0 my ($self, $pos) = @_;
386 0 0       0 seek ($self->{'fh'}, $pos, 0)
387             or croak "Cannot seek $self->{'filename'}: $!";
388             }
389             sub _tell {
390 0     0   0 my ($self) = @_;
391 0         0 my $pos = tell $self->{'fh'};
392 0 0       0 if ($pos < 0) {
393 0         0 croak "Cannot tell file position $self->{'filename'}: $!";
394             }
395 0         0 return $pos;
396             }
397              
398             sub rewind {
399 65     65 1 60 my ($self) = @_;
400             ### OEIS-File rewind() ...
401              
402 65         110 $self->{'i'} = $self->i_start;
403 65         60 $self->{'array_pos'} = 0;
404 65         85 $self->{'next_seek'} = 0;
405             }
406              
407             sub next {
408 0     0 1 0 my ($self) = @_;
409             ### OEIS-File next(): "i=$self->{'i'}"
410              
411 0         0 my $value;
412 0 0       0 if (my $fh = $self->{'fh'}) {
413             ### from readline ...
414 0 0       0 if (defined (my $pos = delete $self->{'next_seek'})) {
415             ### seek to: $pos
416 0         0 _seek($self, $pos);
417             }
418 0         0 return _readline($self);
419              
420             } else {
421             ### from array ...
422 0 0       0 my ($value) = _array_value($self, $self->{'array_pos'}++)
423             or return;
424 0         0 return ($self->{'i'}++, $value);
425             }
426             }
427              
428             # Return $self->{'array'}->[$pos], or no values if $pos past end of array.
429             # Array values are promoted to BigInt if necessary.
430             sub _array_value {
431 0     0   0 my ($self, $pos) = @_;
432             ### _array_value(): $pos
433              
434 0         0 my $array = $self->{'array'};
435 0 0       0 if ($pos > $#$array) {
436             ### past end of array ...
437 0         0 return;
438             }
439 0         0 my $value = $array->[$pos];
440              
441             # large values as Math::BigInt
442             # initially $array has strings, make bigint objects when required
443 0 0 0     0 if (! ref $value && length($value) > _MAX_DIGIT_LENGTH) {
444 0         0 $value = $array->[$pos] = _to_bigint($value);
445             }
446             ### $value
447 0         0 return $value;
448             }
449              
450             # Read a line from an open B-file or A-file, return ($i,$value).
451             # At EOF return empty ().
452             #
453             sub _readline {
454 0     0   0 my ($self) = @_;
455 0         0 my $fh = $self->{'fh'};
456 0         0 while (defined (my $line = <$fh>)) {
457 0         0 chomp $line;
458 0         0 $line =~ tr/\r//d; # delete CR if CRLF line endings, eg. b009000.txt
459             ### $line
460              
461 0 0       0 if ($line =~ /^\s*(#|$)/) {
462             ### ignore blank or comment ...
463             # comment lines with "#" eg. b002182.txt
464 0         0 next;
465             }
466              
467             # leading whitespace allowed as per b195467.txt
468 0 0       0 if (my ($i, $value) = ($line =~ /^\s*
469             ([0-9]+) # i
470             [ \t]+
471             (-?[0-9]+) # value
472             [ \t]*
473             $/x)) {
474             ### _readline: "$i $value"
475 0 0       0 if (length($value) > _MAX_DIGIT_LENGTH) {
476 0         0 $value = _to_bigint($value);
477             }
478 0         0 $self->{'fh_i'} = $i+1;
479 0         0 return ($i, $value);
480             }
481             }
482 0         0 undef $self->{'fh_i'};
483 0         0 return;
484             }
485              
486             # Return true if the a000000.txt file in $self->{'fh'} looks good.
487             # Various a-files are source code or tables rather than sequence values.
488             #
489             sub _afile_is_good {
490 0     0   0 my ($self) = @_;
491 0         0 my $fh = $self->{'fh'};
492 0         0 my $good = 0;
493 0         0 my $prev_i;
494 0         0 while (defined (my $line = <$fh>)) {
495 0         0 chomp $line;
496 0         0 $line =~ tr/\r//d; # delete CR if CRLF line endings, eg. b009000.txt
497             ### $line
498              
499 0 0       0 if ($line =~ /^\s*(#|$)/) {
500             ### ignore blank or comment ...
501 0         0 next;
502             }
503              
504             # Must have line like "0 123". Can have negative OFFSET and so index i,
505             # eg. A166242 (though that one doesn't have an A-file).
506 0 0       0 my ($i,$value) = ($line =~ /^(-?[0-9]+) # i
507             [ \t]+
508             (-?[0-9]+) # value
509             [ \t]*
510             $/x)
511             or last;
512              
513 0 0 0     0 if (defined $prev_i && $i != $prev_i+1) {
514             ### bad A-file, initial "i" values not consecutive ...
515 0         0 last;
516             }
517 0         0 $prev_i = $i;
518              
519 0         0 $good++;
520 0 0       0 if ($good >= 3) {
521             ### three good lines, A-file is good ...
522 0         0 _seek ($self, 0);
523 0         0 return 1;
524             }
525             }
526 0         0 return 0;
527             }
528              
529             sub _read_internal_txt {
530 65     65   62 my ($self, $anum) = @_;
531             ### _read_internal_txt(): $anum
532              
533 65 100       132 return 0 if $self->{'_dont_use_internal'};
534              
535 52         98 foreach my $basefile ("$anum.internal.txt") {
536 52 50       84 my ($fullname, $contents) = _slurp_oeis_file($self,$basefile)
537             or next;
538 0 0       0 if (_HAVE_ENCODE) {
539             # "Internal" text format is utf-8.
540 0         0 $contents = Encode::decode('utf-8', $contents, Encode::FB_PERLQQ());
541             }
542              
543             ### $contents
544              
545             # eg. "%O A007318 0,5"
546 0         0 my $offset;
547 0 0       0 if ($contents =~ /^%O\s+\Q$anum\E\s+(\d+)/im) {
548 0         0 $offset = $1;
549             ### %O line: $offset
550             } else {
551 0         0 $offset = 0;
552             }
553              
554             # eg. "%N A007318 Pascal's triangle ..."
555 0 0       0 if ($contents =~ m{^%N\s+\Q$anum\E\s+(.*)}im) {
556 0         0 _set_description ($self, $1);
557             } else {
558             ### description not matched ...
559             }
560              
561             # eg. "%K A007318 nonn,tabl,nice,easy,core,look,hear,changed"
562 0   0     0 _set_characteristics ($self,
563             $contents =~ /^%K\s+\Q$anum\E\s+(.*)/im && $1);
564              
565             # the eishelp1.html says
566             # %V,%W,%X lines for signed sequences
567             # %S,%T,%U lines for non-negative sequences
568             # though now %S is signed and unsigned both is it?
569             #
570 0 0       0 if (! $self->{'fh'}) {
571 0         0 my @samples;
572             # capital %STU etc, but any case
573 0         0 while ($contents =~ m{^%[VWX]\s+\Q$anum\E\s+(.*)}mg) {
574 0         0 push @samples, $1;
575             }
576 0 0       0 unless (@samples) {
577 0         0 while ($contents =~ m{^%[STU]\s+\Q$anum\E\s+(.*)}mg) {
578 0         0 push @samples, $1;
579             }
580 0 0       0 unless (@samples) {
581 0         0 croak "Oops list of values not found in ",$self->{'filename'};
582             }
583             }
584             # join multiple lines of samples
585 0         0 _split_sample_values ($self, join(', ',@samples));
586             }
587              
588             # %O "OFFSET" is subscript of first number.
589             # Or for digit expansions it's the number of terms before the decimal
590             # point, per http://oeis.org/eishelp2.html#RO
591             #
592 0 0       0 unless ($self->{'characteristic'}->{'digits'}) {
593 0         0 $self->{'i'} = $self->{'i_start'} = $offset;
594             }
595             ### i: $self->{'i'}
596             ### i_start: $self->{'i_start'}
597              
598 0         0 return 1; # success
599             }
600              
601 52         160 return 0; # file not found
602             }
603              
604             sub _read_internal_html {
605 65     65   53 my ($self, $anum) = @_;
606             ### _read_internal_html(): $anum
607              
608 65 100       128 return 0 if $self->{'_dont_use_internal'};
609              
610 52         90 foreach my $basefile ("$anum.internal.html") {
611 52 50       64 my ($fullname, $contents) = _slurp_oeis_file($self,$basefile)
612             or next;
613             # "Internal" files are served as html with a charset indicator
614 0         0 $contents = _decode_html_charset($contents);
615             ### $contents
616              
617 0         0 my $offset;
618 0 0       0 if ($contents =~ /(^|)%O\s+(\d+)/im) {
619 0         0 $offset = $2;
620             ### %O line: $offset
621             } else {
622 0         0 $offset = 0;
623             }
624              
625 0 0       0 if ($contents =~ m{(^|)%N (.*?)(|$)}im) {
626 0         0 _set_description ($self, $2);
627             } else {
628             ### description not matched ...
629             }
630              
631 0   0     0 _set_characteristics ($self,
632             $contents =~ /(^|)%K (.*?)(|$)/im
633             && $2);
634              
635             # the eishelp1.html says
636             # %V,%W,%X lines for signed sequences
637             # %S,%T,%U lines for non-negative sequences
638             # though now %S is signed and unsigned both is it?
639             #
640 0 0       0 if (! $self->{'fh'}) {
641 0         0 my @samples;
642             # capital %STU etc, but any case
643 0         0 while ($contents =~ m{(^|<[tT][tT]>)%[VWX] (.*?)(|$)}mg) {
644 0         0 push @samples, $2;
645             }
646 0 0       0 unless (@samples) {
647 0         0 while ($contents =~ m{(^|<[tT][tT]>)%[STU] (.*?)(|$)}mg) {
648 0         0 push @samples, $2;
649             }
650 0 0       0 unless (@samples) {
651 0         0 croak "Oops list of values not found in ",$self->{'filename'};
652             }
653             }
654             # join multiple lines of samples
655 0         0 _split_sample_values ($self, join(', ',@samples));
656             }
657              
658             # %O "OFFSET" is subscript of first number.
659             # Or for digit expansions it's the number of terms before the decimal
660             # point, per http://oeis.org/eishelp2.html#RO
661             #
662 0 0       0 unless ($self->{'characteristic'}->{'digits'}) {
663 0         0 $self->{'i'} = $self->{'i_start'} = $offset;
664             }
665             ### i: $self->{'i'}
666             ### i_start: $self->{'i_start'}
667              
668 0         0 return 1; # success
669             }
670              
671 52         182 return 0; # file not found
672             }
673              
674             # Fill $self with contents of ~/OEIS/A000000.html but various fragile greps
675             # of the html.
676             # Return 1 if .html or .htm file exists, 0 if not.
677             #
678             sub _read_html {
679 65     65   56 my ($self, $anum) = @_;
680             ### _read_html(): $anum
681              
682 65 100       121 return 0 if $self->{'_dont_use_html'};
683              
684 52         77 foreach my $basefile ("$anum.html", "$anum.htm") {
685 104 50       127 my ($fullname, $contents) = _slurp_oeis_file($self,$basefile)
686             or next;
687 0         0 $contents = _decode_html_charset($contents);
688              
689 0 0       0 if ($contents =~
690             m{$anum[ \t]*\n.*? # target anum
691             ]*>\s*(?:
692             ]*> #
693             \s*
694             (.*?) # text through to ...
695             (
| or
696             }isx) {
697 0         0 _set_description ($self, $1);
698             } else {
699             ### description not matched ...
700             }
701              
702 0   0     0 my $offset = ($contents =~ /OFFSET.*?<[tT][tT]>(\d+)/s
703             && $1);
704             ### $offset
705              
706             # fragile grep out of the html ...
707 0         0 my $keywords;
708 0 0       0 if ($contents =~ m{KEYWORD.*?<[tT][tT][^>]*>(.*?)}s) {
709             ### html keywords match: $1
710 0         0 $keywords = $1;
711             } else {
712             # die "Oops, KEYWORD not matched: $anum";
713             }
714 0         0 _set_characteristics ($self, $keywords);
715              
716 0 0       0 if (! $self->{'fh'}) {
717             # fragile grep out of the html ...
718 0         0 $contents =~ s{>graph.*}{};
719 0         0 $contents =~ m{.*([^<]+)}i;
720 0         0 my $list = $1;
721 0         0 _split_sample_values ($self, $list);
722             }
723              
724             # %O "OFFSET" is subscript of first number, but for digit expansions
725             # it's the position of the decimal point
726             # http://oeis.org/eishelp2.html#RO
727 0 0       0 if (! $self->{'characteristic'}->{'digits'}) {
728 0         0 $self->{'i'} = $self->{'i_start'} = $offset;
729             }
730             ### i: $self->{'i'}
731             ### i_start: $self->{'i_start'}
732              
733 0         0 return 1;
734             }
735 52         122 return 0;
736             }
737              
738             # Return the contents of ~/OEIS/$filename.
739             # $filename is like "A000000.html" to be taken relative to oeis_dir().
740             # If $filename cannot be read then return undef.
741             sub _slurp_oeis_file {
742 208     208   189 my ($self,$filename) = @_;
743 208         210 $filename = File::Spec->catfile (oeis_dir(), $filename);
744             ### $filename
745              
746 208 50       5824 if (! open FH, "< $filename") {
747             ### cannot open file: $!
748 208         590 return;
749             }
750 0           my $contents = do { local $/; }; # slurp
  0            
  0            
751 0 0         close FH
752             or return;
753 0   0       $self->{'filename'} ||= $filename;
754 0           return ($filename, $contents);
755             }
756              
757             sub _set_description {
758 0     0     my ($self, $description) = @_;
759             ### _set_description(): $description
760              
761 0           $description =~ s/\s+$//; # trailing whitespace
762 0           $description =~ s/\s+/ /g; # collapse whitespace
763 0           $description =~ s/<[^>]*?>//sg; # tags
764 0           $description =~ s/</
765 0           $description =~ s/>/>/ig; # unentitize >
766 0           $description =~ s/&/&/ig; # unentitize &
767 0           $description =~ s/&#(\d+);/chr($1)/ge; # unentitize numeric ' and "
  0            
768              
769             # ENHANCE-ME: maybe __x() if made available, or an sprintf "... %s" would
770             # be enough ...
771 0           $description .= "\n";
772 0 0         if ($self->{'fh'}) {
773             $description .= sprintf(Math::NumSeq::__('Values from B-file %s'),
774 0           $self->{'filename'})
775             } else {
776             $description .= sprintf(Math::NumSeq::__('Values from %s'),
777 0           $self->{'filename'})
778             }
779 0           $self->{'description'} = $description;
780             }
781              
782             sub _set_characteristics {
783 0     0     my ($self, $keywords) = @_;
784             ### _set_characteristics()
785             ### $keywords
786              
787 0 0         if (! defined $keywords) {
788 0           return; # if perhaps match of .html failed
789             }
790              
791 0           $keywords =~ s{<[^>]*>}{}g; # tags
792             ### $keywords
793              
794 0   0       foreach my $key (split /[, \t]+/, ($keywords||'')) {
795             ### $key
796 0           $self->{'characteristic'}->{"OEIS_$key"} = 1;
797             }
798              
799             # if ($self->{'characteristic'}->{'OEIS_cofr'}) {
800             # $self->{'characteristic'}->{'continued_fraction'} = 1;
801             # }
802              
803             # "cons" means decimal digits of a constant
804             # but don't reckon A000012 all-ones that way
805             # "base" means non-decimal, it seems, maybe
806 0 0 0       if ($self->{'characteristic'}->{'OEIS_cons'}
      0        
807             && ! $self->{'characteristic'}->{'OEIS_base'}
808             && $self->{'anum'} ne 'A000012') {
809 0           $self->{'values_min'} = 0;
810 0           $self->{'values_max'} = 9;
811 0           $self->{'characteristic'}->{'digits'} = 10;
812             }
813              
814 0 0         if (defined (my $description = $self->{'description'})) {
815 0 0         if ($description =~ /expansion of .* in base (\d+)/i) {
816 0           $self->{'values_min'} = 0;
817 0           $self->{'values_max'} = $1 - 1;
818 0           $self->{'characteristic'}->{'digits'} = $1;
819             }
820 0 0         if ($description =~ /^number of /i) {
821 0           $self->{'characteristic'}->{'count'} = 1;
822             }
823             }
824             }
825              
826             sub _split_sample_values {
827 0     0     my ($self, $str) = @_;
828             ### _split_sample_values(): $str
829 0 0 0       unless (defined $str && $str =~ m{^([0-9,-]|\s)+$}) {
830 0 0         croak "Oops list of sample values not recognised in ",$self->{'filename'},"\n",
831             (defined $str ? $str : ());
832             }
833 0           $self->{'array'} = [ split /[, \t\r\n]+/, $str ];
834             }
835              
836             sub _decode_html_charset {
837 0     0     my ($contents) = @_;
838              
839             # eg.
840             # HTTP::Message has a blob of code for this, using the full HTTP::Parser,
841             # but a slack regexp should be enough for OEIS pages.
842             #
843 0 0 0       if (_HAVE_ENCODE
844             && $contents =~ m{]+
845             http-equiv=[^>]+
846             content-type[^>]+
847             charset=([a-z0-9-_]+)}isx) {
848 0           return Encode::decode($1, $contents, Encode::FB_PERLQQ());
849             } else {
850 0           return $contents;
851             }
852             }
853              
854             #------------------------------------------------------------------------------
855              
856             # Similar bsearch to Search::Dict, but Search::Dict doesn't allow for
857             # comment lines at the start of the file or blank lines at the end.
858             #
859             #use Smart::Comments;
860              
861             sub ith {
862 0     0 1   my ($self, $i) = @_;
863             ### ith(): "$i cf fh_i=".($self->{'fh_i'} || -999)
864              
865 0 0         if (my $fh = $self->{'fh'}) {
866 0 0         if (! defined $self->{'next_seek'}) {
867 0           $self->{'next_seek'} = tell($fh);
868             }
869              
870 0 0 0       if (defined $self->{'fh_i'} && $i <= $self->{'fh_i'} + 20) {
871             ### fh_i is target ...
872 0 0         if (my ($line_i, $value) = _readline($self)) {
873 0 0         if ($line_i == $i) {
874 0           return $value;
875             }
876             }
877             }
878              
879 0           my $lo = 0;
880 0           my $hi = -s $fh;
881 0           for (;;) {
882             ### at: "lo=$lo hi=$hi consider mid=".int(($lo+$hi)/2)
883 0           my $mid = int(($lo+$hi)/2);
884 0           _seek ($self, $mid);
885              
886 0 0         if (! defined(readline $fh)) {
887             ### mid is EOF ...
888 0           last;
889             }
890             ### skip partial line to: tell($fh)
891 0           $mid = tell($fh);
892 0 0         if ($mid >= $hi) {
893 0           last;
894             }
895              
896 0 0         my ($line_i,$value) = _readline($self)
897             or last; # only blank lines between $mid and EOF, go linear
898              
899             ### $line_i
900             ### $value
901 0 0         if ($line_i == $i) {
902             ### found by binary search ...
903 0           return $value;
904             }
905 0 0         if ($line_i < $i) {
906             ### line_i before the target, advance lo ...
907 0           $lo = tell($fh);
908             } else {
909             ### line_i after target, reduce hi ...
910 0           $hi = $mid;
911             }
912             }
913              
914 0           _seek ($self, $lo);
915 0           for (;;) {
916 0 0         my ($line_i,$value) = _readline($self)
917             or last;
918 0 0         if ($line_i == $i) {
919             ### found by linear search ...
920 0           $self->{'fh_i'} = $line_i+1;
921 0           return $value;
922             }
923 0 0         if ($line_i > $i) {
924 0           return undef;
925             }
926             }
927 0           return undef;
928              
929             } else {
930 0           $i -= $self->i_start;
931 0 0         unless ($i >= 0) {
932 0           return undef; # negative or NaN
933             }
934 0           return $self->{'array'}->[$i];
935             }
936             }
937              
938             1;
939             __END__