File Coverage

blib/lib/String/Markov.pm
Criterion Covered Total %
statement 96 96 100.0
branch 24 24 100.0
condition 11 12 91.6
subroutine 13 13 100.0
pod 4 8 50.0
total 148 153 96.7


line stmt bran cond sub pod time code
1             package String::Markov;
2              
3             # ABSTRACT: A Moo-based, text-oriented Markov Chain module
4              
5             our $VERSION = 0.008;
6              
7 1     1   1282 use 5.010;
  1         3  
  1         60  
8 1     1   759 use Moo;
  1         18438  
  1         9  
9 1     1   2660 use namespace::autoclean;
  1         17065  
  1         9  
10              
11 1     1   967 use Unicode::Normalize qw(normalize);
  1         1967  
  1         121  
12 1     1   9 use List::Util qw(sum);
  1         2  
  1         1234  
13              
14             has normalize => (is => 'rw', default => sub { 'C' });
15             has do_chomp => (is => 'rw', default => sub { 1 });
16             has null => (is => 'ro', default => sub { "\0" });
17             has stable => (is => 'ro', default => sub { 1 });
18             has order => (is => 'ro', isa => sub {
19             die "Need an integer greater than zero" if !$_[0] || $_[0] =~ /\D/;
20             }, default => sub { 2 });
21              
22             has ['split_sep','join_sep'] => (
23             is => 'rw',
24             default => sub { undef }
25             );
26              
27             has ['transition_count','row_sum'] => (
28             is => 'ro',
29             isa => sub { die "Need a hash ref" if ref $_[0] ne 'HASH'; },
30             default => sub { {} }
31             );
32              
33             around BUILDARGS => sub {
34             my ($orig, $class, @arg) = @_;
35             my %ahash;
36              
37             %ahash = @arg == 1 ? %{$arg[0]} : @arg;
38              
39             my $sep = delete $ahash{sep} // '';
40             die "ERR: sep argument must be scalar; did you mean to set split_sep instead?" if ref $sep;
41             $ahash{split_sep} //= $sep;
42             $ahash{join_sep} //= $sep;
43              
44             return $class->$orig(\%ahash);
45             };
46              
47             sub join_prob {
48 143     143 0 166 my ($self, $orig_prob) = @_;
49 143         141 my %p;
50              
51 143         117 @p{@{$orig_prob->[0]}} = @{$orig_prob->[1]};
  143         429  
  143         217  
52              
53 143         396 return \%p;
54             }
55              
56             sub split_prob {
57 485     485 0 602 my ($self, $orig_prob) = @_;
58              
59 485 100       963 if ($self->stable) {
60 353         1491 my @k = sort keys %$orig_prob;
61             return [
62 353         2059 \@k,
63 353         1090 [@{$orig_prob}{@k}],
64             ];
65             } else {
66             return [
67 132         898 [keys %$orig_prob],
68             [values %$orig_prob],
69             ];
70             }
71             }
72              
73             sub split_all_prob {
74 7     7 0 18 my $self = shift;
75 7         24 my $tc = $self->transition_count;
76 7         18 my $nt = {};
77              
78 7         61 while (my ($state, $prob) = each %$tc) {
79 316         523 $nt->{$state} = $self->split_prob($prob);
80             }
81              
82 7         467 %$tc = %$nt;
83             }
84              
85             sub split_line {
86 42     42 1 67 my ($self, $sample) = @_;
87 42 100       157 if (my $norm = $self->normalize) {
88 40         171 $sample = normalize($norm, $sample);
89             }
90 42         1363 return split($self->split_sep, $sample);
91             }
92              
93             sub add_sample {
94 46     46 1 20611 my ($self, $sample) = @_;
95 46         166 my $n = $self->order;
96 46         125 my $null = $self->null;
97              
98 46         79 my $sref = ref $sample;
99 46         170 my @nms = ($null,) x $n;
100              
101 46 100       191 if ($sref eq 'ARRAY') {
    100          
102 2         6 push @nms, @$sample;
103             } elsif (!$sref) {
104 43 100       193 die 'ERR: missing split separator,' if !defined $self->split_sep;
105 42         144 push @nms, $self->split_line($sample);
106             } else {
107 1         8 die "ERR: bad sample type $sref";
108             }
109              
110 44         93 push @nms, $null;
111              
112 44   100     203 my $sep = $self->join_sep // '';
113 44         98 my $count = $self->transition_count;
114 44         88 my $sum = $self->row_sum;
115 44         149 for my $i (0 .. ($#nms - $n)) {
116 611         1459 my $cur = join($sep, @nms[$i .. ($i + $n - 1)]);
117 611         772 my $nxt = $nms[$i + $n];
118 611         1009 my $prob = $count->{$cur};
119 611 100 100     1883 if ($prob && ref $prob ne 'HASH') {
120 143         272 $count->{$cur} = $self->join_prob($prob);
121             }
122 611         1424 ++$count->{$cur}{$nxt};
123 611         1150 ++$sum->{$cur};
124             }
125              
126 44         613 return $self;
127             }
128              
129             sub add_files {
130 7     7 1 2070 my ($self, @files) = @_;
131 7         33 my $do_chomp = $self->do_chomp;
132              
133 7         31 local @ARGV = @files;
134 7         1086 while(my $sample = <>) {
135 26 100       82 chomp $sample if $do_chomp;
136 26         84 $self->add_sample($sample);
137             }
138              
139 7         42 $self->split_all_prob();
140              
141 7         112 return $self;
142             }
143              
144             sub sample_next_state {
145 6671     6671 0 14380 my ($self, @cur_state) = @_;
146 6671 100       16373 die "ERR: wrong amount of state" if @cur_state != $self->order;
147              
148 6668         9485 my $count = $self->transition_count;
149 6668         8231 my $sum = $self->row_sum;
150              
151 6668   100     18356 my $cur = join($self->join_sep // '', @cur_state);
152 6668         12249 my $thresh = $sum->{$cur};
153 6668 100       11772 return undef if !$thresh;
154              
155 6652         8734 $thresh *= rand();
156              
157 6652         10372 my $prob = $count->{$cur};
158 6652 100       13647 if (ref $prob ne 'ARRAY') {
159 169         363 $prob = $self->split_prob($prob);
160 169         328 $count->{$cur} = $prob;
161             }
162              
163 6652         6332 my $s = 0;
164 6652         6140 my $i = 0;
165 6652         6205 my ($k, $v) = @{$prob};
  6652         10318  
166 6652   66     7108 do {
167 7522         34952 $s += $v->[$i];
168             } while ($thresh > $s && ++$i);
169 6652         27978 return $k->[$i];
170             }
171              
172             sub generate_sample {
173 423     423 1 124429 my ($self) = @_;
174              
175 423         962 my $null = $self->null;
176 423         803 my $n = $self->order;
177 423   100     1129 my $sep = $self->join_sep // '';
178 423         1028 my @nm = ($null,) x $n;
179              
180 423         449 do {
181 6624         14891 push @nm, $self->sample_next_state(@nm[-$n .. -1]);
182             } while ($nm[-1] ne $null);
183              
184 423         3939 @nm = @nm[$n .. ($#nm-1)];
185              
186             return wantarray ?
187 423 100       4228 @nm :
    100          
188             defined $self->join_sep ?
189             join($sep, @nm) :
190             \@nm;
191              
192             }
193              
194             __PACKAGE__->meta->make_immutable;
195              
196             1;
197              
198             __END__