9 use OpenILS::Utils::MFHD::Date;
11 use base 'MARC::Field';
16 my $class = ref($proto) || $proto;
18 my $last_enum = undef;
20 $self->{_mfhdc_ENUMS} = {};
21 $self->{_mfhdc_CHRONS} = {};
22 $self->{_mfhdc_PATTERN} = {};
23 $self->{_mfhdc_COPY} = undef;
24 $self->{_mfhdc_UNIT} = undef;
25 $self->{_mfhdc_COMPRESSIBLE} = 1; # until proven otherwise
27 foreach my $subfield ($self->subfields) {
28 my ($key, $val) = @$subfield;
31 } elsif ($key =~ /[a-h]/) {
32 # Enumeration Captions
33 $self->{_mfhdc_ENUMS}->{$key} = {CAPTION => $val,
41 } elsif ($key =~ /[i-m]/) {
43 $self->{_mfhdc_CHRONS}->{$key} = $val;
44 } elsif ($key eq 'u') {
45 # Bib units per next higher enumeration level
46 carp('$u specified for top-level enumeration')
47 unless defined($last_enum);
48 $self->{_mfhdc_ENUMS}->{$last_enum}->{COUNT} = $val;
49 } elsif ($key eq 'v') {
50 carp '$v specified for top-level enumeration'
51 unless defined($last_enum);
52 $self->{_mfhdc_ENUMS}->{$last_enum}->{RESTART} = ($val eq 'r');
53 } elsif ($key =~ /[npwz]/) {
54 # Publication Pattern info ('o' == type of unit, 'q'..'t' undefined)
55 $self->{_mfhdc_PATTERN}->{$key} = $val;
56 } elsif ($key =~ /x/) {
57 # Calendar change can have multiple comma-separated values
58 $self->{_mfhdc_PATTERN}->{x} = [split /,/, $val];
59 } elsif ($key eq 'y') {
60 $self->{_mfhdc_PATTERN}->{y} = {}
61 unless exists $self->{_mfhdc_PATTERN}->{y};
62 update_pattern($self, $val);
63 } elsif ($key eq 'o') {
65 $self->{_mfhdc_UNIT} = $val;
66 } elsif ($key eq 't') {
67 $self->{_mfhdc_COPY} = $val;
69 carp "Unknown caption subfield '$key'";
73 # subsequent levels of enumeration (primary and alternate)
74 # If an enumeration level doesn't document the number
75 # of "issues" per "volume", or whether numbering of issues
76 # restarts, then we can't compress.
77 foreach my $key ('b', 'c', 'd', 'e', 'f', 'h') {
78 if (exists $self->{_mfhdc_ENUMS}->{$key}) {
79 my $pattern = $self->{_mfhdc_ENUMS}->{$key};
80 if (!$pattern->{RESTART} || !$pattern->{COUNT}
81 || ($pattern->{COUNT} eq 'var')
82 || ($pattern->{COUNT} eq 'und')) {
83 $self->{_mfhdc_COMPRESSIBLE} = 0;
89 my $pat = $self->{_mfhdc_PATTERN};
91 # Sanity check publication frequency vs publication pattern:
92 # if the frequency is a number, then the pattern better
93 # have that number of values associated with it.
94 if (exists($pat->{w}) && ($pat->{w} =~ /^\d+$/)
95 && ($pat->{w} != scalar(@{$pat->{y}->{p}}))) {
96 carp("Caption::new: publication frequency '$pat->{w}' != publication pattern @{$pat->{y}->{p}}");
100 # If there's a $x subfield and a $j, then it's compressible
101 if (exists $pat->{x} && exists $self->{_mfhdc_CHRONS}->{'j'}) {
102 $self->{_mfhdc_COMPRESSIBLE} = 1;
105 bless ($self, $class);
113 my $pathash = $self->{_mfhdc_PATTERN}->{y};
114 my ($pubcode, $pat) = unpack("a1a*", $val);
116 $pathash->{$pubcode} = [] unless exists $pathash->{$pubcode};
117 push @{$pathash->{$pubcode}}, $pat;
122 my $pattern = $self->{_mfhdc_PATTERN}->{y};
130 return $self->{_mfhdc_COMPRESSIBLE};
137 if (exists $self->{_mfhdc_CHRONS}->{$key}) {
138 return $self->{_mfhdc_CHRONS}->{$key};
148 if (exists $self->{_mfhdc_ENUMS}->{$key}) {
149 return $self->{_mfhdc_ENUMS}->{$key};
150 } elsif (exists $self->{_mfhdc_CHRONS}->{$key}) {
151 return $self->{_mfhdc_CHRONS}->{$key};
160 my $val = $self->capfield($key);
163 return $val->{CAPTION};
169 sub calendar_change {
172 return $self->{_mfhdc_PATTERN}->{x};
175 # If items are identified by chronology only, with no separate
176 # enumeration (eg, a newspaper issue), then the chronology is
177 # recorded in the enumeration subfields $a - $f. We can tell
178 # that this is the case if there are $a - $f subfields and no
179 # chronology subfields ($i-$k), and none of the $a-$f subfields
180 # have associated $u or $v subfields, but there's a $w and no $x
182 sub enumeration_is_chronology {
185 # There is always a '$a' subfield in well-formed fields.
186 return 0 if exists $self->{_mfhdc_CHRONS}->{i}
187 || exists $self->{_mfhdc_PATTERN}->{x};
189 foreach my $key ('a' .. 'f') {
192 last if !exists $self->{_mfhdc_ENUMS}->{$key};
194 $enum = $self->{_mfhdc_ENUMS}->{$key};
195 return 0 if defined $enum->{COUNT} || defined $enum->{RESTART};
198 return (exists $self->{_mfhdc_PATTERN}->{w});
201 sub regularity_match {
206 # we can't match something that doesn't exist.
207 return 0 if !exists $self->{_mfhdc_PATTERN}->{y}->{$pubcode};
209 foreach my $regularity (@{$self->{_mfhdc_PATTERN}->{y}->{$pubcode}}) {
210 my $chroncode= substr($regularity, 0, 1);
211 my $matchfunc = MFHD::Date::dispatch($chroncode);
212 my @pats = split(/,/, substr($regularity, 1));
214 if (!defined $matchfunc) {
215 carp "Unrecognized chroncode '$chroncode'";
220 foreach my $pat (@pats) {
221 $pat =~ s|/.+||; # If it's a combined date, match the start
222 if ($matchfunc->($pat, @date)) {
235 # printf("# is_omitted: testing date %s: %d\n", join('/', @date),
236 # $self->regularity_match('o', @date));
237 return $self->regularity_match('o', @date);
244 return $self->regularity_match('p', @date);
251 return $self->regularity_match('c', @date);
254 sub enum_is_combined {
256 my $subfield = shift;
258 my $level = ord($subfield) - ord('a') + 1;
260 return 0 if !exists $self->{_mfhdc_PATTERN}->{y}->{c};
262 foreach my $regularity (@{$self->{_mfhdc_PATTERN}->{y}->{c}}) {
263 next unless $regularity =~ m/^e$level/o;
265 my @pats = split(/,/, substr($regularity, 2));
267 foreach my $pat (@pats) {
268 $pat =~ s|/.+||; # if it's a combined issue, match the start
269 return 1 if ($iss eq $pat);
277 # Test to see if $m1/$d1 is on or after $m2/$d2
278 # if $d2 is undefined, test is based on just months
280 my ($m1, $d1, $m2, $d2) = @_;
283 || ($m1 == $m2 && ((!defined $d2) || ($d1 >= $d2))));
286 sub calendar_increment {
290 my $cal_change = $self->calendar_change;
296 # A calendar change is defined, need to check if it applies
297 if ((scalar(@new) == 2 && $new[1] > 20) || (scalar(@new) == 1)) {
298 carp "Can't calculate date change for ", $self->as_string;
302 foreach my $change (@{$cal_change}) {
305 if (length($change) == 2) {
307 } elsif (length($change) == 4) {
308 ($month, $day) = unpack("a2a2", $change);
311 if ($cur->[0] == $new[0]) {
312 # Same year, so a 'simple' month/day comparison will be fine
313 $incr = (!on_or_after($cur->[1], $cur->[2], $month, $day)
314 && on_or_after($new[1], $new[2], $month, $day));
316 # @cur is in the year before @new. There are
317 # two possible cases for the calendar change date that
318 # indicate that it's time to change the volume:
319 # (1) the change date is AFTER @cur in the year, or
320 # (2) the change date is BEFORE @new in the year.
322 # -------|------|------X------|------|
323 # @cur (1) Jan 1 (2) @new
325 $incr = (on_or_after($new[1], $new[2], $month, $day)
326 || !on_or_after($cur->[1], $cur->[2], $month, $day));
328 return $incr if $incr;
342 my $reg = $self->{_mfhdc_REGULARITY};
343 my $pattern = $self->{_mfhdc_PATTERN};
344 my $freq = $pattern->{w};
346 foreach my $i (0..$#keys) {
347 $cur[$i] = $next->{$keys[$i]} if exists $next->{$keys[$i]};
350 # If the current issue has a combined date (eg, May/June)
351 # get rid of the first date and base the calculation
352 # on the final date in the combined issue.
353 $cur[-1] =~ s|^[^/]+/||;
355 if (defined $pattern->{y}->{p}) {
356 # There is a $y publication pattern defined in the record:
357 # use it to calculate the next issue date.
359 # XXX TODO: need to handle combined issues.
360 foreach my $pubpat (@{$pattern->{y}->{p}}) {
361 my $chroncode = substr($pubpat, 0, 1);
362 my $genfunc = MFHD::Date::generator($chroncode);
363 my @pats = split(/,/, substr($pubpat, 1));
365 if (!defined $genfunc) {
366 carp "Unrecognized chroncode '$chroncode'";
370 foreach my $pat (@pats) {
371 @candidate = $genfunc->($pat, @cur);
372 while ($self->is_omitted(@candidate)) {
373 # printf("# pubpat omitting date '%s'\n",
374 # join('/', @candidate));
375 @candidate = $genfunc->($pat, @candidate);
378 # printf("# testing candidate date '%s'\n", join('/', @candidate));
379 if (!defined($new[0])
380 || !on_or_after($candidate[0], $candidate[1], $new[0], $new[1])) {
381 # first time through the loop
382 # or @candidate is before @new => @candidate is the next
385 # printf("# selecting candidate date '%s'\n", join('/', @new));
390 # There is no $y publication pattern defined, so use
391 # the $w frequency to figure out the next date
393 if (!defined($freq)) {
394 carp "Undefined frequency in next_date!";
395 } elsif (!MFHD::Date::can_increment($freq)) {
396 carp "Don't know how to deal with frequency '$freq'!";
399 # One of the standard defined issue frequencies
401 @new = MFHD::Date::incr_date($freq, @cur);
403 while ($self->is_omitted(@new)) {
404 @new = MFHD::Date::incr_date($freq, @new);
407 if ($self->is_combined(@new)) {
408 my @second_date = MFHD::Date::incr_date($freq, @new);
410 # I am cheating: This code assumes that only the smallest
411 # time increment is combined. So, no "Apr 15/May 1" allowed.
412 $new[-1] = $new[-1] . '/' . $second_date[-1];
417 for my $i (0..$#new) {
418 $next->{$keys[$i]} = $new[$i];
420 # Figure out if we need to adust volume number
421 # right now just use the $carry that was passed in.
422 # in long run, need to base this on ($carry or date_change)
424 # if $carry is set, the date doesn't matter: we're not
425 # going to increment the v. number twice at year-change.
426 $next->{a} += $carry;
427 } elsif (defined $pattern->{x}) {
428 $next->{a} += $self->calendar_increment(\@cur, @new);
436 # First handle any "alternative enumeration", since they're
437 # a lot simpler, and don't depend on the the calendar
438 foreach my $key ('h', 'g') {
439 next if !exists $next->{$key};
440 if (!$self->capstr($key)) {
441 warn "Holding data exists for $key, but no caption specified";
446 my $cap = $self->capfield($key);
447 if ($cap->{RESTART} && $cap->{COUNT}
448 && ($next->{$key} == $cap->{COUNT})) {
462 # $carry keeps track of whether we need to carry into the next
463 # higher level of enumeration. It's not actually necessary except
464 # for when the loop ends: if we need to carry from $b into $a
465 # then $carry will be set when the loop ends.
467 # We need to keep track of this because there are two different
468 # reasons why we might increment the highest level of enumeration ($a)
469 # 1) we hit the correct number of items in $b (ie, 5th iss of quarterly)
470 # 2) it's the right time of the year.
473 foreach my $key (reverse('b'..'f')) {
474 next if !exists $next->{$key};
476 if (!$self->capstr($key)) {
477 # Just assume that it increments continuously and give up
478 warn "Holding data exists for $key, but no caption specified";
484 # If the current issue has a combined issue number (eg, 2/3)
485 # get rid of the first issue number and base the calculation
486 # on the final issue number in the combined issue.
487 if ($next->{$key} =~ m|/|) {
488 $next->{$key} =~ s|^[^/]+/||;
491 my $cap = $self->capfield($key);
492 if ($cap->{RESTART} && $cap->{COUNT}
493 && ($next->{$key} eq $cap->{COUNT})) {
497 # If I don't need to "carry" beyond here, then I just increment
498 # this level of the enumeration and stop looping, since the
499 # "next" hash has been initialized with the current values
505 # You can't have a combined issue that spans two volumes: no.12/1
507 if ($self->enum_is_combined($key, $next->{$key})) {
508 $next->{$key} .= '/' . ($next->{$key} + 1);
514 # The easy part is done. There are two things left to do:
515 # 1) Calculate the date of the next issue, if necessary
516 # 2) Increment the highest level of enumeration (either by date
517 # or because $carry is set because of the above loop
519 if (!$self->subfield('i')) {
520 # The simple case: if there is no chronology specified
521 # then just check $carry and return
522 $next->{'a'} += $carry;
524 # Figure out date of next issue, then decide if we need
525 # to adjust top level enumeration based on that
526 $self->next_date($next, $carry, ('i'..'m'));
535 # Initialize $next with current enumeration & chronology, then
536 # we can just operate on $next, based on the contents of the caption
538 if ($self->enumeration_is_chronology) {
539 foreach my $key ('a' .. 'h') {
540 $next->{$key} = $holding->{_mfhdh_SUBFIELDS}->{$key}
541 if defined $holding->{_mfhdh_SUBFIELDS}->{$key};
543 $self->next_date($next, 0, ('a' .. 'h'));
548 foreach my $key ('a' .. 'h') {
549 $next->{$key} = $holding->{_mfhdh_SUBFIELDS}->{$key}->{HOLDINGS}
550 if defined $holding->{_mfhdh_SUBFIELDS}->{$key};
553 foreach my $key ('i'..'m') {
554 $next->{$key} = $holding->{_mfhdh_SUBFIELDS}->{$key}
555 if defined $holding->{_mfhdh_SUBFIELDS}->{$key};
558 if (exists $next->{'h'}) {
559 $self->next_alt_enum($next);
562 $self->next_enum($next);