10 use base 'MARC::Field';
15 my $class = ref($proto) || $proto;
17 my $last_enum = undef;
19 $self->{_mfhdc_ENUMS} = {};
20 $self->{_mfhdc_CHRONS} = {};
21 $self->{_mfhdc_PATTERN} = {};
22 $self->{_mfhdc_COPY} = undef;
23 $self->{_mfhdc_UNIT} = undef;
24 $self->{_mfhdc_COMPRESSIBLE} = 1; # until proven otherwise
26 foreach my $subfield ($self->subfields) {
27 my ($key, $val) = @$subfield;
30 } elsif ($key =~ /[a-h]/) {
31 # Enumeration Captions
32 $self->{_mfhdc_ENUMS}->{$key} = {CAPTION => $val,
40 } elsif ($key =~ /[i-m]/) {
42 $self->{_mfhdc_CHRONS}->{$key} = $val;
43 } elsif ($key eq 'u') {
44 # Bib units per next higher enumeration level
45 carp('$u specified for top-level enumeration')
46 unless defined($last_enum);
47 $self->{_mfhdc_ENUMS}->{$last_enum}->{COUNT} = $val;
48 } elsif ($key eq 'v') {
49 carp '$v specified for top-level enumeration'
50 unless defined($last_enum);
51 $self->{_mfhdc_ENUMS}->{$last_enum}->{RESTART} = ($val eq 'r');
52 } elsif ($key =~ /[npwz]/) {
53 # Publication Pattern info ('o' == type of unit, 'q'..'t' undefined)
54 $self->{_mfhdc_PATTERN}->{$key} = $val;
55 } elsif ($key =~ /x/) {
56 # Calendar change can have multiple comma-separated values
57 $self->{_mfhdc_PATTERN}->{x} = [split /,/, $val];
58 } elsif ($key eq 'y') {
59 $self->{_mfhdc_PATTERN}->{y} = []
60 unless exists $self->{_mfhdc_PATTERN}->{y};
61 push @{$self->{_mfhdc_PATTERN}->{y}}, $val;
62 } elsif ($key eq 'o') {
64 $self->{_mfhdc_UNIT} = $val;
65 } elsif ($key eq 't') {
66 $self->{_mfhdc_COPY} = $val;
68 carp "Unknown caption subfield '$key'";
72 # subsequent levels of enumeration (primary and alternate)
73 # If an enumeration level doesn't document the number
74 # of "issues" per "volume", or whether numbering of issues
75 # restarts, then we can't compress.
76 foreach my $key ('b', 'c', 'd', 'e', 'f', 'h') {
77 if (exists $self->{_mfhdc_ENUMS}->{$key}) {
78 my $pattern = $self->{_mfhdc_ENUMS}->{$key};
79 if (!$pattern->{RESTART} || !$pattern->{COUNT}
80 || ($pattern->{COUNT} eq 'var')
81 || ($pattern->{COUNT} eq 'und')) {
82 $self->{_mfhdc_COMPRESSIBLE} = 0;
88 # If there's a $x subfield and a $j, then it's compressible
89 if (exists $self->{_mfhdc_PATTERN}->{x} && exists $self->{_mfhdc_CHRONS}->{'j'}) {
90 $self->{_mfhdc_COMPRESSIBLE} = 1;
93 bless ($self, $class);
95 if (exists $self->{_mfhdc_PATTERN}->{y}) {
96 $self->decode_pattern;
104 my $pattern = $self->{_mfhdc_PATTERN}->{y};
112 return $self->{_mfhdc_COMPRESSIBLE};
119 if (exists $self->{_mfhdc_CHRONS}->{$key}) {
120 return $self->{_mfhdc_CHRONS}->{$key};
130 if (exists $self->{_mfhdc_ENUMS}->{$key}) {
131 return $self->{_mfhdc_ENUMS}->{$key};
132 } elsif (exists $self->{_mfhdc_CHRONS}->{$key}) {
133 return $self->{_mfhdc_CHRONS}->{$key};
142 my $val = $self->capfield($key);
145 return $val->{CAPTION};
151 sub calendar_change {
154 return $self->{_mfhdc_PATTERN}->{x};
157 # If items are identified by chronology only, with no separate
158 # enumeration (eg, a newspaper issue), then the chronology is
159 # recorded in the enumeration subfields $a - $f. We can tell
160 # that this is the case if there are $a - $f subfields and no
161 # chronology subfields ($i-$k), and none of the $a-$f subfields
162 # have associated $u or $v subfields, but there's a $w and no $x
164 sub enumeration_is_chronology {
167 # There is always a '$a' subfield in well-formed fields.
168 return 0 if exists $self->{_mfhdc_CHRONS}->{i}
169 || exists $self->{_mfhdc_PATTERN}->{x};
171 foreach my $key ('a' .. 'f') {
174 last if !exists $self->{_mfhdc_ENUMS}->{$key};
176 $enum = $self->{_mfhdc_ENUMS}->{$key};
177 return 0 if defined $enum->{COUNT} || defined $enum->{RESTART};
180 return (exists $self->{_mfhdc_PATTERN}->{w});
193 my $daypat = '(mo|tu|we|th|fr|sa|su)';
194 my $weekpat = '(99|98|97|00|01|02|03|04|05)';
196 my $monthpat = '(01|02|03|04|05|06|07|08|09|10|11|12)';
197 my $seasonpat = '(21|22|23|24)';
199 # Initialize $weeknopat to be '(01|02|03|...|51|52|53)'
201 foreach my $weekno (1..52) {
202 $weeknopat .= sprintf('%02d|', $weekno);
209 # Translate daynames into day of week for DateTime
210 # also used to check if dayname is valid.
212 if (exists $daynames{$pat}) {
214 # figure out day of week for date and compare
215 my $dt = DateTime->new(year => $date[0],
218 return ($dt->day_of_week == $daynames{$pat});
219 } elsif (length($pat) == 2) {
221 return $pat == $date[3];
222 } elsif (length($pat) == 4) {
225 $mon = substr($pat, 0, 2);
226 $day = substr($pat, 2, 2);
228 return (($mon == $date[1]) && ($day == $date[2]));
230 carp "Invalid day pattern '$pat'";
235 # Calcuate date of "n"th last "dayname" of month: second last Tuesday
236 sub last_week_of_month {
240 my $end_dt = DateTime->last_day_of_month(year => $dt->year,
241 month => $dt->month);
243 $day = $daynames{$day};
244 while ($end_dt->day_of_week != $day) {
245 $end_dt->subtract(days => 1);
248 # 99: last week of month, 98: second last, etc.
249 for (my $i = 99 - $week; $i > 0; $i--) {
250 $end_dt->subtract(weeks => 1);
264 return (($dt->month == $month)
265 && (($dt->week_of_month == $weekno)
266 || ($dt->week_of_month == last_day_of_month($dt, $weekno, 'th')->week_of_month)));
270 if ($daynames{$day} != $dt->day_of_week) {
271 # if it's the wrong day of the week, rest doesn't matter
275 if (!defined $month) {
277 return (($dt->weekday_of_month == $weekno)
278 || ($dt->weekday_of_month == last_day_of_month($dt, $weekno, $day)->weekday_of_month));
282 if ($month != $dt->month) {
283 # If it's the wrong month, then we're done
287 # It's the right day of the week
288 # It's the right month
290 if ($weekno == $dt->weekday_of_month) {
291 # If this matches, then we're counting from the beginning
292 # of the month and it matches and we're done.
296 # only case left is that the week number is counting from
297 # the end of the month: eg, second last wednesday
298 return (last_week_of_month($weekno, $day)->weekday_of_month == $dt->weekday_of_month);
304 my $dt = DateTime->new(year => $date[0],
308 if ($pat =~ m/^$weekpat$daypat$/) {
309 # WWdd: 03we = Third Wednesday
310 return check_date($dt, undef, $1, $2);
311 } elsif ($pat =~ m/^$monthpat$weekpat$daypat$/) {
312 # MMWWdd: 0599tu Last Tuesday in May XXX WRITE ME
313 return check_date($dt, $1, $2, $3);
314 } elsif ($pat =~ m/^$monthpat$weekpat$/) {
315 # MMWW: 1204: Fourth week in December XXX WRITE ME
316 return check_date($dt, $1, $2, undef);
318 carp "invalid week pattern '$pat'";
327 return ($pat eq $date[1]);
334 return ($pat eq $date[1]);
349 # We handle enumeration patterns separately. This just
350 # ensures that when we're processing chronological patterns
351 # we don't match an enumeration pattern.
357 'e' => \&match_issue, # not really a "chron" code
359 'm' => \&match_month,
360 's' => \&match_season,
364 sub regularity_match {
369 # we can't match something that doesn't exist.
370 return 0 if !exists $self->{_mfhdc_PATTERN}->{y};
372 foreach my $regularity (@{$self->{_mfhdc_PATTERN}->{y}}) {
373 next unless $regularity =~ m/^$pubcode/;
375 my $chroncode= substr($regularity, 1, 1);
376 my @pats = split(/,/, substr($regularity, 2));
378 if (!exists $dispatch{$chroncode}) {
379 carp "Unrecognized chroncode '$chroncode'";
384 foreach my $pat (@pats) {
385 $pat =~ s|/.+||; # If it's a combined date, match the start
386 if ($dispatch{$chroncode}->($pat, @date)) {
399 return $self->regularity_match('o', @date);
406 return $self->regularity_match('p', @date);
413 return $self->regularity_match('c', @date);
416 sub enum_is_combined {
418 my $subfield = shift;
420 my $level = ord($subfield) - ord('a') + 1;
422 return 0 if !exists $self->{_mfhdc_PATTERN}->{y};
424 foreach my $regularity (@{$self->{_mfhdc_PATTERN}->{y}}) {
425 next unless $regularity =~ m/^ce$level/o;
427 my @pats = split(/,/, substr($regularity, 3));
429 foreach my $pat (@pats) {
430 $pat =~ s|/.+||; # if it's a combined issue, match the start
431 return 1 if ($iss eq $pat);
440 a => {years => 1}, # annual
441 b => {months => 2}, # bimonthly
442 c => {days => 3}, # semiweekly
443 d => {days => 1}, # daily
444 e => {weeks => 2}, # biweekly
445 f => {months => 6}, # semiannual
446 g => {years => 2}, # biennial
447 h => {years => 3}, # triennial
448 i => {days => 2}, # three times / week
449 j => {days => 10}, # three times /month
451 m => {months => 1}, # monthly
452 q => {months => 3}, # quarterly
453 s => {days => 15}, # semimonthly
454 t => {months => 4}, # three times / year
455 w => {weeks => 1}, # weekly
456 # x => completely irregular
463 if (scalar(@new) == 1) {
464 # only a year is specified. Next date is easy
465 $new[0] += $incr->{years} || 1;
466 } elsif (scalar(@new) == 2) {
467 # Year and month or season
470 $new[1] += ($incr->{months}/3) || 1;
474 $new[1] -= 4; # 25 - 4 == 21 == Spring after Winter
478 $new[1] += $incr->{months} || 1;
484 $new[1] = '0' . $new[1] if ($new[1] < 10);
486 } elsif (scalar(@new) == 3) {
487 # Year, Month, Day: now it gets complicated.
489 if ($new[2] =~ /^[0-9]+$/) {
490 # A single number for the day of month, relatively simple
491 my $dt = DateTime->new(year => $new[0],
496 $new[1] = $dt->month;
499 $new[1] = '0' . $new[1] if ($new[1] < 10);
500 $new[2] = '0' . $new[2] if ($new[2] < 10);
502 warn("Don't know how to cope with @new");
508 # Test to see if $m1/$d1 is on or after $m2/$d2
509 # if $d2 is undefined, test is based on just months
511 my ($m1, $d1, $m2, $d2) = @_;
514 || ($m1 == $m2 && ((!defined $d2) || ($d1 >= $d2))));
517 sub calendar_increment {
521 my $cal_change = $self->calendar_change;
527 # A calendar change is defined, need to check if it applies
528 if ((scalar(@new) == 2 && $new[1] > 20) || (scalar(@new) == 1)) {
529 carp "Can't calculate date change for ", $self->as_string;
533 foreach my $change (@{$cal_change}) {
536 if (length($change) == 2) {
538 } elsif (length($change) == 4) {
539 ($month, $day) = unpack("a2a2", $change);
542 if ($cur->[0] == $new[0]) {
543 # Same year, so a 'simple' month/day comparison will be fine
544 $incr = (!on_or_after($cur->[1], $cur->[2], $month, $day)
545 && on_or_after($new[1], $new[2], $month, $day));
547 # @cur is in the year before @new. There are
548 # two possible cases for the calendar change date that
549 # indicate that it's time to change the volume:
550 # (1) the change date is AFTER @cur in the year, or
551 # (2) the change date is BEFORE @new in the year.
553 # -------|------|------X------|------|
554 # @cur (1) Jan 1 (2) @new
556 $incr = (on_or_after($new[1], $new[2], $month, $day)
557 || !on_or_after($cur->[1], $cur->[2], $month, $day));
559 return $incr if $incr;
572 my $reg = $self->{_mfhdc_REGULARITY};
573 my $pattern = $self->{_mfhdc_PATTERN};
574 my $freq = $pattern->{w};
576 foreach my $i (0..$#keys) {
577 $new[$i] = $cur[$i] = $next->{$keys[$i]} if exists $next->{$keys[$i]};
580 # If the current issue has a combined date (eg, May/June)
581 # get rid of the first date and base the calculation
582 # on the final date in the combined issue.
583 $new[-1] =~ s|^[^/]+/||;
585 # If $frequency is not one of the standard codes defined in %increments
586 # then there has to be a $yp publication regularity pattern that
587 # lists the dates of publication. Use that that list to find the next
588 # date following the current one.
589 # XXX: the code doesn't handle this case yet.
590 if (!defined($freq)) {
591 carp "Undefined frequency in next_date!";
592 } elsif (!exists $increments{$freq}) {
593 carp "Don't know how to deal with frequency '$freq'!";
596 # One of the standard defined issue frequencies
598 @new = incr_date($increments{$freq}, @new);
600 while ($self->is_omitted(@new)) {
601 @new = incr_date($increments{$freq}, @new);
604 if ($self->is_combined(@new)) {
605 my @second_date = incr_date($increments{$freq}, @new);
607 # I am cheating: This code assumes that only the smallest
608 # time increment is combined. So, no "Apr 15/May 1" allowed.
609 $new[-1] = $new[-1] . '/' . $second_date[-1];
613 for my $i (0..$#new) {
614 $next->{$keys[$i]} = $new[$i];
617 # Figure out if we need to adust volume number
618 # right now just use the $carry that was passed in.
619 # in long run, need to base this on ($carry or date_change)
621 # if $carry is set, the date doesn't matter: we're not
622 # going to increment the v. number twice at year-change.
623 $next->{a} += $carry;
624 } elsif (defined $self->{_mfhdc_PATTERN}->{x}) {
625 $next->{a} += $self->calendar_increment(\@cur, @new);
633 # First handle any "alternative enumeration", since they're
634 # a lot simpler, and don't depend on the the calendar
635 foreach my $key ('h', 'g') {
636 next if !exists $next->{$key};
637 if (!$self->capstr($key)) {
638 warn "Holding data exists for $key, but no caption specified";
643 my $cap = $self->capfield($key);
644 if ($cap->{RESTART} && $cap->{COUNT}
645 && ($next->{$key} == $cap->{COUNT})) {
659 # $carry keeps track of whether we need to carry into the next
660 # higher level of enumeration. It's not actually necessary except
661 # for when the loop ends: if we need to carry from $b into $a
662 # then $carry will be set when the loop ends.
664 # We need to keep track of this because there are two different
665 # reasons why we might increment the highest level of enumeration ($a)
666 # 1) we hit the correct number of items in $b (ie, 5th iss of quarterly)
667 # 2) it's the right time of the year.
670 foreach my $key (reverse('b'..'f')) {
671 next if !exists $next->{$key};
673 if (!$self->capstr($key)) {
674 # Just assume that it increments continuously and give up
675 warn "Holding data exists for $key, but no caption specified";
681 # If the current issue has a combined issue number (eg, 2/3)
682 # get rid of the first issue number and base the calculation
683 # on the final issue number in the combined issue.
684 if ($next->{$key} =~ m|/|) {
685 $next->{$key} =~ s|^[^/]+/||;
688 my $cap = $self->capfield($key);
689 if ($cap->{RESTART} && $cap->{COUNT}
690 && ($next->{$key} eq $cap->{COUNT})) {
694 # If I don't need to "carry" beyond here, then I just increment
695 # this level of the enumeration and stop looping, since the
696 # "next" hash has been initialized with the current values
702 # You can't have a combined issue that spans two volumes: no.12/1
704 if ($self->enum_is_combined($key, $next->{$key})) {
705 $next->{$key} .= '/' . ($next->{$key} + 1);
711 # The easy part is done. There are two things left to do:
712 # 1) Calculate the date of the next issue, if necessary
713 # 2) Increment the highest level of enumeration (either by date
714 # or because $carry is set because of the above loop
716 if (!$self->subfield('i')) {
717 # The simple case: if there is no chronology specified
718 # then just check $carry and return
719 $next->{'a'} += $carry;
721 # Figure out date of next issue, then decide if we need
722 # to adjust top level enumeration based on that
723 $self->next_date($next, $carry, ('i'..'m'));