]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/perlmods/lib/OpenILS/Utils/MFHD/Caption.pm
Relax MFHD subfield 'a' requirement for caption/patterns
[working/Evergreen.git] / Open-ILS / src / perlmods / lib / OpenILS / Utils / MFHD / Caption.pm
1 package MFHD::Caption;
2 use strict;
3 use integer;
4 use Carp;
5
6 use Data::Dumper;
7
8 use OpenILS::Utils::MFHD::Date;
9
10 use base 'MARC::Field';
11
12 sub new {
13     my $proto     = shift;
14     my $class     = ref($proto) || $proto;
15     my $self      = shift;
16     my $last_enum = undef;
17
18     $self->{_mfhdc_ENUMS}        = {};
19     $self->{_mfhdc_CHRONS}       = {};
20     $self->{_mfhdc_PATTERN}      = {};
21     $self->{_mfhdc_COPY}         = undef;
22     $self->{_mfhdc_UNIT}         = undef;
23     $self->{_mfhdc_LINK_ID}      = undef;
24     $self->{_mfhdc_COMPRESSIBLE} = 1;       # until proven otherwise
25
26     foreach my $subfield ($self->subfields) {
27         my ($key, $val) = @$subfield;
28         if ($key eq '8') {
29             $self->{_mfhdc_LINK_ID} = $val;
30         } elsif ($key =~ /[a-h]/) {
31             # Enumeration Captions
32             $self->{_mfhdc_ENUMS}->{$key} = {
33                 CAPTION => $val,
34                 COUNT   => undef,
35                 RESTART => undef
36             };
37             if ($key =~ /[ag]/) {
38                 $last_enum = undef;
39             } else {
40                 $last_enum = $key;
41             }
42         } elsif ($key =~ /[i-m]/) {
43             # Chronology captions
44             $self->{_mfhdc_CHRONS}->{$key} = $val;
45         } elsif ($key eq 'u') {
46             # Bib units per next higher enumeration level
47
48         # Some files seem to have "empty" $u subfields,
49         # especially for top level of enumeration. Just drop them
50         next if (!defined($val) || !$val);
51
52             carp('$u specified for top-level enumeration')
53               unless defined($last_enum);
54             $self->{_mfhdc_ENUMS}->{$last_enum}->{COUNT} = $val;
55         } elsif ($key eq 'v') {
56         # Is this level of enumeration continuous, or does it restart?
57
58         # Some files seem to have "empty" $v subfields,
59         # especially for top level of enumeration. Just drop them
60         next if (!defined($val) || !$val);
61
62             carp '$v specified for top-level enumeration'
63               unless defined($last_enum);
64             $self->{_mfhdc_ENUMS}->{$last_enum}->{RESTART} = ($val eq 'r');
65         } elsif ($key =~ /[npwz]/) {
66             # Publication Pattern info ('o' == type of unit, 'q'..'t' undefined)
67             $self->{_mfhdc_PATTERN}->{$key} = $val;
68         } elsif ($key =~ /x/) {
69             # Calendar change can have multiple comma-separated values
70             $self->{_mfhdc_PATTERN}->{x} = [split /,/, $val];
71         } elsif ($key eq 'y') {
72             $self->{_mfhdc_PATTERN}->{y} = {}
73               unless exists $self->{_mfhdc_PATTERN}->{y};
74             update_pattern($self, $val);
75         } elsif ($key eq 'o') {
76             # Type of unit
77             $self->{_mfhdc_UNIT} = $val;
78         } elsif ($key eq 't') {
79             $self->{_mfhdc_COPY} = $val;
80         } else {
81             carp "Unknown caption subfield '$key'";
82         }
83     }
84
85     # subsequent levels of enumeration (primary and alternate)
86     # If an enumeration level doesn't document the number
87     # of "issues" per "volume", or whether numbering of issues
88     # restarts, then we can't compress.
89     foreach my $key ('b', 'c', 'd', 'e', 'f', 'h') {
90         if (exists $self->{_mfhdc_ENUMS}->{$key}) {
91             my $pattern = $self->{_mfhdc_ENUMS}->{$key};
92             if (   !$pattern->{RESTART}
93                 || !$pattern->{COUNT}
94                 || ($pattern->{COUNT} eq 'var')
95                 || ($pattern->{COUNT} eq 'und')) {
96                 $self->{_mfhdc_COMPRESSIBLE} = 0;
97                 last;
98             }
99         }
100     }
101
102     my $pat = $self->{_mfhdc_PATTERN};
103
104     # If there's a $x subfield and a $j, then it's compressible
105     if (exists $pat->{x} && exists $self->{_mfhdc_CHRONS}->{'j'}) {
106         $self->{_mfhdc_COMPRESSIBLE} = 1;
107     }
108
109     bless($self, $class);
110
111     return $self;
112 }
113
114 sub update_pattern {
115     my $self    = shift;
116     my $val     = shift;
117     my $pathash = $self->{_mfhdc_PATTERN}->{y};
118     my ($pubcode, $pat) = unpack("a1a*", $val);
119
120     $pathash->{$pubcode} = [] unless exists $pathash->{$pubcode};
121     push @{$pathash->{$pubcode}}, $pat;
122 }
123
124 sub decode_pattern {
125     my $self    = shift;
126     my $pattern = $self->{_mfhdc_PATTERN}->{y};
127
128     # XXX WRITE ME (?)
129 }
130
131 sub pattern {
132     my $self = shift;
133
134     return $self->{_mfhdc_PATTERN};
135 }
136
137 sub compressible {
138     my $self = shift;
139
140     return $self->{_mfhdc_COMPRESSIBLE};
141 }
142
143 sub chrons {
144     my $self = shift;
145     my $key  = shift;
146
147     if (exists $self->{_mfhdc_CHRONS}->{$key}) {
148         return $self->{_mfhdc_CHRONS}->{$key};
149     } else {
150         return undef;
151     }
152 }
153
154 sub capfield {
155     my $self = shift;
156     my $key  = shift;
157
158     if (exists $self->{_mfhdc_ENUMS}->{$key}) {
159         return $self->{_mfhdc_ENUMS}->{$key};
160     } elsif (exists $self->{_mfhdc_CHRONS}->{$key}) {
161         return $self->{_mfhdc_CHRONS}->{$key};
162     } else {
163         return undef;
164     }
165 }
166
167 sub capstr {
168     my $self = shift;
169     my $key  = shift;
170     my $val  = $self->capfield($key);
171
172     if (ref $val) {
173         return $val->{CAPTION};
174     } else {
175         return $val;
176     }
177 }
178
179 sub type_of_unit {
180     my $self = shift;
181
182     return $self->{_mfhdc_UNIT};
183 }
184
185 sub link_id {
186     my $self = shift;
187
188     return $self->{_mfhdc_LINK_ID};
189 }
190
191 sub calendar_change {
192     my $self = shift;
193
194     return $self->{_mfhdc_PATTERN}->{x};
195 }
196
197 # If items are identified by chronology only, with no separate
198 # enumeration (eg, a newspaper issue), then the chronology is
199 # recorded in the enumeration subfields $a - $f.  We can tell
200 # that this is the case if there are $a - $f subfields and no
201 # chronology subfields ($i-$k), and none of the $a-$f subfields
202 # have associated $u or $v subfields, but there's a $w and no $x
203
204 sub enumeration_is_chronology {
205     my $self = shift;
206
207     # There is always a '$a' subfield in well-formed fields.
208     return 0
209       if exists $self->{_mfhdc_CHRONS}->{i}
210           || exists $self->{_mfhdc_PATTERN}->{x};
211
212     foreach my $key ('a'..'f') {
213         my $enum;
214
215         last if !exists $self->{_mfhdc_ENUMS}->{$key};
216
217         $enum = $self->{_mfhdc_ENUMS}->{$key};
218         return 0 if defined $enum->{COUNT} || defined $enum->{RESTART};
219     }
220
221     return (exists $self->{_mfhdc_PATTERN}->{w});
222 }
223
224 sub regularity_match {
225     my $self    = shift;
226     my $pubcode = shift;
227     my @date    = @_;
228
229     # we can't match something that doesn't exist.
230     return 0 if !exists $self->{_mfhdc_PATTERN}->{y}->{$pubcode};
231
232     foreach my $regularity (@{$self->{_mfhdc_PATTERN}->{y}->{$pubcode}}) {
233         my $chroncode = substr($regularity, 0, 1);
234         my $matchfunc = MFHD::Date::dispatch($chroncode);
235         my @pats      = split(/,/, substr($regularity, 1));
236
237         if (!defined $matchfunc) {
238             carp "Unrecognized chroncode '$chroncode'";
239             return 0;
240         }
241
242         # XXX WRITE ME
243         foreach my $pat (@pats) {
244             $pat =~ s|/.+||;    # If it's a combined date, match the start
245             if ($matchfunc->($pat, @date)) {
246                 return 1;
247             }
248         }
249     }
250
251     return 0;
252 }
253
254 sub is_omitted {
255     my $self = shift;
256     my @date = @_;
257
258     #     printf("# is_omitted: testing date %s: %d\n", join('/', @date),
259     #      $self->regularity_match('o', @date));
260     return $self->regularity_match('o', @date);
261 }
262
263 sub is_published {
264     my $self = shift;
265     my @date = @_;
266
267     return $self->regularity_match('p', @date);
268 }
269
270 sub is_combined {
271     my $self = shift;
272     my @date = @_;
273
274     return $self->regularity_match('c', @date);
275 }
276
277 sub enum_is_combined {
278     my $self     = shift;
279     my $subfield = shift;
280     my $iss      = shift;
281     my $level    = ord($subfield) - ord('a') + 1;
282
283     return 0 if !exists $self->{_mfhdc_PATTERN}->{y}->{c};
284
285     foreach my $regularity (@{$self->{_mfhdc_PATTERN}->{y}->{c}}) {
286         next unless $regularity =~ m/^e$level/o;
287
288         my @pats = split(/,/, substr($regularity, 2));
289
290         foreach my $pat (@pats) {
291             $pat =~ s|/.+||;    # if it's a combined issue, match the start
292             return 1 if ($iss eq $pat);
293         }
294     }
295
296     return 0;
297 }
298
299 # Test to see if $dt1 is on or after $dt2
300 # if length(@{$dt2} == 2, then just month/day are compared
301 # if length(@{$dt2} == 1, then just the months are compared
302 sub on_or_after {
303     my $dt1 = shift;
304     my $dt2 = shift;
305
306 #     printf("# on_or_after(%s, %s): ", join('/', @{$dt1}), join('/', @{$dt2}));
307
308     foreach my $i (0..(scalar(@{$dt2}) - 1)) {
309         if ($dt1->[$i] > $dt2->[$i]) {
310             #       printf("after - pass\n");
311             # $dt1 occurs AFTER $dt2
312             return 1;
313         } elsif ($dt1->[$i] < $dt2->[$i]) {
314             #       printf("before - fail\n");
315             # $dt1 occurs BEFORE $dt2
316             return 0;
317         }
318         # both are still equal, keep going
319     }
320
321     # We fell out of the loop with them being equal, so it's 'on'
322     #     printf("on - pass\n");
323     return 1;
324 }
325
326 sub calendar_increment {
327     my $self       = shift;
328     my $cur        = shift;
329     my $new        = shift;
330     my $cal_change = $self->calendar_change;
331     my $month;
332     my $day;
333     my $cur_before;
334     my $new_on_or_after;
335
336     # A calendar change is defined, need to check if it applies
337     if (scalar(@{$new}) == 1) {
338         croak "Can't calculate date change for ", $self->as_string;
339         return 0;
340     }
341
342     foreach my $change (@{$cal_change}) {
343         my $incr;
344
345         if (length($change) == 2) {
346             $month = $change;
347         } elsif (length($change) == 4) {
348             ($month, $day) = unpack("a2a2", $change);
349         }
350
351         #   printf("# calendar_increment('%s', '%s'): change on '%s/%s'\n",
352         #          join('/', @{$cur}), join('/', @{$new}),
353         #          $month, defined($day) ? $day : 'UNDEF');
354
355         if ($cur->[0] == $new->[0]) {
356             # Same year, so a 'simple' month/day comparison will be fine
357             $incr =
358               (     !on_or_after([$cur->[1], $cur->[2]], [$month, $day])
359                   && on_or_after([$new->[1], $new->[2]], [$month, $day]));
360         } else {
361             # @cur is in the year before @new. There are
362             # two possible cases for the calendar change date that
363             # indicate that it's time to change the volume:
364             # (1) the change date is AFTER @cur in the year, or
365             # (2) the change date is BEFORE @new in the year.
366             #
367             #  -------|------|------X------|------|
368             #       @cur    (1)   Jan 1   (2)   @new
369
370             $incr =
371               (on_or_after([$new->[1], $new->[2]], [$month, $day])
372                   || !on_or_after([$cur->[1], $cur->[2]], [$month, $day]));
373         }
374         return $incr if $incr;
375     }
376
377     return 0;
378 }
379
380 sub next_chron {
381     my $self  = shift;
382     my $next  = shift;
383     my $carry = shift;
384     my @keys  = @_;
385     my @cur;
386     my @new;
387     my @newend;    # only used for combined issues
388     my $incr;
389
390     my $reg     = $self->{_mfhdc_REGULARITY};
391     my $pattern = $self->{_mfhdc_PATTERN};
392     my $freq    = $pattern->{w};
393
394     foreach my $i (0..$#keys) {
395         if (exists $next->{$keys[$i]}) {
396             $cur[$i] = $next->{$keys[$i]};
397             # If the current issue has a combined date (eg, May/June)
398             # get rid of the first date and base the calculation
399             # on the final date in the combined issue.
400             $cur[$i] =~ s|^[^/]+/||;
401         }
402     }
403
404     if (defined $pattern->{y}->{p}) {
405         # There is a $y publication pattern defined in the record:
406         # use it to calculate the next issue date.
407
408         foreach my $pubpat (@{$pattern->{y}->{p}}, @{$pattern->{y}->{c}}) {
409             my $chroncode = substr($pubpat, 0, 1);
410             my $genfunc   = MFHD::Date::generator($chroncode);
411             my @pats      = split(/,/, substr($pubpat, 1));
412
413             next if $chroncode eq 'e';
414
415             if (!defined $genfunc) {
416                 carp "Unrecognized chroncode '$chroncode'";
417                 return undef;
418             }
419
420             foreach my $pat (@pats) {
421                 my $combined = $pat =~ m|/|;
422                 my ($start, $end);
423                 my @candidate;
424
425                 #       printf("# next_date: generating with pattern '%s'\n", $pat);
426
427                 if ($combined) {
428                     ($start, $end) = split('/', $pat, 2);
429                 } else {
430                     ($start, $end) = (undef, undef);
431                 }
432
433                 @candidate = $genfunc->($start || $pat, \@cur, $self);
434
435                 while ($self->is_omitted(@candidate)) {
436                     #           printf("# pubpat omitting date '%s'\n",
437                     #              join('/', @candidate));
438                     @candidate = $genfunc->($start || $pat, \@candidate, $self);
439                 }
440
441                 #       printf("# testing new candidate '%s' against '%s'\n",
442                 #              join('/', @candidate), join('/', @new));
443
444                 if (!defined($new[0]) || !on_or_after(\@candidate, \@new)) {
445                     # first time through the loop
446                     # or @candidate is before @new =>
447                     # @candidate is the next issue.
448                     @new = @candidate;
449                     if (defined $end) {
450                         @newend = $genfunc->($end, \@cur, $self);
451                     } else {
452                         $newend[0] = undef;
453                     }
454
455            #            printf("# selecting candidate date '%s'\n", join('/', @new));
456                 }
457             }
458         }
459
460         $new[1] = 24 if ($new[1] == 20); # restore fake early winter
461
462         if (defined($newend[0])) {
463             # The best match was a combined issue
464             foreach my $i (0..$#new) {
465                 # don't combine identical fields
466                 next if $new[$i] eq $newend[$i];
467                 $new[$i] .= '/' . $newend[$i];
468             }
469         }
470     }
471
472     if (scalar @new == 0) {
473         # There was no suitable publication pattern defined,
474         # so use the $w frequency to figure out the next date
475         if (!defined($freq)) {
476             croak "Undefined frequency in next_chron!";
477         } elsif (!MFHD::Date::can_increment($freq)) {
478             croak "Don't know how to deal with frequency '$freq'!";
479         } else {
480             # One of the standard defined issue frequencies
481             @new = MFHD::Date::incr_date($freq, @cur);
482
483             while ($self->is_omitted(@new)) {
484                 @new = MFHD::Date::incr_date($freq, @new);
485             }
486
487             if ($self->is_combined(@new)) {
488                 my @second_date = MFHD::Date::incr_date($freq, @new);
489                 foreach my $i (0..$#new) {
490                     # don't combine identical fields
491                     next if $new[$i] eq $second_date[$i];
492                     $new[$i] .= '/' . $second_date[$i];
493                 }
494             }
495         }
496     }
497
498     for my $i (0..$#new) {
499         $next->{$keys[$i]} = $new[$i];
500     }
501
502     # Figure out if we need to adjust volume number
503     #
504     # If we are incrementing based on date, $carry doesn't
505     # matter: we're not going to increment the v. number twice
506     #
507     # It is conceivable that a serial could increment based on date for some
508     # volumes and issue numbering for other volumes, but until a real case
509     # comes up, let's assume that defined calendar changes always trump $u
510     if (defined $pattern->{x}) {
511         my $increment = $self->calendar_increment(\@cur, \@new);
512         # if we hit a calendar change, restart dependant restarters
513         # regardless of whether they thought they should
514         if ($increment) {
515             $next->{a} += $increment;
516             foreach my $key ('b'..'f') {
517                 next if !exists $next->{$key};
518                 my $cap = $self->capfield($key);
519                 if ($cap->{RESTART}) {
520                     $next->{$key} = 1;
521                     if ($self->enum_is_combined($key, $next->{$key})) {
522                         $next->{$key} .= '/' . ($next->{$key} + 1);
523                     }
524                 } else {
525                     last; # if we find a non-restarting level, stop
526                 }
527             }
528         }
529     } elsif ($carry and exists $next->{a}) {
530         # If we have only chron fields ('i' through 'm'), don't vivicate an 'a'.
531         # It would be more standard for the chron fields to have been moved to
532         # the enum fields, but imperfect patterns exist, and that's an odd
533         # rule anyway.
534         $next->{a} += $carry;
535     }
536 }
537
538 sub winter_starts_year {
539     my $self = shift;
540
541     my $pubpats = $self->{_mfhdc_PATTERN}->{y}->{p};
542     my $freq = $self->{_mfhdc_PATTERN}->{w};
543
544     if ($freq =~ /^\d$/) {
545         foreach my $pubpat (@$pubpats) {
546             my $chroncode = substr($pubpat, 0, 1);
547             if ($chroncode eq 's') {
548                 # check first instance only
549                 if (substr($pubpat, 1, 2) == 24) {
550                     return 1;
551                 } else {
552                     return 0;
553                 }
554             }
555         }
556     }
557     return 0;
558 }
559
560
561 sub next_alt_enum {
562     my $self = shift;
563     my $next = shift;
564
565     # First handle any "alternative enumeration", since they're
566     # a lot simpler, and don't depend on the the calendar
567     foreach my $key ('h', 'g') {
568         next if !exists $next->{$key};
569         if (!$self->capstr($key)) {
570             warn "Holding data exists for $key, but no caption specified";
571             $next->{$key} += 1;
572             last;
573         }
574
575         my $cap = $self->capfield($key);
576         if (   $cap->{RESTART}
577             && $cap->{COUNT}
578             && ($next->{$key} == $cap->{COUNT})) {
579             $next->{$key} = 1;
580         } else {
581             $next->{$key} += 1;
582             last;
583         }
584     }
585 }
586
587 # Check caption for $ype subfield, specifying that there's a
588 # particular publication pattern for the given level of enumeration
589 # returns the pattern string or undef
590 sub enum_pubpat {
591     my $self  = shift;
592     my $level = shift;
593
594     return undef if !exists $self->{_mfhdc_PATTERN}->{y}->{p};
595
596     foreach my $reg (@{$self->{_mfhdc_PATTERN}->{y}->{p}}) {
597         if ($reg =~ m/^e$level/o) {
598             return substr($reg, 2);
599         }
600     }
601     return undef;
602 }
603
604 sub next_enum {
605     my $self = shift;
606     my $next = shift;
607     my $carry;
608
609     # $carry keeps track of whether we need to carry into the next
610     # higher level of enumeration. It's not actually necessary except
611     # for when the loop ends: if we need to carry from $b into $a
612     # then $carry will be set when the loop ends.
613     #
614     # We need to keep track of this because there are two different
615     # reasons why we might increment the highest level of enumeration ($a)
616     # 1) we hit the correct number of items in $b (ie, 5th iss of quarterly)
617     # 2) it's the right time of the year.
618     #
619
620     # If there's a subfield b, then we will go through the loop at
621     # least once. If there's no subfield b, then there's only a single
622     # level of enumeration, so we just add one to it and we're done.
623     if (exists $next->{b}) {
624         $carry = 0;
625     } else {
626         $carry = 1;
627     }
628     foreach my $key (reverse('b'..'f')) {
629         my $level;
630         my $pubpat;
631
632         next if !exists $next->{$key};
633
634         # If the current issue has a combined issue number (eg, 2/3)
635         # get rid of the first issue number and base the calculation
636         # on the final issue number in the combined issue.
637         if ($next->{$key} =~ m|/|) {
638             $next->{$key} =~ s|^[^/]+/||;
639         }
640
641         $level = ord($key) - ord('a') + 1;    # enumeration level
642
643         $pubpat = $self->enum_pubpat($level);
644
645         if ($pubpat) {
646             #       printf("# next_enum: found pubpat '%s' for subfield '%s'\n",
647             #          $pubpat, $key);
648             my @pats = split(/,/, $pubpat);
649
650             # If we fall out the bottom of the loop, then $carry
651             # will still be 1, and we will reset the current
652             # level to the first value in @pats and increment
653             # then next higher level.
654             $carry = 1;
655
656             foreach my $pat (@pats) {
657                 my $combined = $pat =~ m|/|;
658                 my $end;
659
660              #      printf("# next_enum: checking current '%s' against pat '%s'\n",
661              #             $next->{$key}, $pat);
662
663                 if ($combined) {
664                     ($pat, $end) = split('/', $pat, 2);
665                 } else {
666                     $end = undef;
667                 }
668
669                 if ($pat > $next->{$key}) {
670                     $carry = 0;
671                     $next->{$key} = $pat;
672                     $next->{$key} .= '/' . $end if $end;
673      #          printf("# next_enum: selecting new issue no. %s\n", $next->{$key});
674                     last;    # We've found the correct next issue number
675                 }
676             }
677             if ($carry) {
678                 $next->{$key} = $pats[0];
679             } else {
680                 last;        # exit the top level loop because we're done
681             }
682
683         } else {
684             # No enumeration publication pattern specified for this level,
685             # just keep adding one.
686
687             if (!$self->capstr($key)) {
688                 # Just assume that it increments continuously and give up
689                 warn "Holding data exists for $key, but no caption specified";
690                 $next->{$key} += 1;
691                 $carry = 0;
692                 last;
693             }
694
695         #       printf("# next_enum: no publication pattern, using frequency\n");
696
697             my $cap = $self->capfield($key);
698             if (   $cap->{RESTART}
699                 && $cap->{COUNT}
700                 && ($next->{$key} eq $cap->{COUNT})) {
701                 $next->{$key} = 1;
702                 $carry = 1;
703             } elsif ($cap->{COUNT} > 0 and !($next->{$key} % $cap->{COUNT})) {
704                 # If we have a non-restarting enum, but we define a count,
705                 # we need to carry to the next level when the current value
706                 # divides evenly by the count
707                 # XXX: this code naively assumes that there has never been an
708                 # issue number anomaly of any kind (like an extra issue), but this
709                 # limit is inherent in the standard
710                 $next->{$key} += 1;
711                 $carry = 1;
712             } else {
713                 # If I don't need to "carry" beyond here, then I just increment
714                 # this level of the enumeration and stop looping, since the
715                 # "next" hash has been initialized with the current values
716
717                 $next->{$key} += 1;
718                 $carry = 0;
719             }
720
721             # You can't have a combined issue that spans two volumes: no.12/1
722             # is forbidden
723             if ($self->enum_is_combined($key, $next->{$key})) {
724                 $next->{$key} .= '/' . ($next->{$key} + 1);
725             }
726
727             last if !$carry;
728         }
729     }
730
731     # The easy part is done. There are two things left to do:
732     # 1) Calculate the date of the next issue, if necessary
733     # 2) Increment the highest level of enumeration (either by date
734     #    or because $carry is set because of the above loop
735
736     if (!$self->subfield('i') || !$next->{i}) {
737         # The simple case: if there is no chronology specified
738         # then just check $carry and return
739         $next->{'a'} += $carry;
740     } else {
741         # Figure out date of next issue, then decide if we need
742         # to adjust top level enumeration based on that
743         $self->next_chron($next, $carry, ('i'..'m'));
744     }
745 }
746
747 sub next {
748     my $self    = shift;
749     my $holding = shift;
750     my $next    = {};
751
752     # If the holding is compressed and not open ended, base next() on the
753     # closing date.  If the holding is open-ended, next() is undefined
754     my $index;
755     if ($holding->is_compressed) {
756         return undef if $holding->is_open_ended;
757         # TODO: error on next for open-ended holdings?
758         $index = 1;
759     } else {
760         $index = 0;
761     }
762
763     # Initialize $next with current enumeration & chronology, then
764     # we can just operate on $next, based on the contents of the caption
765     foreach my $key ('a'..'m') {
766         my $holding_values = $holding->field_values($key);
767         $next->{$key} = ${$holding_values}[$index] if defined $holding_values;
768     }
769
770     if ($self->enumeration_is_chronology) {
771         $self->next_chron($next, 0, ('a'..'h'));
772         return $next;
773     }
774
775     if (exists $next->{'h'}) {
776         $self->next_alt_enum($next);
777     }
778
779     $self->next_enum($next);
780
781     return ($next);
782 }
783
784 # return a simple subfields list
785 sub subfields_list {
786     my $self = shift;
787     my @subfields;
788
789     foreach my $subfield ($self->subfields) {
790         push(@subfields, $subfield->[0], $subfield->[1]);
791     }
792     return @subfields;
793 }
794
795 1;