1 /*
2  * Copyright (C) 2018 The Android Open Source Project
3  *
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
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
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.
15  */
16 
17 #ifndef LIBTEXTCLASSIFIER_UTILS_CALENDAR_CALENDAR_COMMON_H_
18 #define LIBTEXTCLASSIFIER_UTILS_CALENDAR_CALENDAR_COMMON_H_
19 
20 #include "annotator/types.h"
21 #include "utils/base/integral_types.h"
22 #include "utils/base/logging.h"
23 #include "utils/base/macros.h"
24 
25 namespace libtextclassifier3 {
26 namespace calendar {
27 
28 // Macro to reduce the amount of boilerplate needed for propagating errors.
29 #define TC3_CALENDAR_CHECK(EXPR) \
30   if (!(EXPR)) {                 \
31     return false;                \
32   }
33 
34 // An implementation of CalendarLib that is independent of the particular
35 // calendar implementation used (implementation type is passed as template
36 // argument).
37 template <class TCalendar>
38 class CalendarLibTempl {
39  public:
40   bool InterpretParseData(const DateParseData& parse_data,
41                           int64 reference_time_ms_utc,
42                           const std::string& reference_timezone,
43                           const std::string& reference_locale,
44                           TCalendar* calendar,
45                           DatetimeGranularity* granularity) const;
46 
47   DatetimeGranularity GetGranularity(const DateParseData& data) const;
48 
49  private:
50   // Adjusts the calendar's time instant according to a relative date reference
51   // in the parsed data.
52   bool ApplyRelationField(const DateParseData& parse_data,
53                           TCalendar* calendar) const;
54 
55   // Round the time instant's precision down to the given granularity.
56   bool RoundToGranularity(DatetimeGranularity granularity,
57                           TCalendar* calendar) const;
58 
59   // Adjusts time in steps of relation_type, by distance steps.
60   // For example:
61   // - Adjusting by -2 MONTHS will return the beginning of the 1st
62   //   two weeks ago.
63   // - Adjusting by +4 Wednesdays will return the beginning of the next
64   //   Wednesday at least 4 weeks from now.
65   // If allow_today is true, the same day of the week may be kept
66   // if it already matches the relation type.
67   bool AdjustByRelation(DateParseData::RelationType relation_type, int distance,
68                         bool allow_today, TCalendar* calendar) const;
69 };
70 
71 template <class TCalendar>
InterpretParseData(const DateParseData & parse_data,int64 reference_time_ms_utc,const std::string & reference_timezone,const std::string & reference_locale,TCalendar * calendar,DatetimeGranularity * granularity)72 bool CalendarLibTempl<TCalendar>::InterpretParseData(
73     const DateParseData& parse_data, int64 reference_time_ms_utc,
74     const std::string& reference_timezone, const std::string& reference_locale,
75     TCalendar* calendar, DatetimeGranularity* granularity) const {
76   TC3_CALENDAR_CHECK(calendar->Initialize(reference_timezone, reference_locale,
77                                           reference_time_ms_utc))
78 
79   bool should_round_to_granularity = true;
80   *granularity = GetGranularity(parse_data);
81 
82   // Apply each of the parsed fields in order of increasing granularity.
83   static const int64 kMillisInHour = 1000 * 60 * 60;
84   if (parse_data.field_set_mask & DateParseData::Fields::ZONE_OFFSET_FIELD) {
85     TC3_CALENDAR_CHECK(
86         calendar->SetZoneOffset(parse_data.zone_offset * kMillisInHour))
87   }
88   if (parse_data.field_set_mask & DateParseData::Fields::DST_OFFSET_FIELD) {
89     TC3_CALENDAR_CHECK(
90         calendar->SetDstOffset(parse_data.dst_offset * kMillisInHour))
91   }
92   if (parse_data.field_set_mask & DateParseData::Fields::RELATION_FIELD) {
93     TC3_CALENDAR_CHECK(ApplyRelationField(parse_data, calendar));
94     // Don't round to the granularity for relative expressions that specify the
95     // distance. So that, e.g. "in 2 hours" when it's 8:35:03 will result in
96     // 10:35:03.
97     if (parse_data.field_set_mask &
98         DateParseData::Fields::RELATION_DISTANCE_FIELD) {
99       should_round_to_granularity = false;
100     }
101   } else {
102     // By default, the parsed time is interpreted to be on the reference day.
103     // But a parsed date should have time 0:00:00 unless specified.
104     TC3_CALENDAR_CHECK(calendar->SetHourOfDay(0))
105     TC3_CALENDAR_CHECK(calendar->SetMinute(0))
106     TC3_CALENDAR_CHECK(calendar->SetSecond(0))
107     TC3_CALENDAR_CHECK(calendar->SetMillisecond(0))
108   }
109   if (parse_data.field_set_mask & DateParseData::Fields::YEAR_FIELD) {
110     TC3_CALENDAR_CHECK(calendar->SetYear(parse_data.year))
111   }
112   if (parse_data.field_set_mask & DateParseData::Fields::MONTH_FIELD) {
113     // ICU has months starting at 0, Java and Datetime parser at 1, so we
114     // need to subtract 1.
115     TC3_CALENDAR_CHECK(calendar->SetMonth(parse_data.month - 1))
116   }
117   if (parse_data.field_set_mask & DateParseData::Fields::DAY_FIELD) {
118     TC3_CALENDAR_CHECK(calendar->SetDayOfMonth(parse_data.day_of_month))
119   }
120   if (parse_data.field_set_mask & DateParseData::Fields::HOUR_FIELD) {
121     if (parse_data.field_set_mask & DateParseData::Fields::AMPM_FIELD &&
122         parse_data.ampm == DateParseData::AMPM::PM && parse_data.hour < 12) {
123       TC3_CALENDAR_CHECK(calendar->SetHourOfDay(parse_data.hour + 12))
124     } else if (parse_data.ampm == DateParseData::AMPM::AM &&
125                parse_data.hour == 12) {
126       // Do nothing. 12am == 0.
127     } else {
128       TC3_CALENDAR_CHECK(calendar->SetHourOfDay(parse_data.hour))
129     }
130   }
131   if (parse_data.field_set_mask & DateParseData::Fields::MINUTE_FIELD) {
132     TC3_CALENDAR_CHECK(calendar->SetMinute(parse_data.minute))
133   }
134   if (parse_data.field_set_mask & DateParseData::Fields::SECOND_FIELD) {
135     TC3_CALENDAR_CHECK(calendar->SetSecond(parse_data.second))
136   }
137 
138   if (should_round_to_granularity) {
139     TC3_CALENDAR_CHECK(RoundToGranularity(*granularity, calendar))
140   }
141   return true;
142 }
143 
144 template <class TCalendar>
ApplyRelationField(const DateParseData & parse_data,TCalendar * calendar)145 bool CalendarLibTempl<TCalendar>::ApplyRelationField(
146     const DateParseData& parse_data, TCalendar* calendar) const {
147   constexpr int relation_type_mask = DateParseData::Fields::RELATION_TYPE_FIELD;
148   constexpr int relation_distance_mask =
149       DateParseData::Fields::RELATION_DISTANCE_FIELD;
150   switch (parse_data.relation) {
151     case DateParseData::Relation::UNSPECIFIED:
152       TC3_LOG(ERROR) << "UNSPECIFIED RelationType.";
153       return false;
154     case DateParseData::Relation::NEXT:
155       if (parse_data.field_set_mask & relation_type_mask) {
156         TC3_CALENDAR_CHECK(AdjustByRelation(parse_data.relation_type,
157                                             /*distance=*/1,
158                                             /*allow_today=*/false, calendar));
159       }
160       return true;
161     case DateParseData::Relation::NEXT_OR_SAME:
162       if (parse_data.field_set_mask & relation_type_mask) {
163         TC3_CALENDAR_CHECK(AdjustByRelation(parse_data.relation_type,
164                                             /*distance=*/1,
165                                             /*allow_today=*/true, calendar))
166       }
167       return true;
168     case DateParseData::Relation::LAST:
169       if (parse_data.field_set_mask & relation_type_mask) {
170         TC3_CALENDAR_CHECK(AdjustByRelation(parse_data.relation_type,
171                                             /*distance=*/-1,
172                                             /*allow_today=*/false, calendar))
173       }
174       return true;
175     case DateParseData::Relation::NOW:
176       return true;  // NOOP
177     case DateParseData::Relation::TOMORROW:
178       TC3_CALENDAR_CHECK(calendar->AddDayOfMonth(1));
179       return true;
180     case DateParseData::Relation::YESTERDAY:
181       TC3_CALENDAR_CHECK(calendar->AddDayOfMonth(-1));
182       return true;
183     case DateParseData::Relation::PAST:
184       if ((parse_data.field_set_mask & relation_type_mask) &&
185           (parse_data.field_set_mask & relation_distance_mask)) {
186         TC3_CALENDAR_CHECK(AdjustByRelation(parse_data.relation_type,
187                                             -parse_data.relation_distance,
188                                             /*allow_today=*/false, calendar))
189       }
190       return true;
191     case DateParseData::Relation::FUTURE:
192       if ((parse_data.field_set_mask & relation_type_mask) &&
193           (parse_data.field_set_mask & relation_distance_mask)) {
194         TC3_CALENDAR_CHECK(AdjustByRelation(parse_data.relation_type,
195                                             parse_data.relation_distance,
196                                             /*allow_today=*/false, calendar))
197       }
198       return true;
199   }
200   return false;
201 }
202 
203 template <class TCalendar>
RoundToGranularity(DatetimeGranularity granularity,TCalendar * calendar)204 bool CalendarLibTempl<TCalendar>::RoundToGranularity(
205     DatetimeGranularity granularity, TCalendar* calendar) const {
206   // Force recomputation before doing the rounding.
207   int unused;
208   TC3_CALENDAR_CHECK(calendar->GetDayOfWeek(&unused));
209 
210   switch (granularity) {
211     case GRANULARITY_YEAR:
212       TC3_CALENDAR_CHECK(calendar->SetMonth(0));
213       TC3_FALLTHROUGH_INTENDED;
214     case GRANULARITY_MONTH:
215       TC3_CALENDAR_CHECK(calendar->SetDayOfMonth(1));
216       TC3_FALLTHROUGH_INTENDED;
217     case GRANULARITY_DAY:
218       TC3_CALENDAR_CHECK(calendar->SetHourOfDay(0));
219       TC3_FALLTHROUGH_INTENDED;
220     case GRANULARITY_HOUR:
221       TC3_CALENDAR_CHECK(calendar->SetMinute(0));
222       TC3_FALLTHROUGH_INTENDED;
223     case GRANULARITY_MINUTE:
224       TC3_CALENDAR_CHECK(calendar->SetSecond(0));
225       break;
226 
227     case GRANULARITY_WEEK:
228       int first_day_of_week;
229       TC3_CALENDAR_CHECK(calendar->GetFirstDayOfWeek(&first_day_of_week));
230       TC3_CALENDAR_CHECK(calendar->SetDayOfWeek(first_day_of_week));
231       TC3_CALENDAR_CHECK(calendar->SetHourOfDay(0));
232       TC3_CALENDAR_CHECK(calendar->SetMinute(0));
233       TC3_CALENDAR_CHECK(calendar->SetSecond(0));
234       break;
235 
236     case GRANULARITY_UNKNOWN:
237     case GRANULARITY_SECOND:
238       break;
239   }
240   return true;
241 }
242 
243 template <class TCalendar>
AdjustByRelation(DateParseData::RelationType relation_type,int distance,bool allow_today,TCalendar * calendar)244 bool CalendarLibTempl<TCalendar>::AdjustByRelation(
245     DateParseData::RelationType relation_type, int distance, bool allow_today,
246     TCalendar* calendar) const {
247   const int distance_sign = distance < 0 ? -1 : 1;
248   switch (relation_type) {
249     case DateParseData::RelationType::MONDAY:
250     case DateParseData::RelationType::TUESDAY:
251     case DateParseData::RelationType::WEDNESDAY:
252     case DateParseData::RelationType::THURSDAY:
253     case DateParseData::RelationType::FRIDAY:
254     case DateParseData::RelationType::SATURDAY:
255     case DateParseData::RelationType::SUNDAY:
256       if (!allow_today) {
257         // If we're not including the same day as the reference, skip it.
258         TC3_CALENDAR_CHECK(calendar->AddDayOfMonth(distance_sign))
259       }
260       // Keep walking back until we hit the desired day of the week.
261       while (distance != 0) {
262         int day_of_week;
263         TC3_CALENDAR_CHECK(calendar->GetDayOfWeek(&day_of_week))
264         if (day_of_week == static_cast<int>(relation_type)) {
265           distance += -distance_sign;
266           if (distance == 0) break;
267         }
268         TC3_CALENDAR_CHECK(calendar->AddDayOfMonth(distance_sign))
269       }
270       return true;
271     case DateParseData::RelationType::SECOND:
272       TC3_CALENDAR_CHECK(calendar->AddSecond(distance));
273       return true;
274     case DateParseData::RelationType::MINUTE:
275       TC3_CALENDAR_CHECK(calendar->AddMinute(distance));
276       return true;
277     case DateParseData::RelationType::HOUR:
278       TC3_CALENDAR_CHECK(calendar->AddHourOfDay(distance));
279       return true;
280     case DateParseData::RelationType::DAY:
281       TC3_CALENDAR_CHECK(calendar->AddDayOfMonth(distance));
282       return true;
283     case DateParseData::RelationType::WEEK:
284       TC3_CALENDAR_CHECK(calendar->AddDayOfMonth(7 * distance))
285       TC3_CALENDAR_CHECK(calendar->SetDayOfWeek(1))
286       return true;
287     case DateParseData::RelationType::MONTH:
288       TC3_CALENDAR_CHECK(calendar->AddMonth(distance))
289       TC3_CALENDAR_CHECK(calendar->SetDayOfMonth(1))
290       return true;
291     case DateParseData::RelationType::YEAR:
292       TC3_CALENDAR_CHECK(calendar->AddYear(distance))
293       TC3_CALENDAR_CHECK(calendar->SetDayOfYear(1))
294       return true;
295     default:
296       TC3_LOG(ERROR) << "Unknown relation type: "
297                      << static_cast<int>(relation_type);
298       return false;
299   }
300   return false;
301 }
302 
303 template <class TCalendar>
GetGranularity(const DateParseData & data)304 DatetimeGranularity CalendarLibTempl<TCalendar>::GetGranularity(
305     const DateParseData& data) const {
306   DatetimeGranularity granularity = DatetimeGranularity::GRANULARITY_YEAR;
307   if ((data.field_set_mask & DateParseData::YEAR_FIELD) ||
308       (data.field_set_mask & DateParseData::RELATION_TYPE_FIELD &&
309        (data.relation_type == DateParseData::RelationType::YEAR))) {
310     granularity = DatetimeGranularity::GRANULARITY_YEAR;
311   }
312   if ((data.field_set_mask & DateParseData::MONTH_FIELD) ||
313       (data.field_set_mask & DateParseData::RELATION_TYPE_FIELD &&
314        (data.relation_type == DateParseData::RelationType::MONTH))) {
315     granularity = DatetimeGranularity::GRANULARITY_MONTH;
316   }
317   if (data.field_set_mask & DateParseData::RELATION_TYPE_FIELD &&
318       (data.relation_type == DateParseData::RelationType::WEEK)) {
319     granularity = DatetimeGranularity::GRANULARITY_WEEK;
320   }
321   if (data.field_set_mask & DateParseData::DAY_FIELD ||
322       (data.field_set_mask & DateParseData::RELATION_FIELD &&
323        (data.relation == DateParseData::Relation::NOW ||
324         data.relation == DateParseData::Relation::TOMORROW ||
325         data.relation == DateParseData::Relation::YESTERDAY)) ||
326       (data.field_set_mask & DateParseData::RELATION_TYPE_FIELD &&
327        (data.relation_type == DateParseData::RelationType::MONDAY ||
328         data.relation_type == DateParseData::RelationType::TUESDAY ||
329         data.relation_type == DateParseData::RelationType::WEDNESDAY ||
330         data.relation_type == DateParseData::RelationType::THURSDAY ||
331         data.relation_type == DateParseData::RelationType::FRIDAY ||
332         data.relation_type == DateParseData::RelationType::SATURDAY ||
333         data.relation_type == DateParseData::RelationType::SUNDAY ||
334         data.relation_type == DateParseData::RelationType::DAY))) {
335     granularity = DatetimeGranularity::GRANULARITY_DAY;
336   }
337   if (data.field_set_mask & DateParseData::HOUR_FIELD ||
338       (data.field_set_mask & DateParseData::RELATION_TYPE_FIELD &&
339        (data.relation_type == DateParseData::RelationType::HOUR))) {
340     granularity = DatetimeGranularity::GRANULARITY_HOUR;
341   }
342   if (data.field_set_mask & DateParseData::MINUTE_FIELD ||
343       (data.field_set_mask & DateParseData::RELATION_TYPE_FIELD &&
344        data.relation_type == DateParseData::RelationType::MINUTE)) {
345     granularity = DatetimeGranularity::GRANULARITY_MINUTE;
346   }
347   if (data.field_set_mask & DateParseData::SECOND_FIELD ||
348       (data.field_set_mask & DateParseData::RELATION_TYPE_FIELD &&
349        (data.relation_type == DateParseData::RelationType::SECOND))) {
350     granularity = DatetimeGranularity::GRANULARITY_SECOND;
351   }
352 
353   return granularity;
354 }
355 
356 };  // namespace calendar
357 
358 #undef TC3_CALENDAR_CHECK
359 
360 }  // namespace libtextclassifier3
361 
362 #endif  // LIBTEXTCLASSIFIER_UTILS_CALENDAR_CALENDAR_COMMON_H_
363