1 """
2 Terms Routines
4 This module contains functions for manipulating terms, such as determining
5 the current term, finding the next or previous term, converting dates to
6 terms, and more.
7 """
8 import time, datetime, re
10 # year to count terms from
11 EPOCH = 1970
13 # seasons list
14 SEASONS = [ 'w', 's', 'f' ]
17 def validate(term):
18     """
19     Determines whether a term is well-formed.
21     Parameters:
22         term - the term string
24     Returns: whether the term is valid (boolean)
26     Example: validate("f2006") -> True
27     """
29     regex = '^[wsf][0-9]{4}\$'
30     return re.match(regex, term) is not None
33 def parse(term):
34     """Helper function to convert a term string to the number of terms
35        since the epoch. Such numbers are intended for internal use only."""
37     if not validate(term):
38         raise Exception("malformed term: %s" % term)
40     year = int( term[1:] )
41     season = SEASONS.index( term )
43     return (year - EPOCH) * len(SEASONS) + season
46 def generate(term):
47     """Helper function to convert a year and season to a term string."""
49     year = int(term / len(SEASONS)) + EPOCH
50     season = term % len(SEASONS)
52     return "%s%04d" % ( SEASONS[season], year )
55 def next(term):
56     """
57     Returns the next term. (convenience function)
59     Parameters:
60         term - the term string
62     Retuns: the term string of the following term
64     Example: next("f2006") -> "w2007"
65     """
70 def previous(term):
71     """
72     Returns the previous term. (convenience function)
74     Parameters:
75         term - the term string
77     Returns: the term string of the preceding term
79     Example: previous("f2006") -> "s2006"
80     """
86     """
87     Calculates a term relative to some base term.
89     Parameters:
90         term   - the base term
91         offset - the number of terms since term (may be negative)
93     Returns: the term that comes offset terms after term
94     """
96     return generate(parse(term) + offset)
99 def delta(initial, final):
100     """
101     Calculates the distance between two terms.
102     It should be true that add(a, delta(a, b)) == b.
104     Parameters:
105         initial - the base term
106         final   - the term at some offset from the base term
108     Returns: the offset of final relative to initial
109     """
111     return parse(final) - parse(initial)
114 def compare(first, second):
115     """
116     Compares two terms. This function is suitable
117     for use with list.sort().
119     Parameters:
120         first  - base term for comparison
121         second - term to compare to
123     Returns: > 0 (if first >  second)
124              = 0 (if first == second)
125              < 0 (if first <  second)
126     """
127     return delta(second, first)
130 def interval(base, count):
131     """
132     Returns a list of adjacent terms.
134     Parameters:
135         base    - the first term in the interval
136         count   - the number of terms to include
138     Returns: a list of count terms starting with initial
140     Example: interval('f2006', 3) -> [ 'f2006', 'w2007', 's2007' ]
141     """
143     terms = []
145     for num in xrange(count):
148     return terms
151 def tstamp(timestamp):
152     """Helper to convert seconds since the epoch
153     to terms since the epoch."""
155     # let python determine the month and year
156     date = datetime.date.fromtimestamp(timestamp)
158     # determine season
159     if date.month <= 4:
160         season = SEASONS.index('w')
161     elif date.month <= 8:
162         season = SEASONS.index('s')
163     else:
164         season = SEASONS.index('f')
166     return (date.year - EPOCH) * len(SEASONS) + season
169 def from_timestamp(timestamp):
170     """
171     Converts a number of seconds since
172     the epoch to a number of terms since
173     the epoch.
175     This function notes that:
176         WINTER = JANUARY to APRIL
177         SPRING = MAY to AUGUST
178         FALL   = SEPTEMBER to DECEMBER
180     Parameters:
181         timestamp - number of seconds since the epoch
183     Returns: the number of terms since the epoch
185     Example: from_timestamp(1166135779) -> 'f2006'
186     """
188     return generate( tstamp(timestamp) )
191 def curr():
192     """Helper to determine the current term."""
194     return tstamp( time.time() )
197 def current():
198     """
199     Determines the current term.
201     Returns: current term
203     Example: current() -> 'f2006'
204     """
206     return generate( curr() )
209 def next_unregistered(registered):
210     """
211     Find the first future or current unregistered term.
212     Intended as the 'default' for registrations.
214     Parameters:
215         registered - a list of terms a member is registered for
217     Returns: the next unregistered term
218     """
220     # get current term number
221     now = curr()
223     # never registered -> current term is next
224     if len( registered) < 1:
225         return generate( now )
227     # return the first unregistered, or the current term (whichever is greater)
228     return generate(max([max(map(parse, registered))+1, now]))
232 ### Tests ###
234 if __name__ == '__main__':
236     from ceo.test import test, assert_equal, success
238     test(parse); assert_equal(110, parse('f2006')); success()
239     test(generate); assert_equal('f2006', generate(110)); success()
240     test(next); assert_equal('w2007', next('f2006')); success()
241     test(previous); assert_equal('s2006', previous('f2006')); success()
242     test(delta); assert_equal(1, delta('f2006', 'w2007')); success()
243     test(compare); assert_equal(-1, compare('f2006', 'w2007')); success()