1 # MFHD::Holding provides some additional holdings logic to a MARC::Field
2 # object. In its current state it is primarily read-only, as direct changes
3 # to the underlying MARC::Field are not reflected in the MFHD logic layer, and
4 # only the 'increment', 'notes', and 'seqno' methods do updates to the
15 use base 'MARC::Field';
19 my $class = ref($proto) || $proto;
23 my $last_enum = undef;
25 $self->{_mfhdh_SEQNO} = $seqno;
26 $self->{_mfhdh_CAPTION} = $caption;
27 $self->{_mfhdh_DESCR} = {};
28 $self->{_mfhdh_COPY} = undef;
29 $self->{_mfhdh_BREAK} = undef;
30 $self->{_mfhdh_NOTES} = {};
31 $self->{_mfhdh_NOTES}{public} = [];
32 $self->{_mfhdh_NOTES}{private} = [];
33 $self->{_mfhdh_COPYRIGHT} = [];
34 $self->{_mfhdh_COMPRESSED} = $self->indicator(2) eq '0' ? 1 : 0;
35 $self->{_mfhdh_OPEN_ENDED} = 0;
37 foreach my $subfield ($self->subfields) {
38 my ($key, $val) = @$subfield;
40 if ($key =~ /[a-m]/) {
41 if ($self->{_mfhdh_COMPRESSED}) {
42 $self->{_mfhdh_FIELDS}->{$key}{HOLDINGS} = [split(/\-/, $val)];
44 $self->{_mfhdh_FIELDS}->{$key}{HOLDINGS} = [$val];
46 if ($key =~ /[a-h]/) {
47 # Enumeration specific details of holdings
48 $self->{_mfhdh_FIELDS}->{$key}{UNIT} = undef;
51 } elsif ($key eq 'o') {
52 warn '$o specified prior to first enumeration'
53 unless defined($last_enum);
54 $self->{_mfhdh_FIELDS}->{$last_enum}->{UNIT} = $val;
56 } elsif ($key =~ /[npq]/) {
57 $self->{_mfhdh_DESCR}->{$key} = $val;
58 } elsif ($key eq 's') {
59 push @{$self->{_mfhdh_COPYRIGHT}}, $val;
60 } elsif ($key eq 't') {
61 $self->{_mfhdh_COPY} = $val;
62 } elsif ($key eq 'w') {
63 carp "Unrecognized break indicator '$val'"
64 unless $val =~ /^[gn]$/;
65 $self->{_mfhdh_BREAK} = $val;
66 } elsif ($key eq 'x') {
67 push @{$self->{_mfhdh_NOTES}{private}}, $val;
68 } elsif ($key eq 'z') {
69 push @{$self->{_mfhdh_NOTES}{public}}, $val;
73 if ( $self->{_mfhdh_COMPRESSED}
74 && $self->{_mfhdh_FIELDS}{'a'}{HOLDINGS}[1] eq '') {
75 $self->{_mfhdh_OPEN_ENDED} = 1;
82 # accessor to the object's field hash
84 # We are avoiding calling these elements 'subfields' because they are more
85 # than simply the MARC subfields, although in the current implementation they
86 # are indexed on the subfield key
91 return $self->{_mfhdh_FIELDS};
95 # Given a field key, returns an array ref of one (for single statements)
96 # or two (for compressed statements) values
99 my ($self, $key) = @_;
101 if (exists $self->fields->{$key}) {
102 my @values = @{$self->fields->{$key}{HOLDINGS}};
113 $self->{_mfhdh_SEQNO} = $_[0];
114 $self->update(8 => $self->caption->link_id . '.' . $_[0]);
117 return $self->{_mfhdh_SEQNO};
123 return $self->{_mfhdh_COMPRESSED};
129 return $self->{_mfhdh_OPEN_ENDED};
135 return $self->{_mfhdh_CAPTION};
145 } elsif ($type ne 'public' && $type ne 'private') {
146 carp("Notes being applied without specifiying type");
147 unshift(@notes, $type);
151 if (ref($notes[0])) {
152 $self->{_mfhdh_NOTES}{$type} = $notes[0];
153 $self->_replace_note_subfields($type, @{$notes[0]});
156 $self->{_mfhdh_NOTES}{$type} = \@notes;
158 $self->{_mfhdh_NOTES}{$type} = [];
160 $self->_replace_note_subfields($type, @notes);
163 return $self->{_mfhdh_NOTES}{$type};
167 # utility function for 'notes' method
169 sub _replace_note_subfields {
173 my %note_subfield_ids = ('public' => 'z', 'private' => 'x');
175 $self->delete_subfield(code => $note_subfield_ids{$type});
177 foreach my $note (@notes) {
178 $self->add_subfields($note_subfield_ids{$type} => $note);
183 # return a simple subfields list (for easier revivification from database)
189 foreach my $subfield ($self->subfields) {
190 push(@subfields, $subfield->[0], $subfield->[1]);
196 # Called by method 'format_part' for formatting the chronology portion of
197 # the holding statement
201 my $holdings = shift;
202 my $caption = $self->caption;
224 foreach my $i (0..@keys) {
230 last if !defined $caption->capstr($key);
232 $capstr = $caption->capstr($key);
233 if (substr($capstr, 0, 1) eq '(') {
234 # a caption enclosed in parentheses is not displayed
238 # If this is the second level of chronology, then it's
239 # likely to be a month or season, so we should use the
240 # string name rather than the number given.
242 # account for possible combined issue chronology
243 my @chron_parts = split('/', $holdings->{$key});
244 for (my $i = 0; $i < @chron_parts; $i++) {
245 $chron_parts[$i] = $month{$chron_parts[$i]};
247 $chron = join('/', @chron_parts);
249 $chron = $holdings->{$key};
252 $str .= (($i == 0 || $str =~ /[. ]$/) ? '' : ':') . $capstr . $chron;
259 # Called by method 'format' for each member of a possibly compressed holding
263 my $holding_values = shift;
264 my $caption = $self->caption;
267 if ($caption->type_of_unit) {
268 $str = $caption->type_of_unit . ' ';
271 if ($caption->enumeration_is_chronology) {
272 # if issues are identified by chronology only, then the
273 # chronology data is stored in the enumeration subfields,
274 # so format those fields as if they were chronological.
275 $str = $self->format_chron($holding_values, 'a'..'f');
277 # OK, there is enumeration data and maybe chronology
278 # data as well, format both parts appropriately
281 foreach my $key ('a'..'f') {
286 last if !defined $caption->capstr($key);
288 $capstr = $caption->capstr($key);
289 if (substr($capstr, 0, 1) eq '(') {
290 # a caption enclosed in parentheses is not displayed
294 ($key eq 'a' ? '' : ':') . $capstr . $holding_values->{$key};
298 if (defined $caption->capstr('i')) {
300 $str .= $self->format_chron($holding_values, 'i'..'l');
304 if ($caption->capstr('g')) {
305 # There's at least one level of alternative enumeration
307 foreach my $key ('g', 'h') {
309 ($key eq 'g' ? '' : ':')
310 . $caption->capstr($key)
311 . $holding_values->{$key};
314 # This assumes that alternative chronology is only ever
315 # provided if there is an alternative enumeration.
316 if ($caption->capstr('m')) {
317 # Alternative Chronology
319 $str .= $caption->capstr('m') . $holding_values->{'m'};
325 # Breaks in the sequence
326 if (defined($self->{_mfhdh_BREAK})) {
327 if ($self->{_mfhdh_BREAK} eq 'n') {
328 $str .= ' non-gap break';
329 } elsif ($self->{_mfhdh_BREAK} eq 'g') {
332 warn "unrecognized break indicator '$self->{_mfhdh_BREAK}'";
340 # Create and return a string which conforms to display standard Z39.71
344 my $subfields = $self->fields;
349 foreach my $key (keys %$subfields) {
350 ($holding_start{$key}, $holding_end{$key}) =
351 @{$self->field_values($key)};
354 if ($self->is_compressed) {
355 # deal with open-ended statements
357 if ($self->is_open_ended) {
360 $formatted_end = $self->format_part(\%holding_end);
363 $self->format_part(\%holding_start) . ' - ' . $formatted_end;
365 $formatted = $self->format_part(\%holding_start);
369 if (@{$self->notes}) {
370 $formatted .= ' Note: ' . join(', ', @{$self->notes});
376 # next: Given a holding statement, return a hash containing the
377 # enumeration values for the next issues, whether we hold it or not
378 # Just pass through to Caption::next
382 my $caption = $self->caption;
384 return $caption->next($self);
388 # matches($pat): check to see if $self matches the enumeration hashref passed
389 # in as $pat, as returned by the 'next' method. e.g.:
390 # $holding2->matches($holding1->next) # true if $holding2 directly follows
393 # Always returns false if $self is compressed
399 return 0 if $self->is_compressed;
401 foreach my $key ('a'..'f') {
402 # If a subfield exists in $self but not in $pat, or vice versa
403 # or if the field has different values, then fail
405 defined($self->field_values($key)) != exists($pat->{$key})
406 || (exists $pat->{$key}
407 && ($self->field_values($key)->[0] ne $pat->{$key}))
416 # Check that all the fields in a holdings statement are
417 # included in the corresponding caption.
422 foreach my $key (keys %{$self->fields}) {
423 if (!$self->caption || !$self->caption->capfield($key)) {
431 # Replace a single holding with it's next prediction
434 # If the holding is compressed, the range is expanded
439 my $next = $self->next();
441 if ($self->is_compressed) { # expand range
442 foreach my $key (keys %{$next}) {
443 my @values = @{$self->field_values($key)};
444 $values[1] = $next->{$key};
445 $self->fields->{$key}{HOLDINGS} = \@values;
446 $next->{$key} = join('-', @values);
449 foreach my $key (keys %{$next}) {
450 $self->fields->{$key}{HOLDINGS}[0] = $next->{$key};
454 $self->seqno($self->seqno + 1);
455 $self->update(%{$next}); # update underlying subfields
460 # Basic, working, unoptimized clone operation
465 my $clone_field = $self->SUPER::clone();
466 return new MFHD::Holding($self->seqno, $clone_field, $self->caption);
470 # Turn a chronology instance into date(s) in YYYY-MM-DD format
472 # In list context it returns a list of start and (possibly undefined)
475 # In scalar context, it returns a YYYY-MM-DD date string of either the
476 # single date or the (possibly undefined) end date of a compressed holding
480 my $caption = $self->caption;
483 if ($caption->enumeration_is_chronology) {
489 my @chron_start = (0, 1, 1);
490 my @chron_end = (0, 1, 1);
491 my @chrons = (\@chron_start, \@chron_end);
492 foreach my $key (@keys) {
493 my $capstr = $caption->capstr($key);
494 last if !defined($capstr);
495 if ($capstr =~ /year/) {
496 ($chron_start[0], $chron_end[0]) = @{$self->field_values($key)};
497 } elsif ($capstr =~ /month/) {
498 ($chron_start[1], $chron_end[1]) = @{$self->field_values($key)};
499 } elsif ($capstr =~ /day/) {
500 ($chron_start[2], $chron_end[2]) = @{$self->field_values($key)};
501 } elsif ($capstr =~ /season/) {
502 my @seasons = @{$self->field_values($key)};
503 for (my $i = 0; $i < @seasons; $i++) {
504 $seasons[$i] = &_uncombine($seasons[$i], 0);
505 if ($seasons[$i] == 21) {
506 $chrons[$i]->[1] = 3;
507 $chrons[$i]->[2] = 20;
508 } elsif ($seasons[$i] == 22) {
509 $chrons[$i]->[1] = 6;
510 $chrons[$i]->[2] = 21;
511 } elsif ($seasons[$i] == 23) {
512 $chrons[$i]->[1] = 9;
513 $chrons[$i]->[2] = 22;
514 } elsif ($seasons[$i] == 24) {
515 $chrons[$i]->[1] = 12;
516 $chrons[$i]->[2] = 21;
523 foreach my $chron (@chrons) {
525 if ($chron->[0] != 0) {
527 &_uncombine($chron->[0], 0) . '-'
528 . sprintf('%02d', $chron->[1]) . '-'
529 . sprintf('%02d', $chron->[2]);
536 } elsif ($self->is_compressed) {
544 # utility function for uncombining instance parts
547 my ($combo, $pos) = @_;
550 carp("Function 'uncombine' is not an instance method");
554 my @parts = split('/', $combo);