1.1 --- a/tests/qualifiers.py Sun Oct 22 17:02:40 2017 +0200
1.2 +++ b/tests/qualifiers.py Mon Oct 23 23:05:57 2017 +0200
1.3 @@ -69,7 +69,8 @@
1.4 print
1.5
1.6 qualifiers = [
1.7 - ("DAILY", {"interval" : 1})
1.8 + ("DAILY", {"interval" : 1}),
1.9 + ("COUNT", {"values" : [10]})
1.10 ]
1.11
1.12 l = order_qualifiers(qualifiers)
1.13 @@ -81,7 +82,7 @@
1.14 show(l)
1.15
1.16 s = get_selector(dt, qualifiers)
1.17 -l = s.materialise(dt, (1997, 12, 24), 10)
1.18 +l = s.materialise(dt, (1997, 12, 24))
1.19 print len(l) == 10, 10, len(l)
1.20 print l[0] == (1997, 9, 2, 9, 0, 0), (1997, 9, 2, 9, 0, 0), l[0]
1.21 print l[-1] == (1997, 9, 11, 9, 0, 0), (1997, 9, 11, 9, 0, 0), l[-1]
1.22 @@ -145,7 +146,8 @@
1.23 print
1.24
1.25 qualifiers = [
1.26 - ("DAILY", {"interval" : 10})
1.27 + ("DAILY", {"interval" : 10}),
1.28 + ("COUNT", {"values" : [5]})
1.29 ]
1.30
1.31 l = order_qualifiers(qualifiers)
1.32 @@ -157,7 +159,7 @@
1.33 show(l)
1.34
1.35 s = get_selector(dt, qualifiers)
1.36 -l = s.materialise(dt, (1997, 12, 24, 0, 0, 0), 5)
1.37 +l = s.materialise(dt, (1997, 12, 24, 0, 0, 0))
1.38 print len(l) == 5, 5, len(l)
1.39 print l[0] == (1997, 9, 2, 9, 0, 0), (1997, 9, 2, 9, 0, 0), l[0]
1.40 print l[-1] == (1997, 10, 12, 9, 0, 0), (1997, 10, 12, 9, 0, 0), l[-1]
1.41 @@ -205,7 +207,8 @@
1.42 print
1.43
1.44 qualifiers = [
1.45 - ("WEEKLY", {"interval" : 1})
1.46 + ("WEEKLY", {"interval" : 1}),
1.47 + ("COUNT", {"values" : [10]})
1.48 ]
1.49
1.50 l = order_qualifiers(qualifiers)
1.51 @@ -217,7 +220,7 @@
1.52 show(l)
1.53
1.54 s = get_selector(dt, qualifiers)
1.55 -l = s.materialise(dt, (1997, 12, 24, 0, 0, 0), 10)
1.56 +l = s.materialise(dt, (1997, 12, 24, 0, 0, 0))
1.57 print len(l) == 10, 10, len(l)
1.58 print l[0] == (1997, 9, 2, 9, 0, 0), (1997, 9, 2, 9, 0, 0), l[0]
1.59 print l[-1] == (1997, 11, 4, 9, 0, 0), (1997, 11, 4, 9, 0, 0), l[-1]
1.60 @@ -243,6 +246,25 @@
1.61 print
1.62
1.63 qualifiers = [
1.64 + ("WEEKLY", {"interval" : 1})
1.65 + ]
1.66 +
1.67 +l = order_qualifiers(qualifiers)
1.68 +show(l)
1.69 +dt = (1997, 9, 2)
1.70 +l = get_datetime_structure(dt)
1.71 +show(l)
1.72 +l = combine_datetime_with_qualifiers(dt, qualifiers)
1.73 +show(l)
1.74 +
1.75 +s = get_selector(dt, qualifiers)
1.76 +l = s.materialise(dt, (1997, 12, 24, 0, 0, 0))
1.77 +print len(l) == 17, 17, len(l)
1.78 +print l[0] == (1997, 9, 2), (1997, 9, 2), l[0]
1.79 +print l[-1] == (1997, 12, 23), (1997, 12, 23), l[-1]
1.80 +print
1.81 +
1.82 +qualifiers = [
1.83 ("WEEKLY", {"interval" : 2})
1.84 ]
1.85
1.86 @@ -283,7 +305,8 @@
1.87
1.88 qualifiers = [
1.89 ("WEEKLY", {"interval" : 1}),
1.90 - ("BYDAY", {"values" : [(2, None), (4, None)]})
1.91 + ("BYDAY", {"values" : [(2, None), (4, None)]}),
1.92 + ("COUNT", {"values" : [10]})
1.93 ]
1.94
1.95 l = order_qualifiers(qualifiers)
1.96 @@ -295,7 +318,7 @@
1.97 show(l)
1.98
1.99 s = get_selector(dt, qualifiers)
1.100 -l = s.materialise(dt, (1997, 12, 24, 0, 0, 0), 10)
1.101 +l = s.materialise(dt, (1997, 12, 24, 0, 0, 0))
1.102 print len(l) == 10, 10, len(l)
1.103 print l[0] == (1997, 9, 2, 9, 0, 0), (1997, 9, 2, 9, 0, 0), l[0]
1.104 print l[-1] == (1997, 10, 2, 9, 0, 0), (1997, 10, 2, 9, 0, 0), l[-1]
1.105 @@ -323,7 +346,8 @@
1.106
1.107 qualifiers = [
1.108 ("WEEKLY", {"interval" : 2}),
1.109 - ("BYDAY", {"values" : [(2, None), (4, None)]})
1.110 + ("BYDAY", {"values" : [(2, None), (4, None)]}),
1.111 + ("COUNT", {"values" : [8]})
1.112 ]
1.113
1.114 l = order_qualifiers(qualifiers)
1.115 @@ -335,7 +359,7 @@
1.116 show(l)
1.117
1.118 s = get_selector(dt, qualifiers)
1.119 -l = s.materialise(dt, (1997, 12, 24, 0, 0, 0), 8)
1.120 +l = s.materialise(dt, (1997, 12, 24, 0, 0, 0))
1.121 print len(l) == 8, 8, len(l)
1.122 print l[0] == (1997, 9, 2, 9, 0, 0), (1997, 9, 2, 9, 0, 0), l[0]
1.123 print l[-1] == (1997, 10, 16, 9, 0, 0), (1997, 10, 16, 9, 0, 0), l[-1]
1.124 @@ -343,7 +367,8 @@
1.125
1.126 qualifiers = [
1.127 ("MONTHLY", {"interval" : 1}),
1.128 - ("BYDAY", {"values" : [(5, 1)]})
1.129 + ("BYDAY", {"values" : [(5, 1)]}),
1.130 + ("COUNT", {"values" : [10]})
1.131 ]
1.132
1.133 l = order_qualifiers(qualifiers)
1.134 @@ -355,7 +380,7 @@
1.135 show(l)
1.136
1.137 s = get_selector(dt, qualifiers)
1.138 -l = s.materialise(dt, (1998, 12, 24, 0, 0, 0), 10)
1.139 +l = s.materialise(dt, (1998, 12, 24, 0, 0, 0))
1.140 print len(l) == 10, 10, len(l)
1.141 print l[0] == (1997, 9, 5, 9, 0, 0), (1997, 9, 5, 9, 0, 0), l[0]
1.142 print l[-1] == (1998, 6, 5, 9, 0, 0), (1998, 6, 5, 9, 0, 0), l[-1]
1.143 @@ -383,7 +408,8 @@
1.144
1.145 qualifiers = [
1.146 ("MONTHLY", {"interval" : 2}),
1.147 - ("BYDAY", {"values" : [(7, 1), (7, -1)]})
1.148 + ("BYDAY", {"values" : [(7, 1), (7, -1)]}),
1.149 + ("COUNT", {"values" : [10]})
1.150 ]
1.151
1.152 l = order_qualifiers(qualifiers)
1.153 @@ -395,7 +421,7 @@
1.154 show(l)
1.155
1.156 s = get_selector(dt, qualifiers)
1.157 -l = s.materialise(dt, (1998, 12, 24, 0, 0, 0), 10)
1.158 +l = s.materialise(dt, (1998, 12, 24, 0, 0, 0))
1.159 print len(l) == 10, 10, len(l)
1.160 print l[0] == (1997, 9, 7, 9, 0, 0), (1997, 9, 7, 9, 0, 0), l[0]
1.161 print l[-1] == (1998, 5, 31, 9, 0, 0), (1998, 5, 31, 9, 0, 0), l[-1]
1.162 @@ -403,7 +429,8 @@
1.163
1.164 qualifiers = [
1.165 ("MONTHLY", {"interval" : 1}),
1.166 - ("BYDAY", {"values" : [(1, -2)]})
1.167 + ("BYDAY", {"values" : [(1, -2)]}),
1.168 + ("COUNT", {"values" : [6]})
1.169 ]
1.170
1.171 l = order_qualifiers(qualifiers)
1.172 @@ -415,7 +442,7 @@
1.173 show(l)
1.174
1.175 s = get_selector(dt, qualifiers)
1.176 -l = s.materialise(dt, (1998, 12, 24, 0, 0, 0), 6)
1.177 +l = s.materialise(dt, (1998, 12, 24, 0, 0, 0))
1.178 print len(l) == 6, 6, len(l)
1.179 print l[0] == (1997, 9, 22, 9, 0, 0), (1997, 9, 22, 9, 0, 0), l[0]
1.180 print l[-1] == (1998, 2, 16, 9, 0, 0), (1998, 2, 16, 9, 0, 0), l[-1]
1.181 @@ -423,7 +450,8 @@
1.182
1.183 qualifiers = [
1.184 ("MONTHLY", {"interval" : 1}),
1.185 - ("BYMONTHDAY", {"values" : [-3]})
1.186 + ("BYMONTHDAY", {"values" : [-3]}),
1.187 + ("COUNT", {"values" : [6]})
1.188 ]
1.189
1.190 l = order_qualifiers(qualifiers)
1.191 @@ -435,7 +463,7 @@
1.192 show(l)
1.193
1.194 s = get_selector(dt, qualifiers)
1.195 -l = s.materialise(dt, (1998, 12, 24, 0, 0, 0), 6)
1.196 +l = s.materialise(dt, (1998, 12, 24, 0, 0, 0))
1.197 print len(l) == 6, 6, len(l)
1.198 print l[0] == (1997, 9, 28, 9, 0, 0), (1997, 9, 28, 9, 0, 0), l[0]
1.199 print l[-1] == (1998, 2, 26, 9, 0, 0), (1998, 2, 26, 9, 0, 0), l[-1]
1.200 @@ -443,7 +471,8 @@
1.201
1.202 qualifiers = [
1.203 ("MONTHLY", {"interval" : 1}),
1.204 - ("BYMONTHDAY", {"values" : [15, 2]}) # test ordering
1.205 + ("BYMONTHDAY", {"values" : [15, 2]}), # test ordering
1.206 + ("COUNT", {"values" : [10]})
1.207 ]
1.208
1.209 l = order_qualifiers(qualifiers)
1.210 @@ -455,7 +484,7 @@
1.211 show(l)
1.212
1.213 s = get_selector(dt, qualifiers)
1.214 -l = s.materialise(dt, (1998, 12, 24, 0, 0, 0), 10)
1.215 +l = s.materialise(dt, (1998, 12, 24, 0, 0, 0))
1.216 print len(l) == 10, 10, len(l)
1.217 print l[0] == (1997, 9, 2, 9, 0, 0), (1997, 9, 2, 9, 0, 0), l[0]
1.218 print l[-1] == (1998, 1, 15, 9, 0, 0), (1998, 1, 15, 9, 0, 0), l[-1]
1.219 @@ -463,7 +492,8 @@
1.220
1.221 qualifiers = [
1.222 ("MONTHLY", {"interval" : 1}),
1.223 - ("BYMONTHDAY", {"values" : [1, -1]})
1.224 + ("BYMONTHDAY", {"values" : [1, -1]}),
1.225 + ("COUNT", {"values" : [10]})
1.226 ]
1.227
1.228 l = order_qualifiers(qualifiers)
1.229 @@ -475,7 +505,7 @@
1.230 show(l)
1.231
1.232 s = get_selector(dt, qualifiers)
1.233 -l = s.materialise(dt, (1998, 12, 24, 0, 0, 0), 10)
1.234 +l = s.materialise(dt, (1998, 12, 24, 0, 0, 0))
1.235 print len(l) == 10, 10, len(l)
1.236 print l[0] == (1997, 9, 30, 9, 0, 0), (1997, 9, 30, 9, 0, 0), l[0]
1.237 print l[-1] == (1998, 2, 1, 9, 0, 0), (1998, 2, 1, 9, 0, 0), l[-1]
1.238 @@ -483,7 +513,8 @@
1.239
1.240 qualifiers = [
1.241 ("MONTHLY", {"interval" : 18}),
1.242 - ("BYMONTHDAY", {"values" : [10, 11, 12, 13, 14, 15]})
1.243 + ("BYMONTHDAY", {"values" : [10, 11, 12, 13, 14, 15]}),
1.244 + ("COUNT", {"values" : [10]})
1.245 ]
1.246
1.247 l = order_qualifiers(qualifiers)
1.248 @@ -495,7 +526,7 @@
1.249 show(l)
1.250
1.251 s = get_selector(dt, qualifiers)
1.252 -l = s.materialise(dt, (1999, 12, 24, 0, 0, 0), 10)
1.253 +l = s.materialise(dt, (1999, 12, 24, 0, 0, 0))
1.254 print len(l) == 10, 10, len(l)
1.255 print l[0] == (1997, 9, 10, 9, 0, 0), (1997, 9, 10, 9, 0, 0), l[0]
1.256 print l[-1] == (1999, 3, 13, 9, 0, 0), (1999, 3, 13, 9, 0, 0), l[-1]
1.257 @@ -523,7 +554,8 @@
1.258
1.259 qualifiers = [
1.260 ("YEARLY", {"interval" : 1}),
1.261 - ("BYMONTH", {"values" : [6, 7]})
1.262 + ("BYMONTH", {"values" : [6, 7]}),
1.263 + ("COUNT", {"values" : [10]})
1.264 ]
1.265
1.266 l = order_qualifiers(qualifiers)
1.267 @@ -535,7 +567,7 @@
1.268 show(l)
1.269
1.270 s = get_selector(dt, qualifiers)
1.271 -l = s.materialise(dt, (2001, 12, 24, 0, 0, 0), 10)
1.272 +l = s.materialise(dt, (2001, 12, 24, 0, 0, 0))
1.273 print len(l) == 10, 10, len(l)
1.274 print l[0] == (1997, 6, 10, 9, 0, 0), (1997, 6, 10, 9, 0, 0), l[0]
1.275 print l[-1] == (2001, 7, 10, 9, 0, 0), (2001, 7, 10, 9, 0, 0), l[-1]
1.276 @@ -543,7 +575,8 @@
1.277
1.278 qualifiers = [
1.279 ("YEARLY", {"interval" : 2}),
1.280 - ("BYMONTH", {"values" : [1, 2, 3]})
1.281 + ("BYMONTH", {"values" : [1, 2, 3]}),
1.282 + ("COUNT", {"values" : [10]})
1.283 ]
1.284
1.285 l = order_qualifiers(qualifiers)
1.286 @@ -555,7 +588,7 @@
1.287 show(l)
1.288
1.289 s = get_selector(dt, qualifiers)
1.290 -l = s.materialise(dt, (2003, 12, 24, 0, 0, 0), 10)
1.291 +l = s.materialise(dt, (2003, 12, 24, 0, 0, 0))
1.292 print len(l) == 10, 10, len(l)
1.293 print l[0] == (1997, 3, 10, 9, 0, 0), (1997, 3, 10, 9, 0, 0), l[0]
1.294 print l[-1] == (2003, 3, 10, 9, 0, 0), (2003, 3, 10, 9, 0, 0), l[-1]
1.295 @@ -563,7 +596,8 @@
1.296
1.297 qualifiers = [
1.298 ("YEARLY", {"interval" : 3}),
1.299 - ("BYYEARDAY", {"values" : [1, 100, 200]})
1.300 + ("BYYEARDAY", {"values" : [1, 100, 200]}),
1.301 + ("COUNT", {"values" : [10]})
1.302 ]
1.303
1.304 l = order_qualifiers(qualifiers)
1.305 @@ -575,7 +609,7 @@
1.306 show(l)
1.307
1.308 s = get_selector(dt, qualifiers)
1.309 -l = s.materialise(dt, (2006, 2, 1, 0, 0, 0), 10)
1.310 +l = s.materialise(dt, (2006, 2, 1, 0, 0, 0))
1.311 print len(l) == 10, 10, len(l)
1.312 print l[0] == (1997, 1, 1, 9, 0, 0), (1997, 1, 1, 9, 0, 0), l[0]
1.313 print l[-1] == (2006, 1, 1, 9, 0, 0), (2006, 1, 1, 9, 0, 0), l[-1]
1.314 @@ -732,7 +766,9 @@
1.315
1.316 qualifiers = [
1.317 ("MONTHLY", {"interval" : 1}),
1.318 - ("BYDAY", {"values" : [(2, None), (3, None), (4, None)]})
1.319 + ("BYDAY", {"values" : [(2, None), (3, None), (4, None)]}),
1.320 + ("BYSETPOS", {"values" : [3]}),
1.321 + ("COUNT", {"values" : [3]})
1.322 ]
1.323
1.324 l = order_qualifiers(qualifiers)
1.325 @@ -744,7 +780,7 @@
1.326 show(l)
1.327
1.328 s = get_selector(dt, qualifiers)
1.329 -l = s.materialise(dt, (1997, 12, 24, 0, 0, 0), 3, [3])
1.330 +l = s.materialise(dt, (1997, 12, 24, 0, 0, 0))
1.331 print len(l) == 3, 3, len(l)
1.332 print l[0] == (1997, 9, 4, 9, 0, 0), (1997, 9, 4, 9, 0, 0), l[0]
1.333 print l[-1] == (1997, 11, 6, 9, 0, 0), (1997, 11, 6, 9, 0, 0), l[-1]
1.334 @@ -754,7 +790,8 @@
1.335
1.336 qualifiers = [
1.337 ("MONTHLY", {"interval" : 1}),
1.338 - ("BYDAY", {"values" : [(1, None), (2, None), (3, None), (4, None), (5, None)]})
1.339 + ("BYDAY", {"values" : [(1, None), (2, None), (3, None), (4, None), (5, None)]}),
1.340 + ("BYSETPOS", {"values" : [-2]})
1.341 ]
1.342
1.343 l = order_qualifiers(qualifiers)
1.344 @@ -766,7 +803,7 @@
1.345 show(l)
1.346
1.347 s = get_selector(dt, qualifiers)
1.348 -l = s.materialise(dt, (1998, 4, 1, 0, 0, 0), None, [-2])
1.349 +l = s.materialise(dt, (1998, 4, 1, 0, 0, 0))
1.350 print len(l) == 7, 7, len(l)
1.351 print l[0] == (1997, 9, 29, 9, 0, 0), (1997, 9, 29, 9, 0, 0), l[0]
1.352 print l[-1] == (1998, 3, 30, 9, 0, 0), (1998, 3, 30, 9, 0, 0), l[-1]
2.1 --- a/vRecurrence.py Sun Oct 22 17:02:40 2017 +0200
2.2 +++ b/vRecurrence.py Mon Oct 23 23:05:57 2017 +0200
2.3 @@ -141,14 +141,11 @@
2.4
2.5 d = {}
2.6 for value in values:
2.7 - parts = value.split("=", 1)
2.8 - if len(parts) < 2:
2.9 + try:
2.10 + key, value = value.split("=", 1)
2.11 + d[key] = value
2.12 + except ValueError:
2.13 continue
2.14 - key, value = parts
2.15 - if key in ("COUNT", "BYSETPOS"):
2.16 - d[key] = int(value)
2.17 - else:
2.18 - d[key] = value
2.19 return d
2.20
2.21 def get_qualifiers(values):
2.22 @@ -163,10 +160,10 @@
2.23 interval = 1
2.24
2.25 for value in values:
2.26 - parts = value.split("=", 1)
2.27 - if len(parts) < 2:
2.28 + try:
2.29 + key, value = value.split("=", 1)
2.30 + except ValueError:
2.31 continue
2.32 - key, value = parts
2.33
2.34 # Accept frequency indicators as qualifiers.
2.35
2.36 @@ -179,9 +176,9 @@
2.37 interval = int(value)
2.38 continue
2.39
2.40 - # Accept enumerators as qualifiers.
2.41 + # Accept result set selection, truncation and enumerators as qualifiers.
2.42
2.43 - elif enum.has_key(key):
2.44 + elif key in ("BYSETPOS", "COUNT") or enum.has_key(key):
2.45 qualifier = (key, {"values" : get_qualifier_values(key, value)})
2.46
2.47 # Ignore other items.
2.48 @@ -232,6 +229,12 @@
2.49 "Return the 'qualifiers' in order of increasing resolution."
2.50
2.51 l = []
2.52 + max_level = 0
2.53 +
2.54 + # Special qualifiers.
2.55 +
2.56 + setpos = None
2.57 + count = None
2.58
2.59 for qualifier, args in qualifiers:
2.60
2.61 @@ -240,18 +243,43 @@
2.62
2.63 if enum.has_key(qualifier):
2.64 level = enum[qualifier]
2.65 +
2.66 + # Certain enumerators produce their values in a special way.
2.67 +
2.68 if special_enum_levels.has_key(qualifier):
2.69 args["interval"] = 1
2.70 selector = special_enum_levels[qualifier]
2.71 else:
2.72 selector = Enum
2.73 +
2.74 + elif qualifier == "BYSETPOS":
2.75 + setpos = args
2.76 + continue
2.77 +
2.78 + elif qualifier == "COUNT":
2.79 + count = args
2.80 + continue
2.81 +
2.82 else:
2.83 level = freq[qualifier]
2.84 selector = Pattern
2.85
2.86 l.append(selector(level, args, qualifier))
2.87 + max_level = max(level, max_level)
2.88
2.89 - l.sort(key=lambda x: x.level)
2.90 + # Add the result set selector at the maximum level of enumeration.
2.91 +
2.92 + if setpos is not None:
2.93 + l.append(PositionSelector(max_level, setpos, "BYSETPOS"))
2.94 +
2.95 + # Add the result set truncator at the top level.
2.96 +
2.97 + if count is not None:
2.98 + l.append(LimitSelector(0, count, "COUNT"))
2.99 +
2.100 + # Make BYSETPOS sort earlier than the enumeration it modifies.
2.101 +
2.102 + l.sort(key=lambda x: (x.level, x.qualifier != "BYSETPOS" and 1 or 0))
2.103 return l
2.104
2.105 def get_datetime_structure(datetime):
2.106 @@ -650,9 +678,10 @@
2.107 "Convert 'setpos' to 0-based indexes."
2.108
2.109 l = []
2.110 - for pos in setpos:
2.111 - index = pos < 0 and pos or pos - 1
2.112 - l.append(index)
2.113 + if setpos:
2.114 + for pos in setpos:
2.115 + index = pos < 0 and pos or pos - 1
2.116 + l.append(index)
2.117 return l
2.118
2.119 # Classes for producing instances from recurrence structures.
2.120 @@ -681,12 +710,11 @@
2.121 return "%s(%r, %r, %r, %r)" % (self.__class__.__name__, self.level,
2.122 self.args, self.qualifier, self.first)
2.123
2.124 - def materialise(self, start, end, count=None, setpos=None, inclusive=False):
2.125 + def materialise(self, start, end, count=None, inclusive=False):
2.126
2.127 """
2.128 Starting at 'start', materialise instances up to but not including any
2.129 - at 'end' or later, returning at most 'count' if specified, and returning
2.130 - only the occurrences indicated by 'setpos' if specified. A list of
2.131 + at 'end' or later, returning at most 'count' if specified. A list of
2.132 instances is returned.
2.133
2.134 If 'inclusive' is specified, the selection of instances will include the
2.135 @@ -695,66 +723,36 @@
2.136
2.137 start = to_tuple(start)
2.138 end = to_tuple(end)
2.139 - counter = Counter(count)
2.140 - results = self.materialise_items(start, start, end, counter, setpos, inclusive)
2.141 - return results[:count]
2.142 -
2.143 - def materialise_item(self, current, earliest, next, counter, setpos=None, inclusive=False):
2.144 -
2.145 - """
2.146 - Given the 'current' instance, the 'earliest' acceptable instance, the
2.147 - 'next' instance, an instance 'counter', and the optional 'setpos'
2.148 - criteria, return a list of result items. Where no selection within the
2.149 - current instance occurs, the current instance will be returned as a
2.150 - result if the same or later than the earliest acceptable instance.
2.151 - """
2.152 -
2.153 - if self.selecting:
2.154 - return self.selecting.materialise_items(current, earliest, next,
2.155 - counter, setpos, inclusive)
2.156 - else:
2.157 - return [current]
2.158 -
2.159 - def select_positions(self, results, setpos):
2.160 -
2.161 - "Select in 'results' the 1-based positions given by 'setpos'."
2.162 -
2.163 - l = []
2.164 - for index in convert_positions(setpos):
2.165 - l.append(results[index])
2.166 - return l
2.167 -
2.168 - def filter_by_period(self, result, start, end, inclusive):
2.169 -
2.170 - "Return whether 'result' occurs at or after 'start' and before 'end'."
2.171 -
2.172 - return start <= result and (inclusive and result <= end or result < end)
2.173 + results = self.materialise_items(start, start, end, inclusive)
2.174 + return list(results)[:count]
2.175
2.176 class Pattern(Selector):
2.177
2.178 "A selector of time periods according to a repeating pattern."
2.179
2.180 - def materialise_items(self, context, start, end, counter, setpos=None, inclusive=False):
2.181 + def __init__(self, level, args, qualifier, selecting=None, first=False):
2.182 + Selector.__init__(self, level, args, qualifier, selecting, first)
2.183 +
2.184 + multiple = get_multiple(self.qualifier)
2.185 + interval = self.args.get("interval", 1)
2.186 +
2.187 + # Define the step between result periods.
2.188 +
2.189 + self.step = scale(interval * multiple, level)
2.190 +
2.191 + # Define the scale of a single period.
2.192 +
2.193 + self.unit_step = scale(multiple, level)
2.194 +
2.195 + def materialise_items(self, context, start, end, inclusive=False):
2.196
2.197 """
2.198 - Bounded by the given 'context', return periods within 'start' to 'end',
2.199 - updating the 'counter', selecting only the indexes specified by 'setpos'
2.200 - (if given).
2.201 + Bounded by the given 'context', return periods within 'start' to 'end'.
2.202
2.203 If 'inclusive' is specified, the selection of periods will include those
2.204 starting at the end of the search period, if present in the results.
2.205 """
2.206
2.207 - # Define the step between result periods.
2.208 -
2.209 - multiple = get_multiple(self.qualifier)
2.210 - interval = self.args.get("interval", 1) * multiple
2.211 - step = scale(interval, self.level)
2.212 -
2.213 - # Define the scale of a single period.
2.214 -
2.215 - unit_step = scale(multiple, self.level)
2.216 -
2.217 # Employ the context as the current period if this is the first
2.218 # qualifier in the selection chain.
2.219
2.220 @@ -767,51 +765,18 @@
2.221 else:
2.222 current = precision(context, self.level, firstvalues[self.level])
2.223
2.224 - results = []
2.225 -
2.226 - # Obtain periods before the end (and also at the end if inclusive),
2.227 - # provided that any limit imposed by the counter has not been exceeded.
2.228 -
2.229 - while (inclusive and current <= end or current < end) and \
2.230 - not counter.at_limit():
2.231 -
2.232 - # Increment the current datetime by the step for the next period.
2.233 -
2.234 - next = update(current, step)
2.235 -
2.236 - # Determine the end point of the current period.
2.237 -
2.238 - current_end = update(current, unit_step)
2.239 -
2.240 - # Obtain any period or periods within the bounds defined by the
2.241 - # current period and any contraining start and end points, plus
2.242 - # counter, setpos and inclusive details.
2.243 -
2.244 - interval_results = self.materialise_item(current,
2.245 - max(current, start), min(current_end, end),
2.246 - counter, setpos, inclusive)
2.247 -
2.248 - # Update the counter with the number of identified results.
2.249 -
2.250 - counter += len(interval_results)
2.251 -
2.252 - # Accumulate the results.
2.253 -
2.254 - results += interval_results
2.255 -
2.256 - # Visit the next instance.
2.257 -
2.258 - current = next
2.259 -
2.260 - return results
2.261 + return PatternIterator(self, current, start, end, inclusive,
2.262 + self.step, self.unit_step)
2.263
2.264 class WeekDayFilter(Selector):
2.265
2.266 "A selector of instances specified in terms of day numbers."
2.267
2.268 - def materialise_items(self, context, start, end, counter, setpos=None, inclusive=False):
2.269 - step = scale(1, WEEKS)
2.270 - results = []
2.271 + def __init__(self, level, args, qualifier, selecting=None, first=False):
2.272 + Selector.__init__(self, level, args, qualifier, selecting, first)
2.273 + self.step = scale(1, WEEKS)
2.274 +
2.275 + def materialise_items(self, context, start, end, inclusive=False):
2.276
2.277 # Get weekdays in the year.
2.278
2.279 @@ -830,130 +795,42 @@
2.280 else:
2.281 current = context
2.282 values = [value for (value, index) in self.args["values"]]
2.283 -
2.284 - while (inclusive and current <= end or current < end):
2.285 - next = update(current, step)
2.286 -
2.287 - if date(*current).isoweekday() in values:
2.288 - results += self.materialise_item(current,
2.289 - max(current, start), min(next, end),
2.290 - counter, inclusive=inclusive)
2.291 - current = next
2.292 -
2.293 - if setpos:
2.294 - return self.select_positions(results, setpos)
2.295 - else:
2.296 - return results
2.297 -
2.298 - # Find each of the given days.
2.299 + return WeekDayIterator(self, current, start, end, inclusive, self.step, values)
2.300
2.301 - for value, index in sort_weekdays(self.args["values"], first_day, last_day):
2.302 - offset = timedelta(7 * (index - 1))
2.303 -
2.304 - current = precision(to_tuple(get_first_day(first_day, value) + offset), DAYS)
2.305 - next = update(current, step)
2.306 -
2.307 - # To support setpos, only current and next bound the search, not
2.308 - # the period in addition.
2.309 -
2.310 - results += self.materialise_item(current, current, next, counter,
2.311 - inclusive=inclusive)
2.312 -
2.313 - # Extract selected positions and remove out-of-period instances.
2.314 -
2.315 - if setpos:
2.316 - results = self.select_positions(results, setpos)
2.317 -
2.318 - return filter(lambda result:
2.319 - self.filter_by_period(result, start, end, inclusive),
2.320 - results)
2.321 + current = first_day
2.322 + values = sort_weekdays(self.args["values"], first_day, last_day)
2.323 + return WeekDayGeneralIterator(self, current, start, end, inclusive, self.step, values)
2.324
2.325 class Enum(Selector):
2.326
2.327 "A generic value selector."
2.328
2.329 - def materialise_items(self, context, start, end, counter, setpos=None, inclusive=False):
2.330 - step = scale(1, self.level)
2.331 - results = []
2.332 -
2.333 - # Select each value at the current resolution.
2.334 -
2.335 - for value in sort_values(self.args["values"]):
2.336 - current = precision(context, self.level, value)
2.337 - next = update(current, step)
2.338 + def __init__(self, level, args, qualifier, selecting=None, first=False):
2.339 + Selector.__init__(self, level, args, qualifier, selecting, first)
2.340 + self.step = scale(1, level)
2.341
2.342 - # To support setpos, only current and next bound the search, not
2.343 - # the period in addition.
2.344 -
2.345 - results += self.materialise_item(current, current, next, counter,
2.346 - setpos, inclusive)
2.347 -
2.348 - # Extract selected positions and remove out-of-period instances.
2.349 -
2.350 - if setpos:
2.351 - results = self.select_positions(results, setpos)
2.352 -
2.353 - return filter(lambda result:
2.354 - self.filter_by_period(result, start, end, inclusive),
2.355 - results)
2.356 + def materialise_items(self, context, start, end, inclusive=False):
2.357 + values = sort_values(self.args["values"])
2.358 + return EnumIterator(self, context, start, end, inclusive, self.step, values)
2.359
2.360 class MonthDayFilter(Enum):
2.361
2.362 "A selector of month days."
2.363
2.364 - def materialise_items(self, context, start, end, counter, setpos=None, inclusive=False):
2.365 - step = scale(1, self.level)
2.366 - results = []
2.367 -
2.368 + def materialise_items(self, context, start, end, inclusive=False):
2.369 last_day = end_of_month(context)[2]
2.370 -
2.371 - for value in sort_values(self.args["values"], last_day):
2.372 - current = precision(context, self.level, value)
2.373 - next = update(current, step)
2.374 -
2.375 - # To support setpos, only current and next bound the search, not
2.376 - # the period in addition.
2.377 -
2.378 - results += self.materialise_item(current, current, next, counter,
2.379 - inclusive=inclusive)
2.380 -
2.381 - # Extract selected positions and remove out-of-period instances.
2.382 -
2.383 - if setpos:
2.384 - results = self.select_positions(results, setpos)
2.385 -
2.386 - return filter(lambda result:
2.387 - self.filter_by_period(result, start, end, inclusive),
2.388 - results)
2.389 + values = sort_values(self.args["values"], last_day)
2.390 + return EnumIterator(self, context, start, end, inclusive, self.step, values)
2.391
2.392 class YearDayFilter(Enum):
2.393
2.394 "A selector of days in years."
2.395
2.396 - def materialise_items(self, context, start, end, counter, setpos=None, inclusive=False):
2.397 - step = scale(1, self.level)
2.398 - results = []
2.399 -
2.400 + def materialise_items(self, context, start, end, inclusive=False):
2.401 + first_day = start_of_year(context)
2.402 year_length = get_year_length(context)
2.403 -
2.404 - for value in sort_values(self.args["values"], year_length):
2.405 - current = day_in_year(context, value)
2.406 - next = update(current, step)
2.407 -
2.408 - # To support setpos, only current and next bound the search, not
2.409 - # the period in addition.
2.410 -
2.411 - results += self.materialise_item(current, current, next, counter,
2.412 - inclusive=inclusive)
2.413 -
2.414 - # Extract selected positions and remove out-of-period instances.
2.415 -
2.416 - if setpos:
2.417 - results = self.select_positions(results, setpos)
2.418 -
2.419 - return filter(lambda result:
2.420 - self.filter_by_period(result, start, end, inclusive),
2.421 - results)
2.422 + values = sort_values(self.args["values"], year_length)
2.423 + return YearDayFilterIterator(self, first_day, start, end, inclusive, self.step, values)
2.424
2.425 special_enum_levels = {
2.426 "BYDAY" : WeekDayFilter,
2.427 @@ -961,20 +838,361 @@
2.428 "BYYEARDAY" : YearDayFilter,
2.429 }
2.430
2.431 -class Counter:
2.432 +class LimitSelector(Selector):
2.433 +
2.434 + "A result set limit selector."
2.435 +
2.436 + def materialise_items(self, context, start, end, inclusive=False):
2.437 + limit = self.args["values"][0]
2.438 + return LimitIterator(self, context, start, end, inclusive, limit)
2.439 +
2.440 +class PositionSelector(Selector):
2.441
2.442 - "A counter to track instance quantities."
2.443 + "A result set position selector."
2.444 +
2.445 + def __init__(self, level, args, qualifier, selecting=None, first=False):
2.446 + Selector.__init__(self, level, args, qualifier, selecting, first)
2.447 + self.step = scale(1, level)
2.448 +
2.449 + def materialise_items(self, context, start, end, inclusive=False):
2.450 + values = convert_positions(sort_values(self.args["values"]))
2.451 + return PositionIterator(self, context, start, end, inclusive, self.step, values)
2.452 +
2.453 +# Iterator classes.
2.454
2.455 - def __init__(self, limit):
2.456 - self.current = 0
2.457 - self.limit = limit
2.458 +class SelectorIterator:
2.459 +
2.460 + "An iterator over selected data."
2.461 +
2.462 + def __init__(self, selector, current, start, end, inclusive):
2.463 +
2.464 + """
2.465 + Define an iterator having the 'current' point in time, 'start' and 'end'
2.466 + limits, and whether the selection is 'inclusive'.
2.467 + """
2.468
2.469 - def __iadd__(self, n):
2.470 - self.current += n
2.471 + self.selector = selector
2.472 + self.current = current
2.473 + self.start = start
2.474 + self.end = end
2.475 + self.inclusive = inclusive
2.476 +
2.477 + # Iterator over selections within this selection.
2.478 +
2.479 + self.iterator = None
2.480 +
2.481 + def __iter__(self):
2.482 return self
2.483
2.484 + def next_item(self, earliest, next):
2.485 +
2.486 + """
2.487 + Given the 'earliest' acceptable instance and the 'next' instance, return
2.488 + a list of result items.
2.489 +
2.490 + Where no selection within the current instance occurs, the current
2.491 + instance will be returned as a result if the same or later than the
2.492 + earliest acceptable instance.
2.493 + """
2.494 +
2.495 + # Obtain an item from a selector operating within this selection.
2.496 +
2.497 + selecting = self.selector.selecting
2.498 +
2.499 + if selecting:
2.500 +
2.501 + # Obtain an iterator for any selector within the current period.
2.502 +
2.503 + if not self.iterator:
2.504 + self.iterator = selecting.materialise_items(self.current,
2.505 + earliest, next, self.inclusive)
2.506 +
2.507 + # Attempt to obtain and return a value.
2.508 +
2.509 + return self.iterator.next()
2.510 +
2.511 + # Return items within this selection.
2.512 +
2.513 + else:
2.514 + return self.current
2.515 +
2.516 + def filter_by_period(self, result):
2.517 +
2.518 + "Return whether 'result' occurs within the selection period."
2.519 +
2.520 + return (self.inclusive and result <= self.end or result < self.end) and \
2.521 + self.start <= result
2.522 +
2.523 def at_limit(self):
2.524 - return self.limit is not None and self.current >= self.limit
2.525 +
2.526 + "Obtain periods before the end (and also at the end if inclusive)."
2.527 +
2.528 + return not self.inclusive and self.current == self.end or \
2.529 + self.current > self.end
2.530 +
2.531 +class PatternIterator(SelectorIterator):
2.532 +
2.533 + "An iterator for a general selection pattern."
2.534 +
2.535 + def __init__(self, selector, current, start, end, inclusive, step, unit_step):
2.536 + SelectorIterator.__init__(self, selector, current, start, end, inclusive)
2.537 + self.step = step
2.538 + self.unit_step = unit_step
2.539 +
2.540 + def next(self):
2.541 +
2.542 + "Return the next value."
2.543 +
2.544 + while not self.at_limit():
2.545 +
2.546 + # Increment the current datetime by the step for the next period.
2.547 +
2.548 + next = update(self.current, self.step)
2.549 +
2.550 + # Determine the end point of the current period.
2.551 +
2.552 + current_end = update(self.current, self.unit_step)
2.553 +
2.554 + # Obtain any period or periods within the bounds defined by the
2.555 + # current period and any contraining start and end points.
2.556 +
2.557 + try:
2.558 + result = self.next_item(max(self.current, self.start),
2.559 + min(current_end, self.end))
2.560 +
2.561 + # Obtain the next period if not selecting within this period.
2.562 +
2.563 + if not self.selector.selecting:
2.564 + self.current = next
2.565 +
2.566 + # Filter out periods.
2.567 +
2.568 + if self.filter_by_period(result):
2.569 + return result
2.570 +
2.571 + # Move on to the next instance.
2.572 +
2.573 + except StopIteration:
2.574 + self.current = next
2.575 + self.iterator = None
2.576 +
2.577 + raise StopIteration
2.578 +
2.579 +class WeekDayIterator(SelectorIterator):
2.580 +
2.581 + "An iterator over weekday selections in week periods."
2.582 +
2.583 + def __init__(self, selector, current, start, end, inclusive, step, values):
2.584 + SelectorIterator.__init__(self, selector, current, start, end, inclusive)
2.585 + self.step = step
2.586 + self.values = values
2.587 +
2.588 + def next(self):
2.589 +
2.590 + "Return the next value."
2.591 +
2.592 + while not self.at_limit():
2.593 +
2.594 + # Increment the current datetime by the step for the next period.
2.595 +
2.596 + next = update(self.current, self.step)
2.597 +
2.598 + # Determine whether the day is one chosen.
2.599 +
2.600 + if date(*self.current).isoweekday() in self.values:
2.601 + try:
2.602 + result = self.next_item(max(self.current, self.start),
2.603 + min(next, self.end))
2.604 +
2.605 + # Obtain the next period if not selecting within this period.
2.606 +
2.607 + if not self.selector.selecting:
2.608 + self.current = next
2.609 +
2.610 + return result
2.611 +
2.612 + # Move on to the next instance.
2.613 +
2.614 + except StopIteration:
2.615 + self.current = next
2.616 + self.iterator = None
2.617 +
2.618 + else:
2.619 + self.current = next
2.620 + self.iterator = None
2.621 +
2.622 + raise StopIteration
2.623 +
2.624 +class EnumIterator(SelectorIterator):
2.625 +
2.626 + "An iterator over specific value selections."
2.627 +
2.628 + def __init__(self, selector, current, start, end, inclusive, step, values):
2.629 + SelectorIterator.__init__(self, selector, current, start, end, inclusive)
2.630 + self.step = step
2.631 +
2.632 + # Derive selected periods from a base and the indicated values.
2.633 +
2.634 + self.base = current
2.635 +
2.636 + # Iterate over the indicated period values.
2.637 +
2.638 + self.values = iter(values)
2.639 + self.value = None
2.640 +
2.641 + def get_selected_period(self):
2.642 +
2.643 + "Return the period indicated by the current value."
2.644 +
2.645 + return precision(self.base, self.selector.level, self.value)
2.646 +
2.647 + def next(self):
2.648 +
2.649 + "Return the next value."
2.650 +
2.651 + while True:
2.652 +
2.653 + # Find each of the given selected values.
2.654 +
2.655 + if not self.selector.selecting or self.value is None:
2.656 + self.value = self.values.next()
2.657 +
2.658 + # Select a period for each value at the current resolution.
2.659 +
2.660 + self.current = self.get_selected_period()
2.661 + next = update(self.current, self.step)
2.662 +
2.663 + # To support setpos, only current and next bound the search, not
2.664 + # the period in addition.
2.665 +
2.666 + try:
2.667 + return self.next_item(self.current, next)
2.668 +
2.669 + # Move on to the next instance.
2.670 +
2.671 + except StopIteration:
2.672 + self.value = None
2.673 + self.iterator = None
2.674 +
2.675 + raise StopIteration
2.676 +
2.677 +class WeekDayGeneralIterator(EnumIterator):
2.678 +
2.679 + "An iterator over weekday selections in month and year periods."
2.680 +
2.681 + def get_selected_period(self):
2.682 +
2.683 + "Return the day indicated by the current day value."
2.684 +
2.685 + value, index = self.value
2.686 + offset = timedelta(7 * (index - 1))
2.687 + weekday0 = get_first_day(self.base, value)
2.688 + return precision(to_tuple(weekday0 + offset), DAYS)
2.689 +
2.690 +class YearDayFilterIterator(EnumIterator):
2.691 +
2.692 + "An iterator over day-in-year selections."
2.693 +
2.694 + def get_selected_period(self):
2.695 +
2.696 + "Return the day indicated by the current day value."
2.697 +
2.698 + offset = timedelta(self.value - 1)
2.699 + return precision(to_tuple(date(*self.base) + offset), DAYS)
2.700 +
2.701 +class LimitIterator(SelectorIterator):
2.702 +
2.703 + "A result set limiting iterator."
2.704 +
2.705 + def __init__(self, selector, context, start, end, inclusive, limit):
2.706 + SelectorIterator.__init__(self, selector, context, start, end, inclusive)
2.707 + self.limit = limit
2.708 + self.count = 0
2.709 +
2.710 + def next(self):
2.711 +
2.712 + "Return the next value."
2.713 +
2.714 + if self.count < self.limit:
2.715 + self.count += 1
2.716 + result = self.next_item(self.start, self.end)
2.717 + return result
2.718 +
2.719 + raise StopIteration
2.720 +
2.721 +class PositionIterator(SelectorIterator):
2.722 +
2.723 + "An iterator over results, selecting positions."
2.724 +
2.725 + def __init__(self, selector, context, start, end, inclusive, step, positions):
2.726 + SelectorIterator.__init__(self, selector, context, start, end, inclusive)
2.727 + self.step = step
2.728 +
2.729 + # Positions to select.
2.730 +
2.731 + self.positions = positions
2.732 +
2.733 + # Queue to hold values matching the negative position values.
2.734 +
2.735 + self.queue_length = positions and positions[0] < 0 and abs(positions[0]) or 0
2.736 + self.queue = []
2.737 +
2.738 + # Result position.
2.739 +
2.740 + self.resultpos = 0
2.741 +
2.742 + def next(self):
2.743 +
2.744 + "Return the next value."
2.745 +
2.746 + while True:
2.747 + try:
2.748 + result = self.next_item(self.start, self.end)
2.749 +
2.750 + # Positive positions can have their values released immediately.
2.751 +
2.752 + selected = self.resultpos in self.positions
2.753 + self.resultpos += 1
2.754 +
2.755 + if selected:
2.756 + return result
2.757 +
2.758 + # Negative positions must be held until this iterator completes and
2.759 + # then be released.
2.760 +
2.761 + if self.queue_length:
2.762 + self.queue.append(result)
2.763 + if len(self.queue) > self.queue_length:
2.764 + del self.queue[0]
2.765 +
2.766 + except StopIteration:
2.767 +
2.768 + # With a queue and positions, attempt to find the referenced
2.769 + # positions.
2.770 +
2.771 + if self.queue and self.positions:
2.772 + index = self.positions[0]
2.773 + del self.positions[0]
2.774 +
2.775 + # Only negative positions are used at this point.
2.776 +
2.777 + if index < 0:
2.778 + try:
2.779 + return self.queue[index]
2.780 + except IndexError:
2.781 + pass
2.782 +
2.783 + # With only positive positions remaining, signal the end of
2.784 + # the collection.
2.785 +
2.786 + else:
2.787 + raise
2.788 +
2.789 + # With no queue or positions remaining, signal the end of the
2.790 + # collection.
2.791 +
2.792 + else:
2.793 + raise
2.794
2.795 # Public functions.
2.796
2.797 @@ -987,10 +1205,19 @@
2.798 """
2.799
2.800 current = selectors[0]
2.801 - current.first = True
2.802 + current.first = first = True
2.803 +
2.804 for selector in selectors[1:]:
2.805 current.selecting = selector
2.806 +
2.807 + # Allow selectors within the limit selector to act as if they are first
2.808 + # in the chain and will operate using the supplied datetime context.
2.809 +
2.810 + first = isinstance(current, LimitSelector)
2.811 +
2.812 current = selector
2.813 + current.first = first
2.814 +
2.815 return selectors[0]
2.816
2.817 def get_selector(dt, qualifiers):