It's doubtful that a user would need to mount a floppy disk
[mspang/pyceo.git] / ceo / terms.py
1 """
2 Terms Routines
3
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
9
10 # year to count terms from
11 EPOCH = 1970
12
13 # seasons list
14 SEASONS = [ 'w', 's', 'f' ]
15
16
17 def validate(term):
18     """
19     Determines whether a term is well-formed.
20
21     Parameters:
22         term - the term string
23
24     Returns: whether the term is valid (boolean)
25
26     Example: validate("f2006") -> True
27     """
28
29     regex = '^[wsf][0-9]{4}$'
30     return re.match(regex, term) is not None
31
32
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."""
36
37     if not validate(term):
38         raise Exception("malformed term: %s" % term)
39
40     year = int( term[1:] )
41     season = SEASONS.index( term[0] )
42
43     return (year - EPOCH) * len(SEASONS) + season
44
45
46 def generate(term):
47     """Helper function to convert a year and season to a term string."""
48     
49     year = int(term / len(SEASONS)) + EPOCH
50     season = term % len(SEASONS)
51     
52     return "%s%04d" % ( SEASONS[season], year )
53
54
55 def next(term):
56     """
57     Returns the next term. (convenience function)
58
59     Parameters:
60         term - the term string
61
62     Retuns: the term string of the following term
63
64     Example: next("f2006") -> "w2007"
65     """
66     
67     return add(term, 1)
68
69
70 def previous(term):
71     """
72     Returns the previous term. (convenience function)
73
74     Parameters:
75         term - the term string
76
77     Returns: the term string of the preceding term
78
79     Example: previous("f2006") -> "s2006"
80     """
81
82     return add(term, -1)
83
84
85 def add(term, offset):
86     """
87     Calculates a term relative to some base term.
88     
89     Parameters:
90         term   - the base term
91         offset - the number of terms since term (may be negative)
92
93     Returns: the term that comes offset terms after term
94     """
95
96     return generate(parse(term) + offset)
97
98
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.
103
104     Parameters:
105         initial - the base term
106         final   - the term at some offset from the base term
107
108     Returns: the offset of final relative to initial
109     """
110
111     return parse(final) - parse(initial)
112
113
114 def compare(first, second):
115     """
116     Compares two terms. This function is suitable
117     for use with list.sort().
118
119     Parameters:
120         first  - base term for comparison
121         second - term to compare to
122
123     Returns: > 0 (if first >  second)
124              = 0 (if first == second)
125              < 0 (if first <  second)
126     """
127     return delta(second, first)
128              
129
130 def interval(base, count):
131     """
132     Returns a list of adjacent terms.
133
134     Parameters:
135         base    - the first term in the interval
136         count   - the number of terms to include
137
138     Returns: a list of count terms starting with initial
139
140     Example: interval('f2006', 3) -> [ 'f2006', 'w2007', 's2007' ]
141     """
142     
143     terms = []
144
145     for num in xrange(count):
146         terms.append( add(base, num) )
147     
148     return terms
149         
150
151 def tstamp(timestamp):
152     """Helper to convert seconds since the epoch
153     to terms since the epoch."""
154
155     # let python determine the month and year
156     date = datetime.date.fromtimestamp(timestamp)
157
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')
165
166     return (date.year - EPOCH) * len(SEASONS) + season
167
168
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.
174
175     This function notes that:
176         WINTER = JANUARY to APRIL
177         SPRING = MAY to AUGUST
178         FALL   = SEPTEMBER to DECEMBER
179     
180     Parameters:
181         timestamp - number of seconds since the epoch
182
183     Returns: the number of terms since the epoch
184
185     Example: from_timestamp(1166135779) -> 'f2006'
186     """
187
188     return generate( tstamp(timestamp) )
189     
190
191 def curr():
192     """Helper to determine the current term."""
193
194     return tstamp( time.time() )
195
196
197 def current():
198     """
199     Determines the current term.
200
201     Returns: current term
202
203     Example: current() -> 'f2006'
204     """
205
206     return generate( curr() )
207     
208
209 def next_unregistered(registered):
210     """
211     Find the first future or current unregistered term.
212     Intended as the 'default' for registrations.
213
214     Parameters:
215         registered - a list of terms a member is registered for
216
217     Returns: the next unregistered term
218     """
219     
220     # get current term number
221     now = curr()
222
223     # never registered -> current term is next
224     if len( registered) < 1:
225         return generate( now )
226
227     # return the first unregistered, or the current term (whichever is greater)
228     return generate(max([max(map(parse, registered))+1, now]))
229
230
231
232 ### Tests ###
233
234 if __name__ == '__main__':
235
236     from ceo.test import test, assert_equal, success
237
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()
244     test(add); assert_equal('w2010', add('f2006', delta('f2006', 'w2010'))); success()
245     test(interval); assert_equal(['f2006', 'w2007', 's2007'], interval('f2006', 3)); success()
246     test(from_timestamp); assert_equal('f2006', from_timestamp(1166135779)); success()
247     test(current); assert_equal(True, parse( current() ) >= 110 ); success()
248
249     test(next_unregistered)
250     assert_equal( next(current()), next_unregistered([ current() ]))
251     assert_equal( current(), next_unregistered([]))
252     assert_equal( current(), next_unregistered([ previous(current()) ]))
253     assert_equal( current(), next_unregistered([ add(current(), -2) ]))
254     success()