3.1 --- a/tests/internal/qualifiers.py Fri Oct 20 23:37:08 2017 +0200
3.2 +++ b/tests/internal/qualifiers.py Tue Oct 24 20:33:02 2017 +0200
3.3 @@ -69,7 +69,8 @@
3.4 print
3.5
3.6 qualifiers = [
3.7 - ("DAILY", {"interval" : 1})
3.8 + ("DAILY", {"interval" : 1}),
3.9 + ("COUNT", {"values" : [10]})
3.10 ]
3.11
3.12 l = order_qualifiers(qualifiers)
3.13 @@ -81,7 +82,7 @@
3.14 show(l)
3.15
3.16 s = get_selector(dt, qualifiers)
3.17 -l = s.materialise(dt, (1997, 12, 24), 10)
3.18 +l = s.materialise(dt, (1997, 12, 24))
3.19 print len(l) == 10, 10, len(l)
3.20 print l[0] == (1997, 9, 2, 9, 0, 0), (1997, 9, 2, 9, 0, 0), l[0]
3.21 print l[-1] == (1997, 9, 11, 9, 0, 0), (1997, 9, 11, 9, 0, 0), l[-1]
3.22 @@ -145,7 +146,8 @@
3.23 print
3.24
3.25 qualifiers = [
3.26 - ("DAILY", {"interval" : 10})
3.27 + ("DAILY", {"interval" : 10}),
3.28 + ("COUNT", {"values" : [5]})
3.29 ]
3.30
3.31 l = order_qualifiers(qualifiers)
3.32 @@ -157,7 +159,7 @@
3.33 show(l)
3.34
3.35 s = get_selector(dt, qualifiers)
3.36 -l = s.materialise(dt, (1997, 12, 24, 0, 0, 0), 5)
3.37 +l = s.materialise(dt, (1997, 12, 24, 0, 0, 0))
3.38 print len(l) == 5, 5, len(l)
3.39 print l[0] == (1997, 9, 2, 9, 0, 0), (1997, 9, 2, 9, 0, 0), l[0]
3.40 print l[-1] == (1997, 10, 12, 9, 0, 0), (1997, 10, 12, 9, 0, 0), l[-1]
3.41 @@ -205,7 +207,8 @@
3.42 print
3.43
3.44 qualifiers = [
3.45 - ("WEEKLY", {"interval" : 1})
3.46 + ("WEEKLY", {"interval" : 1}),
3.47 + ("COUNT", {"values" : [10]})
3.48 ]
3.49
3.50 l = order_qualifiers(qualifiers)
3.51 @@ -217,7 +220,7 @@
3.52 show(l)
3.53
3.54 s = get_selector(dt, qualifiers)
3.55 -l = s.materialise(dt, (1997, 12, 24, 0, 0, 0), 10)
3.56 +l = s.materialise(dt, (1997, 12, 24, 0, 0, 0))
3.57 print len(l) == 10, 10, len(l)
3.58 print l[0] == (1997, 9, 2, 9, 0, 0), (1997, 9, 2, 9, 0, 0), l[0]
3.59 print l[-1] == (1997, 11, 4, 9, 0, 0), (1997, 11, 4, 9, 0, 0), l[-1]
3.60 @@ -243,6 +246,25 @@
3.61 print
3.62
3.63 qualifiers = [
3.64 + ("WEEKLY", {"interval" : 1})
3.65 + ]
3.66 +
3.67 +l = order_qualifiers(qualifiers)
3.68 +show(l)
3.69 +dt = (1997, 9, 2)
3.70 +l = get_datetime_structure(dt)
3.71 +show(l)
3.72 +l = combine_datetime_with_qualifiers(dt, qualifiers)
3.73 +show(l)
3.74 +
3.75 +s = get_selector(dt, qualifiers)
3.76 +l = s.materialise(dt, (1997, 12, 24, 0, 0, 0))
3.77 +print len(l) == 17, 17, len(l)
3.78 +print l[0] == (1997, 9, 2), (1997, 9, 2), l[0]
3.79 +print l[-1] == (1997, 12, 23), (1997, 12, 23), l[-1]
3.80 +print
3.81 +
3.82 +qualifiers = [
3.83 ("WEEKLY", {"interval" : 2})
3.84 ]
3.85
3.86 @@ -283,7 +305,8 @@
3.87
3.88 qualifiers = [
3.89 ("WEEKLY", {"interval" : 1}),
3.90 - ("BYDAY", {"values" : [(2, None), (4, None)]})
3.91 + ("BYDAY", {"values" : [(2, None), (4, None)]}),
3.92 + ("COUNT", {"values" : [10]})
3.93 ]
3.94
3.95 l = order_qualifiers(qualifiers)
3.96 @@ -295,7 +318,7 @@
3.97 show(l)
3.98
3.99 s = get_selector(dt, qualifiers)
3.100 -l = s.materialise(dt, (1997, 12, 24, 0, 0, 0), 10)
3.101 +l = s.materialise(dt, (1997, 12, 24, 0, 0, 0))
3.102 print len(l) == 10, 10, len(l)
3.103 print l[0] == (1997, 9, 2, 9, 0, 0), (1997, 9, 2, 9, 0, 0), l[0]
3.104 print l[-1] == (1997, 10, 2, 9, 0, 0), (1997, 10, 2, 9, 0, 0), l[-1]
3.105 @@ -323,7 +346,8 @@
3.106
3.107 qualifiers = [
3.108 ("WEEKLY", {"interval" : 2}),
3.109 - ("BYDAY", {"values" : [(2, None), (4, None)]})
3.110 + ("BYDAY", {"values" : [(2, None), (4, None)]}),
3.111 + ("COUNT", {"values" : [8]})
3.112 ]
3.113
3.114 l = order_qualifiers(qualifiers)
3.115 @@ -335,7 +359,7 @@
3.116 show(l)
3.117
3.118 s = get_selector(dt, qualifiers)
3.119 -l = s.materialise(dt, (1997, 12, 24, 0, 0, 0), 8)
3.120 +l = s.materialise(dt, (1997, 12, 24, 0, 0, 0))
3.121 print len(l) == 8, 8, len(l)
3.122 print l[0] == (1997, 9, 2, 9, 0, 0), (1997, 9, 2, 9, 0, 0), l[0]
3.123 print l[-1] == (1997, 10, 16, 9, 0, 0), (1997, 10, 16, 9, 0, 0), l[-1]
3.124 @@ -343,7 +367,8 @@
3.125
3.126 qualifiers = [
3.127 ("MONTHLY", {"interval" : 1}),
3.128 - ("BYDAY", {"values" : [(5, 1)]})
3.129 + ("BYDAY", {"values" : [(5, 1)]}),
3.130 + ("COUNT", {"values" : [10]})
3.131 ]
3.132
3.133 l = order_qualifiers(qualifiers)
3.134 @@ -355,7 +380,7 @@
3.135 show(l)
3.136
3.137 s = get_selector(dt, qualifiers)
3.138 -l = s.materialise(dt, (1998, 12, 24, 0, 0, 0), 10)
3.139 +l = s.materialise(dt, (1998, 12, 24, 0, 0, 0))
3.140 print len(l) == 10, 10, len(l)
3.141 print l[0] == (1997, 9, 5, 9, 0, 0), (1997, 9, 5, 9, 0, 0), l[0]
3.142 print l[-1] == (1998, 6, 5, 9, 0, 0), (1998, 6, 5, 9, 0, 0), l[-1]
3.143 @@ -383,7 +408,8 @@
3.144
3.145 qualifiers = [
3.146 ("MONTHLY", {"interval" : 2}),
3.147 - ("BYDAY", {"values" : [(7, 1), (7, -1)]})
3.148 + ("BYDAY", {"values" : [(7, 1), (7, -1)]}),
3.149 + ("COUNT", {"values" : [10]})
3.150 ]
3.151
3.152 l = order_qualifiers(qualifiers)
3.153 @@ -395,7 +421,7 @@
3.154 show(l)
3.155
3.156 s = get_selector(dt, qualifiers)
3.157 -l = s.materialise(dt, (1998, 12, 24, 0, 0, 0), 10)
3.158 +l = s.materialise(dt, (1998, 12, 24, 0, 0, 0))
3.159 print len(l) == 10, 10, len(l)
3.160 print l[0] == (1997, 9, 7, 9, 0, 0), (1997, 9, 7, 9, 0, 0), l[0]
3.161 print l[-1] == (1998, 5, 31, 9, 0, 0), (1998, 5, 31, 9, 0, 0), l[-1]
3.162 @@ -403,7 +429,8 @@
3.163
3.164 qualifiers = [
3.165 ("MONTHLY", {"interval" : 1}),
3.166 - ("BYDAY", {"values" : [(1, -2)]})
3.167 + ("BYDAY", {"values" : [(1, -2)]}),
3.168 + ("COUNT", {"values" : [6]})
3.169 ]
3.170
3.171 l = order_qualifiers(qualifiers)
3.172 @@ -415,7 +442,7 @@
3.173 show(l)
3.174
3.175 s = get_selector(dt, qualifiers)
3.176 -l = s.materialise(dt, (1998, 12, 24, 0, 0, 0), 6)
3.177 +l = s.materialise(dt, (1998, 12, 24, 0, 0, 0))
3.178 print len(l) == 6, 6, len(l)
3.179 print l[0] == (1997, 9, 22, 9, 0, 0), (1997, 9, 22, 9, 0, 0), l[0]
3.180 print l[-1] == (1998, 2, 16, 9, 0, 0), (1998, 2, 16, 9, 0, 0), l[-1]
3.181 @@ -423,7 +450,8 @@
3.182
3.183 qualifiers = [
3.184 ("MONTHLY", {"interval" : 1}),
3.185 - ("BYMONTHDAY", {"values" : [-3]})
3.186 + ("BYMONTHDAY", {"values" : [-3]}),
3.187 + ("COUNT", {"values" : [6]})
3.188 ]
3.189
3.190 l = order_qualifiers(qualifiers)
3.191 @@ -435,7 +463,7 @@
3.192 show(l)
3.193
3.194 s = get_selector(dt, qualifiers)
3.195 -l = s.materialise(dt, (1998, 12, 24, 0, 0, 0), 6)
3.196 +l = s.materialise(dt, (1998, 12, 24, 0, 0, 0))
3.197 print len(l) == 6, 6, len(l)
3.198 print l[0] == (1997, 9, 28, 9, 0, 0), (1997, 9, 28, 9, 0, 0), l[0]
3.199 print l[-1] == (1998, 2, 26, 9, 0, 0), (1998, 2, 26, 9, 0, 0), l[-1]
3.200 @@ -443,7 +471,8 @@
3.201
3.202 qualifiers = [
3.203 ("MONTHLY", {"interval" : 1}),
3.204 - ("BYMONTHDAY", {"values" : [2, 15]})
3.205 + ("BYMONTHDAY", {"values" : [15, 2]}), # test ordering
3.206 + ("COUNT", {"values" : [10]})
3.207 ]
3.208
3.209 l = order_qualifiers(qualifiers)
3.210 @@ -455,7 +484,7 @@
3.211 show(l)
3.212
3.213 s = get_selector(dt, qualifiers)
3.214 -l = s.materialise(dt, (1998, 12, 24, 0, 0, 0), 10)
3.215 +l = s.materialise(dt, (1998, 12, 24, 0, 0, 0))
3.216 print len(l) == 10, 10, len(l)
3.217 print l[0] == (1997, 9, 2, 9, 0, 0), (1997, 9, 2, 9, 0, 0), l[0]
3.218 print l[-1] == (1998, 1, 15, 9, 0, 0), (1998, 1, 15, 9, 0, 0), l[-1]
3.219 @@ -463,7 +492,8 @@
3.220
3.221 qualifiers = [
3.222 ("MONTHLY", {"interval" : 1}),
3.223 - ("BYMONTHDAY", {"values" : [1, -1]})
3.224 + ("BYMONTHDAY", {"values" : [1, -1]}),
3.225 + ("COUNT", {"values" : [10]})
3.226 ]
3.227
3.228 l = order_qualifiers(qualifiers)
3.229 @@ -475,7 +505,7 @@
3.230 show(l)
3.231
3.232 s = get_selector(dt, qualifiers)
3.233 -l = s.materialise(dt, (1998, 12, 24, 0, 0, 0), 10)
3.234 +l = s.materialise(dt, (1998, 12, 24, 0, 0, 0))
3.235 print len(l) == 10, 10, len(l)
3.236 print l[0] == (1997, 9, 30, 9, 0, 0), (1997, 9, 30, 9, 0, 0), l[0]
3.237 print l[-1] == (1998, 2, 1, 9, 0, 0), (1998, 2, 1, 9, 0, 0), l[-1]
3.238 @@ -483,7 +513,8 @@
3.239
3.240 qualifiers = [
3.241 ("MONTHLY", {"interval" : 18}),
3.242 - ("BYMONTHDAY", {"values" : [10, 11, 12, 13, 14, 15]})
3.243 + ("BYMONTHDAY", {"values" : [10, 11, 12, 13, 14, 15]}),
3.244 + ("COUNT", {"values" : [10]})
3.245 ]
3.246
3.247 l = order_qualifiers(qualifiers)
3.248 @@ -495,7 +526,7 @@
3.249 show(l)
3.250
3.251 s = get_selector(dt, qualifiers)
3.252 -l = s.materialise(dt, (1999, 12, 24, 0, 0, 0), 10)
3.253 +l = s.materialise(dt, (1999, 12, 24, 0, 0, 0))
3.254 print len(l) == 10, 10, len(l)
3.255 print l[0] == (1997, 9, 10, 9, 0, 0), (1997, 9, 10, 9, 0, 0), l[0]
3.256 print l[-1] == (1999, 3, 13, 9, 0, 0), (1999, 3, 13, 9, 0, 0), l[-1]
3.257 @@ -523,7 +554,8 @@
3.258
3.259 qualifiers = [
3.260 ("YEARLY", {"interval" : 1}),
3.261 - ("BYMONTH", {"values" : [6, 7]})
3.262 + ("BYMONTH", {"values" : [6, 7]}),
3.263 + ("COUNT", {"values" : [10]})
3.264 ]
3.265
3.266 l = order_qualifiers(qualifiers)
3.267 @@ -535,7 +567,7 @@
3.268 show(l)
3.269
3.270 s = get_selector(dt, qualifiers)
3.271 -l = s.materialise(dt, (2001, 12, 24, 0, 0, 0), 10)
3.272 +l = s.materialise(dt, (2001, 12, 24, 0, 0, 0))
3.273 print len(l) == 10, 10, len(l)
3.274 print l[0] == (1997, 6, 10, 9, 0, 0), (1997, 6, 10, 9, 0, 0), l[0]
3.275 print l[-1] == (2001, 7, 10, 9, 0, 0), (2001, 7, 10, 9, 0, 0), l[-1]
3.276 @@ -543,7 +575,8 @@
3.277
3.278 qualifiers = [
3.279 ("YEARLY", {"interval" : 2}),
3.280 - ("BYMONTH", {"values" : [1, 2, 3]})
3.281 + ("BYMONTH", {"values" : [1, 2, 3]}),
3.282 + ("COUNT", {"values" : [10]})
3.283 ]
3.284
3.285 l = order_qualifiers(qualifiers)
3.286 @@ -555,7 +588,7 @@
3.287 show(l)
3.288
3.289 s = get_selector(dt, qualifiers)
3.290 -l = s.materialise(dt, (2003, 12, 24, 0, 0, 0), 10)
3.291 +l = s.materialise(dt, (2003, 12, 24, 0, 0, 0))
3.292 print len(l) == 10, 10, len(l)
3.293 print l[0] == (1997, 3, 10, 9, 0, 0), (1997, 3, 10, 9, 0, 0), l[0]
3.294 print l[-1] == (2003, 3, 10, 9, 0, 0), (2003, 3, 10, 9, 0, 0), l[-1]
3.295 @@ -563,7 +596,8 @@
3.296
3.297 qualifiers = [
3.298 ("YEARLY", {"interval" : 3}),
3.299 - ("BYYEARDAY", {"values" : [1, 100, 200]})
3.300 + ("BYYEARDAY", {"values" : [1, 100, 200]}),
3.301 + ("COUNT", {"values" : [10]})
3.302 ]
3.303
3.304 l = order_qualifiers(qualifiers)
3.305 @@ -575,7 +609,7 @@
3.306 show(l)
3.307
3.308 s = get_selector(dt, qualifiers)
3.309 -l = s.materialise(dt, (2006, 2, 1, 0, 0, 0), 10)
3.310 +l = s.materialise(dt, (2006, 2, 1, 0, 0, 0))
3.311 print len(l) == 10, 10, len(l)
3.312 print l[0] == (1997, 1, 1, 9, 0, 0), (1997, 1, 1, 9, 0, 0), l[0]
3.313 print l[-1] == (2006, 1, 1, 9, 0, 0), (2006, 1, 1, 9, 0, 0), l[-1]
3.314 @@ -732,7 +766,9 @@
3.315
3.316 qualifiers = [
3.317 ("MONTHLY", {"interval" : 1}),
3.318 - ("BYDAY", {"values" : [(2, None), (3, None), (4, None)]})
3.319 + ("BYDAY", {"values" : [(2, None), (3, None), (4, None)]}),
3.320 + ("BYSETPOS", {"values" : [3]}),
3.321 + ("COUNT", {"values" : [3]})
3.322 ]
3.323
3.324 l = order_qualifiers(qualifiers)
3.325 @@ -744,7 +780,7 @@
3.326 show(l)
3.327
3.328 s = get_selector(dt, qualifiers)
3.329 -l = s.materialise(dt, (1997, 12, 24, 0, 0, 0), 3, [3])
3.330 +l = s.materialise(dt, (1997, 12, 24, 0, 0, 0))
3.331 print len(l) == 3, 3, len(l)
3.332 print l[0] == (1997, 9, 4, 9, 0, 0), (1997, 9, 4, 9, 0, 0), l[0]
3.333 print l[-1] == (1997, 11, 6, 9, 0, 0), (1997, 11, 6, 9, 0, 0), l[-1]
3.334 @@ -754,7 +790,8 @@
3.335
3.336 qualifiers = [
3.337 ("MONTHLY", {"interval" : 1}),
3.338 - ("BYDAY", {"values" : [(1, None), (2, None), (3, None), (4, None), (5, None)]})
3.339 + ("BYDAY", {"values" : [(1, None), (2, None), (3, None), (4, None), (5, None)]}),
3.340 + ("BYSETPOS", {"values" : [-2]})
3.341 ]
3.342
3.343 l = order_qualifiers(qualifiers)
3.344 @@ -766,7 +803,7 @@
3.345 show(l)
3.346
3.347 s = get_selector(dt, qualifiers)
3.348 -l = s.materialise(dt, (1998, 4, 1, 0, 0, 0), None, [-2])
3.349 +l = s.materialise(dt, (1998, 4, 1, 0, 0, 0))
3.350 print len(l) == 7, 7, len(l)
3.351 print l[0] == (1997, 9, 29, 9, 0, 0), (1997, 9, 29, 9, 0, 0), l[0]
3.352 print l[-1] == (1998, 3, 30, 9, 0, 0), (1998, 3, 30, 9, 0, 0), l[-1]
3.353 @@ -804,4 +841,20 @@
3.354 print l[0] == (2018, 1, 1), (2018, 1, 1), l[0]
3.355 print l[-1] == (2018, 1, 31), (2018, 1, 31), l[-1]
3.356
3.357 +qualifiers = get_qualifiers(["FREQ=MONTHLY", "BYDAY=WE,1FR,2MO,2FR"])
3.358 +
3.359 +l = order_qualifiers(qualifiers)
3.360 +show(l)
3.361 +dt = (2017, 10, 15)
3.362 +l = get_datetime_structure(dt)
3.363 +show(l)
3.364 +l = combine_datetime_with_qualifiers(dt, qualifiers)
3.365 +show(l)
3.366 +
3.367 +s = get_selector(dt, qualifiers)
3.368 +l = s.materialise(dt, (2018, 1, 1))
3.369 +print len(l) == 17
3.370 +print l[0] == (2017, 10, 18), (2017, 10, 18), l[0]
3.371 +print l[-1] == (2017, 12, 27), (2017, 12, 27), l[-1]
3.372 +
3.373 # vim: tabstop=4 expandtab shiftwidth=4
4.1 --- a/vRecurrence.py Fri Oct 20 23:37:08 2017 +0200
4.2 +++ b/vRecurrence.py Tue Oct 24 20:33:02 2017 +0200
4.3 @@ -70,6 +70,10 @@
4.4 "SECONDLY"
4.5 )
4.6
4.7 +# Symbols corresponding to resolution levels.
4.8 +
4.9 +YEARS, MONTHS, WEEKS, DAYS, HOURS, MINUTES, SECONDS = 0, 1, 2, 5, 6, 7, 8
4.10 +
4.11 # Enumeration levels, employed by BY... qualifiers.
4.12
4.13 enum_levels = (
4.14 @@ -98,19 +102,34 @@
4.15
4.16 firstvalues = [0, 1, 1, 1, 1, 1, 0, 0, 0]
4.17
4.18 -# Map from qualifiers to interval units. Here, weeks are defined as 7 days.
4.19 +# Map from qualifiers to interval multiples. Here, weeks are defined as 7 days.
4.20
4.21 -units = {"WEEKLY" : 7}
4.22 +multiples = {"WEEKLY" : 7}
4.23
4.24 # Make dictionaries mapping qualifiers to levels.
4.25
4.26 -freq = dict([(level, i) for (i, level) in enumerate(freq_levels) if level])
4.27 -enum = dict([(level, i) for (i, level) in enumerate(enum_levels) if level])
4.28 -weekdays = dict([(weekday, i+1) for (i, weekday) in enumerate(["MO", "TU", "WE", "TH", "FR", "SA", "SU"])])
4.29 +freq = {}
4.30 +for i, level in enumerate(freq_levels):
4.31 + if level:
4.32 + freq[level] = i
4.33 +
4.34 +enum = {}
4.35 +for i, level in enumerate(enum_levels):
4.36 + if level:
4.37 + enum[level] = i
4.38 +
4.39 +# Weekdays: name -> 1-based value
4.40 +
4.41 +weekdays = {}
4.42 +for i, weekday in enumerate(["MO", "TU", "WE", "TH", "FR", "SA", "SU"]):
4.43 + weekdays[weekday] = i + 1
4.44
4.45 # Functions for structuring the recurrences.
4.46
4.47 def get_next(it):
4.48 +
4.49 + "Return the next value from iterator 'it' or None if no more values exist."
4.50 +
4.51 try:
4.52 return it.next()
4.53 except StopIteration:
4.54 @@ -122,14 +141,11 @@
4.55
4.56 d = {}
4.57 for value in values:
4.58 - parts = value.split("=", 1)
4.59 - if len(parts) < 2:
4.60 + try:
4.61 + key, value = value.split("=", 1)
4.62 + d[key] = value
4.63 + except ValueError:
4.64 continue
4.65 - key, value = parts
4.66 - if key in ("COUNT", "BYSETPOS"):
4.67 - d[key] = int(value)
4.68 - else:
4.69 - d[key] = value
4.70 return d
4.71
4.72 def get_qualifiers(values):
4.73 @@ -144,10 +160,10 @@
4.74 interval = 1
4.75
4.76 for value in values:
4.77 - parts = value.split("=", 1)
4.78 - if len(parts) < 2:
4.79 + try:
4.80 + key, value = value.split("=", 1)
4.81 + except ValueError:
4.82 continue
4.83 - key, value = parts
4.84
4.85 # Accept frequency indicators as qualifiers.
4.86
4.87 @@ -160,9 +176,9 @@
4.88 interval = int(value)
4.89 continue
4.90
4.91 - # Accept enumerators as qualifiers.
4.92 + # Accept result set selection, truncation and enumerators as qualifiers.
4.93
4.94 - elif enum.has_key(key):
4.95 + elif key in ("BYSETPOS", "COUNT") or enum.has_key(key):
4.96 qualifier = (key, {"values" : get_qualifier_values(key, value)})
4.97
4.98 # Ignore other items.
4.99 @@ -186,10 +202,15 @@
4.100 suitable values.
4.101 """
4.102
4.103 + # For non-weekday selection, obtain a list of day numbers.
4.104 +
4.105 if qualifier != "BYDAY":
4.106 return map(int, value.split(","))
4.107
4.108 + # For weekday selection, obtain the weekday number and instance number.
4.109 +
4.110 values = []
4.111 +
4.112 for part in value.split(","):
4.113 weekday = weekdays.get(part[-2:])
4.114 if not weekday:
4.115 @@ -208,6 +229,12 @@
4.116 "Return the 'qualifiers' in order of increasing resolution."
4.117
4.118 l = []
4.119 + max_level = 0
4.120 +
4.121 + # Special qualifiers.
4.122 +
4.123 + setpos = None
4.124 + count = None
4.125
4.126 for qualifier, args in qualifiers:
4.127
4.128 @@ -216,18 +243,43 @@
4.129
4.130 if enum.has_key(qualifier):
4.131 level = enum[qualifier]
4.132 +
4.133 + # Certain enumerators produce their values in a special way.
4.134 +
4.135 if special_enum_levels.has_key(qualifier):
4.136 args["interval"] = 1
4.137 selector = special_enum_levels[qualifier]
4.138 else:
4.139 selector = Enum
4.140 +
4.141 + elif qualifier == "BYSETPOS":
4.142 + setpos = args
4.143 + continue
4.144 +
4.145 + elif qualifier == "COUNT":
4.146 + count = args
4.147 + continue
4.148 +
4.149 else:
4.150 level = freq[qualifier]
4.151 selector = Pattern
4.152
4.153 l.append(selector(level, args, qualifier))
4.154 + max_level = max(level, max_level)
4.155
4.156 - l.sort(key=lambda x: x.level)
4.157 + # Add the result set selector at the maximum level of enumeration.
4.158 +
4.159 + if setpos is not None:
4.160 + l.append(PositionSelector(max_level, setpos, "BYSETPOS"))
4.161 +
4.162 + # Add the result set truncator at the top level.
4.163 +
4.164 + if count is not None:
4.165 + l.append(LimitSelector(0, count, "COUNT"))
4.166 +
4.167 + # Make BYSETPOS sort earlier than the enumeration it modifies.
4.168 +
4.169 + l.sort(key=lambda x: (x.level, x.qualifier != "BYSETPOS" and 1 or 0))
4.170 return l
4.171
4.172 def get_datetime_structure(datetime):
4.173 @@ -237,15 +289,15 @@
4.174 l = []
4.175 offset = 0
4.176
4.177 - for level, value in enumerate(datetime):
4.178 + for pos, value in enumerate(datetime):
4.179
4.180 # At the day number, adjust the frequency level offset to reference
4.181 # days (and then hours, minutes, seconds).
4.182
4.183 - if level == 2:
4.184 + if pos == 2:
4.185 offset = 3
4.186
4.187 - l.append(Enum(level + offset, {"values" : [value]}, "DT"))
4.188 + l.append(Enum(pos + offset, {"values" : [value]}, "DT"))
4.189
4.190 return l
4.191
4.192 @@ -318,7 +370,9 @@
4.193
4.194 # Ignore datetime values that conflict with day-level qualifiers.
4.195
4.196 - if not l or from_dt.level != freq["DAILY"] or l[-1].level not in daylevels:
4.197 + if not l or from_dt.level != freq["DAILY"] or \
4.198 + l[-1].level not in daylevels:
4.199 +
4.200 l.append(from_dt)
4.201
4.202 from_dt = get_next(iter_dt)
4.203 @@ -352,24 +406,120 @@
4.204 repeat = Pattern(level - 1, {"interval" : 1}, None)
4.205 l.append(repeat)
4.206
4.207 +def get_multiple(qualifier):
4.208 +
4.209 + "Return the time unit multiple for 'qualifier'."
4.210 +
4.211 + return multiples.get(qualifier, 1)
4.212 +
4.213 # Datetime arithmetic.
4.214
4.215 -def combine(t1, t2):
4.216 +def is_year_only(t):
4.217 +
4.218 + "Return if 't' describes a year."
4.219 +
4.220 + return len(t) == lengths[YEARS]
4.221 +
4.222 +def is_month_only(t):
4.223 +
4.224 + "Return if 't' describes a month within a year."
4.225 +
4.226 + return len(t) == lengths[MONTHS]
4.227 +
4.228 +def start_of_year(t):
4.229 +
4.230 + "Return the start of the year referenced by 't'."
4.231 +
4.232 + return (t[0], 1, 1)
4.233 +
4.234 +def end_of_year(t):
4.235 +
4.236 + "Return the end of the year referenced by 't'."
4.237 +
4.238 + return (t[0], 12, 31)
4.239 +
4.240 +def start_of_month(t):
4.241 +
4.242 + "Return the start of the month referenced by 't'."
4.243 +
4.244 + year, month = t[:2]
4.245 + return (year, month, 1)
4.246 +
4.247 +def end_of_month(t):
4.248 +
4.249 + "Return the end of the month referenced by 't'."
4.250 +
4.251 + year, month = t[:2]
4.252 + return update(update((year, month, 1), (0, 1, 0)), (0, 0, -1))
4.253 +
4.254 +def day_in_year(t, number):
4.255 +
4.256 + "Return the day in the year referenced by 't' with the given 'number'."
4.257 +
4.258 + return to_tuple(date(*start_of_year(t)) + timedelta(number - 1))
4.259 +
4.260 +def get_year_length(t):
4.261 +
4.262 + "Return the length of the year referenced by 't'."
4.263 +
4.264 + first_day = date(*start_of_year(t))
4.265 + last_day = date(*end_of_year(t))
4.266 + return (last_day - first_day).days + 1
4.267 +
4.268 +def get_weekday(t):
4.269 +
4.270 + "Return the 1-based weekday for the month referenced by 't'."
4.271 +
4.272 + year, month = t[:2]
4.273 + return monthrange(year, month)[0] + 1
4.274 +
4.275 +def get_ordered_weekdays(t):
4.276
4.277 """
4.278 - Combine tuples 't1' and 't2', returning a copy of 't1' filled with values
4.279 - from 't2' in positions where 0 appeared in 't1'.
4.280 + Return the 1-based weekday sequence describing the first week of the month
4.281 + referenced by 't'.
4.282 """
4.283
4.284 - return tuple(map(lambda x, y: x or y, t1, t2))
4.285 + first = get_weekday(t)
4.286 + return range(first, 8) + range(1, first)
4.287 +
4.288 +def get_last_weekday_instance(weekday, first_day, last_day):
4.289 +
4.290 + """
4.291 + Return the last instance number for 'weekday' within the period from
4.292 + 'first_day' to 'last_day' inclusive.
4.293
4.294 -def scale(interval, pos):
4.295 + Here, 'weekday' is 1-based (starting on Monday), the returned limit is
4.296 + 1-based.
4.297 + """
4.298 +
4.299 + weekday0 = get_first_day(first_day, weekday)
4.300 + days = (date(*last_day) - weekday0).days
4.301 + return days / 7 + 1
4.302 +
4.303 +def precision(t, level, value=None):
4.304
4.305 """
4.306 - Scale the given 'interval' value to the indicated position 'pos', returning
4.307 - a tuple with leading zero elements and 'interval' at the stated position.
4.308 + Return 't' trimmed in resolution to the indicated resolution 'level',
4.309 + setting 'value' at the given resolution if indicated.
4.310 """
4.311
4.312 + pos = positions[level]
4.313 +
4.314 + if value is None:
4.315 + return t[:pos + 1]
4.316 + else:
4.317 + return t[:pos] + (value,)
4.318 +
4.319 +def scale(interval, level):
4.320 +
4.321 + """
4.322 + Scale the given 'interval' value in resolution to the indicated resolution
4.323 + 'level', returning a tuple with leading zero elements and 'interval' at the
4.324 + stated position.
4.325 + """
4.326 +
4.327 + pos = positions[level]
4.328 return (0,) * pos + (interval,)
4.329
4.330 def get_seconds(t):
4.331 @@ -413,24 +563,26 @@
4.332 d = datetime(*updated_for_months)
4.333 s = timedelta(step[2], get_seconds(step))
4.334
4.335 - return to_tuple(d + s, len(t))
4.336 + return to_tuple(d + s)[:len(t)]
4.337
4.338 -def to_tuple(d, n=None):
4.339 +def to_tuple(d):
4.340
4.341 - "Return 'd' as a tuple, optionally trimming the result to 'n' positions."
4.342 + "Return date or datetime 'd' as a tuple."
4.343
4.344 if not isinstance(d, date):
4.345 return d
4.346 - if n is None:
4.347 - if isinstance(d, datetime):
4.348 - n = 6
4.349 - else:
4.350 - n = 3
4.351 + if isinstance(d, datetime):
4.352 + n = 6
4.353 + else:
4.354 + n = 3
4.355 return d.timetuple()[:n]
4.356
4.357 def get_first_day(first_day, weekday):
4.358
4.359 - "Return the first occurrence at or after 'first_day' of 'weekday'."
4.360 + """
4.361 + Return the first occurrence at or after 'first_day' of 'weekday' as a date
4.362 + instance.
4.363 + """
4.364
4.365 first_day = date(*first_day)
4.366 first_weekday = first_day.isoweekday()
4.367 @@ -441,7 +593,10 @@
4.368
4.369 def get_last_day(last_day, weekday):
4.370
4.371 - "Return the last occurrence at or before 'last_day' of 'weekday'."
4.372 + """
4.373 + Return the last occurrence at or before 'last_day' of 'weekday' as a date
4.374 + instance.
4.375 + """
4.376
4.377 last_day = date(*last_day)
4.378 last_weekday = last_day.isoweekday()
4.379 @@ -450,6 +605,85 @@
4.380 else:
4.381 return last_day - timedelta(last_weekday - weekday)
4.382
4.383 +# Value expansion and sorting.
4.384 +
4.385 +def sort_values(values, limit=None):
4.386 +
4.387 + """
4.388 + Sort the given 'values' using 'limit' to determine the positions of negative
4.389 + values.
4.390 + """
4.391 +
4.392 + # Convert negative values to positive values according to the value limit.
4.393 +
4.394 + if limit is not None:
4.395 + l = map(lambda x, limit=limit: x < 0 and x + 1 + limit or x, values)
4.396 + else:
4.397 + l = values[:]
4.398 +
4.399 + l.sort()
4.400 + return l
4.401 +
4.402 +def compare_weekday_selectors(x, y, weekdays):
4.403 +
4.404 + """
4.405 + Compare 'x' and 'y' values of the form (weekday number, instance number)
4.406 + using 'weekdays' to define an ordering of the weekday numbers.
4.407 + """
4.408 +
4.409 + return cmp((x[1], weekdays.index(x[0])), (y[1], weekdays.index(y[0])))
4.410 +
4.411 +def sort_weekdays(values, first_day, last_day):
4.412 +
4.413 + """
4.414 + Return a sorted copy of the given 'values', each having the form (weekday
4.415 + number, instance number) using 'weekdays' to define the ordering of the
4.416 + weekday numbers and 'limit' to determine the positions of negative instance
4.417 + numbers.
4.418 + """
4.419 +
4.420 + weekdays = get_ordered_weekdays(first_day)
4.421 +
4.422 + # Expand the values to incorporate specific weekday instances.
4.423 +
4.424 + l = []
4.425 +
4.426 + for weekday, index in values:
4.427 +
4.428 + # Obtain the last instance number of the weekday in the period.
4.429 +
4.430 + limit = get_last_weekday_instance(weekday, first_day, last_day)
4.431 +
4.432 + # For specific instances, convert negative selections.
4.433 +
4.434 + if index is not None:
4.435 + l.append((weekday, index < 0 and index + 1 + limit or index))
4.436 +
4.437 + # For None, introduce selections for all instances of the weekday.
4.438 +
4.439 + else:
4.440 + index = 1
4.441 + while index <= limit:
4.442 + l.append((weekday, index))
4.443 + index += 1
4.444 +
4.445 + # Sort the values so that the resulting dates are ordered.
4.446 +
4.447 + fn = lambda x, y, weekdays=weekdays: compare_weekday_selectors(x, y, weekdays)
4.448 + l.sort(cmp=fn)
4.449 + return l
4.450 +
4.451 +def convert_positions(setpos):
4.452 +
4.453 + "Convert 'setpos' to 0-based indexes."
4.454 +
4.455 + l = []
4.456 + if setpos:
4.457 + for pos in setpos:
4.458 + index = pos < 0 and pos or pos - 1
4.459 + l.append(index)
4.460 + return l
4.461 +
4.462 # Classes for producing instances from recurrence structures.
4.463
4.464 class Selector:
4.465 @@ -472,20 +706,15 @@
4.466 self.selecting = selecting
4.467 self.first = first
4.468
4.469 - # Define the index of values from datetimes involved with this selector.
4.470 -
4.471 - self.pos = positions[level]
4.472 + def __repr__(self):
4.473 + return "%s(%r, %r, %r, %r)" % (self.__class__.__name__, self.level,
4.474 + self.args, self.qualifier, self.first)
4.475
4.476 - def __repr__(self):
4.477 - return "%s(%r, %r, %r, %r)" % (self.__class__.__name__, self.level, self.args, self.qualifier, self.first)
4.478 -
4.479 - def materialise(self, start, end, count=None, setpos=None, inclusive=False):
4.480 + def select(self, start, end, inclusive=False):
4.481
4.482 """
4.483 - Starting at 'start', materialise instances up to but not including any
4.484 - at 'end' or later, returning at most 'count' if specified, and returning
4.485 - only the occurrences indicated by 'setpos' if specified. A list of
4.486 - instances is returned.
4.487 + Return an iterator over instances starting at 'start' and continuing up
4.488 + to but not including any at 'end' or later.
4.489
4.490 If 'inclusive' is specified, the selection of instances will include the
4.491 end of the search period if present in the results.
4.492 @@ -493,287 +722,125 @@
4.493
4.494 start = to_tuple(start)
4.495 end = to_tuple(end)
4.496 - counter = count and [0, count]
4.497 - results = self.materialise_items(start, start, end, counter, setpos, inclusive)
4.498 - results.sort()
4.499 - return results[:count]
4.500 + return self.materialise_items(start, start, end, inclusive)
4.501
4.502 - def materialise_item(self, current, earliest, next, counter, setpos=None, inclusive=False):
4.503 + def materialise(self, start, end, inclusive=False):
4.504
4.505 """
4.506 - Given the 'current' instance, the 'earliest' acceptable instance, the
4.507 - 'next' instance, an instance 'counter', and the optional 'setpos'
4.508 - criteria, return a list of result items. Where no selection within the
4.509 - current instance occurs, the current instance will be returned as a
4.510 - result if the same or later than the earliest acceptable instance.
4.511 - """
4.512 -
4.513 - if self.selecting:
4.514 - return self.selecting.materialise_items(current, earliest, next, counter, setpos, inclusive)
4.515 - elif earliest <= current:
4.516 - return [current]
4.517 - else:
4.518 - return []
4.519 -
4.520 - def convert_positions(self, setpos):
4.521 -
4.522 - "Convert 'setpos' to 0-based indexes."
4.523 -
4.524 - l = []
4.525 - for pos in setpos:
4.526 - lower = pos < 0 and pos or pos - 1
4.527 - upper = pos > 0 and pos or pos < -1 and pos + 1 or None
4.528 - l.append((lower, upper))
4.529 - return l
4.530 -
4.531 - def select_positions(self, results, setpos):
4.532 -
4.533 - "Select in 'results' the 1-based positions given by 'setpos'."
4.534 -
4.535 - results.sort()
4.536 - l = []
4.537 - for lower, upper in self.convert_positions(setpos):
4.538 - l += results[lower:upper]
4.539 - return l
4.540 -
4.541 - def filter_by_period(self, results, start, end, inclusive):
4.542 -
4.543 - """
4.544 - Filter 'results' so that only those at or after 'start' and before 'end'
4.545 - are returned.
4.546 + Starting at 'start', materialise instances up to but not including any
4.547 + at 'end' or later. A list of instances is returned.
4.548
4.549 If 'inclusive' is specified, the selection of instances will include the
4.550 end of the search period if present in the results.
4.551 """
4.552
4.553 - l = []
4.554 - for result in results:
4.555 - if start <= result and (inclusive and result <= end or result < end):
4.556 - l.append(result)
4.557 - return l
4.558 + return list(self.select(start, end, inclusive))
4.559
4.560 class Pattern(Selector):
4.561
4.562 "A selector of time periods according to a repeating pattern."
4.563
4.564 - def materialise_items(self, context, start, end, counter, setpos=None, inclusive=False):
4.565 + def __init__(self, level, args, qualifier, selecting=None, first=False):
4.566 + Selector.__init__(self, level, args, qualifier, selecting, first)
4.567 +
4.568 + multiple = get_multiple(self.qualifier)
4.569 + interval = self.args.get("interval", 1)
4.570 +
4.571 + # Define the step between result periods.
4.572 +
4.573 + self.step = scale(interval * multiple, level)
4.574 +
4.575 + # Define the scale of a single period.
4.576 +
4.577 + self.unit_step = scale(multiple, level)
4.578 +
4.579 + def materialise_items(self, context, start, end, inclusive=False):
4.580
4.581 """
4.582 - Bounded by the given 'context', return periods within 'start' to 'end',
4.583 - updating the 'counter', selecting only the indexes specified by 'setpos'
4.584 - (if given).
4.585 + Bounded by the given 'context', return periods within 'start' to 'end'.
4.586
4.587 If 'inclusive' is specified, the selection of periods will include those
4.588 starting at the end of the search period, if present in the results.
4.589 """
4.590
4.591 - # Define the step between result periods.
4.592 -
4.593 - interval = self.args.get("interval", 1) * units.get(self.qualifier, 1)
4.594 - step = scale(interval, self.pos)
4.595 -
4.596 - # Define the scale of a single period.
4.597 -
4.598 - unit_interval = units.get(self.qualifier, 1)
4.599 - unit_step = scale(unit_interval, self.pos)
4.600 -
4.601 # Employ the context as the current period if this is the first
4.602 # qualifier in the selection chain.
4.603
4.604 if self.first:
4.605 - current = context[:self.pos+1]
4.606 + current = precision(context, self.level)
4.607
4.608 # Otherwise, obtain the first value at this resolution within the
4.609 # context period.
4.610
4.611 else:
4.612 - first = scale(firstvalues[self.level], self.pos)
4.613 - current = combine(context[:self.pos], first)
4.614 -
4.615 - results = []
4.616 -
4.617 - # Obtain periods before the end (and also at the end if inclusive),
4.618 - # provided that any limit imposed by the counter has not been exceeded.
4.619 -
4.620 - while (inclusive and current <= end or current < end) and \
4.621 - (counter is None or counter[0] < counter[1]):
4.622 -
4.623 - # Increment the current datetime by the step for the next period.
4.624 -
4.625 - next = update(current, step)
4.626 -
4.627 - # Determine the end point of the current period.
4.628 -
4.629 - current_end = update(current, unit_step)
4.630 + current = precision(context, self.level, firstvalues[self.level])
4.631
4.632 - # Obtain any period or periods within the bounds defined by the
4.633 - # current period and any contraining start and end points, plus
4.634 - # counter, setpos and inclusive details.
4.635 -
4.636 - interval_results = self.materialise_item(current, max(current, start), min(current_end, end), counter, setpos, inclusive)
4.637 -
4.638 - # Update the counter with the number of identified results.
4.639 -
4.640 - if counter is not None:
4.641 - counter[0] += len(interval_results)
4.642 -
4.643 - # Accumulate the results.
4.644 -
4.645 - results += interval_results
4.646 -
4.647 - # Visit the next instance.
4.648 -
4.649 - current = next
4.650 -
4.651 - return results
4.652 + return PatternIterator(self, current, start, end, inclusive,
4.653 + self.step, self.unit_step)
4.654
4.655 class WeekDayFilter(Selector):
4.656
4.657 "A selector of instances specified in terms of day numbers."
4.658
4.659 - def materialise_items(self, context, start, end, counter, setpos=None, inclusive=False):
4.660 - step = scale(1, 2)
4.661 - results = []
4.662 + def __init__(self, level, args, qualifier, selecting=None, first=False):
4.663 + Selector.__init__(self, level, args, qualifier, selecting, first)
4.664 + self.step = scale(1, WEEKS)
4.665 +
4.666 + def materialise_items(self, context, start, end, inclusive=False):
4.667
4.668 # Get weekdays in the year.
4.669
4.670 - if len(context) == 1:
4.671 - first_day = (context[0], 1, 1)
4.672 - last_day = (context[0], 12, 31)
4.673 + if is_year_only(context):
4.674 + first_day = start_of_year(context)
4.675 + last_day = end_of_year(context)
4.676
4.677 # Get weekdays in the month.
4.678
4.679 - elif len(context) == 2:
4.680 - first_day = (context[0], context[1], 1)
4.681 - last_day = update((context[0], context[1], 1), (0, 1, 0))
4.682 - last_day = update(last_day, (0, 0, -1))
4.683 + elif is_month_only(context):
4.684 + first_day = start_of_month(context)
4.685 + last_day = end_of_month(context)
4.686
4.687 # Get weekdays in the week.
4.688
4.689 else:
4.690 current = context
4.691 values = [value for (value, index) in self.args["values"]]
4.692 -
4.693 - while (inclusive and current <= end or current < end):
4.694 - next = update(current, step)
4.695 - if date(*current).isoweekday() in values:
4.696 - results += self.materialise_item(current, max(current, start), min(next, end), counter, inclusive=inclusive)
4.697 - current = next
4.698 -
4.699 - if setpos:
4.700 - return self.select_positions(results, setpos)
4.701 - else:
4.702 - return results
4.703 -
4.704 - # Find each of the given days.
4.705 -
4.706 - for value, index in self.args["values"]:
4.707 - if index is not None:
4.708 - offset = timedelta(7 * (abs(index) - 1))
4.709 -
4.710 - if index < 0:
4.711 - current = to_tuple(get_last_day(last_day, value) - offset, 3)
4.712 - else:
4.713 - current = to_tuple(get_first_day(first_day, value) + offset, 3)
4.714 -
4.715 - next = update(current, step)
4.716 + return WeekDayIterator(self, current, start, end, inclusive, self.step, values)
4.717
4.718 - # To support setpos, only current and next bound the search, not
4.719 - # the period in addition.
4.720 -
4.721 - results += self.materialise_item(current, current, next, counter, inclusive=inclusive)
4.722 -
4.723 - else:
4.724 - if index < 0:
4.725 - current = to_tuple(get_last_day(last_day, value), 3)
4.726 - direction = operator.sub
4.727 - else:
4.728 - current = to_tuple(get_first_day(first_day, value), 3)
4.729 - direction = operator.add
4.730 -
4.731 - while first_day <= current <= last_day:
4.732 - next = update(current, step)
4.733 -
4.734 - # To support setpos, only current and next bound the search, not
4.735 - # the period in addition.
4.736 -
4.737 - results += self.materialise_item(current, current, next, counter, inclusive=inclusive)
4.738 - current = to_tuple(direction(date(*current), timedelta(7)), 3)
4.739 -
4.740 - # Extract selected positions and remove out-of-period instances.
4.741 -
4.742 - if setpos:
4.743 - results = self.select_positions(results, setpos)
4.744 -
4.745 - return self.filter_by_period(results, start, end, inclusive)
4.746 + current = first_day
4.747 + values = sort_weekdays(self.args["values"], first_day, last_day)
4.748 + return WeekDayGeneralIterator(self, current, start, end, inclusive, self.step, values)
4.749
4.750 class Enum(Selector):
4.751 - def materialise_items(self, context, start, end, counter, setpos=None, inclusive=False):
4.752 - step = scale(1, self.pos)
4.753 - results = []
4.754 - for value in self.args["values"]:
4.755 - current = combine(context[:self.pos], scale(value, self.pos))
4.756 - next = update(current, step)
4.757 +
4.758 + "A generic value selector."
4.759
4.760 - # To support setpos, only current and next bound the search, not
4.761 - # the period in addition.
4.762 -
4.763 - results += self.materialise_item(current, current, next, counter, setpos, inclusive)
4.764 + def __init__(self, level, args, qualifier, selecting=None, first=False):
4.765 + Selector.__init__(self, level, args, qualifier, selecting, first)
4.766 + self.step = scale(1, level)
4.767
4.768 - # Extract selected positions and remove out-of-period instances.
4.769 -
4.770 - if setpos:
4.771 - results = self.select_positions(results, setpos)
4.772 -
4.773 - return self.filter_by_period(results, start, end, inclusive)
4.774 + def materialise_items(self, context, start, end, inclusive=False):
4.775 + values = sort_values(self.args["values"])
4.776 + return EnumIterator(self, context, start, end, inclusive, self.step, values)
4.777
4.778 class MonthDayFilter(Enum):
4.779 - def materialise_items(self, context, start, end, counter, setpos=None, inclusive=False):
4.780 - last_day = monthrange(context[0], context[1])[1]
4.781 - step = scale(1, self.pos)
4.782 - results = []
4.783 - for value in self.args["values"]:
4.784 - if value < 0:
4.785 - value = last_day + 1 + value
4.786 - current = combine(context, scale(value, self.pos))
4.787 - next = update(current, step)
4.788 +
4.789 + "A selector of month days."
4.790
4.791 - # To support setpos, only current and next bound the search, not
4.792 - # the period in addition.
4.793 -
4.794 - results += self.materialise_item(current, current, next, counter, inclusive=inclusive)
4.795 -
4.796 - # Extract selected positions and remove out-of-period instances.
4.797 -
4.798 - if setpos:
4.799 - results = self.select_positions(results, setpos)
4.800 -
4.801 - return self.filter_by_period(results, start, end, inclusive)
4.802 + def materialise_items(self, context, start, end, inclusive=False):
4.803 + last_day = end_of_month(context)[2]
4.804 + values = sort_values(self.args["values"], last_day)
4.805 + return EnumIterator(self, context, start, end, inclusive, self.step, values)
4.806
4.807 class YearDayFilter(Enum):
4.808 - def materialise_items(self, context, start, end, counter, setpos=None, inclusive=False):
4.809 - first_day = date(context[0], 1, 1)
4.810 - next_first_day = date(context[0] + 1, 1, 1)
4.811 - year_length = (next_first_day - first_day).days
4.812 - step = scale(1, self.pos)
4.813 - results = []
4.814 - for value in self.args["values"]:
4.815 - if value < 0:
4.816 - value = year_length + 1 + value
4.817 - current = to_tuple(first_day + timedelta(value - 1), 3)
4.818 - next = update(current, step)
4.819 +
4.820 + "A selector of days in years."
4.821
4.822 - # To support setpos, only current and next bound the search, not
4.823 - # the period in addition.
4.824 -
4.825 - results += self.materialise_item(current, current, next, counter, inclusive=inclusive)
4.826 -
4.827 - # Extract selected positions and remove out-of-period instances.
4.828 -
4.829 - if setpos:
4.830 - results = self.select_positions(results, setpos)
4.831 -
4.832 - return self.filter_by_period(results, start, end, inclusive)
4.833 + def materialise_items(self, context, start, end, inclusive=False):
4.834 + first_day = start_of_year(context)
4.835 + year_length = get_year_length(context)
4.836 + values = sort_values(self.args["values"], year_length)
4.837 + return YearDayFilterIterator(self, first_day, start, end, inclusive, self.step, values)
4.838
4.839 special_enum_levels = {
4.840 "BYDAY" : WeekDayFilter,
4.841 @@ -781,6 +848,362 @@
4.842 "BYYEARDAY" : YearDayFilter,
4.843 }
4.844
4.845 +class LimitSelector(Selector):
4.846 +
4.847 + "A result set limit selector."
4.848 +
4.849 + def materialise_items(self, context, start, end, inclusive=False):
4.850 + limit = self.args["values"][0]
4.851 + return LimitIterator(self, context, start, end, inclusive, limit)
4.852 +
4.853 +class PositionSelector(Selector):
4.854 +
4.855 + "A result set position selector."
4.856 +
4.857 + def __init__(self, level, args, qualifier, selecting=None, first=False):
4.858 + Selector.__init__(self, level, args, qualifier, selecting, first)
4.859 + self.step = scale(1, level)
4.860 +
4.861 + def materialise_items(self, context, start, end, inclusive=False):
4.862 + values = convert_positions(sort_values(self.args["values"]))
4.863 + return PositionIterator(self, context, start, end, inclusive, self.step, values)
4.864 +
4.865 +# Iterator classes.
4.866 +
4.867 +class SelectorIterator:
4.868 +
4.869 + "An iterator over selected data."
4.870 +
4.871 + def __init__(self, selector, current, start, end, inclusive):
4.872 +
4.873 + """
4.874 + Define an iterator having the 'current' point in time, 'start' and 'end'
4.875 + limits, and whether the selection is 'inclusive'.
4.876 + """
4.877 +
4.878 + self.selector = selector
4.879 + self.current = current
4.880 + self.start = start
4.881 + self.end = end
4.882 + self.inclusive = inclusive
4.883 +
4.884 + # Iterator over selections within this selection.
4.885 +
4.886 + self.iterator = None
4.887 +
4.888 + def __iter__(self):
4.889 + return self
4.890 +
4.891 + def next_item(self, earliest, next):
4.892 +
4.893 + """
4.894 + Given the 'earliest' acceptable instance and the 'next' instance, return
4.895 + a list of result items.
4.896 +
4.897 + Where no selection within the current instance occurs, the current
4.898 + instance will be returned as a result if the same or later than the
4.899 + earliest acceptable instance.
4.900 + """
4.901 +
4.902 + # Obtain an item from a selector operating within this selection.
4.903 +
4.904 + selecting = self.selector.selecting
4.905 +
4.906 + if selecting:
4.907 +
4.908 + # Obtain an iterator for any selector within the current period.
4.909 +
4.910 + if not self.iterator:
4.911 + self.iterator = selecting.materialise_items(self.current,
4.912 + earliest, next, self.inclusive)
4.913 +
4.914 + # Attempt to obtain and return a value.
4.915 +
4.916 + return self.iterator.next()
4.917 +
4.918 + # Return items within this selection.
4.919 +
4.920 + else:
4.921 + return self.current
4.922 +
4.923 + def filter_by_period(self, result):
4.924 +
4.925 + "Return whether 'result' occurs within the selection period."
4.926 +
4.927 + return (self.inclusive and result <= self.end or result < self.end) and \
4.928 + self.start <= result
4.929 +
4.930 + def at_limit(self):
4.931 +
4.932 + "Obtain periods before the end (and also at the end if inclusive)."
4.933 +
4.934 + return not self.inclusive and self.current == self.end or \
4.935 + self.current > self.end
4.936 +
4.937 +class PatternIterator(SelectorIterator):
4.938 +
4.939 + "An iterator for a general selection pattern."
4.940 +
4.941 + def __init__(self, selector, current, start, end, inclusive, step, unit_step):
4.942 + SelectorIterator.__init__(self, selector, current, start, end, inclusive)
4.943 + self.step = step
4.944 + self.unit_step = unit_step
4.945 +
4.946 + def next(self):
4.947 +
4.948 + "Return the next value."
4.949 +
4.950 + while not self.at_limit():
4.951 +
4.952 + # Increment the current datetime by the step for the next period.
4.953 +
4.954 + next = update(self.current, self.step)
4.955 +
4.956 + # Determine the end point of the current period.
4.957 +
4.958 + current_end = update(self.current, self.unit_step)
4.959 +
4.960 + # Obtain any period or periods within the bounds defined by the
4.961 + # current period and any contraining start and end points.
4.962 +
4.963 + try:
4.964 + result = self.next_item(max(self.current, self.start),
4.965 + min(current_end, self.end))
4.966 +
4.967 + # Obtain the next period if not selecting within this period.
4.968 +
4.969 + if not self.selector.selecting:
4.970 + self.current = next
4.971 +
4.972 + # Filter out periods.
4.973 +
4.974 + if self.filter_by_period(result):
4.975 + return result
4.976 +
4.977 + # Move on to the next instance.
4.978 +
4.979 + except StopIteration:
4.980 + self.current = next
4.981 + self.iterator = None
4.982 +
4.983 + raise StopIteration
4.984 +
4.985 +class WeekDayIterator(SelectorIterator):
4.986 +
4.987 + "An iterator over weekday selections in week periods."
4.988 +
4.989 + def __init__(self, selector, current, start, end, inclusive, step, values):
4.990 + SelectorIterator.__init__(self, selector, current, start, end, inclusive)
4.991 + self.step = step
4.992 + self.values = values
4.993 +
4.994 + def next(self):
4.995 +
4.996 + "Return the next value."
4.997 +
4.998 + while not self.at_limit():
4.999 +
4.1000 + # Increment the current datetime by the step for the next period.
4.1001 +
4.1002 + next = update(self.current, self.step)
4.1003 +
4.1004 + # Determine whether the day is one chosen.
4.1005 +
4.1006 + if date(*self.current).isoweekday() in self.values:
4.1007 + try:
4.1008 + result = self.next_item(max(self.current, self.start),
4.1009 + min(next, self.end))
4.1010 +
4.1011 + # Obtain the next period if not selecting within this period.
4.1012 +
4.1013 + if not self.selector.selecting:
4.1014 + self.current = next
4.1015 +
4.1016 + return result
4.1017 +
4.1018 + # Move on to the next instance.
4.1019 +
4.1020 + except StopIteration:
4.1021 + self.current = next
4.1022 + self.iterator = None
4.1023 +
4.1024 + else:
4.1025 + self.current = next
4.1026 + self.iterator = None
4.1027 +
4.1028 + raise StopIteration
4.1029 +
4.1030 +class EnumIterator(SelectorIterator):
4.1031 +
4.1032 + "An iterator over specific value selections."
4.1033 +
4.1034 + def __init__(self, selector, current, start, end, inclusive, step, values):
4.1035 + SelectorIterator.__init__(self, selector, current, start, end, inclusive)
4.1036 + self.step = step
4.1037 +
4.1038 + # Derive selected periods from a base and the indicated values.
4.1039 +
4.1040 + self.base = current
4.1041 +
4.1042 + # Iterate over the indicated period values.
4.1043 +
4.1044 + self.values = iter(values)
4.1045 + self.value = None
4.1046 +
4.1047 + def get_selected_period(self):
4.1048 +
4.1049 + "Return the period indicated by the current value."
4.1050 +
4.1051 + return precision(self.base, self.selector.level, self.value)
4.1052 +
4.1053 + def next(self):
4.1054 +
4.1055 + "Return the next value."
4.1056 +
4.1057 + while True:
4.1058 +
4.1059 + # Find each of the given selected values.
4.1060 +
4.1061 + if not self.selector.selecting or self.value is None:
4.1062 + self.value = self.values.next()
4.1063 +
4.1064 + # Select a period for each value at the current resolution.
4.1065 +
4.1066 + self.current = self.get_selected_period()
4.1067 + next = update(self.current, self.step)
4.1068 +
4.1069 + # To support setpos, only current and next bound the search, not
4.1070 + # the period in addition.
4.1071 +
4.1072 + try:
4.1073 + return self.next_item(self.current, next)
4.1074 +
4.1075 + # Move on to the next instance.
4.1076 +
4.1077 + except StopIteration:
4.1078 + self.value = None
4.1079 + self.iterator = None
4.1080 +
4.1081 + raise StopIteration
4.1082 +
4.1083 +class WeekDayGeneralIterator(EnumIterator):
4.1084 +
4.1085 + "An iterator over weekday selections in month and year periods."
4.1086 +
4.1087 + def get_selected_period(self):
4.1088 +
4.1089 + "Return the day indicated by the current day value."
4.1090 +
4.1091 + value, index = self.value
4.1092 + offset = timedelta(7 * (index - 1))
4.1093 + weekday0 = get_first_day(self.base, value)
4.1094 + return precision(to_tuple(weekday0 + offset), DAYS)
4.1095 +
4.1096 +class YearDayFilterIterator(EnumIterator):
4.1097 +
4.1098 + "An iterator over day-in-year selections."
4.1099 +
4.1100 + def get_selected_period(self):
4.1101 +
4.1102 + "Return the day indicated by the current day value."
4.1103 +
4.1104 + offset = timedelta(self.value - 1)
4.1105 + return precision(to_tuple(date(*self.base) + offset), DAYS)
4.1106 +
4.1107 +class LimitIterator(SelectorIterator):
4.1108 +
4.1109 + "A result set limiting iterator."
4.1110 +
4.1111 + def __init__(self, selector, context, start, end, inclusive, limit):
4.1112 + SelectorIterator.__init__(self, selector, context, start, end, inclusive)
4.1113 + self.limit = limit
4.1114 + self.count = 0
4.1115 +
4.1116 + def next(self):
4.1117 +
4.1118 + "Return the next value."
4.1119 +
4.1120 + if self.count < self.limit:
4.1121 + self.count += 1
4.1122 + result = self.next_item(self.start, self.end)
4.1123 + return result
4.1124 +
4.1125 + raise StopIteration
4.1126 +
4.1127 +class PositionIterator(SelectorIterator):
4.1128 +
4.1129 + "An iterator over results, selecting positions."
4.1130 +
4.1131 + def __init__(self, selector, context, start, end, inclusive, step, positions):
4.1132 + SelectorIterator.__init__(self, selector, context, start, end, inclusive)
4.1133 + self.step = step
4.1134 +
4.1135 + # Positions to select.
4.1136 +
4.1137 + self.positions = positions
4.1138 +
4.1139 + # Queue to hold values matching the negative position values.
4.1140 +
4.1141 + self.queue_length = positions and positions[0] < 0 and abs(positions[0]) or 0
4.1142 + self.queue = []
4.1143 +
4.1144 + # Result position.
4.1145 +
4.1146 + self.resultpos = 0
4.1147 +
4.1148 + def next(self):
4.1149 +
4.1150 + "Return the next value."
4.1151 +
4.1152 + while True:
4.1153 + try:
4.1154 + result = self.next_item(self.start, self.end)
4.1155 +
4.1156 + # Positive positions can have their values released immediately.
4.1157 +
4.1158 + selected = self.resultpos in self.positions
4.1159 + self.resultpos += 1
4.1160 +
4.1161 + if selected:
4.1162 + return result
4.1163 +
4.1164 + # Negative positions must be held until this iterator completes and
4.1165 + # then be released.
4.1166 +
4.1167 + if self.queue_length:
4.1168 + self.queue.append(result)
4.1169 + if len(self.queue) > self.queue_length:
4.1170 + del self.queue[0]
4.1171 +
4.1172 + except StopIteration:
4.1173 +
4.1174 + # With a queue and positions, attempt to find the referenced
4.1175 + # positions.
4.1176 +
4.1177 + if self.queue and self.positions:
4.1178 + index = self.positions[0]
4.1179 + del self.positions[0]
4.1180 +
4.1181 + # Only negative positions are used at this point.
4.1182 +
4.1183 + if index < 0:
4.1184 + try:
4.1185 + return self.queue[index]
4.1186 + except IndexError:
4.1187 + pass
4.1188 +
4.1189 + # With only positive positions remaining, signal the end of
4.1190 + # the collection.
4.1191 +
4.1192 + else:
4.1193 + raise
4.1194 +
4.1195 + # With no queue or positions remaining, signal the end of the
4.1196 + # collection.
4.1197 +
4.1198 + else:
4.1199 + raise
4.1200 +
4.1201 # Public functions.
4.1202
4.1203 def connect_selectors(selectors):
4.1204 @@ -792,10 +1215,19 @@
4.1205 """
4.1206
4.1207 current = selectors[0]
4.1208 - current.first = True
4.1209 + current.first = first = True
4.1210 +
4.1211 for selector in selectors[1:]:
4.1212 current.selecting = selector
4.1213 +
4.1214 + # Allow selectors within the limit selector to act as if they are first
4.1215 + # in the chain and will operate using the supplied datetime context.
4.1216 +
4.1217 + first = isinstance(current, LimitSelector)
4.1218 +
4.1219 current = selector
4.1220 + current.first = first
4.1221 +
4.1222 return selectors[0]
4.1223
4.1224 def get_selector(dt, qualifiers):