summaryrefslogtreecommitdiffstats
blob: ee1b553eea94a1cd67d35e4e5acb0dc6bcb2190a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
#ifndef _DATE_TIME_POSIX_TIME_ZONE__
#define _DATE_TIME_POSIX_TIME_ZONE__

/* Copyright (c) 2003-2005 CrystalClear Software, Inc.
 * Subject to the Boost Software License, Version 1.0. (See accompanying
 * file LICENSE_1_0.txt or http://www.boost.org/LICENSE_1_0.txt)
 * Author: Jeff Garland, Bart Garst
 * $Date: 2012-09-22 15:33:33 -0700 (Sat, 22 Sep 2012) $
 */

#include <string>
#include <sstream>
#include <stdexcept>
#include <boost/tokenizer.hpp>
#include <boost/throw_exception.hpp>
#include <boost/date_time/gregorian/gregorian.hpp>
#include <boost/date_time/time_zone_names.hpp>
#include <boost/date_time/time_zone_base.hpp>
#include <boost/date_time/local_time/dst_transition_day_rules.hpp>
#include <boost/date_time/posix_time/posix_time.hpp>
#include <boost/date_time/string_convert.hpp>
#include <boost/date_time/time_parsing.hpp>

namespace boost{
namespace local_time{

  //! simple exception for UTC and Daylight savings start/end offsets
  struct bad_offset : public std::out_of_range
  {
    bad_offset(std::string const& msg = std::string()) :
      std::out_of_range(std::string("Offset out of range: " + msg)) {}
  };
  //! simple exception for UTC daylight savings adjustment
  struct bad_adjustment : public std::out_of_range
  {
    bad_adjustment(std::string const& msg = std::string()) :
      std::out_of_range(std::string("Adjustment out of range: " + msg)) {}
  };

  typedef boost::date_time::dst_adjustment_offsets<boost::posix_time::time_duration> dst_adjustment_offsets;

  //! A time zone class constructed from a POSIX time zone string
  /*! A POSIX time zone string takes the form of:<br>
   * "std offset dst [offset],start[/time],end[/time]" (w/no spaces)
   * 'std' specifies the abbrev of the time zone.<br>
   * 'offset' is the offset from UTC.<br>
   * 'dst' specifies the abbrev of the time zone during daylight savings time.<br>
   * The second offset is how many hours changed during DST. Default=1<br>
   * 'start' and'end' are the dates when DST goes into (and out of) effect.<br>
   * 'offset' takes the form of: [+|-]hh[:mm[:ss]] {h=0-23, m/s=0-59}<br>
   * 'time' and 'offset' take the same form. Time defaults=02:00:00<br>
   * 'start' and 'end' can be one of three forms:<br>
   * Mm.w.d {month=1-12, week=1-5 (5 is always last), day=0-6}<br>
   * Jn {n=1-365 Feb29 is never counted}<br>
   * n  {n=0-365 Feb29 is counted in leap years}<br>
   * Example "PST-5PDT01:00:00,M4.1.0/02:00:00,M10.1.0/02:00:00"
   * <br>
   * Exceptions will be thrown under these conditions:<br>
   * An invalid date spec (see date class)<br>
   * A boost::local_time::bad_offset exception will be thrown for:<br>
   * A DST start or end offset that is negative or more than 24 hours<br>
   * A UTC zone that is greater than +14 or less than -12 hours<br>
   * A boost::local_time::bad_adjustment exception will be thrown for:<br>
   * A DST adjustment that is 24 hours or more (positive or negative)<br>
   *
   * Note that UTC zone offsets can be greater than +12:
   * http://www.worldtimezone.com/utc/utc+1200.html
   */
  template<class CharT>
  class posix_time_zone_base : public date_time::time_zone_base<posix_time::ptime,CharT> {
  public:
    typedef boost::posix_time::time_duration time_duration_type;
    typedef date_time::time_zone_names_base<CharT> time_zone_names;
    typedef date_time::time_zone_base<posix_time::ptime,CharT> base_type;
    typedef typename base_type::string_type string_type;
    typedef CharT char_type;
    typedef typename base_type::stringstream_type stringstream_type;
    typedef boost::char_separator<char_type, std::char_traits<char_type> > char_separator_type;
    typedef boost::tokenizer<char_separator_type,
                             typename string_type::const_iterator,
                             string_type> tokenizer_type;
    typedef typename tokenizer_type::iterator tokenizer_iterator_type;

    //! Construct from a POSIX time zone string
    posix_time_zone_base(const string_type& s) :
      //zone_names_("std_name","std_abbrev","no-dst","no-dst"),
      zone_names_(),
      has_dst_(false),
      base_utc_offset_(posix_time::hours(0)),
      dst_offsets_(posix_time::hours(0),posix_time::hours(0),posix_time::hours(0)),
      dst_calc_rules_()
    {
#ifdef __HP_aCC
      // Work around bug in aC++ compiler: see QXCR1000880488 in the
      // HP bug tracking system
      const char_type sep_chars[2] = {',',0};
#else
      const char_type sep_chars[2] = {','};
#endif
      char_separator_type sep(sep_chars);
      tokenizer_type tokens(s, sep);
      tokenizer_iterator_type it = tokens.begin(), end = tokens.end();
      if (it == end)
        BOOST_THROW_EXCEPTION(std::invalid_argument("Could not parse time zone name"));
      calc_zone(*it++);
      if(has_dst_)
      {
        if (it == end)
          BOOST_THROW_EXCEPTION(std::invalid_argument("Could not parse DST begin time"));
        string_type dst_begin = *it++;

        if (it == end)
          BOOST_THROW_EXCEPTION(std::invalid_argument("Could not parse DST end time"));
        string_type dst_end = *it;
        calc_rules(dst_begin, dst_end);
      }
    }
    virtual ~posix_time_zone_base() {}
    //!String for the zone when not in daylight savings (eg: EST)
    virtual string_type std_zone_abbrev()const
    {
      return zone_names_.std_zone_abbrev();
    }
    //!String for the timezone when in daylight savings (eg: EDT)
    /*! For those time zones that have no DST, an empty string is used */
    virtual string_type dst_zone_abbrev() const
    {
      return zone_names_.dst_zone_abbrev();
    }
    //!String for the zone when not in daylight savings (eg: Eastern Standard Time)
    /*! The full STD name is not extracted from the posix time zone string.
     * Therefore, the STD abbreviation is used in it's place */
    virtual string_type std_zone_name()const
    {
      return zone_names_.std_zone_name();
    }
    //!String for the timezone when in daylight savings (eg: Eastern Daylight Time)
    /*! The full DST name is not extracted from the posix time zone string.
     * Therefore, the STD abbreviation is used in it's place. For time zones
     * that have no DST, an empty string is used */
    virtual string_type dst_zone_name()const
    {
      return zone_names_.dst_zone_name();
    }
    //! True if zone uses daylight savings adjustments otherwise false
    virtual bool has_dst()const
    {
      return has_dst_;
    }
    //! Local time that DST starts -- NADT if has_dst is false
    virtual posix_time::ptime dst_local_start_time(gregorian::greg_year y)const
    {
      gregorian::date d(gregorian::not_a_date_time);
      if(has_dst_)
      {
        d = dst_calc_rules_->start_day(y);
      }
      return posix_time::ptime(d, dst_offsets_.dst_start_offset_);
    }
    //! Local time that DST ends -- NADT if has_dst is false
    virtual posix_time::ptime dst_local_end_time(gregorian::greg_year y)const
    {
      gregorian::date d(gregorian::not_a_date_time);
      if(has_dst_)
      {
        d = dst_calc_rules_->end_day(y);
      }
      return posix_time::ptime(d, dst_offsets_.dst_end_offset_);
    }
    //! Base offset from UTC for zone (eg: -07:30:00)
    virtual time_duration_type base_utc_offset()const
    {
      return base_utc_offset_;
    }
    //! Adjustment forward or back made while DST is in effect
    virtual time_duration_type dst_offset()const
    {
      return dst_offsets_.dst_adjust_;
    }

    //! Returns a POSIX time_zone string for this object
    virtual string_type to_posix_string() const
    {
      // std offset dst [offset],start[/time],end[/time] - w/o spaces
      stringstream_type ss;
      ss.fill('0');
      boost::shared_ptr<dst_calc_rule> no_rules;
      // std
      ss << std_zone_abbrev();
      // offset
      if(base_utc_offset().is_negative()) {
        // inverting the sign guarantees we get two digits
        ss << '-' << std::setw(2) << base_utc_offset().invert_sign().hours();
      }
      else {
        ss << '+' << std::setw(2) << base_utc_offset().hours();
      }
      if(base_utc_offset().minutes() != 0 || base_utc_offset().seconds() != 0) {
        ss << ':' << std::setw(2) << base_utc_offset().minutes();
        if(base_utc_offset().seconds() != 0) {
          ss << ':' << std::setw(2) << base_utc_offset().seconds();
        }
      }
      if(dst_calc_rules_ != no_rules) {
        // dst
        ss << dst_zone_abbrev();
        // dst offset
        if(dst_offset().is_negative()) {
        // inverting the sign guarantees we get two digits
          ss << '-' << std::setw(2) << dst_offset().invert_sign().hours();
        }
        else {
          ss << '+' << std::setw(2) << dst_offset().hours();
        }
        if(dst_offset().minutes() != 0 || dst_offset().seconds() != 0) {
          ss << ':' << std::setw(2) << dst_offset().minutes();
          if(dst_offset().seconds() != 0) {
            ss << ':' << std::setw(2) << dst_offset().seconds();
          }
        }
        // start/time
        ss << ',' << date_time::convert_string_type<char, char_type>(dst_calc_rules_->start_rule_as_string()) << '/'
           << std::setw(2) << dst_offsets_.dst_start_offset_.hours() << ':'
           << std::setw(2) << dst_offsets_.dst_start_offset_.minutes();
        if(dst_offsets_.dst_start_offset_.seconds() != 0) {
          ss << ':' << std::setw(2) << dst_offsets_.dst_start_offset_.seconds();
        }
        // end/time
        ss << ',' << date_time::convert_string_type<char, char_type>(dst_calc_rules_->end_rule_as_string()) << '/'
           << std::setw(2) << dst_offsets_.dst_end_offset_.hours() << ':'
           << std::setw(2) << dst_offsets_.dst_end_offset_.minutes();
        if(dst_offsets_.dst_end_offset_.seconds() != 0) {
          ss << ':' << std::setw(2) << dst_offsets_.dst_end_offset_.seconds();
        }
      }

      return ss.str();
    }
  private:
    time_zone_names zone_names_;
    bool has_dst_;
    time_duration_type base_utc_offset_;
    dst_adjustment_offsets dst_offsets_;
    boost::shared_ptr<dst_calc_rule> dst_calc_rules_;

    /*! Extract time zone abbreviations for STD & DST as well
     * as the offsets for the time shift that occurs and how
     * much of a shift. At this time full time zone names are
     * NOT extracted so the abbreviations are used in their place */
    void calc_zone(const string_type& obj){
      const char_type empty_string[2] = {'\0'};
      stringstream_type ss(empty_string);
      typename string_type::const_pointer sit = obj.c_str(), obj_end = sit + obj.size();
      string_type l_std_zone_abbrev, l_dst_zone_abbrev;

      // get 'std' name/abbrev
      while(std::isalpha(*sit)){
        ss << *sit++;
      }
      l_std_zone_abbrev = ss.str();
      ss.str(empty_string);

      // get UTC offset
      if(sit != obj_end){
        // get duration
        while(sit != obj_end && !std::isalpha(*sit)){
          ss << *sit++;
        }
        base_utc_offset_ = date_time::str_from_delimited_time_duration<time_duration_type,char_type>(ss.str());
        ss.str(empty_string);

        // base offset must be within range of -12 hours to +14 hours
        if(base_utc_offset_ < time_duration_type(-12,0,0) ||
          base_utc_offset_ > time_duration_type(14,0,0))
        {
          boost::throw_exception(bad_offset(posix_time::to_simple_string(base_utc_offset_)));
        }
      }

      // get DST data if given
      if(sit != obj_end){
        has_dst_ = true;

        // get 'dst' name/abbrev
        while(sit != obj_end && std::isalpha(*sit)){
          ss << *sit++;
        }
        l_dst_zone_abbrev = ss.str();
        ss.str(empty_string);

        // get DST offset if given
        if(sit != obj_end){
          // get duration
          while(sit != obj_end && !std::isalpha(*sit)){
            ss << *sit++;
          }
          dst_offsets_.dst_adjust_ = date_time::str_from_delimited_time_duration<time_duration_type,char_type>(ss.str());
          ss.str(empty_string);
        }
        else{ // default DST offset
          dst_offsets_.dst_adjust_ = posix_time::hours(1);
        }

        // adjustment must be within +|- 1 day
        if(dst_offsets_.dst_adjust_ <= time_duration_type(-24,0,0) ||
            dst_offsets_.dst_adjust_ >= time_duration_type(24,0,0))
        {
          boost::throw_exception(bad_adjustment(posix_time::to_simple_string(dst_offsets_.dst_adjust_)));
        }
      }
      // full names not extracted so abbrevs used in their place
      zone_names_ = time_zone_names(l_std_zone_abbrev, l_std_zone_abbrev, l_dst_zone_abbrev, l_dst_zone_abbrev);
    }

    void calc_rules(const string_type& start, const string_type& end){
#ifdef __HP_aCC
      // Work around bug in aC++ compiler: see QXCR1000880488 in the
      // HP bug tracking system
      const char_type sep_chars[2] = {'/',0};
#else
      const char_type sep_chars[2] = {'/'};
#endif
      char_separator_type sep(sep_chars);
      tokenizer_type st_tok(start, sep);
      tokenizer_type et_tok(end, sep);
      tokenizer_iterator_type sit = st_tok.begin();
      tokenizer_iterator_type eit = et_tok.begin();

      // generate date spec
      char_type x = string_type(*sit).at(0);
      if(x == 'M'){
        M_func(*sit, *eit);
      }
      else if(x == 'J'){
        julian_no_leap(*sit, *eit);
      }
      else{
        julian_day(*sit, *eit);
      }

      ++sit;
      ++eit;
      // generate durations
      // starting offset
      if(sit != st_tok.end()){
        dst_offsets_.dst_start_offset_ =  date_time::str_from_delimited_time_duration<time_duration_type,char_type>(*sit);
      }
      else{
        // default
        dst_offsets_.dst_start_offset_ = posix_time::hours(2);
      }
      // start/end offsets must fall on given date
      if(dst_offsets_.dst_start_offset_ < time_duration_type(0,0,0) ||
          dst_offsets_.dst_start_offset_ >= time_duration_type(24,0,0))
      {
        boost::throw_exception(bad_offset(posix_time::to_simple_string(dst_offsets_.dst_start_offset_)));
      }

      // ending offset
      if(eit != et_tok.end()){
        dst_offsets_.dst_end_offset_ =  date_time::str_from_delimited_time_duration<time_duration_type,char_type>(*eit);
      }
      else{
        // default
        dst_offsets_.dst_end_offset_ = posix_time::hours(2);
      }
      // start/end offsets must fall on given date
      if(dst_offsets_.dst_end_offset_ < time_duration_type(0,0,0) ||
        dst_offsets_.dst_end_offset_ >= time_duration_type(24,0,0))
      {
        boost::throw_exception(bad_offset(posix_time::to_simple_string(dst_offsets_.dst_end_offset_)));
      }
    }

    /* Parses out a start/end date spec from a posix time zone string.
     * Date specs come in three possible formats, this function handles
     * the 'M' spec. Ex "M2.2.4" => 2nd month, 2nd week, 4th day .
     */
    void M_func(const string_type& s, const string_type& e){
      typedef gregorian::nth_kday_of_month nkday;
      unsigned short sm=0,sw=0,sd=0,em=0,ew=0,ed=0; // start/end month,week,day
#ifdef __HP_aCC
      // Work around bug in aC++ compiler: see QXCR1000880488 in the
      // HP bug tracking system
      const char_type sep_chars[3] = {'M','.',0};
#else
      const char_type sep_chars[3] = {'M','.'};
#endif
      char_separator_type sep(sep_chars);
      tokenizer_type stok(s, sep), etok(e, sep);

      tokenizer_iterator_type it = stok.begin();
      sm = lexical_cast<unsigned short>(*it++);
      sw = lexical_cast<unsigned short>(*it++);
      sd = lexical_cast<unsigned short>(*it);

      it = etok.begin();
      em = lexical_cast<unsigned short>(*it++);
      ew = lexical_cast<unsigned short>(*it++);
      ed = lexical_cast<unsigned short>(*it);

      dst_calc_rules_ = shared_ptr<dst_calc_rule>(
        new nth_kday_dst_rule(
          nth_last_dst_rule::start_rule(
            static_cast<nkday::week_num>(sw),sd,sm),
          nth_last_dst_rule::start_rule(
            static_cast<nkday::week_num>(ew),ed,em)
          )
      );
    }

    //! Julian day. Feb29 is never counted, even in leap years
    // expects range of 1-365
    void julian_no_leap(const string_type& s, const string_type& e){
      typedef gregorian::gregorian_calendar calendar;
      const unsigned short year = 2001; // Non-leap year
      unsigned short sm=1;
      int sd=0;
      sd = lexical_cast<int>(s.substr(1)); // skip 'J'
      while(sd >= calendar::end_of_month_day(year,sm)){
        sd -= calendar::end_of_month_day(year,sm++);
      }
      unsigned short em=1;
      int ed=0;
      ed = lexical_cast<int>(e.substr(1)); // skip 'J'
      while(ed > calendar::end_of_month_day(year,em)){
        ed -= calendar::end_of_month_day(year,em++);
      }

      dst_calc_rules_ = shared_ptr<dst_calc_rule>(
        new partial_date_dst_rule(
          partial_date_dst_rule::start_rule(
            sd, static_cast<date_time::months_of_year>(sm)),
          partial_date_dst_rule::end_rule(
            ed, static_cast<date_time::months_of_year>(em))
          )
      );
    }

    //! Julian day. Feb29 is always counted, but exception thrown in non-leap years
    // expects range of 0-365
    void julian_day(const string_type& s, const string_type& e){
      int sd=0, ed=0;
      sd = lexical_cast<int>(s);
      ed = lexical_cast<int>(e);
      dst_calc_rules_ = shared_ptr<dst_calc_rule>(
        new partial_date_dst_rule(
          partial_date_dst_rule::start_rule(++sd),// args are 0-365
          partial_date_dst_rule::end_rule(++ed) // pd expects 1-366
          )
      );
    }

    //! helper function used when throwing exceptions
    static std::string td_as_string(const time_duration_type& td)
    {
      std::string s;
#if defined(USE_DATE_TIME_PRE_1_33_FACET_IO)
      s = posix_time::to_simple_string(td);
#else
      std::stringstream ss;
      ss << td;
      s = ss.str();
#endif
      return s;
    }
  };

  typedef posix_time_zone_base<char> posix_time_zone;

} } // namespace boost::local_time


#endif // _DATE_TIME_POSIX_TIME_ZONE__