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.
17 #include "folly/experimental/cron/date_time_utils.h"
19 #include <boost/date_time/c_local_time_adjustor.hpp>
22 #include "folly/Format.h"
24 namespace folly { namespace cron {
26 using namespace boost::local_time;
27 using namespace boost::posix_time;
30 // NB: The exceptions below are intended to confirm that the underlying
31 // libraries behave in a sane way. This makes them untestable. I got each
32 // of them to fire by temporarily changing their checks, so if they do fire,
33 // the printouts should be okay. It's fine to change them to CHECKs.
35 time_t getUTCOffset(time_t utc_time, time_zone_ptr tz) {
36 auto utc_pt = from_time_t(utc_time);
37 auto local_pt = utcPTimeToTimezoneLocalPTime(utc_pt, tz);
38 return (local_pt - utc_pt).total_seconds();
41 ptime utcPTimeToTimezoneLocalPTime(ptime utc_pt, time_zone_ptr tz) {
43 return local_date_time{utc_pt, tz}.local_time();
45 return boost::date_time::c_local_adjustor<ptime>::utc_to_local(utc_pt);
49 UTCTimestampsForLocalTime _boostTimezoneLocalPTimeToUTCTimestamps(
53 UTCTimestampsForLocalTime res;
54 auto local_date = local_pt.date();
55 auto local_time = local_pt.time_of_day();
57 auto save_timestamp_if_valid = [&](bool is_dst, time_t *out) {
59 auto local_dt = local_date_time(local_date, local_time, tz, is_dst);
60 // local_date_time() ignores is_dst if the timezone does not have
61 // DST (instead of throwing dst_not_valid). So, we must confirm
62 // that our is_dst guess was correct to avoid storing the same
63 // timestamp in both fields of res (same as problem (b) in the
64 // localtime_r code path).
65 if (local_dt.is_dst() == is_dst) {
66 *out = (local_dt.utc_time() - from_time_t(0)).total_seconds();
68 } catch (dst_not_valid& e) {
69 // Continue, we're trying both values of is_dst
74 save_timestamp_if_valid(true, &res.dst_time);
75 save_timestamp_if_valid(false, &res.non_dst_time);
76 } catch (time_label_invalid& e) {
77 // This local time label was skipped by DST, so res will be empty.
82 UTCTimestampsForLocalTime _systemTimezoneLocalPTimeToUTCTimestamps(
85 UTCTimestampsForLocalTime res;
86 struct tm tm = to_tm(local_pt);
87 auto save_timestamp_if_valid = [tm, &local_pt](int is_dst, time_t *out) {
88 // Try to make a UTC timestamp based on our DST guess and local time.
89 struct tm tmp_tm = tm; // Make a copy since mktime changes the tm
90 tmp_tm.tm_isdst = is_dst;
91 time_t t = mktime(&tmp_tm);
92 if (t == -1) { // Not sure of the error cause or how to handle it.
93 throw runtime_error(folly::format(
94 "{}: mktime error {}", to_simple_string(local_pt), errno
98 // Convert the timestamp to a local time to see if the guess was right.
100 auto out_tm = localtime_r(&t, &new_tm);
101 if (out_tm == nullptr) { // Not sure if such errors can be handled.
102 throw runtime_error(folly::format(
103 "{}: localtime_r error {}", to_simple_string(local_pt), errno
107 // Does the original tm argree with the tm generated from the mktime()
108 // UTC timestamp? (We'll check tm_isdst separately.)
110 // This test never passes when we have a local time label that is
111 // skipped when a DST change moves the clock forward.
113 // A valid local time label always has one or two valid DST values.
114 // When the timezone has not DST, that value is "false".
116 // This test always passes when:
117 // - The DST value is ambiguous (due to the local clock moving back).
118 // - We guessed the uniquely valid DST value.
120 // The test may or may not always pass (implementation-dependent) when
121 // we did not guess a valid DST value.
122 // (a) If it does not pass, we are good, because we also try the other
123 // DST value, which will make the test pass, and then res will have
124 // a unique timestamp.
125 // (b) If it does pass, we're in more trouble, because it means that
126 // the implementation ignored our is_dst value. Then, the timestamp
127 // t is the same as for the other is_dst value. But, we don't want
128 // res to be labeled ambiguous, and we don't want to randomly pick
129 // a DST value to set to kNotATime, because clients may want to
130 // know the real DST value. The solution is the extra test below.
132 tm.tm_sec == new_tm.tm_sec && tm.tm_min == new_tm.tm_min &&
133 tm.tm_hour == new_tm.tm_hour && tm.tm_mday == new_tm.tm_mday &&
134 tm.tm_mon == new_tm.tm_mon && tm.tm_year == new_tm.tm_year &&
135 // To fix problem (b) above, we must assume that localtime_r returns
136 // the correct tm_isdst (if not, it's a system bug anyhow). Then, we
137 // can just check our DST guess against the truth. If our guess was
138 // invalid, we shouldn't store the result, avoiding (b).
139 !( // tm_isdst can also be negative but we'll check that later
140 (new_tm.tm_isdst == 0 && is_dst) || (new_tm.tm_isdst > 0 && !is_dst)
145 return new_tm.tm_isdst < 0; // Used for a sanity-check below.
148 bool neg_isdst1 = save_timestamp_if_valid(1, &res.dst_time);
149 bool neg_isdst2 = save_timestamp_if_valid(0, &res.non_dst_time);
151 // The only legitimate way for localtime_r() to give back a negative
152 // tm_isdst is if the input local time label is ambiguous due to DST.
153 if (neg_isdst1 || neg_isdst2) {
154 if (neg_isdst1 ^ neg_isdst2) { // Can't be ambiguous half the time
155 throw runtime_error(folly::format(
156 "{}: one tm_isdst negative but not both", to_simple_string(local_pt)
159 if (!res.isAmbiguous()) {
160 throw runtime_error(folly::format(
161 "{}: negative tm_isdst but time label is unambiguous",
162 to_simple_string(local_pt)
169 UTCTimestampsForLocalTime timezoneLocalPTimeToUTCTimestamps(
173 UTCTimestampsForLocalTime res;
175 res = _boostTimezoneLocalPTimeToUTCTimestamps(local_pt, tz);
177 res = _systemTimezoneLocalPTimeToUTCTimestamps(local_pt);
179 // Both code paths have fixes to prevent this (see e.g. problem (b) above).
180 if (res.isAmbiguous() && res.dst_time == res.non_dst_time) {
181 throw runtime_error(folly::format(
182 "{}: local time maps to {} regardless of tm_isdst",
183 to_simple_string(local_pt), res.dst_time