2 * Copyright 2014 Facebook, Inc.
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
18 * This is a small extension on top of boost::date_time, which handles
19 * conversions between "local time labels" (e.g. "2am, March 10, 2013") and
20 * POSIX UTC timestamps.
22 * Our library hides two sources of complexity:
24 * - Time zones. You can easily use the system time zone (pass a NULL
25 * pointer), or provide a boost::time_zone_ptr (usually created via a
26 * POSIX-like timezone format, or from the boost::date_time timezone DB).
28 * - The one-to-many relationship between time labels and UTC timestamps.
30 * UTC timestamps are effectively monotonic (aside from leap seconds,
31 * which are ignored in POSIX time, and are irrelevant for Cron).
33 * Local time labels, on the other hand, can move forward or backward due
34 * to daylight-savings changes. Thus, when the local clock rewinds due
35 * to DST, some local time labels become ambiguous (is it 1:30am before
36 * or after the DST rewind?). When the local time moves forward due to
37 * DST, some local time labels are skipped (in the US Pacific timezone,
38 * 2:30am never happened on March 10, 2013).
40 * As a consequence, timezoneLocalPTimeToUTCTimestamps() returns a struct
41 * UTCTimestampsForLocalTime that can represent 0, 1, or 2 UTC timestamps.
43 * The ambiguity could be avoided by adding an 'is_dst' flag to the local
44 * time label, but this is not useful for the purposes of Cron (and is
45 * handled adequately in existing libraries).
47 * Going from UTC to a local time label is easy and unambiguous, see
48 * utcPTimeToTimezoneLocalPTime().
50 * CAVEAT: We use boost::posix_time::ptime to represent both UTC timestamps,
51 * *and* local time labels. This is confusing -- it would be better if
52 * local time labels should used a separate type. However, a ptime is very
53 * convenient for the purpose, since it supports all the usual time
54 * operations you might want to do. Patches are welcome.
56 * Our library thus accounts for the following deficiencies of
59 * - boost::date_time has almost no support for the system timezone (the only
60 * related feature is the hacky "c_local_adjustor"). In contrast, our
61 * library interprets a time_zone_ptr value of NULL as referring to the
62 * system timezone, and then does the right thing.
64 * - boost::date_time has a rather annoying exception-based API for
65 * determining whether a local time label is ambiguous, nonexistent, or
66 * unique. Our struct is much more usable.
71 #include <boost/date_time/local_time/local_time_types.hpp>
72 #include <boost/date_time/posix_time/posix_time_types.hpp>
77 #include "folly/Format.h"
79 namespace folly { namespace cron {
82 * How many seconds must be added to UTC in order to get the local time for
83 * the given time point?
85 time_t getUTCOffset(time_t utc_time, boost::local_time::time_zone_ptr tz);
88 * Convert a UTC ptime into a timezone-local ptime.
90 * If tz is a null pointer, use the local timezone.
92 * This is a lossy transformation, since the UTC offset of a timezone
93 * is not constant -- see timezoneLocalPTimeToUTCTimestamps()
94 * for a detailed explanation.
96 boost::posix_time::ptime utcPTimeToTimezoneLocalPTime(
97 boost::posix_time::ptime utc_pt,
98 boost::local_time::time_zone_ptr tz
102 * A local time label can correspond to 0, 1, or 2 UTC timestamps due to
104 * - If the clock went back and your label lands in the repeated interval,
105 * you'll get both timestamps.
106 * - If the clock went forward, and your label landed in the skipped time,
108 * - For all other labels you get exactly one timestamp.
109 * See also timezoneLocalPTimeToUTCTimestamps().
111 struct UTCTimestampsForLocalTime {
112 static const time_t kNotATime = -1; // Might not portable, but easy.
114 UTCTimestampsForLocalTime() : dst_time{kNotATime}, non_dst_time{kNotATime} {}
116 bool isNotATime() const {
117 return dst_time == kNotATime && non_dst_time == kNotATime;
120 bool isAmbiguous() const {
121 return dst_time != kNotATime && non_dst_time != kNotATime;
124 time_t getUnique() const {
126 throw std::runtime_error(folly::format(
127 "Local time maps to both {} and {}", dst_time, non_dst_time
129 } else if (dst_time != kNotATime) {
131 } else if (non_dst_time != kNotATime) {
134 throw std::runtime_error("This local time was skipped due to DST");
139 * For ambiguous local time labels, return the pair of (lesser UTC
140 * timestamp, greater UTC timestamp).
142 * NOTE: This may not be strictly necessary, since DST is probably less
143 * than non-DST in all real timezones, but it's better to be safe than
146 * More specifically, the POSIX timezone specification (IEEE Std 1003.1)
147 * allows DST to be either ahead or behind of the regular timezone, so the
148 * local timezone could shift either way. The docs for
149 * boost::local_time::posix_time_zone (which is not even a POSIX-compliant
150 * implementation, see README) are ambiguous, but can be read as intending
151 * to forbid DST that sets the clock backwards.
153 std::pair<time_t, time_t> getBothInOrder() const {
154 if (!isAmbiguous()) {
155 throw std::runtime_error(folly::format(
156 "{} and {} is not ambiguous", dst_time, non_dst_time
159 if (dst_time < non_dst_time) {
160 return std::make_pair(dst_time, non_dst_time);
162 return std::make_pair(non_dst_time, dst_time);
170 * Convert a timezone-local ptime into UTC epoch timestamp(s).
172 * If tz is a null pointer, use the local timezone.
174 * WARNING 1: When DST sets back the clock, some local times become
175 * ambiguous -- you cannot tell if the timestamp lies before or after the
176 * DST change. For example, "November 3 01:30:00 2013" could be either PST
177 * or PDT, with a difference of one hour.
179 * WARNING 2: You can inadvertently make a local time that does not exist
180 * because a daylight savings change skips that time period.
182 UTCTimestampsForLocalTime timezoneLocalPTimeToUTCTimestamps(
183 boost::posix_time::ptime local_pt,
184 boost::local_time::time_zone_ptr tz