source: josm/trunk/data/validator/opening_hours.js@ 7767

Last change on this file since 7767 was 7537, checked in by Don-vip, 10 years ago

see #10513 - opening_hours: replace prototype definition by standard function as a workaround to Java 7 / Webstart incompatibility

File size: 191.5 KB
Line 
1/*
2 * For information see https://github.com/ypid/opening_hours.js
3 * and the doc directory which contains internal documentation and design.
4 */
5/* jshint laxbreak: true */
6/* jshint boss: true */
7/* jshint loopfunc: true */
8
9(function (root, factory) {
10 // constants (holidays, error correction) {{{
11 // holidays {{{
12 var holidays = {
13 'fr': { // {{{
14 'PH': { // http://fr.wikipedia.org/wiki/F%C3%AAtes_et_jours_f%C3%A9ri%C3%A9s_en_France
15 "Jour de l'an" : [ 1, 1 ],
16 "Vendredi saint" : [ 'easter', -2, [ 'Moselle', 'Bas-Rhin', 'Haut-Rhin', 'Guadeloupe', 'Martinique', 'Polynésie française' ] ],
17 "Lundi de Pâques" : [ 'easter', 1 ],
18 "Saint-Pierre-Chanel" : [ 4, 28, [ 'Wallis-et-Futuna' ] ],
19 "Fête du Travail" : [ 5, 1 ],
20 "Fête de la Victoire" : [ 5, 8 ],
21 "Abolition de l'esclavage" : [ 5, 22, [ 'Martinique' ] ],
22 "Abolition de l'esclavage" : [ 5, 27, [ 'Guadeloupe' ] ],
23 "Jeudi de l'Ascension" : [ 'easter', 39 ],
24 "Lundi de Pentecôte" : [ 'easter', 50 ],
25 "Abolition de l'esclavage" : [ 6, 10, [ 'Guyane' ] ],
26 "Fête de l'autonomie" : [ 6, 29, [ 'Polynésie française' ] ],
27 "Fête nationale" : [ 7, 14 ],
28 "Fête Victor Schoelcher" : [ 7, 21, [ 'Guadeloupe', 'Martinique' ] ],
29 "Fête du Territoire" : [ 7, 29, [ 'Wallis-et-Futuna' ] ],
30 "Assomption" : [ 8, 15 ],
31 "Fête de la citoyenneté" : [ 9, 24, [ 'Nouvelle-Calédonie' ] ],
32 "Toussaint" : [ 11, 1 ],
33 "Armistice" : [ 11, 11 ],
34 "Abolition de l'esclavage" : [ 12, 20, [ 'Réunion' ] ],
35 "Noël" : [ 12, 25 ],
36 "Saint-Étienne " : [ 12, 26, [ 'Moselle', 'Bas-Rhin', 'Haut-Rhin' ] ]
37 }
38 }, // }}}
39 'de': { // {{{
40 'PH': { // http://de.wikipedia.org/wiki/Feiertage_in_Deutschland
41 'Neujahrstag' : [ 1, 1 ], // month 1, day 1, whole Germany
42 'Heilige Drei Könige' : [ 1, 6, [ 'Baden-Württemberg', 'Bayern', 'Sachsen-Anhalt'] ], // only in the specified states
43 'Tag der Arbeit' : [ 5, 1 ], // whole Germany
44 'Karfreitag' : [ 'easter', -2 ], // two days before easter
45 'Ostersonntag' : [ 'easter', 0, [ 'Brandenburg'] ],
46 'Ostermontag' : [ 'easter', 1 ],
47 'Christi Himmelfahrt' : [ 'easter', 39 ],
48 'Pfingstsonntag' : [ 'easter', 49, [ 'Brandenburg'] ],
49 'Pfingstmontag' : [ 'easter', 50 ],
50 'Fronleichnam' : [ 'easter', 60, [ 'Baden-Württemberg', 'Bayern', 'Hessen', 'Nordrhein-Westfalen', 'Rheinland-Pfalz', 'Saarland' ] ],
51 'Mariä Himmelfahrt' : [ 8, 15, [ 'Saarland'] ],
52 'Tag der Deutschen Einheit' : [ 10, 3 ],
53 'Reformationstag' : [ 10, 31, [ 'Brandenburg', 'Mecklenburg-Vorpommern', 'Sachsen', 'Sachsen-Anhalt', 'Thüringen'] ],
54 'Allerheiligen' : [ 11, 1, [ 'Baden-Württemberg', 'Bayern', 'Nordrhein-Westfalen', 'Rheinland-Pfalz', 'Saarland' ] ],
55 '1. Weihnachtstag' : [ 12, 25 ],
56 '2. Weihnachtstag' : [ 12, 26 ],
57 // 'Silvester' : [ 12, 31 ], // for testing
58 },
59 'Baden-Württemberg': { // does only apply in Baden-Württemberg
60 // This more specific rule set overwrites the country wide one (they are just ignored).
61 // You may use this instead of the country wide with some
62 // additional holidays for some states, if one state
63 // totally disagrees about how to do holidays …
64 // 'PH': {
65 // '2. Weihnachtstag' : [ 12, 26 ],
66 // },
67
68 // school holiday normally variate between states
69 'SH': [ // generated by convert_ical_to_json
70 // You may can adjust this script to use other resources (for other countries) too.
71 {
72 name: 'Osterferien',
73 2005: [ 3, 24, /* to */ 3, 24, 3, 29, /* to */ 4, 2 ],
74 2006: [ 4, 18, /* to */ 4, 22 ],
75 2007: [ 4, 2, /* to */ 4, 14 ],
76 2008: [ 3, 17, /* to */ 3, 28 ],
77 2009: [ 4, 9, /* to */ 4, 9, 4, 14, /* to */ 4, 17 ],
78 2010: [ 4, 1, /* to */ 4, 1, 4, 6, /* to */ 4, 10 ],
79 2011: [ 4, 21, /* to */ 4, 21, 4, 26, /* to */ 4, 30 ],
80 2012: [ 4, 2, /* to */ 4, 13 ],
81 2013: [ 3, 25, /* to */ 4, 5 ],
82 2014: [ 4, 14, /* to */ 4, 25 ],
83 2015: [ 3, 30, /* to */ 4, 10 ],
84 2016: [ 3, 29, /* to */ 4, 2 ],
85 2017: [ 4, 10, /* to */ 4, 21 ],
86 },
87 {
88 name: 'Pfingstferien',
89 2005: [ 5, 17, /* to */ 5, 28 ],
90 2006: [ 5, 29, /* to */ 6, 10 ],
91 2007: [ 5, 29, /* to */ 6, 9 ],
92 2008: [ 5, 13, /* to */ 5, 23 ],
93 2009: [ 5, 25, /* to */ 6, 6 ],
94 2010: [ 5, 25, /* to */ 6, 5 ],
95 2011: [ 6, 14, /* to */ 6, 25 ],
96 2012: [ 5, 29, /* to */ 6, 9 ],
97 2013: [ 5, 21, /* to */ 6, 1 ],
98 2014: [ 6, 10, /* to */ 6, 21 ],
99 2015: [ 5, 26, /* to */ 6, 6 ],
100 2016: [ 5, 17, /* to */ 5, 28 ],
101 2017: [ 6, 6, /* to */ 6, 16 ],
102 },
103 {
104 name: 'Sommerferien',
105 2005: [ 7, 28, /* to */ 9, 10 ],
106 2006: [ 8, 3, /* to */ 9, 16 ],
107 2007: [ 7, 26, /* to */ 9, 8 ],
108 2008: [ 7, 24, /* to */ 9, 6 ],
109 2009: [ 7, 30, /* to */ 9, 12 ],
110 2010: [ 7, 29, /* to */ 9, 11 ],
111 2011: [ 7, 28, /* to */ 9, 10 ],
112 2012: [ 7, 26, /* to */ 9, 8 ],
113 2013: [ 7, 25, /* to */ 9, 7 ],
114 2014: [ 7, 31, /* to */ 9, 13 ],
115 2015: [ 7, 30, /* to */ 9, 12 ],
116 2016: [ 7, 28, /* to */ 9, 10 ],
117 2017: [ 7, 27, /* to */ 9, 9 ],
118 },
119 {
120 name: 'Herbstferien',
121 2005: [ 11, 2, /* to */ 11, 4 ],
122 2006: [ 10, 30, /* to */ 11, 3 ],
123 2007: [ 10, 29, /* to */ 11, 3 ],
124 2008: [ 10, 27, /* to */ 10, 31 ],
125 2009: [ 10, 26, /* to */ 10, 31 ],
126 2010: [ 11, 2, /* to */ 11, 6 ],
127 2011: [ 10, 31, /* to */ 10, 31, 11, 2, /* to */ 11, 4 ],
128 2012: [ 10, 29, /* to */ 11, 2 ],
129 2013: [ 10, 28, /* to */ 10, 30 ],
130 2014: [ 10, 27, /* to */ 10, 30 ],
131 2015: [ 11, 2, /* to */ 11, 6 ],
132 2016: [ 11, 2, /* to */ 11, 4 ],
133 },
134 {
135 name: 'Weihnachtsferien',
136 2005: [ 12, 22, /* to */ 1, 5 ],
137 2006: [ 12, 27, /* to */ 1, 5 ],
138 2007: [ 12, 24, /* to */ 1, 5 ],
139 2008: [ 12, 22, /* to */ 1, 10 ],
140 2009: [ 12, 23, /* to */ 1, 9 ],
141 2010: [ 12, 23, /* to */ 1, 8 ],
142 2011: [ 12, 23, /* to */ 1, 5 ],
143 2012: [ 12, 24, /* to */ 1, 5 ],
144 2013: [ 12, 23, /* to */ 1, 4 ],
145 2014: [ 12, 22, /* to */ 1, 5 ],
146 2015: [ 12, 23, /* to */ 1, 9 ],
147 2016: [ 12, 23, /* to */ 1, 7 ],
148 },
149 ],
150 },
151 'Mecklenburg-Vorpommern': {
152 'SH': [
153 {
154 name: 'Winterferien',
155 2010: [ 2, 6, /* to */ 2, 20 ],
156 2011: [ 2, 7, /* to */ 2, 19 ],
157 2012: [ 2, 6, /* to */ 2, 17 ],
158 2013: [ 2, 4, /* to */ 2, 15 ],
159 2014: [ 2, 3, /* to */ 2, 15 ],
160 2015: [ 2, 2, /* to */ 2, 14 ],
161 2016: [ 2, 1, /* to */ 2, 13 ],
162 2017: [ 2, 6, /* to */ 2, 18 ],
163 },
164 {
165 name: 'Osterferien',
166 2010: [ 3, 29, /* to */ 4, 7 ],
167 2011: [ 4, 16, /* to */ 4, 27 ],
168 2012: [ 4, 2, /* to */ 4, 11 ],
169 2013: [ 3, 25, /* to */ 4, 3 ],
170 2014: [ 4, 14, /* to */ 4, 23 ],
171 2015: [ 3, 30, /* to */ 4, 8 ],
172 2016: [ 3, 21, /* to */ 3, 30 ],
173 2017: [ 4, 10, /* to */ 4, 19 ],
174 },
175 {
176 name: 'Pfingstferien',
177 2010: [ 5, 21, /* to */ 5, 22 ],
178 2011: [ 6, 10, /* to */ 6, 14 ],
179 2012: [ 5, 25, /* to */ 5, 29 ],
180 2013: [ 5, 17, /* to */ 5, 21 ],
181 2014: [ 6, 6, /* to */ 6, 10 ],
182 2015: [ 5, 22, /* to */ 5, 26 ],
183 2016: [ 5, 14, /* to */ 5, 17 ],
184 2017: [ 6, 2, /* to */ 6, 6 ],
185 },
186 {
187 name: 'Sommerferien',
188 2010: [ 7, 12, /* to */ 8, 21 ],
189 2011: [ 7, 4, /* to */ 8, 13 ],
190 2012: [ 6, 23, /* to */ 8, 4 ],
191 2013: [ 6, 22, /* to */ 8, 3 ],
192 2014: [ 7, 14, /* to */ 8, 23 ],
193 2015: [ 7, 20, /* to */ 8, 29 ],
194 2016: [ 7, 25, /* to */ 9, 3 ],
195 2017: [ 7, 24, /* to */ 9, 2 ],
196 },
197 {
198 name: 'Herbstferien',
199 2010: [ 10, 18, /* to */ 10, 23 ],
200 2011: [ 10, 17, /* to */ 10, 21 ],
201 2012: [ 10, 1, /* to */ 10, 5 ],
202 2013: [ 10, 14, /* to */ 10, 19 ],
203 2014: [ 10, 20, /* to */ 10, 25 ],
204 2015: [ 10, 24, /* to */ 10, 30 ],
205 2016: [ 10, 24, /* to */ 10, 28 ],
206 },
207 {
208 name: 'Weihnachtsferien',
209 2010: [ 12, 23, /* to */ 12, 31 ],
210 2011: [ 12, 23, /* to */ 1, 3 ],
211 2012: [ 12, 21, /* to */ 1, 4 ],
212 2013: [ 12, 23, /* to */ 1, 3 ],
213 2014: [ 12, 22, /* to */ 1, 2 ],
214 2015: [ 12, 21, /* to */ 1, 2 ],
215 2016: [ 12, 22, /* to */ 1, 2 ],
216 },
217 ],
218 },
219 'Hessen': {
220 'SH': [
221 {
222 name: 'Osterferien',
223 2010: [ 3, 29, /* to */ 4, 10 ],
224 2011: [ 4, 18, /* to */ 4, 30 ],
225 2012: [ 4, 2, /* to */ 4, 14 ],
226 2013: [ 3, 25, /* to */ 4, 6 ],
227 2014: [ 4, 14, /* to */ 4, 26 ],
228 2015: [ 3, 30, /* to */ 4, 11 ],
229 2016: [ 3, 29, /* to */ 4, 9 ],
230 2017: [ 4, 3, /* to */ 4, 15 ],
231 2018: [ 3, 26, /* to */ 4, 7 ],
232 },
233 {
234 name: 'Sommerferien',
235 2010: [ 7, 5, /* to */ 8, 14 ],
236 2011: [ 6, 27, /* to */ 8, 5 ],
237 2012: [ 7, 2, /* to */ 8, 10 ],
238 2013: [ 7, 8, /* to */ 8, 16 ],
239 2014: [ 7, 28, /* to */ 9, 5 ],
240 2015: [ 7, 27, /* to */ 9, 5 ],
241 2016: [ 7, 18, /* to */ 8, 26 ],
242 2017: [ 7, 3, /* to */ 8, 11 ],
243 },
244 {
245 name: 'Herbstferien',
246 2010: [ 10, 11, /* to */ 10, 22 ],
247 2011: [ 10, 10, /* to */ 10, 22 ],
248 2012: [ 10, 15, /* to */ 10, 27 ],
249 2013: [ 10, 14, /* to */ 10, 26 ],
250 2014: [ 10, 20, /* to */ 11, 1 ],
251 2015: [ 10, 19, /* to */ 10, 31 ],
252 2016: [ 10, 17, /* to */ 10, 29 ],
253 2017: [ 10, 9, /* to */ 10, 21 ],
254 },
255 {
256 name: 'Weihnachtsferien',
257 2010: [ 12, 20, /* to */ 1, 7 ],
258 2011: [ 12, 21, /* to */ 1, 6 ],
259 2012: [ 12, 24, /* to */ 1, 12 ],
260 2013: [ 12, 23, /* to */ 1, 11 ],
261 2014: [ 12, 22, /* to */ 1, 10 ],
262 2015: [ 12, 23, /* to */ 1, 9 ],
263 2016: [ 12, 22, /* to */ 1, 7 ],
264 2017: [ 12, 24, /* to */ 1, 13 ],
265 },
266 ],
267 },
268 'Schleswig-Holstein': {
269 'SH': [
270 {
271 name: 'Osterferien',
272 2010: [ 4, 3, /* to */ 4, 17 ],
273 2011: [ 4, 15, /* to */ 4, 30 ],
274 2012: [ 3, 30, /* to */ 4, 13 ],
275 2013: [ 3, 25, /* to */ 4, 9 ],
276 2014: [ 4, 16, /* to */ 5, 2 ],
277 2015: [ 4, 1, /* to */ 4, 17 ],
278 2016: [ 3, 24, /* to */ 4, 9 ],
279 2017: [ 4, 7, /* to */ 4, 21 ],
280 },
281 {
282 name: 'Sommerferien',
283 2010: [ 7, 12, /* to */ 8, 21 ],
284 2011: [ 7, 4, /* to */ 8, 13 ],
285 2012: [ 6, 25, /* to */ 8, 4 ],
286 2013: [ 6, 24, /* to */ 8, 3 ],
287 2014: [ 7, 14, /* to */ 8, 23 ],
288 2015: [ 7, 20, /* to */ 8, 29 ],
289 2016: [ 7, 25, /* to */ 9, 3 ],
290 2017: [ 7, 24, /* to */ 9, 2 ],
291 },
292 {
293 name: 'Pfingstferien',
294 2011: [ 6, 3, /* to */ 6, 4 ],
295 2012: [ 5, 18, /* to */ 5, 18 ],
296 2013: [ 5, 10, /* to */ 5, 10 ],
297 2014: [ 5, 30, /* to */ 5, 30 ],
298 2015: [ 5, 15, /* to */ 5, 15 ],
299 2016: [ 5, 6, /* to */ 5, 6 ],
300 2017: [ 5, 26, /* to */ 5, 26 ],
301 },
302 {
303 name: 'Herbstferien',
304 2010: [ 10, 11, /* to */ 10, 23 ],
305 2011: [ 10, 10, /* to */ 10, 22 ],
306 2012: [ 10, 4, /* to */ 10, 19 ],
307 2013: [ 10, 4, /* to */ 10, 18 ],
308 2014: [ 10, 13, /* to */ 10, 25 ],
309 2015: [ 10, 19, /* to */ 10, 31 ],
310 2016: [ 10, 17, /* to */ 10, 29 ],
311 },
312 {
313 name: 'Weihnachtsferien',
314 2010: [ 12, 23, /* to */ 1, 7 ],
315 2011: [ 12, 23, /* to */ 1, 6 ],
316 2012: [ 12, 24, /* to */ 1, 5 ],
317 2013: [ 12, 23, /* to */ 1, 6 ],
318 2014: [ 12, 22, /* to */ 1, 6 ],
319 2015: [ 12, 21, /* to */ 1, 6 ],
320 2016: [ 12, 23, /* to */ 1, 6 ],
321 },
322 ],
323 },
324 'Berlin': {
325 'SH': [
326 {
327 name: 'Winterferien',
328 2010: [ 2, 1, /* to */ 2, 6 ],
329 2011: [ 1, 31, /* to */ 2, 5 ],
330 2012: [ 1, 30, /* to */ 2, 4 ],
331 2013: [ 2, 4, /* to */ 2, 9 ],
332 2014: [ 2, 3, /* to */ 2, 8 ],
333 2015: [ 2, 2, /* to */ 2, 7 ],
334 2016: [ 2, 1, /* to */ 2, 6 ],
335 2017: [ 1, 30, /* to */ 2, 4 ],
336 },
337 {
338 name: 'Osterferien',
339 2010: [ 3, 31, /* to */ 4, 10 ],
340 2011: [ 4, 18, /* to */ 4, 30 ],
341 2012: [ 4, 2, /* to */ 4, 14, 4, 30, /* to */ 4, 30 ],
342 2013: [ 3, 25, /* to */ 4, 6 ],
343 2014: [ 4, 14, /* to */ 4, 26, 5, 2, /* to */ 5, 2 ],
344 2015: [ 3, 30, /* to */ 4, 11 ],
345 2016: [ 3, 21, /* to */ 4, 2 ],
346 2017: [ 4, 10, /* to */ 4, 22 ],
347 },
348 {
349 name: 'Pfingstferien',
350 2010: [ 5, 14, /* to */ 5, 14, 5, 25, /* to */ 5, 25 ],
351 2011: [ 6, 3, /* to */ 6, 3 ],
352 2012: [ 5, 18, /* to */ 5, 18 ],
353 2013: [ 5, 10, /* to */ 5, 10, 5, 21, /* to */ 5, 21 ],
354 2014: [ 5, 30, /* to */ 5, 30 ],
355 2015: [ 5, 15, /* to */ 5, 15 ],
356 2016: [ 5, 6, /* to */ 5, 6, 5, 17, /* to */ 5, 17 ],
357 2017: [ 5, 26, /* to */ 5, 26 ],
358 },
359 {
360 name: 'Sommerferien',
361 2010: [ 7, 7, /* to */ 8, 21 ],
362 2011: [ 6, 29, /* to */ 8, 12 ],
363 2012: [ 6, 20, /* to */ 8, 3 ],
364 2013: [ 6, 19, /* to */ 8, 2 ],
365 2014: [ 7, 9, /* to */ 8, 22 ],
366 2015: [ 7, 15, /* to */ 8, 28 ],
367 2016: [ 7, 20, /* to */ 9, 2 ],
368 2017: [ 7, 19, /* to */ 9, 1 ],
369 },
370 {
371 name: 'Herbstferien',
372 2010: [ 10, 11, /* to */ 10, 23 ],
373 2011: [ 10, 4, /* to */ 10, 14 ],
374 2012: [ 10, 1, /* to */ 10, 13 ],
375 2013: [ 9, 30, /* to */ 10, 12 ],
376 2014: [ 10, 20, /* to */ 11, 1 ],
377 2015: [ 10, 19, /* to */ 10, 31 ],
378 2016: [ 10, 17, /* to */ 10, 28 ],
379 },
380 {
381 name: 'Weihnachtsferien',
382 2010: [ 12, 23, /* to */ 1, 1 ],
383 2011: [ 12, 23, /* to */ 1, 3 ],
384 2012: [ 12, 24, /* to */ 1, 4 ],
385 2013: [ 12, 23, /* to */ 1, 3 ],
386 2014: [ 12, 22, /* to */ 1, 2 ],
387 2015: [ 12, 23, /* to */ 1, 2 ],
388 2016: [ 12, 23, /* to */ 1, 3 ],
389 },
390 ],
391 },
392 'Saarland': {
393 'SH': [
394 {
395 name: 'Winterferien',
396 2010: [ 2, 15, /* to */ 2, 20 ],
397 2011: [ 3, 7, /* to */ 3, 12 ],
398 2012: [ 2, 20, /* to */ 2, 25 ],
399 2013: [ 2, 11, /* to */ 2, 16 ],
400 2014: [ 3, 3, /* to */ 3, 8 ],
401 2015: [ 2, 16, /* to */ 2, 21 ],
402 },
403 {
404 name: 'Osterferien',
405 2010: [ 3, 29, /* to */ 4, 10 ],
406 2011: [ 4, 18, /* to */ 4, 30 ],
407 2012: [ 4, 2, /* to */ 4, 14 ],
408 2013: [ 3, 25, /* to */ 4, 6 ],
409 2014: [ 4, 14, /* to */ 4, 26 ],
410 2015: [ 3, 30, /* to */ 4, 11 ],
411 },
412 {
413 name: 'Sommerferien',
414 2010: [ 7, 5, /* to */ 8, 14 ],
415 2011: [ 6, 24, /* to */ 8, 6 ],
416 2012: [ 7, 2, /* to */ 8, 14 ],
417 2013: [ 7, 8, /* to */ 8, 17 ],
418 2014: [ 7, 28, /* to */ 9, 6 ],
419 2015: [ 7, 27, /* to */ 9, 4 ],
420 2016: [ 7, 18, /* to */ 8, 26 ],
421 2017: [ 7, 3, /* to */ 8, 14 ],
422 },
423 {
424 name: 'Herbstferien',
425 2010: [ 10, 11, /* to */ 10, 23 ],
426 2011: [ 10, 4, /* to */ 10, 15 ],
427 2012: [ 10, 22, /* to */ 11, 3 ],
428 2013: [ 10, 21, /* to */ 11, 2 ],
429 2014: [ 10, 20, /* to */ 10, 31 ],
430 },
431 {
432 name: 'Weihnachtsferien',
433 2010: [ 12, 20, /* to */ 1, 1 ],
434 2011: [ 12, 23, /* to */ 1, 4 ],
435 2012: [ 12, 24, /* to */ 1, 5 ],
436 2013: [ 12, 20, /* to */ 1, 4 ],
437 2014: [ 12, 22, /* to */ 1, 7 ],
438 },
439 ],
440 },
441 'Bremen': {
442 'SH': [
443 {
444 name: 'Winterferien',
445 2010: [ 2, 1, /* to */ 2, 2 ],
446 2011: [ 1, 31, /* to */ 2, 1 ],
447 2012: [ 1, 30, /* to */ 1, 31 ],
448 2013: [ 1, 31, /* to */ 2, 1 ],
449 2014: [ 1, 30, /* to */ 1, 31 ],
450 2015: [ 2, 2, /* to */ 2, 3 ],
451 2016: [ 1, 28, /* to */ 1, 29 ],
452 2017: [ 1, 30, /* to */ 1, 31 ],
453 },
454 {
455 name: 'Osterferien',
456 2010: [ 3, 19, /* to */ 4, 6 ],
457 2011: [ 4, 16, /* to */ 4, 30 ],
458 2012: [ 3, 26, /* to */ 4, 11, 4, 30, /* to */ 4, 30 ],
459 2013: [ 3, 16, /* to */ 4, 2 ],
460 2014: [ 4, 3, /* to */ 4, 22, 5, 2, /* to */ 5, 2 ],
461 2015: [ 3, 25, /* to */ 4, 10 ],
462 2016: [ 3, 18, /* to */ 4, 2 ],
463 2017: [ 4, 10, /* to */ 4, 22 ],
464 },
465 {
466 name: 'Pfingstferien',
467 2010: [ 5, 14, /* to */ 5, 14, 5, 25, /* to */ 5, 25 ],
468 2011: [ 6, 3, /* to */ 6, 3, 6, 14, /* to */ 6, 14 ],
469 2012: [ 5, 18, /* to */ 5, 18, 5, 29, /* to */ 5, 29 ],
470 2013: [ 5, 10, /* to */ 5, 10, 5, 21, /* to */ 5, 21 ],
471 2014: [ 5, 30, /* to */ 5, 30, 6, 10, /* to */ 6, 10 ],
472 2015: [ 5, 15, /* to */ 5, 15, 5, 26, /* to */ 5, 26 ],
473 2016: [ 5, 6, /* to */ 5, 6, 5, 17, /* to */ 5, 17 ],
474 2017: [ 5, 26, /* to */ 5, 26, 6, 6, /* to */ 6, 6 ],
475 },
476 {
477 name: 'Sommerferien',
478 2010: [ 6, 24, /* to */ 8, 4 ],
479 2011: [ 7, 7, /* to */ 8, 17 ],
480 2012: [ 7, 23, /* to */ 8, 31 ],
481 2013: [ 6, 27, /* to */ 8, 7 ],
482 2014: [ 7, 31, /* to */ 9, 10 ],
483 2015: [ 7, 23, /* to */ 9, 2 ],
484 2016: [ 6, 23, /* to */ 8, 3 ],
485 2017: [ 6, 22, /* to */ 8, 2 ],
486 },
487 {
488 name: 'Herbstferien',
489 2010: [ 10, 9, /* to */ 10, 23 ],
490 2011: [ 10, 17, /* to */ 10, 29 ],
491 2012: [ 10, 22, /* to */ 11, 3 ],
492 2013: [ 10, 4, /* to */ 10, 18 ],
493 2014: [ 10, 27, /* to */ 11, 8 ],
494 2015: [ 10, 19, /* to */ 10, 31 ],
495 2016: [ 10, 4, /* to */ 10, 15 ],
496 },
497 {
498 name: 'Weihnachtsferien',
499 2010: [ 12, 22, /* to */ 1, 5 ],
500 2011: [ 12, 23, /* to */ 1, 4 ],
501 2012: [ 12, 24, /* to */ 1, 5 ],
502 2013: [ 12, 23, /* to */ 1, 3 ],
503 2014: [ 12, 22, /* to */ 1, 5 ],
504 2015: [ 12, 23, /* to */ 1, 6 ],
505 2016: [ 12, 21, /* to */ 1, 6 ],
506 },
507 ],
508 },
509 'Bayern': {
510 'SH': [
511 {
512 name: 'Winterferien',
513 2010: [ 2, 15, /* to */ 2, 20 ],
514 2011: [ 3, 7, /* to */ 3, 11 ],
515 2012: [ 2, 20, /* to */ 2, 24 ],
516 2013: [ 2, 11, /* to */ 2, 15 ],
517 2014: [ 3, 3, /* to */ 3, 7 ],
518 2015: [ 2, 16, /* to */ 2, 20 ],
519 2016: [ 2, 8, /* to */ 2, 12 ],
520 2017: [ 2, 27, /* to */ 3, 3 ],
521 },
522 {
523 name: 'Osterferien',
524 2010: [ 3, 29, /* to */ 4, 10 ],
525 2011: [ 4, 18, /* to */ 4, 30 ],
526 2012: [ 4, 2, /* to */ 4, 14 ],
527 2013: [ 3, 25, /* to */ 4, 6 ],
528 2014: [ 4, 14, /* to */ 4, 26 ],
529 2015: [ 3, 30, /* to */ 4, 11 ],
530 2016: [ 3, 21, /* to */ 4, 1 ],
531 2017: [ 4, 10, /* to */ 4, 22 ],
532 },
533 {
534 name: 'Pfingstferien',
535 2010: [ 5, 25, /* to */ 6, 5 ],
536 2011: [ 6, 14, /* to */ 6, 25 ],
537 2012: [ 5, 29, /* to */ 6, 9 ],
538 2013: [ 5, 21, /* to */ 5, 31 ],
539 2014: [ 6, 10, /* to */ 6, 21 ],
540 2015: [ 5, 26, /* to */ 6, 5 ],
541 2016: [ 5, 17, /* to */ 5, 28 ],
542 2017: [ 6, 6, /* to */ 6, 16 ],
543 },
544 {
545 name: 'Sommerferien',
546 2010: [ 8, 2, /* to */ 9, 13 ],
547 2011: [ 7, 30, /* to */ 9, 12 ],
548 2012: [ 8, 1, /* to */ 9, 12 ],
549 2013: [ 7, 31, /* to */ 9, 11 ],
550 2014: [ 7, 30, /* to */ 9, 15 ],
551 2015: [ 8, 1, /* to */ 9, 14 ],
552 2016: [ 7, 30, /* to */ 9, 12 ],
553 2017: [ 7, 29, /* to */ 9, 11 ],
554 },
555 {
556 name: 'Herbstferien',
557 2010: [ 11, 2, /* to */ 11, 5 ],
558 2011: [ 10, 31, /* to */ 11, 5 ],
559 2012: [ 10, 29, /* to */ 11, 3 ],
560 2013: [ 10, 28, /* to */ 10, 31 ],
561 2014: [ 10, 27, /* to */ 10, 31 ],
562 2015: [ 11, 2, /* to */ 11, 7 ],
563 2016: [ 10, 31, /* to */ 11, 4 ],
564 },
565 {
566 name: 'Weihnachtsferien',
567 2010: [ 12, 24, /* to */ 1, 7 ],
568 2011: [ 12, 27, /* to */ 1, 5 ],
569 2012: [ 12, 24, /* to */ 1, 5 ],
570 2013: [ 12, 23, /* to */ 1, 4 ],
571 2014: [ 12, 24, /* to */ 1, 5 ],
572 2015: [ 12, 24, /* to */ 1, 5 ],
573 2016: [ 12, 24, /* to */ 1, 5 ],
574 },
575 ],
576 },
577 'Niedersachsen': {
578 'SH': [
579 {
580 name: 'Winterferien',
581 2010: [ 2, 1, /* to */ 2, 2 ],
582 2011: [ 1, 31, /* to */ 2, 1 ],
583 2012: [ 1, 30, /* to */ 1, 31 ],
584 2013: [ 1, 31, /* to */ 2, 1 ],
585 2014: [ 1, 30, /* to */ 1, 31 ],
586 2015: [ 2, 2, /* to */ 2, 3 ],
587 2016: [ 1, 28, /* to */ 1, 29 ],
588 2017: [ 1, 30, /* to */ 1, 31 ],
589 },
590 {
591 name: 'Osterferien',
592 2010: [ 3, 19, /* to */ 4, 6 ],
593 2011: [ 4, 16, /* to */ 4, 30 ],
594 2012: [ 3, 26, /* to */ 4, 11, 4, 30, /* to */ 4, 30 ],
595 2013: [ 3, 16, /* to */ 4, 2 ],
596 2014: [ 4, 3, /* to */ 4, 22, 5, 2, /* to */ 5, 2 ],
597 2015: [ 3, 25, /* to */ 4, 10 ],
598 2016: [ 3, 18, /* to */ 4, 2 ],
599 2017: [ 4, 10, /* to */ 4, 22 ],
600 },
601 {
602 name: 'Pfingstferien',
603 2010: [ 5, 14, /* to */ 5, 14, 5, 25, /* to */ 5, 25 ],
604 2011: [ 6, 3, /* to */ 6, 3, 6, 14, /* to */ 6, 14 ],
605 2012: [ 5, 18, /* to */ 5, 18, 5, 29, /* to */ 5, 29 ],
606 2013: [ 5, 10, /* to */ 5, 10, 5, 21, /* to */ 5, 21 ],
607 2014: [ 5, 30, /* to */ 5, 30, 6, 10, /* to */ 6, 10 ],
608 2015: [ 5, 15, /* to */ 5, 15, 5, 26, /* to */ 5, 26 ],
609 2016: [ 5, 6, /* to */ 5, 6, 5, 17, /* to */ 5, 17 ],
610 2017: [ 5, 26, /* to */ 5, 26, 6, 6, /* to */ 6, 6 ],
611 },
612 {
613 name: 'Sommerferien',
614 2010: [ 6, 24, /* to */ 8, 4 ],
615 2011: [ 7, 7, /* to */ 8, 17 ],
616 2012: [ 7, 23, /* to */ 8, 31 ],
617 2013: [ 6, 27, /* to */ 8, 7 ],
618 2014: [ 7, 31, /* to */ 9, 10 ],
619 2015: [ 7, 23, /* to */ 9, 2 ],
620 2016: [ 6, 23, /* to */ 8, 3 ],
621 2017: [ 6, 22, /* to */ 8, 2 ],
622 },
623 {
624 name: 'Herbstferien',
625 2010: [ 10, 9, /* to */ 10, 23 ],
626 2011: [ 10, 17, /* to */ 10, 29 ],
627 2012: [ 10, 22, /* to */ 11, 3 ],
628 2013: [ 10, 4, /* to */ 10, 18 ],
629 2014: [ 10, 27, /* to */ 11, 8 ],
630 2015: [ 10, 19, /* to */ 10, 31 ],
631 2016: [ 10, 4, /* to */ 10, 15 ],
632 },
633 {
634 name: 'Weihnachtsferien',
635 2010: [ 12, 22, /* to */ 1, 5 ],
636 2011: [ 12, 23, /* to */ 1, 4 ],
637 2012: [ 12, 24, /* to */ 1, 5 ],
638 2013: [ 12, 23, /* to */ 1, 3 ],
639 2014: [ 12, 22, /* to */ 1, 5 ],
640 2015: [ 12, 23, /* to */ 1, 6 ],
641 2016: [ 12, 21, /* to */ 1, 6 ],
642 },
643 ],
644 },
645 'Nordrhein-Westfalen': {
646 'SH': [
647 {
648 name: 'Osterferien',
649 2010: [ 3, 27, /* to */ 4, 10 ],
650 2011: [ 4, 18, /* to */ 4, 30 ],
651 2012: [ 4, 2, /* to */ 4, 14 ],
652 2013: [ 3, 25, /* to */ 4, 6 ],
653 2014: [ 4, 14, /* to */ 4, 26 ],
654 2015: [ 3, 30, /* to */ 4, 11 ],
655 2016: [ 3, 21, /* to */ 4, 2 ],
656 2017: [ 4, 10, /* to */ 4, 22 ],
657 },
658 {
659 name: 'Pfingstferien',
660 2010: [ 5, 25, /* to */ 5, 25 ],
661 2012: [ 5, 29, /* to */ 5, 29 ],
662 2013: [ 5, 21, /* to */ 5, 21 ],
663 2014: [ 6, 10, /* to */ 6, 10 ],
664 2015: [ 5, 26, /* to */ 5, 26 ],
665 2016: [ 5, 17, /* to */ 5, 17 ],
666 2017: [ 6, 6, /* to */ 6, 6 ],
667 },
668 {
669 name: 'Sommerferien',
670 2010: [ 7, 15, /* to */ 8, 27 ],
671 2011: [ 7, 25, /* to */ 9, 6 ],
672 2012: [ 7, 9, /* to */ 8, 21 ],
673 2013: [ 7, 22, /* to */ 9, 3 ],
674 2014: [ 7, 7, /* to */ 8, 19 ],
675 2015: [ 6, 29, /* to */ 8, 11 ],
676 2016: [ 7, 11, /* to */ 8, 23 ],
677 2017: [ 7, 17, /* to */ 8, 29 ],
678 },
679 {
680 name: 'Herbstferien',
681 2010: [ 10, 11, /* to */ 10, 23 ],
682 2011: [ 10, 24, /* to */ 11, 5 ],
683 2012: [ 10, 8, /* to */ 10, 20 ],
684 2013: [ 10, 21, /* to */ 11, 2 ],
685 2014: [ 10, 6, /* to */ 10, 18 ],
686 2015: [ 10, 5, /* to */ 10, 17 ],
687 2016: [ 10, 10, /* to */ 10, 21 ],
688 },
689 {
690 name: 'Weihnachtsferien',
691 2010: [ 12, 24, /* to */ 1, 8 ],
692 2011: [ 12, 23, /* to */ 1, 6 ],
693 2012: [ 12, 21, /* to */ 1, 4 ],
694 2013: [ 12, 23, /* to */ 1, 7 ],
695 2014: [ 12, 22, /* to */ 1, 6 ],
696 2015: [ 12, 23, /* to */ 1, 6 ],
697 2016: [ 12, 23, /* to */ 1, 6 ],
698 },
699 ],
700 },
701 'Sachsen': {
702 'SH': [
703 {
704 name: 'Winterferien',
705 2010: [ 2, 8, /* to */ 2, 20 ],
706 2011: [ 2, 12, /* to */ 2, 26 ],
707 2012: [ 2, 13, /* to */ 2, 25 ],
708 2013: [ 2, 4, /* to */ 2, 15 ],
709 2014: [ 2, 17, /* to */ 3, 1 ],
710 2015: [ 2, 9, /* to */ 2, 21 ],
711 2016: [ 2, 8, /* to */ 2, 20 ],
712 2017: [ 2, 13, /* to */ 2, 24 ],
713 },
714 {
715 name: 'Osterferien',
716 2010: [ 4, 1, /* to */ 4, 10 ],
717 2011: [ 4, 22, /* to */ 4, 30 ],
718 2012: [ 4, 6, /* to */ 4, 14 ],
719 2013: [ 3, 29, /* to */ 4, 6 ],
720 2014: [ 4, 18, /* to */ 4, 26 ],
721 2015: [ 4, 2, /* to */ 4, 11 ],
722 2016: [ 3, 25, /* to */ 4, 2 ],
723 2017: [ 4, 13, /* to */ 4, 22 ],
724 },
725 {
726 name: 'Pfingstferien',
727 2010: [ 5, 14, /* to */ 5, 14 ],
728 2011: [ 6, 3, /* to */ 6, 3 ],
729 2012: [ 5, 18, /* to */ 5, 18 ],
730 2013: [ 5, 10, /* to */ 5, 10, 5, 18, /* to */ 5, 22 ],
731 2014: [ 5, 30, /* to */ 5, 30 ],
732 2015: [ 5, 15, /* to */ 5, 15 ],
733 2016: [ 5, 6, /* to */ 5, 6 ],
734 2017: [ 5, 26, /* to */ 5, 26 ],
735 },
736 {
737 name: 'Sommerferien',
738 2010: [ 6, 28, /* to */ 8, 6 ],
739 2011: [ 7, 11, /* to */ 8, 19 ],
740 2012: [ 7, 23, /* to */ 8, 31 ],
741 2013: [ 7, 15, /* to */ 8, 23 ],
742 2014: [ 7, 21, /* to */ 8, 29 ],
743 2015: [ 7, 13, /* to */ 8, 21 ],
744 2016: [ 6, 27, /* to */ 8, 5 ],
745 2017: [ 6, 26, /* to */ 8, 4 ],
746 },
747 {
748 name: 'Herbstferien',
749 2010: [ 10, 4, /* to */ 10, 16 ],
750 2011: [ 10, 17, /* to */ 10, 28 ],
751 2012: [ 10, 22, /* to */ 11, 2 ],
752 2013: [ 10, 21, /* to */ 11, 1 ],
753 2014: [ 10, 20, /* to */ 10, 31 ],
754 2015: [ 10, 12, /* to */ 10, 24 ],
755 2016: [ 10, 3, /* to */ 10, 15 ],
756 },
757 {
758 name: 'Weihnachtsferien',
759 2010: [ 12, 23, /* to */ 1, 1 ],
760 2011: [ 12, 23, /* to */ 1, 2 ],
761 2012: [ 12, 22, /* to */ 1, 2 ],
762 2013: [ 12, 21, /* to */ 1, 3 ],
763 2014: [ 12, 22, /* to */ 1, 3 ],
764 2015: [ 12, 21, /* to */ 1, 2 ],
765 2016: [ 12, 23, /* to */ 1, 2 ],
766 },
767 ],
768 },
769 'Thüringen': {
770 'SH': [
771 {
772 name: 'Winterferien',
773 2010: [ 2, 1, /* to */ 2, 6 ],
774 2011: [ 1, 31, /* to */ 2, 5 ],
775 2012: [ 2, 6, /* to */ 2, 11 ],
776 2013: [ 2, 18, /* to */ 2, 23 ],
777 2014: [ 2, 17, /* to */ 2, 22 ],
778 2015: [ 2, 2, /* to */ 2, 7 ],
779 2016: [ 2, 1, /* to */ 2, 6 ],
780 2017: [ 2, 6, /* to */ 2, 11 ],
781 },
782 {
783 name: 'Osterferien',
784 2010: [ 3, 29, /* to */ 4, 9 ],
785 2011: [ 4, 18, /* to */ 4, 30 ],
786 2012: [ 4, 2, /* to */ 4, 13 ],
787 2013: [ 3, 25, /* to */ 4, 6 ],
788 2014: [ 4, 19, /* to */ 5, 2 ],
789 2015: [ 3, 30, /* to */ 4, 11 ],
790 2016: [ 3, 24, /* to */ 4, 2 ],
791 2017: [ 4, 10, /* to */ 4, 21 ],
792 },
793 {
794 name: 'Sommerferien',
795 2010: [ 6, 24, /* to */ 8, 4 ],
796 2011: [ 7, 11, /* to */ 8, 19 ],
797 2012: [ 7, 23, /* to */ 8, 31 ],
798 2013: [ 7, 15, /* to */ 8, 23 ],
799 2014: [ 7, 21, /* to */ 8, 29 ],
800 2015: [ 7, 13, /* to */ 8, 21 ],
801 2016: [ 6, 27, /* to */ 8, 10 ],
802 2017: [ 6, 26, /* to */ 8, 9 ],
803 },
804 {
805 name: 'Pfingstferien',
806 2011: [ 6, 11, /* to */ 6, 14 ],
807 2012: [ 5, 25, /* to */ 5, 29 ],
808 2013: [ 5, 10, /* to */ 5, 10 ],
809 2014: [ 5, 30, /* to */ 5, 30 ],
810 2015: [ 5, 15, /* to */ 5, 15 ],
811 2016: [ 5, 6, /* to */ 5, 6 ],
812 2017: [ 5, 26, /* to */ 5, 26 ],
813 },
814 {
815 name: 'Herbstferien',
816 2010: [ 10, 9, /* to */ 10, 23 ],
817 2011: [ 10, 17, /* to */ 10, 28 ],
818 2012: [ 10, 22, /* to */ 11, 3 ],
819 2013: [ 10, 21, /* to */ 11, 2 ],
820 2014: [ 10, 6, /* to */ 10, 18 ],
821 2015: [ 10, 5, /* to */ 10, 17 ],
822 2016: [ 10, 10, /* to */ 10, 22 ],
823 },
824 {
825 name: 'Weihnachtsferien',
826 2010: [ 12, 23, /* to */ 1, 1 ],
827 2011: [ 12, 23, /* to */ 1, 1 ],
828 2012: [ 12, 24, /* to */ 1, 5 ],
829 2013: [ 12, 23, /* to */ 1, 4 ],
830 2014: [ 12, 22, /* to */ 1, 3 ],
831 2015: [ 12, 23, /* to */ 1, 2 ],
832 2016: [ 12, 23, /* to */ 12, 31 ],
833 },
834 ],
835 },
836 'Hamburg': {
837 'SH': [
838 {
839 name: 'Winterferien',
840 2010: [ 1, 29, /* to */ 1, 29 ],
841 2011: [ 1, 31, /* to */ 1, 31 ],
842 2012: [ 1, 30, /* to */ 1, 30 ],
843 2013: [ 2, 1, /* to */ 2, 1 ],
844 2014: [ 1, 31, /* to */ 1, 31 ],
845 2015: [ 1, 30, /* to */ 1, 30 ],
846 2016: [ 1, 29, /* to */ 1, 29 ],
847 2017: [ 1, 30, /* to */ 1, 30 ],
848 },
849 {
850 name: 'Osterferien',
851 2010: [ 3, 8, /* to */ 3, 20 ],
852 2011: [ 3, 7, /* to */ 3, 18 ],
853 2012: [ 3, 5, /* to */ 3, 16 ],
854 2013: [ 3, 4, /* to */ 3, 15 ],
855 2014: [ 3, 3, /* to */ 3, 14 ],
856 2015: [ 3, 2, /* to */ 3, 13 ],
857 2016: [ 3, 7, /* to */ 3, 18 ],
858 2017: [ 3, 6, /* to */ 3, 17 ],
859 },
860 {
861 name: 'Pfingstferien',
862 2010: [ 5, 14, /* to */ 5, 22 ],
863 2011: [ 4, 26, /* to */ 4, 29, 6, 3, /* to */ 6, 3 ],
864 2012: [ 4, 30, /* to */ 5, 4, 5, 18, /* to */ 5, 18 ],
865 2013: [ 5, 2, /* to */ 5, 10 ],
866 2014: [ 4, 28, /* to */ 5, 2, 5, 30, /* to */ 5, 30 ],
867 2015: [ 5, 11, /* to */ 5, 15 ],
868 2016: [ 5, 6, /* to */ 5, 6, 5, 17, /* to */ 5, 20 ],
869 2017: [ 5, 22, /* to */ 5, 26 ],
870 },
871 {
872 name: 'Sommerferien',
873 2010: [ 7, 8, /* to */ 8, 18 ],
874 2011: [ 6, 30, /* to */ 8, 10 ],
875 2012: [ 6, 21, /* to */ 8, 1 ],
876 2013: [ 6, 20, /* to */ 7, 31 ],
877 2014: [ 7, 10, /* to */ 8, 20 ],
878 2015: [ 7, 16, /* to */ 8, 26 ],
879 2016: [ 7, 21, /* to */ 8, 31 ],
880 2017: [ 7, 20, /* to */ 8, 30 ],
881 },
882 {
883 name: 'Herbstferien',
884 2010: [ 10, 4, /* to */ 10, 15 ],
885 2011: [ 10, 4, /* to */ 10, 14 ],
886 2012: [ 10, 1, /* to */ 10, 12 ],
887 2013: [ 9, 30, /* to */ 10, 11 ],
888 2014: [ 10, 13, /* to */ 10, 24 ],
889 2015: [ 10, 19, /* to */ 10, 30 ],
890 2016: [ 10, 17, /* to */ 10, 28 ],
891 },
892 {
893 name: 'Weihnachtsferien',
894 2010: [ 12, 23, /* to */ 1, 3 ],
895 2011: [ 12, 27, /* to */ 1, 6 ],
896 2012: [ 12, 21, /* to */ 1, 4 ],
897 2013: [ 12, 19, /* to */ 1, 3 ],
898 2014: [ 12, 22, /* to */ 1, 6 ],
899 2015: [ 12, 21, /* to */ 1, 1 ],
900 2016: [ 12, 27, /* to */ 1, 6 ],
901 },
902 ],
903 },
904 'Sachsen-Anhalt': {
905 'SH': [
906 {
907 name: 'Winterferien',
908 2010: [ 2, 8, /* to */ 2, 13 ],
909 2011: [ 2, 5, /* to */ 2, 12 ],
910 2012: [ 2, 4, /* to */ 2, 11 ],
911 2013: [ 2, 1, /* to */ 2, 8 ],
912 2014: [ 2, 1, /* to */ 2, 12 ],
913 2015: [ 2, 2, /* to */ 2, 14 ],
914 2016: [ 2, 1, /* to */ 2, 10 ],
915 2017: [ 2, 4, /* to */ 2, 11 ],
916 },
917 {
918 name: 'Osterferien',
919 2010: [ 3, 29, /* to */ 4, 9 ],
920 2011: [ 4, 18, /* to */ 4, 27 ],
921 2012: [ 4, 2, /* to */ 4, 7 ],
922 2013: [ 3, 25, /* to */ 3, 30 ],
923 2014: [ 4, 14, /* to */ 4, 17 ],
924 2015: [ 4, 2, /* to */ 4, 2 ],
925 2016: [ 3, 24, /* to */ 3, 24 ],
926 2017: [ 4, 10, /* to */ 4, 13 ],
927 },
928 {
929 name: 'Pfingstferien',
930 2010: [ 5, 14, /* to */ 5, 22 ],
931 2011: [ 6, 14, /* to */ 6, 18 ],
932 2012: [ 5, 18, /* to */ 5, 25 ],
933 2013: [ 5, 10, /* to */ 5, 18 ],
934 2014: [ 5, 30, /* to */ 6, 7 ],
935 2015: [ 5, 15, /* to */ 5, 23 ],
936 2016: [ 5, 6, /* to */ 5, 14 ],
937 2017: [ 5, 26, /* to */ 5, 26 ],
938 },
939 {
940 name: 'Sommerferien',
941 2010: [ 6, 24, /* to */ 8, 4 ],
942 2011: [ 7, 11, /* to */ 8, 24 ],
943 2012: [ 7, 23, /* to */ 9, 5 ],
944 2013: [ 7, 15, /* to */ 8, 28 ],
945 2014: [ 7, 21, /* to */ 9, 3 ],
946 2015: [ 7, 13, /* to */ 8, 26 ],
947 2016: [ 6, 27, /* to */ 8, 10 ],
948 2017: [ 6, 26, /* to */ 8, 9 ],
949 },
950 {
951 name: 'Herbstferien',
952 2010: [ 10, 18, /* to */ 10, 23 ],
953 2011: [ 10, 17, /* to */ 10, 22 ],
954 2012: [ 10, 29, /* to */ 11, 2 ],
955 2013: [ 10, 21, /* to */ 10, 25 ],
956 2014: [ 10, 27, /* to */ 10, 30 ],
957 2015: [ 10, 17, /* to */ 10, 24 ],
958 2016: [ 10, 4, /* to */ 10, 15 ],
959 },
960 {
961 name: 'Weihnachtsferien',
962 2010: [ 12, 22, /* to */ 1, 5 ],
963 2011: [ 12, 22, /* to */ 1, 7 ],
964 2012: [ 12, 19, /* to */ 1, 4 ],
965 2013: [ 12, 21, /* to */ 1, 3 ],
966 2014: [ 12, 22, /* to */ 1, 5 ],
967 2015: [ 12, 21, /* to */ 1, 5 ],
968 2016: [ 12, 19, /* to */ 1, 2 ],
969 },
970 ],
971 },
972 'Rheinland-Pfalz': {
973 'SH': [
974 {
975 name: 'Osterferien',
976 2010: [ 3, 26, /* to */ 4, 9 ],
977 2011: [ 4, 18, /* to */ 4, 29 ],
978 2012: [ 3, 29, /* to */ 4, 13 ],
979 2013: [ 3, 20, /* to */ 4, 5 ],
980 2014: [ 4, 11, /* to */ 4, 25 ],
981 2015: [ 3, 26, /* to */ 4, 10 ],
982 2016: [ 3, 18, /* to */ 4, 1 ],
983 2017: [ 4, 10, /* to */ 4, 21 ],
984 },
985 {
986 name: 'Sommerferien',
987 2010: [ 7, 5, /* to */ 8, 13 ],
988 2011: [ 6, 27, /* to */ 8, 5 ],
989 2012: [ 7, 2, /* to */ 8, 10 ],
990 2013: [ 7, 8, /* to */ 8, 16 ],
991 2014: [ 7, 28, /* to */ 9, 5 ],
992 2015: [ 7, 27, /* to */ 9, 4 ],
993 2016: [ 7, 18, /* to */ 8, 26 ],
994 2017: [ 7, 3, /* to */ 8, 11 ],
995 },
996 {
997 name: 'Herbstferien',
998 2010: [ 10, 11, /* to */ 10, 22 ],
999 2011: [ 10, 4, /* to */ 10, 14 ],
1000 2012: [ 10, 1, /* to */ 10, 12 ],
1001 2013: [ 10, 4, /* to */ 10, 18 ],
1002 2014: [ 10, 20, /* to */ 10, 31 ],
1003 2015: [ 10, 19, /* to */ 10, 30 ],
1004 2016: [ 10, 10, /* to */ 10, 21 ],
1005 },
1006 {
1007 name: 'Weihnachtsferien',
1008 2010: [ 12, 23, /* to */ 1, 7 ],
1009 2011: [ 12, 22, /* to */ 1, 6 ],
1010 2012: [ 12, 20, /* to */ 1, 4 ],
1011 2013: [ 12, 23, /* to */ 1, 7 ],
1012 2014: [ 12, 22, /* to */ 1, 7 ],
1013 2015: [ 12, 23, /* to */ 1, 8 ],
1014 2016: [ 12, 22, /* to */ 1, 6 ],
1015 },
1016 ],
1017 },
1018 'Brandenburg': {
1019 'SH': [
1020 {
1021 name: 'Winterferien',
1022 2010: [ 2, 1, /* to */ 2, 6 ],
1023 2011: [ 1, 31, /* to */ 2, 5 ],
1024 2012: [ 1, 30, /* to */ 2, 4 ],
1025 2013: [ 2, 4, /* to */ 2, 9 ],
1026 2014: [ 2, 3, /* to */ 2, 8 ],
1027 2015: [ 2, 2, /* to */ 2, 7 ],
1028 2016: [ 2, 1, /* to */ 2, 6 ],
1029 2017: [ 1, 30, /* to */ 2, 4 ],
1030 },
1031 {
1032 name: 'Osterferien',
1033 2010: [ 3, 31, /* to */ 4, 10 ],
1034 2011: [ 4, 20, /* to */ 4, 30 ],
1035 2012: [ 4, 4, /* to */ 4, 14, 4, 30, /* to */ 4, 30 ],
1036 2013: [ 3, 27, /* to */ 4, 6 ],
1037 2014: [ 4, 16, /* to */ 4, 26, 5, 2, /* to */ 5, 2 ],
1038 2015: [ 4, 1, /* to */ 4, 11 ],
1039 2016: [ 3, 23, /* to */ 4, 2 ],
1040 2017: [ 4, 12, /* to */ 4, 22 ],
1041 },
1042 {
1043 name: 'Pfingstferien',
1044 2010: [ 5, 14, /* to */ 5, 14 ],
1045 2011: [ 6, 3, /* to */ 6, 3 ],
1046 2012: [ 5, 18, /* to */ 5, 18 ],
1047 2013: [ 5, 10, /* to */ 5, 10 ],
1048 2014: [ 5, 30, /* to */ 5, 30 ],
1049 2015: [ 5, 15, /* to */ 5, 15 ],
1050 2016: [ 5, 6, /* to */ 5, 6, 5, 17, /* to */ 5, 17 ],
1051 2017: [ 5, 26, /* to */ 5, 26 ],
1052 },
1053 {
1054 name: 'Sommerferien',
1055 2010: [ 7, 8, /* to */ 8, 21 ],
1056 2011: [ 6, 30, /* to */ 8, 13 ],
1057 2012: [ 6, 21, /* to */ 8, 3 ],
1058 2013: [ 6, 20, /* to */ 8, 2 ],
1059 2014: [ 7, 10, /* to */ 8, 22 ],
1060 2015: [ 7, 16, /* to */ 8, 28 ],
1061 2016: [ 7, 21, /* to */ 9, 3 ],
1062 2017: [ 7, 20, /* to */ 9, 1 ],
1063 },
1064 {
1065 name: 'Herbstferien',
1066 2010: [ 10, 11, /* to */ 10, 23 ],
1067 2011: [ 10, 4, /* to */ 10, 14 ],
1068 2012: [ 10, 1, /* to */ 10, 13 ],
1069 2013: [ 9, 30, /* to */ 10, 12, 11, 1, /* to */ 11, 1 ],
1070 2014: [ 10, 20, /* to */ 11, 1 ],
1071 2015: [ 10, 19, /* to */ 10, 30 ],
1072 2016: [ 10, 17, /* to */ 10, 28 ],
1073 },
1074 {
1075 name: 'Weihnachtsferien',
1076 2010: [ 12, 23, /* to */ 1, 1 ],
1077 2011: [ 12, 23, /* to */ 1, 3 ],
1078 2012: [ 12, 24, /* to */ 1, 4 ],
1079 2013: [ 12, 23, /* to */ 1, 3 ],
1080 2014: [ 12, 22, /* to */ 1, 2 ],
1081 2015: [ 12, 23, /* to */ 1, 2 ],
1082 2016: [ 12, 23, /* to */ 1, 3 ],
1083 },
1084 ],
1085 },
1086 }, // }}}
1087 'at': { // {{{
1088 'PH': { // http://de.wikipedia.org/wiki/Feiertage_in_%C3%96sterreich
1089 'Neujahrstag' : [ 1, 1 ],
1090 'Heilige Drei Könige' : [ 1, 6 ],
1091 // 'Josef' : [ 3, 19, [ 'Kärnten', 'Steiermark', 'Tirol', 'Vorarlberg' ] ],
1092 // 'Karfreitag' : [ 'easter', -2 ],
1093 'Ostermontag' : [ 'easter', 1 ],
1094 'Staatsfeiertag' : [ 5, 1 ],
1095 // 'Florian' : [ 5, 4, [ 'Oberösterreich' ] ],
1096 'Christi Himmelfahrt' : [ 'easter', 39 ],
1097 'Pfingstmontag' : [ 'easter', 50 ],
1098 'Fronleichnam' : [ 'easter', 60 ],
1099 'Mariä Himmelfahrt' : [ 8, 15 ],
1100 // 'Rupert' : [ 9, 24, [ 'Salzburg' ] ],
1101 // 'Tag der Volksabstimmung' : [ 10, 10, [ 'Kärnten' ] ],
1102 'Nationalfeiertag' : [ 10, 26 ],
1103 'Allerheiligen' : [ 11, 1 ],
1104 // 'Martin' : [ 11, 11, [ 'Burgenland' ] ],
1105 // 'Leopold' : [ 11, 15, [ 'Niederösterreich', 'Wien' ] ],
1106 'Mariä Empfängnis' : [ 12, 8 ],
1107 // 'Heiliger Abend' : [ 12, 24 ],
1108 'Christtag' : [ 12, 25 ],
1109 'Stefanitag' : [ 12, 26 ],
1110 // 'Silvester' : [ 12, 31 ],
1111 },
1112 }, // }}}
1113 'ca': { // {{{
1114 'PH': { // https://en.wikipedia.org/wiki/Public_holidays_in_Canada
1115 "New Year's Day" : [ 1, 1 ],
1116 "Good Friday" : [ 'easter', -2 ],
1117 "Canada Day" : [ 'canadaDay', 0 ],
1118 "Labour Day" : [ 'firstSeptemberMonday', 0 ],
1119 "Christmas Day" : [ 12, 25 ]
1120 },
1121 'Alberta': {
1122 'PH': {
1123 "New Year's Day" : [ 1, 1 ],
1124 "Alberta Family Day" : [ 'firstFebruaryMonday', 14 ],
1125 "Good Friday" : [ 'easter', -2 ],
1126 "Easter Monday" : [ 'easter', 1 ],
1127 "Victoria Day" : [ 'victoriaDay', 0 ],
1128 "Canada Day" : [ 'canadaDay', 0 ],
1129 "Heritage Day" : [ 'firstAugustMonday', 0 ],
1130 "Labour Day" : [ 'firstSeptemberMonday', 0 ],
1131 "Thanksgiving" : [ 'firstOctoberMonday', 7 ],
1132 "Remembrance Day" : [ 11, 11 ],
1133 "Christmas Day" : [ 12, 25 ],
1134 "Boxing Day" : [ 12, 26 ]
1135 },
1136 },
1137 'British Columbia': {
1138 'PH': {
1139 "New Year's Day" : [ 1, 1 ],
1140 "Family Day" : [ 'firstFebruaryMonday', 7 ],
1141 "Good Friday" : [ 'easter', -2 ],
1142 "Victoria Day" : [ 'victoriaDay', 0 ],
1143 "Canada Day" : [ 'canadaDay', 0 ],
1144 "British Columbia Day" : [ 'firstAugustMonday', 0 ],
1145 "Labour Day" : [ 'firstSeptemberMonday', 0 ],
1146 "Thanksgiving" : [ 'firstOctoberMonday', 7 ],
1147 "Remembrance Day" : [ 11, 11 ],
1148 "Christmas Day" : [ 12, 25 ]
1149 },
1150 },
1151 'Manitoba': {
1152 'PH': {
1153 "New Year's Day" : [ 1, 1 ],
1154 "Louis Riel Day" : [ 'firstFebruaryMonday', 14 ],
1155 "Good Friday" : [ 'easter', -2 ],
1156 "Victoria Day" : [ 'victoriaDay', 0 ],
1157 "Canada Day" : [ 'canadaDay', 0 ],
1158 "Civic Holiday" : [ 'firstAugustMonday', 0 ],
1159 "Labour Day" : [ 'firstSeptemberMonday', 0 ],
1160 "Thanksgiving" : [ 'firstOctoberMonday', 7 ],
1161 "Remembrance Day" : [ 11, 11 ],
1162 "Christmas Day" : [ 12, 25 ]
1163 },
1164 },
1165 'New Brunswick': {
1166 'PH': {
1167 "New Year's Day" : [ 1, 1 ],
1168 "Good Friday" : [ 'easter', -2 ],
1169 "Victoria Day" : [ 'victoriaDay', 0 ],
1170 "Canada Day" : [ 'canadaDay', 0 ],
1171 "New Brunswick Day" : [ 'firstAugustMonday', 0 ],
1172 "Labour Day" : [ 'firstSeptemberMonday', 0 ],
1173 "Thanksgiving" : [ 'firstOctoberMonday', 7 ],
1174 "Remembrance Day" : [ 11, 11 ],
1175 "Christmas Day" : [ 12, 25 ],
1176 "Boxing Day" : [ 12, 26 ]
1177 },
1178 },
1179 'Newfoundland and Labrador': {
1180 'PH': {
1181 "New Year's Day" : [ 1, 1 ],
1182 "Saint Patrick's Day" : [ 3, 17 ],
1183 "Good Friday" : [ 'easter', -2 ],
1184 "Saint George's Day" : [ 4, 23 ],
1185 "Discovery Day" : [ 6, 24 ],
1186 "Memorial Day" : [ 7, 1 ],
1187 "Orangemen's Day" : [ 7, 12 ],
1188 "Labour Day" : [ 'firstSeptemberMonday', 0 ],
1189 "Armistice Day" : [ 11, 11 ],
1190 "Christmas Day" : [ 12, 25 ]
1191 },
1192 },
1193 'Northwest Territories': {
1194 'PH': {
1195 "New Year's Day" : [ 1, 1 ],
1196 "Good Friday" : [ 'easter', -2 ],
1197 "Victoria Day" : [ 'victoriaDay', 0 ],
1198 "National Aboriginal Day" : [ 6, 21 ],
1199 "Canada Day" : [ 'canadaDay', 0 ],
1200 "Civic Holiday" : [ 'firstAugustMonday', 0 ],
1201 "Labour Day" : [ 'firstSeptemberMonday', 0 ],
1202 "Thanksgiving" : [ 'firstOctoberMonday', 7 ],
1203 "Remembrance Day" : [ 11, 11 ],
1204 "Christmas Day" : [ 12, 25 ]
1205 },
1206 },
1207 'Nova Scotia': {
1208 'PH': {
1209 "New Year's Day" : [ 1, 1 ],
1210 "Good Friday" : [ 'easter', -2 ],
1211 "Victoria Day" : [ 'victoriaDay', 0 ],
1212 "Canada Day" : [ 'canadaDay', 0 ],
1213 "Natal Day" : [ 'firstAugustMonday', 0 ],
1214 "Labour Day" : [ 'firstSeptemberMonday', 0 ],
1215 "Thanksgiving" : [ 'firstOctoberMonday', 7 ],
1216 "Remembrance Day" : [ 11, 11 ],
1217 "Christmas Day" : [ 12, 25 ],
1218 "Boxing Day" : [ 12, 26 ]
1219 },
1220 },
1221 'Nunavut': {
1222 'PH': {
1223 "New Year's Day" : [ 1, 1 ],
1224 "Good Friday" : [ 'easter', -2 ],
1225 "Victoria Day" : [ 'victoriaDay', 0 ],
1226 "Canada Day" : [ 'canadaDay', 0 ],
1227 "Nunavut Day" : [ 7, 9 ],
1228 "Civic Holiday" : [ 'firstAugustMonday', 0 ],
1229 "Labour Day" : [ 'firstSeptemberMonday', 0 ],
1230 "Thanksgiving" : [ 'firstOctoberMonday', 7 ],
1231 "Remembrance Day" : [ 11, 11 ],
1232 "Christmas Day" : [ 12, 25 ]
1233 },
1234 },
1235 'Ontario': {
1236 'PH': {
1237 "New Year's Day" : [ 1, 1 ],
1238 "Family Day" : [ 'firstFebruaryMonday', 14 ],
1239 "Good Friday" : [ 'easter', -2 ],
1240 "Victoria Day" : [ 'victoriaDay', 0 ],
1241 "Canada Day" : [ 'canadaDay', 0 ],
1242 "August Civic Public Holiday" : [ 'firstAugustMonday', 0 ],
1243 "Labour Day" : [ 'firstSeptemberMonday', 0 ],
1244 "Thanksgiving" : [ 'firstOctoberMonday', 7 ],
1245 "Remembrance Day" : [ 11, 11 ],
1246 "Christmas Day" : [ 12, 25 ],
1247 "Boxing Day" : [ 12, 26 ]
1248 },
1249 },
1250 'Prince Edward Island': {
1251 'PH': {
1252 "New Year's Day" : [ 1, 1 ],
1253 "Islander Day" : [ 'firstFebruaryMonday', 14 ],
1254 "Good Friday" : [ 'easter', -2 ],
1255 "Easter Monday" : [ 'easter', 1 ],
1256 "Victoria Day" : [ 'victoriaDay', 0 ],
1257 "Canada Day" : [ 'canadaDay', 0 ],
1258 "Civic Holiday" : [ 'firstAugustMonday', 0 ],
1259 "Gold Cup Parade Day" : [ 'firstAugustMonday', 18 ],
1260 "Labour Day" : [ 'firstSeptemberMonday', 0 ],
1261 "Thanksgiving" : [ 'firstOctoberMonday', 7 ],
1262 "Remembrance Day" : [ 11, 11 ],
1263 "Christmas Day" : [ 12, 25 ],
1264 "Boxing Day" : [ 12, 26 ]
1265 },
1266 },
1267 'Quebec': {
1268 'PH': {
1269 "Jour de l'an" : [ 1, 1 ],
1270 "Vendredi saint" : [ 'easter', -2 ],
1271 "Lundi de Pâques" : [ 'easter', 1 ],
1272 "Journée nationale des patriotes" : [ 'victoriaDay', 0 ],
1273 "Fête nationale du Québec" : [ 6, 24 ],
1274 "Fête du Canada" : [ 'canadaDay', 0 ],
1275 "Fête du Travail" : [ 'firstSeptemberMonday', 0 ],
1276 "Jour de l'Action de grâce" : [ 'firstOctoberMonday', 7 ],
1277 "Noël" : [ 12, 25 ]
1278 },
1279 },
1280 'Saskatchewan': {
1281 'PH': {
1282 "New Year's Day" : [ 1, 1 ],
1283 "Family Day" : [ 'firstFebruaryMonday', 14 ],
1284 "Good Friday" : [ 'easter', -2 ],
1285 "Victoria Day" : [ 'victoriaDay', 0 ],
1286 "Canada Day" : [ 'canadaDay', 0 ],
1287 "Saskatchewan Day" : [ 'firstAugustMonday', 0 ],
1288 "Labour Day" : [ 'firstSeptemberMonday', 0 ],
1289 "Thanksgiving" : [ 'firstOctoberMonday', 7 ],
1290 "Remembrance Day" : [ 11, 11 ],
1291 "Christmas Day" : [ 12, 25 ]
1292 },
1293 },
1294 'Yukon': {
1295 'PH': {
1296 "New Year's Day" : [ 1, 1 ],
1297 "Heritage Day" : [ 'lastFebruarySunday', -2 ],
1298 "Good Friday" : [ 'easter', -2 ],
1299 "Easter Monday" : [ 'easter', 1 ],
1300 "Victoria Day" : [ 'victoriaDay', 0 ],
1301 "Canada Day" : [ 'canadaDay', 0 ],
1302 "Discovery Day" : [ 'firstAugustMonday', 14 ],
1303 "Labour Day" : [ 'firstSeptemberMonday', 0 ],
1304 "Thanksgiving" : [ 'firstOctoberMonday', 7 ],
1305 "Remembrance Day" : [ 11, 11 ],
1306 "Christmas Day" : [ 12, 25 ],
1307 "Boxing Day" : [ 12, 26 ]
1308 },
1309 },
1310 }, // }}}
1311 'ua': { // {{{
1312 'PH': { // http://uk.wikipedia.org/wiki/%D0%A1%D0%B2%D1%8F%D1%82%D0%B0_%D1%82%D0%B0_%D0%BF%D0%B0%D0%BC%27%D1%8F%D1%82%D0%BD%D1%96_%D0%B4%D0%BD%D1%96_%D0%B2_%D0%A3%D0%BA%D1%80%D0%B0%D1%97%D0%BD%D1%96
1313 "Новий рік" : [ 1, 1 ],
1314 "Різдво" : [ 1, 7 ],
1315 "Міжнародний жіночий день" : [ 3, 8 ],
1316 "Великдень" : [ 'orthodox easter', 1 ],
1317 "День Праці 1" : [ 5, 1 ],
1318 "День Праці 2" : [ 5, 2 ],
1319 "День Перемоги" : [ 5, 9 ],
1320 "День Конституції України" : [ 6, 28 ],
1321 "День Незалежності України" : [ 8, 24 ],
1322 }
1323 }, // }}}
1324 'si': { // {{{
1325 'PH': { // http://www.vlada.si/o_sloveniji/politicni_sistem/prazniki/
1326 'novo leto' : [ 1, 1 ],
1327 'Prešernov dan, slovenski kulturni praznik' : [ 2, 8 ],
1328 'velikonočna nedelja' : [ 'easter', 0 ],
1329 'velikonočni ponedeljek' : [ 'easter', 1 ],
1330 'dan upora proti okupatorju' : [ 4, 27 ],
1331 'praznik dela 1' : [ 5, 1 ],
1332 'praznik dela 2' : [ 5, 2 ],
1333 'binkoštna nedelja - binkošti' : [ 'easter', 49 ],
1334 'dan državnosti' : [ 6, 25 ],
1335 'Marijino vnebovzetje' : [ 8, 15 ],
1336 'dan reformacije' : [ 10, 31 ],
1337 'dan spomina na mrtve' : [ 11, 1 ],
1338 'božič' : [ 12, 25 ],
1339 'dan samostojnosti in enotnosti' : [ 12, 26 ],
1340 },
1341 }, // }}}
1342 };
1343 // }}}
1344
1345 // error correction {{{
1346 // Taken form http://www.netzwolf.info/j/osm/time_domain.js
1347 // Credits go to Netzwolf
1348 //
1349 // Key to word_error_correction is the token name except wrong_words
1350 var word_error_correction = {
1351 wrong_words: { /* {{{ */
1352 'Assuming "<ok>" for "<ko>".': {
1353 'Frühling': 'Mar-May',
1354 'Frühjahr': 'Mar-May',
1355 'Sommer': 'Jun-Aug',
1356 'Herbst': 'Sep-Nov',
1357 'winter': 'Dec-Feb',
1358 }, '"<ko>" wird als "<ok>" interpertiert.': {
1359 'spring': 'Mar-May',
1360 'summer': 'Jun-Aug',
1361 'autumn': 'Sep-Nov',
1362 // 'winter': 'Dec-Feb', // Same as in English.
1363 // morning: '08:00-12:00',
1364 // evening: '13:00-18:00',
1365 '_': '-',
1366 'daytime': 'sunrise-sunset',
1367 }, 'Bitte benutze die englische Schreibweise "<ok>" für "<ko>".': {
1368 'sommer': 'summer',
1369 'werktag': 'Mo-Fr',
1370 'werktags': 'Mo-Fr',
1371 }, 'Bitte benutze "<ok>" für "<ko>". Beispiel: "Mo-Fr 08:00-12:00; Tu off".': {
1372 'ruhetag': 'off',
1373 'ruhetage': 'off',
1374 'geschlossen': 'off',
1375 'geschl': 'off',
1376 // 'ausser': 'off',
1377 // 'außer': 'off',
1378 }, 'Neem de engelse afkorting "<ok>" voor "<ko>" alstublieft.': {
1379 'gesloten': 'off',
1380 'feestdag': 'PH',
1381 'feestdagen': 'PH',
1382 }, 'Assuming "<ok>" for "<ko>". Please avoid using "workday": http://wiki.openstreetmap.org/wiki/Talk:Key:opening_hours#need_syntax_for_holidays_and_workingdays': {
1383 // Used around 260 times but the problem is, that work day might be different in other countries.
1384 'wd': 'Mo-Fr',
1385 'on work day': 'Mo-Fr',
1386 'on work days': 'Mo-Fr',
1387 'weekday': 'Mo-Fr',
1388 'weekdays': 'Mo-Fr',
1389 'vardagar': 'Mo-Fr',
1390 }, 'Please use something like "Mo off" instead "<ko>".': {
1391 'except': 'off',
1392 }, 'Please omit "<ko>" or use a colon instead: "12:00-14:00".': {
1393 'h': '',
1394 }, 'Please omit "<ko>".': {
1395 'season': '',
1396 'hs': '',
1397 'hrs': '',
1398 'hours': '',
1399 '·': '',
1400 }, 'Please omit "<ko>". The key must not be in the value.': {
1401 'opening_hours=': '',
1402 }, 'Please omit "<ko>". You might want to express open end which can be specified as "12:00+" for example.': {
1403 'from': '',
1404 }, 'You can use notation "<ok>" for "<ko>" in the case that you want to express open end times. Example: "12:00+".': {
1405 'til late': '+',
1406 'till late': '+',
1407 '-late': '+',
1408 '-open end': '+',
1409 '-openend': '+',
1410 }, 'Please use notation "<ok>" for "<ko>". If the times are unsure or vary consider a comment e.g. 12:00-14:00 "only on sunshine".': {
1411 '~': '-',
1412 '~': '-',
1413 }, 'Please use notation "<ok>" for "<ko>". Fallback rule: 12:00-14:00 || "call us"': {
1414 'otherwise': '||',
1415 }, 'You can use notation "<ok>" for "<ko>" temporally if the syntax will still be valid.': {
1416 '?': 'unknown "please add this if known"',
1417 }, 'Please use notation "<ok>" for "<ko>". Although using "–" is typographical correct, the opening_hours syntax is defined with the normal hyphen. Correct typography should be done on application level …': {
1418 '–': '-',
1419 }, 'Please use notation "<ok>" for "<ko>".': {
1420 '→': '-',
1421 '−': '-',
1422 '=': '-',
1423 'ー': '-',
1424 'to': '-',
1425 'до': '-',
1426 'a': '-', // language unknown
1427 'as': '-', // language unknown
1428 'á': '-', // language unknown
1429 'ás': '-', // language unknown
1430 'às': '-', // language unknown
1431 'ate': '-', // language unknown
1432 'till': '-',
1433 'til': '-',
1434 'until': '-',
1435 'through': '-',
1436 'and': ',',
1437 '&': ',',
1438 // '/': ',', // Can not be corrected as / is a valid token
1439 ':': ':',
1440 '°°': ':00',
1441 'always': '24/7',
1442 'nonstop': '24/7',
1443 '24x7': '24/7',
1444 'anytime': '24/7',
1445 'all day': '24/7',
1446 'daily': 'Mo-Su',
1447 'everyday': 'Mo-Su',
1448 'every day': 'Mo-Su',
1449 'all days': 'Mo-Su',
1450 '7j/7': 'Mo-Su', // I guess that it means that
1451 '7/7': 'Mo-Su', // I guess that it means that
1452 /* {{{
1453 * Fixing this causes to ignore the following warning: "There should be no
1454 * reason to differ more than 6 days from a constrained
1455 * weekdays. If so tell us …".
1456 * The following mistake is expected to occur more often.
1457 */
1458 '7days': 'Mo-Su',
1459 '7 days': 'Mo-Su',
1460 // }}}
1461 '7 days a week': 'Mo-Su',
1462 '7 days/week': 'Mo-Su',
1463 '24 hours 7 days a week': '24/7',
1464 '24 hours': '00:00-24:00',
1465 'midday': '12:00',
1466 'midnight': '00:00',
1467 'holiday': 'PH',
1468 'holidays': 'PH',
1469 'public holidays': 'PH',
1470 'public holiday': 'PH',
1471 'day after public holiday': 'PH +1 day',
1472 'one day after public holiday': 'PH +1 day',
1473 'day before public holiday': 'PH -1 day',
1474 'one day before public holiday': 'PH -1 day',
1475 'school holiday': 'SH',
1476 'school holidays': 'SH',
1477 // summerholiday: 'SH',
1478 // summerholidays: 'SH',
1479 /* Not implemented {{{ */
1480 // 'day after school holiday': 'SH +1 day',
1481 // 'one day after school holiday': 'SH +1 day',
1482 // 'day before school holiday': 'SH -1 day',
1483 // 'one day before school holiday': 'SH -1 day',
1484 /* }}} */
1485 'weekend': 'Sa,Su',
1486 'weekends': 'Sa,Su',
1487 'daylight': 'sunrise-sunset',
1488 }, 'Please use notation "<ok>" for "<ko>". Those characters look very similar but are not the same!': {
1489 'оff': 'off', // Russian o
1490 }, 'Please use time format in 24 hours notation ("<ko>"). If PM is used you might have to convert the hours to the 24 hours notation.': {
1491 'pm': '',
1492 'рм': '',
1493 'am': '',
1494 'ам': '',
1495 }, 'Bitte verzichte auf "<ko>".': {
1496 'uhr': '',
1497 'geöffnet': '',
1498 'zwischen': '',
1499 }, 'Bitte verzichte auf "<ko>". Sie möchten eventuell eine Öffnungszeit ohne vorgegebenes Ende (Open End) angeben. Beispiel: "12:00+"': {
1500 'ab': '',
1501 'von': '',
1502 }, 'Es sieht so aus also möchten Sie zusätzliche Einschränkungen für eine Öffnungszeit geben. Falls sich dies nicht mit der Syntax ausdrücken lässt können Kommentare verwendet werden. Zusätzlich sollte eventuell das Schlüsselwort `open` benutzt werden. Bitte probiere "<ok>" für "<ko>".': {
1503 'damen': 'open "Damen"',
1504 'herren': 'open "Herren"',
1505 }, 'Bitte benutze die Schreibweise "<ok>" für "<ko>".': {
1506 'bis': '-',
1507 'täglich': 'Mo-Su',
1508 'schulferien': 'SH',
1509 'sonn-/feiertag': 'PH,Su',
1510 'sonn-/feiertags': 'PH,Su',
1511 'an sonn- und feiertagen': 'PH,Su',
1512 'nur sonn-/feiertags': 'PH,Su',
1513 }, 'Bitte benutze die Schreibweise "<ok>" für "<ko>". Es ist war typografisch korrekt aber laut der Spezifikation für opening_hours nicht erlaubt. Siehe auch: http://wiki.openstreetmap.org/wiki/DE:Key:opening_hours:specification.': {
1514 '„': '"',
1515 '“': '"',
1516 '”': '"',
1517 }, 'Please use notation "<ok>" for "<ko>". The used quote signs might be typographically correct but are not defined in the specification. See http://wiki.openstreetmap.org/wiki/Key:opening_hours:specification.': {
1518 '«': '"',
1519 '»': '"',
1520 '‚': '"',
1521 '‘': '"',
1522 '’': '"',
1523 '「': '"',
1524 '」': '"',
1525 '『': '"',
1526 '』': '"',
1527 }, 'Please use notation "<ok>" for "<ko>". The used quote signs are not defined in the specification. See http://wiki.openstreetmap.org/wiki/Key:opening_hours:specification.': {
1528 "'": '"',
1529 }, 'You might want to use comments instead of brackets (which are not valid in this context). If you do, replace "<ok>" with "<ko>".': {
1530 // '(': '"',
1531 // ')': '"',
1532 }, 'Bitte benutze die Schreibweise "<ok>" als Ersatz für "und" bzw. "u.".': {
1533 'und': ',',
1534 'u': ',',
1535 }, 'Bitte benutze die englische Abkürzung "<ok>" für "<ko>".': {
1536 'feiertag': 'PH',
1537 'feiertags': 'PH',
1538 'feiertage': 'PH',
1539 'feiertagen': 'PH'
1540 }, 'S\'il vous plaît utiliser "<ok>" pour "<ko>".': {
1541 'fermé': 'off',
1542 'et': ',',
1543 'à': '-',
1544 'jours fériés': 'PH',
1545 }
1546 }, /* }}} */
1547
1548 month: { /* {{{ */
1549 'default': {
1550 'jan': 0,
1551 'feb': 1,
1552 'mar': 2,
1553 'apr': 3,
1554 'may': 4,
1555 'jun': 5,
1556 'jul': 6,
1557 'aug': 7,
1558 'sep': 8,
1559 'oct': 9,
1560 'nov': 10,
1561 'dec': 11,
1562 }, 'Please use the English abbreviation "<ok>" for "<ko>".': {
1563 'jänner': 0, // Austria
1564 'january': 0,
1565 'february': 1,
1566 'march': 2,
1567 'april': 3,
1568 // 'may': 4,
1569 'june': 5,
1570 'july': 6,
1571 'august': 7,
1572 'september': 8,
1573 'sept': 8,
1574 'october': 9,
1575 'november': 10,
1576 'december': 11,
1577 }, 'Bitte benutze die englische Abkürzung "<ok>" für "<ko>".': {
1578 'januar': 0,
1579 'februar': 1,
1580 'märz': 2,
1581 'maerz': 2,
1582 'mai': 4,
1583 'juni': 5,
1584 'juli': 6,
1585 'okt': 9,
1586 'oktober': 9,
1587 'dez': 11,
1588 'dezember': 11,
1589 }, 'S\'il vous plaît utiliser l\'abréviation "<ok>" pour "<ko>".': {
1590 'janvier': 0,
1591 'février': 1,
1592 'fév': 1,
1593 'mars': 2,
1594 'avril': 3,
1595 'avr': 3,
1596 'mai': 4,
1597 'juin': 5,
1598 'juillet': 6,
1599 'août': 7,
1600 'aoû': 7,
1601 'septembre': 8,
1602 'octobre': 9,
1603 'novembre': 10,
1604 'décembre': 11,
1605 }, 'Neem de engelse afkorting "<ok>" voor "<ko>" alstublieft.': {
1606 'januari': 0,
1607 'februari': 1,
1608 'maart': 2,
1609 'mei': 4,
1610 'augustus': 7,
1611 }
1612 }, /* }}} */
1613
1614 calcday: {
1615 'default': {
1616 'day': 'day',
1617 'days': 'days',
1618 },
1619 },
1620
1621 weekday: { // {{{ Good source: http://www.omniglot.com/language/time/days.htm */
1622 'default': {
1623 'su': 0,
1624 'mo': 1,
1625 'tu': 2,
1626 'we': 3,
1627 'th': 4,
1628 'fr': 5,
1629 'sa': 6,
1630 }, 'Assuming "<ok>" for "<ko>"': {
1631 'm': 1,
1632 'w': 3,
1633 'f': 5,
1634 }, 'Please use the abbreviation "<ok>" for "<ko>".': {
1635 'sun': 0,
1636 'sunday': 0,
1637 'sundays': 0,
1638 'mon': 1,
1639 'monday': 1,
1640 'mondays': 1,
1641 'tue': 2,
1642 'tues': 2, // Used here: http://www.westerhambeauty.co.uk/contact.php
1643 'tuesday': 2,
1644 'tuesdays': 2,
1645 'wed': 3,
1646 'weds': 3,
1647 'wednesday': 3,
1648 'wednesdays': 3,
1649 'thu': 4,
1650 'thur': 4,
1651 'thurs': 4,
1652 'thursday': 4,
1653 'thursdays': 4,
1654 'fri': 5,
1655 'friday': 5,
1656 'fridays': 5,
1657 'sat': 6,
1658 'saturday': 6,
1659 'saturdays': 6,
1660 }, 'Bitte benutze die englische Abkürzung "<ok>" für "<ko>". Could also mean Saturday in Polish …': {
1661 'so': 0,
1662 }, 'Bitte benutze die englische Abkürzung "<ok>" für "<ko>".': {
1663 'son': 0,
1664 'sonntag': 0,
1665 'sonn-': 0,
1666 'sonntags': 0,
1667 'montag': 1,
1668 'montags': 1,
1669 'di': 2,
1670 'die': 2,
1671 'dienstag': 2,
1672 'dienstags': 2,
1673 'mi': 3,
1674 'mit': 3,
1675 'mittwoch': 3,
1676 'mittwochs': 3,
1677 'do': 4,
1678 'don': 4,
1679 'donnerstag': 4,
1680 'donnerstags': 4,
1681 'fre': 5,
1682 'freitag': 5,
1683 'freitags': 5,
1684 'sam': 6,
1685 'samstag': 6,
1686 'samstags': 6,
1687 }, 'S\'il vous plaît utiliser l\'abréviation "<ok>" pour "<ko>".': {
1688 'dim': 0,
1689 'dimanche': 0,
1690 'lu': 1,
1691 'lun': 1,
1692 'lundi': 1,
1693 'mardi': 2,
1694 'mer': 3,
1695 'mercredi': 3,
1696 'je': 4,
1697 'jeu': 4,
1698 'jeudi': 4,
1699 've': 5,
1700 'ven': 5,
1701 'vendredi': 5,
1702 'samedi': 6,
1703 }, 'Neem de engelse afkorting "<ok>" voor "<ko>" alstublieft.': {
1704 'zo': 0,
1705 'zon': 0,
1706 'zontag': 0, // correct?
1707 'zondag': 0,
1708 'maandag': 1,
1709 'din': 2,
1710 'dinsdag': 2,
1711 'wo': 3,
1712 'woe': 3,
1713 'woensdag': 3,
1714 'donderdag': 4,
1715 'vr': 5,
1716 'vri': 5,
1717 'vrijdag': 5,
1718 'za': 6,
1719 'zat': 6,
1720 'zaterdag': 6,
1721 }, 'Please use the English abbreviation "<ok>" for "<ko>".': { // FIXME: Translate to Czech.
1722 'neděle': 0,
1723 'ne': 0,
1724 'pondělí': 1,
1725 'po': 1,
1726 'úterý': 2,
1727 'út': 2,
1728 'středa': 3,
1729 'st': 3,
1730 'čtvrtek': 4,
1731 'čt': 4,
1732 'pátek': 5,
1733 'pá': 5,
1734 'sobota': 6,
1735 }, 'Please use the English abbreviation "<ok>" (Spanish) for "<ko>".': {
1736 'martes': 0,
1737 'miércoles': 1,
1738 'jueves': 2,
1739 'viernes': 3,
1740 'sábado': 4,
1741 'domingo': 5,
1742 'lunes': 6,
1743 }, 'Please use the English abbreviation "<ok>" (Indonesian) for "<ko>".': {
1744 'selasa': 0,
1745 'rabu': 1,
1746 'kami': 2,
1747 'jumat': 3,
1748 'sabtu': 4,
1749 'minggu': 5,
1750 'senin': 6,
1751 }, 'Please use the English abbreviation "<ok>" (Swedish) for "<ko>".': {
1752 'söndag': 0,
1753 'söndagar': 0,
1754 'måndag': 1,
1755 'ma': 1,
1756 'tisdag': 2,
1757 'onsdag': 3, // Same in Danish
1758 'torsdag': 4,
1759 'fredag': 5,
1760 'lördag': 6,
1761 'lördagar': 6,
1762 }, 'Please use the English abbreviation "<ok>" (Polish) for "<ko>".': {
1763 'niedziela': 0, 'niedz': 0, 'n': 0, 'ndz': 0,
1764 'poniedziałek': 1, 'poniedzialek': 1, 'pon': 1, 'pn': 1,
1765 'wtorek': 2, 'wt': 2,
1766 'środa': 3, 'sroda': 3, 'śr': 3, 'sr': 3,
1767 'czwartek': 4, 'czw': 4, 'cz': 4,
1768 'piątek': 5, 'piatek': 5, 'pt': 5,
1769 'sobota': 6, 'sob': 6, // 'so': 6 // abbreviation also used in German
1770 }, 'Please use the English abbreviation "<ok>" (Russian) for "<ko>".': {
1771 'воскресенье' : 0,
1772 'Вс' : 0,
1773 "voskresen'ye": 0,
1774 'понедельник' : 1,
1775 'Пн' : 1,
1776 "ponedel'nik" : 1,
1777 'вторник' : 2,
1778 'vtornik' : 2,
1779 'среда' : 3,
1780 'sreda' : 3,
1781 'четверг' : 4,
1782 'chetverk' : 4,
1783 'пятница' : 5,
1784 'pyatnitsa' : 5,
1785 'суббота' : 6,
1786 'subbota' : 6,
1787 }, 'Please use the English abbreviation "<ok>" (Danish) for "<ko>".': {
1788 'søndag' : 0,
1789 'mandag' : 1,
1790 'tirsdag': 2,
1791 'onsdag' : 3, // Same in Swedish
1792 'torsdag': 4,
1793 'fredag' : 5,
1794 'lørdag' : 6,
1795 },
1796 }, /* }}} */
1797
1798 timevar: { /* {{{ Special time variables which actual value depends on the date and the position of the facility. */
1799 'default': {
1800 'sunrise': 'sunrise',
1801 'sunset': 'sunset',
1802 'dawn': 'dawn',
1803 'dusk': 'dusk',
1804 }, 'Please use notation "<ok>" for "<ko>".': {
1805 'sundown': 'sunset',
1806 }, 'Bitte benutze die Schreibweise "<ok>" für "<ko>".': {
1807 'morgendämmerung': 'dawn',
1808 'abenddämmerung': 'dusk',
1809 'sonnenaufgang': 'sunrise',
1810 'sonnenuntergang': 'sunset',
1811 },
1812 }, /* }}} */
1813
1814 'event': { // variable events
1815 'default': {
1816 'easter': 'easter',
1817 }, 'Bitte benutze die Schreibweise "<ok>" für "<ko>".': {
1818 'ostern': 'easter',
1819 },
1820 },
1821 };
1822 // }}}
1823 // }}}
1824
1825 // make the library accessible for the outside world {{{
1826 if (typeof exports === 'object') {
1827 // For nodejs
1828 var SunCalc = require('suncalc');
1829 module.exports = factory(SunCalc, holidays, word_error_correction);
1830 } else {
1831 // For browsers
1832 root.opening_hours = factory(root.SunCalc, holidays, word_error_correction);
1833 }
1834 /// }}}
1835}(this, function (SunCalc, holidays, word_error_correction) {
1836 return function(value, nominatiomJSON, oh_mode) {
1837 // short constants {{{
1838 var word_value_replacement = { // If the correct values can not be calculated.
1839 dawn : 60 * 5 + 30,
1840 sunrise : 60 * 6,
1841 sunset : 60 * 18,
1842 dusk : 60 * 18 + 30,
1843 };
1844 var months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
1845 var weekdays = ['Su','Mo','Tu','We','Th','Fr','Sa'];
1846 var default_prettify_conf = {
1847 // Update README.md if changed.
1848 'zero_pad_hour': true, // enforce ("%02d", hour)
1849 'one_zero_if_hour_zero': false, // only one zero "0" if hour is zero "0"
1850 'leave_off_closed': true, // leave keywords "off" and "closed" as is
1851 'keyword_for_off_closed': 'off', // use given keyword instead of "off" or "closed"
1852 'rule_sep_string': ' ', // separate rules by string
1853 'print_semicolon': true, // print token which separates normal rules
1854 '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
1855 'sep_one_day_between': ',', // separator which should be used
1856 'zero_pad_month_and_week_numbers': false, // Format week (e.g. `week 01`) and month day numbers (e.g. `Jan 01`) with "%02d".
1857 };
1858
1859 var minutes_in_day = 60 * 24;
1860 var msec_in_day = 1000 * 60 * minutes_in_day;
1861 var msec_in_week = msec_in_day * 7;
1862
1863 var library_name = 'opening_hours.js';
1864 var repository_url = 'https://github.com/ypid/' + library_name;
1865 var issues_url = repository_url + '/issues?state=open';
1866 // }}}
1867
1868 // constructor parameters {{{
1869 // Evaluate additional information which can be given. They are
1870 // required to reasonably calculate 'sunrise' and to use the correct
1871 // holidays.
1872 var location_cc, location_state, lat, lon;
1873 if (typeof nominatiomJSON != 'undefined') {
1874 if (typeof nominatiomJSON.address != 'undefined' &&
1875 typeof nominatiomJSON.address.state != 'undefined') { // country_code will be tested later …
1876 location_cc = nominatiomJSON.address.country_code;
1877 location_state = nominatiomJSON.address.state;
1878 }
1879
1880 if (typeof nominatiomJSON.lon != 'undefined') { // lat will be tested later …
1881 lat = nominatiomJSON.lat;
1882 lon = nominatiomJSON.lon;
1883 }
1884 }
1885
1886 // 0: time ranges (default), tags: opening_hours, lit, …
1887 // 1: points in time
1888 // 2: both (time ranges and points in time), tags: collection_times, service_times
1889 if (typeof oh_mode == 'undefined') {
1890 oh_mode = 0;
1891 } else if (!(typeof oh_mode == 'number' && (oh_mode === 0 || oh_mode == 1 || oh_mode == 2))) {
1892 throw 'The third constructor parameter is oh_mode and must be a number (0, 1 or 2)';
1893 }
1894 // }}}
1895
1896 // Tokenize value and generate selector functions. {{{
1897 if (value.match(/^(?:\s*;?\s*)+$/))
1898 throw 'Value contains nothing meaningful which can be parsed';
1899
1900 var parsing_warnings = []; // Elements are fed into function formatWarnErrorMessage(nrule, at, message)
1901 var done_with_warnings = false; // The functions which throw warnings can be called multiple times.
1902 var done_with_selector_reordering = false;
1903 var done_with_selector_reordering_warnings = false;
1904 var tokens = tokenize(value);
1905 // console.log(JSON.stringify(tokens, null, ' '));
1906 var prettified_value = '';
1907 var week_stable = true;
1908
1909 var rules = [];
1910 var new_tokens = [];
1911
1912 for (var nrule = 0; nrule < tokens.length; nrule++) {
1913 if (tokens[nrule][0].length === 0) {
1914 // Rule does contain nothing useful e.g. second rule of '10:00-12:00;' (empty) which needs to be handled.
1915 parsing_warnings.push([nrule, -1,
1916 'This rule does not contain anything useful. Please remove this empty rule.'
1917 + (nrule == tokens.length - 1 && nrule > 0 && !tokens[nrule][1] ?
1918 ' Might it be possible that you are a programmer and adding a semicolon after each statement is hardwired in your muscle memory ;) ?'
1919 + ' The thing is that the semicolon in the opening_hours syntax is defined as rule separator.'
1920 + ' So for compatibility reasons you should omit this last semicolon.': '')
1921 ]);
1922 continue;
1923 }
1924
1925 var continue_at = 0;
1926 var next_rule_is_additional = false;
1927 do {
1928 if (continue_at == tokens[nrule][0].length) break;
1929 // Additional rule does contain nothing useful e.g. second rule of '10:00-12:00,' (empty) which needs to be handled.
1930
1931 var selectors = {
1932 // Time selectors
1933 time: [],
1934
1935 // Temporary array of selectors from time wrapped to the next day
1936 wraptime: [],
1937
1938 // Date selectors
1939 weekday: [],
1940 holiday: [],
1941 week: [],
1942 month: [],
1943 monthday: [],
1944 year: [],
1945
1946 // Array with non-empty date selector types, with most optimal ordering
1947 date: [],
1948
1949 fallback: tokens[nrule][1],
1950 additional: continue_at ? true : false,
1951 meaning: true,
1952 unknown: false,
1953 comment: undefined,
1954 build_from_token_rule: undefined,
1955 };
1956
1957 selectors.build_from_token_rule = [ nrule, continue_at, new_tokens.length ];
1958 continue_at = parseGroup(tokens[nrule][0], continue_at, selectors, nrule);
1959 if (typeof continue_at == 'object') {
1960 continue_at = continue_at[0];
1961 } else {
1962 continue_at = 0;
1963 }
1964
1965 // console.log('Current tokens: ' + JSON.stringify(tokens[nrule], null, ' '));
1966
1967 new_tokens.push(
1968 [
1969 tokens[nrule][0].slice(
1970 selectors.build_from_token_rule[1],
1971 continue_at === 0
1972 ? tokens[nrule][0].length
1973 : continue_at
1974 ),
1975 tokens[nrule][1],
1976 tokens[nrule][2],
1977 ]
1978 );
1979
1980 if (next_rule_is_additional && new_tokens.length > 1) {
1981 // Move 'rule separator' from last token of last rule to first token of this rule.
1982 new_tokens[new_tokens.length - 1][0].unshift(new_tokens[new_tokens.length - 2][0].pop());
1983 }
1984
1985 next_rule_is_additional = continue_at === 0 ? false : true;
1986
1987 if (selectors.year.length > 0)
1988 selectors.date.push(selectors.year);
1989 if (selectors.holiday.length > 0)
1990 selectors.date.push(selectors.holiday);
1991 if (selectors.month.length > 0)
1992 selectors.date.push(selectors.month);
1993 if (selectors.monthday.length > 0)
1994 selectors.date.push(selectors.monthday);
1995 if (selectors.week.length > 0)
1996 selectors.date.push(selectors.week);
1997 if (selectors.weekday.length > 0)
1998 selectors.date.push(selectors.weekday);
1999
2000 // console.log('weekday: ' + JSON.stringify(selectors.weekday, null, '\t'));
2001 rules.push(selectors);
2002
2003 // This handles selectors with time ranges wrapping over midnight (e.g. 10:00-02:00)
2004 // it generates wrappers for all selectors and creates a new rule.
2005 if (selectors.wraptime.length > 0) {
2006 var wrapselectors = {
2007 time: selectors.wraptime,
2008 date: [],
2009
2010 meaning: selectors.meaning,
2011 unknown: selectors.unknown,
2012 comment: selectors.comment,
2013
2014 wrapped: true,
2015 // build_from_token_rule: selectors.build_from_token_rule,
2016 // Not (yet) needed.
2017 };
2018
2019 for (var dselg = 0; dselg < selectors.date.length; dselg++) {
2020 wrapselectors.date.push([]);
2021 for (var dsel = 0; dsel < selectors.date[dselg].length; dsel++) {
2022 wrapselectors.date[wrapselectors.date.length-1].push(
2023 generateDateShifter(selectors.date[dselg][dsel], -msec_in_day)
2024 );
2025 }
2026 }
2027
2028 rules.push(wrapselectors);
2029 }
2030 } while (continue_at);
2031 }
2032 // console.log(JSON.stringify(tokens, null, ' '));
2033 // console.log(JSON.stringify(new_tokens, null, ' '));
2034 // }}}
2035
2036 /* Format warning or error message for the user. {{{
2037 *
2038 * :param nrule: Rule number starting with zero.
2039 * :param at: Token position at which the issue occurred.
2040 * :param message: Human readable string with the message.
2041 * :returns: String with position of the warning or error marked for the user.
2042 */
2043 function formatWarnErrorMessage(nrule, at, message) {
2044 // FIXME: Change to new_tokens.
2045 if (typeof nrule == 'number') {
2046 var pos = 0;
2047 if (nrule == -1) { // Usage of rule index not required because we do have access to value.length.
2048 pos = value.length - at;
2049 } else { // Issue accrued at a later time, position in string needs to be reconstructed.
2050 if (typeof tokens[nrule][0][at] == 'undefined') {
2051 if (typeof tokens[nrule][0] && at == -1) {
2052 pos = value.length;
2053 if (typeof tokens[nrule+1] == 'object' && typeof tokens[nrule+1][2] == 'number') {
2054 pos -= tokens[nrule+1][2];
2055 } else if (typeof tokens[nrule][2] == 'number') {
2056 pos -= tokens[nrule][2];
2057 }
2058 } else {
2059 // Given position is invalid.
2060 //
2061 formatLibraryBugMessage('Bug in warning generation code which could not determine the exact position of the warning or error in value.');
2062 pos = value.length;
2063 if (typeof tokens[nrule][2] != 'undefined') {
2064 // Fallback: Point to last token in the rule which caused the problem.
2065 // Run real_test regularly to fix the problem before a user is confronted with it.
2066 pos -= tokens[nrule][2];
2067 console.warn('Last token for rule: ' + tokens[nrule]);
2068 console.log(value.substring(0, pos) + ' <--- (' + message + ')');
2069 console.log('\n');
2070 } {
2071 console.warn('tokens[nrule][2] is undefined. This is ok if nrule is the last rule.');
2072 }
2073 }
2074 } else {
2075 pos = value.length;
2076 if (typeof tokens[nrule][0][at+1] != 'undefined') {
2077 pos -= tokens[nrule][0][at+1][2];
2078 } else if (typeof tokens[nrule][2] != 'undefined') {
2079 pos -= tokens[nrule][2];
2080 }
2081 }
2082 }
2083 return value.substring(0, pos) + ' <--- (' + message + ')';
2084 } else if (typeof nrule == 'string') {
2085 return nrule.substring(0, at) + ' <--- (' + message + ')';
2086 }
2087 }
2088 // }}}
2089
2090 /* Format internal library error message. {{{
2091 *
2092 * :param message: Human readable string with the error message.
2093 * :returns: Error message for the user.
2094 */
2095 function formatLibraryBugMessage(message) {
2096 if (typeof message == 'undefined')
2097 message = '';
2098 else
2099 message = ' ' + message;
2100
2101 message = 'An error occurred during evaluation of the value "' + value + '".'
2102 + ' Please file a bug report here: ' + issues_url + '.'
2103 + message;
2104 console.log(message);
2105 return message;
2106 }
2107 // }}}
2108
2109 /* Tokenize input stream. {{{
2110 *
2111 * :param value: Raw opening_hours value.
2112 * :returns: Tokenized list object. Complex structure. Check the
2113 * internal documentation in the doc directory for details.
2114 */
2115 function tokenize(value) {
2116 var all_tokens = [];
2117 var curr_rule_tokens = [];
2118
2119 var last_rule_fallback_terminated = false;
2120
2121 while (value !== '') {
2122 // console.log("Parsing value: " + value);
2123 var tmp;
2124 if (tmp = value.match(/^week\b/i)) {
2125 // Reserved keywords.
2126 curr_rule_tokens.push([tmp[0].toLowerCase(), tmp[0].toLowerCase(), value.length ]);
2127 value = value.substr(tmp[0].length);
2128 } else if (tmp = value.match(/^(?:off\b|closed\b|open\b|unknown\b)/i)) {
2129 // Reserved keywords.
2130 curr_rule_tokens.push([tmp[0].toLowerCase(), 'state', value.length ]);
2131 value = value.substr(tmp[0].length);
2132 } else if (tmp = value.match(/^24\/7/i)) {
2133 // Reserved keyword.
2134 curr_rule_tokens.push([tmp[0], tmp[0], value.length ]);
2135 value = value.substr(tmp[0].length);
2136 } else if (tmp = value.match(/^(?:PH|SH)/i)) {
2137 // special day name (holidays)
2138 curr_rule_tokens.push([tmp[0].toUpperCase(), 'holiday', value.length ]);
2139 value = value.substr(2);
2140 } else if (tmp = value.match(/^(&|_|→|–|−|=|·|opening_hours=|ー|\?|~|~|:|°°|24x7|24 hours 7 days a week|24 hours|7 ?days(?:(?: a |\/)week)?|7j?\/7|all days?|every day|-?(?:(?:till? )?late|open[ ]?end)|(?:(?:one )?day (?:before|after) )?(?:school|public) holidays?|days?\b|до|рм|ам|jours fériés|on work days?|(?:nur |an )?sonn-(?:(?: und |\/)feiertag(?:s|en))?|[a-zäößàáéøčěíúýřПнВсо]+\b|à|á|mo|tu|we|th|fr|sa|su|jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)\.?/i)) {
2141 /* Handle all remaining words and specific other characters with error tolerance.
2142 *
2143 * à|á: Word boundary does not work with unicode chars: 'test à test'.match(/\bà\b/i)
2144 * https://stackoverflow.com/questions/10590098/javascript-regexp-word-boundaries-unicode-characters
2145 * Order in the regular expression capturing group is important in some cases.
2146 *
2147 * mo|tu|we|th|fr|sa|su|jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec: Prefer defended keywords
2148 * if used in cases like 'mo12:00-14:00' (when keyword is followed by number).
2149 */
2150 var correct_val = returnCorrectWordOrToken(tmp[1].toLowerCase(), value.length);
2151 // console.log('Error tolerance for string "' + tmp[1] + '" returned "' + correct_val + '".');
2152 if (typeof correct_val == 'object') {
2153 curr_rule_tokens.push([ correct_val[0], correct_val[1], value.length ]);
2154 value = value.substr(tmp[0].length);
2155 } else if (typeof correct_val == 'string') {
2156 if (tmp[1].toLowerCase() == 'pm') {
2157 var hours_token_at = curr_rule_tokens.length - 1;
2158 var hours_token;
2159 if (hours_token_at >= 0) {
2160 if (hours_token_at -2 >= 0 &&
2161 matchTokens(
2162 curr_rule_tokens, hours_token_at - 2,
2163 'number', 'timesep', 'number'
2164 )
2165 ) {
2166 hours_token_at -= 2;
2167 hours_token = curr_rule_tokens[hours_token_at];
2168 } else if (matchTokens(curr_rule_tokens, hours_token_at, 'number')) {
2169 hours_token = curr_rule_tokens[hours_token_at];
2170 }
2171
2172 if (typeof hours_token == 'object' && hours_token[0] <= 12) {
2173 hours_token[0] += 12;
2174 curr_rule_tokens[hours_token_at] = hours_token;
2175 }
2176 }
2177 }
2178 var correct_tokens = tokenize(correct_val)[0];
2179 if (correct_tokens[1] === true) { // last_rule_fallback_terminated
2180 throw formatLibraryBugMessage();
2181 }
2182 for (var i = 0; i < correct_tokens[0].length; i++) {
2183 curr_rule_tokens.push([correct_tokens[0][i][0], correct_tokens[0][i][1], value.length]);
2184 // value.length - tmp[0].length does not have the desired effect for all test cases.
2185 }
2186
2187 value = value.substr(tmp[0].length);
2188 // value = correct_val + value.substr(tmp[0].length);
2189 // Does not work because it would generate the wrong length for formatWarnErrorMessage.
2190 } else {
2191 // other single-character tokens
2192 curr_rule_tokens.push([value[0].toLowerCase(), value[0].toLowerCase(), value.length - 1 ]);
2193 value = value.substr(1);
2194 }
2195 } else if (tmp = value.match(/^\d+/)) {
2196 // number
2197 if (Number(tmp[0]) > 1900) { // Assumed to be a year number.
2198 curr_rule_tokens.push([tmp[0], 'year', value.length ]);
2199 if (Number(tmp[0]) >= 2100) // Probably an error
2200 parsing_warnings.push([ -1, value.length - 1,
2201 'The number ' + Number(tmp[0]) + ' will be interpreted as year.'
2202 + ' This is probably not intended. Times can be specified as "12:00".'
2203 ]);
2204 } else {
2205 curr_rule_tokens.push([Number(tmp[0]), 'number', value.length ]);
2206 }
2207
2208 value = value.substr(tmp[0].length);
2209 } else if (tmp = value.match(/^"([^"]+)"/)) {
2210 // Comment following the specification.
2211 // Any character is allowed inside the comment except " itself.
2212 curr_rule_tokens.push([tmp[1], 'comment', value.length ]);
2213 value = value.substr(tmp[0].length);
2214 } else if (tmp = value.match(/^(["'„“‚‘’«「『])([^"'“”‘’»」』;|]*)(["'”“‘’»」』])/)) {
2215 // Comments with error tolerance.
2216 // The comments still have to be somewhat correct meaning
2217 // the start and end quote signs used have to be
2218 // appropriate. So “testing„ will not match as it is not a
2219 // quote but rather something unknown which the user should
2220 // fix first.
2221 // console.log('Matched: ' + JSON.stringify(tmp));
2222 for (var pos = 1; pos <= 3; pos += 2) {
2223 // console.log('Pos: ' + pos + ', substring: ' + tmp[pos]);
2224 var correct_val = returnCorrectWordOrToken(tmp[pos],
2225 value.length - (pos == 3 ? tmp[1].length + tmp[2].length : 0)
2226 );
2227 if (typeof correct_val != 'string' && tmp[pos] != '"') {
2228 throw formatLibraryBugMessage(
2229 'A character for error tolerance was allowed in the regular expression'
2230 + ' but is not covered by word_error_correction'
2231 + ' which is needed to format a proper message for the user.'
2232 );
2233 }
2234 }
2235 curr_rule_tokens.push([tmp[2], 'comment', value.length ]);
2236 value = value.substr(tmp[0].length);
2237 } else if (value.match(/^;/)) {
2238 // semicolon terminates rule
2239 // next tokens belong to a new rule
2240 all_tokens.push([ curr_rule_tokens, last_rule_fallback_terminated, value.length ]);
2241 value = value.substr(1);
2242
2243 curr_rule_tokens = [];
2244 last_rule_fallback_terminated = false;
2245 } else if (value.match(/^\|\|/)) {
2246 // || terminates rule
2247 // Next tokens belong to a fallback rule.
2248 if (curr_rule_tokens.length === 0)
2249 throw formatWarnErrorMessage(-1, value.length - 2, 'Rule before fallback rule does not contain anything useful');
2250
2251 all_tokens.push([ curr_rule_tokens, last_rule_fallback_terminated, value.length ]);
2252 curr_rule_tokens = [];
2253 // curr_rule_tokens = [ [ '||', 'rule separator', value.length ] ];
2254 // FIXME: Use this. Unknown bug needs to be solved in the process.
2255 value = value.substr(2);
2256
2257 last_rule_fallback_terminated = true;
2258 } else if (value.match(/^(?:␣|\s)/)) {
2259 // Using "␣" as space is not expected to be a normal
2260 // mistake. Just ignore it to make using taginfo easier.
2261 value = value.substr(1);
2262 } else if (tmp = value.match(/^\s+/)) {
2263 // whitespace is ignored
2264 value = value.substr(tmp[0].length);
2265 } else if (value.match(/^[:.]/)) {
2266 // time separator
2267 if (value[0] == '.' && !done_with_warnings)
2268 parsing_warnings.push([ -1, value.length - 1, 'Please use ":" as hour/minute-separator' ]);
2269 curr_rule_tokens.push([ ':', 'timesep', value.length ]);
2270 value = value.substr(1);
2271 } else {
2272 // other single-character tokens
2273 curr_rule_tokens.push([value[0].toLowerCase(), value[0].toLowerCase(), value.length ]);
2274 value = value.substr(1);
2275 }
2276 }
2277
2278 all_tokens.push([ curr_rule_tokens, last_rule_fallback_terminated ]);
2279
2280 return all_tokens;
2281 }
2282 // }}}
2283
2284 /* error correction/tolerance function {{{
2285 * Go through word_error_correction hash and get correct value back.
2286 *
2287 * :param word: Wrong Word or character.
2288 * :param value_length: Current value_length (used for warnings).
2289 * :returns:
2290 * * (valid) opening_hours sub string.
2291 * * object with [ internal_value, token_name ] if value is correct.
2292 * * undefined if word could not be found (and thus is not be corrected).
2293 */
2294 function returnCorrectWordOrToken(word, value_length) {
2295 for (var token_name in word_error_correction) {
2296 for (var comment in word_error_correction[token_name]) {
2297 for (var old_val in word_error_correction[token_name][comment]) {
2298 if (old_val == word) {
2299 var val = word_error_correction[token_name][comment][old_val];
2300 if (comment == 'default') {
2301 // Return internal representation of word.
2302 return [ val, token_name ];
2303 } else if (token_name == 'wrong_words' && !done_with_warnings) {
2304 // Replace wrong words or characters with correct ones.
2305 // This will return a string which is then being tokenized.
2306 parsing_warnings.push([ -1, value_length - old_val.length,
2307 comment.replace(/<ko>/, old_val).replace(/<ok>/, val) ]);
2308 return val;
2309 } else {
2310 // Get correct string value from the 'default' hash and generate warning.
2311 var correct_abbr;
2312 for (correct_abbr in word_error_correction[token_name]['default']) {
2313 if (word_error_correction[token_name]['default'][correct_abbr] == val)
2314 break;
2315 }
2316 if (typeof correct_abbr == 'undefined') {
2317 throw formatLibraryBugMessage('Including the stacktrace.');
2318 }
2319 if (token_name != 'timevar') {
2320 // Everything else than timevar:
2321 // E.g. 'Mo' start with a upper case letter.
2322 // It just looks better.
2323 correct_abbr = correct_abbr.charAt(0).toUpperCase()
2324 + correct_abbr.slice(1);
2325 }
2326 if (!done_with_warnings)
2327 parsing_warnings.push([ -1, value_length - old_val.length,
2328 comment.replace(/<ko>/, old_val).replace(/<ok>/, correct_abbr) ]);
2329 return [ val, token_name ];
2330 }
2331 }
2332 }
2333 }
2334 }
2335 return undefined;
2336 }
2337 // }}}
2338
2339 /* return warnings as list {{{
2340 *
2341 * :param it: Iterator object if available (optional).
2342 * :returns: Warnings as list with one warning per element.
2343 */
2344 function getWarnings(it) {
2345 if (!done_with_warnings && typeof it == 'object') {
2346 /* getWarnings was called in a state without critical errors.
2347 * We can do extended tests.
2348 */
2349
2350 /* Place all tests in this function if an additional (high
2351 * level) test is added and this does not require to rewrite
2352 * big parts of (sub) selector parsers only to get the
2353 * position. If that is the case, then rather place the test
2354 * code in the (sub) selector parser function directly.
2355 */
2356
2357 var wide_range_selectors = [ 'year', 'month', 'week', 'holiday' ];
2358 var small_range_selectors = [ 'weekday', 'time', '24/7', 'state', 'comment'];
2359
2360 // How many times was a selector_type used per rule? {{{
2361 var used_selectors = [];
2362 var used_selectors_types_array = [];
2363 var has_token = {};
2364
2365 for (var nrule = 0; nrule < new_tokens.length; nrule++) {
2366 if (new_tokens[nrule][0].length === 0) continue;
2367 // Rule does contain nothing useful e.g. second rule of '10:00-12:00;' (empty) which needs to be handled.
2368
2369 var selector_start_end_type = [ 0, 0, undefined ],
2370 prettified_group_value = [];
2371 // console.log(new_tokens[nrule][0]);
2372
2373 used_selectors[nrule] = {};
2374 used_selectors_types_array[nrule] = [];
2375
2376 do {
2377 selector_start_end_type = getSelectorRange(new_tokens[nrule][0], selector_start_end_type[1]);
2378 // console.log(selector_start_end_type, new_tokens[nrule][0].length);
2379
2380 if (selector_start_end_type[0] == selector_start_end_type[1] &&
2381 new_tokens[nrule][0][selector_start_end_type[0]][0] == '24/7'
2382 ) {
2383 has_token['24/7'] = true;
2384 }
2385
2386 if (typeof used_selectors[nrule][selector_start_end_type[2]] != 'object') {
2387 used_selectors[nrule][selector_start_end_type[2]] = [ selector_start_end_type[1] ];
2388 } else {
2389 used_selectors[nrule][selector_start_end_type[2]].push(selector_start_end_type[1]);
2390 }
2391 used_selectors_types_array[nrule].push(selector_start_end_type[2]);
2392
2393 selector_start_end_type[1]++;
2394 } while (selector_start_end_type[1] < new_tokens[nrule][0].length);
2395 }
2396 // console.log('used_selectors: ' + JSON.stringify(used_selectors, null, ' '));
2397 // }}}
2398
2399 for (var nrule = 0; nrule < used_selectors.length; nrule++) {
2400
2401 /* Check if more than one not connected selector of the same type is used in one rule {{{ */
2402 for (var selector_type in used_selectors[nrule]) {
2403 // console.log(selector_type + ' use at: ' + used_selectors[nrule][selector_type].length);
2404 if (used_selectors[nrule][selector_type].length > 1) {
2405 parsing_warnings.push([nrule, used_selectors[nrule][selector_type][used_selectors[nrule][selector_type].length - 1],
2406 'You have used ' + used_selectors[nrule][selector_type].length
2407 + (selector_type.match(/^(?:comment|state)/) ?
2408 ' ' + selector_type
2409 + (selector_type == 'state' ? ' keywords' : 's')
2410 + ' in one rule.'
2411 + ' You may only use one in one rule.'
2412 :
2413 ' not connected ' + selector_type
2414 + (selector_type.match(/^(?:month|weekday)$/) ? 's' : ' ranges')
2415 + ' in one rule.'
2416 + ' This is probably an error.'
2417 + ' Equal selector types can (and should) always be written in conjunction separated by comma or something.'
2418 + ' Example for time ranges "12:00-13:00,15:00-18:00".'
2419 + ' Example for weekdays "Mo-We,Fr".'
2420 )
2421 + ' Rules can be separated by ";".' ]
2422 );
2423 done_with_selector_reordering = true; // Correcting the selector order makes no sense if this kind of issue exists.
2424 }
2425 }
2426 /* }}} */
2427
2428 /* Check if change default state rule is not the first rule {{{ */
2429 if ( typeof used_selectors[nrule].state === 'object'
2430 && Object.keys(used_selectors[nrule]).length === 1
2431 ) {
2432
2433 if (nrule !== 0) {
2434 parsing_warnings.push([nrule, new_tokens[nrule][0].length - 1,
2435 "This rule which changes the default state (which is closed) for all following rules is not the first rule."
2436 + " The rule will overwrite all previous rules."
2437 + " It can be legitimate to change the default state to open for example"
2438 + " and then only specify for which times the facility is closed."
2439 ]);
2440 }
2441 /* }}} */
2442 /* Check if a rule (with state open) has no time selector {{{ */
2443 } else if (typeof used_selectors[nrule].time === 'undefined') {
2444 if ( ( typeof used_selectors[nrule].state === 'object'
2445 && new_tokens[nrule][0][used_selectors[nrule].state[0]][0] === 'open'
2446 && typeof used_selectors[nrule].comment === 'undefined'
2447 ) || ( typeof used_selectors[nrule].comment === 'undefined'
2448 && typeof used_selectors[nrule].state === 'undefined'
2449 ) &&
2450 typeof used_selectors[nrule]['24/7'] === 'undefined'
2451 ) {
2452
2453 parsing_warnings.push([nrule, new_tokens[nrule][0].length - 1,
2454 "This rule is not very explicit because there is no time selector being used."
2455 + " Please add a time selector to this rule or use a comment to make it more explicit."
2456 ]);
2457 }
2458 }
2459 /* }}} */
2460
2461 /* Check if empty comment was given {{{ */
2462 if (typeof used_selectors[nrule].comment === 'object'
2463 && new_tokens[nrule][0][used_selectors[nrule].comment[0]][0].length === 0
2464 ) {
2465
2466 parsing_warnings.push([nrule, used_selectors[nrule].comment[0],
2467 "You have used an empty comment."
2468 + " Please either write something in the comment or use the keyword unknown instead."
2469 ]);
2470 }
2471 /* }}} */
2472
2473 /* Check if rule with closed|off modifier is additional {{{ */
2474 /* FIXME: Enable this test. */
2475 if (typeof new_tokens[nrule][0][0] === 'object'
2476 && new_tokens[nrule][0][0][0] === ','
2477 && new_tokens[nrule][0][0][1] === 'rule separator'
2478 && typeof used_selectors[nrule].state === 'object'
2479 && (
2480 new_tokens[nrule][0][used_selectors[nrule].state[0]][0] === 'closed'
2481 || new_tokens[nrule][0][used_selectors[nrule].state[0]][0] === 'off'
2482 )
2483 ) {
2484
2485 // parsing_warnings.push([nrule, new_tokens[nrule][0].length - 1,
2486 // "This rule will be evaluated as closed but it was specified as additional rule."
2487 // + " It is enough to specify this rule as normal rule using the \";\" character."
2488 // + " See https://wiki.openstreetmap.org/wiki/Key:opening_hours:specification#explain:rule_modifier:closed."
2489 // ]);
2490 }
2491 /* }}} */
2492
2493 /* Check for valid use of <separator_for_readability> {{{ */
2494 for (var i = 0; i < used_selectors_types_array[nrule].length - 1; i++) {
2495 var selector_type = used_selectors_types_array[nrule][i];
2496 var next_selector_type = used_selectors_types_array[nrule][i+1];
2497 if ( ( wide_range_selectors.indexOf(selector_type) != -1
2498 && wide_range_selectors.indexOf(next_selector_type) != -1
2499 ) || ( small_range_selectors.indexOf(selector_type) != -1
2500 && small_range_selectors.indexOf(next_selector_type) != -1)
2501 ) {
2502
2503 if (new_tokens[nrule][0][used_selectors[nrule][selector_type][0]][0] == ':') {
2504 parsing_warnings.push([nrule, used_selectors[nrule][selector_type][0],
2505 "You have used the optional symbol <separator_for_readability> in the wrong place."
2506 + " Please check the syntax specification to see where it could be used or remove it."
2507 ]);
2508 }
2509 }
2510 }
2511 /* }}} */
2512
2513 }
2514
2515 /* Check if 24/7 is used and it does not mean 24/7 because there are other rules {{{ */
2516 var has_advanced = it.advance();
2517
2518 if (has_advanced === true && has_token['24/7'] && !done_with_warnings) {
2519 parsing_warnings.push([ -1, 0, 'You used 24/7 in a way that is probably not interpreted as "24 hours 7 days a week".'
2520 // Probably because of: "24/7; 12:00-14:00 open", ". Needs extra testing.
2521 + ' For correctness you might want to use "open" or "closed"'
2522 + ' for this rule and then write your exceptions which should achieve the same goal and is more clear'
2523 + ' e.g. "open; Mo 12:00-14:00 off".']);
2524 }
2525 /* }}} */
2526
2527 prettifyValue();
2528 }
2529 done_with_warnings = true;
2530
2531 var warnings = [];
2532 // FIXME: Sort based on parsing_warnings[1], tricky …
2533 for (var i = 0; i < parsing_warnings.length; i++) {
2534 warnings.push( formatWarnErrorMessage(parsing_warnings[i][0], parsing_warnings[i][1], parsing_warnings[i][2]) );
2535 }
2536 return warnings;
2537 }
2538
2539 /* Helpers for getWarnings {{{ */
2540
2541 /* Check if token is the begin of a selector and why. {{{
2542 *
2543 * :param tokens: List of token objects.
2544 * :param at: Position where to start.
2545 * :returns:
2546 * * false the current token is not the begin of a selector.
2547 * * Position in token array from where the decision was made that
2548 * the token is the start of a selector.
2549 */
2550 function tokenIsTheBeginOfSelector(tokens, at) {
2551 if (typeof tokens[at][3] == 'string') {
2552 return 3;
2553 } else if (tokens[at][1] == 'comment'
2554 || tokens[at][1] == 'state'
2555 || tokens[at][1] == '24/7'
2556 || tokens[at][1] == 'rule separator'
2557 ){
2558
2559 return 1;
2560 } else {
2561 return false;
2562 }
2563 }
2564 /* }}} */
2565
2566 /* Get start and end position of a selector. {{{
2567 * For example this value 'Mo-We,Fr' will return the position of the
2568 * token lexeme 'Mo' and 'Fr' e.g. there indexes [ 0, 4 ] in the
2569 * selector array of tokens.
2570 *
2571 * :param tokens: List of token objects.
2572 * :param at: Position where to start.
2573 * :returns: Array:
2574 * 0. Index of first token in selector array of tokens.
2575 * 1. Index of last token in selector array of tokens.
2576 * 2. Selector type.
2577 */
2578 function getSelectorRange(tokens, at) {
2579 var selector_start = at,
2580 selector_end,
2581 pos_in_token_array;
2582
2583 for (; selector_start >= 0; selector_start--) {
2584 pos_in_token_array = tokenIsTheBeginOfSelector(tokens, selector_start);
2585 if (pos_in_token_array)
2586 break;
2587 }
2588 selector_end = selector_start;
2589
2590 if (pos_in_token_array === 1) {
2591 // Selector consists of a single token.
2592
2593 // Include tailing colon.
2594 if (selector_end + 1 < tokens.length && tokens[selector_end + 1][0] == ':')
2595 selector_end++;
2596
2597 return [ selector_start, selector_end, tokens[selector_start][pos_in_token_array] ];
2598 }
2599
2600 for (selector_end++; selector_end < tokens.length ; selector_end++) {
2601 if (tokenIsTheBeginOfSelector(tokens, selector_end))
2602 return [ selector_start, selector_end - 1, tokens[selector_start][pos_in_token_array] ];
2603 }
2604
2605 return [ selector_start, selector_end - 1, tokens[selector_start][pos_in_token_array] ];
2606 }
2607 /* }}} */
2608 /* }}} */
2609 /* }}} */
2610
2611 /* Prettify raw value from user. {{{
2612 * The value is generated by putting the tokens back together to a string.
2613 *
2614 * :param argument_hash: Hash which can contain:
2615 * 'conf': Configuration hash.
2616 * 'get_internals: If true export internal data structures.
2617 * 'rule_index: Only prettify the rule with this index.
2618 * :returns: Prettified value string or object if get_internals is true.
2619 */
2620 function prettifyValue(argument_hash) {
2621 var user_conf = {},
2622 get_internals = false,
2623 rule_index;
2624 if (typeof argument_hash != 'undefined') {
2625
2626 if (typeof argument_hash.conf === 'object')
2627 user_conf = argument_hash.conf;
2628
2629 if (typeof argument_hash.rule_index === 'number')
2630 rule_index = argument_hash.rule_index;
2631
2632 if (argument_hash.get_internals === true)
2633 get_internals = true;
2634 }
2635
2636 for (var key in default_prettify_conf) {
2637 if (typeof user_conf[key] == 'undefined')
2638 user_conf[key] = default_prettify_conf[key];
2639 }
2640
2641 prettified_value = '';
2642 var prettified_value_array = [];
2643
2644 for (var nrule = 0; nrule < new_tokens.length; nrule++) {
2645 if (new_tokens[nrule][0].length === 0) continue;
2646 // Rule does contain nothing useful e.g. second rule of '10:00-12:00;' (empty) which needs to be handled.
2647
2648 if (typeof rule_index == 'number') {
2649 if (rule_index != nrule) continue;
2650 } else {
2651 if (nrule !== 0)
2652 prettified_value += (
2653 new_tokens[nrule][1]
2654 ? user_conf.rule_sep_string + '|| '
2655 : (
2656 new_tokens[nrule][0][0][1] == 'rule separator'
2657 ? ','
2658 : (
2659 user_conf.print_semicolon
2660 ? ';'
2661 : ''
2662 )
2663 ) +
2664 user_conf.rule_sep_string);
2665 }
2666
2667 var selector_start_end_type = [ 0, 0, undefined ],
2668 prettified_group_value = [];
2669 // console.log(new_tokens[nrule][0]);
2670 var count = 0;
2671
2672
2673 do {
2674 selector_start_end_type = getSelectorRange(new_tokens[nrule][0], selector_start_end_type[1]);
2675 // console.log(selector_start_end_type, new_tokens[nrule][0].length, count);
2676
2677 if (count > 50) {
2678 throw formatLibraryBugMessage('infinite loop');
2679 }
2680
2681 if (selector_start_end_type[2] != 'rule separator') {
2682 prettified_group_value.push(
2683 [
2684 selector_start_end_type,
2685 prettifySelector(
2686 new_tokens[nrule][0],
2687 selector_start_end_type[0],
2688 selector_start_end_type[1],
2689 selector_start_end_type[2],
2690 user_conf
2691 ),
2692 ]
2693 );
2694 }
2695
2696 selector_start_end_type[1]++;
2697 count++;
2698 // console.log(selector_start_end_type, new_tokens[nrule][0].length, count);
2699 } while (selector_start_end_type[1] < new_tokens[nrule][0].length);
2700 // console.log('Prettified value: ' + JSON.stringify(prettified_group_value, null, ' '));
2701 var not_sorted_prettified_group_value = prettified_group_value.slice();
2702
2703 if (!done_with_selector_reordering) {
2704 prettified_group_value.sort(
2705 function (a, b) {
2706 var selector_order = [ 'year', 'month', 'week', 'holiday', 'weekday', 'time', '24/7', 'state', 'comment'];
2707 return selector_order.indexOf(a[0][2]) - selector_order.indexOf(b[0][2]);
2708 }
2709 );
2710 }
2711 var old_prettified_value_length = prettified_value.length;
2712
2713 prettified_value += prettified_group_value.map(
2714 function (array) {
2715 return array[1];
2716 }
2717 ).join(' ');
2718
2719 prettified_value_array.push( prettified_group_value );
2720
2721 if (!done_with_selector_reordering_warnings) {
2722 for (var i = 0, l = not_sorted_prettified_group_value.length; i < l; i++) {
2723 if (not_sorted_prettified_group_value[i] != prettified_group_value[i]) {
2724 // console.log(i + ': ' + prettified_group_value[i][0][2]);
2725 var length = i + old_prettified_value_length; // i: Number of spaces in string.
2726 for (var x = 0; x <= i; x++) {
2727 length += prettified_group_value[x][1].length;
2728 // console.log('Length: ' + length + ' ' + prettified_group_value[x][1]);
2729 }
2730 // console.log(length);
2731 parsing_warnings.push([ prettified_value, length,
2732 'The selector "' + prettified_group_value[i][0][2] + '" was switched with'
2733 + ' the selector "' + not_sorted_prettified_group_value[i][0][2] + '"'
2734 + ' for readablitity and compatibiltity reasons.'
2735 ]);
2736 }
2737 }
2738 }
2739 }
2740
2741 done_with_selector_reordering_warnings = true;
2742 // console.log(JSON.stringify(prettified_value_array, null, ' '));
2743
2744 if (get_internals) {
2745 return [ prettified_value_array, new_tokens ];
2746 } else {
2747 return prettified_value;
2748 }
2749 }
2750 // }}}
2751
2752 /* Check selector array of tokens for specific token name pattern. {{{
2753 *
2754 * :param tokens: List of token objects.
2755 * :param at: Position at which the matching should begin.
2756 * :param token_name(s): One or many token_name strings which have to match in that order.
2757 * :returns: true if token_name(s) match in order false otherwise.
2758 */
2759 function matchTokens(tokens, at /*, matches... */) {
2760 if (at + arguments.length - 2 > tokens.length)
2761 return false;
2762 for (var i = 0; i < arguments.length - 2; i++) {
2763 if (tokens[at + i][1] !== arguments[i + 2])
2764 return false;
2765 }
2766
2767 return true;
2768 }
2769 // }}}
2770
2771 /* Generate selector wrapper with time offset {{{
2772 *
2773 * :param func: Generated selector code function.
2774 * :param shirt: Time to shift in milliseconds.
2775 * :param token_name(s): One or many token_name strings which have to match in that order.
2776 * :returns: See selector code.
2777 */
2778 function generateDateShifter(func, shift) {
2779 return function(date) {
2780 var res = func(new Date(date.getTime() + shift));
2781
2782 if (typeof res[1] === 'undefined')
2783 return res;
2784 return [ res[0], new Date(res[1].getTime() - shift) ];
2785 };
2786 }
2787 // }}}
2788
2789 /* Top-level parser {{{
2790 *
2791 * :param tokens: List of token objects.
2792 * :param at: Position where to start.
2793 * :param selectors: Reference to selector object.
2794 * :param nrule: Rule number starting with 0.
2795 * :returns: See selector code.
2796 */
2797 function parseGroup(tokens, at, selectors, nrule) {
2798 var rule_modifier_specified = false;
2799
2800 // console.log(tokens); // useful for debugging of tokenize
2801 while (at < tokens.length) {
2802 // console.log('Parsing at position', at +':', tokens[at]);
2803 if (matchTokens(tokens, at, 'weekday')) {
2804 at = parseWeekdayRange(tokens, at, selectors);
2805 } else if (matchTokens(tokens, at, '24/7')) {
2806 selectors.time.push(function(date) { return [true]; });
2807 // Not needed. If there is no selector it automatically matches everything.
2808 // WRONG: This only works if there is no other selector in this selector group ...
2809 at++;
2810 } else if (matchTokens(tokens, at, 'holiday')) {
2811 if (matchTokens(tokens, at+1, ','))
2812 at = parseHoliday(tokens, at, selectors, true);
2813 else
2814 at = parseHoliday(tokens, at, selectors, false);
2815 week_stable = false;
2816 } else if (matchTokens(tokens, at, 'month', 'number')
2817 || matchTokens(tokens, at, 'month', 'weekday')
2818 || matchTokens(tokens, at, 'year', 'month', 'number')
2819 || matchTokens(tokens, at, 'year', 'event')
2820 || matchTokens(tokens, at, 'event')) {
2821
2822 at = parseMonthdayRange(tokens, at, nrule);
2823 week_stable = false;
2824 } else if (matchTokens(tokens, at, 'year')) {
2825 at = parseYearRange(tokens, at);
2826 week_stable = false;
2827 } else if (matchTokens(tokens, at, 'month')) {
2828 at = parseMonthRange(tokens, at);
2829 // week_stable = false; // Decided based on the actual value/tokens.
2830 } else if (matchTokens(tokens, at, 'week')) {
2831 tokens[at][3] = 'week';
2832 at = parseWeekRange(tokens, at);
2833
2834 } else if (at !== 0 && at != tokens.length - 1 && tokens[at][0] == ':') {
2835 /* Ignore colon if they appear somewhere else than as time separator.
2836 * Except the start or end of the value.
2837 * This provides compatibility with the syntax proposed by Netzwolf:
2838 * http://wiki.openstreetmap.org/wiki/Key:opening_hours:specification#separator_for_readability
2839 * Check for valid use of <separator_for_readability> is implemented in function getWarnings().
2840 */
2841
2842 if (!done_with_warnings && matchTokens(tokens, at-1, 'holiday'))
2843 parsing_warnings.push([nrule, at, 'Please don’t use ":" after ' + tokens[at-1][1] + '.']);
2844
2845 at++;
2846 } else if (matchTokens(tokens, at, 'number', 'timesep')
2847 || matchTokens(tokens, at, 'timevar')
2848 || matchTokens(tokens, at, '(', 'timevar')
2849 || matchTokens(tokens, at, 'number', '-')) {
2850
2851 at = parseTimeRange(tokens, at, selectors, false);
2852
2853 } else if (matchTokens(tokens, at, 'state')) {
2854
2855 if (tokens[at][0] == 'open') {
2856 selectors.meaning = true;
2857 } else if (tokens[at][0] == 'closed' || tokens[at][0] == 'off') {
2858 selectors.meaning = false;
2859 } else {
2860 selectors.meaning = false;
2861 selectors.unknown = true;
2862 }
2863
2864 rule_modifier_specified = true;
2865 at++;
2866 if (typeof tokens[at] == 'object' && tokens[at][0] == ',') // additional rule
2867 at = [ at + 1 ];
2868
2869 } else if (matchTokens(tokens, at, 'comment')) {
2870 selectors.comment = tokens[at][0];
2871 if (!rule_modifier_specified) {
2872 // Then it is unknown. Either with unknown explicitly
2873 // specified or just a comment.
2874 selectors.meaning = false;
2875 selectors.unknown = true;
2876 }
2877
2878 rule_modifier_specified = true;
2879 at++;
2880 if (typeof tokens[at] == 'object' && tokens[at][0] == ',') // additional rule
2881 at = [ at + 1 ];
2882 } else if ((at === 0 || at == tokens.length - 1) && matchTokens(tokens, at, 'rule separator')) {
2883 at++;
2884 console.log("value: " + nrule);
2885 // throw formatLibraryBugMessage('Not implemented yet.');
2886 } else {
2887 var warnings = getWarnings();
2888 throw formatWarnErrorMessage(nrule, at, 'Unexpected token: "' + tokens[at][1]
2889 + '" This means that the syntax is not valid at that point or it is currently not supported.')
2890 + (warnings ? ' ' + warnings.join('; ') : '');
2891 }
2892
2893 if (typeof at == 'object') { // additional rule
2894 tokens[at[0] - 1][1] = 'rule separator';
2895 break;
2896 }
2897 }
2898
2899 return at;
2900 }
2901
2902 function get_last_token_pos_in_token_group(tokens, at, last_at) {
2903 for (at++; at < last_at; at++) {
2904 if (typeof tokens[at] != 'undefined') {
2905 if (typeof tokens[at][3] == 'string'
2906 || tokens[at][1] == 'comment'
2907 || tokens[at][1] == 'state'){
2908
2909 return at - 1;
2910 }
2911 }
2912 }
2913 return last_at;
2914 }
2915 // }}}
2916
2917 // helper functions for sub parser {{{
2918
2919 /* For given date, returns date moved to the start of the day with an offset specified in minutes. {{{
2920 * For example, if date is 2014-05-19_18:17:12, dateAtDayMinutes would
2921 * return 2014-05-19_02:00:00 for minutes=120.
2922 *
2923 * :param date: Date object.
2924 * :param minutes: Minutes used as offset starting from midnight of current day.
2925 * :returns: Moved date object.
2926 */
2927 function dateAtDayMinutes(date, minutes) {
2928 return new Date(date.getFullYear(), date.getMonth(), date.getDate(), 0, minutes);
2929 }
2930 // }}}
2931
2932 /* For given date, returns date moved to the specific day of week {{{
2933 *
2934 * :param date: Date object.
2935 * :param weekday: Integer number for day of week. Starting with zero (Sunday).
2936 * :returns: Moved date object.
2937 */
2938 function dateAtNextWeekday(date, weekday) {
2939 var delta = weekday - date.getDay();
2940 return new Date(date.getFullYear(), date.getMonth(), date.getDate() + delta + (delta < 0 ? 7 : 0));
2941 }
2942 // }}}
2943
2944 /* Function to determine whether an array contains a value {{{
2945 * Source: http://stackoverflow.com/a/1181586
2946 *
2947 * :param needle: Element to find.
2948 * :returns: Index of element if present, if not present returns -1.
2949 */
2950 function indexOf(needle) {
2951 if(typeof Array.prototype.indexOf === 'function') {
2952 indexOf = Array.prototype.indexOf;
2953 } else {
2954 indexOf = function(needle) {
2955 var i = -1, index = -1;
2956 for(i = 0; i < this.length; i++) {
2957 if(this[i] === needle) {
2958 index = i;
2959 break;
2960 }
2961 }
2962 return index;
2963 };
2964 }
2965 return indexOf.call(this, needle);
2966 }
2967 // }}}
2968
2969 /* Numeric list parser (1,2,3-4,-1) {{{
2970 * Used in weekday parser above.
2971 *
2972 * :param tokens: List of token objects.
2973 * :param at: Position where to start.
2974 * :param func: Function func(from, to, at).
2975 * :returns: Position at which the token does not belong to the list any more.
2976 */
2977 function parseNumRange(tokens, at, func) {
2978 for (; at < tokens.length; at++) {
2979 if (matchTokens(tokens, at, 'number', '-', 'number')) {
2980 // Number range
2981 func(tokens[at][0], tokens[at+2][0], at);
2982 at += 3;
2983 } else if (matchTokens(tokens, at, '-', 'number')) {
2984 // Negative number
2985 func(-tokens[at+1][0], -tokens[at+1][0], at);
2986 at += 2;
2987 } else if (matchTokens(tokens, at, 'number')) {
2988 // Single number
2989 func(tokens[at][0], tokens[at][0], at);
2990 at++;
2991 } else {
2992 throw formatWarnErrorMessage(nrule, at + matchTokens(tokens, at, '-'),
2993 'Unexpected token in number range: ' + tokens[at][1]);
2994 }
2995
2996 if (!matchTokens(tokens, at, ','))
2997 break;
2998 }
2999
3000 return at;
3001 }
3002 // }}}
3003
3004 /* List parser for constrained weekdays in month range {{{
3005 * e.g. Su[-1] which selects the last Sunday of the month.
3006 *
3007 * :param tokens: List of token objects.
3008 * :param at: Position where to start.
3009 * :returns: Array:
3010 * 0. Constrained weekday number.
3011 * 1. Position at which the token does not belong to the list any more (after ']' token).
3012 */
3013 function getConstrainedWeekday(tokens, at) {
3014 var number = 0;
3015 var endat = parseNumRange(tokens, at, function(from, to, at) {
3016
3017 // bad number
3018 if (from === 0 || from < -5 || from > 5)
3019 throw formatWarnErrorMessage(nrule, at,
3020 'Number between -5 and 5 (except 0) expected');
3021
3022 if (from == to) {
3023 if (number !== 0)
3024 throw formatWarnErrorMessage(nrule, at,
3025 'You can not use more than one constrained weekday in a month range');
3026 number = from;
3027 } else {
3028 throw formatWarnErrorMessage(nrule, at+2,
3029 'You can not use a range of constrained weekdays in a month range');
3030 }
3031 });
3032
3033 if (!matchTokens(tokens, endat, ']'))
3034 throw formatWarnErrorMessage(nrule, endat, '"]" expected.');
3035
3036 return [ number, endat + 1 ];
3037 }
3038 // }}}
3039
3040 // Check if period is ok. Period 0 or 1 don’t make much sense.
3041 function checkPeriod(at, period, period_type, parm_string) {
3042 if (done_with_warnings)
3043 return;
3044
3045 if (period === 0) {
3046 throw formatWarnErrorMessage(nrule, at,
3047 'You can not use '+ period_type +' ranges with period equals zero.');
3048 } else if (period === 1) {
3049 if (typeof parm_string == 'string' && parm_string == 'no_end_year')
3050 parsing_warnings.push([nrule, at,
3051 'Please don’t use '+ period_type +' ranges with period equals one.'
3052 + ' If you want to express that a facility is open starting from a year without limit use "<year>+".']);
3053 else
3054 parsing_warnings.push([nrule, at,
3055 'Please don’t use '+ period_type +' ranges with period equals one.']);
3056 }
3057 }
3058
3059 /* Get date moved to constrained weekday (and moved for add_days. {{{
3060 * E.g. used for 'Aug Su[-1] -1 day'.
3061 *
3062 * :param year: Year as integer.
3063 * :param month: Month as integer starting with zero.
3064 * :param weekday: Integer number for day of week. Starting with zero (Sunday).
3065 * :param constrained_weekday: Position where to start.
3066 * :returns: Date object.
3067 */
3068 function getDateForConstrainedWeekday(year, month, weekday, constrained_weekday, add_days) {
3069 var tmp_date = dateAtNextWeekday(
3070 new Date(year, month + (constrained_weekday[0] > 0 ? 0 : 1), 1), weekday);
3071
3072 tmp_date.setDate(tmp_date.getDate() + (constrained_weekday[0] + (constrained_weekday[0] > 0 ? -1 : 0)) * 7);
3073
3074 if (typeof add_days != 'undefined' && add_days[1])
3075 tmp_date.setDate(tmp_date.getDate() + add_days[0]);
3076
3077 return tmp_date;
3078 }
3079 // }}}
3080
3081 /* Check if date is valid. {{{
3082 *
3083 * :param month: Month as integer starting with zero.
3084 * :param date: Day of month as integer.
3085 * :returns: undefined. There is no real return value. This function just throws an exception if something is wrong.
3086 */
3087 function checkIfDateIsValid(month, day, nrule, at) {
3088 // May use this instead. The problem is that this does not give feedback as precise as the code which is used in this function.
3089 // var testDate = new Date(year, month, day);
3090 // if (testDate.getDate() != day || testDate.getMonth() != month || testDate.getFullYear() != year) {
3091 // console.error('date not valid');
3092 // }
3093
3094 // https://en.wikipedia.org/wiki/Month#Julian_and_Gregorian_calendars
3095 if (day < 1 || day > 31)
3096 throw formatWarnErrorMessage(nrule, at, 'Day must be between 1 and 31.');
3097 if ((month==3 || month==5 || month==8 || month==10) && day==31)
3098 throw formatWarnErrorMessage(nrule, at, 'Month ' + months[month] + " doesn't have 31 days.!");
3099 if (month == 1 && day == 30)
3100 throw formatWarnErrorMessage(nrule, at, 'Month ' + months[1]+ " either has 28 or 29 days (leap years).");
3101 }
3102 // }}}
3103 // }}}
3104
3105 /* Time range parser (10:00-12:00,14:00-16:00) {{{
3106 *
3107 * :param tokens: List of token objects.
3108 * :param at: Position where to start.
3109 * :param selectors: Reference to selector object.
3110 * :param extended_open_end: Used for combined time range with open end.
3111 * extended_open_end: <time> - <time> +
3112 * param at is here A (if extended_open_end is true)
3113 * :returns: Position at which the token does not belong to the selector anymore.
3114 */
3115 function parseTimeRange(tokens, at, selectors, extended_open_end) {
3116 if (!extended_open_end)
3117 tokens[at][3] = 'time';
3118
3119 for (; at < tokens.length; at++) {
3120 var has_time_var_calc = [], has_normal_time = []; // element 0: start time, 1: end time
3121 has_normal_time[0] = matchTokens(tokens, at, 'number', 'timesep', 'number');
3122 has_time_var_calc[0] = matchTokens(tokens, at, '(', 'timevar');
3123 var minutes_from,
3124 minutes_to;
3125 if (has_normal_time[0] || matchTokens(tokens, at, 'timevar') || has_time_var_calc[0]) {
3126 // relying on the fact that always *one* of them is true
3127
3128 var is_point_in_time = false; // default no time range
3129 var has_open_end = false; // default no open end
3130 var timevar_add = [ 0, 0 ];
3131 var timevar_string = []; // capture timevar string like 'sunrise' to calculate it for the current date.
3132 var point_in_time_period;
3133
3134 // minutes_from
3135 if (has_normal_time[0]) {
3136 minutes_from = getMinutesByHoursMinutes(tokens, nrule, at+has_time_var_calc[0]);
3137 } else {
3138 timevar_string[0] = tokens[at+has_time_var_calc[0]][0];
3139 minutes_from = word_value_replacement[timevar_string[0]];
3140
3141 if (has_time_var_calc[0]) {
3142 timevar_add[0] = parseTimevarCalc(tokens, at);
3143 minutes_from += timevar_add[0];
3144 }
3145 }
3146
3147 var at_end_time = at+(has_normal_time[0] ? 3 : (has_time_var_calc[0] ? 7 : 1))+1; // after '-'
3148 if (!matchTokens(tokens, at_end_time - 1, '-')) { // not time range
3149 if (matchTokens(tokens, at_end_time - 1, '+')) {
3150 has_open_end = true;
3151 } else {
3152 if (oh_mode === 0) {
3153 throw formatWarnErrorMessage(nrule,
3154 at+(
3155 has_normal_time[0] ? (
3156 typeof tokens[at+3] == 'object' ? 3 : 2
3157 ) : (
3158 has_time_var_calc[0] ? 2 : (
3159 typeof tokens[at+1] != 'undefined' ? 1 : 0
3160 )
3161 )
3162 ),
3163 'hyphen (-) or open end (+) in time range '
3164 + (has_time_var_calc[0] ? 'calculation ' : '') + 'expected.'
3165 + ' For working with points in time, the mode for ' + library_name + ' has to be altered.'
3166 + ' Maybe wrong tag?');
3167 } else {
3168 minutes_to = minutes_from + 1;
3169 is_point_in_time = true;
3170 }
3171 }
3172 }
3173
3174 // minutes_to
3175 if (has_open_end) {
3176 if (extended_open_end === 1)
3177 minutes_from += minutes_in_day;
3178 if (minutes_from >= 22 * 60)
3179 minutes_to = minutes_from + 8 * 60;
3180 else if (minutes_from >= 17 * 60)
3181 minutes_to = minutes_from + 10 * 60;
3182 else
3183 minutes_to = minutes_in_day;
3184 } else if (!is_point_in_time) {
3185 has_normal_time[1] = matchTokens(tokens, at_end_time, 'number', 'timesep', 'number');
3186 has_time_var_calc[1] = matchTokens(tokens, at_end_time, '(', 'timevar');
3187 if (!has_normal_time[1] && !matchTokens(tokens, at_end_time, 'timevar') && !has_time_var_calc[1]) {
3188 throw formatWarnErrorMessage(nrule, at_end_time - (typeof tokens[at_end_time] != 'undefined' ? 0 : 1),
3189 'Time range does not continue as expected');
3190 } else {
3191 if (has_normal_time[1]) {
3192 minutes_to = getMinutesByHoursMinutes(tokens, nrule, at_end_time);
3193 } else {
3194 timevar_string[1] = tokens[at_end_time+has_time_var_calc[1]][0];
3195 minutes_to = word_value_replacement[timevar_string[1]];
3196 }
3197
3198 if (has_time_var_calc[1]) {
3199 timevar_add[1] = parseTimevarCalc(tokens, at_end_time);
3200 minutes_to += timevar_add[1];
3201 }
3202 }
3203 }
3204
3205 at = at_end_time + (is_point_in_time ? -1 :
3206 (has_normal_time[1] ? 3 : (has_time_var_calc[1] ? 7 : !has_open_end))
3207 );
3208
3209 if (matchTokens(tokens, at, '/', 'number')) {
3210 if (matchTokens(tokens, at + 2, 'timesep', 'number')) { // /hours:minutes
3211 point_in_time_period = getMinutesByHoursMinutes(tokens, nrule, at + 1);
3212 at += 4;
3213 } else { // /minutes
3214 point_in_time_period = tokens[at + 1][0];
3215 at += 2;
3216 if (matchTokens(tokens, at, 'timesep'))
3217 throw formatWarnErrorMessage(nrule, at,
3218 'Time period does not continue as expected. Exampe "/01:30".');
3219 }
3220
3221 // Check at this later state in the if condition to get the correct position.
3222 if (oh_mode === 0)
3223 throw formatWarnErrorMessage(nrule, at - 1,
3224 'opening_hours is running in "time range mode". Found point in time.');
3225
3226 is_point_in_time = true;
3227 } else if (matchTokens(tokens, at, '+')) {
3228 parseTimeRange(tokens, at_end_time, selectors, minutes_to < minutes_from ? 1 : true);
3229 at++;
3230 } else if (oh_mode == 1 && !is_point_in_time) {
3231 throw formatWarnErrorMessage(nrule, at_end_time,
3232 'opening_hours is running in "points in time mode". Found time range.');
3233 }
3234
3235 if (typeof lat != 'undefined') { // lon will also be defined (see above)
3236 if (!has_normal_time[0] || !(has_normal_time[1] || has_open_end || is_point_in_time) )
3237 week_stable = false;
3238 } else { // we can not calculate exact times so we use the already applied constants (word_value_replacement).
3239 timevar_string = [];
3240 }
3241
3242 // Normalize minutes into range.
3243 if (!extended_open_end && minutes_from >= minutes_in_day)
3244 throw formatWarnErrorMessage(nrule, at_end_time - 2,
3245 'Time range starts outside of the current day');
3246 if (minutes_to < minutes_from || ((has_normal_time[0] && has_normal_time[1]) && minutes_from == minutes_to))
3247 minutes_to += minutes_in_day;
3248 if (minutes_to > minutes_in_day * 2)
3249 throw formatWarnErrorMessage(nrule, at_end_time + (has_normal_time[1] ? 4 : (has_time_var_calc[1] ? 7 : 1)) - 2,
3250 'Time spanning more than two midnights not supported');
3251
3252 // This shortcut makes always-open range check faster.
3253 if (minutes_from === 0 && minutes_to == minutes_in_day) {
3254 selectors.time.push(function(date) { return [true]; });
3255 } else {
3256 if (minutes_to > minutes_in_day) { // has_normal_time[1] must be true
3257 selectors.time.push(function(minutes_from, minutes_to, timevar_string, timevar_add, has_open_end, is_point_in_time, point_in_time_period, extended_open_end) { return function(date) {
3258 var ourminutes = date.getHours() * 60 + date.getMinutes();
3259
3260 if (timevar_string[0]) {
3261 var date_from = SunCalc.getTimes(date, lat, lon)[timevar_string[0]];
3262 minutes_from = date_from.getHours() * 60 + date_from.getMinutes() + timevar_add[0];
3263 }
3264 if (timevar_string[1]) {
3265 var date_to = SunCalc.getTimes(date, lat, lon)[timevar_string[1]];
3266 minutes_to = date_to.getHours() * 60 + date_to.getMinutes() + timevar_add[1];
3267 minutes_to += minutes_in_day;
3268 // Needs to be added because it was added by
3269 // normal times: if (minutes_to < minutes_from)
3270 // above the selector construction.
3271 } else if (is_point_in_time && typeof point_in_time_period != 'number') {
3272 minutes_to = minutes_from + 1;
3273 }
3274
3275 if (typeof point_in_time_period == 'number') {
3276 if (ourminutes < minutes_from) {
3277 return [false, dateAtDayMinutes(date, minutes_from)];
3278 } else if (ourminutes <= minutes_to) {
3279 for (var cur_min = minutes_from; ourminutes + point_in_time_period >= cur_min; cur_min += point_in_time_period) {
3280 if (cur_min == ourminutes) {
3281 return [true, dateAtDayMinutes(date, ourminutes + 1)];
3282 } else if (ourminutes < cur_min) {
3283 return [false, dateAtDayMinutes(date, cur_min)];
3284 }
3285 }
3286 }
3287 return [false, dateAtDayMinutes(date, minutes_in_day)];
3288 } else {
3289 if (ourminutes < minutes_from)
3290 return [false, dateAtDayMinutes(date, minutes_from)];
3291 else
3292 return [true, dateAtDayMinutes(date, minutes_to), has_open_end, extended_open_end];
3293 }
3294 }}(minutes_from, minutes_to, timevar_string, timevar_add, has_open_end, is_point_in_time, point_in_time_period, extended_open_end));
3295
3296 selectors.wraptime.push(function(minutes_from, minutes_to, timevar_string, timevar_add, has_open_end, is_point_in_time, point_in_time_period, extended_open_end) { return function(date) {
3297 var ourminutes = date.getHours() * 60 + date.getMinutes();
3298
3299 if (timevar_string[0]) {
3300 var date_from = SunCalc.getTimes(date, lat, lon)[timevar_string[0]];
3301 minutes_from = date_from.getHours() * 60 + date_from.getMinutes() + timevar_add[0];
3302 }
3303 if (timevar_string[1]) {
3304 var date_to = SunCalc.getTimes(date, lat, lon)[timevar_string[1]];
3305 minutes_to = date_to.getHours() * 60 + date_to.getMinutes() + timevar_add[1];
3306 // minutes_in_day does not need to be added.
3307 // For normal times in it was added in: if (minutes_to < // minutes_from)
3308 // above the selector construction and
3309 // subtracted in the selector construction call
3310 // which returns the selector function.
3311 }
3312
3313 if (typeof point_in_time_period == 'number') {
3314 if (ourminutes <= minutes_to) {
3315 for (var cur_min = 0; ourminutes + point_in_time_period >= cur_min; cur_min += point_in_time_period) {
3316 if (cur_min == ourminutes) {
3317 return [true, dateAtDayMinutes(date, ourminutes + 1)];
3318 } else if (ourminutes < cur_min) {
3319 return [false, dateAtDayMinutes(date, cur_min)];
3320 }
3321 }
3322 }
3323 } else {
3324 if (ourminutes < minutes_to)
3325 return [true, dateAtDayMinutes(date, minutes_to), has_open_end, extended_open_end];
3326 }
3327 return [false, undefined];
3328 }}(minutes_from, minutes_to - minutes_in_day, timevar_string, timevar_add, has_open_end, is_point_in_time, point_in_time_period, extended_open_end));
3329 } else {
3330 selectors.time.push(function(minutes_from, minutes_to, timevar_string, timevar_add, has_open_end, is_point_in_time, point_in_time_period, extended_open_end) { return function(date) {
3331 var ourminutes = date.getHours() * 60 + date.getMinutes();
3332
3333 if (timevar_string[0]) {
3334 var date_from = SunCalc.getTimes(date, lat, lon)[timevar_string[0]];
3335 minutes_from = date_from.getHours() * 60 + date_from.getMinutes() + timevar_add[0];
3336 }
3337 if (timevar_string[1]) {
3338 var date_to = SunCalc.getTimes(date, lat, lon)[timevar_string[1]];
3339 minutes_to = date_to.getHours() * 60 + date_to.getMinutes() + timevar_add[1];
3340 } else if (is_point_in_time && typeof point_in_time_period != 'number') {
3341 minutes_to = minutes_from + 1;
3342 }
3343
3344 if (typeof point_in_time_period == 'number') {
3345 if (ourminutes < minutes_from) {
3346 return [false, dateAtDayMinutes(date, minutes_from)];
3347 } else if (ourminutes <= minutes_to) {
3348 for (var cur_min = minutes_from; ourminutes + point_in_time_period >= cur_min; cur_min += point_in_time_period) {
3349 if (cur_min == ourminutes) {
3350 return [true, dateAtDayMinutes(date, ourminutes + 1)];
3351 } else if (ourminutes < cur_min) {
3352 return [false, dateAtDayMinutes(date, cur_min)];
3353 }
3354 }
3355 }
3356 return [false, dateAtDayMinutes(date, minutes_in_day)];
3357 } else {
3358 if (ourminutes < minutes_from)
3359 return [false, dateAtDayMinutes(date, minutes_from)];
3360 else if (ourminutes < minutes_to)
3361 return [true, dateAtDayMinutes(date, minutes_to), has_open_end];
3362 else
3363 return [false, dateAtDayMinutes(date, minutes_from + minutes_in_day)];
3364 }
3365 }}(minutes_from, minutes_to, timevar_string, timevar_add, has_open_end, is_point_in_time, point_in_time_period, extended_open_end));
3366 }
3367 }
3368
3369 } else if (matchTokens(tokens, at, 'number', '-', 'number')) { // "Mo 09-18" (Please don’t use this) -> "Mo 09:00-18:00".
3370 minutes_from = tokens[at][0] * 60;
3371 minutes_to = tokens[at+2][0] * 60;
3372 if (!done_with_warnings)
3373 parsing_warnings.push([nrule, at + 2,
3374 'Time range without minutes specified. Not very explicit!'
3375 + ' Please use this syntax instead "'
3376 + (tokens[at][0] < 10 ? '0' : '') + tokens[at][0] + ':00-'
3377 + (tokens[at+2][0] < 10 ? '0' : '') + tokens[at+2][0] + ':00".']);
3378
3379 if (minutes_from >= minutes_in_day)
3380 throw formatWarnErrorMessage(nrule, at,
3381 'Time range starts outside of the current day');
3382 if (minutes_to < minutes_from)
3383 minutes_to += minutes_in_day;
3384 if (minutes_to > minutes_in_day * 2)
3385 throw formatWarnErrorMessage(nrule, at + 2,
3386 'Time spanning more than two midnights not supported');
3387
3388 if (minutes_to > minutes_in_day) {
3389 selectors.time.push(function(minutes_from, minutes_to) { return function(date) {
3390 var ourminutes = date.getHours() * 60 + date.getMinutes();
3391
3392 if (ourminutes < minutes_from)
3393 return [false, dateAtDayMinutes(date, minutes_from)];
3394 else
3395 return [true, dateAtDayMinutes(date, minutes_to)];
3396 }}(minutes_from, minutes_to));
3397
3398 selectors.wraptime.push(function(minutes_from, minutes_to) { return function(date) {
3399 var ourminutes = date.getHours() * 60 + date.getMinutes();
3400
3401 if (ourminutes < minutes_to)
3402 return [true, dateAtDayMinutes(date, minutes_to)];
3403 else
3404 return [false, undefined];
3405 }}(minutes_from, minutes_to - minutes_in_day));
3406 } else {
3407 selectors.time.push(function(minutes_from, minutes_to) { return function(date) {
3408 var ourminutes = date.getHours() * 60 + date.getMinutes();
3409
3410 if (ourminutes < minutes_from)
3411 return [false, dateAtDayMinutes(date, minutes_from)];
3412 else if (ourminutes < minutes_to)
3413 return [true, dateAtDayMinutes(date, minutes_to), has_open_end];
3414 else
3415 return [false, dateAtDayMinutes(date, minutes_from + minutes_in_day)];
3416 }}(minutes_from, minutes_to));
3417 }
3418
3419 at += 3;
3420 } else { // additional rule
3421 if (matchTokens(tokens, at, '('))
3422 throw formatWarnErrorMessage(nrule, at, 'Missing variable time (e.g. sunrise) after: "' + tokens[at][1] + '"');
3423 if (matchTokens(tokens, at, 'number', 'timesep'))
3424 throw formatWarnErrorMessage(nrule, at+1, 'Missing minutes in time range after: "' + tokens[at+1][1] + '"');
3425 if (matchTokens(tokens, at, 'number'))
3426 throw formatWarnErrorMessage(nrule, at + (typeof tokens[at+1] != 'undefined' ? 1 : 0),
3427 'Missing time separator in time range after: "' + tokens[at][1] + '"');
3428 return [ at ];
3429 }
3430
3431 if (!matchTokens(tokens, at, ','))
3432 break;
3433 }
3434
3435 return at;
3436 }
3437 // }}}
3438
3439 /* Helpers for time range parser {{{ */
3440
3441 /* Get time in minutes from <hour>:<minute> (tokens). {{{
3442 * Only used if throwing an error is wanted.
3443 *
3444 * :param tokens: List of token objects.
3445 * :param nrule: Rule number starting with 0.
3446 * :param at: Position at which the time begins.
3447 * :returns: Time in minutes.
3448 */
3449 function getMinutesByHoursMinutes(tokens, nrule, at) {
3450 if (tokens[at+2][0] > 59)
3451 throw formatWarnErrorMessage(nrule, at+2,
3452 'Minutes are greater than 59.');
3453 return tokens[at][0] * 60 + tokens[at+2][0];
3454 }
3455 // }}}
3456
3457 /* Get time in minutes from "(sunrise-01:30)" {{{
3458 * Extract the added or subtracted time from "(sunrise-01:30)"
3459 * returns time in minutes e.g. -90.
3460 *
3461 * :param tokens: List of token objects.
3462 * :param at: Position where the specification for the point in time could be.
3463 * :returns: Time in minutes on suggest, throws an exception otherwise.
3464 */
3465 function parseTimevarCalc(tokens, at) {
3466 var error;
3467 if (matchTokens(tokens, at+2, '+') || matchTokens(tokens, at+2, '-')) {
3468 if (matchTokens(tokens, at+3, 'number', 'timesep', 'number')) {
3469 if (matchTokens(tokens, at+6, ')')) {
3470 var add_or_subtract = tokens[at+2][0] == '+' ? '1' : '-1';
3471 var minutes = getMinutesByHoursMinutes(tokens, nrule, at+3) * add_or_subtract;
3472 if (minutes === 0)
3473 parsing_warnings.push([ nrule, at+5, 'Adding zero in a variable time calculation does not change the variable time.'
3474 + ' Please omit the calculation (example: "sunrise-(sunset-00:00)").' ]
3475 );
3476 return minutes;
3477 } else {
3478 error = [ at+6, '. Missing ")".'];
3479 }
3480 } else {
3481 error = [ at+5, ' (time).'];
3482 }
3483 } else {
3484 error = [ at+2, '. "+" or "-" expected.'];
3485 }
3486
3487 if (error)
3488 throw formatWarnErrorMessage(nrule, error[0],
3489 'Calculcation with variable time is not in the right syntax' + error[1]);
3490 }
3491 /* }}} */
3492 /* }}} */
3493
3494 /* Weekday range parser (Mo,We-Fr,Sa[1-2,-1],PH). {{{
3495 *
3496 * :param tokens: List of token objects.
3497 * :param at: Position where the weekday tokens could be.
3498 * :param selectors: Reference to selector object.
3499 * :returns: Position at which the token does not belong to the selector anymore.
3500 */
3501 function parseWeekdayRange(tokens, at, selectors, in_holiday_selector) {
3502 if (!in_holiday_selector) {
3503 in_holiday_selector = true;
3504 tokens[at][3] = 'weekday';
3505 }
3506
3507 for (; at < tokens.length; at++) {
3508 if (matchTokens(tokens, at, 'weekday', '[')) {
3509 // Conditional weekday (Mo[3])
3510 var numbers = [];
3511
3512 // Get list of constraints
3513 var endat = parseNumRange(tokens, at+2, function(from, to, at) {
3514
3515 // bad number
3516 if (from === 0 || from < -5 || from > 5)
3517 throw formatWarnErrorMessage(nrule, at,
3518 'Number between -5 and 5 (except 0) expected');
3519
3520 if (from == to) {
3521 numbers.push(from);
3522 } else if (from < to) {
3523 for (var i = from; i <= to; i++) {
3524 // bad number
3525 if (i === 0 || i < -5 || i > 5)
3526 throw formatWarnErrorMessage(nrule, at+2,
3527 'Number between -5 and 5 (except 0) expected.');
3528
3529 numbers.push(i);
3530 }
3531 } else {
3532 throw formatWarnErrorMessage(nrule, at+2,
3533 'Bad range: ' + from + '-' + to);
3534 }
3535 });
3536
3537 if (!matchTokens(tokens, endat, ']'))
3538 throw formatWarnErrorMessage(nrule, endat, '"]" or more numbers expected.');
3539
3540 var add_days = getMoveDays(tokens, endat+1, 6, 'constrained weekdays');
3541 week_stable = false;
3542
3543 // Create selector for each list element.
3544 for (var nnumber = 0; nnumber < numbers.length; nnumber++) {
3545
3546 selectors.weekday.push(function(weekday, number, add_days) { return function(date) {
3547 var date_num = getValueForDate(date, false); // Year not needed to distinguish.
3548 var start_of_this_month = new Date(date.getFullYear(), date.getMonth(), 1);
3549 var start_of_next_month = new Date(date.getFullYear(), date.getMonth() + 1, 1);
3550
3551 var target_day_this_month;
3552
3553 target_day_this_month = getDateForConstrainedWeekday(date.getFullYear(), date.getMonth(), weekday, [ number ]);
3554
3555 var target_day_with_added_days_this_month = new Date(target_day_this_month.getFullYear(),
3556 target_day_this_month.getMonth(), target_day_this_month.getDate() + add_days);
3557
3558 // The target day with added days can be before this month
3559 if (target_day_with_added_days_this_month.getTime() < start_of_this_month.getTime()) {
3560 // but in this case, the target day without the days added needs to be in this month
3561 if (target_day_this_month.getTime() >= start_of_this_month.getTime()) {
3562 // so we calculate it for the month
3563 // following this month and hope that the
3564 // target day will actually be this month.
3565
3566 target_day_with_added_days_this_month = dateAtNextWeekday(
3567 new Date(date.getFullYear(), date.getMonth() + (number > 0 ? 0 : 1) + 1, 1), weekday);
3568 target_day_this_month.setDate(target_day_with_added_days_this_month.getDate()
3569 + (number + (number > 0 ? -1 : 0)) * 7 + add_days);
3570 } else {
3571 // Calculated target day is not inside this month
3572 // therefore the specified weekday (e.g. fifth Sunday)
3573 // does not exist this month. Try it next month.
3574 return [false, start_of_next_month];
3575 }
3576 } else if (target_day_with_added_days_this_month.getTime() >= start_of_next_month.getTime()) {
3577 // The target day is in the next month. If the target day without the added days is not in this month
3578 if (target_day_this_month.getTime() >= start_of_next_month.getTime())
3579 return [false, start_of_next_month];
3580 }
3581
3582 var target_day_with_added_moved_days_this_month;
3583 if (add_days > 0) {
3584 target_day_with_added_moved_days_this_month = dateAtNextWeekday(
3585 new Date(date.getFullYear(), date.getMonth() + (number > 0 ? 0 : 1) -1, 1), weekday);
3586 target_day_with_added_moved_days_this_month.setDate(target_day_with_added_moved_days_this_month.getDate()
3587 + (number + (number > 0 ? -1 : 0)) * 7 + add_days);
3588
3589 if (date_num == getValueForDate(target_day_with_added_moved_days_this_month, false))
3590 return [true, dateAtDayMinutes(date, minutes_in_day)];
3591 } else if (add_days < 0) {
3592 target_day_with_added_moved_days_this_month = dateAtNextWeekday(
3593 new Date(date.getFullYear(), date.getMonth() + (number > 0 ? 0 : 1) + 1, 1), weekday);
3594 target_day_with_added_moved_days_this_month.setDate(target_day_with_added_moved_days_this_month.getDate()
3595 + (number + (number > 0 ? -1 : 0)) * 7 + add_days);
3596
3597 if (target_day_with_added_moved_days_this_month.getTime() >= start_of_next_month.getTime()) {
3598 if (target_day_with_added_days_this_month.getTime() >= start_of_next_month.getTime())
3599 return [false, target_day_with_added_moved_days_this_month];
3600 } else {
3601 if (target_day_with_added_days_this_month.getTime() < start_of_next_month.getTime()
3602 && getValueForDate(target_day_with_added_days_this_month, false) == date_num)
3603 return [true, dateAtDayMinutes(date, minutes_in_day)];
3604
3605 target_day_with_added_days_this_month = target_day_with_added_moved_days_this_month;
3606 }
3607 }
3608
3609 // we hit the target day
3610 if (date.getDate() == target_day_with_added_days_this_month.getDate()) {
3611 return [true, dateAtDayMinutes(date, minutes_in_day)];
3612 }
3613
3614 // we're before target day
3615 if (date.getDate() < target_day_with_added_days_this_month.getDate()) {
3616 return [false, target_day_with_added_days_this_month];
3617 }
3618
3619 // we're after target day, set check date to next month
3620 return [false, start_of_next_month];
3621 }}(tokens[at][0], numbers[nnumber], add_days[0]));
3622 }
3623
3624 at = endat + 1 + add_days[1];
3625 } else if (matchTokens(tokens, at, 'weekday')) {
3626 // Single weekday (Mo) or weekday range (Mo-Fr)
3627 var is_range = matchTokens(tokens, at+1, '-', 'weekday');
3628
3629 var weekday_from = tokens[at][0];
3630 var weekday_to = is_range ? tokens[at+2][0] : weekday_from;
3631
3632 var inside = true;
3633
3634 // handle reversed range
3635 if (weekday_to < weekday_from) {
3636 var tmp = weekday_to;
3637 weekday_to = weekday_from - 1;
3638 weekday_from = tmp + 1;
3639 inside = false;
3640 }
3641
3642 if (weekday_to < weekday_from) { // handle full range
3643 selectors.weekday.push(function(date) { return [true]; });
3644 // Not needed. If there is no selector it automatically matches everything.
3645 // WRONG: This only works if there is no other selector in this selector group ...
3646 } else {
3647 selectors.weekday.push(function(weekday_from, weekday_to, inside) { return function(date) {
3648 var ourweekday = date.getDay();
3649
3650 if (ourweekday < weekday_from || ourweekday > weekday_to) {
3651 return [!inside, dateAtNextWeekday(date, weekday_from)];
3652 } else {
3653 return [inside, dateAtNextWeekday(date, weekday_to + 1)];
3654 }
3655 }}(weekday_from, weekday_to, inside));
3656 }
3657
3658 at += is_range ? 3 : 1;
3659 } else if (matchTokens(tokens, at, 'holiday')) {
3660 week_stable = false;
3661 return parseHoliday(tokens, at, selectors, true, in_holiday_selector);
3662 } else {
3663 throw formatWarnErrorMessage(nrule, at, 'Unexpected token in weekday range: ' + tokens[at][1]);
3664 }
3665
3666 if (!matchTokens(tokens, at, ','))
3667 break;
3668 }
3669
3670 return at;
3671 }
3672 // }}}
3673
3674 /* Get the number of days a date should be moved (if any). {{{
3675 *
3676 * :param tokens: List of token objects.
3677 * :param at: Position where the date moving tokens could be.
3678 * :param max_differ: Maximal number of days to move (could also be zero if there are no day move tokens).
3679 * :returns: Array:
3680 * 0. Days to add.
3681 * 1. How many tokens.
3682 */
3683 function getMoveDays(tokens, at, max_differ, name) {
3684 var add_days = [ 0, 0 ]; // [ 'days to add', 'how many tokens' ]
3685 add_days[0] = matchTokens(tokens, at, '+') || (matchTokens(tokens, at, '-') ? -1 : 0);
3686 if (add_days[0] !== 0 && matchTokens(tokens, at+1, 'number', 'calcday')) {
3687 // continues with '+ 5 days' or something like that
3688 if (tokens[at+1][0] > max_differ)
3689 throw formatWarnErrorMessage(nrule, at+2,
3690 'There should be no reason to differ more than ' + max_differ + ' days from a ' + name + '. If so tell us …');
3691 add_days[0] *= tokens[at+1][0];
3692 if (add_days[0] === 0 && !done_with_warnings)
3693 parsing_warnings.push([ nrule, at+2, 'Adding 0 does not change the date. Please omit this.' ]);
3694 add_days[1] = 3;
3695 } else {
3696 add_days[0] = 0;
3697 }
3698 return add_days;
3699 }
3700 // }}}
3701
3702 /* Holiday parser for public and school holidays (PH,SH) {{{
3703 *
3704 * :param tokens: List of token objects.
3705 * :param at: Position where to start.
3706 * :param selectors: Reference to selector object.
3707 * :param 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.
3708 * :returns: Position at which the token does not belong to the selector anymore.
3709 */
3710 function parseHoliday(tokens, at, selectors, push_to_weekday, in_holiday_selector) {
3711 if (!in_holiday_selector) {
3712
3713 if (push_to_weekday)
3714 tokens[at][3] = 'weekday';
3715 else
3716 tokens[at][3] = 'holiday'; // Could also be holiday but this is not important here.
3717 }
3718
3719 for (; at < tokens.length; at++) {
3720 if (matchTokens(tokens, at, 'holiday')) {
3721 if (tokens[at][0] == 'PH') {
3722 var applying_holidays = getMatchingHoliday(tokens[at][0]);
3723
3724 // Only allow moving one day in the past or in the future.
3725 // This makes implementation easier because only one holiday is assumed to be moved to the next year.
3726 var add_days = getMoveDays(tokens, at+1, 1, 'public holiday');
3727
3728 var selector = function(applying_holidays, add_days) { return function(date) {
3729
3730 var holidays = getApplyingHolidaysForYear(applying_holidays, date.getFullYear(), add_days);
3731 // Needs to be calculated each time because of movable days.
3732
3733 var date_num = getValueForDate(date, true);
3734
3735 for (var i = 0; i < holidays.length; i++) {
3736 var next_holiday_date_num = getValueForDate(holidays[i][0], true);
3737
3738 if (date_num < next_holiday_date_num) {
3739
3740 if (add_days[0] > 0) {
3741 // Calculate the last holiday from previous year to tested against it.
3742 var holidays_last_year = getApplyingHolidaysForYear(applying_holidays, date.getFullYear() - 1, add_days);
3743 var last_holiday_last_year = holidays_last_year[holidays_last_year.length - 1];
3744 var last_holiday_last_year_num = getValueForDate(last_holiday_last_year[0], true);
3745
3746 if (date_num < last_holiday_last_year_num ) {
3747 return [ false, last_holiday_last_year[0] ];
3748 } else if (date_num == last_holiday_last_year_num) {
3749 return [true, dateAtDayMinutes(last_holiday_last_year[0], minutes_in_day),
3750 'Day after ' +last_holiday_last_year[1] ];
3751 }
3752 }
3753
3754 return [ false, holidays[i][0] ];
3755 } else if (date_num == next_holiday_date_num) {
3756 return [true, new Date(date.getFullYear(), date.getMonth(), date.getDate() + 1),
3757 (add_days[0] > 0 ? 'Day after ' : (add_days[0] < 0 ? 'Day before ' : '')) + holidays[i][1] ];
3758 }
3759 }
3760
3761 if (add_days[0] < 0) {
3762 // Calculate the first holiday from next year to tested against it.
3763 var holidays_next_year = getApplyingHolidaysForYear(applying_holidays, date.getFullYear() + 1, add_days);
3764 var first_holidays_next_year = holidays_next_year[0];
3765 var first_holidays_next_year_num = getValueForDate(first_holidays_next_year[0], true);
3766 if (date_num == first_holidays_next_year_num) {
3767 return [true, dateAtDayMinutes(first_holidays_next_year[0], minutes_in_day),
3768 'Day before ' + first_holidays_next_year[1] ];
3769 }
3770 }
3771
3772 // continue next year
3773 return [ false, new Date(holidays[0][0].getFullYear() + 1,
3774 holidays[0][0].getMonth(),
3775 holidays[0][0].getDate()) ];
3776
3777 }}(applying_holidays, add_days);
3778
3779 if (push_to_weekday)
3780 selectors.weekday.push(selector);
3781 else
3782 selectors.holiday.push(selector);
3783
3784 at += 1 + add_days[1];
3785 } else if (tokens[at][0] == 'SH') {
3786 var applying_holidays = getMatchingHoliday(tokens[at][0]);
3787
3788 var holidays = []; // needs to be sorted each time because of movable days
3789
3790 var selector = function(applying_holidays) { return function(date) {
3791 var date_num = getValueForDate(date);
3792
3793 // Iterate over holiday array containing the different holiday ranges.
3794 for (var i = 0; i < applying_holidays.length; i++) {
3795
3796 var holiday = getSHForYear(applying_holidays[i], date.getFullYear());
3797
3798 for (var h = 0; h < holiday.length; h+=4) {
3799 var holiday_to_plus = new Date(date.getFullYear(), holiday[2+h] - 1, holiday[3+h] + 1);
3800 var holiday_from = (holiday[0+h] - 1) * 100 + holiday[1+h];
3801 var holiday_to = (holiday[2+h] - 1) * 100 + holiday[3+h];
3802 holiday_to_plus = getValueForDate(holiday_to_plus);
3803
3804 var holiday_ends_next_year = holiday_to < holiday_from;
3805
3806 if (date_num < holiday_from) { // date is before selected holiday
3807
3808 // check if we are in the holidays from the last year spanning into this year
3809 var last_year_holiday = getSHForYear(applying_holidays[applying_holidays.length - 1], date.getFullYear() - 1, false);
3810 if (typeof last_year_holiday != 'undefined') {
3811 var last_year_holiday_from = (last_year_holiday[last_year_holiday.length - 4] - 1) * 100
3812 + last_year_holiday[last_year_holiday.length - 3]; // e.g. 1125
3813 var last_year_holiday_to = (last_year_holiday[last_year_holiday.length - 2] - 1) * 100
3814 + last_year_holiday[last_year_holiday.length - 1]; // e.g. 0005
3815
3816 if (last_year_holiday_to < last_year_holiday_from && date_num < last_year_holiday_to)
3817 return [ true, new Date(date.getFullYear(),
3818 last_year_holiday[last_year_holiday.length - 2] - 1,
3819 last_year_holiday[last_year_holiday.length - 1] + 1),
3820 applying_holidays[applying_holidays.length - 1].name ];
3821 else
3822 return [ false, new Date(date.getFullYear(), holiday[0+h] - 1, holiday[1+h]) ];
3823 } else { // school holidays for last year are not defined.
3824 return [ false, new Date(date.getFullYear(), holiday[0+h] - 1, holiday[1+h]) ];
3825 }
3826 } else if (holiday_from <= date_num && (date_num < holiday_to_plus || holiday_ends_next_year)) {
3827 return [ true, new Date(date.getFullYear() + holiday_ends_next_year, holiday[2+h] - 1, holiday[3+h] + 1),
3828 applying_holidays[i].name ];
3829 } else if (holiday_to_plus == date_num) { // selected holiday end is equal to month and day
3830 if (h + 4 < holiday.length) { // next holiday is next date range of the same holidays
3831 h += 4;
3832 return [ false, new Date(date.getFullYear(), holiday[0+h] - 1, holiday[1+h]) ];
3833 } else {
3834 if (i + 1 == applying_holidays.length) { // last holidays are handled, continue all over again
3835 var holiday = getSHForYear(applying_holidays[0], date.getFullYear() + 1);
3836 return [ false, new Date(date.getFullYear() + !holiday_ends_next_year, holiday[0+h] - 1, holiday[1+h]) ];
3837 } else { // return the start of the next holidays
3838 var holiday = getSHForYear(applying_holidays[i+1], date.getFullYear());
3839 return [ false, new Date(date.getFullYear(), holiday[0] - 1, holiday[1]) ];
3840 }
3841 }
3842 }
3843 }
3844 }
3845 return [ false ];
3846 }}(applying_holidays);
3847
3848 if (push_to_weekday)
3849 selectors.weekday.push(selector);
3850 else
3851 selectors.holiday.push(selector);
3852 at += 1;
3853 }
3854 } else if (matchTokens(tokens, at, 'weekday')) {
3855 return parseWeekdayRange(tokens, at, selectors, true);
3856 } else {
3857 throw formatWarnErrorMessage(nrule, at, 'Unexpected token (school holiday parser): ' + tokens[at][1]);
3858 }
3859
3860 if (!matchTokens(tokens, at, ','))
3861 break;
3862 }
3863
3864 return at;
3865 }
3866
3867 // Helpers for holiday parsers {{{
3868
3869 /* Returns a number for a date which can then be used to compare just the dates (without the time). {{{
3870 * 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.
3871 * Example: Returns 20150015 for Jan 01 2015
3872 *
3873 * :param date: Date object.
3874 * :param include_year: Boolean. If true include the year.
3875 * :returns: Number for the date.
3876 */
3877 function getValueForDate(date, include_year) {
3878 // Implicit because undefined evaluates to false.
3879 // include_year = typeof include_year != 'undefined' ? include_year : false;
3880
3881 return (include_year ? date.getFullYear() * 10000 : 0) + date.getMonth() * 100 + date.getDate();
3882 }
3883 // }}}
3884
3885 // return the school holiday definition e.g. [ 5, 25, /* to */ 6, 5 ],
3886 // for the specified year
3887 function getSHForYear(SH_hash, year, fatal) {
3888 if (typeof fatal == 'undefined')
3889 fatal = true;
3890
3891 var holiday = SH_hash[year];
3892 if (typeof holiday == 'undefined') {
3893 holiday = SH_hash['default']; // applies for any year without explicit definition
3894 if (typeof holiday == 'undefined') {
3895 if (fatal) {
3896 throw formatLibraryBugMessage('School holiday ' + SH_hash.name + ' has no definition for the year ' + year + '.'
3897 + ' You can also add them: ' + repository_url);
3898 } else {
3899 return undefined;
3900 }
3901 }
3902 }
3903 return holiday;
3904 }
3905
3906 // Return closed holiday definition available.
3907 // First try to get the state, if missing get the country wide holidays
3908 // (which can be limited to some states).
3909 function getMatchingHoliday(type_of_holidays) {
3910 if (typeof location_cc != 'undefined') {
3911 if (holidays.hasOwnProperty(location_cc)) {
3912 if (typeof location_state != 'undefined') {
3913 if (holidays[location_cc][location_state]
3914 && holidays[location_cc][location_state][type_of_holidays]) {
3915 // if holidays for the state are specified use it
3916 // and ignore lesser specific ones (for the country)
3917 return holidays[location_cc][location_state][type_of_holidays];
3918 } else if (holidays[location_cc][type_of_holidays]) {
3919 // holidays are only defined country wide
3920 var matching_holiday = {}; // holidays in the country wide scope can be limited to certain states
3921 for (var holiday_name in holidays[location_cc][type_of_holidays]) {
3922 if (typeof holidays[location_cc][type_of_holidays][holiday_name][2] === 'object') {
3923 if (-1 != holidays[location_cc][type_of_holidays][holiday_name][2].indexOf(location_state))
3924 matching_holiday[holiday_name] = holidays[location_cc][type_of_holidays][holiday_name];
3925 } else {
3926 matching_holiday[holiday_name] = holidays[location_cc][type_of_holidays][holiday_name];
3927 }
3928 }
3929 if (Object.keys(matching_holiday).length === 0)
3930 throw formatLibraryBugMessage('There are no holidays ' + type_of_holidays + ' defined for country ' + location_cc + '.'
3931 + ' You can also add them: ' + repository_url);
3932 return matching_holiday;
3933 } else {
3934 throw formatLibraryBugMessage('Holidays ' + type_of_holidays + ' are not defined for country ' + location_cc
3935 + ' and state ' + location_state + '.'
3936 + ' You can also add them: ' + repository_url);
3937 }
3938 }
3939 } else {
3940 throw formatLibraryBugMessage('No holidays are defined for country ' + location_cc + '.'
3941 + ' You can also add them: ' + repository_url);
3942 }
3943 } else { // we have no idea which holidays do apply because the country code was not provided
3944 throw 'Country code missing which is needed to select the correct holidays (see README how to provide it)';
3945 }
3946 }
3947
3948 function getMovableEventsForYear(Y) {
3949 // calculate easter
3950 var C = Math.floor(Y/100);
3951 var N = Y - 19*Math.floor(Y/19);
3952 var K = Math.floor((C - 17)/25);
3953 var I = C - Math.floor(C/4) - Math.floor((C - K)/3) + 19*N + 15;
3954 I = I - 30*Math.floor((I/30));
3955 I = I - Math.floor(I/28)*(1 - Math.floor(I/28)*Math.floor(29/(I + 1))*Math.floor((21 - N)/11));
3956 var J = Y + Math.floor(Y/4) + I + 2 - C + Math.floor(C/4);
3957 J = J - 7*Math.floor(J/7);
3958 var L = I - J;
3959 var M = 3 + Math.floor((L + 40)/44);
3960 var D = L + 28 - 31*Math.floor(M/4);
3961
3962 // calculate orthodox easter
3963 var oA = Y % 4;
3964 var oB = Y % 7;
3965 var oC = Y % 19;
3966 var oD = (19*oC + 15) % 30;
3967 var oE = (2*oA+4*oB - oD + 34) % 7;
3968 var oF = oD+oE;
3969
3970 var oDate;
3971 if (oF < 9) {
3972 oDate = new Date(Y, 4-1, oF+4);
3973 } else {
3974 if ((oF+4)<31) {
3975 oDate = new Date(Y, 4-1, oF+4);
3976 } else {
3977 oDate = new Date(Y, 5-1, oF-26);
3978 }
3979 }
3980
3981 // calculate last Sunday in February
3982 var lastFebruaryDay = new Date(Y, 2, 0);
3983 var lastFebruarySunday = lastFebruaryDay.getDate() - lastFebruaryDay.getDay();
3984
3985 // calculate Victoria Day. last Monday before or on May 24
3986 var may_24 = new Date(Y, 4, 24);
3987 var victoriaDay = 24 - ((6 + may_24.getDay()) % 7);
3988
3989 // calculate Canada Day. July 1st unless 1st is on Sunday, then July 2.
3990 var july_1 = new Date(Y, 6, 1);
3991 var canadaDay = july_1.getDay() === 0 ? 2 : 1;
3992
3993 // calculate first Monday for each month
3994 var firstMondays = {};
3995 for (var i = 0; i < 12; i++) {
3996 var first = new Date(Y, i, 1);
3997 var firstMonday = 1 + ((8 - first.getDay()) % 7);
3998 firstMondays[i] = firstMonday;
3999 }
4000
4001 return {
4002 'firstFebruaryMonday': new Date(Y, 1, firstMondays[1]),
4003 'lastFebruarySunday': new Date(Y, 1, lastFebruarySunday),
4004 'easter': new Date(Y, M - 1, D),
4005 'victoriaDay': new Date(Y, 4, victoriaDay),
4006 'canadaDay': new Date(Y, 6, canadaDay),
4007 'firstAugustMonday': new Date(Y, 7, firstMondays[7]),
4008 'firstSeptemberMonday': new Date(Y, 8, firstMondays[8]),
4009 'firstOctoberMonday': new Date(Y, 9, firstMondays[9]),
4010 'orthodox easter' : oDate,
4011 };
4012 }
4013
4014 function getApplyingHolidaysForYear(applying_holidays, year, add_days) {
4015 var movableDays = getMovableEventsForYear(year);
4016
4017 var sorted_holidays = [];
4018 var next_holiday;
4019
4020 for (var holiday_name in applying_holidays) {
4021 if (typeof applying_holidays[holiday_name][0] == 'string') {
4022 var selected_movableDay = movableDays[applying_holidays[holiday_name][0]];
4023 if (!selected_movableDay)
4024 throw 'Movable day ' + applying_holidays[holiday_name][0] + ' can not not be calculated.'
4025 + ' Please add the formula how to calculate it.';
4026 next_holiday = new Date(selected_movableDay.getFullYear(),
4027 selected_movableDay.getMonth(),
4028 selected_movableDay.getDate()
4029 + applying_holidays[holiday_name][1]
4030 );
4031 if (year != next_holiday.getFullYear())
4032 throw 'The movable day ' + applying_holidays[holiday_name][0] + ' plus '
4033 + applying_holidays[holiday_name][1]
4034 + ' days is not in the year of the movable day anymore. Currently not supported.';
4035 } else {
4036 next_holiday = new Date(year,
4037 applying_holidays[holiday_name][0] - 1,
4038 applying_holidays[holiday_name][1]
4039 );
4040 }
4041 if (add_days[0])
4042 next_holiday.setDate(next_holiday.getDate() + add_days[0]);
4043
4044 sorted_holidays.push([ next_holiday, holiday_name ]);
4045 }
4046
4047 sorted_holidays = sorted_holidays.sort(function(a,b){
4048 if (a[0].getTime() < b[0].getTime()) return -1;
4049 if (a[0].getTime() > b[0].getTime()) return 1;
4050 return 0;
4051 });
4052
4053 return sorted_holidays;
4054 }
4055 // }}}
4056 // }}}
4057
4058 /* Year range parser (2013,2016-2018,2020/2). {{{
4059 *
4060 * :param tokens: List of token objects.
4061 * :param at: Position where to start.
4062 * :returns: Position at which the token does not belong to the selector anymore.
4063 */
4064 function parseYearRange(tokens, at) {
4065 tokens[at][3] = 'year';
4066 for (; at < tokens.length; at++) {
4067 if (matchTokens(tokens, at, 'year')) {
4068 var is_range = false,
4069 has_period,
4070 period;
4071 if (matchTokens(tokens, at+1, '-', 'year', '/', 'number')) {
4072 is_range = true;
4073 has_period = true;
4074 period = parseInt(tokens[at+4][0]);
4075 checkPeriod(at+4, period, 'year');
4076 } else {
4077 is_range = matchTokens(tokens, at+1, '-', 'year');
4078 has_period = matchTokens(tokens, at+1, '/', 'number');
4079 if (has_period) {
4080 period = parseInt(tokens[at+2][0]);
4081 checkPeriod(at+2, period, 'year', 'no_end_year');
4082 } else if (matchTokens(tokens, at+1, '+')) {
4083 period = 1;
4084 has_period = 2;
4085 }
4086 }
4087
4088 var year_from = parseInt(tokens[at][0]);
4089 // error checking {{{
4090 if (is_range && tokens[at+2][0] <= year_from) {
4091 // handle reversed range
4092 if (tokens[at+2][0] == year_from)
4093 throw formatWarnErrorMessage(nrule, at,
4094 'A year range in which the start year is equal to the end year does not make sense.'
4095 + ' Please remove the end year. E.g. "' + year_from + ' May 23"');
4096 else
4097 throw formatWarnErrorMessage(nrule, at,
4098 'A year range in which the start year is greater than the end year does not make sense.'
4099 + ' Please turn it over.');
4100 }
4101 // }}}
4102
4103 selectors.year.push(function(tokens, at, year_from, is_range, has_period, period) { return function(date) {
4104 var ouryear = date.getFullYear();
4105 var year_to = is_range ? parseInt(tokens[at+2][0]) : year_from;
4106
4107 if (ouryear < year_from ){
4108 return [false, new Date(year_from, 0, 1)];
4109 } else if (has_period) {
4110 if (year_from <= ouryear) {
4111 if (is_range && ouryear > year_to)
4112 return [false];
4113 if (period > 0) {
4114 if ((ouryear - year_from) % period === 0) {
4115 return [true, new Date(ouryear + 1, 0, 1)];
4116 } else {
4117 return [false, new Date(ouryear + period - 1, 0, 1)];
4118 }
4119 }
4120 }
4121 } else if (is_range) {
4122 if (ouryear <= year_to)
4123 return [true, new Date(year_to + 1, 0, 1)];
4124 } else if (ouryear == year_from) {
4125 return [true];
4126 }
4127
4128 return [false];
4129
4130 }}(tokens, at, year_from, is_range, has_period, period));
4131
4132 at += 1 + (is_range ? 2 : 0) + (has_period ? (has_period == 2 ? 1 : 2) : 0);
4133 } else {
4134 throw formatWarnErrorMessage(nrule, at, 'Unexpected token in year range: ' + tokens[at][1]);
4135 }
4136
4137 if (!matchTokens(tokens, at, ','))
4138 break;
4139 }
4140
4141 return at;
4142 }
4143 // }}}
4144
4145 /* Week range parser (week 11-20, week 1-53/2). {{{
4146 *
4147 * :param tokens: List of token objects.
4148 * :param at: Position where to start.
4149 * :returns: Position at which the token does not belong to the selector anymore.
4150 */
4151 function parseWeekRange(tokens, at) {
4152 for (; at < tokens.length; at++) {
4153 if (matchTokens(tokens, at, 'week')) {
4154 at++;
4155 }
4156 if (matchTokens(tokens, at, 'number')) {
4157 var is_range = matchTokens(tokens, at+1, '-', 'number'), period = 0;
4158 var week_from = tokens[at][0];
4159 var week_to = is_range ? tokens[at+2][0] : week_from;
4160 if (week_from > week_to) {
4161 throw formatWarnErrorMessage(nrule, at+2,
4162 'You have specified a week range in reverse order or leaping over a year. This is (currently) not supported.');
4163 }
4164 if (week_from < 1) {
4165 throw formatWarnErrorMessage(nrule, at,
4166 'You have specified a week date less then one. A valid week date range is 1-53.');
4167 }
4168 if (week_to > 53) {
4169 throw formatWarnErrorMessage(nrule, is_range ? at+2 : at,
4170 'You have specified a week date greater then 53. A valid week date range is 1-53.');
4171 }
4172 if (is_range) {
4173 period = matchTokens(tokens, at+3, '/', 'number');
4174 if (period) {
4175 period = tokens[at+4][0];
4176 if (period < 2) {
4177 throw formatWarnErrorMessage(nrule, at+4,
4178 'You have specified a week period which is less than two.'
4179 + ' If you want to select the whole range from week ' + week_from + ' to week ' + week_to + ' then just omit the "/' + period + '".');
4180 } else if (period > 26) {
4181 throw formatWarnErrorMessage(nrule, at+4,
4182 'You have specified a week period which is greater than 26.'
4183 + ' 26.5 is the half of the maximum 53 week dates per year so a week date period greater than 26 would only apply once per year.'
4184 + ' Please specify the week selector as "week ' + week_from + '" if that is what you want to express.');
4185 }
4186 }
4187 }
4188
4189 if (week_stable && (!(week_from <= 1 && week_to >= 53) || period)) {
4190 week_stable = false;
4191 }
4192
4193 if (!period && week_from == 1 && week_to == 53) {
4194 /* Shortcut and work around bug. */
4195 selectors.week.push(function(date) { return [true]; });
4196 } else {
4197
4198 selectors.week.push(function(week_from, week_to, is_range, period) { return function(date) {
4199 var ourweek = getWeekNumber(date);
4200
4201 // console.log("week_from: %s, week_to: %s", week_from, week_to);
4202 // console.log("ourweek: %s, date: %s", ourweek, date);
4203
4204 // before range
4205 if (ourweek < week_from) {
4206 // console.log("Before: " + getNextDateOfISOWeek(week_from, date));
4207 return [false, getNextDateOfISOWeek(week_from, date)];
4208 }
4209
4210 // we're after range, set check date to next year
4211 if (ourweek > week_to) {
4212 // console.log("After");
4213 return [false, getNextDateOfISOWeek(week_from, date)];
4214 }
4215
4216 // we're in range
4217 if (period) {
4218 var in_period = (ourweek - week_from) % period === 0;
4219 if (in_period) {
4220 return [true, getNextDateOfISOWeek(ourweek + 1, date)];
4221 } else {
4222 return [false, getNextDateOfISOWeek(ourweek + period - 1, date)];
4223 }
4224 }
4225
4226 // console.log("Match");
4227 return [true, getNextDateOfISOWeek(week_to == 53 ? 1 : week_to + 1, date)];
4228 }}(week_from, week_to, is_range, period));
4229 }
4230
4231 at += 1 + (is_range ? 2 : 0) + (period ? 2 : 0);
4232 } else {
4233 throw formatWarnErrorMessage(nrule, at, 'Unexpected token in week range: ' + tokens[at][1]);
4234 }
4235
4236 if (!matchTokens(tokens, at, ','))
4237 break;
4238 }
4239
4240 return at;
4241 }
4242
4243 // http://stackoverflow.com/a/6117889
4244 /* For a given date, get the ISO week number
4245 *
4246 * Based on information at:
4247 *
4248 * http://www.merlyn.demon.co.uk/weekcalc.htm#WNR
4249 *
4250 * Algorithm is to find nearest thursday, it's year
4251 * is the year of the week number. Then get weeks
4252 * between that date and the first day of that year.
4253 *
4254 * Note that dates in one year can be weeks of previous
4255 * or next year, overlap is up to 3 days.
4256 *
4257 * e.g. 2014/12/29 is Monday in week 1 of 2015
4258 * 2012/1/1 is Sunday in week 52 of 2011
4259 */
4260 function getWeekNumber(d) {
4261 // Copy date so don't modify original
4262 d = new Date(+d);
4263 d.setHours(0,0,0);
4264 // Set to nearest Thursday: current date + 4 - current day number
4265 // Make Sunday's day number 7
4266 d.setDate(d.getDate() + 4 - (d.getDay()||7));
4267 // Get first day of year
4268 var yearStart = new Date(d.getFullYear(),0,1);
4269 // Calculate full weeks to nearest Thursday
4270 return Math.ceil(( ( (d - yearStart) / 86400000) + 1)/7)
4271 }
4272 // http://stackoverflow.com/a/16591175
4273 function getDateOfISOWeek(w, y) {
4274 var simple = new Date(y, 0, 1 + (w - 1) * 7);
4275 var dow = simple.getDay();
4276 var ISOweekStart = simple;
4277 if (dow <= 4)
4278 ISOweekStart.setDate(simple.getDate() - simple.getDay() + 1);
4279 else
4280 ISOweekStart.setDate(simple.getDate() + 8 - simple.getDay());
4281 return ISOweekStart;
4282 }
4283 function getNextDateOfISOWeek(week, date) {
4284 var next_date;
4285 for (var i = -1; i <= 1; i++) {
4286 next_date = getDateOfISOWeek(week, date.getFullYear() + i);
4287 if (next_date.getTime() > date.getTime()) {
4288 return next_date;
4289 }
4290 }
4291 throw formatLibraryBugMessage();
4292 }
4293 // }}}
4294
4295 /* Month range parser (Jan,Feb-Mar). {{{
4296 *
4297 * :param tokens: List of token objects.
4298 * :param at: Position where to start.
4299 * :param push_to_monthday: Will push the selector into the monthday selector array which has the desired side effect of working in conjunction with the monthday selectors (either the month match or the monthday).
4300 * :returns: Position at which the token does not belong to the selector anymore.
4301 */
4302 function parseMonthRange(tokens, at, push_to_monthday, in_selector) {
4303 if (!in_selector)
4304 tokens[at][3] = 'month';
4305
4306 for (; at < tokens.length; at++) {
4307 // Use parseMonthdayRange if '<month> <daynum>' and not '<month> <hour>:<minute>'
4308 if (matchTokens(tokens, at, 'month', 'number') && !matchTokens(tokens, at+2, 'timesep', 'number')) {
4309 return parseMonthdayRange(tokens, at, nrule, true);
4310 } else if (matchTokens(tokens, at, 'month')) {
4311 // Single month (Jan) or month range (Feb-Mar)
4312 var is_range = matchTokens(tokens, at+1, '-', 'month');
4313
4314 var month_from = tokens[at][0];
4315 var month_to = is_range ? tokens[at+2][0] : month_from;
4316
4317 if (is_range && week_stable) {
4318 if (month_from !== (month_to + 1) % 12)
4319 week_stable = false;
4320 } else {
4321 week_stable = false;
4322 }
4323
4324 var inside = true;
4325
4326 // handle reversed range
4327 if (month_to < month_from) {
4328 var tmp = month_to;
4329 month_to = month_from - 1;
4330 month_from = tmp + 1;
4331 inside = false;
4332 }
4333
4334 var selector = function(tokens, at, month_from, month_to, is_range, inside) { return function(date) {
4335 var ourmonth = date.getMonth();
4336
4337 // handle full range
4338 if (month_to < month_from)
4339 return [!inside];
4340
4341 if (ourmonth < month_from || ourmonth > month_to) {
4342 return [!inside, dateAtNextMonth(date, month_from)];
4343 } else {
4344 return [inside, dateAtNextMonth(date, month_to + 1)];
4345 }
4346 }}(tokens, at, month_from, month_to, is_range, inside);
4347
4348 if (push_to_monthday === true)
4349 selectors.monthday.push(selector);
4350 else
4351 selectors.month.push(selector);
4352
4353 at += is_range ? 3 : 1;
4354 } else {
4355 throw formatWarnErrorMessage(nrule, at, 'Unexpected token in month range: ' + tokens[at][1]);
4356 }
4357
4358 if (!matchTokens(tokens, at, ','))
4359 break;
4360 }
4361
4362 return at;
4363 }
4364
4365 function dateAtNextMonth(date, month) {
4366 return new Date(date.getFullYear(), month < date.getMonth() ? month + 12 : month);
4367 }
4368 // }}}
4369
4370 /* Month day range parser (Jan 26-31; Jan 26-Feb 26). {{{
4371 *
4372 * :param tokens: List of token objects.
4373 * :param at: Position where to start.
4374 * :param nrule: Rule number starting with 0.
4375 * :param push_to_month: Will push the selector into the month selector array which has the desired side effect of working in conjunction with the month selectors (either the month match or the monthday).
4376 * :returns: Position at which the token does not belong to the selector anymore.
4377 */
4378 function parseMonthdayRange(tokens, at, nrule, push_to_month) {
4379 if (!push_to_month)
4380 tokens[at][3] = 'month';
4381
4382 for (; at < tokens.length; at++) {
4383 var has_year = [], has_month = [], has_event = [], has_calc = [], has_constrained_weekday = [];
4384 has_year[0] = matchTokens(tokens, at, 'year');
4385 has_month[0] = matchTokens(tokens, at+has_year[0], 'month', 'number');
4386 has_event[0] = matchTokens(tokens, at+has_year[0], 'event');
4387
4388 if (has_event[0])
4389 has_calc[0] = getMoveDays(tokens, at+has_year[0]+1, 200, 'event like easter');
4390
4391 var at_range_sep;
4392 if (matchTokens(tokens, at+has_year[0], 'month', 'weekday', '[')) {
4393 has_constrained_weekday[0] = getConstrainedWeekday(tokens, at+has_year[0]+3);
4394 has_calc[0] = getMoveDays(tokens, has_constrained_weekday[0][1], 6, 'constrained weekdays');
4395 at_range_sep = has_constrained_weekday[0][1] + (typeof has_calc[0] != 'undefined' && has_calc[0][1] ? 3 : 0);
4396 } else {
4397 at_range_sep = at+has_year[0]
4398 + (has_event[0]
4399 ? (typeof has_calc[0] != 'undefined' && has_calc[0][1] ? 4 : 1)
4400 : 2);
4401 }
4402
4403 var at_sec_event_or_month;
4404 if ((has_month[0] || has_event[0] || has_constrained_weekday[0]) && matchTokens(tokens, at_range_sep, '-')) {
4405 has_year[1] = matchTokens(tokens, at_range_sep+1, 'year');
4406 at_sec_event_or_month = at_range_sep+1+has_year[1];
4407 has_month[1] = matchTokens(tokens, at_sec_event_or_month, 'month', 'number');
4408 if (!has_month[1]) {
4409 has_event[1] = matchTokens(tokens, at_sec_event_or_month, 'event');
4410 if (has_event[1]) {
4411 has_calc[1] = getMoveDays(tokens, at_sec_event_or_month+1, 366, 'event like easter');
4412 } else if (matchTokens(tokens, at_sec_event_or_month, 'month', 'weekday', '[')) {
4413 has_constrained_weekday[1] = getConstrainedWeekday(tokens, at_sec_event_or_month+3);
4414 has_calc[1] = getMoveDays(tokens, has_constrained_weekday[1][1], 6, 'constrained weekdays');
4415 }
4416 }
4417 }
4418
4419 // monthday range like Jan 26-Feb 26 {{{
4420 if (has_year[0] == has_year[1] && (has_month[1] || has_event[1] || has_constrained_weekday[1])) {
4421
4422 if (has_month[0])
4423 checkIfDateIsValid(tokens[at+has_year[0]][0], tokens[at+has_year[0]+1][0], nrule, at+has_year[0]+1);
4424 if (has_month[1])
4425 checkIfDateIsValid(tokens[at_sec_event_or_month][0], tokens[at_sec_event_or_month+1][0], nrule, at_sec_event_or_month+1);
4426
4427 var selector = function(tokens, at, nrule, has_year, has_event, has_calc, at_sec_event_or_month, has_constrained_weekday) { return function(date) {
4428 var start_of_next_year = new Date(date.getFullYear() + 1, 0, 1);
4429
4430 var movableDays,
4431 from_date;
4432 if (has_event[0]) {
4433 movableDays = getMovableEventsForYear(has_year[0] ? parseInt(tokens[at][0]) : date.getFullYear());
4434 from_date = movableDays[tokens[at+has_year[0]][0]];
4435
4436 if (typeof has_calc[0] != 'undefined' && has_calc[0][1]) {
4437 var from_year_before_calc = from_date.getFullYear();
4438 from_date.setDate(from_date.getDate() + has_calc[0][0]);
4439 if (from_year_before_calc != from_date.getFullYear())
4440 throw formatWarnErrorMessage(nrule, at+has_year[0]+has_calc[0][1]*3,
4441 'The movable day ' + tokens[at+has_year[0]][0] + ' plus ' + has_calc[0][0]
4442 + ' days is not in the year of the movable day anymore. Currently not supported.');
4443 }
4444 } else if (has_constrained_weekday[0]) {
4445 from_date = getDateForConstrainedWeekday((has_year[0] ? tokens[at][0] : date.getFullYear()), // year
4446 tokens[at+has_year[0]][0], // month
4447 tokens[at+has_year[0]+1][0], // weekday
4448 has_constrained_weekday[0],
4449 has_calc[0]);
4450 } else {
4451 from_date = new Date((has_year[0] ? tokens[at][0] : date.getFullYear()),
4452 tokens[at+has_year[0]][0], tokens[at+has_year[0]+1][0]);
4453 }
4454
4455 var to_date;
4456 if (has_event[1]) {
4457 movableDays = getMovableEventsForYear(has_year[1]
4458 ? parseInt(tokens[at_sec_event_or_month-1][0])
4459 : date.getFullYear());
4460 to_date = movableDays[tokens[at_sec_event_or_month][0]];
4461
4462 if (typeof has_calc[1] != 'undefined' && has_calc[1][1]) {
4463 var to_year_before_calc = to_date.getFullYear();
4464 to_date.setDate(to_date.getDate() + has_calc[1][0]);
4465 if (to_year_before_calc != to_date.getFullYear())
4466 throw formatWarnErrorMessage(nrule, at_sec_event_or_month+has_calc[1][1],
4467 'The movable day ' + tokens[at_sec_event_or_month][0] + ' plus ' + has_calc[1][0]
4468 + ' days is not in the year of the movable day anymore. Currently not supported.');
4469 }
4470 } else if (has_constrained_weekday[1]) {
4471 to_date = getDateForConstrainedWeekday((has_year[1] ? tokens[at_sec_event_or_month-1][0] : date.getFullYear()), // year
4472 tokens[at_sec_event_or_month][0], // month
4473 tokens[at_sec_event_or_month+1][0], // weekday
4474 has_constrained_weekday[1],
4475 has_calc[1]);
4476 } else {
4477 to_date = new Date((has_year[1] ? tokens[at_sec_event_or_month-1][0] : date.getFullYear()),
4478 tokens[at_sec_event_or_month][0], tokens[at_sec_event_or_month+1][0] + 1);
4479 }
4480
4481 var inside = true;
4482
4483 if (to_date < from_date) {
4484 var tmp = to_date;
4485 to_date = from_date;
4486 from_date = tmp;
4487 inside = false;
4488 }
4489
4490 if (date.getTime() < from_date.getTime()) {
4491 return [!inside, from_date];
4492 } else if (date.getTime() < to_date.getTime()) {
4493 return [inside, to_date];
4494 } else {
4495 if (has_year[0]) {
4496 return [!inside];
4497 } else {
4498 return [!inside, start_of_next_year];
4499 }
4500 }
4501 }}(tokens, at, nrule, has_year, has_event, has_calc, at_sec_event_or_month, has_constrained_weekday);
4502
4503 if (push_to_month === true)
4504 selectors.month.push(selector);
4505 else
4506 selectors.monthday.push(selector);
4507
4508 at = (has_constrained_weekday[1]
4509 ? has_constrained_weekday[1][1]
4510 : at_sec_event_or_month + (has_event[1] ? 1 : 2))
4511 + (typeof has_calc[1] != 'undefined' ? has_calc[1][1] : 0);
4512
4513 // }}}
4514 // Monthday range like Jan 26-31 {{{
4515 } else if (has_month[0]) {
4516
4517 has_year = has_year[0];
4518 var year = tokens[at][0]; // Could be month if has no year. Tested later.
4519 var month = tokens[at+has_year][0];
4520
4521 var first_round = true;
4522
4523 do {
4524 var range_from = tokens[at+1 + has_year][0];
4525 var is_range = matchTokens(tokens, at+2+has_year, '-', 'number');
4526 var period = undefined;
4527 var range_to = tokens[at+has_year+(is_range ? 3 : 1)][0] + 1;
4528 if (is_range && matchTokens(tokens, at+has_year+4, '/', 'number')) {
4529 period = tokens[at+has_year+5][0];
4530 checkPeriod(at+has_year+5, period, 'day');
4531 }
4532
4533 if (first_round) {
4534 var at_timesep_if_monthRange = at + has_year + 1 // at month number
4535 + (is_range ? 2 : 0) + (period ? 2 : 0)
4536 + !(is_range || period); // if not range nor has period, add one
4537
4538 // Check for '<month> <timespan>'
4539 if (matchTokens(tokens, at_timesep_if_monthRange, 'timesep', 'number')
4540 && (matchTokens(tokens, at_timesep_if_monthRange+2, '+')
4541 || matchTokens(tokens, at_timesep_if_monthRange+2, '-')
4542 || oh_mode !== 0)) {
4543 return parseMonthRange(tokens, at, true, true);
4544 }
4545 }
4546
4547 // error checking {{{
4548 if (range_to < range_from)
4549 throw formatWarnErrorMessage(nrule, at+has_year+3,
4550 'Range in wrong order. From day is greater than to day.');
4551
4552 checkIfDateIsValid(month, range_from, nrule, at+1 + has_year);
4553 checkIfDateIsValid(month, range_to - 1 /* added previously */,
4554 nrule, at+has_year+(is_range ? 3 : 1));
4555 // }}}
4556
4557 var selector = function(year, has_year, month, range_from, range_to, period) { return function(date) {
4558 var start_of_next_year = new Date(date.getFullYear() + 1, 0, 1);
4559
4560 var from_date = new Date(has_year ? year : date.getFullYear(),
4561 month, range_from);
4562 if (month == 1 && range_from != from_date.getDate()) // Only on leap years does this day exist.
4563 return [false]; // If day 29 does not exist,
4564 // then the date object adds one day to date
4565 // and this selector should not match.
4566 var to_date = new Date(from_date.getFullYear(),
4567 month, range_to);
4568 if (month == 1 && is_range && range_to != to_date.getDate()) // Only on leap years does this day exist.
4569 return [false];
4570
4571 if (date.getTime() < from_date.getTime())
4572 return [false, from_date];
4573 else if (date.getTime() >= to_date.getTime())
4574 return [false, start_of_next_year];
4575 else if (!period)
4576 return [true, to_date];
4577
4578 var nday = Math.floor((date.getTime() - from_date.getTime()) / msec_in_day);
4579 var in_period = nday % period;
4580
4581 if (in_period === 0)
4582 return [true, new Date(date.getFullYear(), date.getMonth(), date.getDate() + 1)];
4583 else
4584 return [false, new Date(date.getFullYear(), date.getMonth(), date.getDate() + period - in_period)];
4585
4586 }}(year, has_year, month, range_from, range_to, period);
4587
4588 if (push_to_month === true)
4589 selectors.month.push(selector);
4590 else
4591 selectors.monthday.push(selector);
4592
4593 at += 2 + has_year + (is_range ? 2 : 0) + (period ? 2 : 0);
4594
4595 first_round = false;
4596 }
4597 while (matchTokens(tokens, at, ',', 'number'))
4598
4599
4600 // }}}
4601 // Only event like easter {{{
4602 } else if (has_event[0]) {
4603
4604 var selector = function(tokens, at, nrule, has_year, add_days) { return function(date) {
4605
4606 // console.log('enter selector with date: ' + date);
4607 var movableDays = getMovableEventsForYear((has_year ? tokens[at][0] : date.getFullYear()));
4608 var event_date = movableDays[tokens[at+has_year][0]];
4609 if (!event_date)
4610 throw 'Movable day ' + tokens[at+has_year][0] + ' can not not be calculated.'
4611 + ' Please add the formula how to calculate it.';
4612
4613 if (add_days[0]) {
4614 event_date.setDate(event_date.getDate() + add_days[0]);
4615 if (date.getFullYear() != event_date.getFullYear())
4616 throw formatWarnErrorMessage(nrule, at+has_year+add_days[1], 'The movable day ' + tokens[at+has_year][0] + ' plus '
4617 + add_days[0]
4618 + ' days is not in the year of the movable day anymore. Currently not supported.');
4619 }
4620
4621 if (date.getTime() < event_date.getTime())
4622 return [false, event_date];
4623 // else if (date.getTime() < event_date.getTime() + msec_in_day) // does not work because of daylight saving times
4624 else if (event_date.getMonth() * 100 + event_date.getDate() == date.getMonth() * 100 + date.getDate())
4625 return [true, new Date(date.getFullYear(), date.getMonth(), date.getDate() + 1)];
4626 else
4627 return [false, new Date(date.getFullYear() + 1, 0, 1)];
4628
4629 }}(tokens, at, nrule, has_year[0], has_calc[0]);
4630
4631 if (push_to_month === true)
4632 selectors.month.push(selector);
4633 else
4634 selectors.monthday.push(selector);
4635
4636 at += has_year[0] + has_event[0] + (typeof has_calc[0][1] != 'undefined' && has_calc[0][1] ? 3 : 0);
4637 // }}}
4638 } else if (has_constrained_weekday[0]) {
4639 at = parseMonthRange(tokens, at);
4640 } else if (matchTokens(tokens, at, 'month')) {
4641 return parseMonthRange(tokens, at, true, true);
4642 } else {
4643 // throw 'Unexpected token in monthday range: "' + tokens[at] + '"';
4644 return at;
4645 }
4646
4647 if (!matchTokens(tokens, at, ','))
4648 break;
4649 }
4650
4651 return at;
4652 }
4653 // }}}
4654
4655 /* Main selector traversal function (return state array for date). {{{
4656 * Checks for given date which rule and those which state and comment applies.
4657 *
4658 * :param date: Date object.
4659 * :returns: Array:
4660 * 0. resultstate: State: true for 'open', false for 'closed'.
4661 * 1. changedate: Next change as date object.
4662 * 2. unknown: true if state open is not sure.
4663 * 3. comment: Comment which applies for this time range (from date to changedate).
4664 * 4. match_rule: Rule number starting with 0 (nrule).
4665 */
4666 this.getStatePair = function(date) {
4667 var resultstate = false;
4668 var changedate;
4669 var unknown = false;
4670 var comment;
4671 var match_rule;
4672
4673 var date_matching_rules = [];
4674
4675 for (var nrule = 0; nrule < rules.length; nrule++) {
4676 var matching_date_rule = true;
4677 // console.log(nrule, 'length', rules[nrule].date.length);
4678
4679 // Try each date selector type
4680 for (var ndateselector = 0; ndateselector < rules[nrule].date.length; ndateselector++) {
4681 var dateselectors = rules[nrule].date[ndateselector];
4682 // console.log(nrule, ndateselector);
4683
4684 var has_matching_selector = false;
4685 for (var datesel = 0; datesel < dateselectors.length; datesel++) {
4686 var res = dateselectors[datesel](date);
4687 if (res[0]) {
4688 has_matching_selector = true;
4689
4690 if (typeof res[2] == 'string') { // holiday name
4691 comment = [ res[2] ];
4692 }
4693
4694 }
4695 if (typeof changedate === 'undefined' || (typeof res[1] !== 'undefined' && res[1].getTime() < changedate.getTime()))
4696 changedate = res[1];
4697 }
4698
4699 if (!has_matching_selector) {
4700 matching_date_rule = false;
4701 // We can ignore other date selectors, as the state won't change
4702 // anyway until THIS selector matches (due to conjunction of date
4703 // selectors of different types).
4704 // This is also an optimization, if widest date selector types
4705 // are checked first.
4706 break;
4707 }
4708
4709 }
4710
4711 if (matching_date_rule) {
4712 // The following lines implement date overwriting logic (e.g. for
4713 // "Mo-Fr 10:00-20:00; We 10:00-16:00", We rule overrides Mo-Fr rule partly (We).
4714 //
4715 // 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:
4716 // 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.
4717 if ((rules[nrule].date.length > 0 || nrule > 0 && rules[nrule].meaning && rules[nrule-1].date.length === 0)
4718 && (rules[nrule].meaning || rules[nrule].unknown)
4719 && !rules[nrule].wrapped && !rules[nrule].additional && !rules[nrule].fallback
4720 ) {
4721
4722 // var old_date_matching_rules = date_matching_rules;
4723 date_matching_rules = [];
4724 // for (var nrule = 0; nrule < old_date_matching_rules.length; nrule++) {
4725 // if (!rules[old_date_matching_rules[nrule]].wrapped)
4726 // date_matching_rules.push(nrule);
4727 // }
4728 }
4729 date_matching_rules.push(nrule);
4730 }
4731 }
4732
4733 rule:
4734 for (var nrule = 0; nrule < date_matching_rules.length; nrule++) {
4735 var rule = date_matching_rules[nrule];
4736
4737 // console.log('Processing rule ' + rule + ': with date ' + date
4738 // + ' and ' + rules[rule].time.length + ' time selectors (comment: "' + rules[rule].comment + '").');
4739
4740 // there is no time specified, state applies to the whole day
4741 if (rules[rule].time.length === 0) {
4742 // console.log('there is no time', date);
4743 if (!rules[rule].fallback || (rules[rule].fallback && !(resultstate || unknown))) {
4744 resultstate = rules[rule].meaning;
4745 unknown = rules[rule].unknown;
4746 match_rule = rule;
4747
4748 if (typeof rules[rule].comment != 'undefined')
4749 comment = rules[rule].comment;
4750 else if (typeof comment == 'object') // holiday name
4751 comment = comment[0];
4752
4753 // if (rules[rule].fallback)
4754 // break rule; // fallback rule matched, no need for checking the rest
4755 // WRONG: What if closing rules follow?
4756 }
4757 }
4758
4759 for (var timesel = 0; timesel < rules[rule].time.length; timesel++) {
4760 var res = rules[rule].time[timesel](date);
4761
4762 // console.log('res:', res);
4763 if (res[0]) {
4764 if (!rules[rule].fallback || (rules[rule].fallback && !(resultstate || unknown))) {
4765 resultstate = rules[rule].meaning;
4766 unknown = rules[rule].unknown;
4767 match_rule = rule;
4768
4769 if (typeof rules[rule].comment == 'string') // only use comment if one is specified
4770 comment = rules[rule].comment;
4771 else if (typeof comment == 'object') // holiday name
4772 comment = comment[0];
4773 else if (comment === 'Specified as open end. Closing time was guessed.')
4774 comment = rules[rule].comment;
4775
4776 // open end
4777 if (res[2] === true && (resultstate || unknown)) {
4778 if (typeof comment == 'undefined')
4779 comment = 'Specified as open end. Closing time was guessed.';
4780
4781 resultstate = false;
4782 unknown = true;
4783
4784 /* Hack to make second rule in '07:00+,12:00-16:00; 16:00-24:00 closed "needed because of open end"' obsolete {{{ */
4785 if (typeof rules[rule].time[timesel+1] == 'function') {
4786
4787 var next_res = rules[rule].time[timesel+1](date);
4788 if ( !next_res[0]
4789 // && next_res[2]
4790 && typeof next_res[1] == 'object'
4791 // && getValueForDate(next_res[1], true) != getValueForDate(date, true) // Just to be sure.
4792 && rules[rule].time[timesel](new Date(date.getTime() - 1))[0]
4793 /* To keep the following two apart:
4794 * 'sunrise-14:00,14:00+',
4795 * '07:00+,12:00-16:00',
4796 */
4797 ) {
4798
4799 // console.log("07:00+,12:00-16:00 matched.");
4800
4801 resultstate = false;
4802 unknown = false;
4803 }
4804 }
4805
4806 /* Hack to handle '17:00+,13:00-02:00' {{{ */
4807 /* Not enabled. To complicated, just don‘t use them …
4808 * It gets even crazier …
4809 * Time wrapping over midnight is
4810 * stored in the next internal rule:
4811 * '17:00-00:00 unknown "Specified as open end. Closing time was guessed.", 13:00-00:00 open' // First internal rule.
4812 * + ', ' overwritten part: 00:00-03:00 open + '00:00-02:00 open', // Second internal rule.
4813 */
4814 if ( false
4815 && typeof rules[rule-1] == 'object'
4816 && rules[rule].build_from_token_rule.toString() == rules[rule-1].build_from_token_rule.toString()
4817 && typeof rules[rule] == 'object'
4818 && rules[rule].build_from_token_rule.toString() == rules[rule].build_from_token_rule.toString()
4819 ) {
4820
4821 var last_wrapping_time_selector = rules[rule].time[rules[rule].time.length - 1];
4822 var last_w_res = last_wrapping_time_selector(new Date(date.getTime() - 1));
4823 // console.log(last_w_res);
4824
4825 if ( last_w_res[0]
4826 && typeof last_w_res[2] == 'undefined'
4827 && (typeof last_w_res[2] == 'undefined' || last_w_res[2] === false) // Not match for 'Tu 23:59-40:00+'
4828 && typeof last_w_res[1] == 'object'
4829 && date.getTime() == last_w_res[1].getTime()
4830 ) {
4831
4832 // '05:00-06:00,17:00+,13:00-02:00',
4833
4834 // console.log("17:00+,13:00-02:00 matched.");
4835 // console.log(JSON.stringify(rules, null, ' '));
4836
4837 resultstate = false;
4838 unknown = false;
4839 }
4840 /* }}} */
4841 }
4842 /* }}} */
4843 }
4844
4845 if (rules[rule].fallback) {
4846 if (typeof changedate === 'undefined' || (typeof res[1] !== 'undefined' && res[1] < changedate))
4847 changedate = res[1];
4848
4849 // break rule; // Fallback rule matched, no need for checking the rest.
4850 // WRONG: What if 'off' is used after fallback rule.
4851 }
4852 }
4853 }
4854 if (typeof changedate === 'undefined' || (typeof res[1] !== 'undefined' && res[1] < changedate))
4855 changedate = res[1];
4856 }
4857 }
4858
4859 // console.log('changedate', changedate, resultstate, comment, match_rule);
4860 return [ resultstate, changedate, unknown, comment, match_rule ];
4861 };
4862 // }}}
4863
4864 /* Generate prettified value for selector based on tokens. {{{
4865 *
4866 * :param tokens: List of token objects.
4867 * :param at: Position where to start.
4868 * :param last_at: Position where to stop.
4869 * :param conf: Configuration options.
4870 * :returns: Prettified value.
4871 */
4872 function prettifySelector(tokens, selector_start, selector_end, selector_type, conf) {
4873
4874 var value = '';
4875 var at = selector_start;
4876 while (at <= selector_end) {
4877 // console.log('At: ' + at + ', token: ' + tokens[at]);
4878 if (matchTokens(tokens, at, 'weekday')) {
4879 if (!conf.leave_weekday_sep_one_day_betw
4880 && at - selector_start > 1 && (matchTokens(tokens, at-1, ',') || matchTokens(tokens, at-1, '-'))
4881 && matchTokens(tokens, at-2, 'weekday')
4882 && tokens[at][0] == (tokens[at-2][0] + 1) % 7) {
4883 value = value.substring(0, value.length - 1) + conf.sep_one_day_between;
4884 }
4885 value += weekdays[tokens[at][0]];
4886 } else if (at - selector_start > 0 // e.g. '09:0' -> '09:00'
4887 && selector_type == 'time'
4888 && matchTokens(tokens, at-1, 'timesep')
4889 && matchTokens(tokens, at, 'number')) {
4890 value += (tokens[at][0] < 10 ? '0' : '') + tokens[at][0].toString();
4891 } else if (selector_type == 'time' // e.g. '9:00' -> ' 09:00'
4892 && conf.zero_pad_hour
4893 && at != tokens.length
4894 && matchTokens(tokens, at, 'number')
4895 && matchTokens(tokens, at+1, 'timesep')) {
4896 value += (
4897 tokens[at][0] < 10 ?
4898 (tokens[at][0] === 0 && conf.one_zero_if_hour_zero ?
4899 '' : '0') :
4900 '') + tokens[at][0].toString();
4901 } else if (selector_type == 'time' // e.g. '9-18' -> '09:00-18:00'
4902 && at + 2 <= selector_end
4903 && matchTokens(tokens, at, 'number')
4904 && matchTokens(tokens, at+1, '-')
4905 && matchTokens(tokens, at+2, 'number')) {
4906 value += (tokens[at][0] < 10 ?
4907 (tokens[at][0] === 0 && conf.one_zero_if_hour_zero ? '' : '0')
4908 : '') + tokens[at][0].toString();
4909 value += ':00-'
4910 + (tokens[at+2][0] < 10 ? '0' : '') + tokens[at+2][0].toString()
4911 + ':00';
4912 at += 2;
4913 } else if (matchTokens(tokens, at, 'comment')) {
4914 value += '"' + tokens[at][0].toString() + '"';
4915 } else if (matchTokens(tokens, at, 'closed')) {
4916 value += (conf.leave_off_closed ? tokens[at][0] : conf.keyword_for_off_closed);
4917 } else if (at - selector_start > 0 && matchTokens(tokens, at, 'number')
4918 && (matchTokens(tokens, at-1, 'month') && selector_type == 'month'
4919 || matchTokens(tokens, at-1, 'week') && selector_type == 'week'
4920 )) {
4921 value += ' '
4922 + (conf.zero_pad_month_and_week_numbers && tokens[at][0] < 10 ? '0' : '')
4923 + tokens[at][0];
4924 } else if (at - selector_start > 0 && matchTokens(tokens, at, 'month')
4925 && matchTokens(tokens, at-1, 'year')) {
4926 value += ' ' + months[[tokens[at][0]]];
4927 } else if (at - selector_start > 0 && matchTokens(tokens, at, 'event')
4928 && matchTokens(tokens, at-1, 'year')) {
4929 value += ' ' + tokens[at][0];
4930 } else if (matchTokens(tokens, at, 'month')) {
4931 value += months[[tokens[at][0]]];
4932 if (at + 1 <= selector_end && matchTokens(tokens, at+1, 'weekday'))
4933 value += ' ';
4934 } else if (at + 2 <= selector_end
4935 && (matchTokens(tokens, at, '-') || matchTokens(tokens, at, '+'))
4936 && matchTokens(tokens, at+1, 'number', 'calcday')) {
4937 value += ' ' + tokens[at][0] + tokens[at+1][0] + ' day' + (Math.abs(tokens[at+1][0]) == 1 ? '' : 's');
4938 at += 2;
4939 } else if (at == selector_end
4940 && selector_type == 'weekday'
4941 && tokens[at][0] == ':') {
4942 // Do nothing.
4943 } else {
4944 value += tokens[at][0].toString();
4945 }
4946 at++;
4947 }
4948 return value;
4949 }
4950 // }}}
4951
4952 //======================================================================
4953 // Public interface {{{
4954 // All functions below are considered public.
4955 //======================================================================
4956
4957 // Iterator interface {{{
4958 this.getIterator = function(date) {
4959 return new function(oh) {
4960 if (typeof date === 'undefined')
4961 date = new Date();
4962
4963 var prevstate = [ undefined, date, undefined, undefined, undefined ];
4964 var state = oh.getStatePair(date);
4965
4966 /* getDate {{{ */
4967 this.getDate = function() {
4968 return prevstate[1];
4969 };
4970 /* }}} */
4971
4972 /* setDate {{{ */
4973 this.setDate = function(date) {
4974 if (typeof date != 'object')
4975 throw 'Date parameter needed.';
4976
4977 prevstate = [ undefined, date, undefined, undefined, undefined ];
4978 state = oh.getStatePair(date);
4979 };
4980 /* }}} */
4981
4982 /* getState: Check whether facility is `open' {{{ */
4983 this.getState = function() {
4984 return state[0];
4985 };
4986 /* }}} */
4987
4988 /* getUnknown: Checks whether the opening state is conditional or unknown {{{ */
4989 this.getUnknown = function() {
4990 return state[2];
4991 };
4992 /* }}} */
4993
4994 /* getStateString: Get state string. Either 'open', 'unknown' or 'closed' {{{ */
4995 this.getStateString = function(past) {
4996 return (state[0] ? 'open' : (state[2] ? 'unknown' : (past ? 'closed' : 'close')));
4997 };
4998 /* }}} */
4999
5000 /* getComment: Get the comment, undefined in none {{{ */
5001 this.getComment = function() {
5002 return state[3];
5003 };
5004 /* }}} */
5005
5006 /* getMatchingRule: Get the rule which matched thus deterrents the current state {{{ */
5007 this.getMatchingRule = function() {
5008 if (typeof state[4] == 'undefined')
5009 return undefined;
5010
5011 return rules[state[4]].build_from_token_rule[2];
5012 };
5013 /* }}} */
5014
5015 /* advance: Advances to the next position {{{ */
5016 this.advance = function(datelimit) {
5017 if (typeof datelimit === 'undefined')
5018 datelimit = new Date(prevstate[1].getTime() + msec_in_day * 366 * 5);
5019 else if (datelimit.getTime() <= prevstate[1].getTime())
5020 return false; // The limit for advance needs to be after the current time.
5021
5022 do {
5023 // open range, we won't be able to advance
5024 if (typeof state[1] === 'undefined')
5025 return false;
5026
5027 // console.log('\n' + 'previous check time:', prevstate[1]
5028 // + ', current check time:',
5029 // // (state[1].getHours() < 10 ? '0' : '') + state[1].getHours() +
5030 // // ':'+(state[1].getMinutes() < 10 ? '0' : '')+ state[1].getMinutes(), state[1].getDate(),
5031 // state[1],
5032 // (state[0] ? 'open' : (state[2] ? 'unknown' : 'closed')) + ', comment:', state[3]);
5033
5034 // We're going backwards or staying at place.
5035 // This always indicates coding error in a selector code.
5036 if (state[1].getTime() <= prevstate[1].getTime())
5037 throw 'Fatal: infinite loop in nextChange';
5038
5039 // don't advance beyond limits (same as open range)
5040 if (state[1].getTime() >= datelimit.getTime())
5041 return false;
5042
5043 // do advance
5044 prevstate = state;
5045 state = oh.getStatePair(prevstate[1]);
5046 } while (state[0] === prevstate[0] && state[2] === prevstate[2] && state[3] === prevstate[3]);
5047 return true;
5048 };
5049 /* }}} */
5050 }(this);
5051 };
5052 // }}}
5053
5054 // Simple API {{{
5055
5056 this.getState = function(date) {
5057 var it = this.getIterator(date);
5058 return it.getState();
5059 };
5060
5061 this.getUnknown = function(date) {
5062 var it = this.getIterator(date);
5063 return it.getUnknown();
5064 };
5065
5066 this.getStateString = function(date, past) {
5067 var it = this.getIterator(date);
5068 return it.getStateString(past);
5069 };
5070
5071 this.getComment = function(date) {
5072 var it = this.getIterator(date);
5073 return it.getComment();
5074 };
5075
5076 this.getMatchingRule = function(date) {
5077 var it = this.getIterator(date);
5078 return it.getMatchingRule();
5079 };
5080
5081 /* Not available for iterator API {{{ */
5082 /* getWarnings: Get warnings, empty list if none {{{ */
5083 this.getWarnings = function() {
5084 var it = this.getIterator();
5085 return getWarnings(it);
5086 };
5087 /* }}} */
5088
5089 /* prettifyValue: Get a nicely formated value {{{ */
5090 this.prettifyValue = function(argument_hash) {
5091 this.getWarnings();
5092 /* getWarnings has to be run before prettifyValue because some
5093 * decisions if a certain aspect makes sense to prettify or not
5094 * are based on the warnings.
5095 * Basically, both functions depend on each other in some way :(
5096 * See done_with_selector_reordering.
5097 */
5098 return prettifyValue(argument_hash);
5099 };
5100 /* }}} */
5101
5102 /* getNextChange: Get time of next status change {{{ */
5103 this.getNextChange = function(date, maxdate) {
5104 var it = this.getIterator(date);
5105 if (!it.advance(maxdate))
5106 return undefined;
5107 return it.getDate();
5108 };
5109 /* }}} */
5110
5111 /* isWeekStable: Checks whether open intervals are same for every week. {{{ */
5112 this.isWeekStable = function() {
5113 return week_stable;
5114 };
5115 /* }}} */
5116 /* }}} */
5117 /* }}} */
5118
5119 // High-level API {{{
5120
5121 /* getOpenIntervals: Get array of open intervals between two dates {{{ */
5122 this.getOpenIntervals = function(from, to) {
5123 var res = [];
5124
5125 var it = this.getIterator(from);
5126
5127 if (it.getState() || it.getUnknown())
5128 res.push([from, undefined, it.getUnknown(), it.getComment()]);
5129
5130 while (it.advance(to)) {
5131 if (it.getState() || it.getUnknown()) {
5132 if (res.length !== 0 && typeof res[res.length - 1][1] == 'undefined') {
5133 // last state was also open or unknown
5134 res[res.length - 1][1] = it.getDate();
5135 }
5136 res.push([it.getDate(), undefined, it.getUnknown(), it.getComment()]);
5137 } else {
5138 if (res.length !== 0 && typeof res[res.length - 1][1] == 'undefined') {
5139 // only use the first time as closing/change time and ignore closing times which might follow
5140 res[res.length - 1][1] = it.getDate();
5141 }
5142 }
5143 }
5144
5145 if (res.length > 0 && typeof res[res.length - 1][1] === 'undefined')
5146 res[res.length - 1][1] = to;
5147
5148 return res;
5149 };
5150 /* }}} */
5151
5152 /* getOpenDuration: Get total number of milliseconds a facility is open,unknown within a given date range {{{ */
5153 this.getOpenDuration = function(from, to) {
5154 // console.log('-----------');
5155
5156 var open = 0;
5157 var unknown = 0;
5158
5159 var it = this.getIterator(from);
5160 var prevdate = (it.getState() || it.getUnknown()) ? from : undefined;
5161 var prevstate = it.getState();
5162 var prevunknown = it.getUnknown();
5163
5164 while (it.advance(to)) {
5165 if (it.getState() || it.getUnknown()) {
5166
5167 if (typeof prevdate !== 'undefined') {
5168 // last state was also open or unknown
5169 if (prevunknown) //
5170 unknown += it.getDate().getTime() - prevdate.getTime();
5171 else if (prevstate)
5172 open += it.getDate().getTime() - prevdate.getTime();
5173 }
5174
5175 prevdate = it.getDate();
5176 prevstate = it.getState();
5177 prevunknown = it.getUnknown();
5178 // console.log('if', prevdate, open / (1000 * 60 * 60), unknown / (1000 * 60 * 60));
5179 } else {
5180 // console.log('else', prevdate);
5181 if (typeof prevdate !== 'undefined') {
5182 if (prevunknown)
5183 unknown += it.getDate().getTime() - prevdate.getTime();
5184 else
5185 open += it.getDate().getTime() - prevdate.getTime();
5186 prevdate = undefined;
5187 }
5188 }
5189 }
5190
5191 if (typeof prevdate !== 'undefined') {
5192 if (prevunknown)
5193 unknown += to.getTime() - prevdate.getTime();
5194 else
5195 open += to.getTime() - prevdate.getTime();
5196 }
5197
5198 return [ open, unknown ];
5199 };
5200 /* }}} */
5201 /* }}} */
5202 /* }}} */
5203 };
5204}));
5205// vim: set ts=4 sw=4 tw=0 noet foldmarker={{{,}}} foldlevel=0 foldmethod=marker :
Note: See TracBrowser for help on using the repository browser.