source: josm/trunk/data/opening_hours.js@ 6418

Last change on this file since 6418 was 6370, checked in by simon04, 10 years ago

fix #9157 - add opening_hours validation test

This test utilizes https://github.com/ypid/opening_hours.js which is licensed
under the New (2-clause) BSD license.

File size: 131.1 KB
Line 
1(function (root, factory) {
2 //======================================================================
3 // Constants
4 //======================================================================
5 var holidays = {
6 'de': {
7 'PH': { // http://de.wikipedia.org/wiki/Feiertage_in_Deutschland
8 'Neujahrstag' : [ 1, 1 ], // month 1, day 1, whole Germany
9 'Heilige Drei Könige' : [ 1, 6, [ 'Baden-Württemberg', 'Bayern', 'Sachsen-Anhalt'] ], // only in the specified states
10 'Tag der Arbeit' : [ 5, 1 ], // whole Germany
11 'Karfreitag' : [ 'easter', -2 ], // two days before easter
12 'Ostersonntag' : [ 'easter', 0, [ 'Brandenburg'] ],
13 'Ostermontag' : [ 'easter', 1 ],
14 'Christi Himmelfahrt' : [ 'easter', 39 ],
15 'Pfingstsonntag' : [ 'easter', 49, [ 'Brandenburg'] ],
16 'Pfingstmontag' : [ 'easter', 50 ],
17 'Fronleichnam' : [ 'easter', 60, [ 'Baden-Württemberg', 'Bayern', 'Hessen', 'Nordrhein-Westfalen', 'Rheinland-Pfalz', 'Saarland' ] ],
18 'Mariä Himmelfahrt' : [ 8, 15, [ 'Saarland'] ],
19 'Tag der Deutschen Einheit' : [ 10, 3 ],
20 'Reformationstag' : [ 10, 31, [ 'Brandenburg', 'Mecklenburg-Vorpommern', 'Sachsen', 'Sachsen-Anhalt', 'Thüringen'] ],
21 'Allerheiligen' : [ 11, 1, [ 'Baden-Württemberg', 'Bayern', 'Nordrhein-Westfalen', 'Rheinland-Pfalz', 'Saarland' ] ],
22 '1. Weihnachtstag' : [ 12, 25 ],
23 '2. Weihnachtstag' : [ 12, 26 ],
24 // 'Silvester' : [ 12, 31 ], // for testing
25 },
26 'Baden-Württemberg': { // does only apply in Baden-Württemberg
27 // This more specific rule set overwrites the country wide one (they are just ignored).
28 // You may use this instead of the country wide with some
29 // additional holidays for some states, if one state
30 // totally disagrees about how to do public holidays …
31 // 'PH': {
32 // '2. Weihnachtstag' : [ 12, 26 ],
33 // },
34
35 // school holiday normally variate between states
36 'SH': [ // generated by convert_ical_to_json
37 // You may can adjust this script to use other resources (for other countries) too.
38 {
39 name: 'Osterferien',
40 2005: [ 3, 24, /* to */ 3, 24, 3, 29, /* to */ 4, 2 ],
41 2006: [ 4, 18, /* to */ 4, 22 ],
42 2007: [ 4, 2, /* to */ 4, 14 ],
43 2008: [ 3, 17, /* to */ 3, 28 ],
44 2009: [ 4, 9, /* to */ 4, 9, 4, 14, /* to */ 4, 17 ],
45 2010: [ 4, 1, /* to */ 4, 1, 4, 6, /* to */ 4, 10 ],
46 2011: [ 4, 21, /* to */ 4, 21, 4, 26, /* to */ 4, 30 ],
47 2012: [ 4, 2, /* to */ 4, 13 ],
48 2013: [ 3, 25, /* to */ 4, 5 ],
49 2014: [ 4, 14, /* to */ 4, 25 ],
50 2015: [ 3, 30, /* to */ 4, 10 ],
51 2016: [ 3, 29, /* to */ 4, 2 ],
52 2017: [ 4, 10, /* to */ 4, 21 ],
53 },
54 {
55 name: 'Pfingstferien',
56 2005: [ 5, 17, /* to */ 5, 28 ],
57 2006: [ 5, 29, /* to */ 6, 10 ],
58 2007: [ 5, 29, /* to */ 6, 9 ],
59 2008: [ 5, 13, /* to */ 5, 23 ],
60 2009: [ 5, 25, /* to */ 6, 6 ],
61 2010: [ 5, 25, /* to */ 6, 5 ],
62 2011: [ 6, 14, /* to */ 6, 25 ],
63 2012: [ 5, 29, /* to */ 6, 9 ],
64 2013: [ 5, 21, /* to */ 6, 1 ],
65 2014: [ 6, 10, /* to */ 6, 21 ],
66 2015: [ 5, 26, /* to */ 6, 6 ],
67 2016: [ 5, 17, /* to */ 5, 28 ],
68 2017: [ 6, 6, /* to */ 6, 16 ],
69 },
70 {
71 name: 'Sommerferien',
72 2005: [ 7, 28, /* to */ 9, 10 ],
73 2006: [ 8, 3, /* to */ 9, 16 ],
74 2007: [ 7, 26, /* to */ 9, 8 ],
75 2008: [ 7, 24, /* to */ 9, 6 ],
76 2009: [ 7, 30, /* to */ 9, 12 ],
77 2010: [ 7, 29, /* to */ 9, 11 ],
78 2011: [ 7, 28, /* to */ 9, 10 ],
79 2012: [ 7, 26, /* to */ 9, 8 ],
80 2013: [ 7, 25, /* to */ 9, 7 ],
81 2014: [ 7, 31, /* to */ 9, 13 ],
82 2015: [ 7, 30, /* to */ 9, 12 ],
83 2016: [ 7, 28, /* to */ 9, 10 ],
84 2017: [ 7, 27, /* to */ 9, 9 ],
85 },
86 {
87 name: 'Herbstferien',
88 2005: [ 11, 2, /* to */ 11, 4 ],
89 2006: [ 10, 30, /* to */ 11, 3 ],
90 2007: [ 10, 29, /* to */ 11, 3 ],
91 2008: [ 10, 27, /* to */ 10, 31 ],
92 2009: [ 10, 26, /* to */ 10, 31 ],
93 2010: [ 11, 2, /* to */ 11, 6 ],
94 2011: [ 10, 31, /* to */ 10, 31, 11, 2, /* to */ 11, 4 ],
95 2012: [ 10, 29, /* to */ 11, 2 ],
96 2013: [ 10, 28, /* to */ 10, 30 ],
97 2014: [ 10, 27, /* to */ 10, 30 ],
98 2015: [ 11, 2, /* to */ 11, 6 ],
99 2016: [ 11, 2, /* to */ 11, 4 ],
100 },
101 {
102 name: 'Weihnachtsferien',
103 2005: [ 12, 22, /* to */ 1, 5 ],
104 2006: [ 12, 27, /* to */ 1, 5 ],
105 2007: [ 12, 24, /* to */ 1, 5 ],
106 2008: [ 12, 22, /* to */ 1, 10 ],
107 2009: [ 12, 23, /* to */ 1, 9 ],
108 2010: [ 12, 23, /* to */ 1, 8 ],
109 2011: [ 12, 23, /* to */ 1, 5 ],
110 2012: [ 12, 24, /* to */ 1, 5 ],
111 2013: [ 12, 23, /* to */ 1, 4 ],
112 2014: [ 12, 22, /* to */ 1, 5 ],
113 2015: [ 12, 23, /* to */ 1, 9 ],
114 2016: [ 12, 23, /* to */ 1, 7 ],
115 },
116 ],
117 },
118 'Mecklenburg-Vorpommern': {
119 'SH': [
120 {
121 name: 'Winterferien',
122 2010: [ 2, 6, /* to */ 2, 20 ],
123 2011: [ 2, 7, /* to */ 2, 19 ],
124 2012: [ 2, 6, /* to */ 2, 17 ],
125 2013: [ 2, 4, /* to */ 2, 15 ],
126 2014: [ 2, 3, /* to */ 2, 15 ],
127 2015: [ 2, 2, /* to */ 2, 14 ],
128 2016: [ 2, 1, /* to */ 2, 13 ],
129 2017: [ 2, 6, /* to */ 2, 18 ],
130 },
131 {
132 name: 'Osterferien',
133 2010: [ 3, 29, /* to */ 4, 7 ],
134 2011: [ 4, 16, /* to */ 4, 27 ],
135 2012: [ 4, 2, /* to */ 4, 11 ],
136 2013: [ 3, 25, /* to */ 4, 3 ],
137 2014: [ 4, 14, /* to */ 4, 23 ],
138 2015: [ 3, 30, /* to */ 4, 8 ],
139 2016: [ 3, 21, /* to */ 3, 30 ],
140 2017: [ 4, 10, /* to */ 4, 19 ],
141 },
142 {
143 name: 'Pfingstferien',
144 2010: [ 5, 21, /* to */ 5, 22 ],
145 2011: [ 6, 10, /* to */ 6, 14 ],
146 2012: [ 5, 25, /* to */ 5, 29 ],
147 2013: [ 5, 17, /* to */ 5, 21 ],
148 2014: [ 6, 6, /* to */ 6, 10 ],
149 2015: [ 5, 22, /* to */ 5, 26 ],
150 2016: [ 5, 14, /* to */ 5, 17 ],
151 2017: [ 6, 2, /* to */ 6, 6 ],
152 },
153 {
154 name: 'Sommerferien',
155 2010: [ 7, 12, /* to */ 8, 21 ],
156 2011: [ 7, 4, /* to */ 8, 13 ],
157 2012: [ 6, 23, /* to */ 8, 4 ],
158 2013: [ 6, 22, /* to */ 8, 3 ],
159 2014: [ 7, 14, /* to */ 8, 23 ],
160 2015: [ 7, 20, /* to */ 8, 29 ],
161 2016: [ 7, 25, /* to */ 9, 3 ],
162 2017: [ 7, 24, /* to */ 9, 2 ],
163 },
164 {
165 name: 'Herbstferien',
166 2010: [ 10, 18, /* to */ 10, 23 ],
167 2011: [ 10, 17, /* to */ 10, 21 ],
168 2012: [ 10, 1, /* to */ 10, 5 ],
169 2013: [ 10, 14, /* to */ 10, 19 ],
170 2014: [ 10, 20, /* to */ 10, 25 ],
171 2015: [ 10, 24, /* to */ 10, 30 ],
172 2016: [ 10, 24, /* to */ 10, 28 ],
173 },
174 {
175 name: 'Weihnachtsferien',
176 2010: [ 12, 23, /* to */ 12, 31 ],
177 2011: [ 12, 23, /* to */ 1, 3 ],
178 2012: [ 12, 21, /* to */ 1, 4 ],
179 2013: [ 12, 23, /* to */ 1, 3 ],
180 2014: [ 12, 22, /* to */ 1, 2 ],
181 2015: [ 12, 21, /* to */ 1, 2 ],
182 2016: [ 12, 22, /* to */ 1, 2 ],
183 },
184 ],
185 },
186 'Hessen': {
187 'SH': [
188 {
189 name: 'Osterferien',
190 2010: [ 3, 29, /* to */ 4, 10 ],
191 2011: [ 4, 18, /* to */ 4, 30 ],
192 2012: [ 4, 2, /* to */ 4, 14 ],
193 2013: [ 3, 25, /* to */ 4, 6 ],
194 2014: [ 4, 14, /* to */ 4, 26 ],
195 2015: [ 3, 30, /* to */ 4, 11 ],
196 2016: [ 3, 29, /* to */ 4, 9 ],
197 2017: [ 4, 3, /* to */ 4, 15 ],
198 2018: [ 3, 26, /* to */ 4, 7 ],
199 },
200 {
201 name: 'Sommerferien',
202 2010: [ 7, 5, /* to */ 8, 14 ],
203 2011: [ 6, 27, /* to */ 8, 5 ],
204 2012: [ 7, 2, /* to */ 8, 10 ],
205 2013: [ 7, 8, /* to */ 8, 16 ],
206 2014: [ 7, 28, /* to */ 9, 5 ],
207 2015: [ 7, 27, /* to */ 9, 5 ],
208 2016: [ 7, 18, /* to */ 8, 26 ],
209 2017: [ 7, 3, /* to */ 8, 11 ],
210 },
211 {
212 name: 'Herbstferien',
213 2010: [ 10, 11, /* to */ 10, 22 ],
214 2011: [ 10, 10, /* to */ 10, 22 ],
215 2012: [ 10, 15, /* to */ 10, 27 ],
216 2013: [ 10, 14, /* to */ 10, 26 ],
217 2014: [ 10, 20, /* to */ 11, 1 ],
218 2015: [ 10, 19, /* to */ 10, 31 ],
219 2016: [ 10, 17, /* to */ 10, 29 ],
220 2017: [ 10, 9, /* to */ 10, 21 ],
221 },
222 {
223 name: 'Weihnachtsferien',
224 2010: [ 12, 20, /* to */ 1, 7 ],
225 2011: [ 12, 21, /* to */ 1, 6 ],
226 2012: [ 12, 24, /* to */ 1, 12 ],
227 2013: [ 12, 23, /* to */ 1, 11 ],
228 2014: [ 12, 22, /* to */ 1, 10 ],
229 2015: [ 12, 23, /* to */ 1, 9 ],
230 2016: [ 12, 22, /* to */ 1, 7 ],
231 2017: [ 12, 24, /* to */ 1, 13 ],
232 },
233 ],
234 },
235 'Schleswig-Holstein': {
236 'SH': [
237 {
238 name: 'Osterferien',
239 2010: [ 4, 3, /* to */ 4, 17 ],
240 2011: [ 4, 15, /* to */ 4, 30 ],
241 2012: [ 3, 30, /* to */ 4, 13 ],
242 2013: [ 3, 25, /* to */ 4, 9 ],
243 2014: [ 4, 16, /* to */ 5, 2 ],
244 2015: [ 4, 1, /* to */ 4, 17 ],
245 2016: [ 3, 24, /* to */ 4, 9 ],
246 2017: [ 4, 7, /* to */ 4, 21 ],
247 },
248 {
249 name: 'Sommerferien',
250 2010: [ 7, 12, /* to */ 8, 21 ],
251 2011: [ 7, 4, /* to */ 8, 13 ],
252 2012: [ 6, 25, /* to */ 8, 4 ],
253 2013: [ 6, 24, /* to */ 8, 3 ],
254 2014: [ 7, 14, /* to */ 8, 23 ],
255 2015: [ 7, 20, /* to */ 8, 29 ],
256 2016: [ 7, 25, /* to */ 9, 3 ],
257 2017: [ 7, 24, /* to */ 9, 2 ],
258 },
259 {
260 name: 'Pfingstferien',
261 2011: [ 6, 3, /* to */ 6, 4 ],
262 2012: [ 5, 18, /* to */ 5, 18 ],
263 2013: [ 5, 10, /* to */ 5, 10 ],
264 2014: [ 5, 30, /* to */ 5, 30 ],
265 2015: [ 5, 15, /* to */ 5, 15 ],
266 2016: [ 5, 6, /* to */ 5, 6 ],
267 2017: [ 5, 26, /* to */ 5, 26 ],
268 },
269 {
270 name: 'Herbstferien',
271 2010: [ 10, 11, /* to */ 10, 23 ],
272 2011: [ 10, 10, /* to */ 10, 22 ],
273 2012: [ 10, 4, /* to */ 10, 19 ],
274 2013: [ 10, 4, /* to */ 10, 18 ],
275 2014: [ 10, 13, /* to */ 10, 25 ],
276 2015: [ 10, 19, /* to */ 10, 31 ],
277 2016: [ 10, 17, /* to */ 10, 29 ],
278 },
279 {
280 name: 'Weihnachtsferien',
281 2010: [ 12, 23, /* to */ 1, 7 ],
282 2011: [ 12, 23, /* to */ 1, 6 ],
283 2012: [ 12, 24, /* to */ 1, 5 ],
284 2013: [ 12, 23, /* to */ 1, 6 ],
285 2014: [ 12, 22, /* to */ 1, 6 ],
286 2015: [ 12, 21, /* to */ 1, 6 ],
287 2016: [ 12, 23, /* to */ 1, 6 ],
288 },
289 ],
290 },
291 'Berlin': {
292 'SH': [
293 {
294 name: 'Winterferien',
295 2010: [ 2, 1, /* to */ 2, 6 ],
296 2011: [ 1, 31, /* to */ 2, 5 ],
297 2012: [ 1, 30, /* to */ 2, 4 ],
298 2013: [ 2, 4, /* to */ 2, 9 ],
299 2014: [ 2, 3, /* to */ 2, 8 ],
300 2015: [ 2, 2, /* to */ 2, 7 ],
301 2016: [ 2, 1, /* to */ 2, 6 ],
302 2017: [ 1, 30, /* to */ 2, 4 ],
303 },
304 {
305 name: 'Osterferien',
306 2010: [ 3, 31, /* to */ 4, 10 ],
307 2011: [ 4, 18, /* to */ 4, 30 ],
308 2012: [ 4, 2, /* to */ 4, 14, 4, 30, /* to */ 4, 30 ],
309 2013: [ 3, 25, /* to */ 4, 6 ],
310 2014: [ 4, 14, /* to */ 4, 26, 5, 2, /* to */ 5, 2 ],
311 2015: [ 3, 30, /* to */ 4, 11 ],
312 2016: [ 3, 21, /* to */ 4, 2 ],
313 2017: [ 4, 10, /* to */ 4, 22 ],
314 },
315 {
316 name: 'Pfingstferien',
317 2010: [ 5, 14, /* to */ 5, 14, 5, 25, /* to */ 5, 25 ],
318 2011: [ 6, 3, /* to */ 6, 3 ],
319 2012: [ 5, 18, /* to */ 5, 18 ],
320 2013: [ 5, 10, /* to */ 5, 10, 5, 21, /* to */ 5, 21 ],
321 2014: [ 5, 30, /* to */ 5, 30 ],
322 2015: [ 5, 15, /* to */ 5, 15 ],
323 2016: [ 5, 6, /* to */ 5, 6, 5, 17, /* to */ 5, 17 ],
324 2017: [ 5, 26, /* to */ 5, 26 ],
325 },
326 {
327 name: 'Sommerferien',
328 2010: [ 7, 7, /* to */ 8, 21 ],
329 2011: [ 6, 29, /* to */ 8, 12 ],
330 2012: [ 6, 20, /* to */ 8, 3 ],
331 2013: [ 6, 19, /* to */ 8, 2 ],
332 2014: [ 7, 9, /* to */ 8, 22 ],
333 2015: [ 7, 15, /* to */ 8, 28 ],
334 2016: [ 7, 20, /* to */ 9, 2 ],
335 2017: [ 7, 19, /* to */ 9, 1 ],
336 },
337 {
338 name: 'Herbstferien',
339 2010: [ 10, 11, /* to */ 10, 23 ],
340 2011: [ 10, 4, /* to */ 10, 14 ],
341 2012: [ 10, 1, /* to */ 10, 13 ],
342 2013: [ 9, 30, /* to */ 10, 12 ],
343 2014: [ 10, 20, /* to */ 11, 1 ],
344 2015: [ 10, 19, /* to */ 10, 31 ],
345 2016: [ 10, 17, /* to */ 10, 28 ],
346 },
347 {
348 name: 'Weihnachtsferien',
349 2010: [ 12, 23, /* to */ 1, 1 ],
350 2011: [ 12, 23, /* to */ 1, 3 ],
351 2012: [ 12, 24, /* to */ 1, 4 ],
352 2013: [ 12, 23, /* to */ 1, 3 ],
353 2014: [ 12, 22, /* to */ 1, 2 ],
354 2015: [ 12, 23, /* to */ 1, 2 ],
355 2016: [ 12, 23, /* to */ 1, 3 ],
356 },
357 ],
358 },
359 'Saarland': {
360 'SH': [
361 {
362 name: 'Winterferien',
363 2010: [ 2, 15, /* to */ 2, 20 ],
364 2011: [ 3, 7, /* to */ 3, 12 ],
365 2012: [ 2, 20, /* to */ 2, 25 ],
366 2013: [ 2, 11, /* to */ 2, 16 ],
367 2014: [ 3, 3, /* to */ 3, 8 ],
368 2015: [ 2, 16, /* to */ 2, 21 ],
369 },
370 {
371 name: 'Osterferien',
372 2010: [ 3, 29, /* to */ 4, 10 ],
373 2011: [ 4, 18, /* to */ 4, 30 ],
374 2012: [ 4, 2, /* to */ 4, 14 ],
375 2013: [ 3, 25, /* to */ 4, 6 ],
376 2014: [ 4, 14, /* to */ 4, 26 ],
377 2015: [ 3, 30, /* to */ 4, 11 ],
378 },
379 {
380 name: 'Sommerferien',
381 2010: [ 7, 5, /* to */ 8, 14 ],
382 2011: [ 6, 24, /* to */ 8, 6 ],
383 2012: [ 7, 2, /* to */ 8, 14 ],
384 2013: [ 7, 8, /* to */ 8, 17 ],
385 2014: [ 7, 28, /* to */ 9, 6 ],
386 2015: [ 7, 27, /* to */ 9, 4 ],
387 2016: [ 7, 18, /* to */ 8, 26 ],
388 2017: [ 7, 3, /* to */ 8, 14 ],
389 },
390 {
391 name: 'Herbstferien',
392 2010: [ 10, 11, /* to */ 10, 23 ],
393 2011: [ 10, 4, /* to */ 10, 15 ],
394 2012: [ 10, 22, /* to */ 11, 3 ],
395 2013: [ 10, 21, /* to */ 11, 2 ],
396 2014: [ 10, 20, /* to */ 10, 31 ],
397 },
398 {
399 name: 'Weihnachtsferien',
400 2010: [ 12, 20, /* to */ 1, 1 ],
401 2011: [ 12, 23, /* to */ 1, 4 ],
402 2012: [ 12, 24, /* to */ 1, 5 ],
403 2013: [ 12, 20, /* to */ 1, 4 ],
404 2014: [ 12, 22, /* to */ 1, 7 ],
405 },
406 ],
407 },
408 'Bremen': {
409 'SH': [
410 {
411 name: 'Winterferien',
412 2010: [ 2, 1, /* to */ 2, 2 ],
413 2011: [ 1, 31, /* to */ 2, 1 ],
414 2012: [ 1, 30, /* to */ 1, 31 ],
415 2013: [ 1, 31, /* to */ 2, 1 ],
416 2014: [ 1, 30, /* to */ 1, 31 ],
417 2015: [ 2, 2, /* to */ 2, 3 ],
418 2016: [ 1, 28, /* to */ 1, 29 ],
419 2017: [ 1, 30, /* to */ 1, 31 ],
420 },
421 {
422 name: 'Osterferien',
423 2010: [ 3, 19, /* to */ 4, 6 ],
424 2011: [ 4, 16, /* to */ 4, 30 ],
425 2012: [ 3, 26, /* to */ 4, 11, 4, 30, /* to */ 4, 30 ],
426 2013: [ 3, 16, /* to */ 4, 2 ],
427 2014: [ 4, 3, /* to */ 4, 22, 5, 2, /* to */ 5, 2 ],
428 2015: [ 3, 25, /* to */ 4, 10 ],
429 2016: [ 3, 18, /* to */ 4, 2 ],
430 2017: [ 4, 10, /* to */ 4, 22 ],
431 },
432 {
433 name: 'Pfingstferien',
434 2010: [ 5, 14, /* to */ 5, 14, 5, 25, /* to */ 5, 25 ],
435 2011: [ 6, 3, /* to */ 6, 3, 6, 14, /* to */ 6, 14 ],
436 2012: [ 5, 18, /* to */ 5, 18, 5, 29, /* to */ 5, 29 ],
437 2013: [ 5, 10, /* to */ 5, 10, 5, 21, /* to */ 5, 21 ],
438 2014: [ 5, 30, /* to */ 5, 30, 6, 10, /* to */ 6, 10 ],
439 2015: [ 5, 15, /* to */ 5, 15, 5, 26, /* to */ 5, 26 ],
440 2016: [ 5, 6, /* to */ 5, 6, 5, 17, /* to */ 5, 17 ],
441 2017: [ 5, 26, /* to */ 5, 26, 6, 6, /* to */ 6, 6 ],
442 },
443 {
444 name: 'Sommerferien',
445 2010: [ 6, 24, /* to */ 8, 4 ],
446 2011: [ 7, 7, /* to */ 8, 17 ],
447 2012: [ 7, 23, /* to */ 8, 31 ],
448 2013: [ 6, 27, /* to */ 8, 7 ],
449 2014: [ 7, 31, /* to */ 9, 10 ],
450 2015: [ 7, 23, /* to */ 9, 2 ],
451 2016: [ 6, 23, /* to */ 8, 3 ],
452 2017: [ 6, 22, /* to */ 8, 2 ],
453 },
454 {
455 name: 'Herbstferien',
456 2010: [ 10, 9, /* to */ 10, 23 ],
457 2011: [ 10, 17, /* to */ 10, 29 ],
458 2012: [ 10, 22, /* to */ 11, 3 ],
459 2013: [ 10, 4, /* to */ 10, 18 ],
460 2014: [ 10, 27, /* to */ 11, 8 ],
461 2015: [ 10, 19, /* to */ 10, 31 ],
462 2016: [ 10, 4, /* to */ 10, 15 ],
463 },
464 {
465 name: 'Weihnachtsferien',
466 2010: [ 12, 22, /* to */ 1, 5 ],
467 2011: [ 12, 23, /* to */ 1, 4 ],
468 2012: [ 12, 24, /* to */ 1, 5 ],
469 2013: [ 12, 23, /* to */ 1, 3 ],
470 2014: [ 12, 22, /* to */ 1, 5 ],
471 2015: [ 12, 23, /* to */ 1, 6 ],
472 2016: [ 12, 21, /* to */ 1, 6 ],
473 },
474 ],
475 },
476 'Bayern': {
477 'SH': [
478 {
479 name: 'Winterferien',
480 2010: [ 2, 15, /* to */ 2, 20 ],
481 2011: [ 3, 7, /* to */ 3, 11 ],
482 2012: [ 2, 20, /* to */ 2, 24 ],
483 2013: [ 2, 11, /* to */ 2, 15 ],
484 2014: [ 3, 3, /* to */ 3, 7 ],
485 2015: [ 2, 16, /* to */ 2, 20 ],
486 2016: [ 2, 8, /* to */ 2, 12 ],
487 2017: [ 2, 27, /* to */ 3, 3 ],
488 },
489 {
490 name: 'Osterferien',
491 2010: [ 3, 29, /* to */ 4, 10 ],
492 2011: [ 4, 18, /* to */ 4, 30 ],
493 2012: [ 4, 2, /* to */ 4, 14 ],
494 2013: [ 3, 25, /* to */ 4, 6 ],
495 2014: [ 4, 14, /* to */ 4, 26 ],
496 2015: [ 3, 30, /* to */ 4, 11 ],
497 2016: [ 3, 21, /* to */ 4, 1 ],
498 2017: [ 4, 10, /* to */ 4, 22 ],
499 },
500 {
501 name: 'Pfingstferien',
502 2010: [ 5, 25, /* to */ 6, 5 ],
503 2011: [ 6, 14, /* to */ 6, 25 ],
504 2012: [ 5, 29, /* to */ 6, 9 ],
505 2013: [ 5, 21, /* to */ 5, 31 ],
506 2014: [ 6, 10, /* to */ 6, 21 ],
507 2015: [ 5, 26, /* to */ 6, 5 ],
508 2016: [ 5, 17, /* to */ 5, 28 ],
509 2017: [ 6, 6, /* to */ 6, 16 ],
510 },
511 {
512 name: 'Sommerferien',
513 2010: [ 8, 2, /* to */ 9, 13 ],
514 2011: [ 7, 30, /* to */ 9, 12 ],
515 2012: [ 8, 1, /* to */ 9, 12 ],
516 2013: [ 7, 31, /* to */ 9, 11 ],
517 2014: [ 7, 30, /* to */ 9, 15 ],
518 2015: [ 8, 1, /* to */ 9, 14 ],
519 2016: [ 7, 30, /* to */ 9, 12 ],
520 2017: [ 7, 29, /* to */ 9, 11 ],
521 },
522 {
523 name: 'Herbstferien',
524 2010: [ 11, 2, /* to */ 11, 5 ],
525 2011: [ 10, 31, /* to */ 11, 5 ],
526 2012: [ 10, 29, /* to */ 11, 3 ],
527 2013: [ 10, 28, /* to */ 10, 31 ],
528 2014: [ 10, 27, /* to */ 10, 31 ],
529 2015: [ 11, 2, /* to */ 11, 7 ],
530 2016: [ 10, 31, /* to */ 11, 4 ],
531 },
532 {
533 name: 'Weihnachtsferien',
534 2010: [ 12, 24, /* to */ 1, 7 ],
535 2011: [ 12, 27, /* to */ 1, 5 ],
536 2012: [ 12, 24, /* to */ 1, 5 ],
537 2013: [ 12, 23, /* to */ 1, 4 ],
538 2014: [ 12, 24, /* to */ 1, 5 ],
539 2015: [ 12, 24, /* to */ 1, 5 ],
540 2016: [ 12, 24, /* to */ 1, 5 ],
541 },
542 ],
543 },
544 'Niedersachsen': {
545 'SH': [
546 {
547 name: 'Winterferien',
548 2010: [ 2, 1, /* to */ 2, 2 ],
549 2011: [ 1, 31, /* to */ 2, 1 ],
550 2012: [ 1, 30, /* to */ 1, 31 ],
551 2013: [ 1, 31, /* to */ 2, 1 ],
552 2014: [ 1, 30, /* to */ 1, 31 ],
553 2015: [ 2, 2, /* to */ 2, 3 ],
554 2016: [ 1, 28, /* to */ 1, 29 ],
555 2017: [ 1, 30, /* to */ 1, 31 ],
556 },
557 {
558 name: 'Osterferien',
559 2010: [ 3, 19, /* to */ 4, 6 ],
560 2011: [ 4, 16, /* to */ 4, 30 ],
561 2012: [ 3, 26, /* to */ 4, 11, 4, 30, /* to */ 4, 30 ],
562 2013: [ 3, 16, /* to */ 4, 2 ],
563 2014: [ 4, 3, /* to */ 4, 22, 5, 2, /* to */ 5, 2 ],
564 2015: [ 3, 25, /* to */ 4, 10 ],
565 2016: [ 3, 18, /* to */ 4, 2 ],
566 2017: [ 4, 10, /* to */ 4, 22 ],
567 },
568 {
569 name: 'Pfingstferien',
570 2010: [ 5, 14, /* to */ 5, 14, 5, 25, /* to */ 5, 25 ],
571 2011: [ 6, 3, /* to */ 6, 3, 6, 14, /* to */ 6, 14 ],
572 2012: [ 5, 18, /* to */ 5, 18, 5, 29, /* to */ 5, 29 ],
573 2013: [ 5, 10, /* to */ 5, 10, 5, 21, /* to */ 5, 21 ],
574 2014: [ 5, 30, /* to */ 5, 30, 6, 10, /* to */ 6, 10 ],
575 2015: [ 5, 15, /* to */ 5, 15, 5, 26, /* to */ 5, 26 ],
576 2016: [ 5, 6, /* to */ 5, 6, 5, 17, /* to */ 5, 17 ],
577 2017: [ 5, 26, /* to */ 5, 26, 6, 6, /* to */ 6, 6 ],
578 },
579 {
580 name: 'Sommerferien',
581 2010: [ 6, 24, /* to */ 8, 4 ],
582 2011: [ 7, 7, /* to */ 8, 17 ],
583 2012: [ 7, 23, /* to */ 8, 31 ],
584 2013: [ 6, 27, /* to */ 8, 7 ],
585 2014: [ 7, 31, /* to */ 9, 10 ],
586 2015: [ 7, 23, /* to */ 9, 2 ],
587 2016: [ 6, 23, /* to */ 8, 3 ],
588 2017: [ 6, 22, /* to */ 8, 2 ],
589 },
590 {
591 name: 'Herbstferien',
592 2010: [ 10, 9, /* to */ 10, 23 ],
593 2011: [ 10, 17, /* to */ 10, 29 ],
594 2012: [ 10, 22, /* to */ 11, 3 ],
595 2013: [ 10, 4, /* to */ 10, 18 ],
596 2014: [ 10, 27, /* to */ 11, 8 ],
597 2015: [ 10, 19, /* to */ 10, 31 ],
598 2016: [ 10, 4, /* to */ 10, 15 ],
599 },
600 {
601 name: 'Weihnachtsferien',
602 2010: [ 12, 22, /* to */ 1, 5 ],
603 2011: [ 12, 23, /* to */ 1, 4 ],
604 2012: [ 12, 24, /* to */ 1, 5 ],
605 2013: [ 12, 23, /* to */ 1, 3 ],
606 2014: [ 12, 22, /* to */ 1, 5 ],
607 2015: [ 12, 23, /* to */ 1, 6 ],
608 2016: [ 12, 21, /* to */ 1, 6 ],
609 },
610 ],
611 },
612 'Nordrhein-Westfalen': {
613 'SH': [
614 {
615 name: 'Osterferien',
616 2010: [ 3, 27, /* to */ 4, 10 ],
617 2011: [ 4, 18, /* to */ 4, 30 ],
618 2012: [ 4, 2, /* to */ 4, 14 ],
619 2013: [ 3, 25, /* to */ 4, 6 ],
620 2014: [ 4, 14, /* to */ 4, 26 ],
621 2015: [ 3, 30, /* to */ 4, 11 ],
622 2016: [ 3, 21, /* to */ 4, 2 ],
623 2017: [ 4, 10, /* to */ 4, 22 ],
624 },
625 {
626 name: 'Pfingstferien',
627 2010: [ 5, 25, /* to */ 5, 25 ],
628 2012: [ 5, 29, /* to */ 5, 29 ],
629 2013: [ 5, 21, /* to */ 5, 21 ],
630 2014: [ 6, 10, /* to */ 6, 10 ],
631 2015: [ 5, 26, /* to */ 5, 26 ],
632 2016: [ 5, 17, /* to */ 5, 17 ],
633 2017: [ 6, 6, /* to */ 6, 6 ],
634 },
635 {
636 name: 'Sommerferien',
637 2010: [ 7, 15, /* to */ 8, 27 ],
638 2011: [ 7, 25, /* to */ 9, 6 ],
639 2012: [ 7, 9, /* to */ 8, 21 ],
640 2013: [ 7, 22, /* to */ 9, 3 ],
641 2014: [ 7, 7, /* to */ 8, 19 ],
642 2015: [ 6, 29, /* to */ 8, 11 ],
643 2016: [ 7, 11, /* to */ 8, 23 ],
644 2017: [ 7, 17, /* to */ 8, 29 ],
645 },
646 {
647 name: 'Herbstferien',
648 2010: [ 10, 11, /* to */ 10, 23 ],
649 2011: [ 10, 24, /* to */ 11, 5 ],
650 2012: [ 10, 8, /* to */ 10, 20 ],
651 2013: [ 10, 21, /* to */ 11, 2 ],
652 2014: [ 10, 6, /* to */ 10, 18 ],
653 2015: [ 10, 5, /* to */ 10, 17 ],
654 2016: [ 10, 10, /* to */ 10, 21 ],
655 },
656 {
657 name: 'Weihnachtsferien',
658 2010: [ 12, 24, /* to */ 1, 8 ],
659 2011: [ 12, 23, /* to */ 1, 6 ],
660 2012: [ 12, 21, /* to */ 1, 4 ],
661 2013: [ 12, 23, /* to */ 1, 7 ],
662 2014: [ 12, 22, /* to */ 1, 6 ],
663 2015: [ 12, 23, /* to */ 1, 6 ],
664 2016: [ 12, 23, /* to */ 1, 6 ],
665 },
666 ],
667 },
668 'Sachsen': {
669 'SH': [
670 {
671 name: 'Winterferien',
672 2010: [ 2, 8, /* to */ 2, 20 ],
673 2011: [ 2, 12, /* to */ 2, 26 ],
674 2012: [ 2, 13, /* to */ 2, 25 ],
675 2013: [ 2, 4, /* to */ 2, 15 ],
676 2014: [ 2, 17, /* to */ 3, 1 ],
677 2015: [ 2, 9, /* to */ 2, 21 ],
678 2016: [ 2, 8, /* to */ 2, 20 ],
679 2017: [ 2, 13, /* to */ 2, 24 ],
680 },
681 {
682 name: 'Osterferien',
683 2010: [ 4, 1, /* to */ 4, 10 ],
684 2011: [ 4, 22, /* to */ 4, 30 ],
685 2012: [ 4, 6, /* to */ 4, 14 ],
686 2013: [ 3, 29, /* to */ 4, 6 ],
687 2014: [ 4, 18, /* to */ 4, 26 ],
688 2015: [ 4, 2, /* to */ 4, 11 ],
689 2016: [ 3, 25, /* to */ 4, 2 ],
690 2017: [ 4, 13, /* to */ 4, 22 ],
691 },
692 {
693 name: 'Pfingstferien',
694 2010: [ 5, 14, /* to */ 5, 14 ],
695 2011: [ 6, 3, /* to */ 6, 3 ],
696 2012: [ 5, 18, /* to */ 5, 18 ],
697 2013: [ 5, 10, /* to */ 5, 10, 5, 18, /* to */ 5, 22 ],
698 2014: [ 5, 30, /* to */ 5, 30 ],
699 2015: [ 5, 15, /* to */ 5, 15 ],
700 2016: [ 5, 6, /* to */ 5, 6 ],
701 2017: [ 5, 26, /* to */ 5, 26 ],
702 },
703 {
704 name: 'Sommerferien',
705 2010: [ 6, 28, /* to */ 8, 6 ],
706 2011: [ 7, 11, /* to */ 8, 19 ],
707 2012: [ 7, 23, /* to */ 8, 31 ],
708 2013: [ 7, 15, /* to */ 8, 23 ],
709 2014: [ 7, 21, /* to */ 8, 29 ],
710 2015: [ 7, 13, /* to */ 8, 21 ],
711 2016: [ 6, 27, /* to */ 8, 5 ],
712 2017: [ 6, 26, /* to */ 8, 4 ],
713 },
714 {
715 name: 'Herbstferien',
716 2010: [ 10, 4, /* to */ 10, 16 ],
717 2011: [ 10, 17, /* to */ 10, 28 ],
718 2012: [ 10, 22, /* to */ 11, 2 ],
719 2013: [ 10, 21, /* to */ 11, 1 ],
720 2014: [ 10, 20, /* to */ 10, 31 ],
721 2015: [ 10, 12, /* to */ 10, 24 ],
722 2016: [ 10, 3, /* to */ 10, 15 ],
723 },
724 {
725 name: 'Weihnachtsferien',
726 2010: [ 12, 23, /* to */ 1, 1 ],
727 2011: [ 12, 23, /* to */ 1, 2 ],
728 2012: [ 12, 22, /* to */ 1, 2 ],
729 2013: [ 12, 21, /* to */ 1, 3 ],
730 2014: [ 12, 22, /* to */ 1, 3 ],
731 2015: [ 12, 21, /* to */ 1, 2 ],
732 2016: [ 12, 23, /* to */ 1, 2 ],
733 },
734 ],
735 },
736 'Thüringen': {
737 'SH': [
738 {
739 name: 'Winterferien',
740 2010: [ 2, 1, /* to */ 2, 6 ],
741 2011: [ 1, 31, /* to */ 2, 5 ],
742 2012: [ 2, 6, /* to */ 2, 11 ],
743 2013: [ 2, 18, /* to */ 2, 23 ],
744 2014: [ 2, 17, /* to */ 2, 22 ],
745 2015: [ 2, 2, /* to */ 2, 7 ],
746 2016: [ 2, 1, /* to */ 2, 6 ],
747 2017: [ 2, 6, /* to */ 2, 11 ],
748 },
749 {
750 name: 'Osterferien',
751 2010: [ 3, 29, /* to */ 4, 9 ],
752 2011: [ 4, 18, /* to */ 4, 30 ],
753 2012: [ 4, 2, /* to */ 4, 13 ],
754 2013: [ 3, 25, /* to */ 4, 6 ],
755 2014: [ 4, 19, /* to */ 5, 2 ],
756 2015: [ 3, 30, /* to */ 4, 11 ],
757 2016: [ 3, 24, /* to */ 4, 2 ],
758 2017: [ 4, 10, /* to */ 4, 21 ],
759 },
760 {
761 name: 'Sommerferien',
762 2010: [ 6, 24, /* to */ 8, 4 ],
763 2011: [ 7, 11, /* to */ 8, 19 ],
764 2012: [ 7, 23, /* to */ 8, 31 ],
765 2013: [ 7, 15, /* to */ 8, 23 ],
766 2014: [ 7, 21, /* to */ 8, 29 ],
767 2015: [ 7, 13, /* to */ 8, 21 ],
768 2016: [ 6, 27, /* to */ 8, 10 ],
769 2017: [ 6, 26, /* to */ 8, 9 ],
770 },
771 {
772 name: 'Pfingstferien',
773 2011: [ 6, 11, /* to */ 6, 14 ],
774 2012: [ 5, 25, /* to */ 5, 29 ],
775 2013: [ 5, 10, /* to */ 5, 10 ],
776 2014: [ 5, 30, /* to */ 5, 30 ],
777 2015: [ 5, 15, /* to */ 5, 15 ],
778 2016: [ 5, 6, /* to */ 5, 6 ],
779 2017: [ 5, 26, /* to */ 5, 26 ],
780 },
781 {
782 name: 'Herbstferien',
783 2010: [ 10, 9, /* to */ 10, 23 ],
784 2011: [ 10, 17, /* to */ 10, 28 ],
785 2012: [ 10, 22, /* to */ 11, 3 ],
786 2013: [ 10, 21, /* to */ 11, 2 ],
787 2014: [ 10, 6, /* to */ 10, 18 ],
788 2015: [ 10, 5, /* to */ 10, 17 ],
789 2016: [ 10, 10, /* to */ 10, 22 ],
790 },
791 {
792 name: 'Weihnachtsferien',
793 2010: [ 12, 23, /* to */ 1, 1 ],
794 2011: [ 12, 23, /* to */ 1, 1 ],
795 2012: [ 12, 24, /* to */ 1, 5 ],
796 2013: [ 12, 23, /* to */ 1, 4 ],
797 2014: [ 12, 22, /* to */ 1, 3 ],
798 2015: [ 12, 23, /* to */ 1, 2 ],
799 2016: [ 12, 23, /* to */ 12, 31 ],
800 },
801 ],
802 },
803 'Hamburg': {
804 'SH': [
805 {
806 name: 'Winterferien',
807 2010: [ 1, 29, /* to */ 1, 29 ],
808 2011: [ 1, 31, /* to */ 1, 31 ],
809 2012: [ 1, 30, /* to */ 1, 30 ],
810 2013: [ 2, 1, /* to */ 2, 1 ],
811 2014: [ 1, 31, /* to */ 1, 31 ],
812 2015: [ 1, 30, /* to */ 1, 30 ],
813 2016: [ 1, 29, /* to */ 1, 29 ],
814 2017: [ 1, 30, /* to */ 1, 30 ],
815 },
816 {
817 name: 'Osterferien',
818 2010: [ 3, 8, /* to */ 3, 20 ],
819 2011: [ 3, 7, /* to */ 3, 18 ],
820 2012: [ 3, 5, /* to */ 3, 16 ],
821 2013: [ 3, 4, /* to */ 3, 15 ],
822 2014: [ 3, 3, /* to */ 3, 14 ],
823 2015: [ 3, 2, /* to */ 3, 13 ],
824 2016: [ 3, 7, /* to */ 3, 18 ],
825 2017: [ 3, 6, /* to */ 3, 17 ],
826 },
827 {
828 name: 'Pfingstferien',
829 2010: [ 5, 14, /* to */ 5, 22 ],
830 2011: [ 4, 26, /* to */ 4, 29, 6, 3, /* to */ 6, 3 ],
831 2012: [ 4, 30, /* to */ 5, 4, 5, 18, /* to */ 5, 18 ],
832 2013: [ 5, 2, /* to */ 5, 10 ],
833 2014: [ 4, 28, /* to */ 5, 2, 5, 30, /* to */ 5, 30 ],
834 2015: [ 5, 11, /* to */ 5, 15 ],
835 2016: [ 5, 6, /* to */ 5, 6, 5, 17, /* to */ 5, 20 ],
836 2017: [ 5, 22, /* to */ 5, 26 ],
837 },
838 {
839 name: 'Sommerferien',
840 2010: [ 7, 8, /* to */ 8, 18 ],
841 2011: [ 6, 30, /* to */ 8, 10 ],
842 2012: [ 6, 21, /* to */ 8, 1 ],
843 2013: [ 6, 20, /* to */ 7, 31 ],
844 2014: [ 7, 10, /* to */ 8, 20 ],
845 2015: [ 7, 16, /* to */ 8, 26 ],
846 2016: [ 7, 21, /* to */ 8, 31 ],
847 2017: [ 7, 20, /* to */ 8, 30 ],
848 },
849 {
850 name: 'Herbstferien',
851 2010: [ 10, 4, /* to */ 10, 15 ],
852 2011: [ 10, 4, /* to */ 10, 14 ],
853 2012: [ 10, 1, /* to */ 10, 12 ],
854 2013: [ 9, 30, /* to */ 10, 11 ],
855 2014: [ 10, 13, /* to */ 10, 24 ],
856 2015: [ 10, 19, /* to */ 10, 30 ],
857 2016: [ 10, 17, /* to */ 10, 28 ],
858 },
859 {
860 name: 'Weihnachtsferien',
861 2010: [ 12, 23, /* to */ 1, 3 ],
862 2011: [ 12, 27, /* to */ 1, 6 ],
863 2012: [ 12, 21, /* to */ 1, 4 ],
864 2013: [ 12, 19, /* to */ 1, 3 ],
865 2014: [ 12, 22, /* to */ 1, 6 ],
866 2015: [ 12, 21, /* to */ 1, 1 ],
867 2016: [ 12, 27, /* to */ 1, 6 ],
868 },
869 ],
870 },
871 'Sachsen-Anhalt': {
872 'SH': [
873 {
874 name: 'Winterferien',
875 2010: [ 2, 8, /* to */ 2, 13 ],
876 2011: [ 2, 5, /* to */ 2, 12 ],
877 2012: [ 2, 4, /* to */ 2, 11 ],
878 2013: [ 2, 1, /* to */ 2, 8 ],
879 2014: [ 2, 1, /* to */ 2, 12 ],
880 2015: [ 2, 2, /* to */ 2, 14 ],
881 2016: [ 2, 1, /* to */ 2, 10 ],
882 2017: [ 2, 4, /* to */ 2, 11 ],
883 },
884 {
885 name: 'Osterferien',
886 2010: [ 3, 29, /* to */ 4, 9 ],
887 2011: [ 4, 18, /* to */ 4, 27 ],
888 2012: [ 4, 2, /* to */ 4, 7 ],
889 2013: [ 3, 25, /* to */ 3, 30 ],
890 2014: [ 4, 14, /* to */ 4, 17 ],
891 2015: [ 4, 2, /* to */ 4, 2 ],
892 2016: [ 3, 24, /* to */ 3, 24 ],
893 2017: [ 4, 10, /* to */ 4, 13 ],
894 },
895 {
896 name: 'Pfingstferien',
897 2010: [ 5, 14, /* to */ 5, 22 ],
898 2011: [ 6, 14, /* to */ 6, 18 ],
899 2012: [ 5, 18, /* to */ 5, 25 ],
900 2013: [ 5, 10, /* to */ 5, 18 ],
901 2014: [ 5, 30, /* to */ 6, 7 ],
902 2015: [ 5, 15, /* to */ 5, 23 ],
903 2016: [ 5, 6, /* to */ 5, 14 ],
904 2017: [ 5, 26, /* to */ 5, 26 ],
905 },
906 {
907 name: 'Sommerferien',
908 2010: [ 6, 24, /* to */ 8, 4 ],
909 2011: [ 7, 11, /* to */ 8, 24 ],
910 2012: [ 7, 23, /* to */ 9, 5 ],
911 2013: [ 7, 15, /* to */ 8, 28 ],
912 2014: [ 7, 21, /* to */ 9, 3 ],
913 2015: [ 7, 13, /* to */ 8, 26 ],
914 2016: [ 6, 27, /* to */ 8, 10 ],
915 2017: [ 6, 26, /* to */ 8, 9 ],
916 },
917 {
918 name: 'Herbstferien',
919 2010: [ 10, 18, /* to */ 10, 23 ],
920 2011: [ 10, 17, /* to */ 10, 22 ],
921 2012: [ 10, 29, /* to */ 11, 2 ],
922 2013: [ 10, 21, /* to */ 10, 25 ],
923 2014: [ 10, 27, /* to */ 10, 30 ],
924 2015: [ 10, 17, /* to */ 10, 24 ],
925 2016: [ 10, 4, /* to */ 10, 15 ],
926 },
927 {
928 name: 'Weihnachtsferien',
929 2010: [ 12, 22, /* to */ 1, 5 ],
930 2011: [ 12, 22, /* to */ 1, 7 ],
931 2012: [ 12, 19, /* to */ 1, 4 ],
932 2013: [ 12, 21, /* to */ 1, 3 ],
933 2014: [ 12, 22, /* to */ 1, 5 ],
934 2015: [ 12, 21, /* to */ 1, 5 ],
935 2016: [ 12, 19, /* to */ 1, 2 ],
936 },
937 ],
938 },
939 'Rheinland-Pfalz': {
940 'SH': [
941 {
942 name: 'Osterferien',
943 2010: [ 3, 26, /* to */ 4, 9 ],
944 2011: [ 4, 18, /* to */ 4, 29 ],
945 2012: [ 3, 29, /* to */ 4, 13 ],
946 2013: [ 3, 20, /* to */ 4, 5 ],
947 2014: [ 4, 11, /* to */ 4, 25 ],
948 2015: [ 3, 26, /* to */ 4, 10 ],
949 2016: [ 3, 18, /* to */ 4, 1 ],
950 2017: [ 4, 10, /* to */ 4, 21 ],
951 },
952 {
953 name: 'Sommerferien',
954 2010: [ 7, 5, /* to */ 8, 13 ],
955 2011: [ 6, 27, /* to */ 8, 5 ],
956 2012: [ 7, 2, /* to */ 8, 10 ],
957 2013: [ 7, 8, /* to */ 8, 16 ],
958 2014: [ 7, 28, /* to */ 9, 5 ],
959 2015: [ 7, 27, /* to */ 9, 4 ],
960 2016: [ 7, 18, /* to */ 8, 26 ],
961 2017: [ 7, 3, /* to */ 8, 11 ],
962 },
963 {
964 name: 'Herbstferien',
965 2010: [ 10, 11, /* to */ 10, 22 ],
966 2011: [ 10, 4, /* to */ 10, 14 ],
967 2012: [ 10, 1, /* to */ 10, 12 ],
968 2013: [ 10, 4, /* to */ 10, 18 ],
969 2014: [ 10, 20, /* to */ 10, 31 ],
970 2015: [ 10, 19, /* to */ 10, 30 ],
971 2016: [ 10, 10, /* to */ 10, 21 ],
972 },
973 {
974 name: 'Weihnachtsferien',
975 2010: [ 12, 23, /* to */ 1, 7 ],
976 2011: [ 12, 22, /* to */ 1, 6 ],
977 2012: [ 12, 20, /* to */ 1, 4 ],
978 2013: [ 12, 23, /* to */ 1, 7 ],
979 2014: [ 12, 22, /* to */ 1, 7 ],
980 2015: [ 12, 23, /* to */ 1, 8 ],
981 2016: [ 12, 22, /* to */ 1, 6 ],
982 },
983 ],
984 },
985 'Brandenburg': {
986 'SH': [
987 {
988 name: 'Winterferien',
989 2010: [ 2, 1, /* to */ 2, 6 ],
990 2011: [ 1, 31, /* to */ 2, 5 ],
991 2012: [ 1, 30, /* to */ 2, 4 ],
992 2013: [ 2, 4, /* to */ 2, 9 ],
993 2014: [ 2, 3, /* to */ 2, 8 ],
994 2015: [ 2, 2, /* to */ 2, 7 ],
995 2016: [ 2, 1, /* to */ 2, 6 ],
996 2017: [ 1, 30, /* to */ 2, 4 ],
997 },
998 {
999 name: 'Osterferien',
1000 2010: [ 3, 31, /* to */ 4, 10 ],
1001 2011: [ 4, 20, /* to */ 4, 30 ],
1002 2012: [ 4, 4, /* to */ 4, 14, 4, 30, /* to */ 4, 30 ],
1003 2013: [ 3, 27, /* to */ 4, 6 ],
1004 2014: [ 4, 16, /* to */ 4, 26, 5, 2, /* to */ 5, 2 ],
1005 2015: [ 4, 1, /* to */ 4, 11 ],
1006 2016: [ 3, 23, /* to */ 4, 2 ],
1007 2017: [ 4, 12, /* to */ 4, 22 ],
1008 },
1009 {
1010 name: 'Pfingstferien',
1011 2010: [ 5, 14, /* to */ 5, 14 ],
1012 2011: [ 6, 3, /* to */ 6, 3 ],
1013 2012: [ 5, 18, /* to */ 5, 18 ],
1014 2013: [ 5, 10, /* to */ 5, 10 ],
1015 2014: [ 5, 30, /* to */ 5, 30 ],
1016 2015: [ 5, 15, /* to */ 5, 15 ],
1017 2016: [ 5, 6, /* to */ 5, 6, 5, 17, /* to */ 5, 17 ],
1018 2017: [ 5, 26, /* to */ 5, 26 ],
1019 },
1020 {
1021 name: 'Sommerferien',
1022 2010: [ 7, 8, /* to */ 8, 21 ],
1023 2011: [ 6, 30, /* to */ 8, 13 ],
1024 2012: [ 6, 21, /* to */ 8, 3 ],
1025 2013: [ 6, 20, /* to */ 8, 2 ],
1026 2014: [ 7, 10, /* to */ 8, 22 ],
1027 2015: [ 7, 16, /* to */ 8, 28 ],
1028 2016: [ 7, 21, /* to */ 9, 3 ],
1029 2017: [ 7, 20, /* to */ 9, 1 ],
1030 },
1031 {
1032 name: 'Herbstferien',
1033 2010: [ 10, 11, /* to */ 10, 23 ],
1034 2011: [ 10, 4, /* to */ 10, 14 ],
1035 2012: [ 10, 1, /* to */ 10, 13 ],
1036 2013: [ 9, 30, /* to */ 10, 12, 11, 1, /* to */ 11, 1 ],
1037 2014: [ 10, 20, /* to */ 11, 1 ],
1038 2015: [ 10, 19, /* to */ 10, 30 ],
1039 2016: [ 10, 17, /* to */ 10, 28 ],
1040 },
1041 {
1042 name: 'Weihnachtsferien',
1043 2010: [ 12, 23, /* to */ 1, 1 ],
1044 2011: [ 12, 23, /* to */ 1, 3 ],
1045 2012: [ 12, 24, /* to */ 1, 4 ],
1046 2013: [ 12, 23, /* to */ 1, 3 ],
1047 2014: [ 12, 22, /* to */ 1, 2 ],
1048 2015: [ 12, 23, /* to */ 1, 2 ],
1049 2016: [ 12, 23, /* to */ 1, 3 ],
1050 },
1051 ],
1052 },
1053 },
1054 'at': {
1055 'PH': { // http://de.wikipedia.org/wiki/Feiertage_in_%C3%96sterreich
1056 'Neujahrstag' : [ 1, 1 ],
1057 'Heilige Drei Könige' : [ 1, 6 ],
1058 // 'Josef' : [ 3, 19, [ 'Kärnten', 'Steiermark', 'Tirol', 'Vorarlberg' ] ],
1059 // 'Karfreitag' : [ 'easter', -2 ],
1060 'Ostermontag' : [ 'easter', 1 ],
1061 'Staatsfeiertag' : [ 5, 1 ],
1062 // 'Florian' : [ 5, 4, [ 'Oberösterreich' ] ],
1063 'Christi Himmelfahrt' : [ 'easter', 39 ],
1064 'Pfingstmontag' : [ 'easter', 50 ],
1065 'Fronleichnam' : [ 'easter', 60 ],
1066 'Mariä Himmelfahrt' : [ 8, 15 ],
1067 // 'Rupert' : [ 9, 24, [ 'Salzburg' ] ],
1068 // 'Tag der Volksabstimmung' : [ 10, 10, [ 'Kärnten' ] ],
1069 'Nationalfeiertag' : [ 10, 26 ],
1070 'Allerheiligen' : [ 11, 1 ],
1071 // 'Martin' : [ 11, 11, [ 'Burgenland' ] ],
1072 // 'Leopold' : [ 11, 15, [ 'Niederösterreich', 'Wien' ] ],
1073 'Mariä Empfängnis' : [ 12, 8 ],
1074 // 'Heiliger Abend' : [ 12, 24 ],
1075 'Christtag' : [ 12, 25 ],
1076 'Stefanitag' : [ 12, 26 ],
1077 // 'Silvester' : [ 12, 31 ],
1078 },
1079 },
1080 };
1081
1082 //----------------------------------------------------------------------------
1083 // error correction
1084 // Taken form http://www.netzwolf.info/j/osm/time_domain.js
1085 // Credits go to Netzwolf
1086 //
1087 // Key to word_error_correction is the token name except wrong_words
1088 //----------------------------------------------------------------------------
1089 var word_error_correction = {
1090 wrong_words: {
1091 'Assuming "<ok>" for "<ko>"': {
1092 summer: 'May-Oct',
1093 winter: 'Nov-Apr',
1094 }, 'Bitte benutze die englische Schreibweise "<ok>" für "<ko>".': {
1095 sommer: 'summer',
1096 }, 'Bitte benutze "<ok>" für "<ko>". Beispiel: "Mo 08:00-12:00; Tu off"': {
1097 ruhetag: 'off',
1098 ruhetage: 'off',
1099 }, 'Assuming "<ok>" for "<ko>". Please avoid using "workday": http://wiki.openstreetmap.org/wiki/Talk:Key:opening_hours#need_syntax_for_holidays_and_workingdays': {
1100 // // Used around 260 times but the problem is, that work day might be different in other countries.
1101 wd: 'Mo-Fr',
1102 weekday: 'Mo-Fr',
1103 weekdays: 'Mo-Fr',
1104 }, 'Please ommit "<ko>".': {
1105 h: '',
1106 season: '',
1107 }, 'Please ommit "<ko>". You might want to express open end which can be specified as "12:00+" for example': {
1108 from: '',
1109 }, 'Please use notation "<ok>" for "<ko>".': {
1110 '–': '-',
1111 to: '-',
1112 till: '-',
1113 and: ',',
1114 '&': ',',
1115 daily: 'Mo-Su',
1116 always: '24/7',
1117 midnight: '00:00',
1118 }, 'Please use time format in 24 hours notation ("<ko>").': {
1119 pm: '',
1120 am: '',
1121 }, 'Bitte verzichte auf "<ko>".': {
1122 uhr: '',
1123 }, 'Bitte verzichte auf "<ko>". Sie möchten eventuell eine Öffnungszeit ohne vorgegebenes Ende angeben. Beispiel: "12:00+"': {
1124 ab: '',
1125 von: '',
1126 }, 'Bitte benutze die Schreibweise "<ok>" für "<ko>".': {
1127 bis: '-',
1128 und: ',',
1129 }, 'Bitte benutze die englische Abkürzung "<ok>" für "<ko>".': {
1130 feiertag: 'PH',
1131 feiertage: 'PH',
1132 feiertagen: 'PH'
1133 }, 'S\'il vous plaît utiliser "<ok>" pour "<ko>".': {
1134 'fermé': 'off',
1135 'et': ',',
1136 'à': '-',
1137 }, 'Neem de engelse afkorting "<ok>" voor "<ko>" alstublieft.': {
1138 feestdag: 'PH',
1139 feestdagen: 'PH',
1140 }
1141 },
1142
1143 month: {
1144 'default': {
1145 jan: 0,
1146 feb: 1,
1147 mar: 2,
1148 apr: 3,
1149 may: 4,
1150 jun: 5,
1151 jul: 6,
1152 aug: 7,
1153 sep: 8,
1154 oct: 9,
1155 nov: 10,
1156 dec: 11,
1157 }, 'Please use the englisch abbreviation "<ok>" for "<ko>".': {
1158 january: 0,
1159 february: 1,
1160 march: 2,
1161 april: 3,
1162 // may: 4,
1163 june: 5,
1164 july: 6,
1165 august: 7,
1166 september: 8,
1167 sept: 8,
1168 october: 9,
1169 november: 10,
1170 december: 11,
1171 }, 'Bitte benutze die englische Abkürzung "<ok>" für "<ko>".': {
1172 januar: 0,
1173 februar: 1,
1174 'märz': 2,
1175 maerz: 2,
1176 mai: 4,
1177 juni: 5,
1178 juli: 6,
1179 okt: 9,
1180 oktober: 9,
1181 dez: 11,
1182 dezember: 11,
1183 }, 'S\'il vous plaît utiliser l\'abréviation "<ok>" pour "<ko>".': {
1184 janvier: 0,
1185 février: 1,
1186 fév: 1,
1187 mars: 2,
1188 avril: 3,
1189 avr: 3,
1190 mai: 4,
1191 juin: 5,
1192 juillet: 6,
1193 août: 7,
1194 aoû: 7,
1195 septembre: 8,
1196 octobre: 9,
1197 novembre: 10,
1198 décembre: 11,
1199 }, 'Neem de engelse afkorting "<ok>" voor "<ko>" alstublieft.': {
1200 januari: 0,
1201 februari: 1,
1202 maart: 2,
1203 mei: 4,
1204 augustus: 7,
1205 }
1206 },
1207
1208 weekday: { // good source: http://www.omniglot.com/language/time/days.htm
1209 'default': {
1210 su: 0,
1211 mo: 1,
1212 tu: 2,
1213 we: 3,
1214 th: 4,
1215 fr: 5,
1216 sa: 6,
1217 }, 'Please use the abbreviation "<ok>" for "<ko>".': {
1218 sun: 0,
1219 sunday: 0,
1220 sundays: 0,
1221 m: 1,
1222 mon: 1,
1223 monday: 1,
1224 mondays: 1,
1225 tue: 2,
1226 tuesday: 2,
1227 tuesdays: 2,
1228 w: 3,
1229 wed: 3,
1230 wednesday: 3,
1231 wednesdays: 3,
1232 thu: 4,
1233 thursday: 4,
1234 thursdays: 4,
1235 f: 5,
1236 fri: 5,
1237 friday: 5,
1238 fridays: 5,
1239 sat: 6,
1240 saturday: 6,
1241 saturdays: 6,
1242 }, 'Bitte benutze die englische Abkürzung "<ok>" für "<ko>".': {
1243 so: 0,
1244 son: 0,
1245 sonntag: 0,
1246 montag: 1,
1247 di: 2,
1248 die: 2,
1249 dienstag: 2,
1250 mi: 3,
1251 mit: 3,
1252 mittwoch: 3,
1253 'do': 4,
1254 don: 4,
1255 donnerstag: 4,
1256 fre: 5,
1257 freitag: 5,
1258 sam: 6,
1259 samstag: 6,
1260 }, 'S\'il vous plaît utiliser l\'abréviation "<ok>" pour "<ko>".': {
1261 dim: 0,
1262 dimanche: 0,
1263 lu: 1,
1264 lun: 1,
1265 lundi: 1,
1266 mardi: 2,
1267 mer: 3,
1268 mercredi: 3,
1269 je: 4,
1270 jeu: 4,
1271 jeudi: 4,
1272 ve: 5,
1273 ven: 5,
1274 vendredi: 5,
1275 samedi: 6,
1276 }, 'Neem de engelse afkorting "<ok>" voor "<ko>" alstublieft.': {
1277 zo: 0,
1278 zon: 0,
1279 zontag: 0,
1280 maandag: 1,
1281 din: 2,
1282 dinsdag: 2,
1283 wo: 3,
1284 woe: 3,
1285 woensdag: 3,
1286 donderdag: 4,
1287 vr: 5,
1288 vri: 5,
1289 vrijdag: 5,
1290 za: 6,
1291 zat: 6,
1292 zaterdag: 6,
1293 }, 'Please use the englisch abbreviation "<ok>" for "<ko>".': { // FIXME: Translate to Czech.
1294 'neděle': 0,
1295 'ne': 0,
1296 'pondělí': 1,
1297 'po': 1,
1298 'úterý': 2,
1299 'út': 2,
1300 'středa': 3,
1301 'st': 3,
1302 'čtvrtek': 4,
1303 'čt': 4,
1304 'pátek': 5,
1305 'pá': 5,
1306 'sobota': 6,
1307 }, 'Please use the englisch abbreviation "<ok>" for "<ko>".': {
1308 // Spanish.
1309 'martes': 0,
1310 'miércoles': 1,
1311 'jueves': 2,
1312 'viernes': 3,
1313 'sábado': 4,
1314 'domingo': 5,
1315 'lunes': 6,
1316 // Indonesian.
1317 'selasa': 0,
1318 'rabu': 1,
1319 'kami': 2,
1320 'jumat': 3,
1321 'sabtu': 4,
1322 'minggu': 5,
1323 'senin': 6,
1324 // Swedish
1325 'söndag': 0,
1326 'måndag': 1,
1327 ma: 1,
1328 'tisdag': 2,
1329 'onsdag': 3,
1330 'torsdag': 4,
1331 'fredag': 5,
1332 'lördag': 6,
1333 },
1334 },
1335
1336 timevar: { // Special time variables which actual value depends on the date and the position of the facility.
1337 'default': {
1338 sunrise: 'sunrise',
1339 sunset: 'sunset',
1340 dawn: 'dawn',
1341 dusk: 'dusk',
1342 }, 'Please use notation "<ok>" for "<ko>".': {
1343 sundown: 'sunset',
1344 }, 'Bitte benutze die Schreibweise "<ok>" für "<ko>".': {
1345 'morgendämmerung': 'dawn',
1346 'abenddämmerung': 'dusk',
1347 sonnenaufgang: 'sunrise',
1348 sonnenuntergang: ',',
1349 },
1350 },
1351
1352 'event': { // variable events
1353 'default': {
1354 easter: 'easter',
1355 }, 'Bitte benutze die Schreibweise "<ok>" für "<ko>".': {
1356 ostern: 'easter',
1357 },
1358 },
1359 };
1360
1361 if (typeof exports === 'object') {
1362 // For nodejs
1363 var SunCalc = require('suncalc');
1364 module.exports = factory(SunCalc, holidays, word_error_correction);
1365 } else {
1366 // For browsers
1367 root.opening_hours = factory(root.SunCalc, holidays, word_error_correction);
1368 }
1369}(this, function (SunCalc, holidays, word_error_correction) {
1370 return function(value, nominatiomJSON, oh_mode) {
1371 var word_value_replacement = { // if the correct values can not be calculated
1372 dawn : 60 * 5 + 30,
1373 sunrise : 60 * 6,
1374 sunset : 60 * 18,
1375 dusk : 60 * 18 + 30,
1376 };
1377 var months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
1378 var weekdays = ['Su','Mo','Tu','We','Th','Fr','Sa'];
1379 var default_prettify_conf = {
1380 'leading_zero_hour': true, // enforce leading zero
1381 'one_zero_if_hour_zero': false, // only one zero "0" if hour is zero "0"
1382 'leave_off_closed': true, // leave keywords of and closed as is
1383 'keyword_for_off_closed': 'off', // use given keyword instead of "off" or "closed"
1384 'block_sep_string': ' ', // separate blocks by string
1385 'print_semicolon': true, // print token which separates normal blocks
1386 'leave_weekday_sep_one_day_betw': true, // use the separator (either "," or "-" which is used to separate days which follow to each other like Sa,Su or Su-Mo
1387 'sep_one_day_between': ',' // separator which should be used
1388 };
1389
1390 var minutes_in_day = 60 * 24;
1391 var msec_in_day = 1000 * 60 * minutes_in_day;
1392 var msec_in_week = msec_in_day * 7;
1393
1394 //======================================================================
1395 // Constructor - entry to parsing code
1396 //======================================================================
1397 // Terminology:
1398 //
1399 // Mo-Fr 10:00-11:00; Th 10:00-12:00
1400 // \_____block_____/ \____block___/
1401 //
1402 // The README refers to blocks as rules, which is more intuitive but less clear.
1403 // Because of that only the README uses the term rule in that context.
1404 // In all internal parts of this project, the term block is used.
1405 //
1406 // Mo-Fr Jan 10:00-11:00
1407 // \__/ \_/ \_________/
1408 // selectors (left to right: weekday, month, time)
1409 //
1410 // Logic:
1411 // - Tokenize
1412 // Foreach block:
1413 // - Run toplevel (block) parser
1414 // - Which calls subparser for specific selector types
1415 // - Which produce selector functions
1416
1417
1418 // Evaluate additional information which can be given. They are
1419 // required to reasonably calculate 'sunrise' and so on and to use the
1420 // correct holidays.
1421 var location_cc, location_state, lat, lon;
1422 if (typeof nominatiomJSON != 'undefined') {
1423 if (typeof nominatiomJSON.address != 'undefined' &&
1424 typeof nominatiomJSON.address.state != 'undefined') { // country_code will be tested later …
1425 location_cc = nominatiomJSON.address.country_code;
1426 location_state = nominatiomJSON.address.state;
1427 }
1428
1429 if (typeof nominatiomJSON.lon != 'undefined') { // lat will be tested later …
1430 lat = nominatiomJSON.lat;
1431 lon = nominatiomJSON.lon;
1432 }
1433 }
1434
1435 // 0: time ranges (opening_hours, lit, …) default
1436 // 1: points in time (collection_times, service_times, …)
1437 // 2: both (time ranges and points in time)
1438 if (typeof oh_mode == 'undefined') {
1439 oh_mode = 0;
1440 } else if (!(typeof oh_mode == 'number' && (oh_mode == 0 || oh_mode == 1 || oh_mode == 2))) {
1441 throw 'The third constructor parameter is oh_mode and must be a number (0, 1 or 2)'
1442 }
1443
1444 if (value.match(/^(\s*;?\s*)+$/))
1445 throw 'Value contains nothing meaningful which can be parsed';
1446
1447 var parsing_warnings = [];
1448 var done_with_warnings = false; // The functions which throw warnings can be called multiple times.
1449 var has_token = {};
1450 var tokens = tokenize(value);
1451 // console.log(JSON.stringify(tokens, null, '\t'));
1452 var prettified_value = '';
1453 var used_subparsers = {}; // Used sub parsers for one block, will be asdreset for each block. Declared as global, because it is manipulation inside much sub parsers.
1454 var week_stable = true;
1455
1456 var blocks = [];
1457
1458 for (var nblock = 0; nblock < tokens.length; nblock++) {
1459 if (tokens[nblock][0].length == 0) continue;
1460 // Block does contain nothing useful e.g. second block of '10:00-12:00;' (empty) which needs to be handled.
1461
1462 var continue_at = 0;
1463 do {
1464 if (continue_at == tokens[nblock][0].length) break;
1465 // Block does contain nothing useful e.g. second block of '10:00-12:00,' (empty) which needs to be handled.
1466
1467 var selectors = {
1468 // Time selectors
1469 time: [],
1470
1471 // Temporary array of selectors from time wrapped to the next day
1472 wraptime: [],
1473
1474 // Date selectors
1475 weekday: [],
1476 holiday: [],
1477 week: [],
1478 month: [],
1479 monthday: [],
1480 year: [],
1481
1482 // Array with non-empty date selector types, with most optimal ordering
1483 date: [],
1484
1485 fallback: tokens[nblock][1],
1486 additional: continue_at ? true : false,
1487 meaning: true,
1488 unknown: false,
1489 comment: undefined,
1490 };
1491
1492 continue_at = parseGroup(tokens[nblock][0], continue_at, selectors, nblock);
1493 if (typeof continue_at == 'object')
1494 continue_at = continue_at[0];
1495 else
1496 continue_at = 0;
1497
1498 if (selectors.year.length > 0)
1499 selectors.date.push(selectors.year);
1500 if (selectors.holiday.length > 0)
1501 selectors.date.push(selectors.holiday);
1502 if (selectors.month.length > 0)
1503 selectors.date.push(selectors.month);
1504 if (selectors.monthday.length > 0)
1505 selectors.date.push(selectors.monthday);
1506 if (selectors.week.length > 0)
1507 selectors.date.push(selectors.week);
1508 if (selectors.weekday.length > 0)
1509 selectors.date.push(selectors.weekday);
1510
1511 blocks.push(selectors);
1512
1513 // this handles selectors with time ranges wrapping over midnight (e.g. 10:00-02:00)
1514 // it generates wrappers for all selectors and creates a new block
1515 if (selectors.wraptime.length > 0) {
1516 var wrapselectors = {
1517 time: selectors.wraptime,
1518 date: [],
1519
1520 meaning: selectors.meaning,
1521 unknown: selectors.unknown,
1522 comment: selectors.comment,
1523
1524 wrapped: true,
1525 };
1526
1527 for (var dselg = 0; dselg < selectors.date.length; dselg++) {
1528 wrapselectors.date.push([]);
1529 for (var dsel = 0; dsel < selectors.date[dselg].length; dsel++) {
1530 wrapselectors.date[wrapselectors.date.length-1].push(
1531 generateDateShifter(selectors.date[dselg][dsel], -msec_in_day)
1532 );
1533 }
1534 }
1535
1536 blocks.push(wrapselectors);
1537 }
1538 } while (continue_at)
1539 }
1540
1541 // Tokenization function: Splits string into parts.
1542 // output: array of arrays of pairs [content, type]
1543 function tokenize(value) {
1544 var all_tokens = new Array();
1545 var curr_block_tokens = new Array();
1546
1547 var last_block_fallback_terminated = false;
1548
1549 while (value != '') {
1550 var tmp;
1551 if (tmp = value.match(/^(?:week\b|open|unknown)/i)) {
1552 // reserved word
1553 curr_block_tokens.push([tmp[0].toLowerCase(), tmp[0].toLowerCase(), value.length ]);
1554 value = value.substr(tmp[0].length);
1555 } else if (tmp = value.match(/^24\/7/i)) {
1556 // reserved word
1557 has_token[tmp[0]] = true;
1558 curr_block_tokens.push([tmp[0], tmp[0], value.length ]);
1559 value = value.substr(tmp[0].length);
1560 } else if (tmp = value.match(/^(?:off|closed)/i)) {
1561 // reserved word
1562 curr_block_tokens.push([tmp[0].toLowerCase(), 'closed', value.length ]);
1563 value = value.substr(tmp[0].length);
1564 } else if (tmp = value.match(/^(?:PH|SH)/i)) {
1565 // special day name (holidays)
1566 curr_block_tokens.push([tmp[0].toUpperCase(), 'holiday', value.length ]);
1567 value = value.substr(2);
1568 } else if (tmp = value.match(/^days?/i)) {
1569 curr_block_tokens.push([tmp[0].toLowerCase(), 'calcday', value.length ]);
1570 value = value.substr(tmp[0].length);
1571 } else if (tmp = value.match(/^(&|–|[a-zA-ZäÄàÀéÉ]+\b)\.?/i)) {
1572 // Handle all remaining words with error tolerance
1573 var correct_val = returnCorrectWordOrToken(tmp[1].toLowerCase(), value.length);
1574 if (typeof correct_val == 'object') {
1575 curr_block_tokens.push([ correct_val[0], correct_val[1], value.length ]);
1576 value = value.substr(tmp[0].length);
1577 } else if (typeof correct_val == 'string') {
1578 value = correct_val + value.substr(tmp[0].length);
1579 } else {
1580 // other single-character tokens
1581 curr_block_tokens.push([value[0].toLowerCase(), value[0].toLowerCase(), value.length - 1 ]);
1582 value = value.substr(1);
1583 }
1584 } else if (tmp = value.match(/^\d+/)) {
1585 // number
1586 if (tmp[0] > 1900) // assumed to be a year number
1587 curr_block_tokens.push([tmp[0], 'year', value.length ]);
1588 else
1589 curr_block_tokens.push([+tmp[0], 'number', value.length ]);
1590 value = value.substr(tmp[0].length);
1591 } else if (tmp = value.match(/^"([^"]*)"/)) {
1592 // comment
1593 curr_block_tokens.push([tmp[1], 'comment', value.length ]);
1594 value = value.substr(tmp[0].length);
1595 } else if (value.match(/^;/)) {
1596 // semicolon terminates block
1597 // next tokens belong to a new block
1598 all_tokens.push([ curr_block_tokens, last_block_fallback_terminated, value.length ]);
1599 value = value.substr(1);
1600
1601 curr_block_tokens = [];
1602 last_block_fallback_terminated = false;
1603 } else if (value.match(/^\|\|/)) {
1604 // || terminates block
1605 // next tokens belong to a fallback block
1606 if (curr_block_tokens.length == 0)
1607 throw formatWarnErrorMessage(-1, value.length - 2, 'Rule before fallback rule does not contain anything useful');
1608
1609 all_tokens.push([ curr_block_tokens, last_block_fallback_terminated, value.length ]);
1610 value = value.substr(2);
1611
1612 curr_block_tokens = [];
1613 last_block_fallback_terminated = true;
1614 } else if (value.match(/^(?:␣|\s)/)) {
1615 // Using "␣" as space is not expected to be a normal mistake. Just ignore it to make using taginfo easier.
1616 value = value.substr(1);
1617 } else if (tmp = value.match(/^\s+/)) {
1618 // whitespace is ignored
1619 value = value.substr(tmp[0].length);
1620 } else if (value.match(/^[:.]/)) {
1621 // time separator
1622 if (value[0] == '.' && !done_with_warnings)
1623 parsing_warnings.push([ -1, value.length - 1, 'Please use ":" as hour/minute-separator' ]);
1624 curr_block_tokens.push([ ':', 'timesep', value.length ]);
1625 value = value.substr(1);
1626 } else {
1627 // other single-character tokens
1628 curr_block_tokens.push([value[0].toLowerCase(), value[0].toLowerCase(), value.length ]);
1629 value = value.substr(1);
1630 }
1631 }
1632
1633 all_tokens.push([ curr_block_tokens, last_block_fallback_terminated ]);
1634
1635 return all_tokens;
1636 }
1637
1638 // error correction/tolerance
1639 function returnCorrectWordOrToken(word, value_length) {
1640 for (var token_name in word_error_correction) {
1641 for (var comment in word_error_correction[token_name]) {
1642 for (var old_val in word_error_correction[token_name][comment]) {
1643 if (old_val == word) {
1644 var val = word_error_correction[token_name][comment][old_val];
1645 if (token_name == 'wrong_words' && !done_with_warnings) {
1646 parsing_warnings.push([ -1, value_length - old_val.length,
1647 comment.replace(/<ko>/, old_val).replace(/<ok>/, val) ]);
1648 return val;
1649 } else if (comment != 'default'){
1650 var correct_abbr;
1651 for (correct_abbr in word_error_correction[token_name]['default']) {
1652 if (word_error_correction[token_name]['default'][correct_abbr] == val)
1653 break;
1654 }
1655 if (token_name != 'timevar') { // normally written in lower case
1656 correct_abbr = correct_abbr.charAt(0).toUpperCase() + correct_abbr.slice(1);
1657 }
1658 if (!done_with_warnings)
1659 parsing_warnings.push([ -1, value_length - old_val.length,
1660 comment.replace(/<ko>/, old_val).replace(/<ok>/, correct_abbr) ]);
1661 }
1662 return [ val, token_name ];
1663 }
1664 }
1665 }
1666 }
1667 }
1668
1669 function getWarnings(it) {
1670
1671 if (typeof it == 'object') { // getWarnings was called in a state without critical errors. We can do extended tests.
1672
1673 // Check if 24/7 is used and it does not mean 24/7 because there are other rules. This can be avoided.
1674 var has_advanced = it.advance();
1675
1676 if (has_advanced === true && has_token['24/7'] && !done_with_warnings) {
1677 parsing_warnings.push([ -1, 0, 'You used 24/7 in a way that is probably not interpreted as "24 hours 7 days a week".'
1678 // Probably because of: "24/7; 12:00-14:00 open", ". Needs extra testing.
1679 + ' For correctness you might want to use "open" or "closed"'
1680 + ' for this rule and then write your exceptions which should achieve the same goal and is more clear'
1681 + ' e.g. "open; Mo 12:00-14:00 off".']);
1682 }
1683 }
1684
1685 var warnings = [];
1686 for (var i = 0; i < parsing_warnings.length; i++) {
1687 warnings.push( formatWarnErrorMessage(parsing_warnings[i][0], parsing_warnings[i][1], parsing_warnings[i][2]) );
1688 }
1689 return warnings;
1690 }
1691
1692 // Function to check token array for specific pattern
1693 function matchTokens(tokens, at /*, matches... */) {
1694 if (at + arguments.length - 2 > tokens.length)
1695 return false;
1696 for (var i = 0; i < arguments.length - 2; i++) {
1697 if (tokens[at + i][1] !== arguments[i + 2])
1698 return false;
1699 }
1700
1701 return true;
1702 }
1703
1704 function generateDateShifter(func, shift) {
1705 return function(date) {
1706 var res = func(new Date(date.getTime() + shift));
1707
1708 if (typeof res[1] === 'undefined')
1709 return res;
1710 return [ res[0], new Date(res[1].getTime() - shift) ];
1711 }
1712 }
1713
1714 //======================================================================
1715 // Top-level parser
1716 //======================================================================
1717 function parseGroup(tokens, at, selectors, nblock, conf) {
1718 var prettified_group_value = '';
1719 used_subparsers = { 'time ranges': 0 };
1720
1721 // console.log(tokens); // useful for debugging of tokenize
1722 while (at < tokens.length) {
1723 var old_at = at;
1724 // console.log('Parsing at position', at +':', tokens[at]);
1725 if (matchTokens(tokens, at, 'weekday')) {
1726 at = parseWeekdayRange(tokens, at, selectors);
1727 } else if (matchTokens(tokens, at, '24/7')) {
1728 // selectors.time.push(function(date) { return [true]; });
1729 // Not needed. If there is no selector it automatically matches everything.
1730 at++;
1731 } else if (matchTokens(tokens, at, 'holiday')) {
1732 if (matchTokens(tokens, at+1, ','))
1733 at = parseHoliday(tokens, at, selectors, true);
1734 else
1735 at = parseHoliday(tokens, at, selectors, false);
1736 week_stable = false;
1737 } else if (matchTokens(tokens, at, 'month', 'number')
1738 || matchTokens(tokens, at, 'month', 'weekday')
1739 || matchTokens(tokens, at, 'year', 'month', 'number')
1740 || matchTokens(tokens, at, 'year', 'event')
1741 || matchTokens(tokens, at, 'event')) {
1742 at = parseMonthdayRange(tokens, at);
1743 week_stable = false;
1744 } else if (matchTokens(tokens, at, 'year')) {
1745 at = parseYearRange(tokens, at);
1746 week_stable = false;
1747 } else if (matchTokens(tokens, at, 'month')) {
1748 at = parseMonthRange(tokens, at);
1749 // week_stable = false; // decided based on actual values
1750 } else if (matchTokens(tokens, at, 'week')) {
1751 at = parseWeekRange(tokens, at + 1);
1752 week_stable = false;
1753 } else if (at != 0 && at != tokens.length - 1 && tokens[at][0] == ':') {
1754 // Ignore colon if they appear somewhere else than as time separator.
1755 // This provides compatibility with the syntax proposed by Netzwolf:
1756 // http://www.netzwolf.info/en/cartography/osm/time_domain/specification
1757 if (!done_with_warnings && matchTokens(tokens, at-1, 'weekday') || matchTokens(tokens, at-1, 'holiday'))
1758 parsing_warnings.push([nblock, at, 'Please don’t use ":" after ' + tokens[at-1][1] + '.']);
1759
1760 if (prettified_group_value[-1] != ' ')
1761 prettified_group_value = prettified_group_value.substring(0, prettified_group_value.length - 1);
1762 at++;
1763 } else if (matchTokens(tokens, at, 'number', 'timesep')
1764 || matchTokens(tokens, at, 'timevar')
1765 || matchTokens(tokens, at, '(', 'timevar')) {
1766 at = parseTimeRange(tokens, at, selectors);
1767
1768 if (typeof used_subparsers['time ranges'] != 'number')
1769 used_subparsers['time ranges'] = 1;
1770 else
1771 used_subparsers['time ranges']++;
1772 } else if (matchTokens(tokens, at, 'closed')) {
1773 selectors.meaning = false;
1774 at++;
1775 if (matchTokens(tokens, at, ',')) // additional block
1776 at = [ at + 1 ];
1777
1778 if (typeof used_subparsers['state keywords'] != 'number')
1779 used_subparsers['state keywords'] = 1;
1780 else
1781 used_subparsers['state keywords']++;
1782 } else if (matchTokens(tokens, at, 'open')) {
1783 selectors.meaning = true;
1784 at++;
1785 if (matchTokens(tokens, at, ',')) // additional block
1786 at = [ at + 1 ];
1787
1788 if (typeof used_subparsers['state keywords'] != 'number')
1789 used_subparsers['state keywords'] = 1;
1790 else
1791 used_subparsers['state keywords']++;
1792 } else if (matchTokens(tokens, at, 'unknown')) {
1793 selectors.meaning = false;
1794 selectors.unknown = true;
1795 at++;
1796 if (matchTokens(tokens, at, ',')) // additional block
1797 at = [ at + 1 ];
1798
1799 if (typeof used_subparsers['state keywords'] != 'number')
1800 used_subparsers['state keywords'] = 1;
1801 else
1802 used_subparsers['state keywords']++;
1803 } else if (matchTokens(tokens, at, 'comment')) {
1804 selectors.comment = tokens[at][0];
1805 if (at > 0) {
1806 if (!matchTokens(tokens, at - 1, 'open')
1807 && !matchTokens(tokens, at - 1, 'closed')) {
1808 // Then it is unknown. Either with unknown explicitly
1809 // specified or just a comment behind.
1810 selectors.meaning = false;
1811 selectors.unknown = true;
1812 }
1813 } else { // block starts with comment
1814 // selectors.time.push(function(date) { return [true]; });
1815 // Not needed. If there is no selector it automatically matches everything.
1816 selectors.meaning = false;
1817 selectors.unknown = true;
1818 }
1819 at++;
1820 if (matchTokens(tokens, at, ',')) // additional block
1821 at = [ at + 1 ];
1822
1823 if (typeof used_subparsers['comments'] != 'number')
1824 used_subparsers['comments'] = 1;
1825 else
1826 used_subparsers['comments']++;
1827 } else {
1828 var warnings = getWarnings();
1829 throw formatWarnErrorMessage(nblock, at, 'Unexpected token: "' + tokens[at][1]
1830 + '" This means that the syntax is not valid at that point or it is currently not supported.')
1831 + (warnings ? ' ' + warnings.join('; ') : '');
1832 }
1833
1834 if (typeof conf != 'undefined') {
1835 prettified_group_value += prettifySelector(tokens, old_at, at, conf, used_subparsers['time ranges']);
1836 }
1837
1838 if (typeof at == 'object') // additional block
1839 break;
1840 }
1841
1842 prettified_value += prettified_group_value.replace(/\s+$/, '');
1843
1844 if (!done_with_warnings) {
1845 for (var subparser_name in used_subparsers) {
1846 if (used_subparsers[subparser_name] > 1) {
1847 if (subparser_name.match(/^(?:comments|state keywords)/)) {
1848 parsing_warnings.push([nblock, at - 1, 'You have used ' + used_subparsers[subparser_name]
1849 + ' ' + subparser_name + ' in one rule.'
1850 + ' You may only use one in one rule.'
1851 + ' Rules can be separated by ";".' ]);
1852 } else {
1853 parsing_warnings.push([nblock, at - 1, 'You have used ' + used_subparsers[subparser_name]
1854 + ' not connected ' + subparser_name + ' in one rule.'
1855 + ' This is probably an error.'
1856 + ' Equal selector types can (and should) always be written in conjunction separated by comma or something.'
1857 + ' Example for time ranges "12:00-13:00,15:00-18:00". Rules can be separated by ";".']);
1858 }
1859 }
1860 }
1861 }
1862
1863
1864 return at;
1865 }
1866
1867 //======================================================================
1868 // Time range parser (10:00-12:00,14:00-16:00)
1869 //======================================================================
1870 function parseTimeRange(tokens, at, selectors) {
1871 for (; at < tokens.length; at++) {
1872 var has_time_var_calc = [], has_normal_time = []; // element 0: start time, 1: end time
1873 has_normal_time[0] = matchTokens(tokens, at, 'number', 'timesep', 'number');
1874 has_time_var_calc[0] = matchTokens(tokens, at, '(', 'timevar');
1875 if (has_normal_time[0] || matchTokens(tokens, at, 'timevar') || has_time_var_calc[0]) {
1876 // relying on the fact that always *one* of them is true
1877
1878 var is_point_in_time = false; // no time range
1879
1880 if (has_normal_time[0])
1881 var minutes_from = getMinutesByHoursMinutes(tokens, nblock, at+has_time_var_calc[0]);
1882 else
1883 var minutes_from = word_value_replacement[tokens[at+has_time_var_calc[0]][0]];
1884
1885 var timevar_add = [ 0, 0 ];
1886 if (has_time_var_calc[0]) {
1887 timevar_add[0] = parseTimevarCalc(tokens, at);
1888 minutes_from += timevar_add[0];
1889 }
1890
1891 var has_open_end = false;
1892 if (!matchTokens(tokens, at+(has_normal_time[0] ? 3 : (has_time_var_calc[0] ? 7 : 1)), '-')) {
1893 if (matchTokens(tokens, at+(has_normal_time[0] ? 3 : (has_time_var_calc[0] ? 7 : 1)), '+')) {
1894 has_open_end = true;
1895 } else {
1896 if (oh_mode == 0) {
1897 throw formatWarnErrorMessage(nblock, at+(has_normal_time[0] ? 3 : (has_time_var_calc[0] ? 2 : 1)),
1898 'hyphen (-) or open end (+) in time range '
1899 + (has_time_var_calc[0] ? 'calculation ' : '')
1900 + 'expected. For working with points in time, the mode for opening_hours.js has to be altered. Maybe wrong tag?');
1901 } else {
1902 var minutes_to = minutes_from + 1;
1903 is_point_in_time = true;
1904 }
1905 }
1906 }
1907
1908 var at_end_time = at+(has_normal_time[0] ? 3 : (has_time_var_calc[0] ? 7 : 1))+1; // after '-'
1909 if (has_open_end) {
1910 if (minutes_from >= 22 * 60)
1911 var minutes_to = minutes_from + 60 * 8;
1912 else if (minutes_from >= 17 * 60)
1913 var minutes_to = minutes_from + 60 * 10;
1914 else
1915 var minutes_to = minutes_in_day;
1916 } else if (!is_point_in_time) {
1917 has_normal_time[1] = matchTokens(tokens, at_end_time, 'number', 'timesep', 'number');
1918 has_time_var_calc[1] = matchTokens(tokens, at_end_time, '(', 'timevar');
1919 if (!has_normal_time[1] && !matchTokens(tokens, at_end_time, 'timevar') && !has_time_var_calc[1]) {
1920 if (!is_point_in_time)
1921 throw formatWarnErrorMessage(nblock, at_end_time, 'time range does not continue as expected');
1922 } else if (oh_mode == 1) {
1923 throw formatWarnErrorMessage(nblock, at_end_time, 'opening_hours is running in "points in time mode".'
1924 + ' Found time range.');
1925 } else {
1926 if (has_normal_time[1])
1927 var minutes_to = getMinutesByHoursMinutes(tokens, nblock, at_end_time);
1928 else
1929 var minutes_to = word_value_replacement[tokens[at_end_time+has_time_var_calc[1]][0]];
1930
1931 if (has_time_var_calc[1]) {
1932 timevar_add[1] = parseTimevarCalc(tokens, at_end_time);
1933 minutes_to += timevar_add[1];
1934 }
1935
1936 // this shortcut makes always-open range check faster
1937 // and is also useful in tests, as it doesn't produce
1938 // extra check points which may hide errors in other
1939 // selectors
1940 // if (minutes_from == 0 && minutes_to == minutes_in_day)
1941 // selectors.time.push(function(date) { return [true]; });
1942 // Not needed. If there is no selector it automatically matches everything.
1943 }
1944 }
1945
1946 // normalize minutes into range
1947 // XXX: what if it's further than tomorrow?
1948 // XXX: this is incorrect, as it assumes the same day
1949 // should cooperate with date selectors to select the next day
1950 if (minutes_from >= minutes_in_day)
1951 throw formatWarnErrorMessage(nblock, at_end_time - 1,
1952 'Time range starts outside of the current day');
1953 if (minutes_to < minutes_from || ((has_normal_time[0] && has_normal_time[1]) && minutes_from == minutes_to))
1954 minutes_to += minutes_in_day;
1955 if (minutes_to > minutes_in_day * 2)
1956 throw formatWarnErrorMessage(nblock, at_end_time + (has_normal_time[1] ? 3 : (has_time_var_calc[1] ? 7 : 1)) - 1,
1957 'Time spanning more than two midnights not supported');
1958
1959 var timevar_string = [];
1960 if (typeof lat != 'undefined') { // lon will also be defined (see above)
1961 if ((!has_normal_time[0] || !has_normal_time[1]) && !has_open_end)
1962 week_stable = false;
1963 if (!has_normal_time[0])
1964 timevar_string[0] = tokens[at+has_time_var_calc[0]][0];
1965 if (!has_normal_time[1] && !has_open_end && !is_point_in_time)
1966 timevar_string[1] = tokens[at_end_time+has_time_var_calc[1]][0]
1967 } // else: we can not calculate exact times so we use the already applied constants (word_value_replacement).
1968
1969 if (minutes_to > minutes_in_day) { // has_normal_time[1] must be true
1970 selectors.time.push(function(minutes_from, minutes_to, timevar_string, timevar_add, has_open_end, is_point_in_time) { return function(date) {
1971 var ourminutes = date.getHours() * 60 + date.getMinutes();
1972
1973 if (timevar_string[0]) {
1974 var date_from = eval('SunCalc.getTimes(date, lat, lon).' + timevar_string[0]);
1975 minutes_from = date_from.getHours() * 60 + date_from.getMinutes() + timevar_add[0];
1976 }
1977 if (timevar_string[1]) {
1978 var date_to = eval('SunCalc.getTimes(date, lat, lon).' + timevar_string[1]);
1979 minutes_to = date_to.getHours() * 60 + date_to.getMinutes() + timevar_add[1];
1980 minutes_to += minutes_in_day;
1981 // Needs to be added because it was added by
1982 // normal times in: if (minutes_to < // minutes_from)
1983 // above the selector construction.
1984 } else if (is_point_in_time) {
1985 minutes_to = minutes_from + 1;
1986 }
1987
1988 if (ourminutes < minutes_from)
1989 return [false, dateAtDayMinutes(date, minutes_from)];
1990 else
1991 return [true, dateAtDayMinutes(date, minutes_to), has_open_end];
1992 }}(minutes_from, minutes_to, timevar_string, timevar_add, has_open_end, is_point_in_time));
1993
1994 selectors.wraptime.push(function(minutes_from, minutes_to, timevar_string, timevar_add, has_open_end, is_point_in_time) { return function(date) {
1995 var ourminutes = date.getHours() * 60 + date.getMinutes();
1996
1997 if (timevar_string[0]) {
1998 var date_from = eval('SunCalc.getTimes(date, lat, lon).' + timevar_string[0]);
1999 minutes_from = date_from.getHours() * 60 + date_from.getMinutes() + timevar_add[0];
2000 }
2001 if (timevar_string[1]) {
2002 var date_to = eval('SunCalc.getTimes(date, lat, lon).' + timevar_string[1]);
2003 minutes_to = date_to.getHours() * 60 + date_to.getMinutes() + timevar_add[1];
2004 // minutes_in_day does not need to be added.
2005 // For normal times in it was added in: if (minutes_to < // minutes_from)
2006 // above the selector construction and
2007 // subtracted in the selector construction call
2008 // which returns the selector function.
2009 } else if (is_point_in_time) {
2010 minutes_to = minutes_from + 1;
2011 }
2012
2013 if (ourminutes < minutes_to)
2014 return [true, dateAtDayMinutes(date, minutes_to), has_open_end];
2015 else
2016 return [false, undefined];
2017 }}(minutes_from, minutes_to - minutes_in_day, timevar_string, timevar_add, has_open_end, is_point_in_time));
2018 } else {
2019 selectors.time.push(function(minutes_from, minutes_to, timevar_string, timevar_add, has_open_end, is_point_in_time) { return function(date) {
2020 var ourminutes = date.getHours() * 60 + date.getMinutes();
2021
2022 if (timevar_string[0]) {
2023 var date_from = eval('SunCalc.getTimes(date, lat, lon).' + timevar_string[0]);
2024 minutes_from = date_from.getHours() * 60 + date_from.getMinutes() + timevar_add[0];
2025 }
2026 if (timevar_string[1]) {
2027 var date_to = eval('SunCalc.getTimes(date, lat, lon).' + timevar_string[1]);
2028 minutes_to = date_to.getHours() * 60 + date_to.getMinutes() + timevar_add[1];
2029 } else if (is_point_in_time) {
2030 minutes_to = minutes_from + 1;
2031 }
2032
2033 if (ourminutes < minutes_from)
2034 return [false, dateAtDayMinutes(date, minutes_from)];
2035 else if (ourminutes < minutes_to)
2036 return [true, dateAtDayMinutes(date, minutes_to), has_open_end];
2037 else
2038 return [false, dateAtDayMinutes(date, minutes_from + minutes_in_day)];
2039 }}(minutes_from, minutes_to, timevar_string, timevar_add, has_open_end, is_point_in_time));
2040 }
2041
2042 at = at_end_time + (is_point_in_time ? -1 :
2043 (has_normal_time[1] ? 3 : (has_time_var_calc[1] ? 7 : !has_open_end))
2044 );
2045 } else { // additional block
2046 if (matchTokens(tokens, at, '('))
2047 throw formatWarnErrorMessage(nblock, at+1, 'Missing variable time (e.g. sunrise) after: "' + tokens[at][1] + '"');
2048 if (matchTokens(tokens, at, 'number', 'timesep'))
2049 throw formatWarnErrorMessage(nblock, at+2, 'Missing minutes in time range after: "' + tokens[at+1][1] + '"');
2050 if (matchTokens(tokens, at, 'number'))
2051 throw formatWarnErrorMessage(nblock, at+2, 'Missing time seperator in time range after: "' + tokens[at][1] + '"');
2052 return [ at ];
2053 }
2054
2055 if (!matchTokens(tokens, at, ','))
2056 break;
2057 }
2058
2059 return at;
2060 }
2061
2062 // for given date, returns date moved to the start of specified day minute
2063 function dateAtDayMinutes(date, minutes) {
2064 return new Date(date.getFullYear(), date.getMonth(), date.getDate(), 0, minutes);
2065 }
2066
2067 // extract the added or subtracted time of "(sunrise-01:30)"
2068 // returns in minutes e.g. -90
2069 function parseTimevarCalc(tokens, at) {
2070 if (matchTokens(tokens, at+2, '+') || matchTokens(tokens, at+2, '-')) {
2071 if (matchTokens(tokens, at+3, 'number', 'timesep', 'number')) {
2072 if (matchTokens(tokens, at+6, ')')) {
2073 var add_or_subtract = tokens[at+2][0] == '+' ? '1' : '-1';
2074 return (tokens[at+3][0] * 60 + tokens[at+5][0]) * add_or_subtract;
2075 } else {
2076 error = [ at+6, '. Missing ")".'];
2077 }
2078 } else {
2079 error = [ at+5, ' (time).'];
2080 }
2081 } else {
2082 error = [ at+2, '. "+" or "-" expected.'];
2083 }
2084
2085 if (error)
2086 throw formatWarnErrorMessage(nblock, error[0],
2087 'Calculcation with variable time is not in the right syntax' + error[1]);
2088 }
2089
2090 // Only used if throwing an error is wanted.
2091 function getMinutesByHoursMinutes(tokens, nblock, at) {
2092 if (tokens[at+2][0] > 59)
2093 throw formatWarnErrorMessage(nblock, at+2,
2094 'Minutes are greter than 59.');
2095 return tokens[at][0] * 60 + tokens[at+2][0];
2096 }
2097
2098 //======================================================================
2099 // Weekday range parser (Mo,We-Fr,Sa[1-2,-1])
2100 //======================================================================
2101 function parseWeekdayRange(tokens, at, selectors) {
2102 for (; at < tokens.length; at++) {
2103 if (matchTokens(tokens, at, 'weekday', '[')) {
2104 // Conditional weekday (Mo[3])
2105 var numbers = [];
2106
2107 // Get list of constraints
2108 var endat = parseNumRange(tokens, at+2, function(from, to, at) {
2109
2110 // bad number
2111 if (from == 0 || from < -5 || from > 5)
2112 throw formatWarnErrorMessage(nblock, at,
2113 'Number between -5 and 5 (except 0) expected');
2114
2115 if (from == to) {
2116 numbers.push(from);
2117 } else if (from < to) {
2118 for (var i = from; i <= to; i++) {
2119 // bad number
2120 if (i == 0 || i < -5 || i > 5)
2121 throw formatWarnErrorMessage(nblock, at+2,
2122 'Number between -5 and 5 (except 0) expected.');
2123
2124 numbers.push(i);
2125 }
2126 } else {
2127 throw formatWarnErrorMessage(nblock, at+2,
2128 'Bad range: ' + from + '-' + to);
2129 }
2130 });
2131
2132 if (!matchTokens(tokens, endat, ']'))
2133 throw formatWarnErrorMessage(nblock, endat, '"]" or more numbers expected.');
2134
2135 var add_days = getMoveDays(tokens, endat+1, 6, 'constrained weekdays');
2136 week_stable = false;
2137
2138 // Create selector for each list element
2139 for (var nnumber = 0; nnumber < numbers.length; nnumber++) {
2140
2141 selectors.weekday.push(function(weekday, number, add_days) { return function(date) {
2142 var date_num = getValueForDate(date, false); // Year not needed to distinguish.
2143 var start_of_this_month = new Date(date.getFullYear(), date.getMonth(), 1);
2144 var start_of_next_month = new Date(date.getFullYear(), date.getMonth() + 1, 1);
2145
2146 var target_day_this_month;
2147
2148 target_day_this_month = getDateForConstrainedWeekday(date.getFullYear(), date.getMonth(), weekday, [ number ]);
2149
2150 var target_day_with_added_days_this_month = new Date(target_day_this_month.getFullYear(),
2151 target_day_this_month.getMonth(), target_day_this_month.getDate() + add_days);
2152
2153 // The target day with added days can be before this month
2154 if (target_day_with_added_days_this_month.getTime() < start_of_this_month.getTime()) {
2155 // but in this case, the target day without the days added needs to be in this month
2156 if (target_day_this_month.getTime() >= start_of_this_month.getTime()) {
2157 // so we calculate it for the month
2158 // following this month and hope that the
2159 // target day will actually be this month.
2160
2161 target_day_with_added_days_this_month = dateAtNextWeekday(
2162 new Date(date.getFullYear(), date.getMonth() + (number > 0 ? 0 : 1) + 1, 1), weekday);
2163 target_day_this_month.setDate(target_day_with_added_days_this_month.getDate()
2164 + (number + (number > 0 ? -1 : 0)) * 7 + add_days);
2165 } else {
2166 // Calculated target day is not inside this month
2167 // therefore the specified weekday (e.g. fifth Sunday)
2168 // does not exist this month. Try it next month.
2169 return [false, start_of_next_month];
2170 }
2171 } else if (target_day_with_added_days_this_month.getTime() >= start_of_next_month.getTime()) {
2172 // The target day is in the next month. If the target day without the added days is not in this month
2173 if (target_day_this_month.getTime() >= start_of_next_month.getTime())
2174 return [false, start_of_next_month];
2175 }
2176
2177 if (add_days > 0) {
2178 var target_day_with_added_moved_days_this_month = dateAtNextWeekday(
2179 new Date(date.getFullYear(), date.getMonth() + (number > 0 ? 0 : 1) -1, 1), weekday);
2180 target_day_with_added_moved_days_this_month.setDate(target_day_with_added_moved_days_this_month.getDate()
2181 + (number + (number > 0 ? -1 : 0)) * 7 + add_days);
2182
2183 if (date_num == getValueForDate(target_day_with_added_moved_days_this_month, false))
2184 return [true, dateAtDayMinutes(date, minutes_in_day)];
2185 } else if (add_days < 0) {
2186 var target_day_with_added_moved_days_this_month = dateAtNextWeekday(
2187 new Date(date.getFullYear(), date.getMonth() + (number > 0 ? 0 : 1) + 1, 1), weekday);
2188 target_day_with_added_moved_days_this_month.setDate(target_day_with_added_moved_days_this_month.getDate()
2189 + (number + (number > 0 ? -1 : 0)) * 7 + add_days);
2190
2191 if (target_day_with_added_moved_days_this_month.getTime() >= start_of_next_month.getTime()) {
2192 if (target_day_with_added_days_this_month.getTime() >= start_of_next_month.getTime())
2193 return [false, target_day_with_added_moved_days_this_month];
2194 } else {
2195 if (target_day_with_added_days_this_month.getTime() < start_of_next_month.getTime()
2196 && getValueForDate(target_day_with_added_days_this_month, false) == date_num)
2197 return [true, dateAtDayMinutes(date, minutes_in_day)];
2198
2199 target_day_with_added_days_this_month = target_day_with_added_moved_days_this_month;
2200 }
2201 }
2202
2203 // we hit the target day
2204 if (date.getDate() == target_day_with_added_days_this_month.getDate()) {
2205 return [true, dateAtDayMinutes(date, minutes_in_day)];
2206 }
2207
2208 // we're before target day
2209 if (date.getDate() < target_day_with_added_days_this_month.getDate()) {
2210 return [false, target_day_with_added_days_this_month];
2211 }
2212
2213 // we're after target day, set check date to next month
2214 return [false, start_of_next_month];
2215 }}(tokens[at][0], numbers[nnumber], add_days[0]));
2216 }
2217
2218 at = endat + 1 + add_days[1];
2219 } else if (matchTokens(tokens, at, 'weekday')) {
2220 // Single weekday (Mo) or weekday range (Mo-Fr)
2221 var is_range = matchTokens(tokens, at+1, '-', 'weekday');
2222
2223 var weekday_from = tokens[at][0];
2224 var weekday_to = is_range ? tokens[at+2][0] : weekday_from;
2225
2226 var inside = true;
2227
2228 // handle reversed range
2229 if (weekday_to < weekday_from) {
2230 var tmp = weekday_to;
2231 weekday_to = weekday_from - 1;
2232 weekday_from = tmp + 1;
2233 inside = false;
2234 }
2235
2236 if (weekday_to < weekday_from) { // handle full range
2237 // selectors.weekday.push(function(date) { return [true]; });
2238 // Not needed. If there is no selector it automatically matches everything.
2239 } else {
2240 selectors.weekday.push(function(weekday_from, weekday_to, inside) { return function(date) {
2241 var ourweekday = date.getDay();
2242
2243 if (ourweekday < weekday_from || ourweekday > weekday_to) {
2244 return [!inside, dateAtNextWeekday(date, weekday_from)];
2245 } else {
2246 return [inside, dateAtNextWeekday(date, weekday_to + 1)];
2247 }
2248 }}(weekday_from, weekday_to, inside));
2249 }
2250
2251 at += is_range ? 3 : 1;
2252 } else if (matchTokens(tokens, at, 'holiday')) {
2253 week_stable = false;
2254 return parseHoliday(tokens, at, selectors, true);
2255 } else {
2256 throw formatWarnErrorMessage(nblock, at, 'Unexpected token in weekday range: ' + tokens[at][1]);
2257 }
2258
2259 if (!matchTokens(tokens, at, ','))
2260 break;
2261 }
2262
2263 if (typeof used_subparsers['weekdays'] != 'number')
2264 used_subparsers['weekdays'] = 1;
2265 else
2266 used_subparsers['weekdays']++;
2267
2268 return at;
2269 }
2270
2271 // Numeric list parser (1,2,3-4,-1), used in weekday parser above
2272 function parseNumRange(tokens, at, func) {
2273 for (; at < tokens.length; at++) {
2274 if (matchTokens(tokens, at, 'number', '-', 'number')) {
2275 // Number range
2276 func(tokens[at][0], tokens[at+2][0], at);
2277 at += 3;
2278 } else if (matchTokens(tokens, at, '-', 'number')) {
2279 // Negative number
2280 func(-tokens[at+1][0], -tokens[at+1][0], at);
2281 at += 2
2282 } else if (matchTokens(tokens, at, 'number')) {
2283 // Single number
2284 func(tokens[at][0], tokens[at][0], at);
2285 at++;
2286 } else {
2287 throw formatWarnErrorMessage(nblock, at + matchTokens(tokens, at, '-'),
2288 'Unexpected token in number range: ' + tokens[at][1]);
2289 }
2290
2291 if (!matchTokens(tokens, at, ','))
2292 break;
2293 }
2294
2295 return at;
2296 }
2297
2298 function getMoveDays(tokens, at, max_differ, name) {
2299 var add_days = [ 0, 0 ]; // [ 'add days', 'how many tokens' ]
2300 add_days[0] = matchTokens(tokens, at, '+') || (matchTokens(tokens, at, '-') ? -1 : 0);
2301 if (add_days[0] != 0 && matchTokens(tokens, at+1, 'number', 'calcday')) {
2302 // continues with '+ 5 days' or something like that
2303 if (tokens[at+1][0] > max_differ)
2304 throw formatWarnErrorMessage(nblock, at+2,
2305 'There should be no reason to differ more than ' + max_differ + ' days from a ' + name + '. If so tell us …');
2306 add_days[0] *= tokens[at+1][0];
2307 if (add_days[0] == 0 && !done_with_warnings)
2308 parsing_warnings.push([ nblock, at+2, 'Adding 0 does not change the date. Please omit this.' ]);
2309 add_days[1] = 3;
2310 } else {
2311 add_days[0] = 0;
2312 }
2313 return add_days;
2314 }
2315
2316
2317 // for given date, returns date moved to the specific day of week
2318 function dateAtNextWeekday(date, day) {
2319 var delta = day - date.getDay();
2320 return new Date(date.getFullYear(), date.getMonth(), date.getDate() + delta + (delta < 0 ? 7 : 0));
2321 }
2322
2323 //======================================================================
2324 // Holiday parser for public and school holidays (PH,SH)
2325 // push_to_weekday will push the selector into the weekday selector array which has the desired side effect of working in conjunction with the weekday selectors (either the holiday match or the weekday), which is the normal and expected behavior.
2326 //======================================================================
2327 function parseHoliday(tokens, at, selectors, push_to_weekday) {
2328 for (; at < tokens.length; at++) {
2329 if (matchTokens(tokens, at, 'holiday')) {
2330 if (tokens[at][0] == 'PH') {
2331 var applying_holidays = getMatchingHoliday(tokens[at][0]);
2332
2333 // Only allow moving one day in the past or in the future.
2334 // This makes implementation easier because only one holiday is assumed to be moved to the next year.
2335 var add_days = getMoveDays(tokens, at+1, 1, 'public holiday');
2336
2337 var selector = function(applying_holidays, add_days) { return function(date) {
2338
2339 var holidays = getApplyingHolidaysForYear(applying_holidays, date.getFullYear(), add_days);
2340 // Needs to be calculated each time because of movable days.
2341
2342 var date_num = getValueForDate(date, true);
2343
2344 for (var i = 0; i < holidays.length; i++) {
2345 var next_holiday_date_num = getValueForDate(holidays[i][0], true);
2346
2347 if (date_num < next_holiday_date_num) {
2348
2349 if (add_days[0] > 0) {
2350 // Calculate the last holiday from previous year to tested against it.
2351 var holidays_last_year = getApplyingHolidaysForYear(applying_holidays, date.getFullYear() - 1, add_days);
2352 var last_holiday_last_year = holidays_last_year[holidays_last_year.length - 1];
2353 var last_holiday_last_year_num = getValueForDate(last_holiday_last_year[0], true);
2354
2355 if (date_num < last_holiday_last_year_num ) {
2356 return [ false, last_holiday_last_year[0] ];
2357 } else if (date_num == last_holiday_last_year_num) {
2358 return [true, dateAtDayMinutes(last_holiday_last_year[0], minutes_in_day),
2359 'Day after ' +last_holiday_last_year[1] ];
2360 }
2361 }
2362
2363 return [ false, holidays[i][0] ];
2364 } else if (date_num == next_holiday_date_num) {
2365 return [true, new Date(date.getFullYear(), date.getMonth(), date.getDate() + 1),
2366 (add_days[0] > 0 ? 'Day after ' : (add_days[0] < 0 ? 'Day before ' : '')) + holidays[i][1] ];
2367 }
2368 }
2369
2370 if (add_days[0] < 0) {
2371 // Calculate the first holiday from next year to tested against it.
2372 var holidays_next_year = getApplyingHolidaysForYear(applying_holidays, date.getFullYear() + 1, add_days);
2373 var first_holidays_next_year = holidays_next_year[0];
2374 var first_holidays_next_year_num = getValueForDate(first_holidays_next_year[0], true);
2375 if (date_num == first_holidays_next_year_num) {
2376 return [true, dateAtDayMinutes(first_holidays_next_year[0], minutes_in_day),
2377 'Day before ' + first_holidays_next_year[1] ];
2378 }
2379 }
2380
2381 // continue next year
2382 return [ false, new Date(holidays[0][0].getFullYear() + 1,
2383 holidays[0][0].getMonth(),
2384 holidays[0][0].getDate()) ];
2385
2386 }}(applying_holidays, add_days);
2387
2388 if (push_to_weekday)
2389 selectors.weekday.push(selector);
2390 else
2391 selectors.holiday.push(selector);
2392
2393 at += 1 + add_days[1];
2394 } else if (tokens[at][0] == 'SH') {
2395 var applying_holidays = getMatchingHoliday(tokens[at][0]);
2396
2397 var holidays = []; // needs to be sorted each time because of movable days
2398
2399 var selector = function(applying_holidays) { return function(date) {
2400 var date_num = getValueForDate(date);
2401
2402 // Iterate over holiday array containing the different holiday ranges.
2403 for (var i = 0; i < applying_holidays.length; i++) {
2404
2405 var holiday = getSHForYear(applying_holidays[i], date.getFullYear());
2406
2407 for (var h = 0; h < holiday.length; h+=4) {
2408 var holiday_to_plus = new Date(date.getFullYear(), holiday[2+h] - 1, holiday[3+h] + 1);
2409 var holiday_from = (holiday[0+h] - 1) * 100 + holiday[1+h];
2410 var holiday_to = (holiday[2+h] - 1) * 100 + holiday[3+h];
2411 holiday_to_plus = getValueForDate(holiday_to_plus);
2412
2413 var holiday_ends_next_year = holiday_to < holiday_from;
2414
2415 if (date_num < holiday_from) { // date is before selected holiday
2416
2417 // check if we are in the holidays from the last year spanning into this year
2418 var last_year_holiday = getSHForYear(applying_holidays[applying_holidays.length - 1], date.getFullYear() - 1, false);
2419 if (typeof last_year_holiday != 'undefined') {
2420 var last_year_holiday_from = (last_year_holiday[last_year_holiday.length - 4] - 1) * 100
2421 + last_year_holiday[last_year_holiday.length - 3]; // e.g. 1125
2422 var last_year_holiday_to = (last_year_holiday[last_year_holiday.length - 2] - 1) * 100
2423 + last_year_holiday[last_year_holiday.length - 1]; // e.g. 0005
2424
2425 if (last_year_holiday_to < last_year_holiday_from && date_num < last_year_holiday_to)
2426 return [ true, new Date(date.getFullYear(),
2427 last_year_holiday[last_year_holiday.length - 2] - 1,
2428 last_year_holiday[last_year_holiday.length - 1] + 1),
2429 applying_holidays[applying_holidays.length - 1].name ];
2430 else
2431 return [ false, new Date(date.getFullYear(), holiday[0+h] - 1, holiday[1+h]) ];
2432 } else { // school holidays for last year are not defined.
2433 return [ false, new Date(date.getFullYear(), holiday[0+h] - 1, holiday[1+h]) ];
2434 }
2435 } else if (holiday_from <= date_num && (date_num < holiday_to_plus || holiday_ends_next_year)) {
2436 return [ true, new Date(date.getFullYear() + holiday_ends_next_year, holiday[2+h] - 1, holiday[3+h] + 1),
2437 applying_holidays[i].name ];
2438 } else if (holiday_to_plus == date_num) { // selected holiday end is equal to month and day
2439 if (h + 4 < holiday.length) { // next holiday is next date range of the same holidays
2440 h += 4;
2441 return [ false, new Date(date.getFullYear(), holiday[0+h] - 1, holiday[1+h]) ];
2442 } else {
2443 if (i + 1 == applying_holidays.length) { // last holidays are handled, continue all over again
2444 var holiday = getSHForYear(applying_holidays[0], date.getFullYear() + 1);
2445 return [ false, new Date(date.getFullYear() + !holiday_ends_next_year, holiday[0+h] - 1, holiday[1+h]) ];
2446 } else { // return the start of the next holidays
2447 var holiday = getSHForYear(applying_holidays[i+1], date.getFullYear());
2448 return [ false, new Date(date.getFullYear(), holiday[0] - 1, holiday[1]) ];
2449 }
2450 }
2451 }
2452 }
2453 }
2454 return [ false ];
2455 }}(applying_holidays);
2456
2457 if (push_to_weekday)
2458 selectors.weekday.push(selector);
2459 else
2460 selectors.holiday.push(selector);
2461 at += 1;
2462 }
2463 } else if (matchTokens(tokens, at, 'weekday')) {
2464 return parseWeekdayRange(tokens, at, selectors);
2465 } else {
2466 throw formatWarnErrorMessage(nblock, at, 'Unexpected token (school holiday parser): ' + tokens[at][1]);
2467 }
2468
2469 if (!matchTokens(tokens, at, ','))
2470 break;
2471 }
2472
2473 return at;
2474 }
2475
2476 // Returns a number for a date which can then be used to compare just the dates (without the time).
2477 // This is necessary because a selector could be called for the middle of the day and we need to tell if it matches that day.
2478 // Example: Returns 20150015 for Jan 01 2015
2479 function getValueForDate(date, include_year) {
2480 // Implicit because undefined evaluates to false
2481 // include_year = typeof include_year != 'undefined' ? include_year : false;
2482
2483 return (include_year ? date.getFullYear() * 10000 : 0) + date.getMonth() * 100 + date.getDate();
2484 }
2485
2486 // return the school holiday definition e.g. [ 5, 25, /* to */ 6, 5 ],
2487 // for the specified year
2488 function getSHForYear(SH_hash, year, fatal) {
2489 if (typeof fatal == 'undefined')
2490 fatal = true;
2491
2492 var holiday = SH_hash[year];
2493 if (typeof holiday == 'undefined') {
2494 holiday = SH_hash['default']; // applies for any year without explicit definition
2495 if (typeof holiday == 'undefined') {
2496 if (fatal) {
2497 throw 'School holiday ' + SH_hash.name + ' has no definition for the year ' + year + '.';
2498 } else {
2499 return undefined;
2500 }
2501 }
2502 }
2503 return holiday;
2504 }
2505
2506 // Return closed holiday definition available.
2507 // First try to get the state, if missing get the country wide holidays
2508 // (which can be limited to some states).
2509 function getMatchingHoliday(type_of_holidays) {
2510 if (typeof location_cc != 'undefined') {
2511 if (holidays.hasOwnProperty(location_cc)) {
2512 if (typeof location_state != 'undefined') {
2513 if (holidays[location_cc][location_state]
2514 && holidays[location_cc][location_state][type_of_holidays]) {
2515 // if holidays for the state are specified use it
2516 // and ignore lesser specific ones (for the country)
2517 return holidays[location_cc][location_state][type_of_holidays];
2518 } else if (holidays[location_cc][type_of_holidays]) {
2519 // holidays are only defined country wide
2520 matching_holiday = {}; // holidays in the country wide scope can be limited to certain states
2521 for (var holiday_name in holidays[location_cc][type_of_holidays]) {
2522 if (typeof holidays[location_cc][type_of_holidays][holiday_name][2] === 'object') {
2523 if (-1 != indexOf.call(holidays[location_cc][type_of_holidays][holiday_name][2], location_state))
2524 matching_holiday[holiday_name] = holidays[location_cc][type_of_holidays][holiday_name];
2525 } else {
2526 matching_holiday[holiday_name] = holidays[location_cc][type_of_holidays][holiday_name];
2527 }
2528 }
2529 if (Object.keys(matching_holiday).length == 0)
2530 throw 'There are no holidays ' + type_of_holidays + ' defined for country ' + location_cc + '.'
2531 + ' Please add them: https://github.com/ypid/opening_hours.js ';
2532 return matching_holiday;
2533 } else {
2534 throw 'Holidays ' + type_of_holidays + ' are not defined for country ' + location_cc
2535 + ' and state ' + location_state + '.'
2536 + ' Please add them.';
2537 }
2538 }
2539 } else {
2540 throw 'No holidays are defined for country ' + location_cc + '. Please add them: https://github.com/ypid/opening_hours.js ';
2541 }
2542 } else { // we have no idea which holidays do apply because the country code was not provided
2543 throw 'Country code missing which is needed to select the correct holidays (see README how to provide it)'
2544 }
2545 }
2546
2547 function getMovableEventsForYear(Y) {
2548 // calculate easter
2549 var C = Math.floor(Y/100);
2550 var N = Y - 19*Math.floor(Y/19);
2551 var K = Math.floor((C - 17)/25);
2552 var I = C - Math.floor(C/4) - Math.floor((C - K)/3) + 19*N + 15;
2553 I = I - 30*Math.floor((I/30));
2554 I = I - Math.floor(I/28)*(1 - Math.floor(I/28)*Math.floor(29/(I + 1))*Math.floor((21 - N)/11));
2555 var J = Y + Math.floor(Y/4) + I + 2 - C + Math.floor(C/4);
2556 J = J - 7*Math.floor(J/7);
2557 var L = I - J;
2558 var M = 3 + Math.floor((L + 40)/44);
2559 var D = L + 28 - 31*Math.floor(M/4);
2560
2561 return {
2562 'easter': new Date(Y, M - 1, D),
2563 };
2564 }
2565
2566 function indexOf(needle) {
2567 if(typeof Array.prototype.indexOf === 'function') {
2568 indexOf = Array.prototype.indexOf;
2569 } else {
2570 indexOf = function(needle) {
2571 var i = -1, index = -1;
2572 for(i = 0; i < this.length; i++) {
2573 if(this[i] === needle) {
2574 index = i;
2575 break;
2576 }
2577 }
2578 return index;
2579 };
2580 }
2581 return indexOf.call(this, needle);
2582 }
2583
2584 function getApplyingHolidaysForYear(applying_holidays, year, add_days) {
2585 var movableDays = getMovableEventsForYear(year);
2586
2587 var sorted_holidays = [];
2588
2589 for (var holiday_name in applying_holidays) {
2590 if (typeof applying_holidays[holiday_name][0] == 'string') {
2591 var selected_movableDay = movableDays[applying_holidays[holiday_name][0]];
2592 if (!selected_movableDay)
2593 throw 'Movable day ' + applying_holidays[holiday_name][0] + ' can not not be calculated.'
2594 + ' Please add the formula how to calculate it.';
2595 var next_holiday = new Date(selected_movableDay.getFullYear(),
2596 selected_movableDay.getMonth(),
2597 selected_movableDay.getDate()
2598 + applying_holidays[holiday_name][1]
2599 );
2600 if (year != next_holiday.getFullYear())
2601 throw 'The movable day ' + applying_holidays[holiday_name][0] + ' plus '
2602 + applying_holidays[holiday_name][1]
2603 + ' days is not in the year of the movable day anymore. Currently not supported.';
2604 } else {
2605 var next_holiday = new Date(year,
2606 applying_holidays[holiday_name][0] - 1,
2607 applying_holidays[holiday_name][1]
2608 );
2609 }
2610 if (add_days[0])
2611 next_holiday.setDate(next_holiday.getDate() + add_days[0]);
2612
2613 sorted_holidays.push([ next_holiday, holiday_name ]);
2614 }
2615
2616 sorted_holidays = sorted_holidays.sort(function(a,b){
2617 if (a[0].getTime() < b[0].getTime()) return -1;
2618 if (a[0].getTime() > b[0].getTime()) return 1;
2619 return 0;
2620 });
2621
2622 return sorted_holidays;
2623 }
2624
2625 //======================================================================
2626 // Year range parser (2013,2016-2018,2020/2)
2627 //======================================================================
2628 function parseYearRange(tokens, at) {
2629 for (; at < tokens.length; at++) {
2630 if (matchTokens(tokens, at, 'year')) {
2631 var is_range = false, has_period = false;
2632 if (matchTokens(tokens, at+1, '-', 'year', '/', 'number')) {
2633 var is_range = true;
2634 var has_period = true;
2635 if (tokens[at+4][0] == 1 && !done_with_warnings)
2636 parsing_warnings.push([nblock, at+1+3, 'Please don’t use year ranges with period equals one (see README)']);
2637 } else {
2638 var is_range = matchTokens(tokens, at+1, '-', 'year');
2639 var has_period = matchTokens(tokens, at+1, '/', 'number');
2640 }
2641
2642 selectors.year.push(function(tokens, at, is_range, has_period) { return function(date) {
2643 var ouryear = date.getFullYear();
2644 var year_from = tokens[at][0];
2645 var year_to = is_range ? tokens[at+2][0] : year_from;
2646
2647 // handle reversed range
2648 if (year_to < year_from) {
2649 var tmp = year_to;
2650 year_to = year_from;
2651 year_from = tmp;
2652 }
2653
2654 if (ouryear < year_from ){
2655 return [false, new Date(year_from, 0, 1)];
2656 } else if (has_period) {
2657 if (year_from <= ouryear) {
2658 if (is_range) {
2659 var period = tokens[at+4][0];
2660
2661 if (year_to < ouryear)
2662 return [false];
2663 } else {
2664 var period = tokens[at+2][0];
2665 }
2666 if (period > 0) {
2667 if ((ouryear - year_from) % period == 0) {
2668 return [true, new Date(ouryear + 1, 0, 1)];
2669 } else {
2670 return [false, new Date(ouryear + period - 1, 0, 1)];
2671 }
2672 }
2673 }
2674 } else if (is_range) {
2675 if (year_from <= ouryear && ouryear <= year_to)
2676 return [true, new Date(year_to + 1, 0, 1)];
2677 } else if (ouryear == year_from) {
2678 return [true];
2679 }
2680
2681 return [false];
2682
2683 }}(tokens, at, is_range, has_period));
2684
2685 at += 1 + (is_range ? 2 : 0) + (has_period ? 2 : 0);
2686 } else {
2687 throw formatWarnErrorMessage(nblock, at, 'Unexpected token in year range: ' + tokens[at][1]);
2688 }
2689
2690 if (!matchTokens(tokens, at, ','))
2691 break;
2692 }
2693
2694 if (typeof used_subparsers['year ranges'] != 'number')
2695 used_subparsers['year ranges'] = 1;
2696 else
2697 used_subparsers['year ranges']++;
2698
2699 return at;
2700 }
2701
2702 //======================================================================
2703 // Week range parser (week 11-20, week 1-53/2)
2704 //======================================================================
2705 function parseWeekRange(tokens, at) {
2706 for (; at < tokens.length; at++) {
2707 if (matchTokens(tokens, at, 'number')) {
2708 var is_range = matchTokens(tokens, at+1, '-', 'number'), has_period = false;
2709 if (is_range) {
2710 has_period = matchTokens(tokens, at+3, '/', 'number');
2711 // if (week_stable) {
2712 // if (tokens[at][0] == 1 && tokens[at+2][0] >) // Maximum?
2713 // week_stable = true;
2714 // else
2715 // week_stable = false;
2716 // } else {
2717 // week_stable = false;
2718 // }
2719 }
2720
2721 selectors.week.push(function(tokens, at, is_range, has_period) { return function(date) {
2722 var ourweek = Math.floor((date - dateAtWeek(date, 0)) / msec_in_week);
2723
2724 var week_from = tokens[at][0] - 1;
2725 var week_to = is_range ? tokens[at+2][0] - 1 : week_from;
2726
2727 var start_of_next_year = new Date(date.getFullYear() + 1, 0, 1);
2728
2729 // before range
2730 if (ourweek < week_from)
2731 return [false, getMinDate(dateAtWeek(date, week_from), start_of_next_year)];
2732
2733 // we're after range, set check date to next year
2734 if (ourweek > week_to)
2735 return [false, start_of_next_year];
2736
2737 // we're in range
2738 var period;
2739 if (has_period) {
2740 var period = tokens[at+4][0];
2741 if (period > 1) {
2742 var in_period = (ourweek - week_from) % period == 0;
2743 if (in_period)
2744 return [true, getMinDate(dateAtWeek(date, ourweek + 1), start_of_next_year)];
2745 else
2746 return [false, getMinDate(dateAtWeek(date, ourweek + period - 1), start_of_next_year)];
2747 }
2748 }
2749
2750 return [true, getMinDate(dateAtWeek(date, week_to + 1), start_of_next_year)];
2751 }}(tokens, at, is_range, has_period));
2752
2753 at += 1 + (is_range ? 2 : 0) + (has_period ? 2 : 0);
2754 } else {
2755 throw formatWarnErrorMessage(nblock, at, 'Unexpected token in week range: ' + tokens[at][1]);
2756 }
2757
2758 if (!matchTokens(tokens, at, ','))
2759 break;
2760 }
2761
2762 if (typeof used_subparsers['week ranges'] != 'number')
2763 used_subparsers['week ranges'] = 1;
2764 else
2765 used_subparsers['week ranges']++;
2766
2767 return at;
2768 }
2769
2770 function dateAtWeek(date, week) {
2771 var tmpdate = new Date(date.getFullYear(), 0, 1);
2772 tmpdate.setDate(1 - (tmpdate.getDay() + 6) % 7 + week * 7); // start of week n where week starts on Monday
2773 return tmpdate;
2774 }
2775
2776 function getMinDate(date /*, ...*/) {
2777 for (var i = 1; i < arguments.length; i++)
2778 if (arguments[i].getTime() < date.getTime())
2779 date = arguments[i];
2780 return date;
2781 }
2782
2783 //======================================================================
2784 // Month range parser (Jan,Feb-Mar)
2785 //======================================================================
2786 function parseMonthRange(tokens, at) {
2787 for (; at < tokens.length; at++) {
2788 if (matchTokens(tokens, at, 'month')) {
2789 // Single month (Jan) or month range (Feb-Mar)
2790 var is_range = matchTokens(tokens, at+1, '-', 'month');
2791
2792 if (is_range && week_stable) {
2793 var month_from = tokens[at][0];
2794 var month_to = tokens[at+2][0];
2795 if (month_from == (month_to + 1) % 12)
2796 week_stable = true;
2797 else
2798 week_stable = false;
2799 } else {
2800 week_stable = false;
2801 }
2802
2803 selectors.month.push(function(tokens, at, is_range) { return function(date) {
2804 var ourmonth = date.getMonth();
2805 var month_from = tokens[at][0];
2806 var month_to = is_range ? tokens[at+2][0] : month_from;
2807
2808 var inside = true;
2809
2810 // handle reversed range
2811 if (month_to < month_from) {
2812 var tmp = month_to;
2813 month_to = month_from - 1;
2814 month_from = tmp + 1;
2815 inside = false;
2816 }
2817
2818 // handle full range
2819 if (month_to < month_from)
2820 return [!inside];
2821
2822 if (ourmonth < month_from || ourmonth > month_to) {
2823 return [!inside, dateAtNextMonth(date, month_from)];
2824 } else {
2825 return [inside, dateAtNextMonth(date, month_to + 1)];
2826 }
2827 }}(tokens, at, is_range));
2828
2829 at += is_range ? 3 : 1;
2830 } else {
2831 throw formatWarnErrorMessage(nblock, at, 'Unexpected token in month range: ' + tokens[at][1]);
2832 }
2833
2834 if (!matchTokens(tokens, at, ','))
2835 break;
2836 }
2837
2838 if (typeof used_subparsers['months'] != 'number')
2839 used_subparsers['months'] = 1;
2840 else
2841 used_subparsers['months']++;
2842
2843 return at;
2844 }
2845
2846 function dateAtNextMonth(date, month) {
2847 return new Date(date.getFullYear(), month < date.getMonth() ? month + 12 : month);
2848 }
2849
2850 function getConstrainedWeekday(tokens, at) {
2851 var number = 0;
2852 var endat = parseNumRange(tokens, at, function(from, to, at) {
2853
2854 // bad number
2855 if (from == 0 || from < -5 || from > 5)
2856 throw formatWarnErrorMessage(nblock, at,
2857 'Number between -5 and 5 (except 0) expected');
2858
2859 if (from == to) {
2860 if (number != 0)
2861 throw formatWarnErrorMessage(nblock, at,
2862 'You can not use a more than one constrained weekday in a month range');
2863 number = from;
2864 } else {
2865 throw formatWarnErrorMessage(nblock, at+2,
2866 'You can not use a range of constrained weekdays in a month range');
2867 }
2868 });
2869
2870 if (!matchTokens(tokens, endat, ']'))
2871 throw formatWarnErrorMessage(nblock, endat, '"]" expected.');
2872
2873 return [ number, endat + 1 ];
2874 }
2875
2876 function getDateForConstrainedWeekday(year, month, weekday, constrained_weekday, add_days) {
2877 var tmp_date = dateAtNextWeekday(
2878 new Date(year, month + (constrained_weekday[0] > 0 ? 0 : 1), 1), weekday);
2879
2880 tmp_date.setDate(tmp_date.getDate() + (constrained_weekday[0] + (constrained_weekday[0] > 0 ? -1 : 0)) * 7);
2881
2882 if (typeof add_days != 'undefined' && add_days[1])
2883 tmp_date.setDate(tmp_date.getDate() + add_days[0]);
2884
2885 return tmp_date;
2886 }
2887
2888 //======================================================================
2889 // Month day range parser (Jan 26-31; Jan 26-Feb 26)
2890 //======================================================================
2891 function parseMonthdayRange(tokens, at) {
2892 for (; at < tokens.length; at++) {
2893 var has_year = [], has_month = [], has_event = [], has_calc = [], has_constrained_weekday = [], has_calc = [];
2894 has_year[0] = matchTokens(tokens, at, 'year');
2895 has_month[0] = matchTokens(tokens, at+has_year[0], 'month', 'number');
2896 has_event[0] = matchTokens(tokens, at+has_year[0], 'event');
2897 if (has_event[0])
2898 has_calc[0] = getMoveDays(tokens, at+has_year[0]+1, 200, 'event like easter');
2899
2900 if (matchTokens(tokens, at+has_year[0], 'month', 'weekday', '[')) {
2901 has_constrained_weekday[0] = getConstrainedWeekday(tokens, at+has_year[0]+3);
2902 has_calc[0] = getMoveDays(tokens, has_constrained_weekday[0][1], 6, 'constrained weekdays');
2903 var at_range_sep = has_constrained_weekday[0][1] + (typeof has_calc[0] != 'undefined' && has_calc[0][1] ? 3 : 0);
2904 } else {
2905 var at_range_sep = at+has_year[0]
2906 + (has_event[0]
2907 ? (typeof has_calc[0] != 'undefined' && has_calc[0][1] ? 4 : 1)
2908 : 2);
2909 }
2910
2911 if ((has_month[0] || has_event[0] || has_constrained_weekday[0]) && matchTokens(tokens, at_range_sep, '-')) {
2912 has_year[1] = matchTokens(tokens, at_range_sep+1, 'year');
2913 var at_sec_event_or_month = at_range_sep+1+has_year[1];
2914 has_month[1] = matchTokens(tokens, at_sec_event_or_month, 'month', 'number');
2915 if (!has_month[1]) {
2916 has_event[1] = matchTokens(tokens, at_sec_event_or_month, 'event');
2917 if (has_event[1]) {
2918 has_calc[1] = getMoveDays(tokens, at_sec_event_or_month+1, 366, 'event like easter');
2919 } else if (matchTokens(tokens, at_sec_event_or_month, 'month', 'weekday', '[')) {
2920 has_constrained_weekday[1] = getConstrainedWeekday(tokens, at_sec_event_or_month+3);
2921 has_calc[1] = getMoveDays(tokens, has_constrained_weekday[1][1], 6, 'constrained weekdays');
2922 }
2923 }
2924 }
2925
2926 if (has_year[0] == has_year[1] && (has_month[1] || has_event[1] || has_constrained_weekday[1])) {
2927
2928 selectors.monthday.push(function(tokens, at, nblock, has_year, has_event, has_calc, at_sec_event_or_month, has_constrained_weekday) { return function(date) {
2929 var start_of_next_year = new Date(date.getFullYear() + 1, 0, 1);
2930
2931 if (has_event[0]) {
2932 var movableDays = getMovableEventsForYear(has_year[0] ? parseInt(tokens[at][0]) : date.getFullYear());
2933 var from_date = movableDays[tokens[at+has_year[0]][0]];
2934
2935 if (typeof has_calc[0] != 'undefined' && has_calc[0][1]) {
2936 var from_year_before_calc = from_date.getFullYear();
2937 from_date.setDate(from_date.getDate() + has_calc[0][0]);
2938 if (from_year_before_calc != from_date.getFullYear())
2939 throw formatWarnErrorMessage(nblock, at+has_year[0]+has_calc[0][1]*3,
2940 'The movable day ' + tokens[at+has_year[0]][0] + ' plus ' + has_calc[0][0]
2941 + ' days is not in the year of the movable day anymore. Currently not supported.');
2942 }
2943 } else if (has_constrained_weekday[0]) {
2944 var from_date = getDateForConstrainedWeekday((has_year[0] ? tokens[at][0] : date.getFullYear()), // year
2945 tokens[at+has_year[0]][0], // month
2946 tokens[at+has_year[0]+1][0], // weekday
2947 has_constrained_weekday[0],
2948 has_calc[0]);
2949 // var from_date_without_calc = getDateForConstrainedWeekday((has_year[0] ? tokens[at][0] : date.getFullYear()), // year
2950 // tokens[at+has_year[0]][0], // month
2951 // tokens[at+has_year[0]+1][0], // weekday
2952 // has_constrained_weekday[0],
2953 // [ 0, 0 ]);
2954 // if (from_date_without_calc.getFullYear() != from_date.getFullYear())
2955 // throw formatWarnErrorMessage(nblock, at+has_year[0]+has_calc[0][1],
2956 // 'The constrained ' + weekdays[tokens[at+has_year[0]+1][0]] + ' plus ' + has_calc[0][0]
2957 // + ' days is not in the year of the movable day anymore. Currently not supported.');
2958 } else {
2959 var from_date = new Date((has_year[0] ? tokens[at][0] : date.getFullYear()),
2960 tokens[at+has_year[0]][0], tokens[at+has_year[0]+1][0]);
2961 }
2962
2963 if (has_event[1]) {
2964 var movableDays = getMovableEventsForYear(has_year[1]
2965 ? parseInt(tokens[at_sec_event_or_month-1][0])
2966 : date.getFullYear());
2967 var to_date = movableDays[tokens[at_sec_event_or_month][0]];
2968
2969 if (typeof has_calc[1] != 'undefined' && has_calc[1][1]) {
2970 var to_year_before_calc = to_date.getFullYear();
2971 to_date.setDate(to_date.getDate() + has_calc[1][0]);
2972 if (to_year_before_calc != to_date.getFullYear())
2973 throw formatWarnErrorMessage(nblock, at_sec_event_or_month+has_calc[1][1],
2974 'The movable day ' + tokens[at_sec_event_or_month][0] + ' plus ' + has_calc[1][0]
2975 + ' days is not in the year of the movable day anymore. Currently not supported.');
2976 }
2977 } else if (has_constrained_weekday[1]) {
2978 var to_date = getDateForConstrainedWeekday((has_year[1] ? tokens[at_sec_event_or_month-1][0] : date.getFullYear()), // year
2979 tokens[at_sec_event_or_month][0], // month
2980 tokens[at_sec_event_or_month+1][0], // weekday
2981 has_constrained_weekday[1],
2982 has_calc[1]);
2983 } else {
2984 var to_date = new Date((has_year[1] ? tokens[at_sec_event_or_month-1][0] : date.getFullYear()),
2985 tokens[at_sec_event_or_month][0], tokens[at_sec_event_or_month+1][0] + 1);
2986 }
2987
2988 var inside = true;
2989
2990 if (to_date < from_date) {
2991 var tmp = to_date;
2992 to_date = from_date;
2993 from_date = tmp;
2994 inside = false;
2995 }
2996
2997 if (date.getTime() < from_date.getTime()) {
2998 return [!inside, from_date];
2999 } else if (date.getTime() < to_date.getTime()) {
3000 return [inside, to_date];
3001 } else {
3002 if (has_year[0]) {
3003 return [!inside];
3004 } else {
3005 // // back matching, if from_date is moved to last year
3006 // var from_date_next_year = getDateForConstrainedWeekday(date.getFullYear() + 1, // year
3007 // tokens[at+has_year[0]][0], // month
3008 // tokens[at+has_year[0]+1][0], // weekday
3009 // has_constrained_weekday[0],
3010 // has_calc[0]);
3011 // if (date.getFullYear() == from_date_next_year.getFullYear()) {
3012 // if (date.getTime() < from_date_next_year.getTime()) {
3013 // return [!inside, from_date_next_year];
3014 // }
3015 // }
3016
3017 return [!inside, start_of_next_year];
3018 }
3019 }
3020 }}(tokens, at, nblock, has_year, has_event, has_calc, at_sec_event_or_month, has_constrained_weekday));
3021
3022 at = (has_constrained_weekday[1]
3023 ? has_constrained_weekday[1][1]
3024 : at_sec_event_or_month + (has_event[1] ? 1 : 2))
3025 + (typeof has_calc[1] != 'undefined' ? has_calc[1][1] : 0);
3026
3027 } else if (has_month[0]) {
3028
3029 var is_range = matchTokens(tokens, at+2+has_year[0], '-', 'number'), has_period = false;
3030 if (is_range)
3031 has_period = matchTokens(tokens, at+4+has_year[0], '/', 'number');
3032
3033 var at_timesep_if_monthRange = at + has_year[0] + 1 // at month number
3034 + (is_range ? 2 : 0) + (has_period ? 2 : 0)
3035 + !(is_range || has_period); // if not range nor has_period, add one
3036
3037 if (matchTokens(tokens, at_timesep_if_monthRange, 'timesep', 'number')
3038 && (matchTokens(tokens, at_timesep_if_monthRange+2, '+')
3039 || matchTokens(tokens, at_timesep_if_monthRange+2, '-')))
3040 return parseMonthRange(tokens, at);
3041
3042 selectors.monthday.push(function(tokens, at, is_range, has_period, has_year) { return function(date) {
3043 var start_of_next_year = new Date(date.getFullYear() + 1, 0, 1);
3044
3045 var from_date = new Date((has_year ? tokens[at][0] : date.getFullYear()),
3046 tokens[at+has_year][0], tokens[at+1 + has_year][0]);
3047 var to_date = new Date(from_date.getFullYear(), from_date.getMonth(),
3048 tokens[at+(is_range ? 3 : 1)+has_year][0] + 1);
3049
3050 if (date.getTime() < from_date.getTime())
3051 return [false, from_date];
3052 else if (date.getTime() >= to_date.getTime())
3053 return [false, start_of_next_year];
3054 else if (!has_period)
3055 return [true, to_date];
3056
3057 var period = tokens[at+5][0];
3058 var nday = Math.floor((date.getTime() - from_date.getTime()) / msec_in_day);
3059 var in_period = nday % period;
3060
3061 if (in_period == 0)
3062 return [true, new Date(date.getFullYear(), date.getMonth(), date.getDate() + 1)];
3063 else
3064 return [false, new Date(date.getFullYear(), date.getMonth(), date.getDate() + period - in_period)];
3065 }}(tokens, at, is_range, has_period, has_year[0]));
3066
3067 at += 2 + has_year[0] + (is_range ? 2 : 0) + (has_period ? 2 : 0);
3068
3069 } else if (has_event[0]) {
3070
3071 selectors.monthday.push(function(tokens, at, nblock, has_event, has_year, add_days) { return function(date) {
3072
3073 // console.log('enter selector with date: ' + date);
3074 var movableDays = getMovableEventsForYear((has_year ? tokens[at][0] : date.getFullYear()));
3075 var event_date = movableDays[tokens[at+has_year][0]];
3076 if (!event_date)
3077 throw 'Movable day ' + tokens[at+has_year][0] + ' can not not be calculated.'
3078 + ' Please add the formula how to calculate it.';
3079
3080 if (add_days[0]) {
3081 event_date.setDate(event_date.getDate() + add_days[0]);
3082 if (date.getFullYear() != event_date.getFullYear())
3083 throw formatWarnErrorMessage(nblock, at+has_year+add_days[1], 'The movable day ' + tokens[at+has_year][0] + ' plus '
3084 + add_days[0]
3085 + ' days is not in the year of the movable day anymore. Currently not supported.');
3086 }
3087
3088 if (date.getTime() < event_date.getTime())
3089 return [false, event_date];
3090 // else if (date.getTime() < event_date.getTime() + msec_in_day) // does not work because of daylight saving times
3091 else if (event_date.getMonth() * 100 + event_date.getDate() == date.getMonth() * 100 + date.getDate())
3092 return [true, new Date(date.getFullYear(), date.getMonth(), date.getDate() + 1)];
3093 else
3094 return [false, new Date(date.getFullYear() + 1, 0, 1)];
3095
3096 }}(tokens, at, nblock, has_event[0], has_year[0], has_calc[0]));
3097
3098 at += has_year[0] + has_event[0] + (typeof has_calc[0][1] != 'undefined' && has_calc[0][1] ? 3 : 0);
3099 } else if (has_constrained_weekday[0]) {
3100 at = parseMonthRange(tokens, at);
3101 } else if (matchTokens(tokens, at, 'month')) {
3102 return parseMonthRange(tokens, at);
3103 } else {
3104 // throw 'Unexpected token in monthday range: "' + tokens[at] + '"';
3105 return at;
3106 }
3107
3108 if (!matchTokens(tokens, at, ','))
3109 break;
3110 }
3111
3112 if (typeof used_subparsers['monthday ranges'] != 'number')
3113 used_subparsers['monhday ranges'] = 1;
3114 else
3115 used_subparsers['monhday ranges']++;
3116
3117 // console.log(tokens[at-1], 'return');
3118 return at;
3119 }
3120
3121 //======================================================================
3122 // Main selector traversal function
3123 //======================================================================
3124 this.getStatePair = function(date) {
3125 var resultstate = false;
3126 var changedate;
3127 var unknown = false;
3128 var comment = undefined;
3129 var match_block;
3130
3131 var date_matching_blocks = [];
3132
3133 for (var nblock = 0; nblock < blocks.length; nblock++) {
3134 var matching_date_block = true;
3135 // console.log(nblock, 'length', blocks[nblock].date.length);
3136
3137 // Try each date selector type
3138 for (var ndateselector = 0; ndateselector < blocks[nblock].date.length; ndateselector++) {
3139 var dateselectors = blocks[nblock].date[ndateselector];
3140 // console.log(nblock, ndateselector);
3141
3142 var has_matching_selector = false;
3143 for (var datesel = 0; datesel < dateselectors.length; datesel++) {
3144 var res = dateselectors[datesel](date);
3145 if (res[0]) {
3146 has_matching_selector = true;
3147
3148 if (typeof res[2] == 'string') { // holiday name
3149 comment = [ res[2] ];
3150 }
3151
3152 }
3153 if (typeof changedate === 'undefined' || (typeof res[1] !== 'undefined' && res[1].getTime() < changedate.getTime()))
3154 changedate = res[1];
3155 }
3156
3157 if (!has_matching_selector) {
3158 matching_date_block = false;
3159 // We can ignore other date selectors, as the state won't change
3160 // anyway until THIS selector matches (due to conjunction of date
3161 // selectors of different types).
3162 // This is also an optimization, if widest date selector types
3163 // are checked first.
3164 break;
3165 }
3166
3167 }
3168
3169 if (matching_date_block) {
3170 // The following lines implement date overwriting logic (e.g. for
3171 // "Mo-Fr 10:00-20:00; We 10:00-16:00", We block overrides Mo-Fr block.
3172 //
3173 // This is the only way to be consistent. I thought about ("22:00-02:00; Tu 12:00-14:00") letting Th override 22:00-02:00 partly:
3174 // Like: Th 00:00-02:00,12:00-14:00 but this would result in including 22:00-00:00 for Th which is probably not what you want.
3175 if (blocks[nblock].date.length > 0 && (blocks[nblock].meaning || blocks[nblock].unknown)
3176 && !blocks[nblock].wrapped && !blocks[nblock].additional && !blocks[nblock].fallback) {
3177 // var old_date_matching_blocks = date_matching_blocks;
3178 date_matching_blocks = [];
3179 // for (var nblock = 0; nblock < old_date_matching_blocks.length; nblock++) {
3180 // if (!blocks[old_date_matching_blocks[nblock]].wrapped)
3181 // date_matching_blocks.push(nblock);
3182 // }
3183 }
3184 date_matching_blocks.push(nblock);
3185 }
3186 }
3187
3188 block:
3189 for (var nblock = 0; nblock < date_matching_blocks.length; nblock++) {
3190 var block = date_matching_blocks[nblock];
3191
3192 // console.log('Processing block ' + block + ':\t' + blocks[block].comment + ' with date', date,
3193 // 'and', blocks[block].time.length, 'time selectors');
3194
3195 // there is no time specified, state applies to the whole day
3196 if (blocks[block].time.length == 0) {
3197 // console.log('there is no time', date);
3198 if (!blocks[block].fallback || (blocks[block].fallback && !(resultstate || unknown))) {
3199 resultstate = blocks[block].meaning;
3200 unknown = blocks[block].unknown;
3201 match_block = block;
3202
3203 if (typeof blocks[block].comment != 'undefined')
3204 comment = blocks[block].comment;
3205 else if (typeof comment == 'object') // holiday name
3206 comment = comment[0];
3207
3208 if (blocks[block].fallback)
3209 break block; // fallback block matched, no need for checking the rest
3210 }
3211 }
3212
3213 for (var timesel = 0; timesel < blocks[block].time.length; timesel++) {
3214 var res = blocks[block].time[timesel](date);
3215
3216 // console.log('res:', res);
3217 if (res[0]) {
3218 if (!blocks[block].fallback || (blocks[block].fallback && !(resultstate || unknown))) {
3219 resultstate = blocks[block].meaning;
3220 unknown = blocks[block].unknown;
3221 match_block = block;
3222
3223 if (typeof blocks[block].comment != 'undefined') // only use comment if one is specified
3224 comment = blocks[block].comment;
3225 else if (typeof comment == 'object') // holiday name
3226 comment = comment[0];
3227 else if (comment === 'Specified as open end. Closing time was guessed.')
3228 comment = blocks[block].comment;
3229
3230 // open end
3231 if (typeof res[2] == 'boolean' && res[2] && (resultstate || unknown)) {
3232 if (typeof comment == 'undefined')
3233 comment = 'Specified as open end. Closing time was guessed.';
3234 resultstate = false;
3235 unknown = true;
3236 }
3237
3238 if (blocks[block].fallback) {
3239 if (typeof changedate === 'undefined' || (typeof res[1] !== 'undefined' && res[1] < changedate))
3240 changedate = res[1];
3241
3242 break block; // fallback block matched, no need for checking the rest
3243 }
3244 }
3245 }
3246 if (typeof changedate === 'undefined' || (typeof res[1] !== 'undefined' && res[1] < changedate))
3247 changedate = res[1];
3248 }
3249 }
3250
3251 // console.log('changedate', changedate, resultstate, comment, match_block);
3252 return [ resultstate, changedate, unknown, comment, match_block ];
3253 }
3254
3255 function formatWarnErrorMessage(nblock, at, message) {
3256 var pos = 0;
3257 if (nblock == -1) { // usage of block index not required because we do have access to value.length
3258 pos = value.length - at;
3259 } else { // Issue accrued at a later time, position in string needs to be reconstructed.
3260 if (typeof tokens[nblock][0][at] == 'undefined') {
3261 pos = value.length;
3262 } else {
3263 pos = value.length;
3264 if (typeof tokens[nblock][0][at+1] != 'undefined')
3265 pos -= tokens[nblock][0][at+1][2];
3266 else if (typeof tokens[nblock][2] != 'undefined')
3267 pos -= tokens[nblock][2];
3268 }
3269 }
3270 return value.substring(0, pos) + ' <--- (' + message + ')';
3271 }
3272
3273 function prettifySelector(tokens, at, last_at, conf, used_parseTimeRange) {
3274 var value = '';
3275 var start_at = at;
3276 while (at < last_at) {
3277 if (matchTokens(tokens, at, 'weekday')) {
3278 if (!conf.leave_weekday_sep_one_day_betw
3279 && at - start_at > 1 && (matchTokens(tokens, at-1, ',') || matchTokens(tokens, at-1, '-'))
3280 && matchTokens(tokens, at-2, 'weekday')
3281 && tokens[at][0] == (tokens[at-2][0] + 1) % 7) {
3282 value = value.substring(0, value.length - 1) + conf.sep_one_day_between;
3283 }
3284 value += weekdays[tokens[at][0]];
3285 } else if (at - start_at > 0 && used_parseTimeRange > 0 && matchTokens(tokens, at-1, 'timesep')
3286 && matchTokens(tokens, at, 'number')) {
3287 value += (tokens[at][0] < 10 ? '0' : '') + tokens[at][0].toString();
3288 } else if (used_parseTimeRange > 0 && conf.leading_zero_hour && at != tokens.length
3289 && matchTokens(tokens, at+1, 'timesep')) {
3290 value += (tokens[at][0] < 10 ? (tokens[at][0] == 0 && conf.one_zero_if_hour_zero ? '' : '0') : '') + tokens[at][0].toString();
3291 } else if (matchTokens(tokens, at, 'comment')) {
3292 value += '"' + tokens[at][0].toString() + '"';
3293 } else if (matchTokens(tokens, at, 'closed')) {
3294 value += (conf.leave_off_closed ? tokens[at][0] : conf.keyword_for_off_closed);
3295 } else if (at - start_at > 0 && matchTokens(tokens, at, 'number')
3296 && (matchTokens(tokens, at-1, 'month')
3297 || matchTokens(tokens, at-1, 'week')
3298 )) {
3299 value += ' ' + tokens[at][0];
3300 } else if (at - start_at > 0 && matchTokens(tokens, at, 'month')
3301 && matchTokens(tokens, at-1, 'year')) {
3302 value += ' ' + months[[tokens[at][0]]];
3303 } else if (at - start_at > 0 && matchTokens(tokens, at, 'event')
3304 && matchTokens(tokens, at-1, 'year')) {
3305 value += ' ' + tokens[at][0];
3306 } else if (matchTokens(tokens, at, 'month')) {
3307 value += months[[tokens[at][0]]];
3308 if (at + 1 < last_at && matchTokens(tokens, at+1, 'weekday'))
3309 value += ' ';
3310 } else if (at + 2 < last_at
3311 && (matchTokens(tokens, at, '-') || matchTokens(tokens, at, '+'))
3312 && matchTokens(tokens, at+1, 'number', 'calcday')) {
3313 value += ' ' + tokens[at][0] + tokens[at+1][0] + ' day' + (Math.abs(tokens[at+1][0]) == 1 ? '' : 's');
3314 at += 2;
3315 } else {
3316 // if (matchTokens(tokens, at, 'open') || matchTokens(tokens, at, 'unknown'))
3317 // value += ' ';
3318
3319 value += tokens[at][0].toString();
3320 }
3321 at++;
3322 }
3323 return value + ' ';
3324 }
3325
3326 //======================================================================
3327 // Public interface
3328 // All functions below are considered public.
3329 //======================================================================
3330
3331 //======================================================================
3332 // Iterator interface
3333 //======================================================================
3334 this.getIterator = function(date) {
3335 return new function(oh) {
3336 if (typeof date === 'undefined')
3337 date = new Date();
3338
3339 var prevstate = [ undefined, date, undefined, undefined, undefined ];
3340 var state = oh.getStatePair(date);
3341
3342 this.setDate = function(date) {
3343 if (typeof date != 'object')
3344 throw 'Date as parameter needed.';
3345
3346 prevstate = [ undefined, date, undefined, undefined, undefined ];
3347 state = oh.getStatePair(date);
3348 }
3349
3350 this.getDate = function() {
3351 return prevstate[1];
3352 }
3353
3354 this.getState = function() {
3355 return state[0];
3356 }
3357
3358 this.getUnknown = function() {
3359 return state[2];
3360 }
3361
3362 this.getStateString = function(past) {
3363 return (state[0] ? 'open' : (state[2] ? 'unknown' : (past ? 'closed' : 'close')));
3364 }
3365
3366 this.getComment = function() {
3367 return state[3];
3368 }
3369
3370 this.getMatchingRule = function(user_conf) {
3371 if (typeof state[4] == 'undefined')
3372 return undefined;
3373
3374 if (typeof user_conf != 'object')
3375 var user_conf = {};
3376 for (key in default_prettify_conf) {
3377 if (typeof user_conf[key] != 'undefined')
3378 user_conf[key] = default_prettify_conf[key];
3379 }
3380
3381 var really_done_with_warnings = done_with_warnings; // getWarnings can be called later.
3382 done_with_warnings = true;
3383 prettified_value = '';
3384 var selectors = { // Not really needed. This whole thing is only necessary because of the token used for additional blocks.
3385 time: [], weekday: [], holiday: [], week: [], month: [], monthday: [], year: [], wraptime: [],
3386
3387 fallback: false, // does not matter
3388 additional: false,
3389 meaning: true,
3390 unknown: false,
3391 comment: undefined,
3392 };
3393
3394 parseGroup(tokens[state[4]][0], 0, selectors, state[4], user_conf);
3395
3396 done_with_warnings = really_done_with_warnings;
3397
3398 return prettified_value;
3399 }
3400
3401 this.advance = function(datelimit) {
3402 if (typeof datelimit === 'undefined')
3403 datelimit = new Date(prevstate[1].getTime() + msec_in_day * 366 * 5);
3404 else if (datelimit.getTime() <= prevstate[1].getTime())
3405 return false; // The limit for advance needs to be after the current time.
3406
3407 do {
3408 // open range, we won't be able to advance
3409 if (typeof state[1] === 'undefined')
3410 return false;
3411
3412 // console.log('\n' + 'previous check time:', prevstate[1]
3413 // + ', current check time:',
3414 // // (state[1].getHours() < 10 ? '0' : '') + state[1].getHours() +
3415 // // ':'+(state[1].getMinutes() < 10 ? '0' : '')+ state[1].getMinutes(), state[1].getDate(),
3416 // state[1],
3417 // (state[0] ? 'open' : (state[2] ? 'unknown' : 'closed')) + ', comment:', state[3]);
3418
3419 // We're going backwards or staying at place.
3420 // This always indicates coding error in a selector code.
3421 if (state[1].getTime() <= prevstate[1].getTime())
3422 throw 'Fatal: infinite loop in nextChange';
3423
3424 // don't advance beyond limits (same as open range)
3425 if (state[1].getTime() >= datelimit.getTime())
3426 return false;
3427
3428 // do advance
3429 prevstate = state;
3430 state = oh.getStatePair(prevstate[1]);
3431 } while (state[0] === prevstate[0] && state[2] === prevstate[2] && state[3] === prevstate[3]);
3432 return true;
3433 }
3434 }(this);
3435 }
3436
3437 // get parse warnings
3438 // returns an empty string if there are no warnings
3439 this.getWarnings = function() {
3440 var it = this.getIterator();
3441 return getWarnings(it);
3442 }
3443
3444 // get a nicely formated value.
3445 this.prettifyValue = function(user_conf) {
3446 if (typeof user_conf != 'object')
3447 var user_conf = {};
3448
3449 for (key in default_prettify_conf) {
3450 if (typeof user_conf[key] == 'undefined')
3451 user_conf[key] = default_prettify_conf[key];
3452 }
3453
3454 var really_done_with_warnings = done_with_warnings; // getWarnings can be called later.
3455 done_with_warnings = true;
3456
3457 prettified_value = '';
3458 for (var nblock = 0; nblock < tokens.length; nblock++) {
3459 if (tokens[nblock][0].length == 0) continue;
3460 // Block does contain nothing useful e.g. second block of '10:00-12:00;' (empty) which needs to be handled.
3461
3462 if (nblock != 0)
3463 prettified_value += (tokens[nblock][1]
3464 ? user_conf.block_sep_string + '|| '
3465 : (user_conf.print_semicolon ? ';' : '') + user_conf.block_sep_string);
3466
3467 var continue_at = 0;
3468 do {
3469 if (continue_at == tokens[nblock][0].length) break;
3470 // Block does contain nothing useful e.g. second block of '10:00-12:00,' (empty) which needs to be handled.
3471
3472 var selectors = { // Not really needed. This whole thing is only necessary because of the token used for additional blocks.
3473 time: [], weekday: [], holiday: [], week: [], month: [], monthday: [], year: [], wraptime: [],
3474
3475 fallback: tokens[nblock][1],
3476 additional: continue_at ? true : false,
3477 meaning: true,
3478 unknown: false,
3479 comment: undefined,
3480 };
3481
3482 continue_at = parseGroup(tokens[nblock][0], continue_at, selectors, nblock, user_conf);
3483
3484 if (typeof continue_at == 'object') {
3485 continue_at = continue_at[0];
3486 prettified_value += user_conf.block_sep_string;
3487 } else {
3488 continue_at = 0;
3489 }
3490
3491 } while (continue_at)
3492 }
3493
3494 done_with_warnings = really_done_with_warnings;
3495
3496 return prettified_value;
3497 }
3498
3499 // check whether facility is `open' on the given date (or now)
3500 this.getState = function(date) {
3501 var it = this.getIterator(date);
3502 return it.getState();
3503 }
3504
3505 // If the state of a amenity is conditional. Conditions can be expressed in comments.
3506 // True will only be returned if the state is false as the getState only
3507 // returns true if the amenity is really open. So you may want to check
3508 // the resold of getUnknown if getState returned false.
3509 this.getUnknown = function(date) {
3510 var it = this.getIterator(date);
3511 return it.getUnknown();
3512 }
3513
3514 // Return state string. Either 'open', 'unknown' or 'closed'.
3515 this.getStateString = function(date, past) {
3516 var it = this.getIterator(date);
3517 return it.getStateString(past);
3518 }
3519
3520 // Returns the comment.
3521 // Most often this will be an empty string as comments are not used that
3522 // often in OSM yet.
3523 this.getComment = function(date) {
3524 var it = this.getIterator(date);
3525 return it.getComment();
3526 }
3527
3528 this.getMatchingRule = function(date) {
3529 var it = this.getIterator(date);
3530 return it.getMatchingRule();
3531 }
3532
3533 // returns time of next status change
3534 this.getNextChange = function(date, maxdate) {
3535 var it = this.getIterator(date);
3536 if (!it.advance(maxdate))
3537 return undefined;
3538 return it.getDate();
3539 }
3540
3541 // return array of open intervals between two dates
3542 this.getOpenIntervals = function(from, to) {
3543 var res = [];
3544
3545 var it = this.getIterator(from);
3546
3547 if (it.getState() || it.getUnknown())
3548 res.push([from, undefined, it.getUnknown(), it.getComment()]);
3549
3550 while (it.advance(to)) {
3551 if (it.getState() || it.getUnknown()) {
3552 if (res.length != 0 && typeof res[res.length - 1][1] == 'undefined') {
3553 // last state was also open or unknown
3554 res[res.length - 1][1] = it.getDate();
3555 }
3556 res.push([it.getDate(), undefined, it.getUnknown(), it.getComment()]);
3557 } else {
3558 if (res.length != 0 && typeof res[res.length - 1][1] == 'undefined') {
3559 // only use the first time as closing/change time and ignore closing times which might follow
3560 res[res.length - 1][1] = it.getDate();
3561 }
3562 }
3563 }
3564
3565 if (res.length > 0 && typeof res[res.length - 1][1] === 'undefined')
3566 res[res.length - 1][1] = to;
3567
3568 return res;
3569 }
3570
3571 // return total number of milliseconds a facility is open within a given date range
3572 this.getOpenDuration = function(from, to) {
3573 // console.log('-----------');
3574
3575 var open = 0;
3576 var unknown = 0;
3577
3578 var it = this.getIterator(from);
3579 var prevdate = (it.getState() || it.getUnknown()) ? from : undefined;
3580 var prevstate = it.getState();
3581 var prevunknown = it.getUnknown();
3582
3583 while (it.advance(to)) {
3584 if (it.getState() || it.getUnknown()) {
3585
3586 if (typeof prevdate !== 'undefined') {
3587 // last state was also open or unknown
3588 if (prevunknown) //
3589 unknown += it.getDate().getTime() - prevdate.getTime();
3590 else if (prevstate)
3591 open += it.getDate().getTime() - prevdate.getTime();
3592 }
3593
3594 prevdate = it.getDate();
3595 prevstate = it.getState();
3596 prevunknown = it.getUnknown();
3597 // console.log('if', prevdate, open / (1000 * 60 * 60), unknown / (1000 * 60 * 60));
3598 } else {
3599 // console.log('else', prevdate);
3600 if (typeof prevdate !== 'undefined') {
3601 if (prevunknown)
3602 unknown += it.getDate().getTime() - prevdate.getTime();
3603 else
3604 open += it.getDate().getTime() - prevdate.getTime();
3605 prevdate = undefined;
3606 }
3607 }
3608 }
3609
3610 if (typeof prevdate !== 'undefined') {
3611 if (prevunknown)
3612 unknown += to.getTime() - prevdate.getTime();
3613 else
3614 open += to.getTime() - prevdate.getTime();
3615 }
3616
3617 return [ open, unknown ];
3618 }
3619
3620 this.isWeekStable = function() {
3621 return week_stable;
3622 }
3623 }
3624}));
Note: See TracBrowser for help on using the repository browser.