File Coverage

blib/lib/Finance/QuoteHist/Generic.pm
Criterion Covered Total %
statement 21 713 2.9
branch 0 426 0.0
condition 0 209 0.0
subroutine 7 86 8.1
pod 20 69 28.9
total 48 1503 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   28 use strict;
  5         7  
  5         116  
17 5     5   20 use Carp;
  5         9  
  5         204  
18              
19 5     5   23 use vars qw($VERSION);
  5         10  
  5         216  
20             $VERSION = "1.22";
21              
22 5     5   26 use LWP::UserAgent;
  5         8  
  5         104  
23 5     5   19 use HTTP::Request;
  5         10  
  5         96  
24 5     5   1809 use Date::Manip;
  5         638457  
  5         40318  
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   4170 eval "use $CSV_Class";
  5         48207  
  5         160  
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           $normal_date = ParseDate($date);
676             }
677 0 0         $normal_date or return undef;
678 0 0         return $normal_date if $self->target_mode eq 'intraday';
679 0           join('/', $self->ymd($normal_date));
680             }
681              
682             sub snap_start_date {
683 0     0 0   my($self, $date) = @_;
684 0           my $g = $self->granularity;
685 0 0         if ($g =~ /^(m|w)/i) {
686 0 0         if ($1 eq 'm') {
687 0           my($dom) = UnixDate($date, '%d') - 1;
688 0 0         $date = DateCalc($date, "- $dom days") if $dom;
689             }
690             else {
691 0           my $dow = Date_DayOfWeek(UnixDate($date, '%m', '%d', '%Y')) - 1;
692 0 0         $date = DateCalc($date, "- $dow days") if $dow;
693             }
694             }
695 0           $date;
696             }
697              
698             sub snap_end_date {
699 0     0 0   my($self, $date) = @_;
700 0           my $g = $self->granularity;
701 0 0         if ($g =~ /^(m|w)/i) {
702 0 0         if ($1 eq 'm') {
703 0           my($m, $y) = UnixDate($date, '%m', '%Y');
704 0           my $last = Date_DaysInMonth($m, $y);
705 0           $date = ParseDateString("$y$m$last");
706             }
707             else {
708 0           my $dow = Date_DayOfWeek(UnixDate($date, '%m', '%d', '%Y')) - 1;
709 0 0         $date = DateCalc($date, "+ " . (6 - $dow) . ' days')
710             unless $dow == 6;
711             }
712             }
713 0           $date;
714             }
715              
716             sub number_normalize_rows {
717             # Strip non-numeric noise from numeric fields
718 0     0 0   my($self, $rows, $dcol) = @_;
719 0 0         $dcol = $self->label_column('date') unless defined $dcol;
720             # filtered rows might not have same columns
721 0           my @cols = grep($_ != $dcol, 0 .. $#{$rows->[0]});
  0            
722 0           foreach my $row (@$rows) {
723 0           s/[^\d\.]//go foreach @{$row}[@cols];
  0            
724             }
725 0           $rows;
726             }
727              
728             sub precision_normalize_rows {
729             # Round off numeric fields, if requested (%.4f by default). Volume
730             # is the exception -- we just round that into an integer. This
731             # should probably only be called for 'quote' targets because it
732             # knows details about where the numbers of interest reside.
733 0     0 0   my($self, $rows) = @_;
734 0           my $target_mode = $self->target_mode;
735 0 0 0       croak "precision_normalize invoked in '$target_mode' mode rather than 'quote' or 'intraday' mode.\n"
736             unless $target_mode eq 'quote' || $target_mode eq 'intraday';
737 0           my @columns;
738 0 0         if ($target_mode ne 'intraday') {
739 0           @columns = $self->label_column(qw(open high low close));
740 0 0         push(@columns, $self->label_column('adj')) if $self->adjuster;
741             }
742             else {
743 0           @columns = $self->label_column(qw(high low close));
744             }
745 0           my $vol_col = $self->label_column($Default_Vol_Pat);
746 0           foreach my $row (@$rows) {
747             $row->[$_] = sprintf("%.$self->{quote_precision}f", $row->[$_])
748 0           foreach @columns;
749 0           $row->[$vol_col] = int $row->[$vol_col];
750             }
751 0           $rows;
752             }
753              
754             ### Single row filters
755              
756             sub is_quote_row {
757 0     0 0   my($self, $row, $dcol) = @_;
758 0 0         ref $row or croak "Row ref required\n";
759             # Skip date in first field
760 0 0         $dcol = $self->label_column('date') unless defined $dcol;
761 0           foreach (0 .. $#$row) {
762 0 0         next if $_ == $dcol;
763 0 0         next if $row->[$_] =~ /^\s*$/;
764 0 0         if ($row->[$_] !~ /^\s*\$*[\d\.,]+\s*$/) {
765 0           return 0;
766             }
767             }
768 0           1;
769             }
770              
771             sub row_not_seen {
772 0     0 0   my($self, $symbol, $row, $dcol) = @_;
773 0 0         ref $row or croak "Row ref required\n";
774 0 0         $symbol or croak "ticker symbol required\n";
775 0           my $mode = $self->target_mode;
776 0 0         my $res = $self->{results}{$mode} or return 1;
777 0 0         my $mres = $res->{$symbol} or return 1;
778 0 0         $dcol = $self->label_column('date') unless defined $dcol;
779 0 0         $mres->{$row->[$dcol]} or return 1;
780 0           return 0;
781             }
782              
783             sub date_in_range {
784 0     0 0   my $self = shift;
785 0           my $date = shift;
786 0 0         $date = $self->date_standardize($date) or return undef;
787 0 0 0       return 0 if $self->{start_date} && $date lt $self->{start_date};
788 0 0 0       return 0 if $self->{end_date} && $date gt $self->{end_date};
789 0           1;
790             }
791              
792             ### Label and label mapping/extraction management
793              
794 0     0 0   sub default_target_mode { $Default_Target_Mode }
795 0     0 0   sub default_parse_mode { $Default_Parse_Mode }
796 0     0 0   sub default_granularity { $Default_Granularity }
797              
798             sub set_label_pattern {
799 0     0 0   my $self = shift;
800 0           my %parms = @_;
801 0   0       my $target_mode = $parms{target_mode} || $self->target_mode;
802 0   0       my $parse_mode = $parms{parse_mode} || $self->parse_mode;
803 0           my $label = $parms{label};
804 0 0         croak "Column label required\n" unless $label;
805 0   0       my $l2p = $self->{_label_pat}{$target_mode}{$parse_mode} ||= {};
806 0   0       my $p2l = $self->{_pat_label}{$target_mode}{$parse_mode} ||= {};
807 0           my $pattern = $parms{pattern};
808 0 0         if ($pattern) {
809 0           $l2p->{$label} = $pattern;
810 0           delete $self->{label_map};
811 0           delete $self->{pattern_map};
812             }
813 0 0 0       my $pat = $l2p->{$label} ||= ($label =~ $Default_Vol_Pat ?
814             qr/\s*$label/i : qr/^\s*$label/i);
815 0   0       $p2l->{$pat} ||= $label;
816 0           $pat;
817             }
818              
819             sub label_pattern {
820 0     0 0   my $self = shift;
821 0           my $target_mode = $self->target_mode;
822 0           my $parse_mode = $self->parse_mode;
823 0           my $label = shift;
824 0 0         croak "column label required\n" unless $label;
825 0   0       my $l2p = $self->{_label_pat}{$target_mode}{$parse_mode} ||= {};
826 0   0       my $pat = $l2p->{$label} || $self->set_label_pattern(label => $label);
827 0           $pat;
828             }
829              
830             sub label_column {
831 0     0 0   my $self = shift;
832 0           my @cols;
833 0 0         if (!$self->{label_map}) {
834 0           delete $self->{pattern_map};
835 0           my @labels = $self->labels;
836 0           foreach my $i (0 .. $#labels) {
837 0           $self->{label_map}{$labels[$i]} = $i;
838             }
839             }
840 0           foreach (@_) {
841 0 0         croak "Unknown label '$_'\n" unless exists $self->{label_map}{$_};
842 0           push(@cols, $self->{label_map}{$_});
843             }
844 0 0         unless (wantarray) {
845 0 0         croak "multiple columns in scalar context\n" if @cols > 1;
846 0           return $cols[0];
847             }
848 0           @cols;
849             }
850              
851             sub pattern_column {
852 0     0 0   my $self = shift;
853 0 0         if (!$self->{pattern_map}) {
854 0           my @patterns = $self->patterns;
855 0           foreach my $i (0 .. $#patterns) {
856 0           $self->{pattern_map}{$patterns[$i]} = $i;
857             }
858             }
859 0 0         return unless @_;
860 0           my $pattern = shift;
861 0 0         croak "Unknown pattern '$pattern'\n" unless $self->{_pat_map}{$pattern};
862 0           $self->{pattern_map{$pattern}};
  0            
863             }
864              
865             sub pattern_map {
866 0     0 0   my $self = shift;
867 0 0         $self->pattern_column unless $self->{pattern_map};
868 0           $self->{pattern_map};
869             }
870              
871             sub label_map {
872 0     0 0   my $self = shift;
873 0 0         $self->label_column unless $self->{label_map};
874 0           $self->{label_map};
875             }
876              
877             sub pattern_label {
878 0     0 0   my $self = shift;
879 0           my %parms = @_;
880 0   0       my $target_mode = $parms{target_mode} || $self->target_mode;
881 0   0       my $parse_mode = $parms{parse_mode} || $self->parse_mode;
882 0 0         my $pat = $parms{pattern} or croak "pattern required for label lookup\n";
883 0   0       my $p2l = $self->{_pat_label}{$target_mode}{$parse_mode} ||= {};
884 0           my $label = $p2l->{$pat};
885 0 0         unless (defined $label) {
886 0           delete $parms{pattern};
887 0           $self->set_label_pattern(%parms, label => $_) foreach $self->labels;
888             }
889 0           $label;
890             }
891              
892             sub patterns {
893 0     0 0   my $self = shift;
894 0           my %parms = @_;
895 0   0       $parms{target_mode} ||= $self->target_mode;
896 0   0       $parms{parse_mode} ||= $self->parse_mode;
897 0           map($self->label_pattern($_), $self->labels(%parms));
898             }
899              
900             sub columns {
901 0     0 0   my $self = shift;
902 0           my %parms = @_;
903 0   0       $parms{target_mode} ||= $self->target_mode;
904 0   0       $parms{parse_mode} ||= $self->parse_mode;
905 0           $self->label_column($self->labels(%parms));
906             }
907              
908             sub default_labels {
909 0     0 0   my $self = shift;
910 0           my %parms = @_;
911 0   0       my $target_mode = $parms{target_mode} || $self->target_mode;
912 0           my $tm = $Default_Labels{$target_mode};
913 0 0         unless ($tm) {
914 0           $tm = $Default_Labels{$self->default_target_mode};
915             }
916 0   0       my $parse_mode = $parms{parse_mode} || $self->parse_mode;
917 0           my $labels = $tm->{$parse_mode};
918 0 0         unless ($labels) {
919 0           $labels = $tm->{$self->default_parse_mode};
920             }
921 0           @$labels;
922             }
923              
924             sub labels {
925 0     0 1   my $self = shift;
926 0           my %parms = @_;
927 0   0       my $target_mode = $parms{target_mode} || $self->target_mode;
928 0   0       my $parse_mode = $parms{parse_mode} || $self->parse_mode;
929 0           my $tm = $self->{_labels}{$target_mode};
930 0 0 0       if ($parms{labels} || ! $tm->{$parse_mode}) {
931 0           delete $self->{label_map};
932 0           delete $self->{pattern_map};
933             }
934 0 0         $tm->{$parse_mode} = $parms{labels} if $parms{labels};
935 0   0       my $labels = $tm->{$parse_mode} ||= [$self->default_labels(
936             target_mode => $target_mode,
937             parse_mode => $parse_mode)];
938 0           @$labels;
939             }
940              
941             sub parse_mode {
942 0     0 1   my $self = shift;
943 0 0         if (@_) {
944 0           $self->{parse_mode} = shift;
945             }
946 0 0         $self->{parse_mode} || $self->default_parse_mode;
947             }
948              
949             sub target_mode {
950 0     0 1   my $self = shift;
951 0 0         if (@_) {
952 0           $self->{target_mode} = shift;
953             }
954 0 0         $self->{target_mode} || $self->default_target_mode;
955             }
956              
957             sub granularity {
958 0     0 1   my $self = shift;
959 0 0         if (@_) {
960 0           $self->{granularity} = shift;
961             }
962 0 0         $self->{granularity} || $self->default_granularity;
963             }
964              
965             sub lineup {
966 0     0 1   my $self = shift;
967 0 0         $self->{lineup} = \@_ if @_;
968 0 0         return unless $self->{lineup};
969 0           @{$self->{lineup}};
  0            
970             }
971              
972             ### Parser methods
973              
974             sub pre_parser {
975 0     0 0   my($self, %parms) = @_;
976 0   0       my $parse_mode = $parms{parse_mode} || $self->parse_mode;
977 0           my $method = "${parse_mode}_pre_parser";
978 0 0         return unless $self->can($method);
979 0           $self->$method(%parms, parse_mode => $parse_mode);
980             }
981              
982             sub parser {
983 0     0 0   my($self, %parms) = @_;
984 0   0       my $parse_mode = $parms{parse_mode} || $self->parse_mode;
985 0           my $make_parser = "${parse_mode}_parser";
986 0           $self->$make_parser(%parms, parse_mode => $parse_mode);
987             }
988              
989             sub html_parser {
990             # HTML::TableExtract supports automatic column reordering.
991 0     0 0   my $self = shift;
992 0           my $class = HTML_CLASS;
993 0           my @labels = $self->labels(@_);
994 0           my @patterns = $self->patterns(@_);
995 0           my(%pat_map, %label_map);
996 0           $pat_map{$patterns[$_]} = $_ foreach 0 .. $#patterns;
997 0           $label_map{$labels[$_]} = $_ foreach 0 .. $#labels;
998 0           $self->pattern_map(\%pat_map);
999 0           $self->label_map(\%label_map);
1000             sub {
1001 0     0     my $data = shift;
1002 0           my $html_string;
1003 0 0         if (ref $data) {
1004 0           local($/);
1005 0           $html_string = <$data>;
1006             }
1007             else {
1008 0           $html_string = $data;
1009             }
1010 0           my %te_parms = (
1011             headers => \@patterns,
1012             automap => 1,
1013             );
1014 0 0         $te_parms{debug} = $self->{debug} if $self->{debug} > 2;
1015 0 0         my $te = $class->new(%te_parms) or croak "Problem creating $class\n";
1016 0           $te->parse($html_string);
1017 0           $te->eof;
1018 0           my $ts = $te->first_table_found;
1019 0 0         [ $ts ? $ts->rows() : ()];
1020             }
1021 0           }
1022              
1023             sub csv_parser {
1024             # Text::CSV_XS doesn't column slice or re-order, so we do.
1025 0     0 0   my $self = shift;
1026 0           my @patterns = $self->patterns(@_);
1027             sub {
1028 0     0     my $data = shift;
1029 0 0         return [] unless defined $data;
1030 0 0         my @csv_lines = ref $data ? <$data> : split("\n", $data);
1031             # BOM squad (byte order mark, as csv from google tends to be)
1032 0 0         if ($csv_lines[0] =~ s/^\xEF\xBB\xBF//) {
1033 0           for my $i (0 .. $#csv_lines) {
1034 0           utf8::decode($csv_lines[$i]);
1035             }
1036             }
1037             # might be unix, windows, or mac style newlines
1038 0           s/\s+$// foreach @csv_lines;
1039 0 0 0       return [] if !@csv_lines || $csv_lines[0] =~ /(no data)|error/i;
1040             # attempt to get rid of comments at front of csv data
1041 0           while (@csv_lines) {
1042 0 0 0       last if $csv_lines[0] =~ /date/i || $csv_lines[0] =~ /\d+$/;
1043 0 0         print STDERR "CSV reject line: $csv_lines[0]\n" if $self->{verbose};
1044 0           shift @csv_lines;
1045             }
1046 0           my $first_line = $csv_lines[0];
1047 0 0         my $sep_char = $first_line =~ /date\s*(\S)/i ? $1 : ',';
1048 0 0         my $cp = $CSV_Class->new({sep_char => $sep_char, binary => 1})
1049             or croak "Problem creating $CSV_Class\n";
1050 0           my @pat_slice;
1051 0 0         if ($first_line =~ /date/i) {
1052             # derive column detection and ordering
1053 0 0         $cp->parse($first_line) or croak ("Problem parsing (" .
1054             $cp->error_input . ") : " . $cp->error_diag . "\n");
1055 0           my @headers = $cp->fields;
1056 0           my @pats = @patterns;
1057 0           my @labels = map($self->pattern_label(pattern => $_), @patterns);
1058 0           my(%pat_map, %label_map);
1059 0           HEADER: for my $i (0 .. $#headers) {
1060 0 0         last unless @pats;
1061 0           my $header = $headers[$i];
1062 0           for my $pi (0 .. $#pats) {
1063 0           my $pat = $pats[$pi];
1064 0 0         if ($header =~ /$pat/) {
1065 0           my $label = $labels[$pi];
1066 0           splice(@pats, $pi, 1);
1067 0           splice(@labels, $pi, 1);
1068 0           $pat_map{$pat} = $i;
1069 0           $label_map{$label} = $i;
1070 0           next HEADER;
1071             }
1072             }
1073             }
1074 0           shift @csv_lines;
1075 0           @pat_slice = map($pat_map{$_}, @patterns);
1076             }
1077             else {
1078             # no header row, trust natural order and presence
1079 0           @pat_slice = 0 .. $#patterns;
1080             }
1081 0           my @rows;
1082 0           foreach my $line (@csv_lines) {
1083 0 0         $cp->parse($line) or next;
1084 0           my @fields = $cp->fields;
1085 0           push(@rows, [@fields[@pat_slice]]);
1086             }
1087 0           \@rows;
1088 0           };
1089             }
1090              
1091             ### Accessors, generators
1092              
1093             sub start_date {
1094 0     0 1   my $self = shift;
1095 0 0         if (@_) {
1096 0           my $start_date = shift;
1097 0 0         my $clear = @_ ? shift : 1;
1098 0 0         $self->clear_cache if $clear;
1099 0 0         $self->{start_date} = defined $start_date ?
1100             $self->date_standardize($start_date) : undef;
1101             }
1102 0           $self->{start_date};
1103             }
1104              
1105             sub end_date {
1106 0     0 1   my $self = shift;
1107 0 0         if (@_) {
1108 0           my $end_date = shift;
1109 0 0         my $clear = @_ ? shift : 1;
1110 0 0         $self->clear_cache if $clear;
1111 0 0         $self->{end_date} = defined $end_date ?
1112             $self->date_standardize($end_date) : undef;
1113             }
1114 0           $self->{end_date};
1115             }
1116              
1117             sub date_standardize {
1118 0     0 0   my($self, @dates) = @_;
1119 0 0         return unless @dates;
1120 0           foreach (@dates) {
1121 0 0         $_ = ParseDate($_) or Carp::confess "Could not parse date '$_'\n";
1122 0           s/\d\d:.*//;
1123             }
1124 0 0         @dates > 1 ? @dates : ($dates[0]);
1125             }
1126              
1127             sub mydates {
1128 0     0 0   my $self = shift;
1129 0           $self->dates($self->{start_date}, $self->{end_date});
1130             }
1131              
1132             sub dates {
1133 0     0 1   my($self, $sdate, $edate) = @_;
1134 0 0 0       $sdate && $edate or croak "Start date and end date strings required\n";
1135 0           my($sd, $ed) = sort($self->date_standardize($sdate, $edate));
1136 0           my @dates;
1137 0 0         push(@dates, $sd) if Date_IsWorkDay($sd);
1138 0           my $cd = $self->date_standardize(Date_NextWorkDay($sd, 1));
1139 0           while ($cd <= $ed) {
1140 0           push(@dates, $cd);
1141 0           $cd = $self->date_standardize(Date_NextWorkDay($cd));
1142             }
1143 0           @dates;
1144             }
1145              
1146             sub symbols {
1147 0     0 1   my($self, @symbols) = @_;
1148 0 0         if (@symbols) {
1149 0           my %seen;
1150 0           grep(++$seen{$_}, grep(uc $_, @symbols));
1151 0           $self->{symbols} = [sort keys %seen];
1152 0           $self->clear_cache;
1153             }
1154 0           @{$self->{symbols}};
  0            
1155             }
1156              
1157             sub successors {
1158 0     0 0   my $self = shift;
1159 0           @{$self->{successors}};
  0            
1160             }
1161              
1162             sub clear_cache {
1163 0     0 1   my $self = shift;
1164 0           delete $self->{url_cache};
1165 0           delete $self->{results};
1166 0           1;
1167             }
1168              
1169             sub result_modes {
1170 0     0 0   my $self = shift;
1171 0 0         return () unless $self->{results};
1172 0           sort keys %{$self->{results}};
  0            
1173             }
1174              
1175             sub result_symbols {
1176 0     0 0   my($self, $target_mode) = @_;
1177 0   0       $target_mode ||= $self->target_mode;
1178 0 0         return () unless $self->{sources}{$target_mode};
1179 0           sort keys %{$self->{results}{$target_mode}};
  0            
1180             }
1181              
1182             sub results {
1183 0     0 0   my($self, $target_mode, $symbol) = @_;
1184 0           $self->{results}{$target_mode}{$symbol};
1185             }
1186              
1187 0     0 1   sub quote_source { shift->source(shift, 'quote') }
1188 0     0 1   sub dividend_source { shift->source(shift, 'dividend') }
1189 0     0 1   sub split_source { shift->source(shift, 'split') }
1190 0     0 0   sub intraday_source { shift->source(shift, 'intraday') }
1191              
1192 0     0 1   sub row_filter { shift->{row_filter} }
1193              
1194             sub source {
1195 0     0 0   my($self, $symbol, $target_mode) = @_;
1196 0 0         croak "Ticker symbol required\n" unless $symbol;
1197 0   0       $target_mode ||= $self->target_mode;
1198 0 0         $self->{sources}{$target_mode}{$symbol} || '';
1199             }
1200              
1201             sub _target_source {
1202 0     0     my($self, $target_mode, $symbol, $source) = @_;
1203 0 0         croak "Target mode required\n" unless $target_mode;
1204 0 0         croak "Ticker symbol required\n" unless $symbol;
1205 0           $symbol = uc $symbol;
1206 0 0         if ($source) {
1207 0           $self->{sources}{$target_mode}{$symbol} = $source;
1208             }
1209 0           $self->{sources}{$target_mode}{$symbol};
1210             }
1211              
1212             ###
1213              
1214             sub _summon_champion {
1215             # Instantiate the next class in line if this class failed in
1216             # fetching any quotes. Make sure and pass along the remaining
1217             # champions to the new champion.
1218 0     0     my($self, $champion_class, @bad_symbols) = @_;
1219 0 0 0       return undef unless ref $self->{lineup} && @{$self->{lineup}};
  0            
1220 0 0         print STDERR "Loading $champion_class\n" if $self->{verbose};
1221 0           eval "require $champion_class;";
1222 0 0         die $@ if $@;
1223             my $champion = $champion_class->new
1224             (
1225             symbols => [@bad_symbols],
1226             start_date => $self->{start_date},
1227             end_date => $self->{end_date},
1228             adjusted => $self->{adjusted},
1229             verbose => $self->{verbose},
1230 0           lineup => [],
1231             );
1232 0           $champion;
1233             }
1234              
1235             ### Toolbox
1236              
1237 0     0 0   sub save_query { shift->_save_restore_query(1) }
1238 0     0 0   sub restore_query { shift->_save_restore_query(0) }
1239             sub _save_restore_query {
1240 0     0     my($self, $save) = @_;
1241 0 0         $save = 1 unless defined $save;
1242 0           foreach (qw(parse_mode target_mode start_date end_date granularity quiet)) {
1243 0           my $qstr = "_query_$_";
1244 0 0         if ($save) {
1245 0           $self->{$qstr} = $self->{$_};
1246             }
1247             else {
1248 0 0         $self->{$_} = $self->{$qstr} if exists $self->{$qstr};
1249             }
1250             }
1251 0           $self;
1252             }
1253              
1254             sub ymd {
1255 0     0 0   my $self = shift;
1256 0           my @res = $_[0] =~ /^\s*(\d{4})(\d{2})(\d{2})/o;
1257 0           shift =~ /^\s*(\d{4})(\d{2})(\d{2})/o;
1258             }
1259              
1260             sub date_iterator {
1261 0     0 0   my $self = shift;
1262 0           my %parms = @_;
1263 0           my $start_date = $parms{start_date};
1264 0   0       my $end_date = $parms{end_date} || 'today';
1265 0           my $increment = $parms{increment};
1266 0   0       my $units = $parms{units} || 'days';
1267 0 0 0       $increment && $increment > 0 or croak "Increment > 0 required\n";
1268 0 0         $start_date = ParseDate($start_date) if $start_date;
1269 0 0         $end_date = ParseDate($end_date) if $end_date;
1270 0 0 0       if ($start_date && $start_date gt $end_date) {
1271 0           ($start_date, $end_date) = ($end_date, $start_date);
1272             }
1273 0           my($low_date, $high_date);
1274 0           $high_date = $end_date;
1275             sub {
1276 0 0   0     return () unless $end_date;
1277 0           $low_date = DateCalc($high_date, "- $increment $units");
1278 0 0 0       if ($start_date && $low_date lt $start_date) {
1279 0           $low_date = $start_date;
1280 0           undef $start_date;
1281 0           undef $end_date;
1282 0 0         return () if $low_date eq $high_date;
1283             }
1284 0           my @date_pair = ($low_date, $high_date);
1285 0           $high_date = $low_date;
1286 0           @date_pair;
1287             }
1288 0           }
1289              
1290             1;
1291              
1292             __END__