File Coverage

blib/lib/Religion/Bible/Reference.pm
Criterion Covered Total %
statement 104 106 98.1
branch 37 48 77.0
condition 2 3 66.6
subroutine 20 20 100.0
pod 7 7 100.0
total 170 184 92.3


line stmt bran cond sub pod time code
1 2     2   44655 use warnings;
  2         4  
  2         57  
2 2     2   10 use strict;
  2         5  
  2         160  
3             package Religion::Bible::Reference;
4             # ABSTRACT: canonicalize shorthand bible references
5             $Religion::Bible::Reference::VERSION = '0.016';
6 2         17 use Sub::Exporter -setup => {
7             exports => [ qw(bibref) ],
8             groups => { default => [ qw(bibref) ] },
9 2     2   1921 };
  2         27011  
10              
11             my %book_chapters;
12             my %book_abbrev;
13             my %book_short;
14              
15             BEGIN {
16 2     2   7 for my $attr (qw(book chapter ranges)) {
17 2     2   713 no strict 'refs';
  2         3  
  2         154  
18             *$attr = sub {
19 9 50   9   48 return $_[0]->{$attr} if @_ == 1;
20 0         0 return $_[0]->{$attr} = $_[1];
21 6         69 };
22             }
23             }
24              
25 2     2   1416 use Religion::Bible::Reference::Standard;
  2         5  
  2         2558  
26              
27             #pod =head1 SYNOPSIS
28             #pod
29             #pod use Religion::Bible::Reference;
30             #pod
31             #pod my $quote = bibref("jn8:32");
32             #pod
33             #pod print "($quote)"; # (John 8:32)
34             #pod print $quote->book; # John
35             #pod
36             #pod =head1 DESCRIPTION
37             #pod
38             #pod This module converts simple text descriptions of bible references and ranges
39             #pod into objects that stringify into a canonical form.
40             #pod
41             #pod B This module is mostly an idea and not so much a guaranteed
42             #pod interface or well-tested implementation. If you're interested in either of
43             #pod those existing, you should let me know.
44             #pod
45             #pod =func bibref
46             #pod
47             #pod my $ref = bibref($ref_string)
48             #pod
49             #pod This function is exported by default, and constructs a new
50             #pod Religion::Bible::Reference
51             #pod
52             #pod Reference strings must be a book followed by a list of chapters, verses, or
53             #pod ranges. The following are all valid ranges:
54             #pod
55             #pod Pro 23:12, 23:15-17
56             #pod st.jn8:32
57             #pod Song of Solomon 8:7-8
58             #pod 2 John 1
59             #pod
60             #pod =cut
61              
62 9     9 1 2875 sub bibref { __PACKAGE__->new(@_); }
63              
64             #pod =method new
65             #pod
66             #pod my $ref = Religion::Bible::Reference->new($ref_string)
67             #pod
68             #pod This method acts just like the exported C function.
69             #pod
70             #pod =cut
71              
72             # ok:
73             # jn8
74             # jn8:32
75             # jn8:30-32
76             # jn8:25-28,30-32
77             # jn8:1,3-4,6
78              
79             sub _parse_ref {
80 9     9   16 my ($class, $ref_string) = @_;
81 9         39 my $range_regex = qr/\d+(?::(?:\d[-,]?)+)?/;
82              
83 9         182 (my $book = $ref_string) =~ s/\s*($range_regex)\z//;
84 9         30 my $ranges = $1;
85              
86 9         57 return (book => $book, ranges => $ranges);
87             }
88              
89             sub new {
90 9     9 1 17 my ($class, $ref_string) = @_;
91              
92 9         27 my %bibref = $class->_parse_ref($ref_string);
93              
94 9         15 my $self;
95              
96 9 50       31 return unless $self->{book} = $class->canonicalize_book($bibref{book});
97              
98 9         45 bless $self => $class;
99              
100 9 50       29 return unless my $range = $self->_parse_ranges($bibref{ranges});
101              
102 9         31 $self->{chapter} = $range->{chapter};
103 9         17 $self->{ranges} = $range->{ranges};
104              
105 9 100       34 return unless $class->_validate_ranges(
106             $self->{book},
107             $self->{chapter},
108             $self->{ranges},
109             );
110              
111 7         32 return $self;
112             }
113              
114             sub _validate_ranges {
115 9     9   14 my ($class, $book, $chapter, $ranges) = @_;
116              
117 9         17 foreach my $range (@$ranges) {
118 14 100       40 return unless $class->validate_verse($book, $chapter, $range->[0]);
119 13 100       33 return unless $class->validate_verse($book, $chapter, $range->[1]);
120             }
121 7         21 return 1;
122             }
123              
124             sub _parse_ranges {
125 9     9   17 my ($self, $string) = @_;
126              
127 9         41 my ($chapter, $rest) = $string =~ /\A(\d+)(?::(.+))?\z/;
128              
129 9 50       20 return unless $chapter;
130 9 100       34 return { chapter => $string,
131             ranges => [[ 1, $book_chapters{$self->{book}}[$chapter - 1] ]] }
132             unless $rest;
133              
134 8         31 my @range_strings = split /,\s?/, $rest;
135              
136 8         11 my @range;
137              
138 8         11 for my $rs (@range_strings) {
139 13         46 my ($start, $end) = $rs =~ /\A(\d+)(?:-(\d+))?\z/;
140 13 50       28 return unless $start;
141 13 100       48 push @range, [ $start, (defined $end ? $end : $start) ];
142             }
143              
144 8         50 return { chapter => $chapter, ranges => \@range };
145             }
146              
147             #pod =method stringify
148             #pod
149             #pod $self->stringify
150             #pod
151             #pod This method returns a string representing the reference, using the canonical
152             #pod book name.
153             #pod
154             #pod =cut
155              
156             sub stringify {
157 7     7 1 1337 my ($self) = @_;
158 7         20 my $string = $self->{book}
159             . q{ }
160             . $self->{chapter};
161              
162 7 50       10 return unless @{ $self->{ranges} };
  7         22  
163              
164 14         31 $string .=
165 7         15 ':' . join(', ', map { $self->_stringify_range($_) } @{ $self->{ranges} })
  7         14  
166             ;
167             }
168              
169             sub _stringify_range {
170 15     15   22 my ($self, $range) = @_;
171              
172 15 100       17 map { $_->[0] == $_->[1] ? $_->[0] : "$_->[0]-$_->[1]" } $range
  15         115  
173             }
174              
175             sub _register_book_set {
176 2     2   5 my ($class, $package) = @_;
177              
178 2         10 my @books = $package->_books;
179 2         10 for my $book (@books) {
180 132         143 my $full = $book->{full};
181 132         261 $book_chapters{ $full } = $book->{verses};
182 132         163 $book_abbrev { $full } = $book->{abbreviations};
183 132         306 $book_short { $full } = $book->{short};
184             }
185             }
186              
187             #pod =method stringify_short
188             #pod
189             #pod my $str = $self->stringify_short
190             #pod
191             #pod This method returns a string representing the reference, using the short book
192             #pod name.
193             #pod
194             #pod In other words, John 8:32 would be Jn 8:32. All short forms should safely
195             #pod round-trip back via parsing.
196             #pod
197             #pod =cut
198              
199             sub stringify_short {
200 1     1 1 2 my ($self) = @_;
201              
202 1         5 my $string = $book_short{ $self->{book} }
203             . q{ }
204             . $self->{chapter};
205              
206 1 50       2 return unless @{ $self->{ranges} };
  1         7  
207              
208 1         3 $string .=
209 1         2 ':' . join(', ', map { $self->_stringify_range($_) } @{ $self->{ranges} })
  1         3  
210             ;
211             }
212              
213             __PACKAGE__->_register_book_set("Religion::Bible::Reference::Standard");
214              
215             #pod =method canonicalize_book
216             #pod
217             #pod my $book = $class->canonicalize_book($book_abbrev)
218             #pod
219             #pod If possible, this method returns the canonical name of the book whose
220             #pod abbreviation was passed.
221             #pod
222             #pod =cut
223              
224             # mdxi suggests that I could have a list of pre-limiting regex, something like
225             # this:
226             # [ qr/\A(?:1|First)/, [ '1 Kings', '1 Samuel' ...
227             # so that if a passed string matches the regex, it's only checked against those
228             # entries in the associated list; good idea, for future revision
229              
230             sub canonicalize_book {
231 9     9 1 15 my ($class, $book_abbrev) = @_;
232 9 50       32 return $book_abbrev if $book_abbrev{$book_abbrev};
233 9         85 my $lc_abbrev = lc($book_abbrev);
234 9         121 for my $book (keys %book_abbrev) {
235 207 50       398 return $book if lc($book) eq $lc_abbrev;
236 207         217 for (@{$book_abbrev{$book}}) {
  207         369  
237 318 100       415 if (ref $_) { return $book if $book_abbrev =~ m/$_/; }
  75 100       493  
238 243 50       591 else { return $book if $lc_abbrev eq lc($_); }
239             }
240             }
241 0         0 return;
242             }
243              
244             #pod =method validate_verse
245             #pod
246             #pod $class->validate_verse($book, $chapter, $verse)
247             #pod
248             #pod This method returns true if the given book, chapter, and verse exists;
249             #pod otherwise it returns false.
250             #pod
251             #pod =cut
252              
253             sub validate_verse {
254 27     27 1 34 my ($self, $book, $chapter, $verse) = @_;
255 27 50       57 return unless exists $book_chapters{$book};
256 27 100       94 return unless defined $book_chapters{$book}[$chapter - 1];
257 26 100       66 return unless $book_chapters{$book}[$chapter - 1] >= $verse;
258 25         70 return 1
259             }
260              
261             #pod =method iterator
262             #pod
263             #pod my $iterator = $bibref->iterator;
264             #pod
265             #pod while (my $verse = $iterator->next) {
266             #pod my $text = retrieve($verse);
267             #pod print "$text\n";
268             #pod }
269             #pod
270             #pod =cut
271              
272             sub iterator {
273 3     3 1 21 my ($self) = @_;
274              
275 3         6 my $iterator = {
276             book => $self->book,
277             chapter => $self->chapter,
278 3         10 ranges => [ @{ $self->ranges } ],
279             };
280              
281 3         16 bless $iterator => 'Religion::Bible::Reference::Iterator';
282             }
283              
284             package Religion::Bible::Reference::Iterator;
285             $Religion::Bible::Reference::Iterator::VERSION = '0.016';
286             sub next { ## no critic # honestly, next is a great method for an iterator
287 64     64   7089 my ($self) = @_;
288 64 100       57 return unless @{ $self->{ranges} };
  64         151  
289              
290 61   66     136 $self->{position} ||= $self->{ranges}[0][0];
291 61         69 my $position = $self->{position};
292              
293 61 100       108 if ($position == $self->{ranges}[0][1]) {
294 7         9 shift @{ $self->{ranges} };
  7         10  
295 7         13 undef $self->{position};
296             } else {
297 54         60 $self->{position}++;
298             }
299 61 100       150 return wantarray ? (@$self{qw(book chapter)}, $position) : $position;
300             }
301              
302             #pod =head1 TODO
303             #pod
304             #pod =for :list
305             #pod * allow L instead of registered abbrevs
306             #pod * clean up regex/lists
307             #pod * make public the interface to load modules of books and abbreviations
308             #pod * make an interface to unload modules
309             #pod
310             #pod =cut
311              
312             1;
313              
314             __END__