File Coverage

blib/lib/Acme/Grep2D.pm
Criterion Covered Total %
statement 249 255 97.6
branch 21 34 61.7
condition 10 12 83.3
subroutine 23 25 92.0
pod 16 16 100.0
total 319 342 93.2


line stmt bran cond sub pod time code
1             package Acme::Grep2D;
2              
3 7     7   225102 use warnings;
  7         18  
  7         244  
4 7     7   41 use strict;
  7         14  
  7         229  
5 7     7   3285 use Data::Dumper;
  7         36115  
  7         415  
6 7     7   7394 use Perl6::Attributes;
  7         223338  
  7         51  
7              
8             =head1 NAME
9              
10             Acme::Grep2D - Grep in 2 dimensions
11              
12             =head1 VERSION
13              
14             Version 0.01
15              
16             =cut
17              
18             our $VERSION = '0.01';
19              
20              
21             =head1 SYNOPSIS
22              
23             use Acme::Grep2D;
24              
25             my $foo = Acme::Grep2D->new(text => ??);
26             ...
27              
28             =head1 DESCRIPTION
29              
30             For testing another module, I needed the ability to grep in 2 dimensions,
31             hence this module.
32              
33             This module can grep forwards, backwards, up, down, and diagonally in a
34             given text string. Given the text:
35              
36             THIST T S
37             .H H H II
38             ..I II SIHTH
39             ...SS T T
40              
41             We can find all occurances of THIS.
42              
43             Full Perl regexp is allowed, with a few limitations. Unlike regular
44             grep, you get back (for each match) an array containing array
45             references with the following contents:
46              
47             [$length, $x, $y, $dx, $dy, ??]
48              
49             Operational note: there is one more argument at the end of the
50             returned array reference (as indicated by ??). Don't mess with
51             this. It's reserved for future use.
52              
53             =head1 METHODS
54              
55             =cut
56              
57             =head2 B
58              
59             $g2d = Acme::Grep2D->new(text => ??);
60              
61             Constructor. Specify text pattern to be grepped
62             (multiline, with newlines).
63              
64             Example:
65              
66             my $text = <<'EOF';
67             foobarf
68             .o,,,o
69             ,,o?f?fr
70             <<,b ooa
71             ##a#a ob
72             @r@@@rbo
73             ------ao
74             ~~~~~~rf
75             EOF
76              
77             $g2d = Acme::Grep2D->new(text => $text);
78            
79             Now, our grep will have no problem finding all of the "foobar"
80             strings in the text (see B or other more directional methods).
81              
82             The author is interested in any novel use you might find for this
83             module (other than solving newspaper puzzles).
84              
85             =cut
86              
87             sub new {
88 5     5 1 1938 my ($class, %opts) = @_;
89 5         16 my $self = \%opts;
90 5         16 bless $self, $class;
91 5         40 $.Class = $class;
92 5         33 ./_required('text');
93 5         27 ./_init();
94 5         34 return $self;
95             }
96              
97             # check for mandatory options
98             sub _required {
99 5     5   16 my ($self, $name) = @_;
100 5 50       34 die "$.Class - $name is required\n" unless defined $self->{$name};
101             }
102              
103             # adjust dimensions to be rectangular, and figure out what's
104             # in there in all directions
105             sub _init {
106 5     5   13 my ($self) = @_;
107 5         16 my $text = $.text;
108 5         13 my @text;
109            
110             # split on newlines, preserving them spatially
111 5         37 while ((my $n = index($text, "\n")) >= 0) {
112 45         88 my $chunk = substr($text, 0, $n);
113 45         81 push(@text, $chunk);
114 45         207 $text = substr($text, $n+1);
115             }
116 5         46 chomp foreach @text;
117              
118 5         12 my @len;
119 5         48 push(@len, length($_)) foreach @text;
120 5         16 my $maxlen = $len[0];
121 5         10 my $nlines = @text;
122              
123             #determine max length of each string
124 45 100       179 map {
125 5         26 $maxlen = $len[$_] if $len[$_] > $maxlen;
126             } 0..($nlines-1);
127              
128             # make all lines same length
129 45         114 map {
130 5         18 $text[$_] .= ' ' x ($maxlen-$len[$_]);
131             } 0..($nlines-1);
132             #print Dumper(\@text);
133              
134 5         14 my @diagLR;
135             my @diagRL;
136 0         0 my @vertical;
137 5         12 my $x = 0;
138 5         20 my $y = 0;
139 5         13 my $max = $nlines;
140 5 100       29 $max = $maxlen if $maxlen < $nlines;
141              
142             # find text along diagonal L->R
143 5         24 for (my $char=0; $char < $maxlen; $char++) {
144 67         81 my @d;
145 67         104 $x = $char;
146 67         96 my $y = 0;
147 67         485 my @origin = ($x, $y);
148             map {
149 67 100 66     204 if ($y < $nlines && $x < $maxlen) {
  620         2639  
150 433         767 my $char = substr($text[$y], $x, 1);
151 433 50       1366 push(@d, $char) if defined $char;
152             }
153 620         691 $x++;
154 620         969 $y++;
155             } 0..$nlines-1;
156 67         189 unshift(@d, \@origin);
157 67 50       349 push(@diagLR, \@d) if @d;
158             }
159              
160 5         26 for (my $line=1; $line < $nlines; $line++) {
161 40         47 my @d;
162 40         163 $x = 0;
163 40         53 my $y = $line;
164 40         79 my @origin = ($x, $y);
165             map {
166 40 100 100     107 if ($y < $nlines && $x < $maxlen) {
  416         1331  
167 187         288 my $char = substr($text[$y], $x, 1);
168 187 50       592 push(@d, $char) if defined $char;
169             }
170 416         405 $x++;
171 416         569 $y++;
172             } 0..$nlines-1;
173 40         97 unshift(@d, \@origin);
174 40 50       204 push(@diagLR, \@d) if @d;
175             }
176              
177             # find text along diagonal R->L
178 5         25 for (my $char=0; $char < $maxlen; $char++) {
179 67         77 my @d;
180 67         89 $x = $char;
181 67         83 my $y = 0;
182 67         125 my @origin = ($x, $y);
183             map {
184 67 100 66     141 if ($y < $nlines && $x >= 0) {
  620         2437  
185 433         682 my $char = substr($text[$y], $x, 1);
186 433 50       1362 push(@d, $char) if defined $char;
187             }
188 620         651 $x--;
189 620         926 $y++;
190             } 0..$nlines-1;
191 67         167 unshift(@d, \@origin);
192 67 50       347 push(@diagRL, \@d) if @d;
193             }
194              
195 5         25 for (my $line=1; $line < $nlines; $line++) {
196 40         49 my @d;
197 40         46 $x = $maxlen-1;
198 40         55 my $y = $line;
199 40         84 my @origin = ($x, $y);
200             map {
201 40 100 100     102 if ($y < $nlines && $x >= 0) {
  416         1277  
202 187         295 my $char = substr($text[$y], $x, 1);
203 187 50       546 push(@d, $char) if defined $char;
204             }
205 416         413 $x--;
206 416         571 $y++;
207             } 0..$nlines-1;
208 40         97 unshift(@d, \@origin);
209 40 50       200 push(@diagRL, \@d) if @d;
210             }
211              
212             # find text along vertical
213 5         22 for (my $char=0; $char < $maxlen; $char++) {
214 67         69 my @d;
215 67         115 my @origin = ($char, $y);
216 67         799 push(@d, substr($text[$_], $char, 1)) for 0..$nlines-1;
217 67         145 unshift(@d, \@origin);
218 67         284 push(@vertical, \@d);
219             }
220              
221             # correct LR to make text greppable
222 107         338 map {
223 5         14 my ($coords, @text) = @$_;
224 107         188 my $text = join('', @text);
225 107         477 $_ = [$text, $coords];
226             } @diagLR;
227              
228             # correct RL to make text greppable
229 107         280 map {
230 5         14 my ($coords, @text) = @$_;
231 107         176 my $text = join('', @text);
232 107         373 $_ = [$text, $coords];
233             } @diagRL;
234              
235             # correct vertical to make text greppable
236 67         209 map {
237 5         15 my ($coords, @text) = @$_;
238 67         131 my $text = join('', @text);
239 67         278 $_ = [$text, $coords];
240             } @vertical;
241 5         22 $.diagLR = \@diagLR;
242 5         14 $.diagRL = \@diagRL;
243 5         32 $.vertical = \@vertical;
244 5         15 $.maxlen = $maxlen;
245 5         98 $.nlines = $nlines;
246 5         49 $.text = \@text;
247             }
248              
249             # reverse a string
250             sub _reverse {
251 796     796   970 my ($self, $text) = @_;
252 796         2837 my @text = split //, $text;
253 796         2916 return join '', reverse(@text);
254             }
255              
256             =head2 B
257              
258             $g2d->Grep($re);
259              
260             Find the regular expression ($re) no matter where it occurs in
261             text.
262              
263             The difference from a regular grep is that "coordinate" information
264             is returned for matches. This is the length of the
265             found match, x and y coordinates, along with
266             directional movement information (dx, dy).
267             It's easiest to use B to access matches.
268              
269             =cut
270              
271             sub Grep {
272 10     10 1 1604 my ($self, $re) = @_;
273 10         21 my @matches;
274              
275             # find things "normally," like a regular grep
276 10         35 push(@matches, ./grep_h($re));
277              
278             # find things in the L->R diagonal vector
279 10         46 push(@matches, ./grep_lr($re));
280              
281             # find things in the R->L diagonal vector
282 10         40 push(@matches, ./grep_rl($re));
283              
284             # find things in the vertical vector
285 10         39 push(@matches, ./grep_v($re));
286              
287 10         52 return @matches;
288             }
289              
290             sub _ref {
291 0     0   0 my ($self, $ref) = @_;
292 0 0       0 return \$ref if ref($ref) eq 'SCALAR';
293 0 0       0 return \$ref->[0] if ref($ref) eq 'ARRAY';
294             }
295              
296             =head2 B
297              
298             @matches = $g2d->grep_hf($re);
299              
300             Search text normally, left to right.
301              
302             =cut
303              
304             sub grep_hf {
305 10     10 1 19 my ($self, $re) = @_;
306 10         14 my @matches;
307 10         17 my $n = 0;
308             # find things "normally," like a regular grep
309 10         17 foreach (@{$.text}) {
  10         34  
310 105         117 my $text = $_;
311 105         559 while ($text =~/($re)/g) {
312 4         23 push(@matches, [length($1), _start(\$text,$1), $n, 1, 0, \$_])
313             }
314 105         175 $n++;
315             };
316 10         30 return @matches;
317             }
318              
319             =head2 B
320              
321             @matches = $g2d->grep_hf($re);
322              
323             Search text normally, but right to left.
324              
325             =cut
326              
327             sub grep_hr {
328 10     10 1 25 my ($self, $re) = @_;
329 10         14 my @matches;
330 10         15 my $n = 0;
331             # find things "normally," like a regular grep
332 10         18 foreach (@{$.text}) {
  10         31  
333 105         132 my $text = $_;
334 105         207 $text = ./_reverse($text);
335 105         503 while ($text =~/($re)/g) {
336 4         16 push(@matches,
337             [length($1), length($text)-(_start(\$text,$1)+1),
338             $n, -1, 0, \$_])
339             }
340 105         189 $n++;
341             };
342 10         28 return @matches;
343             }
344              
345             =head2 B
346              
347             @matches = $g2d->grep_h($re);
348              
349             Search text normally, in both directions.
350              
351             =cut
352              
353             sub grep_h {
354 10     10 1 16 my ($self, $re) = @_;
355 10         17 my @matches;
356 10         32 push(@matches, ./grep_hf($re));
357 10         38 push(@matches, ./grep_hr($re));
358 10         26 return @matches;
359             }
360              
361              
362             =head2 B
363              
364             @matches = grep_vf($re);
365              
366             Search text vertically, down.
367              
368             =cut
369              
370             sub grep_vf {
371 10     10 1 18 my ($self, $re) = @_;
372 10         13 my @matches;
373             # find things in the vertical vector
374 10         19 foreach (@{$.vertical}) {
  10         37  
375 167         219 my ($text, $coords) = @$_;
376 167         191 my ($x, $y) = @$coords;
377 167         702 push(@matches, [length($1), $x, _start(\$text, $1),
378             0, 1, \$_]) while ($text =~ /($re)/g);
379             }
380 10         28 return @matches;
381             }
382              
383             =head2 B
384              
385             @matches = grep_vr($re);
386              
387             Search text vertically, up.
388              
389             =cut
390              
391             sub grep_vr {
392 10     10 1 24 my ($self, $re) = @_;
393 10         19 my @matches;
394             # find things in the vertical vector
395 10         14 foreach (@{$.vertical}) {
  10         28  
396 167         255 my ($text, $coords) = @$_;
397 167         184 my ($x, $y) = @$coords;
398 167         283 $text = ./_reverse($text);
399 167         873 push(@matches, [length($1),$x, length($text)-_start(\$text, $1)-1,
400             0, -1, \$_]) while ($text =~ /($re)/g);
401             }
402 10         27 return @matches;
403             }
404              
405             =head2 B
406              
407             @matches = $g2d->grep_v($re);
408              
409             Search text vertically, both directions.
410              
411             =cut
412              
413             sub grep_v {
414 10     10 1 18 my ($self, $re) = @_;
415 10         15 my @matches;
416 10         38 push(@matches, ./grep_vf($re));
417 10         33 push(@matches, ./grep_vr($re));
418 10         23 return @matches;
419             }
420              
421             =head2 B
422              
423             @matches = $g2d->grep_rlf($re);
424              
425             Search the R->L vector top to bottom.
426              
427             =cut
428              
429             sub grep_rlf {
430 10     10 1 18 my ($self, $re) = @_;
431 10         12 my @matches;
432             # find things in the R->L diagonal vector
433 10         15 foreach (@{$.diagRL}) {
  10         33  
434 262         376 my ($text, $coords) = @$_;
435 262         334 my ($x, $y) = @$coords;
436 262         1108 while ($text =~ /($re)/g) {
437 3         14 my $off = _start(\$text, $1);
438 3         8 my $length = length($1);
439 3         28 push(@matches, [$length, $x-$off, $off+$y, -1, 1, \$_]);
440             }
441             }
442 10         28 return @matches;
443             }
444              
445             =head2 B
446              
447             @matches = $g2d->grep_rlr($re);
448              
449             Search the R->L vector bottom to top.
450              
451             =cut
452              
453             sub grep_rlr {
454 10     10 1 49 my ($self, $re) = @_;
455 10         16 my @matches;
456             # find things in the R->L diagonal vector
457 10         13 foreach (@{$.diagRL}) {
  10         36  
458 262         356 my ($text, $coords) = @$_;
459 262         314 my ($x, $y) = @$coords;
460 262         501 $text = ./_reverse($text);
461 262         358 $x -= length($text);
462 262         275 $y += length($text);
463 262         305 $x++;
464 262         246 $y--;
465 262         1148 while ($text =~ /($re)/g) {
466 5         14 my $off = _start(\$text, $1);
467 5         13 my $length = length($1);
468 5         40 push(@matches, [$length, $x+$off, $y-$off, 1, -1, \$_]);
469             }
470             }
471 10         27 return @matches;
472             }
473              
474             =head2 B
475              
476             @matches = $g2d->grep_rlf($re);
477              
478             Search the R->L both directions.
479              
480             =cut
481              
482             sub grep_rl {
483 10     10 1 18 my ($self, $re) = @_;
484 10         56 my @matches;
485 10         38 push(@matches, ./grep_rlf($re));
486 10         42 push(@matches, ./grep_rlr($re));
487 10         21 return @matches;
488             }
489              
490             =head2 B
491              
492             @matches = $g2d->grep_lrf($re);
493              
494             Search the L->R top to bottom.
495              
496             =cut
497              
498             sub grep_lrf {
499 10     10 1 18 my ($self, $re) = @_;
500 10         12 my @matches;
501             # find things in the L->R diagonal vector
502 10         19 foreach (@{$.diagLR}) {
  10         32  
503 262         361 my ($text, $coords) = @$_;
504 262         303 my ($x, $y) = @$coords;
505 262         1122 while ($text =~ /($re)/g) {
506 3         11 my $off = _start(\$text,$1);
507 3         25 push(@matches,
508             [length($1), $x+$off, $off+$y, 1, 1, \$_])
509             }
510             }
511 10         30 return @matches;
512             }
513              
514             =head2 B
515              
516             @matches = $g2d->grep_lrr($re);
517              
518             Search the L->R bottom to top.
519              
520             =cut
521              
522             sub grep_lrr {
523 10     10 1 15 my ($self, $re) = @_;
524 10         13 my @matches;
525             # find things in the L->R diagonal vector
526 10         15 foreach (@{$.diagLR}) {
  10         28  
527 262         386 my ($text, $coords) = @$_;
528 262         310 my ($x, $y) = @$coords;
529 262         443 $text = ./_reverse($text);
530 262         1158 while ($text =~ /($re)/g) {
531 2         9 my $off = _start(\$text,$1);
532 2         7 my $length = length($1);
533 2         5 $x += length($text);
534 2         4 $y += length($text);
535 2         3 $x--;
536 2         4 $y--;
537 2         21 push(@matches,
538             [length($1), $x-$off, $y-$off, -1, -1, \$_])
539             }
540             }
541 10         25 return @matches;
542             }
543              
544             =head2 B
545              
546             @matches = $g2d->grep_lr($re);
547              
548             Search the L->R both directions.
549              
550             =cut
551              
552             sub grep_lr {
553 10     10 1 21 my ($self, $re) = @_;
554 10         11 my @matches;
555 10         37 push(@matches, ./grep_lrf($re));
556 10         40 push(@matches, ./grep_lrr($re));
557 10         34 return @matches;
558             }
559              
560             =head2 B
561              
562             $result = $g2d->extract($info);
563              
564             Extract pattern match described by $info, which is a single return
565             from B. E.g.
566              
567             my @matches = $g2d->Grep(qr(foo\w+));
568             map {
569             print "Matched ", $g2d->extract($_), "\n";
570             } @matches;
571              
572             =cut
573              
574             sub extract {
575 28     28 1 14529 my ($self, $info) = @_;
576 28         54 my ($length, $x, $y, $dx, $dy) = @$info;
577 28         32 my @result;
578 127         248 map {
579 28         53 push(@result, substr($.text->[$y], $x, 1));
580 127         167 $x += $dx;
581 127         202 $y += $dy;
582             } 1..$length;
583 28         121 return join('', @result);
584             }
585              
586             sub _start {
587 26     26   59 my ($textRef, $one) = @_;
588 26         166 return pos($$textRef) - length($one);
589             }
590              
591             =head2 B
592              
593             $textRef = $g2d->text();
594              
595             Return an array reference to our internal text buffer. This
596             is for future use. Don't mess with the return, or bad things
597             may happen.
598              
599             =cut
600              
601             sub text {
602 0     0 1   my ($self) = @_;
603 0           return $.text;
604             }
605              
606             =head1 AUTHOR
607              
608             X Cramps, C<< >>
609              
610             =head1 BUGS
611              
612             Please report any bugs or feature requests to
613             C, or through
614             the web interface at
615             L.
616             I will be notified, and then you'll
617             automatically be notified of progress on your bug as I make changes.
618              
619             =head1 SUPPORT
620              
621             You can find documentation for this module with the perldoc command.
622              
623             perldoc Acme::Grep2D
624              
625             You can also look for information at:
626              
627             =over 4
628              
629             =item * RT: CPAN's request tracker
630              
631             L
632              
633             =item * AnnoCPAN: Annotated CPAN documentation
634              
635             L
636              
637             =item * CPAN Ratings
638              
639             L
640              
641             =item * Search CPAN
642              
643             L
644              
645             =back
646              
647              
648             =head1 ACKNOWLEDGEMENTS
649              
650             Captain Beefheart and the Magic Band. Fast & bulbous. Tight, also.
651              
652             =head1 COPYRIGHT & LICENSE
653              
654             Copyright 2009 X Cramps, all rights reserved.
655              
656             This program is free software; you can redistribute it and/or modify it
657             under the same terms as Perl itself.
658              
659             =cut
660              
661             1;