File Coverage

blib/lib/Ucam/Term.pm
Criterion Covered Total %
statement 179 182 98.3
branch 56 64 87.5
condition n/a
subroutine 36 36 100.0
pod 10 10 100.0
total 281 292 96.2


line stmt bran cond sub pod time code
1             package Ucam::Term;
2              
3 2     2   110455 use strict;
  2         2  
  2         42  
4 2     2   7 use warnings;
  2         3  
  2         37  
5              
6 2     2   6 use Carp;
  2         5  
  2         96  
7 2     2   1601 use DateTime;
  2         641065  
  2         99  
8 2     2   1489 use DateTime::Span;
  2         73154  
  2         51  
9 2     2   17 use DateTime::Duration;
  2         3  
  2         275  
10              
11             our $VERSION = "3.00";
12              
13             =head1 NAME
14              
15             Ucam::Term - return information about the start and end dates of terms
16             (and a few related events) at the University of Cambridge, UK.
17              
18             =head1 SYNOPSIS
19              
20             use Ucam::Term;
21              
22             my $term = Ucam::Term->new('m',2010);
23             print $term->name, " term ", $term->year, " starts ";
24             print $term->dates->start, "\n";
25              
26             my $term = Ucam::Term->new('e',2015);
27             print "General Admission ", $term->year, " starts ";
28             print $term->general_admission->start, "\n";
29              
30             =head1 DESCRIPTION
31              
32             The academic year at the University of Cambridge, UK, is divided into
33             three I<Terms> - I<Michaelmas> (autumn), I<Lent> (spring) and
34             I<Easter> (summer). The half-way point of each term is called the
35             I<Division of Term>. Three quarters of each Term is designated I<Full
36             Term>, during which lectures are given and when undergraduate students
37             are normally required to be in residence. Near the end of the Easter
38             Term there are three or four days of I<General Admission>, during which
39             degrees are conferred on those who have just successfully completed
40             courses, followed by the I<Long Vacation period of residence> during
41             which some additional courses of instruction are given.
42              
43             The dates of some of these events and periods are fixed, but others
44             depend on dates that appear in the University's published 'Ordnances'
45             (see L</SEE ALSO> below for references) which are updated from time to
46             time. This version of the module contains data covering the period
47             from the Michaelmas Term 2007 to the Easter Term 2030.
48              
49             This module returns L<DateTime> or L<DateTime::Span> objects
50             corresponding to these events and periods. Note that the
51             DateTime::Span objects run from 00:00 (inclusive) on the day the
52             period starts to 00:00 (exclusive) on the day after the period ends
53             (see DateTime::Span->end_is_open). This means that
54             union/intersection/complement/intersects/contains operations will all
55             work correctly, but that it is normally necessary to subtract one day
56             from the end date of the span in order to find the date of the last
57             day of the period.
58              
59             =cut
60              
61             # See Ordinances, Chapter II, Section 10 'Dates of Term and Full Term'
62             # and Ordinances, Chapter II, Section 12 'Admission to Degrees'
63             # http://www.admin.cam.ac.uk/univ/so/ as ammended in respect of
64             # General Admission by grace 1 of 13 February 2013
65             # (http://www.admin.cam.ac.uk/reporter/2012-13/weekly/6297/section6.shtml)
66             # and by grace 2 of 5 February 2014
67             # (http://www.admin.cam.ac.uk/reporter/2013-14/weekly/6336/section8.shtml)
68              
69             # "The dates on which Full Terms begin and end shall be as shown in
70             # the table appended to these regulations"
71              
72             # This data represents the first day of full term and of General
73             # Admission (Thursdays before 2014, Wednesdays from 2014 onwards),
74             # extracted from the table in S&O Chapter II, Section 10 (2009 edition
75             # for 2007 to 2020, 2012 edition for 2011 to 2030)
76              
77 2         223 use constant FULL_TERM_START =>
78             # Jan Apr Jun/Jul Oct
79             { 2007 => { m => 2 },
80             2008 => { l => 15, e => 22, g => 26, m => 7 },
81             2009 => { l => 13, e => 21, g => 25, m => 6 },
82             2010 => { l => 12, e => 20, g => 24, m => 5 },
83             2011 => { l => 18, e => 26, g => 30, m => 4 },
84             2012 => { l => 17, e => 24, g => 28, m => 2 },
85             2013 => { l => 15, e => 23, g => 27, m => 8 },
86             2014 => { l => 14, e => 22, g => 25, m => 7 },
87             2015 => { l => 13, e => 21, g => 24, m => 6 },
88             2016 => { l => 12, e => 19, g => 22, m => 4 },
89             2017 => { l => 17, e => 25, g => 28, m => 3 },
90             2018 => { l => 16, e => 24, g => 27, m => 2 },
91             2019 => { l => 15, e => 23, g => 26, m => 8 },
92             2020 => { l => 14, e => 21, g => 24, m => 6 },
93             2021 => { l => 19, e => 27, g => 30, m => 5 },
94             2022 => { l => 18, e => 26, g => 29, m => 4 },
95             2023 => { l => 17, e => 25, g => 28, m => 3 },
96             2024 => { l => 16, e => 23, g => 26, m => 8 },
97             2025 => { l => 21, e => 29, g => 2, m => 7 },
98             2026 => { l => 20, e => 28, g => 1, m => 6 },
99             2027 => { l => 19, e => 27, g => 30, m => 5 },
100             2028 => { l => 18, e => 25, g => 28, m => 3 },
101             2029 => { l => 16, e => 24, g => 27, m => 2 },
102             2030 => { l => 15, e => 23, g => 26, },
103 2     2   8 };
  2         3  
104              
105 2     2   9 use constant TERM_NAME => { m => 'Michaelmas', l => 'Lent', e => 'Easter' };
  2         4  
  2         98  
106              
107 2     2   8 use constant JAN => 1;
  2         3  
  2         99  
108 2     2   23 use constant FEB => 2;
  2         3  
  2         96  
109 2     2   9 use constant MAR => 3;
  2         2  
  2         92  
110 2     2   9 use constant APR => 4;
  2         4  
  2         80  
111 2     2   8 use constant MAY => 5;
  2         6  
  2         87  
112 2     2   15 use constant JUN => 6;
  2         2  
  2         89  
113 2     2   9 use constant JUL => 7;
  2         1  
  2         100  
114 2     2   8 use constant AUG => 8;
  2         6  
  2         82  
115 2     2   8 use constant SEP => 9;
  2         2  
  2         99  
116 2     2   8 use constant OCT => 10;
  2         3  
  2         94  
117 2     2   12 use constant NOV => 11;
  2         2  
  2         110  
118 2     2   10 use constant DEC => 12;
  2         3  
  2         101  
119              
120             # The legths of the various terms
121 2     2   11 use constant EIGHTY_DAYS => DateTime::Duration->new( days => 80 );
  2         6  
  2         18  
122 2     2   409 use constant SEVENTY_DAYS => DateTime::Duration->new( days => 70 );
  2         3  
  2         7  
123 2     2   242 use constant SIXTY_DAYS => DateTime::Duration->new( days => 60 );
  2         3  
  2         8  
124 2     2   245 use constant FIFTYTHREE_DAYS => DateTime::Duration->new( days => 53 );
  2         3  
  2         8  
125 2     2   219 use constant THREE_DAYS => DateTime::Duration->new( days => 3 );
  2         2  
  2         8  
126 2     2   242 use constant FOUR_DAYS => DateTime::Duration->new( days => 4 );
  2         2  
  2         7  
127              
128             =head1 METHODS
129              
130             =over 4
131              
132             =item * Ucam::Term->new()
133              
134             Create a new Ucam::Term object
135              
136             my $term = Ucam::Term->new('michaelmas',2010);
137              
138             Requires two arguments: term and year. Term can be 'm', 'mich',
139             'michaelmas', 'l', 'lent', 'e', 'easter' in any mixture of case. Year
140             should be the full 4-digit year number. Croaks if given an
141             unrecognised term. Otherwise returns a new Ucam::Term object.
142              
143             =cut
144              
145             sub new {
146              
147 41     41 1 20629 my $class = shift;
148 41         71 my ($term,$year) = @_;
149              
150 41         75 my $self = bless({}, $class);
151              
152 41 100       308 if ($term =~ /^m(ics(aelmas)?)?/i) {
    100          
    100          
153 6         8 $term = 'm';
154             }
155             elsif ($term =~ /^l(ent)?/i) {
156 3         5 $term = 'l';
157             }
158             elsif ($term =~ /^e(aster)?/i) {
159 31         56 $term = 'e';
160             }
161             else {
162 1         22 croak ("Unrecognised term: '$term'");
163             }
164              
165 40         81 $self->{term} = $term;
166 40         56 $self->{year} = $year;
167 40         56 $self->{cache} = {};
168              
169 40         89 return $self;
170              
171             }
172              
173             =item * Ucam::Term->available_years()
174              
175             List years with useful information
176              
177             my @years = Ucam::Term->available_years
178              
179             Returns a sorted list of years for which at least some term date
180             information is available. Some years returned will not have dates for
181             all terms available (typically the first year will have only
182             Michaelemas defined, the last only Lent and Easter). Convenient for
183             iterating over all information available.
184              
185             =cut
186              
187             sub available_years {
188              
189 2     2 1 365 return sort keys %{FULL_TERM_START()};
  2         31  
190              
191             }
192              
193             =item * $term->name()
194              
195             Extract term name
196              
197             my $name = $term->name
198              
199             Return the full, human-readable name of the term represented by the
200             object ('Michaelmas', 'Lent', or 'Easter')
201              
202             =cut
203              
204             sub name {
205 1     1 1 254 my $self = shift;
206              
207 1         5 return TERM_NAME->{$self->{term}};
208              
209             }
210              
211             =item * $term->year()
212              
213             Extract term year
214              
215             my $year = $term->year
216              
217             Return the year of the term represented by the object.
218              
219             =cut
220              
221             sub year {
222 25     25 1 34 my $self = shift;
223              
224 25         85 return $self->{year};
225              
226             }
227              
228             =item * $term->dates()
229              
230             Return the term start/end dates for the object
231              
232             my $span = $term->dates
233              
234             Return a DateTime::Span representing the corresponding term. This runs
235             from 00:00 on the first day of term (inclusive) to 00:00 on the day
236             after term ends (exclusive). Returns undef unless the module has data
237             for this term (even though this is really only needed for Easter terms)
238              
239             =cut
240              
241             sub dates {
242 41     41 1 4381 my $self = shift;
243              
244 41         108 my $term_start = FULL_TERM_START->{$self->{year}}->{$self->{term}};
245 41 100       93 return undef unless defined($term_start);
246              
247             # Extract from cache if available
248 39 100       118 return $self->{cache}->{term} if $self->{cache}->{term};
249              
250 27         31 my ($start, $duration);
251              
252             # "The Michaelmas Term shall begin on 1 October and shall consist of
253             # eighty days, ending on 19 December"
254 27 100       100 if ($self->{term} eq 'm') {
    100          
    50          
255             $start = DateTime->new(year=>$self->{year},
256 1         16 month=>OCT,
257             day=>1);
258 1         374 $duration = EIGHTY_DAYS;
259             }
260              
261             # "The Lent Term shall begin on 5 January and shall consist of eighty
262             # days, ending on 25 March or in any leap year on 24 March"
263             elsif ($self->{term} eq 'l') {
264             $start = DateTime->new(year=>$self->{year},
265 1         5 month=>JAN,
266             day=>5);
267 1         192 $duration = EIGHTY_DAYS;
268             }
269              
270             # "The Easter Term shall begin on 10 April and shall consist of seventy
271             # days ending on 18 June, provided that in any year in which Full
272             # Easter Term begins on or after 22 April the Easter Term shall begin
273             # on 17 April and end on 25 June"
274             elsif ($self->{term} eq 'e') {
275 25 100       39 if ($term_start >= 22) {
276             $start = DateTime->new(year=>$self->{year},
277 19         72 month=>APR,
278             day=>17);
279             }
280             else {
281             $start = DateTime->new(year=>$self->{year},
282 6         22 month=>APR,
283             day=>10);
284             }
285 25         4696 $duration = SEVENTY_DAYS;
286             }
287              
288             else {
289 0         0 croak ('This can\'t happen - unrecognised term');
290             }
291              
292 27         83 my $result = DateTime::Span->
293             from_datetime_and_duration(start => $start,
294             duration => $duration);
295              
296             # Cache the result and return it
297 27         32438 $self->{cache}->{term} = $result;
298 27         58 return $result;
299              
300             }
301              
302             =item * $term->fullterm_dates()
303              
304             Return the full term start/end dates for the object
305              
306             my $span = $term->fullterm_dates
307              
308             Return a DateTime::Span representing the corresponding full term. This
309             runs from 00:00 on the first day of full term (inclusive) to 00:00 on
310             the day after full term ends (exclusive). Returns undef unless the
311             module has data for this term.
312              
313             =cut
314              
315             sub fullterm_dates {
316 33     33 1 2408 my $self = shift;
317              
318 33         71 my $term_start = FULL_TERM_START->{$self->{year}}->{$self->{term}};
319 33 100       68 return undef unless defined($term_start);
320              
321             # Extract from cache if available
322 32 100       77 return $self->{cache}->{fullterm} if $self->{cache}->{fullterm};
323              
324 28         26 my ($start,$duration);
325              
326             # "Full Term shall consist of three-fourths of the whole term
327             # reckoned from the first day of Full Term as hereinafter determined"
328              
329 28 100       119 if ($self->{term} eq 'm') {
    100          
    50          
330             $start = DateTime->new(year=>$self->{year},
331 1         4 month=>OCT,
332             day=>$term_start);
333 1         186 $duration = SIXTY_DAYS;
334             }
335              
336             elsif ($self->{term} eq 'l') {
337             $start = DateTime->new(year=>$self->{year},
338 1         5 month=>JAN,
339             day=>$term_start);
340 1         176 $duration = SIXTY_DAYS;
341             }
342              
343             elsif ($self->{term} eq 'e') {
344             $start = DateTime->new(year=>$self->{year},
345 26         82 month=>APR,
346             day=>$term_start);
347 26         4733 $duration = FIFTYTHREE_DAYS;
348             }
349              
350             else {
351 0         0 croak ('This can\'t happen - unrecognised term');
352             }
353              
354 28         80 my $result = DateTime::Span->
355             from_datetime_and_duration(start => $start,
356             duration => $duration);
357              
358              
359             # Cache the result and return it
360 28         33446 $self->{cache}->{fullterm} = $result;
361 28         98 return $result;
362              
363             }
364              
365             =item * $term->division()
366              
367             Returns the date of the division of term
368              
369             $division = $term->division
370              
371             Returns a DateTime object representing 00:00 on the day corresponding
372             to the division of term.
373              
374             =cut
375              
376             sub division {
377 5     5 1 1758 my $self = shift;
378              
379 5         18 my $term_start = FULL_TERM_START->{$self->{year}}->{$self->{term}};
380 5 100       15 return undef unless defined($term_start);
381              
382             # Extract from cache if available
383 4 50       9 return $self->{cache}->{division} if $self->{cache}->{division};
384              
385             # http://www.cam.ac.uk/univ/termdates.html: "Division of Term is
386             # half-way through Term (not Full Term). The dates are the same for
387             # every year except for Easter term: 9 November, 13 February, and 14
388             # May or 21 May depending on whether Easter Term starts on 10 April or
389             # 17 April"
390              
391 4         5 my $division;
392              
393 4 100       20 if ($self->{term} eq 'm') {
    100          
    50          
394             $division = DateTime->new(year=>$self->{year},
395 1         4 month=>NOV,
396             day=>9);
397             }
398              
399             elsif ($self->{term} eq 'l') {
400             $division = DateTime->new(year=>$self->{year},
401 1         6 month=>FEB,
402             day=>13);
403             }
404              
405             elsif ($self->{term} eq 'e') {
406 2 100       6 if ($term_start >= 22) {
407             $division = DateTime->new(year=>$self->{year},
408 1         5 month=>MAY,
409             day=>21);
410             }
411             else {
412             $division = DateTime->new(year=>$self->{year},
413 1         6 month=>MAY,
414             day=>14);
415             }
416             }
417              
418             else {
419 0         0 croak ('This can\'t happen - unrecognised term');
420             }
421              
422              
423             # Cache the result and return it
424 4         918 $self->{cache}->{division} = $division;
425 4         18 return $division;
426              
427             }
428              
429             =item * $term->general_admission()
430              
431             Return the start/end dates for General Admission based on the table
432             that appears in Ordinances, Chapter II, Section 10 'Dates of Term and
433             Full Term' (see general_admission_alg() for what should be the same
434             data derived from the algorythm in Ordinances, Chapter II, Section 12
435             'Admission to Degrees')
436              
437             my $span = $term->general_admission
438              
439             Return a DateTime::Span representing the period of General Admission
440             following an Easter term. The span runs from 00:00 on the first day of
441             General Admission (inclusive) to 00:00 on the day after the final day
442             (exclusive). Returns undef unless the module has data for this
443             term. Croaks if called on an object that doesn't represent an Easter
444             term.
445              
446             =cut
447              
448             sub general_admission {
449 70     70 1 15656 my $self = shift;
450              
451             croak "Can only call general_admission() on an Easter term object"
452 70 100       202 unless $self->{term} eq 'e';
453              
454 68         153 my $term_start = FULL_TERM_START->{$self->{year}}->{$self->{term}};
455 68 50       125 return undef unless defined($term_start);
456              
457             # Extract from cache if available
458 68 100       214 return $self->{cache}->{ga} if $self->{cache}->{ga};
459              
460             # General admission can fall in June or July
461 28         53 my $dayone = FULL_TERM_START->{$self->{year}}->{'g'};
462 28 100       57 my $month = $dayone > 15 ? JUN : JUL;
463             my $start = DateTime->new(year=>$self->{year},
464 28         95 month=>$month,
465             day=>$dayone);
466              
467             # Upto 2013, GA was three day, from 2014 it's 4 dayss
468 28 100       5706 my $duration = $self->{year} <= 2013 ? THREE_DAYS : FOUR_DAYS;
469              
470 28         95 my $ga = DateTime::Span->
471             from_datetime_and_duration(start => $start,
472             duration => $duration);
473             # Cache the result and return it
474 28         34656 $self->{cache}->{ga} = $ga;
475 28         92 return $ga;
476              
477             }
478              
479             =item * $term->general_admission_alg()
480              
481             Return the start/end dates for General Admission based on the
482             algorythm that appears in Ordinances, Chapter II, Section 12
483             'Admission to Degrees' (see general_admission() for what should be the
484             same data derived from the algorythm in Ordinances, Chapter II,
485             Section 10 'Dates of Trem and Full Term' )
486              
487             my $span = $term->general_admission_alg
488              
489             Return a DateTime::Span representing the period of General Admission
490             following an Easter term. The span runs from 00:00 on the first day of
491             General Admission (inclusive) to 00:00 on the day after the final day
492             (exclusive). Returns undef unless the module has data for this
493             term. Croaks if called on an object that doesn't represent an Easter
494             term.
495              
496             =cut
497              
498             sub general_admission_alg {
499 48     48 1 2767 my $self = shift;
500              
501             croak "Can only call general_admission() on an Easter term object"
502 48 50       104 unless $self->{term} eq 'e';
503              
504 48         93 my $term_start = FULL_TERM_START->{$self->{year}}->{$self->{term}};
505 48 50       83 return undef unless defined($term_start);
506              
507             # Extract from cache if available
508 48 100       135 return $self->{cache}->{ga_alg} if $self->{cache}->{ga_alg};
509            
510             # In Ordnances upto the set published in 2013 the rule was:
511             #
512             # "In every year the Thursday, Friday, and Saturday after the third
513             # Sunday in June shall be days of General Admission to Degrees, save
514             # that, in accordance with Regulation 3 for Terms and Long Vacation,
515             # in any year in which Full Easter Term begins on or after 22 April
516             # the days of General Admission shall be the Thursday, Friday, and
517             # Saturday after the fourth Sunday in June"
518             #
519             # adding "Wednesday" in 2013 itself. However this didn't actually
520             # produce the dates published (since 2010) elsewhere in Ordnances.
521             # To fix this the rule was changed from the 2014 edition onnward by
522             # Grace 2 of 5 February 2014 to be:
523             #
524             # "Every year the Wednesday, Thursday, Friday, and Saturday in the
525             # week next but one following the last week of Full Easter Term shall
526             # be days of General Admission to Degrees."
527             #
528             # Conviniently this produces the same dates as the old algorythm
529             # (when adjusted for the inclusion of Wednesday only from 2013) for
530             # all years supported by this module.
531              
532             # Saturday after the last day of Full Easter Term
533 24         54 my $start = $self->fullterm_dates->end;
534              
535             # Move to the week next but one
536 24         731 $start->add (weeks => 2);
537            
538             # Thursday or Wednesday and three or four days
539 24         14430 my $duration;
540 24 100       93 if ($self->year <= 2013) {
541 6         23 $start->subtract (days => 2);
542 6         4086 $duration = THREE_DAYS;
543             }
544             else {
545 18         60 $start->subtract (days => 3);
546 18         11820 $duration = FOUR_DAYS;
547             }
548              
549 24         108 my $ga = DateTime::Span->
550             from_datetime_and_duration(start => $start,
551             duration => $duration);
552             # Cache the result and return it
553 24         27817 $self->{cache}->{ga_alg} = $ga;
554 24         78 return $ga;
555              
556             }
557              
558             =item * $term->long_vac()
559              
560             Return the start/end dates for the 'Long Vacation period of residence'
561              
562             my $span = $term->long_vac
563              
564             Return a DateTime::Span representing the period of the 'Long Vacation
565             period of residence' that follows an Easter term. The span runs from
566             00:00 on the first day (inclusive) to 00:00 on the day after the final
567             day (exclusive). Returns undef unless the module has data for this
568             term. Croaks if called on an object that doesn't represent an Easter
569             term.
570              
571             =cut
572              
573             sub long_vac {
574 12     12 1 4917 my $self = shift;
575              
576             croak "Can only call long_vac() on an Easter term object"
577 12 100       50 unless $self->{term} eq 'e';
578              
579 10         19 my $ga = $self->general_admission;
580 10 50       19 return undef unless defined($ga);
581              
582             # Extract from cache if available
583 10 100       45 return $self->{cache}->{lv} if $self->{cache}->{lv};
584              
585             # "A course of instruction given during the Long Vacation shall not
586             # occupy more than four weeks. Except with the approval of the
587             # Council on the recommendation of the General Board, no such course
588             # given within the Precincts of the University shall begin earlier
589             # than the second Monday after General Admission or end later than the
590             # sixth Saturday after the Saturday of General Admission"
591              
592             # "second Monday after General Admission" is 1 week
593             # and 1 day after the last 'day' (Sunday) of General
594             # Admission
595 4         14 my $start = $ga->end->clone->add(days => 1, weeks => 1);
596             # "sixth Saturday after the Saturday of General
597             # Admission" is 6 weeks less 1 day after the first 'day'
598             # (Sunday) of General Admission. Plus one to the next day
599 4         3077 my $end = $ga->end->clone->add(weeks => 6);
600              
601 4         2611 my $lv = DateTime::Span->from_datetimes(start => $start, before => $end );
602              
603             # Cache the result and return it
604 4         2934 $self->{cache}->{lv} = $lv;
605 4         18 return $lv;
606              
607             }
608              
609             =back
610              
611             =head1 AUTHOR
612              
613             Jon Warbrick, jw35@cam.ac.uk.
614              
615             =head1 SEE ALSO
616              
617             University of Cambridge Statutes and Ordnances, Chapter II
618             ("Matriculation, Residence, Admission to Degrees")
619             (L<http://www.admin.cam.ac.uk/univ/so/>) contains most of the rules
620             implemented by this module:
621              
622             =over 4
623              
624             "Section 10: DATES OF TERM AND FULL TERM"
625              
626             "1. The Michaelmas Term shall begin on 1 October and shall consist of
627             eighty days, ending on 19 December. The Lent Term shall begin on 5
628             January and shall consist of eighty days, ending on 25 March or in any
629             leap year on 24 March. The Easter Term shall begin on 10 April and
630             shall consist of seventy days ending on 18 June, provided that in any
631             year in which Full Easter Term begins on or after 22 April the Easter
632             Term shall begin on 17 April and end on 25 June.
633              
634             "2. Full Term shall consist of three-fourths of the whole term
635             reckoned from the first day of Full Term as hereinafter determined.
636              
637             "3. The dates on which Full Terms begin and end shall be as shown in
638             the table appended to these regulations.
639              
640             "...
641              
642             "8. A course of instruction given during the Long Vacation shall not
643             occupy more than four weeks. Except with the approval of the Council
644             on the recommendation of the General Board, no such course given
645             within the Precincts of the University shall begin earlier than the
646             second Monday after General Admission or end later than the sixth
647             Saturday after the Saturday of General Admission."
648              
649             "Section 12: ADMISSION TO DEGREES"
650              
651             [Prior to 2013]:
652              
653             "13. In every year the Thursday, Friday, and Saturday after the third
654             Sunday in June shall be days of General Admission to Degrees, save
655             that, in accordance with Regulation 3 for Terms and Long Vacation, in
656             any year in which Full Easter Term begins on or after 22 April the
657             days of General Admission shall be the Thursday, Friday, and Saturday
658             after the fourth Sunday in June. On each day of General Admission
659             there shall be one or more Congregations for General Admission to
660             Degrees at such hours as the Vice-Chancellor shall appoint."
661              
662             [As ammended by Grace 1 of 13 February 2013
663             (http://www.admin.cam.ac.uk/reporter/2012-13/weekly/6297/section6.shtml)
664             with effect from 1 October 2013]:
665              
666             "13. Every year the Wednesday, Thursday, Friday, and Saturday after the
667             third Sunday in June shall be days of General Admission to Degrees,
668             save that, in accordance to Regulation 3 for Terms and Long Vacation,
669             in any year in which Full Easter Term begins on or after 22 April the
670             days of General Admission shall be the Wednesday, Thursday, Friday,
671             and Saturday after the fourth Sunday in June. On each day of General
672             Admission there shall be one or more Congregations for General
673             Admission to Degrees at such hours as the Vice-Chancellor shall
674             appoint."
675              
676             [As ammended by Grace 2 of 5 February 2014
677             (http://www.admin.cam.ac.uk/reporter/2013-14/weekly/6336/section8.shtml)]
678              
679             "14. Every year the Wednesday, Thursday, Friday, and Saturday in the
680             week next but one following the last week of Full Easter Term shall be
681             days of General Admission to Degrees."
682              
683             [In practice, the dates of General Admission as given in the table in
684             Section 10: 'Dates of Term and Full Term' appear to be cannonical -
685             between 2010 and 2014 the dates in the table and the ordnance diagreed
686             and it was the ordanance that was updated]
687              
688             =back
689              
690             Division of Term is best described in
691             L<http://www.cam.ac.uk/univ/termdates.html>:
692              
693             =over 4
694              
695             "Division of Term is half-way through Term (not Full Term). The dates
696             are the same for every year except for Easter term: 9 November, 13
697             February, and 14 May or 21 May depending on whether Easter Term starts
698             on 10 April or 17 April."
699              
700             =back
701              
702             See also L<DateTime>, L<DateTime::Span>
703              
704             =head1 COPYRIGHT and LICENSE
705              
706             Copyright (c) 2010, 2013, 2017 University of Cambridge Information
707             Services. This program is free software; you can distribute it and/or
708             modify it under the same terms as Perl itself.
709              
710             The full text of the license can be found in the LICENSE file included
711             with this module.