File Coverage

blib/lib/Text/Sequence.pm
Criterion Covered Total %
statement 73 79 92.4
branch 22 26 84.6
condition n/a
subroutine 10 12 83.3
pod 6 6 100.0
total 111 123 90.2


line stmt bran cond sub pod time code
1             package Text::Sequence;
2              
3 1     1   1032 use strict;
  1         2  
  1         35  
4 1     1   5 use Carp;
  1         2  
  1         81  
5 1     1   5 use vars qw($VERSION);
  1         2  
  1         1568  
6              
7             $VERSION = '0.25';
8              
9             =pod
10              
11             =head1 NAME
12              
13             Text::Sequence - spot one-dimensional sequences in patterns of text
14              
15             =head1 SYNOPSIS
16              
17             use Text::Sequence;
18            
19             my @list = get_files_in_dir();
20             my ($sequences, $singletons) = Text::Sequence::find($somedir);
21              
22              
23             my $sequence = $sequences->[0];
24             print $sequence->template();
25              
26             my $num = 0;
27             foreach my ($element) ($sequence->members()) {
28             ++$num;
29             print "$num) $filename\n";
30             }
31            
32             =head1 DESCRIPTION
33              
34             A sequence could be a list of files like
35              
36             00001.jpg
37             00002.jpg
38             00003.jpg
39             ...
40             05000.jpg
41            
42            
43             or
44              
45             raw.0001.txt
46             ...
47             raw.0093.txt
48              
49             or
50              
51             foo3a.html
52             foo3b.html
53             foo3c.html
54              
55             or even
56              
57             1.mp3
58             100.mp3
59            
60             in which case their templates would be
61              
62             %.5d.tif
63            
64             raw.%.4d.txt
65              
66             foo3%s.html
67            
68             %d.mp3
69            
70             respectively.
71            
72             This library will attempt to
73              
74             =over 4
75              
76             =item find all sequences in a given list
77              
78             =item tell you which elements are missing from a sequence
79              
80             =item be able to cope with non padded numbers in sequences
81              
82             =back
83              
84             It does B spot multi-dimensional sequences, e.g. C.
85              
86             =head1 METHODS
87              
88              
89             =head2 find( @elements )
90              
91             my ($sequences, $singletons) = Text::Sequence::find($somedir);
92              
93             A static method to find all the sequences in a list of elements.
94             Both are returned as arrayrefs.
95              
96             =cut
97              
98             sub find {
99 7     7 1 6799 my @elements = @_;
100 7         22 my %candidates = _find_candidates(@elements);
101 7         27 my @seqs = _find_sequences(\%candidates);
102              
103             # Find singletons by process of elimination, going through
104             # all sequence members.
105 7         15 my %singletons = map { $_ => 1 } @elements;
  77         194  
106 7         20 foreach my $seq (@seqs) {
107 18         34 my @members = $seq->members;
108 18         47 delete $singletons{$seq->template($_)} foreach @members;
109             }
110            
111 7         80 return (\@seqs, [ keys %singletons ]);
112             }
113              
114              
115             sub _find_candidates {
116 7     7   13 my %candidates;
117              
118 7         16 foreach my $element (@_) {
119 77 50       260 next unless $element =~ /\d/; # nothing without numbers
120              
121 77         437 while ($element =~ /\G.*?(?:(\d+)|(?
122 102         147 my $cand = $element;
123              
124 102 100       255 if (defined $1) {
    50          
125             # Numerical sequence
126 83         394 my $num = substr($cand, $-[1], $+[1] - $-[1], '%d');
127            
128             # There could be multiple lengths of the number we just
129             # changed to a %d, need to analyse the length frequencies
130             # in conjunction with the padding to see if differing
131             # lengths are still part of the same sequence (e.g.
132             # to distinguish foo.%3d.bar from foo.%02d.bar).
133 83         179 my $length = length($num);
134             # Note that a single zero is not counted as padded.
135 83 100       889 my $pad = ($num =~ /^0\d/) ? 'p' : '';
136             # Note how we "de-pad" the members here.
137 83         90 push @{ $candidates{$cand}{formats}{$pad . $length} }, $num + 0;
  83         371  
138 83         584 $candidates{$cand}{count}++;
139             }
140             elsif (defined $2) {
141 19         63 my $letter = substr($cand, $-[2], $+[2] - $-[2], '%s');
142 19         59 push @{ $candidates{$cand}{formats}{letter} }, $letter;
  19         55  
143 19         108 $candidates{$cand}{count}++;
144             }
145             else {
146 0         0 die "BUG! Missing number or letter at pos ", pos($element),
147             " of '$element', match was '$&'";
148             }
149             }
150             }
151 7         51 return %candidates;
152             }
153              
154             sub _find_sequences {
155 7     7   12 my ($candidates) = @_;
156              
157 7         10 my @seqs;
158              
159 7         23 foreach my $cand (keys %$candidates) {
160             # it's not a sequence if there's only 1
161 35 100       215 next if $candidates->{$cand}{count} == 1;
162              
163 15         26 my $formats = $candidates->{$cand}{formats};
164              
165 15 100       33 if (my $letters = $formats->{letter}) {
166 5         13 push @seqs, Text::Sequence->new($cand, @$letters);
167 5         9 next;
168             }
169              
170             # That was the easy bit, numbers are much harder.
171              
172             # First look for padded numbers. Padding is quite a
173             # deliberate action, so our best effort assumption is that if
174             # there is a number padded to length n, any other (non-padded)
175             # numbers of length n must belong to the same sequence. It's
176             # not quite optimal, but we'd need some serious AI to separate
177             # things like (1, 4, 64, 256, 07 .. 13) into
178             #
179             # [ map 4**$_, 0 .. 3 ] and [ 07 .. 13 ]
180             #
181             # The following code will separate it into
182             #
183             # [ 07 .. 13, 64 ] and [ 1, 4, 256 ]
184             #
185 10         58 foreach my $padded (grep /^p/, keys %$formats) {
186 9         30 (my $length = $padded) =~ s/^p//;
187 9         16 my @members = (
188 9 100       40 @{ $formats->{$padded} },
189 9         15 @{ $formats->{$length} || [] },
190             );
191 9         39 delete @$formats{$padded, $length};
192 9         49 (my $pcand = $cand) =~ s/%d/%.${length}d/;
193 9         32 push @seqs, Text::Sequence->new($pcand, @members);
194             }
195             # Now the remaining elements (if any) all get swept into the
196             # %d non-padded bucket.
197 10         27 my @members = ( map @{ $formats->{$_} }, keys %$formats );
  8         24  
198 10 100       37 push @seqs, Text::Sequence->new($cand, @members) if @members;
199             }
200              
201 7         21 return @seqs;
202             }
203              
204              
205              
206             =head2 new( $template, @member_nums )
207              
208             Creates a new sequence object.
209              
210             =cut
211              
212             sub new {
213 18     18 1 24 my $class = shift;
214 18 50       39 my $template = shift or die "You must pass a template\n";
215              
216 18         31 my $self = bless {
217             template => $template,
218             re => _to_re($template),
219             members => [ @_ ],
220             }, $class;
221              
222 18         62 return $self;
223             }
224              
225              
226             sub _to_re {
227 18     18   25 my $re = shift;
228              
229 18 100       83 if ($re =~ m!%\.(\d+)d!) {
    100          
    50          
230 9         65 my $m = $1;
231 9         67 $re =~ s!$&!(\\d{$m})!;
232             } elsif ($re =~ m!%d!) {
233 4         25 $re =~ s!$&!(\\d+)!;
234             } elsif ($re =~ m!%s!) {
235 5         32 $re =~ s!$&!(.+=?)!;
236             }
237              
238 18         123 return $re;
239              
240             }
241              
242             =head2 template( $number_or_letter )
243              
244             Tell you the template of the sequence, in C-like formats.
245              
246             If you pass in a number or letter then it will substitute it in to
247             return an actual sequence element.
248              
249             =cut
250              
251             sub template {
252 118     118 1 264 my $self = shift;
253            
254 118 100       184 if (@_) {
255 82         380 return sprintf($self->{template}, $_[0]);
256             } else {
257 36         107 return $self->{template};
258             }
259             }
260              
261             =head2 members()
262              
263             Returns a list describing the members of the sequence. Each item in
264             the list is a letter or (non-padded) number which can be substituted
265             into the template to obtain the original element
266              
267             For members of the same width, order is preserved from the original
268             call to C.
269              
270             =cut
271              
272             sub members {
273 30     30 1 24608 my $self = shift;
274 30         40 return @{ $self->{members} };
  30         389  
275             }
276              
277             =head2 in( $string)
278              
279             Tells you whether a particular string is in this sequence.
280              
281             =cut
282              
283             sub in {
284 0     0 1   my $self = shift;
285 0           my $test = shift;
286            
287 0           my $re = $self->{re};
288            
289 0           return $test =~ m!$re!;
290              
291             }
292              
293             =head2 re
294              
295             Returns the regular expression used to determine whether something
296             is in the sequence or not.
297              
298             =cut
299              
300             sub re {
301 0     0 1   return $_[0]->{re};
302             }
303              
304              
305             =head1 AUTHOR
306              
307             Simon Wistow
308             Adam Spiers
309              
310             =head1 COPYRIGHT
311              
312             Copyright (c) 2004 - Simon Wistow
313              
314             =head1 BUGS
315              
316             Can't insist on sequences being contiguous (yet).
317              
318             =head1 SEE ALSO
319              
320             =cut
321              
322             1;