1 # \$Id: terms.py 44 2006-12-31 07:09:27Z mspang \$
2 """
3 Terms Routines
5 This module contains functions for manipulating
6 terms, such as determining the current term,
7 finding the next or previous term, converting
8 dates to terms, and more.
9 """
10 import time, datetime, re
12 # year to count terms from
13 EPOCH = 1970
15 # seasons list
16 SEASONS = [ 'w', 's', 'f' ]
19 def valid(term):
20     """
21     Determines whether a term is well-formed:
23     Parameters:
24         term - the term string
26     Returns: whether the term is valid (boolean)
28     Example: valid("f2006") -> True
29     """
31     regex = '^[wsf][0-9]{4}\$'
32     return re.match(regex, term) != None
35 def parse(term):
36     """Helper function to convert a term string to the number of terms
37        since the epoch. Such numbers are intended for internal use only."""
39     if not valid(term):
40         raise Exception("malformed term: %s" % term)
42     year = int( term[1:] )
43     season = SEASONS.index( term )
45     return (year - EPOCH) * len(SEASONS) + season
48 def generate(term):
49     """Helper function to convert a year and season to a term string."""
51     year = int(term / len(SEASONS)) + EPOCH
52     season = term % len(SEASONS)
54     return "%s%04d" % ( SEASONS[season], year )
57 def next(term):
58     """
59     Returns the next term. (convenience function)
61     Parameters:
62         term - the term string
64     Retuns: the term string of the following term
66     Example: next("f2006") -> "w2007"
67     """
69     return add(term, 1)
72 def previous(term):
73     """
74     Returns the previous term. (convenience function)
76     Parameters:
77         term - the term string
79     Returns: the term string of the preceding term
81     Example: previous("f2006") -> "s2006"
82     """
84     return add(term, -1)
87 def add(term, offset):
88     """
89     Calculates a term relative to some base term.
91     Parameters:
92         term   - the base term
93         offset - the number of terms since term (may be negative)
95     Returns: the term that comes offset terms after term
96     """
98     return generate(parse(term) + offset)
101 def delta(initial, final):
102     """
103     Calculates the distance between two terms.
104     It should be true that add(a, delta(a, b)) == b.
106     Parameters:
107         initial - the base term
108         final   - the term at some offset from the base term
110     Returns: the offset of final relative to initial
111     """
113     return parse(final) - parse(initial)
116 def compare(first, second):
117     """
118     Compares two terms. This function is suitable
119     for use with list.sort().
121     Parameters:
122         first  - base term for comparison
123         second - term to compare to
125     Returns: > 0 (if first >  second)
126              = 0 (if first == second)
127              < 0 (if first <  second)
128     """
129     return delta(second, first)
132 def interval(base, count):
133     """
134     Returns a list of adjacent terms.
136     Parameters:
137         base    - the first term in the interval
138         count   - the number of terms to include
140     Returns: a list of count terms starting with initial
142     Example: interval('f2006', 3) -> [ 'f2006', 'w2007', 's2007' ]
143     """
145     terms = []
147     for num in xrange(count):
148         terms.append( add(base, num) )
150     return terms
153 def tstamp(timestamp):
154     """Helper to convert seconds since the epoch
155     to terms since the epoch."""
157     # let python determine the month and year
158     date = datetime.date.fromtimestamp(timestamp)
160     # determine season
161     if date.month <= 4:
162         season = SEASONS.index('w')
163     elif date.month <= 8:
164         season = SEASONS.index('s')
165     else:
166         season = SEASONS.index('f')
168     return (date.year - EPOCH) * len(SEASONS) + season
171 def from_timestamp(timestamp):
172     """
173     Converts a number of seconds since
174     the epoch to a number of terms since
175     the epoch.
177     This function notes that:
178         WINTER = JANUARY to APRIL
179         SPRING = MAY TO AUGUST
180         FALL   = SEPTEMBER TO DECEMBER
182     Parameters:
183         timestamp - number of seconds since the epoch
185     Returns: the number of terms since the epoch
187     Example: from_timestamp(1166135779) -> 'f2006'
188     """
190     return generate( tstamp(timestamp) )
193 def curr():
194     """Helper to determine the current term."""
196     return tstamp( time.time() )
199 def current():
200     """
201     Determines the current term.
203     Returns: current term
205     Example: current() -> 'f2006'
206     """
208     return generate( curr() )
211 def next_unregistered(registered):
212     """
213     Find the first future or current unregistered term.
214     Intended as the 'default' for registrations.
216     Parameters:
217         registered - a list of terms a member is registered for
219     Returns: the next unregistered term
220     """
222     # get current term number
223     now = curr()
225     # never registered -> current term is next
226     if len( registered) < 1:
227         return generate( now )
229     # return the first unregistered, or the current term (whichever is greater)
230     return generate(max([max(map(parse, registered))+1, now]))
234 ### Tests ###
236 if __name__ == '__main__':
238     assert parse('f2006') == 110
239     assert generate(110) == 'f2006'
240     assert next('f2006') == 'w2007'
241     assert previous('f2006') == 's2006'
242     assert delta('f2006', 'w2007') == 1
243     assert add('f2006', delta('f2006', 'w2010')) == 'w2010'
244     assert interval('f2006', 3) == ['f2006', 'w2007', 's2007']
245     assert from_timestamp(1166135779) == 'f2006'
246     assert parse( current() ) >= 110
247     assert next_unregistered( [current()] ) == next( current() )
248     assert next_unregistered( [] ) == current()
249     assert next_unregistered( [previous(current())] ) == current()
250     assert next_unregistered( [add(current(), -2)] ) == current()
252     print "All tests passed." "\n"