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