File Coverage

blib/lib/Data/Frame.pm
Criterion Covered Total %
statement 10 12 83.3
branch n/a
condition n/a
subroutine 4 4 100.0
pod n/a
total 14 16 87.5


line stmt bran cond sub pod time code
1             package Data::Frame;
2             # ABSTRACT: data frame implementation
3             $Data::Frame::VERSION = '0.002';
4 2     2   126323 use strict;
  2         4  
  2         59  
5 2     2   8 use warnings;
  2         2  
  2         39  
6              
7 2     2   956 use Tie::IxHash;
  2         6621  
  2         52  
8 2     2   694 use Tie::IxHash::Extension;
  0            
  0            
9             use PDL::Lite;
10             use Data::Perl ();
11             use List::AllUtils;
12             use Try::Tiny;
13             use PDL::SV;
14             use PDL::StringfiableExtension;
15             use Carp;
16             use Scalar::Util qw(blessed);
17              
18             use Text::Table::Tiny;
19              
20             use Data::Frame::Column::Helper;
21              
22             use overload (
23             '""' => \&Data::Frame::string,
24             '==' => \&Data::Frame::equal,
25             'eq' => \&Data::Frame::equal,
26             );
27              
28             {
29             # TODO temporary column role
30             no strict;
31             *PDL::number_of_rows = sub { $_[0]->getdim(0) };
32             *Data::Perl::Collection::Array::number_of_rows = sub { $_[0]->count };
33             }
34              
35             use Moo;
36              
37             has _columns => ( is => 'ro', default => sub { Tie::IxHash->new; } );
38              
39             has _row_names => ( is => 'rw', predicate => 1 );
40              
41             around new => sub {
42             my $orig = shift;
43             my ($class, %args) = @_;
44             my $colspec = delete $args{columns};
45              
46             my $self = $orig->(@_);
47              
48             if( defined $colspec ) {
49             my @columns =
50             ref $colspec eq 'HASH'
51             ? map { ($_, $colspec->{$_} ) } sort { $a cmp $b } keys %$colspec
52             : @$colspec;
53             $self->add_columns(@columns);
54             }
55              
56             $self;
57             };
58              
59             sub string {
60             my ($self) = @_;
61             my $rows = [];
62             push @$rows, [ '', @{ $self->column_names } ];
63             for my $r_idx ( 0..$self->number_of_rows-1 ) {
64             my $r = [
65             $self->row_names->slice($r_idx)->squeeze->string,
66             map {
67             my $col = $self->nth_column($_);
68             $col->slice($r_idx)->squeeze->string
69             } 0..$self->number_of_columns-1 ];
70             push @$rows, $r;
71             }
72             {
73             # clear column separators
74             local $Text::Table::Tiny::COLUMN_SEPARATOR = '';
75             local $Text::Table::Tiny::CORNER_MARKER = '';
76              
77             Text::Table::Tiny::table(rows => $rows, header_row => 1)
78             }
79             }
80              
81             sub number_of_columns {
82             my ($self) = @_;
83             $self->_columns->Length;
84             }
85              
86             sub number_of_rows {
87             my ($self) = @_;
88             if( $self->number_of_columns ) {
89             return $self->nth_column(0)->number_of_rows;
90             }
91             0;
92             }
93              
94             # supports negative indices
95             sub nth_column {
96             my ($self, $index) = @_;
97             confess "requires index" unless defined $index;
98             confess "column index out of bounds" if $index >= $self->number_of_columns;
99             # fine if $index < 0 because negative indices are supported
100             $self->_columns->Values( $index );
101             }
102              
103             sub column_names {
104             my ($self, @colnames) = @_;
105             if( @colnames ) {
106             try {
107             $self->_columns->RenameKeys( @colnames );
108             } catch {
109             confess "incorrect number of column names" if /@{[ Tie::IxHash::ERROR_KEY_LENGTH_MISMATCH ]}/;
110             };
111             }
112             [ $self->_columns->Keys ];
113             }
114              
115             sub row_names {
116             my ($self, @rest) = @_;
117             if( @rest ) {
118             # setting row names
119             my $new_rows;
120             if( ref $rest[0] ) {
121             if( ref $rest[0] eq 'ARRAY' ) {
122             $new_rows = Data::Perl::array( @{ $rest[0] });
123             } elsif( $rest[0]->isa('PDL') ) {
124             # TODO just run uniq?
125             $new_rows = Data::Perl::array( @{ $rest[0]->unpdl } );
126             } else {
127             $new_rows = Data::Perl::array(@rest);
128             }
129             } else {
130             $new_rows = Data::Perl::array(@rest);
131             }
132              
133             confess "invalid row names length"
134             if $self->number_of_rows != $new_rows->count;
135             confess "non-unique row names"
136             if $new_rows->count != $new_rows->uniq->count;
137              
138             return $self->_row_names( PDL::SV->new($new_rows) );
139             }
140             if( not $self->_has_row_names ) {
141             # if it has never been set before
142             return sequence($self->number_of_rows);
143             }
144             # else, if row_names has been set
145             return $self->_row_names;
146             }
147              
148             sub _make_actual_row_names {
149             my ($self) = @_;
150             if( not $self->_has_row_names ) {
151             $self->_row_names( $self->row_names );
152             }
153             }
154              
155             sub column {
156             my ($self, $colname) = @_;
157             confess "column $colname does not exist" unless $self->_columns->EXISTS( $colname );
158             $self->_columns->FETCH( $colname );
159             }
160              
161             sub _column_validate {
162             my ($self, $name, $data) = @_;
163             if( $name =~ /^\d+$/ ) {
164             confess "invalid column name: $name can not be an integer";
165             }
166             if( $self->number_of_columns ) {
167             if( $data->number_of_rows != $self->number_of_rows ) {
168             confess "number of rows in column is @{[ $data->number_of_rows ]}; expected @{[ $self->number_of_rows ]}";
169             }
170             }
171             1;
172             }
173              
174             sub add_columns {
175             my ($self, @columns) = @_;
176             confess "uneven number of elements for column specification" unless @columns % 2 == 0;
177             for ( List::AllUtils::pairs(@columns) ) {
178             my ( $name, $data ) = @$_;
179             $self->add_column( $name => $data );
180             }
181             }
182              
183             sub add_column {
184             my ($self, $name, $data) = @_;
185             confess "column $name already exists"
186             if $self->_columns->EXISTS( $name );
187              
188             # TODO apply column role to data
189             $data = PDL::SV->new( $data ) if ref $data eq 'ARRAY';
190              
191             $self->_column_validate( $name => $data);
192              
193              
194             $self->_columns->Push( $name => $data );
195             }
196              
197             # R
198             # > iris[c(1,2,3,3,3,3),]
199             # PDL
200             # $ sequence(10,4)->dice(X,[0,1,1,0])
201             sub select_rows {
202             my ($self, @which_rest) = @_;
203              
204             my $which = [];
205             if( @which_rest > 1 ) {
206             $which = \@which_rest; # array to arrayref
207             } else {
208             $which = $which_rest[0]; # get the first value off
209             }
210              
211             $which = PDL::Core::topdl($which); # ensure it is a PDL
212              
213             my $colnames = $self->column_names;
214             my $colspec = [ map {
215             ( $colnames->[$_] => $self->nth_column($_)->dice($which) )
216             } 0..$self->number_of_columns-1 ];
217              
218             $self->_make_actual_row_names;
219             my $select_df = Data::Frame->new(
220             columns => $colspec,
221             _row_names => $self->row_names->dice( $which ) );
222             }
223              
224             sub _column_helper {
225             my ($self) = @_;
226             Data::Frame::Column::Helper->new( df => $self );
227             }
228              
229             sub equal {
230             my ($self, $other, $d) = @_;
231             if( blessed($self) && $self->isa('Data::Frame') && blessed($other) && $other->isa('Data::Frame') ) {
232             if( $self->number_of_columns == $other->number_of_columns ) {
233             my @eq_cols = map { $self->nth_column($_) == $other->nth_column($_) }
234             0..$self->number_of_columns-1;
235             my @colnames = @{ $self->columns };
236             my @colspec = List::AllUtils::mesh( @colnames, @eq_cols );
237             return Data::Frame->new( columns => \@colspec );
238             } else {
239             die "number of columns is not equal: @{[$self->number_of_columns]} != @{[$other->number_of_columns]}";
240             }
241             }
242             }
243              
244             1;
245              
246             __END__