in sql_utils/public/functions/date_time_util.cc [1454:1586]
static absl::Status TimestampTruncAtLeastMinute(absl::Time timestamp,
TimestampScale scale,
absl::TimeZone timezone,
DateTimestampPart part,
absl::Time* output) {
const absl::TimeZone::CivilInfo info = timezone.At(timestamp);
// Given a valid input timestamp, truncation should never result in failure
// when reconstructing the timestamp from its parts, so SQL_RET_CHECK the results.
switch (part) {
case YEAR:
SQL_RET_CHECK(TimestampFromParts(info.cs.year(), 1 /* month */, 1 /* mday */,
0 /* hour */, 0 /* minute */, 0 /* second */,
0 /* subsecond */, scale, timezone, output));
return absl::OkStatus();
case ISOYEAR: {
absl::CivilDay day = absl::CivilDay(info.cs);
if (!IsValidCivilDay(day)) {
return MakeEvalError()
<< "Invalid date value: " << CivilDayToEpochDays(day);
}
absl::CivilDay iso_civil_day =
date_time_util_internal::GetFirstDayOfIsoYear(day);
SQL_RET_CHECK(TimestampFromParts(iso_civil_day.year(), iso_civil_day.month(),
iso_civil_day.day(), 0 /* hour */,
0 /* minute */, 0 /* second */,
0 /* subsecond */, scale, timezone, output));
return absl::OkStatus();
}
case QUARTER:
SQL_RET_CHECK(TimestampFromParts(
info.cs.year(), (info.cs.month() - 1) / 3 * 3 + 1, 1 /* mday */,
0 /* hour */, 0 /* minute */, 0 /* second */, 0 /* subsecond */,
scale, timezone, output));
return absl::OkStatus();
case MONTH:
SQL_RET_CHECK(TimestampFromParts(info.cs.year(), info.cs.month(),
1 /* mday */, 0 /* hour */, 0 /* minute */,
0 /* second */, 0 /* subsecond */, scale,
timezone, output));
return absl::OkStatus();
case WEEK:
case ISOWEEK:
case WEEK_MONDAY:
case WEEK_TUESDAY:
case WEEK_WEDNESDAY:
case WEEK_THURSDAY:
case WEEK_FRIDAY:
case WEEK_SATURDAY: {
// Convert to a CivilDay...
SQL_ASSIGN_OR_RETURN(absl::Weekday weekday, GetFirstWeekDayOfWeek(part));
absl::CivilDay week_truncated_day =
PrevWeekdayOrToday(absl::CivilDay(info.cs), weekday);
if (week_truncated_day.year() < 1) {
return MakeEvalError()
<< "Truncating " << TimestampErrorString(timestamp, timezone)
<< " to the nearest week causes overflow";
}
SQL_RET_CHECK(TimestampFromParts(
week_truncated_day.year(), week_truncated_day.month(),
week_truncated_day.day(), 0 /* hour */, 0 /* minute */,
0 /* second */, 0 /* subsecond */, scale, timezone, output));
return absl::OkStatus();
}
case DAY:
SQL_RET_CHECK(TimestampFromParts(info.cs.year(), info.cs.month(),
info.cs.day(), 0 /* hour */, 0 /* minute */,
0 /* second */, 0 /* subsecond */, scale,
timezone, output));
return absl::OkStatus();
case HOUR:
case MINUTE: {
// For HOUR or MINUTE truncation of a timestamp, we identify the
// minute/second/subsecond parts as viewed via the specified time zone,
// and subtract that interval from the timestamp.
//
// This operation is performed by adding the timezone seconds offset from
// UTC (given the input timestamp) to 'align' the minute/seconds parts
// for truncation. The minute/seconds parts can then be effectively
// truncated from the value. We then re-adjust the result timestamp by
// the seconds offset value.
//
// Note that with these semantics, when you truncate a timestamp based
// on a specified time zone and then convert the resulting truncated
// timestamp to a civil time in that same time zone, you are not
// guaranteed to get a civil time that is at the hour or minute boundary
// in that time zone. For instance, this will happen when the truncation
// crosses a DST change and the DST change is not a (multiple of an)
// hour. See the unit tests for specific examples.
// Note that as documented, usage of CivilInfo.offset is discouraged.
const int64_t seconds_offset_east_of_UTC = info.offset;
int64_t timestamp_seconds;
// Note: This conversion truncates the subseconds part
SQL_RET_CHECK(FromTime(timestamp, kSeconds, ×tamp_seconds))
<< "timestamp: " << timestamp
<< ", scale: seconds, timestamp_seconds: " << timestamp_seconds;
// Adjust the timestamp by the time zone offset.
timestamp_seconds += seconds_offset_east_of_UTC;
// Truncate seconds from the timestamp to the hour/minute boundary
const int64_t truncate_granularity = (part == HOUR ? 3600 : 60);
int64_t num_seconds_to_truncate =
timestamp_seconds % truncate_granularity;
if (num_seconds_to_truncate < 0) {
num_seconds_to_truncate += truncate_granularity;
}
timestamp_seconds -= num_seconds_to_truncate;
// Re-adjust the timestamp by the time zone offset.
timestamp_seconds -= seconds_offset_east_of_UTC;
*output = MakeTime(timestamp_seconds, kSeconds);
return absl::OkStatus();
}
case DATE:
case DAYOFWEEK:
case DAYOFYEAR:
return MakeEvalError() << "Unsupported DateTimestampPart "
<< DateTimestampPart_Name(part);
case SECOND:
case MILLISECOND:
case MICROSECOND:
case NANOSECOND:
SQL_RET_CHECK_FAIL() << "Should not reach here for part="
<< DateTimestampPart_Name(part);
default:
return MakeEvalError()
<< "Unexpected DateTimestampPart " << DateTimestampPart_Name(part);
}
}