File Coverage

blib/lib/Business/ISMN.pm
Criterion Covered Total %
statement 120 151 79.4
branch 26 46 56.5
condition 8 18 44.4
subroutine 27 35 77.1
pod 17 17 100.0
total 198 267 74.1


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