File Coverage

blib/lib/Business/ISMN.pm
Criterion Covered Total %
statement 122 153 79.7
branch 26 46 56.5
condition 8 18 44.4
subroutine 28 36 77.7
pod 17 17 100.0
total 201 270 74.4


line stmt bran cond sub pod time code
1 2     2   71976 use 5.008;
  2         8  
2              
3             package Business::ISMN;
4 2     2   12 use strict;
  2         3  
  2         57  
5              
6 2         10 use subs qw(
7             _common_format _checksum is_valid_checksum
8             INVALID_PUBLISHER_CODE
9             BAD_CHECKSUM
10             GOOD_ISMN
11             BAD_ISMN
12 2     2   1150 );
  2         47  
13 2     2   173 use vars qw( $debug %country_data $MAX_COUNTRY_CODE_LENGTH );
  2         5  
  2         152  
14              
15 2     2   14 use Carp qw(carp);
  2         5  
  2         95  
16 2     2   11 use Exporter qw(import);
  2         3  
  2         74  
17 2     2   14 use List::Util qw(sum);
  2         4  
  2         208  
18 2     2   951 use Tie::Cycle;
  2         1468  
  2         60  
19 2     2   841 use Business::ISMN::Data;
  2         4  
  2         1799  
20              
21             my $debug = 0;
22              
23             our @EXPORT_OK = qw(is_valid_checksum ean_to_ismn ismn_to_ean
24             INVALID_PUBLISHER_CODE BAD_CHECKSUM GOOD_ISMN BAD_ISMN);
25              
26             our $VERSION = '1.202';
27              
28 62     62   97 sub INVALID_PUBLISHER_CODE { -3 };
29 64     64   98 sub BAD_CHECKSUM { -1 };
30 183     183   455 sub GOOD_ISMN { 1 };
31 10     10   33 sub BAD_ISMN { 0 };
32              
33             my %Lengths = qw(
34             0 3
35             1 4
36             2 4
37             3 4
38             4 5
39             5 5
40             6 5
41             7 6
42             8 6
43             9 7
44             );
45              
46             sub new {
47 61     61 1 705 my $class = shift;
48 61         111 my $common_data = _common_format shift;
49              
50 61 50       130 return unless defined $common_data;
51              
52 61         101 my $self = {};
53 61         98 bless $self, $class;
54              
55 61         114 $self->{'ismn'} = $common_data;
56 61         113 $self->{'positions'} = [1,undef,9];
57              
58             # we don't know if we have a valid publisher code,
59             # so let's assume we don't
60 61         101 $self->{'valid'} = INVALID_PUBLISHER_CODE;
61              
62             # let's check the publisher code.
63 61         131 my $code_length = $Lengths{ substr( $self->{'ismn'}, 1, 1 ) };
64             $self->{publisher_code} = substr(
65 61         126 $self->{'ismn'},
66             1,
67             $code_length
68             );
69              
70 61         80 my $code_end = $code_length + 1;
71              
72 61         90 $self->{'positions'}[1] = $code_end;
73              
74 61 100       107 return $self unless $self->is_valid_country_code;
75              
76             # we have a good publisher code, so
77             # assume we have a bad checksum until we check
78 60         109 $self->{'valid'} = BAD_CHECKSUM;
79              
80 60         126 $self->{'article_code'} = substr( $self->{'ismn'}, $code_end, 9 - $code_end );
81 60         119 $self->{'checksum'} = substr( $self->{'ismn'}, -1, 1 );
82              
83 60         104 $self->{'valid'} = is_valid_checksum( $self->{'ismn'} );
84              
85 60         132 return $self;
86             }
87              
88              
89             #it's your fault if you muck with the internals yourself
90             # none of these take arguments
91 4     4 1 7 sub ismn () { my $self = shift; return $self->{'ismn'} }
  4         15  
92 64     64 1 1508 sub is_valid () { my $self = shift; return $self->{'valid'} }
  64         114  
93 1     1 1 3 sub country () { my $self = shift; return $self->{'country'} }
  1         6  
94 0     0 1 0 sub publisher () { carp "publisher is deprecated. Use country instead."; &country }
  0         0  
95 62     62 1 86 sub publisher_code () { my $self = shift; return $self->{'publisher_code'} }
  62         114  
96 0     0 1 0 sub article_code () { my $self = shift; return $self->{'article_code'} }
  0         0  
97 0     0 1 0 sub checksum () { my $self = shift; return $self->{'checksum'} }
  0         0  
98 0     0 1 0 sub hyphen_positions () { my $self = shift; return @{$self->{'positions'}} }
  0         0  
  0         0  
99              
100              
101             sub fix_checksum {
102 1     1 1 2 my $self = shift;
103              
104 1         4 my $last_char = substr($self->{'ismn'}, 9, 1);
105 1         3 my $checksum = _checksum $self->ismn;
106              
107 1         4 substr($self->{'ismn'}, 9, 1) = $checksum;
108              
109 1         4 $self->_check_validity;
110              
111 1 50       3 return 0 if $last_char eq $checksum;
112 1         2 return 1;
113             }
114              
115             sub as_string {
116 3     3 1 6 my $self = shift;
117 3         5 my $array_ref = shift;
118              
119             #this allows one to override the positions settings from the
120             #constructor
121 3 100       9 $array_ref = $self->{'positions'} unless ref $array_ref eq 'ARRAY';
122              
123 3 50       8 return unless $self->is_valid eq GOOD_ISMN;
124 3         8 my $ismn = $self->ismn;
125              
126 3         9 foreach my $position ( sort { $b <=> $a } @$array_ref )
  2         6  
127             {
128 3 50 33     11 next if $position > 9 or $position < 1;
129 3         8 substr($ismn, $position, 0) = '-';
130             }
131              
132 3         13 return $ismn;
133             }
134              
135             sub as_ean {
136 1     1 1 6 my $self = shift;
137              
138 1 50       5 my $ismn = ref $self ? $self->as_string([]) : _common_format $self;
139              
140 1 50 33     9 return unless ( defined $ismn and length $ismn == 10 );
141              
142             # the M becomes a zero in bookland
143 1         4 substr( $ismn, 0, 1 ) = '0';
144              
145 1         4 my $ean = '979' . substr($ismn, 0, 9);
146              
147 1         2 my $sum = 0;
148 1         3 foreach my $index ( 0, 2, 4, 6, 8, 10 ) {
149 6         9 $sum += substr($ean, $index, 1);
150 6         11 $sum += 3 * substr($ean, $index + 1, 1);
151             }
152              
153             #take the next higher multiple of 10 and subtract the sum.
154             #if $sum is 37, the next highest multiple of ten is 40. the
155             #check digit would be 40 - 37 => 3.
156 1         6 $ean .= ( 10 * ( int( $sum / 10 ) + 1 ) - $sum ) % 10;
157              
158 1         6 return $ean;
159             }
160              
161             sub is_valid_country_code {
162 61     61 1 90 my $self = shift;
163 61         96 my $code = $self->publisher_code;
164              
165 61         87 my $success = 0;
166              
167 61         110 foreach my $tuple ( @publisher_tuples ) {
168 2     2   17 no warnings;
  2         6  
  2         1376  
169 1778 100 100     4423 next if( defined $tuple->[2] and $code > $tuple->[2] );
170 458 100       726 last if $code < $tuple->[1];
171 457 100 66     1193 if( $code >= $tuple->[1] and $code <= $tuple->[2] ) {
172 60         67 $success = 1;
173 60         116 $self->{'country'} = $tuple->[0];
174 60         73 last;
175             }
176             }
177              
178 61         126 return $success;
179             }
180              
181             sub is_valid_publisher_code {
182 0     0 1 0 carp "is_valid_publisher_code is deprecated. Use is_valid_country_code";
183 0         0 &is_valid_country_code
184             }
185              
186             sub is_valid_checksum {
187 68     68   662 my $data = _common_format shift;
188              
189 68 100       155 return BAD_ISMN unless defined $data;
190              
191 62 100       113 return GOOD_ISMN if substr($data, 9, 1) eq _checksum $data;
192              
193 2         6 return BAD_CHECKSUM;
194             }
195              
196             sub ean_to_ismn {
197 0     0 1 0 my $ean = shift;
198              
199 0         0 $ean =~ s/[^0-9]//g;
200              
201 0 0       0 return unless length $ean == 13;
202 0 0       0 return unless substr($ean, 0, 3) eq 979;
203              
204             #XXX: fix to change leading 0 back to M
205 0         0 my $ismn = Business::ISMN->new( substr($ean, 3, 9) . '1' );
206              
207 0         0 $ismn->fix_checksum;
208              
209 0 0       0 return $ismn->as_string([]) if $ismn->is_valid;
210              
211 0         0 return;
212             }
213              
214              
215             sub ismn_to_ean {
216 0     0 1 0 my $ismn = _common_format shift;
217              
218 0 0 0     0 return unless (defined $ismn and is_valid_checksum($ismn) eq GOOD_ISMN);
219              
220 0         0 return as_ean($ismn);
221             }
222              
223             sub png_barcode {
224 0     0 1 0 my $self = shift;
225              
226 0         0 my $ean = ismn_to_ean( $self->as_string([]) );
227              
228 0         0 eval "use GD::Barcode::EAN13";
229 0 0       0 if( $@ ) {
230 0         0 carp "GD::Barcode::EAN13 required to make PNG barcodes";
231 0         0 return;
232             }
233              
234 0         0 my $image = GD::Barcode::EAN13->new($ean)->plot->png;
235              
236 0         0 return $image;
237             }
238              
239             #internal function. you don't get to use this one.
240             sub _check_validity {
241 1     1   2 my $self = shift;
242              
243 1 50 33     4 if( is_valid_checksum $self->{'ismn'} eq GOOD_ISMN
244             and defined $self->{'publisher_code'} ) {
245 0         0 $self->{'valid'} = GOOD_ISMN;
246             }
247             else {
248             $self->{'valid'} = INVALID_PUBLISHER_CODE
249 1 50       4 unless defined $self->{'publisher_code'};
250             $self->{'valid'} = GOOD_ISMN
251 1 50       4 unless is_valid_checksum $self->{'ismn'} ne GOOD_ISMN;
252             }
253             }
254              
255             #internal function. you don't get to use this one.
256             sub _checksum {
257 63     63   103 my $data = _common_format shift;
258              
259 63         267 tie my $factor, 'Tie::Cycle', [ 1, 3 ];
260 63 50       1334 return unless defined $data;
261              
262 63         80 my $sum = 9;
263              
264 63         248 foreach my $digit ( split //, substr( $data, 1, 8 ) ) {
265 504         923 my $mult = $factor;
266 504         5511 $sum += $digit * $mult;
267             }
268              
269             #return what the check digit should be
270             # the extra mod 10 turns 10 into 0.
271 63         134 my $checksum = ( 10 - ($sum % 10) ) % 10;
272              
273 63         235 return $checksum;
274             }
275              
276             #internal function. you don't get to use this one.
277             sub _common_format {
278 2     2   16 no warnings qw(uninitialized);
  2         4  
  2         273  
279             #we want uppercase X's
280 192     192   345 my $data = uc shift;
281              
282             # get rid of everything except decimal digits and X
283             # and leading M
284 192         490 $data =~ s/[^0-9M]//g;
285              
286 192 100       794 return $1 if $data =~ m/
287             ^
288             (
289             M
290             \d{9}
291             )
292             $
293             /x;
294              
295 6         10 return;
296             }
297              
298             1;
299              
300             __END__