static absl::Status TimestampTruncAtLeastMinute()

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, &timestamp_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);
  }
}