File Coverage

blib/lib/Finance/QuoteHist/Generic.pm
Criterion Covered Total %
statement 21 715 2.9
branch 0 428 0.0
condition 0 209 0.0
subroutine 7 86 8.1
pod 20 69 28.9
total 48 1507 3.1


line stmt bran cond sub pod time code
1             package Finance::QuoteHist::Generic;
2              
3             # http://www.stanford.edu/dept/OOD/RESEARCH/top-ten-faq/how_do_i_find_an_historical_st.html
4             #
5             # Shortcut: Use adjusted close price
6             #
7             # For the mathematically inclined, one shortcut to determining the value
8             # after splits is to use the adjusted close price from the historical
9             # quote tool. For June 2, 1997, it lists a market close price of 33.13
10             # and an adjusted close price of 1.38. Divide 33.13 by 1.38 and you come
11             # up with 24.007. Multiply by 1,000 and you come pretty close to the
12             # 24,000 share figure determined above. Or you could divide 1.38 by
13             # 33.13, which gives you 0.041654. Divide $33,130 by 0.041654, and you
14             # get $795K, which is very close to the $808K figure above.
15              
16 5     5   39 use strict;
  5         10  
  5         143  
17 5     5   24 use Carp;
  5         11  
  5         257  
18              
19 5     5   26 use vars qw($VERSION);
  5         11  
  5         232  
20             $VERSION = "1.22";
21              
22 5     5   30 use LWP::UserAgent;
  5         10  
  5         92  
23 5     5   22 use HTTP::Request;
  5         10  
  5         148  
24 5     5   2452 use Date::Manip;
  5         840710  
  5         49956  
25              
26             my $CSV_XS_Class = 'Text::CSV_XS';
27             my $CSV_PP_Class = 'Text::CSV_PP';
28             my $CSV_Class = $CSV_XS_Class;
29 5     5   5372 eval "use $CSV_Class";
  5         64491  
  5         228  
30             if ($@) {
31             $CSV_Class = $CSV_PP_Class;
32             eval "use $CSV_Class";
33             croak "Could not load either $CSV_XS_Class or $CSV_PP_Class : $@\n" if $@;
34             }
35              
36             my $HTE_CLASS;
37             my $HTE_Class = 'HTML::TableExtract';
38             sub HTML_CLASS {
39 0 0   0 0   if (!$HTE_CLASS) {
40 0           eval "use $HTE_Class";
41 0 0         croak $@ if $@;
42 0           $HTE_CLASS = $HTE_Class;
43             }
44 0           $HTE_CLASS;
45             }
46              
47             my $Default_Target_Mode = 'quote';
48             my $Default_Parse_Mode = 'html';
49             my $Default_Granularity = 'daily';
50             my $Default_Vol_Pat = qr(vol|shares)i;
51              
52             my %Default_Labels;
53             $Default_Labels{quote}{$Default_Parse_Mode} =
54             [qw( date open high low close ), $Default_Vol_Pat];
55             $Default_Labels{dividend}{$Default_Parse_Mode} =
56             [qw( date div )];
57             $Default_Labels{'split'}{$Default_Parse_Mode} =
58             [qw( date post pre )];
59             $Default_Labels{intraday}{$Default_Parse_Mode} =
60             [qw( date time high low close ), $Default_Vol_Pat];
61              
62             my @Scalar_Flags = qw(
63             verbose
64             quiet
65             zthresh
66             quote_precision
67             attempts
68             adjusted
69             has_non_adjusted
70             env_proxy
71             debug
72             parse_mode
73             target_mode
74             granularity
75             auto_proxy
76             row_filter
77             ua_params
78             );
79             my $SF_pat = join('|', @Scalar_Flags);
80              
81             my @Array_Flags = qw(
82             symbols
83             lineup
84             );
85             my $AF_pat = join('|', @Array_Flags);
86              
87             my @Hash_Flags = qw( ua_params );
88             my $HF_pat = join('|', @Hash_Flags);
89              
90             sub new {
91 0     0 1   my $that = shift;
92 0   0       my $class = ref($that) || $that;
93 0           my(%parms, $k, $v);
94 0           while (($k,$v) = splice(@_, 0, 2)) {
95 0 0 0       if ($k eq 'start_date' || $k eq 'end_date' && $v !~ /^\s*$/) {
    0 0        
    0          
    0          
    0          
96 0           $parms{$k} = __PACKAGE__->date_standardize($v);
97             }
98             elsif ($k =~ /^$AF_pat$/o) {
99 0 0         if (UNIVERSAL::isa($v, 'ARRAY')) {
    0          
100 0           $parms{$k} = $v;
101             }
102             elsif (ref $v) {
103 0           croak "$k must be passed as an array ref or single-entry string\n";
104             }
105             else {
106 0           $parms{$k} = [$v];
107             }
108             }
109             elsif ($k =~ /^$HF_pat$/o) {
110 0 0         if (UNIVERSAL::isa($v, 'HASH')) {
111 0           $parms{$k} = $v;
112             }
113             else {
114 0           croak "$k must be passed as a hash ref\n";
115             }
116             }
117             elsif ($k eq 'row_filter') {
118 0 0         croak "$k must be sub ref\n" unless UNIVERSAL::isa($v, 'CODE');
119 0           $parms{$k} = $v;
120             }
121             elsif ($k =~ /^$SF_pat$/o) {
122 0           $parms{$k} = $v;
123             }
124             }
125 0   0       $parms{end_date} ||= __PACKAGE__->date_standardize('today');
126 0 0         $parms{symbols} or croak "Symbol list required\n";
127              
128 0           my $start_date = delete $parms{start_date};
129 0           my $end_date = delete $parms{end_date};
130 0           my $symbols = delete $parms{symbols};
131              
132             # Defaults
133 0 0         $parms{zthresh} = 30 unless $parms{zthresh};
134 0 0         $parms{attempts} = 3 unless $parms{attempts};
135 0 0         $parms{adjusted} = 1 unless exists $parms{adjusted};
136 0 0         $parms{has_non_adjusted} = 0 unless defined $parms{has_non_adjusted};
137 0 0         $parms{quote_precision} = 4 unless defined $parms{quote_precision};
138 0 0         $parms{auto_proxy} = 1 unless exists $parms{auto_proxy};
139 0 0         $parms{debug} = 0 unless defined $parms{debug};
140              
141 0           my $self = \%parms;
142 0           bless $self, $class;
143              
144 0   0       my $ua_params = $parms{ua_params} || {};
145 0 0         if ($parms{env_proxy}) {
    0          
146 0           $ua_params->{env_proxy} = 1;
147             }
148             elsif ($parms{auto_proxy}) {
149 0 0         $ua_params->{env_proxy} = 1 if $ENV{http_proxy};
150             }
151 0   0       $self->{ua} ||= LWP::UserAgent->new(%$ua_params);
152              
153 0 0         if ($self->granularity !~ /^d/i) {
154 0           $start_date = $self->snap_start_date($start_date);
155 0           $end_date = $self->snap_end_date($end_date);
156             }
157              
158 0           $self->start_date($start_date);
159 0           $self->end_date($end_date);
160 0           $self->symbols(@$symbols);
161              
162             # These are used for constructing method names for target types.
163 0           $self->{target_order} = [qw(quote split dividend)];
164 0           grep($self->{targets}{$_} = "${_}s", @{$self->{target_order}});
  0            
165              
166 0           $self;
167             }
168              
169             ### User interface stubs
170              
171 0     0 1   sub quotes { shift->getter(target_mode => 'quote')->() }
172 0     0 1   sub dividends { shift->getter(target_mode => 'dividend')->() }
173 0     0 1   sub splits { shift->getter(target_mode => 'split')->() }
174 0     0 0   sub intraday { shift->getter(target_mode => 'intraday')->() }
175              
176             *intraday_quotes = *intraday;
177              
178             sub target_worthy {
179 0     0 0   my $self = shift;
180 0           my %parms = @_;
181 0   0       my $target_mode = $parms{target_mode} || $self->target_mode;
182 0   0       my $parse_mode = $parms{parse_mode} || $self->parse_mode;
183             # forcing url_maker into a boolean role here, using a dummy symbol
184 0           my $capable = $self->url_maker(
185             %parms,
186             target_mode => $target_mode,
187             parse_mode => $parse_mode,
188             symbol => 'waggledance',
189             );
190 0   0       my $worthy = $capable && UNIVERSAL::isa($capable, 'CODE');
191 0 0         if ($self->{verbose}) {
192 0 0         print STDERR "Seeing if ", ref $self,
193             " can get ($target_mode, $parse_mode) : ",
194             $worthy ? "yes\n" : "no\n";
195             }
196 0           $worthy;
197             }
198              
199 0     0 0   sub granularities { qw( daily ) }
200              
201             ### Data retrieval
202              
203             sub ua {
204 0     0 1   my $self = shift;
205 0 0         @_ ? $self->{ua} = shift : $self->{ua};
206             }
207              
208             sub fetch {
209             # HTTP::Request and LWP::UserAgent Wrangler
210 0     0 0   my($self, $request) = splice(@_, 0, 2);
211 0 0         $request or croak "Request or URL required\n";
212              
213 0 0 0       if (! ref $request || ! $request->isa('HTTP::Request')) {
214 0           $request = HTTP::Request->new(GET => $request);
215             }
216              
217 0           my $trys = $self->{attempts};
218 0           my $response = $self->ua->request($request, @_);
219 0           $self->{_lwp_success} = 0;
220 0           while (! $response->is_success) {
221 0 0         last unless $trys;
222 0           $self->{_lwp_status} = $response->status_line;
223             print STDERR "Bad fetch",
224             $response->is_error ? ' (' . $response->status_line . '), ' : ', ',
225 0 0         "trying again...\n" if $self->{debug};
    0          
226 0           $response = $self->ua->request($request, @_);
227 0           --$trys;
228             }
229 0           $self->{_lwp_success} = $response->is_success;
230 0 0         return undef unless $response->is_success;
231             print STDERR 'Fetch complete. (' . length($response->content) . " chars)\n"
232 0 0         if $self->{verbose};
233 0           $response->content;
234             }
235              
236             sub getter {
237             # closure factory to get results for a particular target_mode and
238             # parse_mode
239 0     0 0   my $self = shift;
240              
241 0           my %parms = @_;
242 0   0       my $target_mode = $parms{target_mode} || $self->target_mode;
243 0   0       my $parse_mode = $parms{parse_mode} || $self->parse_mode;
244 0           my @column_labels = $self->labels(
245             %parms, target_mode => $target_mode, parse_mode => $parse_mode
246             );
247 0           my %extractors = $self->extractors(
248             %parms, target_mode => $target_mode, parse_mode => $parse_mode
249             );
250              
251             # return our closure
252             sub {
253 0 0   0     my @symbols = @_ ? @_ : $self->symbols;
254              
255 0           my @rows;
256              
257             # cache check
258             my @not_seen;
259 0           foreach my $symbol (@symbols) {
260 0           my @r = $self->result_rows($target_mode, $symbol);
261 0 0         if (@r) {
262 0           push(@rows, @r);
263             }
264             else {
265 0           push(@not_seen, $symbol);
266             }
267             }
268 0 0         return @rows unless @not_seen;
269              
270 0           my $original_target_mode = $self->target_mode;
271 0           my $original_parse_mode = $self->parse_mode;
272              
273 0           $self->target_mode($target_mode);
274 0           $self->parse_mode($parse_mode);
275              
276 0           my $dcol = $self->label_column('date');
277 0           my(%empty_fetch, %saw_good_rows);
278 0           my $last_data = '';
279              
280 0           my $target_worthy = $self->target_worthy(
281             %parms,
282             target_mode => $target_mode,
283             parse_mode => $parse_mode
284             );
285 0 0         if (!$target_worthy) {
286             # make sure and empty @symbols
287 0           ++$empty_fetch{$_} while $_ = pop @symbols;
288             }
289              
290 0           SYMBOL: foreach my $s (@symbols) {
291 0           my $urlmaker = $self->url_maker(
292             target_mode => $target_mode,
293             parse_mode => $parse_mode,
294             symbol => $s,
295             );
296 0 0         UNIVERSAL::isa($urlmaker, 'CODE') or croak "urlmaker not a code ref.\n";
297 0           my $so_far_so_good = 0;
298 0           URL: while (my $url = $urlmaker->()) {
299 0 0         if ($empty_fetch{$s}) {
300             print STDERR ref $self,
301             " passing on $s ($target_mode) for now, empty fetch\n"
302 0 0         if $self->{verbose};
303 0           last URL;
304             }
305              
306 0 0         if ($self->{verbose}) {
307 0           my $uri = $url;
308 0 0         $uri = $url->uri if UNIVERSAL::isa($url, 'HTTP::Request');
309 0           print STDERR "Processing ($s:$target_mode) $uri\n";
310             }
311              
312             # We're a bit more persistent with quotes. It is more suspicious
313             # if we get no quote rows, but it is nevertheless possible.
314 0 0         my $trys = $target_mode eq 'quote' ? $self->{attempts} : 1;
315 0           my $initial_trys = $trys;
316 0           my($data, $rows) = ('', []);
317             do {
318             print STDERR "$s Trying ($target_mode) again due to no rows...\n"
319 0 0 0       if $self->{verbose} && $trys != $initial_trys;
320 0 0         if (!($data = $self->{url_cache}{$url})) {
321 0           $data = $self->fetch($url);
322 0 0         if (my $pre_parser = $self->pre_parser) {
323 0           $data = $pre_parser->(
324             $data,
325             target_mode => $target_mode,
326             parse_mode => $parse_mode,
327             );
328             }
329             }
330             # make sure our url_maker hasn't sent us into a twister
331 0 0 0       if ($data && $data eq $last_data) {
332             print STDERR "Redundant data fetch, assuming end of URLs.\n"
333 0 0         if $self->{verbose};
334 0           last URL;
335             }
336             else {
337 0 0         $last_data = defined $data ? $data : '';
338             }
339 0           $rows = $self->rows($self->parser->($data));
340 0 0 0       last URL if $so_far_so_good && !@$rows;
341 0           --$trys;
342 0   0       } while !@$rows && $trys && $self->{_lwp_success};
      0        
343 0           $so_far_so_good = 1;
344              
345 0 0 0       if ($target_mode ne 'quote' || $target_mode ne 'intraday') {
346             # We are not very stubborn about dividends and splits right
347             # now. This is because we cannot prove a successful negative
348             # (i.e., say there were no dividends or splits over the time
349             # period...or perhaps there were, but it is a defunct
350             # symbol...whatever...quotes should always be present unless
351             # they are defunct, which is dealt with later.
352 0 0 0       if (!$self->{_lwp_success} || !$data) {
    0 0        
353 0           ++$empty_fetch{$s};
354 0           @$rows = ();
355             }
356             elsif ($self->{_lwp_success} && !@$rows) {
357 0           ++$empty_fetch{$s};
358             }
359             }
360              
361             # Raw cache
362 0           $self->{url_cache}{$url} = $data;
363            
364             # Extraction filters. This is an opportunity to extract rows
365             # that are not what we are looking for, but contain valuable
366             # information nevertheless. An example of this would be the
367             # split and dividend rows you see in Yahoo HTML quote output. An
368             # extraction filter method should expect an array ref as an
369             # argument, representing a single row, and should return another
370             # array ref with extracted output. If there is a return value,
371             # then this row will be filtered from the primary output.
372 0           my(%extractions, $ecount, $rc);
373 0           $rc = @$rows;
374 0 0         if (%extractors) {
375 0           my(@filtered, $row);
376 0           while ($row = pop(@$rows)) {
377 0           my $erow;
378 0           foreach my $mode (sort keys %extractors) {
379 0   0       $extractions{$mode} ||= [];
380 0           my $em = $extractors{$mode};
381 0 0         if ($erow = $em->($row)) {
382             print STDERR "$s extract ($mode) got $s, ",
383 0 0         join(', ', @$erow), "\n" if $self->{verbose};
384 0           push(@{$extractions{$mode}}, [@$erow]);
  0            
385 0           ++$ecount;
386 0           last;
387             }
388             }
389 0 0         push(@filtered, $row) unless $erow;
390             }
391 0 0 0       if ($self->{verbose} && $ecount) {
392 0           print STDERR "$s Trimmed to ",$rc - $ecount,
393             " rows after $ecount extractions.\n";
394             }
395 0           $rows = \@filtered;
396             }
397              
398 0 0         if ($extractions{$target_mode}) {
399 0           $rows = [@{$extractions{$target_mode}}];
  0            
400             print STDERR "Coopted to ", scalar @$rows,
401             " rows after $target_mode extraction redundancy.\n"
402 0 0         if $self->{verbose};
403             }
404              
405 0 0         if (@$rows) {
406             # Normalization steps
407              
408 0 0         if ($target_mode eq 'split') {
409 0 0         if (@{$rows->[0]} == 2) {
  0            
410 0           foreach (@$rows) {
411 0 0         if ($_->[-1] =~ /(split\s+)?(\d+)\D+(\d+)/is) {
412 0           splice(@$_, -1, 1, $2, $3);
413             }
414             }
415             }
416             }
417              
418             # Saving the rounding operations until after the adjust
419             # routine is deliberate since we don't want to be auto-
420             # adjusting pre-rounded numbers.
421 0           $self->number_normalize_rows($rows);
422            
423             # Do the same for the extraction rows, plus store the
424             # extracted rows
425 0           foreach my $mode (keys %extractions) {
426             # _store_results splices each row...don't do it twice
427 0 0         next if $mode eq $target_mode;
428 0           $self->target_mode($mode);
429 0           $self->number_normalize_rows($extractions{$mode});
430 0           $self->_target_source($mode, $s, ref $self);
431 0           $self->_store_results($mode, $s, $dcol, $extractions{$mode});
432             }
433             # restore original target mode
434 0           $self->target_mode($target_mode);
435            
436 0 0 0       if ($target_mode eq 'quote' || $target_mode eq 'intraday') {
437 0           my $count = @$rows;
438 0   0       @$rows = grep($self->is_quote_row($_) &&
439             $self->row_not_seen($s, $_), @$rows);
440 0 0         if ($self->{verbose}) {
441 0 0         if ($count == @$rows) {
442 0           print STDERR "$s Retained $count rows\n";
443             }
444             else {
445 0           print STDERR "$s Retained $count raw rows, trimmed to ",
446             scalar @$rows, " rows due to noise\n";
447             }
448             }
449            
450             }
451 0 0         if ($target_mode eq 'quote') {
452             # zcount is an attempt to capture null values; if there are
453             # too many we assume there is something wrong with the
454             # remote data
455 0           my $close_col = $self->label_column('close');
456 0           my($zcount, $hcount) = (0,0);
457 0           foreach (@$rows) {
458 0           foreach (@$_) {
459             # Sometimes N/A appears
460 0           s%^\s*N/A\s*$%%;
461             }
462 0           my $q = $_->[$close_col];
463 0 0 0       if (defined $q && $q =~ /\d+/) { ++$hcount }
  0            
464 0           else { ++$zcount }
465             }
466 0 0         my $pct = $hcount ? 100 * $zcount / ($zcount + $hcount) : 100;
467 0 0 0       if (!$trys || $pct >= $self->{zthresh}) {
468 0 0         ++$empty_fetch{$s} unless $saw_good_rows{$s};
469             }
470             else {
471             # For defunct symbols, we could conceivably get quotes
472             # over a date range that contains blocks of time where the
473             # ticker was actively traded, as well as blocks of time
474             # where the ticker doesn't exist. If we got good data over
475             # some of the blocks, then we take note of it so we don't
476             # toss the whole set of queries for this symbol.
477 0           ++$saw_good_rows{$s};
478             }
479             $self->precision_normalize_rows($rows)
480 0 0 0       if @$rows && $self->{quote_precision};
481             }
482              
483 0 0 0       last URL if !$ecount && !@$rows;
484 0 0         $self->_store_results($target_mode, $s, $dcol, $rows) if @$rows;
485 0           $self->_target_source($target_mode, $s, ref $self);
486             }
487             }
488             }
489              
490 0           $self->_store_empty_fetches([keys %empty_fetch]);
491            
492             # Check for bad fetches. If we failed on some symbols, punt them to
493             # our champion class.
494 0 0         if (%empty_fetch) {
495 0           my @bad_symbols = $self->empty_fetches;
496 0           my @champion_classes = $self->lineup;
497 0   0       while (@champion_classes && @bad_symbols) {
498             print STDERR "Bad fetch for ", join(',', @bad_symbols), "\n"
499 0 0 0       if $self->{verbose} && $target_worthy;
500 0           my $champion =
501             $self->_summon_champion(shift @champion_classes, @bad_symbols);
502 0 0 0       next unless $champion &&
503             $champion->target_worthy(target_mode => $target_mode);
504 0 0         print STDERR ref $champion, ", my hero!\n" if $self->{verbose};
505             # Hail Mary
506 0           $champion->getter(target_mode => $target_mode)->();
507             # Our champion was the source for these symbols (including
508             # extracted info).
509 0           foreach my $mode ($champion->result_modes) {
510 0           foreach my $symbol ($champion->result_symbols($mode)) {
511 0           $self->_target_source($mode, $symbol, ref $champion);
512 0           $self->_copy_results($mode, $symbol,
513             $champion->results($mode, $symbol));
514             }
515             }
516 0           @bad_symbols = $champion->empty_fetches;
517             }
518 0 0 0       if (@bad_symbols && !$self->{quiet}) {
519 0           print STDERR "WARNING: Could not fetch $target_mode for some symbols (",join(', ', @bad_symbols), "). Abandoning request for these symbols.";
520 0 0         if ($target_mode ne 'quote') {
521 0           print STDERR " Don't worry, though, we were looking for ${target_mode}s. These are less likely to exist compared to quotes.";
522             }
523 0 0         if ($self->{_lwp_status}) {
524 0           print STDERR "\n\nLast status: $self->{_lwp_status}\n";
525             }
526 0           print STDERR "\n";
527             }
528             }
529            
530 0           $self->target_mode($original_target_mode);
531 0           $self->parse_mode($original_parse_mode);
532              
533 0           @rows = $self->result_rows($target_mode);
534 0 0         if ($self->{verbose}) {
535 0           print STDERR "Class ", ref $self, " returning ", scalar @rows,
536             " composite rows.\n";
537             }
538              
539             # Return the loot.
540 0 0         wantarray ? @rows : \@rows;
541 0           };
542             }
543              
544             sub _store_results {
545 0     0     my($self, $mode, $symbol, $dcol, $rows) = @_;
546 0           foreach my $row (@$rows) {
547 0           my $date = splice(@$row, $dcol, 1);
548 0           $self->{results}{$mode}{$symbol}{$date} = $row;
549             }
550             }
551              
552             sub _copy_results {
553 0     0     my($self, $mode, $symbol, $results) = @_;
554 0           foreach my $date (sort keys %$results) {
555 0           $self->{results}{$mode}{$symbol}{$date} = [@{$results->{$date}}];
  0            
556             }
557             }
558              
559             sub result_rows {
560 0     0 0   my($self, $target_mode, @symbols) = @_;
561 0   0       $target_mode ||= $self->target_mode;
562 0 0         @symbols = $self->result_symbols($target_mode) unless @symbols;
563 0           my @rows;
564 0           foreach my $symbol (@symbols) {
565 0           my $results = $self->results($target_mode, $symbol);
566 0           foreach my $date (sort keys %$results) {
567 0           push(@rows, [$symbol, $date, @{$results->{$date}}]);
  0            
568             }
569             }
570 0           sort { $a->[1] cmp $b->[1] } @rows;
  0            
571             }
572              
573             sub _store_empty_fetches {
574 0     0     my $self = shift;
575 0   0       my $ref = shift || [];
576 0           @$ref = sort @$ref;
577 0           $self->{empty_fetches} = $ref;
578             }
579              
580             sub empty_fetches {
581 0     0 0   my $self = shift;
582 0 0         return () unless $self->{empty_fetches};
583 0           @{$self->{empty_fetches}}
  0            
584             }
585              
586 0     0 1   sub extractors { () }
587              
588             sub rows {
589 0     0 0   my($self, $rows) = @_;
590 0 0         return wantarray ? () : [] unless $rows;
    0          
591 0           my $rc = @$rows;
592 0 0         print STDERR "Got $rc raw rows\n" if $self->{verbose};
593              
594             # Load user filter if present
595 0           my $row_filter = $self->row_filter;
596              
597             # Prep the rows
598 0           foreach my $row (@$rows) {
599 0 0         $row_filter->($row) if $row_filter;
600 0           foreach (@$row) {
601             # Zap leading and trailing white space
602 0 0         next unless defined;
603 0           s/^\s+//; s/\s+$//;
  0            
604             }
605             }
606             # Pass only rows with a valid date that is in range (and store the
607             # processed value while we are at it)
608 0           my $target_mode = $self->target_mode;
609 0           my @date_rows;
610 0           my $dcol = $self->label_column('date');
611 0 0         my $tcol = $self->label_column('time') if $target_mode eq 'intraday';
612 0           my $r;
613 0           while($r = pop @$rows) {
614 0           my $date = $r->[$dcol];
615 0 0         if ($target_mode eq 'intraday') {
616 0           my $time = splice(@$r, $tcol, 1);
617 0           $date = join('', $date, $time);
618             }
619 0           $date = $self->date_normalize($date);
620 0 0         unless ($date) {
621 0 0         print STDERR "Reject row (no date): '$r->[$dcol]'\n" if $self->{verbose};
622 0           next;
623             }
624 0 0         next unless $self->date_in_range($date);
625 0           $r->[$dcol] = $date;
626 0           push(@date_rows, $r);
627             }
628              
629             print STDERR "Trimmed to ", scalar @date_rows, " applicable date rows\n"
630 0 0 0       if $self->{verbose} && @date_rows != $rc;
631              
632 0 0         return wantarray ? @date_rows : \@date_rows;
633             }
634              
635             ### Adjustment triggers and manipulation
636              
637             sub adjuster {
638             # In order to be an adjuster, it must first be enabled. In addition,
639             # there has to be a column specified as the adjusted value. This is
640             # not as generic as I would like it, but so far it's just for
641             # Yahoo...it should work for any site with "adj" in the column
642             # label...this column should be the adjusted closing value.
643 0     0 0   my $self = shift;
644 0 0         return 0 if !$self->{adjusted};
645 0           foreach ($self->labels) {
646 0 0         return 1 if /adj/i;
647             }
648 0           0;
649             }
650              
651 0 0   0 0   sub adjusted { shift->{adjusted} ? 1 : 0 }
652              
653             ### Bulk manipulation filters
654              
655             sub date_normalize_rows {
656             # Place dates into a consistent format, courtesy of Date::Manip
657 0     0 0   my($self, $rows, $dcol) = @_;
658 0 0         $dcol = $self->label_column('date') unless defined $dcol;
659 0           foreach my $row (@$rows) {
660 0           $row->[$dcol] = $self->date_normalize($row->[$dcol]);
661             }
662 0           $rows;
663             }
664              
665             sub date_normalize {
666 0     0 0   my($self, $date) = @_;
667 0 0         return unless $date;
668 0           my $normal_date;
669 0 0 0       if ($self->granularity =~ /^m/ && $date =~ m{^\s*(\D+)[-/]+(\d{2,})\s*$}) {
670 0           my($m, $y) = ($1, $2);
671 0 0         $y += 1900 if length $y == 2;
672 0 0         $normal_date = ParseDate($m =~ /^\d+$/ ? "$y/$m/01" : "$m 01 $y");
673             }
674             else {
675 0 0         if ($date =~ /^\d{10}$/) {
676 0           $normal_date = ParseDateString("epoch $date");
677             }
678             else {
679 0           $normal_date = ParseDate($date);
680             }
681             }
682 0 0         $normal_date or return undef;
683 0 0         return $normal_date if $self->target_mode eq 'intraday';
684 0           join('/', $self->ymd($normal_date));
685             }
686              
687             sub snap_start_date {
688 0     0 0   my($self, $date) = @_;
689 0           my $g = $self->granularity;
690 0 0         if ($g =~ /^(m|w)/i) {
691 0 0         if ($1 eq 'm') {
692 0           my($dom) = UnixDate($date, '%d') - 1;
693 0 0         $date = DateCalc($date, "- $dom days") if $dom;
694             }
695             else {
696 0           my $dow = Date_DayOfWeek(UnixDate($date, '%m', '%d', '%Y')) - 1;
697 0 0         $date = DateCalc($date, "- $dow days") if $dow;
698             }
699             }
700 0           $date;
701             }
702              
703             sub snap_end_date {
704 0     0 0   my($self, $date) = @_;
705 0           my $g = $self->granularity;
706 0 0         if ($g =~ /^(m|w)/i) {
707 0 0         if ($1 eq 'm') {
708 0           my($m, $y) = UnixDate($date, '%m', '%Y');
709 0           my $last = Date_DaysInMonth($m, $y);
710 0           $date = ParseDateString("$y$m$last");
711             }
712             else {
713 0           my $dow = Date_DayOfWeek(UnixDate($date, '%m', '%d', '%Y')) - 1;
714 0 0         $date = DateCalc($date, "+ " . (6 - $dow) . ' days')
715             unless $dow == 6;
716             }
717             }
718 0           $date;
719             }
720              
721             sub number_normalize_rows {
722             # Strip non-numeric noise from numeric fields
723 0     0 0   my($self, $rows, $dcol) = @_;
724 0 0         $dcol = $self->label_column('date') unless defined $dcol;
725             # filtered rows might not have same columns
726 0           my @cols = grep($_ != $dcol, 0 .. $#{$rows->[0]});
  0            
727 0           foreach my $row (@$rows) {
728 0           s/[^\d\.]//go foreach @{$row}[@cols];
  0            
729             }
730 0           $rows;
731             }
732              
733             sub precision_normalize_rows {
734             # Round off numeric fields, if requested (%.4f by default). Volume
735             # is the exception -- we just round that into an integer. This
736             # should probably only be called for 'quote' targets because it
737             # knows details about where the numbers of interest reside.
738 0     0 0   my($self, $rows) = @_;
739 0           my $target_mode = $self->target_mode;
740 0 0 0       croak "precision_normalize invoked in '$target_mode' mode rather than 'quote' or 'intraday' mode.\n"
741             unless $target_mode eq 'quote' || $target_mode eq 'intraday';
742 0           my @columns;
743 0 0         if ($target_mode ne 'intraday') {
744 0           @columns = $self->label_column(qw(open high low close));
745 0 0         push(@columns, $self->label_column('adj')) if $self->adjuster;
746             }
747             else {
748 0           @columns = $self->label_column(qw(high low close));
749             }
750 0           my $vol_col = $self->label_column($Default_Vol_Pat);
751 0           foreach my $row (@$rows) {
752             $row->[$_] = sprintf("%.$self->{quote_precision}f", $row->[$_])
753 0           foreach @columns;
754 0           $row->[$vol_col] = int $row->[$vol_col];
755             }
756 0           $rows;
757             }
758              
759             ### Single row filters
760              
761             sub is_quote_row {
762 0     0 0   my($self, $row, $dcol) = @_;
763 0 0         ref $row or croak "Row ref required\n";
764             # Skip date in first field
765 0 0         $dcol = $self->label_column('date') unless defined $dcol;
766 0           foreach (0 .. $#$row) {
767 0 0         next if $_ == $dcol;
768 0 0         next if $row->[$_] =~ /^\s*$/;
769 0 0         if ($row->[$_] !~ /^\s*\$*[\d\.,]+\s*$/) {
770 0           return 0;
771             }
772             }
773 0           1;
774             }
775              
776             sub row_not_seen {
777 0     0 0   my($self, $symbol, $row, $dcol) = @_;
778 0 0         ref $row or croak "Row ref required\n";
779 0 0         $symbol or croak "ticker symbol required\n";
780 0           my $mode = $self->target_mode;
781 0 0         my $res = $self->{results}{$mode} or return 1;
782 0 0         my $mres = $res->{$symbol} or return 1;
783 0 0         $dcol = $self->label_column('date') unless defined $dcol;
784 0 0         $mres->{$row->[$dcol]} or return 1;
785 0           return 0;
786             }
787              
788             sub date_in_range {
789 0     0 0   my $self = shift;
790 0           my $date = shift;
791 0 0         $date = $self->date_standardize($date) or return undef;
792 0 0 0       return 0 if $self->{start_date} && $date lt $self->{start_date};
793 0 0 0       return 0 if $self->{end_date} && $date gt $self->{end_date};
794 0           1;
795             }
796              
797             ### Label and label mapping/extraction management
798              
799 0     0 0   sub default_target_mode { $Default_Target_Mode }
800 0     0 0   sub default_parse_mode { $Default_Parse_Mode }
801 0     0 0   sub default_granularity { $Default_Granularity }
802              
803             sub set_label_pattern {
804 0     0 0   my $self = shift;
805 0           my %parms = @_;
806 0   0       my $target_mode = $parms{target_mode} || $self->target_mode;
807 0   0       my $parse_mode = $parms{parse_mode} || $self->parse_mode;
808 0           my $label = $parms{label};
809 0 0         croak "Column label required\n" unless $label;
810 0   0       my $l2p = $self->{_label_pat}{$target_mode}{$parse_mode} ||= {};
811 0   0       my $p2l = $self->{_pat_label}{$target_mode}{$parse_mode} ||= {};
812 0           my $pattern = $parms{pattern};
813 0 0         if ($pattern) {
814 0           $l2p->{$label} = $pattern;
815 0           delete $self->{label_map};
816 0           delete $self->{pattern_map};
817             }
818 0 0 0       my $pat = $l2p->{$label} ||= ($label =~ $Default_Vol_Pat ?
819             qr/\s*$label/i : qr/^\s*$label/i);
820 0   0       $p2l->{$pat} ||= $label;
821 0           $pat;
822             }
823              
824             sub label_pattern {
825 0     0 0   my $self = shift;
826 0           my $target_mode = $self->target_mode;
827 0           my $parse_mode = $self->parse_mode;
828 0           my $label = shift;
829 0 0         croak "column label required\n" unless $label;
830 0   0       my $l2p = $self->{_label_pat}{$target_mode}{$parse_mode} ||= {};
831 0   0       my $pat = $l2p->{$label} || $self->set_label_pattern(label => $label);
832 0           $pat;
833             }
834              
835             sub label_column {
836 0     0 0   my $self = shift;
837 0           my @cols;
838 0 0         if (!$self->{label_map}) {
839 0           delete $self->{pattern_map};
840 0           my @labels = $self->labels;
841 0           foreach my $i (0 .. $#labels) {
842 0           $self->{label_map}{$labels[$i]} = $i;
843             }
844             }
845 0           foreach (@_) {
846 0 0         croak "Unknown label '$_'\n" unless exists $self->{label_map}{$_};
847 0           push(@cols, $self->{label_map}{$_});
848             }
849 0 0         unless (wantarray) {
850 0 0         croak "multiple columns in scalar context\n" if @cols > 1;
851 0           return $cols[0];
852             }
853 0           @cols;
854             }
855              
856             sub pattern_column {
857 0     0 0   my $self = shift;
858 0 0         if (!$self->{pattern_map}) {
859 0           my @patterns = $self->patterns;
860 0           foreach my $i (0 .. $#patterns) {
861 0           $self->{pattern_map}{$patterns[$i]} = $i;
862             }
863             }
864 0 0         return unless @_;
865 0           my $pattern = shift;
866 0 0         croak "Unknown pattern '$pattern'\n" unless $self->{_pat_map}{$pattern};
867 0           $self->{pattern_map{$pattern}};
  0            
868             }
869              
870             sub pattern_map {
871 0     0 0   my $self = shift;
872 0 0         $self->pattern_column unless $self->{pattern_map};
873 0           $self->{pattern_map};
874             }
875              
876             sub label_map {
877 0     0 0   my $self = shift;
878 0 0         $self->label_column unless $self->{label_map};
879 0           $self->{label_map};
880             }
881              
882             sub pattern_label {
883 0     0 0   my $self = shift;
884 0           my %parms = @_;
885 0   0       my $target_mode = $parms{target_mode} || $self->target_mode;
886 0   0       my $parse_mode = $parms{parse_mode} || $self->parse_mode;
887 0 0         my $pat = $parms{pattern} or croak "pattern required for label lookup\n";
888 0   0       my $p2l = $self->{_pat_label}{$target_mode}{$parse_mode} ||= {};
889 0           my $label = $p2l->{$pat};
890 0 0         unless (defined $label) {
891 0           delete $parms{pattern};
892 0           $self->set_label_pattern(%parms, label => $_) foreach $self->labels;
893             }
894 0           $label;
895             }
896              
897             sub patterns {
898 0     0 0   my $self = shift;
899 0           my %parms = @_;
900 0   0       $parms{target_mode} ||= $self->target_mode;
901 0   0       $parms{parse_mode} ||= $self->parse_mode;
902 0           map($self->label_pattern($_), $self->labels(%parms));
903             }
904              
905             sub columns {
906 0     0 0   my $self = shift;
907 0           my %parms = @_;
908 0   0       $parms{target_mode} ||= $self->target_mode;
909 0   0       $parms{parse_mode} ||= $self->parse_mode;
910 0           $self->label_column($self->labels(%parms));
911             }
912              
913             sub default_labels {
914 0     0 0   my $self = shift;
915 0           my %parms = @_;
916 0   0       my $target_mode = $parms{target_mode} || $self->target_mode;
917 0           my $tm = $Default_Labels{$target_mode};
918 0 0         unless ($tm) {
919 0           $tm = $Default_Labels{$self->default_target_mode};
920             }
921 0   0       my $parse_mode = $parms{parse_mode} || $self->parse_mode;
922 0           my $labels = $tm->{$parse_mode};
923 0 0         unless ($labels) {
924 0           $labels = $tm->{$self->default_parse_mode};
925             }
926 0           @$labels;
927             }
928              
929             sub labels {
930 0     0 1   my $self = shift;
931 0           my %parms = @_;
932 0   0       my $target_mode = $parms{target_mode} || $self->target_mode;
933 0   0       my $parse_mode = $parms{parse_mode} || $self->parse_mode;
934 0           my $tm = $self->{_labels}{$target_mode};
935 0 0 0       if ($parms{labels} || ! $tm->{$parse_mode}) {
936 0           delete $self->{label_map};
937 0           delete $self->{pattern_map};
938             }
939 0 0         $tm->{$parse_mode} = $parms{labels} if $parms{labels};
940 0   0       my $labels = $tm->{$parse_mode} ||= [$self->default_labels(
941             target_mode => $target_mode,
942             parse_mode => $parse_mode)];
943 0           @$labels;
944             }
945              
946             sub parse_mode {
947 0     0 1   my $self = shift;
948 0 0         if (@_) {
949 0           $self->{parse_mode} = shift;
950             }
951 0 0         $self->{parse_mode} || $self->default_parse_mode;
952             }
953              
954             sub target_mode {
955 0     0 1   my $self = shift;
956 0 0         if (@_) {
957 0           $self->{target_mode} = shift;
958             }
959 0 0         $self->{target_mode} || $self->default_target_mode;
960             }
961              
962             sub granularity {
963 0     0 1   my $self = shift;
964 0 0         if (@_) {
965 0           $self->{granularity} = shift;
966             }
967 0 0         $self->{granularity} || $self->default_granularity;
968             }
969              
970             sub lineup {
971 0     0 1   my $self = shift;
972 0 0         $self->{lineup} = \@_ if @_;
973 0 0         return unless $self->{lineup};
974 0           @{$self->{lineup}};
  0            
975             }
976              
977             ### Parser methods
978              
979             sub pre_parser {
980 0     0 0   my($self, %parms) = @_;
981 0   0       my $parse_mode = $parms{parse_mode} || $self->parse_mode;
982 0           my $method = "${parse_mode}_pre_parser";
983 0 0         return unless $self->can($method);
984 0           $self->$method(%parms, parse_mode => $parse_mode);
985             }
986              
987             sub parser {
988 0     0 0   my($self, %parms) = @_;
989 0   0       my $parse_mode = $parms{parse_mode} || $self->parse_mode;
990 0           my $make_parser = "${parse_mode}_parser";
991 0           $self->$make_parser(%parms, parse_mode => $parse_mode);
992             }
993              
994             sub html_parser {
995             # HTML::TableExtract supports automatic column reordering.
996 0     0 0   my $self = shift;
997 0           my $class = HTML_CLASS;
998 0           my @labels = $self->labels(@_);
999 0           my @patterns = $self->patterns(@_);
1000 0           my(%pat_map, %label_map);
1001 0           $pat_map{$patterns[$_]} = $_ foreach 0 .. $#patterns;
1002 0           $label_map{$labels[$_]} = $_ foreach 0 .. $#labels;
1003 0           $self->pattern_map(\%pat_map);
1004 0           $self->label_map(\%label_map);
1005             sub {
1006 0     0     my $data = shift;
1007 0           my $html_string;
1008 0 0         if (ref $data) {
1009 0           local($/);
1010 0           $html_string = <$data>;
1011             }
1012             else {
1013 0           $html_string = $data;
1014             }
1015 0           my %te_parms = (
1016             headers => \@patterns,
1017             automap => 1,
1018             );
1019 0 0         $te_parms{debug} = $self->{debug} if $self->{debug} > 2;
1020 0 0         my $te = $class->new(%te_parms) or croak "Problem creating $class\n";
1021 0           $te->parse($html_string);
1022 0           $te->eof;
1023 0           my $ts = $te->first_table_found;
1024 0 0         [ $ts ? $ts->rows() : ()];
1025             }
1026 0           }
1027              
1028             sub csv_parser {
1029             # Text::CSV_XS doesn't column slice or re-order, so we do.
1030 0     0 0   my $self = shift;
1031 0           my @patterns = $self->patterns(@_);
1032             sub {
1033 0     0     my $data = shift;
1034 0 0         return [] unless defined $data;
1035 0 0         my @csv_lines = ref $data ? <$data> : split("\n", $data);
1036             # BOM squad (byte order mark, as csv from google tends to be)
1037 0 0         if ($csv_lines[0] =~ s/^\xEF\xBB\xBF//) {
1038 0           for my $i (0 .. $#csv_lines) {
1039 0           utf8::decode($csv_lines[$i]);
1040             }
1041             }
1042             # might be unix, windows, or mac style newlines
1043 0           s/\s+$// foreach @csv_lines;
1044 0 0 0       return [] if !@csv_lines || $csv_lines[0] =~ /(no data)|error/i;
1045             # attempt to get rid of comments at front of csv data
1046 0           while (@csv_lines) {
1047 0 0 0       last if $csv_lines[0] =~ /date/i || $csv_lines[0] =~ /\d+$/;
1048 0 0         print STDERR "CSV reject line: $csv_lines[0]\n" if $self->{verbose};
1049 0           shift @csv_lines;
1050             }
1051 0           my $first_line = $csv_lines[0];
1052 0 0         my $sep_char = $first_line =~ /date\s*(\S)/i ? $1 : ',';
1053 0 0         my $cp = $CSV_Class->new({sep_char => $sep_char, binary => 1})
1054             or croak "Problem creating $CSV_Class\n";
1055 0           my @pat_slice;
1056 0 0         if ($first_line =~ /date/i) {
1057             # derive column detection and ordering
1058 0 0         $cp->parse($first_line) or croak ("Problem parsing (" .
1059             $cp->error_input . ") : " . $cp->error_diag . "\n");
1060 0           my @headers = $cp->fields;
1061 0           my @pats = @patterns;
1062 0           my @labels = map($self->pattern_label(pattern => $_), @patterns);
1063 0           my(%pat_map, %label_map);
1064 0           HEADER: for my $i (0 .. $#headers) {
1065 0 0         last unless @pats;
1066 0           my $header = $headers[$i];
1067 0           for my $pi (0 .. $#pats) {
1068 0           my $pat = $pats[$pi];
1069 0 0         if ($header =~ /$pat/) {
1070 0           my $label = $labels[$pi];
1071 0           splice(@pats, $pi, 1);
1072 0           splice(@labels, $pi, 1);
1073 0           $pat_map{$pat} = $i;
1074 0           $label_map{$label} = $i;
1075 0           next HEADER;
1076             }
1077             }
1078             }
1079 0           shift @csv_lines;
1080 0           @pat_slice = map($pat_map{$_}, @patterns);
1081             }
1082             else {
1083             # no header row, trust natural order and presence
1084 0           @pat_slice = 0 .. $#patterns;
1085             }
1086 0           my @rows;
1087 0           foreach my $line (@csv_lines) {
1088 0 0         $cp->parse($line) or next;
1089 0           my @fields = $cp->fields;
1090 0           push(@rows, [@fields[@pat_slice]]);
1091             }
1092 0           \@rows;
1093 0           };
1094             }
1095              
1096             ### Accessors, generators
1097              
1098             sub start_date {
1099 0     0 1   my $self = shift;
1100 0 0         if (@_) {
1101 0           my $start_date = shift;
1102 0 0         my $clear = @_ ? shift : 1;
1103 0 0         $self->clear_cache if $clear;
1104 0 0         $self->{start_date} = defined $start_date ?
1105             $self->date_standardize($start_date) : undef;
1106             }
1107 0           $self->{start_date};
1108             }
1109              
1110             sub end_date {
1111 0     0 1   my $self = shift;
1112 0 0         if (@_) {
1113 0           my $end_date = shift;
1114 0 0         my $clear = @_ ? shift : 1;
1115 0 0         $self->clear_cache if $clear;
1116 0 0         $self->{end_date} = defined $end_date ?
1117             $self->date_standardize($end_date) : undef;
1118             }
1119 0           $self->{end_date};
1120             }
1121              
1122             sub date_standardize {
1123 0     0 0   my($self, @dates) = @_;
1124 0 0         return unless @dates;
1125 0           foreach (@dates) {
1126 0 0         $_ = ParseDate($_) or Carp::confess "Could not parse date '$_'\n";
1127 0           s/\d\d:.*//;
1128             }
1129 0 0         @dates > 1 ? @dates : ($dates[0]);
1130             }
1131              
1132             sub mydates {
1133 0     0 0   my $self = shift;
1134 0           $self->dates($self->{start_date}, $self->{end_date});
1135             }
1136              
1137             sub dates {
1138 0     0 1   my($self, $sdate, $edate) = @_;
1139 0 0 0       $sdate && $edate or croak "Start date and end date strings required\n";
1140 0           my($sd, $ed) = sort($self->date_standardize($sdate, $edate));
1141 0           my @dates;
1142 0 0         push(@dates, $sd) if Date_IsWorkDay($sd);
1143 0           my $cd = $self->date_standardize(Date_NextWorkDay($sd, 1));
1144 0           while ($cd <= $ed) {
1145 0           push(@dates, $cd);
1146 0           $cd = $self->date_standardize(Date_NextWorkDay($cd));
1147             }
1148 0           @dates;
1149             }
1150              
1151             sub symbols {
1152 0     0 1   my($self, @symbols) = @_;
1153 0 0         if (@symbols) {
1154 0           my %seen;
1155 0           grep(++$seen{$_}, grep(uc $_, @symbols));
1156 0           $self->{symbols} = [sort keys %seen];
1157 0           $self->clear_cache;
1158             }
1159 0           @{$self->{symbols}};
  0            
1160             }
1161              
1162             sub successors {
1163 0     0 0   my $self = shift;
1164 0           @{$self->{successors}};
  0            
1165             }
1166              
1167             sub clear_cache {
1168 0     0 1   my $self = shift;
1169 0           delete $self->{url_cache};
1170 0           delete $self->{results};
1171 0           1;
1172             }
1173              
1174             sub result_modes {
1175 0     0 0   my $self = shift;
1176 0 0         return () unless $self->{results};
1177 0           sort keys %{$self->{results}};
  0            
1178             }
1179              
1180             sub result_symbols {
1181 0     0 0   my($self, $target_mode) = @_;
1182 0   0       $target_mode ||= $self->target_mode;
1183 0 0         return () unless $self->{sources}{$target_mode};
1184 0           sort keys %{$self->{results}{$target_mode}};
  0            
1185             }
1186              
1187             sub results {
1188 0     0 0   my($self, $target_mode, $symbol) = @_;
1189 0           $self->{results}{$target_mode}{$symbol};
1190             }
1191              
1192 0     0 1   sub quote_source { shift->source(shift, 'quote') }
1193 0     0 1   sub dividend_source { shift->source(shift, 'dividend') }
1194 0     0 1   sub split_source { shift->source(shift, 'split') }
1195 0     0 0   sub intraday_source { shift->source(shift, 'intraday') }
1196              
1197 0     0 1   sub row_filter { shift->{row_filter} }
1198              
1199             sub source {
1200 0     0 0   my($self, $symbol, $target_mode) = @_;
1201 0 0         croak "Ticker symbol required\n" unless $symbol;
1202 0   0       $target_mode ||= $self->target_mode;
1203 0 0         $self->{sources}{$target_mode}{$symbol} || '';
1204             }
1205              
1206             sub _target_source {
1207 0     0     my($self, $target_mode, $symbol, $source) = @_;
1208 0 0         croak "Target mode required\n" unless $target_mode;
1209 0 0         croak "Ticker symbol required\n" unless $symbol;
1210 0           $symbol = uc $symbol;
1211 0 0         if ($source) {
1212 0           $self->{sources}{$target_mode}{$symbol} = $source;
1213             }
1214 0           $self->{sources}{$target_mode}{$symbol};
1215             }
1216              
1217             ###
1218              
1219             sub _summon_champion {
1220             # Instantiate the next class in line if this class failed in
1221             # fetching any quotes. Make sure and pass along the remaining
1222             # champions to the new champion.
1223 0     0     my($self, $champion_class, @bad_symbols) = @_;
1224 0 0 0       return undef unless ref $self->{lineup} && @{$self->{lineup}};
  0            
1225 0 0         print STDERR "Loading $champion_class\n" if $self->{verbose};
1226 0           eval "require $champion_class;";
1227 0 0         die $@ if $@;
1228             my $champion = $champion_class->new
1229             (
1230             symbols => [@bad_symbols],
1231             start_date => $self->{start_date},
1232             end_date => $self->{end_date},
1233             adjusted => $self->{adjusted},
1234             verbose => $self->{verbose},
1235 0           lineup => [],
1236             );
1237 0           $champion;
1238             }
1239              
1240             ### Toolbox
1241              
1242 0     0 0   sub save_query { shift->_save_restore_query(1) }
1243 0     0 0   sub restore_query { shift->_save_restore_query(0) }
1244             sub _save_restore_query {
1245 0     0     my($self, $save) = @_;
1246 0 0         $save = 1 unless defined $save;
1247 0           foreach (qw(parse_mode target_mode start_date end_date granularity quiet)) {
1248 0           my $qstr = "_query_$_";
1249 0 0         if ($save) {
1250 0           $self->{$qstr} = $self->{$_};
1251             }
1252             else {
1253 0 0         $self->{$_} = $self->{$qstr} if exists $self->{$qstr};
1254             }
1255             }
1256 0           $self;
1257             }
1258              
1259             sub ymd {
1260 0     0 0   my $self = shift;
1261 0           my @res = $_[0] =~ /^\s*(\d{4})(\d{2})(\d{2})/o;
1262 0           shift =~ /^\s*(\d{4})(\d{2})(\d{2})/o;
1263             }
1264              
1265             sub date_iterator {
1266 0     0 0   my $self = shift;
1267 0           my %parms = @_;
1268 0           my $start_date = $parms{start_date};
1269 0   0       my $end_date = $parms{end_date} || 'today';
1270 0           my $increment = $parms{increment};
1271 0   0       my $units = $parms{units} || 'days';
1272 0 0 0       $increment && $increment > 0 or croak "Increment > 0 required\n";
1273 0 0         $start_date = ParseDate($start_date) if $start_date;
1274 0 0         $end_date = ParseDate($end_date) if $end_date;
1275 0 0 0       if ($start_date && $start_date gt $end_date) {
1276 0           ($start_date, $end_date) = ($end_date, $start_date);
1277             }
1278 0           my($low_date, $high_date);
1279 0           $high_date = $end_date;
1280             sub {
1281 0 0   0     return () unless $end_date;
1282 0           $low_date = DateCalc($high_date, "- $increment $units");
1283 0 0 0       if ($start_date && $low_date lt $start_date) {
1284 0           $low_date = $start_date;
1285 0           undef $start_date;
1286 0           undef $end_date;
1287 0 0         return () if $low_date eq $high_date;
1288             }
1289 0           my @date_pair = ($low_date, $high_date);
1290 0           $high_date = $low_date;
1291 0           @date_pair;
1292             }
1293 0           }
1294              
1295             1;
1296              
1297             __END__