| line | stmt | bran | cond | sub | pod | time | code | 
| 1 |  |  |  |  |  |  | package TableData::Object::Base; | 
| 2 |  |  |  |  |  |  |  | 
| 3 |  |  |  |  |  |  | our $AUTHORITY = 'cpan:PERLANCAR'; # AUTHORITY | 
| 4 |  |  |  |  |  |  | our $DATE = '2020-05-29'; # DATE | 
| 5 |  |  |  |  |  |  | our $DIST = 'TableData-Object'; # DIST | 
| 6 |  |  |  |  |  |  | our $VERSION = '0.112'; # VERSION | 
| 7 |  |  |  |  |  |  |  | 
| 8 | 4 |  |  | 4 |  | 2257 | use 5.010; | 
|  | 4 |  |  |  |  | 57 |  | 
| 9 | 4 |  |  | 4 |  | 18 | use strict; | 
|  | 4 |  |  |  |  | 6 |  | 
|  | 4 |  |  |  |  | 102 |  | 
| 10 | 4 |  |  | 4 |  | 19 | use warnings; | 
|  | 4 |  |  |  |  | 6 |  | 
|  | 4 |  |  |  |  | 133 |  | 
| 11 |  |  |  |  |  |  |  | 
| 12 | 4 |  |  | 4 |  | 1735 | use Scalar::Util::Numeric qw(isint isfloat); | 
|  | 4 |  |  |  |  | 2302 |  | 
|  | 4 |  |  |  |  | 5602 |  | 
| 13 |  |  |  |  |  |  |  | 
| 14 |  |  |  |  |  |  | sub _array_is_numeric { | 
| 15 | 4 |  |  | 4 |  | 12 | my $self = shift; | 
| 16 | 4 |  |  |  |  | 8 | for (@{$_[0]}) { | 
|  | 4 |  |  |  |  | 17 |  | 
| 17 | 8 | 100 | 66 |  |  | 71 | return 0 if defined($_) && !isint($_) && !isfloat($_); | 
|  |  |  | 66 |  |  |  |  | 
| 18 |  |  |  |  |  |  | } | 
| 19 | 3 |  |  |  |  | 12 | return 1; | 
| 20 |  |  |  |  |  |  | } | 
| 21 |  |  |  |  |  |  |  | 
| 22 |  |  |  |  |  |  | sub _list_is_numeric { | 
| 23 | 0 |  |  | 0 |  | 0 | my $self = shift; | 
| 24 | 0 |  |  |  |  | 0 | $self->_array_is_numeric(\@_); | 
| 25 |  |  |  |  |  |  | } | 
| 26 |  |  |  |  |  |  |  | 
| 27 |  |  |  |  |  |  | sub cols_by_name { | 
| 28 | 15 |  |  | 15 | 1 | 2985 | my $self = shift; | 
| 29 | 15 |  |  |  |  | 83 | $self->{cols_by_name}; | 
| 30 |  |  |  |  |  |  | } | 
| 31 |  |  |  |  |  |  |  | 
| 32 |  |  |  |  |  |  | sub cols_by_idx { | 
| 33 | 15 |  |  | 15 | 1 | 28 | my $self = shift; | 
| 34 | 15 |  |  |  |  | 61 | $self->{cols_by_idx}; | 
| 35 |  |  |  |  |  |  | } | 
| 36 |  |  |  |  |  |  |  | 
| 37 |  |  |  |  |  |  | sub col_exists { | 
| 38 | 42 |  |  | 42 | 1 | 5133 | my ($self, $name_or_idx) = @_; | 
| 39 | 42 | 50 |  |  |  | 129 | if ($name_or_idx =~ /\A[0-9][1-9]*\z/) { | 
| 40 | 0 |  |  |  |  | 0 | return $name_or_idx <= @{ $self->{cols_by_idx} }; | 
|  | 0 |  |  |  |  | 0 |  | 
| 41 |  |  |  |  |  |  | } else { | 
| 42 | 42 |  |  |  |  | 207 | return exists $self->{cols_by_name}{$name_or_idx}; | 
| 43 |  |  |  |  |  |  | } | 
| 44 |  |  |  |  |  |  | } | 
| 45 |  |  |  |  |  |  |  | 
| 46 |  |  |  |  |  |  | sub col_name { | 
| 47 | 45 |  |  | 45 | 1 | 4125 | my ($self, $name_or_idx) = @_; | 
| 48 | 45 | 100 |  |  |  | 164 | if ($name_or_idx =~ /\A[0-9][1-9]*\z/) { | 
| 49 | 6 |  |  |  |  | 29 | return $self->{cols_by_idx}[$name_or_idx]; | 
| 50 |  |  |  |  |  |  | } else { | 
| 51 | 39 | 100 |  |  |  | 166 | return exists($self->{cols_by_name}{$name_or_idx}) ? | 
| 52 |  |  |  |  |  |  | $name_or_idx : undef; | 
| 53 |  |  |  |  |  |  | } | 
| 54 |  |  |  |  |  |  | } | 
| 55 |  |  |  |  |  |  |  | 
| 56 |  |  |  |  |  |  | sub col_idx { | 
| 57 | 79 |  |  | 79 | 1 | 4165 | my ($self, $name_or_idx) = @_; | 
| 58 | 79 | 100 |  |  |  | 315 | if ($name_or_idx =~ /\A[0-9][1-9]*\z/) { | 
| 59 | 23 | 100 |  |  |  | 35 | return $name_or_idx < @{ $self->{cols_by_idx} } ? $name_or_idx : undef; | 
|  | 23 |  |  |  |  | 100 |  | 
| 60 |  |  |  |  |  |  | } else { | 
| 61 | 56 |  |  |  |  | 164 | return $self->{cols_by_name}{$name_or_idx}; | 
| 62 |  |  |  |  |  |  | } | 
| 63 |  |  |  |  |  |  | } | 
| 64 |  |  |  |  |  |  |  | 
| 65 |  |  |  |  |  |  | sub col_count { | 
| 66 | 12 |  |  | 12 | 1 | 20 | my $self = shift; | 
| 67 | 12 |  |  |  |  | 16 | scalar @{ $self->{cols_by_idx} }; | 
|  | 12 |  |  |  |  | 36 |  | 
| 68 |  |  |  |  |  |  | } | 
| 69 |  |  |  |  |  |  |  | 
| 70 |  |  |  |  |  |  | sub col_content { | 
| 71 | 26 |  |  | 26 | 1 | 3935 | my ($self, $name_or_idx) = @_; | 
| 72 |  |  |  |  |  |  |  | 
| 73 | 26 |  |  |  |  | 57 | my $col_idx = $self->col_idx($name_or_idx); | 
| 74 | 26 | 100 |  |  |  | 85 | return undef unless defined $col_idx; | 
| 75 |  |  |  |  |  |  |  | 
| 76 | 18 |  |  |  |  | 60 | my $row_count = $self->row_count; | 
| 77 | 18 | 50 |  |  |  | 39 | return [] unless $row_count; | 
| 78 |  |  |  |  |  |  |  | 
| 79 | 18 |  |  |  |  | 31 | my $col_content = []; | 
| 80 | 18 |  |  |  |  | 57 | for my $i (0 .. $row_count-1) { | 
| 81 | 56 |  |  |  |  | 108 | my $row = $self->row_as_aos($i); | 
| 82 | 56 |  |  |  |  | 100 | $col_content->[$i] = $row->[$col_idx]; | 
| 83 |  |  |  |  |  |  | } | 
| 84 | 18 |  |  |  |  | 87 | $col_content; | 
| 85 |  |  |  |  |  |  | } | 
| 86 |  |  |  |  |  |  |  | 
| 87 |  |  |  |  |  |  | sub _select { | 
| 88 | 28 |  |  | 28 |  | 89 | my ($self, $_as, $cols0, $excl_cols, $func_filter_row, $sorts) = @_; | 
| 89 |  |  |  |  |  |  |  | 
| 90 |  |  |  |  |  |  | # determine result's columns & spec | 
| 91 | 28 |  |  |  |  | 100 | my $spec; | 
| 92 |  |  |  |  |  |  | my %newcols_to_origcols; | 
| 93 | 28 |  |  |  |  | 0 | my @cols0; # original column names but with '*' expanded | 
| 94 | 28 |  |  |  |  | 0 | my @newcols; | 
| 95 | 28 | 100 |  |  |  | 63 | if ($cols0) { | 
| 96 | 24 |  |  |  |  | 59 | $spec = {fields=>{}}; | 
| 97 | 24 |  |  |  |  | 41 | my $i = 0; | 
| 98 | 24 |  |  |  |  | 43 | for my $col0 (@$cols0) { | 
| 99 | 35 |  |  |  |  | 49 | my @add; | 
| 100 | 35 | 100 |  |  |  | 65 | if ($col0 eq '*') { | 
| 101 | 4 |  |  |  |  | 9 | @add = @{ $self->{cols_by_idx} }; | 
|  | 4 |  |  |  |  | 14 |  | 
| 102 |  |  |  |  |  |  | } else { | 
| 103 | 31 | 100 |  |  |  | 62 | die "Column '$col0' does not exist" unless $self->col_exists($col0); | 
| 104 | 27 |  |  |  |  | 53 | @add = ($col0); | 
| 105 |  |  |  |  |  |  | } | 
| 106 |  |  |  |  |  |  |  | 
| 107 | 31 |  |  |  |  | 59 | for my $add (@add) { | 
| 108 | 35 | 100 | 100 |  |  | 78 | next if $excl_cols && (grep {$add eq $_} @$excl_cols); | 
|  | 6 |  |  |  |  | 28 |  | 
| 109 | 32 |  |  |  |  | 49 | push @cols0, $add; | 
| 110 | 32 |  |  |  |  | 43 | my $j = 1; | 
| 111 | 32 |  |  |  |  | 41 | my $col = $add; | 
| 112 | 32 |  |  |  |  | 74 | while (defined $newcols_to_origcols{$col}) { | 
| 113 | 8 |  |  |  |  | 12 | $j++; | 
| 114 | 8 |  |  |  |  | 34 | $col = "${add}_$j"; | 
| 115 |  |  |  |  |  |  | } | 
| 116 | 32 |  |  |  |  | 50 | $newcols_to_origcols{$col} = $add; | 
| 117 | 32 |  |  |  |  | 44 | push @newcols, $col; | 
| 118 |  |  |  |  |  |  |  | 
| 119 |  |  |  |  |  |  | $spec->{fields}{$col} = { | 
| 120 | 32 |  | 50 |  |  | 40 | %{$self->{spec}{fields}{$add} // {}}, | 
|  | 32 |  |  |  |  | 199 |  | 
| 121 |  |  |  |  |  |  | pos=>$i, | 
| 122 |  |  |  |  |  |  | }; | 
| 123 | 32 |  |  |  |  | 83 | $i++; | 
| 124 |  |  |  |  |  |  | } | 
| 125 |  |  |  |  |  |  | } | 
| 126 | 20 |  |  |  |  | 34 | $cols0 = \@cols0; | 
| 127 |  |  |  |  |  |  | } else { | 
| 128 |  |  |  |  |  |  | # XXX excl_cols is not being observed | 
| 129 | 4 |  |  |  |  | 12 | $spec = $self->{spec}; | 
| 130 | 4 |  |  |  |  | 37 | $cols0 = $self->{cols_by_idx}; | 
| 131 | 4 |  |  |  |  | 10 | @newcols = @{ $self->{cols_by_idx} }; | 
|  | 4 |  |  |  |  | 16 |  | 
| 132 | 4 |  |  |  |  | 35 | for (@newcols) { $newcols_to_origcols{$_} = $_ } | 
|  | 8 |  |  |  |  | 46 |  | 
| 133 |  |  |  |  |  |  | } | 
| 134 |  |  |  |  |  |  |  | 
| 135 | 24 |  |  |  |  | 44 | my $rows = []; | 
| 136 |  |  |  |  |  |  |  | 
| 137 |  |  |  |  |  |  | # filter rows | 
| 138 | 24 |  |  |  |  | 36 | for my $row (@{ $self->rows_as_aohos }) { | 
|  | 24 |  |  |  |  | 95 |  | 
| 139 | 69 | 100 | 100 |  |  | 210 | next unless !$func_filter_row || $func_filter_row->($self, $row); | 
| 140 | 61 |  |  |  |  | 122 | push @$rows, $row; | 
| 141 |  |  |  |  |  |  | } | 
| 142 |  |  |  |  |  |  |  | 
| 143 |  |  |  |  |  |  | # sort rows | 
| 144 | 20 | 100 | 66 |  |  | 85 | if ($sorts && @$sorts) { | 
| 145 |  |  |  |  |  |  | # determine whether each column mentioned in $sorts is numeric, to | 
| 146 |  |  |  |  |  |  | # decide whether to use <=> or cmp. | 
| 147 | 4 |  |  |  |  | 9 | my %col_is_numeric; | 
| 148 | 4 |  |  |  |  | 12 | for my $sortcol (@$sorts) { | 
| 149 | 4 | 50 |  |  |  | 39 | my ($reverse, $col) = $sortcol =~ /\A(-?)(.+)/ | 
| 150 |  |  |  |  |  |  | or die "Invalid sort column specification '$sortcol'"; | 
| 151 | 4 | 50 |  |  |  | 16 | next if defined $col_is_numeric{$col}; | 
| 152 | 4 |  |  |  |  | 20 | my $sch = $self->{spec}{fields}{$col}{schema}; | 
| 153 | 4 | 50 |  |  |  | 16 | if ($sch) { | 
| 154 | 0 |  |  |  |  | 0 | require Data::Sah::Util::Type; | 
| 155 | 0 |  |  |  |  | 0 | $col_is_numeric{$col} = Data::Sah::Util::Type::is_numeric($sch); | 
| 156 |  |  |  |  |  |  | } else { | 
| 157 | 4 |  |  |  |  | 19 | my $col_name = $self->col_name($col); | 
| 158 | 4 | 50 |  |  |  | 13 | defined($col_name) or die "Unknown sort column '$col'"; | 
| 159 |  |  |  |  |  |  | $col_is_numeric{$col} = $self->_array_is_numeric( | 
| 160 | 4 |  |  |  |  | 13 | [map {$_->{$col_name}} @$rows]); | 
|  | 9 |  |  |  |  | 46 |  | 
| 161 |  |  |  |  |  |  | } | 
| 162 |  |  |  |  |  |  | } | 
| 163 |  |  |  |  |  |  |  | 
| 164 |  |  |  |  |  |  | $rows = [sort { | 
| 165 | 4 |  |  |  |  | 24 | for my $sortcol (@$sorts) { | 
|  | 5 |  |  |  |  | 13 |  | 
| 166 | 5 |  |  |  |  | 25 | my ($reverse, $col) = $sortcol =~ /\A(-?)(.+)/; | 
| 167 | 5 |  |  |  |  | 17 | my $name = $self->col_name($col); | 
| 168 |  |  |  |  |  |  | my $cmp = ($reverse ? -1:1) * | 
| 169 |  |  |  |  |  |  | ($col_is_numeric{$col} ? | 
| 170 |  |  |  |  |  |  | ($a->{$name} <=> $b->{$name}) : | 
| 171 | 5 | 100 |  |  |  | 41 | ($a->{$name} cmp $b->{$name})); | 
|  |  | 100 |  |  |  |  |  | 
| 172 | 5 | 50 |  |  |  | 29 | return $cmp if $cmp; | 
| 173 |  |  |  |  |  |  | } | 
| 174 |  |  |  |  |  |  | 0; | 
| 175 |  |  |  |  |  |  | } @$rows]; | 
| 176 |  |  |  |  |  |  | } # sort rows | 
| 177 |  |  |  |  |  |  |  | 
| 178 |  |  |  |  |  |  | # select columns & convert back to aoaos if that's the requested form | 
| 179 |  |  |  |  |  |  | { | 
| 180 | 20 |  |  |  |  | 28 | my $rows2 = []; | 
|  | 20 |  |  |  |  | 34 |  | 
| 181 | 20 |  |  |  |  | 36 | for my $row0 (@$rows) { | 
| 182 | 61 |  |  |  |  | 75 | my $row; | 
| 183 | 61 | 100 |  |  |  | 95 | if ($_as eq 'aoaos') { | 
| 184 | 48 |  |  |  |  | 94 | $row = []; | 
| 185 | 48 |  |  |  |  | 64 | for my $i (0..$#{$cols0}) { | 
|  | 48 |  |  |  |  | 87 |  | 
| 186 | 91 |  |  |  |  | 174 | $row->[$i] = $row0->{$cols0->[$i]}; | 
| 187 |  |  |  |  |  |  | } | 
| 188 |  |  |  |  |  |  | } else { | 
| 189 | 13 |  |  |  |  | 17 | $row = {}; | 
| 190 | 13 |  |  |  |  | 24 | for my $i (0..$#newcols) { | 
| 191 |  |  |  |  |  |  | $row->{$newcols[$i]} = | 
| 192 | 32 |  |  |  |  | 78 | $row0->{$newcols_to_origcols{$newcols[$i]}}; | 
| 193 |  |  |  |  |  |  | } | 
| 194 |  |  |  |  |  |  | } | 
| 195 | 61 |  |  |  |  | 99 | push @$rows2, $row; | 
| 196 |  |  |  |  |  |  | } | 
| 197 | 20 |  |  |  |  | 49 | $rows = $rows2; | 
| 198 |  |  |  |  |  |  | } | 
| 199 |  |  |  |  |  |  |  | 
| 200 |  |  |  |  |  |  | # return result as object | 
| 201 | 20 | 100 |  |  |  | 41 | if ($_as eq 'aoaos') { | 
| 202 | 16 |  |  |  |  | 2308 | require TableData::Object::aoaos; | 
| 203 | 16 |  |  |  |  | 76 | return TableData::Object::aoaos->new($rows, $spec); | 
| 204 |  |  |  |  |  |  | } else { | 
| 205 | 4 |  |  |  |  | 1687 | require TableData::Object::aohos; | 
| 206 | 4 |  |  |  |  | 31 | return TableData::Object::aohos->new($rows, $spec); | 
| 207 |  |  |  |  |  |  | } | 
| 208 |  |  |  |  |  |  | } | 
| 209 |  |  |  |  |  |  |  | 
| 210 |  |  |  |  |  |  | sub select_as_aoaos { | 
| 211 | 24 |  |  | 24 | 1 | 6162 | my ($self, $cols, $excl_cols, $func_filter_row, $sorts) = @_; | 
| 212 | 24 |  |  |  |  | 74 | $self->_select('aoaos', $cols, $excl_cols, $func_filter_row, $sorts); | 
| 213 |  |  |  |  |  |  | } | 
| 214 |  |  |  |  |  |  |  | 
| 215 |  |  |  |  |  |  | sub select_as_aohos { | 
| 216 | 4 |  |  | 4 | 1 | 17 | my ($self, $cols, $excl_cols, $func_filter_row, $sorts) = @_; | 
| 217 | 4 |  |  |  |  | 32 | $self->_select('aohos', $cols, $excl_cols, $func_filter_row, $sorts); | 
| 218 |  |  |  |  |  |  | } | 
| 219 |  |  |  |  |  |  |  | 
| 220 | 0 |  |  | 0 | 1 | 0 | sub uniq_col_names { die "Must be implemented by subclass" } | 
| 221 |  |  |  |  |  |  |  | 
| 222 | 0 |  |  | 0 | 1 | 0 | sub const_col_names { die "Must be implemented by subclass" } | 
| 223 |  |  |  |  |  |  |  | 
| 224 | 4 |  |  | 4 | 1 | 1200 | sub del_col { die "Must be implemented by subclass" } | 
| 225 |  |  |  |  |  |  |  | 
| 226 | 2 |  |  | 2 | 1 | 419 | sub rename_col { die "Must be implemented by subclass" } | 
| 227 |  |  |  |  |  |  |  | 
| 228 | 0 |  |  | 0 | 1 |  | sub switch_cols { die "Must be implemented by subclass" } | 
| 229 |  |  |  |  |  |  |  | 
| 230 |  |  |  |  |  |  | 1; | 
| 231 |  |  |  |  |  |  | # ABSTRACT: Base class for TableData::Object::* | 
| 232 |  |  |  |  |  |  |  | 
| 233 |  |  |  |  |  |  | __END__ |