Ticket #9157: 9157c.patch

File 9157c.patch, 104.1 KB (added by simon04, 11 years ago)
  • CONTRIBUTION

    diff --git a/CONTRIBUTION b/CONTRIBUTION
    index 1f32f02..3373f23 100644
    a b with Apache license version 2.0.  
    4242
    4343The signpost code (http://code.google.com/p/oauth-signpost/)
    4444is from Matthias Käppler and licensed with the Apache License 2.0.
     45
     46The opening hour validation uses code from opening_hour.js
     47(https://github.com/ypid/opening_hours.js) which is licensed
     48with the New (2-clause) BSD license.
     49 No newline at end of file
  • new file data/opening_hours.js

    diff --git a/data/opening_hours.js b/data/opening_hours.js
    new file mode 100644
    index 0000000..353a340
    - +  
     1(function (root, factory) {
     2        if (typeof exports === 'object') {
     3                // For nodejs
     4                var SunCalc = require('suncalc');
     5                module.exports = factory(SunCalc);
     6        } else {
     7                // For browsers
     8                root.opening_hours = factory(root.SunCalc);
     9        }
     10}(this, function (SunCalc) {
     11        var SunCalc;
     12        return function(value, nominatiomJSON) {
     13                //======================================================================
     14                // Constants
     15                //======================================================================
     16                var holidays = {
     17                        'de': {
     18                                'PH': { // http://de.wikipedia.org/wiki/Feiertage_in_Deutschland
     19                                        'Neujahrstag'               : [  1,  1 ], // month 1, day 1, whole Germany
     20                                        'Heilige Drei Könige'       : [  1,  6, [ 'Baden-Württemberg', 'Bayern', 'Sachsen-Anhalt'] ], // only in the specified states
     21                                        'Tag der Arbeit'            : [  5,  1 ], // whole Germany
     22                                        'Karfreitag'                : [ 'easter', -2 ], // two days before easter
     23                                        'Ostersonntag'              : [ 'easter',  0, [ 'Brandenburg'] ],
     24                                        'Ostermontag'               : [ 'easter',  1 ],
     25                                        'Christi Himmelfahrt'       : [ 'easter', 39 ],
     26                                        'Pfingstsonntag'            : [ 'easter', 49, [ 'Brandenburg'] ],
     27                                        'Pfingstmontag'             : [ 'easter', 50 ],
     28                                        'Fronleichnam'              : [ 'easter', 60, [ 'Baden-Württemberg', 'Bayern', 'Hessen', 'Nordrhein-Westfalen', 'Rheinland-Pfalz', 'Saarland' ] ],
     29                                        'Mariä Himmelfahrt'         : [  8, 15, [ 'Saarland'] ],
     30                                        'Tag der Deutschen Einheit' : [ 10,  3 ],
     31                                        'Reformationstag'           : [ 10, 31, [ 'Brandenburg', 'Mecklenburg-Vorpommern', 'Sachsen', 'Sachsen-Anhalt', 'Thüringen'] ],
     32                                        'Allerheiligen'             : [ 11,  1, [ 'Baden-Württemberg', 'Bayern', 'Nordrhein-Westfalen', 'Rheinland-Pfalz', 'Saarland' ] ],
     33                                        '1. Weihnachtstag'          : [ 12, 25 ],
     34                                        '2. Weihnachtstag'          : [ 12, 26 ],
     35                                        // 'Silvester'                 : [ 12, 31 ], // for testing
     36                                },
     37                                'Baden-Württemberg': { // does only apply in Baden-Württemberg
     38                                        // This more specific rule set overwrites the country wide one (they are just ignored).
     39                                        // You may use this instead of the country wide with some
     40                                        // additional holidays for some states, if one state
     41                                        // totally disagrees about how to do public holidays …
     42                                        // 'PH': {
     43                                        //      '2. Weihnachtstag'          : [ 12, 26 ],
     44                                        // },
     45
     46                                        // school holiday normally variate between states
     47                                        'SH': [ // generated by convert_ical_to_json
     48                                                        // You may can adjust this script to use other resources (for other countries) too.
     49                                                {
     50                                                        name: 'Osterferien',
     51                                                        2005: [  3, 24, /* to */  3, 24,   3, 29, /* to */  4,  2 ],
     52                                                        2006: [  4, 18, /* to */  4, 22 ],
     53                                                        2007: [  4,  2, /* to */  4, 14 ],
     54                                                        2008: [  3, 17, /* to */  3, 28 ],
     55                                                        2009: [  4,  9, /* to */  4,  9,   4, 14, /* to */  4, 17 ],
     56                                                        2010: [  4,  1, /* to */  4,  1,   4,  6, /* to */  4, 10 ],
     57                                                        2011: [  4, 21, /* to */  4, 21,   4, 26, /* to */  4, 30 ],
     58                                                        2012: [  4,  2, /* to */  4, 13 ],
     59                                                        2013: [  3, 25, /* to */  4,  5 ],
     60                                                        2014: [  4, 14, /* to */  4, 25 ],
     61                                                        2015: [  3, 30, /* to */  4, 10 ],
     62                                                        2016: [  3, 29, /* to */  4,  2 ],
     63                                                        2017: [  4, 10, /* to */  4, 21 ],
     64                                                },
     65                                                {
     66                                                        name: 'Pfingstferien',
     67                                                        2005: [  5, 17, /* to */  5, 28 ],
     68                                                        2006: [  5, 29, /* to */  6, 10 ],
     69                                                        2007: [  5, 29, /* to */  6,  9 ],
     70                                                        2008: [  5, 13, /* to */  5, 23 ],
     71                                                        2009: [  5, 25, /* to */  6,  6 ],
     72                                                        2010: [  5, 25, /* to */  6,  5 ],
     73                                                        2011: [  6, 14, /* to */  6, 25 ],
     74                                                        2012: [  5, 29, /* to */  6,  9 ],
     75                                                        2013: [  5, 21, /* to */  6,  1 ],
     76                                                        2014: [  6, 10, /* to */  6, 21 ],
     77                                                        2015: [  5, 26, /* to */  6,  6 ],
     78                                                        2016: [  5, 17, /* to */  5, 28 ],
     79                                                        2017: [  6,  6, /* to */  6, 16 ],
     80                                                },
     81                                                {
     82                                                        name: 'Sommerferien',
     83                                                        2005: [  7, 28, /* to */  9, 10 ],
     84                                                        2006: [  8,  3, /* to */  9, 16 ],
     85                                                        2007: [  7, 26, /* to */  9,  8 ],
     86                                                        2008: [  7, 24, /* to */  9,  6 ],
     87                                                        2009: [  7, 30, /* to */  9, 12 ],
     88                                                        2010: [  7, 29, /* to */  9, 11 ],
     89                                                        2011: [  7, 28, /* to */  9, 10 ],
     90                                                        2012: [  7, 26, /* to */  9,  8 ],
     91                                                        2013: [  7, 25, /* to */  9,  7 ],
     92                                                        2014: [  7, 31, /* to */  9, 13 ],
     93                                                        2015: [  7, 30, /* to */  9, 12 ],
     94                                                        2016: [  7, 28, /* to */  9, 10 ],
     95                                                        2017: [  7, 27, /* to */  9,  9 ],
     96                                                },
     97                                                {
     98                                                        name: 'Herbstferien',
     99                                                        2005: [ 11,  2, /* to */ 11,  4 ],
     100                                                        2006: [ 10, 30, /* to */ 11,  3 ],
     101                                                        2007: [ 10, 29, /* to */ 11,  3 ],
     102                                                        2008: [ 10, 27, /* to */ 10, 31 ],
     103                                                        2009: [ 10, 26, /* to */ 10, 31 ],
     104                                                        2010: [ 11,  2, /* to */ 11,  6 ],
     105                                                        2011: [ 10, 31, /* to */ 10, 31,  11,  2, /* to */ 11,  4 ],
     106                                                        2012: [ 10, 29, /* to */ 11,  2 ],
     107                                                        2013: [ 10, 28, /* to */ 10, 30 ],
     108                                                        2014: [ 10, 27, /* to */ 10, 30 ],
     109                                                        2015: [ 11,  2, /* to */ 11,  6 ],
     110                                                        2016: [ 11,  2, /* to */ 11,  4 ],
     111                                                },
     112                                                {
     113                                                        name: 'Weihnachtsferien',
     114                                                        2005: [ 12, 22, /* to */  1,  5 ],
     115                                                        2006: [ 12, 27, /* to */  1,  5 ],
     116                                                        2007: [ 12, 24, /* to */  1,  5 ],
     117                                                        2008: [ 12, 22, /* to */  1, 10 ],
     118                                                        2009: [ 12, 23, /* to */  1,  9 ],
     119                                                        2010: [ 12, 23, /* to */  1,  8 ],
     120                                                        2011: [ 12, 23, /* to */  1,  5 ],
     121                                                        2012: [ 12, 24, /* to */  1,  5 ],
     122                                                        2013: [ 12, 23, /* to */  1,  4 ],
     123                                                        2014: [ 12, 22, /* to */  1,  5 ],
     124                                                        2015: [ 12, 23, /* to */  1,  9 ],
     125                                                        2016: [ 12, 23, /* to */  1,  7 ],
     126                                                },
     127                                        ],
     128                                },
     129                                'Bremen': {
     130                                        'SH': [
     131                                                {
     132                                                        name: 'Winterferien',
     133                                                        2012: [  1, 30, /* to */  1, 31 ],
     134                                                        2013: [  1, 31, /* to */  2,  1 ],
     135                                                        2014: [  1, 30, /* to */  1, 31 ],
     136                                                        2015: [  2,  2, /* to */  2,  3 ],
     137                                                },
     138                                                {
     139                                                        name: 'Osterferien',
     140                                                        2012: [  3, 26, /* to */  4, 11,   4, 30, /* to */  4, 30 ],
     141                                                        2013: [  3, 16, /* to */  4,  2 ],
     142                                                        2014: [  4,  3, /* to */  4, 22,   5,  2, /* to */  5,  2 ],
     143                                                        2015: [  3, 25, /* to */  4, 10 ],
     144                                                },
     145                                                {
     146                                                        name: 'Pfingstferien',
     147                                                        2012: [  5, 18, /* to */  5, 18,   5, 29, /* to */  5, 29 ],
     148                                                        2013: [  5, 10, /* to */  5, 10,   5, 21, /* to */  5, 21 ],
     149                                                        2014: [  5, 30, /* to */  5, 30,   6, 10, /* to */  6, 10 ],
     150                                                        2015: [  5, 15, /* to */  5, 15,   5, 26, /* to */  5, 26 ],
     151                                                },
     152                                                {
     153                                                        name: 'Sommerferien',
     154                                                        2012: [  7, 23, /* to */  8, 31 ],
     155                                                        2013: [  6, 27, /* to */  8,  7 ],
     156                                                        2014: [  7, 31, /* to */  9, 10 ],
     157                                                        2015: [  7, 23, /* to */  9,  2 ],
     158                                                },
     159                                                {
     160                                                        name: 'Herbstferien',
     161                                                        2012: [ 10, 22, /* to */ 11,  3 ],
     162                                                        2013: [ 10,  4, /* to */ 10, 18 ],
     163                                                        2014: [ 10, 27, /* to */ 11,  8 ],
     164                                                        2015: [ 10, 19, /* to */ 10, 31 ],
     165                                                },
     166                                                {
     167                                                        name: 'Weihnachtsferien',
     168                                                        2012: [ 12, 24, /* to */  1,  5 ],
     169                                                        2013: [ 12, 23, /* to */  1,  4],
     170                                                        2014: [ 12, 22, /* to */  1,  5 ],
     171                                                        2015: [ 12, 23, /* to */ 12, 31 ],
     172                                                },
     173                                        ],
     174                                },
     175                        },
     176                        'at': {
     177                                'PH': { // http://de.wikipedia.org/wiki/Feiertage_in_%C3%96sterreich
     178                                        'Neujahrstag'               : [  1,  1 ],
     179                                        'Heilige Drei Könige'       : [  1,  6 ],
     180                                        // 'Josef'                     : [  3, 19, [ 'Kärnten', 'Steiermark', 'Tirol', 'Vorarlberg' ] ],
     181                                        // 'Karfreitag'                : [ 'easter', -2 ],
     182                                        'Ostermontag'               : [ 'easter',  1 ],
     183                                        'Staatsfeiertag'            : [  5,  1 ],
     184                                        // 'Florian'                   : [  5,  4, [ 'Oberösterreich' ] ],
     185                                        'Christi Himmelfahrt'       : [ 'easter', 39 ],
     186                                        'Pfingstmontag'             : [ 'easter', 50 ],
     187                                        'Fronleichnam'              : [ 'easter', 60 ],
     188                                        'Mariä Himmelfahrt'         : [  8, 15 ],
     189                                        // 'Rupert'                    : [  9, 24, [ 'Salzburg' ] ],
     190                                        // 'Tag der Volksabstimmung'   : [ 10, 10, [ 'Kärnten' ] ],
     191                                        'Nationalfeiertag'          : [ 10, 26 ],
     192                                        'Allerheiligen'             : [ 11,  1 ],
     193                                        // 'Martin'                    : [ 11, 11, [ 'Burgenland' ] ],
     194                                        // 'Leopold'                   : [ 11, 15, [ 'Niederösterreich', 'Wien' ] ],
     195                                        'Mariä Empfängnis'          : [ 12,  8 ],
     196                                        // 'Heiliger Abend'            : [ 12, 24 ],
     197                                        'Christtag'                 : [ 12, 25 ],
     198                                        'Stefanitag'                : [ 12, 26 ],
     199                                        // 'Silvester'                 : [ 12, 31 ],
     200                                },
     201                        },
     202                };
     203
     204                //----------------------------------------------------------------------------
     205                //  error correction
     206                //  Taken form http://www.netzwolf.info/j/osm/time_domain.js
     207                //  Credits go to Netzwolf
     208                //
     209                //  Key to word_error_correction is the token name except wrong_words
     210                //----------------------------------------------------------------------------
     211                var word_error_correction = {
     212                        wrong_words: {
     213                                'Assuming "<ok>" for "<ko>"': {
     214                                        summer: 'May-Oct',
     215                                        winter: 'Nov-Apr',
     216                                }, 'Bitte benutze die englische Schreibweise "<ok>" für "<ko>".': {
     217                                        sommer: 'summer',
     218                                }, 'Assuming "<ok>" for "<ko>". Please avoid using "workday": http://wiki.openstreetmap.org/wiki/Talk:Key:opening_hours#need_syntax_for_holidays_and_workingdays': {
     219                                        //      // Used around 260 times but the problem is, that work day might be different in other countries.
     220                                        wd:       'Mo-Fr',
     221                                        weekday:  'Mo-Fr',
     222                                        weekdays: 'Mo-Fr',
     223                                }, 'Please ommit "<ko>".': {
     224                                        h: '',
     225                                }, 'Please ommit "<ko>". You might want to express open end which can be specified as "12:00+" for example': {
     226                                        from: '',
     227                                }, 'Please use notation "<ok>" for "<ko>".': {
     228                                        '–':  '-',
     229                                        to:   '-',
     230                                        till: '-',
     231                                        and:  ',',
     232                                        '&':  ',',
     233                                        daily:    'Mo-Su',
     234                                        always:   '24/7',
     235                                        midnight: '00:00',
     236                                }, 'Please use time format in 24 hours notation ("<ko>").': {
     237                                        pm: '',
     238                                        am: '',
     239                                }, 'Bitte verzichte auf "<ko>".': {
     240                                        uhr: '',
     241                                }, 'Bitte verzichte auf "<ko>". Sie möchten eventuell eine Öffnungszeit ohne vorgegebenes Ende angeben. Beispiel: "12:00+"': {
     242                                        ab:  '',
     243                                        von: '',
     244                                }, 'Bitte benutze die Schreibweise "<ok>" für "<ko>".': {
     245                                        bis: '-',
     246                                        und: ',',
     247                                }, 'Bitte benutze die englische Abkürzung "<ok>" für "<ko>".': {
     248                                        feiertag:   'PH',
     249                                        feiertage:  'PH',
     250                                        feiertagen: 'PH'
     251                                }, 'S\'il vous plaît utiliser "<ok>" pour "<ko>".': {
     252                                        'fermé': 'off',
     253                                        'et':    ',',
     254                                        'à':     '-',
     255                                }, 'Neem de engelse afkorting "<ok>" voor "<ko>" alstublieft.': {
     256                                        feestdag:   'PH',
     257                                        feestdagen: 'PH',
     258                                }
     259                        },
     260
     261                        month: {
     262                                'default': {
     263                                        jan:  0,
     264                                        feb:  1,
     265                                        mar:  2,
     266                                        apr:  3,
     267                                        may:  4,
     268                                        jun:  5,
     269                                        jul:  6,
     270                                        aug:  7,
     271                                        sep:  8,
     272                                        oct:  9,
     273                                        nov: 10,
     274                                        dec: 11,
     275                                }, 'Please use the englisch abbreviation "<ok>" for "<ko>".': {
     276                                        january:    0,
     277                                        february:   1,
     278                                        march:      2,
     279                                        april:      3,
     280                                        // may:     4,
     281                                        june:       5,
     282                                        july:       6,
     283                                        august:     7,
     284                                        september:  8,
     285                                        october:    9,
     286                                        november:  10,
     287                                        december:  11,
     288                                }, 'Bitte benutze die englische Abkürzung "<ok>" für "<ko>".': {
     289                                        januar:    0,
     290                                        februar:   1,
     291                                        märz:      2,
     292                                        maerz:     2,
     293                                        mai:       4,
     294                                        juni:      5,
     295                                        juli:      6,
     296                                        okt:       9,
     297                                        oktober:   9,
     298                                        dez:      11,
     299                                        dezember: 11,
     300                                }, 'S\'il vous plaît utiliser l\'abréviation "<ok>" pour "<ko>".': {
     301                                        janvier:    0,
     302                                        février:    1,
     303                                        fév:        1,
     304                                        mars:       2,
     305                                        avril:      3,
     306                                        avr:        3,
     307                                        mai:        4,
     308                                        juin:       5,
     309                                        juillet:    6,
     310                                        août:       7,
     311                                        aoû:        7,
     312                                        septembre:  8,
     313                                        octobre:    9,
     314                                        novembre:  10,
     315                                        décembre:  11,
     316                                }, 'Neem de engelse afkorting "<ok>" voor "<ko>" alstublieft.': {
     317                                        januari:  0,
     318                                        februari: 1,
     319                                        maart:    2,
     320                                        mei:      4,
     321                                        augustus: 7,
     322                                }
     323                        },
     324
     325                        weekday: {
     326                                'default': {
     327                                        su: 0,
     328                                        mo: 1,
     329                                        tu: 2,
     330                                        we: 3,
     331                                        th: 4,
     332                                        fr: 5,
     333                                        sa: 6,
     334                                }, 'Please use the abbreviation "<ok>" for "<ko>".': {
     335                                        sun:        0,
     336                                        sunday:     0,
     337                                        sundays:    0,
     338                                        mon:        1,
     339                                        monday:     1,
     340                                        mondays:    1,
     341                                        tue:        2,
     342                                        tuesday:    2,
     343                                        tuesdays:   2,
     344                                        wed:        3,
     345                                        wednesday:  3,
     346                                        wednesdays: 3,
     347                                        thu:        4,
     348                                        thursday:   4,
     349                                        thursdays:  4,
     350                                        fri:        5,
     351                                        friday:     5,
     352                                        fridays:    5,
     353                                        sat:        6,
     354                                        saturday:   6,
     355                                        saturdays:  6,
     356                                }, 'Bitte benutze die englische Abkürzung "<ok>" für "<ko>".': {
     357                                        so:         0,
     358                                        son:        0,
     359                                        sonntag:    0,
     360                                        montag:     1,
     361                                        di:         2,
     362                                        die:        2,
     363                                        dienstag:   2,
     364                                        mi:         3,
     365                                        mit:        3,
     366                                        mittwoch:   3,
     367                                        'do':       4,
     368                                        don:        4,
     369                                        donnerstag: 4,
     370                                        fre:        5,
     371                                        freitag:    5,
     372                                        sam:        6,
     373                                        samstag:    6,
     374                                }, 'S\'il vous plaît utiliser l\'abréviation "<ok>" pour "<ko>".': {
     375                                        dim:      0,
     376                                        dimanche: 0,
     377                                        lu:       1,
     378                                        lun:      1,
     379                                        lundi:    1,
     380                                        mardi:    2,
     381                                        mer:      3,
     382                                        mercredi: 3,
     383                                        je:       4,
     384                                        jeu:      4,
     385                                        jeudi:    4,
     386                                        ve:       5,
     387                                        ven:      5,
     388                                        vendredi: 5,
     389                                        samedi:   6,
     390                                }, 'Neem de engelse afkorting "<ok>" voor "<ko>" alstublieft.': {
     391                                        zo:        0,
     392                                        zon:       0,
     393                                        zontag:    0,
     394                                        maandag:   1,
     395                                        din:       2,
     396                                        dinsdag:   2,
     397                                        wo:        3,
     398                                        woe:       3,
     399                                        woensdag:  3,
     400                                        donderdag: 4,
     401                                        vr:        5,
     402                                        vri:       5,
     403                                        vrijdag:   5,
     404                                        za:        6,
     405                                        zat:       6,
     406                                        zaterdag:  6,
     407                                }
     408                        },
     409
     410                        timevar: { // Special time variables which actual value depends on the date and the position of the facility.
     411                                'default': {
     412                                        sunrise: 'sunrise',
     413                                        sunset:  'sunset',
     414                                        dawn:    'dawn',
     415                                        dusk:    'dusk',
     416                                }, 'Please use notation "<ok>" for "<ko>".': {
     417                                        sundown: 'sunset',
     418                                }, 'Bitte benutze die Schreibweise "<ok>" für "<ko>".': {
     419                                        'morgendämmerung': 'dawn',
     420                                        'abenddämmerung':  'dusk',
     421                                        sonnenaufgang: 'sunrise',
     422                                        sonnenuntergang: ',',
     423                                },
     424                        },
     425
     426                        'event': { // variable events
     427                                'default': {
     428                                        easter: 'easter',
     429                                }, 'Bitte benutze die Schreibweise "<ok>" für "<ko>".': {
     430                                        ostern: 'easter',
     431                                },
     432                        },
     433                };
     434                var word_value_replacement = { // if the correct values can not be calculated
     435                        dawn    : 60 * 5 + 30,
     436                        sunrise : 60 * 6,
     437                        sunset  : 60 * 18,
     438                        dusk    : 60 * 18 + 30,
     439                };
     440                var months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
     441                var default_prettify_conf = {
     442                        'leading_zero_hour': true,       // enforce leading zero
     443                        'one_zero_if_hour_zero': false,  // only one zero "0" if hour is zero "0"
     444                        'leave_off_closed': true,        // leave keywords of and closed as is
     445                        'keyword_for_off_closed': 'off', // use given keyword instead of "off" or "closed"
     446                        'block_sep_string': ' ',         // separate blocks by string
     447                        'print_semicolon': true,         // print token which separates normal blocks
     448                };
     449
     450                var minutes_in_day = 60 * 24;
     451                var msec_in_day    = 1000 * 60 * minutes_in_day;
     452                var msec_in_week   = msec_in_day * 7;
     453
     454                //======================================================================
     455                // Constructor - entry to parsing code
     456                //======================================================================
     457                // Terminology:
     458                //
     459                // Mo-Fr 10:00-11:00; Th 10:00-12:00
     460                // \_____block_____/  \____block___/
     461                //
     462                // The README refers to blocks as rules, which is more intuitive but less clear.
     463                // Because of that only the README uses the term rule in that context.
     464                // In all internal parts of this project, the term block is used.
     465                //
     466                // Mo-Fr Jan 10:00-11:00
     467                // \__/  \_/ \_________/
     468                // selectors (left to right: weekday, month, time)
     469                //
     470                // Logic:
     471                // - Tokenize
     472                // Foreach block:
     473                //   - Run toplevel (block) parser
     474                //     - Which calls subparser for specific selector types
     475                //       - Which produce selector functions
     476
     477
     478                // Evaluate additional information which can be given. They are
     479                // required to reasonably calculate 'sunrise' and so on and to use the
     480                // correct holidays.
     481                var location_cc, location_state, lat, lon;
     482                if (typeof nominatiomJSON != 'undefined') {
     483                        if (typeof nominatiomJSON.address != 'undefined' &&
     484                                        typeof nominatiomJSON.address.state != 'undefined') { // country_code will be tested later …
     485                                                location_cc    = nominatiomJSON.address.country_code;
     486                                                location_state = nominatiomJSON.address.state;
     487                        }
     488
     489                        if (typeof nominatiomJSON.lon != 'undefined') { // lat will be tested later …
     490                                lat = nominatiomJSON.lat;
     491                                lon = nominatiomJSON.lon;
     492                        }
     493                }
     494
     495                if (value.match(/^(\s*;?\s*)+$/)) throw 'Value contains nothing meaningful which can be parsed';
     496
     497                var parsing_warnings = [];
     498                var has_token = {};
     499                var tokens = tokenize(value);
     500                // console.log(JSON.stringify(tokens, null, '\t'));
     501                var prettified_value = '';
     502                var week_stable = true;
     503
     504                var blocks = [];
     505
     506                for (var nblock = 0; nblock < tokens.length; nblock++) {
     507                        if (tokens[nblock][0].length == 0) continue;
     508                        // Block does contain nothing useful e.g. second block of '10:00-12:00;' (empty) which needs to be handled.
     509
     510                        var continue_at = 0;
     511                        do {
     512                                if (continue_at == tokens[nblock][0].length) break;
     513                                // Block does contain nothing useful e.g. second block of '10:00-12:00,' (empty) which needs to be handled.
     514
     515                                var selectors = {
     516                                        // Time selectors
     517                                        time: [],
     518
     519                                        // Temporary array of selectors from time wrapped to the next day
     520                                        wraptime: [],
     521
     522                                        // Date selectors
     523                                        weekday: [],
     524                                        holiday: [],
     525                                        week: [],
     526                                        month: [],
     527                                        monthday: [],
     528                                        year: [],
     529
     530                                        // Array with non-empty date selector types, with most optimal ordering
     531                                        date: [],
     532
     533                                        fallback: tokens[nblock][1],
     534                                        additional: continue_at ? true : false,
     535                                        meaning: true,
     536                                        unknown: false,
     537                                        comment: undefined,
     538                                };
     539
     540                                continue_at = parseGroup(tokens[nblock][0], continue_at, selectors);
     541                                if (typeof continue_at == 'object')
     542                                        continue_at = continue_at[0];
     543                                else
     544                                        continue_at = 0;
     545
     546                                if (selectors.year.length > 0)
     547                                        selectors.date.push(selectors.year);
     548                                if (selectors.holiday.length > 0)
     549                                        selectors.date.push(selectors.holiday);
     550                                if (selectors.month.length > 0)
     551                                        selectors.date.push(selectors.month);
     552                                if (selectors.monthday.length > 0)
     553                                        selectors.date.push(selectors.monthday);
     554                                if (selectors.week.length > 0)
     555                                        selectors.date.push(selectors.week);
     556                                if (selectors.weekday.length > 0)
     557                                        selectors.date.push(selectors.weekday);
     558
     559                                blocks.push(selectors);
     560
     561                                // this handles selectors with time ranges wrapping over midnight (e.g. 10:00-02:00)
     562                                // it generates wrappers for all selectors and creates a new block
     563                                if (selectors.wraptime.length > 0) {
     564                                        var wrapselectors = {
     565                                                time: selectors.wraptime,
     566                                                date: [],
     567
     568                                                meaning: selectors.meaning,
     569                                                unknown: selectors.unknown,
     570                                                comment: selectors.comment,
     571
     572                                                wrapped: true,
     573                                        };
     574
     575                                        for (var dselg = 0; dselg < selectors.date.length; dselg++) {
     576                                                wrapselectors.date.push([]);
     577                                                for (var dsel = 0; dsel < selectors.date[dselg].length; dsel++) {
     578                                                        wrapselectors.date[wrapselectors.date.length-1].push(
     579                                                                        generateDateShifter(selectors.date[dselg][dsel], -msec_in_day)
     580                                                                );
     581                                                }
     582                                        }
     583
     584                                        blocks.push(wrapselectors);
     585                                }
     586                        } while (continue_at)
     587                }
     588
     589                // Tokenization function: Splits string into parts.
     590                // output: array of arrays of pairs [content, type]
     591                function tokenize(value) {
     592                        var all_tokens       = new Array();
     593                        var curr_block_tokens = new Array();
     594
     595                        var last_block_fallback_terminated = false;
     596
     597                        while (value != '') {
     598                                var tmp;
     599                                if (tmp = value.match(/^(?:week\b|open|unknown)/i)) {
     600                                        // reserved word
     601                                        curr_block_tokens.push([tmp[0].toLowerCase(), tmp[0].toLowerCase(), value.length ]);
     602                                        value = value.substr(tmp[0].length);
     603                                } else if (tmp = value.match(/^24\/7/i)) {
     604                                        // reserved word
     605                                        has_token[tmp[0]] = true;
     606                                        curr_block_tokens.push([tmp[0], tmp[0], value.length ]);
     607                                        value = value.substr(tmp[0].length);
     608                                } else if (tmp = value.match(/^(?:off|closed)/i)) {
     609                                        // reserved word
     610                                        curr_block_tokens.push([tmp[0].toLowerCase(), 'closed', value.length ]);
     611                                        value = value.substr(tmp[0].length);
     612                                } else if (tmp = value.match(/^(?:PH|SH)/i)) {
     613                                        // special day name (holidays)
     614                                        curr_block_tokens.push([tmp[0].toUpperCase(), 'holiday', value.length ]);
     615                                        value = value.substr(2);
     616                                } else if (tmp = value.match(/^days?/i)) {
     617                                        curr_block_tokens.push([tmp[0].toLowerCase(), 'calcday', value.length ]);
     618                                        value = value.substr(tmp[0].length);
     619                                } else if (tmp = value.match(/^(:?&|–|[a-zA-Z]+\b)/i)) {
     620                                        // Handle all remaining words with error tolerance
     621                                        var correct_val = returnCorrectWordOrToken(tmp[0].toLowerCase(), value.length);
     622                                        if (typeof correct_val == 'object') {
     623                                                curr_block_tokens.push([ correct_val[0], correct_val[1], value.length ]);
     624                                                value = value.substr(tmp[0].length);
     625                                        } else if (typeof correct_val == 'string') {
     626                                                value = correct_val + value.substr(tmp[0].length);
     627                                        } else {
     628                                                // other single-character tokens
     629                                                curr_block_tokens.push([value[0].toLowerCase(), value[0].toLowerCase(), value.length - 1 ]);
     630                                                value = value.substr(1);
     631                                        }
     632                                } else if (tmp = value.match(/^\d+/)) {
     633                                        // number
     634                                        if (tmp[0] > 1900) // assumed to be a year number
     635                                                curr_block_tokens.push([tmp[0], 'year', value.length ]);
     636                                        else
     637                                                curr_block_tokens.push([+tmp[0], 'number', value.length ]);
     638                                        value = value.substr(tmp[0].length);
     639                                } else if (tmp = value.match(/^"([^"]*)"/)) {
     640                                        // comment
     641                                        curr_block_tokens.push([tmp[1], 'comment', value.length ]);
     642                                        value = value.substr(tmp[0].length);
     643                                } else if (value.match(/^;/)) {
     644                                        // semicolon terminates block
     645                                        // next tokens belong to a new block
     646                                        all_tokens.push([ curr_block_tokens, last_block_fallback_terminated, value.length ]);
     647                                        value = value.substr(1);
     648
     649                                        curr_block_tokens = [];
     650                                        last_block_fallback_terminated = false;
     651                                } else if (value.match(/^\|\|/)) {
     652                                        // || terminates block
     653                                        // next tokens belong to a fallback block
     654                                        if (curr_block_tokens.length == 0)
     655                                                throw formatWarnErrorMessage(-1, value.length - 2, 'Rule before fallback rule does not contain anything useful');
     656
     657                                        all_tokens.push([ curr_block_tokens, last_block_fallback_terminated, value.length ]);
     658                                        value = value.substr(2);
     659
     660                                        curr_block_tokens = [];
     661                                        last_block_fallback_terminated = true;
     662                                } else if (value.match(/^\s/)) {
     663                                        value = value.substr(1);
     664                                } else if (tmp = value.match(/^\s+/)) {
     665                                        // whitespace is ignored
     666                                        value = value.substr(tmp[0].length);
     667                                } else if (value.match(/^[:.]/)) {
     668                                        // time separator
     669                                        if (value[0] == '.')
     670                                                parsing_warnings.push([ -1, value.length - 1, 'Please use ":" as hour/minute-separator' ]);
     671                                        curr_block_tokens.push([ ':', 'timesep', value.length ]);
     672                                        value = value.substr(1);
     673                                } else {
     674                                        // other single-character tokens
     675                                        curr_block_tokens.push([value[0].toLowerCase(), value[0].toLowerCase(), value.length ]);
     676                                        value = value.substr(1);
     677                                }
     678                        }
     679
     680                        all_tokens.push([ curr_block_tokens, last_block_fallback_terminated ]);
     681
     682                        return all_tokens;
     683                }
     684
     685                // error correction/tolerance
     686                function returnCorrectWordOrToken(word, value_length) {
     687                        for (var token_name in word_error_correction) {
     688                                for (var comment in word_error_correction[token_name]) {
     689                                        for (var old_val in word_error_correction[token_name][comment]) {
     690                                                if (old_val == word) {
     691                                                        var val = word_error_correction[token_name][comment][old_val];
     692                                                        if (token_name == 'wrong_words') {
     693                                                                parsing_warnings.push([ -1, value_length - old_val.length,
     694                                                                        comment.replace(/<ko>/, old_val).replace(/<ok>/, val) ]);
     695                                                                return val;
     696                                                        } else if (comment != 'default'){
     697                                                                var correct_abbr;
     698                                                                for (correct_abbr in word_error_correction[token_name]['default']) {
     699                                                                        if (word_error_correction[token_name]['default'][correct_abbr] == val)
     700                                                                                break;
     701                                                                }
     702                                                                if (token_name != 'timevar') { // normally written in lower case
     703                                                                        correct_abbr = correct_abbr.charAt(0).toUpperCase() + correct_abbr.slice(1);
     704                                                                }
     705                                                                parsing_warnings.push([ -1, value_length - old_val.length,
     706                                                                        comment.replace(/<ko>/, old_val).replace(/<ok>/, correct_abbr) ]);
     707                                                        }
     708                                                        return [ val, token_name ];
     709                                                }
     710                                        }
     711                                }
     712                        }
     713                }
     714
     715                function getWarnings(it) {
     716
     717                        if (typeof it == 'object') { // getWarnings was called in a state without critical errors. We can do extended tests.
     718
     719                                // Check if 24/7 is used and it does not mean 24/7 because there are other rules. This can be avoided.
     720                                var has_advanced = it.advance();
     721
     722                                if (has_advanced === true && has_token['24/7']) // Probably because of: "24/7; 12:00-14:00 open", ". Needs extra testing.
     723                                        parsing_warnings.push([ -1, 0, 'You used 24/7 in a way that is probably not interpreted as "24 hours 7 days a week".'
     724                                                        + ' For correctness you might want to use "open" or "closed"'
     725                                                        + ' for this rule and then write your exceptions which should achieve the same goal and is more clear'
     726                                                        + ' e.g. "open; Mo 12:00-14:00 off".']);
     727                        }
     728
     729                        var warnings = [];
     730                        for (var i = 0; i < parsing_warnings.length; i++) {
     731                                warnings.push( formatWarnErrorMessage(parsing_warnings[i][0], parsing_warnings[i][1], parsing_warnings[i][2]) );
     732                        }
     733                        return warnings;
     734                }
     735
     736                // Function to check token array for specific pattern
     737                function matchTokens(tokens, at /*, matches... */) {
     738                        if (at + arguments.length - 2 > tokens.length)
     739                                return false;
     740                        for (var i = 0; i < arguments.length - 2; i++) {
     741                                if (tokens[at + i][1] !== arguments[i + 2])
     742                                        return false;
     743                        }
     744
     745                        return true;
     746                }
     747
     748                function generateDateShifter(func, shift) {
     749                        return function(date) {
     750                                var res = func(new Date(date.getTime() + shift));
     751
     752                                if (typeof res[1] === 'undefined')
     753                                        return res;
     754                                return [ res[0], new Date(res[1].getTime() - shift) ];
     755                        }
     756                }
     757
     758                //======================================================================
     759                // Top-level parser
     760                //======================================================================
     761                function parseGroup(tokens, at, selectors, conf) {
     762                        var prettified_group_value = '';
     763
     764                        // console.log(tokens); // useful for debugging of tokenize
     765                        while (at < tokens.length) {
     766                                var old_at = at;
     767                                var last_subparser;
     768                                // console.log('Parsing at position', at +':', tokens[at]);
     769                                if (matchTokens(tokens, at, 'weekday')) {
     770                                        at = parseWeekdayRange(tokens, at, selectors);
     771                                } else if (matchTokens(tokens, at, '24/7')) {
     772                                        // selectors.time.push(function(date) { return [true]; }); // Not needed. If there is now selector it automatically matches everything.
     773                                        at++;
     774                                } else if (matchTokens(tokens, at, 'holiday')) {
     775                                        if (matchTokens(tokens, at+1, ',', 'weekday'))
     776                                                at = parseHoliday(tokens, at, selectors, true);
     777                                        else
     778                                                at = parseHoliday(tokens, at, selectors, false);
     779                                        week_stable = false;
     780                                } else if (matchTokens(tokens, at, 'month', 'number')
     781                                                || matchTokens(tokens, at, 'year', 'month', 'number')
     782                                                || matchTokens(tokens, at, 'year', 'event')
     783                                                || matchTokens(tokens, at, 'event')) {
     784                                        at = parseMonthdayRange(tokens, at);
     785                                        week_stable = false;
     786                                } else if (matchTokens(tokens, at, 'year')) {
     787                                        at = parseYearRange(tokens, at);
     788                                        week_stable = false;
     789                                } else if (matchTokens(tokens, at, 'month')) {
     790                                        at = parseMonthRange(tokens, at);
     791                                        // week_stable = false; // decided based on actual values
     792                                } else if (matchTokens(tokens, at, 'week')) {
     793                                        at = parseWeekRange(tokens, at + 1);
     794                                        week_stable = false;
     795                                } else if (at != 0 && at != tokens.length - 1 && tokens[at][0] == ':') {
     796                                        // Ignore colon if they appear somewhere else than as time separator.
     797                                        // This provides compatibility with the syntax proposed by Netzwolf:
     798                                        // http://www.netzwolf.info/en/cartography/osm/time_domain/specification
     799                                        if (matchTokens(tokens, at-1, 'weekday') || matchTokens(tokens, at-1, 'holiday'))
     800                                                parsing_warnings.push([nblock, at, 'Please don’t use ":" after ' + tokens[at-1][1] + '.']);
     801
     802                                        if (prettified_group_value[-1] != ' ')
     803                                                prettified_group_value = prettified_group_value.substring(0, prettified_group_value.length - 1)
     804                                        at++;
     805                                } else if (matchTokens(tokens, at, 'number', 'timesep')
     806                                                || matchTokens(tokens, at, 'timevar')
     807                                                || matchTokens(tokens, at, '(', 'timevar')) {
     808                                        at = parseTimeRange(tokens, at, selectors);
     809                                        last_subparser = 'time';
     810                                } else if (matchTokens(tokens, at, 'closed')) {
     811                                        selectors.meaning = false;
     812                                        at++;
     813                                        if (matchTokens(tokens, at, ',')) // additional block
     814                                                at = [ at + 1 ];
     815                                } else if (matchTokens(tokens, at, 'open')) {
     816                                        selectors.meaning = true;
     817                                        at++;
     818                                        if (matchTokens(tokens, at, ',')) // additional block
     819                                                at = [ at + 1 ];
     820                                } else if (matchTokens(tokens, at, 'unknown')) {
     821                                        selectors.meaning = false;
     822                                        selectors.unknown = true;
     823                                        at++;
     824                                        if (matchTokens(tokens, at, ',')) // additional block
     825                                                at = [ at + 1 ];
     826                                } else if (matchTokens(tokens, at, 'comment')) {
     827                                        selectors.comment = tokens[at][0];
     828                                        if (at > 0) {
     829                                                if (!matchTokens(tokens, at - 1, 'open')
     830                                                        && !matchTokens(tokens, at - 1, 'closed')) {
     831                                                        // Then it is unknown. Either with unknown explicitly
     832                                                        // specified or just a comment behind.
     833                                                        selectors.meaning = false;
     834                                                        selectors.unknown = true;
     835                                                }
     836                                        } else { // block starts with comment
     837                                                selectors.time.push(function(date) { return [true]; });
     838                                                selectors.meaning = false;
     839                                                selectors.unknown = true;
     840                                        }
     841                                        at++;
     842                                        if (matchTokens(tokens, at, ',')) // additional block
     843                                                at = [ at + 1 ];
     844                                } else {
     845                                        var warnings = getWarnings();
     846                                        throw formatWarnErrorMessage(nblock, at, 'Unexpected token: "' + tokens[at][1]
     847                                                + '" This means that the syntax is not valid at that point.') + (warnings ? ' ' + warnings.join('; ') : '');
     848                                }
     849
     850                                if (typeof conf != 'undefined')
     851                                        prettified_group_value += prettifySelector(tokens, old_at, at, conf, last_subparser);
     852
     853                                if (typeof at == 'object') // additional block
     854                                        break;
     855                        }
     856
     857                        prettified_value += prettified_group_value.replace(/\s+$/, '');
     858
     859                        return at;
     860                }
     861
     862                //======================================================================
     863                // Time range parser (10:00-12:00,14:00-16:00)
     864                //======================================================================
     865                function parseTimeRange(tokens, at, selectors) {
     866                        for (; at < tokens.length; at++) {
     867                                var has_time_var_calc = [], has_normal_time = []; // element 0: start time, 1: end time
     868                                has_normal_time[0] = matchTokens(tokens, at, 'number', 'timesep', 'number');
     869                                has_time_var_calc[0] = matchTokens(tokens, at, '(', 'timevar');
     870                                if (has_normal_time[0] || matchTokens(tokens, at, 'timevar') || has_time_var_calc[0]) {
     871                                        // relying on the fact that always *one* of them is true
     872
     873                                        if (has_normal_time[0])
     874                                                var minutes_from = tokens[at+has_time_var_calc[0]][0] * 60 + tokens[at+has_time_var_calc[0]+2][0];
     875                                        else
     876                                                var minutes_from = word_value_replacement[tokens[at+has_time_var_calc[0]][0]];
     877
     878                                        var timevar_add = [ 0, 0 ];
     879                                        if (has_time_var_calc[0]) {
     880                                                timevar_add[0] = parseTimevarCalc(tokens, at);
     881                                                minutes_from += timevar_add[0];
     882                                        }
     883
     884                                        var has_open_end = false;
     885                                        if (!matchTokens(tokens, at+(has_normal_time[0] ? 3 : (has_time_var_calc[0] ? 7 : 1)), '-')) {
     886                                                if (matchTokens(tokens, at+(has_normal_time[0] ? 3 : (has_time_var_calc[0] ? 7 : 1)), '+')) {
     887                                                        has_open_end = true;
     888                                                } else {
     889                                                        throw formatWarnErrorMessage(nblock, at+(has_normal_time[0] ? 3 : (has_time_var_calc[0] ? 2 : 1)),
     890                                                                'hyphen (-) or open end (+) in time range '
     891                                                                + (has_time_var_calc[0] ? 'calculation ' : '')
     892                                                                + 'expected');
     893                                                }
     894                                        }
     895
     896                                        var at_end_time = at+(has_normal_time[0] ? 3 : (has_time_var_calc[0] ? 7 : 1))+1; // after '-'
     897                                        if (has_open_end) {
     898                                                if (minutes_from >= 22 * 60)
     899                                                        var minutes_to = minutes_from + 60 * 8;
     900                                                else if (minutes_from >= 17 * 60)
     901                                                        var minutes_to = minutes_from + 60 * 10;
     902                                                else
     903                                                        var minutes_to = minutes_in_day;
     904                                        } else {
     905                                                has_normal_time[1] = matchTokens(tokens, at_end_time, 'number', 'timesep', 'number');
     906                                                has_time_var_calc[1]      = matchTokens(tokens, at_end_time, '(', 'timevar');
     907                                                if (!has_normal_time[1] && !matchTokens(tokens, at_end_time, 'timevar') && !has_time_var_calc[1])
     908                                                        throw formatWarnErrorMessage(nblock, at_end_time, 'time range does not continue as expected');
     909
     910                                                if (has_normal_time[1])
     911                                                        var minutes_to = tokens[at_end_time][0] * 60 + tokens[at_end_time+2][0]
     912                                                else
     913                                                        var minutes_to = word_value_replacement[tokens[at_end_time+has_time_var_calc[1]][0]];
     914
     915                                                if (has_time_var_calc[1]) {
     916                                                        timevar_add[1] = parseTimevarCalc(tokens, at_end_time);
     917                                                        minutes_to += timevar_add[1];
     918                                                }
     919
     920                                                // this shortcut makes always-open range check faster
     921                                                // and is also useful in tests, as it doesn't produce
     922                                                // extra check points which may hide errors in other
     923                                                // selectors
     924                                                if (minutes_from == 0 && minutes_to == minutes_in_day)
     925                                                        selectors.time.push(function(date) { return [true]; });
     926                                        }
     927
     928                                        // normalize minutes into range
     929                                        // XXX: what if it's further than tomorrow?
     930                                        // XXX: this is incorrect, as it assumes the same day
     931                                        //      should cooperate with date selectors to select the next day
     932                                        if (minutes_from >= minutes_in_day)
     933                                                throw formatWarnErrorMessage(nblock, at_end_time + (has_normal_time[1] ? 3 : (has_time_var_calc[1] ? 7 : 1)) - 1,
     934                                                        'Time range starts outside of the current day');
     935                                        if (minutes_to < minutes_from || ((has_normal_time[0] && has_normal_time[1]) && minutes_from == minutes_to))
     936                                                minutes_to += minutes_in_day;
     937                                        if (minutes_to > minutes_in_day * 2)
     938                                                throw formatWarnErrorMessage(nblock, at_end_time + (has_normal_time[1] ? 3 : (has_time_var_calc[1] ? 7 : 1)) - 1,
     939                                                        'Time spanning more than two midnights not supported');
     940
     941                                        var timevar_string = [];
     942                                        if (typeof lat != 'undefined') { // lon will also be defined (see above)
     943                                                if ((!has_normal_time[0] || !has_normal_time[1]) && !has_open_end)
     944                                                        week_stable = false;
     945                                                if (!has_normal_time[0])
     946                                                        timevar_string[0] = tokens[at+has_time_var_calc[0]][0];
     947                                                if (!has_normal_time[1] && !has_open_end)
     948                                                        timevar_string[1]   = tokens[at_end_time+has_time_var_calc[1]][0]
     949                                        } // else: we can not calculate exact times so we use the already applied constants (word_value_replacement).
     950
     951                                        if (minutes_to > minutes_in_day) { // has_normal_time[1] must be true
     952                                                selectors.time.push(function(minutes_from, minutes_to, timevar_string, timevar_add, has_open_end) { return function(date) {
     953                                                        var ourminutes = date.getHours() * 60 + date.getMinutes();
     954
     955                                                        if (timevar_string[0]) {
     956                                                                var date_from = eval('SunCalc.getTimes(date, lat, lon).' + timevar_string[0]);
     957                                                                minutes_from  = date_from.getHours() * 60 + date_from.getMinutes() + timevar_add[0];
     958                                                        }
     959                                                        if (timevar_string[1]) {
     960                                                                var date_to = eval('SunCalc.getTimes(date, lat, lon).' + timevar_string[1]);
     961                                                                minutes_to  = date_to.getHours() * 60 + date_to.getMinutes() + timevar_add[1];
     962                                                                minutes_to += minutes_in_day;
     963                                                                // Needs to be added because it was added by
     964                                                                // normal times in: if (minutes_to < // minutes_from)
     965                                                                // above the selector construction.
     966                                                        }
     967
     968                                                        if (ourminutes < minutes_from)
     969                                                                return [false, dateAtDayMinutes(date, minutes_from)];
     970                                                        else
     971                                                                return [true, dateAtDayMinutes(date, minutes_to), has_open_end];
     972                                                }}(minutes_from, minutes_to, timevar_string, timevar_add, has_open_end));
     973
     974                                                selectors.wraptime.push(function(minutes_from, minutes_to, timevar_string, timevar_add, has_open_end) { return function(date) {
     975                                                        var ourminutes = date.getHours() * 60 + date.getMinutes();
     976
     977                                                        if (timevar_string[0]) {
     978                                                                var date_from = eval('SunCalc.getTimes(date, lat, lon).' + timevar_string[0]);
     979                                                                minutes_from  = date_from.getHours() * 60 + date_from.getMinutes() + timevar_add[0];
     980                                                        }
     981                                                        if (timevar_string[1]) {
     982                                                                var date_to = eval('SunCalc.getTimes(date, lat, lon).' + timevar_string[1]);
     983                                                                minutes_to  = date_to.getHours() * 60 + date_to.getMinutes() + timevar_add[1];
     984                                                                // minutes_in_day does not need to be added.
     985                                                                // For normal times in it was added in: if (minutes_to < // minutes_from)
     986                                                                // above the selector construction and
     987                                                                // subtracted in the selector construction call
     988                                                                // which returns the selector function.
     989                                                        }
     990
     991                                                        if (ourminutes < minutes_to)
     992                                                                return [true, dateAtDayMinutes(date, minutes_to), has_open_end];
     993                                                        else
     994                                                                return [false, undefined];
     995                                                }}(minutes_from, minutes_to - minutes_in_day, timevar_string, timevar_add, has_open_end));
     996                                        } else {
     997                                                selectors.time.push(function(minutes_from, minutes_to, timevar_string, timevar_add, has_open_end) { return function(date) {
     998                                                        var ourminutes = date.getHours() * 60 + date.getMinutes();
     999
     1000                                                        if (timevar_string[0]) {
     1001                                                                var date_from = eval('SunCalc.getTimes(date, lat, lon).' + timevar_string[0]);
     1002                                                                minutes_from  = date_from.getHours() * 60 + date_from.getMinutes() + timevar_add[0];
     1003                                                        }
     1004                                                        if (timevar_string[1]) {
     1005                                                                var date_to = eval('SunCalc.getTimes(date, lat, lon).' + timevar_string[1]);
     1006                                                                minutes_to  = date_to.getHours() * 60 + date_to.getMinutes() + timevar_add[1];
     1007                                                        }
     1008
     1009                                                        if (ourminutes < minutes_from)
     1010                                                                return [false, dateAtDayMinutes(date, minutes_from)];
     1011                                                        else if (ourminutes < minutes_to)
     1012                                                                return [true, dateAtDayMinutes(date, minutes_to), has_open_end];
     1013                                                        else
     1014                                                                return [false, dateAtDayMinutes(date, minutes_from + minutes_in_day)];
     1015                                                }}(minutes_from, minutes_to, timevar_string, timevar_add, has_open_end));
     1016                                        }
     1017
     1018                                        at = at_end_time + (has_normal_time[1] ? 3 : (has_time_var_calc[1] ? 7 : !has_open_end));
     1019                                } else { // additional block
     1020                                        if (matchTokens(tokens, at, '('))
     1021                                                throw formatWarnErrorMessage(nblock, at+1, 'Missing variable time (e.g. sunrise) after: "' + tokens[at][1] + '"');
     1022                                        if (matchTokens(tokens, at, 'number', 'timesep'))
     1023                                                throw formatWarnErrorMessage(nblock, at+2, 'Missing minutes in time range after: "' + tokens[at+1][1] + '"');
     1024                                        if (matchTokens(tokens, at, 'number'))
     1025                                                throw formatWarnErrorMessage(nblock, at+2, 'Missing time seperator in time range after: "' + tokens[at][1] + '"');
     1026                                        return [ at ];
     1027                                }
     1028
     1029                                if (!matchTokens(tokens, at, ','))
     1030                                        break;
     1031                        }
     1032
     1033                        return at;
     1034                }
     1035
     1036                // for given date, returns date moved to the start of specified day minute
     1037                function dateAtDayMinutes(date, minutes) {
     1038                        return new Date(date.getFullYear(), date.getMonth(), date.getDate(), 0, minutes);
     1039                }
     1040
     1041                // extract the added or subtracted time of "(sunrise-01:30)"
     1042                // returns in minutes e.g. -90
     1043                function parseTimevarCalc(tokens, at) {
     1044                        if (matchTokens(tokens, at+2, '+') || matchTokens(tokens, at+2, '-')) {
     1045                                if (matchTokens(tokens, at+3, 'number', 'timesep', 'number')) {
     1046                                        if (matchTokens(tokens, at+6, ')')) {
     1047                                                var add_or_subtract = tokens[at+2][0] == '+' ? '1' : '-1';
     1048                                                return (tokens[at+3][0] * 60 + tokens[at+5][0]) * add_or_subtract;
     1049                                        } else {
     1050                                                error = [ at+6, '. Missing ")".'];
     1051                                        }
     1052                                } else {
     1053                                        error = [ at+5, ' (time).'];
     1054                                }
     1055                        } else {
     1056                                error = [ at+2, '. "+" or "-" expected.'];
     1057                        }
     1058
     1059                        if (error)
     1060                                throw formatWarnErrorMessage(nblock, error[0],
     1061                                        'Calculcation with variable time is not in the right syntax' + error[1]);
     1062                }
     1063
     1064                //======================================================================
     1065                // Weekday range parser (Mo,We-Fr,Sa[1-2,-1])
     1066                //======================================================================
     1067                function parseWeekdayRange(tokens, at, selectors) {
     1068                        for (; at < tokens.length; at++) {
     1069                                if (matchTokens(tokens, at, 'weekday', '[')) {
     1070                                        // Conditional weekday (Mo[3])
     1071                                        var numbers = [];
     1072
     1073                                        // Get list of constraints
     1074                                        var endat = parseNumRange(tokens, at+2, function(from, to, at) {
     1075
     1076                                                // bad number
     1077                                                if (from == 0 || from < -5 || from > 5)
     1078                                                        throw formatWarnErrorMessage(nblock, at,
     1079                                                                'Number between -5 and 5 (except 0) expected');
     1080
     1081                                                if (from == to) {
     1082                                                        numbers.push(from);
     1083                                                } else if (from < to) {
     1084                                                        for (var i = from; i <= to; i++) {
     1085                                                                // bad number
     1086                                                                if (i == 0 || i < -5 || i > 5)
     1087                                                                        throw formatWarnErrorMessage(nblock, at+2,
     1088                                                                                'Number between -5 and 5 (except 0) expected.');
     1089
     1090                                                                numbers.push(i);
     1091                                                        }
     1092                                                } else {
     1093                                                        throw formatWarnErrorMessage(nblock, at+2,
     1094                                                                'Bad range: ' + from + '-' + to);
     1095                                                }
     1096                                        });
     1097
     1098                                        if (!matchTokens(tokens, endat, ']'))
     1099                                                throw formatWarnErrorMessage(nblock, endat, '"]" or more numbers expected.');
     1100
     1101                                        var add_days = getMoveDays(tokens, endat+1, 6, 'constrained weekdays');
     1102                                        week_stable = false;
     1103
     1104                                        // Create selector for each list element
     1105                                        for (var nnumber = 0; nnumber < numbers.length; nnumber++) {
     1106
     1107                                                selectors.weekday.push(function(weekday, number, add_days) { return function(date) {
     1108                                                        var date_num = getValueForDate(date, false); // Year not needed to distinguish.
     1109                                                        var start_of_this_month = new Date(date.getFullYear(), date.getMonth(), 1);
     1110                                                        var start_of_next_month = new Date(date.getFullYear(), date.getMonth() + 1, 1);
     1111
     1112                                                        var target_day_this_month;
     1113
     1114                                                        target_day_this_month = dateAtNextWeekday(
     1115                                                                new Date(date.getFullYear(), date.getMonth() + (number > 0 ? 0 : 1), 1), weekday);
     1116                                                        target_day_this_month.setDate(target_day_this_month.getDate() + (number + (number > 0 ? -1 : 0)) * 7);
     1117
     1118                                                        var target_day_with_added_days_this_month = new Date(target_day_this_month.getFullYear(),
     1119                                                                target_day_this_month.getMonth(), target_day_this_month.getDate() + add_days);
     1120
     1121
     1122                                                        // The target day with added days can be before this month
     1123                                                        if (target_day_with_added_days_this_month.getTime() < start_of_this_month.getTime()) {
     1124                                                                // but in this case, the target day without the days added needs to be in this month
     1125                                                                if (target_day_this_month.getTime() >= start_of_this_month.getTime()) {
     1126                                                                        // so we calculate it for the month
     1127                                                                        // following this month and hope that the
     1128                                                                        // target day will actually be this month.
     1129
     1130                                                                        target_day_with_added_days_this_month = dateAtNextWeekday(
     1131                                                                                new Date(date.getFullYear(), date.getMonth() + (number > 0 ? 0 : 1) + 1, 1), weekday);
     1132                                                                        target_day_this_month.setDate(target_day_with_added_days_this_month.getDate()
     1133                                                                                + (number + (number > 0 ? -1 : 0)) * 7 + add_days);
     1134                                                                } else {
     1135                                                                        // Calculated target day is not inside this month
     1136                                                                        // therefore the specified weekday (e.g. fifth Sunday)
     1137                                                                        // does not exist this month. Try it next month.
     1138                                                                        return [false, start_of_next_month];
     1139                                                                }
     1140                                                        } else if (target_day_with_added_days_this_month.getTime() >= start_of_next_month.getTime()) {
     1141                                                                // The target day is in the next month. If the target day without the added days is not in this month
     1142                                                                if (target_day_this_month.getTime() >= start_of_next_month.getTime())
     1143                                                                        return [false, start_of_next_month];
     1144                                                        }
     1145
     1146                                                        if (add_days > 0) {
     1147                                                                var target_day_with_added_moved_days_this_month = dateAtNextWeekday(
     1148                                                                        new Date(date.getFullYear(), date.getMonth() + (number > 0 ? 0 : 1) -1, 1), weekday);
     1149                                                                target_day_with_added_moved_days_this_month.setDate(target_day_with_added_moved_days_this_month.getDate()
     1150                                                                        + (number + (number > 0 ? -1 : 0)) * 7 + add_days);
     1151
     1152                                                                if (date_num == getValueForDate(target_day_with_added_moved_days_this_month, false))
     1153                                                                        return [true, dateAtDayMinutes(date, minutes_in_day)];
     1154                                                        } else if (add_days < 0) {
     1155                                                                var target_day_with_added_moved_days_this_month = dateAtNextWeekday(
     1156                                                                        new Date(date.getFullYear(), date.getMonth() + (number > 0 ? 0 : 1) + 1, 1), weekday);
     1157                                                                target_day_with_added_moved_days_this_month.setDate(target_day_with_added_moved_days_this_month.getDate()
     1158                                                                        + (number + (number > 0 ? -1 : 0)) * 7 + add_days);
     1159
     1160                                                                if (target_day_with_added_moved_days_this_month.getTime() >= start_of_next_month.getTime()) {
     1161                                                                        if (target_day_with_added_days_this_month.getTime() >= start_of_next_month.getTime())
     1162                                                                                return [false, target_day_with_added_moved_days_this_month];
     1163                                                                } else {
     1164                                                                        if (target_day_with_added_days_this_month.getTime() < start_of_next_month.getTime()
     1165                                                                                && getValueForDate(target_day_with_added_days_this_month, false) == date_num)
     1166                                                                                return [true, dateAtDayMinutes(date, minutes_in_day)];
     1167
     1168                                                                        target_day_with_added_days_this_month = target_day_with_added_moved_days_this_month;
     1169                                                                }
     1170                                                        }
     1171
     1172                                                        // we hit the target day
     1173                                                        if (date.getDate() == target_day_with_added_days_this_month.getDate()) {
     1174                                                                return [true, dateAtDayMinutes(date, minutes_in_day)];
     1175                                                        }
     1176
     1177                                                        // we're before target day
     1178                                                        if (date.getDate() < target_day_with_added_days_this_month.getDate()) {
     1179                                                                return [false, target_day_with_added_days_this_month];
     1180                                                        }
     1181
     1182                                                        // we're after target day, set check date to next month
     1183                                                        return [false, start_of_next_month];
     1184                                                }}(tokens[at][0], numbers[nnumber], add_days[0]));
     1185                                        }
     1186
     1187                                        at = endat + 1 + add_days[1] * 3;
     1188                                } else if (matchTokens(tokens, at, 'weekday')) {
     1189                                        // Single weekday (Mo) or weekday range (Mo-Fr)
     1190                                        var is_range = matchTokens(tokens, at+1, '-', 'weekday');
     1191
     1192                                        var weekday_from = tokens[at][0];
     1193                                        var weekday_to = is_range ? tokens[at+2][0] : weekday_from;
     1194
     1195                                        var inside = true;
     1196
     1197                                        // handle reversed range
     1198                                        if (weekday_to < weekday_from) {
     1199                                                var tmp = weekday_to;
     1200                                                weekday_to = weekday_from - 1;
     1201                                                weekday_from = tmp + 1;
     1202                                                inside = false;
     1203                                        }
     1204
     1205                                        if (weekday_to < weekday_from) {
     1206                                                // handle full range
     1207                                                selectors.weekday.push(function(date) { return [true]; });
     1208                                        } else {
     1209                                                selectors.weekday.push(function(weekday_from, weekday_to, inside) { return function(date) {
     1210                                                        var ourweekday = date.getDay();
     1211
     1212                                                        if (ourweekday < weekday_from || ourweekday > weekday_to) {
     1213                                                                return [!inside, dateAtNextWeekday(date, weekday_from)];
     1214                                                        } else {
     1215                                                                return [inside, dateAtNextWeekday(date, weekday_to + 1)];
     1216                                                        }
     1217                                                }}(weekday_from, weekday_to, inside));
     1218                                        }
     1219
     1220                                        at += is_range ? 3 : 1;
     1221                                } else if (matchTokens(tokens, at, 'holiday')) {
     1222                                        week_stable = false;
     1223                                        return parseHoliday(tokens, at, selectors, true);
     1224                                } else {
     1225                                        throw formatWarnErrorMessage(nblock, at, 'Unexpected token in weekday range: ' + tokens[at][1]);
     1226                                }
     1227
     1228                                if (!matchTokens(tokens, at, ','))
     1229                                        break;
     1230                        }
     1231
     1232                        return at;
     1233                }
     1234
     1235                // Numeric list parser (1,2,3-4,-1), used in weekday parser above
     1236                function parseNumRange(tokens, at, func) {
     1237                        for (; at < tokens.length; at++) {
     1238                                if (matchTokens(tokens, at, 'number', '-', 'number')) {
     1239                                        // Number range
     1240                                        func(tokens[at][0], tokens[at+2][0], at);
     1241                                        at += 3;
     1242                                } else if (matchTokens(tokens, at, '-', 'number')) {
     1243                                        // Negative number
     1244                                        func(-tokens[at+1][0], -tokens[at+1][0], at);
     1245                                        at += 2
     1246                                } else if (matchTokens(tokens, at, 'number')) {
     1247                                        // Single number
     1248                                        func(tokens[at][0], tokens[at][0], at);
     1249                                        at++;
     1250                                } else {
     1251                                        throw formatWarnErrorMessage(nblock, at + matchTokens(tokens, at, '-'),
     1252                                                'Unexpected token in number range: ' + tokens[at][1]);
     1253                                }
     1254
     1255                                if (!matchTokens(tokens, at, ','))
     1256                                        break;
     1257                        }
     1258
     1259                        return at;
     1260                }
     1261
     1262                function getMoveDays(tokens, at, max_differ, name) {
     1263                        var add_days = [ 0, false ];
     1264                        add_days[0] = matchTokens(tokens, at, '+') || (matchTokens(tokens, at, '-') ? -1 : 0);
     1265                        if (add_days[0] != 0 && matchTokens(tokens, at+1, 'number', 'calcday')) {
     1266                                // continues with '+ 5 days' or something like that
     1267                                if (tokens[at+1][0] > max_differ)
     1268                                        throw formatWarnErrorMessage(nblock, at+2,
     1269                                                'There should be no reason to differ more than ' + max_differ + ' days from a ' + name + '. If so tell us …');
     1270                                add_days[0] *= tokens[at+1][0];
     1271                                if (add_days[0] == 0)
     1272                                        parsing_warnings.push([ nblock, at+2, 'Adding 0 does not change the date. Please omit this.' ]);
     1273                                add_days[1] = true;
     1274                        } else {
     1275                                add_days[0] = 0;
     1276                        }
     1277                        return add_days;
     1278                }
     1279
     1280
     1281                // for given date, returns date moved to the specific day of week
     1282                function dateAtNextWeekday(date, day) {
     1283                        var delta = day - date.getDay();
     1284                        return new Date(date.getFullYear(), date.getMonth(), date.getDate() + delta + (delta < 0 ? 7 : 0));
     1285                }
     1286
     1287                //======================================================================
     1288                // Holiday parser for public and school holidays (PH,SH)
     1289                // 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.
     1290                //======================================================================
     1291                function parseHoliday(tokens, at, selectors, push_to_weekday) {
     1292                        for (; at < tokens.length; at++) {
     1293                                if (matchTokens(tokens, at, 'holiday')) {
     1294                                        if (tokens[at][0] == 'PH') {
     1295                                                var applying_holidays = getMatchingHoliday(tokens[at][0]);
     1296
     1297                                                // Only allow moving one day in the past or in the future.
     1298                                                // This makes implementation easier because only one holiday is assumed to be moved to the next year.
     1299                                                var add_days = getMoveDays(tokens, at+1, 1, 'public holiday');
     1300
     1301                                                var selector = function(applying_holidays, add_days) { return function(date) {
     1302
     1303                                                        var holidays = getApplyingHolidaysForYear(applying_holidays, date.getFullYear(), add_days);
     1304                                                        // Needs to be calculated each time because of movable days.
     1305
     1306                                                        var date_num = getValueForDate(date, true);
     1307
     1308                                                        for (var i = 0; i < holidays.length; i++) {
     1309                                                                var next_holiday_date_num = getValueForDate(holidays[i][0], true);
     1310
     1311                                                                if (date_num < next_holiday_date_num) {
     1312
     1313                                                                        if (add_days[0] > 0) {
     1314                                                                                // Calculate the last holiday from previous year to tested against it.
     1315                                                                                var holidays_last_year = getApplyingHolidaysForYear(applying_holidays, date.getFullYear() - 1, add_days);
     1316                                                                                var last_holiday_last_year = holidays_last_year[holidays_last_year.length - 1];
     1317                                                                                var last_holiday_last_year_num = getValueForDate(last_holiday_last_year[0], true);
     1318
     1319                                                                                if (date_num < last_holiday_last_year_num ) {
     1320                                                                                        return [ false, last_holiday_last_year[0] ];
     1321                                                                                } else if (date_num == last_holiday_last_year_num) {
     1322                                                                                        return [true, dateAtDayMinutes(last_holiday_last_year[0], minutes_in_day),
     1323                                                                                                'Day after ' +last_holiday_last_year[1] ];
     1324                                                                                }
     1325                                                                        }
     1326
     1327                                                                        return [ false, holidays[i][0] ];
     1328                                                                } else if (date_num == next_holiday_date_num) {
     1329                                                                        return [true, new Date(date.getFullYear(), date.getMonth(), date.getDate() + 1),
     1330                                                                                (add_days[0] > 0 ? 'Day after ' : (add_days[0] < 0 ? 'Day before ' : '')) + holidays[i][1] ];
     1331                                                                }
     1332                                                        }
     1333
     1334                                                        if (add_days[0] < 0) {
     1335                                                                // Calculate the first holiday from next year to tested against it.
     1336                                                                var holidays_next_year = getApplyingHolidaysForYear(applying_holidays, date.getFullYear() + 1, add_days);
     1337                                                                var first_holidays_next_year = holidays_next_year[0];
     1338                                                                var first_holidays_next_year_num = getValueForDate(first_holidays_next_year[0], true);
     1339                                                                if (date_num == first_holidays_next_year_num) {
     1340                                                                        return [true, dateAtDayMinutes(first_holidays_next_year[0], minutes_in_day),
     1341                                                                                'Day before ' + first_holidays_next_year[1] ];
     1342                                                                }
     1343                                                        }
     1344
     1345                                                        // continue next year
     1346                                                        return [ false, new Date(holidays[0][0].getFullYear() + 1,
     1347                                                                        holidays[0][0].getMonth(),
     1348                                                                        holidays[0][0].getDate()) ];
     1349
     1350                                                }}(applying_holidays, add_days);
     1351
     1352                                                if (push_to_weekday)
     1353                                                        selectors.weekday.push(selector);
     1354                                                else
     1355                                                        selectors.holiday.push(selector);
     1356
     1357                                                at += 1 + add_days[1] * 3;
     1358                                        } else if (tokens[at][0] == 'SH') {
     1359                                                var applying_holidays = getMatchingHoliday(tokens[at][0]);
     1360
     1361                                                var holidays = []; // needs to be sorted each time because of movable days
     1362
     1363                                                var selector = function(applying_holidays) { return function(date) {
     1364                                                        var date_num = getValueForDate(date);
     1365
     1366                                                        // Iterate over holiday array containing the different holiday ranges.
     1367                                                        for (var i = 0; i < applying_holidays.length; i++) {
     1368
     1369                                                                var holiday = getSHForYear(applying_holidays[i], date.getFullYear());
     1370
     1371                                                                for (var h = 0; h < holiday.length; h+=4) {
     1372                                                                        var holiday_to_plus = new Date(date.getFullYear(), holiday[2+h] - 1, holiday[3+h] + 1);
     1373                                                                        var holiday_from = (holiday[0+h] - 1) * 100 + holiday[1+h];
     1374                                                                        var holiday_to   = (holiday[2+h] - 1) * 100 + holiday[3+h];
     1375                                                                        holiday_to_plus  = getValueForDate(holiday_to_plus);
     1376
     1377                                                                        var holiday_ends_next_year = holiday_to < holiday_from;
     1378
     1379                                                                        if (date_num < holiday_from) { // date is before selected holiday
     1380
     1381                                                                                // check if we are in the holidays from the last year spanning into this year
     1382                                                                                var last_year_holiday = getSHForYear(applying_holidays[applying_holidays.length - 1], date.getFullYear() - 1, false);
     1383                                                                                if (typeof last_year_holiday != 'undefined') {
     1384                                                                                        var last_year_holiday_from = (last_year_holiday[last_year_holiday.length - 4] - 1) * 100
     1385                                                                                                + last_year_holiday[last_year_holiday.length - 3]; // e.g. 1125
     1386                                                                                        var last_year_holiday_to   = (last_year_holiday[last_year_holiday.length - 2] - 1) * 100
     1387                                                                                                + last_year_holiday[last_year_holiday.length - 1]; // e.g. 0005
     1388
     1389                                                                                        if (last_year_holiday_to < last_year_holiday_from && date_num < last_year_holiday_to)
     1390                                                                                                return [ true, new Date(date.getFullYear(),
     1391                                                                                                        last_year_holiday[last_year_holiday.length - 2] - 1,
     1392                                                                                                        last_year_holiday[last_year_holiday.length - 1] + 1),
     1393                                                                                                        applying_holidays[applying_holidays.length - 1].name ];
     1394                                                                                        else
     1395                                                                                                return [ false, new Date(date.getFullYear(), holiday[0+h] - 1, holiday[1+h]) ];
     1396                                                                                } else { // school holidays for last year are not defined.
     1397                                                                                        return [ false, new Date(date.getFullYear(), holiday[0+h] - 1, holiday[1+h]) ];
     1398                                                                                }
     1399                                                                        } else if (holiday_from <= date_num && (date_num < holiday_to_plus || holiday_ends_next_year)) {
     1400                                                                                return [ true, new Date(date.getFullYear() + holiday_ends_next_year, holiday[2+h] - 1, holiday[3+h] + 1),
     1401                                                                                        applying_holidays[i].name ];
     1402                                                                        } else if (holiday_to_plus == date_num) { // selected holiday end is equal to month and day
     1403                                                                                if (h + 4 < holiday.length) { // next holiday is next date range of the same holidays
     1404                                                                                        h += 4;
     1405                                                                                        return [ false, new Date(date.getFullYear(), holiday[0+h] - 1, holiday[1+h]) ];
     1406                                                                                } else {
     1407                                                                                        if (i + 1 == applying_holidays.length) { // last holidays are handled, continue all over again
     1408                                                                                                var holiday = getSHForYear(applying_holidays[0], date.getFullYear() + 1);
     1409                                                                                                return [ false, new Date(date.getFullYear() + !holiday_ends_next_year, holiday[0+h] - 1, holiday[1+h]) ];
     1410                                                                                        } else { // return the start of the next holidays
     1411                                                                                                        var holiday = getSHForYear(applying_holidays[i+1], date.getFullYear());
     1412                                                                                                        return [ false, new Date(date.getFullYear(), holiday[0] - 1, holiday[1]) ];
     1413                                                                                        }
     1414                                                                                }
     1415                                                                        }
     1416                                                                }
     1417                                                        }
     1418                                                        return [ false ];
     1419                                                }}(applying_holidays);
     1420
     1421                                                if (push_to_weekday)
     1422                                                        selectors.weekday.push(selector);
     1423                                                else
     1424                                                        selectors.holiday.push(selector);
     1425                                                at += 1;
     1426                                        }
     1427                                } else if (matchTokens(tokens, at, 'weekday')) {
     1428                                        return parseWeekdayRange(tokens, at, selectors);
     1429                                } else {
     1430                                        throw formatWarnErrorMessage(nblock, at, 'Unexpected token (school holiday parser): ' + tokens[at][1]);
     1431                                }
     1432
     1433                                if (!matchTokens(tokens, at, ','))
     1434                                        break;
     1435                        }
     1436
     1437                        return at;
     1438                }
     1439
     1440                // Returns a number for a date which can then be used to compare just the dates (without the time).
     1441                // 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.
     1442                // Example: Returns 20150015 for Jan 01 2015
     1443                function getValueForDate(date, include_year) {
     1444                        // Implicit because undefined evaluates to false
     1445                        // include_year = typeof include_year != 'undefined' ? include_year : false;
     1446
     1447                        return (include_year ? date.getFullYear() * 10000 : 0) + date.getMonth() * 100 + date.getDate();
     1448                }
     1449
     1450                // return the school holiday definition e.g. [ 5, 25, /* to */ 6, 5 ],
     1451                // for the specified year
     1452                function getSHForYear(SH_hash, year, fatal) {
     1453                        if (typeof fatal == 'undefined')
     1454                                fatal = true;
     1455
     1456                        var holiday = SH_hash[year];
     1457                        if (typeof holiday == 'undefined') {
     1458                                holiday = SH_hash['default']; // applies for any year without explicit definition
     1459                                if (typeof holiday == 'undefined') {
     1460                                        if (fatal) {
     1461                                                throw 'School holiday ' + SH_hash.name + ' has no definition for the year ' + year + '.';
     1462                                        } else {
     1463                                                return undefined;
     1464                                        }
     1465                                }
     1466                        }
     1467                        return holiday;
     1468                }
     1469
     1470                // Return closed holiday definition available.
     1471                // First try to get the state, if missing get the country wide holidays
     1472                // (which can be limited to some states).
     1473                function getMatchingHoliday(type_of_holidays) {
     1474                        if (typeof location_cc != 'undefined') {
     1475                                if (holidays.hasOwnProperty(location_cc)) {
     1476                                        if (typeof location_state != 'undefined') {
     1477                                                if (holidays[location_cc][location_state]
     1478                                                                && holidays[location_cc][location_state][type_of_holidays]) {
     1479                                                        // if holidays for the state are specified use it
     1480                                                        // and ignore lesser specific ones (for the country)
     1481                                                        return holidays[location_cc][location_state][type_of_holidays];
     1482                                                } else if (holidays[location_cc][type_of_holidays]) {
     1483                                                        // holidays are only defined country wide
     1484                                                        matching_holiday = {}; // holidays in the country wide scope can be limited to certain states
     1485                                                        for (var holiday_name in holidays[location_cc][type_of_holidays]) {
     1486                                                                if (typeof holidays[location_cc][type_of_holidays][holiday_name][2] === 'object') {
     1487                                                                        if (-1 != indexOf.call(holidays[location_cc][type_of_holidays][holiday_name][2], location_state))
     1488                                                                                matching_holiday[holiday_name] = holidays[location_cc][type_of_holidays][holiday_name];
     1489                                                                } else {
     1490                                                                        matching_holiday[holiday_name] = holidays[location_cc][type_of_holidays][holiday_name];
     1491                                                                }
     1492                                                        }
     1493                                                        if (Object.keys(matching_holiday).length == 0)
     1494                                                                throw 'There are no holidays ' + type_of_holidays + ' defined for country ' + location_cc + '.'
     1495                                                                        + ' Please add them.';
     1496                                                        return matching_holiday;
     1497                                                } else {
     1498                                                        throw 'Holidays ' + type_of_holidays + ' are not defined for country ' + location_cc
     1499                                                                + ' and state ' + location_state + '.'
     1500                                                                + ' Please add them.';
     1501                                                }
     1502                                        }
     1503                                } else {
     1504                                        throw 'No holidays are defined for country ' + location_cc + '. Please add them.';
     1505                                }
     1506                        } else { // we have no idea which holidays do apply because the country code was not provided
     1507                                throw 'Country code missing which is needed to select the correct holidays (see README how to provide it)'
     1508                        }
     1509                }
     1510
     1511                function getMovableEventsForYear(Y) {
     1512                        // calculate easter
     1513                        var C = Math.floor(Y/100);
     1514                        var N = Y - 19*Math.floor(Y/19);
     1515                        var K = Math.floor((C - 17)/25);
     1516                        var I = C - Math.floor(C/4) - Math.floor((C - K)/3) + 19*N + 15;
     1517                        I = I - 30*Math.floor((I/30));
     1518                        I = I - Math.floor(I/28)*(1 - Math.floor(I/28)*Math.floor(29/(I + 1))*Math.floor((21 - N)/11));
     1519                        var J = Y + Math.floor(Y/4) + I + 2 - C + Math.floor(C/4);
     1520                        J = J - 7*Math.floor(J/7);
     1521                        var L = I - J;
     1522                        var M = 3 + Math.floor((L + 40)/44);
     1523                        var D = L + 28 - 31*Math.floor(M/4);
     1524
     1525                        return {
     1526                                'easter': new Date(Y, M - 1, D),
     1527                        };
     1528                }
     1529
     1530                function indexOf(needle) {
     1531                        if(typeof Array.prototype.indexOf === 'function') {
     1532                                indexOf = Array.prototype.indexOf;
     1533                        } else {
     1534                                indexOf = function(needle) {
     1535                                        var i = -1, index = -1;
     1536                                        for(i = 0; i < this.length; i++) {
     1537                                                if(this[i] === needle) {
     1538                                                        index = i;
     1539                                                        break;
     1540                                                }
     1541                                        }
     1542                                        return index;
     1543                                };
     1544                        }
     1545                        return indexOf.call(this, needle);
     1546                }
     1547
     1548                function getApplyingHolidaysForYear(applying_holidays, year, add_days) {
     1549                        var movableDays = getMovableEventsForYear(year);
     1550
     1551                        var sorted_holidays = [];
     1552
     1553                        for (var holiday_name in applying_holidays) {
     1554                                if (typeof applying_holidays[holiday_name][0] == 'string') {
     1555                                        var selected_movableDay = movableDays[applying_holidays[holiday_name][0]];
     1556                                        if (!selected_movableDay)
     1557                                                throw 'Movable day ' + applying_holidays[holiday_name][0] + ' can not not be calculated.'
     1558                                                        + ' Please add the formula how to calculate it.';
     1559                                        var next_holiday = new Date(selected_movableDay.getFullYear(),
     1560                                                        selected_movableDay.getMonth(),
     1561                                                        selected_movableDay.getDate()
     1562                                                        + applying_holidays[holiday_name][1]
     1563                                                );
     1564                                        if (year != next_holiday.getFullYear())
     1565                                                throw 'The movable day ' + applying_holidays[holiday_name][0] + ' plus '
     1566                                                        + applying_holidays[holiday_name][1]
     1567                                                        + ' days is not in the year of the movable day anymore. Currently not supported.';
     1568                                } else {
     1569                                        var next_holiday = new Date(year,
     1570                                                        applying_holidays[holiday_name][0] - 1,
     1571                                                        applying_holidays[holiday_name][1]
     1572                                                );
     1573                                }
     1574                                if (add_days[1]) {
     1575                                        next_holiday.setDate(next_holiday.getDate() + add_days[0]);
     1576                                }
     1577
     1578                                sorted_holidays.push([ next_holiday, holiday_name ]);
     1579                        }
     1580
     1581                        sorted_holidays = sorted_holidays.sort(function(a,b){
     1582                                if (a[0].getTime() < b[0].getTime()) return -1;
     1583                                if (a[0].getTime() > b[0].getTime()) return 1;
     1584                                return 0;
     1585                        });
     1586
     1587                        return sorted_holidays;
     1588                }
     1589
     1590                //======================================================================
     1591                // Year range parser (2013,2016-2018,2020/2)
     1592                //======================================================================
     1593                function parseYearRange(tokens, at) {
     1594                        for (; at < tokens.length; at++) {
     1595                                if (matchTokens(tokens, at, 'year')) {
     1596                                        var is_range = false, has_period = false;
     1597                                        if (matchTokens(tokens, at+1, '-', 'year', '/', 'number')) {
     1598                                                var is_range   = true;
     1599                                                var has_period = true;
     1600                                                if (tokens[at+4][0] == 1)
     1601                                                        parsing_warnings.push([nblock, at+1+3, 'Please don’t use year ranges with period equals one (see README)']);
     1602                                        } else {
     1603                                                var is_range   = matchTokens(tokens, at+1, '-', 'year');
     1604                                                var has_period = matchTokens(tokens, at+1, '/', 'number');
     1605                                        }
     1606
     1607                                        selectors.year.push(function(tokens, at, is_range, has_period) { return function(date) {
     1608                                                var ouryear = date.getFullYear();
     1609                                                var year_from = tokens[at][0];
     1610                                                var year_to = is_range ? tokens[at+2][0] : year_from;
     1611
     1612                                                // handle reversed range
     1613                                                if (year_to < year_from) {
     1614                                                        var tmp = year_to;
     1615                                                        year_to = year_from;
     1616                                                        year_from = tmp;
     1617                                                }
     1618
     1619                                                if (ouryear < year_from ){
     1620                                                        return [false, new Date(year_from, 0, 1)];
     1621                                                } else if (has_period) {
     1622                                                        if (year_from <= ouryear) {
     1623                                                                if (is_range) {
     1624                                                                        var period = tokens[at+4][0];
     1625
     1626                                                                        if (year_to < ouryear)
     1627                                                                                return [false];
     1628                                                                } else {
     1629                                                                        var period = tokens[at+2][0];
     1630                                                                }
     1631                                                                if (period > 0) {
     1632                                                                        if ((ouryear - year_from) % period == 0) {
     1633                                                                                return [true, new Date(ouryear + 1, 0, 1)];
     1634                                                                        } else {
     1635                                                                                return [false, new Date(ouryear + period - 1, 0, 1)];
     1636                                                                        }
     1637                                                                }
     1638                                                        }
     1639                                                } else if (is_range) {
     1640                                                        if (year_from <= ouryear && ouryear <= year_to)
     1641                                                                return [true, new Date(year_to + 1, 0, 1)];
     1642                                                } else if (ouryear == year_from) {
     1643                                                        return [true];
     1644                                                }
     1645
     1646                                                return [false];
     1647
     1648                                        }}(tokens, at, is_range, has_period));
     1649
     1650                                        at += 1 + (is_range ? 2 : 0) + (has_period ? 2 : 0);
     1651                                } else {
     1652                                        throw formatWarnErrorMessage(nblock, at, 'Unexpected token in year range: ' + tokens[at][1]);
     1653                                }
     1654
     1655                                if (!matchTokens(tokens, at, ','))
     1656                                        break;
     1657                        }
     1658
     1659                        return at;
     1660                }
     1661
     1662                //======================================================================
     1663                // Week range parser (week 11-20, week 1-53/2)
     1664                //======================================================================
     1665                function parseWeekRange(tokens, at) {
     1666                        for (; at < tokens.length; at++) {
     1667                                if (matchTokens(tokens, at, 'number')) {
     1668                                        var is_range = matchTokens(tokens, at+1, '-', 'number'), has_period = false;
     1669                                        if (is_range) {
     1670                                                has_period = matchTokens(tokens, at+3, '/', 'number');
     1671                                                // if (week_stable) {
     1672                                                //      if (tokens[at][0] == 1 && tokens[at+2][0] >) // Maximum?
     1673                                                //              week_stable = true;
     1674                                                //      else
     1675                                                //              week_stable = false;
     1676                                                // } else {
     1677                                                //      week_stable = false;
     1678                                                // }
     1679                                        }
     1680
     1681                                        selectors.week.push(function(tokens, at, is_range, has_period) { return function(date) {
     1682                                                var ourweek = Math.floor((date - dateAtWeek(date, 0)) / msec_in_week);
     1683
     1684                                                var week_from = tokens[at][0] - 1;
     1685                                                var week_to = is_range ? tokens[at+2][0] - 1 : week_from;
     1686
     1687                                                var start_of_next_year = new Date(date.getFullYear() + 1, 0, 1);
     1688
     1689                                                // before range
     1690                                                if (ourweek < week_from)
     1691                                                        return [false, getMinDate(dateAtWeek(date, week_from), start_of_next_year)];
     1692
     1693                                                // we're after range, set check date to next year
     1694                                                if (ourweek > week_to)
     1695                                                        return [false, start_of_next_year];
     1696
     1697                                                // we're in range
     1698                                                var period;
     1699                                                if (has_period) {
     1700                                                        var period = tokens[at+4][0];
     1701                                                        if (period > 1) {
     1702                                                                var in_period = (ourweek - week_from) % period == 0;
     1703                                                                if (in_period)
     1704                                                                        return [true, getMinDate(dateAtWeek(date, ourweek + 1), start_of_next_year)];
     1705                                                                else
     1706                                                                        return [false, getMinDate(dateAtWeek(date, ourweek + period - 1), start_of_next_year)];
     1707                                                        }
     1708                                                }
     1709
     1710                                                return [true, getMinDate(dateAtWeek(date, week_to + 1), start_of_next_year)];
     1711                                        }}(tokens, at, is_range, has_period));
     1712
     1713                                        at += 1 + (is_range ? 2 : 0) + (has_period ? 2 : 0);
     1714                                } else {
     1715                                        throw formatWarnErrorMessage(nblock, at, 'Unexpected token in week range: ' + tokens[at][1]);
     1716                                }
     1717
     1718                                if (!matchTokens(tokens, at, ','))
     1719                                        break;
     1720                        }
     1721
     1722                        return at;
     1723                }
     1724
     1725                function dateAtWeek(date, week) {
     1726                        var tmpdate = new Date(date.getFullYear(), 0, 1);
     1727                        tmpdate.setDate(1 - (tmpdate.getDay() + 6) % 7 + week * 7); // start of week n where week starts on Monday
     1728                        return tmpdate;
     1729                }
     1730
     1731                function getMinDate(date /*, ...*/) {
     1732                        for (var i = 1; i < arguments.length; i++)
     1733                                if (arguments[i].getTime() < date.getTime())
     1734                                        date = arguments[i];
     1735                        return date;
     1736                }
     1737
     1738                //======================================================================
     1739                // Month range parser (Jan,Feb-Mar)
     1740                //======================================================================
     1741                function parseMonthRange(tokens, at) {
     1742                        for (; at < tokens.length; at++) {
     1743                                if (matchTokens(tokens, at, 'month')) {
     1744                                        // Single month (Jan) or month range (Feb-Mar)
     1745                                        var is_range = matchTokens(tokens, at+1, '-', 'month');
     1746
     1747                                        if (is_range && week_stable) {
     1748                                                var month_from = tokens[at][0];
     1749                                                var month_to   = tokens[at+2][0];
     1750                                                if (month_from == (month_to + 1) % 12)
     1751                                                        week_stable = true;
     1752                                                else
     1753                                                        week_stable = false;
     1754                                        } else {
     1755                                                week_stable = false;
     1756                                        }
     1757
     1758                                        selectors.month.push(function(tokens, at, is_range) { return function(date) {
     1759                                                var ourmonth = date.getMonth();
     1760                                                var month_from = tokens[at][0];
     1761                                                var month_to = is_range ? tokens[at+2][0] : month_from;
     1762
     1763                                                var inside = true;
     1764
     1765                                                // handle reversed range
     1766                                                if (month_to < month_from) {
     1767                                                        var tmp = month_to;
     1768                                                        month_to = month_from - 1;
     1769                                                        month_from = tmp + 1;
     1770                                                        inside = false;
     1771                                                }
     1772
     1773                                                // handle full range
     1774                                                if (month_to < month_from)
     1775                                                        return [!inside];
     1776
     1777                                                if (ourmonth < month_from || ourmonth > month_to) {
     1778                                                        return [!inside, dateAtNextMonth(date, month_from)];
     1779                                                } else {
     1780                                                        return [inside, dateAtNextMonth(date, month_to + 1)];
     1781                                                }
     1782                                        }}(tokens, at, is_range));
     1783
     1784                                        at += is_range ? 3 : 1;
     1785                                } else {
     1786                                        throw formatWarnErrorMessage(nblock, at, 'Unexpected token in month range: ' + tokens[at][1]);
     1787                                }
     1788
     1789                                if (!matchTokens(tokens, at, ','))
     1790                                        break;
     1791                        }
     1792
     1793                        return at;
     1794                }
     1795
     1796                function dateAtNextMonth(date, month) {
     1797                        return new Date(date.getFullYear(), month < date.getMonth() ? month + 12 : month);
     1798                }
     1799
     1800                //======================================================================
     1801                // Month day range parser (Jan 26-31; Jan 26-Feb 26)
     1802                //======================================================================
     1803                function parseMonthdayRange(tokens, at) {
     1804                        for (; at < tokens.length; at++) {
     1805                                var has_year = [], has_month = [], has_event = [], has_calc = [];
     1806                                has_year[0]  = matchTokens(tokens, at, 'year');
     1807                                has_month[0] = matchTokens(tokens, at+has_year[0], 'month', 'number');
     1808                                has_event[0] = matchTokens(tokens, at+has_year[0], 'event');
     1809                                if (has_event[0])
     1810                                        has_calc[0] = getMoveDays(tokens, at+has_year[0]+1, 200, 'event like easter');
     1811                                var at_range_sep = at+has_year[0]
     1812                                                + (has_event[0]
     1813                                                        ? (typeof has_calc[0] != 'undefined' && has_calc[0][1] ? 4 : 1)
     1814                                                        : 2);
     1815                                if ((has_month[0] || has_event[0]) && matchTokens(tokens, at_range_sep, '-')) {
     1816                                        has_year[1]  = matchTokens(tokens, at_range_sep+1, 'year');
     1817                                        var at_sec_event_or_month = at_range_sep+1+has_year[1];
     1818                                        has_month[1] = matchTokens(tokens, at_sec_event_or_month, 'month', 'number');
     1819                                        if (!has_month[1]) {
     1820                                                has_event[1] = matchTokens(tokens, at_sec_event_or_month, 'event');
     1821                                                if (has_event[1])
     1822                                                        has_calc[1] = getMoveDays(tokens, at_sec_event_or_month+1, 366, 'event like easter');
     1823                                        }
     1824                                }
     1825
     1826                                if (has_year[0] == has_year[1] && (has_month[1] || has_event[1])) {
     1827
     1828                                        selectors.monthday.push(function(tokens, at, nblock, has_year, has_event, has_calc, at_sec_event_or_month) { return function(date) {
     1829                                                var start_of_next_year = new Date(date.getFullYear() + 1, 0, 1);
     1830
     1831                                                if (has_event[0]) {
     1832                                                        var movableDays = getMovableEventsForYear(has_year[0] ? parseInt(tokens[at][0]) : date.getFullYear());
     1833                                                        var from_date   = movableDays[tokens[at+has_year[0]][0]];
     1834
     1835                                                        if (typeof has_calc[0] != 'undefined' && has_calc[0][1]) {
     1836                                                                var from_year_before_calc = from_date.getFullYear();
     1837                                                                from_date.setDate(from_date.getDate() + has_calc[0][0]);
     1838                                                                if (from_year_before_calc != from_date.getFullYear())
     1839                                                                        throw formatWarnErrorMessage(nblock, at+has_year[0]+has_calc[0][1]*3,
     1840                                                                                'The movable day ' + tokens[at+has_year[0]][0] + ' plus ' + has_calc[0][0]
     1841                                                                                + ' days is not in the year of the movable day anymore. Currently not supported.');
     1842                                                        }
     1843                                                } else {
     1844                                                        var from_date = new Date((has_year[0] ? tokens[at][0] : date.getFullYear()),
     1845                                                                tokens[at+has_year[0]][0], tokens[at+has_year[0]+1][0]);
     1846                                                }
     1847
     1848                                                if (has_event[1]) {
     1849                                                        var movableDays = getMovableEventsForYear(has_year[1]
     1850                                                                                ? parseInt(tokens[at_sec_event_or_month-1][0])
     1851                                                                                : date.getFullYear());
     1852                                                        var to_date     = movableDays[tokens[at_sec_event_or_month][0]];
     1853
     1854                                                        if (typeof has_calc[1] != 'undefined' && has_calc[1][1]) {
     1855                                                                var to_year_before_calc = to_date.getFullYear();
     1856                                                                to_date.setDate(to_date.getDate() + has_calc[1][0]);
     1857                                                                if (to_year_before_calc != to_date.getFullYear())
     1858                                                                        throw formatWarnErrorMessage(nblock, at_sec_event_or_month+has_calc[1][1]*3,
     1859                                                                                'The movable day ' + tokens[at_sec_event_or_month][0] + ' plus ' + has_calc[1][0]
     1860                                                                                + ' days is not in the year of the movable day anymore. Currently not supported.');
     1861                                                        }
     1862                                                } else {
     1863                                                        var to_date = new Date((has_year[1] ? tokens[at_sec_event_or_month-1][0] : date.getFullYear()),
     1864                                                                tokens[at_sec_event_or_month][0], tokens[at_sec_event_or_month+1][0] + 1);
     1865                                                }
     1866
     1867                                                var inside = true;
     1868
     1869                                                if (to_date < from_date) {
     1870                                                        var tmp = to_date;
     1871                                                        to_date = from_date;
     1872                                                        from_date = tmp;
     1873                                                        inside = false;
     1874                                                }
     1875
     1876                                                if (date.getTime() < from_date.getTime()) {
     1877                                                        return [!inside, from_date];
     1878                                                } else if (date.getTime() < to_date.getTime()) {
     1879                                                        return [inside, to_date];
     1880                                                } else {
     1881                                                        if (has_year[0])
     1882                                                                return [!inside];
     1883                                                        else
     1884                                                                return [!inside, start_of_next_year];
     1885                                                }
     1886                                        }}(tokens, at, nblock, has_year, has_event, has_calc, at_sec_event_or_month));
     1887
     1888                                        at = at_sec_event_or_month
     1889                                                + (has_event[1]
     1890                                                        ? (typeof has_calc[1] != 'undefined' && has_calc[1][1] ? 4 : 1)
     1891                                                        : 2);
     1892
     1893                                } else if (has_month[0]) {
     1894                                        var is_range = matchTokens(tokens, at+2+has_year[0], '-', 'number'), has_period = false;
     1895                                        if (is_range)
     1896                                                has_period = matchTokens(tokens, at+4+has_year[0], '/', 'number');
     1897
     1898                                        var at_timesep_if_monthRange = at + has_year[0] + 1 // at month number
     1899                                                + (is_range ? 2 : 0) + (has_period ? 2 : 0)
     1900                                                + !(is_range || has_period); // if not range nor has_period, add one
     1901
     1902                                        if (matchTokens(tokens, at_timesep_if_monthRange, 'timesep'))
     1903                                                return parseMonthRange(tokens, at);
     1904
     1905                                        selectors.monthday.push(function(tokens, at, is_range, has_period, has_year) { return function(date) {
     1906                                                var start_of_next_year = new Date(date.getFullYear() + 1, 0, 1);
     1907
     1908                                                var from_date = new Date((has_year ? tokens[at][0] : date.getFullYear()),
     1909                                                        tokens[at+has_year][0], tokens[at+1 + has_year][0]);
     1910                                                var to_date   = new Date(from_date.getFullYear(), from_date.getMonth(),
     1911                                                        tokens[at+(is_range ? 3 : 1)+has_year][0] + 1);
     1912
     1913                                                if (date.getTime() < from_date.getTime())
     1914                                                        return [false, from_date];
     1915                                                else if (date.getTime() >= to_date.getTime())
     1916                                                        return [false, start_of_next_year];
     1917                                                else if (!has_period)
     1918                                                        return [true, to_date];
     1919
     1920                                                var period = tokens[at+5][0];
     1921                                                var nday = Math.floor((date.getTime() - from_date.getTime()) / msec_in_day);
     1922                                                var in_period = nday % period;
     1923
     1924                                                if (in_period == 0)
     1925                                                        return [true, new Date(date.getFullYear(), date.getMonth(), date.getDate() + 1)];
     1926                                                else
     1927                                                        return [false, new Date(date.getFullYear(), date.getMonth(), date.getDate() + period - in_period)];
     1928                                        }}(tokens, at, is_range, has_period, has_year[0]));
     1929
     1930                                        at += 2 + has_year[0] + (is_range ? 2 : 0) + (has_period ? 2 : 0);
     1931
     1932                                } else if (has_event[0]) {
     1933
     1934                                        selectors.monthday.push(function(tokens, at, nblock, has_event, has_year, add_days) { return function(date) {
     1935
     1936                                                // console.log('enter selector with date: ' + date);
     1937                                                var movableDays = getMovableEventsForYear((has_year ? tokens[at][0] : date.getFullYear()));
     1938                                                var event_date = movableDays[tokens[at+has_year][0]];
     1939                                                if (!event_date)
     1940                                                        throw 'Movable day ' + tokens[at+has_year][0] + ' can not not be calculated.'
     1941                                                                + ' Please add the formula how to calculate it.';
     1942
     1943                                                if (add_days[1]) {
     1944                                                        event_date.setDate(event_date.getDate() + add_days[0]);
     1945                                                        if (date.getFullYear() != event_date.getFullYear())
     1946                                                                throw formatWarnErrorMessage(nblock, at+has_year+add_days[1]*3, 'The movable day ' + tokens[at+has_year][0] + ' plus '
     1947                                                                        + add_days[0]
     1948                                                                        + ' days is not in the year of the movable day anymore. Currently not supported.');
     1949                                                }
     1950
     1951                                                if (date.getTime() < event_date.getTime())
     1952                                                        return [false, event_date];
     1953                                                // else if (date.getTime() < event_date.getTime() + msec_in_day) // does not work because of daylight saving times
     1954                                                else if (event_date.getMonth() * 100 + event_date.getDate() == date.getMonth() * 100 + date.getDate())
     1955                                                        return [true, new Date(date.getFullYear(), date.getMonth(), date.getDate() + 1)];
     1956                                                else
     1957                                                        return [false, new Date(date.getFullYear() + 1, 0, 1)];
     1958
     1959                                        }}(tokens, at, nblock, has_event[0], has_year[0], has_calc[0]));
     1960
     1961                                        at += has_year[0] + has_event[0] + (typeof has_calc[0][1] != 'undefined' && has_calc[0][1] ? 3 : 0);
     1962                                } else {
     1963                                        // throw 'Unexpected token in monthday range: "' + tokens[at] + '"';
     1964                                        return at;
     1965                                }
     1966
     1967                                if (!matchTokens(tokens, at, ','))
     1968                                        break;
     1969                        }
     1970
     1971                        return at;
     1972                }
     1973
     1974                //======================================================================
     1975                // Main selector traversal function
     1976                //======================================================================
     1977                this.getStatePair = function(date) {
     1978                        var resultstate = false;
     1979                        var changedate;
     1980                        var unknown = false;
     1981                        var comment = undefined;
     1982                        var match_block;
     1983
     1984                        var date_matching_blocks = [];
     1985
     1986                        for (var nblock = 0; nblock < blocks.length; nblock++) {
     1987                                var matching_date_block = true;
     1988                                // console.log(nblock, 'length',  blocks[nblock].date.length);
     1989
     1990                                // Try each date selector type
     1991                                for (var ndateselector = 0; ndateselector < blocks[nblock].date.length; ndateselector++) {
     1992                                        var dateselectors = blocks[nblock].date[ndateselector];
     1993                                        // console.log(nblock, ndateselector);
     1994
     1995                                        var has_matching_selector = false;
     1996                                        for (var datesel = 0; datesel < dateselectors.length; datesel++) {
     1997                                                var res = dateselectors[datesel](date);
     1998                                                if (res[0]) {
     1999                                                        has_matching_selector = true;
     2000
     2001                                                        if (typeof res[2] == 'string') { // holiday name
     2002                                                                comment = [ res[2] ];
     2003                                                        }
     2004
     2005                                                }
     2006                                                if (typeof changedate === 'undefined' || (typeof res[1] !== 'undefined' && res[1].getTime() < changedate.getTime()))
     2007                                                        changedate = res[1];
     2008                                        }
     2009
     2010                                        if (!has_matching_selector) {
     2011                                                matching_date_block = false;
     2012                                                // We can ignore other date selectors, as the state won't change
     2013                                                // anyway until THIS selector matches (due to conjunction of date
     2014                                                // selectors of different types).
     2015                                                // This is also an optimization, if widest date selector types
     2016                                                // are checked first.
     2017                                                break;
     2018                                        }
     2019
     2020                                }
     2021
     2022                                if (matching_date_block) {
     2023                                        // The following lines implement date overwriting logic (e.g. for
     2024                                        // "Mo-Fr 10:00-20:00; We 10:00-16:00", We block overrides Mo-Fr block.
     2025                                        //
     2026                                        // 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:
     2027                                        // 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.
     2028                                        if (blocks[nblock].date.length > 0 && (blocks[nblock].meaning || blocks[nblock].unknown)
     2029                                                        && !blocks[nblock].wrapped && !blocks[nblock].additional && !blocks[nblock].fallback) {
     2030                                                // var old_date_matching_blocks = date_matching_blocks;
     2031                                                date_matching_blocks = [];
     2032                                                // for (var nblock = 0; nblock < old_date_matching_blocks.length; nblock++) {
     2033                                                //      if (!blocks[old_date_matching_blocks[nblock]].wrapped)
     2034                                                //              date_matching_blocks.push(nblock);
     2035                                                // }
     2036                                        }
     2037                                        date_matching_blocks.push(nblock);
     2038                                }
     2039                        }
     2040
     2041                        block:
     2042                        for (var nblock = 0; nblock < date_matching_blocks.length; nblock++) {
     2043                                var block = date_matching_blocks[nblock];
     2044
     2045                                // console.log('Processing block ' + block + ':\t' + blocks[block].comment + '    with date', date,
     2046                                //      'and', blocks[block].time.length, 'time selectors');
     2047
     2048                                // there is no time specified, state applies to the whole day
     2049                                if (blocks[block].time.length == 0) {
     2050                                        // console.log('there is no time', date);
     2051                                        if (!blocks[block].fallback || (blocks[block].fallback && !(resultstate || unknown))) {
     2052                                                resultstate = blocks[block].meaning;
     2053                                                unknown     = blocks[block].unknown;
     2054                                                match_block = block;
     2055
     2056                                                if (typeof blocks[block].comment != 'undefined')
     2057                                                        comment     = blocks[block].comment;
     2058                                                else if (typeof comment == 'object') // holiday name
     2059                                                        comment = comment[0];
     2060
     2061                                                if (blocks[block].fallback)
     2062                                                        break block; // fallback block matched, no need for checking the rest
     2063                                        }
     2064                                }
     2065
     2066                                for (var timesel = 0; timesel < blocks[block].time.length; timesel++) {
     2067                                        var res = blocks[block].time[timesel](date);
     2068
     2069                                        // console.log('res:', res);
     2070                                        if (res[0]) {
     2071                                                if (!blocks[block].fallback || (blocks[block].fallback && !(resultstate || unknown))) {
     2072                                                        resultstate = blocks[block].meaning;
     2073                                                        unknown     = blocks[block].unknown;
     2074                                                        match_block = block;
     2075
     2076                                                        if (typeof blocks[block].comment != 'undefined') // only use comment if one is specified
     2077                                                                comment     = blocks[block].comment;
     2078                                                        else if (typeof comment == 'object') // holiday name
     2079                                                                comment = comment[0];
     2080                                                        else if (comment === 'Specified as open end. Closing time was guessed.')
     2081                                                                comment = blocks[block].comment;
     2082
     2083                                                        // open end
     2084                                                        if (typeof res[2] == 'boolean' && res[2] && (resultstate || unknown)) {
     2085                                                                if (typeof comment == 'undefined')
     2086                                                                        comment = 'Specified as open end. Closing time was guessed.';
     2087                                                                resultstate = false;
     2088                                                                unknown     = true;
     2089                                                        }
     2090
     2091                                                        if (blocks[block].fallback) {
     2092                                                                if (typeof changedate === 'undefined' || (typeof res[1] !== 'undefined' && res[1] < changedate))
     2093                                                                        changedate = res[1];
     2094
     2095                                                                break block; // fallback block matched, no need for checking the rest
     2096                                                        }
     2097                                                }
     2098                                        }
     2099                                        if (typeof changedate === 'undefined' || (typeof res[1] !== 'undefined' && res[1] < changedate))
     2100                                                changedate = res[1];
     2101                                }
     2102                        }
     2103
     2104                        // console.log('changedate', changedate, resultstate, comment, match_block);
     2105                        return [ resultstate, changedate, unknown, comment, match_block ];
     2106                }
     2107
     2108                function formatWarnErrorMessage(nblock, at, message) {
     2109                        var pos = 0;
     2110                        if (nblock == -1) { // usage of block index not required because we do have access to value.length
     2111                                pos = value.length - at;
     2112                        } else { // issue accrued at a later time
     2113                                if (typeof tokens[nblock][0][at] == 'undefined') {
     2114                                        pos = value.length;
     2115                                } else {
     2116                                        pos = value.length;
     2117                                        if (typeof tokens[nblock][0][at+1] != 'undefined')
     2118                                                pos -= tokens[nblock][0][at+1][2];
     2119                                        else if (typeof tokens[nblock][2] != 'undefined')
     2120                                                pos -= tokens[nblock][2];
     2121                                }
     2122                        }
     2123                        return value.substring(0, pos) + ' <--- (' + message + ')';
     2124                }
     2125
     2126                function prettifySelector(tokens, at, last_at, conf, last_subparser) {
     2127                        var value = '';
     2128                        var start_at = at;
     2129                        while (at < last_at) {
     2130                                if (matchTokens(tokens, at, 'weekday')) {
     2131                                        value += ['Su','Mo','Tu','We','Th','Fr','Sa'][tokens[at][0]];
     2132                                } else if (at - start_at > 0 && last_subparser == 'time' && matchTokens(tokens, at-1, 'timesep')
     2133                                                && matchTokens(tokens, at, 'number')) {
     2134                                        value += (tokens[at][0] < 10 ? '0' : '') + tokens[at][0].toString();
     2135                                } else if (last_subparser == 'time' && conf.leading_zero_hour && at != tokens.length
     2136                                                && matchTokens(tokens, at+1, 'timesep')) {
     2137                                        value += (tokens[at][0] < 10 ? (tokens[at][0] == 0 && conf.one_zero_if_hour_zero ? '' : '0') : '') + tokens[at][0].toString();
     2138                                } else if (matchTokens(tokens, at, 'weekday')) {
     2139                                        value += ['Su','Mo','Tu','We','Th','Fr','Sa'][tokens[at][0]];
     2140                                } else if (matchTokens(tokens, at, 'comment')) {
     2141                                        value += '"' + tokens[at][0].toString() + '"';
     2142                                } else if (matchTokens(tokens, at, 'closed')) {
     2143                                        value += (conf.leave_off_closed ? tokens[at][0] : conf.keyword_for_off_closed);
     2144                                } else if (at - start_at > 0 && matchTokens(tokens, at, 'number')
     2145                                                && (matchTokens(tokens, at-1, 'month')
     2146                                                ||  matchTokens(tokens, at-1, 'week')
     2147                                                )) {
     2148                                        value += ' ' + tokens[at][0];
     2149                                } else if (at - start_at > 0 && matchTokens(tokens, at, 'month')
     2150                                                && matchTokens(tokens, at-1, 'year')) {
     2151                                        value += ' ' + months[[tokens[at][0]]];
     2152                                } else if (at - start_at > 0 && matchTokens(tokens, at, 'event')
     2153                                                && matchTokens(tokens, at-1, 'year')) {
     2154                                        value += ' ' + tokens[at][0];
     2155                                } else if (matchTokens(tokens, at, 'month')) {
     2156                                        value += months[[tokens[at][0]]];
     2157                                } else if (at + 2 < last_at
     2158                                                && (matchTokens(tokens, at, '-') || matchTokens(tokens, at, '+'))
     2159                                                && matchTokens(tokens, at+1, 'number', 'calcday')) {
     2160                                        value += ' ' + tokens[at][0] + tokens[at+1][0] + ' day' + (Math.abs(tokens[at+1][0]) == 1 ? '' : 's');
     2161                                        at += 2;
     2162                                } else {
     2163                                        // if (matchTokens(tokens, at, 'open') || matchTokens(tokens, at, 'unknown'))
     2164                                        //      value += ' ';
     2165
     2166                                        value += tokens[at][0].toString();
     2167                                }
     2168                                at++;
     2169                        }
     2170                        return value + ' ';
     2171                }
     2172
     2173                //======================================================================
     2174                // Public interface
     2175                // All functions below are considered public.
     2176                //======================================================================
     2177
     2178                //======================================================================
     2179                // Iterator interface
     2180                //======================================================================
     2181                this.getIterator = function(date) {
     2182                        return new function(oh) {
     2183                                if (typeof date === 'undefined')
     2184                                        date = new Date();
     2185
     2186                                var prevstate = [ undefined, date, undefined, undefined, undefined ];
     2187                                var state = oh.getStatePair(date);
     2188
     2189                                this.setDate = function(date) {
     2190                                        if (typeof date != 'object')
     2191                                                throw 'Date as parameter needed.';
     2192
     2193                                        prevstate = [ undefined, date, undefined, undefined, undefined ];
     2194                                        state     = oh.getStatePair(date);
     2195                                }
     2196
     2197                                this.getDate = function() {
     2198                                        return prevstate[1];
     2199                                }
     2200
     2201                                this.getState = function() {
     2202                                        return state[0];
     2203                                }
     2204
     2205                                this.getUnknown = function() {
     2206                                        return state[2];
     2207                                }
     2208
     2209                                this.getStateString = function(past) {
     2210                                        return (state[0] ? 'open' : (state[2] ? 'unknown' : (past ? 'closed' : 'close')));
     2211                                }
     2212
     2213                                this.getComment = function() {
     2214                                        return state[3];
     2215                                }
     2216
     2217                                this.getMatchingRule = function(user_conf) {
     2218                                        if (typeof state[4] == 'undefined')
     2219                                                return undefined;
     2220
     2221                                        if (typeof user_conf != 'object')
     2222                                                var user_conf = {};
     2223                                        for (key in default_prettify_conf) {
     2224                                                if (typeof user_conf[key] != 'undefined')
     2225                                                        user_conf[key] = default_prettify_conf[key];
     2226                                        }
     2227
     2228                                        prettified_value = '';
     2229                                        var selectors = { // Not really needed. This whole thing is only necessary because of the token used for additional blocks.
     2230                                                time: [], weekday: [], holiday: [], week: [], month: [], monthday: [], year: [], wraptime: [],
     2231
     2232                                                fallback: false, // does not matter
     2233                                                additional: false,
     2234                                                meaning: true,
     2235                                                unknown: false,
     2236                                                comment: undefined,
     2237                                        };
     2238
     2239                                        parseGroup(tokens[state[4]][0], 0, selectors, user_conf);
     2240
     2241                                        return prettified_value;
     2242                                }
     2243
     2244                                this.advance = function(datelimit) {
     2245                                        if (typeof datelimit === 'undefined')
     2246                                                datelimit = new Date(prevstate[1].getTime() + msec_in_day * 366 * 5);
     2247                                        else if (datelimit.getTime() <= prevstate[1].getTime())
     2248                                                return false; // The limit for advance needs to be after the current time.
     2249
     2250                                        do {
     2251                                                // open range, we won't be able to advance
     2252                                                if (typeof state[1] === 'undefined')
     2253                                                        return false;
     2254
     2255                                                // console.log('\n' + 'previous check time:', prevstate[1]
     2256                                                //      + ', current check time:',
     2257                                                //      // (state[1].getHours() < 10 ? '0' : '') + state[1].getHours() +
     2258                                                //      // ':'+(state[1].getMinutes() < 10 ? '0' : '')+ state[1].getMinutes(), state[1].getDate(),
     2259                                                //      state[1],
     2260                                                //      (state[0] ? 'open' : (state[2] ? 'unknown' : 'closed')) + ', comment:', state[3]);
     2261
     2262                                                // We're going backwards or staying at place.
     2263                                                // This always indicates coding error in a selector code.
     2264                                                if (state[1].getTime() <= prevstate[1].getTime())
     2265                                                        throw 'Fatal: infinite loop in nextChange';
     2266
     2267                                                // don't advance beyond limits (same as open range)
     2268                                                if (state[1].getTime() >= datelimit.getTime())
     2269                                                        return false;
     2270
     2271                                                // do advance
     2272                                                prevstate = state;
     2273                                                state = oh.getStatePair(prevstate[1]);
     2274                                        } while (state[0] === prevstate[0] && state[2] === prevstate[2] && state[3] === prevstate[3]);
     2275                                        return true;
     2276                                }
     2277                        }(this);
     2278                }
     2279
     2280                // get parse warnings
     2281                // returns an empty string if there are no warnings
     2282                this.getWarnings = function() {
     2283                        var it = this.getIterator();
     2284                        return getWarnings(it).join('\n');;
     2285                }
     2286
     2287                // get a nicely formated value.
     2288                this.prettifyValue = function(user_conf) {
     2289                        if (typeof user_conf != 'object')
     2290                                var user_conf = {};
     2291
     2292                        for (key in default_prettify_conf) {
     2293                                if (typeof user_conf[key] == 'undefined')
     2294                                        user_conf[key] = default_prettify_conf[key];
     2295                        }
     2296
     2297                        prettified_value = '';
     2298                        for (var nblock = 0; nblock < tokens.length; nblock++) {
     2299                                if (tokens[nblock][0].length == 0) continue;
     2300                                // Block does contain nothing useful e.g. second block of '10:00-12:00;' (empty) which needs to be handled.
     2301
     2302                                if (nblock != 0)
     2303                                        prettified_value += (tokens[nblock][1]
     2304                                                ?  user_conf.block_sep_string + '|| '
     2305                                                : (user_conf.print_semicolon ? ';' : '') + user_conf.block_sep_string);
     2306
     2307                                var continue_at = 0;
     2308                                do {
     2309                                        if (continue_at == tokens[nblock][0].length) break;
     2310                                        // Block does contain nothing useful e.g. second block of '10:00-12:00,' (empty) which needs to be handled.
     2311
     2312                                        var selectors = { // Not really needed. This whole thing is only necessary because of the token used for additional blocks.
     2313                                                time: [], weekday: [], holiday: [], week: [], month: [], monthday: [], year: [], wraptime: [],
     2314
     2315                                                fallback: tokens[nblock][1],
     2316                                                additional: continue_at ? true : false,
     2317                                                meaning: true,
     2318                                                unknown: false,
     2319                                                comment: undefined,
     2320                                        };
     2321
     2322                                        continue_at = parseGroup(tokens[nblock][0], continue_at, selectors, user_conf);
     2323
     2324                                        if (typeof continue_at == 'object') {
     2325                                                continue_at = continue_at[0];
     2326                                                prettified_value += user_conf.block_sep_string;
     2327                                        } else {
     2328                                                continue_at = 0;
     2329                                        }
     2330
     2331                                } while (continue_at)
     2332                        }
     2333
     2334                        return prettified_value;
     2335                }
     2336
     2337                // check whether facility is `open' on the given date (or now)
     2338                this.getState = function(date) {
     2339                        var it = this.getIterator(date);
     2340                        return it.getState();
     2341                }
     2342
     2343                // If the state of a amenity is conditional. Conditions can be expressed in comments.
     2344                // True will only be returned if the state is false as the getState only
     2345                // returns true if the amenity is really open. So you may want to check
     2346                // the resold of getUnknown if getState returned false.
     2347                this.getUnknown = function(date) {
     2348                        var it = this.getIterator(date);
     2349                        return it.getUnknown();
     2350                }
     2351
     2352                // Return state string. Either 'open', 'unknown' or 'closed'.
     2353                this.getStateString = function(date, past) {
     2354                        var it = this.getIterator(date);
     2355                        return it.getStateString(past);
     2356                }
     2357
     2358                // Returns the comment.
     2359                // Most often this will be an empty string as comments are not used that
     2360                // often in OSM yet.
     2361                this.getComment = function(date) {
     2362                        var it = this.getIterator(date);
     2363                        return it.getComment();
     2364                }
     2365
     2366                this.getMatchingRule = function(date) {
     2367                        var it = this.getIterator(date);
     2368                        return it.getMatchingRule();
     2369                }
     2370
     2371                // returns time of next status change
     2372                this.getNextChange = function(date, maxdate) {
     2373                        var it = this.getIterator(date);
     2374                        if (!it.advance(maxdate))
     2375                                return undefined;
     2376                        return it.getDate();
     2377                }
     2378
     2379                // return array of open intervals between two dates
     2380                this.getOpenIntervals = function(from, to) {
     2381                        var res = [];
     2382
     2383                        var it = this.getIterator(from);
     2384
     2385                        if (it.getState() || it.getUnknown())
     2386                                res.push([from, undefined, it.getUnknown(), it.getComment()]);
     2387
     2388                        while (it.advance(to)) {
     2389                                if (it.getState() || it.getUnknown()) {
     2390                                        if (res.length != 0 && typeof res[res.length - 1][1] == 'undefined') {
     2391                                                // last state was also open or unknown
     2392                                                res[res.length - 1][1] = it.getDate();
     2393                                        }
     2394                                        res.push([it.getDate(), undefined, it.getUnknown(), it.getComment()]);
     2395                                } else {
     2396                                        if (res.length != 0 && typeof res[res.length - 1][1] == 'undefined') {
     2397                                                // only use the first time as closing/change time and ignore closing times which might follow
     2398                                                res[res.length - 1][1] = it.getDate();
     2399                                        }
     2400                                }
     2401                        }
     2402
     2403                        if (res.length > 0 && typeof res[res.length - 1][1] === 'undefined')
     2404                                res[res.length - 1][1] = to;
     2405
     2406                        return res;
     2407                }
     2408
     2409                // return total number of milliseconds a facility is open within a given date range
     2410                this.getOpenDuration = function(from, to) {
     2411                // console.log('-----------');
     2412
     2413                        var open    = 0;
     2414                        var unknown = 0;
     2415
     2416                        var it = this.getIterator(from);
     2417                        var prevdate    = (it.getState() || it.getUnknown()) ? from : undefined;
     2418                        var prevstate   = it.getState();
     2419                        var prevunknown = it.getUnknown();
     2420
     2421                        while (it.advance(to)) {
     2422                                if (it.getState() || it.getUnknown()) {
     2423
     2424                                        if (typeof prevdate !== 'undefined') {
     2425                                                // last state was also open or unknown
     2426                                                if (prevunknown) //
     2427                                                        unknown += it.getDate().getTime() - prevdate.getTime();
     2428                                                else if (prevstate)
     2429                                                        open    += it.getDate().getTime() - prevdate.getTime();
     2430                                        }
     2431
     2432                                        prevdate    = it.getDate();
     2433                                        prevstate   = it.getState();
     2434                                        prevunknown = it.getUnknown();
     2435                                        // console.log('if', prevdate, open / (1000 * 60 * 60), unknown / (1000 * 60 * 60));
     2436                                } else {
     2437                                        // console.log('else', prevdate);
     2438                                        if (typeof prevdate !== 'undefined') {
     2439                                                if (prevunknown)
     2440                                                        unknown += it.getDate().getTime() - prevdate.getTime();
     2441                                                else
     2442                                                        open    += it.getDate().getTime() - prevdate.getTime();
     2443                                                prevdate = undefined;
     2444                                        }
     2445                                }
     2446                        }
     2447
     2448                        if (typeof prevdate !== 'undefined') {
     2449                                if (prevunknown)
     2450                                        unknown += to.getTime() - prevdate.getTime();
     2451                                else
     2452                                        open    += to.getTime() - prevdate.getTime();
     2453                        }
     2454
     2455                        return [ open, unknown ];
     2456                }
     2457
     2458                this.isWeekStable = function() {
     2459                        return week_stable;
     2460                }
     2461        }
     2462}));
  • src/org/openstreetmap/josm/data/validation/OsmValidator.java

    diff --git a/src/org/openstreetmap/josm/data/validation/OsmValidator.java b/src/org/openstreetmap/josm/data/validation/OsmValidator.java
    index c1a7a76..9dc1192 100644
    a b import org.openstreetmap.josm.data.validation.tests.MultipolygonTest;  
    3838import org.openstreetmap.josm.data.validation.tests.NameMismatch;
    3939import org.openstreetmap.josm.data.validation.tests.NodesDuplicatingWayTags;
    4040import org.openstreetmap.josm.data.validation.tests.NodesWithSameName;
     41import org.openstreetmap.josm.data.validation.tests.OpeningHourTest;
    4142import org.openstreetmap.josm.data.validation.tests.OverlappingAreas;
    4243import org.openstreetmap.josm.data.validation.tests.OverlappingWays;
    4344import org.openstreetmap.josm.data.validation.tests.PowerLines;
    public class OsmValidator implements LayerChangeListener {  
    113114        Addresses.class, // ID 2601 .. 2699
    114115        Highways.class, // ID 2701 .. 2799
    115116        BarriersEntrances.class, // ID 2801 .. 2899
     117        OpeningHourTest.class // 2901 .. 2999
    116118    };
    117119
    118120    /**
  • new file src/org/openstreetmap/josm/data/validation/tests/OpeningHourTest.java

    diff --git a/src/org/openstreetmap/josm/data/validation/tests/OpeningHourTest.java b/src/org/openstreetmap/josm/data/validation/tests/OpeningHourTest.java
    new file mode 100644
    index 0000000..9112745
    - +  
     1package org.openstreetmap.josm.data.validation.tests;
     2
     3import org.openstreetmap.josm.data.osm.Node;
     4import org.openstreetmap.josm.data.osm.OsmPrimitive;
     5import org.openstreetmap.josm.data.osm.Relation;
     6import org.openstreetmap.josm.data.osm.Way;
     7import org.openstreetmap.josm.data.validation.Severity;
     8import org.openstreetmap.josm.data.validation.Test;
     9import org.openstreetmap.josm.data.validation.TestError;
     10import org.openstreetmap.josm.io.MirroredInputStream;
     11import sun.org.mozilla.javascript.NativeArray;
     12
     13import javax.script.Invocable;
     14import javax.script.ScriptEngine;
     15import javax.script.ScriptEngineManager;
     16import javax.script.ScriptException;
     17import java.io.InputStreamReader;
     18import java.text.SimpleDateFormat;
     19import java.util.ArrayList;
     20import java.util.Arrays;
     21import java.util.Collections;
     22import java.util.Date;
     23import java.util.List;
     24import java.util.Map;
     25
     26import static org.openstreetmap.josm.tools.I18n.tr;
     27
     28/**
     29 * Tests the correct usage of the opening hour syntax of the tags
     30 * {@code opening_hours}, {@code collection_times}, {@code service_times} according to
     31 * <a href="https://github.com/ypid/opening_hours.js">opening_hours.js</a>.
     32 *
     33 * @author frsantos
     34 */
     35public class OpeningHourTest extends Test {
     36
     37    public static final ScriptEngine ENGINE = new ScriptEngineManager().getEngineByName("rhino");
     38
     39    /**
     40     * Constructs a new {@code OpeningHourTest}.
     41     */
     42    public OpeningHourTest() {
     43        super(tr("Opening hours syntax"),
     44                tr("This plugin checks for correct usage of opening hours syntax."));
     45    }
     46
     47    @Override
     48    public void initialize() throws Exception {
     49        super.initialize();
     50        ENGINE.eval(new InputStreamReader(new MirroredInputStream("resource://data/opening_hours.js")));
     51        ENGINE.eval("var oh = function (x, y) {return new opening_hours(x, y);};");
     52    }
     53
     54    @SuppressWarnings("unchecked")
     55    protected Object parse(String value) throws ScriptException, NoSuchMethodException {
     56        return ((Invocable) ENGINE).invokeFunction("oh", value);
     57    }
     58
     59    protected List<Object> getList(Object obj) {
     60        if (obj == null || "".equals(obj)) {
     61            return Arrays.asList();
     62        } else if (obj instanceof String) {
     63            return Arrays.<Object>asList(((String) obj).split("\\n"));
     64        } else {
     65            throw new IllegalArgumentException("Not expecting class " + obj.getClass());
     66        }
     67    }
     68
     69    /**
     70     * Checks for a correct usage of the opening hour syntax of the {@code value} given according to
     71     * <a href="https://github.com/ypid/opening_hours.js">opening_hours.js</a> and returns a list containing
     72     * validation errors or an empty list. Null values result in an empty list.
     73     * @param value the opening hour value to be checked.
     74     * @return a list of {@link TestError} or an empty list
     75     */
     76    public List<TestError> checkOpeningHourSyntax(final String value) {
     77        if (value == null || value.trim().isEmpty()) {
     78            return Collections.emptyList();
     79        }
     80        try {
     81            final Object r = parse(value);
     82            final List<TestError> errors = new ArrayList<TestError>();
     83            for (final Object i : getList(((Invocable) ENGINE).invokeMethod(r, "getWarnings"))) {
     84                errors.add(new TestError(this, Severity.WARNING, i.toString(), 2901, Collections.<OsmPrimitive>emptyList()));
     85            }
     86            return errors;
     87        } catch (ScriptException ex) {
     88            final String message = ex.getMessage()
     89                    .replace("sun.org.mozilla.javascript.JavaScriptException: ", "")
     90                    .replaceAll("\\(<Unknown source.*", "")
     91                    .trim();
     92            return Arrays.asList(new TestError(this, Severity.ERROR, message, 2901, Collections.<OsmPrimitive>emptyList()));
     93        } catch (final Exception ex) {
     94            throw new RuntimeException(ex);
     95        }
     96    }
     97
     98    protected void check(final OsmPrimitive p, final String tagValue) {
     99        for (TestError e : checkOpeningHourSyntax(tagValue)) {
     100            e.setPrimitives(Collections.singletonList(p));
     101            errors.add(e);
     102        }
     103    }
     104
     105    protected void check(final OsmPrimitive p) {
     106        check(p, p.get("opening_hours"));
     107        // unsupported, cf. https://github.com/AMDmi3/opening_hours.js/issues/12
     108        //check(p, p.get("collection_times"));
     109        //check(p, p.get("service_times"));
     110    }
     111
     112    @Override
     113    public void visit(final Node n) {
     114        check(n);
     115    }
     116
     117    @Override
     118    public void visit(final Relation r) {
     119        check(r);
     120    }
     121
     122    @Override
     123    public void visit(final Way w) {
     124        check(w);
     125    }
     126}
  • new file test/unit/org/openstreetmap/josm/data/validation/tests/OpeningHourTestTest.java

    diff --git a/test/unit/org/openstreetmap/josm/data/validation/tests/OpeningHourTestTest.java b/test/unit/org/openstreetmap/josm/data/validation/tests/OpeningHourTestTest.java
    new file mode 100644
    index 0000000..41d040d
    - +  
     1package org.openstreetmap.josm.data.validation.tests;
     2
     3import org.junit.Before;
     4import org.junit.Test;
     5import org.openstreetmap.josm.Main;
     6import org.openstreetmap.josm.data.Preferences;
     7import org.openstreetmap.josm.data.validation.Severity;
     8import org.openstreetmap.josm.data.validation.TestError;
     9
     10import java.util.List;
     11
     12import static org.hamcrest.CoreMatchers.is;
     13import static org.junit.Assert.assertThat;
     14
     15public class OpeningHourTestTest {
     16
     17    private static final OpeningHourTest OPENING_HOUR_TEST = new OpeningHourTest();
     18
     19    @Before
     20    public void setUp() throws Exception {
     21        Main.pref = new Preferences();
     22        OPENING_HOUR_TEST.initialize();
     23    }
     24
     25    @Test
     26    public void testCheckOpeningHourSyntax1() throws Exception {
     27        // frequently used tags according to http://taginfo.openstreetmap.org/keys/opening_hours#values
     28        assertThat(OPENING_HOUR_TEST.checkOpeningHourSyntax("24/7").isEmpty(), is(true));
     29        assertThat(OPENING_HOUR_TEST.checkOpeningHourSyntax("Mo-Fr 08:30-20:00").isEmpty(), is(true));
     30        assertThat(OPENING_HOUR_TEST.checkOpeningHourSyntax("09:00-21:00").isEmpty(), is(true));
     31        assertThat(OPENING_HOUR_TEST.checkOpeningHourSyntax("Su-Th sunset-24:00, 04:00-sunrise; Fr-Sa sunset-sunrise").isEmpty(), is(true));
     32    }
     33
     34    @Test
     35    public void testCheckOpeningHourSyntax2() throws Exception {
     36        final List<TestError> errors = OPENING_HOUR_TEST.checkOpeningHourSyntax("Mo-Tue");
     37        assertThat(errors.size(), is(1));
     38        assertThat(errors.get(0).getMessage(), is("Mo-Tue <--- (Please use the abbreviation \"Tu\" for \"tue\".)"));
     39        assertThat(errors.get(0).getSeverity(), is(Severity.WARNING));
     40    }
     41
     42    @Test
     43    public void testCheckOpeningHourSyntax3() throws Exception {
     44        final List<TestError> errors = OPENING_HOUR_TEST.checkOpeningHourSyntax("Sa-Su 10.00-20.00");
     45        assertThat(errors.size(), is(2));
     46        assertThat(errors.get(0).getMessage(), is("Sa-Su 10. <--- (Please use \":\" as hour/minute-separator)"));
     47        assertThat(errors.get(0).getSeverity(), is(Severity.WARNING));
     48        assertThat(errors.get(1).getMessage(), is("Sa-Su 10.00-20. <--- (Please use \":\" as hour/minute-separator)"));
     49        assertThat(errors.get(1).getSeverity(), is(Severity.WARNING));
     50    }
     51
     52    @Test
     53    public void testCheckOpeningHourSyntax4() throws Exception {
     54        assertThat(OPENING_HOUR_TEST.checkOpeningHourSyntax(null).isEmpty(), is(true));
     55        assertThat(OPENING_HOUR_TEST.checkOpeningHourSyntax("").isEmpty(), is(true));
     56        assertThat(OPENING_HOUR_TEST.checkOpeningHourSyntax(" ").isEmpty(), is(true));
     57    }
     58
     59    @Test
     60    public void testCheckOpeningHourSyntax5() throws Exception {
     61        assertThat(OPENING_HOUR_TEST.checkOpeningHourSyntax("badtext").size(), is(1));
     62        assertThat(OPENING_HOUR_TEST.checkOpeningHourSyntax("badtext").get(0).getMessage(),
     63                is("ba <--- (Unexpected token: \"b\" This means that the syntax is not valid at that point.)"));
     64    }
     65}