File Coverage

blib/lib/XBRL.pm
Criterion Covered Total %
statement 7 9 77.7
branch n/a
condition n/a
subroutine 3 3 100.0
pod n/a
total 10 12 83.3


line stmt bran cond sub pod time code
1             package XBRL;
2              
3             #use strict;
4 1     1   30194 use warnings;
  1         2  
  1         46  
5              
6 1     1   8 use Carp;
  1         3  
  1         91  
7 1     1   409 use XML::LibXML;
  0            
  0            
8             use XML::LibXML::XPathContext;
9             use XBRL::Context;
10             use XBRL::Unit;
11             use XBRL::Item;
12             use XBRL::Schema;
13             use XBRL::Taxonomy;
14             use XBRL::Dimension;
15             use XBRL::Table;
16              
17             use XBRL::TableHTML;
18              
19             use LWP::UserAgent;
20             use File::Spec qw( splitpath catpath curdir);
21             use File::Temp qw(tempdir);
22             use Cwd;
23             use Data::Dumper;
24             use Text::Capitalize;
25             use Encode qw(decode_utf8);
26              
27             require Exporter;
28              
29             our @ISA = qw(Exporter);
30              
31             our %EXPORT_TAGS = ( 'all' => [ qw(
32            
33             ) ] );
34              
35             our @EXPORT_OK = ( @{ $EXPORT_TAGS{'all'} } );
36              
37             our @EXPORT = qw(
38            
39             );
40              
41             our $VERSION = '0.03';
42             our $agent_string = "Perl XBRL Library $VERSION";
43              
44             our $DEFAULT_CSS = '.label { text-align:left;}
45             .number {text-align:right;}
46             .even { background-color:#ccffcc;}
47             thead { background-color:#ccffcc;}';
48              
49              
50              
51             sub new() {
52             my ($class, $arg_ref ) = @_;
53            
54             my $self = { contexts => {},
55             units => {},
56             items => {},
57             schemas => {},
58             main_schema => undef,
59             linkbases => {},
60             item_index => undef,
61             file => undef,
62             schema_dir => undef,
63             base => undef };
64            
65             bless $self, $class;
66              
67             $self->{'schema_dir'} = $arg_ref->{'schema_dir'};
68             $self->{'file'} = $arg_ref->{'file'};
69              
70             #Check the schema dir
71             if ($self->{'schema_dir'}) {
72             if (-d $self->{'schema_dir'} ) {
73             if ( ! -w $self->{'schema_dir'} ) {
74             #the directory exits but isn't writeable
75             croak "$self->{'schema_dir'} exists but isn't writeable by this user\n";
76             }
77             }
78             else {
79             #try and create the directory
80             mkdir($self->{'schema_dir'}, 777) or croak $self->{'schema_dir'} . " can't be created because: $!\n";
81             }
82             }
83             else {
84             #the schema_dir parameter wasn't there, use tmp
85             $self->{'schema_dir'} = File::Temp->newdir(cleanup=>1);
86             }
87              
88             my ($volume, $dir, $filename);
89              
90             if ($self->{'file'}) {
91             ($volume, $dir, $filename) = File::Spec->splitpath( $self->{'file'});
92             if (! $dir) {
93             #croak "no directory in the file path \n";
94             #my $curdir = File::Spec->curdir();
95             my $curdir = getcwd();
96             my $full_path = File::Spec->catpath( undef, $curdir, $self->{'file'} );
97             if (-e $full_path) {
98             $self->{'base'} = $curdir;
99             $self->{'fullpath'} = $full_path;
100             }
101             else {
102             croak "can't find $full_path to start processing\n";
103             }
104             }
105             else {
106             $self->{'fullpath'} = $self->{'file'};
107             $self->{'file'} = $filename;
108             $self->{'base'} = $dir;
109             }
110             }
111             else {
112             croak "XBRL requires an existing file to begin processing\n";
113             }
114            
115             &parse_file( $self );
116              
117             return $self;
118             }
119              
120             sub parse_file() {
121             my ($self) = @_;
122              
123             if (!$self->{'fullpath'}) {
124             croak "full path not set in parse file but file is set to: $self->{'file'} \n";
125             }
126              
127              
128             my $xc = &make_xpath($self, $self->{'fullpath'});
129              
130             #unless($xc) { croak "Couldn't parse $file \n" };
131            
132             my $ns = &extract_namespaces($self, $self->{'fullpath'});
133              
134             #load the schemas
135             my $s_ref = $xc->findnodes("//*[local-name() = 'schemaRef']");
136             my $schema_file = $s_ref->[0]->getAttribute('xlink:href');
137            
138             my $schema_path = File::Spec->catpath( undef, $self->{'base'}, $schema_file );
139            
140             my $schema_xpath = &make_xpath($self, $schema_path);
141            
142             my $main_schema = XBRL::Schema->new( { file=> $schema_file, xpath=>$schema_xpath });
143            
144             $self->{'taxonomy'} = XBRL::Taxonomy->new( {main_schema => $main_schema} );
145            
146             my $other_schema_files = $self->{'taxonomy'}->get_other_schemas();
147            
148             for my $other (@{$other_schema_files}) {
149             #Get the file
150             my $s_file = &get_file($self, $other, $self->{'schema_dir'});
151             #make the xpath
152             #my $s_path = File::Spec->catpath( undef, $self->{'base'}, $s_file);
153             my $s_xpath = &make_xpath($self, $s_file);
154             #add the schema
155             my $schema = XBRL::Schema->new( { file => $s_file, xpath=>$s_xpath } );
156             $self->{'taxonomy'}->add_schema($schema);
157             }
158              
159              
160             my $lb_files = $self->{'taxonomy'}->get_lb_files();
161            
162             my $sections = $self->{'taxonomy'}->get_sections();
163              
164              
165             for my $file_name (@{$lb_files}) {
166             my $file = &get_file($self, $file_name, $self->{'base'});
167             if (!$file) {
168             print "The basedir is: " . $self->{'basedir'} . "\n";
169             croak "unable to get $file_name\n";
170             }
171            
172             my $lb_xpath = &make_xpath($self, $file);
173            
174              
175             if ($lb_xpath->findnodes("//*[local-name() = 'presentationLink']") ){
176             my %pres = ();
177             for my $sec (@{$sections}) {
178             $pres{$sec->{'uri'}} = &make_arcs($self, "presentationLink", $sec->{'uri'}, $lb_xpath );
179             }
180             $self->{'taxonomy'}->pre(\%pres);
181             }
182             elsif ( $lb_xpath->findnodes("//*[local-name() = 'definitionLink']" )) {
183             my %def = ();
184             for my $sec (@{$sections}) {
185             $def{$sec->{'uri'}} = &make_arcs($self, "definitionLink", $sec->{'uri'}, $lb_xpath );
186             }
187             $self->{'taxonomy'}->def(\%def);
188             }
189             elsif ( $lb_xpath->findnodes("//*[local-name() = 'labelLink']")) {
190             #labels are funny no arc elements
191             $self->{'taxonomy'}->set_labels($lb_xpath);
192             }
193             elsif ( $lb_xpath->findnodes("//*[local-name() = 'calculationLink']") ) {
194             my %calcs = ();
195             for my $sec (@{$sections}) {
196             $calcs{$sec->{'uri'}} = &make_arcs($self, "calculationLink", $sec->{'uri'}, $lb_xpath );
197             }
198             $self->{'taxonomy'}->cal(\%calcs);
199             }
200             else {
201             croak "no findnodes matched for xpath \n";
202             }
203             }
204              
205             #load the contexts
206             my $cons = $xc->findnodes("//*[local-name() = 'context']");
207             for (@$cons) {
208             my $cont = XBRL::Context->new($_);
209             $self->{'contexts'}->{ $cont->id() } = $cont;
210             }
211              
212             #parse the units
213             my $units = $xc->findnodes("//*[local-name() = 'unit']");
214             for (@$units) {
215             my $unit = XBRL::Unit->new($_);
216             $self->{'units'}->{ $unit->id() } = $unit;
217             }
218              
219             #load the items
220             my $raw_items = $xc->findnodes('//*[@contextRef]');
221             my @items = ();
222             for my $instance_xml (@$raw_items) {
223            
224             my $item = XBRL::Item->new($instance_xml);
225             push(@items, $item);
226             #deal with document level tags
227             if ($item->name() =~ m/dei\:EntityRegistrantName/i) {
228             $self->{'firm'} = $item->value();
229             }
230             elsif ($item->name() =~ m/dei\:DocumentType/i) {
231             $self->{'report_type'} = $item->value();
232             }
233             elsif ($item->name() =~ m/dei\:DocumentPeriodEndDate/i) {
234             $self->{'report_date'} = $item->value();
235             }
236             elsif ($item->name() =~ m/dei\:DocumentFiscalPeriodFocus/i) {
237             $self->{'report_period'} = $item->value();
238             }
239             elsif ($item->name() =~ m/dei\:TradingSymbol/i) {
240             $self->{'ticker'} = $item->value();
241             }
242             }
243             $self->{'items'} = \@items;
244              
245             #create the item lookup index
246             my %index = ();
247             for (my $j = 0; $j < @items; $j++) {
248             $index{$items[$j]->name()}{$items[$j]->context()} = $j;
249             }
250             $self->{'item_index'} = \%index;
251             }
252              
253              
254             sub make_arcs() {
255             my ($self, $type, $uri, $xpath) = @_;
256             my @out_arcs;
257             my $section = $xpath->findnodes("//*[local-name() = '" . $type . "'][\@xlink:role = '" . $uri . "' ]");
258            
259             unless ($section) {return undef; }
260              
261             my (@loc_links, @arc_links);
262              
263             for my $node (@{$section}) {
264             push(@loc_links, $node->getChildrenByLocalName('loc'));
265             $type =~ s/Link$/Arc/g;
266             push(@arc_links, $node->getChildrenByLocalName($type));
267             }
268            
269            
270             for my $arc_xml (@arc_links) {
271             my $arc = XBRL::Arc->new();
272             $arc->order($arc_xml->getAttribute('order'));
273             $arc->arcrole($arc_xml->getAttribute('xlink:arcrole'));
274             $arc->closed($arc_xml->getAttribute('xbrldt:closed'));
275             $arc->usable($arc_xml->getAttribute('xbrldt:usable'));
276             $arc->contextElement($arc_xml->getAttribute('xbrldt:contextElement'));
277            
278             for my $loc_xml (@loc_links) {
279            
280             if ($loc_xml->getAttribute('xlink:label') eq $arc_xml->getAttribute('xlink:to')) {
281             #This is the destination loc link
282             my $href = $loc_xml->getAttribute('xlink:href');
283             $arc->to_full($href);
284             $href =~ m/\#([A-Za-z0-9_-].+)$/;
285             $arc->to_short($1);
286            
287             }
288             elsif ($loc_xml->getAttribute('xlink:label') eq $arc_xml->getAttribute('xlink:from') ) {
289             #this is the from link
290             my $href = $loc_xml->getAttribute('xlink:href');
291             $arc->from_full($href);
292             $href =~ m/\#([A-Za-z0-9_-].+)$/;
293             $arc->from_short($1);
294            
295             }
296             }
297             push(@out_arcs, $arc);
298             }
299              
300             # my %unique_hash;
301             # my @final_array;
302             # if (($type =~ m/presentation/) && ($loc_links[0])) {
303             # my $full_url = $loc_links[0]->getAttribute('xlink:href');
304             # $full_url =~ m/\#([A-Za-z0-9_-].+)$/;
305             # &flatten($1, \@out_arcs, \%unique_hash, \@final_array);
306             # return \@final_array;
307             # }
308             # else {
309             return \@out_arcs;
310             #}
311             }
312              
313             sub flatten() {
314             my ($domain_finder, $arc_queue, $unique_hash, $final_array ) = @_;
315            
316              
317             for my $incoming (@{$arc_queue}) {
318             if ($domain_finder eq $incoming->from_short) {
319             if (! $unique_hash->{$incoming->to_short} ) {
320             $unique_hash->{$incoming->to_short}++;
321             push(@{$final_array}, $incoming);
322             &flatten($incoming->to_short(), $arc_queue, $unique_hash, $final_array);
323             }
324             }
325             }
326             }
327              
328             sub get_taxonomy() {
329             my ($self) = @_;
330             return $self->{'taxonomy'};
331             }
332              
333             sub get_context() {
334             my ($self, $id) = @_;
335             return($self->{'contexts'}->{$id});
336             }
337              
338             sub get_all_contexts() {
339             my ($self) = @_;
340             return($self->{'contexts'});
341             }
342              
343             sub get_unit() {
344             my ($self, $id) = @_;
345             return($self->{'units'}->{$id});
346             }
347              
348             sub get_item() {
349             my ($self, $name, $context) = @_;
350             my $item_number = $self->{'item_index'}->{$name}->{$context};
351             unless (defined($item_number)) { $item_number = -1; }
352             return($self->{'items'}[$item_number]);
353             }
354              
355              
356             sub get_all_items() {
357             my ($self) = @_;
358             return($self->{'items'});
359             }
360              
361              
362             sub get_item_all_contexts() {
363             my ($self, $name) = @_;
364             my @item_array = ();
365             for (keys %{$self->{'item_index'}->{$name}}) {
366             my $item_number = $self->{'item_index'}->{$name}->{$_};
367             push(@item_array, $self->{'items'}[$item_number]);
368             }
369             return \@item_array;
370             }
371              
372              
373             sub get_item_by_contexts() {
374             my ($self, $search_context) = @_;
375             my @out_array = ();
376              
377             for my $item (@{$self->{'items'}}) {
378             if ($item->context() eq $search_context) {
379             push(@out_array, $item);
380             }
381             }
382             return \@out_array;
383             }
384              
385             sub make_xpath() {
386             #take a file path and return an xpath context
387             my ($self, $in_file) = @_;
388            
389             my $ns = &extract_namespaces($self, $in_file);
390              
391             my $xml_doc =XML::LibXML->load_xml( location => $in_file);
392              
393              
394             my $xml_xpath = XML::LibXML::XPathContext->new($xml_doc);
395              
396              
397             for (keys %{$ns}) {
398             $xml_xpath->registerNs($_, $ns->{$_});
399             }
400              
401             #p3xbrl.com leaves out the link namespace in its schemas
402             $xml_xpath->registerNs('link', 'http://www.xbrl.org/2003/linkbase');
403              
404              
405             return $xml_xpath;
406             }
407              
408             sub extract_namespaces() {
409             #take an xml string and return an hash ref with name and
410             #urls for all the namespaces
411             my ($self, $xml) = @_;
412             my %out_hash = ();
413             my $parser = XML::LibXML->new();
414             my $doc = $parser->load_xml( location => $xml );
415              
416             my $root = $doc->documentElement();
417              
418             my @ns = $root->getNamespaces();
419             for (@ns) {
420             my $localname = $_->getLocalName();
421             if (!$localname) {
422             $out_hash{'default'} = $_->getData();
423             }
424             else {
425             $out_hash{$localname} = $_->getData();
426             }
427             }
428             return \%out_hash;
429             }
430              
431             sub get_file() {
432             my ( $self, $in_file, $dest_dir ) = @_;
433            
434             if ($in_file =~ m/^http\:\/\//) {
435             $in_file =~ m/^http\:\/\/[a-zA-Z0-9\/].+\/(.*)$/;
436             my $full_path = File::Spec->catpath(undef, $dest_dir, $1);
437             if ( -e $full_path) {
438             return $full_path;
439             }
440            
441             $full_path = File::Spec->catpath(undef, $self->{'schema_dir'}, $1);
442              
443             if ( -e $full_path) {
444             return $full_path;
445             }
446             else {
447             my $ua = LWP::UserAgent->new();
448             $ua->agent($agent_string);
449             my $response = $ua->get($in_file);
450             if ($response->is_success) {
451             my $fh;
452             open($fh, ">$full_path") or croak "can't open $full_path because: $! \n";
453             print $fh $response->content;
454             close $fh;
455             return $full_path;
456             }
457             else {
458             croak "Unable to retrieve $in_file because: " . $response->status_line . "\n";
459             }
460             }
461             }
462             else {
463             #process regular file
464             my ($volume, $dir, $filename) = File::Spec->splitpath( $in_file );
465            
466             if ( ($dir) && (-e $in_file) ) {
467             return $in_file;
468             }
469            
470             my $test_path = File::Spec->catpath(undef, $self->{'base'}, $filename);
471            
472             if ( -e $test_path) {
473             return $test_path;
474             }
475            
476             $test_path = File::Spec->catpath(undef, $self->{'schema_dir'}, $filename);
477             if ( -e $test_path) {
478             return $test_path;
479             }
480             }
481             }
482              
483             sub get_xml_tables() {
484             my ($self) = @_;
485             my $tax = $self->{'taxonomy'};
486             my $sections = $tax->get_sections();
487             my $out_var;
488             for my $sect (@{$sections}) {
489             if ($tax->in_def($sect->{'uri'})) {
490             #Dimension table
491             my $dim = XBRL::Dimension->new($self, $sect->{'uri'});
492             my $final_table;
493             $xml_table = $dim->get_xml_table($sect->{'uri'});
494            
495              
496             if ($final_table) {
497             $out_var = $out_var . $xml_table->as_text() . "\n\n\n";
498             }
499             }
500             else {
501             #Dealing with a regular table
502             my $norm_table = XBRL::Table->new($self);
503             my $xml_table = $norm_table->get_xml_table();
504             $out_var = $out_var . $xml_table->as_text() . "\n\n\n";
505            
506             }
507             }
508            
509             return($out_var);
510             }
511              
512              
513             sub get_html_report() {
514             my ($self, $arg_ref ) = @_;
515             my ($css_file, $css_block);
516             if ($arg_ref->{'css_file'}) {
517             $css_file = $arg_ref->{'css_file'};
518             }
519             elsif ($arg_ref->{'css_block'}) {
520             $css_block = $arg_ref->{'css_block'};
521             }
522              
523             my ($firm, $enddate, $type, $title);
524              
525            
526             my $items = $self->{'items'};
527              
528             #TODO Use the already calculated versions to avoid extra loop
529             for my $item (@{$items}) {
530             if ($item->name() eq 'dei:EntityRegistrantName') {
531             $firm= $item->value();
532             }
533             elsif($item->name() eq 'dei:DocumentType') {
534             $type = $item->value();
535             }
536             elsif ($item->name() eq 'dei:DocumentPeriodEndDate') {
537             $enddate = $item->value();
538             }
539             }
540              
541             if (($type) && ($type eq '10-K')) {
542             $title = $firm . " 10-K for Year Ending: " . $enddate;
543             }
544             elsif (($type) && ($type eq '10-Q')) {
545             $title = $firm . " 10-Q for Quarter Ending: " . $enddate;
546             }
547             else {
548             $title = $firm . " " . $enddate;
549             }
550            
551             my $html = "$title\n";
552              
553             if ($css_file) {
554             $html = $html . '';
555             }
556             else {
557             $html = $html . '';
567              
568             }
569            
570             $title = capitalize_title(decode_utf8($title));
571              
572             $html = $html . "\n";
573              
574             $html = $html . "

$title

\n";
575              
576             my $tax = $self->{'taxonomy'};
577              
578             my $sections = $tax->get_sections();
579            
580             for my $sect (@{$sections}) {
581            
582             my $sect_title;
583             eval{ $sect_title = capitalize_title(decode_utf8($sect->{'def'})) };
584             if ($sect_title) {
585             $html = $html . "

" . $sect_title . "

\n";
586             }
587             else {
588             $html = $html . "

" . $sect->{'def'} . "

\n";
589             }
590            
591             if ($tax->in_def($sect->{'uri'})) {
592             #Dimension table
593             my $dim = XBRL::Dimension->new($self, $sect->{'uri'});
594              
595             my $final_table;
596             $final_table = $dim->get_xml_table($sect->{'uri'});
597            
598             if ($final_table) {
599             my $html_table = XBRL::TableHTML->new( { xml => $final_table } );
600             if (($html_table) && ($html_table->asText())) {
601             $html = $html . $html_table->asText() . "\n\n\n";
602             }
603             }
604             }
605             else {
606             #Dealing with a regular table
607             my $norm_table = XBRL::Table->new($self, $sect->{'uri'});
608             if ($norm_table) {
609            
610             my $html_table = XBRL::TableHTML->new( { xml => $norm_table->get_xml_table($sect->{'uri'})});
611             if ($html_table) {
612             $text = $html_table->asText();
613             if ($text) {
614             $html = $html . $text . "\n\n\n";
615             }
616             }
617             }
618             }
619            
620            
621            
622             }
623            
624             $html = $html . "\n";
625              
626             }
627              
628              
629             sub get_company() {
630             my ($self) = @_;
631             return($self->{'firm'});
632             }
633              
634             sub report_type() {
635             my ($self) = @_;
636             return($self->{'report_type'});
637             }
638              
639             sub report_date() {
640             my ($self) = @_;
641             return($self->{'report_date'});
642             }
643              
644             sub report_period() {
645             my ($self) = @_;
646             return($self->{'report_period'});
647             }
648              
649             sub get_ticker() {
650             my ($self) = @_;
651             return($self->{'ticker'});
652             }
653              
654              
655             sub get_income_statement() {
656             my ($self) = @_;
657             my $tax = $self->get_taxonomy();
658              
659             my $sections = $tax->get_sections();
660              
661             my $income_uri;
662             FOO: {
663             for my $sect (@{$sections}) {
664             my $title = $sect->{'def'};
665             if (($title =~ m/statement/i) &&
666             (($title =~m /operation/i) ||
667             ($title =~ m/income/i) ||
668             ($title =~ m/earning/i) ||
669             ($title =~ m/loss/i) ) &&
670             ($title !~ m/parenthetical/i)) {
671             $income_uri = $sect->{'uri'};
672             }
673             if ($income_uri) {
674             last FOO;
675             }
676             }
677             }
678            
679             if ($tax->in_def($income_uri)) {
680             my $dim = XBRL::Dimension->new($self, $income_uri);
681             return($dim->get_xml_table($income_uri));
682             }
683             else {
684             my $pres_table = XBRL::Table->new($self, $income_uri);
685             return($pres_table->get_xml_table($income_uri));
686             }
687              
688             }
689              
690              
691              
692              
693             1;
694              
695             __END__