]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/perlmods/OpenILS/Utils/MFHD/Caption.pm
A bunch of untested code to support serials predictions
[Evergreen.git] / Open-ILS / src / perlmods / OpenILS / Utils / MFHD / Caption.pm
1 package MFHD::Caption;
2 use strict;
3 use integer;
4 use Carp;
5
6 use DateTime;
7 use MARC::Record;
8
9 sub new
10 {
11     my $proto = shift;
12     my $class = ref($proto) || $proto;
13     my $caption = shift;
14     my $self = {};
15     my $last_enum = undef;
16
17     $self->{CAPTION} = $caption;
18     $self->{ENUMS} = {};
19     $self->{CHRONS} = {};
20     $self->{PATTERN} = {};
21     $self->{COPY} = undef;
22     $self->{UNIT} = undef;
23     $self->{COMPRESSIBLE} = 1;  # until proven otherwise
24
25     foreach my $subfield ($caption->subfields) {
26         my ($key, $val) = @$subfield;
27         if ($key eq '8') {
28             $self->{LINK} = $val;
29         } elsif ($key =~ /[a-h]/) {
30             # Enumeration Captions
31             $self->{ENUMS}->{$key} = {CAPTION => $val,
32                                       COUNT => undef,
33                                       RESTART => undef};
34             if ($key =~ /[ag]/) {
35                 $last_enum = undef;
36             } else {
37                 $last_enum = $key;
38             }
39         } elsif ($key =~ /[i-m]/) {
40             # Chronology captions
41             $self->{CHRONS}->{$key} = $val;
42         } elsif ($key eq 'u') {
43             # Bib units per next higher enumeration level
44             carp('$u specified for top-level enumeration')
45               unless defined($last_enum);
46             $self->{ENUMS}->{$last_enum}->{COUNT} = $val;
47         } elsif ($key eq 'v') {
48             carp '$v specified for top-level enumeration'
49               unless defined($last_enum);
50             $self->{ENUMS}->{$last_enum}->{RESTART} = ($val eq 'r');
51         } elsif ($key =~ /[npwxz]/) {
52             # Publication Pattern ('o' == type of unit, 'q'..'t' undefined)
53             $self->{PATTERN}->{$key} = $val;
54         } elsif ($key eq 'y') {
55             # Publication pattern: 'y' is repeatable
56             $self->{PATTERN}->{y} = [] if (!defined $self->{PATTERN}->{y});
57             push @{$self->{PATTERN}->{y}}, $val;
58         } elsif ($key eq 'o') {
59             # Type of unit
60             $self->{UNIT} = $val;
61         } elsif ($key eq 't') {
62             $self->{COPY} = $val;
63         } else {
64             carp "Unknown caption subfield '$key'";
65         }
66     }
67
68     # subsequent levels of enumeration (primary and alternate)
69     # If an enumeration level doesn't document the number
70     # of "issues" per "volume", or whether numbering of issues
71     # restarts, then we can't compress.
72     foreach my $key ('b', 'c', 'd', 'e', 'f', 'h') {
73         if (exists $self->{ENUMS}->{$key}) {
74             my $pattern = $self->{ENUMS}->{$key};
75             if (!$pattern->{RESTART} || !$pattern->{COUNT}
76                 || ($pattern->{COUNT} eq 'var')
77                 || ($pattern->{COUNT} eq 'und')) {
78                 $self->{COMPRESSIBLE} = 0;
79                 last;
80             }
81         }
82     }
83
84     # If there's a $x subfield and a $j, then it's compressible
85     if (exists $self->{PATTERN}->{x} && exists $self->{CHRONS}->{'j'}) {
86         $self->{COMPRESSIBLE} = 1;
87     }
88
89     bless ($self, $class);
90
91     if (exists $self->{PATTERN}->{y}) {
92         $self->decode_pattern;
93     }
94
95     return $self;
96 }
97
98 sub decode_pattern {
99     my $self = shift;
100     my $pattern = $self->{PATTERN}->{y};
101
102     # XXX WRITE ME (?)
103 }
104
105 sub compressible {
106     my $self = shift;
107
108     return $self->{COMPRESSIBLE};
109 }
110
111 sub caption {
112     my $self = shift;
113     my $key;
114
115     if (@_) {
116         $key = shift;
117         if (exists $self->{ENUMS}->{$key}) {
118             return $self->{ENUMS}->{$key}->{CAPTION};
119         } elsif (exists $self->{CHRONS}->{$key}) {
120             return $self->{CHRONS}->{$key};
121         } else {
122             return undef;
123         }
124     } else {
125         return $self->{CAPTION};
126     }
127 }
128
129 # If items are identified by chronology only, with no separate
130 # enumeration (eg, a newspaper issue), then the chronology is
131 # recorded in the enumeration subfields $a - $f.  We can tell
132 # that this is the case if there are $a - $f subfields and no
133 # chronology subfields ($i-$k), and none of the $a-$f subfields
134 # have associated $u or $v subfields, but there are $w and $y
135 # subfields.
136
137 sub enumeration_is_chronology {
138     my $self = shift;
139
140     # There is always a '$a' subfield in well-formed fields.
141     return 0 if exists $self->{CHRONS}->{i};
142
143     foreach my $key ('a' .. 'f') {
144         my $enum;
145
146         last if !exists $self->{ENUMS}->{$key};
147
148         $enum = $self->{ENUMS}->{$key};
149         return 0 if defined $enum->{COUNT} || defined $enum->{RESTART};
150     }
151
152     return (exists $self->{PATTERN}->{w} && exists $self->{PATTERN}->{y});
153 }
154
155 my %daynames = (
156                 'mo' => 1,
157                 'tu' => 2,
158                 'we' => 3,
159                 'th' => 4,
160                 'fr' => 5,
161                 'sa' => 6,
162                 'su' => 7,
163                );
164
165 my $daypat = '(mo|tu|we|th|fr|sa|su)';
166 my $weekpat = '(99|98|97|00|01|02|03|04|05)';
167 my $weeknopat;
168 my $monthpat = '(01|02|03|04|05|06|07|08|09|10|11|12)';
169 my $seasonpat = '(21|22|23|24)';
170
171 # Initialize $weeknopat to be '(01|02|03|...|51|52|53)'
172 $weeknopat = '(';
173 foreach my $weekno (1..52) {
174     $weeknopat .= sprintf("%02d|", $weekno);
175 }
176 $weeknopat .= '53)';
177
178 sub match_day {
179     my $pat = shift;
180     my @date = @_;
181     # Translate daynames into day of week for DateTime
182     # also used to check if dayname is valid.
183
184     if (exists $daynames{$pat}) {
185         # dd
186         # figure out day of week for date and compare
187         my $dt = DateTime->new(year  => $date[0],
188                                month => $date[1],
189                                day   => $date[2]);
190         return ($dt->day_of_week == $daynames{$pat});
191     } elsif (length($pat) == 2) {
192         # MM
193         return $pat == $date[3];
194     } elsif (length($pat) == 4) {
195         # MMDD
196         my ($mon, $day);
197         $mon = substr($pat, 0, 2);
198         $day = substr($pat, 2, 2);
199
200         return (($mon == $date[1]) && ($day == $date[2]));
201     } else {
202         carp "Invalid day pattern '$pat'";
203         return 0;
204     }
205 }
206
207 # Calcuate date of "n"th last "dayname" of month: second last Tuesday
208 sub last_week_of_month {
209     my $dt = shift;
210     my $week = shift;
211     my $day = shift;
212     my $end_dt = DateTime->last_day_of_month(year  => $dt->year,
213                                              month => $dt->month);
214
215     $day = $daynames{$day};
216     while ($end_dt->day_of_week != $day) {
217         $end_dt->subtract(days => 1);
218     }
219
220     # 99: last week of month, 98: second last, etc.
221     for (my $i = 99 - $week; $i > 0; $i--) {
222         $end_dt->subtract(weeks => 1);
223     }
224
225     return $end_dt;
226 }
227
228 sub check_date {
229     my $dt = shift;
230     my $month = shift;
231     my $weekno = shift;
232     my $day = shift;
233
234     if (!defined $day) {
235         # MMWW
236         return (($dt->month == $month)
237                 && (($dt->week_of_month == $weekno)
238                     || ($dt->week_of_month == last_day_of_month($dt, $weekno, 'th')->week_of_month)));
239     }
240
241     # simple cases first
242     if ($daynames{$day} != $dt->day_of_week) {
243         # if it's the wrong day of the week, rest doesn't matter
244         return 0;
245     }
246
247     if (!defined $month) {
248         # WWdd
249         return (($dt->weekday_of_month == $weekno)
250                 || ($dt->weekday_of_month == last_day_of_month($dt, $weekno, $day)->weekday_of_month));
251     }
252
253     # MMWWdd
254     if ($month != $dt->month) {
255         # If it's the wrong month, then we're done
256         return 0;
257     }
258
259     # It's the right day of the week
260     # It's the right month
261
262     if ($weekno == $dt->weekday_of_month) {
263         # If this matches, then we're counting from the beginning
264         # of the month and it matches and we're done.
265         return 1;
266     }
267
268     # only case left is that the week number is counting from
269     # the end of the month: eg, second last wednesday
270     return (last_week_of_month($weekno, $day)->weekday_of_month == $dt->weekday_of_month);
271 }
272
273 sub match_week {
274     my $pat = shift;
275     my @date = @_;
276     my $dt = DateTime->new(year  => $date[0],
277                            month => $date[1],
278                            day   => $date[2]);
279
280     if ($pat =~ m/^$weekpat$daypat$/) {
281         # WWdd: 03we = Third Wednesday
282         return check_date($dt, undef, $1, $2);
283     } elsif ($pat =~ m/^$monthpat$weekpat$daypat$/) {
284         # MMWWdd: 0599tu Last Tuesday in May XXX WRITE ME
285         return check_date($dt, $1, $2, $3);
286     } elsif ($pat =~ m/^$monthpat$weekpat$/) {
287         # MMWW: 1204: Fourth week in December XXX WRITE ME
288         return check_date($dt, $1, $2, undef);
289     } else {
290         carp "invalid week pattern '$pat'";
291         return 0;
292     }
293 }
294
295 sub match_month {
296     my $pat = shift;
297     my @date = @_;
298
299     return ($pat eq $date[1]);
300 }
301
302 sub match_season {
303     my $pat = shift;
304     my @date = @_;
305
306     return ($pat eq $date[1]);
307 }
308
309 sub match_year {
310     my $pat = shift;
311     my @date = @_;
312
313     # XXX WRITE ME
314 }
315
316 my %dispatch = (
317                 'd' => \&match_day,
318                 'w' => \&match_week,
319                 'm' => \&match_month,
320                 's' => \&match_season,
321                 'y' => \&match_year,
322 );
323 sub regularity_match {
324     my $self = shift;
325     my $pubcode = shift;
326     my @date = @_;
327
328     foreach my $regularity ($self->{PATTERN}->{y}) {
329         next unless $regularity =~ m/^$pubcode/;
330
331         my $chroncode= substr($regularity, 1, 1);
332         my @pats = split(/,/, substr($regularity, 2));
333
334         # XXX WRITE ME
335         foreach my $pat (@pats) {
336             if ($dispatch{$chroncode}->($pat, @date)) {
337                 return 1;
338             }
339         }
340     }
341
342     return 0;
343 }
344
345 sub is_omitted {
346     my $self = shift;
347     my @date = @_;
348
349     return $self->regularity_match('o', @date);
350 }
351
352 sub is_published {
353     my $self = shift;
354     my @date = @_;
355
356     return $self->regularity_match('p', @date);
357 }
358
359 sub is_combined {
360     my $self = shift;
361     my @date = @_;
362
363     return $self->regularity_match('c', @date);
364 }
365
366 1;